diff --git a/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-email/two-factor-auth-email-cache.service.ts b/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-email/two-factor-auth-email-cache.service.ts new file mode 100644 index 00000000000..f382913e83e --- /dev/null +++ b/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-email/two-factor-auth-email-cache.service.ts @@ -0,0 +1,85 @@ +import { inject, Injectable, WritableSignal } from "@angular/core"; +import { Jsonify } from "type-fest"; + +import { ViewCacheService } from "@bitwarden/angular/platform/abstractions/view-cache.service"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; + +const TWO_FACTOR_AUTH_EMAIL_CACHE_KEY = "two-factor-auth-email-cache"; + +/** + * Cache model for the email two factor + */ +export class TwoFactorAuthEmailCache { + emailSent: boolean = false; + + static fromJSON(obj: Partial>): TwoFactorAuthEmailCache { + return Object.assign(new TwoFactorAuthEmailCache(), obj); + } +} + +/** + * Cache service for the two factor auth email component. + */ +@Injectable() +export class TwoFactorAuthEmailComponentCacheService { + private viewCacheService: ViewCacheService = inject(ViewCacheService); + private configService: ConfigService = inject(ConfigService); + + /** True when the feature flag is enabled */ + private featureEnabled: boolean = false; + + /** + * Signal for the cached email state. + */ + private emailCache: WritableSignal = + this.viewCacheService.signal({ + key: TWO_FACTOR_AUTH_EMAIL_CACHE_KEY, + initialValue: null, + deserializer: TwoFactorAuthEmailCache.fromJSON, + }); + + /** + * Must be called once before interacting with the cached data. + */ + async init() { + this.featureEnabled = await this.configService.getFeatureFlag( + FeatureFlag.PM9115_TwoFactorExtensionDataPersistence, + ); + } + + /** + * Cache the email sent state. + */ + cacheData(data: { emailSent: boolean }): void { + if (!this.featureEnabled) { + return; + } + + this.emailCache.set({ + emailSent: data.emailSent, + } as TwoFactorAuthEmailCache); + } + + /** + * Clear the cached email data. + */ + clearCachedData(): void { + if (!this.featureEnabled) { + return; + } + + this.emailCache.set(null); + } + + /** + * Get whether the email has been sent. + */ + getCachedData(): TwoFactorAuthEmailCache | null { + if (!this.featureEnabled) { + return null; + } + + return this.emailCache(); + } +} diff --git a/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-email/two-factor-auth-email.component.ts b/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-email/two-factor-auth-email.component.ts index 971490ed920..b308ca61442 100644 --- a/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-email/two-factor-auth-email.component.ts +++ b/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-email/two-factor-auth-email.component.ts @@ -22,6 +22,7 @@ import { ToastService, } from "@bitwarden/components"; +import { TwoFactorAuthEmailComponentCacheService } from "./two-factor-auth-email-cache.service"; import { TwoFactorAuthEmailComponentService } from "./two-factor-auth-email-component.service"; @Component({ @@ -40,15 +41,19 @@ import { TwoFactorAuthEmailComponentService } from "./two-factor-auth-email-comp AsyncActionsModule, FormsModule, ], + providers: [ + { + provide: TwoFactorAuthEmailComponentCacheService, + }, + ], }) export class TwoFactorAuthEmailComponent implements OnInit { @Input({ required: true }) tokenFormControl: FormControl | undefined = undefined; - @Input({ required: true }) emailSent: boolean = false; @Output() tokenChange = new EventEmitter<{ token: string }>(); - @Output() emailSendEvent = new EventEmitter(); twoFactorEmail: string | undefined = undefined; emailPromise: Promise | undefined; + emailSent = false; constructor( protected i18nService: I18nService, @@ -60,10 +65,18 @@ export class TwoFactorAuthEmailComponent implements OnInit { protected appIdService: AppIdService, private toastService: ToastService, private twoFactorAuthEmailComponentService: TwoFactorAuthEmailComponentService, + private cacheService: TwoFactorAuthEmailComponentCacheService, ) {} async ngOnInit(): Promise { await this.twoFactorAuthEmailComponentService.openPopoutIfApprovedForEmail2fa?.(); + await this.cacheService.init(); + + // Check if email was already sent + const cachedData = this.cacheService.getCachedData(); + if (cachedData?.emailSent) { + this.emailSent = true; + } const providers = await this.twoFactorService.getProviders(); @@ -124,7 +137,8 @@ export class TwoFactorAuthEmailComponent implements OnInit { this.emailPromise = this.apiService.postTwoFactorEmail(request); await this.emailPromise; - this.emailSendEvent.emit(); + this.emailSent = true; + this.cacheService.cacheData({ emailSent: this.emailSent }); if (doToast) { this.toastService.showToast({ diff --git a/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.html b/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.html index 3192978e818..e0c2ffc642a 100644 --- a/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.html +++ b/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.html @@ -14,8 +14,6 @@ diff --git a/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.ts b/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.ts index 54899104bb2..9706a95f857 100644 --- a/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.ts +++ b/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.ts @@ -78,7 +78,6 @@ interface TwoFactorCacheData { token?: string; remember?: boolean; selectedProviderType?: TwoFactorProviderType; - emailSent?: boolean; } @Component({ @@ -114,11 +113,6 @@ export class TwoFactorAuthComponent implements OnInit, OnDestroy { loading = true; - /** - * Whether the email has been sent according to the cache - */ - emailSent = false; - orgSsoIdentifier: string | undefined = undefined; providerType = TwoFactorProviderType; @@ -206,9 +200,6 @@ export class TwoFactorAuthComponent implements OnInit, OnDestroy { this.selectedProviderType = persistedData.selectedProviderType; loadedCachedProviderType = true; } - if (persistedData.emailSent !== undefined) { - this.emailSent = persistedData.emailSent; - } } // Only set default 2FA provider type if we don't have one from cache @@ -242,7 +233,6 @@ export class TwoFactorAuthComponent implements OnInit, OnDestroy { data?.selectedProviderType ?? currentData?.selectedProviderType ?? TwoFactorProviderType.Authenticator, - emailSent: data?.emailSent ?? currentData?.emailSent ?? false, }); } @@ -254,7 +244,6 @@ export class TwoFactorAuthComponent implements OnInit, OnDestroy { token: this.tokenFormControl.value || undefined, remember: this.rememberFormControl.value ?? undefined, selectedProviderType: this.selectedProviderType, - emailSent: this.selectedProviderType === TwoFactorProviderType.Email, }; await this.saveFormDataWithPartialData(formData); @@ -351,7 +340,6 @@ export class TwoFactorAuthComponent implements OnInit, OnDestroy { token: tokenValue, remember: rememberValue, selectedProviderType: this.selectedProviderType, - emailSent: this.selectedProviderType === TwoFactorProviderType.Email, }); try { @@ -379,7 +367,6 @@ export class TwoFactorAuthComponent implements OnInit, OnDestroy { token: "", remember: false, selectedProviderType: this.selectedProviderType, - emailSent: false, }); const dialogRef = TwoFactorOptionsComponent.open(this.dialogService); @@ -400,7 +387,6 @@ export class TwoFactorAuthComponent implements OnInit, OnDestroy { token: "", remember: false, selectedProviderType: response.type, - emailSent: false, }); this.form.reset(); diff --git a/libs/auth/src/common/services/auth-request/two-factor-auth-cache.service.ts b/libs/auth/src/common/services/auth-request/two-factor-auth-cache.service.ts index 39cee4c06f3..906d86b7288 100644 --- a/libs/auth/src/common/services/auth-request/two-factor-auth-cache.service.ts +++ b/libs/auth/src/common/services/auth-request/two-factor-auth-cache.service.ts @@ -15,7 +15,6 @@ export class TwoFactorAuthCache { token: string | undefined = undefined; remember: boolean | undefined = undefined; selectedProviderType: TwoFactorProviderType | undefined = undefined; - emailSent: boolean | undefined = undefined; static fromJSON(obj: Partial>): TwoFactorAuthCache { return Object.assign(new TwoFactorAuthCache(), obj); @@ -26,7 +25,6 @@ export interface TwoFactorAuthData { token?: string; remember?: boolean; selectedProviderType?: TwoFactorProviderType; - emailSent?: boolean; } /** @@ -76,7 +74,6 @@ export class TwoFactorAuthComponentCacheService { token: data.token, remember: data.remember, selectedProviderType: data.selectedProviderType, - emailSent: data.emailSent, } as TwoFactorAuthCache); }