mirror of
https://github.com/bitwarden/browser
synced 2026-01-02 00:23:35 +00:00
[SG-998] and [SG-999] Vault and Autofill team refactor (#4542)
* Move DeprecatedVaultFilterService to vault folder * [libs] move VaultItemsComponent * [libs] move AddEditComponent * [libs] move AddEditCustomFields * [libs] move attachmentsComponent * [libs] folderAddEditComponent * [libs] IconComponent * [libs] PasswordRepormptComponent * [libs] PremiumComponent * [libs] ViewCustomFieldsComponent * [libs] ViewComponent * [libs] PasswordRepromptService * [libs] Move FolderService and FolderApiService abstractions * [libs] FolderService imports * [libs] PasswordHistoryComponent * [libs] move Sync and SyncNotifier abstractions * [libs] SyncService imports * [libs] fix file casing for passwordReprompt abstraction * [libs] SyncNotifier import fix * [libs] CipherServiceAbstraction * [libs] PasswordRepromptService abstraction * [libs] Fix file casing for angular passwordReprompt service * [libs] fix file casing for SyncNotifierService * [libs] CipherRepromptType * [libs] rename CipherRepromptType * [libs] CipherType * [libs] Rename CipherType * [libs] CipherData * [libs] FolderData * [libs] PasswordHistoryData * [libs] AttachmentData * [libs] CardData * [libs] FieldData * [libs] IdentityData * [libs] LocalData * [libs] LoginData * [libs] SecureNoteData * [libs] LoginUriData * [libs] Domain classes * [libs] SecureNote * [libs] Request models * [libs] Response models * [libs] View part 1 * [libs] Views part 2 * [libs] Move folder services * [libs] Views fixes * [libs] Move sync services * [libs] cipher service * [libs] Types * [libs] Sync file casing * [libs] Fix folder service import * [libs] Move spec files * [libs] casing fixes on spec files * [browser] Autofill background, clipboard, commands * [browser] Fix ContextMenusBackground casing * [browser] Rename fix * [browser] Autofill content * [browser] autofill.js * [libs] enpass importer spec fix * [browser] autofill models * [browser] autofill manifest path updates * [browser] Autofill notification files * [browser] autofill services * [browser] Fix file casing * [browser] Vault popup loose components * [browser] Vault components * [browser] Manifest fixes * [browser] Vault services * [cli] vault commands and models * [browser] File capitilization fixes * [desktop] Vault components and services * [web] vault loose components * [web] Vault components * [browser] Fix misc-utils import * [libs] Fix psono spec imports * [fix] Add comments to address lint rules
This commit is contained in:
50
libs/common/src/vault/services/folder/folder-api.service.ts
Normal file
50
libs/common/src/vault/services/folder/folder-api.service.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { ApiService } from "../../../abstractions/api.service";
|
||||
import { FolderApiServiceAbstraction } from "../../../vault/abstractions/folder/folder-api.service.abstraction";
|
||||
import { InternalFolderService } from "../../../vault/abstractions/folder/folder.service.abstraction";
|
||||
import { FolderData } from "../../../vault/models/data/folder.data";
|
||||
import { Folder } from "../../../vault/models/domain/folder";
|
||||
import { FolderRequest } from "../../../vault/models/request/folder.request";
|
||||
import { FolderResponse } from "../../../vault/models/response/folder.response";
|
||||
|
||||
export class FolderApiService implements FolderApiServiceAbstraction {
|
||||
constructor(private folderService: InternalFolderService, private apiService: ApiService) {}
|
||||
|
||||
async save(folder: Folder): Promise<any> {
|
||||
const request = new FolderRequest(folder);
|
||||
|
||||
let response: FolderResponse;
|
||||
if (folder.id == null) {
|
||||
response = await this.postFolder(request);
|
||||
folder.id = response.id;
|
||||
} else {
|
||||
response = await this.putFolder(folder.id, request);
|
||||
}
|
||||
|
||||
const data = new FolderData(response);
|
||||
await this.folderService.upsert(data);
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<any> {
|
||||
await this.deleteFolder(id);
|
||||
await this.folderService.delete(id);
|
||||
}
|
||||
|
||||
async get(id: string): Promise<FolderResponse> {
|
||||
const r = await this.apiService.send("GET", "/folders/" + id, null, true, true);
|
||||
return new FolderResponse(r);
|
||||
}
|
||||
|
||||
private async postFolder(request: FolderRequest): Promise<FolderResponse> {
|
||||
const r = await this.apiService.send("POST", "/folders", request, true, true);
|
||||
return new FolderResponse(r);
|
||||
}
|
||||
|
||||
async putFolder(id: string, request: FolderRequest): Promise<FolderResponse> {
|
||||
const r = await this.apiService.send("PUT", "/folders/" + id, request, true, true);
|
||||
return new FolderResponse(r);
|
||||
}
|
||||
|
||||
private deleteFolder(id: string): Promise<any> {
|
||||
return this.apiService.send("DELETE", "/folders/" + id, null, true, false);
|
||||
}
|
||||
}
|
||||
202
libs/common/src/vault/services/folder/folder.service.spec.ts
Normal file
202
libs/common/src/vault/services/folder/folder.service.spec.ts
Normal file
@@ -0,0 +1,202 @@
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { Arg, Substitute, SubstituteOf } from "@fluffy-spoon/substitute";
|
||||
import { BehaviorSubject, firstValueFrom } from "rxjs";
|
||||
|
||||
import { CryptoService } from "../../../abstractions/crypto.service";
|
||||
import { EncryptService } from "../../../abstractions/encrypt.service";
|
||||
import { I18nService } from "../../../abstractions/i18n.service";
|
||||
import { EncString } from "../../../models/domain/enc-string";
|
||||
import { ContainerService } from "../../../services/container.service";
|
||||
import { StateService } from "../../../services/state.service";
|
||||
import { CipherService } from "../../abstractions/cipher.service";
|
||||
import { FolderData } from "../../models/data/folder.data";
|
||||
import { FolderView } from "../../models/view/folder.view";
|
||||
import { FolderService } from "../../services/folder/folder.service";
|
||||
|
||||
describe("Folder Service", () => {
|
||||
let folderService: FolderService;
|
||||
|
||||
let cryptoService: SubstituteOf<CryptoService>;
|
||||
let encryptService: SubstituteOf<EncryptService>;
|
||||
let i18nService: SubstituteOf<I18nService>;
|
||||
let cipherService: SubstituteOf<CipherService>;
|
||||
let stateService: SubstituteOf<StateService>;
|
||||
let activeAccount: BehaviorSubject<string>;
|
||||
let activeAccountUnlocked: BehaviorSubject<boolean>;
|
||||
|
||||
beforeEach(() => {
|
||||
cryptoService = Substitute.for();
|
||||
encryptService = Substitute.for();
|
||||
i18nService = Substitute.for();
|
||||
cipherService = Substitute.for();
|
||||
stateService = Substitute.for();
|
||||
activeAccount = new BehaviorSubject("123");
|
||||
activeAccountUnlocked = new BehaviorSubject(true);
|
||||
|
||||
stateService.getEncryptedFolders().resolves({
|
||||
"1": folderData("1", "test"),
|
||||
});
|
||||
stateService.activeAccount$.returns(activeAccount);
|
||||
stateService.activeAccountUnlocked$.returns(activeAccountUnlocked);
|
||||
(window as any).bitwardenContainerService = new ContainerService(cryptoService, encryptService);
|
||||
|
||||
folderService = new FolderService(cryptoService, i18nService, cipherService, stateService);
|
||||
});
|
||||
|
||||
it("encrypt", async () => {
|
||||
const model = new FolderView();
|
||||
model.id = "2";
|
||||
model.name = "Test Folder";
|
||||
|
||||
cryptoService.encrypt(Arg.any()).resolves(new EncString("ENC"));
|
||||
cryptoService.decryptToUtf8(Arg.any()).resolves("DEC");
|
||||
|
||||
const result = await folderService.encrypt(model);
|
||||
|
||||
expect(result).toEqual({
|
||||
id: "2",
|
||||
name: {
|
||||
encryptedString: "ENC",
|
||||
encryptionType: 0,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
describe("get", () => {
|
||||
it("exists", async () => {
|
||||
const result = await folderService.get("1");
|
||||
|
||||
expect(result).toEqual({
|
||||
id: "1",
|
||||
name: {
|
||||
decryptedValue: [],
|
||||
encryptedString: "test",
|
||||
encryptionType: 0,
|
||||
},
|
||||
revisionDate: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("not exists", async () => {
|
||||
const result = await folderService.get("2");
|
||||
|
||||
expect(result).toBe(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
it("upsert", async () => {
|
||||
await folderService.upsert(folderData("2", "test 2"));
|
||||
|
||||
expect(await firstValueFrom(folderService.folders$)).toEqual([
|
||||
{
|
||||
id: "1",
|
||||
name: {
|
||||
decryptedValue: [],
|
||||
encryptedString: "test",
|
||||
encryptionType: 0,
|
||||
},
|
||||
revisionDate: null,
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
name: {
|
||||
decryptedValue: [],
|
||||
encryptedString: "test 2",
|
||||
encryptionType: 0,
|
||||
},
|
||||
revisionDate: null,
|
||||
},
|
||||
]);
|
||||
|
||||
expect(await firstValueFrom(folderService.folderViews$)).toEqual([
|
||||
{ id: "1", name: [], revisionDate: null },
|
||||
{ id: "2", name: [], revisionDate: null },
|
||||
{ id: null, name: [], revisionDate: null },
|
||||
]);
|
||||
});
|
||||
|
||||
it("replace", async () => {
|
||||
await folderService.replace({ "2": folderData("2", "test 2") });
|
||||
|
||||
expect(await firstValueFrom(folderService.folders$)).toEqual([
|
||||
{
|
||||
id: "2",
|
||||
name: {
|
||||
decryptedValue: [],
|
||||
encryptedString: "test 2",
|
||||
encryptionType: 0,
|
||||
},
|
||||
revisionDate: null,
|
||||
},
|
||||
]);
|
||||
|
||||
expect(await firstValueFrom(folderService.folderViews$)).toEqual([
|
||||
{ id: "2", name: [], revisionDate: null },
|
||||
{ id: null, name: [], revisionDate: null },
|
||||
]);
|
||||
});
|
||||
|
||||
it("delete", async () => {
|
||||
await folderService.delete("1");
|
||||
|
||||
expect((await firstValueFrom(folderService.folders$)).length).toBe(0);
|
||||
|
||||
expect(await firstValueFrom(folderService.folderViews$)).toEqual([
|
||||
{ id: null, name: [], revisionDate: null },
|
||||
]);
|
||||
});
|
||||
|
||||
it("clearCache", async () => {
|
||||
await folderService.clearCache();
|
||||
|
||||
expect((await firstValueFrom(folderService.folders$)).length).toBe(1);
|
||||
expect((await firstValueFrom(folderService.folderViews$)).length).toBe(0);
|
||||
});
|
||||
|
||||
it("locking should clear", async () => {
|
||||
activeAccountUnlocked.next(false);
|
||||
// Sleep for 100ms to avoid timing issues
|
||||
await new Promise((r) => setTimeout(r, 100));
|
||||
|
||||
expect((await firstValueFrom(folderService.folders$)).length).toBe(0);
|
||||
expect((await firstValueFrom(folderService.folderViews$)).length).toBe(0);
|
||||
});
|
||||
|
||||
describe("clear", () => {
|
||||
it("null userId", async () => {
|
||||
await folderService.clear();
|
||||
|
||||
stateService.received(1).setEncryptedFolders(Arg.any(), Arg.any());
|
||||
|
||||
expect((await firstValueFrom(folderService.folders$)).length).toBe(0);
|
||||
expect((await firstValueFrom(folderService.folderViews$)).length).toBe(0);
|
||||
});
|
||||
|
||||
it("matching userId", async () => {
|
||||
stateService.getUserId().resolves("1");
|
||||
await folderService.clear("1");
|
||||
|
||||
stateService.received(1).setEncryptedFolders(Arg.any(), Arg.any());
|
||||
|
||||
expect((await firstValueFrom(folderService.folders$)).length).toBe(0);
|
||||
expect((await firstValueFrom(folderService.folderViews$)).length).toBe(0);
|
||||
});
|
||||
|
||||
it("missmatching userId", async () => {
|
||||
await folderService.clear("12");
|
||||
|
||||
stateService.received(1).setEncryptedFolders(Arg.any(), Arg.any());
|
||||
|
||||
expect((await firstValueFrom(folderService.folders$)).length).toBe(1);
|
||||
expect((await firstValueFrom(folderService.folderViews$)).length).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
function folderData(id: string, name: string) {
|
||||
const data = new FolderData({} as any);
|
||||
data.id = id;
|
||||
data.name = name;
|
||||
|
||||
return data;
|
||||
}
|
||||
});
|
||||
193
libs/common/src/vault/services/folder/folder.service.ts
Normal file
193
libs/common/src/vault/services/folder/folder.service.ts
Normal file
@@ -0,0 +1,193 @@
|
||||
import { BehaviorSubject, concatMap } from "rxjs";
|
||||
|
||||
import { CryptoService } from "../../../abstractions/crypto.service";
|
||||
import { I18nService } from "../../../abstractions/i18n.service";
|
||||
import { StateService } from "../../../abstractions/state.service";
|
||||
import { Utils } from "../../../misc/utils";
|
||||
import { SymmetricCryptoKey } from "../../../models/domain/symmetric-crypto-key";
|
||||
import { CipherService } from "../../../vault/abstractions/cipher.service";
|
||||
import { InternalFolderService as InternalFolderServiceAbstraction } from "../../../vault/abstractions/folder/folder.service.abstraction";
|
||||
import { CipherData } from "../../../vault/models/data/cipher.data";
|
||||
import { FolderData } from "../../../vault/models/data/folder.data";
|
||||
import { Folder } from "../../../vault/models/domain/folder";
|
||||
import { FolderView } from "../../../vault/models/view/folder.view";
|
||||
|
||||
export class FolderService implements InternalFolderServiceAbstraction {
|
||||
protected _folders: BehaviorSubject<Folder[]> = new BehaviorSubject([]);
|
||||
protected _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
|
||||
) {
|
||||
this.stateService.activeAccountUnlocked$
|
||||
.pipe(
|
||||
concatMap(async (unlocked) => {
|
||||
if (Utils.global.bitwardenContainerService == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!unlocked) {
|
||||
this._folders.next([]);
|
||||
this._folderViews.next([]);
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await this.stateService.getEncryptedFolders();
|
||||
|
||||
await this.updateObservables(data);
|
||||
})
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
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;
|
||||
folder.name = await this.cryptoService.encrypt(model.name, key);
|
||||
return folder;
|
||||
}
|
||||
|
||||
async get(id: string): Promise<Folder> {
|
||||
const folders = this._folders.getValue();
|
||||
|
||||
return folders.find((folder) => folder.id === id);
|
||||
}
|
||||
|
||||
async getAllFromState(): 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated For the CLI only
|
||||
* @param id id of the folder
|
||||
*/
|
||||
async getFromState(id: string): Promise<Folder> {
|
||||
const foldersMap = await this.stateService.getEncryptedFolders();
|
||||
const folder = foldersMap[id];
|
||||
if (folder == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Folder(folder);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Only use in CLI!
|
||||
*/
|
||||
async getAllDecryptedFromState(): Promise<FolderView[]> {
|
||||
const data = await this.stateService.getEncryptedFolders();
|
||||
const folders = Object.values(data || {}).map((f) => new Folder(f));
|
||||
|
||||
return this.decryptFolders(folders);
|
||||
}
|
||||
|
||||
async upsert(folder: FolderData | FolderData[]): Promise<void> {
|
||||
let folders = await this.stateService.getEncryptedFolders();
|
||||
if (folders == null) {
|
||||
folders = {};
|
||||
}
|
||||
|
||||
if (folder instanceof FolderData) {
|
||||
const f = folder as FolderData;
|
||||
folders[f.id] = f;
|
||||
} else {
|
||||
(folder as FolderData[]).forEach((f) => {
|
||||
folders[f.id] = f;
|
||||
});
|
||||
}
|
||||
|
||||
await this.updateObservables(folders);
|
||||
await this.stateService.setEncryptedFolders(folders);
|
||||
}
|
||||
|
||||
async replace(folders: { [id: string]: FolderData }): Promise<void> {
|
||||
await this.updateObservables(folders);
|
||||
await this.stateService.setEncryptedFolders(folders);
|
||||
}
|
||||
|
||||
async clear(userId?: string): Promise<any> {
|
||||
if (userId == null || userId == (await this.stateService.getUserId())) {
|
||||
this._folders.next([]);
|
||||
this._folderViews.next([]);
|
||||
}
|
||||
await this.stateService.setEncryptedFolders(null, { userId: userId });
|
||||
}
|
||||
|
||||
async delete(id: string | string[]): Promise<any> {
|
||||
const folders = await this.stateService.getEncryptedFolders();
|
||||
if (folders == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof id === "string") {
|
||||
if (folders[id] == null) {
|
||||
return;
|
||||
}
|
||||
delete folders[id];
|
||||
} else {
|
||||
(id as string[]).forEach((i) => {
|
||||
delete folders[i];
|
||||
});
|
||||
}
|
||||
|
||||
await this.updateObservables(folders);
|
||||
await this.stateService.setEncryptedFolders(folders);
|
||||
|
||||
// Items in a deleted folder are re-assigned to "No Folder"
|
||||
const ciphers = await this.stateService.getEncryptedCiphers();
|
||||
if (ciphers != null) {
|
||||
const updates: CipherData[] = [];
|
||||
for (const cId in ciphers) {
|
||||
if (ciphers[cId].folderId === id) {
|
||||
ciphers[cId].folderId = null;
|
||||
updates.push(ciphers[cId]);
|
||||
}
|
||||
}
|
||||
if (updates.length > 0) {
|
||||
this.cipherService.upsert(updates);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async updateObservables(foldersMap: { [id: string]: FolderData }) {
|
||||
const folders = Object.values(foldersMap || {}).map((f) => new Folder(f));
|
||||
|
||||
this._folders.next(folders);
|
||||
|
||||
if (await this.cryptoService.hasKey()) {
|
||||
this._folderViews.next(await this.decryptFolders(folders));
|
||||
}
|
||||
}
|
||||
|
||||
private async decryptFolders(folders: Folder[]) {
|
||||
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);
|
||||
|
||||
return decryptedFolders;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user