mirror of
https://github.com/bitwarden/browser.git
synced 2025-02-02 23:11:40 +01:00
sync folders and ciphers. fix dates
This commit is contained in:
parent
ddee5908f1
commit
d0c51bacfd
@ -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>;
|
||||||
|
@ -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>;
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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) {
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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> {
|
||||||
|
@ -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,
|
||||||
});
|
});
|
||||||
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -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[];
|
||||||
|
@ -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;
|
||||||
|
@ -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);
|
||||||
|
@ -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) => {
|
||||||
|
@ -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) => {
|
||||||
|
@ -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:
|
||||||
|
@ -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];
|
||||||
|
Loading…
Reference in New Issue
Block a user