From d70d2cb995a9446bdb53283bce4b6b6b7a3507ed Mon Sep 17 00:00:00 2001 From: Jordan Aasen <166539328+jaasen-livefront@users.noreply.github.com> Date: Wed, 16 Oct 2024 09:21:54 -0700 Subject: [PATCH 01/24] [PM-13452] - add password health raw data component (#11519) * add raw data component * fix tests * simplify logic. fix tests * revert change to default config service * remove cipher report dep. fix tests. * revert changes to mock data and specs * remove mock data * use orgId param * fix test --- .../access-intelligence-routing.module.ts | 3 +- .../access-intelligence.component.html | 7 +- .../access-intelligence.component.ts | 2 + .../password-health.component.html | 57 +++++ .../password-health.component.spec.ts | 114 +++++++++ .../password-health.component.ts | 229 ++++++++++++++++++ 6 files changed, 408 insertions(+), 4 deletions(-) create mode 100644 apps/web/src/app/tools/access-intelligence/password-health.component.html create mode 100644 apps/web/src/app/tools/access-intelligence/password-health.component.spec.ts create mode 100644 apps/web/src/app/tools/access-intelligence/password-health.component.ts diff --git a/apps/web/src/app/tools/access-intelligence/access-intelligence-routing.module.ts b/apps/web/src/app/tools/access-intelligence/access-intelligence-routing.module.ts index b35b1fa64a3..88efb2b4832 100644 --- a/apps/web/src/app/tools/access-intelligence/access-intelligence-routing.module.ts +++ b/apps/web/src/app/tools/access-intelligence/access-intelligence-routing.module.ts @@ -1,7 +1,6 @@ import { NgModule } from "@angular/core"; import { RouterModule, Routes } from "@angular/router"; -import { unauthGuardFn } from "@bitwarden/angular/auth/guards"; import { canAccessFeature } from "@bitwarden/angular/platform/guard/feature-flag.guard"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; @@ -11,7 +10,7 @@ const routes: Routes = [ { path: "", component: AccessIntelligenceComponent, - canActivate: [canAccessFeature(FeatureFlag.AccessIntelligence), unauthGuardFn()], + canActivate: [canAccessFeature(FeatureFlag.AccessIntelligence)], data: { titleId: "accessIntelligence", }, diff --git a/apps/web/src/app/tools/access-intelligence/access-intelligence.component.html b/apps/web/src/app/tools/access-intelligence/access-intelligence.component.html index 665f8f6b0c5..df3eee389f6 100644 --- a/apps/web/src/app/tools/access-intelligence/access-intelligence.component.html +++ b/apps/web/src/app/tools/access-intelligence/access-intelligence.component.html @@ -1,6 +1,9 @@ - + + + + diff --git a/apps/web/src/app/tools/access-intelligence/access-intelligence.component.ts b/apps/web/src/app/tools/access-intelligence/access-intelligence.component.ts index 9e5eff6f629..8bdaadbd7e4 100644 --- a/apps/web/src/app/tools/access-intelligence/access-intelligence.component.ts +++ b/apps/web/src/app/tools/access-intelligence/access-intelligence.component.ts @@ -11,6 +11,7 @@ import { HeaderModule } from "../../layouts/header/header.module"; import { ApplicationTableComponent } from "./application-table.component"; import { NotifiedMembersTableComponent } from "./notified-members-table.component"; +import { PasswordHealthComponent } from "./password-health.component"; export enum AccessIntelligenceTabType { AllApps = 0, @@ -26,6 +27,7 @@ export enum AccessIntelligenceTabType { CommonModule, JslibModule, HeaderModule, + PasswordHealthComponent, NotifiedMembersTableComponent, TabsModule, ], diff --git a/apps/web/src/app/tools/access-intelligence/password-health.component.html b/apps/web/src/app/tools/access-intelligence/password-health.component.html new file mode 100644 index 00000000000..32459706449 --- /dev/null +++ b/apps/web/src/app/tools/access-intelligence/password-health.component.html @@ -0,0 +1,57 @@ + +

{{ "passwordsReportDesc" | i18n }}

+
+ + {{ "loading" | i18n }} +
+
+ + + + + {{ "name" | i18n }} + {{ "weakness" | i18n }} + {{ "timesReused" | i18n }} + {{ "timesExposed" | i18n }} + + + + + + + + + + {{ r.name }} + +
+ {{ r.subTitle }} + + + + {{ passwordStrengthMap.get(r.id)[0] | i18n }} + + + + + {{ "reusedXTimes" | i18n: passwordUseMap.get(r.login.password) }} + + + + + {{ "exposedXTimes" | i18n: exposedPasswordMap.get(r.id) }} + + + +
+
+
+
diff --git a/apps/web/src/app/tools/access-intelligence/password-health.component.spec.ts b/apps/web/src/app/tools/access-intelligence/password-health.component.spec.ts new file mode 100644 index 00000000000..4a6d5c50ee1 --- /dev/null +++ b/apps/web/src/app/tools/access-intelligence/password-health.component.spec.ts @@ -0,0 +1,114 @@ +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { ActivatedRoute, convertToParamMap } from "@angular/router"; +import { MockProxy, mock } from "jest-mock-extended"; +import { of } from "rxjs"; + +import { AuditService } from "@bitwarden/common/abstractions/audit.service"; +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +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 { TableBodyDirective } from "@bitwarden/components/src/table/table.component"; + +import { LooseComponentsModule } from "../../shared"; +import { PipesModule } from "../../vault/individual-vault/pipes/pipes.module"; +// eslint-disable-next-line no-restricted-imports +import { cipherData } from "../reports/pages/reports-ciphers.mock"; + +import { PasswordHealthComponent } from "./password-health.component"; + +describe("PasswordHealthComponent", () => { + let component: PasswordHealthComponent; + let fixture: ComponentFixture; + let passwordStrengthService: MockProxy; + let organizationService: MockProxy; + let cipherServiceMock: MockProxy; + let auditServiceMock: MockProxy; + const activeRouteParams = convertToParamMap({ organizationId: "orgId" }); + + beforeEach(async () => { + passwordStrengthService = mock(); + auditServiceMock = mock(); + organizationService = mock({ + get: jest.fn().mockResolvedValue({ id: "orgId" } as Organization), + }); + cipherServiceMock = mock({ + getAllFromApiForOrganization: jest.fn().mockResolvedValue(cipherData), + }); + + await TestBed.configureTestingModule({ + imports: [PasswordHealthComponent, PipesModule, TableModule, LooseComponentsModule], + declarations: [TableBodyDirective], + providers: [ + { provide: CipherService, useValue: cipherServiceMock }, + { provide: PasswordStrengthServiceAbstraction, useValue: passwordStrengthService }, + { provide: OrganizationService, useValue: organizationService }, + { provide: I18nService, useValue: mock() }, + { provide: AuditService, useValue: auditServiceMock }, + { + provide: ActivatedRoute, + useValue: { + paramMap: of(activeRouteParams), + url: of([]), + }, + }, + ], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(PasswordHealthComponent); + component = fixture.componentInstance; + + fixture.detectChanges(); + }); + + it("should initialize component", () => { + expect(component).toBeTruthy(); + }); + + it("should populate reportCiphers with ciphers that have password issues", async () => { + passwordStrengthService.getPasswordStrength.mockReturnValue({ score: 1 } as any); + + auditServiceMock.passwordLeaked.mockResolvedValue(5); + + await component.setCiphers(); + + const cipherIds = component.reportCiphers.map((c) => c.id); + + expect(cipherIds).toEqual([ + "cbea34a8-bde4-46ad-9d19-b05001228ab1", + "cbea34a8-bde4-46ad-9d19-b05001228ab2", + "cbea34a8-bde4-46ad-9d19-b05001228cd3", + ]); + expect(component.reportCiphers.length).toEqual(3); + }); + + it("should correctly populate passwordStrengthMap", async () => { + passwordStrengthService.getPasswordStrength.mockImplementation((password) => { + let score = 0; + if (password === "123") { + score = 1; + } else { + score = 4; + } + return { score } as any; + }); + + auditServiceMock.passwordLeaked.mockResolvedValue(0); + + await component.setCiphers(); + + expect(component.passwordStrengthMap.size).toBeGreaterThan(0); + expect(component.passwordStrengthMap.get("cbea34a8-bde4-46ad-9d19-b05001228ab2")).toEqual([ + "veryWeak", + "danger", + ]); + expect(component.passwordStrengthMap.get("cbea34a8-bde4-46ad-9d19-b05001228cd3")).toEqual([ + "veryWeak", + "danger", + ]); + }); +}); diff --git a/apps/web/src/app/tools/access-intelligence/password-health.component.ts b/apps/web/src/app/tools/access-intelligence/password-health.component.ts new file mode 100644 index 00000000000..6e8e62c50db --- /dev/null +++ b/apps/web/src/app/tools/access-intelligence/password-health.component.ts @@ -0,0 +1,229 @@ +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 { from, map, switchMap, tap } from "rxjs"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { AuditService } from "@bitwarden/common/abstractions/audit.service"; +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; +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"; +import { + BadgeModule, + BadgeVariant, + ContainerComponent, + TableDataSource, + TableModule, +} from "@bitwarden/components"; + +// eslint-disable-next-line no-restricted-imports +import { HeaderModule } from "../../layouts/header/header.module"; +// eslint-disable-next-line no-restricted-imports +import { OrganizationBadgeModule } from "../../vault/individual-vault/organization-badge/organization-badge.module"; +// eslint-disable-next-line no-restricted-imports +import { PipesModule } from "../../vault/individual-vault/pipes/pipes.module"; + +@Component({ + standalone: true, + selector: "tools-password-health", + templateUrl: "password-health.component.html", + imports: [ + BadgeModule, + OrganizationBadgeModule, + CommonModule, + ContainerComponent, + PipesModule, + JslibModule, + HeaderModule, + TableModule, + ], +}) +export class PasswordHealthComponent implements OnInit { + passwordStrengthMap = new Map(); + + weakPasswordCiphers: CipherView[] = []; + + passwordUseMap = new Map(); + + exposedPasswordMap = new Map(); + + dataSource = new TableDataSource(); + + reportCiphers: CipherView[] = []; + reportCipherIds: string[] = []; + + organization: Organization; + + loading = true; + + private destroyRef = inject(DestroyRef); + + constructor( + protected cipherService: CipherService, + protected passwordStrengthService: PasswordStrengthServiceAbstraction, + protected organizationService: OrganizationService, + protected auditService: AuditService, + protected i18nService: I18nService, + protected activatedRoute: ActivatedRoute, + ) {} + + ngOnInit() { + this.activatedRoute.paramMap + .pipe( + takeUntilDestroyed(this.destroyRef), + map((params) => params.get("organizationId")), + switchMap((organizationId) => { + return from(this.organizationService.get(organizationId)); + }), + tap((organization) => { + this.organization = organization; + }), + switchMap(() => from(this.setCiphers())), + ) + .subscribe(); + } + + async setCiphers() { + const allCiphers = await this.cipherService.getAllFromApiForOrganization(this.organization.id); + allCiphers.forEach(async (cipher) => { + this.findWeakPassword(cipher); + this.findReusedPassword(cipher); + await this.findExposedPassword(cipher); + }); + this.dataSource.data = this.reportCiphers; + this.loading = false; + + // const reportIssues = allCiphers.map((c) => { + // if (this.passwordStrengthMap.has(c.id)) { + // return c; + // } + + // if (this.passwordUseMap.has(c.id)) { + // return c; + // } + + // if (this.exposedPasswordMap.has(c.id)) { + // return c; + // } + // }); + } + + protected checkForExistingCipher(ciph: CipherView) { + if (!this.reportCipherIds.includes(ciph.id)) { + this.reportCipherIds.push(ciph.id); + this.reportCiphers.push(ciph); + } + } + + protected async findExposedPassword(cipher: CipherView) { + const { type, login, isDeleted, edit, viewPassword, id } = cipher; + if ( + type !== CipherType.Login || + login.password == null || + login.password === "" || + isDeleted || + (!this.organization && !edit) || + !viewPassword + ) { + return; + } + + const exposedCount = await this.auditService.passwordLeaked(login.password); + if (exposedCount > 0) { + this.exposedPasswordMap.set(id, exposedCount); + this.checkForExistingCipher(cipher); + } + } + + protected findReusedPassword(cipher: CipherView) { + const { type, login, isDeleted, edit, viewPassword } = cipher; + if ( + type !== CipherType.Login || + login.password == null || + login.password === "" || + isDeleted || + (!this.organization && !edit) || + !viewPassword + ) { + return; + } + + if (this.passwordUseMap.has(login.password)) { + this.passwordUseMap.set(login.password, this.passwordUseMap.get(login.password) || 0 + 1); + } else { + this.passwordUseMap.set(login.password, 1); + } + + this.checkForExistingCipher(cipher); + } + + protected findWeakPassword(cipher: CipherView): void { + const { type, login, isDeleted, edit, viewPassword } = cipher; + if ( + type !== CipherType.Login || + login.password == null || + login.password === "" || + isDeleted || + (!this.organization && !edit) || + !viewPassword + ) { + return; + } + + const hasUserName = this.isUserNameNotEmpty(cipher); + let userInput: string[] = []; + if (hasUserName) { + const atPosition = login.username.indexOf("@"); + if (atPosition > -1) { + userInput = userInput + .concat( + login.username + .substring(0, atPosition) + .trim() + .toLowerCase() + .split(/[^A-Za-z0-9]/), + ) + .filter((i) => i.length >= 3); + } else { + userInput = login.username + .trim() + .toLowerCase() + .split(/[^A-Za-z0-9]/) + .filter((i) => i.length >= 3); + } + } + const { score } = this.passwordStrengthService.getPasswordStrength( + login.password, + null, + userInput.length > 0 ? userInput : null, + ); + + if (score != null && score <= 2) { + this.passwordStrengthMap.set(cipher.id, this.scoreKey(score)); + this.checkForExistingCipher(cipher); + } + } + + private isUserNameNotEmpty(c: CipherView): boolean { + return !Utils.isNullOrWhitespace(c.login.username); + } + + private scoreKey(score: number): [string, BadgeVariant] { + switch (score) { + case 4: + return ["strong", "success"]; + case 3: + return ["good", "primary"]; + case 2: + return ["weak", "warning"]; + default: + return ["veryWeak", "danger"]; + } + } +} From 742a8a33ddb27f81e503f466abafc9405d39281a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 16 Oct 2024 13:12:12 -0400 Subject: [PATCH 02/24] [deps] Autofill: Update tldts to v6.1.52 (#11579) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- apps/cli/package.json | 2 +- package-lock.json | 18 +++++++++--------- package.json | 2 +- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/apps/cli/package.json b/apps/cli/package.json index a609224dcb5..c5aeb306230 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -80,7 +80,7 @@ "papaparse": "5.4.1", "proper-lockfile": "4.1.2", "rxjs": "7.8.1", - "tldts": "6.1.51", + "tldts": "6.1.52", "zxcvbn": "4.4.2" } } diff --git a/package-lock.json b/package-lock.json index f16c0c43947..3a0b189e282 100644 --- a/package-lock.json +++ b/package-lock.json @@ -68,7 +68,7 @@ "qrious": "4.0.2", "rxjs": "7.8.1", "tabbable": "6.2.0", - "tldts": "6.1.51", + "tldts": "6.1.52", "utf-8-validate": "6.0.4", "zone.js": "0.13.3", "zxcvbn": "4.4.2" @@ -225,7 +225,7 @@ "papaparse": "5.4.1", "proper-lockfile": "4.1.2", "rxjs": "7.8.1", - "tldts": "6.1.51", + "tldts": "6.1.52", "zxcvbn": "4.4.2" }, "bin": { @@ -36519,21 +36519,21 @@ } }, "node_modules/tldts": { - "version": "6.1.51", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.51.tgz", - "integrity": "sha512-33lfQoL0JsDogIbZ8fgRyvv77GnRtwkNE/MOKocwUgPO1WrSfsq7+vQRKxRQZai5zd+zg97Iv9fpFQSzHyWdLA==", + "version": "6.1.52", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.52.tgz", + "integrity": "sha512-fgrDJXDjbAverY6XnIt0lNfv8A0cf7maTEaZxNykLGsLG7XP+5xhjBTrt/ieAsFjAlZ+G5nmXomLcZDkxXnDzw==", "license": "MIT", "dependencies": { - "tldts-core": "^6.1.51" + "tldts-core": "^6.1.52" }, "bin": { "tldts": "bin/cli.js" } }, "node_modules/tldts-core": { - "version": "6.1.51", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.51.tgz", - "integrity": "sha512-bu9oCYYWC1iRjx+3UnAjqCsfrWNZV1ghNQf49b3w5xE8J/tNShHTzp5syWJfwGH+pxUgTTLUnzHnfuydW7wmbg==", + "version": "6.1.52", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.52.tgz", + "integrity": "sha512-j4OxQI5rc1Ve/4m/9o2WhWSC4jGc4uVbCINdOEJRAraCi0YqTqgMcxUx7DbmuP0G3PCixoof/RZB0Q5Kh9tagw==", "license": "MIT" }, "node_modules/tmp": { diff --git a/package.json b/package.json index d0bc09412bb..86224e7277e 100644 --- a/package.json +++ b/package.json @@ -202,7 +202,7 @@ "qrious": "4.0.2", "rxjs": "7.8.1", "tabbable": "6.2.0", - "tldts": "6.1.51", + "tldts": "6.1.52", "utf-8-validate": "6.0.4", "zone.js": "0.13.3", "zxcvbn": "4.4.2" From 80789465cb0b9646a89a484993a9ed9a1ef43899 Mon Sep 17 00:00:00 2001 From: Shane Melton Date: Wed, 16 Oct 2024 10:34:27 -0700 Subject: [PATCH 03/24] Fix vault item collection storybook (#11582) --- .../src/app/vault/components/vault-items/vault-items.stories.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/web/src/app/vault/components/vault-items/vault-items.stories.ts b/apps/web/src/app/vault/components/vault-items/vault-items.stories.ts index 96089d2b156..f74b73b1030 100644 --- a/apps/web/src/app/vault/components/vault-items/vault-items.stories.ts +++ b/apps/web/src/app/vault/components/vault-items/vault-items.stories.ts @@ -288,6 +288,7 @@ function createCollectionView(i: number): CollectionAdminView { view.id = `collection-${i}`; view.name = `Collection ${i}`; view.organizationId = organization?.id; + view.manage = true; if (group !== undefined) { view.groups = [ From 6e370477762134a1b43ca730f8f0b40a3730d891 Mon Sep 17 00:00:00 2001 From: Jordan Aasen <166539328+jaasen-livefront@users.noreply.github.com> Date: Wed, 16 Oct 2024 12:11:43 -0700 Subject: [PATCH 04/24] account for possible null value (#11589) --- .../send-form/components/options/send-options.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/tools/send/send-ui/src/send-form/components/options/send-options.component.html b/libs/tools/send/send-ui/src/send-form/components/options/send-options.component.html index bcda8b57107..4da3466f708 100644 --- a/libs/tools/send/send-ui/src/send-form/components/options/send-options.component.html +++ b/libs/tools/send/send-ui/src/send-form/components/options/send-options.component.html @@ -43,7 +43,7 @@ > {{ "sendPasswordDescV2" | i18n }} - + Date: Wed, 16 Oct 2024 15:17:23 -0400 Subject: [PATCH 05/24] [PM-11613] Refactor personal-ownership.component to use reactive form (#11440) * Refactor personal-ownership.component to use reactive form * Refactor personal-ownership policy component to use base component form control --- .../policies/personal-ownership.component.html | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/apps/web/src/app/admin-console/organizations/policies/personal-ownership.component.html b/apps/web/src/app/admin-console/organizations/policies/personal-ownership.component.html index 85fb04730d4..2b6c86b1fdc 100644 --- a/apps/web/src/app/admin-console/organizations/policies/personal-ownership.component.html +++ b/apps/web/src/app/admin-console/organizations/policies/personal-ownership.component.html @@ -2,15 +2,7 @@ {{ "personalOwnershipExemption" | i18n }} -
-
- - -
-
+ + + {{ "turnOn" | i18n }} + From 0f525fa9bc356b16052e0c79171d28b3499a40a0 Mon Sep 17 00:00:00 2001 From: Todd Martin <106564991+trmartin4@users.noreply.github.com> Date: Wed, 16 Oct 2024 16:05:11 -0400 Subject: [PATCH 06/24] [PM-11778] Removed super syntax and replaced with this (#11511) --- apps/browser/src/auth/popup/hint.component.ts | 2 +- apps/browser/src/auth/popup/lock.component.ts | 2 +- .../auth/popup/login-via-auth-request.component.ts | 2 +- apps/browser/src/auth/popup/login.component.ts | 4 ++-- apps/browser/src/auth/popup/sso.component.ts | 6 +++--- .../src/auth/popup/two-factor-auth.component.ts | 4 ++-- apps/browser/src/auth/popup/two-factor.component.ts | 12 ++++++------ .../auth/login/login-via-auth-request.component.ts | 2 +- apps/desktop/src/auth/login/login.component.ts | 2 +- apps/desktop/src/auth/sso.component.ts | 4 ++-- apps/desktop/src/auth/two-factor.component.ts | 4 ++-- .../auth/register-form/register-form.component.ts | 2 +- 12 files changed, 23 insertions(+), 23 deletions(-) diff --git a/apps/browser/src/auth/popup/hint.component.ts b/apps/browser/src/auth/popup/hint.component.ts index bc1f68f4c43..e97236fe6a8 100644 --- a/apps/browser/src/auth/popup/hint.component.ts +++ b/apps/browser/src/auth/popup/hint.component.ts @@ -34,7 +34,7 @@ export class HintComponent extends BaseHintComponent { toastService, ); - super.onSuccessfulSubmit = async () => { + this.onSuccessfulSubmit = 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 this.router.navigate([this.successRoute]); diff --git a/apps/browser/src/auth/popup/lock.component.ts b/apps/browser/src/auth/popup/lock.component.ts index 96bda7012d1..75fcfc58f6a 100644 --- a/apps/browser/src/auth/popup/lock.component.ts +++ b/apps/browser/src/auth/popup/lock.component.ts @@ -105,7 +105,7 @@ export class LockComponent extends BaseLockComponent implements OnInit { this.successRoute = "/tabs/current"; this.isInitialLockScreen = (window as any).previousPopupUrl == null; - super.onSuccessfulSubmit = async () => { + 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. diff --git a/apps/browser/src/auth/popup/login-via-auth-request.component.ts b/apps/browser/src/auth/popup/login-via-auth-request.component.ts index 53f29badee6..33ec2acf387 100644 --- a/apps/browser/src/auth/popup/login-via-auth-request.component.ts +++ b/apps/browser/src/auth/popup/login-via-auth-request.component.ts @@ -74,7 +74,7 @@ export class LoginViaAuthRequestComponent extends BaseLoginWithDeviceComponent { loginStrategyService, toastService, ); - super.onSuccessfulLogin = async () => { + this.onSuccessfulLogin = async () => { await syncService.fullSync(true); }; } diff --git a/apps/browser/src/auth/popup/login.component.ts b/apps/browser/src/auth/popup/login.component.ts index ea72fb61f5f..fd4d9bc547a 100644 --- a/apps/browser/src/auth/popup/login.component.ts +++ b/apps/browser/src/auth/popup/login.component.ts @@ -78,10 +78,10 @@ export class LoginComponent extends BaseLoginComponent implements OnInit { registerRouteService, toastService, ); - super.onSuccessfulLogin = async () => { + this.onSuccessfulLogin = async () => { await syncService.fullSync(true); }; - super.successRoute = "/tabs/vault"; + this.successRoute = "/tabs/vault"; this.showPasswordless = flagEnabled("showPasswordless"); } diff --git a/apps/browser/src/auth/popup/sso.component.ts b/apps/browser/src/auth/popup/sso.component.ts index 42222c42b97..988563c2fe6 100644 --- a/apps/browser/src/auth/popup/sso.component.ts +++ b/apps/browser/src/auth/popup/sso.component.ts @@ -79,7 +79,7 @@ export class SsoComponent extends BaseSsoComponent { }); this.clientId = "browser"; - super.onSuccessfulLogin = async () => { + this.onSuccessfulLogin = 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 syncService.fullSync(true); @@ -92,13 +92,13 @@ export class SsoComponent extends BaseSsoComponent { this.win.close(); }; - super.onSuccessfulLoginTde = async () => { + this.onSuccessfulLoginTde = 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 syncService.fullSync(true); }; - super.onSuccessfulLoginTdeNavigate = async () => { + this.onSuccessfulLoginTdeNavigate = async () => { this.win.close(); }; } diff --git a/apps/browser/src/auth/popup/two-factor-auth.component.ts b/apps/browser/src/auth/popup/two-factor-auth.component.ts index 27c95321100..9e755746e6f 100644 --- a/apps/browser/src/auth/popup/two-factor-auth.component.ts +++ b/apps/browser/src/auth/popup/two-factor-auth.component.ts @@ -118,7 +118,7 @@ export class TwoFactorAuthComponent win, toastService, ); - super.onSuccessfulLoginTdeNavigate = async () => { + this.onSuccessfulLoginTdeNavigate = async () => { this.win.close(); }; this.onSuccessfulLoginNavigate = this.goAfterLogIn; @@ -131,7 +131,7 @@ export class TwoFactorAuthComponent // WebAuthn fallback response this.selectedProviderType = TwoFactorProviderType.WebAuthn; this.token = this.route.snapshot.paramMap.get("webAuthnResponse"); - super.onSuccessfulLogin = async () => { + this.onSuccessfulLogin = 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 this.syncService.fullSync(true); diff --git a/apps/browser/src/auth/popup/two-factor.component.ts b/apps/browser/src/auth/popup/two-factor.component.ts index e9167a5087a..27c4604be91 100644 --- a/apps/browser/src/auth/popup/two-factor.component.ts +++ b/apps/browser/src/auth/popup/two-factor.component.ts @@ -87,23 +87,23 @@ export class TwoFactorComponent extends BaseTwoFactorComponent implements OnInit accountService, toastService, ); - super.onSuccessfulLogin = async () => { + this.onSuccessfulLogin = 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 syncService.fullSync(true); }; - super.onSuccessfulLoginTde = async () => { + this.onSuccessfulLoginTde = 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 syncService.fullSync(true); }; - super.onSuccessfulLoginTdeNavigate = async () => { + this.onSuccessfulLoginTdeNavigate = async () => { this.win.close(); }; - super.successRoute = "/tabs/vault"; + this.successRoute = "/tabs/vault"; // FIXME: Chromium 110 has broken WebAuthn support in extensions via an iframe this.webAuthnNewTab = true; } @@ -113,7 +113,7 @@ export class TwoFactorComponent extends BaseTwoFactorComponent implements OnInit // WebAuthn fallback response this.selectedProviderType = TwoFactorProviderType.WebAuthn; this.token = this.route.snapshot.paramMap.get("webAuthnResponse"); - super.onSuccessfulLogin = async () => { + this.onSuccessfulLogin = 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 this.syncService.fullSync(true); @@ -155,7 +155,7 @@ export class TwoFactorComponent extends BaseTwoFactorComponent implements OnInit // eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe this.route.queryParams.pipe(first()).subscribe(async (qParams) => { if (qParams.sso === "true") { - super.onSuccessfulLogin = async () => { + this.onSuccessfulLogin = async () => { // This is not awaited so we don't pause the application while the sync is happening. // This call is executed by the service that lives in the background script so it will continue // the sync even if this tab closes. diff --git a/apps/desktop/src/auth/login/login-via-auth-request.component.ts b/apps/desktop/src/auth/login/login-via-auth-request.component.ts index c0a6a51b907..12be2f01c08 100644 --- a/apps/desktop/src/auth/login/login-via-auth-request.component.ts +++ b/apps/desktop/src/auth/login/login-via-auth-request.component.ts @@ -83,7 +83,7 @@ export class LoginViaAuthRequestComponent extends BaseLoginWithDeviceComponent { toastService, ); - super.onSuccessfulLogin = () => { + this.onSuccessfulLogin = () => { return syncService.fullSync(true); }; } diff --git a/apps/desktop/src/auth/login/login.component.ts b/apps/desktop/src/auth/login/login.component.ts index b43e5bc84f0..6ba143421ca 100644 --- a/apps/desktop/src/auth/login/login.component.ts +++ b/apps/desktop/src/auth/login/login.component.ts @@ -99,7 +99,7 @@ export class LoginComponent extends BaseLoginComponent implements OnInit, OnDest registerRouteService, toastService, ); - super.onSuccessfulLogin = () => { + this.onSuccessfulLogin = () => { return syncService.fullSync(true); }; } diff --git a/apps/desktop/src/auth/sso.component.ts b/apps/desktop/src/auth/sso.component.ts index 6821a548945..760eef14e80 100644 --- a/apps/desktop/src/auth/sso.component.ts +++ b/apps/desktop/src/auth/sso.component.ts @@ -65,13 +65,13 @@ export class SsoComponent extends BaseSsoComponent { accountService, toastService, ); - super.onSuccessfulLogin = async () => { + this.onSuccessfulLogin = 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 syncService.fullSync(true); }; - super.onSuccessfulLoginTde = async () => { + this.onSuccessfulLoginTde = 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 syncService.fullSync(true); diff --git a/apps/desktop/src/auth/two-factor.component.ts b/apps/desktop/src/auth/two-factor.component.ts index d2c5efe929f..0050ec65608 100644 --- a/apps/desktop/src/auth/two-factor.component.ts +++ b/apps/desktop/src/auth/two-factor.component.ts @@ -89,13 +89,13 @@ export class TwoFactorComponent extends BaseTwoFactorComponent implements OnDest accountService, toastService, ); - super.onSuccessfulLogin = async () => { + this.onSuccessfulLogin = 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 syncService.fullSync(true); }; - super.onSuccessfulLoginTde = async () => { + this.onSuccessfulLoginTde = 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 syncService.fullSync(true); diff --git a/apps/web/src/app/auth/register-form/register-form.component.ts b/apps/web/src/app/auth/register-form/register-form.component.ts index bf4a3e8203f..9982af2ab5d 100644 --- a/apps/web/src/app/auth/register-form/register-form.component.ts +++ b/apps/web/src/app/auth/register-form/register-form.component.ts @@ -71,7 +71,7 @@ export class RegisterFormComponent extends BaseRegisterComponent implements OnIn dialogService, toastService, ); - super.modifyRegisterRequest = async (request: RegisterRequest) => { + this.modifyRegisterRequest = async (request: RegisterRequest) => { // Org invites are deep linked. Non-existent accounts are redirected to the register page. // Org user id and token are included here only for validation and two factor purposes. const orgInvite = await acceptOrgInviteService.getOrganizationInvite(); From 80d0f7e385ddfadbe8c18b99f3327182b8f21715 Mon Sep 17 00:00:00 2001 From: Cesar Gonzalez Date: Wed, 16 Oct 2024 15:26:16 -0500 Subject: [PATCH 07/24] [PM-13768] Create account fields are no longer showing the inline menu when identities are turned off (#11592) --- .../src/autofill/services/autofill-overlay-content.service.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/browser/src/autofill/services/autofill-overlay-content.service.ts b/apps/browser/src/autofill/services/autofill-overlay-content.service.ts index 1f0a38ad806..4109662fd66 100644 --- a/apps/browser/src/autofill/services/autofill-overlay-content.service.ts +++ b/apps/browser/src/autofill/services/autofill-overlay-content.service.ts @@ -1158,7 +1158,6 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ } if ( - this.showInlineMenuIdentities && this.inlineMenuFieldQualificationService.isFieldForAccountCreationForm( autofillFieldData, pageDetails, From 84d592a08036839ffee02ace97fa06113e1c47a1 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 16 Oct 2024 22:34:59 +0200 Subject: [PATCH 08/24] [deps] Vault: Update @koa/router to v13 (#10602) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: SmithThe4th --- apps/cli/package.json | 2 +- package-lock.json | 18 ++++++++---------- package.json | 2 +- 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/apps/cli/package.json b/apps/cli/package.json index c5aeb306230..a45e88acfa2 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -57,7 +57,7 @@ }, "dependencies": { "@koa/multer": "3.0.2", - "@koa/router": "12.0.1", + "@koa/router": "13.1.0", "argon2": "0.40.1", "big-integer": "1.6.52", "browser-hrtime": "1.1.8", diff --git a/package-lock.json b/package-lock.json index 3a0b189e282..753d8975573 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,7 +27,7 @@ "@bitwarden/sdk-internal": "0.1.3", "@electron/fuses": "1.8.0", "@koa/multer": "3.0.2", - "@koa/router": "12.0.1", + "@koa/router": "13.1.0", "@microsoft/signalr": "8.0.7", "@microsoft/signalr-protocol-msgpack": "8.0.7", "@ng-select/ng-select": "11.2.0", @@ -202,7 +202,7 @@ "license": "SEE LICENSE IN LICENSE.txt", "dependencies": { "@koa/multer": "3.0.2", - "@koa/router": "12.0.1", + "@koa/router": "13.1.0", "argon2": "0.40.1", "big-integer": "1.6.52", "browser-hrtime": "1.1.8", @@ -7021,20 +7021,17 @@ } }, "node_modules/@koa/router": { - "version": "12.0.1", - "resolved": "https://registry.npmjs.org/@koa/router/-/router-12.0.1.tgz", - "integrity": "sha512-ribfPYfHb+Uw3b27Eiw6NPqjhIhTpVFzEWLwyc/1Xp+DCdwRRyIlAUODX+9bPARF6aQtUu1+/PHzdNvRzcs/+Q==", - "deprecated": "Use v12.0.2 or higher to fix the vulnerability issue", + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/@koa/router/-/router-13.1.0.tgz", + "integrity": "sha512-mNVu1nvkpSd8Q8gMebGbCkDWJ51ODetrFvLKYusej+V0ByD4btqHYnPIzTBLXnQMVUlm/oxVwqmWBY3zQfZilw==", "license": "MIT", "dependencies": { - "debug": "^4.3.4", "http-errors": "^2.0.0", "koa-compose": "^4.1.0", - "methods": "^1.1.2", - "path-to-regexp": "^6.2.1" + "path-to-regexp": "^6.3.0" }, "engines": { - "node": ">= 12" + "node": ">= 18" } }, "node_modules/@leichtgewicht/ip-codec": { @@ -27277,6 +27274,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" diff --git a/package.json b/package.json index 86224e7277e..04b33e77099 100644 --- a/package.json +++ b/package.json @@ -161,7 +161,7 @@ "@bitwarden/sdk-internal": "0.1.3", "@electron/fuses": "1.8.0", "@koa/multer": "3.0.2", - "@koa/router": "12.0.1", + "@koa/router": "13.1.0", "@microsoft/signalr": "8.0.7", "@microsoft/signalr-protocol-msgpack": "8.0.7", "@ng-select/ng-select": "11.2.0", From e256bde1deb33fbb589915d6b1e532ebb34cf58e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 16 Oct 2024 22:35:45 +0200 Subject: [PATCH 09/24] [deps] Vault: Update koa to v2.15.3 (#10567) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: SmithThe4th --- apps/cli/package.json | 2 +- package-lock.json | 18 +++++++++--------- package.json | 4 ++-- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/apps/cli/package.json b/apps/cli/package.json index a45e88acfa2..55bcee689d0 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -68,7 +68,7 @@ "inquirer": "8.2.6", "jsdom": "25.0.1", "jszip": "3.10.1", - "koa": "2.15.0", + "koa": "2.15.3", "koa-bodyparser": "4.4.1", "koa-json": "2.0.2", "lowdb": "1.0.0", diff --git a/package-lock.json b/package-lock.json index 753d8975573..07210a6013a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -47,7 +47,7 @@ "jquery": "3.7.1", "jsdom": "25.0.1", "jszip": "3.10.1", - "koa": "2.15.0", + "koa": "2.15.3", "koa-bodyparser": "4.4.1", "koa-json": "2.0.2", "lowdb": "1.0.0", @@ -103,7 +103,7 @@ "@types/jest": "29.5.12", "@types/jquery": "3.5.30", "@types/jsdom": "21.1.7", - "@types/koa": "2.14.0", + "@types/koa": "2.15.0", "@types/koa__multer": "2.0.7", "@types/koa__router": "12.0.4", "@types/koa-bodyparser": "4.3.7", @@ -213,7 +213,7 @@ "inquirer": "8.2.6", "jsdom": "25.0.1", "jszip": "3.10.1", - "koa": "2.15.0", + "koa": "2.15.3", "koa-bodyparser": "4.4.1", "koa-json": "2.0.2", "lowdb": "1.0.0", @@ -9521,9 +9521,9 @@ } }, "node_modules/@types/koa": { - "version": "2.14.0", - "resolved": "https://registry.npmjs.org/@types/koa/-/koa-2.14.0.tgz", - "integrity": "sha512-DTDUyznHGNHAl+wd1n0z1jxNajduyTh8R53xoewuerdBzGo6Ogj6F2299BFtrexJw4NtgjsI5SMPCmV9gZwGXA==", + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/@types/koa/-/koa-2.15.0.tgz", + "integrity": "sha512-7QFsywoE5URbuVnG3loe03QXuGajrnotr3gQkXcEBShORai23MePfFYdhz90FEtBBpkyIYQbVD+evKtloCgX3g==", "dev": true, "license": "MIT", "dependencies": { @@ -25240,9 +25240,9 @@ } }, "node_modules/koa": { - "version": "2.15.0", - "resolved": "https://registry.npmjs.org/koa/-/koa-2.15.0.tgz", - "integrity": "sha512-KEL/vU1knsoUvfP4MC4/GthpQrY/p6dzwaaGI6Rt4NQuFqkw3qrvsdYF5pz3wOfi7IGTvMPHC9aZIcUKYFNxsw==", + "version": "2.15.3", + "resolved": "https://registry.npmjs.org/koa/-/koa-2.15.3.tgz", + "integrity": "sha512-j/8tY9j5t+GVMLeioLaxweJiKUayFhlGqNTzf2ZGwL0ZCQijd2RLHK0SLW5Tsko8YyyqCZC2cojIb0/s62qTAg==", "license": "MIT", "dependencies": { "accepts": "^1.3.5", diff --git a/package.json b/package.json index 04b33e77099..eb871be7766 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,7 @@ "@types/jest": "29.5.12", "@types/jquery": "3.5.30", "@types/jsdom": "21.1.7", - "@types/koa": "2.14.0", + "@types/koa": "2.15.0", "@types/koa__multer": "2.0.7", "@types/koa__router": "12.0.4", "@types/koa-bodyparser": "4.3.7", @@ -181,7 +181,7 @@ "jquery": "3.7.1", "jsdom": "25.0.1", "jszip": "3.10.1", - "koa": "2.15.0", + "koa": "2.15.3", "koa-bodyparser": "4.4.1", "koa-json": "2.0.2", "lowdb": "1.0.0", From 4b67cd24b451255981218cb5e9436b23fbbda5e7 Mon Sep 17 00:00:00 2001 From: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com> Date: Wed, 16 Oct 2024 18:28:27 -0400 Subject: [PATCH 10/24] Auth/PM-8112 - UI refresh - Registration Components (#11353) * PM-8112 - Update classes of existing registration icons * PM-8112 - Add new icons * PM-8112 - Export icons from libs/auth * PM-8112 - RegistrationStart - Add new user icon as page icon * PM-8112 - Replace RegistrationCheckEmailIcon with new icon so it displays properly * PM-8112 - RegistrationFinish - Add new icon across clients * PM-8112 - Registration start comp - update page icon and page title on state change to match figma * PM-8112 - RegistrationFinish - adding most of framework for changing page title & subtitle when an org invite is in state. * PM-8112 - Add joinOrganizationName to all clients translations * PM-8112 - RegistrationFinish - Remove default page title & subtitle and let onInit logic figure out what to set based on flows. * PM-8112 - RegistrationStart - Fix setAnonLayoutWrapperData calls * PM-8112 - RegistrationFinish - simplify qParams init logic to make handling loading and page title and subtitle setting easier. * PM-8112 - Registration Link expired - move icon to page icon out of main content * PM-8112 - RegistrationFinish - Refactor init logic further into distinct flows. * PM-8112 - Fix icons * PM-8112 - Extension AppRoutingModule - move sign up start & finish routes under extension anon layout * PM-8112 - Fix storybook * PM-8112 - Clean up unused prop * PM-8112 - RegistrationLockAltIcon tweaks * PM-8112 - Update icons to have proper styling * PM-8112 - RegistrationUserAddIcon - remove unnecessary svg class * PM-8112 - Fix icons --- apps/browser/src/_locales/en/messages.json | 9 ++ apps/browser/src/popup/app-routing.module.ts | 86 ++++++------ apps/desktop/src/app/app-routing.module.ts | 10 +- apps/desktop/src/locales/en/messages.json | 9 ++ .../web-registration-finish.service.spec.ts | 30 +++++ .../web-registration-finish.service.ts | 9 ++ apps/web/src/app/oss-routing.module.ts | 12 +- apps/web/src/locales/en/messages.json | 9 ++ libs/auth/src/angular/icons/index.ts | 3 + .../icons/registration-check-email.icon.ts | 29 +++-- .../icons/registration-expired-link.icon.ts | 21 +-- .../icons/registration-lock-alt.icon.ts | 41 ++++++ .../icons/registration-user-add.icon.ts | 24 ++++ ...efault-registration-finish.service.spec.ts | 8 ++ .../default-registration-finish.service.ts | 4 + .../registration-finish.component.ts | 123 +++++++++++------- .../registration-finish.service.ts | 9 +- .../registration-link-expired.component.html | 2 - .../registration-start.component.html | 13 -- .../registration-start.component.ts | 16 ++- .../registration-start.stories.ts | 10 ++ 21 files changed, 336 insertions(+), 141 deletions(-) create mode 100644 libs/auth/src/angular/icons/registration-lock-alt.icon.ts create mode 100644 libs/auth/src/angular/icons/registration-user-add.icon.ts diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 25386222d4e..290663f4347 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -71,6 +71,15 @@ "joinOrganization": { "message": "Join organization" }, + "joinOrganizationName": { + "message": "Join $ORGANIZATIONNAME$", + "placeholders": { + "organizationName": { + "content": "$1", + "example": "My Org Name" + } + } + }, "finishJoiningThisOrganizationBySettingAMasterPassword": { "message": "Finish joining this organization by setting a master password." }, diff --git a/apps/browser/src/popup/app-routing.module.ts b/apps/browser/src/popup/app-routing.module.ts index 0bf26f8c070..1a95ad74839 100644 --- a/apps/browser/src/popup/app-routing.module.ts +++ b/apps/browser/src/popup/app-routing.module.ts @@ -20,9 +20,11 @@ import { LockV2Component, PasswordHintComponent, RegistrationFinishComponent, + RegistrationLockAltIcon, RegistrationStartComponent, RegistrationStartSecondaryComponent, RegistrationStartSecondaryComponentData, + RegistrationUserAddIcon, SetPasswordJitComponent, UserLockIcon, } from "@bitwarden/auth/angular"; @@ -448,6 +450,47 @@ const routes: Routes = [ path: "", component: ExtensionAnonLayoutWrapperComponent, children: [ + { + path: "signup", + canActivate: [canAccessFeature(FeatureFlag.EmailVerification), unauthGuardFn()], + data: { + state: "signup", + pageIcon: RegistrationUserAddIcon, + pageTitle: { + key: "createAccount", + }, + showBackButton: true, + } satisfies RouteDataProperties & ExtensionAnonLayoutWrapperData, + children: [ + { + path: "", + component: RegistrationStartComponent, + }, + { + path: "", + component: RegistrationStartSecondaryComponent, + outlet: "secondary", + data: { + loginRoute: "/home", + } satisfies RegistrationStartSecondaryComponentData, + }, + ], + }, + { + path: "finish-signup", + canActivate: [canAccessFeature(FeatureFlag.EmailVerification), unauthGuardFn()], + data: { + pageIcon: RegistrationLockAltIcon, + state: "finish-signup", + showBackButton: true, + } satisfies RouteDataProperties & ExtensionAnonLayoutWrapperData, + children: [ + { + path: "", + component: RegistrationFinishComponent, + }, + ], + }, { path: "lockV2", canActivate: [canAccessFeature(FeatureFlag.ExtensionRefresh), lockGuard()], @@ -472,49 +515,6 @@ const routes: Routes = [ path: "", component: AnonLayoutWrapperComponent, children: [ - { - path: "signup", - canActivate: [canAccessFeature(FeatureFlag.EmailVerification), unauthGuardFn()], - data: { - state: "signup", - pageTitle: { - key: "createAccount", - }, - } satisfies RouteDataProperties & AnonLayoutWrapperData, - children: [ - { - path: "", - component: RegistrationStartComponent, - }, - { - path: "", - component: RegistrationStartSecondaryComponent, - outlet: "secondary", - data: { - loginRoute: "/home", - } satisfies RegistrationStartSecondaryComponentData, - }, - ], - }, - { - path: "finish-signup", - canActivate: [canAccessFeature(FeatureFlag.EmailVerification), unauthGuardFn()], - data: { - pageTitle: { - key: "setAStrongPassword", - }, - pageSubtitle: { - key: "finishCreatingYourAccountBySettingAPassword", - }, - state: "finish-signup", - } satisfies RouteDataProperties & AnonLayoutWrapperData, - children: [ - { - path: "", - component: RegistrationFinishComponent, - }, - ], - }, { path: "set-password-jit", canActivate: [canAccessFeature(FeatureFlag.EmailVerification)], diff --git a/apps/desktop/src/app/app-routing.module.ts b/apps/desktop/src/app/app-routing.module.ts index e8ae31e78a8..8b8f62047f0 100644 --- a/apps/desktop/src/app/app-routing.module.ts +++ b/apps/desktop/src/app/app-routing.module.ts @@ -19,9 +19,11 @@ import { LockV2Component, PasswordHintComponent, RegistrationFinishComponent, + RegistrationLockAltIcon, RegistrationStartComponent, RegistrationStartSecondaryComponent, RegistrationStartSecondaryComponentData, + RegistrationUserAddIcon, SetPasswordJitComponent, UserLockIcon, } from "@bitwarden/auth/angular"; @@ -169,6 +171,7 @@ const routes: Routes = [ path: "signup", canActivate: [canAccessFeature(FeatureFlag.EmailVerification), unauthGuardFn()], data: { + pageIcon: RegistrationUserAddIcon, pageTitle: { key: "createAccount", }, @@ -192,12 +195,7 @@ const routes: Routes = [ path: "finish-signup", canActivate: [canAccessFeature(FeatureFlag.EmailVerification), unauthGuardFn()], data: { - pageTitle: { - key: "setAStrongPassword", - }, - pageSubtitle: { - key: "finishCreatingYourAccountBySettingAPassword", - }, + pageIcon: RegistrationLockAltIcon, } satisfies AnonLayoutWrapperData, children: [ { diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json index 4647853f715..9924a91fa36 100644 --- a/apps/desktop/src/locales/en/messages.json +++ b/apps/desktop/src/locales/en/messages.json @@ -602,6 +602,15 @@ "joinOrganization": { "message": "Join organization" }, + "joinOrganizationName": { + "message": "Join $ORGANIZATIONNAME$", + "placeholders": { + "organizationName": { + "content": "$1", + "example": "My Org Name" + } + } + }, "finishJoiningThisOrganizationBySettingAMasterPassword": { "message": "Finish joining this organization by setting a master password." }, diff --git a/apps/web/src/app/auth/core/services/registration/web-registration-finish.service.spec.ts b/apps/web/src/app/auth/core/services/registration/web-registration-finish.service.spec.ts index 2faf3f85d10..45eb3c5c0d4 100644 --- a/apps/web/src/app/auth/core/services/registration/web-registration-finish.service.spec.ts +++ b/apps/web/src/app/auth/core/services/registration/web-registration-finish.service.spec.ts @@ -52,6 +52,36 @@ describe("DefaultRegistrationFinishService", () => { expect(service).not.toBeFalsy(); }); + describe("getOrgNameFromOrgInvite()", () => { + let orgInvite: OrganizationInvite | null; + + beforeEach(() => { + orgInvite = new OrganizationInvite(); + orgInvite.organizationId = "organizationId"; + orgInvite.organizationUserId = "organizationUserId"; + orgInvite.token = "orgInviteToken"; + orgInvite.email = "email"; + }); + + it("returns null when the org invite is null", async () => { + acceptOrgInviteService.getOrganizationInvite.mockResolvedValue(null); + + const result = await service.getOrgNameFromOrgInvite(); + + expect(result).toBeNull(); + expect(acceptOrgInviteService.getOrganizationInvite).toHaveBeenCalled(); + }); + + it("returns the organization name from the organization invite when it exists", async () => { + acceptOrgInviteService.getOrganizationInvite.mockResolvedValue(orgInvite); + + const result = await service.getOrgNameFromOrgInvite(); + + expect(result).toEqual(orgInvite.organizationName); + expect(acceptOrgInviteService.getOrganizationInvite).toHaveBeenCalled(); + }); + }); + describe("getMasterPasswordPolicyOptsFromOrgInvite()", () => { let orgInvite: OrganizationInvite | null; diff --git a/apps/web/src/app/auth/core/services/registration/web-registration-finish.service.ts b/apps/web/src/app/auth/core/services/registration/web-registration-finish.service.ts index 5239601bbcd..560196dd195 100644 --- a/apps/web/src/app/auth/core/services/registration/web-registration-finish.service.ts +++ b/apps/web/src/app/auth/core/services/registration/web-registration-finish.service.ts @@ -32,6 +32,15 @@ export class WebRegistrationFinishService super(cryptoService, accountApiService); } + override async getOrgNameFromOrgInvite(): Promise { + const orgInvite = await this.acceptOrgInviteService.getOrganizationInvite(); + if (orgInvite == null) { + return null; + } + + return orgInvite.organizationName; + } + override async getMasterPasswordPolicyOptsFromOrgInvite(): Promise { // If there's a deep linked org invite, use it to get the password policies const orgInvite = await this.acceptOrgInviteService.getOrganizationInvite(); diff --git a/apps/web/src/app/oss-routing.module.ts b/apps/web/src/app/oss-routing.module.ts index fa4da88ce7d..8822800b36d 100644 --- a/apps/web/src/app/oss-routing.module.ts +++ b/apps/web/src/app/oss-routing.module.ts @@ -25,6 +25,9 @@ import { LockV2Component, LockIcon, UserLockIcon, + RegistrationUserAddIcon, + RegistrationLockAltIcon, + RegistrationExpiredLinkIcon, } from "@bitwarden/auth/angular"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; @@ -234,6 +237,7 @@ const routes: Routes = [ path: "signup", canActivate: [canAccessFeature(FeatureFlag.EmailVerification), unauthGuardFn()], data: { + pageIcon: RegistrationUserAddIcon, pageTitle: { key: "createAccount", }, @@ -258,12 +262,7 @@ const routes: Routes = [ path: "finish-signup", canActivate: [canAccessFeature(FeatureFlag.EmailVerification), unauthGuardFn()], data: { - pageTitle: { - key: "setAStrongPassword", - }, - pageSubtitle: { - key: "finishCreatingYourAccountBySettingAPassword", - }, + pageIcon: RegistrationLockAltIcon, titleId: "setAStrongPassword", } satisfies RouteDataProperties & AnonLayoutWrapperData, children: [ @@ -310,6 +309,7 @@ const routes: Routes = [ path: "signup-link-expired", canActivate: [canAccessFeature(FeatureFlag.EmailVerification), unauthGuardFn()], data: { + pageIcon: RegistrationExpiredLinkIcon, pageTitle: { key: "expiredLink", }, diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 96e987a7f64..5125bb1bfe0 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -3781,6 +3781,15 @@ "joinOrganization": { "message": "Join organization" }, + "joinOrganizationName": { + "message": "Join $ORGANIZATIONNAME$", + "placeholders": { + "organizationName": { + "content": "$1", + "example": "My Org Name" + } + } + }, "joinOrganizationDesc": { "message": "You've been invited to join the organization listed above. To accept the invitation, you need to log in or create a new Bitwarden account." }, diff --git a/libs/auth/src/angular/icons/index.ts b/libs/auth/src/angular/icons/index.ts index cfcad992e34..26e668b7841 100644 --- a/libs/auth/src/angular/icons/index.ts +++ b/libs/auth/src/angular/icons/index.ts @@ -3,3 +3,6 @@ export * from "./bitwarden-shield.icon"; export * from "./lock.icon"; export * from "./user-lock.icon"; export * from "./user-verification-biometrics-fingerprint.icon"; +export * from "./registration-user-add.icon"; +export * from "./registration-lock-alt.icon"; +export * from "./registration-expired-link.icon"; diff --git a/libs/auth/src/angular/icons/registration-check-email.icon.ts b/libs/auth/src/angular/icons/registration-check-email.icon.ts index 1d173ff585f..6f7dd6a2d63 100644 --- a/libs/auth/src/angular/icons/registration-check-email.icon.ts +++ b/libs/auth/src/angular/icons/registration-check-email.icon.ts @@ -1,12 +1,23 @@ import { svgIcon } from "@bitwarden/components"; export const RegistrationCheckEmailIcon = svgIcon` - - - - - - - - -`; + + + + + + + + + + + +`; diff --git a/libs/auth/src/angular/icons/registration-expired-link.icon.ts b/libs/auth/src/angular/icons/registration-expired-link.icon.ts index 50bcc53808f..3323c7f0b2b 100644 --- a/libs/auth/src/angular/icons/registration-expired-link.icon.ts +++ b/libs/auth/src/angular/icons/registration-expired-link.icon.ts @@ -1,20 +1,9 @@ import { svgIcon } from "@bitwarden/components"; export const RegistrationExpiredLinkIcon = svgIcon` - - - - - - - - - - + + + `; diff --git a/libs/auth/src/angular/icons/registration-lock-alt.icon.ts b/libs/auth/src/angular/icons/registration-lock-alt.icon.ts new file mode 100644 index 00000000000..511f9710dc6 --- /dev/null +++ b/libs/auth/src/angular/icons/registration-lock-alt.icon.ts @@ -0,0 +1,41 @@ +import { svgIcon } from "@bitwarden/components"; + +export const RegistrationLockAltIcon = svgIcon` + + + + + + + + + + + + + +`; diff --git a/libs/auth/src/angular/icons/registration-user-add.icon.ts b/libs/auth/src/angular/icons/registration-user-add.icon.ts new file mode 100644 index 00000000000..69240cd0298 --- /dev/null +++ b/libs/auth/src/angular/icons/registration-user-add.icon.ts @@ -0,0 +1,24 @@ +import { svgIcon } from "@bitwarden/components"; + +export const RegistrationUserAddIcon = svgIcon` + + + + + + + + + + +`; diff --git a/libs/auth/src/angular/registration/registration-finish/default-registration-finish.service.spec.ts b/libs/auth/src/angular/registration/registration-finish/default-registration-finish.service.spec.ts index 5417d617a9a..fe6b9b2c7dc 100644 --- a/libs/auth/src/angular/registration/registration-finish/default-registration-finish.service.spec.ts +++ b/libs/auth/src/angular/registration/registration-finish/default-registration-finish.service.spec.ts @@ -37,6 +37,14 @@ describe("DefaultRegistrationFinishService", () => { }); }); + describe("getOrgNameFromOrgInvite()", () => { + it("returns null", async () => { + const result = await service.getOrgNameFromOrgInvite(); + + expect(result).toBeNull(); + }); + }); + describe("finishRegistration()", () => { let email: string; let emailVerificationToken: string; diff --git a/libs/auth/src/angular/registration/registration-finish/default-registration-finish.service.ts b/libs/auth/src/angular/registration/registration-finish/default-registration-finish.service.ts index 63b01be9953..6d77c777491 100644 --- a/libs/auth/src/angular/registration/registration-finish/default-registration-finish.service.ts +++ b/libs/auth/src/angular/registration/registration-finish/default-registration-finish.service.ts @@ -15,6 +15,10 @@ export class DefaultRegistrationFinishService implements RegistrationFinishServi protected accountApiService: AccountApiService, ) {} + getOrgNameFromOrgInvite(): Promise { + return null; + } + getMasterPasswordPolicyOptsFromOrgInvite(): Promise { return null; } diff --git a/libs/auth/src/angular/registration/registration-finish/registration-finish.component.ts b/libs/auth/src/angular/registration/registration-finish/registration-finish.component.ts index ef40d95dce9..be9d8abe5b0 100644 --- a/libs/auth/src/angular/registration/registration-finish/registration-finish.component.ts +++ b/libs/auth/src/angular/registration/registration-finish/registration-finish.component.ts @@ -1,7 +1,7 @@ import { CommonModule } from "@angular/common"; import { Component, OnDestroy, OnInit } from "@angular/core"; import { ActivatedRoute, Params, Router, RouterModule } from "@angular/router"; -import { EMPTY, Subject, from, switchMap, takeUntil, tap } from "rxjs"; +import { Subject, firstValueFrom } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options"; @@ -15,6 +15,7 @@ import { ValidationService } from "@bitwarden/common/platform/abstractions/valid import { ToastService } from "@bitwarden/components"; import { LoginStrategyServiceAbstraction, PasswordLoginCredentials } from "../../../common"; +import { AnonLayoutWrapperDataService } from "../../anon-layout/anon-layout-wrapper-data.service"; import { InputPasswordComponent } from "../../input-password/input-password.component"; import { PasswordInputResult } from "../../input-password/password-input-result"; @@ -60,55 +61,72 @@ export class RegistrationFinishComponent implements OnInit, OnDestroy { private accountApiService: AccountApiService, private loginStrategyService: LoginStrategyServiceAbstraction, private logService: LogService, + private anonLayoutWrapperDataService: AnonLayoutWrapperDataService, ) {} async ngOnInit() { - this.listenForQueryParamChanges(); - this.masterPasswordPolicyOptions = - await this.registrationFinishService.getMasterPasswordPolicyOptsFromOrgInvite(); + const qParams = await firstValueFrom(this.activatedRoute.queryParams); + this.handleQueryParams(qParams); + + if ( + qParams.fromEmail && + qParams.fromEmail === "true" && + this.email && + this.emailVerificationToken + ) { + await this.initEmailVerificationFlow(); + } else { + // Org Invite flow OR registration with email verification disabled Flow + const orgInviteFlow = await this.initOrgInviteFlowIfPresent(); + + if (!orgInviteFlow) { + this.initRegistrationWithEmailVerificationDisabledFlow(); + } + } + + this.loading = false; } - private listenForQueryParamChanges() { - this.activatedRoute.queryParams - .pipe( - tap((qParams: Params) => { - if (qParams.email != null && qParams.email.indexOf("@") > -1) { - this.email = qParams.email; - } + private handleQueryParams(qParams: Params) { + if (qParams.email != null && qParams.email.indexOf("@") > -1) { + this.email = qParams.email; + } - if (qParams.token != null) { - this.emailVerificationToken = qParams.token; - } + if (qParams.token != null) { + this.emailVerificationToken = qParams.token; + } - if (qParams.orgSponsoredFreeFamilyPlanToken != null) { - this.orgSponsoredFreeFamilyPlanToken = qParams.orgSponsoredFreeFamilyPlanToken; - } + if (qParams.orgSponsoredFreeFamilyPlanToken != null) { + this.orgSponsoredFreeFamilyPlanToken = qParams.orgSponsoredFreeFamilyPlanToken; + } - if (qParams.acceptEmergencyAccessInviteToken != null && qParams.emergencyAccessId) { - this.acceptEmergencyAccessInviteToken = qParams.acceptEmergencyAccessInviteToken; - this.emergencyAccessId = qParams.emergencyAccessId; - } - }), - switchMap((qParams: Params) => { - if ( - qParams.fromEmail && - qParams.fromEmail === "true" && - this.email && - this.emailVerificationToken - ) { - return from( - this.registerVerificationEmailClicked(this.email, this.emailVerificationToken), - ); - } else { - // org invite flow - this.loading = false; - return EMPTY; - } - }), + if (qParams.acceptEmergencyAccessInviteToken != null && qParams.emergencyAccessId) { + this.acceptEmergencyAccessInviteToken = qParams.acceptEmergencyAccessInviteToken; + this.emergencyAccessId = qParams.emergencyAccessId; + } + } - takeUntil(this.destroy$), - ) - .subscribe(); + private async initOrgInviteFlowIfPresent(): Promise { + this.masterPasswordPolicyOptions = + await this.registrationFinishService.getMasterPasswordPolicyOptsFromOrgInvite(); + + const orgName = await this.registrationFinishService.getOrgNameFromOrgInvite(); + if (orgName) { + // Org invite exists + // Set the page title and subtitle appropriately + this.anonLayoutWrapperDataService.setAnonLayoutWrapperData({ + pageTitle: { + key: "joinOrganizationName", + placeholders: [orgName], + }, + pageSubtitle: { + key: "finishJoiningThisOrganizationBySettingAMasterPassword", + }, + }); + return true; + } + + return false; } async handlePasswordFormSubmit(passwordInputResult: PasswordInputResult) { @@ -162,9 +180,24 @@ export class RegistrationFinishComponent implements OnInit, OnDestroy { this.submitting = false; } + private setDefaultPageTitleAndSubtitle() { + this.anonLayoutWrapperDataService.setAnonLayoutWrapperData({ + pageTitle: { + key: "setAStrongPassword", + }, + pageSubtitle: { + key: "finishCreatingYourAccountBySettingAPassword", + }, + }); + } + + private async initEmailVerificationFlow() { + this.setDefaultPageTitleAndSubtitle(); + await this.registerVerificationEmailClicked(this.email, this.emailVerificationToken); + } + private async registerVerificationEmailClicked(email: string, emailVerificationToken: string) { const request = new RegisterVerificationEmailClickedRequest(email, emailVerificationToken); - try { const result = await this.accountApiService.registerVerificationEmailClicked(request); @@ -174,11 +207,9 @@ export class RegistrationFinishComponent implements OnInit, OnDestroy { message: this.i18nService.t("emailVerifiedV2"), variant: "success", }); - this.loading = false; } } catch (e) { await this.handleRegisterVerificationEmailClickedError(e); - this.loading = false; } } @@ -204,6 +235,10 @@ export class RegistrationFinishComponent implements OnInit, OnDestroy { } } + private initRegistrationWithEmailVerificationDisabledFlow() { + this.setDefaultPageTitleAndSubtitle(); + } + ngOnDestroy(): void { this.destroy$.next(); this.destroy$.complete(); diff --git a/libs/auth/src/angular/registration/registration-finish/registration-finish.service.ts b/libs/auth/src/angular/registration/registration-finish/registration-finish.service.ts index b585aa78ed6..b7abd381084 100644 --- a/libs/auth/src/angular/registration/registration-finish/registration-finish.service.ts +++ b/libs/auth/src/angular/registration/registration-finish/registration-finish.service.ts @@ -3,6 +3,13 @@ import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/mod import { PasswordInputResult } from "../../input-password/password-input-result"; export abstract class RegistrationFinishService { + /** + * Retrieves the organization name from an organization invite if it exists. + * Organization invites can currently only be accepted on the web. + * @returns a promise which resolves to the organization name string or null if no invite exists. + */ + abstract getOrgNameFromOrgInvite(): Promise; + /** * Gets the master password policy options from an organization invite if it exits. * Organization invites can currently only be accepted on the web. @@ -18,7 +25,7 @@ export abstract class RegistrationFinishService { * @param orgSponsoredFreeFamilyPlanToken The optional org sponsored free family plan token. * @param acceptEmergencyAccessInviteToken The optional accept emergency access invite token. * @param emergencyAccessId The optional emergency access id which is required to validate the emergency access invite token. - * Returns a promise which resolves to the captcha bypass token string upon a successful account creation. + * @returns a promise which resolves to the captcha bypass token string upon a successful account creation. */ abstract finishRegistration( email: string, diff --git a/libs/auth/src/angular/registration/registration-link-expired/registration-link-expired.component.html b/libs/auth/src/angular/registration/registration-link-expired/registration-link-expired.component.html index 5aa6866bbe0..77149902310 100644 --- a/libs/auth/src/angular/registration/registration-link-expired/registration-link-expired.component.html +++ b/libs/auth/src/angular/registration/registration-link-expired/registration-link-expired.component.html @@ -1,6 +1,4 @@
- -

- - -

- {{ "checkYourEmail" | i18n }} -

-

{{ "collectionManagement" | i18n }}

{{ "collectionManagementDesc" | i18n }}

@@ -60,12 +64,24 @@ {{ "allowAdminAccessToAllCollectionItemsDesc" | i18n }} - - {{ "limitCollectionCreationDeletionDesc" | i18n }} - - + + + {{ "limitCollectionCreationDesc" | i18n }} + + + + {{ "limitCollectionDeletionDesc" | i18n }} + + + + + + {{ "limitCollectionCreationDeletionDesc" | i18n }} + + + -