From 56f5dba444680746b02f5ede7698f285438f68a2 Mon Sep 17 00:00:00 2001 From: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com> Date: Thu, 18 Jul 2024 17:37:22 -0400 Subject: [PATCH] Auth/PM-6198 - Registration with Email Verification - Call email clicked endpoint (#10139) * PM-6198 - Majority of client work done; WIP on registration finish comp * PM-6198 - Registration Finish - Add registerVerificationEmailClicked logic * PM-6198 - RegistrationLinkExpired component; added translations on other clients just in case we use the component on other clients in the future. * PM-6198 - Clean up comment --- apps/browser/src/_locales/en/messages.json | 15 ++++ apps/desktop/src/locales/en/messages.json | 12 +++ apps/web/src/app/oss-routing.module.ts | 17 ++++ apps/web/src/locales/en/messages.json | 12 +++ .../icons/registration-expired-link.icon.ts | 20 +++++ libs/auth/src/angular/index.ts | 1 + .../registration-finish.component.ts | 90 +++++++++++++++---- .../registration-link-expired.component.html | 27 ++++++ .../registration-link-expired.component.ts | 44 +++++++++ .../auth/abstractions/account-api.service.ts | 14 +++ ...ster-verification-email-clicked.request.ts | 6 ++ .../src/auth/services/account-api.service.ts | 23 +++++ 12 files changed, 264 insertions(+), 17 deletions(-) create mode 100644 libs/auth/src/angular/icons/registration-expired-link.icon.ts create mode 100644 libs/auth/src/angular/registration/registration-link-expired/registration-link-expired.component.html create mode 100644 libs/auth/src/angular/registration/registration-link-expired/registration-link-expired.component.ts create mode 100644 libs/common/src/auth/models/request/registration/register-verification-email-clicked.request.ts diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index e80acbe9b3d..e05d3fcd169 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -660,6 +660,21 @@ "loginExpired": { "message": "Your login session has expired." }, + "logIn": { + "message": "Log in" + }, + "restartRegistration": { + "message": "Restart registration" + }, + "expiredLink": { + "message": "Expired link" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Please restart registration or try logging in." + }, + "youMayAlreadyHaveAnAccount": { + "message": "You may already have an account" + }, "logOutConfirmation": { "message": "Are you sure you want to log out?" }, diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json index b316a62306a..b9f5f825dbe 100644 --- a/apps/desktop/src/locales/en/messages.json +++ b/apps/desktop/src/locales/en/messages.json @@ -792,6 +792,18 @@ "loginExpired": { "message": "Your login session has expired." }, + "restartRegistration": { + "message": "Restart registration" + }, + "expiredLink": { + "message": "Expired link" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Please restart registration or try logging in." + }, + "youMayAlreadyHaveAnAccount": { + "message": "You may already have an account" + }, "logOutConfirmation": { "message": "Are you sure you want to log out?" }, diff --git a/apps/web/src/app/oss-routing.module.ts b/apps/web/src/app/oss-routing.module.ts index a0e0448eef0..a4172ad03ea 100644 --- a/apps/web/src/app/oss-routing.module.ts +++ b/apps/web/src/app/oss-routing.module.ts @@ -18,6 +18,7 @@ import { RegistrationStartSecondaryComponent, RegistrationStartSecondaryComponentData, LockIcon, + RegistrationLinkExpiredComponent, } from "@bitwarden/auth/angular"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; @@ -205,6 +206,22 @@ const routes: Routes = [ }, ], }, + { + path: "signup-link-expired", + canActivate: [canAccessFeature(FeatureFlag.EmailVerification), unauthGuardFn()], + data: { + pageTitle: "expiredLink", + } satisfies AnonLayoutWrapperData, + children: [ + { + path: "", + component: RegistrationLinkExpiredComponent, + data: { + loginRoute: "/login", + } satisfies RegistrationStartSecondaryComponentData, + }, + ], + }, { path: "sso", canActivate: [unauthGuardFn()], diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 20bbf2a8f9c..26a7426837e 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -627,6 +627,18 @@ "loginExpired": { "message": "Your login session has expired." }, + "restartRegistration": { + "message": "Restart registration" + }, + "expiredLink": { + "message": "Expired link" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Please restart registration or try logging in." + }, + "youMayAlreadyHaveAnAccount": { + "message": "You may already have an account" + }, "logOutConfirmation": { "message": "Are you sure you want to log out?" }, diff --git a/libs/auth/src/angular/icons/registration-expired-link.icon.ts b/libs/auth/src/angular/icons/registration-expired-link.icon.ts new file mode 100644 index 00000000000..50bcc53808f --- /dev/null +++ b/libs/auth/src/angular/icons/registration-expired-link.icon.ts @@ -0,0 +1,20 @@ +import { svgIcon } from "@bitwarden/components"; + +export const RegistrationExpiredLinkIcon = svgIcon` +`; diff --git a/libs/auth/src/angular/index.ts b/libs/auth/src/angular/index.ts index dec1d7c08b4..b5944772435 100644 --- a/libs/auth/src/angular/index.ts +++ b/libs/auth/src/angular/index.ts @@ -20,6 +20,7 @@ export * from "./user-verification/user-verification-form-input.component"; // registration export * from "./registration/registration-start/registration-start.component"; export * from "./registration/registration-finish/registration-finish.component"; +export * from "./registration/registration-link-expired/registration-link-expired.component"; export * from "./registration/registration-start/registration-start-secondary.component"; export * from "./registration/registration-env-selector/registration-env-selector.component"; export * from "./registration/registration-finish/registration-finish.service"; 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 43a8951318e..580b339e1eb 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,10 +1,14 @@ import { CommonModule } from "@angular/common"; import { Component, OnDestroy, OnInit } from "@angular/core"; import { ActivatedRoute, Params, Router, RouterModule } from "@angular/router"; -import { Subject, takeUntil } from "rxjs"; +import { Subject, from, switchMap, takeUntil, tap } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options"; +import { AccountApiService } from "@bitwarden/common/auth/abstractions/account-api.service"; +import { RegisterVerificationEmailClickedRequest } from "@bitwarden/common/auth/models/request/registration/register-verification-email-clicked.request"; +import { HttpStatusCode } from "@bitwarden/common/enums"; +import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; import { ToastService } from "@bitwarden/components"; @@ -41,33 +45,43 @@ export class RegistrationFinishComponent implements OnInit, OnDestroy { private i18nService: I18nService, private registrationFinishService: RegistrationFinishService, private validationService: ValidationService, + private accountApiService: AccountApiService, ) {} async ngOnInit() { this.listenForQueryParamChanges(); this.masterPasswordPolicyOptions = await this.registrationFinishService.getMasterPasswordPolicyOptsFromOrgInvite(); - this.loading = false; } private listenForQueryParamChanges() { - this.activatedRoute.queryParams.pipe(takeUntil(this.destroy$)).subscribe((qParams: Params) => { - if (qParams.email != null && qParams.email.indexOf("@") > -1) { - this.email = qParams.email; - } + this.activatedRoute.queryParams + .pipe( + tap((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; + } + }), + switchMap((qParams: Params) => { + if ( + qParams.fromEmail && + qParams.fromEmail === "true" && + this.email && + this.emailVerificationToken + ) { + return from( + this.registerVerificationEmailClicked(this.email, this.emailVerificationToken), + ); + } + }), - if (qParams.fromEmail && qParams.fromEmail === "true") { - this.toastService.showToast({ - title: null, - message: this.i18nService.t("emailVerifiedV2"), - variant: "success", - }); - } - }); + takeUntil(this.destroy$), + ) + .subscribe(); } async handlePasswordFormSubmit(passwordInputResult: PasswordInputResult) { @@ -94,6 +108,48 @@ export class RegistrationFinishComponent implements OnInit, OnDestroy { await this.router.navigate(["/login"], { queryParams: { email: this.email } }); } + private async registerVerificationEmailClicked(email: string, emailVerificationToken: string) { + const request = new RegisterVerificationEmailClickedRequest(email, emailVerificationToken); + + try { + const result = await this.accountApiService.registerVerificationEmailClicked(request); + + if (result == null) { + this.toastService.showToast({ + title: null, + message: this.i18nService.t("emailVerifiedV2"), + variant: "success", + }); + this.loading = false; + } + } catch (e) { + await this.handleRegisterVerificationEmailClickedError(e); + this.loading = false; + } + } + + private async handleRegisterVerificationEmailClickedError(e: unknown) { + if (e instanceof ErrorResponse) { + const errorResponse = e as ErrorResponse; + switch (errorResponse.statusCode) { + case HttpStatusCode.BadRequest: { + if (errorResponse.message.includes("Expired link")) { + await this.router.navigate(["/signup-link-expired"]); + } else { + this.validationService.showError(errorResponse); + } + + break; + } + default: + this.validationService.showError(errorResponse); + break; + } + } else { + this.validationService.showError(e); + } + } + ngOnDestroy(): void { this.destroy$.next(); this.destroy$.complete(); 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 new file mode 100644 index 00000000000..5aa6866bbe0 --- /dev/null +++ b/libs/auth/src/angular/registration/registration-link-expired/registration-link-expired.component.html @@ -0,0 +1,27 @@ +
+ {{ "pleaseRestartRegistrationOrTryLoggingIn" | i18n }}
+ {{ "youMayAlreadyHaveAnAccount" | i18n }}
+