diff --git a/src/abstractions/api.service.ts b/src/abstractions/api.service.ts index 9b9bf73c0a..3ee640286a 100644 --- a/src/abstractions/api.service.ts +++ b/src/abstractions/api.service.ts @@ -117,6 +117,7 @@ export abstract class ApiService { postAccountRecoverDelete: (request: DeleteRecoverRequest) => Promise; postAccountRecoverDeleteToken: (request: VerifyDeleteRecoverRequest) => Promise; + getFolder: (id: string) => Promise; postFolder: (request: FolderRequest) => Promise; putFolder: (id: string, request: FolderRequest) => Promise; deleteFolder: (id: string) => Promise; diff --git a/src/abstractions/sync.service.ts b/src/abstractions/sync.service.ts index ef4b4e8dc8..694c0517ea 100644 --- a/src/abstractions/sync.service.ts +++ b/src/abstractions/sync.service.ts @@ -1,9 +1,16 @@ +import { + SyncCipherNotification, + SyncFolderNotification, +} from '../models/response/notificationResponse'; + export abstract class SyncService { syncInProgress: boolean; getLastSync: () => Promise; setLastSync: (date: Date) => Promise; - syncStarted: () => void; - syncCompleted: (successfully: boolean) => void; fullSync: (forceSync: boolean) => Promise; + syncUpsertFolder: (notification: SyncFolderNotification) => Promise; + syncDeleteFolder: (notification: SyncFolderNotification) => Promise; + syncUpsertCipher: (notification: SyncCipherNotification) => Promise; + syncDeleteCipher: (notification: SyncFolderNotification) => Promise; } diff --git a/src/models/data/cipherData.ts b/src/models/data/cipherData.ts index e3b9944de7..84dece2e81 100644 --- a/src/models/data/cipherData.ts +++ b/src/models/data/cipherData.ts @@ -18,7 +18,7 @@ export class CipherData { edit: boolean; organizationUseTotp: boolean; favorite: boolean; - revisionDate: Date; + revisionDate: string; type: CipherType; sizeName: string; name: string; diff --git a/src/models/data/passwordHistoryData.ts b/src/models/data/passwordHistoryData.ts index 1ef1c87a48..067c46fd18 100644 --- a/src/models/data/passwordHistoryData.ts +++ b/src/models/data/passwordHistoryData.ts @@ -2,7 +2,7 @@ import { PasswordHistoryResponse } from '../response/passwordHistoryResponse'; export class PasswordHistoryData { password: string; - lastUsedDate: Date; + lastUsedDate: string; constructor(response?: PasswordHistoryResponse) { if (response == null) { diff --git a/src/models/domain/cipher.ts b/src/models/domain/cipher.ts index d66ee67cc2..1c3040edc8 100644 --- a/src/models/domain/cipher.ts +++ b/src/models/domain/cipher.ts @@ -54,7 +54,7 @@ export class Cipher extends Domain { this.favorite = obj.favorite; this.organizationUseTotp = obj.organizationUseTotp; this.edit = obj.edit; - this.revisionDate = obj.revisionDate; + this.revisionDate = obj.revisionDate != null ? new Date(obj.revisionDate) : null; this.collectionIds = obj.collectionIds; this.localData = localData; @@ -178,7 +178,7 @@ export class Cipher extends Domain { c.edit = this.edit; c.organizationUseTotp = this.organizationUseTotp; c.favorite = this.favorite; - c.revisionDate = this.revisionDate; + c.revisionDate = this.revisionDate.toISOString(); c.type = this.type; c.collectionIds = this.collectionIds; diff --git a/src/models/domain/folder.ts b/src/models/domain/folder.ts index 77b9fb310d..c0a8d179a9 100644 --- a/src/models/domain/folder.ts +++ b/src/models/domain/folder.ts @@ -8,6 +8,7 @@ import Domain from './domain'; export class Folder extends Domain { id: string; name: CipherString; + revisionDate: Date; constructor(obj?: FolderData, alreadyEncrypted: boolean = false) { super(); @@ -19,6 +20,8 @@ export class Folder extends Domain { id: null, name: null, }, alreadyEncrypted, ['id']); + + this.revisionDate = obj.revisionDate != null ? new Date(obj.revisionDate) : null; } decrypt(): Promise { diff --git a/src/models/domain/password.ts b/src/models/domain/password.ts index c60d0937fb..ef735ba0e1 100644 --- a/src/models/domain/password.ts +++ b/src/models/domain/password.ts @@ -17,8 +17,8 @@ export class Password extends Domain { this.buildDomainModel(this, obj, { password: null, - lastUsedDate: null, - }, alreadyEncrypted, ['lastUsedDate']); + }, alreadyEncrypted); + this.lastUsedDate = new Date(obj.lastUsedDate); } async decrypt(orgId: string): Promise { @@ -30,7 +30,7 @@ export class Password extends Domain { toPasswordHistoryData(): PasswordHistoryData { const ph = new PasswordHistoryData(); - ph.lastUsedDate = this.lastUsedDate; + ph.lastUsedDate = this.lastUsedDate.toISOString(); this.buildDataModel(this, ph, { password: null, }); diff --git a/src/models/request/passwordHistoryRequest.ts b/src/models/request/passwordHistoryRequest.ts index c917de1d10..7b74cb1201 100644 --- a/src/models/request/passwordHistoryRequest.ts +++ b/src/models/request/passwordHistoryRequest.ts @@ -1,9 +1,4 @@ export class PasswordHistoryRequest { password: string; lastUsedDate: Date; - - constructor(response: any) { - this.password = response.Password; - this.lastUsedDate = response.LastUsedDate; - } } diff --git a/src/models/response/cipherResponse.ts b/src/models/response/cipherResponse.ts index 3d2770fb82..87ceec6338 100644 --- a/src/models/response/cipherResponse.ts +++ b/src/models/response/cipherResponse.ts @@ -22,7 +22,7 @@ export class CipherResponse { favorite: boolean; edit: boolean; organizationUseTotp: boolean; - revisionDate: Date; + revisionDate: string; attachments: AttachmentResponse[]; passwordHistory: PasswordHistoryResponse[]; collectionIds: string[]; diff --git a/src/models/response/passwordHistoryResponse.ts b/src/models/response/passwordHistoryResponse.ts index 8630da8f03..03c210de1c 100644 --- a/src/models/response/passwordHistoryResponse.ts +++ b/src/models/response/passwordHistoryResponse.ts @@ -1,6 +1,6 @@ export class PasswordHistoryResponse { password: string; - lastUsedDate: Date; + lastUsedDate: string; constructor(response: any) { this.password = response.Password; diff --git a/src/services/api.service.ts b/src/services/api.service.ts index 4ffdf9e71b..20d28c4241 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -286,6 +286,11 @@ export class ApiService implements ApiServiceAbstraction { // Folder APIs + async getFolder(id: string): Promise { + const r = await this.send('GET', '/folders/' + id, null, true, true); + return new FolderResponse(r); + } + async postFolder(request: FolderRequest): Promise { const r = await this.send('POST', '/folders', request, true, true); return new FolderResponse(r); diff --git a/src/services/cipher.service.ts b/src/services/cipher.service.ts index 5222ca0688..3ade53634c 100644 --- a/src/services/cipher.service.ts +++ b/src/services/cipher.service.ts @@ -644,7 +644,9 @@ export class CipherService implements CipherServiceAbstraction { } if (typeof id === 'string') { - const i = id as string; + if (ciphers[id] == null) { + return; + } delete ciphers[id]; } else { (id as string[]).forEach((i) => { diff --git a/src/services/folder.service.ts b/src/services/folder.service.ts index c03cb0c298..34cecf775c 100644 --- a/src/services/folder.service.ts +++ b/src/services/folder.service.ts @@ -152,7 +152,9 @@ export class FolderService implements FolderServiceAbstraction { } if (typeof id === 'string') { - const i = id as string; + if (folders[id] == null) { + return; + } delete folders[id]; } else { (id as string[]).forEach((i) => { diff --git a/src/services/notifications.service.ts b/src/services/notifications.service.ts index 55327c33a8..cfbb0dc5d4 100644 --- a/src/services/notifications.service.ts +++ b/src/services/notifications.service.ts @@ -2,23 +2,24 @@ import * as signalR from '@aspnet/signalr'; import { NotificationType } from '../enums/notificationType'; -import { CipherService } from '../abstractions/cipher.service'; -import { CollectionService } from '../abstractions/collection.service'; +import { AppIdService } from '../abstractions/appId.service'; import { EnvironmentService } from '../abstractions/environment.service'; -import { FolderService } from '../abstractions/folder.service'; import { NotificationsService as NotificationsServiceAbstraction } from '../abstractions/notifications.service'; -import { SettingsService } from '../abstractions/settings.service'; import { SyncService } from '../abstractions/sync.service'; import { TokenService } from '../abstractions/token.service'; import { UserService } from '../abstractions/user.service'; -import { NotificationResponse } from '../models/response/notificationResponse'; +import { + NotificationResponse, + SyncCipherNotification, + SyncFolderNotification, +} from '../models/response/notificationResponse'; export class NotificationsService implements NotificationsServiceAbstraction { private signalrConnection: signalR.HubConnection; constructor(private userService: UserService, private tokenService: TokenService, - private syncService: SyncService) { } + private syncService: SyncService, private appIdService: AppIdService) { } async init(environmentService: EnvironmentService): Promise { let url = 'https://notifications.bitwarden.com'; @@ -61,21 +62,26 @@ export class NotificationsService implements NotificationsServiceAbstraction { } private async processNotification(notification: NotificationResponse) { - if (notification == null) { + const appId = await this.appIdService.getAppId(); + if (notification == null || notification.contextId === appId) { return; } switch (notification.type) { case NotificationType.SyncCipherCreate: - case NotificationType.SyncCipherDelete: case NotificationType.SyncCipherUpdate: + this.syncService.syncUpsertCipher(notification.payload as SyncCipherNotification); + break; + case NotificationType.SyncCipherDelete: case NotificationType.SyncLoginDelete: - this.syncService.fullSync(false); + this.syncService.syncDeleteCipher(notification.payload as SyncCipherNotification); break; case NotificationType.SyncFolderCreate: - case NotificationType.SyncFolderDelete: case NotificationType.SyncFolderUpdate: - this.syncService.fullSync(false); + this.syncService.syncUpsertFolder(notification.payload as SyncFolderNotification); + break; + case NotificationType.SyncFolderDelete: + this.syncService.syncDeleteFolder(notification.payload as SyncFolderNotification); break; case NotificationType.SyncVault: case NotificationType.SyncCiphers: diff --git a/src/services/sync.service.ts b/src/services/sync.service.ts index 07dd66e003..ae1a8fa7ca 100644 --- a/src/services/sync.service.ts +++ b/src/services/sync.service.ts @@ -18,6 +18,10 @@ 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 { ProfileResponse } from '../models/response/profileResponse'; const Keys = { @@ -57,22 +61,11 @@ export class SyncService implements SyncServiceAbstraction { await this.storageService.save(Keys.lastSyncPrefix + userId, date.toJSON()); } - syncStarted() { - this.syncInProgress = true; - this.messagingService.send('syncStarted'); - } - - syncCompleted(successfully: boolean) { - this.syncInProgress = false; - this.messagingService.send('syncCompleted', { successfully: successfully }); - } - async fullSync(forceSync: boolean): Promise { this.syncStarted(); const isAuthenticated = await this.userService.isAuthenticated(); if (!isAuthenticated) { - this.syncCompleted(false); - return false; + return this.syncCompleted(false); } const now = new Date(); @@ -81,14 +74,12 @@ export class SyncService implements SyncServiceAbstraction { const skipped = needsSyncResult[1]; if (skipped) { - this.syncCompleted(false); - return false; + return this.syncCompleted(false); } if (!needsSync) { await this.setLastSync(now); - this.syncCompleted(false); - return false; + return this.syncCompleted(false); } const userId = await this.userService.getUserId(); @@ -102,16 +93,82 @@ export class SyncService implements SyncServiceAbstraction { await this.syncSettings(userId, response.domains); await this.setLastSync(now); - this.syncCompleted(true); - return true; + return this.syncCompleted(true); } catch (e) { - this.syncCompleted(false); - return false; + return this.syncCompleted(false); } } + async syncUpsertFolder(notification: SyncFolderNotification): Promise { + this.syncStarted(); + if (await this.userService.isAuthenticated()) { + try { + const remoteFolder = await this.apiService.getFolder(notification.id); + const localFolder = await this.folderService.get(notification.id); + if (remoteFolder != null && + (localFolder == null || localFolder.revisionDate < notification.revisionDate)) { + 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 { + 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): Promise { + this.syncStarted(); + if (await this.userService.isAuthenticated()) { + try { + const remoteCipher = await this.apiService.getCipher(notification.id); + const localCipher = await this.cipherService.get(notification.id); + if (remoteCipher != null && + (localCipher == null || localCipher.revisionDate < notification.revisionDate)) { + 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 { } + } + return this.syncCompleted(false); + } + + async syncDeleteCipher(notification: SyncCipherNotification): Promise { + 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, false];