From 96d116d643ce825477e9e88a223c246058c0c36e Mon Sep 17 00:00:00 2001 From: rr-bw <102181210+rr-bw@users.noreply.github.com> Date: Fri, 13 Sep 2024 09:16:25 -0700 Subject: [PATCH] [PM-8116] Auth Browser Refresh: Password Hint Component (#10492) * setup component, services, and web HTML * make Web and Browser functional * make desktop functional * update template to solidify common client HTML * simplify template and class * update browser routing * move canActivate to correct location * simplify post submit routing * update routing to use unauthUiRefreshSwap() * constrain AnonLayout title/subtitle width, reduce height on destkop to account for header * reduce height on browser to account for header (otherwise have to scroll to see EnvSelector * resolve email issue when clicking 'cancel' on extension popout * update routing for web * persist email to popout * update web router and anon-layout min-h based on client * change anchor link to button * remove unnecessary formatting changes * add new icon * remove unnecessary call to loginEmailService --- apps/browser/src/_locales/en/messages.json | 12 ++ apps/browser/src/popup/app-routing.module.ts | 46 +++++++- apps/browser/src/popup/app.module.ts | 2 + apps/desktop/src/app/app-routing.module.ts | 48 +++++++- apps/desktop/src/locales/en/messages.json | 12 ++ apps/web/src/app/oss-routing.module.ts | 67 +++++++---- apps/web/src/locales/en/messages.json | 12 ++ .../src/auth/components/login.component.ts | 1 + .../functions/unauth-ui-refresh-route-swap.ts | 2 + .../anon-layout/anon-layout.component.html | 8 +- libs/auth/src/angular/icons/index.ts | 1 + libs/auth/src/angular/icons/user-lock.icon.ts | 22 ++++ libs/auth/src/angular/index.ts | 7 +- .../password-hint.component.html | 40 +++++++ .../password-hint/password-hint.component.ts | 107 ++++++++++++++++++ 15 files changed, 357 insertions(+), 30 deletions(-) create mode 100644 libs/auth/src/angular/icons/user-lock.icon.ts create mode 100644 libs/auth/src/angular/password-hint/password-hint.component.html create mode 100644 libs/auth/src/angular/password-hint/password-hint.component.ts diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 47e89dcb44..b8d3a2d47c 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -179,6 +179,18 @@ "addItem": { "message": "Add item" }, + "accountEmail": { + "message": "Account email" + }, + "requestHint": { + "message": "Request hint" + }, + "requestPasswordHint": { + "message": "Request password hint" + }, + "enterYourAccountEmailAddressAndYourPasswordHintWillBeSentToYou": { + "message": "Enter your account email address and your password hint will be sent to you" + }, "passwordHint": { "message": "Password hint" }, diff --git a/apps/browser/src/popup/app-routing.module.ts b/apps/browser/src/popup/app-routing.module.ts index f715d38422..0f6a9d9248 100644 --- a/apps/browser/src/popup/app-routing.module.ts +++ b/apps/browser/src/popup/app-routing.module.ts @@ -1,6 +1,8 @@ import { Injectable, NgModule } from "@angular/core"; import { ActivatedRouteSnapshot, RouteReuseStrategy, RouterModule, Routes } from "@angular/router"; +import { EnvironmentSelectorComponent } from "@bitwarden/angular/auth/components/environment-selector.component"; +import { unauthUiRefreshSwap } from "@bitwarden/angular/auth/functions/unauth-ui-refresh-route-swap"; import { authGuard, lockGuard, @@ -15,11 +17,13 @@ import { extensionRefreshSwap } from "@bitwarden/angular/utils/extension-refresh import { AnonLayoutWrapperComponent, AnonLayoutWrapperData, + PasswordHintComponent, RegistrationFinishComponent, RegistrationStartComponent, RegistrationStartSecondaryComponent, RegistrationStartSecondaryComponentData, SetPasswordJitComponent, + UserLockIcon, } from "@bitwarden/auth/angular"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; @@ -27,6 +31,7 @@ import { twofactorRefactorSwap } from "../../../../libs/angular/src/utils/two-fa import { fido2AuthGuard } from "../auth/guards/fido2-auth.guard"; import { AccountSwitcherComponent } from "../auth/popup/account-switching/account-switcher.component"; 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"; @@ -213,12 +218,6 @@ const routes: Routes = [ canActivate: [unauthGuardFn(unauthRouteOverrides)], data: { state: "register" }, }, - { - path: "hint", - component: HintComponent, - canActivate: [unauthGuardFn(unauthRouteOverrides)], - data: { state: "hint" }, - }, { path: "environment", component: EnvironmentComponent, @@ -385,6 +384,41 @@ const routes: Routes = [ canActivate: [authGuard], data: { state: "update-temp-password" }, }, + ...unauthUiRefreshSwap( + HintComponent, + ExtensionAnonLayoutWrapperComponent, + { + path: "hint", + canActivate: [unauthGuardFn(unauthRouteOverrides)], + data: { + state: "hint", + }, + }, + { + path: "", + children: [ + { + path: "hint", + canActivate: [unauthGuardFn(unauthRouteOverrides)], + data: { + pageTitle: "requestPasswordHint", + pageSubtitle: "enterYourAccountEmailAddressAndYourPasswordHintWillBeSentToYou", + pageIcon: UserLockIcon, + showBackButton: true, + state: "hint", + }, + children: [ + { path: "", component: PasswordHintComponent }, + { + path: "", + component: EnvironmentSelectorComponent, + outlet: "environment-selector", + }, + ], + }, + ], + }, + ), { path: "", component: AnonLayoutWrapperComponent, diff --git a/apps/browser/src/popup/app.module.ts b/apps/browser/src/popup/app.module.ts index f8d3c69105..f14dafacb7 100644 --- a/apps/browser/src/popup/app.module.ts +++ b/apps/browser/src/popup/app.module.ts @@ -20,6 +20,7 @@ import { AvatarModule, ButtonModule, ToastModule } from "@bitwarden/components"; import { AccountComponent } from "../auth/popup/account-switching/account.component"; import { CurrentAccountComponent } from "../auth/popup/account-switching/current-account.component"; 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"; @@ -131,6 +132,7 @@ import "../platform/popup/locales"; HeaderComponent, UserVerificationDialogComponent, CurrentAccountComponent, + ExtensionAnonLayoutWrapperComponent, ], declarations: [ ActionButtonsComponent, diff --git a/apps/desktop/src/app/app-routing.module.ts b/apps/desktop/src/app/app-routing.module.ts index 2376eb3844..2e44d2213e 100644 --- a/apps/desktop/src/app/app-routing.module.ts +++ b/apps/desktop/src/app/app-routing.module.ts @@ -1,6 +1,8 @@ import { NgModule } from "@angular/core"; import { RouterModule, Routes } from "@angular/router"; +import { EnvironmentSelectorComponent } from "@bitwarden/angular/auth/components/environment-selector.component"; +import { unauthUiRefreshSwap } from "@bitwarden/angular/auth/functions/unauth-ui-refresh-route-swap"; import { authGuard, lockGuard, @@ -12,11 +14,13 @@ import { canAccessFeature } from "@bitwarden/angular/platform/guard/feature-flag import { AnonLayoutWrapperComponent, AnonLayoutWrapperData, + PasswordHintComponent, RegistrationFinishComponent, RegistrationStartComponent, RegistrationStartSecondaryComponent, RegistrationStartSecondaryComponentData, SetPasswordJitComponent, + UserLockIcon, } from "@bitwarden/auth/angular"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; @@ -94,7 +98,6 @@ const routes: Routes = [ canActivate: [authGuard], }, { path: "accessibility-cookie", component: AccessibilityCookieComponent }, - { path: "hint", component: HintComponent }, { path: "set-password", component: SetPasswordComponent }, { path: "sso", component: SsoComponent }, { @@ -113,10 +116,53 @@ const routes: Routes = [ canActivate: [authGuard], data: { titleId: "removeMasterPassword" }, }, + ...unauthUiRefreshSwap( + HintComponent, + AnonLayoutWrapperComponent, + { + path: "hint", + canActivate: [unauthGuardFn()], + data: { + pageTitle: "passwordHint", + titleId: "passwordHint", + }, + }, + { + path: "", + children: [ + { + path: "hint", + canActivate: [unauthGuardFn()], + data: { + pageTitle: "requestPasswordHint", + pageSubtitle: "enterYourAccountEmailAddressAndYourPasswordHintWillBeSentToYou", + pageIcon: UserLockIcon, + state: "hint", + }, + children: [ + { path: "", component: PasswordHintComponent }, + { + path: "", + component: EnvironmentSelectorComponent, + outlet: "environment-selector", + }, + ], + }, + ], + }, + ), { path: "", component: AnonLayoutWrapperComponent, children: [ + { + path: "hint", + component: PasswordHintComponent, + data: { + pageTitle: "requestPasswordHint", + pageSubtitle: "enterYourAccountEmailAddressAndYourPasswordHintWillBeSentToYou", + }, + }, { path: "signup", canActivate: [canAccessFeature(FeatureFlag.EmailVerification), unauthGuardFn()], diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json index 5991fc4d06..721faa2567 100644 --- a/apps/desktop/src/locales/en/messages.json +++ b/apps/desktop/src/locales/en/messages.json @@ -560,6 +560,18 @@ "settings": { "message": "Settings" }, + "accountEmail": { + "message": "Account email" + }, + "requestHint": { + "message": "Request hint" + }, + "requestPasswordHint": { + "message": "Request password hint" + }, + "enterYourAccountEmailAddressAndYourPasswordHintWillBeSentToYou": { + "message": "Enter your account email address and your password hint will be sent to you" + }, "passwordHint": { "message": "Password hint" }, diff --git a/apps/web/src/app/oss-routing.module.ts b/apps/web/src/app/oss-routing.module.ts index de0e8a2da9..b8d1502b6b 100644 --- a/apps/web/src/app/oss-routing.module.ts +++ b/apps/web/src/app/oss-routing.module.ts @@ -1,6 +1,7 @@ import { NgModule } from "@angular/core"; import { Route, RouterModule, Routes } from "@angular/router"; +import { unauthUiRefreshSwap } from "@bitwarden/angular/auth/functions/unauth-ui-refresh-route-swap"; import { authGuard, lockGuard, @@ -12,13 +13,15 @@ import { canAccessFeature } from "@bitwarden/angular/platform/guard/feature-flag import { AnonLayoutWrapperComponent, AnonLayoutWrapperData, + PasswordHintComponent, RegistrationFinishComponent, RegistrationStartComponent, RegistrationStartSecondaryComponent, RegistrationStartSecondaryComponentData, SetPasswordJitComponent, - LockIcon, RegistrationLinkExpiredComponent, + LockIcon, + UserLockIcon, } from "@bitwarden/auth/angular"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; @@ -167,6 +170,49 @@ const routes: Routes = [ }, ], }, + ...unauthUiRefreshSwap( + AnonLayoutWrapperComponent, + AnonLayoutWrapperComponent, + { + path: "hint", + canActivate: [unauthGuardFn()], + data: { + pageTitle: "passwordHint", + titleId: "passwordHint", + }, + children: [ + { path: "", component: HintComponent }, + { + path: "", + component: EnvironmentSelectorComponent, + outlet: "environment-selector", + }, + ], + }, + { + path: "", + children: [ + { + path: "hint", + canActivate: [unauthGuardFn()], + data: { + pageTitle: "requestPasswordHint", + pageSubtitle: "enterYourAccountEmailAddressAndYourPasswordHintWillBeSentToYou", + pageIcon: UserLockIcon, + state: "hint", + }, + children: [ + { path: "", component: PasswordHintComponent }, + { + path: "", + component: EnvironmentSelectorComponent, + outlet: "environment-selector", + }, + ], + }, + ], + }, + ), { path: "", component: AnonLayoutWrapperComponent, @@ -388,25 +434,6 @@ const routes: Routes = [ }, ], }, - { - path: "hint", - canActivate: [unauthGuardFn()], - data: { - pageTitle: "passwordHint", - titleId: "passwordHint", - } satisfies DataProperties & AnonLayoutWrapperData, - children: [ - { - path: "", - component: HintComponent, - }, - { - path: "", - component: EnvironmentSelectorComponent, - outlet: "environment-selector", - }, - ], - }, { path: "remove-password", component: RemovePasswordComponent, diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 4a189a54d6..d1d10dc967 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -960,6 +960,18 @@ "settings": { "message": "Settings" }, + "accountEmail": { + "message": "Account email" + }, + "requestHint": { + "message": "Request hint" + }, + "requestPasswordHint": { + "message": "Request password hint" + }, + "enterYourAccountEmailAddressAndYourPasswordHintWillBeSentToYou": { + "message": "Enter your account email address and your password hint will be sent to you" + }, "passwordHint": { "message": "Password hint" }, diff --git a/libs/angular/src/auth/components/login.component.ts b/libs/angular/src/auth/components/login.component.ts index b798a8df0b..831d505a38 100644 --- a/libs/angular/src/auth/components/login.component.ts +++ b/libs/angular/src/auth/components/login.component.ts @@ -317,6 +317,7 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit, // Try to load from memory first const email = await firstValueFrom(this.loginEmailService.loginEmail$); const rememberEmail = this.loginEmailService.getRememberEmail(); + if (email) { this.formGroup.controls.email.setValue(email); this.formGroup.controls.rememberEmail.setValue(rememberEmail); diff --git a/libs/angular/src/auth/functions/unauth-ui-refresh-route-swap.ts b/libs/angular/src/auth/functions/unauth-ui-refresh-route-swap.ts index 45dad4a1a7..1146b7b40e 100644 --- a/libs/angular/src/auth/functions/unauth-ui-refresh-route-swap.ts +++ b/libs/angular/src/auth/functions/unauth-ui-refresh-route-swap.ts @@ -20,6 +20,7 @@ export function unauthUiRefreshSwap( defaultComponent: Type, refreshedComponent: Type, options: Route, + altOptions?: Route, ): Routes { return componentRouteSwap( defaultComponent, @@ -29,5 +30,6 @@ export function unauthUiRefreshSwap( return configService.getFeatureFlag(FeatureFlag.UnauthenticatedExtensionUIRefresh); }, options, + altOptions, ); } diff --git a/libs/auth/src/angular/anon-layout/anon-layout.component.html b/libs/auth/src/angular/anon-layout/anon-layout.component.html index 6603fa970d..9e6c27f601 100644 --- a/libs/auth/src/angular/anon-layout/anon-layout.component.html +++ b/libs/auth/src/angular/anon-layout/anon-layout.component.html @@ -1,9 +1,12 @@ +
@@ -23,6 +26,7 @@ {{ title }} +
{{ subtitle }}
diff --git a/libs/auth/src/angular/icons/index.ts b/libs/auth/src/angular/icons/index.ts index f502fbe5f3..cfcad992e3 100644 --- a/libs/auth/src/angular/icons/index.ts +++ b/libs/auth/src/angular/icons/index.ts @@ -1,4 +1,5 @@ export * from "./bitwarden-logo.icon"; export * from "./bitwarden-shield.icon"; export * from "./lock.icon"; +export * from "./user-lock.icon"; export * from "./user-verification-biometrics-fingerprint.icon"; diff --git a/libs/auth/src/angular/icons/user-lock.icon.ts b/libs/auth/src/angular/icons/user-lock.icon.ts new file mode 100644 index 0000000000..fef00a09a9 --- /dev/null +++ b/libs/auth/src/angular/icons/user-lock.icon.ts @@ -0,0 +1,22 @@ +import { svgIcon } from "@bitwarden/components"; + +export const UserLockIcon = svgIcon` + + + + + + + + + + + + + + + + + + +`; diff --git a/libs/auth/src/angular/index.ts b/libs/auth/src/angular/index.ts index f6a9ffde55..bfb3a67aed 100644 --- a/libs/auth/src/angular/index.ts +++ b/libs/auth/src/angular/index.ts @@ -16,7 +16,9 @@ export * from "./fingerprint-dialog/fingerprint-dialog.component"; // password callout export * from "./password-callout/password-callout.component"; -export * from "./vault-timeout-input/vault-timeout-input.component"; + +// password hint +export * from "./password-hint/password-hint.component"; // input password export * from "./input-password/input-password.component"; @@ -40,3 +42,6 @@ export * from "./registration/registration-start/registration-start-secondary.co export * from "./registration/registration-env-selector/registration-env-selector.component"; export * from "./registration/registration-finish/registration-finish.service"; export * from "./registration/registration-finish/default-registration-finish.service"; + +// vault timeout +export * from "./vault-timeout-input/vault-timeout-input.component"; diff --git a/libs/auth/src/angular/password-hint/password-hint.component.html b/libs/auth/src/angular/password-hint/password-hint.component.html new file mode 100644 index 0000000000..2a811a1b3b --- /dev/null +++ b/libs/auth/src/angular/password-hint/password-hint.component.html @@ -0,0 +1,40 @@ +
+ + + + {{ "accountEmail" | i18n }} + + + + + + + + +
+ +
+ + + + + +
diff --git a/libs/auth/src/angular/password-hint/password-hint.component.ts b/libs/auth/src/angular/password-hint/password-hint.component.ts new file mode 100644 index 0000000000..1ae1fd337b --- /dev/null +++ b/libs/auth/src/angular/password-hint/password-hint.component.ts @@ -0,0 +1,107 @@ +import { CommonModule } from "@angular/common"; +import { Component, OnInit } from "@angular/core"; +import { FormBuilder, ReactiveFormsModule, Validators } from "@angular/forms"; +import { Router, RouterModule } from "@angular/router"; +import { firstValueFrom } from "rxjs"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { LoginEmailServiceAbstraction } from "@bitwarden/auth/common"; +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { PasswordHintRequest } from "@bitwarden/common/auth/models/request/password-hint.request"; +import { ClientType } from "@bitwarden/common/enums"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { + AsyncActionsModule, + ButtonModule, + FormFieldModule, + ToastService, +} from "@bitwarden/components"; + +@Component({ + standalone: true, + templateUrl: "./password-hint.component.html", + imports: [ + AsyncActionsModule, + ButtonModule, + CommonModule, + FormFieldModule, + JslibModule, + ReactiveFormsModule, + RouterModule, + ], +}) +export class PasswordHintComponent implements OnInit { + protected clientType: ClientType; + + protected formGroup = this.formBuilder.group({ + email: ["", [Validators.required, Validators.email]], + }); + + protected get email() { + return this.formGroup.controls.email.value; + } + + constructor( + private apiService: ApiService, + private formBuilder: FormBuilder, + private i18nService: I18nService, + private loginEmailService: LoginEmailServiceAbstraction, + private platformUtilsService: PlatformUtilsService, + private toastService: ToastService, + private router: Router, + ) { + this.clientType = this.platformUtilsService.getClientType(); + } + + async ngOnInit(): Promise { + const email = (await firstValueFrom(this.loginEmailService.loginEmail$)) ?? ""; + this.formGroup.controls.email.setValue(email); + } + + submit = async () => { + const isEmailValid = this.validateEmailOrShowToast(this.email); + if (!isEmailValid) { + return; + } + + await this.apiService.postPasswordHint(new PasswordHintRequest(this.email)); + + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("masterPassSent"), + }); + + await this.router.navigate(["login"]); + }; + + protected async cancel() { + this.loginEmailService.setLoginEmail(this.email); + await this.router.navigate(["login"]); + } + + private validateEmailOrShowToast(email: string): boolean { + // If email is null or empty, show error toast and return false + if (email == null || email === "") { + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("emailRequired"), + }); + return false; + } + + // If not a valid email format, show error toast and return false + if (email.indexOf("@") === -1) { + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("invalidEmail"), + }); + return false; + } + + return true; // email is valid + } +}