1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-18 17:23:37 +00:00

feat(auth): [PM-9674] Remove Deprecated LockComponents (#12453)

This PR deletes the legacy lock components from the Angular clients and also removes feature flag control from the routing. The lock component will now be based entirely on the new, recently refreshed LockComponent in libs/auth/angular.
This commit is contained in:
rr-bw
2024-12-20 10:23:03 -08:00
committed by GitHub
parent 2e6031eee9
commit d209da4c94
14 changed files with 31 additions and 1629 deletions

View File

@@ -15,7 +15,6 @@ import {
unauthGuardFn,
} from "@bitwarden/angular/auth/guards";
import { canAccessFeature } from "@bitwarden/angular/platform/guard/feature-flag.guard";
import { extensionRefreshRedirect } from "@bitwarden/angular/utils/extension-refresh-redirect";
import { NewDeviceVerificationNoticeGuard } from "@bitwarden/angular/vault/guards";
import {
AnonLayoutWrapperComponent,
@@ -23,7 +22,7 @@ import {
LoginComponent,
LoginSecondaryContentComponent,
LockIcon,
LockV2Component,
LockComponent,
LoginViaAuthRequestComponent,
PasswordHintComponent,
RegistrationFinishComponent,
@@ -51,7 +50,6 @@ import { twofactorRefactorSwap } from "../../../../libs/angular/src/utils/two-fa
import { AccessibilityCookieComponent } from "../auth/accessibility-cookie.component";
import { maxAccountsGuardFn } from "../auth/guards/max-accounts.guard";
import { HintComponent } from "../auth/hint.component";
import { LockComponent } from "../auth/lock.component";
import { LoginDecryptionOptionsComponentV1 } from "../auth/login/login-decryption-options/login-decryption-options-v1.component";
import { LoginComponentV1 } from "../auth/login/login-v1.component";
import { LoginViaAuthRequestComponentV1 } from "../auth/login/login-via-auth-request-v1.component";
@@ -81,12 +79,6 @@ const routes: Routes = [
children: [], // Children lets us have an empty component.
canActivate: [redirectGuard({ loggedIn: "/vault", loggedOut: "/login", locked: "/lock" })],
},
{
path: "lock",
component: LockComponent,
canActivate: [lockGuard()],
canMatch: [extensionRefreshRedirect("/lockV2")],
},
...twofactorRefactorSwap(
TwoFactorComponent,
AnonLayoutWrapperComponent,
@@ -373,8 +365,8 @@ const routes: Routes = [
],
},
{
path: "lockV2",
canActivate: [canAccessFeature(FeatureFlag.ExtensionRefresh), lockGuard()],
path: "lock",
canActivate: [lockGuard()],
data: {
pageIcon: LockIcon,
pageTitle: {
@@ -385,7 +377,7 @@ const routes: Routes = [
children: [
{
path: "",
component: LockV2Component,
component: LockComponent,
},
],
},

View File

@@ -13,7 +13,6 @@ import { AccessibilityCookieComponent } from "../auth/accessibility-cookie.compo
import { DeleteAccountComponent } from "../auth/delete-account.component";
import { EnvironmentComponent } from "../auth/environment.component";
import { HintComponent } from "../auth/hint.component";
import { LockComponent } from "../auth/lock.component";
import { LoginModule } from "../auth/login/login.module";
import { RegisterComponent } from "../auth/register.component";
import { RemovePasswordComponent } from "../auth/remove-password.component";
@@ -78,7 +77,6 @@ import { SendComponent } from "./tools/send/send.component";
FolderAddEditComponent,
HeaderComponent,
HintComponent,
LockComponent,
NavComponent,
GeneratorComponent,
PasswordGeneratorHistoryComponent,

View File

@@ -1,80 +0,0 @@
<form id="lock-page" (ngSubmit)="submit()">
<div class="content">
<p aria-hidden="true"><i class="bwi bwi-lock bwi-4x text-muted"></i></p>
<p>{{ "yourVaultIsLocked" | i18n }}</p>
<div class="box last">
<div class="box-content">
<div
class="box-content-row box-content-row-flex"
appBoxRow
*ngIf="pinEnabled || masterPasswordEnabled"
>
<div class="row-main" *ngIf="pinEnabled">
<label for="pin">{{ "pin" | i18n }}</label>
<input
id="pin"
type="{{ showPassword ? 'text' : 'password' }}"
name="PIN"
class="monospaced"
[(ngModel)]="pin"
required
appInputVerbatim
/>
</div>
<div class="row-main" *ngIf="masterPasswordEnabled && !pinEnabled">
<label for="masterPassword">{{ "masterPass" | i18n }}</label>
<input
id="masterPassword"
type="{{ showPassword ? 'text' : 'password' }}"
name="MasterPassword"
aria-describedby="masterPasswordHelp"
class="monospaced"
[(ngModel)]="masterPassword"
required
appInputVerbatim
/>
</div>
<div class="action-buttons">
<button
type="button"
class="row-btn"
appStopClick
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
[attr.aria-pressed]="showPassword"
(click)="togglePassword()"
>
<i
class="bwi bwi-lg"
aria-hidden="true"
[ngClass]="{ 'bwi-eye': !showPassword, 'bwi-eye-slash': showPassword }"
></i>
</button>
</div>
</div>
</div>
<div id="masterPasswordHelp" class="box-footer">
{{ "loggedInAsOn" | i18n: email : webVaultHostname }}
</div>
</div>
<div class="buttons with-rows">
<div class="buttons-row" *ngIf="supportsBiometric && biometricLock && biometricReady">
<button
type="button"
class="btn block"
[ngClass]="{ 'primary font-weight-bold': !pinEnabled && !masterPasswordEnabled }"
(click)="unlockBiometric()"
>
{{ biometricText | i18n }}
</button>
</div>
<div class="buttons-row">
<button type="submit" class="btn primary block" *ngIf="pinEnabled || masterPasswordEnabled">
<i class="bwi bwi-unlock" aria-hidden="true"></i> <b>{{ "unlock" | i18n }}</b>
</button>
<button type="button" class="btn block" (click)="logOut()">
{{ "logOut" | i18n }}
</button>
</div>
</div>
</div>
</form>

View File

@@ -1,478 +0,0 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { NO_ERRORS_SCHEMA } from "@angular/core";
import { ComponentFixture, TestBed, fakeAsync, tick } from "@angular/core/testing";
import { ActivatedRoute } from "@angular/router";
import { MockProxy, mock } from "jest-mock-extended";
import { of } from "rxjs";
import { LockComponent as BaseLockComponent } from "@bitwarden/angular/auth/components/lock.component";
import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe";
import { PinServiceAbstraction } from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service";
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service";
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec";
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
import { UserId } from "@bitwarden/common/types/guid";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
import { DialogService, ToastService } from "@bitwarden/components";
import {
KdfConfigService,
KeyService,
BiometricsService as AbstractBiometricService,
BiometricStateService,
} from "@bitwarden/key-management";
import { BiometricsService } from "../key-management/biometrics/biometrics.service";
import { LockComponent } from "./lock.component";
// ipc mock global
const isWindowVisibleMock = jest.fn();
(global as any).ipc = {
platform: {
isWindowVisible: isWindowVisibleMock,
},
keyManagement: {
biometric: {
enabled: jest.fn(),
},
},
};
describe("LockComponent", () => {
let component: LockComponent;
let fixture: ComponentFixture<LockComponent>;
let stateServiceMock: MockProxy<StateService>;
let biometricStateService: MockProxy<BiometricStateService>;
let biometricsService: MockProxy<BiometricsService>;
let messagingServiceMock: MockProxy<MessagingService>;
let broadcasterServiceMock: MockProxy<BroadcasterService>;
let platformUtilsServiceMock: MockProxy<PlatformUtilsService>;
let activatedRouteMock: MockProxy<ActivatedRoute>;
let mockMasterPasswordService: FakeMasterPasswordService;
let mockToastService: MockProxy<ToastService>;
const mockUserId = Utils.newGuid() as UserId;
const accountService: FakeAccountService = mockAccountServiceWith(mockUserId);
beforeEach(async () => {
stateServiceMock = mock<StateService>();
messagingServiceMock = mock<MessagingService>();
broadcasterServiceMock = mock<BroadcasterService>();
platformUtilsServiceMock = mock<PlatformUtilsService>();
mockToastService = mock<ToastService>();
activatedRouteMock = mock<ActivatedRoute>();
activatedRouteMock.queryParams = mock<ActivatedRoute["queryParams"]>();
mockMasterPasswordService = new FakeMasterPasswordService();
biometricStateService = mock();
biometricStateService.dismissedRequirePasswordOnStartCallout$ = of(false);
biometricStateService.promptAutomatically$ = of(false);
biometricStateService.promptCancelled$ = of(false);
await TestBed.configureTestingModule({
declarations: [LockComponent, I18nPipe],
providers: [
{ provide: InternalMasterPasswordServiceAbstraction, useValue: mockMasterPasswordService },
{
provide: I18nService,
useValue: mock<I18nService>(),
},
{
provide: PlatformUtilsService,
useValue: platformUtilsServiceMock,
},
{
provide: MessagingService,
useValue: messagingServiceMock,
},
{
provide: KeyService,
useValue: mock<KeyService>(),
},
{
provide: VaultTimeoutService,
useValue: mock<VaultTimeoutService>(),
},
{
provide: VaultTimeoutSettingsService,
useValue: mock<VaultTimeoutSettingsService>(),
},
{
provide: EnvironmentService,
useValue: mock<EnvironmentService>(),
},
{
provide: StateService,
useValue: stateServiceMock,
},
{
provide: ApiService,
useValue: mock<ApiService>(),
},
{
provide: ActivatedRoute,
useValue: activatedRouteMock,
},
{
provide: BroadcasterService,
useValue: broadcasterServiceMock,
},
{
provide: PolicyApiServiceAbstraction,
useValue: mock<PolicyApiServiceAbstraction>(),
},
{
provide: InternalPolicyService,
useValue: mock<InternalPolicyService>(),
},
{
provide: PasswordStrengthServiceAbstraction,
useValue: mock<PasswordStrengthServiceAbstraction>(),
},
{
provide: LogService,
useValue: mock<LogService>(),
},
{
provide: DialogService,
useValue: mock<DialogService>(),
},
{
provide: DeviceTrustServiceAbstraction,
useValue: mock<DeviceTrustServiceAbstraction>(),
},
{
provide: UserVerificationService,
useValue: mock<UserVerificationService>(),
},
{
provide: PinServiceAbstraction,
useValue: mock<PinServiceAbstraction>(),
},
{
provide: BiometricStateService,
useValue: biometricStateService,
},
{
provide: AbstractBiometricService,
useValue: biometricsService,
},
{
provide: AccountService,
useValue: accountService,
},
{
provide: AuthService,
useValue: mock(),
},
{
provide: KdfConfigService,
useValue: mock<KdfConfigService>(),
},
{
provide: SyncService,
useValue: mock<SyncService>(),
},
{
provide: ToastService,
useValue: mockToastService,
},
],
schemas: [NO_ERRORS_SCHEMA],
}).compileComponents();
fixture = TestBed.createComponent(LockComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
afterEach(() => {
jest.resetAllMocks();
});
describe("ngOnInit", () => {
it("should call super.ngOnInit() once", async () => {
const superNgOnInitSpy = jest.spyOn(BaseLockComponent.prototype, "ngOnInit");
await component.ngOnInit();
expect(superNgOnInitSpy).toHaveBeenCalledTimes(1);
});
it('should set "autoPromptBiometric" to true if "biometricState.promptAutomatically$" resolves to true', async () => {
biometricStateService.promptAutomatically$ = of(true);
await component.ngOnInit();
expect(component["autoPromptBiometric"]).toBe(true);
});
it('should set "autoPromptBiometric" to false if "biometricState.promptAutomatically$" resolves to false', async () => {
biometricStateService.promptAutomatically$ = of(false);
await component.ngOnInit();
expect(component["autoPromptBiometric"]).toBe(false);
});
it('should set "biometricReady" to true if "stateService.getBiometricReady()" resolves to true', async () => {
component["canUseBiometric"] = jest.fn().mockResolvedValue(true);
await component.ngOnInit();
expect(component["biometricReady"]).toBe(true);
});
it('should set "biometricReady" to false if "stateService.getBiometricReady()" resolves to false', async () => {
component["canUseBiometric"] = jest.fn().mockResolvedValue(false);
await component.ngOnInit();
expect(component["biometricReady"]).toBe(false);
});
it("should call displayBiometricUpdateWarning", async () => {
component["displayBiometricUpdateWarning"] = jest.fn();
await component.ngOnInit();
expect(component["displayBiometricUpdateWarning"]).toHaveBeenCalledTimes(1);
});
it("should call delayedAskForBiometric", async () => {
component["delayedAskForBiometric"] = jest.fn();
await component.ngOnInit();
expect(component["delayedAskForBiometric"]).toHaveBeenCalledTimes(1);
expect(component["delayedAskForBiometric"]).toHaveBeenCalledWith(500);
});
it("should call delayedAskForBiometric when queryParams change", async () => {
activatedRouteMock.queryParams = of({ promptBiometric: true });
component["delayedAskForBiometric"] = jest.fn();
await component.ngOnInit();
expect(component["delayedAskForBiometric"]).toHaveBeenCalledTimes(1);
expect(component["delayedAskForBiometric"]).toHaveBeenCalledWith(500);
});
it("should call messagingService.send", async () => {
await component.ngOnInit();
expect(messagingServiceMock.send).toHaveBeenCalledWith("getWindowIsFocused");
});
describe("broadcasterService.subscribe", () => {
it('should call onWindowHidden() when "broadcasterService.subscribe" is called with "windowHidden"', async () => {
component["onWindowHidden"] = jest.fn();
await component.ngOnInit();
broadcasterServiceMock.subscribe.mock.calls[0][1]({ command: "windowHidden" });
expect(component["onWindowHidden"]).toHaveBeenCalledTimes(1);
});
it('should call focusInput() when "broadcasterService.subscribe" is called with "windowIsFocused" is true and deferFocus is false', async () => {
component["focusInput"] = jest.fn();
component["deferFocus"] = null;
await component.ngOnInit();
broadcasterServiceMock.subscribe.mock.calls[0][1]({
command: "windowIsFocused",
windowIsFocused: true,
} as any);
expect(component["deferFocus"]).toBe(false);
expect(component["focusInput"]).toHaveBeenCalledTimes(1);
});
it('should not call focusInput() when "broadcasterService.subscribe" is called with "windowIsFocused" is true and deferFocus is true', async () => {
component["focusInput"] = jest.fn();
component["deferFocus"] = null;
await component.ngOnInit();
broadcasterServiceMock.subscribe.mock.calls[0][1]({
command: "windowIsFocused",
windowIsFocused: false,
} as any);
expect(component["deferFocus"]).toBe(true);
expect(component["focusInput"]).toHaveBeenCalledTimes(0);
});
it('should call focusInput() when "broadcasterService.subscribe" is called with "windowIsFocused" is true and deferFocus is true', async () => {
component["focusInput"] = jest.fn();
component["deferFocus"] = true;
await component.ngOnInit();
broadcasterServiceMock.subscribe.mock.calls[0][1]({
command: "windowIsFocused",
windowIsFocused: true,
} as any);
expect(component["deferFocus"]).toBe(false);
expect(component["focusInput"]).toHaveBeenCalledTimes(1);
});
it('should not call focusInput() when "broadcasterService.subscribe" is called with "windowIsFocused" is false and deferFocus is true', async () => {
component["focusInput"] = jest.fn();
component["deferFocus"] = true;
await component.ngOnInit();
broadcasterServiceMock.subscribe.mock.calls[0][1]({
command: "windowIsFocused",
windowIsFocused: false,
} as any);
expect(component["deferFocus"]).toBe(true);
expect(component["focusInput"]).toHaveBeenCalledTimes(0);
});
});
});
describe("ngOnDestroy", () => {
it("should call super.ngOnDestroy()", () => {
const superNgOnDestroySpy = jest.spyOn(BaseLockComponent.prototype, "ngOnDestroy");
component.ngOnDestroy();
expect(superNgOnDestroySpy).toHaveBeenCalledTimes(1);
});
it("should call broadcasterService.unsubscribe()", () => {
component.ngOnDestroy();
expect(broadcasterServiceMock.unsubscribe).toHaveBeenCalledTimes(1);
});
});
describe("focusInput", () => {
it('should call "focus" on #pin input if pinEnabled is true', () => {
component["pinEnabled"] = true;
global.document.getElementById = jest.fn().mockReturnValue({ focus: jest.fn() });
component["focusInput"]();
expect(global.document.getElementById).toHaveBeenCalledWith("pin");
});
it('should call "focus" on #masterPassword input if pinEnabled is false', () => {
component["pinEnabled"] = false;
global.document.getElementById = jest.fn().mockReturnValue({ focus: jest.fn() });
component["focusInput"]();
expect(global.document.getElementById).toHaveBeenCalledWith("masterPassword");
});
});
describe("delayedAskForBiometric", () => {
beforeEach(() => {
component["supportsBiometric"] = true;
component["autoPromptBiometric"] = true;
});
it('should wait for "delay" milliseconds', fakeAsync(async () => {
const delaySpy = jest.spyOn(global, "setTimeout");
// 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
component["delayedAskForBiometric"](5000);
tick(4000);
component["biometricAsked"] = false;
tick(1000);
component["biometricAsked"] = true;
expect(delaySpy).toHaveBeenCalledWith(expect.any(Function), 5000);
}));
it('should return; if "params" is defined and "params.promptBiometric" is false', fakeAsync(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
component["delayedAskForBiometric"](5000, { promptBiometric: false });
tick(5000);
expect(component["biometricAsked"]).toBe(false);
}));
it('should not return; if "params" is defined and "params.promptBiometric" is true', fakeAsync(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
component["delayedAskForBiometric"](5000, { promptBiometric: true });
tick(5000);
expect(component["biometricAsked"]).toBe(true);
}));
it('should not return; if "params" is undefined', fakeAsync(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
component["delayedAskForBiometric"](5000);
tick(5000);
expect(component["biometricAsked"]).toBe(true);
}));
it('should return; if "supportsBiometric" is false', fakeAsync(async () => {
component["supportsBiometric"] = false;
// 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
component["delayedAskForBiometric"](5000);
tick(5000);
expect(component["biometricAsked"]).toBe(false);
}));
it('should return; if "autoPromptBiometric" is false', fakeAsync(async () => {
component["autoPromptBiometric"] = false;
// 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
component["delayedAskForBiometric"](5000);
tick(5000);
expect(component["biometricAsked"]).toBe(false);
}));
it("should call unlockBiometric() if biometricAsked is false and window is visible", fakeAsync(async () => {
isWindowVisibleMock.mockResolvedValue(true);
component["unlockBiometric"] = jest.fn();
component["biometricAsked"] = false;
// 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
component["delayedAskForBiometric"](5000);
tick(5000);
expect(component["unlockBiometric"]).toHaveBeenCalledTimes(1);
}));
it("should not call unlockBiometric() if biometricAsked is false and window is not visible", fakeAsync(async () => {
isWindowVisibleMock.mockResolvedValue(false);
component["unlockBiometric"] = jest.fn();
component["biometricAsked"] = false;
// 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
component["delayedAskForBiometric"](5000);
tick(5000);
expect(component["unlockBiometric"]).toHaveBeenCalledTimes(0);
}));
it("should not call unlockBiometric() if biometricAsked is true", fakeAsync(async () => {
isWindowVisibleMock.mockResolvedValue(true);
component["unlockBiometric"] = jest.fn();
component["biometricAsked"] = true;
// 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
component["delayedAskForBiometric"](5000);
tick(5000);
expect(component["unlockBiometric"]).toHaveBeenCalledTimes(0);
}));
});
describe("canUseBiometric", () => {
it("should call biometric.enabled with current active user", async () => {
await component["canUseBiometric"]();
expect(ipc.keyManagement.biometric.enabled).toHaveBeenCalledWith(mockUserId);
});
});
it('onWindowHidden() should set "showPassword" to false', () => {
component["showPassword"] = true;
component["onWindowHidden"]();
expect(component["showPassword"]).toBe(false);
});
});

View File

@@ -1,235 +0,0 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Component, NgZone, OnDestroy, OnInit } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import { firstValueFrom, map, switchMap } from "rxjs";
import { LockComponent as BaseLockComponent } from "@bitwarden/angular/auth/components/lock.component";
import { PinServiceAbstraction } from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service";
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { DeviceType } from "@bitwarden/common/enums";
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
import { DialogService, ToastService } from "@bitwarden/components";
import {
KdfConfigService,
KeyService,
BiometricsService,
BiometricStateService,
} from "@bitwarden/key-management";
const BroadcasterSubscriptionId = "LockComponent";
@Component({
selector: "app-lock",
templateUrl: "lock.component.html",
})
export class LockComponent extends BaseLockComponent implements OnInit, OnDestroy {
private deferFocus: boolean = null;
protected biometricReady = false;
private biometricAsked = false;
private autoPromptBiometric = false;
private timerId: any;
constructor(
masterPasswordService: InternalMasterPasswordServiceAbstraction,
router: Router,
i18nService: I18nService,
platformUtilsService: PlatformUtilsService,
messagingService: MessagingService,
keyService: KeyService,
vaultTimeoutService: VaultTimeoutService,
vaultTimeoutSettingsService: VaultTimeoutSettingsService,
environmentService: EnvironmentService,
protected override stateService: StateService,
apiService: ApiService,
private route: ActivatedRoute,
private broadcasterService: BroadcasterService,
ngZone: NgZone,
policyApiService: PolicyApiServiceAbstraction,
policyService: InternalPolicyService,
passwordStrengthService: PasswordStrengthServiceAbstraction,
logService: LogService,
dialogService: DialogService,
deviceTrustService: DeviceTrustServiceAbstraction,
userVerificationService: UserVerificationService,
pinService: PinServiceAbstraction,
biometricStateService: BiometricStateService,
biometricsService: BiometricsService,
accountService: AccountService,
authService: AuthService,
kdfConfigService: KdfConfigService,
syncService: SyncService,
toastService: ToastService,
) {
super(
masterPasswordService,
router,
i18nService,
platformUtilsService,
messagingService,
keyService,
vaultTimeoutService,
vaultTimeoutSettingsService,
environmentService,
stateService,
apiService,
logService,
ngZone,
policyApiService,
policyService,
passwordStrengthService,
dialogService,
deviceTrustService,
userVerificationService,
pinService,
biometricStateService,
biometricsService,
accountService,
authService,
kdfConfigService,
syncService,
toastService,
);
}
async ngOnInit() {
await super.ngOnInit();
this.autoPromptBiometric = await firstValueFrom(
this.biometricStateService.promptAutomatically$,
);
this.biometricReady = await this.canUseBiometric();
await this.displayBiometricUpdateWarning();
// 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.delayedAskForBiometric(500);
this.route.queryParams.pipe(switchMap((params) => this.delayedAskForBiometric(500, params)));
this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => {
this.ngZone.run(() => {
switch (message.command) {
case "windowHidden":
this.onWindowHidden();
break;
case "windowIsFocused":
if (this.deferFocus === null) {
this.deferFocus = !message.windowIsFocused;
if (!this.deferFocus) {
this.focusInput();
}
} else if (this.deferFocus && message.windowIsFocused) {
this.focusInput();
this.deferFocus = false;
}
break;
default:
}
});
});
this.messagingService.send("getWindowIsFocused");
// start background listener until destroyed on interval
this.timerId = setInterval(async () => {
this.supportsBiometric = await this.biometricsService.supportsBiometric();
this.biometricReady = await this.canUseBiometric();
}, 1000);
}
ngOnDestroy() {
super.ngOnDestroy();
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
clearInterval(this.timerId);
}
onWindowHidden() {
this.showPassword = false;
}
private async delayedAskForBiometric(delay: number, params?: any) {
await new Promise((resolve) => setTimeout(resolve, delay));
if (params && !params.promptBiometric) {
return;
}
if (!this.supportsBiometric || !this.autoPromptBiometric || this.biometricAsked) {
return;
}
if (await firstValueFrom(this.biometricStateService.promptCancelled$)) {
return;
}
this.biometricAsked = true;
if (await ipc.platform.isWindowVisible()) {
// 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.unlockBiometric();
}
}
private async canUseBiometric() {
const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(map((a) => a?.id)));
return await ipc.keyManagement.biometric.enabled(userId);
}
private focusInput() {
document.getElementById(this.pinEnabled ? "pin" : "masterPassword")?.focus();
}
private async displayBiometricUpdateWarning(): Promise<void> {
if (await firstValueFrom(this.biometricStateService.dismissedRequirePasswordOnStartCallout$)) {
return;
}
if (this.platformUtilsService.getDevice() !== DeviceType.WindowsDesktop) {
return;
}
if (await firstValueFrom(this.biometricStateService.biometricUnlockEnabled$)) {
const response = await this.dialogService.openSimpleDialog({
title: { key: "windowsBiometricUpdateWarningTitle" },
content: { key: "windowsBiometricUpdateWarning" },
type: "warning",
});
await this.biometricStateService.setRequirePasswordOnStart(response);
if (response) {
await this.biometricStateService.setPromptAutomatically(false);
}
this.supportsBiometric = await this.canUseBiometric();
await this.biometricStateService.setDismissedRequirePasswordOnStartCallout();
}
}
get biometricText() {
switch (this.platformUtilsService.getDevice()) {
case DeviceType.MacOsDesktop:
return "unlockWithTouchId";
case DeviceType.WindowsDesktop:
return "unlockWithWindowsHello";
case DeviceType.LinuxDesktop:
return "unlockWithPolkit";
default:
throw new Error("Unsupported platform");
}
}
}