sync folders and ciphers. fix dates

This commit is contained in:
Kyle Spearrin 2018-08-20 16:01:26 -04:00
parent ddee5908f1
commit d0c51bacfd
15 changed files with 127 additions and 49 deletions

View File

@ -117,6 +117,7 @@ export abstract class ApiService {
postAccountRecoverDelete: (request: DeleteRecoverRequest) => Promise<any>; postAccountRecoverDelete: (request: DeleteRecoverRequest) => Promise<any>;
postAccountRecoverDeleteToken: (request: VerifyDeleteRecoverRequest) => Promise<any>; postAccountRecoverDeleteToken: (request: VerifyDeleteRecoverRequest) => Promise<any>;
getFolder: (id: string) => Promise<FolderResponse>;
postFolder: (request: FolderRequest) => Promise<FolderResponse>; postFolder: (request: FolderRequest) => Promise<FolderResponse>;
putFolder: (id: string, request: FolderRequest) => Promise<FolderResponse>; putFolder: (id: string, request: FolderRequest) => Promise<FolderResponse>;
deleteFolder: (id: string) => Promise<any>; deleteFolder: (id: string) => Promise<any>;

View File

@ -1,9 +1,16 @@
import {
SyncCipherNotification,
SyncFolderNotification,
} from '../models/response/notificationResponse';
export abstract class SyncService { export abstract class SyncService {
syncInProgress: boolean; syncInProgress: boolean;
getLastSync: () => Promise<Date>; getLastSync: () => Promise<Date>;
setLastSync: (date: Date) => Promise<any>; setLastSync: (date: Date) => Promise<any>;
syncStarted: () => void;
syncCompleted: (successfully: boolean) => void;
fullSync: (forceSync: boolean) => Promise<boolean>; fullSync: (forceSync: boolean) => Promise<boolean>;
syncUpsertFolder: (notification: SyncFolderNotification) => Promise<boolean>;
syncDeleteFolder: (notification: SyncFolderNotification) => Promise<boolean>;
syncUpsertCipher: (notification: SyncCipherNotification) => Promise<boolean>;
syncDeleteCipher: (notification: SyncFolderNotification) => Promise<boolean>;
} }

View File

@ -18,7 +18,7 @@ export class CipherData {
edit: boolean; edit: boolean;
organizationUseTotp: boolean; organizationUseTotp: boolean;
favorite: boolean; favorite: boolean;
revisionDate: Date; revisionDate: string;
type: CipherType; type: CipherType;
sizeName: string; sizeName: string;
name: string; name: string;

View File

@ -2,7 +2,7 @@ import { PasswordHistoryResponse } from '../response/passwordHistoryResponse';
export class PasswordHistoryData { export class PasswordHistoryData {
password: string; password: string;
lastUsedDate: Date; lastUsedDate: string;
constructor(response?: PasswordHistoryResponse) { constructor(response?: PasswordHistoryResponse) {
if (response == null) { if (response == null) {

View File

@ -54,7 +54,7 @@ export class Cipher extends Domain {
this.favorite = obj.favorite; this.favorite = obj.favorite;
this.organizationUseTotp = obj.organizationUseTotp; this.organizationUseTotp = obj.organizationUseTotp;
this.edit = obj.edit; this.edit = obj.edit;
this.revisionDate = obj.revisionDate; this.revisionDate = obj.revisionDate != null ? new Date(obj.revisionDate) : null;
this.collectionIds = obj.collectionIds; this.collectionIds = obj.collectionIds;
this.localData = localData; this.localData = localData;
@ -178,7 +178,7 @@ export class Cipher extends Domain {
c.edit = this.edit; c.edit = this.edit;
c.organizationUseTotp = this.organizationUseTotp; c.organizationUseTotp = this.organizationUseTotp;
c.favorite = this.favorite; c.favorite = this.favorite;
c.revisionDate = this.revisionDate; c.revisionDate = this.revisionDate.toISOString();
c.type = this.type; c.type = this.type;
c.collectionIds = this.collectionIds; c.collectionIds = this.collectionIds;

View File

@ -8,6 +8,7 @@ import Domain from './domain';
export class Folder extends Domain { export class Folder extends Domain {
id: string; id: string;
name: CipherString; name: CipherString;
revisionDate: Date;
constructor(obj?: FolderData, alreadyEncrypted: boolean = false) { constructor(obj?: FolderData, alreadyEncrypted: boolean = false) {
super(); super();
@ -19,6 +20,8 @@ export class Folder extends Domain {
id: null, id: null,
name: null, name: null,
}, alreadyEncrypted, ['id']); }, alreadyEncrypted, ['id']);
this.revisionDate = obj.revisionDate != null ? new Date(obj.revisionDate) : null;
} }
decrypt(): Promise<FolderView> { decrypt(): Promise<FolderView> {

View File

@ -17,8 +17,8 @@ export class Password extends Domain {
this.buildDomainModel(this, obj, { this.buildDomainModel(this, obj, {
password: null, password: null,
lastUsedDate: null, }, alreadyEncrypted);
}, alreadyEncrypted, ['lastUsedDate']); this.lastUsedDate = new Date(obj.lastUsedDate);
} }
async decrypt(orgId: string): Promise<PasswordHistoryView> { async decrypt(orgId: string): Promise<PasswordHistoryView> {
@ -30,7 +30,7 @@ export class Password extends Domain {
toPasswordHistoryData(): PasswordHistoryData { toPasswordHistoryData(): PasswordHistoryData {
const ph = new PasswordHistoryData(); const ph = new PasswordHistoryData();
ph.lastUsedDate = this.lastUsedDate; ph.lastUsedDate = this.lastUsedDate.toISOString();
this.buildDataModel(this, ph, { this.buildDataModel(this, ph, {
password: null, password: null,
}); });

View File

@ -1,9 +1,4 @@
export class PasswordHistoryRequest { export class PasswordHistoryRequest {
password: string; password: string;
lastUsedDate: Date; lastUsedDate: Date;
constructor(response: any) {
this.password = response.Password;
this.lastUsedDate = response.LastUsedDate;
}
} }

View File

@ -22,7 +22,7 @@ export class CipherResponse {
favorite: boolean; favorite: boolean;
edit: boolean; edit: boolean;
organizationUseTotp: boolean; organizationUseTotp: boolean;
revisionDate: Date; revisionDate: string;
attachments: AttachmentResponse[]; attachments: AttachmentResponse[];
passwordHistory: PasswordHistoryResponse[]; passwordHistory: PasswordHistoryResponse[];
collectionIds: string[]; collectionIds: string[];

View File

@ -1,6 +1,6 @@
export class PasswordHistoryResponse { export class PasswordHistoryResponse {
password: string; password: string;
lastUsedDate: Date; lastUsedDate: string;
constructor(response: any) { constructor(response: any) {
this.password = response.Password; this.password = response.Password;

View File

@ -286,6 +286,11 @@ export class ApiService implements ApiServiceAbstraction {
// Folder APIs // Folder APIs
async getFolder(id: string): Promise<FolderResponse> {
const r = await this.send('GET', '/folders/' + id, null, true, true);
return new FolderResponse(r);
}
async postFolder(request: FolderRequest): Promise<FolderResponse> { async postFolder(request: FolderRequest): Promise<FolderResponse> {
const r = await this.send('POST', '/folders', request, true, true); const r = await this.send('POST', '/folders', request, true, true);
return new FolderResponse(r); return new FolderResponse(r);

View File

@ -644,7 +644,9 @@ export class CipherService implements CipherServiceAbstraction {
} }
if (typeof id === 'string') { if (typeof id === 'string') {
const i = id as string; if (ciphers[id] == null) {
return;
}
delete ciphers[id]; delete ciphers[id];
} else { } else {
(id as string[]).forEach((i) => { (id as string[]).forEach((i) => {

View File

@ -152,7 +152,9 @@ export class FolderService implements FolderServiceAbstraction {
} }
if (typeof id === 'string') { if (typeof id === 'string') {
const i = id as string; if (folders[id] == null) {
return;
}
delete folders[id]; delete folders[id];
} else { } else {
(id as string[]).forEach((i) => { (id as string[]).forEach((i) => {

View File

@ -2,23 +2,24 @@ import * as signalR from '@aspnet/signalr';
import { NotificationType } from '../enums/notificationType'; import { NotificationType } from '../enums/notificationType';
import { CipherService } from '../abstractions/cipher.service'; import { AppIdService } from '../abstractions/appId.service';
import { CollectionService } from '../abstractions/collection.service';
import { EnvironmentService } from '../abstractions/environment.service'; import { EnvironmentService } from '../abstractions/environment.service';
import { FolderService } from '../abstractions/folder.service';
import { NotificationsService as NotificationsServiceAbstraction } from '../abstractions/notifications.service'; import { NotificationsService as NotificationsServiceAbstraction } from '../abstractions/notifications.service';
import { SettingsService } from '../abstractions/settings.service';
import { SyncService } from '../abstractions/sync.service'; import { SyncService } from '../abstractions/sync.service';
import { TokenService } from '../abstractions/token.service'; import { TokenService } from '../abstractions/token.service';
import { UserService } from '../abstractions/user.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 { export class NotificationsService implements NotificationsServiceAbstraction {
private signalrConnection: signalR.HubConnection; private signalrConnection: signalR.HubConnection;
constructor(private userService: UserService, private tokenService: TokenService, constructor(private userService: UserService, private tokenService: TokenService,
private syncService: SyncService) { } private syncService: SyncService, private appIdService: AppIdService) { }
async init(environmentService: EnvironmentService): Promise<void> { async init(environmentService: EnvironmentService): Promise<void> {
let url = 'https://notifications.bitwarden.com'; let url = 'https://notifications.bitwarden.com';
@ -61,21 +62,26 @@ export class NotificationsService implements NotificationsServiceAbstraction {
} }
private async processNotification(notification: NotificationResponse) { private async processNotification(notification: NotificationResponse) {
if (notification == null) { const appId = await this.appIdService.getAppId();
if (notification == null || notification.contextId === appId) {
return; return;
} }
switch (notification.type) { switch (notification.type) {
case NotificationType.SyncCipherCreate: case NotificationType.SyncCipherCreate:
case NotificationType.SyncCipherDelete:
case NotificationType.SyncCipherUpdate: case NotificationType.SyncCipherUpdate:
this.syncService.syncUpsertCipher(notification.payload as SyncCipherNotification);
break;
case NotificationType.SyncCipherDelete:
case NotificationType.SyncLoginDelete: case NotificationType.SyncLoginDelete:
this.syncService.fullSync(false); this.syncService.syncDeleteCipher(notification.payload as SyncCipherNotification);
break; break;
case NotificationType.SyncFolderCreate: case NotificationType.SyncFolderCreate:
case NotificationType.SyncFolderDelete:
case NotificationType.SyncFolderUpdate: 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; break;
case NotificationType.SyncVault: case NotificationType.SyncVault:
case NotificationType.SyncCiphers: case NotificationType.SyncCiphers:

View File

@ -18,6 +18,10 @@ import { CipherResponse } from '../models/response/cipherResponse';
import { CollectionDetailsResponse } from '../models/response/collectionResponse'; import { CollectionDetailsResponse } from '../models/response/collectionResponse';
import { DomainsResponse } from '../models/response/domainsResponse'; import { DomainsResponse } from '../models/response/domainsResponse';
import { FolderResponse } from '../models/response/folderResponse'; import { FolderResponse } from '../models/response/folderResponse';
import {
SyncCipherNotification,
SyncFolderNotification,
} from '../models/response/notificationResponse';
import { ProfileResponse } from '../models/response/profileResponse'; import { ProfileResponse } from '../models/response/profileResponse';
const Keys = { const Keys = {
@ -57,22 +61,11 @@ export class SyncService implements SyncServiceAbstraction {
await this.storageService.save(Keys.lastSyncPrefix + userId, date.toJSON()); 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<boolean> { async fullSync(forceSync: boolean): Promise<boolean> {
this.syncStarted(); this.syncStarted();
const isAuthenticated = await this.userService.isAuthenticated(); const isAuthenticated = await this.userService.isAuthenticated();
if (!isAuthenticated) { if (!isAuthenticated) {
this.syncCompleted(false); return this.syncCompleted(false);
return false;
} }
const now = new Date(); const now = new Date();
@ -81,14 +74,12 @@ export class SyncService implements SyncServiceAbstraction {
const skipped = needsSyncResult[1]; const skipped = needsSyncResult[1];
if (skipped) { if (skipped) {
this.syncCompleted(false); return this.syncCompleted(false);
return false;
} }
if (!needsSync) { if (!needsSync) {
await this.setLastSync(now); await this.setLastSync(now);
this.syncCompleted(false); return this.syncCompleted(false);
return false;
} }
const userId = await this.userService.getUserId(); const userId = await this.userService.getUserId();
@ -102,16 +93,82 @@ export class SyncService implements SyncServiceAbstraction {
await this.syncSettings(userId, response.domains); await this.syncSettings(userId, response.domains);
await this.setLastSync(now); await this.setLastSync(now);
this.syncCompleted(true); return this.syncCompleted(true);
return true;
} catch (e) { } catch (e) {
this.syncCompleted(false); return this.syncCompleted(false);
return false;
} }
} }
async syncUpsertFolder(notification: SyncFolderNotification): Promise<boolean> {
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<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): Promise<boolean> {
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<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 // 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) { private async needsSyncing(forceSync: boolean) {
if (forceSync) { if (forceSync) {
return [true, false]; return [true, false];