From d1fe72a4ab37bd87836e8a2ad03c6c66999c03de Mon Sep 17 00:00:00 2001 From: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com> Date: Tue, 17 Dec 2024 20:34:30 +0100 Subject: [PATCH 01/54] Fix the maxlength org name bug (#12397) --- apps/web/src/locales/en/messages.json | 3 +++ .../providers/clients/create-client-dialog.component.html | 8 ++++++++ .../providers/clients/create-client-dialog.component.ts | 2 +- 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index abc3aa3a1d7..36143682fa2 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -9969,5 +9969,8 @@ }, "domainClaimed": { "message": "Domain claimed" + }, + "organizationNameMaxLength": { + "message": "Organization name cannot exceed 50 characters." } } diff --git a/bitwarden_license/bit-web/src/app/billing/providers/clients/create-client-dialog.component.html b/bitwarden_license/bit-web/src/app/billing/providers/clients/create-client-dialog.component.html index a08f5710f1e..78f2cb41bef 100644 --- a/bitwarden_license/bit-web/src/app/billing/providers/clients/create-client-dialog.component.html +++ b/bitwarden_license/bit-web/src/app/billing/providers/clients/create-client-dialog.component.html @@ -43,6 +43,14 @@ {{ "organizationName" | i18n }} + + {{ "organizationNameMaxLength" | i18n }} + diff --git a/bitwarden_license/bit-web/src/app/billing/providers/clients/create-client-dialog.component.ts b/bitwarden_license/bit-web/src/app/billing/providers/clients/create-client-dialog.component.ts index 18910491a0c..2a27b1b32f3 100644 --- a/bitwarden_license/bit-web/src/app/billing/providers/clients/create-client-dialog.component.ts +++ b/bitwarden_license/bit-web/src/app/billing/providers/clients/create-client-dialog.component.ts @@ -47,7 +47,7 @@ export class CreateClientDialogComponent implements OnInit { protected discountPercentage: number; protected formGroup = new FormGroup({ clientOwnerEmail: new FormControl("", [Validators.required, Validators.email]), - organizationName: new FormControl("", [Validators.required]), + organizationName: new FormControl("", [Validators.required, Validators.maxLength(50)]), seats: new FormControl(null, [Validators.required, Validators.min(1)]), }); protected loading = true; From e2e9a7c345e0594efbf258f17f76d9dd6ecf83ae Mon Sep 17 00:00:00 2001 From: Daniel James Smith <2670567+djsmith85@users.noreply.github.com> Date: Tue, 17 Dec 2024 20:51:58 +0100 Subject: [PATCH 02/54] [PM-15483] PasswordXP-CSV-Importer: Add support for German and Dutch headers (#12216) * Add tests to verify importing German and Dutch headers work * Add method to translate the headers from (German/Dutch into English) while the CSV data is being parsed * Report "importFormatError" when header translation did not work, instead of a generic undefined error (startsWith) * Move passwordxp-csv-importer into a dedicated folder * Introduce files with the language header translations --------- Co-authored-by: Daniel James Smith --- .../spec/passwordxp-csv-importer.spec.ts | 80 ++++++++++++------- .../test-data/passwordxp-csv/dutch-headers.ts | 7 ++ .../passwordxp-csv/german-headers.ts | 7 ++ libs/importer/src/importers/index.ts | 2 +- .../importers/passsordxp/dutch-csv-headers.ts | 10 +++ .../passsordxp/german-csv-headers.ts | 11 +++ .../passwordxp-csv-importer.ts | 39 +++++++-- 7 files changed, 117 insertions(+), 39 deletions(-) create mode 100644 libs/importer/spec/test-data/passwordxp-csv/dutch-headers.ts create mode 100644 libs/importer/spec/test-data/passwordxp-csv/german-headers.ts create mode 100644 libs/importer/src/importers/passsordxp/dutch-csv-headers.ts create mode 100644 libs/importer/src/importers/passsordxp/german-csv-headers.ts rename libs/importer/src/importers/{ => passsordxp}/passwordxp-csv-importer.ts (68%) diff --git a/libs/importer/spec/passwordxp-csv-importer.spec.ts b/libs/importer/spec/passwordxp-csv-importer.spec.ts index f707b1138c5..fda323450c6 100644 --- a/libs/importer/spec/passwordxp-csv-importer.spec.ts +++ b/libs/importer/spec/passwordxp-csv-importer.spec.ts @@ -3,10 +3,46 @@ import { CipherType } from "@bitwarden/common/vault/enums"; import { PasswordXPCsvImporter } from "../src/importers"; import { ImportResult } from "../src/models/import-result"; +import { dutchHeaders } from "./test-data/passwordxp-csv/dutch-headers"; +import { germanHeaders } from "./test-data/passwordxp-csv/german-headers"; import { noFolder } from "./test-data/passwordxp-csv/no-folder.csv"; import { withFolders } from "./test-data/passwordxp-csv/passwordxp-with-folders.csv"; import { withoutFolders } from "./test-data/passwordxp-csv/passwordxp-without-folders.csv"; +async function importLoginWithCustomFields(importer: PasswordXPCsvImporter, csvData: string) { + const result: ImportResult = await importer.parse(csvData); + expect(result.success).toBe(true); + + const cipher = result.ciphers.shift(); + expect(cipher.type).toBe(CipherType.Login); + expect(cipher.name).toBe("Title2"); + expect(cipher.notes).toBe("Test Notes"); + expect(cipher.login.username).toBe("Username2"); + expect(cipher.login.password).toBe("12345678"); + expect(cipher.login.uris[0].uri).toBe("http://URL2.com"); + + expect(cipher.fields.length).toBe(5); + let field = cipher.fields.shift(); + expect(field.name).toBe("Account"); + expect(field.value).toBe("Account2"); + + field = cipher.fields.shift(); + expect(field.name).toBe("Modified"); + expect(field.value).toBe("27-3-2024 08:11:21"); + + field = cipher.fields.shift(); + expect(field.name).toBe("Created"); + expect(field.value).toBe("27-3-2024 08:11:21"); + + field = cipher.fields.shift(); + expect(field.name).toBe("Expire on"); + expect(field.value).toBe("27-5-2024 08:11:21"); + + field = cipher.fields.shift(); + expect(field.name).toBe("Modified by"); + expect(field.value).toBe("someone"); +} + describe("PasswordXPCsvImporter", () => { let importer: PasswordXPCsvImporter; @@ -20,6 +56,12 @@ describe("PasswordXPCsvImporter", () => { expect(result.success).toBe(false); }); + it("should return success false if CSV headers did not get translated", async () => { + const data = germanHeaders.replace("Titel;", "UnknownTitle;"); + const result: ImportResult = await importer.parse(data); + expect(result.success).toBe(false); + }); + it("should skip rows starting with >>>", async () => { const result: ImportResult = await importer.parse(noFolder); expect(result.success).toBe(true); @@ -61,38 +103,16 @@ describe("PasswordXPCsvImporter", () => { expect(cipher.login.uris[0].uri).toBe("http://test"); }); - it("should parse CSV data and import unmapped columns as custom fields", async () => { - const result: ImportResult = await importer.parse(withoutFolders); - expect(result.success).toBe(true); + it("should parse CSV data with English headers and import unmapped columns as custom fields", async () => { + await importLoginWithCustomFields(importer, withoutFolders); + }); - const cipher = result.ciphers.shift(); - expect(cipher.type).toBe(CipherType.Login); - expect(cipher.name).toBe("Title2"); - expect(cipher.notes).toBe("Test Notes"); - expect(cipher.login.username).toBe("Username2"); - expect(cipher.login.password).toBe("12345678"); - expect(cipher.login.uris[0].uri).toBe("http://URL2.com"); + it("should parse CSV data with German headers and import unmapped columns as custom fields", async () => { + await importLoginWithCustomFields(importer, germanHeaders); + }); - expect(cipher.fields.length).toBe(5); - let field = cipher.fields.shift(); - expect(field.name).toBe("Account"); - expect(field.value).toBe("Account2"); - - field = cipher.fields.shift(); - expect(field.name).toBe("Modified"); - expect(field.value).toBe("27-3-2024 08:11:21"); - - field = cipher.fields.shift(); - expect(field.name).toBe("Created"); - expect(field.value).toBe("27-3-2024 08:11:21"); - - field = cipher.fields.shift(); - expect(field.name).toBe("Expire on"); - expect(field.value).toBe("27-5-2024 08:11:21"); - - field = cipher.fields.shift(); - expect(field.name).toBe("Modified by"); - expect(field.value).toBe("someone"); + it("should parse CSV data with Dutch headers and import unmapped columns as custom fields", async () => { + await importLoginWithCustomFields(importer, dutchHeaders); }); it("should parse CSV data with folders and assign items to them", async () => { diff --git a/libs/importer/spec/test-data/passwordxp-csv/dutch-headers.ts b/libs/importer/spec/test-data/passwordxp-csv/dutch-headers.ts new file mode 100644 index 00000000000..9cab04f1e6d --- /dev/null +++ b/libs/importer/spec/test-data/passwordxp-csv/dutch-headers.ts @@ -0,0 +1,7 @@ +export const dutchHeaders = `Titel;Gebruikersnaam;Account;URL;Wachtwoord;Gewijzigd;Gemaakt;Verloopt op;Beschrijving;Gewijzigd door +>>> +Title2;Username2;Account2;http://URL2.com;12345678;27-3-2024 08:11:21;27-3-2024 08:11:21;27-5-2024 08:11:21;Test Notes;someone +Title Test 1;Username1;Account1;http://URL1.com;Password1;27-3-2024 08:10:52;27-3-2024 08:10:52;;Test Notes 2; +Certificate 1;;;;;27-3-2024 10:22:39;27-3-2024 10:22:39;;Test Notes Certicate 1; +test;testtest;;http://test;test;27-3-2024 12:36:59;27-3-2024 12:36:59;;Test Notes 3; +`; diff --git a/libs/importer/spec/test-data/passwordxp-csv/german-headers.ts b/libs/importer/spec/test-data/passwordxp-csv/german-headers.ts new file mode 100644 index 00000000000..a6ac21c76d6 --- /dev/null +++ b/libs/importer/spec/test-data/passwordxp-csv/german-headers.ts @@ -0,0 +1,7 @@ +export const germanHeaders = `Titel;Benutzername;Konto;URL;Passwort;Geändert am;Erstellt am;Läuft ab am;Beschreibung;Geändert von +>>> +Title2;Username2;Account2;http://URL2.com;12345678;27-3-2024 08:11:21;27-3-2024 08:11:21;27-5-2024 08:11:21;Test Notes;someone +Title Test 1;Username1;Account1;http://URL1.com;Password1;27-3-2024 08:10:52;27-3-2024 08:10:52;;Test Notes 2; +Certificate 1;;;;;27-3-2024 10:22:39;27-3-2024 10:22:39;;Test Notes Certicate 1; +test;testtest;;http://test;test;27-3-2024 12:36:59;27-3-2024 12:36:59;;Test Notes 3; +`; diff --git a/libs/importer/src/importers/index.ts b/libs/importer/src/importers/index.ts index 19b22cfa80d..1ba3a0d9eb8 100644 --- a/libs/importer/src/importers/index.ts +++ b/libs/importer/src/importers/index.ts @@ -45,7 +45,7 @@ export { PasswordBossJsonImporter } from "./passwordboss-json-importer"; export { PasswordDragonXmlImporter } from "./passworddragon-xml-importer"; export { PasswordSafeXmlImporter } from "./passwordsafe-xml-importer"; export { PasswordWalletTxtImporter } from "./passwordwallet-txt-importer"; -export { PasswordXPCsvImporter } from "./passwordxp-csv-importer"; +export { PasswordXPCsvImporter } from "./passsordxp/passwordxp-csv-importer"; export { ProtonPassJsonImporter } from "./protonpass/protonpass-json-importer"; export { PsonoJsonImporter } from "./psono/psono-json-importer"; export { RememBearCsvImporter } from "./remembear-csv-importer"; diff --git a/libs/importer/src/importers/passsordxp/dutch-csv-headers.ts b/libs/importer/src/importers/passsordxp/dutch-csv-headers.ts new file mode 100644 index 00000000000..7f9c219de56 --- /dev/null +++ b/libs/importer/src/importers/passsordxp/dutch-csv-headers.ts @@ -0,0 +1,10 @@ +export const dutchHeaderTranslations: { [key: string]: string } = { + Titel: "Title", + Gebruikersnaam: "Username", + Wachtwoord: "Password", + Gewijzigd: "Modified", + Gemaakt: "Created", + "Verloopt op": "Expire on", + Beschrijving: "Description", + "Gewijzigd door": "Modified by", +}; diff --git a/libs/importer/src/importers/passsordxp/german-csv-headers.ts b/libs/importer/src/importers/passsordxp/german-csv-headers.ts new file mode 100644 index 00000000000..584ad0badca --- /dev/null +++ b/libs/importer/src/importers/passsordxp/german-csv-headers.ts @@ -0,0 +1,11 @@ +export const germanHeaderTranslations: { [key: string]: string } = { + Titel: "Title", + Benutzername: "Username", + Konto: "Account", + Passwort: "Password", + "Geändert am": "Modified", + "Erstellt am": "Created", + "Läuft ab am": "Expire on", + Beschreibung: "Description", + "Geändert von": "Modified by", +}; diff --git a/libs/importer/src/importers/passwordxp-csv-importer.ts b/libs/importer/src/importers/passsordxp/passwordxp-csv-importer.ts similarity index 68% rename from libs/importer/src/importers/passwordxp-csv-importer.ts rename to libs/importer/src/importers/passsordxp/passwordxp-csv-importer.ts index 461432e98d4..226a284ec91 100644 --- a/libs/importer/src/importers/passwordxp-csv-importer.ts +++ b/libs/importer/src/importers/passsordxp/passwordxp-csv-importer.ts @@ -1,12 +1,28 @@ import { Utils } from "@bitwarden/common/platform/misc/utils"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; -import { ImportResult } from "../models/import-result"; - -import { BaseImporter } from "./base-importer"; -import { Importer } from "./importer"; +import { ImportResult } from "../../models/import-result"; +import { BaseImporter } from "../base-importer"; +import { Importer } from "../importer"; const _mappedColumns = new Set(["Title", "Username", "URL", "Password", "Description"]); +import { dutchHeaderTranslations } from "./dutch-csv-headers"; +import { germanHeaderTranslations } from "./german-csv-headers"; + +/* Translates the headers from non-English to English + * This is necessary because the parser only maps English headers to ciphers + * Currently only supports German and Dutch translations + */ +function translateIntoEnglishHeaders(header: string): string { + const translations: { [key: string]: string } = { + // The header column 'User name' is parsed by the parser, but cannot be used as a variable. This converts it to a valid variable name, prior to parsing. + "User name": "Username", + ...germanHeaderTranslations, + ...dutchHeaderTranslations, + }; + + return translations[header] || header; +} /** * PasswordXP CSV importer @@ -17,15 +33,22 @@ export class PasswordXPCsvImporter extends BaseImporter implements Importer { * @param data */ parse(data: string): Promise { - // The header column 'User name' is parsed by the parser, but cannot be used as a variable. This converts it to a valid variable name, prior to parsing. - data = data.replace(";User name;", ";Username;"); - const result = new ImportResult(); - const results = this.parseCsv(data, true, { skipEmptyLines: true }); + const results = this.parseCsv(data, true, { + skipEmptyLines: true, + transformHeader: translateIntoEnglishHeaders, + }); if (results == null) { result.success = false; return Promise.resolve(result); } + + // If the first row (header check) does not contain the column "Title", then the data is invalid (no translation found) + if (!results[0].Title) { + result.success = false; + return Promise.resolve(result); + } + let currentFolderName = ""; results.forEach((row) => { // Skip rows starting with '>>>' as they indicate items following have no folder assigned to them From c3f58b2e70e3576dfa0a5634086d10a368760ce6 Mon Sep 17 00:00:00 2001 From: Evan Bassler Date: Tue, 17 Dec 2024 13:55:28 -0600 Subject: [PATCH 03/54] fix large icon (#11896) Co-authored-by: Evan Bassler Co-authored-by: Matt Bishop --- .../src/autofill/popup/settings/notifications.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/browser/src/autofill/popup/settings/notifications.component.html b/apps/browser/src/autofill/popup/settings/notifications.component.html index 86fe4923df8..c6446012d0c 100644 --- a/apps/browser/src/autofill/popup/settings/notifications.component.html +++ b/apps/browser/src/autofill/popup/settings/notifications.component.html @@ -50,7 +50,7 @@ {{ "excludedDomains" | i18n }} - + From ac13cf7ce6fb23e1a81ead38008e4011e5217c5b Mon Sep 17 00:00:00 2001 From: rr-bw <102181210+rr-bw@users.noreply.github.com> Date: Tue, 17 Dec 2024 12:48:37 -0800 Subject: [PATCH 04/54] feat(auth): [PM-15945] Add logout option to TDE approval page (#12445) This PR adds a logout option to the TDE approval screen. A TDE user on this page cannot use the "Back" button or click the Bitwarden logo to navigate back to `/` because the user is currently authenticated, which means that navigating to the `/` route would activate the `redirectGuard` and simply route the user back to `/login-initiated`. So we must log the user out first before routing. Feature Flags: `UnauthenticatedExtensionUIRefresh` ON --- .../login-decryption-options.component.html | 4 ++++ .../login-decryption-options.component.ts | 16 ++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/libs/auth/src/angular/login-decryption-options/login-decryption-options.component.html b/libs/auth/src/angular/login-decryption-options/login-decryption-options.component.html index cb340f646f1..b3d218389bf 100644 --- a/libs/auth/src/angular/login-decryption-options/login-decryption-options.component.html +++ b/libs/auth/src/angular/login-decryption-options/login-decryption-options.component.html @@ -56,5 +56,9 @@ > {{ "requestAdminApproval" | i18n }} + + diff --git a/libs/auth/src/angular/login-decryption-options/login-decryption-options.component.ts b/libs/auth/src/angular/login-decryption-options/login-decryption-options.component.ts index 070debf2205..5600077c363 100644 --- a/libs/auth/src/angular/login-decryption-options/login-decryption-options.component.ts +++ b/libs/auth/src/angular/login-decryption-options/login-decryption-options.component.ts @@ -30,6 +30,7 @@ import { AsyncActionsModule, ButtonModule, CheckboxModule, + DialogService, FormFieldModule, ToastService, TypographyModule, @@ -90,6 +91,7 @@ export class LoginDecryptionOptionsComponent implements OnInit { private apiService: ApiService, private destroyRef: DestroyRef, private deviceTrustService: DeviceTrustServiceAbstraction, + private dialogService: DialogService, private formBuilder: FormBuilder, private i18nService: I18nService, private keyService: KeyService, @@ -298,4 +300,18 @@ export class LoginDecryptionOptionsComponent implements OnInit { this.loginEmailService.setLoginEmail(this.email); await this.router.navigate(["/admin-approval-requested"]); } + + async logOut() { + const confirmed = await this.dialogService.openSimpleDialog({ + title: { key: "logOut" }, + content: { key: "logOutConfirmation" }, + acceptButtonText: { key: "logOut" }, + type: "warning", + }); + + const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; + if (confirmed) { + this.messagingService.send("logout", { userId: userId }); + } + } } From 5a582dfc6f7311373e7d58034192a76fade3bb0b Mon Sep 17 00:00:00 2001 From: Oscar Hinton Date: Tue, 17 Dec 2024 23:29:48 +0100 Subject: [PATCH 05/54] [CL-135] Migrate component library to standalone components (#12389) * Migrate component library to standalone components * Fix tests --- .../navigation-switcher.component.spec.ts | 11 +++----- .../password-health.component.spec.ts | 3 +-- .../src/async-actions/async-actions.module.ts | 5 +--- .../src/async-actions/bit-action.directive.ts | 1 + .../src/async-actions/bit-submit.directive.ts | 1 + .../async-actions/form-button.directive.ts | 1 + .../components/src/avatar/avatar.component.ts | 3 +++ libs/components/src/avatar/avatar.module.ts | 4 +-- .../src/badge-list/badge-list.component.ts | 6 ++++- .../src/badge-list/badge-list.module.ts | 6 +---- libs/components/src/badge/badge.directive.ts | 1 + libs/components/src/badge/badge.module.ts | 4 +-- .../src/banner/banner.component.spec.ts | 4 +-- .../components/src/banner/banner.component.ts | 6 +++++ libs/components/src/banner/banner.module.ts | 7 +---- .../src/breadcrumbs/breadcrumb.component.ts | 3 +++ .../src/breadcrumbs/breadcrumbs.component.ts | 8 ++++++ .../src/breadcrumbs/breadcrumbs.module.ts | 9 +------ .../components/src/button/button.component.ts | 3 +++ libs/components/src/button/button.module.ts | 4 +-- .../src/checkbox/checkbox.component.ts | 1 + .../src/checkbox/checkbox.module.ts | 7 +---- .../color-password.component.ts | 3 +++ .../color-password/color-password.module.ts | 4 +-- libs/components/src/dialog/dialog.module.ts | 23 ++-------------- .../src/dialog/dialog/dialog.component.ts | 15 +++++++++++ .../directives/dialog-close.directive.ts | 1 + .../dialog-title-container.directive.ts | 1 + .../simple-configurable-dialog.component.ts | 17 +++++++++++- .../simple-dialog/simple-dialog.component.ts | 10 ++++++- .../form-control/form-control.component.ts | 6 +++++ .../src/form-control/form-control.module.ts | 6 +---- .../src/form-control/hint.component.ts | 1 + .../src/form-field/error-summary.component.ts | 5 ++++ .../src/form-field/error.component.ts | 1 + .../src/form-field/form-field.component.ts | 4 +++ .../src/form-field/form-field.module.ts | 17 ++++++------ .../password-input-toggle.directive.ts | 1 + .../src/form-field/prefix.directive.ts | 1 + .../src/form-field/suffix.directive.ts | 1 + .../src/icon-button/icon-button.component.ts | 3 +++ .../src/icon-button/icon-button.module.ts | 4 +-- libs/components/src/icon/icon.component.ts | 1 + .../src/icon/icon.components.spec.ts | 2 +- libs/components/src/icon/icon.module.ts | 4 +-- libs/components/src/input/input.directive.ts | 1 + libs/components/src/input/input.module.ts | 4 +-- libs/components/src/link/link.directive.ts | 2 ++ libs/components/src/link/link.module.ts | 4 +-- .../src/menu/menu-divider.component.ts | 1 + .../src/menu/menu-item.directive.ts | 3 +++ .../src/menu/menu-trigger-for.directive.ts | 1 + libs/components/src/menu/menu.component.ts | 4 ++- libs/components/src/menu/menu.module.ts | 6 +---- libs/components/src/menu/menu.stories.ts | 16 +++--------- .../multi-select/multi-select.component.ts | 15 +++++++++-- .../src/multi-select/multi-select.module.ts | 9 +------ .../src/navigation/nav-divider.component.ts | 3 +++ .../src/navigation/nav-group.component.ts | 12 ++++++++- .../src/navigation/nav-item.component.ts | 14 ++++++++-- .../src/navigation/nav-logo.component.ts | 6 +++++ .../src/navigation/navigation.module.ts | 19 -------------- .../src/navigation/side-nav.component.ts | 8 ++++++ .../src/no-items/no-items.component.ts | 3 +++ .../src/no-items/no-items.module.ts | 6 +---- .../src/progress/progress.component.ts | 3 +++ .../src/progress/progress.module.ts | 4 +-- .../radio-button/radio-button.component.ts | 5 ++++ .../src/radio-button/radio-button.module.ts | 5 +--- .../src/radio-button/radio-group.component.ts | 4 +++ .../src/radio-button/radio-input.component.ts | 1 + .../components/src/search/search.component.ts | 11 +++++++- libs/components/src/search/search.module.ts | 7 +---- .../components/src/select/option.component.ts | 1 + .../components/src/select/select.component.ts | 13 ++++++++-- libs/components/src/select/select.module.ts | 6 +---- libs/components/src/shared/i18n.pipe.ts | 1 + libs/components/src/shared/shared.module.ts | 3 +-- libs/components/src/table/cell.directive.ts | 1 + libs/components/src/table/row.directive.ts | 1 + .../src/table/sortable.component.ts | 3 +++ .../src/table/table-scroll.component.ts | 17 ++++++++++++ libs/components/src/table/table.component.ts | 4 +++ libs/components/src/table/table.module.ts | 6 +++-- .../src/tabs/shared/tab-header.component.ts | 1 + .../shared/tab-list-container.directive.ts | 1 + .../tabs/shared/tab-list-item.directive.ts | 5 +++- .../src/tabs/tab-group/tab-body.component.ts | 4 ++- .../src/tabs/tab-group/tab-group.component.ts | 12 +++++++++ .../src/tabs/tab-group/tab-label.directive.ts | 1 + .../src/tabs/tab-group/tab.component.ts | 1 + .../tabs/tab-nav-bar/tab-link.component.ts | 4 ++- .../tabs/tab-nav-bar/tab-nav-bar.component.ts | 5 ++++ libs/components/src/tabs/tabs.module.ts | 26 +++++++------------ libs/components/src/toast/toast.module.ts | 5 +--- libs/components/src/toast/toastr.component.ts | 4 +++ .../toggle-group/toggle-group.component.ts | 1 + .../src/toggle-group/toggle-group.module.ts | 6 +---- .../src/toggle-group/toggle.component.ts | 3 +++ .../src/typography/typography.directive.ts | 1 + .../src/typography/typography.module.ts | 4 +-- 101 files changed, 330 insertions(+), 216 deletions(-) diff --git a/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.component.spec.ts b/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.component.spec.ts index a07f56db2d7..382ce8e026b 100644 --- a/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.component.spec.ts +++ b/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.component.spec.ts @@ -6,7 +6,7 @@ import { BehaviorSubject } from "rxjs"; import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { BitIconButtonComponent } from "@bitwarden/components/src/icon-button/icon-button.component"; +import { IconButtonModule, NavigationModule } from "@bitwarden/components"; import { NavItemComponent } from "@bitwarden/components/src/navigation/nav-item.component"; import { ProductSwitcherItem, ProductSwitcherService } from "../shared/product-switcher.service"; @@ -45,13 +45,8 @@ describe("NavigationProductSwitcherComponent", () => { mockProducts$.next({ bento: [], other: [] }); await TestBed.configureTestingModule({ - imports: [RouterModule], - declarations: [ - NavigationProductSwitcherComponent, - NavItemComponent, - BitIconButtonComponent, - I18nPipe, - ], + imports: [RouterModule, NavigationModule, IconButtonModule], + declarations: [NavigationProductSwitcherComponent, I18nPipe], providers: [ { provide: ProductSwitcherService, useValue: productSwitcherService }, { diff --git a/bitwarden_license/bit-web/src/app/tools/access-intelligence/password-health.component.spec.ts b/bitwarden_license/bit-web/src/app/tools/access-intelligence/password-health.component.spec.ts index 98637d0decb..1f1756731f6 100644 --- a/bitwarden_license/bit-web/src/app/tools/access-intelligence/password-health.component.spec.ts +++ b/bitwarden_license/bit-web/src/app/tools/access-intelligence/password-health.component.spec.ts @@ -13,7 +13,6 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { TableModule } from "@bitwarden/components"; -import { TableBodyDirective } from "@bitwarden/components/src/table/table.component"; import { LooseComponentsModule } from "@bitwarden/web-vault/app/shared"; import { PipesModule } from "@bitwarden/web-vault/app/vault/individual-vault/pipes/pipes.module"; @@ -27,7 +26,7 @@ describe("PasswordHealthComponent", () => { beforeEach(async () => { await TestBed.configureTestingModule({ imports: [PasswordHealthComponent, PipesModule, TableModule, LooseComponentsModule], - declarations: [TableBodyDirective], + declarations: [], providers: [ { provide: CipherService, useValue: mock() }, { provide: I18nService, useValue: mock() }, diff --git a/libs/components/src/async-actions/async-actions.module.ts b/libs/components/src/async-actions/async-actions.module.ts index 8ff1deb2784..bff4286f890 100644 --- a/libs/components/src/async-actions/async-actions.module.ts +++ b/libs/components/src/async-actions/async-actions.module.ts @@ -1,14 +1,11 @@ import { NgModule } from "@angular/core"; -import { SharedModule } from "../shared"; - import { BitActionDirective } from "./bit-action.directive"; import { BitSubmitDirective } from "./bit-submit.directive"; import { BitFormButtonDirective } from "./form-button.directive"; @NgModule({ - imports: [SharedModule], - declarations: [BitActionDirective, BitFormButtonDirective, BitSubmitDirective], + imports: [BitActionDirective, BitFormButtonDirective, BitSubmitDirective], exports: [BitActionDirective, BitFormButtonDirective, BitSubmitDirective], }) export class AsyncActionsModule {} diff --git a/libs/components/src/async-actions/bit-action.directive.ts b/libs/components/src/async-actions/bit-action.directive.ts index 32ac73f418d..3e793ae2ecd 100644 --- a/libs/components/src/async-actions/bit-action.directive.ts +++ b/libs/components/src/async-actions/bit-action.directive.ts @@ -15,6 +15,7 @@ import { FunctionReturningAwaitable, functionToObservable } from "../utils/funct */ @Directive({ selector: "[bitAction]", + standalone: true, }) export class BitActionDirective implements OnDestroy { private destroy$ = new Subject(); diff --git a/libs/components/src/async-actions/bit-submit.directive.ts b/libs/components/src/async-actions/bit-submit.directive.ts index 838d78af8b2..a38e76aaca6 100644 --- a/libs/components/src/async-actions/bit-submit.directive.ts +++ b/libs/components/src/async-actions/bit-submit.directive.ts @@ -14,6 +14,7 @@ import { FunctionReturningAwaitable, functionToObservable } from "../utils/funct */ @Directive({ selector: "[formGroup][bitSubmit]", + standalone: true, }) export class BitSubmitDirective implements OnInit, OnDestroy { private destroy$ = new Subject(); diff --git a/libs/components/src/async-actions/form-button.directive.ts b/libs/components/src/async-actions/form-button.directive.ts index e4685188693..7c92865b984 100644 --- a/libs/components/src/async-actions/form-button.directive.ts +++ b/libs/components/src/async-actions/form-button.directive.ts @@ -25,6 +25,7 @@ import { BitSubmitDirective } from "./bit-submit.directive"; */ @Directive({ selector: "button[bitFormButton]", + standalone: true, }) export class BitFormButtonDirective implements OnDestroy { private destroy$ = new Subject(); diff --git a/libs/components/src/avatar/avatar.component.ts b/libs/components/src/avatar/avatar.component.ts index e1758d795d6..76ff702e88b 100644 --- a/libs/components/src/avatar/avatar.component.ts +++ b/libs/components/src/avatar/avatar.component.ts @@ -1,5 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore +import { NgIf, NgClass } from "@angular/common"; import { Component, Input, OnChanges } from "@angular/core"; import { DomSanitizer, SafeResourceUrl } from "@angular/platform-browser"; @@ -18,6 +19,8 @@ const SizeClasses: Record = { @Component({ selector: "bit-avatar", template: ``, + standalone: true, + imports: [NgIf, NgClass], }) export class AvatarComponent implements OnChanges { @Input() border = false; diff --git a/libs/components/src/avatar/avatar.module.ts b/libs/components/src/avatar/avatar.module.ts index ea78ff3a1d2..4ef0978cbec 100644 --- a/libs/components/src/avatar/avatar.module.ts +++ b/libs/components/src/avatar/avatar.module.ts @@ -1,11 +1,9 @@ -import { CommonModule } from "@angular/common"; import { NgModule } from "@angular/core"; import { AvatarComponent } from "./avatar.component"; @NgModule({ - imports: [CommonModule], + imports: [AvatarComponent], exports: [AvatarComponent], - declarations: [AvatarComponent], }) export class AvatarModule {} diff --git a/libs/components/src/badge-list/badge-list.component.ts b/libs/components/src/badge-list/badge-list.component.ts index 9270e5e1238..ac8cb3281ab 100644 --- a/libs/components/src/badge-list/badge-list.component.ts +++ b/libs/components/src/badge-list/badge-list.component.ts @@ -1,12 +1,16 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore +import { CommonModule } from "@angular/common"; import { Component, Input, OnChanges } from "@angular/core"; -import { BadgeVariant } from "../badge"; +import { BadgeModule, BadgeVariant } from "../badge"; +import { I18nPipe } from "../shared/i18n.pipe"; @Component({ selector: "bit-badge-list", templateUrl: "badge-list.component.html", + standalone: true, + imports: [CommonModule, BadgeModule, I18nPipe], }) export class BadgeListComponent implements OnChanges { private _maxItems: number; diff --git a/libs/components/src/badge-list/badge-list.module.ts b/libs/components/src/badge-list/badge-list.module.ts index d2a4ce211b1..9359fe2c5c5 100644 --- a/libs/components/src/badge-list/badge-list.module.ts +++ b/libs/components/src/badge-list/badge-list.module.ts @@ -1,13 +1,9 @@ import { NgModule } from "@angular/core"; -import { BadgeModule } from "../badge"; -import { SharedModule } from "../shared"; - import { BadgeListComponent } from "./badge-list.component"; @NgModule({ - imports: [SharedModule, BadgeModule], + imports: [BadgeListComponent], exports: [BadgeListComponent], - declarations: [BadgeListComponent], }) export class BadgeListModule {} diff --git a/libs/components/src/badge/badge.directive.ts b/libs/components/src/badge/badge.directive.ts index f39f8f87639..eef876a664d 100644 --- a/libs/components/src/badge/badge.directive.ts +++ b/libs/components/src/badge/badge.directive.ts @@ -31,6 +31,7 @@ const hoverStyles: Record = { @Directive({ selector: "span[bitBadge], a[bitBadge], button[bitBadge]", providers: [{ provide: FocusableElement, useExisting: BadgeDirective }], + standalone: true, }) export class BadgeDirective implements FocusableElement { @HostBinding("class") get classList() { diff --git a/libs/components/src/badge/badge.module.ts b/libs/components/src/badge/badge.module.ts index e1b8292363f..e7f3770785a 100644 --- a/libs/components/src/badge/badge.module.ts +++ b/libs/components/src/badge/badge.module.ts @@ -1,11 +1,9 @@ -import { CommonModule } from "@angular/common"; import { NgModule } from "@angular/core"; import { BadgeDirective } from "./badge.directive"; @NgModule({ - imports: [CommonModule], + imports: [BadgeDirective], exports: [BadgeDirective], - declarations: [BadgeDirective], }) export class BadgeModule {} diff --git a/libs/components/src/banner/banner.component.spec.ts b/libs/components/src/banner/banner.component.spec.ts index 29f10016a15..2bbc7965642 100644 --- a/libs/components/src/banner/banner.component.spec.ts +++ b/libs/components/src/banner/banner.component.spec.ts @@ -2,7 +2,6 @@ import { ComponentFixture, TestBed } from "@angular/core/testing"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { SharedModule } from "../shared/shared.module"; import { I18nMockService } from "../utils/i18n-mock.service"; import { BannerComponent } from "./banner.component"; @@ -13,8 +12,7 @@ describe("BannerComponent", () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [SharedModule], - declarations: [BannerComponent], + imports: [BannerComponent], providers: [ { provide: I18nService, diff --git a/libs/components/src/banner/banner.component.ts b/libs/components/src/banner/banner.component.ts index 7d59ceb0ee9..d3f64329978 100644 --- a/libs/components/src/banner/banner.component.ts +++ b/libs/components/src/banner/banner.component.ts @@ -1,7 +1,11 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore +import { CommonModule } from "@angular/common"; import { Component, Input, OnInit, Output, EventEmitter } from "@angular/core"; +import { IconButtonModule } from "../icon-button"; +import { I18nPipe } from "../shared/i18n.pipe"; + type BannerTypes = "premium" | "info" | "warning" | "danger"; const defaultIcon: Record = { @@ -14,6 +18,8 @@ const defaultIcon: Record = { @Component({ selector: "bit-banner", templateUrl: "./banner.component.html", + standalone: true, + imports: [CommonModule, IconButtonModule, I18nPipe], }) export class BannerComponent implements OnInit { @Input("bannerType") bannerType: BannerTypes = "info"; diff --git a/libs/components/src/banner/banner.module.ts b/libs/components/src/banner/banner.module.ts index 2c819fbc5b4..3301218ed1a 100644 --- a/libs/components/src/banner/banner.module.ts +++ b/libs/components/src/banner/banner.module.ts @@ -1,14 +1,9 @@ -import { CommonModule } from "@angular/common"; import { NgModule } from "@angular/core"; -import { IconButtonModule } from "../icon-button"; -import { SharedModule } from "../shared/shared.module"; - import { BannerComponent } from "./banner.component"; @NgModule({ - imports: [CommonModule, SharedModule, IconButtonModule], + imports: [BannerComponent], exports: [BannerComponent], - declarations: [BannerComponent], }) export class BannerModule {} diff --git a/libs/components/src/breadcrumbs/breadcrumb.component.ts b/libs/components/src/breadcrumbs/breadcrumb.component.ts index d6128540442..ce18bde171f 100644 --- a/libs/components/src/breadcrumbs/breadcrumb.component.ts +++ b/libs/components/src/breadcrumbs/breadcrumb.component.ts @@ -1,11 +1,14 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore +import { NgIf } from "@angular/common"; import { Component, EventEmitter, Input, Output, TemplateRef, ViewChild } from "@angular/core"; import { QueryParamsHandling } from "@angular/router"; @Component({ selector: "bit-breadcrumb", templateUrl: "./breadcrumb.component.html", + standalone: true, + imports: [NgIf], }) export class BreadcrumbComponent { @Input() diff --git a/libs/components/src/breadcrumbs/breadcrumbs.component.ts b/libs/components/src/breadcrumbs/breadcrumbs.component.ts index 64ca8146c80..6e8fbf5c25a 100644 --- a/libs/components/src/breadcrumbs/breadcrumbs.component.ts +++ b/libs/components/src/breadcrumbs/breadcrumbs.component.ts @@ -1,10 +1,18 @@ +import { CommonModule } from "@angular/common"; import { Component, ContentChildren, Input, QueryList } from "@angular/core"; +import { RouterModule } from "@angular/router"; + +import { IconButtonModule } from "../icon-button"; +import { LinkModule } from "../link"; +import { MenuModule } from "../menu"; import { BreadcrumbComponent } from "./breadcrumb.component"; @Component({ selector: "bit-breadcrumbs", templateUrl: "./breadcrumbs.component.html", + standalone: true, + imports: [CommonModule, LinkModule, RouterModule, IconButtonModule, MenuModule], }) export class BreadcrumbsComponent { @Input() diff --git a/libs/components/src/breadcrumbs/breadcrumbs.module.ts b/libs/components/src/breadcrumbs/breadcrumbs.module.ts index 0812b552f9a..89b57fd19b5 100644 --- a/libs/components/src/breadcrumbs/breadcrumbs.module.ts +++ b/libs/components/src/breadcrumbs/breadcrumbs.module.ts @@ -1,17 +1,10 @@ -import { CommonModule } from "@angular/common"; import { NgModule } from "@angular/core"; -import { RouterModule } from "@angular/router"; - -import { IconButtonModule } from "../icon-button"; -import { LinkModule } from "../link"; -import { MenuModule } from "../menu"; import { BreadcrumbComponent } from "./breadcrumb.component"; import { BreadcrumbsComponent } from "./breadcrumbs.component"; @NgModule({ - imports: [CommonModule, LinkModule, IconButtonModule, MenuModule, RouterModule], - declarations: [BreadcrumbsComponent, BreadcrumbComponent], + imports: [BreadcrumbsComponent, BreadcrumbComponent], exports: [BreadcrumbsComponent, BreadcrumbComponent], }) export class BreadcrumbsModule {} diff --git a/libs/components/src/button/button.component.ts b/libs/components/src/button/button.component.ts index 67b57d576ab..96311f91529 100644 --- a/libs/components/src/button/button.component.ts +++ b/libs/components/src/button/button.component.ts @@ -1,6 +1,7 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { coerceBooleanProperty } from "@angular/cdk/coercion"; +import { NgClass } from "@angular/common"; import { Input, HostBinding, Component } from "@angular/core"; import { ButtonLikeAbstraction, ButtonType } from "../shared/button-like.abstraction"; @@ -46,6 +47,8 @@ const buttonStyles: Record = { selector: "button[bitButton], a[bitButton]", templateUrl: "button.component.html", providers: [{ provide: ButtonLikeAbstraction, useExisting: ButtonComponent }], + standalone: true, + imports: [NgClass], }) export class ButtonComponent implements ButtonLikeAbstraction { @HostBinding("class") get classList() { diff --git a/libs/components/src/button/button.module.ts b/libs/components/src/button/button.module.ts index 448e7c9dcf6..f1a86eff3ab 100644 --- a/libs/components/src/button/button.module.ts +++ b/libs/components/src/button/button.module.ts @@ -1,11 +1,9 @@ -import { CommonModule } from "@angular/common"; import { NgModule } from "@angular/core"; import { ButtonComponent } from "./button.component"; @NgModule({ - imports: [CommonModule], + imports: [ButtonComponent], exports: [ButtonComponent], - declarations: [ButtonComponent], }) export class ButtonModule {} diff --git a/libs/components/src/checkbox/checkbox.component.ts b/libs/components/src/checkbox/checkbox.component.ts index 1ca27e84b82..0ce6f1889b5 100644 --- a/libs/components/src/checkbox/checkbox.component.ts +++ b/libs/components/src/checkbox/checkbox.component.ts @@ -9,6 +9,7 @@ import { BitFormControlAbstraction } from "../form-control"; selector: "input[type=checkbox][bitCheckbox]", template: "", providers: [{ provide: BitFormControlAbstraction, useExisting: CheckboxComponent }], + standalone: true, }) export class CheckboxComponent implements BitFormControlAbstraction { @HostBinding("class") diff --git a/libs/components/src/checkbox/checkbox.module.ts b/libs/components/src/checkbox/checkbox.module.ts index d03b9cf5050..3abfb4b1bfd 100644 --- a/libs/components/src/checkbox/checkbox.module.ts +++ b/libs/components/src/checkbox/checkbox.module.ts @@ -1,14 +1,9 @@ -import { CommonModule } from "@angular/common"; import { NgModule } from "@angular/core"; -import { FormControlModule } from "../form-control"; -import { SharedModule } from "../shared"; - import { CheckboxComponent } from "./checkbox.component"; @NgModule({ - imports: [SharedModule, CommonModule, FormControlModule], - declarations: [CheckboxComponent], + imports: [CheckboxComponent], exports: [CheckboxComponent], }) export class CheckboxModule {} diff --git a/libs/components/src/color-password/color-password.component.ts b/libs/components/src/color-password/color-password.component.ts index 35732760ac7..cbf746e9d73 100644 --- a/libs/components/src/color-password/color-password.component.ts +++ b/libs/components/src/color-password/color-password.component.ts @@ -1,5 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore +import { NgFor, NgIf } from "@angular/common"; import { Component, HostBinding, Input } from "@angular/core"; import { Utils } from "@bitwarden/common/platform/misc/utils"; @@ -23,6 +24,8 @@ enum CharacterType { }} `, preserveWhitespaces: false, + standalone: true, + imports: [NgFor, NgIf], }) export class ColorPasswordComponent { @Input() password: string = null; diff --git a/libs/components/src/color-password/color-password.module.ts b/libs/components/src/color-password/color-password.module.ts index 692c206bb4c..3ebc1c80e12 100644 --- a/libs/components/src/color-password/color-password.module.ts +++ b/libs/components/src/color-password/color-password.module.ts @@ -1,11 +1,9 @@ -import { CommonModule } from "@angular/common"; import { NgModule } from "@angular/core"; import { ColorPasswordComponent } from "./color-password.component"; @NgModule({ - imports: [CommonModule], + imports: [ColorPasswordComponent], exports: [ColorPasswordComponent], - declarations: [ColorPasswordComponent], }) export class ColorPasswordModule {} diff --git a/libs/components/src/dialog/dialog.module.ts b/libs/components/src/dialog/dialog.module.ts index bc37f749c05..f31fdd52060 100644 --- a/libs/components/src/dialog/dialog.module.ts +++ b/libs/components/src/dialog/dialog.module.ts @@ -1,44 +1,25 @@ import { DialogModule as CdkDialogModule } from "@angular/cdk/dialog"; import { NgModule } from "@angular/core"; -import { ReactiveFormsModule } from "@angular/forms"; - -import { AsyncActionsModule } from "../async-actions"; -import { ButtonModule } from "../button"; -import { IconButtonModule } from "../icon-button"; -import { SharedModule } from "../shared"; -import { TypographyModule } from "../typography"; import { DialogComponent } from "./dialog/dialog.component"; import { DialogService } from "./dialog.service"; import { DialogCloseDirective } from "./directives/dialog-close.directive"; -import { DialogTitleContainerDirective } from "./directives/dialog-title-container.directive"; -import { SimpleConfigurableDialogComponent } from "./simple-dialog/simple-configurable-dialog/simple-configurable-dialog.component"; import { IconDirective, SimpleDialogComponent } from "./simple-dialog/simple-dialog.component"; @NgModule({ imports: [ - SharedModule, - AsyncActionsModule, - ButtonModule, CdkDialogModule, - IconButtonModule, - ReactiveFormsModule, - TypographyModule, - ], - declarations: [ DialogCloseDirective, - DialogTitleContainerDirective, DialogComponent, SimpleDialogComponent, - SimpleConfigurableDialogComponent, IconDirective, ], exports: [ CdkDialogModule, - DialogComponent, - SimpleDialogComponent, DialogCloseDirective, + DialogComponent, IconDirective, + SimpleDialogComponent, ], providers: [DialogService], }) diff --git a/libs/components/src/dialog/dialog/dialog.component.ts b/libs/components/src/dialog/dialog/dialog.component.ts index 2f901d10d2d..ed47201805a 100644 --- a/libs/components/src/dialog/dialog/dialog.component.ts +++ b/libs/components/src/dialog/dialog/dialog.component.ts @@ -1,14 +1,29 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { coerceBooleanProperty } from "@angular/cdk/coercion"; +import { CommonModule } from "@angular/common"; import { Component, HostBinding, Input } from "@angular/core"; +import { BitIconButtonComponent } from "../../icon-button/icon-button.component"; +import { I18nPipe } from "../../shared/i18n.pipe"; +import { TypographyDirective } from "../../typography/typography.directive"; import { fadeIn } from "../animations"; +import { DialogCloseDirective } from "../directives/dialog-close.directive"; +import { DialogTitleContainerDirective } from "../directives/dialog-title-container.directive"; @Component({ selector: "bit-dialog", templateUrl: "./dialog.component.html", animations: [fadeIn], + standalone: true, + imports: [ + CommonModule, + DialogTitleContainerDirective, + TypographyDirective, + BitIconButtonComponent, + DialogCloseDirective, + I18nPipe, + ], }) export class DialogComponent { /** Background color */ diff --git a/libs/components/src/dialog/directives/dialog-close.directive.ts b/libs/components/src/dialog/directives/dialog-close.directive.ts index 5e44ced7c21..5e5fda3e014 100644 --- a/libs/components/src/dialog/directives/dialog-close.directive.ts +++ b/libs/components/src/dialog/directives/dialog-close.directive.ts @@ -3,6 +3,7 @@ import { Directive, HostBinding, HostListener, Input, Optional } from "@angular/ @Directive({ selector: "[bitDialogClose]", + standalone: true, }) export class DialogCloseDirective { @Input("bitDialogClose") dialogResult: any; diff --git a/libs/components/src/dialog/directives/dialog-title-container.directive.ts b/libs/components/src/dialog/directives/dialog-title-container.directive.ts index e17487f2780..cf46396967b 100644 --- a/libs/components/src/dialog/directives/dialog-title-container.directive.ts +++ b/libs/components/src/dialog/directives/dialog-title-container.directive.ts @@ -6,6 +6,7 @@ let nextId = 0; @Directive({ selector: "[bitDialogTitleContainer]", + standalone: true, }) export class DialogTitleContainerDirective implements OnInit { @HostBinding("id") id = `bit-dialog-title-${nextId++}`; diff --git a/libs/components/src/dialog/simple-dialog/simple-configurable-dialog/simple-configurable-dialog.component.ts b/libs/components/src/dialog/simple-dialog/simple-configurable-dialog/simple-configurable-dialog.component.ts index 29d52e9cf07..60b2e1c3a3f 100644 --- a/libs/components/src/dialog/simple-dialog/simple-configurable-dialog/simple-configurable-dialog.component.ts +++ b/libs/components/src/dialog/simple-dialog/simple-configurable-dialog/simple-configurable-dialog.component.ts @@ -1,12 +1,17 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { DialogRef, DIALOG_DATA } from "@angular/cdk/dialog"; +import { NgIf } from "@angular/common"; import { Component, Inject } from "@angular/core"; -import { FormGroup } from "@angular/forms"; +import { FormGroup, ReactiveFormsModule } from "@angular/forms"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { SimpleDialogOptions, SimpleDialogType, Translation } from "../.."; +import { BitSubmitDirective } from "../../../async-actions/bit-submit.directive"; +import { BitFormButtonDirective } from "../../../async-actions/form-button.directive"; +import { ButtonComponent } from "../../../button/button.component"; +import { SimpleDialogComponent, IconDirective } from "../simple-dialog.component"; const DEFAULT_ICON: Record = { primary: "bwi-business", @@ -26,6 +31,16 @@ const DEFAULT_COLOR: Record = { @Component({ templateUrl: "./simple-configurable-dialog.component.html", + standalone: true, + imports: [ + ReactiveFormsModule, + BitSubmitDirective, + SimpleDialogComponent, + IconDirective, + ButtonComponent, + BitFormButtonDirective, + NgIf, + ], }) export class SimpleConfigurableDialogComponent { get iconClasses() { diff --git a/libs/components/src/dialog/simple-dialog/simple-dialog.component.ts b/libs/components/src/dialog/simple-dialog/simple-dialog.component.ts index 912b0299f66..c02a13bd150 100644 --- a/libs/components/src/dialog/simple-dialog/simple-dialog.component.ts +++ b/libs/components/src/dialog/simple-dialog/simple-dialog.component.ts @@ -1,14 +1,22 @@ +import { NgIf } from "@angular/common"; import { Component, ContentChild, Directive } from "@angular/core"; +import { TypographyDirective } from "../../typography/typography.directive"; import { fadeIn } from "../animations"; +import { DialogTitleContainerDirective } from "../directives/dialog-title-container.directive"; -@Directive({ selector: "[bitDialogIcon]" }) +@Directive({ + selector: "[bitDialogIcon]", + standalone: true, +}) export class IconDirective {} @Component({ selector: "bit-simple-dialog", templateUrl: "./simple-dialog.component.html", animations: [fadeIn], + standalone: true, + imports: [NgIf, DialogTitleContainerDirective, TypographyDirective], }) export class SimpleDialogComponent { @ContentChild(IconDirective) icon!: IconDirective; diff --git a/libs/components/src/form-control/form-control.component.ts b/libs/components/src/form-control/form-control.component.ts index 6c24e7a53e6..9b87c44157a 100644 --- a/libs/components/src/form-control/form-control.component.ts +++ b/libs/components/src/form-control/form-control.component.ts @@ -1,15 +1,21 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { coerceBooleanProperty } from "@angular/cdk/coercion"; +import { NgClass, NgIf } from "@angular/common"; import { Component, ContentChild, HostBinding, Input } from "@angular/core"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { I18nPipe } from "../shared/i18n.pipe"; +import { TypographyDirective } from "../typography/typography.directive"; + import { BitFormControlAbstraction } from "./form-control.abstraction"; @Component({ selector: "bit-form-control", templateUrl: "form-control.component.html", + standalone: true, + imports: [NgClass, TypographyDirective, NgIf, I18nPipe], }) export class FormControlComponent { @Input() label: string; diff --git a/libs/components/src/form-control/form-control.module.ts b/libs/components/src/form-control/form-control.module.ts index f6969a97e9c..df168d8e98f 100644 --- a/libs/components/src/form-control/form-control.module.ts +++ b/libs/components/src/form-control/form-control.module.ts @@ -1,15 +1,11 @@ import { NgModule } from "@angular/core"; -import { SharedModule } from "../shared"; -import { TypographyModule } from "../typography"; - import { FormControlComponent } from "./form-control.component"; import { BitHintComponent } from "./hint.component"; import { BitLabel } from "./label.component"; @NgModule({ - imports: [SharedModule, BitLabel, TypographyModule], - declarations: [FormControlComponent, BitHintComponent], + imports: [BitLabel, FormControlComponent, BitHintComponent], exports: [FormControlComponent, BitLabel, BitHintComponent], }) export class FormControlModule {} diff --git a/libs/components/src/form-control/hint.component.ts b/libs/components/src/form-control/hint.component.ts index c1f21bf2545..4fee0d4560f 100644 --- a/libs/components/src/form-control/hint.component.ts +++ b/libs/components/src/form-control/hint.component.ts @@ -8,6 +8,7 @@ let nextId = 0; host: { class: "tw-text-muted tw-font-normal tw-inline-block tw-mt-1 tw-text-xs", }, + standalone: true, }) export class BitHintComponent { @HostBinding() id = `bit-hint-${nextId++}`; diff --git a/libs/components/src/form-field/error-summary.component.ts b/libs/components/src/form-field/error-summary.component.ts index f374740b20e..beed32a88ac 100644 --- a/libs/components/src/form-field/error-summary.component.ts +++ b/libs/components/src/form-field/error-summary.component.ts @@ -1,8 +1,11 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore +import { NgIf } from "@angular/common"; import { Component, Input } from "@angular/core"; import { AbstractControl, UntypedFormGroup } from "@angular/forms"; +import { I18nPipe } from "../shared/i18n.pipe"; + @Component({ selector: "bit-error-summary", template: ` @@ -12,6 +15,8 @@ import { AbstractControl, UntypedFormGroup } from "@angular/forms"; class: "tw-block tw-text-danger tw-mt-2", "aria-live": "assertive", }, + standalone: true, + imports: [NgIf, I18nPipe], }) export class BitErrorSummary { @Input() diff --git a/libs/components/src/form-field/error.component.ts b/libs/components/src/form-field/error.component.ts index a0f7906b366..27adbf7d313 100644 --- a/libs/components/src/form-field/error.component.ts +++ b/libs/components/src/form-field/error.component.ts @@ -14,6 +14,7 @@ let nextId = 0; class: "tw-block tw-mt-1 tw-text-danger tw-text-xs", "aria-live": "assertive", }, + standalone: true, }) export class BitErrorComponent { @HostBinding() id = `bit-error-${nextId++}`; diff --git a/libs/components/src/form-field/form-field.component.ts b/libs/components/src/form-field/form-field.component.ts index 6f425e41496..9f41c6cf6ac 100644 --- a/libs/components/src/form-field/form-field.component.ts +++ b/libs/components/src/form-field/form-field.component.ts @@ -1,5 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore +import { CommonModule } from "@angular/common"; import { AfterContentChecked, booleanAttribute, @@ -16,6 +17,7 @@ import { import { BitHintComponent } from "../form-control/hint.component"; import { BitLabel } from "../form-control/label.component"; import { inputBorderClasses } from "../input/input.directive"; +import { I18nPipe } from "../shared/i18n.pipe"; import { BitErrorComponent } from "./error.component"; import { BitFormFieldControl } from "./form-field-control"; @@ -23,6 +25,8 @@ import { BitFormFieldControl } from "./form-field-control"; @Component({ selector: "bit-form-field", templateUrl: "./form-field.component.html", + standalone: true, + imports: [CommonModule, BitErrorComponent, I18nPipe], }) export class BitFormFieldComponent implements AfterContentChecked { @ContentChild(BitFormFieldControl) input: BitFormFieldControl; diff --git a/libs/components/src/form-field/form-field.module.ts b/libs/components/src/form-field/form-field.module.ts index 989375167d4..88d7ffcc78b 100644 --- a/libs/components/src/form-field/form-field.module.ts +++ b/libs/components/src/form-field/form-field.module.ts @@ -1,11 +1,8 @@ import { NgModule } from "@angular/core"; import { FormControlModule } from "../form-control"; -import { BitInputDirective } from "../input/input.directive"; import { InputModule } from "../input/input.module"; -import { MultiSelectComponent } from "../multi-select/multi-select.component"; import { MultiSelectModule } from "../multi-select/multi-select.module"; -import { SharedModule } from "../shared"; import { BitErrorSummary } from "./error-summary.component"; import { BitErrorComponent } from "./error.component"; @@ -15,8 +12,11 @@ import { BitPrefixDirective } from "./prefix.directive"; import { BitSuffixDirective } from "./suffix.directive"; @NgModule({ - imports: [SharedModule, FormControlModule, InputModule, MultiSelectModule], - declarations: [ + imports: [ + FormControlModule, + InputModule, + MultiSelectModule, + BitErrorComponent, BitErrorSummary, BitFormFieldComponent, @@ -25,15 +25,16 @@ import { BitSuffixDirective } from "./suffix.directive"; BitSuffixDirective, ], exports: [ + FormControlModule, + InputModule, + MultiSelectModule, + BitErrorComponent, BitErrorSummary, BitFormFieldComponent, - BitInputDirective, BitPasswordInputToggleDirective, BitPrefixDirective, BitSuffixDirective, - MultiSelectComponent, - FormControlModule, ], }) export class FormFieldModule {} diff --git a/libs/components/src/form-field/password-input-toggle.directive.ts b/libs/components/src/form-field/password-input-toggle.directive.ts index a696a88c468..933722db5b4 100644 --- a/libs/components/src/form-field/password-input-toggle.directive.ts +++ b/libs/components/src/form-field/password-input-toggle.directive.ts @@ -18,6 +18,7 @@ import { BitFormFieldComponent } from "./form-field.component"; @Directive({ selector: "[bitPasswordInputToggle]", + standalone: true, }) export class BitPasswordInputToggleDirective implements AfterContentInit, OnChanges { /** diff --git a/libs/components/src/form-field/prefix.directive.ts b/libs/components/src/form-field/prefix.directive.ts index 34fcbf85233..b44e90cbaad 100644 --- a/libs/components/src/form-field/prefix.directive.ts +++ b/libs/components/src/form-field/prefix.directive.ts @@ -4,6 +4,7 @@ import { BitIconButtonComponent } from "../icon-button/icon-button.component"; @Directive({ selector: "[bitPrefix]", + standalone: true, }) export class BitPrefixDirective implements OnInit { @HostBinding("class") @Input() get classList() { diff --git a/libs/components/src/form-field/suffix.directive.ts b/libs/components/src/form-field/suffix.directive.ts index 28736ce78a9..baf1afce763 100644 --- a/libs/components/src/form-field/suffix.directive.ts +++ b/libs/components/src/form-field/suffix.directive.ts @@ -4,6 +4,7 @@ import { BitIconButtonComponent } from "../icon-button/icon-button.component"; @Directive({ selector: "[bitSuffix]", + standalone: true, }) export class BitSuffixDirective implements OnInit { @HostBinding("class") @Input() get classList() { diff --git a/libs/components/src/icon-button/icon-button.component.ts b/libs/components/src/icon-button/icon-button.component.ts index 97016f9fd0c..ac7dff0408b 100644 --- a/libs/components/src/icon-button/icon-button.component.ts +++ b/libs/components/src/icon-button/icon-button.component.ts @@ -1,5 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore +import { NgClass } from "@angular/common"; import { Component, ElementRef, HostBinding, Input } from "@angular/core"; import { ButtonLikeAbstraction, ButtonType } from "../shared/button-like.abstraction"; @@ -134,6 +135,8 @@ const sizes: Record = { { provide: ButtonLikeAbstraction, useExisting: BitIconButtonComponent }, { provide: FocusableElement, useExisting: BitIconButtonComponent }, ], + standalone: true, + imports: [NgClass], }) export class BitIconButtonComponent implements ButtonLikeAbstraction, FocusableElement { @Input("bitIconButton") icon: string; diff --git a/libs/components/src/icon-button/icon-button.module.ts b/libs/components/src/icon-button/icon-button.module.ts index fb4e8589717..26f48cdb177 100644 --- a/libs/components/src/icon-button/icon-button.module.ts +++ b/libs/components/src/icon-button/icon-button.module.ts @@ -1,11 +1,9 @@ -import { CommonModule } from "@angular/common"; import { NgModule } from "@angular/core"; import { BitIconButtonComponent } from "./icon-button.component"; @NgModule({ - imports: [CommonModule], - declarations: [BitIconButtonComponent], + imports: [BitIconButtonComponent], exports: [BitIconButtonComponent], }) export class IconButtonModule {} diff --git a/libs/components/src/icon/icon.component.ts b/libs/components/src/icon/icon.component.ts index 55615d4dae3..2382d197bec 100644 --- a/libs/components/src/icon/icon.component.ts +++ b/libs/components/src/icon/icon.component.ts @@ -8,6 +8,7 @@ import { Icon, isIcon } from "./icon"; @Component({ selector: "bit-icon", template: ``, + standalone: true, }) export class BitIconComponent { @Input() set icon(icon: Icon) { diff --git a/libs/components/src/icon/icon.components.spec.ts b/libs/components/src/icon/icon.components.spec.ts index 351ed5f0218..7d499cdd419 100644 --- a/libs/components/src/icon/icon.components.spec.ts +++ b/libs/components/src/icon/icon.components.spec.ts @@ -9,7 +9,7 @@ describe("IconComponent", () => { beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [BitIconComponent], + imports: [BitIconComponent], }).compileComponents(); fixture = TestBed.createComponent(BitIconComponent); diff --git a/libs/components/src/icon/icon.module.ts b/libs/components/src/icon/icon.module.ts index 32e95fd0468..3d15b5bb3c3 100644 --- a/libs/components/src/icon/icon.module.ts +++ b/libs/components/src/icon/icon.module.ts @@ -1,11 +1,9 @@ -import { CommonModule } from "@angular/common"; import { NgModule } from "@angular/core"; import { BitIconComponent } from "./icon.component"; @NgModule({ - imports: [CommonModule], - declarations: [BitIconComponent], + imports: [BitIconComponent], exports: [BitIconComponent], }) export class IconModule {} diff --git a/libs/components/src/input/input.directive.ts b/libs/components/src/input/input.directive.ts index 4a6a03295d4..f6c6c3d542e 100644 --- a/libs/components/src/input/input.directive.ts +++ b/libs/components/src/input/input.directive.ts @@ -30,6 +30,7 @@ export function inputBorderClasses(error: boolean) { @Directive({ selector: "input[bitInput], select[bitInput], textarea[bitInput]", providers: [{ provide: BitFormFieldControl, useExisting: BitInputDirective }], + standalone: true, }) export class BitInputDirective implements BitFormFieldControl { @HostBinding("class") @Input() get classList() { diff --git a/libs/components/src/input/input.module.ts b/libs/components/src/input/input.module.ts index cfc49cefb7d..9399cb06517 100644 --- a/libs/components/src/input/input.module.ts +++ b/libs/components/src/input/input.module.ts @@ -1,11 +1,9 @@ -import { CommonModule } from "@angular/common"; import { NgModule } from "@angular/core"; import { BitInputDirective } from "./input.directive"; @NgModule({ - imports: [CommonModule], - declarations: [BitInputDirective], + imports: [BitInputDirective], exports: [BitInputDirective], }) export class InputModule {} diff --git a/libs/components/src/link/link.directive.ts b/libs/components/src/link/link.directive.ts index b127d80fedf..52aba557661 100644 --- a/libs/components/src/link/link.directive.ts +++ b/libs/components/src/link/link.directive.ts @@ -68,6 +68,7 @@ abstract class LinkDirective { @Directive({ selector: "a[bitLink]", + standalone: true, }) export class AnchorLinkDirective extends LinkDirective { @HostBinding("class") get classList() { @@ -79,6 +80,7 @@ export class AnchorLinkDirective extends LinkDirective { @Directive({ selector: "button[bitLink]", + standalone: true, }) export class ButtonLinkDirective extends LinkDirective { @HostBinding("class") get classList() { diff --git a/libs/components/src/link/link.module.ts b/libs/components/src/link/link.module.ts index b8b54d57c00..52d2f29e53c 100644 --- a/libs/components/src/link/link.module.ts +++ b/libs/components/src/link/link.module.ts @@ -1,11 +1,9 @@ -import { CommonModule } from "@angular/common"; import { NgModule } from "@angular/core"; import { AnchorLinkDirective, ButtonLinkDirective } from "./link.directive"; @NgModule({ - imports: [CommonModule], + imports: [AnchorLinkDirective, ButtonLinkDirective], exports: [AnchorLinkDirective, ButtonLinkDirective], - declarations: [AnchorLinkDirective, ButtonLinkDirective], }) export class LinkModule {} diff --git a/libs/components/src/menu/menu-divider.component.ts b/libs/components/src/menu/menu-divider.component.ts index 194506ee50f..55b5c013c93 100644 --- a/libs/components/src/menu/menu-divider.component.ts +++ b/libs/components/src/menu/menu-divider.component.ts @@ -3,5 +3,6 @@ import { Component } from "@angular/core"; @Component({ selector: "bit-menu-divider", templateUrl: "./menu-divider.component.html", + standalone: true, }) export class MenuDividerComponent {} diff --git a/libs/components/src/menu/menu-item.directive.ts b/libs/components/src/menu/menu-item.directive.ts index 5fdc8fabfce..d0975e8e391 100644 --- a/libs/components/src/menu/menu-item.directive.ts +++ b/libs/components/src/menu/menu-item.directive.ts @@ -1,10 +1,13 @@ import { FocusableOption } from "@angular/cdk/a11y"; import { coerceBooleanProperty } from "@angular/cdk/coercion"; +import { NgClass } from "@angular/common"; import { Component, ElementRef, HostBinding, Input } from "@angular/core"; @Component({ selector: "[bitMenuItem]", templateUrl: "menu-item.component.html", + standalone: true, + imports: [NgClass], }) export class MenuItemDirective implements FocusableOption { @HostBinding("class") classList = [ diff --git a/libs/components/src/menu/menu-trigger-for.directive.ts b/libs/components/src/menu/menu-trigger-for.directive.ts index d318a77ef00..786554e981c 100644 --- a/libs/components/src/menu/menu-trigger-for.directive.ts +++ b/libs/components/src/menu/menu-trigger-for.directive.ts @@ -19,6 +19,7 @@ import { MenuComponent } from "./menu.component"; @Directive({ selector: "[bitMenuTriggerFor]", exportAs: "menuTrigger", + standalone: true, }) export class MenuTriggerForDirective implements OnDestroy { @HostBinding("attr.aria-expanded") isOpen = false; diff --git a/libs/components/src/menu/menu.component.ts b/libs/components/src/menu/menu.component.ts index f0bf4f81df9..a39dceb4454 100644 --- a/libs/components/src/menu/menu.component.ts +++ b/libs/components/src/menu/menu.component.ts @@ -1,6 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { FocusKeyManager } from "@angular/cdk/a11y"; +import { FocusKeyManager, CdkTrapFocus } from "@angular/cdk/a11y"; import { Component, Output, @@ -19,6 +19,8 @@ import { MenuItemDirective } from "./menu-item.directive"; selector: "bit-menu", templateUrl: "./menu.component.html", exportAs: "menuComponent", + standalone: true, + imports: [CdkTrapFocus], }) export class MenuComponent implements AfterContentInit { @ViewChild(TemplateRef) templateRef: TemplateRef; diff --git a/libs/components/src/menu/menu.module.ts b/libs/components/src/menu/menu.module.ts index b165629e6c5..117460df559 100644 --- a/libs/components/src/menu/menu.module.ts +++ b/libs/components/src/menu/menu.module.ts @@ -1,6 +1,3 @@ -import { A11yModule } from "@angular/cdk/a11y"; -import { OverlayModule } from "@angular/cdk/overlay"; -import { CommonModule } from "@angular/common"; import { NgModule } from "@angular/core"; import { MenuDividerComponent } from "./menu-divider.component"; @@ -9,8 +6,7 @@ import { MenuTriggerForDirective } from "./menu-trigger-for.directive"; import { MenuComponent } from "./menu.component"; @NgModule({ - imports: [A11yModule, CommonModule, OverlayModule], - declarations: [MenuComponent, MenuTriggerForDirective, MenuItemDirective, MenuDividerComponent], + imports: [MenuComponent, MenuTriggerForDirective, MenuItemDirective, MenuDividerComponent], exports: [MenuComponent, MenuTriggerForDirective, MenuItemDirective, MenuDividerComponent], }) export class MenuModule {} diff --git a/libs/components/src/menu/menu.stories.ts b/libs/components/src/menu/menu.stories.ts index c5d232b2057..65fafd2d04d 100644 --- a/libs/components/src/menu/menu.stories.ts +++ b/libs/components/src/menu/menu.stories.ts @@ -3,23 +3,15 @@ import { Meta, StoryObj, moduleMetadata } from "@storybook/angular"; import { ButtonModule } from "../button/button.module"; -import { MenuDividerComponent } from "./menu-divider.component"; -import { MenuItemDirective } from "./menu-item.directive"; import { MenuTriggerForDirective } from "./menu-trigger-for.directive"; -import { MenuComponent } from "./menu.component"; +import { MenuModule } from "./menu.module"; export default { title: "Component Library/Menu", component: MenuTriggerForDirective, decorators: [ moduleMetadata({ - declarations: [ - MenuTriggerForDirective, - MenuComponent, - MenuItemDirective, - MenuDividerComponent, - ], - imports: [OverlayModule, ButtonModule], + imports: [MenuModule, OverlayModule, ButtonModule], }), ], parameters: { @@ -51,7 +43,7 @@ export const OpenMenu: Story = { Disabled button - +
@@ -67,7 +59,7 @@ export const ClosedMenu: Story = {
- + Anchor link Another link diff --git a/libs/components/src/multi-select/multi-select.component.ts b/libs/components/src/multi-select/multi-select.component.ts index a18d5aa0b60..53e51bfe2f9 100644 --- a/libs/components/src/multi-select/multi-select.component.ts +++ b/libs/components/src/multi-select/multi-select.component.ts @@ -2,6 +2,7 @@ // @ts-strict-ignore import { coerceBooleanProperty } from "@angular/cdk/coercion"; import { hasModifierKey } from "@angular/cdk/keycodes"; +import { NgIf } from "@angular/common"; import { Component, Input, @@ -13,12 +14,20 @@ import { Optional, Self, } from "@angular/core"; -import { ControlValueAccessor, NgControl, Validators } from "@angular/forms"; -import { NgSelectComponent } from "@ng-select/ng-select"; +import { + ControlValueAccessor, + NgControl, + Validators, + ReactiveFormsModule, + FormsModule, +} from "@angular/forms"; +import { NgSelectComponent, NgSelectModule } from "@ng-select/ng-select"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { BadgeModule } from "../badge"; import { BitFormFieldControl } from "../form-field/form-field-control"; +import { I18nPipe } from "../shared/i18n.pipe"; import { SelectItemView } from "./models/select-item-view"; @@ -29,6 +38,8 @@ let nextId = 0; selector: "bit-multi-select", templateUrl: "./multi-select.component.html", providers: [{ provide: BitFormFieldControl, useExisting: MultiSelectComponent }], + standalone: true, + imports: [NgSelectModule, ReactiveFormsModule, FormsModule, BadgeModule, NgIf, I18nPipe], }) /** * This component has been implemented to only support Multi-select list events diff --git a/libs/components/src/multi-select/multi-select.module.ts b/libs/components/src/multi-select/multi-select.module.ts index 88de53b5481..c8cc899db00 100644 --- a/libs/components/src/multi-select/multi-select.module.ts +++ b/libs/components/src/multi-select/multi-select.module.ts @@ -1,16 +1,9 @@ -import { CommonModule } from "@angular/common"; import { NgModule } from "@angular/core"; -import { FormsModule } from "@angular/forms"; -import { NgSelectModule } from "@ng-select/ng-select"; - -import { BadgeModule } from "../badge"; -import { SharedModule } from "../shared"; import { MultiSelectComponent } from "./multi-select.component"; @NgModule({ - imports: [CommonModule, FormsModule, NgSelectModule, BadgeModule, SharedModule], + imports: [MultiSelectComponent], exports: [MultiSelectComponent], - declarations: [MultiSelectComponent], }) export class MultiSelectModule {} diff --git a/libs/components/src/navigation/nav-divider.component.ts b/libs/components/src/navigation/nav-divider.component.ts index 008d3f46c35..eff381e1c94 100644 --- a/libs/components/src/navigation/nav-divider.component.ts +++ b/libs/components/src/navigation/nav-divider.component.ts @@ -1,3 +1,4 @@ +import { CommonModule } from "@angular/common"; import { Component } from "@angular/core"; import { SideNavService } from "./side-nav.service"; @@ -5,6 +6,8 @@ import { SideNavService } from "./side-nav.service"; @Component({ selector: "bit-nav-divider", templateUrl: "./nav-divider.component.html", + standalone: true, + imports: [CommonModule], }) export class NavDividerComponent { constructor(protected sideNavService: SideNavService) {} diff --git a/libs/components/src/navigation/nav-group.component.ts b/libs/components/src/navigation/nav-group.component.ts index 07494c0b7da..58d93ddd3a4 100644 --- a/libs/components/src/navigation/nav-group.component.ts +++ b/libs/components/src/navigation/nav-group.component.ts @@ -1,3 +1,4 @@ +import { CommonModule } from "@angular/common"; import { AfterContentInit, booleanAttribute, @@ -11,13 +12,22 @@ import { SkipSelf, } from "@angular/core"; +import { IconButtonModule } from "../icon-button"; +import { I18nPipe } from "../shared/i18n.pipe"; + import { NavBaseComponent } from "./nav-base.component"; +import { NavGroupAbstraction, NavItemComponent } from "./nav-item.component"; import { SideNavService } from "./side-nav.service"; @Component({ selector: "bit-nav-group", templateUrl: "./nav-group.component.html", - providers: [{ provide: NavBaseComponent, useExisting: NavGroupComponent }], + providers: [ + { provide: NavBaseComponent, useExisting: NavGroupComponent }, + { provide: NavGroupAbstraction, useExisting: NavGroupComponent }, + ], + standalone: true, + imports: [CommonModule, NavItemComponent, IconButtonModule, I18nPipe], }) export class NavGroupComponent extends NavBaseComponent implements AfterContentInit { @ContentChildren(NavBaseComponent, { diff --git a/libs/components/src/navigation/nav-item.component.ts b/libs/components/src/navigation/nav-item.component.ts index 8348638568b..c8d464119ce 100644 --- a/libs/components/src/navigation/nav-item.component.ts +++ b/libs/components/src/navigation/nav-item.component.ts @@ -1,14 +1,24 @@ +import { CommonModule } from "@angular/common"; import { Component, HostListener, Input, Optional } from "@angular/core"; +import { RouterModule } from "@angular/router"; import { BehaviorSubject, map } from "rxjs"; +import { IconButtonModule } from "../icon-button"; + import { NavBaseComponent } from "./nav-base.component"; -import { NavGroupComponent } from "./nav-group.component"; import { SideNavService } from "./side-nav.service"; +// Resolves a circular dependency between `NavItemComponent` and `NavItemGroup` when using standalone components. +export abstract class NavGroupAbstraction { + abstract setOpen(open: boolean): void; +} + @Component({ selector: "bit-nav-item", templateUrl: "./nav-item.component.html", providers: [{ provide: NavBaseComponent, useExisting: NavItemComponent }], + standalone: true, + imports: [CommonModule, IconButtonModule, RouterModule], }) export class NavItemComponent extends NavBaseComponent { /** Forces active styles to be shown, regardless of the `routerLinkActiveOptions` */ @@ -52,7 +62,7 @@ export class NavItemComponent extends NavBaseComponent { constructor( protected sideNavService: SideNavService, - @Optional() private parentNavGroup: NavGroupComponent, + @Optional() private parentNavGroup: NavGroupAbstraction, ) { super(); } diff --git a/libs/components/src/navigation/nav-logo.component.ts b/libs/components/src/navigation/nav-logo.component.ts index cbad5b869e7..8a84970500c 100644 --- a/libs/components/src/navigation/nav-logo.component.ts +++ b/libs/components/src/navigation/nav-logo.component.ts @@ -1,14 +1,20 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore +import { NgIf } from "@angular/common"; import { Component, Input } from "@angular/core"; +import { RouterLinkActive, RouterLink } from "@angular/router"; import { Icon } from "../icon"; +import { BitIconComponent } from "../icon/icon.component"; +import { NavItemComponent } from "./nav-item.component"; import { SideNavService } from "./side-nav.service"; @Component({ selector: "bit-nav-logo", templateUrl: "./nav-logo.component.html", + standalone: true, + imports: [NgIf, RouterLinkActive, RouterLink, BitIconComponent, NavItemComponent], }) export class NavLogoComponent { /** Icon that is displayed when the side nav is closed */ diff --git a/libs/components/src/navigation/navigation.module.ts b/libs/components/src/navigation/navigation.module.ts index 852bd1c0a25..a08fbaddb98 100644 --- a/libs/components/src/navigation/navigation.module.ts +++ b/libs/components/src/navigation/navigation.module.ts @@ -1,13 +1,4 @@ -import { A11yModule } from "@angular/cdk/a11y"; -import { OverlayModule } from "@angular/cdk/overlay"; -import { CommonModule } from "@angular/common"; import { NgModule } from "@angular/core"; -import { RouterModule } from "@angular/router"; - -import { IconModule } from "../icon"; -import { IconButtonModule } from "../icon-button/icon-button.module"; -import { LinkModule } from "../link"; -import { SharedModule } from "../shared/shared.module"; import { NavDividerComponent } from "./nav-divider.component"; import { NavGroupComponent } from "./nav-group.component"; @@ -17,16 +8,6 @@ import { SideNavComponent } from "./side-nav.component"; @NgModule({ imports: [ - CommonModule, - SharedModule, - IconButtonModule, - OverlayModule, - RouterModule, - IconModule, - A11yModule, - LinkModule, - ], - declarations: [ NavDividerComponent, NavGroupComponent, NavItemComponent, diff --git a/libs/components/src/navigation/side-nav.component.ts b/libs/components/src/navigation/side-nav.component.ts index a4af51772b3..c86a517100f 100644 --- a/libs/components/src/navigation/side-nav.component.ts +++ b/libs/components/src/navigation/side-nav.component.ts @@ -1,7 +1,13 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore +import { CdkTrapFocus } from "@angular/cdk/a11y"; +import { CommonModule } from "@angular/common"; import { Component, ElementRef, Input, ViewChild } from "@angular/core"; +import { BitIconButtonComponent } from "../icon-button/icon-button.component"; +import { I18nPipe } from "../shared/i18n.pipe"; + +import { NavDividerComponent } from "./nav-divider.component"; import { SideNavService } from "./side-nav.service"; export type SideNavVariant = "primary" | "secondary"; @@ -9,6 +15,8 @@ export type SideNavVariant = "primary" | "secondary"; @Component({ selector: "bit-side-nav", templateUrl: "side-nav.component.html", + standalone: true, + imports: [CommonModule, CdkTrapFocus, NavDividerComponent, BitIconButtonComponent, I18nPipe], }) export class SideNavComponent { @Input() variant: SideNavVariant = "primary"; diff --git a/libs/components/src/no-items/no-items.component.ts b/libs/components/src/no-items/no-items.component.ts index d85c6a34571..ee9e0ee0581 100644 --- a/libs/components/src/no-items/no-items.component.ts +++ b/libs/components/src/no-items/no-items.component.ts @@ -1,6 +1,7 @@ import { Component, Input } from "@angular/core"; import { Icons } from ".."; +import { BitIconComponent } from "../icon/icon.component"; /** * Component for displaying a message when there are no items to display. Expects title, description and button slots. @@ -8,6 +9,8 @@ import { Icons } from ".."; @Component({ selector: "bit-no-items", templateUrl: "./no-items.component.html", + standalone: true, + imports: [BitIconComponent], }) export class NoItemsComponent { @Input() icon = Icons.Search; diff --git a/libs/components/src/no-items/no-items.module.ts b/libs/components/src/no-items/no-items.module.ts index 9fe6eb37aa9..49c3c73f133 100644 --- a/libs/components/src/no-items/no-items.module.ts +++ b/libs/components/src/no-items/no-items.module.ts @@ -1,13 +1,9 @@ -import { CommonModule } from "@angular/common"; import { NgModule } from "@angular/core"; -import { IconModule } from "../icon"; - import { NoItemsComponent } from "./no-items.component"; @NgModule({ - imports: [CommonModule, IconModule], + imports: [NoItemsComponent], exports: [NoItemsComponent], - declarations: [NoItemsComponent], }) export class NoItemsModule {} diff --git a/libs/components/src/progress/progress.component.ts b/libs/components/src/progress/progress.component.ts index 37206dc6ae4..04e535158b1 100644 --- a/libs/components/src/progress/progress.component.ts +++ b/libs/components/src/progress/progress.component.ts @@ -1,3 +1,4 @@ +import { CommonModule } from "@angular/common"; import { Component, Input } from "@angular/core"; type SizeTypes = "small" | "default" | "large"; @@ -19,6 +20,8 @@ const BackgroundClasses: Record = { @Component({ selector: "bit-progress", templateUrl: "./progress.component.html", + standalone: true, + imports: [CommonModule], }) export class ProgressComponent { @Input() barWidth = 0; diff --git a/libs/components/src/progress/progress.module.ts b/libs/components/src/progress/progress.module.ts index 8ab09189d19..cc93c4c3bd0 100644 --- a/libs/components/src/progress/progress.module.ts +++ b/libs/components/src/progress/progress.module.ts @@ -1,11 +1,9 @@ -import { CommonModule } from "@angular/common"; import { NgModule } from "@angular/core"; import { ProgressComponent } from "./progress.component"; @NgModule({ - imports: [CommonModule], + imports: [ProgressComponent], exports: [ProgressComponent], - declarations: [ProgressComponent], }) export class ProgressModule {} diff --git a/libs/components/src/radio-button/radio-button.component.ts b/libs/components/src/radio-button/radio-button.component.ts index dc294103d42..042a54edf47 100644 --- a/libs/components/src/radio-button/radio-button.component.ts +++ b/libs/components/src/radio-button/radio-button.component.ts @@ -1,12 +1,17 @@ import { Component, HostBinding, Input } from "@angular/core"; +import { FormControlModule } from "../form-control/form-control.module"; + import { RadioGroupComponent } from "./radio-group.component"; +import { RadioInputComponent } from "./radio-input.component"; let nextId = 0; @Component({ selector: "bit-radio-button", templateUrl: "radio-button.component.html", + standalone: true, + imports: [FormControlModule, RadioInputComponent], }) export class RadioButtonComponent { @HostBinding("attr.id") @Input() id = `bit-radio-button-${nextId++}`; diff --git a/libs/components/src/radio-button/radio-button.module.ts b/libs/components/src/radio-button/radio-button.module.ts index 21fd9427046..7b05c27b4ff 100644 --- a/libs/components/src/radio-button/radio-button.module.ts +++ b/libs/components/src/radio-button/radio-button.module.ts @@ -1,16 +1,13 @@ -import { CommonModule } from "@angular/common"; import { NgModule } from "@angular/core"; import { FormControlModule } from "../form-control"; -import { SharedModule } from "../shared"; import { RadioButtonComponent } from "./radio-button.component"; import { RadioGroupComponent } from "./radio-group.component"; import { RadioInputComponent } from "./radio-input.component"; @NgModule({ - imports: [CommonModule, SharedModule, FormControlModule], - declarations: [RadioInputComponent, RadioButtonComponent, RadioGroupComponent], + imports: [FormControlModule, RadioInputComponent, RadioButtonComponent, RadioGroupComponent], exports: [FormControlModule, RadioInputComponent, RadioButtonComponent, RadioGroupComponent], }) export class RadioButtonModule {} diff --git a/libs/components/src/radio-button/radio-group.component.ts b/libs/components/src/radio-button/radio-group.component.ts index 2cddb4fb7bc..b9e48f46445 100644 --- a/libs/components/src/radio-button/radio-group.component.ts +++ b/libs/components/src/radio-button/radio-group.component.ts @@ -1,15 +1,19 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore +import { NgIf, NgTemplateOutlet } from "@angular/common"; import { Component, ContentChild, HostBinding, Input, Optional, Self } from "@angular/core"; import { ControlValueAccessor, NgControl, Validators } from "@angular/forms"; import { BitLabel } from "../form-control/label.component"; +import { I18nPipe } from "../shared/i18n.pipe"; let nextId = 0; @Component({ selector: "bit-radio-group", templateUrl: "radio-group.component.html", + standalone: true, + imports: [NgIf, NgTemplateOutlet, I18nPipe], }) export class RadioGroupComponent implements ControlValueAccessor { selected: unknown; diff --git a/libs/components/src/radio-button/radio-input.component.ts b/libs/components/src/radio-button/radio-input.component.ts index 580e5bca25e..4a9f5dede60 100644 --- a/libs/components/src/radio-button/radio-input.component.ts +++ b/libs/components/src/radio-button/radio-input.component.ts @@ -11,6 +11,7 @@ let nextId = 0; selector: "input[type=radio][bitRadio]", template: "", providers: [{ provide: BitFormControlAbstraction, useExisting: RadioInputComponent }], + standalone: true, }) export class RadioInputComponent implements BitFormControlAbstraction { @HostBinding("attr.id") @Input() id = `bit-radio-input-${nextId++}`; diff --git a/libs/components/src/search/search.component.ts b/libs/components/src/search/search.component.ts index bc98e5a293b..6ec79eaa84e 100644 --- a/libs/components/src/search/search.component.ts +++ b/libs/components/src/search/search.component.ts @@ -1,11 +1,18 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { Component, ElementRef, Input, ViewChild } from "@angular/core"; -import { ControlValueAccessor, NG_VALUE_ACCESSOR } from "@angular/forms"; +import { + ControlValueAccessor, + NG_VALUE_ACCESSOR, + ReactiveFormsModule, + FormsModule, +} from "@angular/forms"; import { isBrowserSafariApi } from "@bitwarden/platform"; +import { InputModule } from "../input/input.module"; import { FocusableElement } from "../shared/focusable-element"; +import { I18nPipe } from "../shared/i18n.pipe"; let nextId = 0; @@ -23,6 +30,8 @@ let nextId = 0; useExisting: SearchComponent, }, ], + standalone: true, + imports: [InputModule, ReactiveFormsModule, FormsModule, I18nPipe], }) export class SearchComponent implements ControlValueAccessor, FocusableElement { private notifyOnChange: (v: string) => void; diff --git a/libs/components/src/search/search.module.ts b/libs/components/src/search/search.module.ts index 62072774900..cb9761eae6b 100644 --- a/libs/components/src/search/search.module.ts +++ b/libs/components/src/search/search.module.ts @@ -1,14 +1,9 @@ import { NgModule } from "@angular/core"; -import { FormsModule } from "@angular/forms"; - -import { InputModule } from "../input/input.module"; -import { SharedModule } from "../shared"; import { SearchComponent } from "./search.component"; @NgModule({ - imports: [SharedModule, InputModule, FormsModule], - declarations: [SearchComponent], + imports: [SearchComponent], exports: [SearchComponent], }) export class SearchModule {} diff --git a/libs/components/src/select/option.component.ts b/libs/components/src/select/option.component.ts index b32b124be25..841ceda3648 100644 --- a/libs/components/src/select/option.component.ts +++ b/libs/components/src/select/option.component.ts @@ -7,6 +7,7 @@ import { Option } from "./option"; @Component({ selector: "bit-option", template: ``, + standalone: true, }) export class OptionComponent implements Option { @Input() diff --git a/libs/components/src/select/select.component.ts b/libs/components/src/select/select.component.ts index cdcf794e489..8f75c5be42b 100644 --- a/libs/components/src/select/select.component.ts +++ b/libs/components/src/select/select.component.ts @@ -1,5 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore +import { NgIf } from "@angular/common"; import { Component, ContentChildren, @@ -12,8 +13,14 @@ import { Output, EventEmitter, } from "@angular/core"; -import { ControlValueAccessor, NgControl, Validators } from "@angular/forms"; -import { NgSelectComponent } from "@ng-select/ng-select"; +import { + ControlValueAccessor, + NgControl, + Validators, + ReactiveFormsModule, + FormsModule, +} from "@angular/forms"; +import { NgSelectComponent, NgSelectModule } from "@ng-select/ng-select"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -28,6 +35,8 @@ let nextId = 0; selector: "bit-select", templateUrl: "select.component.html", providers: [{ provide: BitFormFieldControl, useExisting: SelectComponent }], + standalone: true, + imports: [NgSelectModule, ReactiveFormsModule, FormsModule, NgIf], }) export class SelectComponent implements BitFormFieldControl, ControlValueAccessor { @ViewChild(NgSelectComponent) select: NgSelectComponent; diff --git a/libs/components/src/select/select.module.ts b/libs/components/src/select/select.module.ts index 4391a518174..8807ed63a48 100644 --- a/libs/components/src/select/select.module.ts +++ b/libs/components/src/select/select.module.ts @@ -1,14 +1,10 @@ -import { CommonModule } from "@angular/common"; import { NgModule } from "@angular/core"; -import { FormsModule } from "@angular/forms"; -import { NgSelectModule } from "@ng-select/ng-select"; import { OptionComponent } from "./option.component"; import { SelectComponent } from "./select.component"; @NgModule({ - imports: [CommonModule, NgSelectModule, FormsModule], - declarations: [SelectComponent, OptionComponent], + imports: [SelectComponent, OptionComponent], exports: [SelectComponent, OptionComponent], }) export class SelectModule {} diff --git a/libs/components/src/shared/i18n.pipe.ts b/libs/components/src/shared/i18n.pipe.ts index f428d9297c0..91bf0b3198d 100644 --- a/libs/components/src/shared/i18n.pipe.ts +++ b/libs/components/src/shared/i18n.pipe.ts @@ -7,6 +7,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic */ @Pipe({ name: "i18n", + standalone: true, }) export class I18nPipe implements PipeTransform { constructor(private i18nService: I18nService) {} diff --git a/libs/components/src/shared/shared.module.ts b/libs/components/src/shared/shared.module.ts index dcf2e2bc05f..253b049f8fe 100644 --- a/libs/components/src/shared/shared.module.ts +++ b/libs/components/src/shared/shared.module.ts @@ -4,8 +4,7 @@ import { NgModule } from "@angular/core"; import { I18nPipe } from "./i18n.pipe"; @NgModule({ - imports: [CommonModule], - declarations: [I18nPipe], + imports: [CommonModule, I18nPipe], exports: [CommonModule, I18nPipe], }) export class SharedModule {} diff --git a/libs/components/src/table/cell.directive.ts b/libs/components/src/table/cell.directive.ts index 61c75571063..8928fe7c095 100644 --- a/libs/components/src/table/cell.directive.ts +++ b/libs/components/src/table/cell.directive.ts @@ -2,6 +2,7 @@ import { Directive, HostBinding } from "@angular/core"; @Directive({ selector: "th[bitCell], td[bitCell]", + standalone: true, }) export class CellDirective { @HostBinding("class") get classList() { diff --git a/libs/components/src/table/row.directive.ts b/libs/components/src/table/row.directive.ts index 19f3d3f775b..23347224af9 100644 --- a/libs/components/src/table/row.directive.ts +++ b/libs/components/src/table/row.directive.ts @@ -2,6 +2,7 @@ import { Directive, HostBinding, Input } from "@angular/core"; @Directive({ selector: "tr[bitRow]", + standalone: true, }) export class RowDirective { @Input() alignContent: "top" | "middle" | "bottom" | "baseline" = "middle"; diff --git a/libs/components/src/table/sortable.component.ts b/libs/components/src/table/sortable.component.ts index dc3d8dc14f0..d3309c03aa9 100644 --- a/libs/components/src/table/sortable.component.ts +++ b/libs/components/src/table/sortable.component.ts @@ -1,6 +1,7 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { coerceBooleanProperty } from "@angular/cdk/coercion"; +import { NgClass } from "@angular/common"; import { Component, HostBinding, Input, OnInit } from "@angular/core"; import type { SortDirection, SortFn } from "./table-data-source"; @@ -14,6 +15,8 @@ import { TableComponent } from "./table.component"; `, + standalone: true, + imports: [NgClass], }) export class SortableComponent implements OnInit { /** diff --git a/libs/components/src/table/table-scroll.component.ts b/libs/components/src/table/table-scroll.component.ts index 9e308b7da59..34cd8c5d9ca 100644 --- a/libs/components/src/table/table-scroll.component.ts +++ b/libs/components/src/table/table-scroll.component.ts @@ -1,5 +1,12 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore +import { + CdkVirtualScrollViewport, + CdkVirtualScrollableWindow, + CdkFixedSizeVirtualScroll, + CdkVirtualForOf, +} from "@angular/cdk/scrolling"; +import { CommonModule } from "@angular/common"; import { AfterContentChecked, Component, @@ -14,6 +21,7 @@ import { TrackByFunction, } from "@angular/core"; +import { RowDirective } from "./row.directive"; import { TableComponent } from "./table.component"; /** @@ -42,6 +50,15 @@ export class BitRowDef { selector: "bit-table-scroll", templateUrl: "./table-scroll.component.html", providers: [{ provide: TableComponent, useExisting: TableScrollComponent }], + standalone: true, + imports: [ + CommonModule, + CdkVirtualScrollViewport, + CdkVirtualScrollableWindow, + CdkFixedSizeVirtualScroll, + CdkVirtualForOf, + RowDirective, + ], }) export class TableScrollComponent extends TableComponent diff --git a/libs/components/src/table/table.component.ts b/libs/components/src/table/table.component.ts index 8bc7754b16b..cd0a2a6c65e 100644 --- a/libs/components/src/table/table.component.ts +++ b/libs/components/src/table/table.component.ts @@ -1,6 +1,7 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { isDataSource } from "@angular/cdk/collections"; +import { CommonModule } from "@angular/common"; import { AfterContentChecked, Component, @@ -16,6 +17,7 @@ import { TableDataSource } from "./table-data-source"; @Directive({ selector: "ng-template[body]", + standalone: true, }) export class TableBodyDirective { // eslint-disable-next-line @typescript-eslint/explicit-member-accessibility @@ -25,6 +27,8 @@ export class TableBodyDirective { @Component({ selector: "bit-table", templateUrl: "./table.component.html", + standalone: true, + imports: [CommonModule], }) export class TableComponent implements OnDestroy, AfterContentChecked { @Input() dataSource: TableDataSource; diff --git a/libs/components/src/table/table.module.ts b/libs/components/src/table/table.module.ts index 1f1b705c69e..68993612772 100644 --- a/libs/components/src/table/table.module.ts +++ b/libs/components/src/table/table.module.ts @@ -9,8 +9,10 @@ import { BitRowDef, TableScrollComponent } from "./table-scroll.component"; import { TableBodyDirective, TableComponent } from "./table.component"; @NgModule({ - imports: [CommonModule, ScrollingModule, BitRowDef], - declarations: [ + imports: [ + CommonModule, + ScrollingModule, + BitRowDef, CellDirective, RowDirective, SortableComponent, diff --git a/libs/components/src/tabs/shared/tab-header.component.ts b/libs/components/src/tabs/shared/tab-header.component.ts index 4712df0549a..c45bafb3d52 100644 --- a/libs/components/src/tabs/shared/tab-header.component.ts +++ b/libs/components/src/tabs/shared/tab-header.component.ts @@ -10,5 +10,6 @@ import { Component } from "@angular/core"; "tw-h-16 tw-pl-4 tw-bg-background-alt tw-flex tw-items-end tw-border-0 tw-border-b tw-border-solid tw-border-secondary-300", }, template: ``, + standalone: true, }) export class TabHeaderComponent {} diff --git a/libs/components/src/tabs/shared/tab-list-container.directive.ts b/libs/components/src/tabs/shared/tab-list-container.directive.ts index 1cf8a762d58..cedae44e582 100644 --- a/libs/components/src/tabs/shared/tab-list-container.directive.ts +++ b/libs/components/src/tabs/shared/tab-list-container.directive.ts @@ -8,5 +8,6 @@ import { Directive } from "@angular/core"; host: { class: "tw-inline-flex tw-flex-wrap tw-leading-5", }, + standalone: true, }) export class TabListContainerDirective {} diff --git a/libs/components/src/tabs/shared/tab-list-item.directive.ts b/libs/components/src/tabs/shared/tab-list-item.directive.ts index 7514f5417e6..87435133a23 100644 --- a/libs/components/src/tabs/shared/tab-list-item.directive.ts +++ b/libs/components/src/tabs/shared/tab-list-item.directive.ts @@ -7,7 +7,10 @@ import { Directive, ElementRef, HostBinding, Input } from "@angular/core"; * Directive used for styling tab header items for both nav links (anchor tags) * and content tabs (button tags) */ -@Directive({ selector: "[bitTabListItem]" }) +@Directive({ + selector: "[bitTabListItem]", + standalone: true, +}) export class TabListItemDirective implements FocusableOption { @Input() active: boolean; @Input() disabled: boolean; diff --git a/libs/components/src/tabs/tab-group/tab-body.component.ts b/libs/components/src/tabs/tab-group/tab-body.component.ts index 7cb6664b7c5..45a6a05e7c2 100644 --- a/libs/components/src/tabs/tab-group/tab-body.component.ts +++ b/libs/components/src/tabs/tab-group/tab-body.component.ts @@ -1,11 +1,13 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { TemplatePortal } from "@angular/cdk/portal"; +import { TemplatePortal, CdkPortalOutlet } from "@angular/cdk/portal"; import { Component, HostBinding, Input } from "@angular/core"; @Component({ selector: "bit-tab-body", templateUrl: "tab-body.component.html", + standalone: true, + imports: [CdkPortalOutlet], }) export class TabBodyComponent { private _firstRender: boolean; diff --git a/libs/components/src/tabs/tab-group/tab-group.component.ts b/libs/components/src/tabs/tab-group/tab-group.component.ts index 7b0cb60bb12..54d00343b38 100644 --- a/libs/components/src/tabs/tab-group/tab-group.component.ts +++ b/libs/components/src/tabs/tab-group/tab-group.component.ts @@ -2,6 +2,7 @@ // @ts-strict-ignore import { FocusKeyManager } from "@angular/cdk/a11y"; import { coerceNumberProperty } from "@angular/cdk/coercion"; +import { CommonModule } from "@angular/common"; import { AfterContentChecked, AfterContentInit, @@ -17,8 +18,11 @@ import { } from "@angular/core"; import { Subject, takeUntil } from "rxjs"; +import { TabHeaderComponent } from "../shared/tab-header.component"; +import { TabListContainerDirective } from "../shared/tab-list-container.directive"; import { TabListItemDirective } from "../shared/tab-list-item.directive"; +import { TabBodyComponent } from "./tab-body.component"; import { TabComponent } from "./tab.component"; /** Used to generate unique ID's for each tab component */ @@ -27,6 +31,14 @@ let nextId = 0; @Component({ selector: "bit-tab-group", templateUrl: "./tab-group.component.html", + standalone: true, + imports: [ + CommonModule, + TabHeaderComponent, + TabListContainerDirective, + TabListItemDirective, + TabBodyComponent, + ], }) export class TabGroupComponent implements AfterContentChecked, AfterContentInit, AfterViewInit, OnDestroy diff --git a/libs/components/src/tabs/tab-group/tab-label.directive.ts b/libs/components/src/tabs/tab-group/tab-label.directive.ts index 45da163631b..9a0e59845a1 100644 --- a/libs/components/src/tabs/tab-group/tab-label.directive.ts +++ b/libs/components/src/tabs/tab-group/tab-label.directive.ts @@ -16,6 +16,7 @@ import { Directive, TemplateRef } from "@angular/core"; */ @Directive({ selector: "[bitTabLabel]", + standalone: true, }) export class TabLabelDirective { constructor(public templateRef: TemplateRef) {} diff --git a/libs/components/src/tabs/tab-group/tab.component.ts b/libs/components/src/tabs/tab-group/tab.component.ts index 260cb0c8193..b2c9b1999bc 100644 --- a/libs/components/src/tabs/tab-group/tab.component.ts +++ b/libs/components/src/tabs/tab-group/tab.component.ts @@ -19,6 +19,7 @@ import { TabLabelDirective } from "./tab-label.directive"; host: { role: "tabpanel", }, + standalone: true, }) export class TabComponent implements OnInit { @Input() disabled = false; diff --git a/libs/components/src/tabs/tab-nav-bar/tab-link.component.ts b/libs/components/src/tabs/tab-nav-bar/tab-link.component.ts index 483aa9600b3..0dac6681475 100644 --- a/libs/components/src/tabs/tab-nav-bar/tab-link.component.ts +++ b/libs/components/src/tabs/tab-nav-bar/tab-link.component.ts @@ -2,7 +2,7 @@ // @ts-strict-ignore import { FocusableOption } from "@angular/cdk/a11y"; import { AfterViewInit, Component, HostListener, Input, OnDestroy, ViewChild } from "@angular/core"; -import { IsActiveMatchOptions, RouterLinkActive } from "@angular/router"; +import { IsActiveMatchOptions, RouterLinkActive, RouterModule } from "@angular/router"; import { Subject, takeUntil } from "rxjs"; import { TabListItemDirective } from "../shared/tab-list-item.directive"; @@ -12,6 +12,8 @@ import { TabNavBarComponent } from "./tab-nav-bar.component"; @Component({ selector: "bit-tab-link", templateUrl: "tab-link.component.html", + standalone: true, + imports: [TabListItemDirective, RouterModule], }) export class TabLinkComponent implements FocusableOption, AfterViewInit, OnDestroy { private destroy$ = new Subject(); diff --git a/libs/components/src/tabs/tab-nav-bar/tab-nav-bar.component.ts b/libs/components/src/tabs/tab-nav-bar/tab-nav-bar.component.ts index 81f7f1d4947..305196a0c69 100644 --- a/libs/components/src/tabs/tab-nav-bar/tab-nav-bar.component.ts +++ b/libs/components/src/tabs/tab-nav-bar/tab-nav-bar.component.ts @@ -10,6 +10,9 @@ import { QueryList, } from "@angular/core"; +import { TabHeaderComponent } from "../shared/tab-header.component"; +import { TabListContainerDirective } from "../shared/tab-list-container.directive"; + import { TabLinkComponent } from "./tab-link.component"; @Component({ @@ -18,6 +21,8 @@ import { TabLinkComponent } from "./tab-link.component"; host: { class: "tw-block", }, + standalone: true, + imports: [TabHeaderComponent, TabListContainerDirective], }) export class TabNavBarComponent implements AfterContentInit { @ContentChildren(forwardRef(() => TabLinkComponent)) tabLabels: QueryList; diff --git a/libs/components/src/tabs/tabs.module.ts b/libs/components/src/tabs/tabs.module.ts index fee1a8a7d08..ef1537db67e 100644 --- a/libs/components/src/tabs/tabs.module.ts +++ b/libs/components/src/tabs/tabs.module.ts @@ -1,11 +1,6 @@ -import { PortalModule } from "@angular/cdk/portal"; import { CommonModule } from "@angular/common"; import { NgModule } from "@angular/core"; -import { RouterModule } from "@angular/router"; -import { TabHeaderComponent } from "./shared/tab-header.component"; -import { TabListContainerDirective } from "./shared/tab-list-container.directive"; -import { TabListItemDirective } from "./shared/tab-list-item.directive"; import { TabBodyComponent } from "./tab-group/tab-body.component"; import { TabGroupComponent } from "./tab-group/tab-group.component"; import { TabLabelDirective } from "./tab-group/tab-label.directive"; @@ -14,7 +9,15 @@ import { TabLinkComponent } from "./tab-nav-bar/tab-link.component"; import { TabNavBarComponent } from "./tab-nav-bar/tab-nav-bar.component"; @NgModule({ - imports: [CommonModule, RouterModule, PortalModule], + imports: [ + CommonModule, + TabGroupComponent, + TabComponent, + TabLabelDirective, + TabNavBarComponent, + TabLinkComponent, + TabBodyComponent, + ], exports: [ TabGroupComponent, TabComponent, @@ -22,16 +25,5 @@ import { TabNavBarComponent } from "./tab-nav-bar/tab-nav-bar.component"; TabNavBarComponent, TabLinkComponent, ], - declarations: [ - TabGroupComponent, - TabComponent, - TabLabelDirective, - TabListContainerDirective, - TabListItemDirective, - TabHeaderComponent, - TabNavBarComponent, - TabLinkComponent, - TabBodyComponent, - ], }) export class TabsModule {} diff --git a/libs/components/src/toast/toast.module.ts b/libs/components/src/toast/toast.module.ts index bf39a0be9ad..bf17fde223f 100644 --- a/libs/components/src/toast/toast.module.ts +++ b/libs/components/src/toast/toast.module.ts @@ -1,13 +1,10 @@ -import { CommonModule } from "@angular/common"; import { ModuleWithProviders, NgModule } from "@angular/core"; import { DefaultNoComponentGlobalConfig, GlobalConfig, TOAST_CONFIG } from "ngx-toastr"; -import { ToastComponent } from "./toast.component"; import { BitwardenToastrComponent } from "./toastr.component"; @NgModule({ - imports: [CommonModule, ToastComponent], - declarations: [BitwardenToastrComponent], + imports: [BitwardenToastrComponent], exports: [BitwardenToastrComponent], }) export class ToastModule { diff --git a/libs/components/src/toast/toastr.component.ts b/libs/components/src/toast/toastr.component.ts index 0656b68d863..24209054948 100644 --- a/libs/components/src/toast/toastr.component.ts +++ b/libs/components/src/toast/toastr.component.ts @@ -2,6 +2,8 @@ import { animate, state, style, transition, trigger } from "@angular/animations" import { Component } from "@angular/core"; import { Toast as BaseToastrComponent } from "ngx-toastr"; +import { ToastComponent } from "./toast.component"; + @Component({ template: ` { private id = nextId++; diff --git a/libs/components/src/toggle-group/toggle-group.module.ts b/libs/components/src/toggle-group/toggle-group.module.ts index fe1ce0ec52f..654149611f0 100644 --- a/libs/components/src/toggle-group/toggle-group.module.ts +++ b/libs/components/src/toggle-group/toggle-group.module.ts @@ -1,14 +1,10 @@ -import { CommonModule } from "@angular/common"; import { NgModule } from "@angular/core"; -import { BadgeModule } from "../badge"; - import { ToggleGroupComponent } from "./toggle-group.component"; import { ToggleComponent } from "./toggle.component"; @NgModule({ - imports: [CommonModule, BadgeModule], + imports: [ToggleGroupComponent, ToggleComponent], exports: [ToggleGroupComponent, ToggleComponent], - declarations: [ToggleGroupComponent, ToggleComponent], }) export class ToggleGroupModule {} diff --git a/libs/components/src/toggle-group/toggle.component.ts b/libs/components/src/toggle-group/toggle.component.ts index c7d9dc5bf38..7bd62056763 100644 --- a/libs/components/src/toggle-group/toggle.component.ts +++ b/libs/components/src/toggle-group/toggle.component.ts @@ -1,5 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore +import { NgClass } from "@angular/common"; import { AfterContentChecked, AfterViewInit, @@ -19,6 +20,8 @@ let nextId = 0; selector: "bit-toggle", templateUrl: "./toggle.component.html", preserveWhitespaces: false, + standalone: true, + imports: [NgClass], }) export class ToggleComponent implements AfterContentChecked, AfterViewInit { id = nextId++; diff --git a/libs/components/src/typography/typography.directive.ts b/libs/components/src/typography/typography.directive.ts index e48ef67001f..36d6b996dbe 100644 --- a/libs/components/src/typography/typography.directive.ts +++ b/libs/components/src/typography/typography.directive.ts @@ -31,6 +31,7 @@ const margins: Record = { @Directive({ selector: "[bitTypography]", + standalone: true, }) export class TypographyDirective { @Input("bitTypography") bitTypography: TypographyType; diff --git a/libs/components/src/typography/typography.module.ts b/libs/components/src/typography/typography.module.ts index 7ee66906360..74d1d4d6e6a 100644 --- a/libs/components/src/typography/typography.module.ts +++ b/libs/components/src/typography/typography.module.ts @@ -1,11 +1,9 @@ -import { CommonModule } from "@angular/common"; import { NgModule } from "@angular/core"; import { TypographyDirective } from "./typography.directive"; @NgModule({ - imports: [CommonModule], + imports: [TypographyDirective], exports: [TypographyDirective], - declarations: [TypographyDirective], }) export class TypographyModule {} From 903b5c8d93bb3d19d5960f0eb60ca20b7d604c05 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Wed, 18 Dec 2024 05:33:14 -0800 Subject: [PATCH 06/54] [PM-16116] Add comment about mv2 routing loop (#12447) * Add comment about mv2 routing loop * Add more details to comment --- apps/browser/src/popup/app-routing.module.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/browser/src/popup/app-routing.module.ts b/apps/browser/src/popup/app-routing.module.ts index 85ae861c9d5..3ec2667cd8c 100644 --- a/apps/browser/src/popup/app-routing.module.ts +++ b/apps/browser/src/popup/app-routing.module.ts @@ -661,6 +661,10 @@ const routes: Routes = [ * This ensures that in a passkey flow the `/fido2?` URL does not get * overwritten in the `BrowserRouterService` by the `/lockV2` route. This way, after * unlocking, the user can be redirected back to the `/fido2?` URL. + * + * Also, this prevents a routing loop when using biometrics to unlock the vault in MV2 (Firefox), + * locking up the browser (https://bitwarden.atlassian.net/browse/PM-16116). This involves the + * `popup-router-cache.service` pushing the `lockV2` route to the history. */ doNotSaveUrl: true, } satisfies ExtensionAnonLayoutWrapperData & RouteDataProperties, From 12b698b11dfd40ad951dc3243e14edefe9cd6a04 Mon Sep 17 00:00:00 2001 From: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com> Date: Wed, 18 Dec 2024 16:31:16 +0100 Subject: [PATCH 07/54] organization status changed code changes (#12249) * organization status changed code changes * Remove the stop so a reconnect can be made --- apps/web/src/app/app.component.ts | 14 ++++++++++++++ .../adjust-payment-dialog.component.ts | 1 - libs/common/src/enums/notification-type.enum.ts | 1 + .../src/models/response/notification.response.ts | 14 ++++++++++++++ libs/common/src/services/notifications.service.ts | 5 +++++ 5 files changed, 34 insertions(+), 1 deletion(-) diff --git a/apps/web/src/app/app.component.ts b/apps/web/src/app/app.component.ts index a1ebd141e1d..1075655af9e 100644 --- a/apps/web/src/app/app.component.ts +++ b/apps/web/src/app/app.component.ts @@ -243,6 +243,20 @@ export class AppComponent implements OnDestroy, OnInit { // eslint-disable-next-line @typescript-eslint/no-floating-promises this.router.navigate(["/remove-password"]); break; + case "syncOrganizationStatusChanged": { + const { organizationId, enabled } = message; + const organizations = await firstValueFrom(this.organizationService.organizations$); + const organization = organizations.find((org) => org.id === organizationId); + + if (organization) { + const updatedOrganization = { + ...organization, + enabled: enabled, + }; + await this.organizationService.upsert(updatedOrganization); + } + break; + } default: break; } diff --git a/apps/web/src/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog.component.ts b/apps/web/src/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog.component.ts index 1cc9f5b4e02..b8826f0626a 100644 --- a/apps/web/src/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog.component.ts +++ b/apps/web/src/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog.component.ts @@ -76,7 +76,6 @@ export class AdjustPaymentDialogComponent { } }); await response; - await new Promise((resolve) => setTimeout(resolve, 10000)); this.toastService.showToast({ variant: "success", title: null, diff --git a/libs/common/src/enums/notification-type.enum.ts b/libs/common/src/enums/notification-type.enum.ts index c5853cbe2c0..69cbdff9dd2 100644 --- a/libs/common/src/enums/notification-type.enum.ts +++ b/libs/common/src/enums/notification-type.enum.ts @@ -22,4 +22,5 @@ export enum NotificationType { AuthRequestResponse = 16, SyncOrganizations = 17, + SyncOrganizationStatusChanged = 18, } diff --git a/libs/common/src/models/response/notification.response.ts b/libs/common/src/models/response/notification.response.ts index af79b883f08..473e6fc1d10 100644 --- a/libs/common/src/models/response/notification.response.ts +++ b/libs/common/src/models/response/notification.response.ts @@ -42,6 +42,9 @@ export class NotificationResponse extends BaseResponse { case NotificationType.AuthRequestResponse: this.payload = new AuthRequestPushNotification(payload); break; + case NotificationType.SyncOrganizationStatusChanged: + this.payload = new OrganizationStatusPushNotification(payload); + break; default: break; } @@ -112,3 +115,14 @@ export class AuthRequestPushNotification extends BaseResponse { this.userId = this.getResponseProperty("UserId"); } } + +export class OrganizationStatusPushNotification extends BaseResponse { + organizationId: string; + enabled: boolean; + + constructor(response: any) { + super(response); + this.organizationId = this.getResponseProperty("OrganizationId"); + this.enabled = this.getResponseProperty("Enabled"); + } +} diff --git a/libs/common/src/services/notifications.service.ts b/libs/common/src/services/notifications.service.ts index e240886cf29..6f7c5c9f262 100644 --- a/libs/common/src/services/notifications.service.ts +++ b/libs/common/src/services/notifications.service.ts @@ -218,6 +218,11 @@ export class NotificationsService implements NotificationsServiceAbstraction { }); } break; + case NotificationType.SyncOrganizationStatusChanged: + if (isAuthenticated) { + await this.syncService.fullSync(true); + } + break; default: break; } From 12eb77fd45c18a078454b62428f42c8be2947fca Mon Sep 17 00:00:00 2001 From: Daniel James Smith <2670567+djsmith85@users.noreply.github.com> Date: Wed, 18 Dec 2024 17:13:13 +0100 Subject: [PATCH 08/54] [PM-13455] Wire up results from RiskInsightsReportService (#12206) * Risk insights aggregation in a new service. Initial PR. * Wire up results from RiskInsightsReportService * Ignoring all non-login items and refactoring into a method * Cleaning up the documentation a little * logic for generating the report summary * application summary to list at risk applications not passwords * Adding more documentation and moving types to it's own file * Awaiting the raw data report and adding the start of the test file * Extend access-intelligence.module to provide needed services * Register access-intelligence.module with bit-web AppModule * Use provided RiskInsightsService instead of new'ing one in the component * Removing the injectable attribute from RiskInsightsReportService * Fix tests * Adding more test cases * Removing unnecessary file * Test cases update * Fixing memeber details test to have new member * Fixing password health tests * Moving to observables * removing commented code * commented code * Switching from ternary to if/else * nullable types * one more nullable type * Adding the fixme for strict types * moving the fixme * No need to access the password use map and switching to the observable * PM-13455 fixes to unit tests --------- Co-authored-by: Tom Co-authored-by: Daniel James Smith Co-authored-by: Tom <144813356+ttalty@users.noreply.github.com> Co-authored-by: voommen-livefront --- .../risk-insights-report.service.spec.ts | 56 ++++++++----------- .../services/risk-insights-report.service.ts | 5 +- .../bit-web/src/app/app.module.ts | 2 + .../access-intelligence.module.ts | 24 ++++++++ .../password-health.component.html | 10 ++-- .../password-health.component.spec.ts | 22 +------- .../password-health.component.ts | 41 +++----------- 7 files changed, 64 insertions(+), 96 deletions(-) diff --git a/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/risk-insights-report.service.spec.ts b/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/risk-insights-report.service.spec.ts index 7505b692a8f..705eb1231a9 100644 --- a/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/risk-insights-report.service.spec.ts +++ b/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/risk-insights-report.service.spec.ts @@ -1,5 +1,6 @@ -import { TestBed } from "@angular/core/testing"; +import { mock } from "jest-mock-extended"; import { firstValueFrom } from "rxjs"; +import { ZXCVBNResult } from "zxcvbn"; import { AuditService } from "@bitwarden/common/abstractions/audit.service"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; @@ -12,42 +13,31 @@ import { RiskInsightsReportService } from "./risk-insights-report.service"; describe("RiskInsightsReportService", () => { let service: RiskInsightsReportService; + const pwdStrengthService = mock(); + const auditService = mock(); + const cipherService = mock(); + const memberCipherDetailsService = mock(); beforeEach(() => { - TestBed.configureTestingModule({ - providers: [ - RiskInsightsReportService, - { - provide: PasswordStrengthServiceAbstraction, - useValue: { - getPasswordStrength: (password: string) => { - const score = password.length < 4 ? 1 : 4; - return { score }; - }, - }, - }, - { - provide: AuditService, - useValue: { - passwordLeaked: (password: string) => Promise.resolve(password === "123" ? 100 : 0), - }, - }, - { - provide: CipherService, - useValue: { - getAllFromApiForOrganization: jest.fn().mockResolvedValue(mockCiphers), - }, - }, - { - provide: MemberCipherDetailsApiService, - useValue: { - getMemberCipherDetails: jest.fn().mockResolvedValue(mockMemberCipherDetails), - }, - }, - ], + pwdStrengthService.getPasswordStrength.mockImplementation((password: string) => { + const score = password.length < 4 ? 1 : 4; + return { score } as ZXCVBNResult; }); - service = TestBed.inject(RiskInsightsReportService); + auditService.passwordLeaked.mockImplementation((password: string) => + Promise.resolve(password === "123" ? 100 : 0), + ); + + cipherService.getAllFromApiForOrganization.mockResolvedValue(mockCiphers); + + memberCipherDetailsService.getMemberCipherDetails.mockResolvedValue(mockMemberCipherDetails); + + service = new RiskInsightsReportService( + pwdStrengthService, + auditService, + cipherService, + memberCipherDetailsService, + ); }); it("should generate the raw data report correctly", async () => { diff --git a/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/risk-insights-report.service.ts b/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/risk-insights-report.service.ts index f4b30735584..45746cd2f5a 100644 --- a/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/risk-insights-report.service.ts +++ b/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/risk-insights-report.service.ts @@ -1,7 +1,5 @@ -// FIXME: Update this file to be type safe and remove this and next line +// FIXME: Update this file to be type safe // @ts-strict-ignore - -import { Injectable } from "@angular/core"; import { concatMap, first, from, map, Observable, zip } from "rxjs"; import { AuditService } from "@bitwarden/common/abstractions/audit.service"; @@ -24,7 +22,6 @@ import { import { MemberCipherDetailsApiService } from "./member-cipher-details-api.service"; -@Injectable() export class RiskInsightsReportService { constructor( private passwordStrengthService: PasswordStrengthServiceAbstraction, diff --git a/bitwarden_license/bit-web/src/app/app.module.ts b/bitwarden_license/bit-web/src/app/app.module.ts index 4db1e2f5e20..fd1a3b0b84c 100644 --- a/bitwarden_license/bit-web/src/app/app.module.ts +++ b/bitwarden_license/bit-web/src/app/app.module.ts @@ -20,6 +20,7 @@ import { MaximumVaultTimeoutPolicyComponent } from "./admin-console/policies/max import { AppRoutingModule } from "./app-routing.module"; import { AppComponent } from "./app.component"; import { FreeFamiliesSponsorshipPolicyComponent } from "./billing/policies/free-families-sponsorship.component"; +import { AccessIntelligenceModule } from "./tools/access-intelligence/access-intelligence.module"; /** * This is the AppModule for the commercial version of Bitwarden. @@ -41,6 +42,7 @@ import { FreeFamiliesSponsorshipPolicyComponent } from "./billing/policies/free- AppRoutingModule, OssRoutingModule, OrganizationsModule, // Must be after OssRoutingModule for competing routes to resolve properly + AccessIntelligenceModule, RouterModule, WildcardRoutingModule, // Needs to be last to catch all non-existing routes ], diff --git a/bitwarden_license/bit-web/src/app/tools/access-intelligence/access-intelligence.module.ts b/bitwarden_license/bit-web/src/app/tools/access-intelligence/access-intelligence.module.ts index 3f177119aa8..87b75dee70c 100644 --- a/bitwarden_license/bit-web/src/app/tools/access-intelligence/access-intelligence.module.ts +++ b/bitwarden_license/bit-web/src/app/tools/access-intelligence/access-intelligence.module.ts @@ -1,9 +1,33 @@ import { NgModule } from "@angular/core"; +import { + MemberCipherDetailsApiService, + RiskInsightsReportService, +} from "@bitwarden/bit-common/tools/reports/risk-insights/services"; +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { AuditService } from "@bitwarden/common/abstractions/audit.service"; +import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength/password-strength.service.abstraction"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; + import { AccessIntelligenceRoutingModule } from "./access-intelligence-routing.module"; import { RiskInsightsComponent } from "./risk-insights.component"; @NgModule({ imports: [RiskInsightsComponent, AccessIntelligenceRoutingModule], + providers: [ + { + provide: MemberCipherDetailsApiService, + deps: [ApiService], + }, + { + provide: RiskInsightsReportService, + deps: [ + PasswordStrengthServiceAbstraction, + AuditService, + CipherService, + MemberCipherDetailsApiService, + ], + }, + ], }) export class AccessIntelligenceModule {} diff --git a/bitwarden_license/bit-web/src/app/tools/access-intelligence/password-health.component.html b/bitwarden_license/bit-web/src/app/tools/access-intelligence/password-health.component.html index 5b1fe4610d9..aeaa9f33197 100644 --- a/bitwarden_license/bit-web/src/app/tools/access-intelligence/password-health.component.html +++ b/bitwarden_license/bit-web/src/app/tools/access-intelligence/password-health.component.html @@ -34,10 +34,10 @@ - {{ passwordStrengthMap.get(r.id)[0] | i18n }} + {{ r.weakPasswordDetail?.detailValue.label | i18n }} @@ -46,8 +46,8 @@ - - {{ "exposedXTimes" | i18n: exposedPasswordMap.get(r.id) }} + + {{ "exposedXTimes" | i18n: r.exposedPasswordDetail?.exposedXTimes }} diff --git a/bitwarden_license/bit-web/src/app/tools/access-intelligence/password-health.component.spec.ts b/bitwarden_license/bit-web/src/app/tools/access-intelligence/password-health.component.spec.ts index 1f1756731f6..4329cfbde14 100644 --- a/bitwarden_license/bit-web/src/app/tools/access-intelligence/password-health.component.spec.ts +++ b/bitwarden_license/bit-web/src/app/tools/access-intelligence/password-health.component.spec.ts @@ -3,15 +3,8 @@ import { ActivatedRoute, convertToParamMap } from "@angular/router"; import { mock } from "jest-mock-extended"; import { of } from "rxjs"; -import { - MemberCipherDetailsApiService, - PasswordHealthService, -} from "@bitwarden/bit-common/tools/reports/risk-insights"; -import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { AuditService } from "@bitwarden/common/abstractions/audit.service"; +import { RiskInsightsReportService } from "@bitwarden/bit-common/tools/reports/risk-insights"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; -import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { TableModule } from "@bitwarden/components"; import { LooseComponentsModule } from "@bitwarden/web-vault/app/shared"; import { PipesModule } from "@bitwarden/web-vault/app/vault/individual-vault/pipes/pipes.module"; @@ -28,19 +21,8 @@ describe("PasswordHealthComponent", () => { imports: [PasswordHealthComponent, PipesModule, TableModule, LooseComponentsModule], declarations: [], providers: [ - { provide: CipherService, useValue: mock() }, + { provide: RiskInsightsReportService, useValue: mock() }, { provide: I18nService, useValue: mock() }, - { provide: AuditService, useValue: mock() }, - { provide: ApiService, useValue: mock() }, - { provide: MemberCipherDetailsApiService, useValue: mock() }, - { - provide: PasswordStrengthServiceAbstraction, - useValue: mock(), - }, - { - provide: PasswordHealthService, - useValue: mock(), - }, { provide: ActivatedRoute, useValue: { diff --git a/bitwarden_license/bit-web/src/app/tools/access-intelligence/password-health.component.ts b/bitwarden_license/bit-web/src/app/tools/access-intelligence/password-health.component.ts index 06f7de439cf..62d543a080d 100644 --- a/bitwarden_license/bit-web/src/app/tools/access-intelligence/password-health.component.ts +++ b/bitwarden_license/bit-web/src/app/tools/access-intelligence/password-health.component.ts @@ -4,21 +4,14 @@ import { CommonModule } from "@angular/common"; import { Component, DestroyRef, inject, OnInit } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { ActivatedRoute } from "@angular/router"; -import { map } from "rxjs"; +import { firstValueFrom, map } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; -import { - MemberCipherDetailsApiService, - PasswordHealthService, -} from "@bitwarden/bit-common/tools/reports/risk-insights"; -import { AuditService } from "@bitwarden/common/abstractions/audit.service"; +import { RiskInsightsReportService } from "@bitwarden/bit-common/tools/reports/risk-insights"; +import { CipherHealthReportDetail } from "@bitwarden/bit-common/tools/reports/risk-insights/models/password-health"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; -import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; -import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { BadgeModule, - BadgeVariant, ContainerComponent, TableDataSource, TableModule, @@ -41,28 +34,19 @@ import { PipesModule } from "@bitwarden/web-vault/app/vault/individual-vault/pip HeaderModule, TableModule, ], - providers: [PasswordHealthService, MemberCipherDetailsApiService], }) export class PasswordHealthComponent implements OnInit { - passwordStrengthMap = new Map(); - passwordUseMap = new Map(); - - exposedPasswordMap = new Map(); - - dataSource = new TableDataSource(); + dataSource = new TableDataSource(); loading = true; private destroyRef = inject(DestroyRef); constructor( - protected cipherService: CipherService, - protected passwordStrengthService: PasswordStrengthServiceAbstraction, - protected auditService: AuditService, + protected riskInsightsReportService: RiskInsightsReportService, protected i18nService: I18nService, protected activatedRoute: ActivatedRoute, - protected memberCipherDetailsApiService: MemberCipherDetailsApiService, ) {} ngOnInit() { @@ -78,20 +62,9 @@ export class PasswordHealthComponent implements OnInit { } async setCiphers(organizationId: string) { - const passwordHealthService = new PasswordHealthService( - this.passwordStrengthService, - this.auditService, - this.cipherService, - this.memberCipherDetailsApiService, - organizationId, + this.dataSource.data = await firstValueFrom( + this.riskInsightsReportService.generateRawDataReport$(organizationId), ); - - await passwordHealthService.generateReport(); - - this.dataSource.data = passwordHealthService.reportCiphers; - this.exposedPasswordMap = passwordHealthService.exposedPasswordMap; - this.passwordStrengthMap = passwordHealthService.passwordStrengthMap; - this.passwordUseMap = passwordHealthService.passwordUseMap; this.loading = false; } } From fc37d6df6b888dc649023a08ddd02de400adf66a Mon Sep 17 00:00:00 2001 From: Github Actions Date: Wed, 18 Dec 2024 16:41:40 +0000 Subject: [PATCH 09/54] Bumped client version(s) --- apps/browser/package.json | 2 +- apps/browser/src/manifest.json | 2 +- apps/browser/src/manifest.v3.json | 2 +- package-lock.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/browser/package.json b/apps/browser/package.json index 4610bcb2df8..2b3edd96b6e 100644 --- a/apps/browser/package.json +++ b/apps/browser/package.json @@ -1,6 +1,6 @@ { "name": "@bitwarden/browser", - "version": "2024.12.2", + "version": "2024.12.3", "scripts": { "build": "npm run build:chrome", "build:chrome": "cross-env BROWSER=chrome MANIFEST_VERSION=3 webpack", diff --git a/apps/browser/src/manifest.json b/apps/browser/src/manifest.json index ab14819e807..32fa5135226 100644 --- a/apps/browser/src/manifest.json +++ b/apps/browser/src/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 2, "name": "__MSG_extName__", "short_name": "__MSG_appName__", - "version": "2024.12.2", + "version": "2024.12.3", "description": "__MSG_extDesc__", "default_locale": "en", "author": "Bitwarden Inc.", diff --git a/apps/browser/src/manifest.v3.json b/apps/browser/src/manifest.v3.json index 0a6687b4fb5..87a08bc89e3 100644 --- a/apps/browser/src/manifest.v3.json +++ b/apps/browser/src/manifest.v3.json @@ -3,7 +3,7 @@ "minimum_chrome_version": "102.0", "name": "__MSG_extName__", "short_name": "__MSG_appName__", - "version": "2024.12.2", + "version": "2024.12.3", "description": "__MSG_extDesc__", "default_locale": "en", "author": "Bitwarden Inc.", diff --git a/package-lock.json b/package-lock.json index 997cad9551e..0d743316bd4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -190,7 +190,7 @@ }, "apps/browser": { "name": "@bitwarden/browser", - "version": "2024.12.2" + "version": "2024.12.3" }, "apps/cli": { "name": "@bitwarden/cli", From da9e12dae69c53da61611532ede2a5feb20df015 Mon Sep 17 00:00:00 2001 From: Jordan Aasen <166539328+jaasen-livefront@users.noreply.github.com> Date: Wed, 18 Dec 2024 10:03:15 -0800 Subject: [PATCH 10/54] [PM-15498] - add risk insight data service (#12361) * Adding more test cases * Removing unnecessary file * Test cases update * Adding the fixme for strict types * moving the fixme * add risk insight data service and wire up components to it * hook up all applications to risk insights report service. add loading state * link up remaining props to risk insight report * wire up children to risk insight data service * add missing copy. remove loading state from risk insights * fix types * fix DI issue * remove @Injectable from RiskInsightsDataService --------- Co-authored-by: Tom Co-authored-by: Tom <144813356+ttalty@users.noreply.github.com> --- .../layouts/organization-layout.component.ts | 1 - apps/web/src/locales/en/messages.json | 3 + .../risk-insights/models/password-health.ts | 2 +- .../reports/risk-insights/services/index.ts | 1 + .../services/risk-insights-data.service.ts | 60 ++++++++++ .../services/risk-insights-report.service.ts | 1 + .../access-intelligence.module.ts | 5 + .../all-applications.component.html | 49 ++++---- .../all-applications.component.ts | 101 ++++++++++------- .../risk-insights-loading.component.html | 8 ++ .../risk-insights-loading.component.ts | 14 +++ .../risk-insights.component.html | 105 ++++++++++-------- .../risk-insights.component.ts | 105 ++++++++++++------ 13 files changed, 303 insertions(+), 152 deletions(-) create mode 100644 bitwarden_license/bit-common/src/tools/reports/risk-insights/services/risk-insights-data.service.ts create mode 100644 bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights-loading.component.html create mode 100644 bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights-loading.component.ts diff --git a/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.ts b/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.ts index 8c4f5ce8c46..0b024817edc 100644 --- a/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.ts +++ b/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.ts @@ -57,7 +57,6 @@ export class OrganizationLayoutComponent implements OnInit { showPaymentAndHistory$: Observable; hideNewOrgButton$: Observable; organizationIsUnmanaged$: Observable; - isAccessIntelligenceFeatureEnabled = false; enterpriseOrganization$: Observable; constructor( diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 36143682fa2..abd5779339f 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -3882,6 +3882,9 @@ "updateBrowser": { "message": "Update browser" }, + "generatingRiskInsights": { + "message": "Generating your risk insights..." + }, "updateBrowserDesc": { "message": "You are using an unsupported web browser. The web vault may not function properly." }, diff --git a/bitwarden_license/bit-common/src/tools/reports/risk-insights/models/password-health.ts b/bitwarden_license/bit-common/src/tools/reports/risk-insights/models/password-health.ts index 427cb06d9e0..b8d5852088a 100644 --- a/bitwarden_license/bit-common/src/tools/reports/risk-insights/models/password-health.ts +++ b/bitwarden_license/bit-common/src/tools/reports/risk-insights/models/password-health.ts @@ -25,7 +25,7 @@ export type ApplicationHealthReportDetail = { passwordCount: number; atRiskPasswordCount: number; memberCount: number; - + atRiskMemberCount: number; memberDetails: MemberDetailsFlat[]; atRiskMemberDetails: MemberDetailsFlat[]; }; diff --git a/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/index.ts b/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/index.ts index e930c7666e8..a8e62437b9d 100644 --- a/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/index.ts +++ b/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/index.ts @@ -1,3 +1,4 @@ export * from "./member-cipher-details-api.service"; export * from "./password-health.service"; export * from "./risk-insights-report.service"; +export * from "./risk-insights-data.service"; diff --git a/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/risk-insights-data.service.ts b/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/risk-insights-data.service.ts new file mode 100644 index 00000000000..42bab69fca4 --- /dev/null +++ b/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/risk-insights-data.service.ts @@ -0,0 +1,60 @@ +import { BehaviorSubject } from "rxjs"; +import { finalize } from "rxjs/operators"; + +import { ApplicationHealthReportDetail } from "../models/password-health"; + +import { RiskInsightsReportService } from "./risk-insights-report.service"; + +export class RiskInsightsDataService { + private applicationsSubject = new BehaviorSubject(null); + + applications$ = this.applicationsSubject.asObservable(); + + private isLoadingSubject = new BehaviorSubject(false); + isLoading$ = this.isLoadingSubject.asObservable(); + + private isRefreshingSubject = new BehaviorSubject(false); + isRefreshing$ = this.isRefreshingSubject.asObservable(); + + private errorSubject = new BehaviorSubject(null); + error$ = this.errorSubject.asObservable(); + + private dataLastUpdatedSubject = new BehaviorSubject(null); + dataLastUpdated$ = this.dataLastUpdatedSubject.asObservable(); + + constructor(private reportService: RiskInsightsReportService) {} + + /** + * Fetches the applications report and updates the applicationsSubject. + * @param organizationId The ID of the organization. + */ + fetchApplicationsReport(organizationId: string, isRefresh?: boolean): void { + if (isRefresh) { + this.isRefreshingSubject.next(true); + } else { + this.isLoadingSubject.next(true); + } + this.reportService + .generateApplicationsReport$(organizationId) + .pipe( + finalize(() => { + this.isLoadingSubject.next(false); + this.isRefreshingSubject.next(false); + this.dataLastUpdatedSubject.next(new Date()); + }), + ) + .subscribe({ + next: (reports: ApplicationHealthReportDetail[]) => { + this.applicationsSubject.next(reports); + this.errorSubject.next(null); + }, + error: () => { + this.applicationsSubject.next([]); + }, + }); + } + + refreshApplicationsReport(organizationId: string): void { + this.fetchApplicationsReport(organizationId, true); + } +} diff --git a/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/risk-insights-report.service.ts b/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/risk-insights-report.service.ts index 45746cd2f5a..c5e9e3625de 100644 --- a/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/risk-insights-report.service.ts +++ b/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/risk-insights-report.service.ts @@ -287,6 +287,7 @@ export class RiskInsightsReportService { : newUriDetail.cipherMembers, atRiskMemberDetails: existingUriDetail ? existingUriDetail.atRiskMemberDetails : [], atRiskPasswordCount: existingUriDetail ? existingUriDetail.atRiskPasswordCount : 0, + atRiskMemberCount: existingUriDetail ? existingUriDetail.atRiskMemberDetails.length : 0, } as ApplicationHealthReportDetail; if (isAtRisk) { diff --git a/bitwarden_license/bit-web/src/app/tools/access-intelligence/access-intelligence.module.ts b/bitwarden_license/bit-web/src/app/tools/access-intelligence/access-intelligence.module.ts index 87b75dee70c..2db7af4bb46 100644 --- a/bitwarden_license/bit-web/src/app/tools/access-intelligence/access-intelligence.module.ts +++ b/bitwarden_license/bit-web/src/app/tools/access-intelligence/access-intelligence.module.ts @@ -2,6 +2,7 @@ import { NgModule } from "@angular/core"; import { MemberCipherDetailsApiService, + RiskInsightsDataService, RiskInsightsReportService, } from "@bitwarden/bit-common/tools/reports/risk-insights/services"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; @@ -28,6 +29,10 @@ import { RiskInsightsComponent } from "./risk-insights.component"; MemberCipherDetailsApiService, ], }, + { + provide: RiskInsightsDataService, + deps: [RiskInsightsReportService], + }, ], }) export class AccessIntelligenceModule {} diff --git a/bitwarden_license/bit-web/src/app/tools/access-intelligence/all-applications.component.html b/bitwarden_license/bit-web/src/app/tools/access-intelligence/all-applications.component.html index 4ed31adea78..ea1a4f9db31 100644 --- a/bitwarden_license/bit-web/src/app/tools/access-intelligence/all-applications.component.html +++ b/bitwarden_license/bit-web/src/app/tools/access-intelligence/all-applications.component.html @@ -1,16 +1,11 @@ -
- - {{ "loading" | i18n }} +
+
-
+

- {{ "noAppsInOrgTitle" | i18n: organization.name }} + {{ "noAppsInOrgTitle" | i18n: organization?.name }}

@@ -28,21 +23,21 @@
-
+

{{ "allApplications" | i18n }}

@@ -57,7 +52,7 @@ type="button" buttonType="secondary" bitButton - *ngIf="isCritialAppsFeatureEnabled" + *ngIf="isCriticalAppsFeatureEnabled" [disabled]="!selectedIds.size" [loading]="markingAsCritical" (click)="markAppsAsCritical()" @@ -69,17 +64,17 @@ - - {{ "application" | i18n }} - {{ "atRiskPasswords" | i18n }} - {{ "totalPasswords" | i18n }} - {{ "atRiskMembers" | i18n }} - {{ "totalMembers" | i18n }} + + {{ "application" | i18n }} + {{ "atRiskPasswords" | i18n }} + {{ "totalPasswords" | i18n }} + {{ "atRiskMembers" | i18n }} + {{ "totalMembers" | i18n }} - + - {{ r.name }} + {{ r.applicationName }} - {{ r.atRiskPasswords }} + {{ r.atRiskPasswordCount }} - {{ r.totalPasswords }} + {{ r.passwordCount }} - {{ r.atRiskMembers }} + {{ r.atRiskMemberCount }} - {{ r.totalMembers }} + {{ r.memberCount }} diff --git a/bitwarden_license/bit-web/src/app/tools/access-intelligence/all-applications.component.ts b/bitwarden_license/bit-web/src/app/tools/access-intelligence/all-applications.component.ts index 6ee2ecf1690..f4d3656071d 100644 --- a/bitwarden_license/bit-web/src/app/tools/access-intelligence/all-applications.component.ts +++ b/bitwarden_license/bit-web/src/app/tools/access-intelligence/all-applications.component.ts @@ -1,20 +1,23 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { Component, DestroyRef, inject, OnInit } from "@angular/core"; +import { Component, DestroyRef, OnDestroy, OnInit, inject } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { FormControl } from "@angular/forms"; import { ActivatedRoute } from "@angular/router"; -import { debounceTime, firstValueFrom, map } from "rxjs"; +import { debounceTime, map, Observable, of, Subscription } from "rxjs"; -import { AuditService } from "@bitwarden/common/abstractions/audit.service"; +import { + RiskInsightsDataService, + RiskInsightsReportService, +} from "@bitwarden/bit-common/tools/reports/risk-insights"; +import { + ApplicationHealthReportDetail, + ApplicationHealthReportSummary, +} from "@bitwarden/bit-common/tools/reports/risk-insights/models/password-health"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; -import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { Icons, NoItemsModule, @@ -27,60 +30,76 @@ import { HeaderModule } from "@bitwarden/web-vault/app/layouts/header/header.mod import { SharedModule } from "@bitwarden/web-vault/app/shared"; import { PipesModule } from "@bitwarden/web-vault/app/vault/individual-vault/pipes/pipes.module"; -import { applicationTableMockData } from "./application-table.mock"; +import { ApplicationsLoadingComponent } from "./risk-insights-loading.component"; @Component({ standalone: true, selector: "tools-all-applications", templateUrl: "./all-applications.component.html", - imports: [HeaderModule, CardComponent, SearchModule, PipesModule, NoItemsModule, SharedModule], + imports: [ + ApplicationsLoadingComponent, + HeaderModule, + CardComponent, + SearchModule, + PipesModule, + NoItemsModule, + SharedModule, + ], }) -export class AllApplicationsComponent implements OnInit { - protected dataSource = new TableDataSource(); +export class AllApplicationsComponent implements OnInit, OnDestroy { + protected dataSource = new TableDataSource(); protected selectedIds: Set = new Set(); protected searchControl = new FormControl("", { nonNullable: true }); - private destroyRef = inject(DestroyRef); - protected loading = false; - protected organization: Organization; + protected loading = true; + protected organization = {} as Organization; noItemsIcon = Icons.Security; protected markingAsCritical = false; - isCritialAppsFeatureEnabled = false; + protected applicationSummary = {} as ApplicationHealthReportSummary; + private subscription = new Subscription(); - // MOCK DATA - protected mockData = applicationTableMockData; - protected mockAtRiskMembersCount = 0; - protected mockAtRiskAppsCount = 0; - protected mockTotalMembersCount = 0; - protected mockTotalAppsCount = 0; + destroyRef = inject(DestroyRef); + isLoading$: Observable = of(false); + isCriticalAppsFeatureEnabled = false; async ngOnInit() { - this.activatedRoute.paramMap - .pipe( - takeUntilDestroyed(this.destroyRef), - map(async (params) => { - const organizationId = params.get("organizationId"); - this.organization = await firstValueFrom(this.organizationService.get$(organizationId)); - // TODO: use organizationId to fetch data - }), - ) - .subscribe(); - - this.isCritialAppsFeatureEnabled = await this.configService.getFeatureFlag( + this.isCriticalAppsFeatureEnabled = await this.configService.getFeatureFlag( FeatureFlag.CriticalApps, ); + + const organizationId = this.activatedRoute.snapshot.paramMap.get("organizationId"); + + if (organizationId) { + this.organization = await this.organizationService.get(organizationId); + this.subscription = this.dataService.applications$ + .pipe( + map((applications) => { + if (applications) { + this.dataSource.data = applications; + this.applicationSummary = + this.reportService.generateApplicationsSummary(applications); + } + }), + takeUntilDestroyed(this.destroyRef), + ) + .subscribe(); + this.isLoading$ = this.dataService.isLoading$; + } + } + + ngOnDestroy(): void { + this.subscription?.unsubscribe(); } constructor( protected cipherService: CipherService, - protected passwordStrengthService: PasswordStrengthServiceAbstraction, - protected auditService: AuditService, protected i18nService: I18nService, protected activatedRoute: ActivatedRoute, protected toastService: ToastService, - protected organizationService: OrganizationService, protected configService: ConfigService, + protected dataService: RiskInsightsDataService, + protected organizationService: OrganizationService, + protected reportService: RiskInsightsReportService, ) { - this.dataSource.data = applicationTableMockData; this.searchControl.valueChanges .pipe(debounceTime(200), takeUntilDestroyed()) .subscribe((v) => (this.dataSource.filter = v)); @@ -90,7 +109,7 @@ export class AllApplicationsComponent implements OnInit { // TODO: implement this.toastService.showToast({ variant: "warning", - title: null, + title: "", message: "Not yet implemented", }); }; @@ -103,7 +122,7 @@ export class AllApplicationsComponent implements OnInit { this.selectedIds.clear(); this.toastService.showToast({ variant: "success", - title: null, + title: "", message: this.i18nService.t("appsMarkedAsCritical"), }); resolve(true); @@ -112,8 +131,8 @@ export class AllApplicationsComponent implements OnInit { }); }; - trackByFunction(_: number, item: CipherView) { - return item.id; + trackByFunction(_: number, item: ApplicationHealthReportDetail) { + return item.applicationName; } onCheckboxChange(id: number, event: Event) { diff --git a/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights-loading.component.html b/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights-loading.component.html new file mode 100644 index 00000000000..d6f945bfb92 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights-loading.component.html @@ -0,0 +1,8 @@ +
+ +

{{ "generatingRiskInsights" | i18n }}

+
diff --git a/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights-loading.component.ts b/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights-loading.component.ts new file mode 100644 index 00000000000..1cafa62c608 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights-loading.component.ts @@ -0,0 +1,14 @@ +import { CommonModule } from "@angular/common"; +import { Component } from "@angular/core"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; + +@Component({ + selector: "tools-risk-insights-loading", + standalone: true, + imports: [CommonModule, JslibModule], + templateUrl: "./risk-insights-loading.component.html", +}) +export class ApplicationsLoadingComponent { + constructor() {} +} diff --git a/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights.component.html b/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights.component.html index 6df47e3c46f..e0618c525a7 100644 --- a/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights.component.html +++ b/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights.component.html @@ -1,49 +1,58 @@ -
{{ "accessIntelligence" | i18n }}
-

{{ "riskInsights" | i18n }}

-
- {{ "reviewAtRiskPasswords" | i18n }} -  {{ "learnMore" | i18n }} -
-
- - {{ - "dataLastUpdated" | i18n: (dataLastUpdated | date: "MMMM d, y 'at' h:mm a") - }} - +
{{ "accessIntelligence" | i18n }}
+

{{ "riskInsights" | i18n }}

+
+
- {{ "refresh" | i18n }} - -
- - - - - - - - {{ "criticalApplicationsWithCount" | i18n: criticalApps.length }} - - - - - - - - - - - - - - + + {{ + "dataLastUpdated" | i18n: (dataLastUpdated$ | async | date: "MMMM d, y 'at' h:mm a") + }} + + + {{ "refresh" | i18n }} + + + + + +
+ + + + + + + + {{ "criticalApplicationsWithCount" | i18n: criticalAppsCount }} + + + + + + + + + + + + + + diff --git a/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights.component.ts b/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights.component.ts index 5ea39bd0513..1a90e18f0df 100644 --- a/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights.component.ts +++ b/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights.component.ts @@ -1,11 +1,13 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { CommonModule } from "@angular/common"; -import { Component, OnInit } from "@angular/core"; +import { Component, DestroyRef, OnInit, inject } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { ActivatedRoute, Router } from "@angular/router"; +import { Observable, EMPTY } from "rxjs"; +import { map, switchMap } from "rxjs/operators"; import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { RiskInsightsDataService } from "@bitwarden/bit-common/tools/reports/risk-insights"; +import { ApplicationHealthReportDetail } from "@bitwarden/bit-common/tools/reports/risk-insights/models/password-health"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { AsyncActionsModule, ButtonModule, TabsModule } from "@bitwarden/components"; @@ -43,45 +45,80 @@ export enum RiskInsightsTabType { ], }) export class RiskInsightsComponent implements OnInit { - tabIndex: RiskInsightsTabType; - dataLastUpdated = new Date(); - isCritialAppsFeatureEnabled = false; + tabIndex: RiskInsightsTabType = RiskInsightsTabType.AllApps; - apps: any[] = []; - criticalApps: any[] = []; - notifiedMembers: any[] = []; + dataLastUpdated: Date = new Date(); - async refreshData() { - // TODO: Implement - return new Promise((resolve) => - setTimeout(() => { - this.dataLastUpdated = new Date(); - resolve(true); - }, 1000), - ); + isCriticalAppsFeatureEnabled: boolean = false; + + appsCount: number = 0; + criticalAppsCount: number = 0; + notifiedMembersCount: number = 0; + + private organizationId: string | null = null; + private destroyRef = inject(DestroyRef); + isLoading$: Observable = new Observable(); + isRefreshing$: Observable = new Observable(); + dataLastUpdated$: Observable = new Observable(); + refetching: boolean = false; + + constructor( + private route: ActivatedRoute, + private router: Router, + private configService: ConfigService, + private dataService: RiskInsightsDataService, + ) { + this.route.queryParams.pipe(takeUntilDestroyed()).subscribe(({ tabIndex }) => { + this.tabIndex = !isNaN(Number(tabIndex)) ? Number(tabIndex) : RiskInsightsTabType.AllApps; + }); } - onTabChange = async (newIndex: number) => { + async ngOnInit() { + this.isCriticalAppsFeatureEnabled = await this.configService.getFeatureFlag( + FeatureFlag.CriticalApps, + ); + + this.route.paramMap + .pipe( + takeUntilDestroyed(this.destroyRef), + map((params) => params.get("organizationId")), + switchMap((orgId: string | null) => { + if (orgId) { + this.organizationId = orgId; + this.dataService.fetchApplicationsReport(orgId); + this.isLoading$ = this.dataService.isLoading$; + this.isRefreshing$ = this.dataService.isRefreshing$; + this.dataLastUpdated$ = this.dataService.dataLastUpdated$; + return this.dataService.applications$; + } else { + return EMPTY; + } + }), + ) + .subscribe({ + next: (applications: ApplicationHealthReportDetail[] | null) => { + if (applications) { + this.appsCount = applications.length; + } + }, + }); + } + + /** + * Refreshes the data by re-fetching the applications report. + * This will automatically notify child components subscribed to the RiskInsightsDataService observables. + */ + refreshData(): void { + if (this.organizationId) { + this.dataService.refreshApplicationsReport(this.organizationId); + } + } + + async onTabChange(newIndex: number): Promise { await this.router.navigate([], { relativeTo: this.route, queryParams: { tabIndex: newIndex }, queryParamsHandling: "merge", }); - }; - - async ngOnInit() { - this.isCritialAppsFeatureEnabled = await this.configService.getFeatureFlag( - FeatureFlag.CriticalApps, - ); - } - - constructor( - protected route: ActivatedRoute, - private router: Router, - private configService: ConfigService, - ) { - route.queryParams.pipe(takeUntilDestroyed()).subscribe(({ tabIndex }) => { - this.tabIndex = !isNaN(tabIndex) ? tabIndex : RiskInsightsTabType.AllApps; - }); } } From 179c26434b10d957ca264ec1685198700aa89807 Mon Sep 17 00:00:00 2001 From: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com> Date: Wed, 18 Dec 2024 13:45:27 -0500 Subject: [PATCH 11/54] PM-16172 - Web - LockV2 - make page title consistent with other clients (#12462) --- apps/web/src/app/oss-routing.module.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/app/oss-routing.module.ts b/apps/web/src/app/oss-routing.module.ts index 1903759f959..649f1aba534 100644 --- a/apps/web/src/app/oss-routing.module.ts +++ b/apps/web/src/app/oss-routing.module.ts @@ -534,7 +534,7 @@ const routes: Routes = [ ], data: { pageTitle: { - key: "yourAccountIsLocked", + key: "yourVaultIsLockedV2", }, pageIcon: LockIcon, showReadonlyHostname: true, From 2bb807c5ce07e6c40609be323a63402d0c78e8b6 Mon Sep 17 00:00:00 2001 From: albertboyd5 Date: Wed, 18 Dec 2024 13:20:17 -0600 Subject: [PATCH 12/54] Update Password history button label to Generator history (#12452) --- apps/desktop/src/main/menu/menu.view.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/desktop/src/main/menu/menu.view.ts b/apps/desktop/src/main/menu/menu.view.ts index c78d2347bcd..962c57fdb60 100644 --- a/apps/desktop/src/main/menu/menu.view.ts +++ b/apps/desktop/src/main/menu/menu.view.ts @@ -76,7 +76,7 @@ export class ViewMenu implements IMenubarMenu { private get passwordHistory(): MenuItemConstructorOptions { return { id: "passwordHistory", - label: this.localize("passwordHistory"), + label: this.localize("generatorHistory"), click: () => this.sendMessage("openPasswordHistory"), enabled: !this._isLocked, }; From 6eb30c98c4d8436248560d39a08984ae443584f9 Mon Sep 17 00:00:00 2001 From: Victoria League Date: Wed, 18 Dec 2024 14:25:03 -0500 Subject: [PATCH 13/54] Fix imports for standalone component stories (#12464) --- libs/components/src/async-actions/in-forms.stories.ts | 9 +++------ libs/components/src/async-actions/standalone.stories.ts | 3 +-- libs/components/src/badge/badge.stories.ts | 3 +-- libs/components/src/breadcrumbs/breadcrumbs.stories.ts | 3 +-- libs/components/src/dialog/dialog.service.stories.ts | 9 ++++++--- libs/components/src/toggle-group/toggle-group.stories.ts | 3 +-- 6 files changed, 13 insertions(+), 17 deletions(-) diff --git a/libs/components/src/async-actions/in-forms.stories.ts b/libs/components/src/async-actions/in-forms.stories.ts index ec6005dd607..fb94e43b196 100644 --- a/libs/components/src/async-actions/in-forms.stories.ts +++ b/libs/components/src/async-actions/in-forms.stories.ts @@ -109,20 +109,17 @@ export default { title: "Component Library/Async Actions/In Forms", decorators: [ moduleMetadata({ - declarations: [ + declarations: [PromiseExampleComponent, ObservableExampleComponent], + imports: [ BitSubmitDirective, BitFormButtonDirective, - PromiseExampleComponent, - ObservableExampleComponent, - BitActionDirective, - ], - imports: [ FormsModule, ReactiveFormsModule, FormFieldModule, InputModule, ButtonModule, IconButtonModule, + BitActionDirective, ], providers: [ { diff --git a/libs/components/src/async-actions/standalone.stories.ts b/libs/components/src/async-actions/standalone.stories.ts index 5e15135dc5d..f658dfb0f01 100644 --- a/libs/components/src/async-actions/standalone.stories.ts +++ b/libs/components/src/async-actions/standalone.stories.ts @@ -56,12 +56,11 @@ export default { decorators: [ moduleMetadata({ declarations: [ - BitActionDirective, PromiseExampleComponent, ObservableExampleComponent, RejectedPromiseExampleComponent, ], - imports: [ButtonModule, IconButtonModule], + imports: [ButtonModule, IconButtonModule, BitActionDirective], providers: [ { provide: ValidationService, diff --git a/libs/components/src/badge/badge.stories.ts b/libs/components/src/badge/badge.stories.ts index 6c57bc0cbfb..b8ac7ec8efe 100644 --- a/libs/components/src/badge/badge.stories.ts +++ b/libs/components/src/badge/badge.stories.ts @@ -8,8 +8,7 @@ export default { component: BadgeDirective, decorators: [ moduleMetadata({ - imports: [CommonModule], - declarations: [BadgeDirective], + imports: [CommonModule, BadgeDirective], }), ], args: { diff --git a/libs/components/src/breadcrumbs/breadcrumbs.stories.ts b/libs/components/src/breadcrumbs/breadcrumbs.stories.ts index 300369f2454..9c8ccbccd3f 100644 --- a/libs/components/src/breadcrumbs/breadcrumbs.stories.ts +++ b/libs/components/src/breadcrumbs/breadcrumbs.stories.ts @@ -25,8 +25,7 @@ export default { component: BreadcrumbsComponent, decorators: [ moduleMetadata({ - declarations: [BreadcrumbComponent], - imports: [LinkModule, MenuModule, IconButtonModule, RouterModule], + imports: [LinkModule, MenuModule, IconButtonModule, RouterModule, BreadcrumbComponent], }), applicationConfig({ providers: [ diff --git a/libs/components/src/dialog/dialog.service.stories.ts b/libs/components/src/dialog/dialog.service.stories.ts index e28f0ac4b19..5e938412804 100644 --- a/libs/components/src/dialog/dialog.service.stories.ts +++ b/libs/components/src/dialog/dialog.service.stories.ts @@ -64,13 +64,16 @@ export default { component: StoryDialogComponent, decorators: [ moduleMetadata({ - declarations: [ + declarations: [StoryDialogContentComponent], + imports: [ + SharedModule, + ButtonModule, + DialogModule, + IconButtonModule, DialogCloseDirective, DialogComponent, DialogTitleContainerDirective, - StoryDialogContentComponent, ], - imports: [SharedModule, ButtonModule, DialogModule, IconButtonModule], providers: [ DialogService, { diff --git a/libs/components/src/toggle-group/toggle-group.stories.ts b/libs/components/src/toggle-group/toggle-group.stories.ts index edfa832d6ce..fc8ea0ea929 100644 --- a/libs/components/src/toggle-group/toggle-group.stories.ts +++ b/libs/components/src/toggle-group/toggle-group.stories.ts @@ -13,8 +13,7 @@ export default { }, decorators: [ moduleMetadata({ - declarations: [ToggleGroupComponent, ToggleComponent], - imports: [BadgeModule], + imports: [BadgeModule, ToggleGroupComponent, ToggleComponent], }), ], parameters: { From ef8e8bfcbcb2f826b47a49b0b5ce20e7d23b0fa4 Mon Sep 17 00:00:00 2001 From: albertboyd5 Date: Wed, 18 Dec 2024 14:17:50 -0600 Subject: [PATCH 14/54] [PM-15571] Help link on import vault (#12448) * [PM-15571] BW help Link on import vault page has wrong styling in dark mode * Remove useless code --- libs/importer/src/components/import.component.html | 8 +++++++- libs/importer/src/components/import.component.ts | 2 ++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/libs/importer/src/components/import.component.html b/libs/importer/src/components/import.component.html index 33056265de4..0da8127369e 100644 --- a/libs/importer/src/components/import.component.html +++ b/libs/importer/src/components/import.component.html @@ -80,7 +80,13 @@ See detailed instructions on our help site at - + https://bitwarden.com/help/export-your-data/ diff --git a/libs/importer/src/components/import.component.ts b/libs/importer/src/components/import.component.ts index 247b33aa6a9..b50be773251 100644 --- a/libs/importer/src/components/import.component.ts +++ b/libs/importer/src/components/import.component.ts @@ -53,6 +53,7 @@ import { SectionHeaderComponent, SelectModule, ToastService, + LinkModule, } from "@bitwarden/components"; import { KeyService } from "@bitwarden/key-management"; @@ -115,6 +116,7 @@ const safeProviders: SafeProvider[] = [ ContainerComponent, SectionHeaderComponent, SectionComponent, + LinkModule, ], providers: safeProviders, }) From 456046e095b9bee3938df4e67daf277569e508a2 Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Wed, 18 Dec 2024 15:28:13 -0500 Subject: [PATCH 15/54] Make Sync Optional on SW Start (#12467) --- apps/browser/src/background/main.background.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 019be2923b6..ba9776b80c5 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -1330,7 +1330,7 @@ export default class MainBackground { return new Promise((resolve) => { setTimeout(async () => { await this.refreshBadge(); - await this.fullSync(true); + await this.fullSync(false); this.taskSchedulerService.setInterval( ScheduledTaskNames.scheduleNextSyncInterval, 5 * 60 * 1000, // check every 5 minutes From 51f6594d4b680f250207e8f3dcac9fcc7bb26e16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garc=C3=ADa?= Date: Thu, 19 Dec 2024 09:00:21 +0100 Subject: [PATCH 16/54] [PM-9473] Add messaging for macOS passkey extension and desktop (#10768) * Add messaging for macos passkey provider * fix: credential id conversion * Make build.sh executable Co-authored-by: Colton Hurst * chore: add TODO --------- Co-authored-by: Andreas Coroiu Co-authored-by: Andreas Coroiu Co-authored-by: Colton Hurst --- .../fido2/background/fido2.background.spec.ts | 5 +- .../fido2/background/fido2.background.ts | 5 +- .../browser-fido2-user-interface.service.ts | 6 +- .../browser/src/background/main.background.ts | 15 +- apps/desktop/desktop_native/Cargo.lock | 485 +++++++++++++++++- apps/desktop/desktop_native/Cargo.toml | 2 +- .../desktop_native/macos_provider/.gitignore | 1 + .../desktop_native/macos_provider/Cargo.toml | 30 ++ .../desktop_native/macos_provider/build.sh | 43 ++ .../macos_provider/src/assertion.rs | 46 ++ .../desktop_native/macos_provider/src/lib.rs | 205 ++++++++ .../macos_provider/src/registration.rs | 43 ++ .../macos_provider/uniffi-bindgen.rs | 3 + .../desktop_native/macos_provider/uniffi.toml | 4 + apps/desktop/desktop_native/napi/Cargo.toml | 2 + apps/desktop/desktop_native/napi/index.d.ts | 52 ++ apps/desktop/desktop_native/napi/src/lib.rs | 244 +++++++++ apps/desktop/macos/.gitignore | 1 + .../CredentialProviderViewController.swift | 182 ++++++- .../autofill_extension.entitlements | 4 + .../macos/desktop.xcodeproj/project.pbxproj | 8 + apps/desktop/package.json | 2 +- .../src/app/services/services.module.ts | 29 +- apps/desktop/src/autofill/preload.ts | 83 +++ .../services/desktop-autofill.service.ts | 166 +++++- apps/desktop/src/main.ts | 2 +- .../main/autofill/native-autofill.main.ts | 55 +- .../desktop-fido2-user-interface.service.ts | 125 +++++ ...fido2-authenticator.service.abstraction.ts | 6 +- .../fido2/fido2-client.service.abstraction.ts | 6 +- ...ido2-user-interface.service.abstraction.ts | 4 +- .../fido2/fido2-authenticator.service.spec.ts | 75 +-- .../fido2/fido2-authenticator.service.ts | 14 +- .../services/fido2/fido2-autofill-utils.ts | 10 +- .../fido2/fido2-client.service.spec.ts | 96 ++-- .../services/fido2/fido2-client.service.ts | 23 +- .../noop-fido2-user-interface.service.ts | 2 +- 37 files changed, 1935 insertions(+), 149 deletions(-) create mode 100644 apps/desktop/desktop_native/macos_provider/.gitignore create mode 100644 apps/desktop/desktop_native/macos_provider/Cargo.toml create mode 100755 apps/desktop/desktop_native/macos_provider/build.sh create mode 100644 apps/desktop/desktop_native/macos_provider/src/assertion.rs create mode 100644 apps/desktop/desktop_native/macos_provider/src/lib.rs create mode 100644 apps/desktop/desktop_native/macos_provider/src/registration.rs create mode 100644 apps/desktop/desktop_native/macos_provider/uniffi-bindgen.rs create mode 100644 apps/desktop/desktop_native/macos_provider/uniffi.toml create mode 100644 apps/desktop/macos/.gitignore create mode 100644 apps/desktop/src/platform/services/desktop-fido2-user-interface.service.ts diff --git a/apps/browser/src/autofill/fido2/background/fido2.background.spec.ts b/apps/browser/src/autofill/fido2/background/fido2.background.spec.ts index 99ed4619954..144af0c0a35 100644 --- a/apps/browser/src/autofill/fido2/background/fido2.background.spec.ts +++ b/apps/browser/src/autofill/fido2/background/fido2.background.spec.ts @@ -25,6 +25,7 @@ import { BrowserScriptInjectorService } from "../../../platform/services/browser import { AbortManager } from "../../../vault/background/abort-manager"; import { Fido2ContentScript, Fido2ContentScriptId } from "../enums/fido2-content-script.enum"; import { Fido2PortName } from "../enums/fido2-port-name.enum"; +import { BrowserFido2ParentWindowReference } from "../services/browser-fido2-user-interface.service"; import { Fido2ExtensionMessage } from "./abstractions/fido2.background"; import { Fido2Background } from "./fido2.background"; @@ -56,7 +57,7 @@ describe("Fido2Background", () => { let senderMock!: MockProxy; let logService!: MockProxy; let fido2ActiveRequestManager: MockProxy; - let fido2ClientService!: MockProxy; + let fido2ClientService!: MockProxy>; let vaultSettingsService!: MockProxy; let scriptInjectorServiceMock!: MockProxy; let configServiceMock!: MockProxy; @@ -73,7 +74,7 @@ describe("Fido2Background", () => { }); senderMock = mock({ id: "1", tab: tabMock }); logService = mock(); - fido2ClientService = mock(); + fido2ClientService = mock>(); vaultSettingsService = mock(); abortManagerMock = mock(); abortController = mock(); diff --git a/apps/browser/src/autofill/fido2/background/fido2.background.ts b/apps/browser/src/autofill/fido2/background/fido2.background.ts index f84b7d29a66..e20a0584d20 100644 --- a/apps/browser/src/autofill/fido2/background/fido2.background.ts +++ b/apps/browser/src/autofill/fido2/background/fido2.background.ts @@ -23,10 +23,11 @@ import { ScriptInjectorService } from "../../../platform/services/abstractions/s import { AbortManager } from "../../../vault/background/abort-manager"; import { Fido2ContentScript, Fido2ContentScriptId } from "../enums/fido2-content-script.enum"; import { Fido2PortName } from "../enums/fido2-port-name.enum"; +import { BrowserFido2ParentWindowReference } from "../services/browser-fido2-user-interface.service"; import { - Fido2Background as Fido2BackgroundInterface, Fido2BackgroundExtensionMessageHandlers, + Fido2Background as Fido2BackgroundInterface, Fido2ExtensionMessage, SharedFido2ScriptInjectionDetails, SharedFido2ScriptRegistrationOptions, @@ -56,7 +57,7 @@ export class Fido2Background implements Fido2BackgroundInterface { constructor( private logService: LogService, private fido2ActiveRequestManager: Fido2ActiveRequestManager, - private fido2ClientService: Fido2ClientService, + private fido2ClientService: Fido2ClientService, private vaultSettingsService: VaultSettingsService, private scriptInjectorService: ScriptInjectorService, private configService: ConfigService, diff --git a/apps/browser/src/autofill/fido2/services/browser-fido2-user-interface.service.ts b/apps/browser/src/autofill/fido2/services/browser-fido2-user-interface.service.ts index 872bb1bb52a..04b09a7df32 100644 --- a/apps/browser/src/autofill/fido2/services/browser-fido2-user-interface.service.ts +++ b/apps/browser/src/autofill/fido2/services/browser-fido2-user-interface.service.ts @@ -111,11 +111,15 @@ export type BrowserFido2Message = { sessionId: string } & ( } ); +export type BrowserFido2ParentWindowReference = chrome.tabs.Tab; + /** * Browser implementation of the {@link Fido2UserInterfaceService}. * The user interface is implemented as a popout and the service uses the browser's messaging API to communicate with it. */ -export class BrowserFido2UserInterfaceService implements Fido2UserInterfaceServiceAbstraction { +export class BrowserFido2UserInterfaceService + implements Fido2UserInterfaceServiceAbstraction +{ constructor(private authService: AuthService) {} async newSession( diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index ba9776b80c5..616e18601af 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -201,11 +201,11 @@ import { ImportServiceAbstraction, } from "@bitwarden/importer/core"; import { - DefaultKdfConfigService, - KdfConfigService, BiometricStateService, BiometricsService, DefaultBiometricStateService, + DefaultKdfConfigService, + KdfConfigService, KeyService as KeyServiceAbstraction, } from "@bitwarden/key-management"; import { @@ -232,7 +232,10 @@ import { MainContextMenuHandler } from "../autofill/browser/main-context-menu-ha import LegacyOverlayBackground from "../autofill/deprecated/background/overlay.background.deprecated"; import { Fido2Background as Fido2BackgroundAbstraction } from "../autofill/fido2/background/abstractions/fido2.background"; import { Fido2Background } from "../autofill/fido2/background/fido2.background"; -import { BrowserFido2UserInterfaceService } from "../autofill/fido2/services/browser-fido2-user-interface.service"; +import { + BrowserFido2ParentWindowReference, + BrowserFido2UserInterfaceService, +} from "../autofill/fido2/services/browser-fido2-user-interface.service"; import { AutofillService as AutofillServiceAbstraction } from "../autofill/services/abstractions/autofill.service"; import AutofillService from "../autofill/services/autofill.service"; import { InlineMenuFieldQualificationService } from "../autofill/services/inline-menu-field-qualification.service"; @@ -337,10 +340,10 @@ export default class MainBackground { policyApiService: PolicyApiServiceAbstraction; sendApiService: SendApiServiceAbstraction; userVerificationApiService: UserVerificationApiServiceAbstraction; - fido2UserInterfaceService: Fido2UserInterfaceServiceAbstraction; - fido2AuthenticatorService: Fido2AuthenticatorServiceAbstraction; + fido2UserInterfaceService: Fido2UserInterfaceServiceAbstraction; + fido2AuthenticatorService: Fido2AuthenticatorServiceAbstraction; fido2ActiveRequestManager: Fido2ActiveRequestManagerAbstraction; - fido2ClientService: Fido2ClientServiceAbstraction; + fido2ClientService: Fido2ClientServiceAbstraction; avatarService: AvatarServiceAbstraction; mainContextMenuHandler: MainContextMenuHandler; cipherContextMenuHandler: CipherContextMenuHandler; diff --git a/apps/desktop/desktop_native/Cargo.lock b/apps/desktop/desktop_native/Cargo.lock index 09d3d15e897..1fac307262a 100644 --- a/apps/desktop/desktop_native/Cargo.lock +++ b/apps/desktop/desktop_native/Cargo.lock @@ -62,12 +62,55 @@ dependencies = [ "memchr", ] +[[package]] +name = "anstream" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + [[package]] name = "anstyle" version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" +[[package]] +name = "anstyle-parse" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" +dependencies = [ + "anstyle", + "windows-sys 0.59.0", +] + [[package]] name = "anyhow" version = "1.0.94" @@ -103,6 +146,47 @@ dependencies = [ "zeroize", ] +[[package]] +name = "askama" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b79091df18a97caea757e28cd2d5fda49c6cd4bd01ddffd7ff01ace0c0ad2c28" +dependencies = [ + "askama_derive", + "askama_escape", +] + +[[package]] +name = "askama_derive" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19fe8d6cb13c4714962c072ea496f3392015f0989b1a2847bb4b2d9effd71d83" +dependencies = [ + "askama_parser", + "basic-toml", + "mime", + "mime_guess", + "proc-macro2", + "quote", + "serde", + "syn", +] + +[[package]] +name = "askama_escape" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "619743e34b5ba4e9703bba34deac3427c72507c7159f5fd030aea8cac0cfe341" + +[[package]] +name = "askama_parser" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acb1161c6b64d1c3d83108213c2a2533a342ac225aabd0bda218278c2ddb00c0" +dependencies = [ + "nom", +] + [[package]] name = "async-broadcast" version = "0.7.1" @@ -318,6 +402,15 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" +[[package]] +name = "basic-toml" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "823388e228f614e9558c6804262db37960ec8821856535f5c3f59913140558f8" +dependencies = [ + "serde", +] + [[package]] name = "bcrypt-pbkdf" version = "0.10.0" @@ -329,6 +422,15 @@ dependencies = [ "sha2", ] +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + [[package]] name = "bitflags" version = "2.6.0" @@ -422,6 +524,38 @@ version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" +[[package]] +name = "camino" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b96ec4966b5813e2c0507c1f86115c8c5abaadc3980879c3424042a02fd1ad3" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo-platform" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eee4243f1f26fc7a42710e7439c149e2b10b05472f88090acce52632f231a73a" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror", +] + [[package]] name = "cbc" version = "0.1.2" @@ -487,6 +621,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69371e34337c4c984bbe322360c2547210bf632eb2814bbe78a6e87a2935bd2b" dependencies = [ "clap_builder", + "clap_derive", ] [[package]] @@ -495,11 +630,24 @@ version = "4.5.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e24c1b4099818523236a8ca881d2b45db98dadfb4625cf6608c12069fcbbde1" dependencies = [ + "anstream", "anstyle", "clap_lex", "strsim", ] +[[package]] +name = "clap_derive" +version = "4.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "clap_lex" version = "0.7.3" @@ -525,6 +673,12 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "colorchoice" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -724,6 +878,19 @@ dependencies = [ "syn", ] +[[package]] +name = "dashmap" +version = "5.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" +dependencies = [ + "cfg-if", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + [[package]] name = "der" version = "0.7.9" @@ -815,6 +982,8 @@ dependencies = [ "napi", "napi-build", "napi-derive", + "serde", + "serde_json", "tokio", "tokio-stream", "tokio-util", @@ -1035,6 +1204,15 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f81ec6369c545a7d40e4589b5597581fa1c441fe1cce96dd1de43159910a36a2" +[[package]] +name = "fs-err" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a41f105fe1d5b6b34b2055e3dc59bb79b46b48b2040b9e6c7b4b5de097aa41" +dependencies = [ + "autocfg", +] + [[package]] name = "futures" version = "0.3.31" @@ -1190,12 +1368,35 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" +[[package]] +name = "goblin" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b363a30c165f666402fe6a3024d3bec7ebc898f96a4a23bd1c99f8dbf3f4f47" +dependencies = [ + "log", + "plain", + "scroll", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + [[package]] name = "hashbrown" version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "hermit-abi" version = "0.4.0" @@ -1245,7 +1446,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.15.2", ] [[package]] @@ -1273,6 +1474,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + [[package]] name = "itoa" version = "1.0.14" @@ -1372,6 +1579,21 @@ version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +[[package]] +name = "macos_provider" +version = "0.0.0" +dependencies = [ + "desktop_core", + "futures", + "log", + "oslog", + "serde", + "serde_json", + "tokio", + "tokio-util", + "uniffi", +] + [[package]] name = "md-5" version = "0.10.6" @@ -1397,6 +1619,22 @@ dependencies = [ "autocfg", ] +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -1811,6 +2049,17 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "oslog" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d2043d1f61d77cb2f4b1f7b7b2295f40507f5f8e9d1c8bf10a1ca5f97a3969" +dependencies = [ + "cc", + "dashmap", + "log", +] + [[package]] name = "parking" version = "2.2.1" @@ -1851,6 +2100,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + [[package]] name = "pbkdf2" version = "0.12.2" @@ -1967,6 +2222,12 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + [[package]] name = "polling" version = "3.7.4" @@ -2235,6 +2496,12 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248" +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + [[package]] name = "salsa20" version = "0.10.2" @@ -2262,6 +2529,26 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3cf7c11c38cb994f3d40e8a8cde3bbd1f72a435e4c49e85d6553d8312306152" +[[package]] +name = "scroll" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ab8598aa408498679922eff7fa985c25d58a90771bd6be794434c5277eab1a6" +dependencies = [ + "scroll_derive", +] + +[[package]] +name = "scroll_derive" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f81c2fde025af7e69b1d1420531c8a8811ca898919db177141a85313b1cb932" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "scrypt" version = "0.11.0" @@ -2301,6 +2588,9 @@ name = "semver" version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" +dependencies = [ + "serde", +] [[package]] name = "serde" @@ -2322,6 +2612,18 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_json" +version = "1.0.133" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + [[package]] name = "serde_repr" version = "0.1.19" @@ -2391,6 +2693,12 @@ dependencies = [ "time", ] +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + [[package]] name = "slab" version = "0.4.9" @@ -2406,6 +2714,12 @@ version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +[[package]] +name = "smawk" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" + [[package]] name = "socket2" version = "0.5.8" @@ -2544,6 +2858,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "textwrap" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9" +dependencies = [ + "smawk", +] + [[package]] name = "thiserror" version = "1.0.69" @@ -2648,6 +2971,15 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + [[package]] name = "toml_datetime" version = "0.6.8" @@ -2726,6 +3058,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "unicase" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e51b68083f157f853b6379db119d1c1be0e6e4dec98101079dec41f6f5cf6df" + [[package]] name = "unicode-ident" version = "1.0.14" @@ -2744,6 +3082,136 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" +[[package]] +name = "uniffi" +version = "0.28.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cb08c58c7ed7033150132febe696bef553f891b1ede57424b40d87a89e3c170" +dependencies = [ + "anyhow", + "camino", + "cargo_metadata", + "clap", + "uniffi_bindgen", + "uniffi_build", + "uniffi_core", + "uniffi_macros", +] + +[[package]] +name = "uniffi_bindgen" +version = "0.28.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cade167af943e189a55020eda2c314681e223f1e42aca7c4e52614c2b627698f" +dependencies = [ + "anyhow", + "askama", + "camino", + "cargo_metadata", + "fs-err", + "glob", + "goblin", + "heck", + "once_cell", + "paste", + "serde", + "textwrap", + "toml", + "uniffi_meta", + "uniffi_udl", +] + +[[package]] +name = "uniffi_build" +version = "0.28.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7cf32576e08104b7dc2a6a5d815f37616e66c6866c2a639fe16e6d2286b75b" +dependencies = [ + "anyhow", + "camino", + "uniffi_bindgen", +] + +[[package]] +name = "uniffi_checksum_derive" +version = "0.28.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "802d2051a700e3ec894c79f80d2705b69d85844dafbbe5d1a92776f8f48b563a" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "uniffi_core" +version = "0.28.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7687007d2546c454d8ae609b105daceb88175477dac280707ad6d95bcd6f1f" +dependencies = [ + "anyhow", + "bytes", + "log", + "once_cell", + "paste", + "static_assertions", +] + +[[package]] +name = "uniffi_macros" +version = "0.28.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12c65a5b12ec544ef136693af8759fb9d11aefce740fb76916721e876639033b" +dependencies = [ + "bincode", + "camino", + "fs-err", + "once_cell", + "proc-macro2", + "quote", + "serde", + "syn", + "toml", + "uniffi_meta", +] + +[[package]] +name = "uniffi_meta" +version = "0.28.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a74ed96c26882dac1ca9b93ca23c827e284bacbd7ec23c6f0b0372f747d59e4" +dependencies = [ + "anyhow", + "bytes", + "siphasher", + "uniffi_checksum_derive", +] + +[[package]] +name = "uniffi_testing" +version = "0.28.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6f984f0781f892cc864a62c3a5c60361b1ccbd68e538e6c9fbced5d82268ac" +dependencies = [ + "anyhow", + "camino", + "cargo_metadata", + "fs-err", + "once_cell", +] + +[[package]] +name = "uniffi_udl" +version = "0.28.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037820a4cfc4422db1eaa82f291a3863c92c7d1789dc513489c36223f9b4cdfc" +dependencies = [ + "anyhow", + "textwrap", + "uniffi_meta", + "uniffi_testing", + "weedle2", +] + [[package]] name = "universal-hash" version = "0.5.1" @@ -2754,6 +3222,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "version_check" version = "0.9.5" @@ -2839,6 +3313,15 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "weedle2" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "998d2c24ec099a87daf9467808859f9d82b61f1d9c9701251aea037f514eae0e" +dependencies = [ + "nom", +] + [[package]] name = "widestring" version = "1.1.0" diff --git a/apps/desktop/desktop_native/Cargo.toml b/apps/desktop/desktop_native/Cargo.toml index 6525b38162d..6230a6bfe15 100644 --- a/apps/desktop/desktop_native/Cargo.toml +++ b/apps/desktop/desktop_native/Cargo.toml @@ -1,3 +1,3 @@ [workspace] resolver = "2" -members = ["napi", "core", "proxy"] +members = ["napi", "core", "proxy", "macos_provider"] diff --git a/apps/desktop/desktop_native/macos_provider/.gitignore b/apps/desktop/desktop_native/macos_provider/.gitignore new file mode 100644 index 00000000000..73f0a6381d8 --- /dev/null +++ b/apps/desktop/desktop_native/macos_provider/.gitignore @@ -0,0 +1 @@ +BitwardenMacosProviderFFI.xcframework diff --git a/apps/desktop/desktop_native/macos_provider/Cargo.toml b/apps/desktop/desktop_native/macos_provider/Cargo.toml new file mode 100644 index 00000000000..28cc6372c62 --- /dev/null +++ b/apps/desktop/desktop_native/macos_provider/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "macos_provider" +license = "GPL-3.0" +version = "0.0.0" +edition = "2021" +publish = false + +[[bin]] +name = "uniffi-bindgen" +path = "uniffi-bindgen.rs" + +[lib] +crate-type = ["staticlib", "cdylib"] +bench = false + +[dependencies] +desktop_core = { path = "../core" } +futures = "=0.3.31" +log = "0.4.22" +serde = { version = "1.0.205", features = ["derive"] } +serde_json = "1.0.122" +tokio = { version = "1.39.2", features = ["sync"] } +tokio-util = "0.7.11" +uniffi = { version = "0.28.0", features = ["cli"] } + +[target.'cfg(target_os = "macos")'.dependencies] +oslog = "0.2.0" + +[build-dependencies] +uniffi = { version = "0.28.0", features = ["build"] } diff --git a/apps/desktop/desktop_native/macos_provider/build.sh b/apps/desktop/desktop_native/macos_provider/build.sh new file mode 100755 index 00000000000..21e2e045af4 --- /dev/null +++ b/apps/desktop/desktop_native/macos_provider/build.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash + +cd "$(dirname "$0")" + +rm -r BitwardenMacosProviderFFI.xcframework +rm -r tmp + +mkdir -p ./tmp/target/universal-darwin/release/ + + +cargo build --package macos_provider --target aarch64-apple-darwin --release +cargo build --package macos_provider --target x86_64-apple-darwin --release + +# Create universal libraries +lipo -create ../target/aarch64-apple-darwin/release/libmacos_provider.a \ + ../target/x86_64-apple-darwin/release/libmacos_provider.a \ + -output ./tmp/target/universal-darwin/release/libmacos_provider.a + +# Generate swift bindings +cargo run --bin uniffi-bindgen --features uniffi/cli generate \ + ../target/aarch64-apple-darwin/release/libmacos_provider.dylib \ + --library \ + --language swift \ + --no-format \ + --out-dir tmp/bindings + +# Move generated swift bindings +mkdir -p ../../macos/autofill-extension/ +mv ./tmp/bindings/*.swift ../../macos/autofill-extension/ + +# Massage the generated files to fit xcframework +mkdir tmp/Headers +mv ./tmp/bindings/*.h ./tmp/Headers/ +cat ./tmp/bindings/*.modulemap > ./tmp/Headers/module.modulemap + +# Build xcframework +xcodebuild -create-xcframework \ + -library ./tmp/target/universal-darwin/release/libmacos_provider.a \ + -headers ./tmp/Headers \ + -output ./BitwardenMacosProviderFFI.xcframework + +# Cleanup temporary files +rm -r tmp diff --git a/apps/desktop/desktop_native/macos_provider/src/assertion.rs b/apps/desktop/desktop_native/macos_provider/src/assertion.rs new file mode 100644 index 00000000000..762ceaaed48 --- /dev/null +++ b/apps/desktop/desktop_native/macos_provider/src/assertion.rs @@ -0,0 +1,46 @@ +use std::sync::Arc; + +use serde::{Deserialize, Serialize}; + +use crate::{BitwardenError, Callback, UserVerification}; + +#[derive(uniffi::Record, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PasskeyAssertionRequest { + rp_id: String, + credential_id: Vec, + user_name: String, + user_handle: Vec, + record_identifier: Option, + client_data_hash: Vec, + user_verification: UserVerification, +} + +#[derive(uniffi::Record, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PasskeyAssertionResponse { + rp_id: String, + user_handle: Vec, + signature: Vec, + client_data_hash: Vec, + authenticator_data: Vec, + credential_id: Vec, +} + +#[uniffi::export(with_foreign)] +pub trait PreparePasskeyAssertionCallback: Send + Sync { + fn on_complete(&self, credential: PasskeyAssertionResponse); + fn on_error(&self, error: BitwardenError); +} + +impl Callback for Arc { + fn complete(&self, credential: serde_json::Value) -> Result<(), serde_json::Error> { + let credential = serde_json::from_value(credential)?; + PreparePasskeyAssertionCallback::on_complete(self.as_ref(), credential); + Ok(()) + } + + fn error(&self, error: BitwardenError) { + PreparePasskeyAssertionCallback::on_error(self.as_ref(), error); + } +} diff --git a/apps/desktop/desktop_native/macos_provider/src/lib.rs b/apps/desktop/desktop_native/macos_provider/src/lib.rs new file mode 100644 index 00000000000..5623436d874 --- /dev/null +++ b/apps/desktop/desktop_native/macos_provider/src/lib.rs @@ -0,0 +1,205 @@ +#![cfg(target_os = "macos")] + +use std::{ + collections::HashMap, + sync::{atomic::AtomicU32, Arc, Mutex}, + time::Instant, +}; + +use futures::FutureExt; +use log::{error, info}; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; + +uniffi::setup_scaffolding!(); + +mod assertion; +mod registration; + +use assertion::{PasskeyAssertionRequest, PreparePasskeyAssertionCallback}; +use registration::{PasskeyRegistrationRequest, PreparePasskeyRegistrationCallback}; + +#[derive(uniffi::Enum, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum UserVerification { + Preferred, + Required, + Discouraged, +} + +#[derive(Debug, uniffi::Error, Serialize, Deserialize)] +pub enum BitwardenError { + Internal(String), +} + +// TODO: These have to be named differently than the actual Uniffi traits otherwise +// the generated code will lead to ambiguous trait implementations +// These are only used internally, so it doesn't matter that much +trait Callback: Send + Sync { + fn complete(&self, credential: serde_json::Value) -> Result<(), serde_json::Error>; + fn error(&self, error: BitwardenError); +} + +#[derive(uniffi::Object)] +pub struct MacOSProviderClient { + to_server_send: tokio::sync::mpsc::Sender, + + // We need to keep track of the callbacks so we can call them when we receive a response + response_callbacks_counter: AtomicU32, + #[allow(clippy::type_complexity)] + response_callbacks_queue: Arc, Instant)>>>, +} + +#[uniffi::export] +impl MacOSProviderClient { + #[uniffi::constructor] + pub fn connect() -> Self { + let _ = oslog::OsLogger::new("com.bitwarden.desktop.autofill-extension") + .level_filter(log::LevelFilter::Trace) + .init(); + + let (from_server_send, mut from_server_recv) = tokio::sync::mpsc::channel(32); + let (to_server_send, to_server_recv) = tokio::sync::mpsc::channel(32); + + let client = MacOSProviderClient { + to_server_send, + response_callbacks_counter: AtomicU32::new(0), + response_callbacks_queue: Arc::new(Mutex::new(HashMap::new())), + }; + + let path = desktop_core::ipc::path("autofill"); + + let queue = client.response_callbacks_queue.clone(); + + std::thread::spawn(move || { + let rt = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .expect("Can't create runtime"); + + rt.spawn( + desktop_core::ipc::client::connect(path, from_server_send, to_server_recv) + .map(|r| r.map_err(|e| e.to_string())), + ); + + rt.block_on(async move { + while let Some(message) = from_server_recv.recv().await { + match serde_json::from_str::(&message) { + Ok(SerializedMessage::Command(CommandMessage::Connected)) => { + info!("Connected to server"); + } + Ok(SerializedMessage::Command(CommandMessage::Disconnected)) => { + info!("Disconnected from server"); + } + Ok(SerializedMessage::Message { + sequence_number, + value, + }) => match queue.lock().unwrap().remove(&sequence_number) { + Some((cb, request_start_time)) => { + info!( + "Time to process request: {:?}", + request_start_time.elapsed() + ); + match value { + Ok(value) => { + if let Err(e) = cb.complete(value) { + error!("Error deserializing message: {e}"); + } + } + Err(e) => { + error!("Error processing message: {e:?}"); + cb.error(e) + } + } + } + None => { + error!("No callback found for sequence number: {sequence_number}") + } + }, + Err(e) => { + error!("Error deserializing message: {e}"); + } + }; + } + }); + }); + + client + } + + pub fn prepare_passkey_registration( + &self, + request: PasskeyRegistrationRequest, + callback: Arc, + ) { + self.send_message(request, Box::new(callback)); + } + + pub fn prepare_passkey_assertion( + &self, + request: PasskeyAssertionRequest, + callback: Arc, + ) { + self.send_message(request, Box::new(callback)); + } +} + +#[derive(Serialize, Deserialize)] +#[serde(tag = "command", rename_all = "camelCase")] +enum CommandMessage { + Connected, + Disconnected, +} + +#[derive(Serialize, Deserialize)] +#[serde(untagged, rename_all = "camelCase")] +enum SerializedMessage { + Command(CommandMessage), + Message { + sequence_number: u32, + value: Result, + }, +} + +impl MacOSProviderClient { + fn add_callback(&self, callback: Box) -> u32 { + let sequence_number = self + .response_callbacks_counter + .fetch_add(1, std::sync::atomic::Ordering::SeqCst); + + self.response_callbacks_queue + .lock() + .unwrap() + .insert(sequence_number, (callback, Instant::now())); + + sequence_number + } + + fn send_message( + &self, + message: impl Serialize + DeserializeOwned, + callback: Box, + ) { + let sequence_number = self.add_callback(callback); + + let message = serde_json::to_string(&SerializedMessage::Message { + sequence_number, + value: Ok(serde_json::to_value(message).unwrap()), + }) + .expect("Can't serialize message"); + + if let Err(e) = self.to_server_send.blocking_send(message) { + // Make sure we remove the callback from the queue if we can't send the message + if let Some((cb, _)) = self + .response_callbacks_queue + .lock() + .unwrap() + .remove(&sequence_number) + { + cb.error(BitwardenError::Internal(format!( + "Error sending message: {}", + e + ))); + } + } + } +} diff --git a/apps/desktop/desktop_native/macos_provider/src/registration.rs b/apps/desktop/desktop_native/macos_provider/src/registration.rs new file mode 100644 index 00000000000..d484af58b6c --- /dev/null +++ b/apps/desktop/desktop_native/macos_provider/src/registration.rs @@ -0,0 +1,43 @@ +use std::sync::Arc; + +use serde::{Deserialize, Serialize}; + +use crate::{BitwardenError, Callback, UserVerification}; + +#[derive(uniffi::Record, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PasskeyRegistrationRequest { + rp_id: String, + user_name: String, + user_handle: Vec, + client_data_hash: Vec, + user_verification: UserVerification, + supported_algorithms: Vec, +} + +#[derive(uniffi::Record, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PasskeyRegistrationResponse { + rp_id: String, + client_data_hash: Vec, + credential_id: Vec, + attestation_object: Vec, +} + +#[uniffi::export(with_foreign)] +pub trait PreparePasskeyRegistrationCallback: Send + Sync { + fn on_complete(&self, credential: PasskeyRegistrationResponse); + fn on_error(&self, error: BitwardenError); +} + +impl Callback for Arc { + fn complete(&self, credential: serde_json::Value) -> Result<(), serde_json::Error> { + let credential = serde_json::from_value(credential)?; + PreparePasskeyRegistrationCallback::on_complete(self.as_ref(), credential); + Ok(()) + } + + fn error(&self, error: BitwardenError) { + PreparePasskeyRegistrationCallback::on_error(self.as_ref(), error); + } +} diff --git a/apps/desktop/desktop_native/macos_provider/uniffi-bindgen.rs b/apps/desktop/desktop_native/macos_provider/uniffi-bindgen.rs new file mode 100644 index 00000000000..f6cff6cf1d9 --- /dev/null +++ b/apps/desktop/desktop_native/macos_provider/uniffi-bindgen.rs @@ -0,0 +1,3 @@ +fn main() { + uniffi::uniffi_bindgen_main() +} diff --git a/apps/desktop/desktop_native/macos_provider/uniffi.toml b/apps/desktop/desktop_native/macos_provider/uniffi.toml new file mode 100644 index 00000000000..ba696b8ec15 --- /dev/null +++ b/apps/desktop/desktop_native/macos_provider/uniffi.toml @@ -0,0 +1,4 @@ +[bindings.swift] +ffi_module_name = "BitwardenMacosProviderFFI" +module_name = "BitwardenMacosProvider" +generate_immutable_records = true diff --git a/apps/desktop/desktop_native/napi/Cargo.toml b/apps/desktop/desktop_native/napi/Cargo.toml index 08664eb6a53..6a656cdc574 100644 --- a/apps/desktop/desktop_native/napi/Cargo.toml +++ b/apps/desktop/desktop_native/napi/Cargo.toml @@ -20,6 +20,8 @@ anyhow = "=1.0.94" desktop_core = { path = "../core" } napi = { version = "=2.16.13", features = ["async"] } napi-derive = "=2.16.13" +serde = { version = "1.0.209", features = ["derive"] } +serde_json = "1.0.127" tokio = { version = "=1.41.1" } tokio-util = "=0.7.12" tokio-stream = "=0.1.15" diff --git a/apps/desktop/desktop_native/napi/index.d.ts b/apps/desktop/desktop_native/napi/index.d.ts index b884829e77d..1f37563e4fe 100644 --- a/apps/desktop/desktop_native/napi/index.d.ts +++ b/apps/desktop/desktop_native/napi/index.d.ts @@ -124,6 +124,58 @@ export declare namespace ipc { } export declare namespace autofill { export function runCommand(value: string): Promise + export const enum UserVerification { + Preferred = 'preferred', + Required = 'required', + Discouraged = 'discouraged' + } + export interface PasskeyRegistrationRequest { + rpId: string + userName: string + userHandle: Array + clientDataHash: Array + userVerification: UserVerification + supportedAlgorithms: Array + } + export interface PasskeyRegistrationResponse { + rpId: string + clientDataHash: Array + credentialId: Array + attestationObject: Array + } + export interface PasskeyAssertionRequest { + rpId: string + credentialId: Array + userName: string + userHandle: Array + recordIdentifier?: string + clientDataHash: Array + userVerification: UserVerification + } + export interface PasskeyAssertionResponse { + rpId: string + userHandle: Array + signature: Array + clientDataHash: Array + authenticatorData: Array + credentialId: Array + } + export class IpcServer { + /** + * Create and start the IPC server without blocking. + * + * @param name The endpoint name to listen on. This name uniquely identifies the IPC connection and must be the same for both the server and client. + * @param callback This function will be called whenever a message is received from a client. + */ + static listen(name: string, registrationCallback: (error: null | Error, clientId: number, sequenceNumber: number, message: PasskeyRegistrationRequest) => void, assertionCallback: (error: null | Error, clientId: number, sequenceNumber: number, message: PasskeyAssertionRequest) => void): Promise + /** Return the path to the IPC server. */ + getPath(): string + /** Stop the IPC server. */ + stop(): void + completeRegistration(clientId: number, sequenceNumber: number, response: PasskeyRegistrationResponse): number + completeAssertion(clientId: number, sequenceNumber: number, response: PasskeyAssertionResponse): number + completeError(clientId: number, sequenceNumber: number, error: string): number + } } export declare namespace crypto { export function argon2(secret: Buffer, salt: Buffer, iterations: number, memory: number, parallelism: number): Promise diff --git a/apps/desktop/desktop_native/napi/src/lib.rs b/apps/desktop/desktop_native/napi/src/lib.rs index a7e2144b1dc..170d7bca4f9 100644 --- a/apps/desktop/desktop_native/napi/src/lib.rs +++ b/apps/desktop/desktop_native/napi/src/lib.rs @@ -545,12 +545,256 @@ pub mod ipc { #[napi] pub mod autofill { + use desktop_core::ipc::server::{Message, MessageType}; + use napi::threadsafe_function::{ + ErrorStrategy, ThreadsafeFunction, ThreadsafeFunctionCallMode, + }; + use serde::{de::DeserializeOwned, Deserialize, Serialize}; + #[napi] pub async fn run_command(value: String) -> napi::Result { desktop_core::autofill::run_command(value) .await .map_err(|e| napi::Error::from_reason(e.to_string())) } + + #[derive(Debug, serde::Serialize, serde:: Deserialize)] + pub enum BitwardenError { + Internal(String), + } + + #[napi(string_enum)] + #[derive(Debug, Serialize, Deserialize)] + #[serde(rename_all = "camelCase")] + pub enum UserVerification { + #[napi(value = "preferred")] + Preferred, + #[napi(value = "required")] + Required, + #[napi(value = "discouraged")] + Discouraged, + } + + #[derive(Serialize, Deserialize)] + #[serde(bound = "T: Serialize + DeserializeOwned")] + pub struct PasskeyMessage { + pub sequence_number: u32, + pub value: Result, + } + + #[napi(object)] + #[derive(Debug, Serialize, Deserialize)] + #[serde(rename_all = "camelCase")] + pub struct PasskeyRegistrationRequest { + pub rp_id: String, + pub user_name: String, + pub user_handle: Vec, + pub client_data_hash: Vec, + pub user_verification: UserVerification, + pub supported_algorithms: Vec, + } + + #[napi(object)] + #[derive(Serialize, Deserialize)] + #[serde(rename_all = "camelCase")] + pub struct PasskeyRegistrationResponse { + pub rp_id: String, + pub client_data_hash: Vec, + pub credential_id: Vec, + pub attestation_object: Vec, + } + + #[napi(object)] + #[derive(Debug, Serialize, Deserialize)] + #[serde(rename_all = "camelCase")] + pub struct PasskeyAssertionRequest { + pub rp_id: String, + pub credential_id: Vec, + pub user_name: String, + pub user_handle: Vec, + pub record_identifier: Option, + pub client_data_hash: Vec, + pub user_verification: UserVerification, + } + + #[napi(object)] + #[derive(Serialize, Deserialize)] + #[serde(rename_all = "camelCase")] + pub struct PasskeyAssertionResponse { + pub rp_id: String, + pub user_handle: Vec, + pub signature: Vec, + pub client_data_hash: Vec, + pub authenticator_data: Vec, + pub credential_id: Vec, + } + + #[napi] + pub struct IpcServer { + server: desktop_core::ipc::server::Server, + } + + #[napi] + impl IpcServer { + /// Create and start the IPC server without blocking. + /// + /// @param name The endpoint name to listen on. This name uniquely identifies the IPC connection and must be the same for both the server and client. + /// @param callback This function will be called whenever a message is received from a client. + #[napi(factory)] + pub async fn listen( + name: String, + // Ideally we'd have a single callback that has an enum containing the request values, + // but NAPI doesn't support that just yet + #[napi( + ts_arg_type = "(error: null | Error, clientId: number, sequenceNumber: number, message: PasskeyRegistrationRequest) => void" + )] + registration_callback: ThreadsafeFunction< + (u32, u32, PasskeyRegistrationRequest), + ErrorStrategy::CalleeHandled, + >, + #[napi( + ts_arg_type = "(error: null | Error, clientId: number, sequenceNumber: number, message: PasskeyAssertionRequest) => void" + )] + assertion_callback: ThreadsafeFunction< + (u32, u32, PasskeyAssertionRequest), + ErrorStrategy::CalleeHandled, + >, + ) -> napi::Result { + let (send, mut recv) = tokio::sync::mpsc::channel::(32); + tokio::spawn(async move { + while let Some(Message { + client_id, + kind, + message, + }) = recv.recv().await + { + match kind { + // TODO: We're ignoring the connection and disconnection messages for now + MessageType::Connected | MessageType::Disconnected => continue, + MessageType::Message => { + let Some(message) = message else { + println!("[ERROR] Message is empty"); + continue; + }; + + match serde_json::from_str::>( + &message, + ) { + Ok(msg) => { + let value = msg + .value + .map(|value| (client_id, msg.sequence_number, value)) + .map_err(|e| napi::Error::from_reason(format!("{e:?}"))); + + assertion_callback + .call(value, ThreadsafeFunctionCallMode::NonBlocking); + continue; + } + Err(e) => { + println!("[ERROR] Error deserializing message1: {e}"); + } + } + + match serde_json::from_str::>( + &message, + ) { + Ok(msg) => { + let value = msg + .value + .map(|value| (client_id, msg.sequence_number, value)) + .map_err(|e| napi::Error::from_reason(format!("{e:?}"))); + registration_callback + .call(value, ThreadsafeFunctionCallMode::NonBlocking); + continue; + } + Err(e) => { + println!("[ERROR] Error deserializing message2: {e}"); + } + } + + println!("[ERROR] Received an unknown message2: {message:?}"); + } + } + } + }); + + let path = desktop_core::ipc::path(&name); + + let server = desktop_core::ipc::server::Server::start(&path, send).map_err(|e| { + napi::Error::from_reason(format!( + "Error listening to server - Path: {path:?} - Error: {e} - {e:?}" + )) + })?; + + Ok(IpcServer { server }) + } + + /// Return the path to the IPC server. + #[napi] + pub fn get_path(&self) -> String { + self.server.path.to_string_lossy().to_string() + } + + /// Stop the IPC server. + #[napi] + pub fn stop(&self) -> napi::Result<()> { + self.server.stop(); + Ok(()) + } + + #[napi] + pub fn complete_registration( + &self, + client_id: u32, + sequence_number: u32, + response: PasskeyRegistrationResponse, + ) -> napi::Result { + let message = PasskeyMessage { + sequence_number, + value: Ok(response), + }; + self.send(client_id, serde_json::to_string(&message).unwrap()) + } + + #[napi] + pub fn complete_assertion( + &self, + client_id: u32, + sequence_number: u32, + response: PasskeyAssertionResponse, + ) -> napi::Result { + let message = PasskeyMessage { + sequence_number, + value: Ok(response), + }; + self.send(client_id, serde_json::to_string(&message).unwrap()) + } + + #[napi] + pub fn complete_error( + &self, + client_id: u32, + sequence_number: u32, + error: String, + ) -> napi::Result { + let message: PasskeyMessage<()> = PasskeyMessage { + sequence_number, + value: Err(BitwardenError::Internal(error)), + }; + self.send(client_id, serde_json::to_string(&message).unwrap()) + } + + // TODO: Add a way to send a message to a specific client? + fn send(&self, _client_id: u32, message: String) -> napi::Result { + self.server + .send(message) + .map_err(|e| { + napi::Error::from_reason(format!("Error sending message - Error: {e} - {e:?}")) + }) + // NAPI doesn't support u64 or usize, so we need to convert to u32 + .map(|u| u32::try_from(u).unwrap_or_default()) + } + } } #[napi] diff --git a/apps/desktop/macos/.gitignore b/apps/desktop/macos/.gitignore new file mode 100644 index 00000000000..e5d4324b213 --- /dev/null +++ b/apps/desktop/macos/.gitignore @@ -0,0 +1 @@ +BitwardenMacosProvider.swift diff --git a/apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift b/apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift index 4d03bf97e6c..dbaa8517086 100644 --- a/apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift +++ b/apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift @@ -9,8 +9,46 @@ import AuthenticationServices import os class CredentialProviderViewController: ASCredentialProviderViewController { - let logger = Logger() + let logger: Logger + + // There is something a bit strange about the initialization/deinitialization in this class. + // Sometimes deinit won't be called after a request has successfully finished, + // which would leave this class hanging in memory and the IPC connection open. + // + // If instead I make this a static, the deinit gets called correctly after each request. + // I think we still might want a static regardless, to be able to reuse the connection if possible. + static let client: MacOsProviderClient = { + let instance = MacOsProviderClient.connect() + // setup code + return instance + }() + + init() { + logger = Logger(subsystem: "com.bitwarden.desktop.autofill-extension", category: "credential-provider") + + logger.log("[autofill-extension] initializing extension") + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + logger.log("[autofill-extension] deinitializing extension") + } + + + @IBAction func cancel(_ sender: AnyObject?) { + self.extensionContext.cancelRequest(withError: NSError(domain: ASExtensionErrorDomain, code: ASExtensionError.userCanceled.rawValue)) + } + + @IBAction func passwordSelected(_ sender: AnyObject?) { + let passwordCredential = ASPasswordCredential(user: "j_appleseed", password: "apple1234") + self.extensionContext.completeRequest(withSelectedCredential: passwordCredential, completionHandler: nil) + } + /* Implement this method if your extension supports showing credentials in the QuickType bar. When the user selects a credential from your app, this method will be called with the @@ -21,7 +59,14 @@ class CredentialProviderViewController: ASCredentialProviderViewController { */ + // Deprecated override func provideCredentialWithoutUserInteraction(for credentialIdentity: ASPasswordCredentialIdentity) { + logger.log("[autofill-extension] provideCredentialWithoutUserInteraction called \(credentialIdentity)") + logger.log("[autofill-extension] user \(credentialIdentity.user)") + logger.log("[autofill-extension] id \(credentialIdentity.recordIdentifier ?? "")") + logger.log("[autofill-extension] sid \(credentialIdentity.serviceIdentifier.identifier)") + logger.log("[autofill-extension] sidt \(credentialIdentity.serviceIdentifier.type.rawValue)") + // let databaseIsUnlocked = true // if (databaseIsUnlocked) { let passwordCredential = ASPasswordCredential(user: credentialIdentity.user, password: "example1234") @@ -31,6 +76,67 @@ class CredentialProviderViewController: ASCredentialProviderViewController { // } } + override func provideCredentialWithoutUserInteraction(for credentialRequest: any ASCredentialRequest) { + if let request = credentialRequest as? ASPasskeyCredentialRequest { + if let passkeyIdentity = request.credentialIdentity as? ASPasskeyCredentialIdentity { + + logger.log("[autofill-extension] provideCredentialWithoutUserInteraction2(passkey) called \(request)") + + class CallbackImpl: PreparePasskeyAssertionCallback { + let ctx: ASCredentialProviderExtensionContext + required init(_ ctx: ASCredentialProviderExtensionContext) { + self.ctx = ctx + } + + func onComplete(credential: PasskeyAssertionResponse) { + ctx.completeAssertionRequest(using: ASPasskeyAssertionCredential( + userHandle: credential.userHandle, + relyingParty: credential.rpId, + signature: credential.signature, + clientDataHash: credential.clientDataHash, + authenticatorData: credential.authenticatorData, + credentialID: credential.credentialId + )) + } + + func onError(error: BitwardenError) { + ctx.cancelRequest(withError: error) + } + } + + let userVerification = switch request.userVerificationPreference { + case .preferred: + UserVerification.preferred + case .required: + UserVerification.required + default: + UserVerification.discouraged + } + + let req = PasskeyAssertionRequest( + rpId: passkeyIdentity.relyingPartyIdentifier, + credentialId: passkeyIdentity.credentialID, + userName: passkeyIdentity.userName, + userHandle: passkeyIdentity.userHandle, + recordIdentifier: passkeyIdentity.recordIdentifier, + clientDataHash: request.clientDataHash, + userVerification: userVerification + ) + + CredentialProviderViewController.client.preparePasskeyAssertion(request: req, callback: CallbackImpl(self.extensionContext)) + return + } + } + + if let request = credentialRequest as? ASPasswordCredentialRequest { + logger.log("[autofill-extension] provideCredentialWithoutUserInteraction2(password) called \(request)") + return; + } + + logger.log("[autofill-extension] provideCredentialWithoutUserInteraction2 called wrong") + self.extensionContext.cancelRequest(withError: BitwardenError.Internal("Invalid authentication request")) + } + /* Implement this method if provideCredentialWithoutUserInteraction(for:) can fail with ASExtensionError.userInteractionRequired. In this case, the system may present your extension's @@ -41,34 +147,65 @@ class CredentialProviderViewController: ASCredentialProviderViewController { } */ - @IBAction func cancel(_ sender: AnyObject?) { - self.extensionContext.cancelRequest(withError: NSError(domain: ASExtensionErrorDomain, code: ASExtensionError.userCanceled.rawValue)) - } - @IBAction func passwordSelected(_ sender: AnyObject?) { - let passwordCredential = ASPasswordCredential(user: "j_appleseed", password: "apple1234") - self.extensionContext.completeRequest(withSelectedCredential: passwordCredential, completionHandler: nil) - } - override func prepareInterfaceForExtensionConfiguration() { logger.log("[autofill-extension] prepareInterfaceForExtensionConfiguration called") } override func prepareInterface(forPasskeyRegistration registrationRequest: ASCredentialRequest) { - logger.log("[autofill-extension] prepare interface for registration request \(registrationRequest.description)") + if let request = registrationRequest as? ASPasskeyCredentialRequest { + if let passkeyIdentity = registrationRequest.credentialIdentity as? ASPasskeyCredentialIdentity { + class CallbackImpl: PreparePasskeyRegistrationCallback { + let ctx: ASCredentialProviderExtensionContext + required init(_ ctx: ASCredentialProviderExtensionContext) { + self.ctx = ctx + } -// self.extensionContext.cancelRequest(withError: ExampleError.nope) - } + func onComplete(credential: PasskeyRegistrationResponse) { + ctx.completeRegistrationRequest(using: ASPasskeyRegistrationCredential( + relyingParty: credential.rpId, + clientDataHash: credential.clientDataHash, + credentialID: credential.credentialId, + attestationObject: credential.attestationObject + )) + } - override func prepareInterfaceToProvideCredential(for credentialRequest: ASCredentialRequest) { - logger.log("[autofill-extension] prepare interface for credential request \(credentialRequest.description)") + func onError(error: BitwardenError) { + ctx.cancelRequest(withError: error) + } + } + + let userVerification = switch request.userVerificationPreference { + case .preferred: + UserVerification.preferred + case .required: + UserVerification.required + default: + UserVerification.discouraged + } + + let req = PasskeyRegistrationRequest( + rpId: passkeyIdentity.relyingPartyIdentifier, + userName: passkeyIdentity.userName, + userHandle: passkeyIdentity.userHandle, + clientDataHash: request.clientDataHash, + userVerification: userVerification, + supportedAlgorithms: request.supportedAlgorithms.map{ Int32($0.rawValue) } + ) + CredentialProviderViewController.client.preparePasskeyRegistration(request: req, callback: CallbackImpl(self.extensionContext)) + return + } + } + + // If we didn't get a passkey, return an error + self.extensionContext.cancelRequest(withError: BitwardenError.Internal("Invalid registration request")) } /* - Prepare your UI to list available credentials for the user to choose from. The items in - 'serviceIdentifiers' describe the service the user is logging in to, so your extension can - prioritize the most relevant credentials in the list. - */ + Prepare your UI to list available credentials for the user to choose from. The items in + 'serviceIdentifiers' describe the service the user is logging in to, so your extension can + prioritize the most relevant credentials in the list. + */ override func prepareCredentialList(for serviceIdentifiers: [ASCredentialServiceIdentifier]) { logger.log("[autofill-extension] prepareCredentialList for serviceIdentifiers: \(serviceIdentifiers.count)") @@ -77,18 +214,13 @@ class CredentialProviderViewController: ASCredentialProviderViewController { } } - override func prepareInterfaceToProvideCredential(for credentialIdentity: ASPasswordCredentialIdentity) { - logger.log("[autofill-extension] prepareInterfaceToProvideCredential for credentialIdentity: \(credentialIdentity.user)") - } - override func prepareCredentialList(for serviceIdentifiers: [ASCredentialServiceIdentifier], requestParameters: ASPasskeyCredentialRequestParameters) { logger.log("[autofill-extension] prepareCredentialList(passkey) for serviceIdentifiers: \(serviceIdentifiers.count)") - + logger.log("request parameters: \(requestParameters.relyingPartyIdentifier)") + for serviceIdentifier in serviceIdentifiers { logger.log(" service: \(serviceIdentifier.identifier)") } - - logger.log("request parameters: \(requestParameters.relyingPartyIdentifier)") } } diff --git a/apps/desktop/macos/autofill-extension/autofill_extension.entitlements b/apps/desktop/macos/autofill-extension/autofill_extension.entitlements index 2e600a8d529..86c7195768e 100644 --- a/apps/desktop/macos/autofill-extension/autofill_extension.entitlements +++ b/apps/desktop/macos/autofill-extension/autofill_extension.entitlements @@ -6,5 +6,9 @@ com.apple.security.app-sandbox + com.apple.security.application-groups + + LTZ2PFU5D6.com.bitwarden.desktop + diff --git a/apps/desktop/macos/desktop.xcodeproj/project.pbxproj b/apps/desktop/macos/desktop.xcodeproj/project.pbxproj index 313b158895c..2ac467f3289 100644 --- a/apps/desktop/macos/desktop.xcodeproj/project.pbxproj +++ b/apps/desktop/macos/desktop.xcodeproj/project.pbxproj @@ -7,12 +7,16 @@ objects = { /* Begin PBXBuildFile section */ + 3368DB392C654B8100896B75 /* BitwardenMacosProviderFFI.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3368DB382C654B8100896B75 /* BitwardenMacosProviderFFI.xcframework */; }; + 3368DB3B2C654F3800896B75 /* BitwardenMacosProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3368DB3A2C654F3800896B75 /* BitwardenMacosProvider.swift */; }; E1DF713F2B342F6900F29026 /* AuthenticationServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E1DF713E2B342F6900F29026 /* AuthenticationServices.framework */; }; E1DF71422B342F6900F29026 /* CredentialProviderViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1DF71412B342F6900F29026 /* CredentialProviderViewController.swift */; }; E1DF71452B342F6900F29026 /* CredentialProviderViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = E1DF71432B342F6900F29026 /* CredentialProviderViewController.xib */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + 3368DB382C654B8100896B75 /* BitwardenMacosProviderFFI.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = BitwardenMacosProviderFFI.xcframework; path = ../desktop_native/macos_provider/BitwardenMacosProviderFFI.xcframework; sourceTree = ""; }; + 3368DB3A2C654F3800896B75 /* BitwardenMacosProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BitwardenMacosProvider.swift; sourceTree = ""; }; 968ED08A2C52A47200FFFEE6 /* Production.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Production.xcconfig; sourceTree = ""; }; E1DF713C2B342F6900F29026 /* autofill-extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "autofill-extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; E1DF713E2B342F6900F29026 /* AuthenticationServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AuthenticationServices.framework; path = System/Library/Frameworks/AuthenticationServices.framework; sourceTree = SDKROOT; }; @@ -28,6 +32,7 @@ buildActionMask = 2147483647; files = ( E1DF713F2B342F6900F29026 /* AuthenticationServices.framework in Frameworks */, + 3368DB392C654B8100896B75 /* BitwardenMacosProviderFFI.xcframework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -56,6 +61,7 @@ isa = PBXGroup; children = ( E1DF713E2B342F6900F29026 /* AuthenticationServices.framework */, + 3368DB382C654B8100896B75 /* BitwardenMacosProviderFFI.xcframework */, ); name = Frameworks; sourceTree = ""; @@ -63,6 +69,7 @@ E1DF71402B342F6900F29026 /* autofill-extension */ = { isa = PBXGroup; children = ( + 3368DB3A2C654F3800896B75 /* BitwardenMacosProvider.swift */, E1DF71412B342F6900F29026 /* CredentialProviderViewController.swift */, E1DF71432B342F6900F29026 /* CredentialProviderViewController.xib */, E1DF71462B342F6900F29026 /* Info.plist */, @@ -140,6 +147,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 3368DB3B2C654F3800896B75 /* BitwardenMacosProvider.swift in Sources */, E1DF71422B342F6900F29026 /* CredentialProviderViewController.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 99953603e45..10d5ded3448 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -23,7 +23,7 @@ "build:dev": "concurrently -n Main,Rend -c yellow,cyan \"npm run build:main:dev\" \"npm run build:renderer:dev\"", "build:preload": "cross-env NODE_ENV=production webpack --config webpack.preload.js", "build:preload:watch": "cross-env NODE_ENV=production webpack --config webpack.preload.js --watch", - "build:macos-extension": "node scripts/build-macos-extension.js", + "build:macos-extension": "./desktop_native/macos_provider/build.sh && node scripts/build-macos-extension.js", "build:main": "cross-env NODE_ENV=production webpack --config webpack.main.js", "build:main:dev": "npm run build-native && cross-env NODE_ENV=development webpack --config webpack.main.js", "build:main:watch": "npm run build-native && cross-env NODE_ENV=development webpack --config webpack.main.js --watch", diff --git a/apps/desktop/src/app/services/services.module.ts b/apps/desktop/src/app/services/services.module.ts index ccce1e3bd7c..8fa33215eb5 100644 --- a/apps/desktop/src/app/services/services.module.ts +++ b/apps/desktop/src/app/services/services.module.ts @@ -56,6 +56,8 @@ import { ConfigService } from "@bitwarden/common/platform/abstractions/config/co import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; +import { Fido2AuthenticatorService as Fido2AuthenticatorServiceAbstraction } from "@bitwarden/common/platform/abstractions/fido2/fido2-authenticator.service.abstraction"; +import { Fido2UserInterfaceService as Fido2UserInterfaceServiceAbstraction } from "@bitwarden/common/platform/abstractions/fido2/fido2-user-interface.service.abstraction"; import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service"; import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service"; import { KeyGenerationService as KeyGenerationServiceAbstraction } from "@bitwarden/common/platform/abstractions/key-generation.service"; @@ -73,6 +75,7 @@ import { Message, MessageListener, MessageSender } from "@bitwarden/common/platf // eslint-disable-next-line no-restricted-imports -- Used for dependency injection import { SubjectMessageSender } from "@bitwarden/common/platform/messaging/internal"; import { TaskSchedulerService } from "@bitwarden/common/platform/scheduling"; +import { Fido2AuthenticatorService } from "@bitwarden/common/platform/services/fido2/fido2-authenticator.service"; import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service"; import { DefaultSdkClientFactory } from "@bitwarden/common/platform/services/sdk/default-sdk-client-factory"; import { NoopSdkClientFactory } from "@bitwarden/common/platform/services/sdk/noop-sdk-client-factory"; @@ -80,6 +83,7 @@ import { SystemService } from "@bitwarden/common/platform/services/system.servic import { GlobalStateProvider, StateProvider } from "@bitwarden/common/platform/state"; // eslint-disable-next-line import/no-restricted-paths -- Implementation for memory storage import { MemoryStorageService as MemoryStorageServiceForStateProviders } from "@bitwarden/common/platform/state/storage/memory-storage.service"; +import { SyncService } from "@bitwarden/common/platform/sync"; import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; import { VaultTimeoutStringType } from "@bitwarden/common/types/vault-timeout.type"; import { CipherService as CipherServiceAbstraction } from "@bitwarden/common/vault/abstractions/cipher.service"; @@ -99,6 +103,7 @@ import { DesktopAutofillSettingsService } from "../../autofill/services/desktop- import { DesktopAutofillService } from "../../autofill/services/desktop-autofill.service"; import { ElectronBiometricsService } from "../../key-management/biometrics/electron-biometrics.service"; import { flagEnabled } from "../../platform/flags"; +import { DesktopFido2UserInterfaceService } from "../../platform/services/desktop-fido2-user-interface.service"; import { DesktopSettingsService } from "../../platform/services/desktop-settings.service"; import { ElectronKeyService } from "../../platform/services/electron-key.service"; import { ElectronLogRendererService } from "../../platform/services/electron-log.renderer.service"; @@ -309,7 +314,29 @@ const safeProviders: SafeProvider[] = [ }), safeProvider({ provide: DesktopAutofillService, - deps: [LogService, CipherServiceAbstraction, ConfigService], + deps: [ + LogService, + CipherServiceAbstraction, + ConfigService, + Fido2AuthenticatorServiceAbstraction, + AccountService, + ], + }), + safeProvider({ + provide: Fido2UserInterfaceServiceAbstraction, + useClass: DesktopFido2UserInterfaceService, + deps: [AuthServiceAbstraction, CipherServiceAbstraction, AccountService, LogService], + }), + safeProvider({ + provide: Fido2AuthenticatorServiceAbstraction, + useClass: Fido2AuthenticatorService, + deps: [ + CipherServiceAbstraction, + Fido2UserInterfaceServiceAbstraction, + SyncService, + AccountService, + LogService, + ], }), safeProvider({ provide: NativeMessagingManifestService, diff --git a/apps/desktop/src/autofill/preload.ts b/apps/desktop/src/autofill/preload.ts index 9ce5e1319fd..494544f5858 100644 --- a/apps/desktop/src/autofill/preload.ts +++ b/apps/desktop/src/autofill/preload.ts @@ -1,9 +1,92 @@ import { ipcRenderer } from "electron"; +import type { autofill } from "@bitwarden/desktop-napi"; + import { Command } from "../platform/main/autofill/command"; import { RunCommandParams, RunCommandResult } from "../platform/main/autofill/native-autofill.main"; export default { runCommand: (params: RunCommandParams): Promise> => ipcRenderer.invoke("autofill.runCommand", params), + + listenPasskeyRegistration: ( + fn: ( + clientId: number, + sequenceNumber: number, + request: autofill.PasskeyRegistrationRequest, + completeCallback: ( + error: Error | null, + response: autofill.PasskeyRegistrationResponse, + ) => void, + ) => void, + ) => { + ipcRenderer.on( + "autofill.passkeyRegistration", + ( + event, + data: { + clientId: number; + sequenceNumber: number; + request: autofill.PasskeyRegistrationRequest; + }, + ) => { + const { clientId, sequenceNumber, request } = data; + fn(clientId, sequenceNumber, request, (error, response) => { + if (error) { + ipcRenderer.send("autofill.completeError", { + clientId, + sequenceNumber, + error: error.message, + }); + return; + } + + ipcRenderer.send("autofill.completePasskeyRegistration", { + clientId, + sequenceNumber, + response, + }); + }); + }, + ); + }, + + listenPasskeyAssertion: ( + fn: ( + clientId: number, + sequenceNumber: number, + request: autofill.PasskeyAssertionRequest, + completeCallback: (error: Error | null, response: autofill.PasskeyAssertionResponse) => void, + ) => void, + ) => { + ipcRenderer.on( + "autofill.passkeyAssertion", + ( + event, + data: { + clientId: number; + sequenceNumber: number; + request: autofill.PasskeyAssertionRequest; + }, + ) => { + const { clientId, sequenceNumber, request } = data; + fn(clientId, sequenceNumber, request, (error, response) => { + if (error) { + ipcRenderer.send("autofill.completeError", { + clientId, + sequenceNumber, + error: error.message, + }); + return; + } + + ipcRenderer.send("autofill.completePasskeyAssertion", { + clientId, + sequenceNumber, + response, + }); + }); + }, + ); + }, }; diff --git a/apps/desktop/src/autofill/services/desktop-autofill.service.ts b/apps/desktop/src/autofill/services/desktop-autofill.service.ts index 8c5dd597850..1ce58596b34 100644 --- a/apps/desktop/src/autofill/services/desktop-autofill.service.ts +++ b/apps/desktop/src/autofill/services/desktop-autofill.service.ts @@ -1,12 +1,32 @@ import { Injectable, OnDestroy } from "@angular/core"; -import { EMPTY, Subject, distinctUntilChanged, mergeMap, switchMap, takeUntil } from "rxjs"; +import { autofill } from "desktop_native/napi"; +import { + EMPTY, + Subject, + distinctUntilChanged, + firstValueFrom, + map, + mergeMap, + switchMap, + takeUntil, +} from "rxjs"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { UriMatchStrategy } from "@bitwarden/common/models/domain/domain-service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { + Fido2AuthenticatorGetAssertionParams, + Fido2AuthenticatorGetAssertionResult, + Fido2AuthenticatorMakeCredentialResult, + Fido2AuthenticatorMakeCredentialsParams, + Fido2AuthenticatorService as Fido2AuthenticatorServiceAbstraction, +} from "@bitwarden/common/platform/abstractions/fido2/fido2-authenticator.service.abstraction"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { getCredentialsForAutofill } from "@bitwarden/common/platform/services/fido2/fido2-autofill-utils"; +import { Fido2Utils } from "@bitwarden/common/platform/services/fido2/fido2-utils"; +import { guidToRawFormat } from "@bitwarden/common/platform/services/fido2/guid-utils"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; @@ -26,6 +46,8 @@ export class DesktopAutofillService implements OnDestroy { private logService: LogService, private cipherService: CipherService, private configService: ConfigService, + private fido2AuthenticatorService: Fido2AuthenticatorServiceAbstraction, + private accountService: AccountService, ) {} async init() { @@ -47,6 +69,8 @@ export class DesktopAutofillService implements OnDestroy { takeUntil(this.destroy$), ) .subscribe(); + + this.listenIpc(); } /** Give metadata about all available credentials in the users vault */ @@ -114,6 +138,146 @@ export class DesktopAutofillService implements OnDestroy { }); } + listenIpc() { + ipc.autofill.listenPasskeyRegistration((clientId, sequenceNumber, request, callback) => { + this.logService.warning("listenPasskeyRegistration", clientId, sequenceNumber, request); + this.logService.warning( + "listenPasskeyRegistration2", + this.convertRegistrationRequest(request), + ); + + const controller = new AbortController(); + void this.fido2AuthenticatorService + .makeCredential(this.convertRegistrationRequest(request), null, controller) + .then((response) => { + callback(null, this.convertRegistrationResponse(request, response)); + }) + .catch((error) => { + this.logService.error("listenPasskeyRegistration error", error); + callback(error, null); + }); + }); + + ipc.autofill.listenPasskeyAssertion(async (clientId, sequenceNumber, request, callback) => { + this.logService.warning("listenPasskeyAssertion", clientId, sequenceNumber, request); + + // TODO: For some reason the credentialId is passed as an empty array in the request, so we need to + // get it from the cipher. For that we use the recordIdentifier, which is the cipherId. + if (request.recordIdentifier && request.credentialId.length === 0) { + const cipher = await this.cipherService.get(request.recordIdentifier); + if (!cipher) { + this.logService.error("listenPasskeyAssertion error", "Cipher not found"); + callback(new Error("Cipher not found"), null); + return; + } + + const activeUserId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a?.id)), + ); + + const decrypted = await cipher.decrypt( + await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId), + ); + + const fido2Credential = decrypted.login.fido2Credentials?.[0]; + if (!fido2Credential) { + this.logService.error("listenPasskeyAssertion error", "Fido2Credential not found"); + callback(new Error("Fido2Credential not found"), null); + return; + } + + request.credentialId = Array.from( + guidToRawFormat(decrypted.login.fido2Credentials?.[0].credentialId), + ); + } + + const controller = new AbortController(); + void this.fido2AuthenticatorService + .getAssertion(this.convertAssertionRequest(request), null, controller) + .then((response) => { + callback(null, this.convertAssertionResponse(request, response)); + }) + .catch((error) => { + this.logService.error("listenPasskeyAssertion error", error); + callback(error, null); + }); + }); + } + + private convertRegistrationRequest( + request: autofill.PasskeyRegistrationRequest, + ): Fido2AuthenticatorMakeCredentialsParams { + return { + hash: new Uint8Array(request.clientDataHash), + rpEntity: { + name: request.rpId, + id: request.rpId, + }, + userEntity: { + id: new Uint8Array(request.userHandle), + name: request.userName, + displayName: undefined, + icon: undefined, + }, + credTypesAndPubKeyAlgs: request.supportedAlgorithms.map((alg) => ({ + alg, + type: "public-key", + })), + excludeCredentialDescriptorList: [], + requireResidentKey: true, + requireUserVerification: + request.userVerification === "required" || request.userVerification === "preferred", + fallbackSupported: false, + }; + } + + private convertRegistrationResponse( + request: autofill.PasskeyRegistrationRequest, + response: Fido2AuthenticatorMakeCredentialResult, + ): autofill.PasskeyRegistrationResponse { + return { + rpId: request.rpId, + clientDataHash: request.clientDataHash, + credentialId: Array.from(Fido2Utils.bufferSourceToUint8Array(response.credentialId)), + attestationObject: Array.from( + Fido2Utils.bufferSourceToUint8Array(response.attestationObject), + ), + }; + } + + private convertAssertionRequest( + request: autofill.PasskeyAssertionRequest, + ): Fido2AuthenticatorGetAssertionParams { + return { + rpId: request.rpId, + hash: new Uint8Array(request.clientDataHash), + allowCredentialDescriptorList: [ + { + id: new Uint8Array(request.credentialId), + type: "public-key", + }, + ], + extensions: {}, + requireUserVerification: + request.userVerification === "required" || request.userVerification === "preferred", + fallbackSupported: false, + }; + } + + private convertAssertionResponse( + request: autofill.PasskeyAssertionRequest, + response: Fido2AuthenticatorGetAssertionResult, + ): autofill.PasskeyAssertionResponse { + return { + userHandle: Array.from(response.selectedCredential.userHandle), + rpId: request.rpId, + signature: Array.from(response.signature), + clientDataHash: request.clientDataHash, + authenticatorData: Array.from(response.authenticatorData), + credentialId: Array.from(response.selectedCredential.id), + }; + } + ngOnDestroy(): void { this.destroy$.next(); this.destroy$.complete(); diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index b19f25256b2..a4842249c93 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -261,7 +261,7 @@ export class Main { new EphemeralValueStorageService(); new SSOLocalhostCallbackService(this.environmentService, this.messagingService); - this.nativeAutofillMain = new NativeAutofillMain(this.logService); + this.nativeAutofillMain = new NativeAutofillMain(this.logService, this.windowMain); void this.nativeAutofillMain.init(); } diff --git a/apps/desktop/src/platform/main/autofill/native-autofill.main.ts b/apps/desktop/src/platform/main/autofill/native-autofill.main.ts index b4b7895e8ac..1465831340f 100644 --- a/apps/desktop/src/platform/main/autofill/native-autofill.main.ts +++ b/apps/desktop/src/platform/main/autofill/native-autofill.main.ts @@ -3,6 +3,8 @@ import { ipcMain } from "electron"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { autofill } from "@bitwarden/desktop-napi"; +import { WindowMain } from "../../../main/window.main"; + import { CommandDefinition } from "./command"; export type RunCommandParams = { @@ -14,7 +16,12 @@ export type RunCommandParams = { export type RunCommandResult = C["output"]; export class NativeAutofillMain { - constructor(private logService: LogService) {} + private ipcServer: autofill.IpcServer | null; + + constructor( + private logService: LogService, + private windowMain: WindowMain, + ) {} async init() { ipcMain.handle( @@ -26,6 +33,52 @@ export class NativeAutofillMain { return this.runCommand(params); }, ); + + this.ipcServer = await autofill.IpcServer.listen( + "autofill", + // RegistrationCallback + (error, clientId, sequenceNumber, request) => { + if (error) { + this.logService.error("autofill.IpcServer.registration", error); + return; + } + this.windowMain.win.webContents.send("autofill.passkeyRegistration", { + clientId, + sequenceNumber, + request, + }); + }, + // AssertionCallback + (error, clientId, sequenceNumber, request) => { + if (error) { + this.logService.error("autofill.IpcServer.assertion", error); + return; + } + this.windowMain.win.webContents.send("autofill.passkeyAssertion", { + clientId, + sequenceNumber, + request, + }); + }, + ); + + ipcMain.on("autofill.completePasskeyRegistration", (event, data) => { + this.logService.warning("autofill.completePasskeyRegistration", data); + const { clientId, sequenceNumber, response } = data; + this.ipcServer.completeRegistration(clientId, sequenceNumber, response); + }); + + ipcMain.on("autofill.completePasskeyAssertion", (event, data) => { + this.logService.warning("autofill.completePasskeyAssertion", data); + const { clientId, sequenceNumber, response } = data; + this.ipcServer.completeAssertion(clientId, sequenceNumber, response); + }); + + ipcMain.on("autofill.completeError", (event, data) => { + this.logService.warning("autofill.completeError", data); + const { clientId, sequenceNumber, error } = data; + this.ipcServer.completeAssertion(clientId, sequenceNumber, error); + }); } private async runCommand( diff --git a/apps/desktop/src/platform/services/desktop-fido2-user-interface.service.ts b/apps/desktop/src/platform/services/desktop-fido2-user-interface.service.ts new file mode 100644 index 00000000000..116e8989e02 --- /dev/null +++ b/apps/desktop/src/platform/services/desktop-fido2-user-interface.service.ts @@ -0,0 +1,125 @@ +import { firstValueFrom, map } from "rxjs"; + +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; +import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; +import { + Fido2UserInterfaceService as Fido2UserInterfaceServiceAbstraction, + Fido2UserInterfaceSession, + NewCredentialParams, + PickCredentialParams, +} from "@bitwarden/common/platform/abstractions/fido2/fido2-user-interface.service.abstraction"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { CipherRepromptType, CipherType, SecureNoteType } from "@bitwarden/common/vault/enums"; +import { CardView } from "@bitwarden/common/vault/models/view/card.view"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { IdentityView } from "@bitwarden/common/vault/models/view/identity.view"; +import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view"; +import { LoginView } from "@bitwarden/common/vault/models/view/login.view"; +import { SecureNoteView } from "@bitwarden/common/vault/models/view/secure-note.view"; + +// TODO: This should be moved to the directory of whatever team takes this on + +export class DesktopFido2UserInterfaceService + implements Fido2UserInterfaceServiceAbstraction +{ + constructor( + private authService: AuthService, + private cipherService: CipherService, + private accountService: AccountService, + private logService: LogService, + ) {} + + async newSession( + fallbackSupported: boolean, + _tab: void, + abortController?: AbortController, + ): Promise { + this.logService.warning("newSession", fallbackSupported, abortController); + return new DesktopFido2UserInterfaceSession( + this.authService, + this.cipherService, + this.accountService, + this.logService, + ); + } +} + +export class DesktopFido2UserInterfaceSession implements Fido2UserInterfaceSession { + constructor( + private authService: AuthService, + private cipherService: CipherService, + private accountService: AccountService, + private logService: LogService, + ) {} + + async pickCredential({ + cipherIds, + userVerification, + }: PickCredentialParams): Promise<{ cipherId: string; userVerified: boolean }> { + this.logService.warning("pickCredential", cipherIds, userVerification); + + return { cipherId: cipherIds[0], userVerified: userVerification }; + } + + async confirmNewCredential({ + credentialName, + userName, + userVerification, + rpId, + }: NewCredentialParams): Promise<{ cipherId: string; userVerified: boolean }> { + this.logService.warning( + "confirmNewCredential", + credentialName, + userName, + userVerification, + rpId, + ); + + // Store the passkey on a new cipher to avoid replacing something important + const cipher = new CipherView(); + cipher.name = credentialName; + + cipher.type = CipherType.Login; + cipher.login = new LoginView(); + cipher.login.username = userName; + cipher.login.uris = [new LoginUriView()]; + cipher.login.uris[0].uri = "https://" + rpId; + cipher.card = new CardView(); + cipher.identity = new IdentityView(); + cipher.secureNote = new SecureNoteView(); + cipher.secureNote.type = SecureNoteType.Generic; + cipher.reprompt = CipherRepromptType.None; + + const activeUserId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a?.id)), + ); + + const encCipher = await this.cipherService.encrypt(cipher, activeUserId); + const createdCipher = await this.cipherService.createWithServer(encCipher); + + return { cipherId: createdCipher.id, userVerified: userVerification }; + } + + async informExcludedCredential(existingCipherIds: string[]): Promise { + this.logService.warning("informExcludedCredential", existingCipherIds); + } + + async ensureUnlockedVault(): Promise { + this.logService.warning("ensureUnlockedVault"); + + const status = await firstValueFrom(this.authService.activeAccountStatus$); + if (status !== AuthenticationStatus.Unlocked) { + throw new Error("Vault is not unlocked"); + } + } + + async informCredentialNotFound(): Promise { + this.logService.warning("informCredentialNotFound"); + } + + async close() { + this.logService.warning("close"); + } +} diff --git a/libs/common/src/platform/abstractions/fido2/fido2-authenticator.service.abstraction.ts b/libs/common/src/platform/abstractions/fido2/fido2-authenticator.service.abstraction.ts index bacbafcb323..e9e68ca92c3 100644 --- a/libs/common/src/platform/abstractions/fido2/fido2-authenticator.service.abstraction.ts +++ b/libs/common/src/platform/abstractions/fido2/fido2-authenticator.service.abstraction.ts @@ -8,7 +8,7 @@ import { Fido2CredentialView } from "../../../vault/models/view/fido2-credential * * The authenticator provides key management and cryptographic signatures. */ -export abstract class Fido2AuthenticatorService { +export abstract class Fido2AuthenticatorService { /** * Create and save a new credential as described in: * https://www.w3.org/TR/webauthn-3/#sctn-op-make-cred @@ -19,7 +19,7 @@ export abstract class Fido2AuthenticatorService { **/ makeCredential: ( params: Fido2AuthenticatorMakeCredentialsParams, - tab: chrome.tabs.Tab, + window: ParentWindowReference, abortController?: AbortController, ) => Promise; @@ -33,7 +33,7 @@ export abstract class Fido2AuthenticatorService { */ getAssertion: ( params: Fido2AuthenticatorGetAssertionParams, - tab: chrome.tabs.Tab, + window: ParentWindowReference, abortController?: AbortController, ) => Promise; diff --git a/libs/common/src/platform/abstractions/fido2/fido2-client.service.abstraction.ts b/libs/common/src/platform/abstractions/fido2/fido2-client.service.abstraction.ts index d9cb20995ad..55d9cce8049 100644 --- a/libs/common/src/platform/abstractions/fido2/fido2-client.service.abstraction.ts +++ b/libs/common/src/platform/abstractions/fido2/fido2-client.service.abstraction.ts @@ -15,7 +15,7 @@ export type UserVerification = "discouraged" | "preferred" | "required"; * It is responsible for both marshalling the inputs for the underlying authenticator operations, * and for returning the results of the latter operations to the Web Authentication API's callers. */ -export abstract class Fido2ClientService { +export abstract class Fido2ClientService { isFido2FeatureEnabled: (hostname: string, origin: string) => Promise; /** @@ -28,7 +28,7 @@ export abstract class Fido2ClientService { */ createCredential: ( params: CreateCredentialParams, - tab: chrome.tabs.Tab, + window: ParentWindowReference, abortController?: AbortController, ) => Promise; @@ -43,7 +43,7 @@ export abstract class Fido2ClientService { */ assertCredential: ( params: AssertCredentialParams, - tab: chrome.tabs.Tab, + window: ParentWindowReference, abortController?: AbortController, ) => Promise; } diff --git a/libs/common/src/platform/abstractions/fido2/fido2-user-interface.service.abstraction.ts b/libs/common/src/platform/abstractions/fido2/fido2-user-interface.service.abstraction.ts index 49752138527..7beefc3b4cc 100644 --- a/libs/common/src/platform/abstractions/fido2/fido2-user-interface.service.abstraction.ts +++ b/libs/common/src/platform/abstractions/fido2/fido2-user-interface.service.abstraction.ts @@ -61,7 +61,7 @@ export interface PickCredentialParams { * The service is session based and is intended to be used by the FIDO2 authenticator to open a window, * and then use this window to ask the user for input and/or display messages to the user. */ -export abstract class Fido2UserInterfaceService { +export abstract class Fido2UserInterfaceService { /** * Creates a new session. * Note: This will not necessarily open a window until it is needed to request something from the user. @@ -71,7 +71,7 @@ export abstract class Fido2UserInterfaceService { */ newSession: ( fallbackSupported: boolean, - tab: chrome.tabs.Tab, + window: ParentWindowReference, abortController?: AbortController, ) => Promise; } diff --git a/libs/common/src/platform/services/fido2/fido2-authenticator.service.spec.ts b/libs/common/src/platform/services/fido2/fido2-authenticator.service.spec.ts index e3f79ff9d58..226f4c2cfe9 100644 --- a/libs/common/src/platform/services/fido2/fido2-authenticator.service.spec.ts +++ b/libs/common/src/platform/services/fido2/fido2-authenticator.service.spec.ts @@ -30,6 +30,8 @@ import { parseCredentialId } from "./credential-id-utils"; import { AAGUID, Fido2AuthenticatorService } from "./fido2-authenticator.service"; import { Fido2Utils } from "./fido2-utils"; +type ParentWindowReference = string; + const RpId = "bitwarden.com"; describe("FidoAuthenticatorService", () => { @@ -41,16 +43,16 @@ describe("FidoAuthenticatorService", () => { }); let cipherService!: MockProxy; - let userInterface!: MockProxy; + let userInterface!: MockProxy>; let userInterfaceSession!: MockProxy; let syncService!: MockProxy; let accountService!: MockProxy; - let authenticator!: Fido2AuthenticatorService; - let tab!: chrome.tabs.Tab; + let authenticator!: Fido2AuthenticatorService; + let windowReference!: ParentWindowReference; beforeEach(async () => { cipherService = mock(); - userInterface = mock(); + userInterface = mock>(); userInterfaceSession = mock(); userInterface.newSession.mockResolvedValue(userInterfaceSession); syncService = mock({ @@ -63,7 +65,7 @@ describe("FidoAuthenticatorService", () => { syncService, accountService, ); - tab = { id: 123, windowId: 456 } as chrome.tabs.Tab; + windowReference = Utils.newGuid(); accountService.activeAccount$ = activeAccountSubject; }); @@ -78,19 +80,21 @@ describe("FidoAuthenticatorService", () => { // Spec: Check if at least one of the specified combinations of PublicKeyCredentialType and cryptographic parameters in credTypesAndPubKeyAlgs is supported. If not, return an error code equivalent to "NotSupportedError" and terminate the operation. it("should throw error when input does not contain any supported algorithms", async () => { const result = async () => - await authenticator.makeCredential(invalidParams.unsupportedAlgorithm, tab); + await authenticator.makeCredential(invalidParams.unsupportedAlgorithm, windowReference); await expect(result).rejects.toThrowError(Fido2AuthenticatorErrorCode.NotSupported); }); it("should throw error when requireResidentKey has invalid value", async () => { - const result = async () => await authenticator.makeCredential(invalidParams.invalidRk, tab); + const result = async () => + await authenticator.makeCredential(invalidParams.invalidRk, windowReference); await expect(result).rejects.toThrowError(Fido2AuthenticatorErrorCode.Unknown); }); it("should throw error when requireUserVerification has invalid value", async () => { - const result = async () => await authenticator.makeCredential(invalidParams.invalidUv, tab); + const result = async () => + await authenticator.makeCredential(invalidParams.invalidUv, windowReference); await expect(result).rejects.toThrowError(Fido2AuthenticatorErrorCode.Unknown); }); @@ -103,7 +107,7 @@ describe("FidoAuthenticatorService", () => { it.skip("should throw error if requireUserVerification is set to true", async () => { const params = await createParams({ requireUserVerification: true }); - const result = async () => await authenticator.makeCredential(params, tab); + const result = async () => await authenticator.makeCredential(params, windowReference); await expect(result).rejects.toThrowError(Fido2AuthenticatorErrorCode.Constraint); }); @@ -117,7 +121,7 @@ describe("FidoAuthenticatorService", () => { for (const p of Object.values(invalidParams)) { try { - await authenticator.makeCredential(p, tab); + await authenticator.makeCredential(p, windowReference); // eslint-disable-next-line no-empty } catch {} } @@ -158,7 +162,7 @@ describe("FidoAuthenticatorService", () => { userInterfaceSession.informExcludedCredential.mockResolvedValue(); try { - await authenticator.makeCredential(params, tab); + await authenticator.makeCredential(params, windowReference); // eslint-disable-next-line no-empty } catch {} @@ -169,7 +173,7 @@ describe("FidoAuthenticatorService", () => { it("should throw error", async () => { userInterfaceSession.informExcludedCredential.mockResolvedValue(); - const result = async () => await authenticator.makeCredential(params, tab); + const result = async () => await authenticator.makeCredential(params, windowReference); await expect(result).rejects.toThrowError(Fido2AuthenticatorErrorCode.NotAllowed); }); @@ -180,7 +184,7 @@ describe("FidoAuthenticatorService", () => { excludedCipher.organizationId = "someOrganizationId"; try { - await authenticator.makeCredential(params, tab); + await authenticator.makeCredential(params, windowReference); // eslint-disable-next-line no-empty } catch {} @@ -193,7 +197,7 @@ describe("FidoAuthenticatorService", () => { for (const p of Object.values(invalidParams)) { try { - await authenticator.makeCredential(p, tab); + await authenticator.makeCredential(p, windowReference); // eslint-disable-next-line no-empty } catch {} } @@ -230,7 +234,7 @@ describe("FidoAuthenticatorService", () => { userVerified: userVerification, }); - await authenticator.makeCredential(params, tab); + await authenticator.makeCredential(params, windowReference); expect(userInterfaceSession.confirmNewCredential).toHaveBeenCalledWith({ credentialName: params.rpEntity.name, @@ -250,7 +254,7 @@ describe("FidoAuthenticatorService", () => { }); cipherService.encrypt.mockResolvedValue(encryptedCipher as unknown as Cipher); - await authenticator.makeCredential(params, tab); + await authenticator.makeCredential(params, windowReference); const saved = cipherService.encrypt.mock.lastCall?.[0]; expect(saved).toEqual( @@ -288,7 +292,7 @@ describe("FidoAuthenticatorService", () => { }); const params = await createParams(); - const result = async () => await authenticator.makeCredential(params, tab); + const result = async () => await authenticator.makeCredential(params, windowReference); await expect(result).rejects.toThrowError(Fido2AuthenticatorErrorCode.NotAllowed); }); @@ -302,7 +306,7 @@ describe("FidoAuthenticatorService", () => { const encryptedCipher = { ...existingCipher, reprompt: CipherRepromptType.Password }; cipherService.get.mockResolvedValue(encryptedCipher as unknown as Cipher); - const result = async () => await authenticator.makeCredential(params, tab); + const result = async () => await authenticator.makeCredential(params, windowReference); await expect(result).rejects.toThrowError(Fido2AuthenticatorErrorCode.Unknown); }); @@ -317,7 +321,7 @@ describe("FidoAuthenticatorService", () => { cipherService.encrypt.mockResolvedValue(encryptedCipher as unknown as Cipher); cipherService.updateWithServer.mockRejectedValue(new Error("Internal error")); - const result = async () => await authenticator.makeCredential(params, tab); + const result = async () => await authenticator.makeCredential(params, windowReference); await expect(result).rejects.toThrowError(Fido2AuthenticatorErrorCode.Unknown); }); @@ -358,7 +362,7 @@ describe("FidoAuthenticatorService", () => { }); it("should return attestation object", async () => { - const result = await authenticator.makeCredential(params, tab); + const result = await authenticator.makeCredential(params, windowReference); const attestationObject = CBOR.decode( Fido2Utils.bufferSourceToUint8Array(result.attestationObject).buffer, @@ -455,7 +459,8 @@ describe("FidoAuthenticatorService", () => { describe("invalid input parameters", () => { it("should throw error when requireUserVerification has invalid value", async () => { - const result = async () => await authenticator.getAssertion(invalidParams.invalidUv, tab); + const result = async () => + await authenticator.getAssertion(invalidParams.invalidUv, windowReference); await expect(result).rejects.toThrowError(Fido2AuthenticatorErrorCode.Unknown); }); @@ -468,7 +473,7 @@ describe("FidoAuthenticatorService", () => { it.skip("should throw error if requireUserVerification is set to true", async () => { const params = await createParams({ requireUserVerification: true }); - const result = async () => await authenticator.getAssertion(params, tab); + const result = async () => await authenticator.getAssertion(params, windowReference); await expect(result).rejects.toThrowError(Fido2AuthenticatorErrorCode.Constraint); }); @@ -498,7 +503,7 @@ describe("FidoAuthenticatorService", () => { userInterfaceSession.informCredentialNotFound.mockResolvedValue(); try { - await authenticator.getAssertion(params, tab); + await authenticator.getAssertion(params, windowReference); // eslint-disable-next-line no-empty } catch {} @@ -513,7 +518,7 @@ describe("FidoAuthenticatorService", () => { userInterfaceSession.informCredentialNotFound.mockResolvedValue(); try { - await authenticator.getAssertion(params, tab); + await authenticator.getAssertion(params, windowReference); // eslint-disable-next-line no-empty } catch {} @@ -534,7 +539,7 @@ describe("FidoAuthenticatorService", () => { /** Spec: If credentialOptions is now empty, return an error code equivalent to "NotAllowedError" and terminate the operation. */ it("should throw error", async () => { - const result = async () => await authenticator.getAssertion(params, tab); + const result = async () => await authenticator.getAssertion(params, windowReference); await expect(result).rejects.toThrowError(Fido2AuthenticatorErrorCode.NotAllowed); }); @@ -573,7 +578,7 @@ describe("FidoAuthenticatorService", () => { userVerified: false, }); - await authenticator.getAssertion(params, tab); + await authenticator.getAssertion(params, windowReference); expect(userInterfaceSession.pickCredential).toHaveBeenCalledWith({ cipherIds: ciphers.map((c) => c.id), @@ -590,7 +595,7 @@ describe("FidoAuthenticatorService", () => { userVerified: false, }); - await authenticator.getAssertion(params, tab); + await authenticator.getAssertion(params, windowReference); expect(userInterfaceSession.pickCredential).toHaveBeenCalledWith({ cipherIds: [discoverableCiphers[0].id], @@ -608,7 +613,7 @@ describe("FidoAuthenticatorService", () => { userVerified: userVerification, }); - await authenticator.getAssertion(params, tab); + await authenticator.getAssertion(params, windowReference); expect(userInterfaceSession.pickCredential).toHaveBeenCalledWith({ cipherIds: ciphers.map((c) => c.id), @@ -625,7 +630,7 @@ describe("FidoAuthenticatorService", () => { userVerified: false, }); - const result = async () => await authenticator.getAssertion(params, tab); + const result = async () => await authenticator.getAssertion(params, windowReference); await expect(result).rejects.toThrowError(Fido2AuthenticatorErrorCode.NotAllowed); }); @@ -637,7 +642,7 @@ describe("FidoAuthenticatorService", () => { userVerified: false, }); - const result = async () => await authenticator.getAssertion(params, tab); + const result = async () => await authenticator.getAssertion(params, windowReference); await expect(result).rejects.toThrowError(Fido2AuthenticatorErrorCode.NotAllowed); }); @@ -686,7 +691,7 @@ describe("FidoAuthenticatorService", () => { cipherService.encrypt.mockResolvedValue(encrypted as any); ciphers[0].login.fido2Credentials[0].counter = 9000; - await authenticator.getAssertion(params, tab); + await authenticator.getAssertion(params, windowReference); expect(cipherService.updateWithServer).toHaveBeenCalledWith(encrypted); expect(cipherService.encrypt).toHaveBeenCalledWith( @@ -710,13 +715,13 @@ describe("FidoAuthenticatorService", () => { cipherService.encrypt.mockResolvedValue(encrypted as any); ciphers[0].login.fido2Credentials[0].counter = 0; - await authenticator.getAssertion(params, tab); + await authenticator.getAssertion(params, windowReference); expect(cipherService.updateWithServer).not.toHaveBeenCalled(); }); it("should return an assertion result", async () => { - const result = await authenticator.getAssertion(params, tab); + const result = await authenticator.getAssertion(params, windowReference); const encAuthData = result.authenticatorData; const rpIdHash = encAuthData.slice(0, 32); @@ -757,7 +762,7 @@ describe("FidoAuthenticatorService", () => { for (let i = 0; i < 10; ++i) { await init(); // Reset inputs - const result = await authenticator.getAssertion(params, tab); + const result = await authenticator.getAssertion(params, windowReference); const counter = result.authenticatorData.slice(33, 37); expect(counter).toEqual(new Uint8Array([0, 0, 0x23, 0x29])); // double check that the counter doesn't change @@ -774,7 +779,7 @@ describe("FidoAuthenticatorService", () => { it("should throw unkown error if creation fails", async () => { cipherService.updateWithServer.mockRejectedValue(new Error("Internal error")); - const result = async () => await authenticator.getAssertion(params, tab); + const result = async () => await authenticator.getAssertion(params, windowReference); await expect(result).rejects.toThrowError(Fido2AuthenticatorErrorCode.Unknown); }); diff --git a/libs/common/src/platform/services/fido2/fido2-authenticator.service.ts b/libs/common/src/platform/services/fido2/fido2-authenticator.service.ts index 34117e852ea..376f4dcdced 100644 --- a/libs/common/src/platform/services/fido2/fido2-authenticator.service.ts +++ b/libs/common/src/platform/services/fido2/fido2-authenticator.service.ts @@ -43,10 +43,12 @@ const KeyUsages: KeyUsage[] = ["sign"]; * * It is highly recommended that the W3C specification is used a reference when reading this code. */ -export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstraction { +export class Fido2AuthenticatorService + implements Fido2AuthenticatorServiceAbstraction +{ constructor( private cipherService: CipherService, - private userInterface: Fido2UserInterfaceService, + private userInterface: Fido2UserInterfaceService, private syncService: SyncService, private accountService: AccountService, private logService?: LogService, @@ -54,12 +56,12 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr async makeCredential( params: Fido2AuthenticatorMakeCredentialsParams, - tab: chrome.tabs.Tab, + window: ParentWindowReference, abortController?: AbortController, ): Promise { const userInterfaceSession = await this.userInterface.newSession( params.fallbackSupported, - tab, + window, abortController, ); @@ -209,12 +211,12 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr async getAssertion( params: Fido2AuthenticatorGetAssertionParams, - tab: chrome.tabs.Tab, + window: ParentWindowReference, abortController?: AbortController, ): Promise { const userInterfaceSession = await this.userInterface.newSession( params.fallbackSupported, - tab, + window, abortController, ); try { diff --git a/libs/common/src/platform/services/fido2/fido2-autofill-utils.ts b/libs/common/src/platform/services/fido2/fido2-autofill-utils.ts index 7ef705b95f9..31f6ce10e01 100644 --- a/libs/common/src/platform/services/fido2/fido2-autofill-utils.ts +++ b/libs/common/src/platform/services/fido2/fido2-autofill-utils.ts @@ -3,6 +3,9 @@ import { CipherType } from "../../../vault/enums"; import { CipherView } from "../../../vault/models/view/cipher.view"; import { Fido2CredentialAutofillView } from "../../../vault/models/view/fido2-credential-autofill.view"; +import { Utils } from "../../misc/utils"; + +import { parseCredentialId } from "./credential-id-utils"; // TODO: Move into Fido2AuthenticatorService export async function getCredentialsForAutofill( @@ -15,9 +18,14 @@ export async function getCredentialsForAutofill( ) .map((cipher) => { const credential = cipher.login.fido2Credentials[0]; + + // Credentials are stored as a GUID or b64 string with `b64.` prepended, + // but we need to return them as a URL-safe base64 string + const credId = Utils.fromBufferToUrlB64(parseCredentialId(credential.credentialId)); + return { cipherId: cipher.id, - credentialId: credential.credentialId, + credentialId: credId, rpId: credential.rpId, userHandle: credential.userHandle, userName: credential.userName, diff --git a/libs/common/src/platform/services/fido2/fido2-client.service.spec.ts b/libs/common/src/platform/services/fido2/fido2-client.service.spec.ts index 582849ebc12..51c3d8617ab 100644 --- a/libs/common/src/platform/services/fido2/fido2-client.service.spec.ts +++ b/libs/common/src/platform/services/fido2/fido2-client.service.spec.ts @@ -32,12 +32,14 @@ import { Fido2ClientService } from "./fido2-client.service"; import { Fido2Utils } from "./fido2-utils"; import { guidToRawFormat } from "./guid-utils"; +type ParentWindowReference = string; + const RpId = "bitwarden.com"; const Origin = "https://bitwarden.com"; const VaultUrl = "https://vault.bitwarden.com"; describe("FidoAuthenticatorService", () => { - let authenticator!: MockProxy; + let authenticator!: MockProxy>; let configService!: MockProxy; let authService!: MockProxy; let vaultSettingsService: MockProxy; @@ -45,12 +47,12 @@ describe("FidoAuthenticatorService", () => { let taskSchedulerService: MockProxy; let activeRequest!: MockProxy; let requestManager!: MockProxy; - let client!: Fido2ClientService; - let tab!: chrome.tabs.Tab; + let client!: Fido2ClientService; + let windowReference!: ParentWindowReference; let isValidRpId!: jest.SpyInstance; beforeEach(async () => { - authenticator = mock(); + authenticator = mock>(); configService = mock(); authService = mock(); vaultSettingsService = mock(); @@ -82,7 +84,7 @@ describe("FidoAuthenticatorService", () => { vaultSettingsService.enablePasskeys$ = of(true); domainSettingsService.neverDomains$ = of({}); authService.activeAccountStatus$ = of(AuthenticationStatus.Unlocked); - tab = { id: 123, windowId: 456 } as chrome.tabs.Tab; + windowReference = Utils.newGuid(); }); afterEach(() => { @@ -95,7 +97,7 @@ describe("FidoAuthenticatorService", () => { it("should throw error if sameOriginWithAncestors is false", async () => { const params = createParams({ sameOriginWithAncestors: false }); - const result = async () => await client.createCredential(params, tab); + const result = async () => await client.createCredential(params, windowReference); const rejects = expect(result).rejects; await rejects.toMatchObject({ name: "NotAllowedError" }); @@ -106,7 +108,7 @@ describe("FidoAuthenticatorService", () => { it("should throw error if user.id is too small", async () => { const params = createParams({ user: { id: "", displayName: "displayName", name: "name" } }); - const result = async () => await client.createCredential(params, tab); + const result = async () => await client.createCredential(params, windowReference); await expect(result).rejects.toBeInstanceOf(TypeError); }); @@ -121,7 +123,7 @@ describe("FidoAuthenticatorService", () => { }, }); - const result = async () => await client.createCredential(params, tab); + const result = async () => await client.createCredential(params, windowReference); await expect(result).rejects.toBeInstanceOf(TypeError); }); @@ -136,7 +138,7 @@ describe("FidoAuthenticatorService", () => { origin: "invalid-domain-name", }); - const result = async () => await client.createCredential(params, tab); + const result = async () => await client.createCredential(params, windowReference); const rejects = expect(result).rejects; await rejects.toMatchObject({ name: "SecurityError" }); @@ -151,7 +153,7 @@ describe("FidoAuthenticatorService", () => { rp: { id: "bitwarden.com", name: "Bitwarden" }, }); - const result = async () => await client.createCredential(params, tab); + const result = async () => await client.createCredential(params, windowReference); const rejects = expect(result).rejects; await rejects.toMatchObject({ name: "SecurityError" }); @@ -165,7 +167,7 @@ describe("FidoAuthenticatorService", () => { // `params` actually has a valid rp.id, but we're mocking the function to return false isValidRpId.mockReturnValue(false); - const result = async () => await client.createCredential(params, tab); + const result = async () => await client.createCredential(params, windowReference); const rejects = expect(result).rejects; await rejects.toMatchObject({ name: "SecurityError" }); @@ -179,7 +181,7 @@ describe("FidoAuthenticatorService", () => { }); domainSettingsService.neverDomains$ = of({ "bitwarden.com": null }); - const result = async () => await client.createCredential(params, tab); + const result = async () => await client.createCredential(params, windowReference); await expect(result).rejects.toThrow(FallbackRequestedError); }); @@ -190,7 +192,7 @@ describe("FidoAuthenticatorService", () => { rp: { id: "bitwarden.com", name: "Bitwarden" }, }); - const result = async () => await client.createCredential(params, tab); + const result = async () => await client.createCredential(params, windowReference); const rejects = expect(result).rejects; await rejects.toMatchObject({ name: "SecurityError" }); @@ -204,7 +206,7 @@ describe("FidoAuthenticatorService", () => { }); authenticator.makeCredential.mockResolvedValue(createAuthenticatorMakeResult()); - await client.createCredential(params, tab); + await client.createCredential(params, windowReference); }); // Spec: If credTypesAndPubKeyAlgs is empty, return a DOMException whose name is "NotSupportedError", and terminate this algorithm. @@ -216,7 +218,7 @@ describe("FidoAuthenticatorService", () => { ], }); - const result = async () => await client.createCredential(params, tab); + const result = async () => await client.createCredential(params, windowReference); const rejects = expect(result).rejects; await rejects.toMatchObject({ name: "NotSupportedError" }); @@ -231,7 +233,8 @@ describe("FidoAuthenticatorService", () => { const abortController = new AbortController(); abortController.abort(); - const result = async () => await client.createCredential(params, tab, abortController); + const result = async () => + await client.createCredential(params, windowReference, abortController); const rejects = expect(result).rejects; await rejects.toMatchObject({ name: "AbortError" }); @@ -246,7 +249,7 @@ describe("FidoAuthenticatorService", () => { }); authenticator.makeCredential.mockResolvedValue(createAuthenticatorMakeResult()); - await client.createCredential(params, tab); + await client.createCredential(params, windowReference); expect(authenticator.makeCredential).toHaveBeenCalledWith( expect.objectContaining({ @@ -259,7 +262,7 @@ describe("FidoAuthenticatorService", () => { displayName: params.user.displayName, }), }), - tab, + windowReference, expect.anything(), ); }); @@ -271,7 +274,7 @@ describe("FidoAuthenticatorService", () => { }); authenticator.makeCredential.mockResolvedValue(createAuthenticatorMakeResult()); - const result = await client.createCredential(params, tab); + const result = await client.createCredential(params, windowReference); expect(result.extensions.credProps?.rk).toBe(true); }); @@ -283,7 +286,7 @@ describe("FidoAuthenticatorService", () => { }); authenticator.makeCredential.mockResolvedValue(createAuthenticatorMakeResult()); - const result = await client.createCredential(params, tab); + const result = await client.createCredential(params, windowReference); expect(result.extensions.credProps?.rk).toBe(false); }); @@ -295,7 +298,7 @@ describe("FidoAuthenticatorService", () => { }); authenticator.makeCredential.mockResolvedValue(createAuthenticatorMakeResult()); - const result = await client.createCredential(params, tab); + const result = await client.createCredential(params, windowReference); expect(result.extensions.credProps).toBeUndefined(); }); @@ -307,7 +310,7 @@ describe("FidoAuthenticatorService", () => { new Fido2AuthenticatorError(Fido2AuthenticatorErrorCode.InvalidState), ); - const result = async () => await client.createCredential(params, tab); + const result = async () => await client.createCredential(params, windowReference); const rejects = expect(result).rejects; await rejects.toMatchObject({ name: "InvalidStateError" }); @@ -319,7 +322,7 @@ describe("FidoAuthenticatorService", () => { const params = createParams(); authenticator.makeCredential.mockRejectedValue(new Error("unknown error")); - const result = async () => await client.createCredential(params, tab); + const result = async () => await client.createCredential(params, windowReference); const rejects = expect(result).rejects; await rejects.toMatchObject({ name: "NotAllowedError" }); @@ -330,7 +333,7 @@ describe("FidoAuthenticatorService", () => { const params = createParams(); vaultSettingsService.enablePasskeys$ = of(false); - const result = async () => await client.createCredential(params, tab); + const result = async () => await client.createCredential(params, windowReference); const rejects = expect(result).rejects; await rejects.toThrow(FallbackRequestedError); @@ -340,7 +343,7 @@ describe("FidoAuthenticatorService", () => { const params = createParams(); authService.getAuthStatus.mockResolvedValue(AuthenticationStatus.LoggedOut); - const result = async () => await client.createCredential(params, tab); + const result = async () => await client.createCredential(params, windowReference); const rejects = expect(result).rejects; await rejects.toThrow(FallbackRequestedError); @@ -349,7 +352,7 @@ describe("FidoAuthenticatorService", () => { it("should throw FallbackRequestedError if origin equals the bitwarden vault", async () => { const params = createParams({ origin: VaultUrl }); - const result = async () => await client.createCredential(params, tab); + const result = async () => await client.createCredential(params, windowReference); const rejects = expect(result).rejects; await rejects.toThrow(FallbackRequestedError); @@ -408,7 +411,7 @@ describe("FidoAuthenticatorService", () => { origin: "invalid-domain-name", }); - const result = async () => await client.assertCredential(params, tab); + const result = async () => await client.assertCredential(params, windowReference); const rejects = expect(result).rejects; await rejects.toMatchObject({ name: "SecurityError" }); @@ -423,7 +426,7 @@ describe("FidoAuthenticatorService", () => { rpId: "bitwarden.com", }); - const result = async () => await client.assertCredential(params, tab); + const result = async () => await client.assertCredential(params, windowReference); const rejects = expect(result).rejects; await rejects.toMatchObject({ name: "SecurityError" }); @@ -437,7 +440,7 @@ describe("FidoAuthenticatorService", () => { // `params` actually has a valid rp.id, but we're mocking the function to return false isValidRpId.mockReturnValue(false); - const result = async () => await client.assertCredential(params, tab); + const result = async () => await client.assertCredential(params, windowReference); const rejects = expect(result).rejects; await rejects.toMatchObject({ name: "SecurityError" }); @@ -451,7 +454,7 @@ describe("FidoAuthenticatorService", () => { domainSettingsService.neverDomains$ = of({ "bitwarden.com": null }); - const result = async () => await client.assertCredential(params, tab); + const result = async () => await client.assertCredential(params, windowReference); await expect(result).rejects.toThrow(FallbackRequestedError); }); @@ -462,7 +465,7 @@ describe("FidoAuthenticatorService", () => { rpId: "bitwarden.com", }); - const result = async () => await client.assertCredential(params, tab); + const result = async () => await client.assertCredential(params, windowReference); const rejects = expect(result).rejects; await rejects.toMatchObject({ name: "SecurityError" }); @@ -477,7 +480,8 @@ describe("FidoAuthenticatorService", () => { const abortController = new AbortController(); abortController.abort(); - const result = async () => await client.assertCredential(params, tab, abortController); + const result = async () => + await client.assertCredential(params, windowReference, abortController); const rejects = expect(result).rejects; await rejects.toMatchObject({ name: "AbortError" }); @@ -493,7 +497,7 @@ describe("FidoAuthenticatorService", () => { new Fido2AuthenticatorError(Fido2AuthenticatorErrorCode.InvalidState), ); - const result = async () => await client.assertCredential(params, tab); + const result = async () => await client.assertCredential(params, windowReference); const rejects = expect(result).rejects; await rejects.toMatchObject({ name: "InvalidStateError" }); @@ -505,7 +509,7 @@ describe("FidoAuthenticatorService", () => { const params = createParams(); authenticator.getAssertion.mockRejectedValue(new Error("unknown error")); - const result = async () => await client.assertCredential(params, tab); + const result = async () => await client.assertCredential(params, windowReference); const rejects = expect(result).rejects; await rejects.toMatchObject({ name: "NotAllowedError" }); @@ -516,7 +520,7 @@ describe("FidoAuthenticatorService", () => { const params = createParams(); vaultSettingsService.enablePasskeys$ = of(false); - const result = async () => await client.assertCredential(params, tab); + const result = async () => await client.assertCredential(params, windowReference); const rejects = expect(result).rejects; await rejects.toThrow(FallbackRequestedError); @@ -526,7 +530,7 @@ describe("FidoAuthenticatorService", () => { const params = createParams(); authService.getAuthStatus.mockResolvedValue(AuthenticationStatus.LoggedOut); - const result = async () => await client.assertCredential(params, tab); + const result = async () => await client.assertCredential(params, windowReference); const rejects = expect(result).rejects; await rejects.toThrow(FallbackRequestedError); @@ -535,7 +539,7 @@ describe("FidoAuthenticatorService", () => { it("should throw FallbackRequestedError if origin equals the bitwarden vault", async () => { const params = createParams({ origin: VaultUrl }); - const result = async () => await client.assertCredential(params, tab); + const result = async () => await client.assertCredential(params, windowReference); const rejects = expect(result).rejects; await rejects.toThrow(FallbackRequestedError); @@ -555,7 +559,7 @@ describe("FidoAuthenticatorService", () => { }); authenticator.getAssertion.mockResolvedValue(createAuthenticatorAssertResult()); - await client.assertCredential(params, tab); + await client.assertCredential(params, windowReference); expect(authenticator.getAssertion).toHaveBeenCalledWith( expect.objectContaining({ @@ -573,7 +577,7 @@ describe("FidoAuthenticatorService", () => { }), ], }), - tab, + windowReference, expect.anything(), ); }); @@ -585,7 +589,7 @@ describe("FidoAuthenticatorService", () => { params.rpId = undefined; authenticator.getAssertion.mockResolvedValue(createAuthenticatorAssertResult()); - await client.assertCredential(params, tab); + await client.assertCredential(params, windowReference); }); }); @@ -597,7 +601,7 @@ describe("FidoAuthenticatorService", () => { }); authenticator.getAssertion.mockResolvedValue(createAuthenticatorAssertResult()); - await client.assertCredential(params, tab); + await client.assertCredential(params, windowReference); expect(authenticator.getAssertion).toHaveBeenCalledWith( expect.objectContaining({ @@ -605,7 +609,7 @@ describe("FidoAuthenticatorService", () => { rpId: RpId, allowCredentialDescriptorList: [], }), - tab, + windowReference, expect.anything(), ); }); @@ -627,7 +631,7 @@ describe("FidoAuthenticatorService", () => { }); it("creates an active mediated conditional request", async () => { - await client.assertCredential(params, tab); + await client.assertCredential(params, windowReference); expect(requestManager.newActiveRequest).toHaveBeenCalled(); expect(authenticator.getAssertion).toHaveBeenCalledWith( @@ -635,14 +639,14 @@ describe("FidoAuthenticatorService", () => { assumeUserPresence: true, rpId: RpId, }), - tab, + windowReference, ); }); it("restarts the mediated conditional request if a user aborts the request", async () => { authenticator.getAssertion.mockRejectedValueOnce(new Error()); - await client.assertCredential(params, tab); + await client.assertCredential(params, windowReference); expect(authenticator.getAssertion).toHaveBeenCalledTimes(2); }); @@ -652,7 +656,7 @@ describe("FidoAuthenticatorService", () => { abortController.abort(); authenticator.getAssertion.mockRejectedValueOnce(new DOMException("AbortError")); - await client.assertCredential(params, tab); + await client.assertCredential(params, windowReference); expect(authenticator.getAssertion).toHaveBeenCalledTimes(2); }); diff --git a/libs/common/src/platform/services/fido2/fido2-client.service.ts b/libs/common/src/platform/services/fido2/fido2-client.service.ts index d08d1e2a42d..4bf30ef6537 100644 --- a/libs/common/src/platform/services/fido2/fido2-client.service.ts +++ b/libs/common/src/platform/services/fido2/fido2-client.service.ts @@ -47,7 +47,9 @@ import { guidToRawFormat } from "./guid-utils"; * * It is highly recommended that the W3C specification is used a reference when reading this code. */ -export class Fido2ClientService implements Fido2ClientServiceAbstraction { +export class Fido2ClientService + implements Fido2ClientServiceAbstraction +{ private timeoutAbortController: AbortController; private readonly TIMEOUTS = { NO_VERIFICATION: { @@ -63,7 +65,7 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction { }; constructor( - private authenticator: Fido2AuthenticatorService, + private authenticator: Fido2AuthenticatorService, private configService: ConfigService, private authService: AuthService, private vaultSettingsService: VaultSettingsService, @@ -102,7 +104,7 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction { async createCredential( params: CreateCredentialParams, - tab: chrome.tabs.Tab, + window: ParentWindowReference, abortController = new AbortController(), ): Promise { const parsedOrigin = parse(params.origin, { allowPrivateDomains: true }); @@ -201,7 +203,7 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction { try { makeCredentialResult = await this.authenticator.makeCredential( makeCredentialParams, - tab, + window, abortController, ); } catch (error) { @@ -256,7 +258,7 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction { async assertCredential( params: AssertCredentialParams, - tab: chrome.tabs.Tab, + window: ParentWindowReference, abortController = new AbortController(), ): Promise { const parsedOrigin = parse(params.origin, { allowPrivateDomains: true }); @@ -300,7 +302,7 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction { if (params.mediation === "conditional") { return this.handleMediatedConditionalRequest( params, - tab, + window, abortController, clientDataJSONBytes, ); @@ -324,7 +326,7 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction { try { getAssertionResult = await this.authenticator.getAssertion( getAssertionParams, - tab, + window, abortController, ); } catch (error) { @@ -363,7 +365,7 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction { private async handleMediatedConditionalRequest( params: AssertCredentialParams, - tab: chrome.tabs.Tab, + tab: ParentWindowReference, abortController: AbortController, clientDataJSONBytes: Uint8Array, ): Promise { @@ -379,7 +381,10 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction { `[Fido2Client] started mediated request, available credentials: ${availableCredentials.length}`, ); const requestResult = await this.requestManager.newActiveRequest( - tab.id, + // TODO: This isn't correct, but this.requestManager.newActiveRequest expects a number, + // while this class is currently generic over ParentWindowReference. + // Consider moving requestManager into browser and adding support for ParentWindowReference => tab.id + (tab as any).id, availableCredentials, abortController, ); diff --git a/libs/common/src/platform/services/fido2/noop-fido2-user-interface.service.ts b/libs/common/src/platform/services/fido2/noop-fido2-user-interface.service.ts index 440bd519002..14b4da0ef1b 100644 --- a/libs/common/src/platform/services/fido2/noop-fido2-user-interface.service.ts +++ b/libs/common/src/platform/services/fido2/noop-fido2-user-interface.service.ts @@ -7,7 +7,7 @@ import { * Noop implementation of the {@link Fido2UserInterfaceService}. * This implementation does not provide any user interface. */ -export class Fido2UserInterfaceService implements Fido2UserInterfaceServiceAbstraction { +export class Fido2UserInterfaceService implements Fido2UserInterfaceServiceAbstraction { newSession(): Promise { throw new Error("Not implemented exception"); } From 23212fb9ae33266a7cf22e40905daac0d09fb4dd Mon Sep 17 00:00:00 2001 From: Thomas Rittson <31796059+eliykat@users.noreply.github.com> Date: Thu, 19 Dec 2024 21:53:39 +1000 Subject: [PATCH 17/54] Fix: users can import in PM if they can create new collections (#12472) --- libs/importer/src/components/import.component.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/libs/importer/src/components/import.component.ts b/libs/importer/src/components/import.component.ts index b50be773251..0035fbdf10d 100644 --- a/libs/importer/src/components/import.component.ts +++ b/libs/importer/src/components/import.component.ts @@ -290,7 +290,9 @@ export class ImportComponent implements OnInit, OnDestroy, AfterViewInit { private async initializeOrganizations() { this.organizations$ = concat( this.organizationService.memberOrganizations$.pipe( - map((orgs) => orgs.filter((org) => org.canAccessImport)), + // Import is an alternative way to create collections during onboarding, so import from Password Manager + // is available to any user who can create collections in the organization. + map((orgs) => orgs.filter((org) => org.canAccessImport || org.canCreateNewCollections)), map((orgs) => orgs.sort(Utils.getSortFunction(this.i18nService, "name"))), ), ); From 1d04a0a3998b3a83258c2a2350ebdd33b1dbe424 Mon Sep 17 00:00:00 2001 From: Jason Ng Date: Thu, 19 Dec 2024 09:59:42 -0500 Subject: [PATCH 18/54] [PM-8214] New Device Verification Notice UI (#12360) * starting * setup first page for new device verification notice * update designs for first page. rename components and files * added second page for new device verification notice * update notice page one with bit radio buttons. routing logic. user email * updated routing for new device verification notice to show before vault based on flags, and can navigate back to vault after submission * fix translations. added remind me later link and nav to page 2 * sync the design for mobile and web * update routes in desktop * updated styles for desktop * moved new device verification notice guard * update types for new device notice page one * add null check to page one * types * types for page one, page two, service, and guard * types * update component and guard for null check * add navigation to two step login btn and account email btn * remove empty file * update fill of icons to support light & dark modes * add question mark to email access verification copy * remove unused map * use links for navigation elements - an empty href is needed so the links are keyboard accessible * remove clip path from exclamation svg - No noticeable difference in the end result * inline email message into markup --------- Co-authored-by: Nick Krantz --- apps/browser/src/_locales/en/messages.json | 36 +++++++ apps/browser/src/popup/app-routing.module.ts | 35 ++++++- apps/desktop/src/app/app-routing.module.ts | 35 ++++++- apps/desktop/src/locales/en/messages.json | 36 +++++++ apps/desktop/tailwind.config.js | 1 + apps/web/src/app/oss-routing.module.ts | 35 ++++++- apps/web/src/locales/en/messages.json | 36 +++++++ .../src/services/jslib-services.module.ts | 2 + libs/angular/src/vault/guards/index.ts | 1 + .../new-device-verification-notice.guard.ts | 51 ++++++++++ ...erification-notice-page-one.component.html | 30 ++++++ ...-verification-notice-page-one.component.ts | 82 ++++++++++++++++ ...erification-notice-page-two.component.html | 39 ++++++++ ...-verification-notice-page-two.component.ts | 95 +++++++++++++++++++ libs/vault/src/icons/exclamation-triangle.ts | 7 ++ libs/vault/src/icons/index.ts | 2 + libs/vault/src/icons/user-lock.ts | 17 ++++ libs/vault/src/index.ts | 2 + .../new-device-verification-notice.service.ts | 2 +- 19 files changed, 540 insertions(+), 4 deletions(-) create mode 100644 libs/angular/src/vault/guards/index.ts create mode 100644 libs/angular/src/vault/guards/new-device-verification-notice.guard.ts create mode 100644 libs/vault/src/components/new-device-verification-notice/new-device-verification-notice-page-one.component.html create mode 100644 libs/vault/src/components/new-device-verification-notice/new-device-verification-notice-page-one.component.ts create mode 100644 libs/vault/src/components/new-device-verification-notice/new-device-verification-notice-page-two.component.html create mode 100644 libs/vault/src/components/new-device-verification-notice/new-device-verification-notice-page-two.component.ts create mode 100644 libs/vault/src/icons/exclamation-triangle.ts create mode 100644 libs/vault/src/icons/user-lock.ts diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index c2e9ef60d8c..de438a09467 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -4910,6 +4910,42 @@ "beta": { "message": "Beta" }, + "importantNotice": { + "message": "Important notice" + }, + "setupTwoStepLogin": { + "message": "Set up two-step login" + }, + "newDeviceVerificationNoticeContentPage1": { + "message": "Bitwarden will send a code to your account email to verify logins from new devices starting in February 2025." + }, + "newDeviceVerificationNoticeContentPage2": { + "message": "You can set up two-step login as an alternative way to protect your account or change your email to one you can access." + }, + "remindMeLater": { + "message": "Remind me later" + }, + "newDeviceVerificationNoticePageOneFormContent": { + "message": "Do you have reliable access to your email, $EMAIL$?", + "placeholders": { + "email": { + "content": "$1", + "example": "your_name@email.com" + } + } + }, + "newDeviceVerificationNoticePageOneEmailAccessNo": { + "message": "No, I do not" + }, + "newDeviceVerificationNoticePageOneEmailAccessYes": { + "message": "Yes, I can reliably access my email" + }, + "turnOnTwoStepLogin": { + "message": "Turn on two-step login" + }, + "changeAcctEmail": { + "message": "Change account email" + }, "extensionWidth": { "message": "Extension width" }, diff --git a/apps/browser/src/popup/app-routing.module.ts b/apps/browser/src/popup/app-routing.module.ts index 3ec2667cd8c..ad839bbd7ce 100644 --- a/apps/browser/src/popup/app-routing.module.ts +++ b/apps/browser/src/popup/app-routing.module.ts @@ -19,6 +19,7 @@ import { import { canAccessFeature } from "@bitwarden/angular/platform/guard/feature-flag.guard"; import { extensionRefreshRedirect } from "@bitwarden/angular/utils/extension-refresh-redirect"; import { extensionRefreshSwap } from "@bitwarden/angular/utils/extension-refresh-swap"; +import { NewDeviceVerificationNoticeGuard } from "@bitwarden/angular/vault/guards"; import { AnonLayoutWrapperComponent, AnonLayoutWrapperData, @@ -43,6 +44,11 @@ import { TwoFactorTimeoutIcon, } from "@bitwarden/auth/angular"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { + NewDeviceVerificationNoticePageOneComponent, + NewDeviceVerificationNoticePageTwoComponent, + VaultIcons, +} from "@bitwarden/vault"; import { twofactorRefactorSwap } from "../../../../libs/angular/src/utils/two-factor-component-refactor-route-swap"; import { fido2AuthGuard } from "../auth/guards/fido2-auth.guard"; @@ -715,6 +721,33 @@ const routes: Routes = [ canActivate: [authGuard], data: { elevation: 2 } satisfies RouteDataProperties, }, + { + path: "new-device-notice", + component: ExtensionAnonLayoutWrapperComponent, + canActivate: [], + children: [ + { + path: "", + component: NewDeviceVerificationNoticePageOneComponent, + data: { + pageIcon: VaultIcons.ExclamationTriangle, + pageTitle: { + key: "importantNotice", + }, + }, + }, + { + path: "setup", + component: NewDeviceVerificationNoticePageTwoComponent, + data: { + pageIcon: VaultIcons.UserLock, + pageTitle: { + key: "setupTwoStepLogin", + }, + }, + }, + ], + }, ...extensionRefreshSwap(TabsComponent, TabsV2Component, { path: "tabs", data: { elevation: 0 } satisfies RouteDataProperties, @@ -734,7 +767,7 @@ const routes: Routes = [ }, ...extensionRefreshSwap(VaultFilterComponent, VaultV2Component, { path: "vault", - canActivate: [authGuard], + canActivate: [authGuard, NewDeviceVerificationNoticeGuard], canDeactivate: [clearVaultStateGuard], data: { elevation: 0 } satisfies RouteDataProperties, }), diff --git a/apps/desktop/src/app/app-routing.module.ts b/apps/desktop/src/app/app-routing.module.ts index 21dced5c2aa..c7642638dc3 100644 --- a/apps/desktop/src/app/app-routing.module.ts +++ b/apps/desktop/src/app/app-routing.module.ts @@ -16,6 +16,7 @@ import { } from "@bitwarden/angular/auth/guards"; import { canAccessFeature } from "@bitwarden/angular/platform/guard/feature-flag.guard"; import { extensionRefreshRedirect } from "@bitwarden/angular/utils/extension-refresh-redirect"; +import { NewDeviceVerificationNoticeGuard } from "@bitwarden/angular/vault/guards"; import { AnonLayoutWrapperComponent, AnonLayoutWrapperData, @@ -40,6 +41,11 @@ import { TwoFactorTimeoutIcon, } from "@bitwarden/auth/angular"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { + NewDeviceVerificationNoticePageOneComponent, + NewDeviceVerificationNoticePageTwoComponent, + VaultIcons, +} from "@bitwarden/vault"; import { twofactorRefactorSwap } from "../../../../libs/angular/src/utils/two-factor-component-refactor-route-swap"; import { AccessibilityCookieComponent } from "../auth/accessibility-cookie.component"; @@ -116,10 +122,37 @@ const routes: Routes = [ } satisfies RouteDataProperties & AnonLayoutWrapperData, }, { path: "register", component: RegisterComponent }, + { + path: "new-device-notice", + component: AnonLayoutWrapperComponent, + canActivate: [], + children: [ + { + path: "", + component: NewDeviceVerificationNoticePageOneComponent, + data: { + pageIcon: VaultIcons.ExclamationTriangle, + pageTitle: { + key: "importantNotice", + }, + }, + }, + { + path: "setup", + component: NewDeviceVerificationNoticePageTwoComponent, + data: { + pageIcon: VaultIcons.UserLock, + pageTitle: { + key: "setupTwoStepLogin", + }, + }, + }, + ], + }, { path: "vault", component: VaultComponent, - canActivate: [authGuard], + canActivate: [authGuard, NewDeviceVerificationNoticeGuard], }, { path: "accessibility-cookie", component: AccessibilityCookieComponent }, { path: "set-password", component: SetPasswordComponent }, diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json index f8f81a5ac2c..323d0cd3f7b 100644 --- a/apps/desktop/src/locales/en/messages.json +++ b/apps/desktop/src/locales/en/messages.json @@ -3394,5 +3394,41 @@ }, "fileSavedToDevice": { "message": "File saved to device. Manage from your device downloads." + }, + "importantNotice": { + "message": "Important notice" + }, + "setupTwoStepLogin": { + "message": "Set up two-step login" + }, + "newDeviceVerificationNoticeContentPage1": { + "message": "Bitwarden will send a code to your account email to verify logins from new devices starting in February 2025." + }, + "newDeviceVerificationNoticeContentPage2": { + "message": "You can set up two-step login as an alternative way to protect your account or change your email to one you can access." + }, + "remindMeLater": { + "message": "Remind me later" + }, + "newDeviceVerificationNoticePageOneFormContent": { + "message": "Do you have reliable access to your email, $EMAIL$?", + "placeholders": { + "email": { + "content": "$1", + "example": "your_name@email.com" + } + } + }, + "newDeviceVerificationNoticePageOneEmailAccessNo": { + "message": "No, I do not" + }, + "newDeviceVerificationNoticePageOneEmailAccessYes": { + "message": "Yes, I can reliably access my email" + }, + "turnOnTwoStepLogin": { + "message": "Turn on two-step login" + }, + "changeAcctEmail": { + "message": "Change account email" } } diff --git a/apps/desktop/tailwind.config.js b/apps/desktop/tailwind.config.js index db1dd55694e..a561b93b21a 100644 --- a/apps/desktop/tailwind.config.js +++ b/apps/desktop/tailwind.config.js @@ -6,6 +6,7 @@ config.content = [ "../../libs/components/src/**/*.{html,ts}", "../../libs/auth/src/**/*.{html,ts}", "../../libs/angular/src/**/*.{html,ts}", + "../../libs/vault/src/**/*.{html,ts,mdx}", ]; module.exports = config; diff --git a/apps/web/src/app/oss-routing.module.ts b/apps/web/src/app/oss-routing.module.ts index 649f1aba534..9f2a86c1c06 100644 --- a/apps/web/src/app/oss-routing.module.ts +++ b/apps/web/src/app/oss-routing.module.ts @@ -13,6 +13,7 @@ import { import { canAccessFeature } from "@bitwarden/angular/platform/guard/feature-flag.guard"; import { generatorSwap } from "@bitwarden/angular/tools/generator/generator-swap"; import { extensionRefreshSwap } from "@bitwarden/angular/utils/extension-refresh-swap"; +import { NewDeviceVerificationNoticeGuard } from "@bitwarden/angular/vault/guards"; import { AnonLayoutWrapperComponent, AnonLayoutWrapperData, @@ -40,6 +41,11 @@ import { LoginDecryptionOptionsComponent, } from "@bitwarden/auth/angular"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { + NewDeviceVerificationNoticePageOneComponent, + NewDeviceVerificationNoticePageTwoComponent, + VaultIcons, +} from "@bitwarden/vault"; import { twofactorRefactorSwap } from "../../../../libs/angular/src/utils/two-factor-component-refactor-route-swap"; import { flagEnabled, Flags } from "../utils/flags"; @@ -695,10 +701,37 @@ const routes: Routes = [ }, ], }, + { + path: "new-device-notice", + component: AnonLayoutWrapperComponent, + canActivate: [], + children: [ + { + path: "", + component: NewDeviceVerificationNoticePageOneComponent, + data: { + pageIcon: VaultIcons.ExclamationTriangle, + pageTitle: { + key: "importantNotice", + }, + }, + }, + { + path: "setup", + component: NewDeviceVerificationNoticePageTwoComponent, + data: { + pageIcon: VaultIcons.UserLock, + pageTitle: { + key: "setupTwoStepLogin", + }, + }, + }, + ], + }, { path: "", component: UserLayoutComponent, - canActivate: [deepLinkGuard(), authGuard], + canActivate: [deepLinkGuard(), authGuard, NewDeviceVerificationNoticeGuard], children: [ { path: "vault", diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index abd5779339f..acbb348048c 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -9888,6 +9888,42 @@ "descriptorCode": { "message": "Descriptor code" }, + "importantNotice": { + "message": "Important notice" + }, + "setupTwoStepLogin": { + "message": "Set up two-step login" + }, + "newDeviceVerificationNoticeContentPage1": { + "message": "Bitwarden will send a code to your account email to verify logins from new devices starting in February 2025." + }, + "newDeviceVerificationNoticeContentPage2": { + "message": "You can set up two-step login as an alternative way to protect your account or change your email to one you can access." + }, + "remindMeLater": { + "message": "Remind me later" + }, + "newDeviceVerificationNoticePageOneFormContent": { + "message": "Do you have reliable access to your email, $EMAIL$?", + "placeholders": { + "email": { + "content": "$1", + "example": "your_name@email.com" + } + } + }, + "newDeviceVerificationNoticePageOneEmailAccessNo": { + "message": "No, I do not" + }, + "newDeviceVerificationNoticePageOneEmailAccessYes": { + "message": "Yes, I can reliably access my email" + }, + "turnOnTwoStepLogin": { + "message": "Turn on two-step login" + }, + "changeAcctEmail": { + "message": "Change account email" + }, "removeMembers": { "message": "Remove members" }, diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 0e50cec1b64..0765fd8e4c6 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -298,6 +298,7 @@ import { IndividualVaultExportServiceAbstraction, } from "@bitwarden/vault-export-core"; +import { NewDeviceVerificationNoticeService } from "../../../vault/src/services/new-device-verification-notice.service"; import { FormValidationErrorsService as FormValidationErrorsServiceAbstraction } from "../platform/abstractions/form-validation-errors.service"; import { ViewCacheService } from "../platform/abstractions/view-cache.service"; import { FormValidationErrorsService } from "../platform/services/form-validation-errors.service"; @@ -1401,6 +1402,7 @@ const safeProviders: SafeProvider[] = [ useClass: DefaultLoginDecryptionOptionsService, deps: [MessagingServiceAbstraction], }), + safeProvider(NewDeviceVerificationNoticeService), safeProvider({ provide: UserAsymmetricKeysRegenerationApiService, useClass: DefaultUserAsymmetricKeysRegenerationApiService, diff --git a/libs/angular/src/vault/guards/index.ts b/libs/angular/src/vault/guards/index.ts new file mode 100644 index 00000000000..001a4832372 --- /dev/null +++ b/libs/angular/src/vault/guards/index.ts @@ -0,0 +1 @@ +export * from "./new-device-verification-notice.guard"; diff --git a/libs/angular/src/vault/guards/new-device-verification-notice.guard.ts b/libs/angular/src/vault/guards/new-device-verification-notice.guard.ts new file mode 100644 index 00000000000..a37097e3583 --- /dev/null +++ b/libs/angular/src/vault/guards/new-device-verification-notice.guard.ts @@ -0,0 +1,51 @@ +import { inject } from "@angular/core"; +import { ActivatedRouteSnapshot, CanActivateFn, Router } from "@angular/router"; +import { Observable, firstValueFrom, map } from "rxjs"; + +import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; + +import { NewDeviceVerificationNoticeService } from "../../../../vault/src/services/new-device-verification-notice.service"; + +export const NewDeviceVerificationNoticeGuard: CanActivateFn = async ( + route: ActivatedRouteSnapshot, +) => { + const router = inject(Router); + const configService = inject(ConfigService); + const newDeviceVerificationNoticeService = inject(NewDeviceVerificationNoticeService); + const accountService = inject(AccountService); + + if (route.queryParams["fromNewDeviceVerification"]) { + return true; + } + + const tempNoticeFlag = await configService.getFeatureFlag( + FeatureFlag.NewDeviceVerificationTemporaryDismiss, + ); + const permNoticeFlag = await configService.getFeatureFlag( + FeatureFlag.NewDeviceVerificationPermanentDismiss, + ); + + const currentAcct$: Observable = accountService.activeAccount$.pipe( + map((acct) => acct), + ); + const currentAcct = await firstValueFrom(currentAcct$); + + if (!currentAcct) { + return router.createUrlTree(["/login"]); + } + + const userItems$ = newDeviceVerificationNoticeService.noticeState$(currentAcct.id); + const userItems = await firstValueFrom(userItems$); + + if ( + userItems?.last_dismissal == null && + (userItems?.permanent_dismissal == null || !userItems?.permanent_dismissal) && + (tempNoticeFlag || permNoticeFlag) + ) { + return router.createUrlTree(["/new-device-notice"]); + } + + return true; +}; diff --git a/libs/vault/src/components/new-device-verification-notice/new-device-verification-notice-page-one.component.html b/libs/vault/src/components/new-device-verification-notice/new-device-verification-notice-page-one.component.html new file mode 100644 index 00000000000..316df3aed17 --- /dev/null +++ b/libs/vault/src/components/new-device-verification-notice/new-device-verification-notice-page-one.component.html @@ -0,0 +1,30 @@ +
+

+ {{ "newDeviceVerificationNoticeContentPage1" | i18n }} +

+ + +

+ {{ "newDeviceVerificationNoticePageOneFormContent" | i18n: this.currentEmail }} +

+ + + + {{ "newDeviceVerificationNoticePageOneEmailAccessNo" | i18n }} + + + {{ "newDeviceVerificationNoticePageOneEmailAccessYes" | i18n }} + + +
+ + +
diff --git a/libs/vault/src/components/new-device-verification-notice/new-device-verification-notice-page-one.component.ts b/libs/vault/src/components/new-device-verification-notice/new-device-verification-notice-page-one.component.ts new file mode 100644 index 00000000000..62ae22f5b22 --- /dev/null +++ b/libs/vault/src/components/new-device-verification-notice/new-device-verification-notice-page-one.component.ts @@ -0,0 +1,82 @@ +import { CommonModule } from "@angular/common"; +import { Component, OnInit } from "@angular/core"; +import { FormBuilder, FormControl, ReactiveFormsModule } from "@angular/forms"; +import { Router } from "@angular/router"; +import { firstValueFrom, Observable } from "rxjs"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { ClientType } from "@bitwarden/common/enums"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { UserId } from "@bitwarden/common/types/guid"; +import { + AsyncActionsModule, + ButtonModule, + CardComponent, + FormFieldModule, + RadioButtonModule, + TypographyModule, +} from "@bitwarden/components"; + +import { NewDeviceVerificationNoticeService } from "./../../services/new-device-verification-notice.service"; + +@Component({ + standalone: true, + selector: "app-new-device-verification-notice-page-one", + templateUrl: "./new-device-verification-notice-page-one.component.html", + imports: [ + CardComponent, + CommonModule, + JslibModule, + TypographyModule, + ButtonModule, + RadioButtonModule, + FormFieldModule, + AsyncActionsModule, + ReactiveFormsModule, + ], +}) +export class NewDeviceVerificationNoticePageOneComponent implements OnInit { + protected formGroup = this.formBuilder.group({ + hasEmailAccess: new FormControl(0), + }); + protected isDesktop: boolean; + readonly currentAcct$: Observable = this.accountService.activeAccount$; + protected currentEmail: string = ""; + private currentUserId: UserId | null = null; + + constructor( + private formBuilder: FormBuilder, + private router: Router, + private accountService: AccountService, + private newDeviceVerificationNoticeService: NewDeviceVerificationNoticeService, + private platformUtilsService: PlatformUtilsService, + ) { + this.isDesktop = this.platformUtilsService.getClientType() === ClientType.Desktop; + } + + async ngOnInit() { + const currentAcct = await firstValueFrom(this.currentAcct$); + if (!currentAcct) { + return; + } + this.currentEmail = currentAcct.email; + this.currentUserId = currentAcct.id; + } + + submit = async () => { + if (this.formGroup.controls.hasEmailAccess.value === 0) { + await this.router.navigate(["new-device-notice/setup"]); + } else if (this.formGroup.controls.hasEmailAccess.value === 1) { + await this.newDeviceVerificationNoticeService.updateNewDeviceVerificationNoticeState( + this.currentUserId, + { + last_dismissal: new Date(), + permanent_dismissal: false, + }, + ); + + await this.router.navigate(["/vault"]); + } + }; +} diff --git a/libs/vault/src/components/new-device-verification-notice/new-device-verification-notice-page-two.component.html b/libs/vault/src/components/new-device-verification-notice/new-device-verification-notice-page-two.component.html new file mode 100644 index 00000000000..270b4126252 --- /dev/null +++ b/libs/vault/src/components/new-device-verification-notice/new-device-verification-notice-page-two.component.html @@ -0,0 +1,39 @@ +

+ {{ "newDeviceVerificationNoticeContentPage2" | i18n }} +

+ + + {{ "turnOnTwoStepLogin" | i18n }} + + + + {{ "changeAcctEmail" | i18n }} + + + + diff --git a/libs/vault/src/components/new-device-verification-notice/new-device-verification-notice-page-two.component.ts b/libs/vault/src/components/new-device-verification-notice/new-device-verification-notice-page-two.component.ts new file mode 100644 index 00000000000..630a2fd516c --- /dev/null +++ b/libs/vault/src/components/new-device-verification-notice/new-device-verification-notice-page-two.component.ts @@ -0,0 +1,95 @@ +import { CommonModule } from "@angular/common"; +import { Component, OnInit } from "@angular/core"; +import { Router } from "@angular/router"; +import { firstValueFrom, Observable } from "rxjs"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { ClientType } from "@bitwarden/common/enums"; +import { + Environment, + EnvironmentService, +} from "@bitwarden/common/platform/abstractions/environment.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { UserId } from "@bitwarden/common/types/guid"; +import { ButtonModule, LinkModule, TypographyModule } from "@bitwarden/components"; + +import { NewDeviceVerificationNoticeService } from "../../services/new-device-verification-notice.service"; + +@Component({ + standalone: true, + selector: "app-new-device-verification-notice-page-two", + templateUrl: "./new-device-verification-notice-page-two.component.html", + imports: [CommonModule, JslibModule, TypographyModule, ButtonModule, LinkModule], +}) +export class NewDeviceVerificationNoticePageTwoComponent implements OnInit { + protected isWeb: boolean; + protected isDesktop: boolean; + readonly currentAcct$: Observable = this.accountService.activeAccount$; + private currentUserId: UserId | null = null; + private env$: Observable = this.environmentService.environment$; + + constructor( + private newDeviceVerificationNoticeService: NewDeviceVerificationNoticeService, + private router: Router, + private accountService: AccountService, + private platformUtilsService: PlatformUtilsService, + private environmentService: EnvironmentService, + ) { + this.isWeb = this.platformUtilsService.getClientType() === ClientType.Web; + this.isDesktop = this.platformUtilsService.getClientType() === ClientType.Desktop; + } + + async ngOnInit() { + const currentAcct = await firstValueFrom(this.currentAcct$); + if (!currentAcct) { + return; + } + this.currentUserId = currentAcct.id; + } + + async navigateToTwoStepLogin(event: Event) { + event.preventDefault(); + + const env = await firstValueFrom(this.env$); + const url = env.getWebVaultUrl(); + + if (this.isWeb) { + await this.router.navigate(["/settings/security/two-factor"], { + queryParams: { fromNewDeviceVerification: true }, + }); + } else { + this.platformUtilsService.launchUri( + url + "/#/settings/security/two-factor/?fromNewDeviceVerification=true", + ); + } + } + + async navigateToChangeAcctEmail(event: Event) { + event.preventDefault(); + + const env = await firstValueFrom(this.env$); + const url = env.getWebVaultUrl(); + if (this.isWeb) { + await this.router.navigate(["/settings/account"], { + queryParams: { fromNewDeviceVerification: true }, + }); + } else { + this.platformUtilsService.launchUri( + url + "/#/settings/account/?fromNewDeviceVerification=true", + ); + } + } + + async remindMeLaterSelect() { + await this.newDeviceVerificationNoticeService.updateNewDeviceVerificationNoticeState( + this.currentUserId, + { + last_dismissal: new Date(), + permanent_dismissal: false, + }, + ); + + await this.router.navigate(["/vault"]); + } +} diff --git a/libs/vault/src/icons/exclamation-triangle.ts b/libs/vault/src/icons/exclamation-triangle.ts new file mode 100644 index 00000000000..6340546d1e1 --- /dev/null +++ b/libs/vault/src/icons/exclamation-triangle.ts @@ -0,0 +1,7 @@ +import { svgIcon } from "@bitwarden/components"; + +export const ExclamationTriangle = svgIcon` + + + +`; diff --git a/libs/vault/src/icons/index.ts b/libs/vault/src/icons/index.ts index c1b69a31ef5..2e106782f53 100644 --- a/libs/vault/src/icons/index.ts +++ b/libs/vault/src/icons/index.ts @@ -2,3 +2,5 @@ export * from "./deactivated-org"; export * from "./no-folders"; export * from "./vault"; export * from "./empty-trash"; +export * from "./exclamation-triangle"; +export * from "./user-lock"; diff --git a/libs/vault/src/icons/user-lock.ts b/libs/vault/src/icons/user-lock.ts new file mode 100644 index 00000000000..c1dc3efde39 --- /dev/null +++ b/libs/vault/src/icons/user-lock.ts @@ -0,0 +1,17 @@ +import { svgIcon } from "@bitwarden/components"; + +export const UserLock = svgIcon` + + + + + + + + + + + + + +`; diff --git a/libs/vault/src/index.ts b/libs/vault/src/index.ts index dca9b2dee79..0112de44241 100644 --- a/libs/vault/src/index.ts +++ b/libs/vault/src/index.ts @@ -14,5 +14,7 @@ export { export { DownloadAttachmentComponent } from "./components/download-attachment/download-attachment.component"; export { PasswordHistoryViewComponent } from "./components/password-history-view/password-history-view.component"; +export { NewDeviceVerificationNoticePageOneComponent } from "./components/new-device-verification-notice/new-device-verification-notice-page-one.component"; +export { NewDeviceVerificationNoticePageTwoComponent } from "./components/new-device-verification-notice/new-device-verification-notice-page-two.component"; export * as VaultIcons from "./icons"; diff --git a/libs/vault/src/services/new-device-verification-notice.service.ts b/libs/vault/src/services/new-device-verification-notice.service.ts index 6c7df590b50..bb096ff0c2c 100644 --- a/libs/vault/src/services/new-device-verification-notice.service.ts +++ b/libs/vault/src/services/new-device-verification-notice.service.ts @@ -57,7 +57,7 @@ export class NewDeviceVerificationNoticeService { } async updateNewDeviceVerificationNoticeState( - userId: UserId, + userId: UserId | null, newState: NewDeviceVerificationNotice, ): Promise { await this.noticeState(userId).update(() => { From 0f3803ac910b2df1e4b0a903b4838f7a5ba1f540 Mon Sep 17 00:00:00 2001 From: Nick Krantz <125900171+nick-livefront@users.noreply.github.com> Date: Thu, 19 Dec 2024 09:42:37 -0600 Subject: [PATCH 19/54] [PM-11442] Emergency Cipher Viewing (#12054) * force viewOnly to be true for emergency access * add input to hide password history, applicable when the view is used from emergency view * add extension refresh version of the emergency view dialog * allow emergency access to view password history - `ViewPasswordHistoryService` accepts a cipher id or CipherView. When a CipherView is included, the history component no longer has to fetch the cipher. * remove unused comments * Add fixme comment for removing non-extension refresh code * refactor password history component to accept a full cipher view - Remove the option to pass in only an id --- .../vault-password-history-v2.component.html | 4 +- ...ault-password-history-v2.component.spec.ts | 42 +++++-- .../vault-password-history-v2.component.ts | 32 +++++- ...wser-view-password-history.service.spec.ts | 6 +- .../browser-view-password-history.service.ts | 7 +- .../view/emergency-access-view.component.ts | 21 ++++ .../emergency-add-edit-cipher.component.ts | 12 +- .../view/emergency-view-dialog.component.html | 13 +++ .../emergency-view-dialog.component.spec.ts | 108 ++++++++++++++++++ .../view/emergency-view-dialog.component.ts | 90 +++++++++++++++ .../password-history.component.html | 2 +- .../password-history.component.ts | 18 +-- .../web-view-password-history.service.spec.ts | 8 +- .../web-view-password-history.service.ts | 6 +- .../view-password-history.service.ts | 4 +- .../item-history/item-history-v2.component.ts | 3 +- .../password-history-view.component.spec.ts | 5 +- .../password-history-view.component.ts | 39 +------ 18 files changed, 337 insertions(+), 83 deletions(-) create mode 100644 apps/web/src/app/auth/settings/emergency-access/view/emergency-view-dialog.component.html create mode 100644 apps/web/src/app/auth/settings/emergency-access/view/emergency-view-dialog.component.spec.ts create mode 100644 apps/web/src/app/auth/settings/emergency-access/view/emergency-view-dialog.component.ts diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-password-history-v2/vault-password-history-v2.component.html b/apps/browser/src/vault/popup/components/vault-v2/vault-password-history-v2/vault-password-history-v2.component.html index d4ff0662fe0..b1f01bb9cb0 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/vault-password-history-v2/vault-password-history-v2.component.html +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-password-history-v2/vault-password-history-v2.component.html @@ -1,8 +1,8 @@ - + - + diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-password-history-v2/vault-password-history-v2.component.spec.ts b/apps/browser/src/vault/popup/components/vault-v2/vault-password-history-v2/vault-password-history-v2.component.spec.ts index a375aba302e..9ac17b49386 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/vault-password-history-v2/vault-password-history-v2.component.spec.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-password-history-v2/vault-password-history-v2.component.spec.ts @@ -1,27 +1,40 @@ -import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { ComponentFixture, fakeAsync, TestBed, tick } from "@angular/core/testing"; import { ActivatedRoute } from "@angular/router"; import { mock } from "jest-mock-extended"; -import { Subject } from "rxjs"; +import { BehaviorSubject, Subject } from "rxjs"; import { WINDOW } from "@bitwarden/angular/services/injection-tokens"; -import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { PopupRouterCacheService } from "../../../../../platform/popup/view-cache/popup-router-cache.service"; import { PasswordHistoryV2Component } from "./vault-password-history-v2.component"; describe("PasswordHistoryV2Component", () => { - let component: PasswordHistoryV2Component; let fixture: ComponentFixture; const params$ = new Subject(); + + const mockCipherView = { + id: "111-222-333", + name: "cipher one", + } as CipherView; + + const mockCipher = { + decrypt: jest.fn().mockResolvedValue(mockCipherView), + } as unknown as Cipher; + const back = jest.fn().mockResolvedValue(undefined); + const getCipher = jest.fn().mockResolvedValue(mockCipher); beforeEach(async () => { back.mockClear(); + getCipher.mockClear(); await TestBed.configureTestingModule({ imports: [PasswordHistoryV2Component], @@ -29,8 +42,13 @@ describe("PasswordHistoryV2Component", () => { { provide: WINDOW, useValue: window }, { provide: PlatformUtilsService, useValue: mock() }, { provide: ConfigService, useValue: mock() }, - { provide: CipherService, useValue: mock() }, - { provide: AccountService, useValue: mock() }, + { provide: CipherService, useValue: mock({ get: getCipher }) }, + { + provide: AccountService, + useValue: mock({ + activeAccount$: new BehaviorSubject({ id: "acct-1" } as Account), + }), + }, { provide: PopupRouterCacheService, useValue: { back } }, { provide: ActivatedRoute, useValue: { queryParams: params$ } }, { provide: I18nService, useValue: { t: (key: string) => key } }, @@ -38,19 +56,21 @@ describe("PasswordHistoryV2Component", () => { }).compileComponents(); fixture = TestBed.createComponent(PasswordHistoryV2Component); - component = fixture.componentInstance; fixture.detectChanges(); }); - it("sets the cipherId from the params", () => { - params$.next({ cipherId: "444-33-33-1111" }); + it("loads the cipher from params the cipherId from the params", fakeAsync(() => { + params$.next({ cipherId: mockCipherView.id }); - expect(component["cipherId"]).toBe("444-33-33-1111"); - }); + tick(100); + + expect(getCipher).toHaveBeenCalledWith(mockCipherView.id); + })); it("navigates back when a cipherId is not in the params", () => { params$.next({}); expect(back).toHaveBeenCalledTimes(1); + expect(getCipher).not.toHaveBeenCalled(); }); }); diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-password-history-v2/vault-password-history-v2.component.ts b/apps/browser/src/vault/popup/components/vault-v2/vault-password-history-v2/vault-password-history-v2.component.ts index c70c83f40fc..c8f590ced57 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/vault-password-history-v2/vault-password-history-v2.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-password-history-v2/vault-password-history-v2.component.ts @@ -3,10 +3,14 @@ import { NgIf } from "@angular/common"; import { Component, OnInit } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; -import { first } from "rxjs/operators"; +import { firstValueFrom } from "rxjs"; +import { first, map } from "rxjs/operators"; import { JslibModule } from "@bitwarden/angular/jslib.module"; -import { CipherId } from "@bitwarden/common/types/guid"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { UserId } from "@bitwarden/common/types/guid"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { PasswordHistoryViewComponent } from "../../../../../../../../libs/vault/src/components/password-history-view/password-history-view.component"; import { PopOutComponent } from "../../../../../platform/popup/components/pop-out.component"; @@ -28,18 +32,20 @@ import { PopupRouterCacheService } from "../../../../../platform/popup/view-cach ], }) export class PasswordHistoryV2Component implements OnInit { - protected cipherId: CipherId; + protected cipher: CipherView; constructor( private browserRouterHistory: PopupRouterCacheService, private route: ActivatedRoute, + private cipherService: CipherService, + private accountService: AccountService, ) {} ngOnInit() { // eslint-disable-next-line rxjs-angular/prefer-takeuntil this.route.queryParams.pipe(first()).subscribe((params) => { if (params.cipherId) { - this.cipherId = params.cipherId; + void this.loadCipher(params.cipherId); } else { this.close(); } @@ -49,4 +55,22 @@ export class PasswordHistoryV2Component implements OnInit { close() { void this.browserRouterHistory.back(); } + + /** Load the cipher based on the given Id */ + private async loadCipher(cipherId: string) { + const cipher = await this.cipherService.get(cipherId); + + const activeAccount = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a: { id: string | undefined }) => a)), + ); + + if (!activeAccount?.id) { + throw new Error("Active account is not available."); + } + + const activeUserId = activeAccount.id as UserId; + this.cipher = await cipher.decrypt( + await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId), + ); + } } diff --git a/apps/browser/src/vault/popup/services/browser-view-password-history.service.spec.ts b/apps/browser/src/vault/popup/services/browser-view-password-history.service.spec.ts index ded4686477e..5024b960d9c 100644 --- a/apps/browser/src/vault/popup/services/browser-view-password-history.service.spec.ts +++ b/apps/browser/src/vault/popup/services/browser-view-password-history.service.spec.ts @@ -2,6 +2,8 @@ import { TestBed } from "@angular/core/testing"; import { Router } from "@angular/router"; import { mock, MockProxy } from "jest-mock-extended"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; + import { BrowserViewPasswordHistoryService } from "./browser-view-password-history.service"; describe("BrowserViewPasswordHistoryService", () => { @@ -19,9 +21,9 @@ describe("BrowserViewPasswordHistoryService", () => { describe("viewPasswordHistory", () => { it("navigates to the password history screen", async () => { - await service.viewPasswordHistory("test"); + await service.viewPasswordHistory({ id: "cipher-id" } as CipherView); expect(router.navigate).toHaveBeenCalledWith(["/cipher-password-history"], { - queryParams: { cipherId: "test" }, + queryParams: { cipherId: "cipher-id" }, }); }); }); diff --git a/apps/browser/src/vault/popup/services/browser-view-password-history.service.ts b/apps/browser/src/vault/popup/services/browser-view-password-history.service.ts index 453fe113ebf..5e400da9de5 100644 --- a/apps/browser/src/vault/popup/services/browser-view-password-history.service.ts +++ b/apps/browser/src/vault/popup/services/browser-view-password-history.service.ts @@ -4,6 +4,7 @@ import { inject } from "@angular/core"; import { Router } from "@angular/router"; import { ViewPasswordHistoryService } from "@bitwarden/common/vault/abstractions/view-password-history.service"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; /** * This class handles the premium upgrade process for the browser extension. @@ -14,7 +15,9 @@ export class BrowserViewPasswordHistoryService implements ViewPasswordHistorySer /** * Navigates to the password history screen. */ - async viewPasswordHistory(cipherId: string) { - await this.router.navigate(["/cipher-password-history"], { queryParams: { cipherId } }); + async viewPasswordHistory(cipher: CipherView) { + await this.router.navigate(["/cipher-password-history"], { + queryParams: { cipherId: cipher.id }, + }); } } diff --git a/apps/web/src/app/auth/settings/emergency-access/view/emergency-access-view.component.ts b/apps/web/src/app/auth/settings/emergency-access/view/emergency-access-view.component.ts index 6a72360cfad..7506f6c5d0b 100644 --- a/apps/web/src/app/auth/settings/emergency-access/view/emergency-access-view.component.ts +++ b/apps/web/src/app/auth/settings/emergency-access/view/emergency-access-view.component.ts @@ -4,16 +4,22 @@ import { Component, OnInit, ViewChild, ViewContainerRef } from "@angular/core"; import { ActivatedRoute, Router } from "@angular/router"; import { ModalService } from "@bitwarden/angular/services/modal.service"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { DialogService } from "@bitwarden/components"; +import { CipherFormConfigService, DefaultCipherFormConfigService } from "@bitwarden/vault"; import { EmergencyAccessService } from "../../../emergency-access"; import { EmergencyAccessAttachmentsComponent } from "../attachments/emergency-access-attachments.component"; import { EmergencyAddEditCipherComponent } from "./emergency-add-edit-cipher.component"; +import { EmergencyViewDialogComponent } from "./emergency-view-dialog.component"; @Component({ selector: "emergency-access-view", templateUrl: "emergency-access-view.component.html", + providers: [{ provide: CipherFormConfigService, useClass: DefaultCipherFormConfigService }], }) // eslint-disable-next-line rxjs-angular/prefer-takeuntil export class EmergencyAccessViewComponent implements OnInit { @@ -31,6 +37,8 @@ export class EmergencyAccessViewComponent implements OnInit { private router: Router, private route: ActivatedRoute, private emergencyAccessService: EmergencyAccessService, + private configService: ConfigService, + private dialogService: DialogService, ) {} ngOnInit() { @@ -49,6 +57,19 @@ export class EmergencyAccessViewComponent implements OnInit { } async selectCipher(cipher: CipherView) { + const browserRefreshEnabled = await this.configService.getFeatureFlag( + FeatureFlag.ExtensionRefresh, + ); + + if (browserRefreshEnabled) { + EmergencyViewDialogComponent.open(this.dialogService, { + cipher, + }); + return; + } + + // FIXME PM-15385: Remove below dialog service logic once extension refresh is live. + // eslint-disable-next-line const [_, childComponent] = await this.modalService.openViewRef( EmergencyAddEditCipherComponent, diff --git a/apps/web/src/app/auth/settings/emergency-access/view/emergency-add-edit-cipher.component.ts b/apps/web/src/app/auth/settings/emergency-access/view/emergency-add-edit-cipher.component.ts index 2da8e06449a..59228431e65 100644 --- a/apps/web/src/app/auth/settings/emergency-access/view/emergency-add-edit-cipher.component.ts +++ b/apps/web/src/app/auth/settings/emergency-access/view/emergency-add-edit-cipher.component.ts @@ -1,7 +1,7 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { DatePipe } from "@angular/common"; -import { Component } from "@angular/core"; +import { Component, OnInit } from "@angular/core"; import { CollectionService } from "@bitwarden/admin-console/common"; import { AuditService } from "@bitwarden/common/abstractions/audit.service"; @@ -30,7 +30,7 @@ import { AddEditComponent as BaseAddEditComponent } from "../../../../vault/indi selector: "app-org-vault-add-edit", templateUrl: "../../../../vault/individual-vault/add-edit.component.html", }) -export class EmergencyAddEditCipherComponent extends BaseAddEditComponent { +export class EmergencyAddEditCipherComponent extends BaseAddEditComponent implements OnInit { originalCipher: Cipher = null; viewOnly = true; protected override componentName = "app-org-vault-add-edit"; @@ -85,6 +85,14 @@ export class EmergencyAddEditCipherComponent extends BaseAddEditComponent { this.title = this.i18nService.t("viewItem"); } + async ngOnInit(): Promise { + await super.ngOnInit(); + // The base component `ngOnInit` calculates the `viewOnly` property based on cipher properties + // In the case of emergency access, `viewOnly` should always be true, set it manually here after + // the base `ngOnInit` is complete. + this.viewOnly = true; + } + protected async loadCipher() { return Promise.resolve(this.originalCipher); } diff --git a/apps/web/src/app/auth/settings/emergency-access/view/emergency-view-dialog.component.html b/apps/web/src/app/auth/settings/emergency-access/view/emergency-view-dialog.component.html new file mode 100644 index 00000000000..be38e1d9505 --- /dev/null +++ b/apps/web/src/app/auth/settings/emergency-access/view/emergency-view-dialog.component.html @@ -0,0 +1,13 @@ + + + {{ title }} + +
+ +
+ + + +
diff --git a/apps/web/src/app/auth/settings/emergency-access/view/emergency-view-dialog.component.spec.ts b/apps/web/src/app/auth/settings/emergency-access/view/emergency-view-dialog.component.spec.ts new file mode 100644 index 00000000000..341e44f643b --- /dev/null +++ b/apps/web/src/app/auth/settings/emergency-access/view/emergency-view-dialog.component.spec.ts @@ -0,0 +1,108 @@ +import { DIALOG_DATA, DialogRef } from "@angular/cdk/dialog"; +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { By } from "@angular/platform-browser"; +import { NoopAnimationsModule } from "@angular/platform-browser/animations"; +import { mock } from "jest-mock-extended"; + +import { CollectionService } from "@bitwarden/admin-console/common"; +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; +import { CipherType } from "@bitwarden/common/vault/enums"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { DialogService } from "@bitwarden/components"; + +import { EmergencyViewDialogComponent } from "./emergency-view-dialog.component"; + +describe("EmergencyViewDialogComponent", () => { + let component: EmergencyViewDialogComponent; + let fixture: ComponentFixture; + + const open = jest.fn(); + const close = jest.fn(); + + const mockCipher = { + id: "cipher1", + name: "Cipher", + type: CipherType.Login, + login: { uris: [] }, + card: {}, + } as CipherView; + + beforeEach(async () => { + open.mockClear(); + close.mockClear(); + + await TestBed.configureTestingModule({ + imports: [EmergencyViewDialogComponent, NoopAnimationsModule], + providers: [ + { provide: OrganizationService, useValue: mock() }, + { provide: CollectionService, useValue: mock() }, + { provide: FolderService, useValue: mock() }, + { provide: I18nService, useValue: { t: (...keys: string[]) => keys.join(" ") } }, + { provide: DialogService, useValue: { open } }, + { provide: DialogRef, useValue: { close } }, + { provide: DIALOG_DATA, useValue: { cipher: mockCipher } }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(EmergencyViewDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it("creates", () => { + expect(component).toBeTruthy(); + }); + + it("opens dialog", () => { + EmergencyViewDialogComponent.open({ open } as unknown as DialogService, { cipher: mockCipher }); + + expect(open).toHaveBeenCalled(); + }); + + it("closes the dialog", () => { + EmergencyViewDialogComponent.open({ open } as unknown as DialogService, { cipher: mockCipher }); + fixture.detectChanges(); + + const cancelButton = fixture.debugElement.queryAll(By.css("button")).pop(); + + cancelButton.nativeElement.click(); + + expect(close).toHaveBeenCalled(); + }); + + describe("updateTitle", () => { + it("sets login title", () => { + mockCipher.type = CipherType.Login; + + component["updateTitle"](); + + expect(component["title"]).toBe("viewItemType typelogin"); + }); + + it("sets card title", () => { + mockCipher.type = CipherType.Card; + + component["updateTitle"](); + + expect(component["title"]).toBe("viewItemType typecard"); + }); + + it("sets identity title", () => { + mockCipher.type = CipherType.Identity; + + component["updateTitle"](); + + expect(component["title"]).toBe("viewItemType typeidentity"); + }); + + it("sets note title", () => { + mockCipher.type = CipherType.SecureNote; + + component["updateTitle"](); + + expect(component["title"]).toBe("viewItemType note"); + }); + }); +}); diff --git a/apps/web/src/app/auth/settings/emergency-access/view/emergency-view-dialog.component.ts b/apps/web/src/app/auth/settings/emergency-access/view/emergency-view-dialog.component.ts new file mode 100644 index 00000000000..7da4ce3165b --- /dev/null +++ b/apps/web/src/app/auth/settings/emergency-access/view/emergency-view-dialog.component.ts @@ -0,0 +1,90 @@ +import { DIALOG_DATA, DialogRef } from "@angular/cdk/dialog"; +import { CommonModule } from "@angular/common"; +import { Component, Inject } from "@angular/core"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service"; +import { ViewPasswordHistoryService } from "@bitwarden/common/vault/abstractions/view-password-history.service"; +import { CipherType } from "@bitwarden/common/vault/enums"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { ButtonModule, DialogModule, DialogService } from "@bitwarden/components"; +import { CipherViewComponent } from "@bitwarden/vault"; + +import { WebViewPasswordHistoryService } from "../../../../vault/services/web-view-password-history.service"; + +export interface EmergencyViewDialogParams { + /** The cipher being viewed. */ + cipher: CipherView; +} + +/** Stubbed class, premium upgrade is not applicable for emergency viewing */ +class PremiumUpgradePromptNoop implements PremiumUpgradePromptService { + async promptForPremium() { + return Promise.resolve(); + } +} + +@Component({ + selector: "app-emergency-view-dialog", + templateUrl: "emergency-view-dialog.component.html", + standalone: true, + imports: [ButtonModule, CipherViewComponent, DialogModule, CommonModule, JslibModule], + providers: [ + { provide: ViewPasswordHistoryService, useClass: WebViewPasswordHistoryService }, + { provide: PremiumUpgradePromptService, useClass: PremiumUpgradePromptNoop }, + ], +}) +export class EmergencyViewDialogComponent { + /** + * The title of the dialog. Updates based on the cipher type. + * @protected + */ + protected title: string; + + constructor( + @Inject(DIALOG_DATA) protected params: EmergencyViewDialogParams, + private dialogRef: DialogRef, + private i18nService: I18nService, + ) { + this.updateTitle(); + } + + get cipher(): CipherView { + return this.params.cipher; + } + + cancel = () => { + this.dialogRef.close(); + }; + + private updateTitle() { + const partOne = "viewItemType"; + + const type = this.cipher.type; + + switch (type) { + case CipherType.Login: + this.title = this.i18nService.t(partOne, this.i18nService.t("typeLogin").toLowerCase()); + break; + case CipherType.Card: + this.title = this.i18nService.t(partOne, this.i18nService.t("typeCard").toLowerCase()); + break; + case CipherType.Identity: + this.title = this.i18nService.t(partOne, this.i18nService.t("typeIdentity").toLowerCase()); + break; + case CipherType.SecureNote: + this.title = this.i18nService.t(partOne, this.i18nService.t("note").toLowerCase()); + break; + } + } + + /** + * Opens the EmergencyViewDialog. + */ + static open(dialogService: DialogService, params: EmergencyViewDialogParams) { + return dialogService.open(EmergencyViewDialogComponent, { + data: params, + }); + } +} diff --git a/apps/web/src/app/vault/individual-vault/password-history.component.html b/apps/web/src/app/vault/individual-vault/password-history.component.html index 7127e7ca649..4eaca8f736e 100644 --- a/apps/web/src/app/vault/individual-vault/password-history.component.html +++ b/apps/web/src/app/vault/individual-vault/password-history.component.html @@ -3,7 +3,7 @@ {{ "passwordHistory" | i18n }} - + - -
  • - -
  • -
  • - -
  • -
  • - -
  • -
  • - -
  • - - -
    diff --git a/apps/browser/src/popup/tabs.component.ts b/apps/browser/src/popup/tabs.component.ts deleted file mode 100644 index 7546c9ca13b..00000000000 --- a/apps/browser/src/popup/tabs.component.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Component, OnInit } from "@angular/core"; - -import BrowserPopupUtils from "../platform/popup/browser-popup-utils"; - -@Component({ - selector: "app-tabs", - templateUrl: "tabs.component.html", -}) -export class TabsComponent implements OnInit { - showCurrentTab = true; - - ngOnInit() { - this.showCurrentTab = !BrowserPopupUtils.inPopout(window); - } -} From b27a1a5337f765ee3fa9c07f51083c29c69a68da Mon Sep 17 00:00:00 2001 From: Nick Krantz <125900171+nick-livefront@users.noreply.github.com> Date: Fri, 20 Dec 2024 09:44:36 -0600 Subject: [PATCH 35/54] [PM-12998] View Cipher: Color Password (#12354) * show color password for visible passwords in vault view - The password input will be visually hidden - Adds tests for the login credentials component * formatting --- .../login-credentials-view.component.html | 23 +- .../login-credentials-view.component.spec.ts | 198 ++++++++++++++++++ 2 files changed, 218 insertions(+), 3 deletions(-) create mode 100644 libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.spec.ts diff --git a/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.html b/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.html index 5b6b995d095..8503604bf7c 100644 --- a/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.html +++ b/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.html @@ -28,17 +28,34 @@ > - {{ "password" | i18n }} + + {{ "password" | i18n }} + + + + + + +
    + +
  • +
    + + +
    +
  • + +
    +`; + exports[`AutofillInlineMenuList initAutofillInlineMenuList the locked inline menu for an unauthenticated user creates the views for the locked inline menu 1`] = `
    { expect(autofillInlineMenuList["inlineMenuListContainer"]).toMatchSnapshot(); }); + it("renders correctly when there are multiple TOTP elements with username displayed", async () => { + const totpCipher1 = createAutofillOverlayCipherDataMock(1, { + type: CipherType.Login, + login: { + totp: "123456", + totpField: true, + username: "user1", + }, + }); + + const totpCipher2 = createAutofillOverlayCipherDataMock(2, { + type: CipherType.Login, + login: { + totp: "654321", + totpField: true, + username: "user2", + }, + }); + + postWindowMessage( + createInitAutofillInlineMenuListMessageMock({ + inlineMenuFillType: CipherType.Login, + ciphers: [totpCipher1, totpCipher2], + }), + ); + + await flushPromises(); + const checkSubtitleElement = (username: string) => { + const subtitleElement = autofillInlineMenuList["inlineMenuListContainer"].querySelector( + `span.cipher-subtitle[title="${username}"]`, + ); + expect(subtitleElement).not.toBeNull(); + expect(subtitleElement.textContent).toBe(username); + }; + + checkSubtitleElement("user1"); + checkSubtitleElement("user2"); + + expect(autofillInlineMenuList["inlineMenuListContainer"]).toMatchSnapshot(); + }); + it("creates the view for a totp field", () => { postWindowMessage( createInitAutofillInlineMenuListMessageMock({ diff --git a/apps/browser/src/autofill/overlay/inline-menu/pages/list/autofill-inline-menu-list.ts b/apps/browser/src/autofill/overlay/inline-menu/pages/list/autofill-inline-menu-list.ts index 6cf390d0a29..b7837505d41 100644 --- a/apps/browser/src/autofill/overlay/inline-menu/pages/list/autofill-inline-menu-list.ts +++ b/apps/browser/src/autofill/overlay/inline-menu/pages/list/autofill-inline-menu-list.ts @@ -1163,7 +1163,7 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement { } if (cipher.login?.totpField && cipher.login?.totp) { - return this.buildTotpElement(cipher.login?.totp); + return this.buildTotpElement(cipher.login?.totp, cipher.login?.username); } const subTitleText = this.getSubTitleText(cipher); const cipherSubtitleElement = this.buildCipherSubtitleElement(subTitleText); @@ -1174,13 +1174,24 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement { return cipherDetailsElement; } + /** + * Checks if there is more than one TOTP element being displayed. + * + * @returns {boolean} - Returns true if more than one TOTP element is displayed, otherwise false. + */ + private multipleTotpElements(): boolean { + return ( + this.ciphers.filter((cipher) => cipher.login?.totpField && cipher.login?.totp).length > 1 + ); + } + /** * Builds a TOTP element for a given TOTP code. * * @param totp - The TOTP code to display. */ - private buildTotpElement(totpCode: string): HTMLDivElement | null { + private buildTotpElement(totpCode: string, username?: string): HTMLDivElement | null { if (!totpCode) { return null; } @@ -1196,12 +1207,17 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement { containerElement.appendChild(totpHeading); - const subtitleElement = document.createElement("span"); - subtitleElement.classList.add("cipher-subtitle"); - subtitleElement.textContent = formattedTotpCode; - subtitleElement.setAttribute("aria-label", this.getTranslation("totpCodeAria")); - subtitleElement.setAttribute("data-testid", "totp-code"); - containerElement.appendChild(subtitleElement); + if (this.multipleTotpElements() && username) { + const usernameSubtitle = this.buildCipherSubtitleElement(username); + containerElement.appendChild(usernameSubtitle); + } + + const totpCodeSpan = document.createElement("span"); + totpCodeSpan.classList.add("cipher-subtitle"); + totpCodeSpan.textContent = formattedTotpCode; + totpCodeSpan.setAttribute("aria-label", this.getTranslation("totpCodeAria")); + totpCodeSpan.setAttribute("data-testid", "totp-code"); + containerElement.appendChild(totpCodeSpan); return containerElement; } From 2e6031eee90631274864f9b9ba65e935132ed32f Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Fri, 20 Dec 2024 17:36:04 +0100 Subject: [PATCH 38/54] Increase heap size for builds (#12483) * Increase heap size for builds * Add cross-env to cli * Add cross-env to desktop * Update apps/web/package.json Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com> --------- Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com> --- apps/browser/package.json | 10 +++++----- apps/cli/package.json | 4 ++-- apps/desktop/package.json | 2 +- apps/web/package.json | 4 ++-- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/apps/browser/package.json b/apps/browser/package.json index 2b3edd96b6e..7c839ecc3d9 100644 --- a/apps/browser/package.json +++ b/apps/browser/package.json @@ -14,11 +14,11 @@ "build:watch:firefox": "npm run build:firefox -- --watch", "build:watch:opera": "npm run build:opera -- --watch", "build:watch:safari": "npm run build:safari -- --watch", - "build:prod:chrome": "cross-env NODE_ENV=production NODE_OPTIONS=\"--max-old-space-size=4096\" npm run build:chrome", - "build:prod:edge": "cross-env NODE_ENV=production NODE_OPTIONS=\"--max-old-space-size=4096\" npm run build:edge", - "build:prod:firefox": "cross-env NODE_ENV=production NODE_OPTIONS=\"--max-old-space-size=4096\" npm run build:firefox", - "build:prod:opera": "cross-env NODE_ENV=production NODE_OPTIONS=\"--max-old-space-size=4096\" npm run build:opera", - "build:prod:safari": "cross-env NODE_ENV=production NODE_OPTIONS=\"--max-old-space-size=4096\" npm run build:safari", + "build:prod:chrome": "cross-env NODE_ENV=production NODE_OPTIONS=\"--max-old-space-size=8192\" npm run build:chrome", + "build:prod:edge": "cross-env NODE_ENV=production NODE_OPTIONS=\"--max-old-space-size=8192\" npm run build:edge", + "build:prod:firefox": "cross-env NODE_ENV=production NODE_OPTIONS=\"--max-old-space-size=8192\" npm run build:firefox", + "build:prod:opera": "cross-env NODE_ENV=production NODE_OPTIONS=\"--max-old-space-size=8192\" npm run build:opera", + "build:prod:safari": "cross-env NODE_ENV=production NODE_OPTIONS=\"--max-old-space-size=8192\" npm run build:safari", "dist:chrome": "npm run build:prod:chrome && mkdir -p dist && ./scripts/compress.ps1 dist-chrome.zip", "dist:edge": "npm run build:prod:edge && mkdir -p dist && ./scripts/compress.ps1 dist-edge.zip", "dist:firefox": "npm run build:prod:firefox && mkdir -p dist && ./scripts/compress.ps1 dist-firefox.zip", diff --git a/apps/cli/package.json b/apps/cli/package.json index b7d54e78e1d..b25f327c42a 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -18,14 +18,14 @@ "license": "SEE LICENSE IN LICENSE.txt", "scripts": { "clean": "rimraf dist", - "build:oss": "webpack", + "build:oss": "cross-env NODE_OPTIONS=\"--max-old-space-size=8192\" webpack", "build:oss:debug": "npm run build:oss && node --inspect ./build/bw.js", "build:oss:watch": "webpack --watch", "build:oss:prod": "cross-env NODE_ENV=production webpack", "build:oss:prod:watch": "cross-env NODE_ENV=production webpack --watch", "debug": "node --inspect ./build/bw.js", "publish:npm": "npm run build:oss:prod && npm publish --access public", - "build:bit": "webpack -c ../../bitwarden_license/bit-cli/webpack.config.js", + "build:bit": "cross-env NODE_OPTIONS=\"--max-old-space-size=8192\" webpack -c ../../bitwarden_license/bit-cli/webpack.config.js", "build:bit:debug": "npm run build:bit && node --inspect ./build/bw.js", "build:bit:watch": "webpack --watch -c ../../bitwarden_license/bit-cli/webpack.config.js", "build:bit:prod": "cross-env NODE_ENV=production npm run build:bit", diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 10d5ded3448..7c6d1f37692 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -19,7 +19,7 @@ "postinstall": "electron-rebuild", "start": "cross-env ELECTRON_IS_DEV=0 ELECTRON_NO_UPDATER=1 electron ./build", "build-native": "cd desktop_native && node build.js", - "build": "concurrently -n Main,Rend,Prel -c yellow,cyan \"npm run build:main\" \"npm run build:renderer\" \"npm run build:preload\"", + "build": "cross-env NODE_OPTIONS=\"--max-old-space-size=8192\" concurrently -n Main,Rend,Prel -c yellow,cyan \"npm run build:main\" \"npm run build:renderer\" \"npm run build:preload\"", "build:dev": "concurrently -n Main,Rend -c yellow,cyan \"npm run build:main:dev\" \"npm run build:renderer:dev\"", "build:preload": "cross-env NODE_ENV=production webpack --config webpack.preload.js", "build:preload:watch": "cross-env NODE_ENV=production webpack --config webpack.preload.js --watch", diff --git a/apps/web/package.json b/apps/web/package.json index dee8e7722c9..f2697e1b68d 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -2,8 +2,8 @@ "name": "@bitwarden/web-vault", "version": "2024.12.1", "scripts": { - "build:oss": "webpack", - "build:bit": "webpack -c ../../bitwarden_license/bit-web/webpack.config.js", + "build:oss": "cross-env NODE_OPTIONS=\"--max-old-space-size=8192\" webpack", + "build:bit": "cross-env NODE_OPTIONS=\"--max-old-space-size=8192\" webpack -c ../../bitwarden_license/bit-web/webpack.config.js", "build:oss:watch": "webpack serve", "build:bit:watch": "webpack serve -c ../../bitwarden_license/bit-web/webpack.config.js", "build:bit:dev": "cross-env ENV=development npm run build:bit", From d209da4c945b9fddbc480c779150989596bfd90f Mon Sep 17 00:00:00 2001 From: rr-bw <102181210+rr-bw@users.noreply.github.com> Date: Fri, 20 Dec 2024 10:23:03 -0800 Subject: [PATCH 39/54] feat(auth): [PM-9674] Remove Deprecated LockComponents (#12453) This PR deletes the legacy lock components from the Angular clients and also removes feature flag control from the routing. The lock component will now be based entirely on the new, recently refreshed LockComponent in libs/auth/angular. --- .../src/auth/popup/lock.component.html | 100 ---- apps/browser/src/auth/popup/lock.component.ts | 185 ------- apps/browser/src/popup/app-routing.module.ts | 21 +- apps/browser/src/popup/app.module.ts | 2 - apps/desktop/src/app/app-routing.module.ts | 16 +- apps/desktop/src/app/app.module.ts | 2 - apps/desktop/src/auth/lock.component.html | 80 --- apps/desktop/src/auth/lock.component.spec.ts | 478 ------------------ apps/desktop/src/auth/lock.component.ts | 235 --------- apps/web/src/app/auth/lock.component.html | 27 - apps/web/src/app/auth/lock.component.ts | 51 -- apps/web/src/app/oss-routing.module.ts | 59 +-- .../src/auth/components/lock.component.ts | 398 --------------- libs/auth/src/angular/lock/lock.component.ts | 6 +- 14 files changed, 31 insertions(+), 1629 deletions(-) delete mode 100644 apps/browser/src/auth/popup/lock.component.html delete mode 100644 apps/browser/src/auth/popup/lock.component.ts delete mode 100644 apps/desktop/src/auth/lock.component.html delete mode 100644 apps/desktop/src/auth/lock.component.spec.ts delete mode 100644 apps/desktop/src/auth/lock.component.ts delete mode 100644 apps/web/src/app/auth/lock.component.html delete mode 100644 apps/web/src/app/auth/lock.component.ts delete mode 100644 libs/angular/src/auth/components/lock.component.ts diff --git a/apps/browser/src/auth/popup/lock.component.html b/apps/browser/src/auth/popup/lock.component.html deleted file mode 100644 index fb1b09de49c..00000000000 --- a/apps/browser/src/auth/popup/lock.component.html +++ /dev/null @@ -1,100 +0,0 @@ -
    - -
    -

    - {{ "verifyIdentity" | i18n }} -

    -
    - -
    -
    -
    - -
    -
    -
    -
    - - -
    -
    - - -
    -
    - -
    -
    -
    - -
    -
    - -
    -

    - -

    - {{ biometricError }} -

    - {{ "awaitDesktop" | i18n }} -

    - - -
    -
    -
    diff --git a/apps/browser/src/auth/popup/lock.component.ts b/apps/browser/src/auth/popup/lock.component.ts deleted file mode 100644 index 66de3fb89d2..00000000000 --- a/apps/browser/src/auth/popup/lock.component.ts +++ /dev/null @@ -1,185 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { Component, NgZone, OnInit } from "@angular/core"; -import { Router } from "@angular/router"; -import { firstValueFrom } from "rxjs"; - -import { LockComponent as BaseLockComponent } from "@bitwarden/angular/auth/components/lock.component"; -import { PinServiceAbstraction } from "@bitwarden/auth/common"; -import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; -import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service"; -import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction"; -import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; -import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; -import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; -import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; -import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; -import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; -import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; -import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; -import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; -import { DialogService, ToastService } from "@bitwarden/components"; -import { - KdfConfigService, - KeyService, - BiometricsService, - BiometricStateService, -} from "@bitwarden/key-management"; - -import { BiometricErrors, BiometricErrorTypes } from "../../models/biometricErrors"; -import { BrowserRouterService } from "../../platform/popup/services/browser-router.service"; -import { fido2PopoutSessionData$ } from "../../vault/popup/utils/fido2-popout-session-data"; - -@Component({ - selector: "app-lock", - templateUrl: "lock.component.html", -}) -export class LockComponent extends BaseLockComponent implements OnInit { - private isInitialLockScreen: boolean; - - biometricError: string; - pendingBiometric = false; - fido2PopoutSessionData$ = fido2PopoutSessionData$(); - - constructor( - masterPasswordService: InternalMasterPasswordServiceAbstraction, - router: Router, - i18nService: I18nService, - platformUtilsService: PlatformUtilsService, - messagingService: MessagingService, - keyService: KeyService, - vaultTimeoutService: VaultTimeoutService, - vaultTimeoutSettingsService: VaultTimeoutSettingsService, - environmentService: EnvironmentService, - stateService: StateService, - apiService: ApiService, - logService: LogService, - ngZone: NgZone, - policyApiService: PolicyApiServiceAbstraction, - policyService: InternalPolicyService, - passwordStrengthService: PasswordStrengthServiceAbstraction, - authService: AuthService, - dialogService: DialogService, - deviceTrustService: DeviceTrustServiceAbstraction, - userVerificationService: UserVerificationService, - pinService: PinServiceAbstraction, - private routerService: BrowserRouterService, - biometricStateService: BiometricStateService, - biometricsService: BiometricsService, - accountService: AccountService, - kdfConfigService: KdfConfigService, - syncService: SyncService, - toastService: ToastService, - ) { - super( - masterPasswordService, - router, - i18nService, - platformUtilsService, - messagingService, - keyService, - vaultTimeoutService, - vaultTimeoutSettingsService, - environmentService, - stateService, - apiService, - logService, - ngZone, - policyApiService, - policyService, - passwordStrengthService, - dialogService, - deviceTrustService, - userVerificationService, - pinService, - biometricStateService, - biometricsService, - accountService, - authService, - kdfConfigService, - syncService, - toastService, - ); - this.successRoute = "/tabs/current"; - this.isInitialLockScreen = (window as any).previousPopupUrl == null; - - this.onSuccessfulSubmit = async () => { - const previousUrl = this.routerService.getPreviousUrl(); - if (previousUrl) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigateByUrl(previousUrl); - } else { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate([this.successRoute]); - } - }; - } - - async ngOnInit() { - await super.ngOnInit(); - const autoBiometricsPrompt = await firstValueFrom( - this.biometricStateService.promptAutomatically$, - ); - - window.setTimeout(async () => { - document.getElementById(this.pinEnabled ? "pin" : "masterPassword")?.focus(); - if ( - this.biometricLock && - autoBiometricsPrompt && - this.isInitialLockScreen && - (await this.authService.getAuthStatus()) === AuthenticationStatus.Locked - ) { - await this.unlockBiometric(true); - } - }, 100); - } - - override async unlockBiometric(automaticPrompt: boolean = false): Promise { - if (!this.biometricLock) { - return; - } - - this.biometricError = null; - - let success; - try { - const available = await super.isBiometricUnlockAvailable(); - if (!available) { - if (!automaticPrompt) { - await this.dialogService.openSimpleDialog({ - type: "warning", - title: { key: "biometricsNotAvailableTitle" }, - content: { key: "biometricsNotAvailableDesc" }, - acceptButtonText: { key: "ok" }, - cancelButtonText: null, - }); - } - } else { - this.pendingBiometric = true; - success = await super.unlockBiometric(); - } - } catch (e) { - const error = BiometricErrors[e?.message as BiometricErrorTypes]; - - if (error == null) { - this.logService.error("Unknown error: " + e); - return false; - } - - this.biometricError = this.i18nService.t(error.description); - } finally { - this.pendingBiometric = false; - } - - return success; - } -} diff --git a/apps/browser/src/popup/app-routing.module.ts b/apps/browser/src/popup/app-routing.module.ts index 73147cace23..549a677857c 100644 --- a/apps/browser/src/popup/app-routing.module.ts +++ b/apps/browser/src/popup/app-routing.module.ts @@ -17,7 +17,6 @@ import { unauthGuardFn, } from "@bitwarden/angular/auth/guards"; import { canAccessFeature } from "@bitwarden/angular/platform/guard/feature-flag.guard"; -import { extensionRefreshRedirect } from "@bitwarden/angular/utils/extension-refresh-redirect"; import { extensionRefreshSwap } from "@bitwarden/angular/utils/extension-refresh-swap"; import { NewDeviceVerificationNoticeGuard } from "@bitwarden/angular/vault/guards"; import { @@ -26,7 +25,7 @@ import { LoginComponent, LoginSecondaryContentComponent, LockIcon, - LockV2Component, + LockComponent, LoginViaAuthRequestComponent, PasswordHintComponent, RegistrationFinishComponent, @@ -60,7 +59,6 @@ import { } from "../auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component"; import { HintComponent } from "../auth/popup/hint.component"; import { HomeComponent } from "../auth/popup/home.component"; -import { LockComponent } from "../auth/popup/lock.component"; import { LoginDecryptionOptionsComponentV1 } from "../auth/popup/login-decryption-options/login-decryption-options-v1.component"; import { LoginComponentV1 } from "../auth/popup/login-v1.component"; import { LoginViaAuthRequestComponentV1 } from "../auth/popup/login-via-auth-request-v1.component"; @@ -173,13 +171,6 @@ const routes: Routes = [ canActivate: [fido2AuthGuard], data: { elevation: 1 } satisfies RouteDataProperties, }), - { - path: "lock", - component: LockComponent, - canActivate: [lockGuard()], - canMatch: [extensionRefreshRedirect("/lockV2")], - data: { elevation: 1, doNotSaveUrl: true } satisfies RouteDataProperties, - }, ...twofactorRefactorSwap( TwoFactorComponent, AnonLayoutWrapperComponent, @@ -650,8 +641,8 @@ const routes: Routes = [ ], }, { - path: "lockV2", - canActivate: [canAccessFeature(FeatureFlag.ExtensionRefresh), lockGuard()], + path: "lock", + canActivate: [lockGuard()], data: { pageIcon: LockIcon, pageTitle: { @@ -662,19 +653,19 @@ const routes: Routes = [ elevation: 1, /** * This ensures that in a passkey flow the `/fido2?` URL does not get - * overwritten in the `BrowserRouterService` by the `/lockV2` route. This way, after + * overwritten in the `BrowserRouterService` by the `/lock` route. This way, after * unlocking, the user can be redirected back to the `/fido2?` URL. * * Also, this prevents a routing loop when using biometrics to unlock the vault in MV2 (Firefox), * locking up the browser (https://bitwarden.atlassian.net/browse/PM-16116). This involves the - * `popup-router-cache.service` pushing the `lockV2` route to the history. + * `popup-router-cache.service` pushing the `lock` route to the history. */ doNotSaveUrl: true, } satisfies ExtensionAnonLayoutWrapperData & RouteDataProperties, children: [ { path: "", - component: LockV2Component, + component: LockComponent, }, ], }, diff --git a/apps/browser/src/popup/app.module.ts b/apps/browser/src/popup/app.module.ts index 4695f0820b2..3d8cb267798 100644 --- a/apps/browser/src/popup/app.module.ts +++ b/apps/browser/src/popup/app.module.ts @@ -23,7 +23,6 @@ import { EnvironmentComponent } from "../auth/popup/environment.component"; import { ExtensionAnonLayoutWrapperComponent } from "../auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component"; import { HintComponent } from "../auth/popup/hint.component"; import { HomeComponent } from "../auth/popup/home.component"; -import { LockComponent } from "../auth/popup/lock.component"; import { LoginDecryptionOptionsComponentV1 } from "../auth/popup/login-decryption-options/login-decryption-options-v1.component"; import { LoginComponentV1 } from "../auth/popup/login-v1.component"; import { LoginViaAuthRequestComponentV1 } from "../auth/popup/login-via-auth-request-v1.component"; @@ -156,7 +155,6 @@ import "../platform/popup/locales"; VaultFilterComponent, HintComponent, HomeComponent, - LockComponent, LoginViaAuthRequestComponentV1, LoginComponentV1, LoginDecryptionOptionsComponentV1, diff --git a/apps/desktop/src/app/app-routing.module.ts b/apps/desktop/src/app/app-routing.module.ts index c7642638dc3..7e82bb004fa 100644 --- a/apps/desktop/src/app/app-routing.module.ts +++ b/apps/desktop/src/app/app-routing.module.ts @@ -15,7 +15,6 @@ import { unauthGuardFn, } from "@bitwarden/angular/auth/guards"; import { canAccessFeature } from "@bitwarden/angular/platform/guard/feature-flag.guard"; -import { extensionRefreshRedirect } from "@bitwarden/angular/utils/extension-refresh-redirect"; import { NewDeviceVerificationNoticeGuard } from "@bitwarden/angular/vault/guards"; import { AnonLayoutWrapperComponent, @@ -23,7 +22,7 @@ import { LoginComponent, LoginSecondaryContentComponent, LockIcon, - LockV2Component, + LockComponent, LoginViaAuthRequestComponent, PasswordHintComponent, RegistrationFinishComponent, @@ -51,7 +50,6 @@ import { twofactorRefactorSwap } from "../../../../libs/angular/src/utils/two-fa import { AccessibilityCookieComponent } from "../auth/accessibility-cookie.component"; import { maxAccountsGuardFn } from "../auth/guards/max-accounts.guard"; import { HintComponent } from "../auth/hint.component"; -import { LockComponent } from "../auth/lock.component"; import { LoginDecryptionOptionsComponentV1 } from "../auth/login/login-decryption-options/login-decryption-options-v1.component"; import { LoginComponentV1 } from "../auth/login/login-v1.component"; import { LoginViaAuthRequestComponentV1 } from "../auth/login/login-via-auth-request-v1.component"; @@ -81,12 +79,6 @@ const routes: Routes = [ children: [], // Children lets us have an empty component. canActivate: [redirectGuard({ loggedIn: "/vault", loggedOut: "/login", locked: "/lock" })], }, - { - path: "lock", - component: LockComponent, - canActivate: [lockGuard()], - canMatch: [extensionRefreshRedirect("/lockV2")], - }, ...twofactorRefactorSwap( TwoFactorComponent, AnonLayoutWrapperComponent, @@ -373,8 +365,8 @@ const routes: Routes = [ ], }, { - path: "lockV2", - canActivate: [canAccessFeature(FeatureFlag.ExtensionRefresh), lockGuard()], + path: "lock", + canActivate: [lockGuard()], data: { pageIcon: LockIcon, pageTitle: { @@ -385,7 +377,7 @@ const routes: Routes = [ children: [ { path: "", - component: LockV2Component, + component: LockComponent, }, ], }, diff --git a/apps/desktop/src/app/app.module.ts b/apps/desktop/src/app/app.module.ts index 5bd1c66b87c..5b9a1e3539d 100644 --- a/apps/desktop/src/app/app.module.ts +++ b/apps/desktop/src/app/app.module.ts @@ -13,7 +13,6 @@ import { AccessibilityCookieComponent } from "../auth/accessibility-cookie.compo import { DeleteAccountComponent } from "../auth/delete-account.component"; import { EnvironmentComponent } from "../auth/environment.component"; import { HintComponent } from "../auth/hint.component"; -import { LockComponent } from "../auth/lock.component"; import { LoginModule } from "../auth/login/login.module"; import { RegisterComponent } from "../auth/register.component"; import { RemovePasswordComponent } from "../auth/remove-password.component"; @@ -78,7 +77,6 @@ import { SendComponent } from "./tools/send/send.component"; FolderAddEditComponent, HeaderComponent, HintComponent, - LockComponent, NavComponent, GeneratorComponent, PasswordGeneratorHistoryComponent, diff --git a/apps/desktop/src/auth/lock.component.html b/apps/desktop/src/auth/lock.component.html deleted file mode 100644 index 895eda91e83..00000000000 --- a/apps/desktop/src/auth/lock.component.html +++ /dev/null @@ -1,80 +0,0 @@ -
    -
    - -

    {{ "yourVaultIsLocked" | i18n }}

    -
    -
    -
    -
    - - -
    -
    - - -
    -
    - -
    -
    -
    - -
    -
    -
    - -
    -
    - - -
    -
    -
    -
    diff --git a/apps/desktop/src/auth/lock.component.spec.ts b/apps/desktop/src/auth/lock.component.spec.ts deleted file mode 100644 index 4e59acf89c1..00000000000 --- a/apps/desktop/src/auth/lock.component.spec.ts +++ /dev/null @@ -1,478 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { NO_ERRORS_SCHEMA } from "@angular/core"; -import { ComponentFixture, TestBed, fakeAsync, tick } from "@angular/core/testing"; -import { ActivatedRoute } from "@angular/router"; -import { MockProxy, mock } from "jest-mock-extended"; -import { of } from "rxjs"; - -import { LockComponent as BaseLockComponent } from "@bitwarden/angular/auth/components/lock.component"; -import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe"; -import { PinServiceAbstraction } from "@bitwarden/auth/common"; -import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; -import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service"; -import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction"; -import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; -import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; -import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; -import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; -import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; -import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service"; -import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; -import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; -import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; -import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; -import { UserId } from "@bitwarden/common/types/guid"; -import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; -import { DialogService, ToastService } from "@bitwarden/components"; -import { - KdfConfigService, - KeyService, - BiometricsService as AbstractBiometricService, - BiometricStateService, -} from "@bitwarden/key-management"; - -import { BiometricsService } from "../key-management/biometrics/biometrics.service"; - -import { LockComponent } from "./lock.component"; - -// ipc mock global -const isWindowVisibleMock = jest.fn(); -(global as any).ipc = { - platform: { - isWindowVisible: isWindowVisibleMock, - }, - keyManagement: { - biometric: { - enabled: jest.fn(), - }, - }, -}; - -describe("LockComponent", () => { - let component: LockComponent; - let fixture: ComponentFixture; - let stateServiceMock: MockProxy; - let biometricStateService: MockProxy; - let biometricsService: MockProxy; - let messagingServiceMock: MockProxy; - let broadcasterServiceMock: MockProxy; - let platformUtilsServiceMock: MockProxy; - let activatedRouteMock: MockProxy; - let mockMasterPasswordService: FakeMasterPasswordService; - let mockToastService: MockProxy; - - const mockUserId = Utils.newGuid() as UserId; - const accountService: FakeAccountService = mockAccountServiceWith(mockUserId); - - beforeEach(async () => { - stateServiceMock = mock(); - - messagingServiceMock = mock(); - broadcasterServiceMock = mock(); - platformUtilsServiceMock = mock(); - mockToastService = mock(); - - activatedRouteMock = mock(); - activatedRouteMock.queryParams = mock(); - - mockMasterPasswordService = new FakeMasterPasswordService(); - - biometricStateService = mock(); - biometricStateService.dismissedRequirePasswordOnStartCallout$ = of(false); - biometricStateService.promptAutomatically$ = of(false); - biometricStateService.promptCancelled$ = of(false); - - await TestBed.configureTestingModule({ - declarations: [LockComponent, I18nPipe], - providers: [ - { provide: InternalMasterPasswordServiceAbstraction, useValue: mockMasterPasswordService }, - { - provide: I18nService, - useValue: mock(), - }, - { - provide: PlatformUtilsService, - useValue: platformUtilsServiceMock, - }, - { - provide: MessagingService, - useValue: messagingServiceMock, - }, - { - provide: KeyService, - useValue: mock(), - }, - { - provide: VaultTimeoutService, - useValue: mock(), - }, - { - provide: VaultTimeoutSettingsService, - useValue: mock(), - }, - { - provide: EnvironmentService, - useValue: mock(), - }, - { - provide: StateService, - useValue: stateServiceMock, - }, - { - provide: ApiService, - useValue: mock(), - }, - { - provide: ActivatedRoute, - useValue: activatedRouteMock, - }, - { - provide: BroadcasterService, - useValue: broadcasterServiceMock, - }, - { - provide: PolicyApiServiceAbstraction, - useValue: mock(), - }, - { - provide: InternalPolicyService, - useValue: mock(), - }, - { - provide: PasswordStrengthServiceAbstraction, - useValue: mock(), - }, - { - provide: LogService, - useValue: mock(), - }, - { - provide: DialogService, - useValue: mock(), - }, - { - provide: DeviceTrustServiceAbstraction, - useValue: mock(), - }, - { - provide: UserVerificationService, - useValue: mock(), - }, - { - provide: PinServiceAbstraction, - useValue: mock(), - }, - { - provide: BiometricStateService, - useValue: biometricStateService, - }, - { - provide: AbstractBiometricService, - useValue: biometricsService, - }, - { - provide: AccountService, - useValue: accountService, - }, - { - provide: AuthService, - useValue: mock(), - }, - { - provide: KdfConfigService, - useValue: mock(), - }, - { - provide: SyncService, - useValue: mock(), - }, - { - provide: ToastService, - useValue: mockToastService, - }, - ], - schemas: [NO_ERRORS_SCHEMA], - }).compileComponents(); - - fixture = TestBed.createComponent(LockComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - afterEach(() => { - jest.resetAllMocks(); - }); - - describe("ngOnInit", () => { - it("should call super.ngOnInit() once", async () => { - const superNgOnInitSpy = jest.spyOn(BaseLockComponent.prototype, "ngOnInit"); - await component.ngOnInit(); - expect(superNgOnInitSpy).toHaveBeenCalledTimes(1); - }); - - it('should set "autoPromptBiometric" to true if "biometricState.promptAutomatically$" resolves to true', async () => { - biometricStateService.promptAutomatically$ = of(true); - - await component.ngOnInit(); - expect(component["autoPromptBiometric"]).toBe(true); - }); - - it('should set "autoPromptBiometric" to false if "biometricState.promptAutomatically$" resolves to false', async () => { - biometricStateService.promptAutomatically$ = of(false); - - await component.ngOnInit(); - expect(component["autoPromptBiometric"]).toBe(false); - }); - - it('should set "biometricReady" to true if "stateService.getBiometricReady()" resolves to true', async () => { - component["canUseBiometric"] = jest.fn().mockResolvedValue(true); - - await component.ngOnInit(); - expect(component["biometricReady"]).toBe(true); - }); - - it('should set "biometricReady" to false if "stateService.getBiometricReady()" resolves to false', async () => { - component["canUseBiometric"] = jest.fn().mockResolvedValue(false); - - await component.ngOnInit(); - expect(component["biometricReady"]).toBe(false); - }); - - it("should call displayBiometricUpdateWarning", async () => { - component["displayBiometricUpdateWarning"] = jest.fn(); - await component.ngOnInit(); - expect(component["displayBiometricUpdateWarning"]).toHaveBeenCalledTimes(1); - }); - - it("should call delayedAskForBiometric", async () => { - component["delayedAskForBiometric"] = jest.fn(); - await component.ngOnInit(); - expect(component["delayedAskForBiometric"]).toHaveBeenCalledTimes(1); - expect(component["delayedAskForBiometric"]).toHaveBeenCalledWith(500); - }); - - it("should call delayedAskForBiometric when queryParams change", async () => { - activatedRouteMock.queryParams = of({ promptBiometric: true }); - component["delayedAskForBiometric"] = jest.fn(); - await component.ngOnInit(); - - expect(component["delayedAskForBiometric"]).toHaveBeenCalledTimes(1); - expect(component["delayedAskForBiometric"]).toHaveBeenCalledWith(500); - }); - - it("should call messagingService.send", async () => { - await component.ngOnInit(); - expect(messagingServiceMock.send).toHaveBeenCalledWith("getWindowIsFocused"); - }); - - describe("broadcasterService.subscribe", () => { - it('should call onWindowHidden() when "broadcasterService.subscribe" is called with "windowHidden"', async () => { - component["onWindowHidden"] = jest.fn(); - await component.ngOnInit(); - broadcasterServiceMock.subscribe.mock.calls[0][1]({ command: "windowHidden" }); - expect(component["onWindowHidden"]).toHaveBeenCalledTimes(1); - }); - - it('should call focusInput() when "broadcasterService.subscribe" is called with "windowIsFocused" is true and deferFocus is false', async () => { - component["focusInput"] = jest.fn(); - component["deferFocus"] = null; - await component.ngOnInit(); - broadcasterServiceMock.subscribe.mock.calls[0][1]({ - command: "windowIsFocused", - windowIsFocused: true, - } as any); - expect(component["deferFocus"]).toBe(false); - expect(component["focusInput"]).toHaveBeenCalledTimes(1); - }); - - it('should not call focusInput() when "broadcasterService.subscribe" is called with "windowIsFocused" is true and deferFocus is true', async () => { - component["focusInput"] = jest.fn(); - component["deferFocus"] = null; - await component.ngOnInit(); - broadcasterServiceMock.subscribe.mock.calls[0][1]({ - command: "windowIsFocused", - windowIsFocused: false, - } as any); - expect(component["deferFocus"]).toBe(true); - expect(component["focusInput"]).toHaveBeenCalledTimes(0); - }); - - it('should call focusInput() when "broadcasterService.subscribe" is called with "windowIsFocused" is true and deferFocus is true', async () => { - component["focusInput"] = jest.fn(); - component["deferFocus"] = true; - await component.ngOnInit(); - broadcasterServiceMock.subscribe.mock.calls[0][1]({ - command: "windowIsFocused", - windowIsFocused: true, - } as any); - expect(component["deferFocus"]).toBe(false); - expect(component["focusInput"]).toHaveBeenCalledTimes(1); - }); - - it('should not call focusInput() when "broadcasterService.subscribe" is called with "windowIsFocused" is false and deferFocus is true', async () => { - component["focusInput"] = jest.fn(); - component["deferFocus"] = true; - await component.ngOnInit(); - broadcasterServiceMock.subscribe.mock.calls[0][1]({ - command: "windowIsFocused", - windowIsFocused: false, - } as any); - expect(component["deferFocus"]).toBe(true); - expect(component["focusInput"]).toHaveBeenCalledTimes(0); - }); - }); - }); - - describe("ngOnDestroy", () => { - it("should call super.ngOnDestroy()", () => { - const superNgOnDestroySpy = jest.spyOn(BaseLockComponent.prototype, "ngOnDestroy"); - component.ngOnDestroy(); - expect(superNgOnDestroySpy).toHaveBeenCalledTimes(1); - }); - - it("should call broadcasterService.unsubscribe()", () => { - component.ngOnDestroy(); - expect(broadcasterServiceMock.unsubscribe).toHaveBeenCalledTimes(1); - }); - }); - - describe("focusInput", () => { - it('should call "focus" on #pin input if pinEnabled is true', () => { - component["pinEnabled"] = true; - global.document.getElementById = jest.fn().mockReturnValue({ focus: jest.fn() }); - component["focusInput"](); - expect(global.document.getElementById).toHaveBeenCalledWith("pin"); - }); - - it('should call "focus" on #masterPassword input if pinEnabled is false', () => { - component["pinEnabled"] = false; - global.document.getElementById = jest.fn().mockReturnValue({ focus: jest.fn() }); - component["focusInput"](); - expect(global.document.getElementById).toHaveBeenCalledWith("masterPassword"); - }); - }); - - describe("delayedAskForBiometric", () => { - beforeEach(() => { - component["supportsBiometric"] = true; - component["autoPromptBiometric"] = true; - }); - - it('should wait for "delay" milliseconds', fakeAsync(async () => { - const delaySpy = jest.spyOn(global, "setTimeout"); - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - component["delayedAskForBiometric"](5000); - - tick(4000); - component["biometricAsked"] = false; - - tick(1000); - component["biometricAsked"] = true; - - expect(delaySpy).toHaveBeenCalledWith(expect.any(Function), 5000); - })); - - it('should return; if "params" is defined and "params.promptBiometric" is false', fakeAsync(async () => { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - component["delayedAskForBiometric"](5000, { promptBiometric: false }); - tick(5000); - expect(component["biometricAsked"]).toBe(false); - })); - - it('should not return; if "params" is defined and "params.promptBiometric" is true', fakeAsync(async () => { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - component["delayedAskForBiometric"](5000, { promptBiometric: true }); - tick(5000); - expect(component["biometricAsked"]).toBe(true); - })); - - it('should not return; if "params" is undefined', fakeAsync(async () => { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - component["delayedAskForBiometric"](5000); - tick(5000); - expect(component["biometricAsked"]).toBe(true); - })); - - it('should return; if "supportsBiometric" is false', fakeAsync(async () => { - component["supportsBiometric"] = false; - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - component["delayedAskForBiometric"](5000); - tick(5000); - expect(component["biometricAsked"]).toBe(false); - })); - - it('should return; if "autoPromptBiometric" is false', fakeAsync(async () => { - component["autoPromptBiometric"] = false; - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - component["delayedAskForBiometric"](5000); - tick(5000); - expect(component["biometricAsked"]).toBe(false); - })); - - it("should call unlockBiometric() if biometricAsked is false and window is visible", fakeAsync(async () => { - isWindowVisibleMock.mockResolvedValue(true); - component["unlockBiometric"] = jest.fn(); - component["biometricAsked"] = false; - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - component["delayedAskForBiometric"](5000); - tick(5000); - - expect(component["unlockBiometric"]).toHaveBeenCalledTimes(1); - })); - - it("should not call unlockBiometric() if biometricAsked is false and window is not visible", fakeAsync(async () => { - isWindowVisibleMock.mockResolvedValue(false); - component["unlockBiometric"] = jest.fn(); - component["biometricAsked"] = false; - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - component["delayedAskForBiometric"](5000); - tick(5000); - - expect(component["unlockBiometric"]).toHaveBeenCalledTimes(0); - })); - - it("should not call unlockBiometric() if biometricAsked is true", fakeAsync(async () => { - isWindowVisibleMock.mockResolvedValue(true); - component["unlockBiometric"] = jest.fn(); - component["biometricAsked"] = true; - - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - component["delayedAskForBiometric"](5000); - tick(5000); - - expect(component["unlockBiometric"]).toHaveBeenCalledTimes(0); - })); - }); - - describe("canUseBiometric", () => { - it("should call biometric.enabled with current active user", async () => { - await component["canUseBiometric"](); - - expect(ipc.keyManagement.biometric.enabled).toHaveBeenCalledWith(mockUserId); - }); - }); - - it('onWindowHidden() should set "showPassword" to false', () => { - component["showPassword"] = true; - component["onWindowHidden"](); - expect(component["showPassword"]).toBe(false); - }); -}); diff --git a/apps/desktop/src/auth/lock.component.ts b/apps/desktop/src/auth/lock.component.ts deleted file mode 100644 index ea2ac546ec1..00000000000 --- a/apps/desktop/src/auth/lock.component.ts +++ /dev/null @@ -1,235 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { Component, NgZone, OnDestroy, OnInit } from "@angular/core"; -import { ActivatedRoute, Router } from "@angular/router"; -import { firstValueFrom, map, switchMap } from "rxjs"; - -import { LockComponent as BaseLockComponent } from "@bitwarden/angular/auth/components/lock.component"; -import { PinServiceAbstraction } from "@bitwarden/auth/common"; -import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; -import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service"; -import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction"; -import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; -import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; -import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; -import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; -import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; -import { DeviceType } from "@bitwarden/common/enums"; -import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; -import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; -import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; -import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; -import { DialogService, ToastService } from "@bitwarden/components"; -import { - KdfConfigService, - KeyService, - BiometricsService, - BiometricStateService, -} from "@bitwarden/key-management"; - -const BroadcasterSubscriptionId = "LockComponent"; - -@Component({ - selector: "app-lock", - templateUrl: "lock.component.html", -}) -export class LockComponent extends BaseLockComponent implements OnInit, OnDestroy { - private deferFocus: boolean = null; - protected biometricReady = false; - private biometricAsked = false; - private autoPromptBiometric = false; - private timerId: any; - - constructor( - masterPasswordService: InternalMasterPasswordServiceAbstraction, - router: Router, - i18nService: I18nService, - platformUtilsService: PlatformUtilsService, - messagingService: MessagingService, - keyService: KeyService, - vaultTimeoutService: VaultTimeoutService, - vaultTimeoutSettingsService: VaultTimeoutSettingsService, - environmentService: EnvironmentService, - protected override stateService: StateService, - apiService: ApiService, - private route: ActivatedRoute, - private broadcasterService: BroadcasterService, - ngZone: NgZone, - policyApiService: PolicyApiServiceAbstraction, - policyService: InternalPolicyService, - passwordStrengthService: PasswordStrengthServiceAbstraction, - logService: LogService, - dialogService: DialogService, - deviceTrustService: DeviceTrustServiceAbstraction, - userVerificationService: UserVerificationService, - pinService: PinServiceAbstraction, - biometricStateService: BiometricStateService, - biometricsService: BiometricsService, - accountService: AccountService, - authService: AuthService, - kdfConfigService: KdfConfigService, - syncService: SyncService, - toastService: ToastService, - ) { - super( - masterPasswordService, - router, - i18nService, - platformUtilsService, - messagingService, - keyService, - vaultTimeoutService, - vaultTimeoutSettingsService, - environmentService, - stateService, - apiService, - logService, - ngZone, - policyApiService, - policyService, - passwordStrengthService, - dialogService, - deviceTrustService, - userVerificationService, - pinService, - biometricStateService, - biometricsService, - accountService, - authService, - kdfConfigService, - syncService, - toastService, - ); - } - - async ngOnInit() { - await super.ngOnInit(); - this.autoPromptBiometric = await firstValueFrom( - this.biometricStateService.promptAutomatically$, - ); - this.biometricReady = await this.canUseBiometric(); - - await this.displayBiometricUpdateWarning(); - - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.delayedAskForBiometric(500); - this.route.queryParams.pipe(switchMap((params) => this.delayedAskForBiometric(500, params))); - - this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => { - this.ngZone.run(() => { - switch (message.command) { - case "windowHidden": - this.onWindowHidden(); - break; - case "windowIsFocused": - if (this.deferFocus === null) { - this.deferFocus = !message.windowIsFocused; - if (!this.deferFocus) { - this.focusInput(); - } - } else if (this.deferFocus && message.windowIsFocused) { - this.focusInput(); - this.deferFocus = false; - } - break; - default: - } - }); - }); - this.messagingService.send("getWindowIsFocused"); - - // start background listener until destroyed on interval - this.timerId = setInterval(async () => { - this.supportsBiometric = await this.biometricsService.supportsBiometric(); - this.biometricReady = await this.canUseBiometric(); - }, 1000); - } - - ngOnDestroy() { - super.ngOnDestroy(); - this.broadcasterService.unsubscribe(BroadcasterSubscriptionId); - clearInterval(this.timerId); - } - - onWindowHidden() { - this.showPassword = false; - } - - private async delayedAskForBiometric(delay: number, params?: any) { - await new Promise((resolve) => setTimeout(resolve, delay)); - - if (params && !params.promptBiometric) { - return; - } - - if (!this.supportsBiometric || !this.autoPromptBiometric || this.biometricAsked) { - return; - } - - if (await firstValueFrom(this.biometricStateService.promptCancelled$)) { - return; - } - - this.biometricAsked = true; - if (await ipc.platform.isWindowVisible()) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.unlockBiometric(); - } - } - - private async canUseBiometric() { - const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(map((a) => a?.id))); - return await ipc.keyManagement.biometric.enabled(userId); - } - - private focusInput() { - document.getElementById(this.pinEnabled ? "pin" : "masterPassword")?.focus(); - } - - private async displayBiometricUpdateWarning(): Promise { - if (await firstValueFrom(this.biometricStateService.dismissedRequirePasswordOnStartCallout$)) { - return; - } - - if (this.platformUtilsService.getDevice() !== DeviceType.WindowsDesktop) { - return; - } - - if (await firstValueFrom(this.biometricStateService.biometricUnlockEnabled$)) { - const response = await this.dialogService.openSimpleDialog({ - title: { key: "windowsBiometricUpdateWarningTitle" }, - content: { key: "windowsBiometricUpdateWarning" }, - type: "warning", - }); - - await this.biometricStateService.setRequirePasswordOnStart(response); - if (response) { - await this.biometricStateService.setPromptAutomatically(false); - } - this.supportsBiometric = await this.canUseBiometric(); - await this.biometricStateService.setDismissedRequirePasswordOnStartCallout(); - } - } - - get biometricText() { - switch (this.platformUtilsService.getDevice()) { - case DeviceType.MacOsDesktop: - return "unlockWithTouchId"; - case DeviceType.WindowsDesktop: - return "unlockWithWindowsHello"; - case DeviceType.LinuxDesktop: - return "unlockWithPolkit"; - default: - throw new Error("Unsupported platform"); - } - } -} diff --git a/apps/web/src/app/auth/lock.component.html b/apps/web/src/app/auth/lock.component.html deleted file mode 100644 index f630906223b..00000000000 --- a/apps/web/src/app/auth/lock.component.html +++ /dev/null @@ -1,27 +0,0 @@ -
    - - {{ "masterPass" | i18n }} - - - {{ "loggedInAsEmailOn" | i18n: email : webVaultHostname }} - - -
    - -
    - - -
    -
    diff --git a/apps/web/src/app/auth/lock.component.ts b/apps/web/src/app/auth/lock.component.ts deleted file mode 100644 index 36e9c81f2c7..00000000000 --- a/apps/web/src/app/auth/lock.component.ts +++ /dev/null @@ -1,51 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { Component, OnInit, inject } from "@angular/core"; -import { FormBuilder, Validators } from "@angular/forms"; - -import { LockComponent as BaseLockComponent } from "@bitwarden/angular/auth/components/lock.component"; - -import { SharedModule } from "../shared"; - -@Component({ - selector: "app-lock", - templateUrl: "lock.component.html", - standalone: true, - imports: [SharedModule], -}) -export class LockComponent extends BaseLockComponent implements OnInit { - formBuilder = inject(FormBuilder); - - formGroup = this.formBuilder.group({ - masterPassword: ["", { validators: Validators.required, updateOn: "submit" }], - }); - - get masterPasswordFormControl() { - return this.formGroup.controls.masterPassword; - } - - async ngOnInit() { - await super.ngOnInit(); - - this.masterPasswordFormControl.setValue(this.masterPassword); - - this.onSuccessfulSubmit = async () => { - await this.router.navigateByUrl(this.successRoute); - }; - } - - async superSubmit() { - await super.submit(); - } - - submit = async () => { - this.formGroup.markAllAsTouched(); - - if (this.formGroup.invalid) { - return; - } - - this.masterPassword = this.masterPasswordFormControl.value; - await this.superSubmit(); - }; -} diff --git a/apps/web/src/app/oss-routing.module.ts b/apps/web/src/app/oss-routing.module.ts index 9f2a86c1c06..ad536110b74 100644 --- a/apps/web/src/app/oss-routing.module.ts +++ b/apps/web/src/app/oss-routing.module.ts @@ -12,7 +12,6 @@ import { } from "@bitwarden/angular/auth/guards"; import { canAccessFeature } from "@bitwarden/angular/platform/guard/feature-flag.guard"; import { generatorSwap } from "@bitwarden/angular/tools/generator/generator-swap"; -import { extensionRefreshSwap } from "@bitwarden/angular/utils/extension-refresh-swap"; import { NewDeviceVerificationNoticeGuard } from "@bitwarden/angular/vault/guards"; import { AnonLayoutWrapperComponent, @@ -26,7 +25,7 @@ import { RegistrationLinkExpiredComponent, LoginComponent, LoginSecondaryContentComponent, - LockV2Component, + LockComponent, LockIcon, TwoFactorTimeoutIcon, UserLockIcon, @@ -56,7 +55,6 @@ import { FamiliesForEnterpriseSetupComponent } from "./admin-console/organizatio import { CreateOrganizationComponent } from "./admin-console/settings/create-organization.component"; import { deepLinkGuard } from "./auth/guards/deep-link.guard"; import { HintComponent } from "./auth/hint.component"; -import { LockComponent } from "./auth/lock.component"; import { LoginDecryptionOptionsComponentV1 } from "./auth/login/login-decryption-options/login-decryption-options-v1.component"; import { LoginComponentV1 } from "./auth/login/login-v1.component"; import { LoginViaAuthRequestComponentV1 } from "./auth/login/login-via-auth-request-v1.component"; @@ -509,44 +507,23 @@ const routes: Routes = [ }, }, }, - ...extensionRefreshSwap( - LockComponent, - LockV2Component, - { - path: "lock", - canActivate: [deepLinkGuard(), lockGuard()], - children: [ - { - path: "", - component: LockComponent, - }, - ], - data: { - pageTitle: { - key: "yourVaultIsLockedV2", - }, - pageIcon: LockIcon, - showReadonlyHostname: true, - } satisfies AnonLayoutWrapperData, - }, - { - path: "lock", - canActivate: [deepLinkGuard(), lockGuard()], - children: [ - { - path: "", - component: LockV2Component, - }, - ], - data: { - pageTitle: { - key: "yourVaultIsLockedV2", - }, - pageIcon: LockIcon, - showReadonlyHostname: true, - } satisfies AnonLayoutWrapperData, - }, - ), + { + path: "lock", + canActivate: [deepLinkGuard(), lockGuard()], + children: [ + { + path: "", + component: LockComponent, + }, + ], + data: { + pageTitle: { + key: "yourVaultIsLockedV2", + }, + pageIcon: LockIcon, + showReadonlyHostname: true, + } satisfies AnonLayoutWrapperData, + }, { path: "2fa", canActivate: [unauthGuardFn()], diff --git a/libs/angular/src/auth/components/lock.component.ts b/libs/angular/src/auth/components/lock.component.ts deleted file mode 100644 index 20ad37fd2b4..00000000000 --- a/libs/angular/src/auth/components/lock.component.ts +++ /dev/null @@ -1,398 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { Directive, NgZone, OnDestroy, OnInit } from "@angular/core"; -import { Router } from "@angular/router"; -import { firstValueFrom, Subject } from "rxjs"; -import { concatMap, map, take, takeUntil } from "rxjs/operators"; - -import { PinServiceAbstraction, PinLockType } from "@bitwarden/auth/common"; -import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; -import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service"; -import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction"; -import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; -import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options"; -import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; -import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; -import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; -import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; -import { VerificationType } from "@bitwarden/common/auth/enums/verification-type"; -import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; -import { - MasterPasswordVerification, - MasterPasswordVerificationResponse, -} from "@bitwarden/common/auth/types/verification"; -import { ClientType } from "@bitwarden/common/enums"; -import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; -import { KeySuffixOptions } from "@bitwarden/common/platform/enums"; -import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; -import { UserId } from "@bitwarden/common/types/guid"; -import { UserKey } from "@bitwarden/common/types/key"; -import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; -import { DialogService, ToastService } from "@bitwarden/components"; -import { - KdfConfigService, - KeyService, - BiometricStateService, - BiometricsService, -} from "@bitwarden/key-management"; - -@Directive() -export class LockComponent implements OnInit, OnDestroy { - masterPassword = ""; - pin = ""; - showPassword = false; - email: string; - pinEnabled = false; - masterPasswordEnabled = false; - webVaultHostname = ""; - formPromise: Promise; - supportsBiometric: boolean; - biometricLock: boolean; - - private activeUserId: UserId; - protected successRoute = "vault"; - protected forcePasswordResetRoute = "update-temp-password"; - protected onSuccessfulSubmit: () => Promise; - - private invalidPinAttempts = 0; - private pinLockType: PinLockType; - - private enforcedMasterPasswordOptions: MasterPasswordPolicyOptions = undefined; - - private destroy$ = new Subject(); - - constructor( - protected masterPasswordService: InternalMasterPasswordServiceAbstraction, - protected router: Router, - protected i18nService: I18nService, - protected platformUtilsService: PlatformUtilsService, - protected messagingService: MessagingService, - protected keyService: KeyService, - protected vaultTimeoutService: VaultTimeoutService, - protected vaultTimeoutSettingsService: VaultTimeoutSettingsService, - protected environmentService: EnvironmentService, - protected stateService: StateService, - protected apiService: ApiService, - protected logService: LogService, - protected ngZone: NgZone, - protected policyApiService: PolicyApiServiceAbstraction, - protected policyService: InternalPolicyService, - protected passwordStrengthService: PasswordStrengthServiceAbstraction, - protected dialogService: DialogService, - protected deviceTrustService: DeviceTrustServiceAbstraction, - protected userVerificationService: UserVerificationService, - protected pinService: PinServiceAbstraction, - protected biometricStateService: BiometricStateService, - protected biometricsService: BiometricsService, - protected accountService: AccountService, - protected authService: AuthService, - protected kdfConfigService: KdfConfigService, - protected syncService: SyncService, - protected toastService: ToastService, - ) {} - - async ngOnInit() { - this.accountService.activeAccount$ - .pipe( - concatMap(async (account) => { - this.activeUserId = account?.id; - await this.load(account?.id); - }), - takeUntil(this.destroy$), - ) - .subscribe(); - } - - ngOnDestroy() { - this.destroy$.next(); - this.destroy$.complete(); - } - - async submit() { - if (this.pinEnabled) { - return await this.handlePinRequiredUnlock(); - } - - await this.handleMasterPasswordRequiredUnlock(); - } - - async logOut() { - const confirmed = await this.dialogService.openSimpleDialog({ - title: { key: "logOut" }, - content: { key: "logOutConfirmation" }, - acceptButtonText: { key: "logOut" }, - type: "warning", - }); - - if (confirmed) { - this.messagingService.send("logout", { userId: this.activeUserId }); - } - } - - async unlockBiometric(): Promise { - if (!this.biometricLock) { - return; - } - - await this.biometricStateService.setUserPromptCancelled(); - const userKey = await this.keyService.getUserKeyFromStorage( - KeySuffixOptions.Biometric, - this.activeUserId, - ); - - if (userKey) { - await this.setUserKeyAndContinue(userKey, this.activeUserId, false); - } - - return !!userKey; - } - - async isBiometricUnlockAvailable(): Promise { - if (!(await this.biometricsService.supportsBiometric())) { - return false; - } - return this.biometricsService.isBiometricUnlockAvailable(); - } - - togglePassword() { - this.showPassword = !this.showPassword; - const input = document.getElementById(this.pinEnabled ? "pin" : "masterPassword"); - if (this.ngZone.isStable) { - input.focus(); - } else { - this.ngZone.onStable.pipe(take(1)).subscribe(() => input.focus()); - } - } - - private async handlePinRequiredUnlock() { - if (this.pin == null || this.pin === "") { - this.toastService.showToast({ - variant: "error", - title: this.i18nService.t("errorOccurred"), - message: this.i18nService.t("pinRequired"), - }); - return; - } - - return await this.doUnlockWithPin(); - } - - private async doUnlockWithPin() { - const MAX_INVALID_PIN_ENTRY_ATTEMPTS = 5; - - try { - const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; - const userKey = await this.pinService.decryptUserKeyWithPin(this.pin, userId); - - if (userKey) { - await this.setUserKeyAndContinue(userKey, userId); - return; // successfully unlocked - } - - // Failure state: invalid PIN or failed decryption - this.invalidPinAttempts++; - - // Log user out if they have entered an invalid PIN too many times - if (this.invalidPinAttempts >= MAX_INVALID_PIN_ENTRY_ATTEMPTS) { - this.toastService.showToast({ - variant: "error", - title: null, - message: this.i18nService.t("tooManyInvalidPinEntryAttemptsLoggingOut"), - }); - this.messagingService.send("logout"); - return; - } - - this.toastService.showToast({ - variant: "error", - title: this.i18nService.t("errorOccurred"), - message: this.i18nService.t("invalidPin"), - }); - } catch { - this.toastService.showToast({ - variant: "error", - title: this.i18nService.t("errorOccurred"), - message: this.i18nService.t("unexpectedError"), - }); - } - } - - private async handleMasterPasswordRequiredUnlock() { - if (this.masterPassword == null || this.masterPassword === "") { - this.toastService.showToast({ - variant: "error", - title: this.i18nService.t("errorOccurred"), - message: this.i18nService.t("masterPasswordRequired"), - }); - return; - } - await this.doUnlockWithMasterPassword(); - } - - private async doUnlockWithMasterPassword() { - const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; - - const verification = { - type: VerificationType.MasterPassword, - secret: this.masterPassword, - } as MasterPasswordVerification; - - let passwordValid = false; - let response: MasterPasswordVerificationResponse; - try { - this.formPromise = this.userVerificationService.verifyUserByMasterPassword( - verification, - userId, - this.email, - ); - response = await this.formPromise; - this.enforcedMasterPasswordOptions = MasterPasswordPolicyOptions.fromResponse( - response.policyOptions, - ); - passwordValid = true; - } catch (e) { - this.logService.error(e); - } finally { - this.formPromise = null; - } - - if (!passwordValid) { - this.toastService.showToast({ - variant: "error", - title: this.i18nService.t("errorOccurred"), - message: this.i18nService.t("invalidMasterPassword"), - }); - return; - } - - const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey( - response.masterKey, - userId, - ); - await this.setUserKeyAndContinue(userKey, userId, true); - } - - private async setUserKeyAndContinue( - key: UserKey, - userId: UserId, - evaluatePasswordAfterUnlock = false, - ) { - await this.keyService.setUserKey(key, userId); - - // Now that we have a decrypted user key in memory, we can check if we - // need to establish trust on the current device - const activeAccount = await firstValueFrom(this.accountService.activeAccount$); - await this.deviceTrustService.trustDeviceIfRequired(activeAccount.id); - - await this.doContinue(evaluatePasswordAfterUnlock); - } - - private async doContinue(evaluatePasswordAfterUnlock: boolean) { - await this.biometricStateService.resetUserPromptCancelled(); - this.messagingService.send("unlocked"); - - if (evaluatePasswordAfterUnlock) { - try { - // If we do not have any saved policies, attempt to load them from the service - if (this.enforcedMasterPasswordOptions == undefined) { - this.enforcedMasterPasswordOptions = await firstValueFrom( - this.policyService.masterPasswordPolicyOptions$(), - ); - } - - if (this.requirePasswordChange()) { - const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; - await this.masterPasswordService.setForceSetPasswordReason( - ForceSetPasswordReason.WeakMasterPassword, - userId, - ); - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate([this.forcePasswordResetRoute]); - return; - } - } catch (e) { - // Do not prevent unlock if there is an error evaluating policies - this.logService.error(e); - } - } - - // Vault can be de-synced since notifications get ignored while locked. Need to check whether sync is required using the sync service. - const clientType = this.platformUtilsService.getClientType(); - if (clientType === ClientType.Browser || clientType === ClientType.Desktop) { - // Desktop and Browser have better offline support and to facilitate this we don't make the user wait for what - // could be an HTTP Timeout because their server is unreachable. - await Promise.race([ - this.syncService - .fullSync(false) - .catch((err) => this.logService.error("Error during unlock sync", err)), - new Promise((resolve) => - setTimeout(() => { - this.logService.warning("Skipping sync wait, continuing to unlock."); - resolve(); - }, 5_000), - ), - ]); - } else { - await this.syncService.fullSync(false); - } - - if (this.onSuccessfulSubmit != null) { - await this.onSuccessfulSubmit(); - } else if (this.router != null) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate([this.successRoute]); - } - } - - private async load(userId: UserId) { - this.pinLockType = await this.pinService.getPinLockType(userId); - - this.pinEnabled = await this.pinService.isPinDecryptionAvailable(userId); - - this.masterPasswordEnabled = await this.userVerificationService.hasMasterPassword(); - - this.supportsBiometric = await this.biometricsService.supportsBiometric(); - this.biometricLock = - (await this.vaultTimeoutSettingsService.isBiometricLockSet()) && - ((await this.keyService.hasUserKeyStored(KeySuffixOptions.Biometric)) || - !this.platformUtilsService.supportsSecureStorage()); - this.email = await firstValueFrom( - this.accountService.activeAccount$.pipe(map((a) => a?.email)), - ); - - this.webVaultHostname = (await this.environmentService.getEnvironment()).getHostname(); - } - - /** - * Checks if the master password meets the enforced policy requirements - * If not, returns false - */ - private requirePasswordChange(): boolean { - if ( - this.enforcedMasterPasswordOptions == undefined || - !this.enforcedMasterPasswordOptions.enforceOnLogin - ) { - return false; - } - - const passwordStrength = this.passwordStrengthService.getPasswordStrength( - this.masterPassword, - this.email, - )?.score; - - return !this.policyService.evaluateMasterPassword( - passwordStrength, - this.masterPassword, - this.enforcedMasterPasswordOptions, - ); - } -} diff --git a/libs/auth/src/angular/lock/lock.component.ts b/libs/auth/src/angular/lock/lock.component.ts index bcbc2bd5751..aa7b43c2e53 100644 --- a/libs/auth/src/angular/lock/lock.component.ts +++ b/libs/auth/src/angular/lock/lock.component.ts @@ -75,7 +75,7 @@ const clientTypeToSuccessRouteRecord: Partial> = { IconButtonModule, ], }) -export class LockV2Component implements OnInit, OnDestroy { +export class LockComponent implements OnInit, OnDestroy { private destroy$ = new Subject(); activeAccount: Account | null; @@ -543,8 +543,8 @@ export class LockV2Component implements OnInit, OnDestroy { const previousUrl = this.lockComponentService.getPreviousUrl(); /** * In a passkey flow, the `previousUrl` will still be `/fido2?` at this point - * because the `/lockV2` route doesn't save the URL in the `BrowserRouterService`. This is - * handled by the `doNotSaveUrl` property on the `lockV2` route in `app-routing.module.ts`. + * because the `/lock` route doesn't save the URL in the `BrowserRouterService`. This is + * handled by the `doNotSaveUrl` property on the `/lock` route in `app-routing.module.ts`. */ if (previousUrl) { await this.router.navigateByUrl(previousUrl); From ce5ae478a8a46575be9d20a04d9b4d33cba53132 Mon Sep 17 00:00:00 2001 From: Daniel James Smith <2670567+djsmith85@users.noreply.github.com> Date: Fri, 20 Dec 2024 19:46:24 +0100 Subject: [PATCH 40/54] [PM-15921] Remove v1 generator and generator history (#12345) * Remove v1 generator, generator history page and extension refresh conditional routing * Remove unused keys from en/messages.json --------- Co-authored-by: Daniel James Smith --- apps/browser/src/_locales/en/messages.json | 22 - apps/browser/src/popup/app-routing.module.ts | 14 +- apps/browser/src/popup/app.module.ts | 4 - .../popup/generator/generator.component.html | 588 ------------------ .../popup/generator/generator.component.ts | 88 --- .../password-generator-history.component.html | 48 -- .../password-generator-history.component.ts | 28 - 7 files changed, 7 insertions(+), 785 deletions(-) delete mode 100644 apps/browser/src/tools/popup/generator/generator.component.html delete mode 100644 apps/browser/src/tools/popup/generator/generator.component.ts delete mode 100644 apps/browser/src/tools/popup/generator/password-generator-history.component.html delete mode 100644 apps/browser/src/tools/popup/generator/password-generator-history.component.ts diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index de438a09467..4b7d9eb2992 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -454,9 +454,6 @@ "length": { "message": "Length" }, - "passwordMinLength": { - "message": "Minimum password length" - }, "uppercase": { "message": "Uppercase (A-Z)", "description": "deprecated. Use uppercaseLabel instead." @@ -528,10 +525,6 @@ "minSpecial": { "message": "Minimum special" }, - "avoidAmbChar": { - "message": "Avoid ambiguous characters", - "description": "deprecated. Use avoidAmbiguous instead." - }, "avoidAmbiguous": { "message": "Avoid ambiguous characters", "description": "Label for the avoid ambiguous characters checkbox." @@ -2047,9 +2040,6 @@ "clone": { "message": "Clone" }, - "passwordGeneratorPolicyInEffect": { - "message": "One or more organization policies are affecting your generator settings." - }, "passwordGenerator": { "message": "Password generator" }, @@ -2884,9 +2874,6 @@ "error": { "message": "Error" }, - "regenerateUsername": { - "message": "Regenerate username" - }, "generateUsername": { "message": "Generate username" }, @@ -2927,9 +2914,6 @@ } } }, - "usernameType": { - "message": "Username type" - }, "plusAddressedEmail": { "message": "Plus addressed email", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -2952,12 +2936,6 @@ "websiteName": { "message": "Website name" }, - "whatWouldYouLikeToGenerate": { - "message": "What would you like to generate?" - }, - "passwordType": { - "message": "Password type" - }, "service": { "message": "Service" }, diff --git a/apps/browser/src/popup/app-routing.module.ts b/apps/browser/src/popup/app-routing.module.ts index 549a677857c..1f42c1cf31d 100644 --- a/apps/browser/src/popup/app-routing.module.ts +++ b/apps/browser/src/popup/app-routing.module.ts @@ -86,8 +86,6 @@ import BrowserPopupUtils from "../platform/popup/browser-popup-utils"; import { popupRouterCacheGuard } from "../platform/popup/view-cache/popup-router-cache.service"; import { CredentialGeneratorHistoryComponent } from "../tools/popup/generator/credential-generator-history.component"; import { CredentialGeneratorComponent } from "../tools/popup/generator/credential-generator.component"; -import { GeneratorComponent } from "../tools/popup/generator/generator.component"; -import { PasswordGeneratorHistoryComponent } from "../tools/popup/generator/password-generator-history.component"; import { SendAddEditComponent } from "../tools/popup/send/send-add-edit.component"; import { SendGroupingsComponent } from "../tools/popup/send/send-groupings.component"; import { SendTypeComponent } from "../tools/popup/send/send-type.component"; @@ -330,15 +328,16 @@ const routes: Routes = [ }), { path: "generator", - component: GeneratorComponent, + component: CredentialGeneratorComponent, canActivate: [authGuard], data: { elevation: 0 } satisfies RouteDataProperties, }, - ...extensionRefreshSwap(PasswordGeneratorHistoryComponent, CredentialGeneratorHistoryComponent, { + { path: "generator-history", + component: CredentialGeneratorHistoryComponent, canActivate: [authGuard], data: { elevation: 1 } satisfies RouteDataProperties, - }), + }, { path: "import", component: ImportBrowserV2Component, @@ -757,11 +756,12 @@ const routes: Routes = [ canDeactivate: [clearVaultStateGuard], data: { elevation: 0 } satisfies RouteDataProperties, }, - ...extensionRefreshSwap(GeneratorComponent, CredentialGeneratorComponent, { + { path: "generator", + component: CredentialGeneratorComponent, canActivate: [authGuard], data: { elevation: 0 } satisfies RouteDataProperties, - }), + }, { path: "settings", component: SettingsV2Component, diff --git a/apps/browser/src/popup/app.module.ts b/apps/browser/src/popup/app.module.ts index 3d8cb267798..c2697b2d490 100644 --- a/apps/browser/src/popup/app.module.ts +++ b/apps/browser/src/popup/app.module.ts @@ -56,8 +56,6 @@ import { PopupHeaderComponent } from "../platform/popup/layout/popup-header.comp import { PopupPageComponent } from "../platform/popup/layout/popup-page.component"; import { PopupTabNavigationComponent } from "../platform/popup/layout/popup-tab-navigation.component"; import { FilePopoutCalloutComponent } from "../tools/popup/components/file-popout-callout.component"; -import { GeneratorComponent } from "../tools/popup/generator/generator.component"; -import { PasswordGeneratorHistoryComponent } from "../tools/popup/generator/password-generator-history.component"; import { SendListComponent } from "../tools/popup/send/components/send-list.component"; import { SendAddEditComponent } from "../tools/popup/send/send-add-edit.component"; import { SendGroupingsComponent } from "../tools/popup/send/send-groupings.component"; @@ -160,8 +158,6 @@ import "../platform/popup/locales"; LoginDecryptionOptionsComponentV1, NotificationsSettingsV1Component, AppearanceComponent, - GeneratorComponent, - PasswordGeneratorHistoryComponent, PasswordHistoryComponent, PremiumComponent, RegisterComponent, diff --git a/apps/browser/src/tools/popup/generator/generator.component.html b/apps/browser/src/tools/popup/generator/generator.component.html deleted file mode 100644 index d92d32a5623..00000000000 --- a/apps/browser/src/tools/popup/generator/generator.component.html +++ /dev/null @@ -1,588 +0,0 @@ - -
    - - -
    -

    - {{ "generator" | i18n }} -

    -
    - -
    -
    -
    - - {{ "passwordGeneratorPolicyInEffect" | i18n }} - -
    -
    -
    - - -
    -
    -
    -
    -
    - - -
    -
    -
    -
    -
    - -
    - - -
    -
    -
    -
    - -
    -

    - {{ "options" | i18n }} -

    -
    -
    - -
    - - -
    -
    -
    -
    -
    -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    -
    - -
    -
    -
    - - - -
    -
    - {{ "passwordMinLength" | i18n }} - - {{ passwordOptionsMinLengthForReader$ | async }} - - {{ passwordOptions.minLength }} -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    -
    -
    -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    -
    -
    - -
    - -
    -

    - {{ "options" | i18n }} -

    -
    -
    - -
    - - -
    -
    -
    -
    -
    -
    -
    - - -
    - -
    - - -
    -
    - - -
    -
    - -
    - - -
    -
    - -
    - - -
    -
    - - -
    -
    - - -
    -
    - -
    - - -
    -
    - -
    - - -
    -
    - -
    - - -
    -
    - - -
    -
    -
    -
    -
    -
    -
    - - -
    -
    - -
    - - -
    -
    -
    - - -
    -
    -
    -
    -
    -
    - - -
    -
    - -
    - - -
    -
    -
    - - -
    -
    -
    -
    -
    -
    - - -
    -
    - - -
    -
    -
    -
    -
    diff --git a/apps/browser/src/tools/popup/generator/generator.component.ts b/apps/browser/src/tools/popup/generator/generator.component.ts deleted file mode 100644 index d744c58f4d1..00000000000 --- a/apps/browser/src/tools/popup/generator/generator.component.ts +++ /dev/null @@ -1,88 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { Location } from "@angular/common"; -import { Component, NgZone, OnInit } from "@angular/core"; -import { ActivatedRoute } from "@angular/router"; -import { firstValueFrom } from "rxjs"; - -import { GeneratorComponent as BaseGeneratorComponent } from "@bitwarden/angular/tools/generator/components/generator.component"; -import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; -import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; -import { AddEditCipherInfo } from "@bitwarden/common/vault/types/add-edit-cipher-info"; -import { ToastService } from "@bitwarden/components"; -import { - PasswordGenerationServiceAbstraction, - UsernameGenerationServiceAbstraction, -} from "@bitwarden/generator-legacy"; - -@Component({ - selector: "app-generator", - templateUrl: "generator.component.html", -}) -export class GeneratorComponent extends BaseGeneratorComponent implements OnInit { - private addEditCipherInfo: AddEditCipherInfo; - private cipherState: CipherView; - private cipherService: CipherService; - - constructor( - passwordGenerationService: PasswordGenerationServiceAbstraction, - usernameGenerationService: UsernameGenerationServiceAbstraction, - platformUtilsService: PlatformUtilsService, - i18nService: I18nService, - accountService: AccountService, - cipherService: CipherService, - route: ActivatedRoute, - logService: LogService, - ngZone: NgZone, - private location: Location, - toastService: ToastService, - ) { - super( - passwordGenerationService, - usernameGenerationService, - platformUtilsService, - accountService, - i18nService, - logService, - route, - ngZone, - window, - toastService, - ); - this.cipherService = cipherService; - } - - async ngOnInit() { - this.addEditCipherInfo = await firstValueFrom(this.cipherService.addEditCipherInfo$); - if (this.addEditCipherInfo != null) { - this.cipherState = this.addEditCipherInfo.cipher; - } - this.comingFromAddEdit = this.cipherState != null; - if (this.cipherState?.login?.hasUris) { - this.usernameWebsite = this.cipherState.login.uris[0].hostname; - } - await super.ngOnInit(); - } - - select() { - super.select(); - if (this.type === "password") { - this.cipherState.login.password = this.password; - } else if (this.type === "username") { - this.cipherState.login.username = this.username; - } - this.addEditCipherInfo.cipher = this.cipherState; - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.cipherService.setAddEditCipherInfo(this.addEditCipherInfo); - this.close(); - } - - close() { - this.location.back(); - } -} diff --git a/apps/browser/src/tools/popup/generator/password-generator-history.component.html b/apps/browser/src/tools/popup/generator/password-generator-history.component.html deleted file mode 100644 index 8f4a246fc5e..00000000000 --- a/apps/browser/src/tools/popup/generator/password-generator-history.component.html +++ /dev/null @@ -1,48 +0,0 @@ -
    -
    - -
    -

    - {{ "passwordHistory" | i18n }} -

    -
    - -
    -
    -
    -
    -
    -
    -
    -
    -
    - {{ h.date | date: "medium" }} -
    -
    -
    - -
    -
    -
    -
    -
    -

    {{ "noPasswordsInList" | i18n }}

    -
    -
    diff --git a/apps/browser/src/tools/popup/generator/password-generator-history.component.ts b/apps/browser/src/tools/popup/generator/password-generator-history.component.ts deleted file mode 100644 index 2436ae51a7d..00000000000 --- a/apps/browser/src/tools/popup/generator/password-generator-history.component.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { Location } from "@angular/common"; -import { Component } from "@angular/core"; - -import { PasswordGeneratorHistoryComponent as BasePasswordGeneratorHistoryComponent } from "@bitwarden/angular/tools/generator/components/password-generator-history.component"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { ToastService } from "@bitwarden/components"; -import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; - -@Component({ - selector: "app-password-generator-history", - templateUrl: "password-generator-history.component.html", -}) -export class PasswordGeneratorHistoryComponent extends BasePasswordGeneratorHistoryComponent { - constructor( - passwordGenerationService: PasswordGenerationServiceAbstraction, - platformUtilsService: PlatformUtilsService, - i18nService: I18nService, - private location: Location, - toastService: ToastService, - ) { - super(passwordGenerationService, platformUtilsService, i18nService, window, toastService); - } - - close() { - this.location.back(); - } -} From 64166a9354dc2e1397d38ec5de10f795e68213e3 Mon Sep 17 00:00:00 2001 From: Victoria League Date: Fri, 20 Dec 2024 14:03:50 -0500 Subject: [PATCH 41/54] [PM-16102] Increase clickable area for bit-item actions (#12450) --- .../components/src/badge/badge.component.html | 3 +++ ...{badge.directive.ts => badge.component.ts} | 13 ++++++----- libs/components/src/badge/badge.module.ts | 6 ++--- libs/components/src/badge/badge.stories.ts | 10 ++++----- libs/components/src/badge/index.ts | 2 +- .../src/item/item-action.component.ts | 8 +++++++ .../src/item/item-content.component.ts | 4 ++++ libs/components/src/item/item.stories.ts | 22 +++++++++---------- 8 files changed, 43 insertions(+), 25 deletions(-) create mode 100644 libs/components/src/badge/badge.component.html rename libs/components/src/badge/{badge.directive.ts => badge.component.ts} (89%) diff --git a/libs/components/src/badge/badge.component.html b/libs/components/src/badge/badge.component.html new file mode 100644 index 00000000000..6f586ec3523 --- /dev/null +++ b/libs/components/src/badge/badge.component.html @@ -0,0 +1,3 @@ + + + diff --git a/libs/components/src/badge/badge.directive.ts b/libs/components/src/badge/badge.component.ts similarity index 89% rename from libs/components/src/badge/badge.directive.ts rename to libs/components/src/badge/badge.component.ts index eef876a664d..29644a11ac7 100644 --- a/libs/components/src/badge/badge.directive.ts +++ b/libs/components/src/badge/badge.component.ts @@ -1,6 +1,7 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { Directive, ElementRef, HostBinding, Input } from "@angular/core"; +import { CommonModule } from "@angular/common"; +import { Component, ElementRef, HostBinding, Input } from "@angular/core"; import { FocusableElement } from "../shared/focusable-element"; @@ -28,12 +29,14 @@ const hoverStyles: Record = { info: ["hover:tw-bg-info-600", "hover:tw-border-info-600", "hover:!tw-text-black"], }; -@Directive({ +@Component({ selector: "span[bitBadge], a[bitBadge], button[bitBadge]", - providers: [{ provide: FocusableElement, useExisting: BadgeDirective }], + providers: [{ provide: FocusableElement, useExisting: BadgeComponent }], + imports: [CommonModule], + templateUrl: "badge.component.html", standalone: true, }) -export class BadgeDirective implements FocusableElement { +export class BadgeComponent implements FocusableElement { @HostBinding("class") get classList() { return [ "tw-inline-block", @@ -63,7 +66,7 @@ export class BadgeDirective implements FocusableElement { ] .concat(styles[this.variant]) .concat(this.hasHoverEffects ? hoverStyles[this.variant] : []) - .concat(this.truncate ? ["tw-truncate", this.maxWidthClass] : []); + .concat(this.truncate ? this.maxWidthClass : []); } @HostBinding("attr.title") get titleAttr() { if (this.title !== undefined) { diff --git a/libs/components/src/badge/badge.module.ts b/libs/components/src/badge/badge.module.ts index e7f3770785a..d9a6e712820 100644 --- a/libs/components/src/badge/badge.module.ts +++ b/libs/components/src/badge/badge.module.ts @@ -1,9 +1,9 @@ import { NgModule } from "@angular/core"; -import { BadgeDirective } from "./badge.directive"; +import { BadgeComponent } from "./badge.component"; @NgModule({ - imports: [BadgeDirective], - exports: [BadgeDirective], + imports: [BadgeComponent], + exports: [BadgeComponent], }) export class BadgeModule {} diff --git a/libs/components/src/badge/badge.stories.ts b/libs/components/src/badge/badge.stories.ts index b8ac7ec8efe..5d697f8ad8f 100644 --- a/libs/components/src/badge/badge.stories.ts +++ b/libs/components/src/badge/badge.stories.ts @@ -1,14 +1,14 @@ import { CommonModule } from "@angular/common"; import { Meta, moduleMetadata, StoryObj } from "@storybook/angular"; -import { BadgeDirective } from "./badge.directive"; +import { BadgeComponent } from "./badge.component"; export default { title: "Component Library/Badge", - component: BadgeDirective, + component: BadgeComponent, decorators: [ moduleMetadata({ - imports: [CommonModule, BadgeDirective], + imports: [CommonModule, BadgeComponent], }), ], args: { @@ -21,9 +21,9 @@ export default { url: "https://www.figma.com/file/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=1881%3A16956", }, }, -} as Meta; +} as Meta; -type Story = StoryObj; +type Story = StoryObj; export const Variants: Story = { render: (args) => ({ diff --git a/libs/components/src/badge/index.ts b/libs/components/src/badge/index.ts index a8f5babda91..ae19f4df288 100644 --- a/libs/components/src/badge/index.ts +++ b/libs/components/src/badge/index.ts @@ -1,2 +1,2 @@ -export { BadgeDirective, BadgeVariant } from "./badge.directive"; +export { BadgeComponent, BadgeVariant } from "./badge.component"; export * from "./badge.module"; diff --git a/libs/components/src/item/item-action.component.ts b/libs/components/src/item/item-action.component.ts index 8cabf5c5c23..ae8cb719724 100644 --- a/libs/components/src/item/item-action.component.ts +++ b/libs/components/src/item/item-action.component.ts @@ -8,5 +8,13 @@ import { A11yCellDirective } from "../a11y/a11y-cell.directive"; imports: [], template: ``, providers: [{ provide: A11yCellDirective, useExisting: ItemActionComponent }], + host: { + class: + /** + * `top` and `bottom` units should be kept in sync with `item-content.component.ts`'s y-axis padding. + * we want this `:after` element to be the same height as the `item-content` + */ + "[&>button]:tw-relative [&>button:not([bit-item-content])]:after:tw-content-[''] [&>button]:after:tw-absolute [&>button]:after:tw-block bit-compact:[&>button]:after:tw-top-[-0.8rem] bit-compact:[&>button]:after:tw-bottom-[-0.8rem] [&>button]:after:tw-top-[-0.85rem] [&>button]:after:tw-bottom-[-0.85rem] [&>button]:after:tw-right-[-0.25rem] [&>button]:after:tw-left-[-0.25rem]", + }, }) export class ItemActionComponent extends A11yCellDirective {} diff --git a/libs/components/src/item/item-content.component.ts b/libs/components/src/item/item-content.component.ts index 824b6a596a0..f6cc3f133ad 100644 --- a/libs/components/src/item/item-content.component.ts +++ b/libs/components/src/item/item-content.component.ts @@ -19,6 +19,10 @@ import { TypographyModule } from "../typography"; templateUrl: `item-content.component.html`, host: { class: + /** + * y-axis padding should be kept in sync with `item-action.component.ts`'s `top` and `bottom` units. + * we want this to be the same height as the `item-action`'s `:after` element + */ "fvw-target tw-outline-none tw-text-main hover:tw-text-main tw-no-underline hover:tw-no-underline tw-text-base tw-py-2 tw-px-4 bit-compact:tw-py-1.5 bit-compact:tw-px-2 tw-bg-transparent tw-w-full tw-border-none tw-flex tw-gap-4 tw-items-center tw-justify-between", }, changeDetection: ChangeDetectionStrategy.OnPush, diff --git a/libs/components/src/item/item.stories.ts b/libs/components/src/item/item.stories.ts index 675172565f1..5adf9d3c49d 100644 --- a/libs/components/src/item/item.stories.ts +++ b/libs/components/src/item/item.stories.ts @@ -70,7 +70,7 @@ export const Default: Story = { - + @@ -163,7 +163,7 @@ const multipleActionListTemplate = /*html*/ ` - + @@ -182,7 +182,7 @@ const multipleActionListTemplate = /*html*/ ` - + @@ -201,7 +201,7 @@ const multipleActionListTemplate = /*html*/ ` - + @@ -220,7 +220,7 @@ const multipleActionListTemplate = /*html*/ ` - + @@ -239,7 +239,7 @@ const multipleActionListTemplate = /*html*/ ` - + @@ -258,7 +258,7 @@ const multipleActionListTemplate = /*html*/ ` - + @@ -332,14 +332,14 @@ export const SingleActionWithBadge: Story = { Foobar - Auto-fill + Fill Helloooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo! - Auto-fill + Fill @@ -375,7 +375,7 @@ export const VirtualScrolling: Story = { - + @@ -405,7 +405,7 @@ export const WithoutBorderRadius: Story = { - + From 4b42092c93537d69f2357e9fc5090fd7f48600a2 Mon Sep 17 00:00:00 2001 From: cd-bitwarden <106776772+cd-bitwarden@users.noreply.github.com> Date: Fri, 20 Dec 2024 14:42:15 -0500 Subject: [PATCH 42/54] [PM-15047] Updating admin console 3 dots menu to use th enewer attachments modal (#12402) * Updating admin console 3 dots menu to use th enewer attachments modal * Update apps/web/src/app/vault/org-vault/vault.component.ts Co-authored-by: Shane Melton * lint fix --------- Co-authored-by: --global <> Co-authored-by: Shane Melton --- .../app/vault/org-vault/vault.component.ts | 38 ++++++++----------- 1 file changed, 15 insertions(+), 23 deletions(-) diff --git a/apps/web/src/app/vault/org-vault/vault.component.ts b/apps/web/src/app/vault/org-vault/vault.component.ts index e1aca825960..15453a8c064 100644 --- a/apps/web/src/app/vault/org-vault/vault.component.ts +++ b/apps/web/src/app/vault/org-vault/vault.component.ts @@ -104,6 +104,10 @@ import { } from "../components/vault-item-dialog/vault-item-dialog.component"; import { VaultItemEvent } from "../components/vault-items/vault-item-event"; import { VaultItemsModule } from "../components/vault-items/vault-items.module"; +import { + AttachmentDialogResult, + AttachmentsV2Component, +} from "../individual-vault/attachments-v2.component"; import { BulkDeleteDialogResult, openBulkDeleteDialog, @@ -119,7 +123,6 @@ import { VaultHeaderComponent } from "../org-vault/vault-header/vault-header.com import { getNestedCollectionTree } from "../utils/collection-utils"; import { AddEditComponent } from "./add-edit.component"; -import { AttachmentsComponent } from "./attachments.component"; import { BulkCollectionsDialogComponent, BulkCollectionsDialogResult, @@ -761,29 +764,18 @@ export class VaultComponent implements OnInit, OnDestroy { return; } - let madeAttachmentChanges = false; - - const [modal] = await this.modalService.openViewRef( - AttachmentsComponent, - this.attachmentsModalRef, - (comp) => { - comp.organization = this.organization; - comp.cipherId = cipher.id; - comp.onUploadedAttachment - .pipe(takeUntil(this.destroy$)) - .subscribe(() => (madeAttachmentChanges = true)); - comp.onDeletedAttachment - .pipe(takeUntil(this.destroy$)) - .subscribe(() => (madeAttachmentChanges = true)); - }, - ); - - modal.onClosed.pipe(takeUntil(this.destroy$)).subscribe(() => { - if (madeAttachmentChanges) { - this.refresh(); - } - madeAttachmentChanges = false; + const dialogRef = AttachmentsV2Component.open(this.dialogService, { + cipherId: cipher.id as CipherId, }); + + const result = await firstValueFrom(dialogRef.closed); + + if ( + result.action === AttachmentDialogResult.Removed || + result.action === AttachmentDialogResult.Uploaded + ) { + this.refresh(); + } } async addCipher(cipherType?: CipherType) { From 0619ef507fb6224ecbdc2813192766300752e68a Mon Sep 17 00:00:00 2001 From: Daniel James Smith <2670567+djsmith85@users.noreply.github.com> Date: Fri, 20 Dec 2024 20:46:34 +0100 Subject: [PATCH 43/54] [PM-7114] Remove legacy Send code from browser extension (#12342) * Remove Send grouping, type and state * Delete Send list and add-edit --------- Co-authored-by: Daniel James Smith --- .github/whitelist-capital-letters.txt | 1 - apps/browser/src/_locales/en/messages.json | 88 ---- .../src/models/browserSendComponentState.ts | 20 - apps/browser/src/popup/app-routing.module.ts | 22 +- apps/browser/src/popup/app.component.ts | 4 - apps/browser/src/popup/app.module.ts | 8 - .../src/popup/services/services.module.ts | 6 - .../send/components/send-list.component.html | 98 ----- .../send/components/send-list.component.ts | 38 -- .../popup/send/send-add-edit.component.html | 411 ------------------ .../popup/send/send-add-edit.component.ts | 140 ------ .../popup/send/send-groupings.component.html | 119 ----- .../popup/send/send-groupings.component.ts | 203 --------- .../tools/popup/send/send-type.component.html | 68 --- .../tools/popup/send/send-type.component.ts | 190 -------- .../browser-send-state.service.spec.ts | 59 --- .../services/browser-send-state.service.ts | 71 --- .../popup/services/key-definitions.spec.ts | 39 -- .../tools/popup/services/key-definitions.ts | 27 -- 19 files changed, 8 insertions(+), 1604 deletions(-) delete mode 100644 apps/browser/src/models/browserSendComponentState.ts delete mode 100644 apps/browser/src/tools/popup/send/components/send-list.component.html delete mode 100644 apps/browser/src/tools/popup/send/components/send-list.component.ts delete mode 100644 apps/browser/src/tools/popup/send/send-add-edit.component.html delete mode 100644 apps/browser/src/tools/popup/send/send-add-edit.component.ts delete mode 100644 apps/browser/src/tools/popup/send/send-groupings.component.html delete mode 100644 apps/browser/src/tools/popup/send/send-groupings.component.ts delete mode 100644 apps/browser/src/tools/popup/send/send-type.component.html delete mode 100644 apps/browser/src/tools/popup/send/send-type.component.ts delete mode 100644 apps/browser/src/tools/popup/services/browser-send-state.service.spec.ts delete mode 100644 apps/browser/src/tools/popup/services/browser-send-state.service.ts delete mode 100644 apps/browser/src/tools/popup/services/key-definitions.spec.ts delete mode 100644 apps/browser/src/tools/popup/services/key-definitions.ts diff --git a/.github/whitelist-capital-letters.txt b/.github/whitelist-capital-letters.txt index a320149281b..73d323851e5 100644 --- a/.github/whitelist-capital-letters.txt +++ b/.github/whitelist-capital-letters.txt @@ -37,7 +37,6 @@ ./apps/browser/store/windows/AppxManifest.xml ./apps/browser/src/background/nativeMessaging.background.ts ./apps/browser/src/models/browserComponentState.ts -./apps/browser/src/models/browserSendComponentState.ts ./apps/browser/src/models/browserGroupingsComponentState.ts ./apps/browser/src/models/biometricErrors.ts ./apps/browser/src/browser/safariApp.ts diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 4b7d9eb2992..8fe283ba289 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -1150,9 +1150,6 @@ "moveToOrganization": { "message": "Move to organization" }, - "share": { - "message": "Share" - }, "movedItemToOrg": { "message": "$ITEMNAME$ moved to $ORGNAME$", "placeholders": { @@ -2379,14 +2376,6 @@ "message": "Send details", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "searchSends": { - "message": "Search Sends", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "addSend": { - "message": "Add Send", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendTypeText": { "message": "Text" }, @@ -2403,16 +2392,9 @@ "hideTextByDefault": { "message": "Hide text by default" }, - "maxAccessCountReached": { - "message": "Max access count reached", - "description": "This text will be displayed after a Send has been accessed the maximum amount of times." - }, "expired": { "message": "Expired" }, - "pendingDeletion": { - "message": "Pending deletion" - }, "passwordProtected": { "message": "Password protected" }, @@ -2462,24 +2444,9 @@ "message": "Edit Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendTypeHeader": { - "message": "What type of Send is this?", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendNameDesc": { - "message": "A friendly name to describe this Send.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendFileDesc": { - "message": "The file you want to send." - }, "deletionDate": { "message": "Deletion date" }, - "deletionDateDesc": { - "message": "The Send will be permanently deleted on the specified date and time.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "deletionDateDescV2": { "message": "The Send will be permanently deleted on this date.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2487,10 +2454,6 @@ "expirationDate": { "message": "Expiration date" }, - "expirationDateDesc": { - "message": "If set, access to this Send will expire on the specified date and time.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "oneDay": { "message": "1 day" }, @@ -2506,43 +2469,10 @@ "custom": { "message": "Custom" }, - "maximumAccessCount": { - "message": "Maximum Access Count" - }, - "maximumAccessCountDesc": { - "message": "If set, users will no longer be able to access this Send once the maximum access count is reached.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendPasswordDesc": { - "message": "Optionally require a password for users to access this Send.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendPasswordDescV3": { "message": "Add an optional password for recipients to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendNotesDesc": { - "message": "Private notes about this Send.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendDisableDesc": { - "message": "Deactivate this Send so that no one can access it.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendShareDesc": { - "message": "Copy this Send's link to clipboard upon save.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendTextDesc": { - "message": "The text you want to send." - }, - "sendHideText": { - "message": "Hide this Send's text by default.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "currentAccessCount": { - "message": "Current access count" - }, "createSend": { "message": "New Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2625,18 +2555,6 @@ "sendFileCalloutHeader": { "message": "Before you start" }, - "sendFirefoxCustomDatePopoutMessage1": { - "message": "To use a calendar style date picker", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read '**To use a calendar style date picker ** click here to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage2": { - "message": "click here", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker **click here** to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage3": { - "message": "to pop out your window.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker click here **to pop out your window.**'" - }, "expirationDateIsInvalid": { "message": "The expiration date provided is not valid." }, @@ -2652,15 +2570,9 @@ "dateParsingError": { "message": "There was an error saving your deletion and expiration dates." }, - "hideEmail": { - "message": "Hide my email address from recipients." - }, "hideYourEmail": { "message": "Hide your email address from viewers." }, - "sendOptionsPolicyInEffect": { - "message": "One or more organization policies are affecting your Send options." - }, "passwordPrompt": { "message": "Master password re-prompt" }, diff --git a/apps/browser/src/models/browserSendComponentState.ts b/apps/browser/src/models/browserSendComponentState.ts deleted file mode 100644 index 204595df272..00000000000 --- a/apps/browser/src/models/browserSendComponentState.ts +++ /dev/null @@ -1,20 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { SendView } from "@bitwarden/common/tools/send/models/view/send.view"; -import { DeepJsonify } from "@bitwarden/common/types/deep-jsonify"; - -import { BrowserComponentState } from "./browserComponentState"; - -export class BrowserSendComponentState extends BrowserComponentState { - sends: SendView[]; - - static fromJSON(json: DeepJsonify) { - if (json == null) { - return null; - } - - return Object.assign(new BrowserSendComponentState(), json, { - sends: json.sends?.map((s) => SendView.fromJSON(s)), - }); - } -} diff --git a/apps/browser/src/popup/app-routing.module.ts b/apps/browser/src/popup/app-routing.module.ts index 1f42c1cf31d..0a6ff456938 100644 --- a/apps/browser/src/popup/app-routing.module.ts +++ b/apps/browser/src/popup/app-routing.module.ts @@ -86,9 +86,6 @@ import BrowserPopupUtils from "../platform/popup/browser-popup-utils"; import { popupRouterCacheGuard } from "../platform/popup/view-cache/popup-router-cache.service"; import { CredentialGeneratorHistoryComponent } from "../tools/popup/generator/credential-generator-history.component"; import { CredentialGeneratorComponent } from "../tools/popup/generator/credential-generator.component"; -import { SendAddEditComponent } from "../tools/popup/send/send-add-edit.component"; -import { SendGroupingsComponent } from "../tools/popup/send/send-groupings.component"; -import { SendTypeComponent } from "../tools/popup/send/send-type.component"; import { SendAddEditComponent as SendAddEditV2Component } from "../tools/popup/send-v2/add-edit/send-add-edit.component"; import { SendCreatedComponent } from "../tools/popup/send-v2/send-created/send-created.component"; import { SendV2Component } from "../tools/popup/send-v2/send-v2.component"; @@ -415,21 +412,17 @@ const routes: Routes = [ data: { elevation: 1 } satisfies RouteDataProperties, }), { - path: "send-type", - component: SendTypeComponent, + path: "add-send", + component: SendAddEditV2Component, canActivate: [authGuard], data: { elevation: 1 } satisfies RouteDataProperties, }, - ...extensionRefreshSwap(SendAddEditComponent, SendAddEditV2Component, { - path: "add-send", - canActivate: [authGuard], - data: { elevation: 1 } satisfies RouteDataProperties, - }), - ...extensionRefreshSwap(SendAddEditComponent, SendAddEditV2Component, { + { path: "edit-send", + component: SendAddEditV2Component, canActivate: [authGuard], data: { elevation: 1 } satisfies RouteDataProperties, - }), + }, { path: "send-created", component: SendCreatedComponent, @@ -768,11 +761,12 @@ const routes: Routes = [ canActivate: [authGuard], data: { elevation: 0 } satisfies RouteDataProperties, }, - ...extensionRefreshSwap(SendGroupingsComponent, SendV2Component, { + { path: "send", + component: SendV2Component, canActivate: [authGuard], data: { elevation: 0 } satisfies RouteDataProperties, - }), + }, ], }, { diff --git a/apps/browser/src/popup/app.component.ts b/apps/browser/src/popup/app.component.ts index 248050ed61c..3382e24689a 100644 --- a/apps/browser/src/popup/app.component.ts +++ b/apps/browser/src/popup/app.component.ts @@ -26,7 +26,6 @@ import { PopupCompactModeService } from "../platform/popup/layout/popup-compact- import { PopupWidthService } from "../platform/popup/layout/popup-width.service"; import { PopupViewCacheService } from "../platform/popup/view-cache/popup-view-cache.service"; import { initPopupClosedListener } from "../platform/services/popup-view-cache-background.service"; -import { BrowserSendStateService } from "../tools/popup/services/browser-send-state.service"; import { VaultBrowserStateService } from "../vault/services/vault-browser-state.service"; import { routerTransition } from "./app-routing.animations"; @@ -57,7 +56,6 @@ export class AppComponent implements OnInit, OnDestroy { private i18nService: I18nService, private router: Router, private stateService: StateService, - private browserSendStateService: BrowserSendStateService, private vaultBrowserStateService: VaultBrowserStateService, private cipherService: CipherService, private changeDetectorRef: ChangeDetectorRef, @@ -243,8 +241,6 @@ export class AppComponent implements OnInit, OnDestroy { await Promise.all([ this.vaultBrowserStateService.setBrowserGroupingsComponentState(null), this.vaultBrowserStateService.setBrowserVaultItemsComponentState(null), - this.browserSendStateService.setBrowserSendComponentState(null), - this.browserSendStateService.setBrowserSendTypeComponentState(null), ]); } diff --git a/apps/browser/src/popup/app.module.ts b/apps/browser/src/popup/app.module.ts index c2697b2d490..b547078084b 100644 --- a/apps/browser/src/popup/app.module.ts +++ b/apps/browser/src/popup/app.module.ts @@ -56,10 +56,6 @@ import { PopupHeaderComponent } from "../platform/popup/layout/popup-header.comp import { PopupPageComponent } from "../platform/popup/layout/popup-page.component"; import { PopupTabNavigationComponent } from "../platform/popup/layout/popup-tab-navigation.component"; import { FilePopoutCalloutComponent } from "../tools/popup/components/file-popout-callout.component"; -import { SendListComponent } from "../tools/popup/send/components/send-list.component"; -import { SendAddEditComponent } from "../tools/popup/send/send-add-edit.component"; -import { SendGroupingsComponent } from "../tools/popup/send/send-groupings.component"; -import { SendTypeComponent } from "../tools/popup/send/send-type.component"; import { ActionButtonsComponent } from "../vault/popup/components/action-buttons.component"; import { CipherRowComponent } from "../vault/popup/components/cipher-row.component"; import { AddEditCustomFieldsComponent } from "../vault/popup/components/vault/add-edit-custom-fields.component"; @@ -161,10 +157,6 @@ import "../platform/popup/locales"; PasswordHistoryComponent, PremiumComponent, RegisterComponent, - SendAddEditComponent, - SendGroupingsComponent, - SendListComponent, - SendTypeComponent, SetPasswordComponent, VaultSettingsComponent, ShareComponent, diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index 7014d908ac3..5b27833636f 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -152,7 +152,6 @@ import { ForegroundSyncService } from "../../platform/sync/foreground-sync.servi import { fromChromeRuntimeMessaging } from "../../platform/utils/from-chrome-runtime-messaging"; import { ExtensionLockComponentService } from "../../services/extension-lock-component.service"; import { ForegroundVaultTimeoutService } from "../../services/vault-timeout/foreground-vault-timeout.service"; -import { BrowserSendStateService } from "../../tools/popup/services/browser-send-state.service"; import { FilePopoutUtilsService } from "../../tools/popup/services/file-popout-utils.service"; import { Fido2UserVerificationService } from "../../vault/services/fido2-user-verification.service"; import { VaultBrowserStateService } from "../../vault/services/vault-browser-state.service"; @@ -473,11 +472,6 @@ const safeProviders: SafeProvider[] = [ useClass: UserNotificationSettingsService, deps: [StateProvider], }), - safeProvider({ - provide: BrowserSendStateService, - useClass: BrowserSendStateService, - deps: [StateProvider], - }), safeProvider({ provide: MessageListener, useFactory: (subject: Subject>>, ngZone: NgZone) => diff --git a/apps/browser/src/tools/popup/send/components/send-list.component.html b/apps/browser/src/tools/popup/send/components/send-list.component.html deleted file mode 100644 index 05c8e3e3754..00000000000 --- a/apps/browser/src/tools/popup/send/components/send-list.component.html +++ /dev/null @@ -1,98 +0,0 @@ -
    - -
    - - - -
    -
    diff --git a/apps/browser/src/tools/popup/send/components/send-list.component.ts b/apps/browser/src/tools/popup/send/components/send-list.component.ts deleted file mode 100644 index bab818cce6a..00000000000 --- a/apps/browser/src/tools/popup/send/components/send-list.component.ts +++ /dev/null @@ -1,38 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { Component, EventEmitter, Input, Output } from "@angular/core"; - -import { SendType } from "@bitwarden/common/tools/send/enums/send-type"; -import { SendView } from "@bitwarden/common/tools/send/models/view/send.view"; - -@Component({ - selector: "app-send-list", - templateUrl: "send-list.component.html", -}) -export class SendListComponent { - @Input() sends: SendView[]; - @Input() title: string; - @Input() disabledByPolicy = false; - @Output() onSelected = new EventEmitter(); - @Output() onCopySendLink = new EventEmitter(); - @Output() onRemovePassword = new EventEmitter(); - @Output() onDeleteSend = new EventEmitter(); - - sendType = SendType; - - selectSend(s: SendView) { - this.onSelected.emit(s); - } - - copySendLink(s: SendView) { - this.onCopySendLink.emit(s); - } - - removePassword(s: SendView) { - this.onRemovePassword.emit(s); - } - - delete(s: SendView) { - this.onDeleteSend.emit(s); - } -} diff --git a/apps/browser/src/tools/popup/send/send-add-edit.component.html b/apps/browser/src/tools/popup/send/send-add-edit.component.html deleted file mode 100644 index 38c8a8175bb..00000000000 --- a/apps/browser/src/tools/popup/send/send-add-edit.component.html +++ /dev/null @@ -1,411 +0,0 @@ -
    -
    -
    - -
    -

    - {{ title }} -

    -
    - -
    -
    -
    - - - {{ "sendDisabledWarning" | i18n }} - - - {{ "sendOptionsPolicyInEffect" | i18n }} - - - - -
    -
    -
    - - -
    -
    - -
    - -
    -
    -
    - -
    - - -
    -
    -
    -
    - -
    -
    -
    - -
    {{ send.file.fileName }} ({{ send.file.sizeName }})
    -
    -
    - - -
    -
    - -
    - -
    -
    -
    - - -
    -
    - -
    -
    - - -
    -
    -
    - -
    -

    - {{ "share" | i18n }} -

    -
    - -
    - - -
    -
    -
    - -
    -

    - -

    -
    -
    - -
    -
    - -
    - - -
    -
    - -
    -
    -
    - - -
    -
    - -
    - - -
    -
    - -
    - - -
    -
    - -
    -
    -
    -
    - - -
    - -
    -
    - -
    - -
    -
    -
    - - -
    -
    - -
    - -
    -
    -
    - - -
    -
    -
    - -
    -
    -
    -
    - - - -
    -
    - -
    -
    -
    - -
    - -
    -
    -
    - - -
    -
    - -
    - -
    -
    -
    - - -
    -
    -
    - -
    -
    -
    - - -
    -
    -
    -
    - -
    -
    - -
    -
    -
    -
    diff --git a/apps/browser/src/tools/popup/send/send-add-edit.component.ts b/apps/browser/src/tools/popup/send/send-add-edit.component.ts deleted file mode 100644 index 66b0355bb76..00000000000 --- a/apps/browser/src/tools/popup/send/send-add-edit.component.ts +++ /dev/null @@ -1,140 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { DatePipe, Location } from "@angular/common"; -import { Component, OnInit } from "@angular/core"; -import { FormBuilder } from "@angular/forms"; -import { ActivatedRoute, Router } from "@angular/router"; -import { first } from "rxjs/operators"; - -import { AddEditComponent as BaseAddEditComponent } from "@bitwarden/angular/tools/send/add-edit.component"; -import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; -import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; -import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; -import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction"; -import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction"; -import { DialogService, ToastService } from "@bitwarden/components"; - -import BrowserPopupUtils from "../../../platform/popup/browser-popup-utils"; -import { FilePopoutUtilsService } from "../services/file-popout-utils.service"; - -@Component({ - selector: "app-send-add-edit", - templateUrl: "send-add-edit.component.html", -}) -// eslint-disable-next-line rxjs-angular/prefer-takeuntil -export class SendAddEditComponent extends BaseAddEditComponent implements OnInit { - // Options header - showOptions = false; - // File visibility - isFirefox = false; - inPopout = false; - showFileSelector = false; - - constructor( - i18nService: I18nService, - platformUtilsService: PlatformUtilsService, - stateService: StateService, - messagingService: MessagingService, - policyService: PolicyService, - environmentService: EnvironmentService, - datePipe: DatePipe, - sendService: SendService, - private route: ActivatedRoute, - private router: Router, - private location: Location, - logService: LogService, - sendApiService: SendApiService, - dialogService: DialogService, - formBuilder: FormBuilder, - private filePopoutUtilsService: FilePopoutUtilsService, - billingAccountProfileStateService: BillingAccountProfileStateService, - accountService: AccountService, - toastService: ToastService, - ) { - super( - i18nService, - platformUtilsService, - environmentService, - datePipe, - sendService, - messagingService, - policyService, - logService, - stateService, - sendApiService, - dialogService, - formBuilder, - billingAccountProfileStateService, - accountService, - toastService, - ); - } - - popOutWindow() { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - BrowserPopupUtils.openCurrentPagePopout(window); - } - - async ngOnInit() { - // File visibility - this.showFileSelector = - !this.editMode && !this.filePopoutUtilsService.showFilePopoutMessage(window); - this.inPopout = BrowserPopupUtils.inPopout(window); - this.isFirefox = this.platformUtilsService.isFirefox(); - - // eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe - this.route.queryParams.pipe(first()).subscribe(async (params) => { - if (params.sendId) { - this.sendId = params.sendId; - } - if (params.type) { - const type = parseInt(params.type, null); - this.type = type; - } - await super.ngOnInit(); - }); - - window.setTimeout(() => { - if (!this.editMode) { - document.getElementById("name").focus(); - } - }, 200); - } - - async submit(): Promise { - if (await super.submit()) { - this.cancel(); - return true; - } - - return false; - } - - async delete(): Promise { - if (await super.delete()) { - this.cancel(); - return true; - } - - return false; - } - - cancel() { - // If true, the window was pop'd out on the add-send page. location.back will not work - const isPopup = (window as any)?.previousPopupUrl?.startsWith("/add-send") ?? false; - if (!isPopup) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate(["tabs/send"]); - } else { - this.location.back(); - } - } -} diff --git a/apps/browser/src/tools/popup/send/send-groupings.component.html b/apps/browser/src/tools/popup/send/send-groupings.component.html deleted file mode 100644 index 213afdfa227..00000000000 --- a/apps/browser/src/tools/popup/send/send-groupings.component.html +++ /dev/null @@ -1,119 +0,0 @@ - -
    - -
    -

    {{ "send" | i18n }}

    - -
    - -
    -
    -
    - - {{ "sendDisabledWarning" | i18n }} - -
    - - - -

    {{ "noItemsInList" | i18n }}

    - -
    -
    - -
    -

    - {{ "types" | i18n }} -

    -
    - - -
    -
    -
    -

    - {{ "allSends" | i18n }} -
    {{ sends.length }}
    -

    -
    - -
    -
    -
    - -
    -

    {{ "noItemsInList" | i18n }}

    -
    -
    -
    - - -
    -
    -
    -
    diff --git a/apps/browser/src/tools/popup/send/send-groupings.component.ts b/apps/browser/src/tools/popup/send/send-groupings.component.ts deleted file mode 100644 index 80c2de1cdac..00000000000 --- a/apps/browser/src/tools/popup/send/send-groupings.component.ts +++ /dev/null @@ -1,203 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit } from "@angular/core"; -import { Router } from "@angular/router"; - -import { SendComponent as BaseSendComponent } from "@bitwarden/angular/tools/send/send.component"; -import { SearchService } from "@bitwarden/common/abstractions/search.service"; -import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; -import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; -import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { SendType } from "@bitwarden/common/tools/send/enums/send-type"; -import { SendView } from "@bitwarden/common/tools/send/models/view/send.view"; -import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction"; -import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction"; -import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; -import { DialogService, ToastService } from "@bitwarden/components"; - -import { BrowserSendComponentState } from "../../../models/browserSendComponentState"; -import BrowserPopupUtils from "../../../platform/popup/browser-popup-utils"; -import { BrowserSendStateService } from "../services/browser-send-state.service"; - -const ComponentId = "SendComponent"; - -@Component({ - selector: "app-send-groupings", - templateUrl: "send-groupings.component.html", -}) -export class SendGroupingsComponent extends BaseSendComponent implements OnInit, OnDestroy { - // Header - showLeftHeader = true; - // State Handling - state: BrowserSendComponentState; - private loadedTimeout: number; - - constructor( - sendService: SendService, - i18nService: I18nService, - platformUtilsService: PlatformUtilsService, - environmentService: EnvironmentService, - ngZone: NgZone, - policyService: PolicyService, - searchService: SearchService, - private stateService: BrowserSendStateService, - private router: Router, - private syncService: SyncService, - private changeDetectorRef: ChangeDetectorRef, - private broadcasterService: BroadcasterService, - logService: LogService, - sendApiService: SendApiService, - dialogService: DialogService, - toastService: ToastService, - ) { - super( - sendService, - i18nService, - platformUtilsService, - environmentService, - ngZone, - searchService, - policyService, - logService, - sendApiService, - dialogService, - toastService, - ); - this.onSuccessfulLoad = async () => { - this.selectAll(); - }; - } - - async ngOnInit() { - // Determine Header details - this.showLeftHeader = !( - BrowserPopupUtils.inSidebar(window) && this.platformUtilsService.isFirefox() - ); - // Clear state of Send Type Component - await this.stateService.setBrowserSendTypeComponentState(null); - // Let super class finish - await super.ngOnInit(); - // Handle State Restore if necessary - const restoredScopeState = await this.restoreState(); - if (this.state?.searchText != null) { - this.searchText = this.state.searchText; - } - - if (!this.syncService.syncInProgress) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.load(); - } else { - this.loadedTimeout = window.setTimeout(() => { - if (!this.loaded) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.load(); - } - }, 5000); - } - - if (!this.syncService.syncInProgress || restoredScopeState) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - BrowserPopupUtils.setContentScrollY(window, this.state?.scrollY); - } - - // Load all sends if sync completed in background - this.broadcasterService.subscribe(ComponentId, (message: any) => { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.ngZone.run(async () => { - switch (message.command) { - case "syncCompleted": - window.setTimeout(() => { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.load(); - }, 500); - break; - default: - break; - } - - this.changeDetectorRef.detectChanges(); - }); - }); - } - - ngOnDestroy() { - // Remove timeout - if (this.loadedTimeout != null) { - window.clearTimeout(this.loadedTimeout); - } - // Save state - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.saveState(); - // Unsubscribe - this.broadcasterService.unsubscribe(ComponentId); - } - - async selectType(type: SendType) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate(["/send-type"], { queryParams: { type: type } }); - } - - async selectSend(s: SendView) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate(["/edit-send"], { queryParams: { sendId: s.id } }); - } - - async addSend() { - if (this.disableSend) { - return; - } - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate(["/add-send"]); - } - - async removePassword(s: SendView): Promise { - if (this.disableSend) { - return; - } - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - super.removePassword(s); - } - - showSearching() { - return this.hasSearched || (!this.searchPending && this.isSearchable); - } - - getSendCount(sends: SendView[], type: SendType): number { - return sends.filter((s) => s.type === type).length; - } - - private async saveState() { - this.state = Object.assign(new BrowserSendComponentState(), { - scrollY: BrowserPopupUtils.getContentScrollY(window), - searchText: this.searchText, - sends: this.sends, - }); - await this.stateService.setBrowserSendComponentState(this.state); - } - - private async restoreState(): Promise { - this.state = await this.stateService.getBrowserSendComponentState(); - if (this.state == null) { - return false; - } - - if (this.state.sends != null) { - this.sends = this.state.sends; - } - - return true; - } -} diff --git a/apps/browser/src/tools/popup/send/send-type.component.html b/apps/browser/src/tools/popup/send/send-type.component.html deleted file mode 100644 index 0ad6bed2881..00000000000 --- a/apps/browser/src/tools/popup/send/send-type.component.html +++ /dev/null @@ -1,68 +0,0 @@ -
    -
    - -
    -

    {{ "send" | i18n }}

    - -
    - -
    -
    -
    - - {{ "sendDisabledWarning" | i18n }} - -
    - - -

    {{ "noItemsInList" | i18n }}

    - -
    -
    -
    -

    - {{ groupingTitle }} - {{ filteredSends.length }} -

    -
    - - -
    -
    -
    diff --git a/apps/browser/src/tools/popup/send/send-type.component.ts b/apps/browser/src/tools/popup/send/send-type.component.ts deleted file mode 100644 index bcc24b443fc..00000000000 --- a/apps/browser/src/tools/popup/send/send-type.component.ts +++ /dev/null @@ -1,190 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { Location } from "@angular/common"; -import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit } from "@angular/core"; -import { ActivatedRoute, Router } from "@angular/router"; -import { first } from "rxjs/operators"; - -import { SendComponent as BaseSendComponent } from "@bitwarden/angular/tools/send/send.component"; -import { SearchService } from "@bitwarden/common/abstractions/search.service"; -import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; -import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; -import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { SendType } from "@bitwarden/common/tools/send/enums/send-type"; -import { SendView } from "@bitwarden/common/tools/send/models/view/send.view"; -import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction"; -import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction"; -import { DialogService, ToastService } from "@bitwarden/components"; - -import { BrowserComponentState } from "../../../models/browserComponentState"; -import BrowserPopupUtils from "../../../platform/popup/browser-popup-utils"; -import { BrowserSendStateService } from "../services/browser-send-state.service"; - -const ComponentId = "SendTypeComponent"; - -@Component({ - selector: "app-send-type", - templateUrl: "send-type.component.html", -}) -export class SendTypeComponent extends BaseSendComponent implements OnInit, OnDestroy { - groupingTitle: string; - // State Handling - state: BrowserComponentState; - private refreshTimeout: number; - private applySavedState = true; - - constructor( - sendService: SendService, - i18nService: I18nService, - platformUtilsService: PlatformUtilsService, - environmentService: EnvironmentService, - ngZone: NgZone, - policyService: PolicyService, - searchService: SearchService, - private stateService: BrowserSendStateService, - private route: ActivatedRoute, - private location: Location, - private changeDetectorRef: ChangeDetectorRef, - private broadcasterService: BroadcasterService, - private router: Router, - logService: LogService, - sendApiService: SendApiService, - dialogService: DialogService, - toastService: ToastService, - ) { - super( - sendService, - i18nService, - platformUtilsService, - environmentService, - ngZone, - searchService, - policyService, - logService, - sendApiService, - dialogService, - toastService, - ); - this.onSuccessfulLoad = async () => { - this.selectType(this.type); - }; - this.applySavedState = - (window as any).previousPopupUrl != null && - !(window as any).previousPopupUrl.startsWith("/send-type"); - } - - async ngOnInit() { - // Let super class finish - await super.ngOnInit(); - // eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe - this.route.queryParams.pipe(first()).subscribe(async (params) => { - if (this.applySavedState) { - this.state = await this.stateService.getBrowserSendTypeComponentState(); - if (this.state?.searchText != null) { - this.searchText = this.state.searchText; - } - } - - if (params.type != null) { - this.type = parseInt(params.type, null); - switch (this.type) { - case SendType.Text: - this.groupingTitle = this.i18nService.t("sendTypeText"); - break; - case SendType.File: - this.groupingTitle = this.i18nService.t("sendTypeFile"); - break; - default: - break; - } - await this.load((s) => s.type === this.type); - } - - // Restore state and remove reference - if (this.applySavedState && this.state != null) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - BrowserPopupUtils.setContentScrollY(window, this.state?.scrollY); - } - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.stateService.setBrowserSendTypeComponentState(null); - }); - - // Refresh Send list if sync completed in background - this.broadcasterService.subscribe(ComponentId, (message: any) => { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.ngZone.run(async () => { - switch (message.command) { - case "syncCompleted": - if (message.successfully) { - this.refreshTimeout = window.setTimeout(() => { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.refresh(); - }, 500); - } - break; - default: - break; - } - - this.changeDetectorRef.detectChanges(); - }); - }); - } - - ngOnDestroy() { - // Remove timeout - if (this.refreshTimeout != null) { - window.clearTimeout(this.refreshTimeout); - } - // Save state - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.saveState(); - // Unsubscribe - this.broadcasterService.unsubscribe(ComponentId); - } - - async selectSend(s: SendView) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate(["/edit-send"], { queryParams: { sendId: s.id } }); - } - - async addSend() { - if (this.disableSend) { - return; - } - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate(["/add-send"], { queryParams: { type: this.type } }); - } - - async removePassword(s: SendView): Promise { - if (this.disableSend) { - return; - } - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - super.removePassword(s); - } - - back() { - (window as any).routeDirection = "b"; - this.location.back(); - } - - private async saveState() { - this.state = { - scrollY: BrowserPopupUtils.getContentScrollY(window), - searchText: this.searchText, - }; - await this.stateService.setBrowserSendTypeComponentState(this.state); - } -} diff --git a/apps/browser/src/tools/popup/services/browser-send-state.service.spec.ts b/apps/browser/src/tools/popup/services/browser-send-state.service.spec.ts deleted file mode 100644 index 6f0ae1455ad..00000000000 --- a/apps/browser/src/tools/popup/services/browser-send-state.service.spec.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { - FakeAccountService, - mockAccountServiceWith, -} from "@bitwarden/common/../spec/fake-account-service"; -import { FakeStateProvider } from "@bitwarden/common/../spec/fake-state-provider"; -import { awaitAsync } from "@bitwarden/common/../spec/utils"; - -import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { UserId } from "@bitwarden/common/types/guid"; - -import { BrowserComponentState } from "../../../models/browserComponentState"; -import { BrowserSendComponentState } from "../../../models/browserSendComponentState"; - -import { BrowserSendStateService } from "./browser-send-state.service"; - -describe("Browser Send State Service", () => { - let stateProvider: FakeStateProvider; - - let accountService: FakeAccountService; - let stateService: BrowserSendStateService; - const mockUserId = Utils.newGuid() as UserId; - - beforeEach(() => { - accountService = mockAccountServiceWith(mockUserId); - stateProvider = new FakeStateProvider(accountService); - - stateService = new BrowserSendStateService(stateProvider); - }); - - describe("getBrowserSendComponentState", () => { - it("should return BrowserSendComponentState", async () => { - const state = new BrowserSendComponentState(); - state.scrollY = 0; - state.searchText = "test"; - - await stateService.setBrowserSendComponentState(state); - - await awaitAsync(); - - const actual = await stateService.getBrowserSendComponentState(); - expect(actual).toStrictEqual(state); - }); - }); - - describe("getBrowserSendTypeComponentState", () => { - it("should return BrowserComponentState", async () => { - const state = new BrowserComponentState(); - state.scrollY = 0; - state.searchText = "test"; - - await stateService.setBrowserSendTypeComponentState(state); - - await awaitAsync(); - - const actual = await stateService.getBrowserSendTypeComponentState(); - expect(actual).toStrictEqual(state); - }); - }); -}); diff --git a/apps/browser/src/tools/popup/services/browser-send-state.service.ts b/apps/browser/src/tools/popup/services/browser-send-state.service.ts deleted file mode 100644 index d6c5ae4fd10..00000000000 --- a/apps/browser/src/tools/popup/services/browser-send-state.service.ts +++ /dev/null @@ -1,71 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { Observable, firstValueFrom } from "rxjs"; - -import { ActiveUserState, StateProvider } from "@bitwarden/common/platform/state"; - -import { BrowserComponentState } from "../../../models/browserComponentState"; -import { BrowserSendComponentState } from "../../../models/browserSendComponentState"; - -import { BROWSER_SEND_COMPONENT, BROWSER_SEND_TYPE_COMPONENT } from "./key-definitions"; - -/** Get or set the active user's component state for the Send browser component - */ -export class BrowserSendStateService { - /** Observable that contains the current state for active user Sends including the send data and type counts - * along with the search text and scroll position - */ - browserSendComponentState$: Observable; - - /** Observable that contains the current state for active user Sends that only includes the search text - * and scroll position - */ - browserSendTypeComponentState$: Observable; - - private activeUserBrowserSendComponentState: ActiveUserState; - private activeUserBrowserSendTypeComponentState: ActiveUserState; - - constructor(protected stateProvider: StateProvider) { - this.activeUserBrowserSendComponentState = this.stateProvider.getActive(BROWSER_SEND_COMPONENT); - this.browserSendComponentState$ = this.activeUserBrowserSendComponentState.state$; - - this.activeUserBrowserSendTypeComponentState = this.stateProvider.getActive( - BROWSER_SEND_TYPE_COMPONENT, - ); - this.browserSendTypeComponentState$ = this.activeUserBrowserSendTypeComponentState.state$; - } - - /** Get the active user's browser send component state - * @returns { BrowserSendComponentState } contains the sends and type counts along with the scroll position and search text for the - * send component on the browser - */ - async getBrowserSendComponentState(): Promise { - return await firstValueFrom(this.browserSendComponentState$); - } - - /** Set the active user's browser send component state - * @param { BrowserSendComponentState } value sets the sends along with the scroll position and search text for - * the send component on the browser - */ - async setBrowserSendComponentState(value: BrowserSendComponentState): Promise { - await this.activeUserBrowserSendComponentState.update(() => value, { - shouldUpdate: (current) => !(current == null && value == null), - }); - } - - /** Get the active user's browser component state - * @returns { BrowserComponentState } contains the scroll position and search text for the sends menu on the browser - */ - async getBrowserSendTypeComponentState(): Promise { - return await firstValueFrom(this.browserSendTypeComponentState$); - } - - /** Set the active user's browser component state - * @param { BrowserComponentState } value set the scroll position and search text for the send component on the browser - */ - async setBrowserSendTypeComponentState(value: BrowserComponentState): Promise { - await this.activeUserBrowserSendTypeComponentState.update(() => value, { - shouldUpdate: (current) => !(current == null && value == null), - }); - } -} diff --git a/apps/browser/src/tools/popup/services/key-definitions.spec.ts b/apps/browser/src/tools/popup/services/key-definitions.spec.ts deleted file mode 100644 index 7517771669c..00000000000 --- a/apps/browser/src/tools/popup/services/key-definitions.spec.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { Jsonify } from "type-fest"; - -import { BrowserSendComponentState } from "../../../models/browserSendComponentState"; - -import { BROWSER_SEND_COMPONENT, BROWSER_SEND_TYPE_COMPONENT } from "./key-definitions"; - -describe("Key definitions", () => { - describe("BROWSER_SEND_COMPONENT", () => { - it("should deserialize BrowserSendComponentState", () => { - const keyDef = BROWSER_SEND_COMPONENT; - - const expectedState = { - scrollY: 0, - searchText: "test", - }; - - const result = keyDef.deserializer( - JSON.parse(JSON.stringify(expectedState)) as Jsonify, - ); - - expect(result).toEqual(expectedState); - }); - }); - - describe("BROWSER_SEND_TYPE_COMPONENT", () => { - it("should deserialize BrowserComponentState", () => { - const keyDef = BROWSER_SEND_TYPE_COMPONENT; - - const expectedState = { - scrollY: 0, - searchText: "test", - }; - - const result = keyDef.deserializer(JSON.parse(JSON.stringify(expectedState))); - - expect(result).toEqual(expectedState); - }); - }); -}); diff --git a/apps/browser/src/tools/popup/services/key-definitions.ts b/apps/browser/src/tools/popup/services/key-definitions.ts deleted file mode 100644 index 87c6da126cb..00000000000 --- a/apps/browser/src/tools/popup/services/key-definitions.ts +++ /dev/null @@ -1,27 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { Jsonify } from "type-fest"; - -import { BROWSER_SEND_MEMORY, UserKeyDefinition } from "@bitwarden/common/platform/state"; - -import { BrowserComponentState } from "../../../models/browserComponentState"; -import { BrowserSendComponentState } from "../../../models/browserSendComponentState"; - -export const BROWSER_SEND_COMPONENT = new UserKeyDefinition( - BROWSER_SEND_MEMORY, - "browser_send_component", - { - deserializer: (obj: Jsonify) => - BrowserSendComponentState.fromJSON(obj), - clearOn: ["logout", "lock"], - }, -); - -export const BROWSER_SEND_TYPE_COMPONENT = new UserKeyDefinition( - BROWSER_SEND_MEMORY, - "browser_send_type_component", - { - deserializer: (obj: Jsonify) => BrowserComponentState.fromJSON(obj), - clearOn: ["logout", "lock"], - }, -); From 23210f4a39f7bf61c8b6224b9eda7c1d29cd7380 Mon Sep 17 00:00:00 2001 From: Victoria League Date: Mon, 23 Dec 2024 10:37:12 -0500 Subject: [PATCH 44/54] [PM-16102] Display autofill shortcut on Fill button hover (#12502) --- .../vault-list-items-container.component.html | 2 +- .../vault-list-items-container.component.ts | 29 +++++++++++++++++-- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.html b/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.html index a493ea2171c..067c8dbdf0b 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.html +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.html @@ -55,7 +55,7 @@ bitBadge variant="primary" (click)="doAutofill(cipher)" - [title]="'autofillTitle' | i18n: cipher.name" + [title]="autofillShortcutTooltip() ?? ('autofillTitle' | i18n: cipher.name)" [attr.aria-label]="'autofillTitle' | i18n: cipher.name" > {{ "fill" | i18n }} diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.ts b/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.ts index 3b139d79015..b4022560c35 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.ts @@ -2,12 +2,22 @@ // @ts-strict-ignore import { ScrollingModule } from "@angular/cdk/scrolling"; import { CommonModule } from "@angular/common"; -import { booleanAttribute, Component, EventEmitter, inject, Input, Output } from "@angular/core"; +import { + AfterViewInit, + booleanAttribute, + Component, + EventEmitter, + inject, + Input, + Output, + signal, +} from "@angular/core"; import { Router, RouterLink } from "@angular/router"; import { map } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { @@ -50,7 +60,7 @@ import { ItemMoreOptionsComponent } from "../item-more-options/item-more-options templateUrl: "vault-list-items-container.component.html", standalone: true, }) -export class VaultListItemsContainerComponent { +export class VaultListItemsContainerComponent implements AfterViewInit { private compactModeService = inject(CompactModeService); /** @@ -133,14 +143,29 @@ export class VaultListItemsContainerComponent { return cipher.collections[0]?.name; } + protected autofillShortcutTooltip = signal(undefined); + constructor( private i18nService: I18nService, private vaultPopupAutofillService: VaultPopupAutofillService, private passwordRepromptService: PasswordRepromptService, private cipherService: CipherService, private router: Router, + private platformUtilsService: PlatformUtilsService, ) {} + async ngAfterViewInit() { + const autofillShortcut = await this.platformUtilsService.getAutofillKeyboardShortcut(); + + if (autofillShortcut === "") { + this.autofillShortcutTooltip.set(undefined); + } else { + const autofillTitle = this.i18nService.t("autoFill"); + + this.autofillShortcutTooltip.set(`${autofillTitle} ${autofillShortcut}`); + } + } + /** * Launches the login cipher in a new browser tab. */ From 395258d63ed39305a6fa7f21becbc842210a8db5 Mon Sep 17 00:00:00 2001 From: Victoria League Date: Mon, 23 Dec 2024 12:08:52 -0500 Subject: [PATCH 45/54] [PM-16102] Add min width on interactive badges (#12514) --- libs/components/src/badge/badge.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/components/src/badge/badge.component.ts b/libs/components/src/badge/badge.component.ts index 29644a11ac7..0f8a64ee998 100644 --- a/libs/components/src/badge/badge.component.ts +++ b/libs/components/src/badge/badge.component.ts @@ -65,7 +65,7 @@ export class BadgeComponent implements FocusableElement { "disabled:tw-cursor-not-allowed", ] .concat(styles[this.variant]) - .concat(this.hasHoverEffects ? hoverStyles[this.variant] : []) + .concat(this.hasHoverEffects ? [...hoverStyles[this.variant], "tw-min-w-10"] : []) .concat(this.truncate ? this.maxWidthClass : []); } @HostBinding("attr.title") get titleAttr() { From 35f4edddcff85b55e191857407c7873c60174f8e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 24 Dec 2024 09:41:40 -0500 Subject: [PATCH 46/54] [deps] Platform: Update @types/node to v22.10.2 (#12544) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .../native-messaging-test-runner/package-lock.json | 8 ++++---- apps/desktop/native-messaging-test-runner/package.json | 2 +- package-lock.json | 8 ++++---- package.json | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/apps/desktop/native-messaging-test-runner/package-lock.json b/apps/desktop/native-messaging-test-runner/package-lock.json index 469f8bd1da1..4bf939c8b41 100644 --- a/apps/desktop/native-messaging-test-runner/package-lock.json +++ b/apps/desktop/native-messaging-test-runner/package-lock.json @@ -18,7 +18,7 @@ "yargs": "17.7.2" }, "devDependencies": { - "@types/node": "22.10.1", + "@types/node": "22.10.2", "@types/node-ipc": "9.2.3", "typescript": "4.7.4" } @@ -106,9 +106,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.10.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.1.tgz", - "integrity": "sha512-qKgsUwfHZV2WCWLAnVP1JqnpE6Im6h3Y0+fYgMTasNQ7V++CBX5OT1as0g0f+OyubbFqhf6XVNIsmN4IIhEgGQ==", + "version": "22.10.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.2.tgz", + "integrity": "sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==", "license": "MIT", "dependencies": { "undici-types": "~6.20.0" diff --git a/apps/desktop/native-messaging-test-runner/package.json b/apps/desktop/native-messaging-test-runner/package.json index e32b9e8e987..6ab267be4a8 100644 --- a/apps/desktop/native-messaging-test-runner/package.json +++ b/apps/desktop/native-messaging-test-runner/package.json @@ -23,7 +23,7 @@ "yargs": "17.7.2" }, "devDependencies": { - "@types/node": "22.10.1", + "@types/node": "22.10.2", "@types/node-ipc": "9.2.3", "typescript": "4.7.4" }, diff --git a/package-lock.json b/package-lock.json index a8ae6daf236..6112ddbd966 100644 --- a/package-lock.json +++ b/package-lock.json @@ -111,7 +111,7 @@ "@types/koa-json": "2.0.23", "@types/lowdb": "1.0.15", "@types/lunr": "2.3.7", - "@types/node": "22.10.1", + "@types/node": "22.10.2", "@types/node-fetch": "2.6.4", "@types/node-forge": "1.3.11", "@types/node-ipc": "9.2.3", @@ -10072,9 +10072,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.10.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.1.tgz", - "integrity": "sha512-qKgsUwfHZV2WCWLAnVP1JqnpE6Im6h3Y0+fYgMTasNQ7V++CBX5OT1as0g0f+OyubbFqhf6XVNIsmN4IIhEgGQ==", + "version": "22.10.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.2.tgz", + "integrity": "sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 03991f9d70b..5522f9c5be5 100644 --- a/package.json +++ b/package.json @@ -72,7 +72,7 @@ "@types/koa-json": "2.0.23", "@types/lowdb": "1.0.15", "@types/lunr": "2.3.7", - "@types/node": "22.10.1", + "@types/node": "22.10.2", "@types/node-fetch": "2.6.4", "@types/node-forge": "1.3.11", "@types/node-ipc": "9.2.3", From ef70f7ddced2f7ae064090b94273d3c1d5ef09b1 Mon Sep 17 00:00:00 2001 From: Patrick-Pimentel-Bitwarden Date: Tue, 24 Dec 2024 12:03:04 -0500 Subject: [PATCH 47/54] [PM-4949] Update Accept Organization Invite Flow (#12202) * fix(accept-organization): Update accept organization invite flow Removed old accept organization flow and updated stylings to tailwind. --- .../accept-organization.component.html | 41 ++----------------- .../accept-organization.component.ts | 10 +---- 2 files changed, 6 insertions(+), 45 deletions(-) diff --git a/apps/web/src/app/auth/organization-invite/accept-organization.component.html b/apps/web/src/app/auth/organization-invite/accept-organization.component.html index 88eaa37e8d2..cc08b840c30 100644 --- a/apps/web/src/app/auth/organization-invite/accept-organization.component.html +++ b/apps/web/src/app/auth/organization-invite/accept-organization.component.html @@ -1,9 +1,9 @@ -
    +
    - -

    + +

    @@ -11,36 +11,3 @@

    -
    -
    -
    -

    {{ "joinOrganization" | i18n }}

    -
    -
    -

    - {{ orgName$ | async }} - {{ email }} -

    -

    {{ "joinOrganizationDesc" | i18n }}

    -
    - -
    -
    -
    -
    -
    diff --git a/apps/web/src/app/auth/organization-invite/accept-organization.component.ts b/apps/web/src/app/auth/organization-invite/accept-organization.component.ts index f0425210b3d..660b2759988 100644 --- a/apps/web/src/app/auth/organization-invite/accept-organization.component.ts +++ b/apps/web/src/app/auth/organization-invite/accept-organization.component.ts @@ -56,20 +56,14 @@ export class AcceptOrganizationComponent extends BaseAcceptComponent { async unauthedHandler(qParams: Params): Promise { const invite = OrganizationInvite.fromParams(qParams); await this.acceptOrganizationInviteService.setOrganizationInvitation(invite); - await this.accelerateInviteAcceptIfPossible(invite); + await this.navigateInviteAcceptance(invite); } /** * In certain scenarios, we want to accelerate the user through the accept org invite process * For example, if the user has a BW account already, we want them to be taken to login instead of creation. */ - private async accelerateInviteAcceptIfPossible(invite: OrganizationInvite): Promise { - // if orgUserHasExistingUser is null, we can't determine the user's status - // so we don't want to accelerate the process - if (invite.orgUserHasExistingUser == null) { - return; - } - + private async navigateInviteAcceptance(invite: OrganizationInvite): Promise { // if user exists, send user to login if (invite.orgUserHasExistingUser) { await this.router.navigate(["/login"], { From eb7b2cd3e0b4f6689986340070066379903ee897 Mon Sep 17 00:00:00 2001 From: Daniel Riera Date: Thu, 26 Dec 2024 11:16:57 -0500 Subject: [PATCH 48/54] PM-15593 Standard Login UI is shown when item has no Auth key added (#12432) * PM-15593 - Added getFilteredCiphersForTotpField method - Implemented getFilteredCiphersForTotpField on update list method - Updated tests to not always have a top field * account for no totp code ciphers in totp fields --- .../autofill-inline-menu-list.spec.ts.snap | 552 ++---------------- .../list/autofill-inline-menu-list.spec.ts | 52 +- .../pages/list/autofill-inline-menu-list.ts | 27 +- .../src/autofill/spec/autofill-mocks.ts | 2 +- 4 files changed, 93 insertions(+), 540 deletions(-) diff --git a/apps/browser/src/autofill/overlay/inline-menu/pages/list/__snapshots__/autofill-inline-menu-list.spec.ts.snap b/apps/browser/src/autofill/overlay/inline-menu/pages/list/__snapshots__/autofill-inline-menu-list.spec.ts.snap index 3b8458ec2ab..acd06fb8c65 100644 --- a/apps/browser/src/autofill/overlay/inline-menu/pages/list/__snapshots__/autofill-inline-menu-list.spec.ts.snap +++ b/apps/browser/src/autofill/overlay/inline-menu/pages/list/__snapshots__/autofill-inline-menu-list.spec.ts.snap @@ -681,6 +681,7 @@ exports[`AutofillInlineMenuList initAutofillInlineMenuList the list of ciphers f class="cipher-container" > - -
    - -
  • -
    - - -
    -
  • -
  • -
    - - -
    -
  • -
  • -
    - - -
    -
  • -
  • -
    - -
    -
  • -
  • -
    - -
    {{ startToTrayDescText }}
  • -
    +