diff --git a/src/background.html b/src/background.html index a95f36bb47..8ab1e3135a 100644 --- a/src/background.html +++ b/src/background.html @@ -4,9 +4,7 @@ - - diff --git a/src/background.js b/src/background.js index 91bae6e395..bec397329b 100644 --- a/src/background.js +++ b/src/background.js @@ -1,4 +1,5 @@ // Service imports +import ApiService from './services/api.service'; import AppIdService from './services/appId.service'; import ConstantsService from './services/constants.service'; import CryptoService from './services/crypto.service'; @@ -20,6 +21,7 @@ import { SecureNoteData } from './models/data/secureNoteData'; import { CipherString } from './models/domain/cipherString'; +import { CipherRequest } from './models/request/cipherRequest'; import { DeviceRequest } from './models/request/deviceRequest'; import { DeviceTokenRequest } from './models/request/deviceTokenRequest'; import { FolderRequest } from './models/request/folderRequest'; @@ -78,7 +80,7 @@ var bg_isBackground = true, window.bg_cryptoService = bg_cryptoService = new CryptoService(); window.bg_tokenService = bg_tokenService = new TokenService(); window.bg_appIdService = bg_appIdService = new AppIdService(); - window.bg_apiService = bg_apiService = new ApiService(bg_tokenService, bg_appIdService, bg_utilsService, bg_constantsService, logout); + window.bg_apiService = bg_apiService = new ApiService(bg_tokenService, logout); window.bg_environmentService = bg_environmentService = new EnvironmentService(bg_constantsService, bg_apiService); window.bg_userService = bg_userService = new UserService(bg_tokenService, bg_apiService, bg_cryptoService, bg_utilsService); window.bg_settingsService = bg_settingsService = new SettingsService(bg_userService, bg_utilsService); @@ -907,7 +909,9 @@ var bg_isBackground = true, }); setIcon(); refreshBadgeAndMenu(); - callback(); + if (callback) { + callback(); + } }); }); }); diff --git a/src/enums/cipherType.enum.ts b/src/enums/cipherType.enum.ts new file mode 100644 index 0000000000..c081fb2d8c --- /dev/null +++ b/src/enums/cipherType.enum.ts @@ -0,0 +1,6 @@ +export enum CipherType { + Login = 1, + SecureNote = 2, + Card = 3, + Identity = 4, +} diff --git a/src/enums/fieldType.enum.ts b/src/enums/fieldType.enum.ts new file mode 100644 index 0000000000..c28b26c1da --- /dev/null +++ b/src/enums/fieldType.enum.ts @@ -0,0 +1,5 @@ +export enum FieldType { + Text = 0, + Hidden = 1, + Boolean = 2, +} diff --git a/src/models/api/requestModels.js b/src/models/api/requestModels.js deleted file mode 100644 index 866b949eaf..0000000000 --- a/src/models/api/requestModels.js +++ /dev/null @@ -1,70 +0,0 @@ -window.CipherRequest = function (cipher) { - this.type = cipher.type; - this.folderId = cipher.folderId; - this.organizationId = cipher.organizationId; - this.name = cipher.name ? cipher.name.encryptedString : null; - this.notes = cipher.notes ? cipher.notes.encryptedString : null; - this.favorite = cipher.favorite; - - var constantsService = chrome.extension.getBackgroundPage().bg_constantsService; - switch (this.type) { - case constantsService.cipherType.login: - this.login = { - uri: cipher.login.uri ? cipher.login.uri.encryptedString : null, - username: cipher.login.username ? cipher.login.username.encryptedString : null, - password: cipher.login.password ? cipher.login.password.encryptedString : null, - totp: cipher.login.totp ? cipher.login.totp.encryptedString : null - }; - break; - case constantsService.cipherType.secureNote: - this.secureNote = { - type: cipher.secureNote.type - }; - break; - case constantsService.cipherType.card: - this.card = { - cardholderName: cipher.card.cardholderName ? cipher.card.cardholderName.encryptedString : null, - brand: cipher.card.brand ? cipher.card.brand.encryptedString : null, - number: cipher.card.number ? cipher.card.number.encryptedString : null, - expMonth: cipher.card.expMonth ? cipher.card.expMonth.encryptedString : null, - expYear: cipher.card.expYear ? cipher.card.expYear.encryptedString : null, - code: cipher.card.code ? cipher.card.code.encryptedString : null - }; - break; - case constantsService.cipherType.identity: - this.identity = { - title: cipher.identity.title ? cipher.identity.title.encryptedString : null, - firstName: cipher.identity.firstName ? cipher.identity.firstName.encryptedString : null, - middleName: cipher.identity.middleName ? cipher.identity.middleName.encryptedString : null, - lastName: cipher.identity.lastName ? cipher.identity.lastName.encryptedString : null, - address1: cipher.identity.address1 ? cipher.identity.address1.encryptedString : null, - address2: cipher.identity.address2 ? cipher.identity.address2.encryptedString : null, - address3: cipher.identity.address3 ? cipher.identity.address3.encryptedString : null, - city: cipher.identity.city ? cipher.identity.city.encryptedString : null, - state: cipher.identity.state ? cipher.identity.state.encryptedString : null, - postalCode: cipher.identity.postalCode ? cipher.identity.postalCode.encryptedString : null, - country: cipher.identity.country ? cipher.identity.country.encryptedString : null, - company: cipher.identity.company ? cipher.identity.company.encryptedString : null, - email: cipher.identity.email ? cipher.identity.email.encryptedString : null, - phone: cipher.identity.phone ? cipher.identity.phone.encryptedString : null, - ssn: cipher.identity.ssn ? cipher.identity.ssn.encryptedString : null, - username: cipher.identity.username ? cipher.identity.username.encryptedString : null, - passportNumber: cipher.identity.passportNumber ? cipher.identity.passportNumber.encryptedString : null, - licenseNumber: cipher.identity.licenseNumber ? cipher.identity.licenseNumber.encryptedString : null - }; - break; - default: - break; - } - - if (cipher.fields) { - this.fields = []; - for (var i = 0; i < cipher.fields.length; i++) { - this.fields.push({ - type: cipher.fields[i].type, - name: cipher.fields[i].name ? cipher.fields[i].name.encryptedString : null, - value: cipher.fields[i].value ? cipher.fields[i].value.encryptedString : null, - }); - } - } -}; diff --git a/src/models/domain/environmentUrls.ts b/src/models/domain/environmentUrls.ts new file mode 100644 index 0000000000..0eec60a114 --- /dev/null +++ b/src/models/domain/environmentUrls.ts @@ -0,0 +1,5 @@ +export default class EnvironmentUrls { + base: string; + api: string; + identity: string; +} diff --git a/src/models/domainModels.js b/src/models/domainModels.js index 04d60ce349..4e50ee2a2d 100644 --- a/src/models/domainModels.js +++ b/src/models/domainModels.js @@ -1,4 +1,4 @@ -var Cipher = function (obj, alreadyEncrypted, localData) { +var Cipher = window.Cipher = function (obj, alreadyEncrypted, localData) { this.constantsService = chrome.extension.getBackgroundPage().bg_constantsService; this.utilsService = chrome.extension.getBackgroundPage().bg_utilsService; @@ -55,7 +55,7 @@ var Cipher = function (obj, alreadyEncrypted, localData) { } }; -var Login2 = function (obj, alreadyEncrypted) { +var Login2 = window.Login2 = function (obj, alreadyEncrypted) { buildDomainModel(this, obj, { uri: null, username: null, @@ -64,7 +64,7 @@ var Login2 = function (obj, alreadyEncrypted) { }, alreadyEncrypted, []); }; -var Identity = function (obj, alreadyEncrypted) { +var Identity = window.Identity = function (obj, alreadyEncrypted) { buildDomainModel(this, obj, { title: null, firstName: null, @@ -87,7 +87,7 @@ var Identity = function (obj, alreadyEncrypted) { }, alreadyEncrypted, []); }; -var Card = function (obj, alreadyEncrypted) { +var Card = window.Card = function (obj, alreadyEncrypted) { buildDomainModel(this, obj, { cardholderName: null, brand: null, @@ -98,11 +98,11 @@ var Card = function (obj, alreadyEncrypted) { }, alreadyEncrypted, []); }; -var SecureNote = function (obj, alreadyEncrypted) { +var SecureNote = window.SecureNote = function (obj, alreadyEncrypted) { this.type = obj.type; }; -var Field = function (obj, alreadyEncrypted) { +var Field = window.Field = function (obj, alreadyEncrypted) { this.type = obj.type; buildDomainModel(this, obj, { name: null, @@ -110,7 +110,7 @@ var Field = function (obj, alreadyEncrypted) { }, alreadyEncrypted, []); }; -var Attachment = function (obj, alreadyEncrypted) { +var Attachment = window.Attachment = function (obj, alreadyEncrypted) { this.size = obj.size; buildDomainModel(this, obj, { id: null, @@ -120,7 +120,7 @@ var Attachment = function (obj, alreadyEncrypted) { }, alreadyEncrypted, ['id', 'url', 'sizeName']); }; -var Folder = function (obj, alreadyEncrypted) { +var Folder = window.Folder = function (obj, alreadyEncrypted) { buildDomainModel(this, obj, { id: null, name: null diff --git a/src/models/request/cipherRequest.ts b/src/models/request/cipherRequest.ts new file mode 100644 index 0000000000..e037f53aae --- /dev/null +++ b/src/models/request/cipherRequest.ts @@ -0,0 +1,89 @@ +import { CipherType } from '../../enums/cipherType.enum'; + +class CipherRequest { + type: CipherType; + folderId: string; + organizationId: string; + name: string; + notes: string; + favorite: boolean; + login: any; + secureNote: any; + card: any; + identity: any; + fields: any[]; + + constructor(cipher: any) { + this.type = cipher.type; + this.folderId = cipher.folderId; + this.organizationId = cipher.organizationId; + this.name = cipher.name ? cipher.name.encryptedString : null; + this.notes = cipher.notes ? cipher.notes.encryptedString : null; + this.favorite = cipher.favorite; + + switch (this.type) { + case CipherType.Login: + this.login = { + uri: cipher.login.uri ? cipher.login.uri.encryptedString : null, + username: cipher.login.username ? cipher.login.username.encryptedString : null, + password: cipher.login.password ? cipher.login.password.encryptedString : null, + totp: cipher.login.totp ? cipher.login.totp.encryptedString : null, + }; + break; + case CipherType.SecureNote: + this.secureNote = { + type: cipher.secureNote.type, + }; + break; + case CipherType.Card: + this.card = { + cardholderName: cipher.card.cardholderName ? cipher.card.cardholderName.encryptedString : null, + brand: cipher.card.brand ? cipher.card.brand.encryptedString : null, + number: cipher.card.number ? cipher.card.number.encryptedString : null, + expMonth: cipher.card.expMonth ? cipher.card.expMonth.encryptedString : null, + expYear: cipher.card.expYear ? cipher.card.expYear.encryptedString : null, + code: cipher.card.code ? cipher.card.code.encryptedString : null, + }; + break; + case CipherType.Identity: + this.identity = { + title: cipher.identity.title ? cipher.identity.title.encryptedString : null, + firstName: cipher.identity.firstName ? cipher.identity.firstName.encryptedString : null, + middleName: cipher.identity.middleName ? cipher.identity.middleName.encryptedString : null, + lastName: cipher.identity.lastName ? cipher.identity.lastName.encryptedString : null, + address1: cipher.identity.address1 ? cipher.identity.address1.encryptedString : null, + address2: cipher.identity.address2 ? cipher.identity.address2.encryptedString : null, + address3: cipher.identity.address3 ? cipher.identity.address3.encryptedString : null, + city: cipher.identity.city ? cipher.identity.city.encryptedString : null, + state: cipher.identity.state ? cipher.identity.state.encryptedString : null, + postalCode: cipher.identity.postalCode ? cipher.identity.postalCode.encryptedString : null, + country: cipher.identity.country ? cipher.identity.country.encryptedString : null, + company: cipher.identity.company ? cipher.identity.company.encryptedString : null, + email: cipher.identity.email ? cipher.identity.email.encryptedString : null, + phone: cipher.identity.phone ? cipher.identity.phone.encryptedString : null, + ssn: cipher.identity.ssn ? cipher.identity.ssn.encryptedString : null, + username: cipher.identity.username ? cipher.identity.username.encryptedString : null, + passportNumber: cipher.identity.passportNumber ? + cipher.identity.passportNumber.encryptedString : null, + licenseNumber: cipher.identity.licenseNumber ? cipher.identity.licenseNumber.encryptedString : null, + }; + break; + default: + break; + } + + if (cipher.fields) { + this.fields = []; + for (const field of cipher.fields) { + this.fields.push({ + type: field.type, + name: field.name ? field.name.encryptedString : null, + value: field.value ? field.value.encryptedString : null, + }); + } + } + } +} + +export { CipherRequest }; +(window as any).CipherRequest = CipherRequest; diff --git a/src/models/response/errorResponse.ts b/src/models/response/errorResponse.ts index 6426e44849..03dfda14b0 100644 --- a/src/models/response/errorResponse.ts +++ b/src/models/response/errorResponse.ts @@ -3,21 +3,21 @@ class ErrorResponse { validationErrors: { [key: string]: string[]; }; statusCode: number; - constructor(response: any, identityResponse?: boolean) { + constructor(response: any, status: number, identityResponse?: boolean) { let errorModel = null; - if (identityResponse && response.responseJSON && response.responseJSON.ErrorModel) { - errorModel = response.responseJSON.ErrorModel; - } else if (response.responseJSON) { - errorModel = response.responseJSON; - } else if (response.responseText && response.responseText.indexOf('{') === 0) { - errorModel = JSON.parse(response.responseText); + if (identityResponse && response && response.ErrorModel) { + errorModel = response.ErrorModel; + } else if (response) { + errorModel = response; + //} else if (response.responseText && response.responseText.indexOf('{') === 0) { + // errorModel = JSON.parse(response.responseText); } if (errorModel) { this.message = errorModel.Message; this.validationErrors = errorModel.ValidationErrors; } - this.statusCode = response.status; + this.statusCode = status; } } diff --git a/src/popup/app/accounts/accountsHintController.js b/src/popup/app/accounts/accountsHintController.js index d49899e3b4..e1d877205a 100644 --- a/src/popup/app/accounts/accountsHintController.js +++ b/src/popup/app/accounts/accountsHintController.js @@ -1,4 +1,4 @@ -angular +angular .module('bit.accounts') .controller('accountsHintController', function ($scope, $state, apiService, toastr, $q, utilsService, @@ -31,13 +31,11 @@ function hintPromise(request) { return $q(function (resolve, reject) { - apiService.postPasswordHint(request, - function () { - resolve(); - }, - function (error) { - reject(error); - }); + apiService.postPasswordHint(request).then(function () { + resolve(); + }, function (error) { + reject(error); + }); }); } }); diff --git a/src/popup/app/accounts/accountsRegisterController.js b/src/popup/app/accounts/accountsRegisterController.js index adfe07ea3e..a0cf9d0880 100644 --- a/src/popup/app/accounts/accountsRegisterController.js +++ b/src/popup/app/accounts/accountsRegisterController.js @@ -45,17 +45,17 @@ angular function registerPromise(key, masterPassword, email, hint) { var deferred = $q.defer(); - cryptoService.makeEncKey(key).then(function (encKey) { + var encKey; + cryptoService.makeEncKey(key).then(function (theEncKey) { + encKey = theEncKey; return cryptoService.hashPassword(masterPassword, key); }).then(function (hashedPassword) { var request = new RegisterRequest(email, hashedPassword, hint, encKey.encryptedString); - apiService.postRegister(request, - function () { - deferred.resolve(); - }, - function (error) { - deferred.reject(error); - }); + apiService.postRegister(request).then(function () { + deferred.resolve(); + }, function (error) { + deferred.reject(error); + }); }); return deferred.promise; } diff --git a/src/popup/app/app.js b/src/popup/app/app.js index ab54740d63..c33cf3eb73 100644 --- a/src/popup/app/app.js +++ b/src/popup/app/app.js @@ -21,7 +21,6 @@ require('../../scripts/analytics.js'); require('../../scripts/duo.js'); require('../../scripts/u2f.js'); -require('../../models/api/requestModels.js'); require('../../models/domainModels.js'); require('../less/libs.less'); @@ -42,6 +41,7 @@ import { SecureNoteData } from '../../models/data/secureNoteData'; import { CipherString } from '../../models/domain/cipherString'; +import { CipherRequest } from '../../models/request/cipherRequest'; import { DeviceRequest } from '../../models/request/deviceRequest'; import { DeviceTokenRequest } from '../../models/request/deviceTokenRequest'; import { FolderRequest } from '../../models/request/folderRequest'; diff --git a/src/popup/app/services/authService.js b/src/popup/app/services/authService.js index 2758f6c66e..1164961c94 100644 --- a/src/popup/app/services/authService.js +++ b/src/popup/app/services/authService.js @@ -10,7 +10,8 @@ angular var key = cryptoService.makeKey(masterPassword, email), deferred = $q.defer(), deviceRequest = null, - twoFactorRememberedToken; + twoFactorRememberedToken, + hashedPassword; appIdService.getAppId().then(function (appId) { deviceRequest = new DeviceRequest(appId, utilsService); @@ -18,7 +19,8 @@ angular }).then(function (theTwoFactorRememberedToken) { twoFactorRememberedToken = theTwoFactorRememberedToken; return cryptoService.hashPassword(masterPassword, key); - }).then(function (hashedPassword) { + }).then(function (theHashedPassword) { + hashedPassword = theHashedPassword; var request; if (twoFactorToken && typeof (twoFactorProvider) !== 'undefined' && twoFactorProvider !== null) { @@ -33,45 +35,48 @@ angular request = new TokenRequest(email, hashedPassword, null, null, false, deviceRequest); } - apiService.postIdentityToken(request, function (response) { - // success - if (!response || !response.accessToken) { - return; - } + return apiService.postIdentityToken(request); + }).then(function (response) { + if (!response) { + return; + } - if (response.twoFactorToken) { - tokenService.setTwoFactorToken(response.twoFactorToken, email); - } - - tokenService.setTokens(response.accessToken, response.refreshToken).then(function () { - return cryptoService.setKey(key); - }).then(function () { - return cryptoService.setKeyHash(hashedPassword); - }).then(function () { - userService.setUserIdAndEmail(tokenService.getUserId(), tokenService.getEmail(), - function () { - cryptoService.setEncKey(response.key).then(function () { - return cryptoService.setEncPrivateKey(response.privateKey); - }).then(function () { - chrome.runtime.sendMessage({ command: 'loggedIn' }); - deferred.resolve({ - twoFactor: false, - twoFactorProviders: null - }); - }); - }); - }); - }, function (providers) { + if (!response.accessToken) { // two factor required deferred.resolve({ twoFactor: true, - twoFactorProviders: providers + twoFactorProviders: response }); - }, function (error) { - // error - deferred.reject(error); + return; + } + + if (response.twoFactorToken) { + tokenService.setTwoFactorToken(response.twoFactorToken, email); + } + + return tokenService.setTokens(response.accessToken, response.refreshToken).then(function () { + return cryptoService.setKey(key); + }).then(function () { + return cryptoService.setKeyHash(hashedPassword); + }).then(function () { + userService.setUserIdAndEmail(tokenService.getUserId(), tokenService.getEmail(), + function () { + cryptoService.setEncKey(response.key).then(function () { + return cryptoService.setEncPrivateKey(response.privateKey); + }).then(function () { + chrome.runtime.sendMessage({ command: 'loggedIn' }); + deferred.resolve({ + twoFactor: false, + twoFactorProviders: null + }); + }); + }); }); + }, function (error) { + // error + deferred.reject(error); }); + return deferred.promise; }; diff --git a/src/popup/app/settings/settingsPremiumController.js b/src/popup/app/settings/settingsPremiumController.js index 7170296305..da5fabb63f 100644 --- a/src/popup/app/settings/settingsPremiumController.js +++ b/src/popup/app/settings/settingsPremiumController.js @@ -1,4 +1,4 @@ -angular +angular .module('bit.settings') .controller('settingsPremiumController', function ($scope, i18nService, tokenService, apiService, toastr, SweetAlert, @@ -8,7 +8,7 @@ $scope.price = '$10'; $scope.refresh = function () { - apiService.refreshIdentityToken(function () { + apiService.refreshIdentityToken().then(function () { toastr.success(i18nService.refreshComplete); $timeout(function () { $scope.isPremium = tokenService.getPremium(); diff --git a/src/services/api.service.ts b/src/services/api.service.ts new file mode 100644 index 0000000000..805ec5d45f --- /dev/null +++ b/src/services/api.service.ts @@ -0,0 +1,429 @@ +import AppIdService from './appId.service'; +import ConstantsService from './constants.service'; +import TokenService from './token.service'; +import UtilsService from './utils.service'; + +import EnvironmentUrls from '../models/domain/environmentUrls'; + +import { CipherRequest } from '../models/request/cipherRequest'; +import { DeviceRequest } from '../models/request/deviceRequest'; +import { DeviceTokenRequest } from '../models/request/deviceTokenRequest'; +import { FolderRequest } from '../models/request/folderRequest'; +import { PasswordHintRequest } from '../models/request/passwordHintRequest'; +import { RegisterRequest } from '../models/request/registerRequest'; +import { TokenRequest } from '../models/request/tokenRequest'; +import { TwoFactorEmailRequest } from '../models/request/twoFactorEmailRequest'; + +import { AttachmentResponse } from '../models/response/attachmentResponse'; +import { CipherResponse } from '../models/response/cipherResponse'; +import { DeviceResponse } from '../models/response/deviceResponse'; +import { DomainsResponse } from '../models/response/domainsResponse'; +import { ErrorResponse } from '../models/response/errorResponse'; +import { FolderResponse } from '../models/response/folderResponse'; +import { GlobalDomainResponse } from '../models/response/globalDomainResponse'; +import { IdentityTokenResponse } from '../models/response/identityTokenResponse'; +import { KeysResponse } from '../models/response/keysResponse'; +import { ListResponse } from '../models/response/listResponse'; +import { ProfileOrganizationResponse } from '../models/response/profileOrganizationResponse'; +import { ProfileResponse } from '../models/response/profileResponse'; +import { SyncResponse } from '../models/response/syncResponse'; + +export default class ApiService { + urlsSet: boolean = false; + baseUrl: string; + identityBaseUrl: string; + logoutCallback: Function; + + constructor(private tokenService: TokenService, logoutCallback: Function) { + this.logoutCallback = logoutCallback; + } + + setUrls(urls: EnvironmentUrls) { + this.urlsSet = true; + + if (urls.base != null) { + this.baseUrl = urls.base + '/api'; + this.identityBaseUrl = urls.base + '/identity'; + return; + } + + if (urls.api != null && urls.identity != null) { + this.baseUrl = urls.api; + this.identityBaseUrl = urls.identity; + return; + } + + /* tslint:disable */ + // Desktop + //this.baseUrl = 'http://localhost:4000'; + //this.identityBaseUrl = 'http://localhost:33656'; + + // Desktop HTTPS + //this.baseUrl = 'https://localhost:44377'; + //this.identityBaseUrl = 'https://localhost:44392'; + + // Desktop external + //this.baseUrl = 'http://192.168.1.3:4000'; + //this.identityBaseUrl = 'http://192.168.1.3:33656'; + + // Preview + //this.baseUrl = 'https://preview-api.bitwarden.com'; + //this.identityBaseUrl = 'https://preview-identity.bitwarden.com'; + + // Production + this.baseUrl = 'https://api.bitwarden.com'; + this.identityBaseUrl = 'https://identity.bitwarden.com'; + /* tslint:enable */ + } + + // Auth APIs + + async postIdentityToken(request: TokenRequest): Promise { + const response = await fetch(new Request(this.identityBaseUrl + '/connect/token', { + body: this.qsStringify(request.toIdentityToken()), + cache: 'no-cache', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8', + 'Accept': 'application/json', + }, + method: 'POST', + })); + + const responseJson = await response.json(); + if (response.status === 200) { + return new IdentityTokenResponse(responseJson); + } else if (response.status === 400 && responseJson && responseJson.TwoFactorProviders2 && + Object.keys(responseJson.TwoFactorProviders2).length) { + await this.tokenService.clearTwoFactorToken(request.email); + return responseJson.TwoFactorProviders2; + } else { + return Promise.reject(new ErrorResponse(responseJson, response.status, true)); + } + } + + async refreshIdentityToken(): Promise { + const response = await this.doRefreshToken(); + if (response.status !== 200) { + return Promise.reject(null); + } + } + + // Two Factor APIs + + async postTwoFactorEmail(request: TwoFactorEmailRequest): Promise { + const response = await fetch(new Request(this.baseUrl + '/two-factor/send-email-login', { + body: JSON.stringify(request), + cache: 'no-cache', + headers: { + 'Content-Type': 'application/json; charset=utf-8', + }, + method: 'POST', + })); + + if (response.status !== 200) { + const error = await this.handleError(response, false); + return Promise.reject(error); + } + } + + // Account APIs + + async getAccountRevisionDate(): Promise { + const authHeader = await this.handleTokenState(); + const response = await fetch(new Request(this.baseUrl + '/accounts/revision-date', { + cache: 'no-cache', + headers: { + Accept: 'application/json', + Authorization: authHeader, + }, + })); + + if (response.status === 200) { + return (await response.json() as number); + } else { + const error = await this.handleError(response, false); + return Promise.reject(error); + } + } + + async postPasswordHint(request: PasswordHintRequest): Promise { + const response = await fetch(new Request(this.baseUrl + '/accounts/password-hint', { + body: JSON.stringify(request), + cache: 'no-cache', + headers: { + 'Content-Type': 'application/json; charset=utf-8', + }, + method: 'POST', + })); + + if (response.status !== 200) { + const error = await this.handleError(response, false); + return Promise.reject(error); + } + } + + async postRegister(request: RegisterRequest): Promise { + const response = await fetch(new Request(this.baseUrl + '/accounts/register', { + body: JSON.stringify(request), + cache: 'no-cache', + headers: { + 'Content-Type': 'application/json; charset=utf-8', + }, + method: 'POST', + })); + + if (response.status !== 200) { + const error = await this.handleError(response, false); + return Promise.reject(error); + } + } + + // Folder APIs + + async postFolder(request: FolderRequest): Promise { + const authHeader = await this.handleTokenState(); + const response = await fetch(new Request(this.baseUrl + '/folders', { + body: JSON.stringify(request), + cache: 'no-cache', + headers: { + 'Accept': 'application/json', + 'Authorization': authHeader, + 'Content-Type': 'application/json; charset=utf-8', + }, + method: 'POST', + })); + + if (response.status === 200) { + const responseJson = await response.json(); + return new FolderResponse(responseJson); + } else { + const error = await this.handleError(response, false); + return Promise.reject(error); + } + } + + async putFolder(id: string, request: FolderRequest): Promise { + const authHeader = await this.handleTokenState(); + const response = await fetch(new Request(this.baseUrl + '/folders/' + id, { + body: JSON.stringify(request), + cache: 'no-cache', + headers: { + 'Accept': 'application/json', + 'Authorization': authHeader, + 'Content-Type': 'application/json; charset=utf-8', + }, + method: 'PUT', + })); + + if (response.status === 200) { + const responseJson = await response.json(); + return new FolderResponse(responseJson); + } else { + const error = await this.handleError(response, false); + return Promise.reject(error); + } + } + + async deleteFolder(id: string): Promise { + const authHeader = await this.handleTokenState(); + const response = await fetch(new Request(this.baseUrl + '/folders/' + id, { + cache: 'no-cache', + headers: { + Authorization: authHeader, + }, + method: 'DELETE', + })); + + if (response.status !== 200) { + const error = await this.handleError(response, false); + return Promise.reject(error); + } + } + + // Cipher APIs + + async postCipher(request: CipherRequest): Promise { + const authHeader = await this.handleTokenState(); + const response = await fetch(new Request(this.baseUrl + '/ciphers', { + body: JSON.stringify(request), + cache: 'no-cache', + headers: { + 'Accept': 'application/json', + 'Authorization': authHeader, + 'Content-Type': 'application/json; charset=utf-8', + }, + method: 'POST', + })); + + if (response.status === 200) { + const responseJson = await response.json(); + return new CipherResponse(responseJson); + } else { + const error = await this.handleError(response, false); + return Promise.reject(error); + } + } + + async putCipher(id: string, request: CipherRequest): Promise { + const authHeader = await this.handleTokenState(); + const response = await fetch(new Request(this.baseUrl + '/ciphers/' + id, { + body: JSON.stringify(request), + cache: 'no-cache', + headers: { + 'Accept': 'application/json', + 'Authorization': authHeader, + 'Content-Type': 'application/json; charset=utf-8', + }, + method: 'PUT', + })); + + if (response.status === 200) { + const responseJson = await response.json(); + return new CipherResponse(responseJson); + } else { + const error = await this.handleError(response, false); + return Promise.reject(error); + } + } + + async deleteCipher(id: string): Promise { + const authHeader = await this.handleTokenState(); + const response = await fetch(new Request(this.baseUrl + '/ciphers/' + id, { + cache: 'no-cache', + headers: { + Authorization: authHeader, + }, + method: 'DELETE', + })); + + if (response.status !== 200) { + const error = await this.handleError(response, false); + return Promise.reject(error); + } + } + + // Attachments APIs + + async postCipherAttachment(id: string, data: FormData): Promise { + const authHeader = await this.handleTokenState(); + const response = await fetch(new Request(this.baseUrl + '/ciphers/' + id + '/attachment', { + body: data, + cache: 'no-cache', + headers: { + Accept: 'application/json', + Authorization: authHeader, + }, + method: 'POST', + })); + + if (response.status === 200) { + const responseJson = await response.json(); + return new CipherResponse(responseJson); + } else { + const error = await this.handleError(response, false); + return Promise.reject(error); + } + } + + async deleteCipherAttachment(id: string, attachmentId: string): Promise { + const authHeader = await this.handleTokenState(); + const response = await fetch(new Request(this.baseUrl + '/ciphers/' + id + '/attachment/' + attachmentId, { + cache: 'no-cache', + headers: { + Authorization: authHeader, + }, + method: 'DELETE', + })); + + if (response.status !== 200) { + const error = await this.handleError(response, false); + return Promise.reject(error); + } + } + + // Sync APIs + + async getSync(): Promise { + const authHeader = await this.handleTokenState(); + const response = await fetch(new Request(this.baseUrl + '/sync', { + cache: 'no-cache', + headers: { + Accept: 'application/json', + Authorization: authHeader, + }, + })); + + if (response.status === 200) { + const responseJson = await response.json(); + return new SyncResponse(responseJson); + } else { + const error = await this.handleError(response, false); + return Promise.reject(error); + } + } + + // Helpers + + private async handleError(response: Response, tokenError: boolean): Promise { + if (response != null && (tokenError && response.status === 400) || + response.status === 401 || response.status === 403) { + if (this.logoutCallback) { + this.logoutCallback(true); + } else { + chrome.runtime.sendMessage({ command: 'logout', expired: true }); + } + return null; + } + + const responseJson = await response.json(); + return new ErrorResponse(responseJson, response.status); + } + + private async handleTokenState(): Promise { + const accessToken = await this.tokenService.getToken(); + if (!this.tokenService.tokenNeedsRefresh()) { + return 'Bearer ' + accessToken; + } + + const response = await this.doRefreshToken(); + const responseJson = await response.json(); + const tokenResponse = new IdentityTokenResponse(responseJson); + await this.tokenService.setTokens(tokenResponse.accessToken, tokenResponse.refreshToken); + return 'Bearer ' + tokenResponse.accessToken; + + // TODO: handle error + } + + private async doRefreshToken(): Promise { + const refreshToken = await this.tokenService.getRefreshToken(); + if (refreshToken == null || refreshToken === '') { + throw new Error(); + } + + const response = await fetch(new Request(this.identityBaseUrl + '/connect/token', { + body: this.qsStringify({ + grant_type: 'refresh_token', + client_id: 'browser', + refresh_token: refreshToken, + }), + cache: 'no-cache', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8', + 'Accept': 'application/json', + }, + method: 'POST', + })); + + if (response.status === 200) { + return response; + } else { + return Promise.reject(response); + } + } + + private qsStringify(params: any): string { + return Object.keys(params).map((key) => { + return encodeURIComponent(key) + '=' + encodeURIComponent(params[key]); + }).join('&'); + } +} diff --git a/src/services/syncService.js b/src/services/syncService.js index 6afcb9e6af..88405848b2 100644 --- a/src/services/syncService.js +++ b/src/services/syncService.js @@ -1,4 +1,4 @@ -function SyncService(cipherService, folderService, userService, apiService, settingsService, +function SyncService(cipherService, folderService, userService, apiService, settingsService, cryptoService, logoutCallback) { this.cipherService = cipherService; this.folderService = folderService; @@ -45,7 +45,7 @@ function initSyncService() { } self.userService.getUserId(function (userId) { - self.apiService.getSync(function (response) { + self.apiService.getSync().then(function (response) { syncProfile(self, response.profile).then(function () { return syncFolders(self, userId, response.folders); }).then(function () { @@ -80,7 +80,7 @@ function initSyncService() { return; } - self.apiService.getAccountRevisionDate(function (response) { + self.apiService.getAccountRevisionDate().then(function (response) { var accountRevisionDate = new Date(response); self.getLastSync(function (lastSync) { if (lastSync && accountRevisionDate <= lastSync) { @@ -90,7 +90,7 @@ function initSyncService() { callback(true, false); }); - }, function () { + }, function (error) { callback(false, true); }); }