mirror of
https://github.com/bitwarden/browser.git
synced 2024-12-21 16:18:28 +01: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:
parent
2e6031eee9
commit
d209da4c94
@ -1,100 +0,0 @@
|
||||
<form (ngSubmit)="submit()">
|
||||
<app-header>
|
||||
<div class="left"></div>
|
||||
<h1 class="center">
|
||||
<span class="title">{{ "verifyIdentity" | i18n }}</span>
|
||||
</h1>
|
||||
<div class="right">
|
||||
<button type="submit" *ngIf="pinEnabled || masterPasswordEnabled">
|
||||
{{ "unlock" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</app-header>
|
||||
<main tabindex="-1">
|
||||
<ng-container *ngIf="fido2PopoutSessionData$ | async as fido2Data">
|
||||
<div class="box">
|
||||
<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 }}"
|
||||
(click)="togglePassword()"
|
||||
[attr.aria-pressed]="showPassword"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-lg"
|
||||
[ngClass]="{ 'bwi-eye': !showPassword, 'bwi-eye-slash': showPassword }"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="masterPasswordHelp" class="box-footer">
|
||||
<p>
|
||||
{{
|
||||
fido2Data.isFido2Session
|
||||
? ("yourPasskeyIsLocked" | i18n)
|
||||
: ("yourVaultIsLocked" | i18n)
|
||||
}}
|
||||
</p>
|
||||
{{ "loggedInAsOn" | i18n: email : webVaultHostname }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="box" *ngIf="biometricLock">
|
||||
<div class="box-footer no-pad">
|
||||
<button
|
||||
type="button"
|
||||
class="btn primary block"
|
||||
(click)="unlockBiometric()"
|
||||
appStopClick
|
||||
[disabled]="pendingBiometric"
|
||||
>
|
||||
{{ "unlockWithBiometrics" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-center" *ngIf="!fido2Data.isFido2Session">
|
||||
<button type="button" appStopClick (click)="logOut()">{{ "logOut" | i18n }}</button>
|
||||
</p>
|
||||
<app-callout *ngIf="biometricError" type="danger">{{ biometricError }}</app-callout>
|
||||
<p class="text-center text-muted" *ngIf="pendingBiometric">
|
||||
<i class="bwi bwi-spinner bwi-spin" aria-hidden="true"></i> {{ "awaitDesktop" | i18n }}
|
||||
</p>
|
||||
|
||||
<app-fido2-use-browser-link-v1></app-fido2-use-browser-link-v1>
|
||||
</ng-container>
|
||||
</main>
|
||||
</form>
|
@ -1,185 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Component, NgZone, OnInit } from "@angular/core";
|
||||
import { Router } from "@angular/router";
|
||||
import { firstValueFrom } 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 { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||
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";
|
||||
|
||||
import { BiometricErrors, BiometricErrorTypes } from "../../models/biometricErrors";
|
||||
import { BrowserRouterService } from "../../platform/popup/services/browser-router.service";
|
||||
import { fido2PopoutSessionData$ } from "../../vault/popup/utils/fido2-popout-session-data";
|
||||
|
||||
@Component({
|
||||
selector: "app-lock",
|
||||
templateUrl: "lock.component.html",
|
||||
})
|
||||
export class LockComponent extends BaseLockComponent implements OnInit {
|
||||
private isInitialLockScreen: boolean;
|
||||
|
||||
biometricError: string;
|
||||
pendingBiometric = false;
|
||||
fido2PopoutSessionData$ = fido2PopoutSessionData$();
|
||||
|
||||
constructor(
|
||||
masterPasswordService: InternalMasterPasswordServiceAbstraction,
|
||||
router: Router,
|
||||
i18nService: I18nService,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
messagingService: MessagingService,
|
||||
keyService: KeyService,
|
||||
vaultTimeoutService: VaultTimeoutService,
|
||||
vaultTimeoutSettingsService: VaultTimeoutSettingsService,
|
||||
environmentService: EnvironmentService,
|
||||
stateService: StateService,
|
||||
apiService: ApiService,
|
||||
logService: LogService,
|
||||
ngZone: NgZone,
|
||||
policyApiService: PolicyApiServiceAbstraction,
|
||||
policyService: InternalPolicyService,
|
||||
passwordStrengthService: PasswordStrengthServiceAbstraction,
|
||||
authService: AuthService,
|
||||
dialogService: DialogService,
|
||||
deviceTrustService: DeviceTrustServiceAbstraction,
|
||||
userVerificationService: UserVerificationService,
|
||||
pinService: PinServiceAbstraction,
|
||||
private routerService: BrowserRouterService,
|
||||
biometricStateService: BiometricStateService,
|
||||
biometricsService: BiometricsService,
|
||||
accountService: AccountService,
|
||||
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,
|
||||
);
|
||||
this.successRoute = "/tabs/current";
|
||||
this.isInitialLockScreen = (window as any).previousPopupUrl == null;
|
||||
|
||||
this.onSuccessfulSubmit = async () => {
|
||||
const previousUrl = this.routerService.getPreviousUrl();
|
||||
if (previousUrl) {
|
||||
// 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.router.navigateByUrl(previousUrl);
|
||||
} else {
|
||||
// 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.router.navigate([this.successRoute]);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
await super.ngOnInit();
|
||||
const autoBiometricsPrompt = await firstValueFrom(
|
||||
this.biometricStateService.promptAutomatically$,
|
||||
);
|
||||
|
||||
window.setTimeout(async () => {
|
||||
document.getElementById(this.pinEnabled ? "pin" : "masterPassword")?.focus();
|
||||
if (
|
||||
this.biometricLock &&
|
||||
autoBiometricsPrompt &&
|
||||
this.isInitialLockScreen &&
|
||||
(await this.authService.getAuthStatus()) === AuthenticationStatus.Locked
|
||||
) {
|
||||
await this.unlockBiometric(true);
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
override async unlockBiometric(automaticPrompt: boolean = false): Promise<boolean> {
|
||||
if (!this.biometricLock) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.biometricError = null;
|
||||
|
||||
let success;
|
||||
try {
|
||||
const available = await super.isBiometricUnlockAvailable();
|
||||
if (!available) {
|
||||
if (!automaticPrompt) {
|
||||
await this.dialogService.openSimpleDialog({
|
||||
type: "warning",
|
||||
title: { key: "biometricsNotAvailableTitle" },
|
||||
content: { key: "biometricsNotAvailableDesc" },
|
||||
acceptButtonText: { key: "ok" },
|
||||
cancelButtonText: null,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
this.pendingBiometric = true;
|
||||
success = await super.unlockBiometric();
|
||||
}
|
||||
} catch (e) {
|
||||
const error = BiometricErrors[e?.message as BiometricErrorTypes];
|
||||
|
||||
if (error == null) {
|
||||
this.logService.error("Unknown error: " + e);
|
||||
return false;
|
||||
}
|
||||
|
||||
this.biometricError = this.i18nService.t(error.description);
|
||||
} finally {
|
||||
this.pendingBiometric = false;
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
}
|
@ -17,7 +17,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 { extensionRefreshSwap } from "@bitwarden/angular/utils/extension-refresh-swap";
|
||||
import { NewDeviceVerificationNoticeGuard } from "@bitwarden/angular/vault/guards";
|
||||
import {
|
||||
@ -26,7 +25,7 @@ import {
|
||||
LoginComponent,
|
||||
LoginSecondaryContentComponent,
|
||||
LockIcon,
|
||||
LockV2Component,
|
||||
LockComponent,
|
||||
LoginViaAuthRequestComponent,
|
||||
PasswordHintComponent,
|
||||
RegistrationFinishComponent,
|
||||
@ -60,7 +59,6 @@ import {
|
||||
} from "../auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component";
|
||||
import { HintComponent } from "../auth/popup/hint.component";
|
||||
import { HomeComponent } from "../auth/popup/home.component";
|
||||
import { LockComponent } from "../auth/popup/lock.component";
|
||||
import { LoginDecryptionOptionsComponentV1 } from "../auth/popup/login-decryption-options/login-decryption-options-v1.component";
|
||||
import { LoginComponentV1 } from "../auth/popup/login-v1.component";
|
||||
import { LoginViaAuthRequestComponentV1 } from "../auth/popup/login-via-auth-request-v1.component";
|
||||
@ -173,13 +171,6 @@ const routes: Routes = [
|
||||
canActivate: [fido2AuthGuard],
|
||||
data: { elevation: 1 } satisfies RouteDataProperties,
|
||||
}),
|
||||
{
|
||||
path: "lock",
|
||||
component: LockComponent,
|
||||
canActivate: [lockGuard()],
|
||||
canMatch: [extensionRefreshRedirect("/lockV2")],
|
||||
data: { elevation: 1, doNotSaveUrl: true } satisfies RouteDataProperties,
|
||||
},
|
||||
...twofactorRefactorSwap(
|
||||
TwoFactorComponent,
|
||||
AnonLayoutWrapperComponent,
|
||||
@ -650,8 +641,8 @@ const routes: Routes = [
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "lockV2",
|
||||
canActivate: [canAccessFeature(FeatureFlag.ExtensionRefresh), lockGuard()],
|
||||
path: "lock",
|
||||
canActivate: [lockGuard()],
|
||||
data: {
|
||||
pageIcon: LockIcon,
|
||||
pageTitle: {
|
||||
@ -662,19 +653,19 @@ const routes: Routes = [
|
||||
elevation: 1,
|
||||
/**
|
||||
* This ensures that in a passkey flow the `/fido2?<queryParams>` URL does not get
|
||||
* overwritten in the `BrowserRouterService` by the `/lockV2` route. This way, after
|
||||
* overwritten in the `BrowserRouterService` by the `/lock` route. This way, after
|
||||
* unlocking, the user can be redirected back to the `/fido2?<queryParams>` URL.
|
||||
*
|
||||
* Also, this prevents a routing loop when using biometrics to unlock the vault in MV2 (Firefox),
|
||||
* locking up the browser (https://bitwarden.atlassian.net/browse/PM-16116). This involves the
|
||||
* `popup-router-cache.service` pushing the `lockV2` route to the history.
|
||||
* `popup-router-cache.service` pushing the `lock` route to the history.
|
||||
*/
|
||||
doNotSaveUrl: true,
|
||||
} satisfies ExtensionAnonLayoutWrapperData & RouteDataProperties,
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
component: LockV2Component,
|
||||
component: LockComponent,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -23,7 +23,6 @@ import { EnvironmentComponent } from "../auth/popup/environment.component";
|
||||
import { ExtensionAnonLayoutWrapperComponent } from "../auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component";
|
||||
import { HintComponent } from "../auth/popup/hint.component";
|
||||
import { HomeComponent } from "../auth/popup/home.component";
|
||||
import { LockComponent } from "../auth/popup/lock.component";
|
||||
import { LoginDecryptionOptionsComponentV1 } from "../auth/popup/login-decryption-options/login-decryption-options-v1.component";
|
||||
import { LoginComponentV1 } from "../auth/popup/login-v1.component";
|
||||
import { LoginViaAuthRequestComponentV1 } from "../auth/popup/login-via-auth-request-v1.component";
|
||||
@ -156,7 +155,6 @@ import "../platform/popup/locales";
|
||||
VaultFilterComponent,
|
||||
HintComponent,
|
||||
HomeComponent,
|
||||
LockComponent,
|
||||
LoginViaAuthRequestComponentV1,
|
||||
LoginComponentV1,
|
||||
LoginDecryptionOptionsComponentV1,
|
||||
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
@ -1,27 +0,0 @@
|
||||
<form [bitSubmit]="submit" [formGroup]="formGroup">
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "masterPass" | i18n }}</bit-label>
|
||||
<input
|
||||
type="password"
|
||||
formControlName="masterPassword"
|
||||
bitInput
|
||||
appAutofocus
|
||||
name="masterPassword"
|
||||
required
|
||||
/>
|
||||
<button type="button" bitIconButton bitSuffix bitPasswordInputToggle></button>
|
||||
<bit-hint>{{ "loggedInAsEmailOn" | i18n: email : webVaultHostname }}</bit-hint>
|
||||
</bit-form-field>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="tw-flex tw-gap-2">
|
||||
<button type="submit" bitButton bitFormButton buttonType="primary" block>
|
||||
<i class="bwi bwi-unlock" aria-hidden="true"></i>
|
||||
{{ "unlock" | i18n }}
|
||||
</button>
|
||||
<button type="button" bitButton bitFormButton block (click)="logOut()">
|
||||
{{ "logOut" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
@ -1,51 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Component, OnInit, inject } from "@angular/core";
|
||||
import { FormBuilder, Validators } from "@angular/forms";
|
||||
|
||||
import { LockComponent as BaseLockComponent } from "@bitwarden/angular/auth/components/lock.component";
|
||||
|
||||
import { SharedModule } from "../shared";
|
||||
|
||||
@Component({
|
||||
selector: "app-lock",
|
||||
templateUrl: "lock.component.html",
|
||||
standalone: true,
|
||||
imports: [SharedModule],
|
||||
})
|
||||
export class LockComponent extends BaseLockComponent implements OnInit {
|
||||
formBuilder = inject(FormBuilder);
|
||||
|
||||
formGroup = this.formBuilder.group({
|
||||
masterPassword: ["", { validators: Validators.required, updateOn: "submit" }],
|
||||
});
|
||||
|
||||
get masterPasswordFormControl() {
|
||||
return this.formGroup.controls.masterPassword;
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
await super.ngOnInit();
|
||||
|
||||
this.masterPasswordFormControl.setValue(this.masterPassword);
|
||||
|
||||
this.onSuccessfulSubmit = async () => {
|
||||
await this.router.navigateByUrl(this.successRoute);
|
||||
};
|
||||
}
|
||||
|
||||
async superSubmit() {
|
||||
await super.submit();
|
||||
}
|
||||
|
||||
submit = async () => {
|
||||
this.formGroup.markAllAsTouched();
|
||||
|
||||
if (this.formGroup.invalid) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.masterPassword = this.masterPasswordFormControl.value;
|
||||
await this.superSubmit();
|
||||
};
|
||||
}
|
@ -12,7 +12,6 @@ import {
|
||||
} from "@bitwarden/angular/auth/guards";
|
||||
import { canAccessFeature } from "@bitwarden/angular/platform/guard/feature-flag.guard";
|
||||
import { generatorSwap } from "@bitwarden/angular/tools/generator/generator-swap";
|
||||
import { extensionRefreshSwap } from "@bitwarden/angular/utils/extension-refresh-swap";
|
||||
import { NewDeviceVerificationNoticeGuard } from "@bitwarden/angular/vault/guards";
|
||||
import {
|
||||
AnonLayoutWrapperComponent,
|
||||
@ -26,7 +25,7 @@ import {
|
||||
RegistrationLinkExpiredComponent,
|
||||
LoginComponent,
|
||||
LoginSecondaryContentComponent,
|
||||
LockV2Component,
|
||||
LockComponent,
|
||||
LockIcon,
|
||||
TwoFactorTimeoutIcon,
|
||||
UserLockIcon,
|
||||
@ -56,7 +55,6 @@ import { FamiliesForEnterpriseSetupComponent } from "./admin-console/organizatio
|
||||
import { CreateOrganizationComponent } from "./admin-console/settings/create-organization.component";
|
||||
import { deepLinkGuard } from "./auth/guards/deep-link.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";
|
||||
@ -509,44 +507,23 @@ const routes: Routes = [
|
||||
},
|
||||
},
|
||||
},
|
||||
...extensionRefreshSwap(
|
||||
LockComponent,
|
||||
LockV2Component,
|
||||
{
|
||||
path: "lock",
|
||||
canActivate: [deepLinkGuard(), lockGuard()],
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
component: LockComponent,
|
||||
},
|
||||
],
|
||||
data: {
|
||||
pageTitle: {
|
||||
key: "yourVaultIsLockedV2",
|
||||
},
|
||||
pageIcon: LockIcon,
|
||||
showReadonlyHostname: true,
|
||||
} satisfies AnonLayoutWrapperData,
|
||||
},
|
||||
{
|
||||
path: "lock",
|
||||
canActivate: [deepLinkGuard(), lockGuard()],
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
component: LockV2Component,
|
||||
},
|
||||
],
|
||||
data: {
|
||||
pageTitle: {
|
||||
key: "yourVaultIsLockedV2",
|
||||
},
|
||||
pageIcon: LockIcon,
|
||||
showReadonlyHostname: true,
|
||||
} satisfies AnonLayoutWrapperData,
|
||||
},
|
||||
),
|
||||
{
|
||||
path: "lock",
|
||||
canActivate: [deepLinkGuard(), lockGuard()],
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
component: LockComponent,
|
||||
},
|
||||
],
|
||||
data: {
|
||||
pageTitle: {
|
||||
key: "yourVaultIsLockedV2",
|
||||
},
|
||||
pageIcon: LockIcon,
|
||||
showReadonlyHostname: true,
|
||||
} satisfies AnonLayoutWrapperData,
|
||||
},
|
||||
{
|
||||
path: "2fa",
|
||||
canActivate: [unauthGuardFn()],
|
||||
|
@ -1,398 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Directive, NgZone, OnDestroy, OnInit } from "@angular/core";
|
||||
import { Router } from "@angular/router";
|
||||
import { firstValueFrom, Subject } from "rxjs";
|
||||
import { concatMap, map, take, takeUntil } from "rxjs/operators";
|
||||
|
||||
import { PinServiceAbstraction, PinLockType } 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 { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
|
||||
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 { VerificationType } from "@bitwarden/common/auth/enums/verification-type";
|
||||
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
|
||||
import {
|
||||
MasterPasswordVerification,
|
||||
MasterPasswordVerificationResponse,
|
||||
} from "@bitwarden/common/auth/types/verification";
|
||||
import { ClientType } from "@bitwarden/common/enums";
|
||||
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 { KeySuffixOptions } from "@bitwarden/common/platform/enums";
|
||||
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { UserKey } from "@bitwarden/common/types/key";
|
||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import {
|
||||
KdfConfigService,
|
||||
KeyService,
|
||||
BiometricStateService,
|
||||
BiometricsService,
|
||||
} from "@bitwarden/key-management";
|
||||
|
||||
@Directive()
|
||||
export class LockComponent implements OnInit, OnDestroy {
|
||||
masterPassword = "";
|
||||
pin = "";
|
||||
showPassword = false;
|
||||
email: string;
|
||||
pinEnabled = false;
|
||||
masterPasswordEnabled = false;
|
||||
webVaultHostname = "";
|
||||
formPromise: Promise<MasterPasswordVerificationResponse>;
|
||||
supportsBiometric: boolean;
|
||||
biometricLock: boolean;
|
||||
|
||||
private activeUserId: UserId;
|
||||
protected successRoute = "vault";
|
||||
protected forcePasswordResetRoute = "update-temp-password";
|
||||
protected onSuccessfulSubmit: () => Promise<void>;
|
||||
|
||||
private invalidPinAttempts = 0;
|
||||
private pinLockType: PinLockType;
|
||||
|
||||
private enforcedMasterPasswordOptions: MasterPasswordPolicyOptions = undefined;
|
||||
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
constructor(
|
||||
protected masterPasswordService: InternalMasterPasswordServiceAbstraction,
|
||||
protected router: Router,
|
||||
protected i18nService: I18nService,
|
||||
protected platformUtilsService: PlatformUtilsService,
|
||||
protected messagingService: MessagingService,
|
||||
protected keyService: KeyService,
|
||||
protected vaultTimeoutService: VaultTimeoutService,
|
||||
protected vaultTimeoutSettingsService: VaultTimeoutSettingsService,
|
||||
protected environmentService: EnvironmentService,
|
||||
protected stateService: StateService,
|
||||
protected apiService: ApiService,
|
||||
protected logService: LogService,
|
||||
protected ngZone: NgZone,
|
||||
protected policyApiService: PolicyApiServiceAbstraction,
|
||||
protected policyService: InternalPolicyService,
|
||||
protected passwordStrengthService: PasswordStrengthServiceAbstraction,
|
||||
protected dialogService: DialogService,
|
||||
protected deviceTrustService: DeviceTrustServiceAbstraction,
|
||||
protected userVerificationService: UserVerificationService,
|
||||
protected pinService: PinServiceAbstraction,
|
||||
protected biometricStateService: BiometricStateService,
|
||||
protected biometricsService: BiometricsService,
|
||||
protected accountService: AccountService,
|
||||
protected authService: AuthService,
|
||||
protected kdfConfigService: KdfConfigService,
|
||||
protected syncService: SyncService,
|
||||
protected toastService: ToastService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
this.accountService.activeAccount$
|
||||
.pipe(
|
||||
concatMap(async (account) => {
|
||||
this.activeUserId = account?.id;
|
||||
await this.load(account?.id);
|
||||
}),
|
||||
takeUntil(this.destroy$),
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
async submit() {
|
||||
if (this.pinEnabled) {
|
||||
return await this.handlePinRequiredUnlock();
|
||||
}
|
||||
|
||||
await this.handleMasterPasswordRequiredUnlock();
|
||||
}
|
||||
|
||||
async logOut() {
|
||||
const confirmed = await this.dialogService.openSimpleDialog({
|
||||
title: { key: "logOut" },
|
||||
content: { key: "logOutConfirmation" },
|
||||
acceptButtonText: { key: "logOut" },
|
||||
type: "warning",
|
||||
});
|
||||
|
||||
if (confirmed) {
|
||||
this.messagingService.send("logout", { userId: this.activeUserId });
|
||||
}
|
||||
}
|
||||
|
||||
async unlockBiometric(): Promise<boolean> {
|
||||
if (!this.biometricLock) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.biometricStateService.setUserPromptCancelled();
|
||||
const userKey = await this.keyService.getUserKeyFromStorage(
|
||||
KeySuffixOptions.Biometric,
|
||||
this.activeUserId,
|
||||
);
|
||||
|
||||
if (userKey) {
|
||||
await this.setUserKeyAndContinue(userKey, this.activeUserId, false);
|
||||
}
|
||||
|
||||
return !!userKey;
|
||||
}
|
||||
|
||||
async isBiometricUnlockAvailable(): Promise<boolean> {
|
||||
if (!(await this.biometricsService.supportsBiometric())) {
|
||||
return false;
|
||||
}
|
||||
return this.biometricsService.isBiometricUnlockAvailable();
|
||||
}
|
||||
|
||||
togglePassword() {
|
||||
this.showPassword = !this.showPassword;
|
||||
const input = document.getElementById(this.pinEnabled ? "pin" : "masterPassword");
|
||||
if (this.ngZone.isStable) {
|
||||
input.focus();
|
||||
} else {
|
||||
this.ngZone.onStable.pipe(take(1)).subscribe(() => input.focus());
|
||||
}
|
||||
}
|
||||
|
||||
private async handlePinRequiredUnlock() {
|
||||
if (this.pin == null || this.pin === "") {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: this.i18nService.t("errorOccurred"),
|
||||
message: this.i18nService.t("pinRequired"),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
return await this.doUnlockWithPin();
|
||||
}
|
||||
|
||||
private async doUnlockWithPin() {
|
||||
const MAX_INVALID_PIN_ENTRY_ATTEMPTS = 5;
|
||||
|
||||
try {
|
||||
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
||||
const userKey = await this.pinService.decryptUserKeyWithPin(this.pin, userId);
|
||||
|
||||
if (userKey) {
|
||||
await this.setUserKeyAndContinue(userKey, userId);
|
||||
return; // successfully unlocked
|
||||
}
|
||||
|
||||
// Failure state: invalid PIN or failed decryption
|
||||
this.invalidPinAttempts++;
|
||||
|
||||
// Log user out if they have entered an invalid PIN too many times
|
||||
if (this.invalidPinAttempts >= MAX_INVALID_PIN_ENTRY_ATTEMPTS) {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: null,
|
||||
message: this.i18nService.t("tooManyInvalidPinEntryAttemptsLoggingOut"),
|
||||
});
|
||||
this.messagingService.send("logout");
|
||||
return;
|
||||
}
|
||||
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: this.i18nService.t("errorOccurred"),
|
||||
message: this.i18nService.t("invalidPin"),
|
||||
});
|
||||
} catch {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: this.i18nService.t("errorOccurred"),
|
||||
message: this.i18nService.t("unexpectedError"),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async handleMasterPasswordRequiredUnlock() {
|
||||
if (this.masterPassword == null || this.masterPassword === "") {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: this.i18nService.t("errorOccurred"),
|
||||
message: this.i18nService.t("masterPasswordRequired"),
|
||||
});
|
||||
return;
|
||||
}
|
||||
await this.doUnlockWithMasterPassword();
|
||||
}
|
||||
|
||||
private async doUnlockWithMasterPassword() {
|
||||
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
||||
|
||||
const verification = {
|
||||
type: VerificationType.MasterPassword,
|
||||
secret: this.masterPassword,
|
||||
} as MasterPasswordVerification;
|
||||
|
||||
let passwordValid = false;
|
||||
let response: MasterPasswordVerificationResponse;
|
||||
try {
|
||||
this.formPromise = this.userVerificationService.verifyUserByMasterPassword(
|
||||
verification,
|
||||
userId,
|
||||
this.email,
|
||||
);
|
||||
response = await this.formPromise;
|
||||
this.enforcedMasterPasswordOptions = MasterPasswordPolicyOptions.fromResponse(
|
||||
response.policyOptions,
|
||||
);
|
||||
passwordValid = true;
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
} finally {
|
||||
this.formPromise = null;
|
||||
}
|
||||
|
||||
if (!passwordValid) {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: this.i18nService.t("errorOccurred"),
|
||||
message: this.i18nService.t("invalidMasterPassword"),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(
|
||||
response.masterKey,
|
||||
userId,
|
||||
);
|
||||
await this.setUserKeyAndContinue(userKey, userId, true);
|
||||
}
|
||||
|
||||
private async setUserKeyAndContinue(
|
||||
key: UserKey,
|
||||
userId: UserId,
|
||||
evaluatePasswordAfterUnlock = false,
|
||||
) {
|
||||
await this.keyService.setUserKey(key, userId);
|
||||
|
||||
// Now that we have a decrypted user key in memory, we can check if we
|
||||
// need to establish trust on the current device
|
||||
const activeAccount = await firstValueFrom(this.accountService.activeAccount$);
|
||||
await this.deviceTrustService.trustDeviceIfRequired(activeAccount.id);
|
||||
|
||||
await this.doContinue(evaluatePasswordAfterUnlock);
|
||||
}
|
||||
|
||||
private async doContinue(evaluatePasswordAfterUnlock: boolean) {
|
||||
await this.biometricStateService.resetUserPromptCancelled();
|
||||
this.messagingService.send("unlocked");
|
||||
|
||||
if (evaluatePasswordAfterUnlock) {
|
||||
try {
|
||||
// If we do not have any saved policies, attempt to load them from the service
|
||||
if (this.enforcedMasterPasswordOptions == undefined) {
|
||||
this.enforcedMasterPasswordOptions = await firstValueFrom(
|
||||
this.policyService.masterPasswordPolicyOptions$(),
|
||||
);
|
||||
}
|
||||
|
||||
if (this.requirePasswordChange()) {
|
||||
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
||||
await this.masterPasswordService.setForceSetPasswordReason(
|
||||
ForceSetPasswordReason.WeakMasterPassword,
|
||||
userId,
|
||||
);
|
||||
// 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.router.navigate([this.forcePasswordResetRoute]);
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
// Do not prevent unlock if there is an error evaluating policies
|
||||
this.logService.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
// Vault can be de-synced since notifications get ignored while locked. Need to check whether sync is required using the sync service.
|
||||
const clientType = this.platformUtilsService.getClientType();
|
||||
if (clientType === ClientType.Browser || clientType === ClientType.Desktop) {
|
||||
// Desktop and Browser have better offline support and to facilitate this we don't make the user wait for what
|
||||
// could be an HTTP Timeout because their server is unreachable.
|
||||
await Promise.race([
|
||||
this.syncService
|
||||
.fullSync(false)
|
||||
.catch((err) => this.logService.error("Error during unlock sync", err)),
|
||||
new Promise<void>((resolve) =>
|
||||
setTimeout(() => {
|
||||
this.logService.warning("Skipping sync wait, continuing to unlock.");
|
||||
resolve();
|
||||
}, 5_000),
|
||||
),
|
||||
]);
|
||||
} else {
|
||||
await this.syncService.fullSync(false);
|
||||
}
|
||||
|
||||
if (this.onSuccessfulSubmit != null) {
|
||||
await this.onSuccessfulSubmit();
|
||||
} else if (this.router != null) {
|
||||
// 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.router.navigate([this.successRoute]);
|
||||
}
|
||||
}
|
||||
|
||||
private async load(userId: UserId) {
|
||||
this.pinLockType = await this.pinService.getPinLockType(userId);
|
||||
|
||||
this.pinEnabled = await this.pinService.isPinDecryptionAvailable(userId);
|
||||
|
||||
this.masterPasswordEnabled = await this.userVerificationService.hasMasterPassword();
|
||||
|
||||
this.supportsBiometric = await this.biometricsService.supportsBiometric();
|
||||
this.biometricLock =
|
||||
(await this.vaultTimeoutSettingsService.isBiometricLockSet()) &&
|
||||
((await this.keyService.hasUserKeyStored(KeySuffixOptions.Biometric)) ||
|
||||
!this.platformUtilsService.supportsSecureStorage());
|
||||
this.email = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.email)),
|
||||
);
|
||||
|
||||
this.webVaultHostname = (await this.environmentService.getEnvironment()).getHostname();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the master password meets the enforced policy requirements
|
||||
* If not, returns false
|
||||
*/
|
||||
private requirePasswordChange(): boolean {
|
||||
if (
|
||||
this.enforcedMasterPasswordOptions == undefined ||
|
||||
!this.enforcedMasterPasswordOptions.enforceOnLogin
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const passwordStrength = this.passwordStrengthService.getPasswordStrength(
|
||||
this.masterPassword,
|
||||
this.email,
|
||||
)?.score;
|
||||
|
||||
return !this.policyService.evaluateMasterPassword(
|
||||
passwordStrength,
|
||||
this.masterPassword,
|
||||
this.enforcedMasterPasswordOptions,
|
||||
);
|
||||
}
|
||||
}
|
@ -75,7 +75,7 @@ const clientTypeToSuccessRouteRecord: Partial<Record<ClientType, string>> = {
|
||||
IconButtonModule,
|
||||
],
|
||||
})
|
||||
export class LockV2Component implements OnInit, OnDestroy {
|
||||
export class LockComponent implements OnInit, OnDestroy {
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
activeAccount: Account | null;
|
||||
@ -543,8 +543,8 @@ export class LockV2Component implements OnInit, OnDestroy {
|
||||
const previousUrl = this.lockComponentService.getPreviousUrl();
|
||||
/**
|
||||
* In a passkey flow, the `previousUrl` will still be `/fido2?<queryParams>` at this point
|
||||
* because the `/lockV2` route doesn't save the URL in the `BrowserRouterService`. This is
|
||||
* handled by the `doNotSaveUrl` property on the `lockV2` route in `app-routing.module.ts`.
|
||||
* because the `/lock` route doesn't save the URL in the `BrowserRouterService`. This is
|
||||
* handled by the `doNotSaveUrl` property on the `/lock` route in `app-routing.module.ts`.
|
||||
*/
|
||||
if (previousUrl) {
|
||||
await this.router.navigateByUrl(previousUrl);
|
||||
|
Loading…
Reference in New Issue
Block a user