1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-13 15:03:26 +00:00

Refactor service to use ViewCacheService

This commit is contained in:
Alec Rippberger
2025-03-04 16:54:47 -06:00
parent 50779dd19b
commit ad58fc6086
9 changed files with 151 additions and 51 deletions

View File

@@ -2,7 +2,7 @@ import { Component } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import { TwoFactorOptionsComponentV1 as BaseTwoFactorOptionsComponent } from "@bitwarden/angular/auth/components/two-factor-options-v1.component";
import { TwoFactorFormCacheServiceAbstraction } from "@bitwarden/auth/angular";
import { TwoFactorFormCacheService } from "@bitwarden/auth/angular";
import {
TwoFactorProviderDetails,
TwoFactorService,
@@ -23,7 +23,7 @@ export class TwoFactorOptionsComponentV1 extends BaseTwoFactorOptionsComponent {
platformUtilsService: PlatformUtilsService,
environmentService: EnvironmentService,
private activatedRoute: ActivatedRoute,
private twoFactorFormCacheService: TwoFactorFormCacheServiceAbstraction,
private twoFactorFormCacheService: TwoFactorFormCacheService,
) {
super(twoFactorService, router, i18nService, platformUtilsService, window, environmentService);
}
@@ -36,10 +36,10 @@ export class TwoFactorOptionsComponentV1 extends BaseTwoFactorOptionsComponent {
await super.choose(p);
await this.twoFactorService.setSelectedProvider(p.type);
const persistedData = await this.twoFactorFormCacheService.getFormData();
// Clear the token when changing provider types
await this.twoFactorFormCacheService.saveFormData({
token: persistedData?.token || undefined,
remember: persistedData?.remember ?? undefined,
token: undefined,
remember: undefined,
selectedProviderType: p.type,
emailSent: false,
});

View File

@@ -46,6 +46,7 @@
type="text"
name="Code"
[(ngModel)]="token"
(ngModelChange)="saveFormData()"
required
appAutofocus
inputmode="tel"
@@ -54,7 +55,13 @@
</div>
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="remember">{{ "rememberMe" | i18n }}</label>
<input id="remember" type="checkbox" name="Remember" [(ngModel)]="remember" />
<input
id="remember"
type="checkbox"
name="Remember"
[(ngModel)]="remember"
(ngModelChange)="saveFormData()"
/>
</div>
</div>
</div>
@@ -74,6 +81,7 @@
type="password"
name="Code"
[(ngModel)]="token"
(ngModelChange)="saveFormData()"
required
appAutofocus
appInputVerbatim
@@ -81,7 +89,13 @@
</div>
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="remember">{{ "rememberMe" | i18n }}</label>
<input id="remember" type="checkbox" name="Remember" [(ngModel)]="remember" />
<input
id="remember"
type="checkbox"
name="Remember"
[(ngModel)]="remember"
(ngModelChange)="saveFormData()"
/>
</div>
</div>
</div>
@@ -95,7 +109,13 @@
<div class="box-content">
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="remember">{{ "rememberMe" | i18n }}</label>
<input id="remember" type="checkbox" name="Remember" [(ngModel)]="remember" />
<input
id="remember"
type="checkbox"
name="Remember"
[(ngModel)]="remember"
(ngModelChange)="saveFormData()"
/>
</div>
</div>
</div>
@@ -132,7 +152,13 @@
<div class="box-content">
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="remember">{{ "rememberMe" | i18n }}</label>
<input id="remember" type="checkbox" name="Remember" [(ngModel)]="remember" />
<input
id="remember"
type="checkbox"
name="Remember"
[(ngModel)]="remember"
(ngModelChange)="saveFormData()"
/>
</div>
</div>
</div>

View File

@@ -7,18 +7,19 @@ import { filter, first, takeUntil } from "rxjs/operators";
import { TwoFactorComponentV1 as BaseTwoFactorComponent } from "@bitwarden/angular/auth/components/two-factor-v1.component";
import { WINDOW } from "@bitwarden/angular/services/injection-tokens";
import { TwoFactorFormCacheService } from "@bitwarden/auth/angular";
import {
LoginStrategyServiceAbstraction,
LoginEmailServiceAbstraction,
UserDecryptionOptionsServiceAbstraction,
} from "@bitwarden/auth/common";
import { TwoFactorFormCacheServiceAbstraction } from "@bitwarden/auth/angular";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
@@ -68,7 +69,7 @@ export class TwoFactorComponentV1 extends BaseTwoFactorComponent implements OnIn
toastService: ToastService,
@Inject(WINDOW) protected win: Window,
private browserMessagingApi: ZonedMessageListenerService,
private twoFactorFormCacheService: TwoFactorFormCacheServiceAbstraction,
private twoFactorFormCacheService: TwoFactorFormCacheService,
) {
super(
loginStrategyService,
@@ -156,7 +157,8 @@ export class TwoFactorComponentV1 extends BaseTwoFactorComponent implements OnIn
if (
this.selectedProviderType === TwoFactorProviderType.Email &&
BrowserPopupUtils.inPopup(window)
BrowserPopupUtils.inPopup(window) &&
!(await this.configService.getFeatureFlag(FeatureFlag.PM9115_TwoFactorFormPersistence))
) {
const confirmed = await this.dialogService.openSimpleDialog({
title: { key: "warning" },
@@ -302,4 +304,18 @@ export class TwoFactorComponentV1 extends BaseTwoFactorComponent implements OnIn
// Clear cached data on successful login
await this.twoFactorFormCacheService.clearFormData();
}
/**
* Save the current form state when inputs change
*/
async saveFormData() {
if (this.twoFactorFormCacheService) {
await this.twoFactorFormCacheService.saveFormData({
token: this.token,
remember: this.remember,
selectedProviderType: this.selectedProviderType,
emailSent: this.selectedProviderType === TwoFactorProviderType.Email,
});
}
}
}

View File

@@ -2,6 +2,8 @@ import {
DefaultTwoFactorAuthEmailComponentService,
TwoFactorAuthEmailComponentService,
} from "@bitwarden/auth/angular";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { DialogService } from "@bitwarden/components";
import { openTwoFactorAuthEmailPopout } from "../../auth/popup/utils/auth-popout-window";
@@ -15,12 +17,16 @@ export class ExtensionTwoFactorAuthEmailComponentService
constructor(
private dialogService: DialogService,
private window: Window,
private configService: ConfigService,
) {
super();
}
async openPopoutIfApprovedForEmail2fa(): Promise<void> {
if (BrowserPopupUtils.inPopup(this.window)) {
const isTwoFactorFormPersistenceEnabled = await this.configService.getFeatureFlag(
FeatureFlag.PM9115_TwoFactorFormPersistence,
);
if (BrowserPopupUtils.inPopup(this.window) && isTwoFactorFormPersistenceEnabled) {
const confirmed = await this.dialogService.openSimpleDialog({
title: { key: "warning" },
content: { key: "popup2faCloseMessage" },

View File

@@ -1,28 +1,53 @@
import { Injectable, WritableSignal } from "@angular/core";
import { Observable, from, of, switchMap } from "rxjs";
import { TwoFactorFormCacheServiceAbstraction } from "@bitwarden/auth/angular";
import { ViewCacheService } from "@bitwarden/angular/platform/abstractions/view-cache.service";
import { TwoFactorFormCacheService, TwoFactorFormData } from "@bitwarden/auth/angular";
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service";
/**
* Interface for two-factor form data
*/
interface TwoFactorFormData {
const TWO_FACTOR_FORM_CACHE_KEY = "two-factor-form-cache";
// Utilize function overloading to create a type-safe deserializer to match the exact expected signature
function deserializeFormData(jsonValue: null): null;
function deserializeFormData(jsonValue: {
token?: string;
remember?: boolean;
selectedProviderType?: TwoFactorProviderType;
emailSent?: boolean;
}): TwoFactorFormData;
function deserializeFormData(jsonValue: any): TwoFactorFormData | null {
if (!jsonValue) {
return null;
}
return {
token: jsonValue.token,
remember: jsonValue.remember,
selectedProviderType: jsonValue.selectedProviderType,
emailSent: jsonValue.emailSent,
};
}
const STORAGE_KEY = "twoFactorFormData";
/**
* Service for caching two-factor form data
*/
@Injectable()
export class ExtensionTwoFactorFormCacheService extends TwoFactorFormCacheService {
private formDataCache: WritableSignal<TwoFactorFormData | null>;
export class ExtensionTwoFactorFormCacheService implements TwoFactorFormCacheServiceAbstraction {
constructor(
private storageService: AbstractStorageService,
private viewCacheService: ViewCacheService,
private configService: ConfigService,
) {}
) {
super();
this.formDataCache = this.viewCacheService.signal<TwoFactorFormData | null>({
key: TWO_FACTOR_FORM_CACHE_KEY,
initialValue: null,
deserializer: deserializeFormData,
});
}
isEnabled$(): Observable<boolean> {
return from(this.configService.getFeatureFlag(FeatureFlag.PM9115_TwoFactorFormPersistence));
@@ -38,28 +63,38 @@ export class ExtensionTwoFactorFormCacheService implements TwoFactorFormCacheSer
if (!enabled) {
return of(null);
}
return from(this.storageService.get<TwoFactorFormData>(STORAGE_KEY));
return of(this.formDataCache());
}),
);
}
/**
* Save form data to cache
*/
async saveFormData(data: TwoFactorFormData): Promise<void> {
if (!(await this.isEnabled())) {
return;
}
await this.storageService.save(STORAGE_KEY, data);
// Set the new form data in the cache
this.formDataCache.set({ ...data });
}
/**
* Retrieve form data from cache
*/
async getFormData(): Promise<TwoFactorFormData | null> {
if (!(await this.isEnabled())) {
return null;
}
return await this.storageService.get<TwoFactorFormData>(STORAGE_KEY);
return this.formDataCache();
}
/**
* Clear form data from cache
*/
async clearFormData(): Promise<void> {
await this.storageService.remove(STORAGE_KEY);
this.formDataCache.set(null);
}
}

View File

@@ -31,7 +31,8 @@ import {
TwoFactorAuthDuoComponentService,
TwoFactorAuthWebAuthnComponentService,
SsoComponentService,
TwoFactorFormCacheServiceAbstraction } from "@bitwarden/auth/angular";
TwoFactorFormCacheService,
} from "@bitwarden/auth/angular";
import {
LockService,
LoginEmailService,
@@ -553,7 +554,7 @@ const safeProviders: SafeProvider[] = [
safeProvider({
provide: TwoFactorAuthEmailComponentService,
useClass: ExtensionTwoFactorAuthEmailComponentService,
deps: [DialogService, WINDOW],
deps: [DialogService, WINDOW, ConfigService],
}),
safeProvider({
provide: TwoFactorAuthWebAuthnComponentService,
@@ -561,9 +562,9 @@ const safeProviders: SafeProvider[] = [
deps: [PlatformUtilsService],
}),
safeProvider({
provide: TwoFactorFormCacheServiceAbstraction,
provide: TwoFactorFormCacheService,
useClass: ExtensionTwoFactorFormCacheService,
deps: [AbstractStorageService, ConfigService],
deps: [PopupViewCacheService, ConfigService],
}),
safeProvider({
provide: TwoFactorAuthDuoComponentService,
@@ -658,11 +659,6 @@ const safeProviders: SafeProvider[] = [
useClass: ExtensionLoginDecryptionOptionsService,
deps: [MessagingServiceAbstraction, Router],
}),
safeProvider({
provide: TwoFactorFormCacheServiceAbstraction,
useClass: ExtensionTwoFactorFormCacheService,
deps: [AbstractStorageService, ConfigService],
}),
];
@NgModule({

View File

@@ -1,9 +1,11 @@
import { Observable } from "rxjs";
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
/**
* Interface for two-factor form data
*/
interface TwoFactorFormData {
export interface TwoFactorFormData {
token?: string;
remember?: boolean;
selectedProviderType?: TwoFactorProviderType;
@@ -13,12 +15,17 @@ interface TwoFactorFormData {
/**
* Abstract service for two-factor form caching
*/
export abstract class TwoFactorFormCacheServiceAbstraction {
export abstract class TwoFactorFormCacheService {
/**
* Check if the form persistence feature is enabled
*/
abstract isEnabled(): Promise<boolean>;
/**
* Observable that emits the current enabled state
*/
abstract isEnabled$(): Observable<boolean>;
/**
* Save form data to persistent storage
*/
@@ -29,6 +36,11 @@ export abstract class TwoFactorFormCacheServiceAbstraction {
*/
abstract getFormData(): Promise<TwoFactorFormData | null>;
/**
* Observable that emits the current form data
*/
abstract formData$(): Observable<TwoFactorFormData | null>;
/**
* Clear form data from persistent storage
*/

View File

@@ -22,7 +22,8 @@ import {
ToastService,
} from "@bitwarden/components";
import { TwoFactorFormCacheServiceAbstraction } from "../../abstractions/two-factor-form-cache.service.abstraction";
import { TwoFactorFormCacheService } from "../../abstractions/two-factor-form-cache.service.abstraction";
import { TwoFactorAuthEmailComponentService } from "./two-factor-auth-email-component.service";
@Component({
@@ -41,7 +42,6 @@ import { TwoFactorAuthEmailComponentService } from "./two-factor-auth-email-comp
AsyncActionsModule,
FormsModule,
],
providers: [],
})
export class TwoFactorAuthEmailComponent implements OnInit {
@Input({ required: true }) tokenFormControl: FormControl | undefined = undefined;
@@ -60,7 +60,7 @@ export class TwoFactorAuthEmailComponent implements OnInit {
protected appIdService: AppIdService,
private toastService: ToastService,
private twoFactorAuthEmailComponentService: TwoFactorAuthEmailComponentService,
private twoFactorFormCacheService: TwoFactorFormCacheServiceAbstraction,
private twoFactorFormCacheService: TwoFactorFormCacheService,
) {}
async ngOnInit(): Promise<void> {
@@ -81,12 +81,17 @@ export class TwoFactorAuthEmailComponent implements OnInit {
this.twoFactorEmail = email2faProviderData.Email;
// Check if email has already been sent according to the cache
let cachedData;
let emailAlreadySent = false;
if (this.twoFactorFormCacheService) {
cachedData = await this.twoFactorFormCacheService.getFormData();
try {
const cachedData = await this.twoFactorFormCacheService.getFormData();
emailAlreadySent = cachedData?.emailSent === true;
} catch (e) {
this.logService.error(e);
}
}
if (providers.size > 1 && !cachedData?.emailSent) {
if (providers.size > 1 && !emailAlreadySent) {
await this.sendEmail(false);
}
}
@@ -129,11 +134,15 @@ export class TwoFactorAuthEmailComponent implements OnInit {
// Update cache to indicate email was sent
if (this.twoFactorFormCacheService) {
const cachedData = (await this.twoFactorFormCacheService.getFormData()) || {};
await this.twoFactorFormCacheService.saveFormData({
...cachedData,
emailSent: true,
});
try {
const cachedData = (await this.twoFactorFormCacheService.getFormData()) || {};
await this.twoFactorFormCacheService.saveFormData({
...cachedData,
emailSent: true,
});
} catch (e) {
this.logService.error(e);
}
}
if (doToast) {

View File

@@ -54,8 +54,8 @@ import {
TwoFactorAuthSecurityKeyIcon,
TwoFactorAuthDuoIcon,
} from "../icons/two-factor-auth";
import { TwoFactorFormCacheServiceAbstraction } from "./abstractions/two-factor-form-cache.service.abstraction";
import { TwoFactorFormCacheService } from "./abstractions";
import { TwoFactorAuthAuthenticatorComponent } from "./child-components/two-factor-auth-authenticator.component";
import { TwoFactorAuthDuoComponent } from "./child-components/two-factor-auth-duo/two-factor-auth-duo.component";
import { TwoFactorAuthEmailComponent } from "./child-components/two-factor-auth-email/two-factor-auth-email.component";
@@ -171,7 +171,7 @@ export class TwoFactorAuthComponent implements OnInit, OnDestroy {
private anonLayoutWrapperDataService: AnonLayoutWrapperDataService,
private environmentService: EnvironmentService,
private loginSuccessHandlerService: LoginSuccessHandlerService,
private twoFactorFormCacheService: TwoFactorFormCacheServiceAbstraction,
private twoFactorFormCacheService: TwoFactorFormCacheService,
) {}
async ngOnInit() {