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:
@@ -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,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user