mirror of
https://github.com/bitwarden/browser
synced 2025-12-16 08:13:42 +00:00
The motivation for this is https://github.com/bitwarden/cli/issues/129 where failed sync's are swallowed by try-catch. By moving the try-catch to the outside it is possible to reuse the already existing allowThrowOnError argument which callers can use to signal whether fullSync should throw or ignore errors silently. This patch is companioned with a patch to the SyncCommand CLI command to pass allowThrowOnError.
326 lines
13 KiB
TypeScript
326 lines
13 KiB
TypeScript
import { ApiService } from '../abstractions/api.service';
|
|
import { CipherService } from '../abstractions/cipher.service';
|
|
import { CollectionService } from '../abstractions/collection.service';
|
|
import { CryptoService } from '../abstractions/crypto.service';
|
|
import { FolderService } from '../abstractions/folder.service';
|
|
import { MessagingService } from '../abstractions/messaging.service';
|
|
import { PolicyService } from '../abstractions/policy.service';
|
|
import { SendService } from '../abstractions/send.service';
|
|
import { SettingsService } from '../abstractions/settings.service';
|
|
import { StorageService } from '../abstractions/storage.service';
|
|
import { SyncService as SyncServiceAbstraction } from '../abstractions/sync.service';
|
|
import { UserService } from '../abstractions/user.service';
|
|
|
|
import { CipherData } from '../models/data/cipherData';
|
|
import { CollectionData } from '../models/data/collectionData';
|
|
import { FolderData } from '../models/data/folderData';
|
|
import { OrganizationData } from '../models/data/organizationData';
|
|
import { PolicyData } from '../models/data/policyData';
|
|
import { SendData } from '../models/data/sendData';
|
|
|
|
import { CipherResponse } from '../models/response/cipherResponse';
|
|
import { CollectionDetailsResponse } from '../models/response/collectionResponse';
|
|
import { DomainsResponse } from '../models/response/domainsResponse';
|
|
import { FolderResponse } from '../models/response/folderResponse';
|
|
import {
|
|
SyncCipherNotification,
|
|
SyncFolderNotification,
|
|
} from '../models/response/notificationResponse';
|
|
import { PolicyResponse } from '../models/response/policyResponse';
|
|
import { ProfileResponse } from '../models/response/profileResponse';
|
|
import { SendResponse } from '../models/response/sendResponse';
|
|
|
|
const Keys = {
|
|
lastSyncPrefix: 'lastSync_',
|
|
};
|
|
|
|
export class SyncService implements SyncServiceAbstraction {
|
|
syncInProgress: boolean = false;
|
|
|
|
constructor(private userService: UserService, private apiService: ApiService,
|
|
private settingsService: SettingsService, private folderService: FolderService,
|
|
private cipherService: CipherService, private cryptoService: CryptoService,
|
|
private collectionService: CollectionService, private storageService: StorageService,
|
|
private messagingService: MessagingService, private policyService: PolicyService,
|
|
private sendService: SendService, private logoutCallback: (expired: boolean) => Promise<void>) {
|
|
}
|
|
|
|
async getLastSync(): Promise<Date> {
|
|
const userId = await this.userService.getUserId();
|
|
if (userId == null) {
|
|
return null;
|
|
}
|
|
|
|
const lastSync = await this.storageService.get<any>(Keys.lastSyncPrefix + userId);
|
|
if (lastSync) {
|
|
return new Date(lastSync);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
async setLastSync(date: Date): Promise<any> {
|
|
const userId = await this.userService.getUserId();
|
|
if (userId == null) {
|
|
return;
|
|
}
|
|
|
|
await this.storageService.save(Keys.lastSyncPrefix + userId, date.toJSON());
|
|
}
|
|
|
|
async fullSync(forceSync: boolean, allowThrowOnError = false): Promise<boolean> {
|
|
this.syncStarted();
|
|
const isAuthenticated = await this.userService.isAuthenticated();
|
|
if (!isAuthenticated) {
|
|
return this.syncCompleted(false);
|
|
}
|
|
|
|
const now = new Date();
|
|
var needsSync = false;
|
|
try {
|
|
needsSync = await this.needsSyncing(forceSync);
|
|
} catch (e) {
|
|
if (allowThrowOnError) {
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
if (!needsSync) {
|
|
await this.setLastSync(now);
|
|
return this.syncCompleted(false);
|
|
}
|
|
|
|
const userId = await this.userService.getUserId();
|
|
try {
|
|
const response = await this.apiService.getSync();
|
|
|
|
await this.syncProfile(response.profile);
|
|
await this.syncFolders(userId, response.folders);
|
|
await this.syncCollections(response.collections);
|
|
await this.syncCiphers(userId, response.ciphers);
|
|
await this.syncSends(userId, response.sends);
|
|
await this.syncSettings(userId, response.domains);
|
|
await this.syncPolicies(response.policies);
|
|
|
|
await this.setLastSync(now);
|
|
return this.syncCompleted(true);
|
|
} catch (e) {
|
|
if (allowThrowOnError) {
|
|
throw e;
|
|
} else {
|
|
return this.syncCompleted(false);
|
|
}
|
|
}
|
|
}
|
|
|
|
async syncUpsertFolder(notification: SyncFolderNotification, isEdit: boolean): Promise<boolean> {
|
|
this.syncStarted();
|
|
if (await this.userService.isAuthenticated()) {
|
|
try {
|
|
const localFolder = await this.folderService.get(notification.id);
|
|
if ((!isEdit && localFolder == null) ||
|
|
(isEdit && localFolder != null && localFolder.revisionDate < notification.revisionDate)) {
|
|
const remoteFolder = await this.apiService.getFolder(notification.id);
|
|
if (remoteFolder != null) {
|
|
const userId = await this.userService.getUserId();
|
|
await this.folderService.upsert(new FolderData(remoteFolder, userId));
|
|
this.messagingService.send('syncedUpsertedFolder', { folderId: notification.id });
|
|
return this.syncCompleted(true);
|
|
}
|
|
}
|
|
} catch { }
|
|
}
|
|
return this.syncCompleted(false);
|
|
}
|
|
|
|
async syncDeleteFolder(notification: SyncFolderNotification): Promise<boolean> {
|
|
this.syncStarted();
|
|
if (await this.userService.isAuthenticated()) {
|
|
await this.folderService.delete(notification.id);
|
|
this.messagingService.send('syncedDeletedFolder', { folderId: notification.id });
|
|
this.syncCompleted(true);
|
|
return true;
|
|
}
|
|
return this.syncCompleted(false);
|
|
}
|
|
|
|
async syncUpsertCipher(notification: SyncCipherNotification, isEdit: boolean): Promise<boolean> {
|
|
this.syncStarted();
|
|
if (await this.userService.isAuthenticated()) {
|
|
try {
|
|
let shouldUpdate = true;
|
|
const localCipher = await this.cipherService.get(notification.id);
|
|
if (localCipher != null && localCipher.revisionDate >= notification.revisionDate) {
|
|
shouldUpdate = false;
|
|
}
|
|
|
|
let checkCollections = false;
|
|
if (shouldUpdate) {
|
|
if (isEdit) {
|
|
shouldUpdate = localCipher != null;
|
|
checkCollections = true;
|
|
} else {
|
|
if (notification.collectionIds == null || notification.organizationId == null) {
|
|
shouldUpdate = localCipher == null;
|
|
} else {
|
|
shouldUpdate = false;
|
|
checkCollections = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!shouldUpdate && checkCollections && notification.organizationId != null &&
|
|
notification.collectionIds != null && notification.collectionIds.length > 0) {
|
|
const collections = await this.collectionService.getAll();
|
|
if (collections != null) {
|
|
for (let i = 0; i < collections.length; i++) {
|
|
if (notification.collectionIds.indexOf(collections[i].id) > -1) {
|
|
shouldUpdate = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (shouldUpdate) {
|
|
const remoteCipher = await this.apiService.getCipher(notification.id);
|
|
if (remoteCipher != null) {
|
|
const userId = await this.userService.getUserId();
|
|
await this.cipherService.upsert(new CipherData(remoteCipher, userId));
|
|
this.messagingService.send('syncedUpsertedCipher', { cipherId: notification.id });
|
|
return this.syncCompleted(true);
|
|
}
|
|
}
|
|
} catch (e) {
|
|
if (e != null && e.statusCode === 404 && isEdit) {
|
|
await this.cipherService.delete(notification.id);
|
|
this.messagingService.send('syncedDeletedCipher', { cipherId: notification.id });
|
|
return this.syncCompleted(true);
|
|
}
|
|
}
|
|
}
|
|
return this.syncCompleted(false);
|
|
}
|
|
|
|
async syncDeleteCipher(notification: SyncCipherNotification): Promise<boolean> {
|
|
this.syncStarted();
|
|
if (await this.userService.isAuthenticated()) {
|
|
await this.cipherService.delete(notification.id);
|
|
this.messagingService.send('syncedDeletedCipher', { cipherId: notification.id });
|
|
return this.syncCompleted(true);
|
|
}
|
|
return this.syncCompleted(false);
|
|
}
|
|
|
|
// Helpers
|
|
|
|
private syncStarted() {
|
|
this.syncInProgress = true;
|
|
this.messagingService.send('syncStarted');
|
|
}
|
|
|
|
private syncCompleted(successfully: boolean): boolean {
|
|
this.syncInProgress = false;
|
|
this.messagingService.send('syncCompleted', { successfully: successfully });
|
|
return successfully;
|
|
}
|
|
|
|
private async needsSyncing(forceSync: boolean) {
|
|
if (forceSync) {
|
|
return true;
|
|
}
|
|
|
|
const lastSync = await this.getLastSync();
|
|
if (lastSync == null || lastSync.getTime() === 0) {
|
|
return true;
|
|
}
|
|
|
|
const response = await this.apiService.getAccountRevisionDate();
|
|
if (new Date(response) <= lastSync) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
private async syncProfile(response: ProfileResponse) {
|
|
const stamp = await this.userService.getSecurityStamp();
|
|
if (stamp != null && stamp !== response.securityStamp) {
|
|
if (this.logoutCallback != null) {
|
|
await this.logoutCallback(true);
|
|
}
|
|
|
|
throw new Error('Stamp has changed');
|
|
}
|
|
|
|
await this.cryptoService.setEncKey(response.key);
|
|
await this.cryptoService.setEncPrivateKey(response.privateKey);
|
|
await this.cryptoService.setOrgKeys(response.organizations);
|
|
await this.userService.setSecurityStamp(response.securityStamp);
|
|
|
|
const organizations: { [id: string]: OrganizationData; } = {};
|
|
response.organizations.forEach((o) => {
|
|
organizations[o.id] = new OrganizationData(o);
|
|
});
|
|
return await this.userService.replaceOrganizations(organizations);
|
|
}
|
|
|
|
private async syncFolders(userId: string, response: FolderResponse[]) {
|
|
const folders: { [id: string]: FolderData; } = {};
|
|
response.forEach((f) => {
|
|
folders[f.id] = new FolderData(f, userId);
|
|
});
|
|
return await this.folderService.replace(folders);
|
|
}
|
|
|
|
private async syncCollections(response: CollectionDetailsResponse[]) {
|
|
const collections: { [id: string]: CollectionData; } = {};
|
|
response.forEach((c) => {
|
|
collections[c.id] = new CollectionData(c);
|
|
});
|
|
return await this.collectionService.replace(collections);
|
|
}
|
|
|
|
private async syncCiphers(userId: string, response: CipherResponse[]) {
|
|
const ciphers: { [id: string]: CipherData; } = {};
|
|
response.forEach((c) => {
|
|
ciphers[c.id] = new CipherData(c, userId);
|
|
});
|
|
return await this.cipherService.replace(ciphers);
|
|
}
|
|
|
|
private async syncSends(userId: string, response: SendResponse[]) {
|
|
const sends: { [id: string]: SendData; } = {};
|
|
response.forEach((s) => {
|
|
sends[s.id] = new SendData(s, userId);
|
|
});
|
|
return await this.sendService.replace(sends);
|
|
}
|
|
|
|
private async syncSettings(userId: string, response: DomainsResponse) {
|
|
let eqDomains: string[][] = [];
|
|
if (response != null && response.equivalentDomains != null) {
|
|
eqDomains = eqDomains.concat(response.equivalentDomains);
|
|
}
|
|
|
|
if (response != null && response.globalEquivalentDomains != null) {
|
|
response.globalEquivalentDomains.forEach((global) => {
|
|
if (global.domains.length > 0) {
|
|
eqDomains.push(global.domains);
|
|
}
|
|
});
|
|
}
|
|
|
|
return this.settingsService.setEquivalentDomains(eqDomains);
|
|
}
|
|
|
|
private async syncPolicies(response: PolicyResponse[]) {
|
|
const policies: { [id: string]: PolicyData; } = {};
|
|
if (response != null) {
|
|
response.forEach((p) => {
|
|
policies[p.id] = new PolicyData(p);
|
|
});
|
|
}
|
|
return await this.policyService.replace(policies);
|
|
}
|
|
}
|