mirror of
https://github.com/bitwarden/browser
synced 2025-12-15 15:53:27 +00:00
[PS-1078] Refactor FolderService to use Observables (#3022)
This commit is contained in:
@@ -1,22 +1,22 @@
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
import { FolderData } from "../../models/data/folderData";
|
||||
import { Folder } from "../../models/domain/folder";
|
||||
import { SymmetricCryptoKey } from "../../models/domain/symmetricCryptoKey";
|
||||
import { TreeNode } from "../../models/domain/treeNode";
|
||||
import { FolderView } from "../../models/view/folderView";
|
||||
|
||||
export abstract class FolderService {
|
||||
clearCache: (userId?: string) => Promise<void>;
|
||||
folders$: Observable<Folder[]>;
|
||||
folderViews$: Observable<FolderView[]>;
|
||||
|
||||
clearCache: () => Promise<void>;
|
||||
encrypt: (model: FolderView, key?: SymmetricCryptoKey) => Promise<Folder>;
|
||||
get: (id: string) => Promise<Folder>;
|
||||
getAll: () => Promise<Folder[]>;
|
||||
getAllDecrypted: () => Promise<FolderView[]>;
|
||||
getAllNested: (folders?: FolderView[]) => Promise<TreeNode<FolderView>[]>;
|
||||
getNested: (id: string) => Promise<TreeNode<FolderView>>;
|
||||
}
|
||||
|
||||
export abstract class InternalFolderService extends FolderService {
|
||||
upsert: (folder: FolderData | FolderData[]) => Promise<any>;
|
||||
replace: (folders: { [id: string]: FolderData }) => Promise<any>;
|
||||
upsert: (folder: FolderData | FolderData[]) => Promise<void>;
|
||||
replace: (folders: { [id: string]: FolderData }) => Promise<void>;
|
||||
clear: (userId: string) => Promise<any>;
|
||||
delete: (id: string | string[]) => Promise<any>;
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ import { SymmetricCryptoKey } from "../models/domain/symmetricCryptoKey";
|
||||
import { WindowState } from "../models/domain/windowState";
|
||||
import { CipherView } from "../models/view/cipherView";
|
||||
import { CollectionView } from "../models/view/collectionView";
|
||||
import { FolderView } from "../models/view/folderView";
|
||||
import { SendView } from "../models/view/sendView";
|
||||
|
||||
export abstract class StateService<T extends Account = Account> {
|
||||
@@ -88,8 +87,6 @@ export abstract class StateService<T extends Account = Account> {
|
||||
value: SymmetricCryptoKey,
|
||||
options?: StorageOptions
|
||||
) => Promise<void>;
|
||||
getDecryptedFolders: (options?: StorageOptions) => Promise<FolderView[]>;
|
||||
setDecryptedFolders: (value: FolderView[], options?: StorageOptions) => Promise<void>;
|
||||
getDecryptedOrganizationKeys: (
|
||||
options?: StorageOptions
|
||||
) => Promise<Map<string, SymmetricCryptoKey>>;
|
||||
@@ -183,7 +180,13 @@ export abstract class StateService<T extends Account = Account> {
|
||||
) => Promise<void>;
|
||||
getEncryptedCryptoSymmetricKey: (options?: StorageOptions) => Promise<string>;
|
||||
setEncryptedCryptoSymmetricKey: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
/**
|
||||
* @deprecated Do not call this directly, use FolderService
|
||||
*/
|
||||
getEncryptedFolders: (options?: StorageOptions) => Promise<{ [id: string]: FolderData }>;
|
||||
/**
|
||||
* @deprecated Do not call this directly, use FolderService
|
||||
*/
|
||||
setEncryptedFolders: (
|
||||
value: { [id: string]: FolderData },
|
||||
options?: StorageOptions
|
||||
|
||||
@@ -11,7 +11,6 @@ import { ProviderData } from "../data/providerData";
|
||||
import { SendData } from "../data/sendData";
|
||||
import { CipherView } from "../view/cipherView";
|
||||
import { CollectionView } from "../view/collectionView";
|
||||
import { FolderView } from "../view/folderView";
|
||||
import { SendView } from "../view/sendView";
|
||||
|
||||
import { EncString } from "./encString";
|
||||
@@ -31,15 +30,19 @@ export class DataEncryptionPair<TEncrypted, TDecrypted> {
|
||||
decrypted?: TDecrypted[];
|
||||
}
|
||||
|
||||
// This is a temporary structure to handle migrated `DataEncryptionPair` to
|
||||
// avoid needing a data migration at this stage. It should be replaced with
|
||||
// proper data migrations when `DataEncryptionPair` is deprecated.
|
||||
export class TemporaryDataEncryption<TEncrypted> {
|
||||
encrypted?: { [id: string]: TEncrypted };
|
||||
}
|
||||
|
||||
export class AccountData {
|
||||
ciphers?: DataEncryptionPair<CipherData, CipherView> = new DataEncryptionPair<
|
||||
CipherData,
|
||||
CipherView
|
||||
>();
|
||||
folders?: DataEncryptionPair<FolderData, FolderView> = new DataEncryptionPair<
|
||||
FolderData,
|
||||
FolderView
|
||||
>();
|
||||
folders? = new TemporaryDataEncryption<FolderData>();
|
||||
localData?: any;
|
||||
sends?: DataEncryptionPair<SendData, SendView> = new DataEncryptionPair<SendData, SendView>();
|
||||
collections?: DataEncryptionPair<CollectionData, CollectionView> = new DataEncryptionPair<
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as papa from "papaparse";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { ApiService } from "../abstractions/api.service";
|
||||
import { CipherService } from "../abstractions/cipher.service";
|
||||
@@ -115,7 +116,7 @@ export class ExportService implements ExportServiceAbstraction {
|
||||
const promises = [];
|
||||
|
||||
promises.push(
|
||||
this.folderService.getAllDecrypted().then((folders) => {
|
||||
firstValueFrom(this.folderService.folderViews$).then((folders) => {
|
||||
decFolders = folders;
|
||||
})
|
||||
);
|
||||
@@ -191,7 +192,7 @@ export class ExportService implements ExportServiceAbstraction {
|
||||
const promises = [];
|
||||
|
||||
promises.push(
|
||||
this.folderService.getAll().then((f) => {
|
||||
firstValueFrom(this.folderService.folders$).then((f) => {
|
||||
folders = f;
|
||||
})
|
||||
);
|
||||
|
||||
@@ -1,31 +1,70 @@
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
|
||||
import { BroadcasterService } from "../../abstractions/broadcaster.service";
|
||||
import { CipherService } from "../../abstractions/cipher.service";
|
||||
import { CryptoService } from "../../abstractions/crypto.service";
|
||||
import { FolderService as FolderServiceAbstraction } from "../../abstractions/folder/folder.service.abstraction";
|
||||
import { I18nService } from "../../abstractions/i18n.service";
|
||||
import { StateService } from "../../abstractions/state.service";
|
||||
import { ServiceUtils } from "../../misc/serviceUtils";
|
||||
import { Utils } from "../../misc/utils";
|
||||
import { CipherData } from "../../models/data/cipherData";
|
||||
import { FolderData } from "../../models/data/folderData";
|
||||
import { Folder } from "../../models/domain/folder";
|
||||
import { SymmetricCryptoKey } from "../../models/domain/symmetricCryptoKey";
|
||||
import { TreeNode } from "../../models/domain/treeNode";
|
||||
import { FolderView } from "../../models/view/folderView";
|
||||
|
||||
const NestingDelimiter = "/";
|
||||
const BroadcasterSubscriptionId = "FolderService";
|
||||
|
||||
export class FolderService implements FolderServiceAbstraction {
|
||||
private _folders: BehaviorSubject<Folder[]> = new BehaviorSubject([]);
|
||||
private _folderViews: BehaviorSubject<FolderView[]> = new BehaviorSubject([]);
|
||||
|
||||
folders$ = this._folders.asObservable();
|
||||
folderViews$ = this._folderViews.asObservable();
|
||||
|
||||
constructor(
|
||||
private cryptoService: CryptoService,
|
||||
private i18nService: I18nService,
|
||||
private cipherService: CipherService,
|
||||
private stateService: StateService
|
||||
) {}
|
||||
private stateService: StateService,
|
||||
private broadcasterService: BroadcasterService
|
||||
) {
|
||||
this.stateService.activeAccount.subscribe(async (activeAccount) => {
|
||||
if ((Utils.global as any).bitwardenContainerService == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
async clearCache(userId?: string): Promise<void> {
|
||||
await this.stateService.setDecryptedFolders(null, { userId: userId });
|
||||
if (activeAccount == null) {
|
||||
this._folders.next([]);
|
||||
this._folderViews.next([]);
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await this.stateService.getEncryptedFolders();
|
||||
|
||||
await this.updateObservables(data);
|
||||
});
|
||||
|
||||
// TODO: Broadcasterservice should be removed or replaced with observables
|
||||
this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => {
|
||||
switch (message.command) {
|
||||
case "unlocked": {
|
||||
const data = await this.stateService.getEncryptedFolders();
|
||||
|
||||
await this.updateObservables(data);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async clearCache(): Promise<void> {
|
||||
this._folderViews.next([]);
|
||||
}
|
||||
|
||||
// TODO: This should be moved to EncryptService or something
|
||||
async encrypt(model: FolderView, key?: SymmetricCryptoKey): Promise<Folder> {
|
||||
const folder = new Folder();
|
||||
folder.id = model.id;
|
||||
@@ -34,75 +73,12 @@ export class FolderService implements FolderServiceAbstraction {
|
||||
}
|
||||
|
||||
async get(id: string): Promise<Folder> {
|
||||
const folders = await this.stateService.getEncryptedFolders();
|
||||
// eslint-disable-next-line
|
||||
if (folders == null || !folders.hasOwnProperty(id)) {
|
||||
return null;
|
||||
}
|
||||
const folders = this._folders.getValue();
|
||||
|
||||
return new Folder(folders[id]);
|
||||
return folders.find((folder) => folder.id === id);
|
||||
}
|
||||
|
||||
async getAll(): Promise<Folder[]> {
|
||||
const folders = await this.stateService.getEncryptedFolders();
|
||||
const response: Folder[] = [];
|
||||
for (const id in folders) {
|
||||
// eslint-disable-next-line
|
||||
if (folders.hasOwnProperty(id)) {
|
||||
response.push(new Folder(folders[id]));
|
||||
}
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
async getAllDecrypted(): Promise<FolderView[]> {
|
||||
const decryptedFolders = await this.stateService.getDecryptedFolders();
|
||||
if (decryptedFolders != null) {
|
||||
return decryptedFolders;
|
||||
}
|
||||
|
||||
const hasKey = await this.cryptoService.hasKey();
|
||||
if (!hasKey) {
|
||||
throw new Error("No key.");
|
||||
}
|
||||
|
||||
const decFolders: FolderView[] = [];
|
||||
const promises: Promise<any>[] = [];
|
||||
const folders = await this.getAll();
|
||||
folders.forEach((folder) => {
|
||||
promises.push(folder.decrypt().then((f) => decFolders.push(f)));
|
||||
});
|
||||
|
||||
await Promise.all(promises);
|
||||
decFolders.sort(Utils.getSortFunction(this.i18nService, "name"));
|
||||
|
||||
const noneFolder = new FolderView();
|
||||
noneFolder.name = this.i18nService.t("noneFolder");
|
||||
decFolders.push(noneFolder);
|
||||
|
||||
await this.stateService.setDecryptedFolders(decFolders);
|
||||
return decFolders;
|
||||
}
|
||||
|
||||
async getAllNested(folders?: FolderView[]): Promise<TreeNode<FolderView>[]> {
|
||||
folders = folders ?? (await this.getAllDecrypted());
|
||||
const nodes: TreeNode<FolderView>[] = [];
|
||||
folders.forEach((f) => {
|
||||
const folderCopy = new FolderView();
|
||||
folderCopy.id = f.id;
|
||||
folderCopy.revisionDate = f.revisionDate;
|
||||
const parts = f.name != null ? f.name.replace(/^\/+|\/+$/g, "").split(NestingDelimiter) : [];
|
||||
ServiceUtils.nestedTraverse(nodes, 0, parts, folderCopy, null, NestingDelimiter);
|
||||
});
|
||||
return nodes;
|
||||
}
|
||||
|
||||
async getNested(id: string): Promise<TreeNode<FolderView>> {
|
||||
const folders = await this.getAllNested();
|
||||
return ServiceUtils.getTreeNodeObject(folders, id) as TreeNode<FolderView>;
|
||||
}
|
||||
|
||||
async upsert(folder: FolderData | FolderData[]): Promise<any> {
|
||||
async upsert(folder: FolderData | FolderData[]): Promise<void> {
|
||||
let folders = await this.stateService.getEncryptedFolders();
|
||||
if (folders == null) {
|
||||
folders = {};
|
||||
@@ -117,17 +93,20 @@ export class FolderService implements FolderServiceAbstraction {
|
||||
});
|
||||
}
|
||||
|
||||
await this.stateService.setDecryptedFolders(null);
|
||||
await this.updateObservables(folders);
|
||||
await this.stateService.setEncryptedFolders(folders);
|
||||
}
|
||||
|
||||
async replace(folders: { [id: string]: FolderData }): Promise<any> {
|
||||
await this.stateService.setDecryptedFolders(null);
|
||||
async replace(folders: { [id: string]: FolderData }): Promise<void> {
|
||||
await this.updateObservables(folders);
|
||||
await this.stateService.setEncryptedFolders(folders);
|
||||
}
|
||||
|
||||
async clear(userId?: string): Promise<any> {
|
||||
await this.stateService.setDecryptedFolders(null, { userId: userId });
|
||||
if (userId == null || userId == (await this.stateService.getUserId())) {
|
||||
this._folders.next([]);
|
||||
this._folderViews.next([]);
|
||||
}
|
||||
await this.stateService.setEncryptedFolders(null, { userId: userId });
|
||||
}
|
||||
|
||||
@@ -148,7 +127,7 @@ export class FolderService implements FolderServiceAbstraction {
|
||||
});
|
||||
}
|
||||
|
||||
await this.stateService.setDecryptedFolders(null);
|
||||
await this.updateObservables(folders);
|
||||
await this.stateService.setEncryptedFolders(folders);
|
||||
|
||||
// Items in a deleted folder are re-assigned to "No Folder"
|
||||
@@ -166,4 +145,20 @@ export class FolderService implements FolderServiceAbstraction {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async updateObservables(foldersMap: { [id: string]: FolderData }) {
|
||||
const folders = Object.values(foldersMap || {}).map((f) => new Folder(f));
|
||||
|
||||
const decryptFolderPromises = folders.map((f) => f.decrypt());
|
||||
const decryptedFolders = await Promise.all(decryptFolderPromises);
|
||||
|
||||
decryptedFolders.sort(Utils.getSortFunction(this.i18nService, "name"));
|
||||
|
||||
const noneFolder = new FolderView();
|
||||
noneFolder.name = this.i18nService.t("noneFolder");
|
||||
decryptedFolders.push(noneFolder);
|
||||
|
||||
this._folders.next(folders);
|
||||
this._folderViews.next(decryptedFolders);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,6 @@ import { SymmetricCryptoKey } from "../models/domain/symmetricCryptoKey";
|
||||
import { WindowState } from "../models/domain/windowState";
|
||||
import { CipherView } from "../models/view/cipherView";
|
||||
import { CollectionView } from "../models/view/collectionView";
|
||||
import { FolderView } from "../models/view/folderView";
|
||||
import { SendView } from "../models/view/sendView";
|
||||
|
||||
const keys = {
|
||||
@@ -658,24 +657,6 @@ export class StateService<
|
||||
);
|
||||
}
|
||||
|
||||
@withPrototypeForArrayMembers(FolderView)
|
||||
async getDecryptedFolders(options?: StorageOptions): Promise<FolderView[]> {
|
||||
return (
|
||||
await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions()))
|
||||
)?.data?.folders?.decrypted;
|
||||
}
|
||||
|
||||
async setDecryptedFolders(value: FolderView[], options?: StorageOptions): Promise<void> {
|
||||
const account = await this.getAccount(
|
||||
this.reconcileOptions(options, await this.defaultInMemoryOptions())
|
||||
);
|
||||
account.data.folders.decrypted = value;
|
||||
await this.saveAccount(
|
||||
account,
|
||||
this.reconcileOptions(options, await this.defaultInMemoryOptions())
|
||||
);
|
||||
}
|
||||
|
||||
@withPrototypeForMap(SymmetricCryptoKey, SymmetricCryptoKey.initFromJson)
|
||||
async getDecryptedOrganizationKeys(
|
||||
options?: StorageOptions
|
||||
|
||||
@@ -80,6 +80,7 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction {
|
||||
|
||||
if (userId == null || userId === (await this.stateService.getUserId())) {
|
||||
this.searchService.clearIndex();
|
||||
await this.folderService.clearCache();
|
||||
}
|
||||
|
||||
await this.stateService.setEverBeenUnlocked(true, { userId: userId });
|
||||
@@ -91,7 +92,6 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction {
|
||||
await this.cryptoService.clearKeyPair(true, userId);
|
||||
await this.cryptoService.clearEncKey(true, userId);
|
||||
|
||||
await this.folderService.clearCache(userId);
|
||||
await this.cipherService.clearCache(userId);
|
||||
await this.collectionService.clearCache(userId);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user