mirror of
https://github.com/bitwarden/browser
synced 2025-12-11 05:43:41 +00:00
[PM-7608] Account Security Settings V2 (#10441)
* Implement account security settings v2 * Increase await dialog delay to 500 msec * Update messages * Replace platformservice with biometricsservice * Cleanup * Cleanup * Fix account security component according to feedback * Re-add old message * Re-add old error message * Fix minimum timeout message * Fix screen-reader on custom timeout * Remove debugging configurations * Fix incorrectly changed message * Remove custom vault timeout text * Restore vaultTimeoutPolicyInEffect i18n message in web * Change text to use vaultTimeoutPolicyInEffect1 * Fix tests
This commit is contained in:
@@ -328,7 +328,7 @@
|
|||||||
"createFoldersToOrganize": {
|
"createFoldersToOrganize": {
|
||||||
"message": "Create folders to organize your vault items"
|
"message": "Create folders to organize your vault items"
|
||||||
},
|
},
|
||||||
"deleteFolderPermanently":{
|
"deleteFolderPermanently": {
|
||||||
"message": "Are you sure you want to permanently delete this folder?"
|
"message": "Are you sure you want to permanently delete this folder?"
|
||||||
},
|
},
|
||||||
"deleteFolder": {
|
"deleteFolder": {
|
||||||
@@ -561,6 +561,9 @@
|
|||||||
"sessionTimeoutHeader": {
|
"sessionTimeoutHeader": {
|
||||||
"message": "Session timeout"
|
"message": "Session timeout"
|
||||||
},
|
},
|
||||||
|
"vaultTimeoutHeader": {
|
||||||
|
"message": "Vault timeout"
|
||||||
|
},
|
||||||
"otherOptions": {
|
"otherOptions": {
|
||||||
"message": "Other options"
|
"message": "Other options"
|
||||||
},
|
},
|
||||||
@@ -601,6 +604,9 @@
|
|||||||
"vaultTimeout": {
|
"vaultTimeout": {
|
||||||
"message": "Vault timeout"
|
"message": "Vault timeout"
|
||||||
},
|
},
|
||||||
|
"vaultTimeout1": {
|
||||||
|
"message": "Timeout"
|
||||||
|
},
|
||||||
"lockNow": {
|
"lockNow": {
|
||||||
"message": "Lock now"
|
"message": "Lock now"
|
||||||
},
|
},
|
||||||
@@ -801,6 +807,12 @@
|
|||||||
"twoStepLoginConfirmation": {
|
"twoStepLoginConfirmation": {
|
||||||
"message": "Two-step login makes your account more secure by requiring you to verify your login with another device such as a security key, authenticator app, SMS, phone call, or email. Two-step login can be set up on the bitwarden.com web vault. Do you want to visit the website now?"
|
"message": "Two-step login makes your account more secure by requiring you to verify your login with another device such as a security key, authenticator app, SMS, phone call, or email. Two-step login can be set up on the bitwarden.com web vault. Do you want to visit the website now?"
|
||||||
},
|
},
|
||||||
|
"twoStepLoginConfirmationContent": {
|
||||||
|
"message": "Make your account more secure by setting up two-step login in the Bitwarden web app."
|
||||||
|
},
|
||||||
|
"twoStepLoginConfirmationTitle": {
|
||||||
|
"message": "Continue to web app?"
|
||||||
|
},
|
||||||
"editedFolder": {
|
"editedFolder": {
|
||||||
"message": "Folder saved"
|
"message": "Folder saved"
|
||||||
},
|
},
|
||||||
@@ -1875,9 +1887,18 @@
|
|||||||
"unlockWithPin": {
|
"unlockWithPin": {
|
||||||
"message": "Unlock with PIN"
|
"message": "Unlock with PIN"
|
||||||
},
|
},
|
||||||
|
"setYourPinTitle": {
|
||||||
|
"message": "Set PIN"
|
||||||
|
},
|
||||||
|
"setYourPinButton": {
|
||||||
|
"message": "Set PIN"
|
||||||
|
},
|
||||||
"setYourPinCode": {
|
"setYourPinCode": {
|
||||||
"message": "Set your PIN code for unlocking Bitwarden. Your PIN settings will be reset if you ever fully log out of the application."
|
"message": "Set your PIN code for unlocking Bitwarden. Your PIN settings will be reset if you ever fully log out of the application."
|
||||||
},
|
},
|
||||||
|
"setYourPinCode1": {
|
||||||
|
"message": "Your PIN will be used to unlock Bitwarden instead of your master password. Your PIN will reset if you ever fully log out of Bitwarden."
|
||||||
|
},
|
||||||
"pinRequired": {
|
"pinRequired": {
|
||||||
"message": "PIN code is required."
|
"message": "PIN code is required."
|
||||||
},
|
},
|
||||||
@@ -1899,6 +1920,9 @@
|
|||||||
"lockWithMasterPassOnRestart": {
|
"lockWithMasterPassOnRestart": {
|
||||||
"message": "Lock with master password on browser restart"
|
"message": "Lock with master password on browser restart"
|
||||||
},
|
},
|
||||||
|
"lockWithMasterPassOnRestart1": {
|
||||||
|
"message": "Require master password on browser restart"
|
||||||
|
},
|
||||||
"selectOneCollection": {
|
"selectOneCollection": {
|
||||||
"message": "You must select at least one collection."
|
"message": "You must select at least one collection."
|
||||||
},
|
},
|
||||||
@@ -1921,7 +1945,7 @@
|
|||||||
"message": "Use this password"
|
"message": "Use this password"
|
||||||
},
|
},
|
||||||
"useThisUsername": {
|
"useThisUsername": {
|
||||||
"message": "Use this username"
|
"message": "Use this username"
|
||||||
},
|
},
|
||||||
"securePasswordGenerated": {
|
"securePasswordGenerated": {
|
||||||
"message": "Secure password generated! Don't forget to also update your password on the website."
|
"message": "Secure password generated! Don't forget to also update your password on the website."
|
||||||
@@ -1937,6 +1961,9 @@
|
|||||||
"vaultTimeoutAction": {
|
"vaultTimeoutAction": {
|
||||||
"message": "Vault timeout action"
|
"message": "Vault timeout action"
|
||||||
},
|
},
|
||||||
|
"vaultTimeoutAction1": {
|
||||||
|
"message": "Timeout action"
|
||||||
|
},
|
||||||
"lock": {
|
"lock": {
|
||||||
"message": "Lock",
|
"message": "Lock",
|
||||||
"description": "Verb form: to make secure or inaccessible by"
|
"description": "Verb form: to make secure or inaccessible by"
|
||||||
@@ -2131,12 +2158,12 @@
|
|||||||
"nativeMessagingWrongUserTitle": {
|
"nativeMessagingWrongUserTitle": {
|
||||||
"message": "Account missmatch"
|
"message": "Account missmatch"
|
||||||
},
|
},
|
||||||
"nativeMessagingWrongUserKeyDesc": {
|
|
||||||
"message": "Biometric unlock failed. The biometric secret key failed to unlock the vault. Please try to set up biometrics again."
|
|
||||||
},
|
|
||||||
"nativeMessagingWrongUserKeyTitle": {
|
"nativeMessagingWrongUserKeyTitle": {
|
||||||
"message": "Biometric key missmatch"
|
"message": "Biometric key missmatch"
|
||||||
},
|
},
|
||||||
|
"nativeMessagingWrongUserKeyDesc": {
|
||||||
|
"message": "Biometric unlock failed. The biometric secret key failed to unlock the vault. Please try to set up biometrics again."
|
||||||
|
},
|
||||||
"biometricsNotEnabledTitle": {
|
"biometricsNotEnabledTitle": {
|
||||||
"message": "Biometrics not set up"
|
"message": "Biometrics not set up"
|
||||||
},
|
},
|
||||||
@@ -2522,6 +2549,9 @@
|
|||||||
"minutes": {
|
"minutes": {
|
||||||
"message": "Minutes"
|
"message": "Minutes"
|
||||||
},
|
},
|
||||||
|
"vaultTimeoutPolicyAffectingOptions": {
|
||||||
|
"message": "Enterprise policy requirements have been applied to your timeout options"
|
||||||
|
},
|
||||||
"vaultTimeoutPolicyInEffect": {
|
"vaultTimeoutPolicyInEffect": {
|
||||||
"message": "Your organization policies have set your maximum allowed vault timeout to $HOURS$ hour(s) and $MINUTES$ minute(s).",
|
"message": "Your organization policies have set your maximum allowed vault timeout to $HOURS$ hour(s) and $MINUTES$ minute(s).",
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
@@ -2535,6 +2565,32 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"vaultTimeoutPolicyInEffect1": {
|
||||||
|
"message": "$HOURS$ hour(s) and $MINUTES$ minute(s) maximum.",
|
||||||
|
"placeholders": {
|
||||||
|
"hours": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "5"
|
||||||
|
},
|
||||||
|
"minutes": {
|
||||||
|
"content": "$2",
|
||||||
|
"example": "5"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"vaultTimeoutPolicyMaximumError": {
|
||||||
|
"message": "Timeout exceeds the restriction set by your organization: $HOURS$ hour(s) and $MINUTES$ minute(s) maximum",
|
||||||
|
"placeholders": {
|
||||||
|
"hours": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "5"
|
||||||
|
},
|
||||||
|
"minutes": {
|
||||||
|
"content": "$2",
|
||||||
|
"example": "5"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"vaultTimeoutPolicyWithActionInEffect": {
|
"vaultTimeoutPolicyWithActionInEffect": {
|
||||||
"message": "Your organization policies are affecting your vault timeout. Maximum allowed vault timeout is $HOURS$ hour(s) and $MINUTES$ minute(s). Your vault timeout action is set to $ACTION$.",
|
"message": "Your organization policies are affecting your vault timeout. Maximum allowed vault timeout is $HOURS$ hour(s) and $MINUTES$ minute(s). Your vault timeout action is set to $ACTION$.",
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
@@ -3264,7 +3320,7 @@
|
|||||||
"message": "Unlock your account, opens in a new window",
|
"message": "Unlock your account, opens in a new window",
|
||||||
"description": "Screen reader text (aria-label) for unlock account button in overlay"
|
"description": "Screen reader text (aria-label) for unlock account button in overlay"
|
||||||
},
|
},
|
||||||
"fillCredentialsFor": {
|
"fillCredentialsFor": {
|
||||||
"message": "Fill credentials for",
|
"message": "Fill credentials for",
|
||||||
"description": "Screen reader text for when overlay item is in focused"
|
"description": "Screen reader text for when overlay item is in focused"
|
||||||
},
|
},
|
||||||
@@ -4321,6 +4377,15 @@
|
|||||||
"enterprisePolicyRequirementsApplied": {
|
"enterprisePolicyRequirementsApplied": {
|
||||||
"message": "Enterprise policy requirements have been applied to this setting"
|
"message": "Enterprise policy requirements have been applied to this setting"
|
||||||
},
|
},
|
||||||
|
"retry": {
|
||||||
|
"message": "Retry"
|
||||||
|
},
|
||||||
|
"vaultCustomTimeoutMinimum": {
|
||||||
|
"message": "Minimum custom timeout is 1 minute."
|
||||||
|
},
|
||||||
|
"additionalContentAvailable": {
|
||||||
|
"message": "Additional content is available"
|
||||||
|
},
|
||||||
"fileSavedToDevice": {
|
"fileSavedToDevice": {
|
||||||
"message": "File saved to device. Manage from your device downloads."
|
"message": "File saved to device. Manage from your device downloads."
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
<form [bitSubmit]="submit" [formGroup]="setPinForm">
|
<form [bitSubmit]="submit" [formGroup]="setPinForm">
|
||||||
<bit-dialog>
|
<bit-dialog>
|
||||||
<div class="tw-font-semibold" bitDialogTitle>
|
<div class="tw-font-semibold" bitDialogTitle>
|
||||||
{{ "unlockWithPin" | i18n }}
|
{{ "setYourPinTitle" | i18n }}
|
||||||
</div>
|
</div>
|
||||||
<div bitDialogContent>
|
<div bitDialogContent>
|
||||||
<p>
|
<p>
|
||||||
{{ "setYourPinCode" | i18n }}
|
{{ "setYourPinCode1" | i18n }}
|
||||||
</p>
|
</p>
|
||||||
<bit-form-field>
|
<bit-form-field>
|
||||||
<bit-label>{{ "pin" | i18n }}</bit-label>
|
<bit-label>{{ "pin" | i18n }}</bit-label>
|
||||||
@@ -22,12 +22,12 @@
|
|||||||
bitCheckbox
|
bitCheckbox
|
||||||
formControlName="requireMasterPasswordOnClientRestart"
|
formControlName="requireMasterPasswordOnClientRestart"
|
||||||
/>
|
/>
|
||||||
<span>{{ "lockWithMasterPassOnRestart" | i18n }}</span>
|
<span>{{ "lockWithMasterPassOnRestart1" | i18n }}</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div bitDialogFooter>
|
<div bitDialogFooter>
|
||||||
<button type="submit" bitButton bitFormButton buttonType="primary">
|
<button type="submit" bitButton bitFormButton buttonType="primary">
|
||||||
<span>{{ "ok" | i18n }}</span>
|
<span>{{ "setYourPinButton" | i18n }}</span>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" bitButton bitFormButton buttonType="secondary" bitDialogClose>
|
<button type="button" bitButton bitFormButton buttonType="secondary" bitDialogClose>
|
||||||
{{ "cancel" | i18n }}
|
{{ "cancel" | i18n }}
|
||||||
|
|||||||
@@ -0,0 +1,140 @@
|
|||||||
|
<app-header>
|
||||||
|
<div class="left">
|
||||||
|
<button type="button" routerLink="/tabs/settings">
|
||||||
|
<span class="header-icon"><i class="bwi bwi-angle-left" aria-hidden="true"></i></span>
|
||||||
|
<span>{{ "back" | i18n }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<h1 class="center">
|
||||||
|
<span class="title">{{ "accountSecurity" | i18n }}</span>
|
||||||
|
</h1>
|
||||||
|
<div class="right">
|
||||||
|
<app-pop-out></app-pop-out>
|
||||||
|
</div>
|
||||||
|
</app-header>
|
||||||
|
<main tabindex="-1" [formGroup]="form">
|
||||||
|
<div class="box list">
|
||||||
|
<h2 class="box-header">{{ "unlockMethods" | i18n }}</h2>
|
||||||
|
<div class="box-content single-line">
|
||||||
|
<div class="box-content-row box-content-row-checkbox" appBoxRow *ngIf="supportsBiometric">
|
||||||
|
<label for="biometric">{{ "unlockWithBiometrics" | i18n }}</label>
|
||||||
|
<input id="biometric" type="checkbox" formControlName="biometric" />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="box-content-row box-content-row-checkbox"
|
||||||
|
appBoxRow
|
||||||
|
*ngIf="supportsBiometric && this.form.value.biometric"
|
||||||
|
>
|
||||||
|
<label for="autoBiometricsPrompt">{{ "enableAutoBiometricsPrompt" | i18n }}</label>
|
||||||
|
<input
|
||||||
|
id="autoBiometricsPrompt"
|
||||||
|
type="checkbox"
|
||||||
|
(change)="updateAutoBiometricsPrompt()"
|
||||||
|
formControlName="enableAutoBiometricsPrompt"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="box-content-row box-content-row-checkbox" appBoxRow>
|
||||||
|
<label for="pin">{{ "unlockWithPin" | i18n }}</label>
|
||||||
|
<input id="pin" type="checkbox" formControlName="pin" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="box list">
|
||||||
|
<h2 class="box-header">{{ "sessionTimeoutHeader" | i18n }}</h2>
|
||||||
|
<div class="box-content single-line">
|
||||||
|
<app-callout type="info" *ngIf="vaultTimeoutPolicyCallout | async as policy">
|
||||||
|
<span *ngIf="policy.timeout && policy.action">
|
||||||
|
{{
|
||||||
|
"vaultTimeoutPolicyWithActionInEffect"
|
||||||
|
| i18n: policy.timeout.hours : policy.timeout.minutes : (policy.action | i18n)
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
<span *ngIf="policy.timeout && !policy.action">
|
||||||
|
{{ "vaultTimeoutPolicyInEffect" | i18n: policy.timeout.hours : policy.timeout.minutes }}
|
||||||
|
</span>
|
||||||
|
<span *ngIf="!policy.timeout && policy.action">
|
||||||
|
{{ "vaultTimeoutActionPolicyInEffect" | i18n: (policy.action | i18n) }}
|
||||||
|
</span>
|
||||||
|
</app-callout>
|
||||||
|
<app-vault-timeout-input
|
||||||
|
[vaultTimeoutOptions]="vaultTimeoutOptions"
|
||||||
|
[formControl]="form.controls.vaultTimeout"
|
||||||
|
ngDefaultControl
|
||||||
|
>
|
||||||
|
</app-vault-timeout-input>
|
||||||
|
<div class="box-content-row display-block" appBoxRow>
|
||||||
|
<label for="vaultTimeoutAction">{{ "vaultTimeoutAction" | i18n }}</label>
|
||||||
|
<select
|
||||||
|
id="vaultTimeoutAction"
|
||||||
|
name="VaultTimeoutActions"
|
||||||
|
formControlName="vaultTimeoutAction"
|
||||||
|
>
|
||||||
|
<option *ngFor="let action of availableVaultTimeoutActions" [ngValue]="action">
|
||||||
|
{{ action | i18n }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
*ngIf="!availableVaultTimeoutActions.includes(VaultTimeoutAction.Lock)"
|
||||||
|
id="unlockMethodHelp"
|
||||||
|
class="box-footer"
|
||||||
|
>
|
||||||
|
{{ "unlockMethodNeededToChangeTimeoutActionDesc" | i18n }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="box list">
|
||||||
|
<h2 class="box-header">{{ "otherOptions" | i18n }}</h2>
|
||||||
|
<div class="box-content single-line">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="box-content-row box-content-row-flex text-default"
|
||||||
|
appStopClick
|
||||||
|
(click)="fingerprint()"
|
||||||
|
>
|
||||||
|
<div class="row-main">{{ "fingerprintPhrase" | i18n }}</div>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="box-content-row box-content-row-flex text-default"
|
||||||
|
appStopClick
|
||||||
|
(click)="twoStep()"
|
||||||
|
>
|
||||||
|
<div class="row-main">{{ "twoStepLogin" | i18n }}</div>
|
||||||
|
<i class="bwi bwi-external-link bwi-lg row-sub-icon" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="box-content-row box-content-row-flex text-default"
|
||||||
|
appStopClick
|
||||||
|
(click)="changePassword()"
|
||||||
|
*ngIf="showChangeMasterPass"
|
||||||
|
>
|
||||||
|
<div class="row-main">{{ "changeMasterPassword" | i18n }}</div>
|
||||||
|
<i class="bwi bwi-external-link bwi-lg row-sub-icon" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
*ngIf="
|
||||||
|
!accountSwitcherEnabled && availableVaultTimeoutActions.includes(VaultTimeoutAction.Lock)
|
||||||
|
"
|
||||||
|
type="button"
|
||||||
|
class="box-content-row box-content-row-flex text-default"
|
||||||
|
appStopClick
|
||||||
|
(click)="lock()"
|
||||||
|
>
|
||||||
|
<div class="row-main">{{ "lockNow" | i18n }}</div>
|
||||||
|
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
*ngIf="!accountSwitcherEnabled"
|
||||||
|
type="button"
|
||||||
|
class="box-content-row box-content-row-flex text-default"
|
||||||
|
appStopClick
|
||||||
|
(click)="logOut()"
|
||||||
|
>
|
||||||
|
<div class="row-main">{{ "logOut" | i18n }}</div>
|
||||||
|
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
@@ -0,0 +1,501 @@
|
|||||||
|
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from "@angular/core";
|
||||||
|
import { FormBuilder } from "@angular/forms";
|
||||||
|
import {
|
||||||
|
BehaviorSubject,
|
||||||
|
combineLatest,
|
||||||
|
concatMap,
|
||||||
|
distinctUntilChanged,
|
||||||
|
filter,
|
||||||
|
firstValueFrom,
|
||||||
|
map,
|
||||||
|
Observable,
|
||||||
|
pairwise,
|
||||||
|
startWith,
|
||||||
|
Subject,
|
||||||
|
switchMap,
|
||||||
|
takeUntil,
|
||||||
|
} from "rxjs";
|
||||||
|
|
||||||
|
import { FingerprintDialogComponent } from "@bitwarden/auth/angular";
|
||||||
|
import { PinServiceAbstraction } from "@bitwarden/auth/common";
|
||||||
|
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
|
||||||
|
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service";
|
||||||
|
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||||
|
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||||
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
|
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||||
|
import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum";
|
||||||
|
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||||
|
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||||
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.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 { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service";
|
||||||
|
import { BiometricsService } from "@bitwarden/common/platform/biometrics/biometric.service";
|
||||||
|
import {
|
||||||
|
VaultTimeout,
|
||||||
|
VaultTimeoutOption,
|
||||||
|
VaultTimeoutStringType,
|
||||||
|
} from "@bitwarden/common/types/vault-timeout.type";
|
||||||
|
import { DialogService } from "@bitwarden/components";
|
||||||
|
|
||||||
|
import { BiometricErrors, BiometricErrorTypes } from "../../../models/biometricErrors";
|
||||||
|
import { BrowserApi } from "../../../platform/browser/browser-api";
|
||||||
|
import { enableAccountSwitching } from "../../../platform/flags";
|
||||||
|
import BrowserPopupUtils from "../../../platform/popup/browser-popup-utils";
|
||||||
|
import { SetPinComponent } from "../components/set-pin.component";
|
||||||
|
|
||||||
|
import { AwaitDesktopDialogComponent } from "./await-desktop-dialog.component";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: "auth-account-security",
|
||||||
|
templateUrl: "account-security-v1.component.html",
|
||||||
|
})
|
||||||
|
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||||
|
export class AccountSecurityComponent implements OnInit, OnDestroy {
|
||||||
|
protected readonly VaultTimeoutAction = VaultTimeoutAction;
|
||||||
|
|
||||||
|
availableVaultTimeoutActions: VaultTimeoutAction[] = [];
|
||||||
|
vaultTimeoutOptions: VaultTimeoutOption[];
|
||||||
|
vaultTimeoutPolicyCallout: Observable<{
|
||||||
|
timeout: { hours: number; minutes: number };
|
||||||
|
action: VaultTimeoutAction;
|
||||||
|
}>;
|
||||||
|
supportsBiometric: boolean;
|
||||||
|
showChangeMasterPass = true;
|
||||||
|
accountSwitcherEnabled = false;
|
||||||
|
|
||||||
|
form = this.formBuilder.group({
|
||||||
|
vaultTimeout: [null as VaultTimeout | null],
|
||||||
|
vaultTimeoutAction: [VaultTimeoutAction.Lock],
|
||||||
|
pin: [null as boolean | null],
|
||||||
|
biometric: false,
|
||||||
|
enableAutoBiometricsPrompt: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
private refreshTimeoutSettings$ = new BehaviorSubject<void>(undefined);
|
||||||
|
private destroy$ = new Subject<void>();
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private accountService: AccountService,
|
||||||
|
private pinService: PinServiceAbstraction,
|
||||||
|
private policyService: PolicyService,
|
||||||
|
private formBuilder: FormBuilder,
|
||||||
|
private platformUtilsService: PlatformUtilsService,
|
||||||
|
private i18nService: I18nService,
|
||||||
|
private vaultTimeoutService: VaultTimeoutService,
|
||||||
|
private vaultTimeoutSettingsService: VaultTimeoutSettingsService,
|
||||||
|
public messagingService: MessagingService,
|
||||||
|
private environmentService: EnvironmentService,
|
||||||
|
private cryptoService: CryptoService,
|
||||||
|
private stateService: StateService,
|
||||||
|
private userVerificationService: UserVerificationService,
|
||||||
|
private dialogService: DialogService,
|
||||||
|
private changeDetectorRef: ChangeDetectorRef,
|
||||||
|
private biometricStateService: BiometricStateService,
|
||||||
|
private biometricsService: BiometricsService,
|
||||||
|
) {
|
||||||
|
this.accountSwitcherEnabled = enableAccountSwitching();
|
||||||
|
}
|
||||||
|
|
||||||
|
async ngOnInit() {
|
||||||
|
const maximumVaultTimeoutPolicy = this.policyService.get$(PolicyType.MaximumVaultTimeout);
|
||||||
|
this.vaultTimeoutPolicyCallout = maximumVaultTimeoutPolicy.pipe(
|
||||||
|
filter((policy) => policy != null),
|
||||||
|
map((policy) => {
|
||||||
|
let timeout;
|
||||||
|
if (policy.data?.minutes) {
|
||||||
|
timeout = {
|
||||||
|
hours: Math.floor(policy.data?.minutes / 60),
|
||||||
|
minutes: policy.data?.minutes % 60,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return { timeout: timeout, action: policy.data?.action };
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const showOnLocked =
|
||||||
|
!this.platformUtilsService.isFirefox() && !this.platformUtilsService.isSafari();
|
||||||
|
|
||||||
|
this.vaultTimeoutOptions = [
|
||||||
|
{ name: this.i18nService.t("immediately"), value: 0 },
|
||||||
|
{ name: this.i18nService.t("oneMinute"), value: 1 },
|
||||||
|
{ name: this.i18nService.t("fiveMinutes"), value: 5 },
|
||||||
|
{ name: this.i18nService.t("fifteenMinutes"), value: 15 },
|
||||||
|
{ name: this.i18nService.t("thirtyMinutes"), value: 30 },
|
||||||
|
{ name: this.i18nService.t("oneHour"), value: 60 },
|
||||||
|
{ name: this.i18nService.t("fourHours"), value: 240 },
|
||||||
|
];
|
||||||
|
|
||||||
|
if (showOnLocked) {
|
||||||
|
this.vaultTimeoutOptions.push({
|
||||||
|
name: this.i18nService.t("onLocked"),
|
||||||
|
value: VaultTimeoutStringType.OnLocked,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.vaultTimeoutOptions.push({
|
||||||
|
name: this.i18nService.t("onRestart"),
|
||||||
|
value: VaultTimeoutStringType.OnRestart,
|
||||||
|
});
|
||||||
|
this.vaultTimeoutOptions.push({
|
||||||
|
name: this.i18nService.t("never"),
|
||||||
|
value: VaultTimeoutStringType.Never,
|
||||||
|
});
|
||||||
|
|
||||||
|
const activeAccount = await firstValueFrom(this.accountService.activeAccount$);
|
||||||
|
|
||||||
|
let timeout = await firstValueFrom(
|
||||||
|
this.vaultTimeoutSettingsService.getVaultTimeoutByUserId$(activeAccount.id),
|
||||||
|
);
|
||||||
|
if (timeout === VaultTimeoutStringType.OnLocked && !showOnLocked) {
|
||||||
|
timeout = VaultTimeoutStringType.OnRestart;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialValues = {
|
||||||
|
vaultTimeout: timeout,
|
||||||
|
vaultTimeoutAction: await firstValueFrom(
|
||||||
|
this.vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$(activeAccount.id),
|
||||||
|
),
|
||||||
|
pin: await this.pinService.isPinSet(activeAccount.id),
|
||||||
|
biometric: await this.vaultTimeoutSettingsService.isBiometricLockSet(),
|
||||||
|
enableAutoBiometricsPrompt: await firstValueFrom(
|
||||||
|
this.biometricStateService.promptAutomatically$,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
this.form.patchValue(initialValues, { emitEvent: false });
|
||||||
|
|
||||||
|
this.supportsBiometric = await this.biometricsService.supportsBiometric();
|
||||||
|
this.showChangeMasterPass = await this.userVerificationService.hasMasterPassword();
|
||||||
|
|
||||||
|
this.form.controls.vaultTimeout.valueChanges
|
||||||
|
.pipe(
|
||||||
|
startWith(initialValues.vaultTimeout), // emit to init pairwise
|
||||||
|
pairwise(),
|
||||||
|
concatMap(async ([previousValue, newValue]) => {
|
||||||
|
await this.saveVaultTimeout(previousValue, newValue);
|
||||||
|
}),
|
||||||
|
takeUntil(this.destroy$),
|
||||||
|
)
|
||||||
|
.subscribe();
|
||||||
|
|
||||||
|
this.form.controls.vaultTimeoutAction.valueChanges
|
||||||
|
.pipe(
|
||||||
|
startWith(initialValues.vaultTimeoutAction), // emit to init pairwise
|
||||||
|
pairwise(),
|
||||||
|
concatMap(async ([previousValue, newValue]) => {
|
||||||
|
await this.saveVaultTimeoutAction(previousValue, newValue);
|
||||||
|
}),
|
||||||
|
takeUntil(this.destroy$),
|
||||||
|
)
|
||||||
|
.subscribe();
|
||||||
|
|
||||||
|
this.form.controls.pin.valueChanges
|
||||||
|
.pipe(
|
||||||
|
concatMap(async (value) => {
|
||||||
|
await this.updatePin(value);
|
||||||
|
this.refreshTimeoutSettings$.next();
|
||||||
|
}),
|
||||||
|
takeUntil(this.destroy$),
|
||||||
|
)
|
||||||
|
.subscribe();
|
||||||
|
|
||||||
|
this.form.controls.biometric.valueChanges
|
||||||
|
.pipe(
|
||||||
|
distinctUntilChanged(),
|
||||||
|
concatMap(async (enabled) => {
|
||||||
|
await this.updateBiometric(enabled);
|
||||||
|
if (enabled) {
|
||||||
|
this.form.controls.enableAutoBiometricsPrompt.enable();
|
||||||
|
} else {
|
||||||
|
this.form.controls.enableAutoBiometricsPrompt.disable();
|
||||||
|
}
|
||||||
|
this.refreshTimeoutSettings$.next();
|
||||||
|
}),
|
||||||
|
takeUntil(this.destroy$),
|
||||||
|
)
|
||||||
|
.subscribe();
|
||||||
|
|
||||||
|
this.refreshTimeoutSettings$
|
||||||
|
.pipe(
|
||||||
|
switchMap(() =>
|
||||||
|
combineLatest([
|
||||||
|
this.vaultTimeoutSettingsService.availableVaultTimeoutActions$(),
|
||||||
|
this.vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$(activeAccount.id),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
takeUntil(this.destroy$),
|
||||||
|
)
|
||||||
|
.subscribe(([availableActions, action]) => {
|
||||||
|
this.availableVaultTimeoutActions = availableActions;
|
||||||
|
this.form.controls.vaultTimeoutAction.setValue(action, { emitEvent: false });
|
||||||
|
// NOTE: The UI doesn't properly update without detect changes.
|
||||||
|
// I've even tried using an async pipe, but it still doesn't work. I'm not sure why.
|
||||||
|
// Using an async pipe means that we can't call `detectChanges` AFTER the data has change
|
||||||
|
// meaning that we are forced to use regular class variables instead of observables.
|
||||||
|
this.changeDetectorRef.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.refreshTimeoutSettings$
|
||||||
|
.pipe(
|
||||||
|
switchMap(() =>
|
||||||
|
combineLatest([
|
||||||
|
this.vaultTimeoutSettingsService.availableVaultTimeoutActions$(),
|
||||||
|
maximumVaultTimeoutPolicy,
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
takeUntil(this.destroy$),
|
||||||
|
)
|
||||||
|
.subscribe(([availableActions, policy]) => {
|
||||||
|
if (policy?.data?.action || availableActions.length <= 1) {
|
||||||
|
this.form.controls.vaultTimeoutAction.disable({ emitEvent: false });
|
||||||
|
} else {
|
||||||
|
this.form.controls.vaultTimeoutAction.enable({ emitEvent: false });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveVaultTimeout(previousValue: VaultTimeout, newValue: VaultTimeout) {
|
||||||
|
if (newValue === VaultTimeoutStringType.Never) {
|
||||||
|
const confirmed = await this.dialogService.openSimpleDialog({
|
||||||
|
title: { key: "warning" },
|
||||||
|
content: { key: "neverLockWarning" },
|
||||||
|
type: "warning",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!confirmed) {
|
||||||
|
this.form.controls.vaultTimeout.setValue(previousValue, { emitEvent: false });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The minTimeoutError does not apply to browser because it supports Immediately
|
||||||
|
// So only check for the policyError
|
||||||
|
if (this.form.controls.vaultTimeout.hasError("policyError")) {
|
||||||
|
this.platformUtilsService.showToast(
|
||||||
|
"error",
|
||||||
|
null,
|
||||||
|
this.i18nService.t("vaultTimeoutTooLarge"),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const activeAccount = await firstValueFrom(this.accountService.activeAccount$);
|
||||||
|
|
||||||
|
const vaultTimeoutAction = await firstValueFrom(
|
||||||
|
this.vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$(activeAccount.id),
|
||||||
|
);
|
||||||
|
|
||||||
|
await this.vaultTimeoutSettingsService.setVaultTimeoutOptions(
|
||||||
|
activeAccount.id,
|
||||||
|
newValue,
|
||||||
|
vaultTimeoutAction,
|
||||||
|
);
|
||||||
|
if (newValue === VaultTimeoutStringType.Never) {
|
||||||
|
this.messagingService.send("bgReseedStorage");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveVaultTimeoutAction(previousValue: VaultTimeoutAction, newValue: VaultTimeoutAction) {
|
||||||
|
if (newValue === VaultTimeoutAction.LogOut) {
|
||||||
|
const confirmed = await this.dialogService.openSimpleDialog({
|
||||||
|
title: { key: "vaultTimeoutLogOutConfirmationTitle" },
|
||||||
|
content: { key: "vaultTimeoutLogOutConfirmation" },
|
||||||
|
type: "warning",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!confirmed) {
|
||||||
|
this.form.controls.vaultTimeoutAction.setValue(previousValue, {
|
||||||
|
emitEvent: false,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.form.controls.vaultTimeout.hasError("policyError")) {
|
||||||
|
this.platformUtilsService.showToast(
|
||||||
|
"error",
|
||||||
|
null,
|
||||||
|
this.i18nService.t("vaultTimeoutTooLarge"),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const activeAccount = await firstValueFrom(this.accountService.activeAccount$);
|
||||||
|
|
||||||
|
await this.vaultTimeoutSettingsService.setVaultTimeoutOptions(
|
||||||
|
activeAccount.id,
|
||||||
|
this.form.value.vaultTimeout,
|
||||||
|
newValue,
|
||||||
|
);
|
||||||
|
this.refreshTimeoutSettings$.next();
|
||||||
|
}
|
||||||
|
|
||||||
|
async updatePin(value: boolean) {
|
||||||
|
if (value) {
|
||||||
|
const dialogRef = SetPinComponent.open(this.dialogService);
|
||||||
|
|
||||||
|
if (dialogRef == null) {
|
||||||
|
this.form.controls.pin.setValue(false, { emitEvent: false });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const userHasPinSet = await firstValueFrom(dialogRef.closed);
|
||||||
|
this.form.controls.pin.setValue(userHasPinSet, { emitEvent: false });
|
||||||
|
} else {
|
||||||
|
await this.vaultTimeoutSettingsService.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateBiometric(enabled: boolean) {
|
||||||
|
if (enabled && this.supportsBiometric) {
|
||||||
|
let granted;
|
||||||
|
try {
|
||||||
|
granted = await BrowserApi.requestPermission({ permissions: ["nativeMessaging"] });
|
||||||
|
} catch (e) {
|
||||||
|
// eslint-disable-next-line
|
||||||
|
console.error(e);
|
||||||
|
|
||||||
|
if (this.platformUtilsService.isFirefox() && BrowserPopupUtils.inSidebar(window)) {
|
||||||
|
await this.dialogService.openSimpleDialog({
|
||||||
|
title: { key: "nativeMessaginPermissionSidebarTitle" },
|
||||||
|
content: { key: "nativeMessaginPermissionSidebarDesc" },
|
||||||
|
acceptButtonText: { key: "ok" },
|
||||||
|
cancelButtonText: null,
|
||||||
|
type: "info",
|
||||||
|
});
|
||||||
|
|
||||||
|
this.form.controls.biometric.setValue(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!granted) {
|
||||||
|
await this.dialogService.openSimpleDialog({
|
||||||
|
title: { key: "nativeMessaginPermissionErrorTitle" },
|
||||||
|
content: { key: "nativeMessaginPermissionErrorDesc" },
|
||||||
|
acceptButtonText: { key: "ok" },
|
||||||
|
cancelButtonText: null,
|
||||||
|
type: "danger",
|
||||||
|
});
|
||||||
|
|
||||||
|
this.form.controls.biometric.setValue(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const awaitDesktopDialogRef = AwaitDesktopDialogComponent.open(this.dialogService);
|
||||||
|
const awaitDesktopDialogClosed = firstValueFrom(awaitDesktopDialogRef.closed);
|
||||||
|
|
||||||
|
await this.cryptoService.refreshAdditionalKeys();
|
||||||
|
|
||||||
|
await Promise.race([
|
||||||
|
awaitDesktopDialogClosed.then(async (result) => {
|
||||||
|
if (result !== true) {
|
||||||
|
this.form.controls.biometric.setValue(false);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
this.biometricsService
|
||||||
|
.authenticateBiometric()
|
||||||
|
.then((result) => {
|
||||||
|
this.form.controls.biometric.setValue(result);
|
||||||
|
if (!result) {
|
||||||
|
this.platformUtilsService.showToast(
|
||||||
|
"error",
|
||||||
|
this.i18nService.t("errorEnableBiometricTitle"),
|
||||||
|
this.i18nService.t("errorEnableBiometricDesc"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
// Handle connection errors
|
||||||
|
this.form.controls.biometric.setValue(false);
|
||||||
|
|
||||||
|
const error = BiometricErrors[e.message as BiometricErrorTypes];
|
||||||
|
|
||||||
|
// 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.dialogService.openSimpleDialog({
|
||||||
|
title: { key: error.title },
|
||||||
|
content: { key: error.description },
|
||||||
|
acceptButtonText: { key: "ok" },
|
||||||
|
cancelButtonText: null,
|
||||||
|
type: "danger",
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
awaitDesktopDialogRef.close(true);
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
await this.biometricStateService.setBiometricUnlockEnabled(false);
|
||||||
|
await this.biometricStateService.setFingerprintValidated(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateAutoBiometricsPrompt() {
|
||||||
|
await this.biometricStateService.setPromptAutomatically(
|
||||||
|
this.form.value.enableAutoBiometricsPrompt,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async changePassword() {
|
||||||
|
const confirmed = await this.dialogService.openSimpleDialog({
|
||||||
|
title: { key: "continueToWebApp" },
|
||||||
|
content: { key: "changeMasterPasswordOnWebConfirmation" },
|
||||||
|
type: "info",
|
||||||
|
acceptButtonText: { key: "continue" },
|
||||||
|
});
|
||||||
|
if (confirmed) {
|
||||||
|
const env = await firstValueFrom(this.environmentService.environment$);
|
||||||
|
await BrowserApi.createNewTab(env.getWebVaultUrl());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async twoStep() {
|
||||||
|
const confirmed = await this.dialogService.openSimpleDialog({
|
||||||
|
title: { key: "twoStepLogin" },
|
||||||
|
content: { key: "twoStepLoginConfirmation" },
|
||||||
|
type: "info",
|
||||||
|
});
|
||||||
|
if (confirmed) {
|
||||||
|
// 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
|
||||||
|
BrowserApi.createNewTab("https://bitwarden.com/help/setup-two-step-login/");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fingerprint() {
|
||||||
|
const fingerprint = await this.cryptoService.getFingerprint(
|
||||||
|
await this.stateService.getUserId(),
|
||||||
|
);
|
||||||
|
|
||||||
|
const dialogRef = FingerprintDialogComponent.open(this.dialogService, {
|
||||||
|
fingerprint,
|
||||||
|
});
|
||||||
|
|
||||||
|
return firstValueFrom(dialogRef.closed);
|
||||||
|
}
|
||||||
|
|
||||||
|
async lock() {
|
||||||
|
await this.vaultTimeoutService.lock();
|
||||||
|
}
|
||||||
|
|
||||||
|
async logOut() {
|
||||||
|
const confirmed = await this.dialogService.openSimpleDialog({
|
||||||
|
title: { key: "logOut" },
|
||||||
|
content: { key: "logOutConfirmation" },
|
||||||
|
type: "info",
|
||||||
|
});
|
||||||
|
|
||||||
|
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
||||||
|
if (confirmed) {
|
||||||
|
this.messagingService.send("logout", { userId: userId });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy() {
|
||||||
|
this.destroy$.next();
|
||||||
|
this.destroy$.complete();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,140 +1,126 @@
|
|||||||
<app-header>
|
<popup-page>
|
||||||
<div class="left">
|
<popup-header slot="header" pageTitle="{{ 'accountSecurity' | i18n }}" showBackButton>
|
||||||
<button type="button" routerLink="/tabs/settings">
|
<ng-container slot="end">
|
||||||
<span class="header-icon"><i class="bwi bwi-angle-left" aria-hidden="true"></i></span>
|
<app-pop-out></app-pop-out>
|
||||||
<span>{{ "back" | i18n }}</span>
|
</ng-container>
|
||||||
</button>
|
</popup-header>
|
||||||
</div>
|
|
||||||
<h1 class="center">
|
<div class="tw-bg-background-alt tw-p-2" [formGroup]="form">
|
||||||
<span class="title">{{ "accountSecurity" | i18n }}</span>
|
<bit-section>
|
||||||
</h1>
|
<bit-section-header>
|
||||||
<div class="right">
|
<h2 bitTypography="h6">{{ "unlockMethods" | i18n }}</h2>
|
||||||
<app-pop-out></app-pop-out>
|
</bit-section-header>
|
||||||
</div>
|
<bit-card>
|
||||||
</app-header>
|
<bit-form-control *ngIf="supportsBiometric">
|
||||||
<main tabindex="-1" [formGroup]="form">
|
<input bitCheckbox id="biometric" type="checkbox" formControlName="biometric" />
|
||||||
<div class="box list">
|
<bit-label for="biometric" class="tw-whitespace-normal">{{
|
||||||
<h2 class="box-header">{{ "unlockMethods" | i18n }}</h2>
|
"unlockWithBiometrics" | i18n
|
||||||
<div class="box-content single-line">
|
}}</bit-label>
|
||||||
<div class="box-content-row box-content-row-checkbox" appBoxRow *ngIf="supportsBiometric">
|
</bit-form-control>
|
||||||
<label for="biometric">{{ "unlockWithBiometrics" | i18n }}</label>
|
<bit-form-control class="tw-pl-5" *ngIf="supportsBiometric && this.form.value.biometric">
|
||||||
<input id="biometric" type="checkbox" formControlName="biometric" />
|
<input
|
||||||
</div>
|
bitCheckbox
|
||||||
<div
|
id="autoBiometricsPrompt"
|
||||||
class="box-content-row box-content-row-checkbox"
|
type="checkbox"
|
||||||
appBoxRow
|
formControlName="enableAutoBiometricsPrompt"
|
||||||
*ngIf="supportsBiometric && this.form.value.biometric"
|
/>
|
||||||
>
|
<bit-label for="autoBiometricsPrompt" class="tw-whitespace-normal">{{
|
||||||
<label for="autoBiometricsPrompt">{{ "enableAutoBiometricsPrompt" | i18n }}</label>
|
"enableAutoBiometricsPrompt" | i18n
|
||||||
<input
|
}}</bit-label>
|
||||||
id="autoBiometricsPrompt"
|
</bit-form-control>
|
||||||
type="checkbox"
|
<bit-form-control
|
||||||
(change)="updateAutoBiometricsPrompt()"
|
[disableMargin]="!(this.form.value.pin && showMasterPasswordOnClientRestartOption)"
|
||||||
formControlName="enableAutoBiometricsPrompt"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="box-content-row box-content-row-checkbox" appBoxRow>
|
|
||||||
<label for="pin">{{ "unlockWithPin" | i18n }}</label>
|
|
||||||
<input id="pin" type="checkbox" formControlName="pin" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="box list">
|
|
||||||
<h2 class="box-header">{{ "sessionTimeoutHeader" | i18n }}</h2>
|
|
||||||
<div class="box-content single-line">
|
|
||||||
<app-callout type="info" *ngIf="vaultTimeoutPolicyCallout | async as policy">
|
|
||||||
<span *ngIf="policy.timeout && policy.action">
|
|
||||||
{{
|
|
||||||
"vaultTimeoutPolicyWithActionInEffect"
|
|
||||||
| i18n: policy.timeout.hours : policy.timeout.minutes : (policy.action | i18n)
|
|
||||||
}}
|
|
||||||
</span>
|
|
||||||
<span *ngIf="policy.timeout && !policy.action">
|
|
||||||
{{ "vaultTimeoutPolicyInEffect" | i18n: policy.timeout.hours : policy.timeout.minutes }}
|
|
||||||
</span>
|
|
||||||
<span *ngIf="!policy.timeout && policy.action">
|
|
||||||
{{ "vaultTimeoutActionPolicyInEffect" | i18n: (policy.action | i18n) }}
|
|
||||||
</span>
|
|
||||||
</app-callout>
|
|
||||||
<app-vault-timeout-input
|
|
||||||
[vaultTimeoutOptions]="vaultTimeoutOptions"
|
|
||||||
[formControl]="form.controls.vaultTimeout"
|
|
||||||
ngDefaultControl
|
|
||||||
>
|
|
||||||
</app-vault-timeout-input>
|
|
||||||
<div class="box-content-row display-block" appBoxRow>
|
|
||||||
<label for="vaultTimeoutAction">{{ "vaultTimeoutAction" | i18n }}</label>
|
|
||||||
<select
|
|
||||||
id="vaultTimeoutAction"
|
|
||||||
name="VaultTimeoutActions"
|
|
||||||
formControlName="vaultTimeoutAction"
|
|
||||||
>
|
>
|
||||||
<option *ngFor="let action of availableVaultTimeoutActions" [ngValue]="action">
|
<input bitCheckbox id="pin" type="checkbox" formControlName="pin" />
|
||||||
{{ action | i18n }}
|
<bit-label for="pin" class="tw-whitespace-normal">{{ "unlockWithPin" | i18n }}</bit-label>
|
||||||
</option>
|
</bit-form-control>
|
||||||
</select>
|
<bit-form-control
|
||||||
</div>
|
class="tw-pl-5"
|
||||||
<div
|
disableMargin
|
||||||
*ngIf="!availableVaultTimeoutActions.includes(VaultTimeoutAction.Lock)"
|
*ngIf="this.form.value.pin && showMasterPasswordOnClientRestartOption"
|
||||||
id="unlockMethodHelp"
|
>
|
||||||
class="box-footer"
|
<input
|
||||||
>
|
bitCheckbox
|
||||||
{{ "unlockMethodNeededToChangeTimeoutActionDesc" | i18n }}
|
id="pinEphemeral"
|
||||||
</div>
|
type="checkbox"
|
||||||
</div>
|
formControlName="pinLockWithMasterPassword"
|
||||||
</div>
|
/>
|
||||||
<div class="box list">
|
<bit-label for="pinEphemeral" class="tw-whitespace-normal">{{
|
||||||
<h2 class="box-header">{{ "otherOptions" | i18n }}</h2>
|
"lockWithMasterPassOnRestart1" | i18n
|
||||||
<div class="box-content single-line">
|
}}</bit-label>
|
||||||
<button
|
</bit-form-control>
|
||||||
type="button"
|
</bit-card>
|
||||||
class="box-content-row box-content-row-flex text-default"
|
</bit-section>
|
||||||
appStopClick
|
<bit-section>
|
||||||
(click)="fingerprint()"
|
<bit-section-header>
|
||||||
>
|
<h2 bitTypography="h6">{{ "vaultTimeoutHeader" | i18n }}</h2>
|
||||||
<div class="row-main">{{ "fingerprintPhrase" | i18n }}</div>
|
</bit-section-header>
|
||||||
</button>
|
|
||||||
<button
|
<bit-card>
|
||||||
type="button"
|
<auth-vault-timeout-input
|
||||||
class="box-content-row box-content-row-flex text-default"
|
[vaultTimeoutOptions]="vaultTimeoutOptions"
|
||||||
appStopClick
|
[formControl]="form.controls.vaultTimeout"
|
||||||
(click)="twoStep()"
|
ngDefaultControl
|
||||||
>
|
>
|
||||||
<div class="row-main">{{ "twoStepLogin" | i18n }}</div>
|
</auth-vault-timeout-input>
|
||||||
<i class="bwi bwi-external-link bwi-lg row-sub-icon" aria-hidden="true"></i>
|
<bit-form-field disableMargin>
|
||||||
</button>
|
<bit-label for="vaultTimeoutAction">{{ "vaultTimeoutAction1" | i18n }}</bit-label>
|
||||||
<button
|
<select
|
||||||
type="button"
|
aria-describedby="vaultTimeoutActionHelp"
|
||||||
class="box-content-row box-content-row-flex text-default"
|
bitInput
|
||||||
appStopClick
|
id="vaultTimeoutAction"
|
||||||
(click)="changePassword()"
|
name="VaultTimeoutActions"
|
||||||
*ngIf="showChangeMasterPass"
|
formControlName="vaultTimeoutAction"
|
||||||
>
|
>
|
||||||
<div class="row-main">{{ "changeMasterPassword" | i18n }}</div>
|
<option *ngFor="let action of availableVaultTimeoutActions" [ngValue]="action">
|
||||||
<i class="bwi bwi-external-link bwi-lg row-sub-icon" aria-hidden="true"></i>
|
{{ action | i18n }}
|
||||||
</button>
|
</option>
|
||||||
<button
|
</select>
|
||||||
|
<bit-hint class="tw-text-sm" id="vaultTimeoutActionHelp">
|
||||||
|
{{ "vaultTimeoutActionDesc" | i18n }}
|
||||||
|
</bit-hint>
|
||||||
|
<bit-hint *ngIf="!availableVaultTimeoutActions.includes(VaultTimeoutAction.Lock)">
|
||||||
|
{{ "unlockMethodNeededToChangeTimeoutActionDesc" | i18n }}<br />
|
||||||
|
</bit-hint>
|
||||||
|
</bit-form-field>
|
||||||
|
<bit-hint *ngIf="hasVaultTimeoutPolicy">
|
||||||
|
{{ "vaultTimeoutPolicyAffectingOptions" | i18n }}
|
||||||
|
</bit-hint>
|
||||||
|
</bit-card>
|
||||||
|
</bit-section>
|
||||||
|
<bit-section>
|
||||||
|
<bit-section-header>
|
||||||
|
<h2 bitTypography="h6">{{ "otherOptions" | i18n }}</h2>
|
||||||
|
</bit-section-header>
|
||||||
|
<bit-item>
|
||||||
|
<button bit-item-content type="button" appStopClick (click)="fingerprint()">
|
||||||
|
<div class="row-main">{{ "fingerprintPhrase" | i18n }}</div>
|
||||||
|
</button>
|
||||||
|
</bit-item>
|
||||||
|
<bit-item>
|
||||||
|
<button bit-item-content type="button" appStopClick (click)="twoStep()">
|
||||||
|
{{ "twoStepLogin" | i18n }}
|
||||||
|
<i slot="end" class="bwi bwi-external-link" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
</bit-item>
|
||||||
|
<bit-item *ngIf="showChangeMasterPass">
|
||||||
|
<button bit-item-content type="button" appStopClick (click)="changePassword()">
|
||||||
|
{{ "changeMasterPassword" | i18n }}
|
||||||
|
<i slot="end" class="bwi bwi-external-link" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
</bit-item>
|
||||||
|
<bit-item
|
||||||
*ngIf="
|
*ngIf="
|
||||||
!accountSwitcherEnabled && availableVaultTimeoutActions.includes(VaultTimeoutAction.Lock)
|
!accountSwitcherEnabled && availableVaultTimeoutActions.includes(VaultTimeoutAction.Lock)
|
||||||
"
|
"
|
||||||
type="button"
|
|
||||||
class="box-content-row box-content-row-flex text-default"
|
|
||||||
appStopClick
|
|
||||||
(click)="lock()"
|
|
||||||
>
|
>
|
||||||
|
<button bit-item-content type="button" appStopClick (click)="lock()"></button>
|
||||||
<div class="row-main">{{ "lockNow" | i18n }}</div>
|
<div class="row-main">{{ "lockNow" | i18n }}</div>
|
||||||
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
|
</bit-item>
|
||||||
</button>
|
<bit-item *ngIf="!accountSwitcherEnabled">
|
||||||
<button
|
<button bit-item-content type="button" appStopClick (click)="logOut()"></button>
|
||||||
*ngIf="!accountSwitcherEnabled"
|
|
||||||
type="button"
|
|
||||||
class="box-content-row box-content-row-flex text-default"
|
|
||||||
appStopClick
|
|
||||||
(click)="logOut()"
|
|
||||||
>
|
|
||||||
<div class="row-main">{{ "logOut" | i18n }}</div>
|
<div class="row-main">{{ "logOut" | i18n }}</div>
|
||||||
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
|
</bit-item>
|
||||||
</button>
|
</bit-section>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</popup-page>
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import { DialogRef } from "@angular/cdk/dialog";
|
import { DialogRef } from "@angular/cdk/dialog";
|
||||||
|
import { CommonModule } from "@angular/common";
|
||||||
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from "@angular/core";
|
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from "@angular/core";
|
||||||
import { FormBuilder } from "@angular/forms";
|
import { FormBuilder, FormsModule, ReactiveFormsModule } from "@angular/forms";
|
||||||
|
import { RouterModule } from "@angular/router";
|
||||||
import {
|
import {
|
||||||
BehaviorSubject,
|
BehaviorSubject,
|
||||||
combineLatest,
|
combineLatest,
|
||||||
concatMap,
|
concatMap,
|
||||||
distinctUntilChanged,
|
distinctUntilChanged,
|
||||||
filter,
|
|
||||||
firstValueFrom,
|
firstValueFrom,
|
||||||
map,
|
map,
|
||||||
Observable,
|
|
||||||
pairwise,
|
pairwise,
|
||||||
startWith,
|
startWith,
|
||||||
Subject,
|
Subject,
|
||||||
@@ -17,7 +17,8 @@ import {
|
|||||||
takeUntil,
|
takeUntil,
|
||||||
} from "rxjs";
|
} from "rxjs";
|
||||||
|
|
||||||
import { FingerprintDialogComponent } from "@bitwarden/auth/angular";
|
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||||
|
import { FingerprintDialogComponent, VaultTimeoutInputComponent } from "@bitwarden/auth/angular";
|
||||||
import { PinServiceAbstraction } from "@bitwarden/auth/common";
|
import { PinServiceAbstraction } from "@bitwarden/auth/common";
|
||||||
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.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 { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service";
|
||||||
@@ -39,30 +40,67 @@ import {
|
|||||||
VaultTimeoutOption,
|
VaultTimeoutOption,
|
||||||
VaultTimeoutStringType,
|
VaultTimeoutStringType,
|
||||||
} from "@bitwarden/common/types/vault-timeout.type";
|
} from "@bitwarden/common/types/vault-timeout.type";
|
||||||
import { DialogService, ToastService } from "@bitwarden/components";
|
import {
|
||||||
|
CardComponent,
|
||||||
|
CheckboxModule,
|
||||||
|
DialogService,
|
||||||
|
FormFieldModule,
|
||||||
|
IconButtonModule,
|
||||||
|
ItemModule,
|
||||||
|
LinkModule,
|
||||||
|
SectionComponent,
|
||||||
|
SectionHeaderComponent,
|
||||||
|
SelectModule,
|
||||||
|
TypographyModule,
|
||||||
|
ToastService,
|
||||||
|
} from "@bitwarden/components";
|
||||||
|
|
||||||
import { BiometricErrors, BiometricErrorTypes } from "../../../models/biometricErrors";
|
import { BiometricErrors, BiometricErrorTypes } from "../../../models/biometricErrors";
|
||||||
import { BrowserApi } from "../../../platform/browser/browser-api";
|
import { BrowserApi } from "../../../platform/browser/browser-api";
|
||||||
import { enableAccountSwitching } from "../../../platform/flags";
|
import { enableAccountSwitching } from "../../../platform/flags";
|
||||||
import BrowserPopupUtils from "../../../platform/popup/browser-popup-utils";
|
import BrowserPopupUtils from "../../../platform/popup/browser-popup-utils";
|
||||||
|
import { PopOutComponent } from "../../../platform/popup/components/pop-out.component";
|
||||||
|
import { PopupFooterComponent } from "../../../platform/popup/layout/popup-footer.component";
|
||||||
|
import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component";
|
||||||
|
import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component";
|
||||||
import { SetPinComponent } from "../components/set-pin.component";
|
import { SetPinComponent } from "../components/set-pin.component";
|
||||||
|
|
||||||
import { AwaitDesktopDialogComponent } from "./await-desktop-dialog.component";
|
import { AwaitDesktopDialogComponent } from "./await-desktop-dialog.component";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "auth-account-security",
|
|
||||||
templateUrl: "account-security.component.html",
|
templateUrl: "account-security.component.html",
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
CardComponent,
|
||||||
|
CheckboxModule,
|
||||||
|
CommonModule,
|
||||||
|
FormFieldModule,
|
||||||
|
FormsModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
IconButtonModule,
|
||||||
|
ItemModule,
|
||||||
|
JslibModule,
|
||||||
|
LinkModule,
|
||||||
|
PopOutComponent,
|
||||||
|
PopupFooterComponent,
|
||||||
|
PopupHeaderComponent,
|
||||||
|
PopupPageComponent,
|
||||||
|
RouterModule,
|
||||||
|
SectionComponent,
|
||||||
|
SectionHeaderComponent,
|
||||||
|
SelectModule,
|
||||||
|
TypographyModule,
|
||||||
|
VaultTimeoutInputComponent,
|
||||||
|
],
|
||||||
})
|
})
|
||||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||||
export class AccountSecurityComponent implements OnInit, OnDestroy {
|
export class AccountSecurityComponent implements OnInit, OnDestroy {
|
||||||
protected readonly VaultTimeoutAction = VaultTimeoutAction;
|
protected readonly VaultTimeoutAction = VaultTimeoutAction;
|
||||||
|
|
||||||
|
showMasterPasswordOnClientRestartOption = true;
|
||||||
availableVaultTimeoutActions: VaultTimeoutAction[] = [];
|
availableVaultTimeoutActions: VaultTimeoutAction[] = [];
|
||||||
vaultTimeoutOptions: VaultTimeoutOption[];
|
vaultTimeoutOptions: VaultTimeoutOption[] = [];
|
||||||
vaultTimeoutPolicyCallout: Observable<{
|
hasVaultTimeoutPolicy = false;
|
||||||
timeout: { hours: number; minutes: number };
|
|
||||||
action: VaultTimeoutAction;
|
|
||||||
}>;
|
|
||||||
supportsBiometric: boolean;
|
supportsBiometric: boolean;
|
||||||
showChangeMasterPass = true;
|
showChangeMasterPass = true;
|
||||||
accountSwitcherEnabled = false;
|
accountSwitcherEnabled = false;
|
||||||
@@ -71,6 +109,7 @@ export class AccountSecurityComponent implements OnInit, OnDestroy {
|
|||||||
vaultTimeout: [null as VaultTimeout | null],
|
vaultTimeout: [null as VaultTimeout | null],
|
||||||
vaultTimeoutAction: [VaultTimeoutAction.Lock],
|
vaultTimeoutAction: [VaultTimeoutAction.Lock],
|
||||||
pin: [null as boolean | null],
|
pin: [null as boolean | null],
|
||||||
|
pinLockWithMasterPassword: false,
|
||||||
biometric: false,
|
biometric: false,
|
||||||
enableAutoBiometricsPrompt: true,
|
enableAutoBiometricsPrompt: true,
|
||||||
});
|
});
|
||||||
@@ -102,20 +141,12 @@ export class AccountSecurityComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
|
const hasMasterPassword = await this.userVerificationService.hasMasterPassword();
|
||||||
|
this.showMasterPasswordOnClientRestartOption = hasMasterPassword;
|
||||||
const maximumVaultTimeoutPolicy = this.policyService.get$(PolicyType.MaximumVaultTimeout);
|
const maximumVaultTimeoutPolicy = this.policyService.get$(PolicyType.MaximumVaultTimeout);
|
||||||
this.vaultTimeoutPolicyCallout = maximumVaultTimeoutPolicy.pipe(
|
if ((await firstValueFrom(this.policyService.get$(PolicyType.MaximumVaultTimeout))) != null) {
|
||||||
filter((policy) => policy != null),
|
this.hasVaultTimeoutPolicy = true;
|
||||||
map((policy) => {
|
}
|
||||||
let timeout;
|
|
||||||
if (policy.data?.minutes) {
|
|
||||||
timeout = {
|
|
||||||
hours: Math.floor(policy.data?.minutes / 60),
|
|
||||||
minutes: policy.data?.minutes % 60,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return { timeout: timeout, action: policy.data?.action };
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
const showOnLocked =
|
const showOnLocked =
|
||||||
!this.platformUtilsService.isFirefox() && !this.platformUtilsService.isSafari();
|
!this.platformUtilsService.isFirefox() && !this.platformUtilsService.isSafari();
|
||||||
@@ -161,6 +192,8 @@ export class AccountSecurityComponent implements OnInit, OnDestroy {
|
|||||||
this.vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$(activeAccount.id),
|
this.vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$(activeAccount.id),
|
||||||
),
|
),
|
||||||
pin: await this.pinService.isPinSet(activeAccount.id),
|
pin: await this.pinService.isPinSet(activeAccount.id),
|
||||||
|
pinLockWithMasterPassword:
|
||||||
|
(await this.pinService.getPinLockType(activeAccount.id)) == "EPHEMERAL",
|
||||||
biometric: await this.vaultTimeoutSettingsService.isBiometricLockSet(),
|
biometric: await this.vaultTimeoutSettingsService.isBiometricLockSet(),
|
||||||
enableAutoBiometricsPrompt: await firstValueFrom(
|
enableAutoBiometricsPrompt: await firstValueFrom(
|
||||||
this.biometricStateService.promptAutomatically$,
|
this.biometricStateService.promptAutomatically$,
|
||||||
@@ -185,9 +218,8 @@ export class AccountSecurityComponent implements OnInit, OnDestroy {
|
|||||||
this.form.controls.vaultTimeoutAction.valueChanges
|
this.form.controls.vaultTimeoutAction.valueChanges
|
||||||
.pipe(
|
.pipe(
|
||||||
startWith(initialValues.vaultTimeoutAction), // emit to init pairwise
|
startWith(initialValues.vaultTimeoutAction), // emit to init pairwise
|
||||||
pairwise(),
|
map(async (value) => {
|
||||||
concatMap(async ([previousValue, newValue]) => {
|
await this.saveVaultTimeoutAction(value);
|
||||||
await this.saveVaultTimeoutAction(previousValue, newValue);
|
|
||||||
}),
|
}),
|
||||||
takeUntil(this.destroy$),
|
takeUntil(this.destroy$),
|
||||||
)
|
)
|
||||||
@@ -203,6 +235,22 @@ export class AccountSecurityComponent implements OnInit, OnDestroy {
|
|||||||
)
|
)
|
||||||
.subscribe();
|
.subscribe();
|
||||||
|
|
||||||
|
this.form.controls.pinLockWithMasterPassword.valueChanges
|
||||||
|
.pipe(
|
||||||
|
concatMap(async (value) => {
|
||||||
|
const userId = (await firstValueFrom(this.accountService.activeAccount$)).id;
|
||||||
|
const pinKeyEncryptedUserKey =
|
||||||
|
(await this.pinService.getPinKeyEncryptedUserKeyPersistent(userId)) ||
|
||||||
|
(await this.pinService.getPinKeyEncryptedUserKeyEphemeral(userId));
|
||||||
|
await this.pinService.clearPinKeyEncryptedUserKeyPersistent(userId);
|
||||||
|
await this.pinService.clearPinKeyEncryptedUserKeyEphemeral(userId);
|
||||||
|
await this.pinService.storePinKeyEncryptedUserKey(pinKeyEncryptedUserKey, value, userId);
|
||||||
|
this.refreshTimeoutSettings$.next();
|
||||||
|
}),
|
||||||
|
takeUntil(this.destroy$),
|
||||||
|
)
|
||||||
|
.subscribe();
|
||||||
|
|
||||||
this.form.controls.biometric.valueChanges
|
this.form.controls.biometric.valueChanges
|
||||||
.pipe(
|
.pipe(
|
||||||
distinctUntilChanged(),
|
distinctUntilChanged(),
|
||||||
@@ -219,6 +267,15 @@ export class AccountSecurityComponent implements OnInit, OnDestroy {
|
|||||||
)
|
)
|
||||||
.subscribe();
|
.subscribe();
|
||||||
|
|
||||||
|
this.form.controls.enableAutoBiometricsPrompt.valueChanges
|
||||||
|
.pipe(
|
||||||
|
concatMap(async (enabled) => {
|
||||||
|
await this.biometricStateService.setPromptAutomatically(enabled);
|
||||||
|
}),
|
||||||
|
takeUntil(this.destroy$),
|
||||||
|
)
|
||||||
|
.subscribe();
|
||||||
|
|
||||||
this.refreshTimeoutSettings$
|
this.refreshTimeoutSettings$
|
||||||
.pipe(
|
.pipe(
|
||||||
switchMap(() =>
|
switchMap(() =>
|
||||||
@@ -272,17 +329,6 @@ export class AccountSecurityComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// The minTimeoutError does not apply to browser because it supports Immediately
|
|
||||||
// So only check for the policyError
|
|
||||||
if (this.form.controls.vaultTimeout.hasError("policyError")) {
|
|
||||||
this.toastService.showToast({
|
|
||||||
variant: "error",
|
|
||||||
title: null,
|
|
||||||
message: this.i18nService.t("vaultTimeoutTooLarge"),
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const activeAccount = await firstValueFrom(this.accountService.activeAccount$);
|
const activeAccount = await firstValueFrom(this.accountService.activeAccount$);
|
||||||
|
|
||||||
const vaultTimeoutAction = await firstValueFrom(
|
const vaultTimeoutAction = await firstValueFrom(
|
||||||
@@ -299,8 +345,8 @@ export class AccountSecurityComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async saveVaultTimeoutAction(previousValue: VaultTimeoutAction, newValue: VaultTimeoutAction) {
|
async saveVaultTimeoutAction(value: VaultTimeoutAction) {
|
||||||
if (newValue === VaultTimeoutAction.LogOut) {
|
if (value === VaultTimeoutAction.LogOut) {
|
||||||
const confirmed = await this.dialogService.openSimpleDialog({
|
const confirmed = await this.dialogService.openSimpleDialog({
|
||||||
title: { key: "vaultTimeoutLogOutConfirmationTitle" },
|
title: { key: "vaultTimeoutLogOutConfirmationTitle" },
|
||||||
content: { key: "vaultTimeoutLogOutConfirmation" },
|
content: { key: "vaultTimeoutLogOutConfirmation" },
|
||||||
@@ -308,7 +354,7 @@ export class AccountSecurityComponent implements OnInit, OnDestroy {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!confirmed) {
|
if (!confirmed) {
|
||||||
this.form.controls.vaultTimeoutAction.setValue(previousValue, {
|
this.form.controls.vaultTimeoutAction.setValue(VaultTimeoutAction.Lock, {
|
||||||
emitEvent: false,
|
emitEvent: false,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
@@ -329,7 +375,7 @@ export class AccountSecurityComponent implements OnInit, OnDestroy {
|
|||||||
await this.vaultTimeoutSettingsService.setVaultTimeoutOptions(
|
await this.vaultTimeoutSettingsService.setVaultTimeoutOptions(
|
||||||
activeAccount.id,
|
activeAccount.id,
|
||||||
this.form.value.vaultTimeout,
|
this.form.value.vaultTimeout,
|
||||||
newValue,
|
value,
|
||||||
);
|
);
|
||||||
this.refreshTimeoutSettings$.next();
|
this.refreshTimeoutSettings$.next();
|
||||||
}
|
}
|
||||||
@@ -343,8 +389,13 @@ export class AccountSecurityComponent implements OnInit, OnDestroy {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const userId = await firstValueFrom(
|
||||||
|
this.accountService.activeAccount$.pipe(map((account) => account.id)),
|
||||||
|
);
|
||||||
const userHasPinSet = await firstValueFrom(dialogRef.closed);
|
const userHasPinSet = await firstValueFrom(dialogRef.closed);
|
||||||
this.form.controls.pin.setValue(userHasPinSet, { emitEvent: false });
|
this.form.controls.pin.setValue(userHasPinSet, { emitEvent: false });
|
||||||
|
const requireReprompt = (await this.pinService.getPinLockType(userId)) == "EPHEMERAL";
|
||||||
|
this.form.controls.pinLockWithMasterPassword.setValue(requireReprompt, { emitEvent: false });
|
||||||
} else {
|
} else {
|
||||||
await this.vaultTimeoutSettingsService.clear();
|
await this.vaultTimeoutSettingsService.clear();
|
||||||
}
|
}
|
||||||
@@ -386,77 +437,91 @@ export class AccountSecurityComponent implements OnInit, OnDestroy {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let awaitDesktopDialogRef: DialogRef<boolean, unknown> | undefined;
|
|
||||||
let biometricsResponseReceived = false;
|
|
||||||
|
|
||||||
await this.cryptoService.refreshAdditionalKeys();
|
await this.cryptoService.refreshAdditionalKeys();
|
||||||
|
|
||||||
const waitForUserDialogPromise = async () => {
|
const successful = await this.trySetupBiometrics();
|
||||||
// only show waiting dialog if we have waited for 200 msec to prevent double dialog
|
this.form.controls.biometric.setValue(successful);
|
||||||
// the os will respond instantly if the dialog shows successfully, and the desktop app will respond instantly if something is wrong
|
if (!successful) {
|
||||||
await new Promise((resolve) => setTimeout(resolve, 200));
|
await this.biometricStateService.setBiometricUnlockEnabled(false);
|
||||||
if (biometricsResponseReceived) {
|
await this.biometricStateService.setFingerprintValidated(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async trySetupBiometrics(): Promise<boolean> {
|
||||||
|
let awaitDesktopDialogRef: DialogRef<boolean, unknown> | undefined;
|
||||||
|
let biometricsResponseReceived = false;
|
||||||
|
let setupResult = false;
|
||||||
|
|
||||||
|
const waitForUserDialogPromise = async () => {
|
||||||
|
// only show waiting dialog if we have waited for 500 msec to prevent double dialog
|
||||||
|
// the os will respond instantly if the dialog shows successfully, and the desktop app will respond instantly if something is wrong
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||||
|
if (biometricsResponseReceived) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
awaitDesktopDialogRef = AwaitDesktopDialogComponent.open(this.dialogService);
|
||||||
|
await firstValueFrom(awaitDesktopDialogRef.closed);
|
||||||
|
if (!biometricsResponseReceived) {
|
||||||
|
setupResult = false;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
const biometricsPromise = async () => {
|
||||||
|
try {
|
||||||
|
const result = await this.biometricsService.authenticateBiometric();
|
||||||
|
|
||||||
|
// prevent duplicate dialog
|
||||||
|
biometricsResponseReceived = true;
|
||||||
|
if (awaitDesktopDialogRef) {
|
||||||
|
awaitDesktopDialogRef.close(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
this.platformUtilsService.showToast(
|
||||||
|
"error",
|
||||||
|
this.i18nService.t("errorEnableBiometricTitle"),
|
||||||
|
this.i18nService.t("errorEnableBiometricDesc"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
setupResult = true;
|
||||||
|
} catch (e) {
|
||||||
|
// prevent duplicate dialog
|
||||||
|
biometricsResponseReceived = true;
|
||||||
|
if (awaitDesktopDialogRef) {
|
||||||
|
awaitDesktopDialogRef.close(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.message == "canceled") {
|
||||||
|
setupResult = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
awaitDesktopDialogRef = AwaitDesktopDialogComponent.open(this.dialogService);
|
const error = BiometricErrors[e.message as BiometricErrorTypes];
|
||||||
const result = await firstValueFrom(awaitDesktopDialogRef.closed);
|
const shouldRetry = await this.dialogService.openSimpleDialog({
|
||||||
if (result !== true) {
|
title: { key: error.title },
|
||||||
this.form.controls.biometric.setValue(false);
|
content: { key: error.description },
|
||||||
|
acceptButtonText: { key: "retry" },
|
||||||
|
cancelButtonText: null,
|
||||||
|
type: "danger",
|
||||||
|
});
|
||||||
|
if (shouldRetry) {
|
||||||
|
setupResult = await this.trySetupBiometrics();
|
||||||
|
} else {
|
||||||
|
setupResult = false;
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
};
|
} finally {
|
||||||
|
if (awaitDesktopDialogRef) {
|
||||||
const biometricsPromise = async () => {
|
awaitDesktopDialogRef.close(true);
|
||||||
try {
|
|
||||||
const result = await this.biometricsService.authenticateBiometric();
|
|
||||||
|
|
||||||
// prevent duplicate dialog
|
|
||||||
biometricsResponseReceived = true;
|
|
||||||
if (awaitDesktopDialogRef) {
|
|
||||||
awaitDesktopDialogRef.close(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.form.controls.biometric.setValue(result);
|
|
||||||
if (!result) {
|
|
||||||
this.toastService.showToast({
|
|
||||||
variant: "error",
|
|
||||||
title: this.i18nService.t("errorEnableBiometricTitle"),
|
|
||||||
message: this.i18nService.t("errorEnableBiometricDesc"),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
// prevent duplicate dialog
|
|
||||||
biometricsResponseReceived = true;
|
|
||||||
if (awaitDesktopDialogRef) {
|
|
||||||
awaitDesktopDialogRef.close(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.form.controls.biometric.setValue(false);
|
|
||||||
|
|
||||||
if (e.message == "canceled") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const error = BiometricErrors[e.message as BiometricErrorTypes];
|
|
||||||
await this.dialogService.openSimpleDialog({
|
|
||||||
title: { key: error.title },
|
|
||||||
content: { key: error.description },
|
|
||||||
acceptButtonText: { key: "ok" },
|
|
||||||
cancelButtonText: null,
|
|
||||||
type: "danger",
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
if (awaitDesktopDialogRef) {
|
|
||||||
awaitDesktopDialogRef.close(true);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
};
|
||||||
|
|
||||||
await Promise.race([waitForUserDialogPromise(), biometricsPromise()]);
|
await Promise.all([waitForUserDialogPromise(), biometricsPromise()]);
|
||||||
} else {
|
return setupResult;
|
||||||
await this.biometricStateService.setBiometricUnlockEnabled(false);
|
|
||||||
await this.biometricStateService.setFingerprintValidated(false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateAutoBiometricsPrompt() {
|
async updateAutoBiometricsPrompt() {
|
||||||
@@ -471,6 +536,7 @@ export class AccountSecurityComponent implements OnInit, OnDestroy {
|
|||||||
content: { key: "changeMasterPasswordOnWebConfirmation" },
|
content: { key: "changeMasterPasswordOnWebConfirmation" },
|
||||||
type: "info",
|
type: "info",
|
||||||
acceptButtonText: { key: "continue" },
|
acceptButtonText: { key: "continue" },
|
||||||
|
cancelButtonText: { key: "cancel" },
|
||||||
});
|
});
|
||||||
if (confirmed) {
|
if (confirmed) {
|
||||||
const env = await firstValueFrom(this.environmentService.environment$);
|
const env = await firstValueFrom(this.environmentService.environment$);
|
||||||
@@ -480,9 +546,11 @@ export class AccountSecurityComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
async twoStep() {
|
async twoStep() {
|
||||||
const confirmed = await this.dialogService.openSimpleDialog({
|
const confirmed = await this.dialogService.openSimpleDialog({
|
||||||
title: { key: "twoStepLogin" },
|
title: { key: "twoStepLoginConfirmationTitle" },
|
||||||
content: { key: "twoStepLoginConfirmation" },
|
content: { key: "twoStepLoginConfirmationContent" },
|
||||||
type: "info",
|
type: "info",
|
||||||
|
acceptButtonText: { key: "continue" },
|
||||||
|
cancelButtonText: { key: "cancel" },
|
||||||
});
|
});
|
||||||
if (confirmed) {
|
if (confirmed) {
|
||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ import { LoginComponent } from "../auth/popup/login.component";
|
|||||||
import { RegisterComponent } from "../auth/popup/register.component";
|
import { RegisterComponent } from "../auth/popup/register.component";
|
||||||
import { RemovePasswordComponent } from "../auth/popup/remove-password.component";
|
import { RemovePasswordComponent } from "../auth/popup/remove-password.component";
|
||||||
import { SetPasswordComponent } from "../auth/popup/set-password.component";
|
import { SetPasswordComponent } from "../auth/popup/set-password.component";
|
||||||
|
import { AccountSecurityComponent as AccountSecurityV1Component } from "../auth/popup/settings/account-security-v1.component";
|
||||||
import { AccountSecurityComponent } from "../auth/popup/settings/account-security.component";
|
import { AccountSecurityComponent } from "../auth/popup/settings/account-security.component";
|
||||||
import { SsoComponent } from "../auth/popup/sso.component";
|
import { SsoComponent } from "../auth/popup/sso.component";
|
||||||
import { TwoFactorAuthComponent } from "../auth/popup/two-factor-auth.component";
|
import { TwoFactorAuthComponent } from "../auth/popup/two-factor-auth.component";
|
||||||
@@ -296,12 +297,11 @@ const routes: Routes = [
|
|||||||
canActivate: [authGuard],
|
canActivate: [authGuard],
|
||||||
data: { state: "autofill" },
|
data: { state: "autofill" },
|
||||||
}),
|
}),
|
||||||
{
|
...extensionRefreshSwap(AccountSecurityV1Component, AccountSecurityComponent, {
|
||||||
path: "account-security",
|
path: "account-security",
|
||||||
component: AccountSecurityComponent,
|
|
||||||
canActivate: [authGuard],
|
canActivate: [authGuard],
|
||||||
data: { state: "account-security" },
|
data: { state: "account-security" },
|
||||||
},
|
}),
|
||||||
...extensionRefreshSwap(NotificationsSettingsV1Component, NotificationsSettingsComponent, {
|
...extensionRefreshSwap(NotificationsSettingsV1Component, NotificationsSettingsComponent, {
|
||||||
path: "notifications",
|
path: "notifications",
|
||||||
canActivate: [authGuard],
|
canActivate: [authGuard],
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import { JslibModule } from "@bitwarden/angular/jslib.module";
|
|||||||
import { ColorPasswordCountPipe } from "@bitwarden/angular/pipes/color-password-count.pipe";
|
import { ColorPasswordCountPipe } from "@bitwarden/angular/pipes/color-password-count.pipe";
|
||||||
import { ColorPasswordPipe } from "@bitwarden/angular/pipes/color-password.pipe";
|
import { ColorPasswordPipe } from "@bitwarden/angular/pipes/color-password.pipe";
|
||||||
import { UserVerificationDialogComponent } from "@bitwarden/auth/angular";
|
import { UserVerificationDialogComponent } from "@bitwarden/auth/angular";
|
||||||
import { AvatarModule, ButtonModule, ToastModule } from "@bitwarden/components";
|
import { AvatarModule, ButtonModule, FormFieldModule, ToastModule } from "@bitwarden/components";
|
||||||
|
|
||||||
import { AccountComponent } from "../auth/popup/account-switching/account.component";
|
import { AccountComponent } from "../auth/popup/account-switching/account.component";
|
||||||
import { CurrentAccountComponent } from "../auth/popup/account-switching/current-account.component";
|
import { CurrentAccountComponent } from "../auth/popup/account-switching/current-account.component";
|
||||||
@@ -30,6 +30,7 @@ import { LoginComponent } from "../auth/popup/login.component";
|
|||||||
import { RegisterComponent } from "../auth/popup/register.component";
|
import { RegisterComponent } from "../auth/popup/register.component";
|
||||||
import { RemovePasswordComponent } from "../auth/popup/remove-password.component";
|
import { RemovePasswordComponent } from "../auth/popup/remove-password.component";
|
||||||
import { SetPasswordComponent } from "../auth/popup/set-password.component";
|
import { SetPasswordComponent } from "../auth/popup/set-password.component";
|
||||||
|
import { AccountSecurityComponent as AccountSecurityComponentV1 } from "../auth/popup/settings/account-security-v1.component";
|
||||||
import { AccountSecurityComponent } from "../auth/popup/settings/account-security.component";
|
import { AccountSecurityComponent } from "../auth/popup/settings/account-security.component";
|
||||||
import { VaultTimeoutInputComponent } from "../auth/popup/settings/vault-timeout-input.component";
|
import { VaultTimeoutInputComponent } from "../auth/popup/settings/vault-timeout-input.component";
|
||||||
import { SsoComponent } from "../auth/popup/sso.component";
|
import { SsoComponent } from "../auth/popup/sso.component";
|
||||||
@@ -98,6 +99,7 @@ import "../platform/popup/locales";
|
|||||||
A11yModule,
|
A11yModule,
|
||||||
AppRoutingModule,
|
AppRoutingModule,
|
||||||
AutofillComponent,
|
AutofillComponent,
|
||||||
|
AccountSecurityComponent,
|
||||||
ToastModule.forRoot({
|
ToastModule.forRoot({
|
||||||
maxOpened: 2,
|
maxOpened: 2,
|
||||||
autoDismiss: true,
|
autoDismiss: true,
|
||||||
@@ -132,6 +134,7 @@ import "../platform/popup/locales";
|
|||||||
HeaderComponent,
|
HeaderComponent,
|
||||||
UserVerificationDialogComponent,
|
UserVerificationDialogComponent,
|
||||||
CurrentAccountComponent,
|
CurrentAccountComponent,
|
||||||
|
FormFieldModule,
|
||||||
ExtensionAnonLayoutWrapperComponent,
|
ExtensionAnonLayoutWrapperComponent,
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
@@ -171,7 +174,6 @@ import "../platform/popup/locales";
|
|||||||
SendListComponent,
|
SendListComponent,
|
||||||
SendTypeComponent,
|
SendTypeComponent,
|
||||||
SetPasswordComponent,
|
SetPasswordComponent,
|
||||||
AccountSecurityComponent,
|
|
||||||
SettingsComponent,
|
SettingsComponent,
|
||||||
VaultSettingsComponent,
|
VaultSettingsComponent,
|
||||||
ShareComponent,
|
ShareComponent,
|
||||||
@@ -183,6 +185,7 @@ import "../platform/popup/locales";
|
|||||||
TwoFactorOptionsComponent,
|
TwoFactorOptionsComponent,
|
||||||
UpdateTempPasswordComponent,
|
UpdateTempPasswordComponent,
|
||||||
UserVerificationComponent,
|
UserVerificationComponent,
|
||||||
|
AccountSecurityComponentV1,
|
||||||
VaultTimeoutInputComponent,
|
VaultTimeoutInputComponent,
|
||||||
ViewComponent,
|
ViewComponent,
|
||||||
ViewCustomFieldsComponent,
|
ViewCustomFieldsComponent,
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
bitCheckbox
|
bitCheckbox
|
||||||
formControlName="requireMasterPasswordOnClientRestart"
|
formControlName="requireMasterPasswordOnClientRestart"
|
||||||
/>
|
/>
|
||||||
<span>{{ "lockWithMasterPassOnRestart" | i18n }}</span>
|
<span>{{ "lockWithMasterPassOnRestart1" | i18n }}</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div bitDialogFooter>
|
<div bitDialogFooter>
|
||||||
|
|||||||
@@ -942,6 +942,9 @@
|
|||||||
"vaultTimeout": {
|
"vaultTimeout": {
|
||||||
"message": "Vault timeout"
|
"message": "Vault timeout"
|
||||||
},
|
},
|
||||||
|
"vaultTimeout1": {
|
||||||
|
"message": "Timeout"
|
||||||
|
},
|
||||||
"vaultTimeoutDesc": {
|
"vaultTimeoutDesc": {
|
||||||
"message": "Choose when your vault will take the vault timeout action."
|
"message": "Choose when your vault will take the vault timeout action."
|
||||||
},
|
},
|
||||||
@@ -1567,7 +1570,7 @@
|
|||||||
"recommendedForSecurity": {
|
"recommendedForSecurity": {
|
||||||
"message": "Recommended for security."
|
"message": "Recommended for security."
|
||||||
},
|
},
|
||||||
"lockWithMasterPassOnRestart": {
|
"lockWithMasterPassOnRestart1": {
|
||||||
"message": "Lock with master password on restart"
|
"message": "Lock with master password on restart"
|
||||||
},
|
},
|
||||||
"deleteAccount": {
|
"deleteAccount": {
|
||||||
@@ -2099,8 +2102,8 @@
|
|||||||
"minutes": {
|
"minutes": {
|
||||||
"message": "Minutes"
|
"message": "Minutes"
|
||||||
},
|
},
|
||||||
"vaultTimeoutPolicyInEffect": {
|
"vaultTimeoutPolicyInEffect1": {
|
||||||
"message": "Your organization policies have set your maximum allowed vault timeout to $HOURS$ hour(s) and $MINUTES$ minute(s).",
|
"message": "$HOURS$ hour(s) and $MINUTES$ minute(s) maximum.",
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"hours": {
|
"hours": {
|
||||||
"content": "$1",
|
"content": "$1",
|
||||||
|
|||||||
@@ -1024,8 +1024,8 @@
|
|||||||
"unexpectedError": {
|
"unexpectedError": {
|
||||||
"message": "An unexpected error has occurred."
|
"message": "An unexpected error has occurred."
|
||||||
},
|
},
|
||||||
"expirationDateError" : {
|
"expirationDateError": {
|
||||||
"message":"Please select an expiration date that is in the future."
|
"message": "Please select an expiration date that is in the future."
|
||||||
},
|
},
|
||||||
"emailAddress": {
|
"emailAddress": {
|
||||||
"message": "Email address"
|
"message": "Email address"
|
||||||
@@ -1033,8 +1033,8 @@
|
|||||||
"yourVaultIsLockedV2": {
|
"yourVaultIsLockedV2": {
|
||||||
"message": "Your vault is locked"
|
"message": "Your vault is locked"
|
||||||
},
|
},
|
||||||
"uuid":{
|
"uuid": {
|
||||||
"message" : "UUID"
|
"message": "UUID"
|
||||||
},
|
},
|
||||||
"unlock": {
|
"unlock": {
|
||||||
"message": "Unlock"
|
"message": "Unlock"
|
||||||
@@ -1270,10 +1270,10 @@
|
|||||||
"copyUuid": {
|
"copyUuid": {
|
||||||
"message": "Copy UUID"
|
"message": "Copy UUID"
|
||||||
},
|
},
|
||||||
"errorRefreshingAccessToken":{
|
"errorRefreshingAccessToken": {
|
||||||
"message": "Access Token Refresh Error"
|
"message": "Access Token Refresh Error"
|
||||||
},
|
},
|
||||||
"errorRefreshingAccessTokenDesc":{
|
"errorRefreshingAccessTokenDesc": {
|
||||||
"message": "No refresh token or API keys found. Please try logging out and logging back in."
|
"message": "No refresh token or API keys found. Please try logging out and logging back in."
|
||||||
},
|
},
|
||||||
"warning": {
|
"warning": {
|
||||||
@@ -3993,6 +3993,9 @@
|
|||||||
"vaultTimeout": {
|
"vaultTimeout": {
|
||||||
"message": "Vault timeout"
|
"message": "Vault timeout"
|
||||||
},
|
},
|
||||||
|
"vaultTimeout1": {
|
||||||
|
"message": "Timeout"
|
||||||
|
},
|
||||||
"vaultTimeoutDesc": {
|
"vaultTimeoutDesc": {
|
||||||
"message": "Choose when your vault will take the vault timeout action."
|
"message": "Choose when your vault will take the vault timeout action."
|
||||||
},
|
},
|
||||||
@@ -4997,7 +5000,7 @@
|
|||||||
"youNeedApprovalFromYourAdminToTrySecretsManager": {
|
"youNeedApprovalFromYourAdminToTrySecretsManager": {
|
||||||
"message": "You need approval from your administrator to try Secrets Manager."
|
"message": "You need approval from your administrator to try Secrets Manager."
|
||||||
},
|
},
|
||||||
"smAccessRequestEmailSent" : {
|
"smAccessRequestEmailSent": {
|
||||||
"message": "Access request for secrets manager email sent to admins."
|
"message": "Access request for secrets manager email sent to admins."
|
||||||
},
|
},
|
||||||
"requestAccessSMDefaultEmailContent": {
|
"requestAccessSMDefaultEmailContent": {
|
||||||
@@ -5006,8 +5009,8 @@
|
|||||||
"giveMembersAccess": {
|
"giveMembersAccess": {
|
||||||
"message": "Give members access:"
|
"message": "Give members access:"
|
||||||
},
|
},
|
||||||
"viewAndSelectTheMembers" : {
|
"viewAndSelectTheMembers": {
|
||||||
"message" :"view and select the members you want to give access to Secrets Manager."
|
"message": "view and select the members you want to give access to Secrets Manager."
|
||||||
},
|
},
|
||||||
"openYourOrganizations": {
|
"openYourOrganizations": {
|
||||||
"message": "Open your organization's"
|
"message": "Open your organization's"
|
||||||
@@ -5471,6 +5474,19 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"vaultTimeoutPolicyInEffect1": {
|
||||||
|
"message": "$HOURS$ hour(s) and $MINUTES$ minute(s) maximum.",
|
||||||
|
"placeholders": {
|
||||||
|
"hours": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "5"
|
||||||
|
},
|
||||||
|
"minutes": {
|
||||||
|
"content": "$2",
|
||||||
|
"example": "5"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"vaultTimeoutPolicyWithActionInEffect": {
|
"vaultTimeoutPolicyWithActionInEffect": {
|
||||||
"message": "Your organization policies are affecting your vault timeout. Maximum allowed vault timeout is $HOURS$ hour(s) and $MINUTES$ minute(s). Your vault timeout action is set to $ACTION$.",
|
"message": "Your organization policies are affecting your vault timeout. Maximum allowed vault timeout is $HOURS$ hour(s) and $MINUTES$ minute(s). Your vault timeout action is set to $ACTION$.",
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
@@ -5497,9 +5513,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"customVaultTimeout": {
|
|
||||||
"message": "Custom vault timeout"
|
|
||||||
},
|
|
||||||
"vaultTimeoutToLarge": {
|
"vaultTimeoutToLarge": {
|
||||||
"message": "Your vault timeout exceeds the restriction set by your organization."
|
"message": "Your vault timeout exceeds the restriction set by your organization."
|
||||||
},
|
},
|
||||||
@@ -5944,10 +5957,10 @@
|
|||||||
"selfHostedBaseUrlHint": {
|
"selfHostedBaseUrlHint": {
|
||||||
"message": "Specify the base URL of your on-premises hosted Bitwarden installation. Example: https://bitwarden.company.com"
|
"message": "Specify the base URL of your on-premises hosted Bitwarden installation. Example: https://bitwarden.company.com"
|
||||||
},
|
},
|
||||||
"selfHostedCustomEnvHeader" :{
|
"selfHostedCustomEnvHeader": {
|
||||||
"message": "For advanced configuration, you can specify the base URL of each service independently."
|
"message": "For advanced configuration, you can specify the base URL of each service independently."
|
||||||
},
|
},
|
||||||
"selfHostedEnvFormInvalid" :{
|
"selfHostedEnvFormInvalid": {
|
||||||
"message": "You must add either the base Server URL or at least one custom environment."
|
"message": "You must add either the base Server URL or at least one custom environment."
|
||||||
},
|
},
|
||||||
"apiUrl": {
|
"apiUrl": {
|
||||||
@@ -7709,7 +7722,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"verificationRequired" : {
|
"verificationRequired": {
|
||||||
"message": "Verification required",
|
"message": "Verification required",
|
||||||
"description": "Default title for the user verification dialog."
|
"description": "Default title for the user verification dialog."
|
||||||
},
|
},
|
||||||
@@ -8501,7 +8514,7 @@
|
|||||||
"deleteProviderRecoverConfirmDesc": {
|
"deleteProviderRecoverConfirmDesc": {
|
||||||
"message": "You have requested to delete this Provider. Use the button below to confirm."
|
"message": "You have requested to delete this Provider. Use the button below to confirm."
|
||||||
},
|
},
|
||||||
"deleteProviderWarning": {
|
"deleteProviderWarning": {
|
||||||
"message": "Deleting your provider is permanent. It cannot be undone."
|
"message": "Deleting your provider is permanent. It cannot be undone."
|
||||||
},
|
},
|
||||||
"errorAssigningTargetCollection": {
|
"errorAssigningTargetCollection": {
|
||||||
@@ -8514,7 +8527,7 @@
|
|||||||
"message": "Integrations & SDKs",
|
"message": "Integrations & SDKs",
|
||||||
"description": "The title for the section that deals with integrations and SDKs."
|
"description": "The title for the section that deals with integrations and SDKs."
|
||||||
},
|
},
|
||||||
"integrations":{
|
"integrations": {
|
||||||
"message": "Integrations"
|
"message": "Integrations"
|
||||||
},
|
},
|
||||||
"integrationsDesc": {
|
"integrationsDesc": {
|
||||||
@@ -8585,7 +8598,7 @@
|
|||||||
},
|
},
|
||||||
"createdNewClient": {
|
"createdNewClient": {
|
||||||
"message": "Successfully created new client"
|
"message": "Successfully created new client"
|
||||||
},
|
},
|
||||||
"noAccess": {
|
"noAccess": {
|
||||||
"message": "No access"
|
"message": "No access"
|
||||||
},
|
},
|
||||||
@@ -8821,11 +8834,11 @@
|
|||||||
"placeholders": {
|
"placeholders": {
|
||||||
"value": {
|
"value": {
|
||||||
"content": "$1",
|
"content": "$1",
|
||||||
"example":"increments of 100,000"
|
"example": "increments of 100,000"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"providerReinstate":{
|
"providerReinstate": {
|
||||||
"message": " Contact Customer Support to reinstate your subscription."
|
"message": " Contact Customer Support to reinstate your subscription."
|
||||||
},
|
},
|
||||||
"secretPeopleDescription": {
|
"secretPeopleDescription": {
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
<bit-simple-dialog>
|
<bit-simple-dialog>
|
||||||
<i bitDialogIcon class="bwi bwi-info-circle tw-text-3xl" aria-hidden="true"></i>
|
<i bitDialogIcon class="bwi bwi-info-circle tw-text-3xl" aria-hidden="true"></i>
|
||||||
<span bitDialogTitle>{{ "yourAccountsFingerprint" | i18n }}:</span>
|
<span bitDialogTitle
|
||||||
|
><strong>{{ "yourAccountsFingerprint" | i18n }}:</strong></span
|
||||||
|
>
|
||||||
<span bitDialogContent>
|
<span bitDialogContent>
|
||||||
<strong>{{ data.fingerprint.join("-") }}</strong>
|
{{ data.fingerprint.join("-") }}
|
||||||
</span>
|
</span>
|
||||||
<ng-container bitDialogFooter>
|
<ng-container bitDialogFooter>
|
||||||
<a
|
<a
|
||||||
|
|||||||
@@ -1,29 +1,47 @@
|
|||||||
<div [formGroup]="form">
|
<div [formGroup]="form" class="tw-mb-4">
|
||||||
<bit-form-field>
|
<bit-form-field [disableMargin]="!showCustom">
|
||||||
<bit-label>{{ "vaultTimeout" | i18n }}</bit-label>
|
<bit-label>{{ "vaultTimeout1" | i18n }}</bit-label>
|
||||||
<bit-select formControlName="vaultTimeout">
|
<bit-select formControlName="vaultTimeout">
|
||||||
<bit-option
|
<bit-option
|
||||||
*ngFor="let o of vaultTimeoutOptions"
|
*ngFor="let o of filteredVaultTimeoutOptions"
|
||||||
[value]="o.value"
|
[value]="o.value"
|
||||||
[label]="o.name"
|
[label]="o.name"
|
||||||
></bit-option>
|
></bit-option>
|
||||||
</bit-select>
|
</bit-select>
|
||||||
<bit-hint class="tw-text-sm">{{
|
|
||||||
((canLockVault$ | async) ? "vaultTimeoutDesc" : "vaultTimeoutLogoutDesc") | i18n
|
|
||||||
}}</bit-hint>
|
|
||||||
</bit-form-field>
|
</bit-form-field>
|
||||||
<div class="tw-grid tw-grid-cols-12 tw-gap-4" *ngIf="showCustom" formGroupName="custom">
|
<div class="tw-grid tw-grid-cols-12 tw-gap-4" *ngIf="showCustom" formGroupName="custom">
|
||||||
<bit-form-field class="tw-col-span-6">
|
<bit-form-field class="tw-col-span-6" disableMargin>
|
||||||
<bit-label>{{ "customVaultTimeout" | i18n }}</bit-label>
|
<input
|
||||||
<input bitInput type="number" min="0" formControlName="hours" />
|
bitInput
|
||||||
<bit-hint>{{ "hours" | i18n }}</bit-hint>
|
type="number"
|
||||||
|
min="0"
|
||||||
|
formControlName="hours"
|
||||||
|
aria-labelledby="maximum-error"
|
||||||
|
/>
|
||||||
|
<bit-label>{{ "hours" | i18n }}</bit-label>
|
||||||
</bit-form-field>
|
</bit-form-field>
|
||||||
<bit-form-field class="tw-col-span-6 tw-self-end">
|
<bit-form-field class="tw-col-span-6 tw-self-end" disableMargin>
|
||||||
<input bitInput type="number" min="0" name="minutes" formControlName="minutes" />
|
<input
|
||||||
<bit-hint>{{ "minutes" | i18n }}</bit-hint>
|
bitInput
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
name="minutes"
|
||||||
|
formControlName="minutes"
|
||||||
|
aria-labelledby="maximum-error"
|
||||||
|
/>
|
||||||
|
<bit-label>{{ "minutes" | i18n }}</bit-label>
|
||||||
</bit-form-field>
|
</bit-form-field>
|
||||||
</div>
|
</div>
|
||||||
<small *ngIf="!exceedsMinimumTimout" class="tw-text-danger">
|
<bit-hint *ngIf="vaultTimeoutPolicy != null && !exceedsMaximumTimeout">
|
||||||
|
{{ "vaultTimeoutPolicyInEffect1" | i18n: vaultTimeoutPolicyHours : vaultTimeoutPolicyMinutes }}
|
||||||
|
</bit-hint>
|
||||||
|
<small *ngIf="!exceedsMinimumTimeout" class="tw-text-danger">
|
||||||
<i class="bwi bwi-error" aria-hidden="true"></i> {{ "vaultCustomTimeoutMinimum" | i18n }}
|
<i class="bwi bwi-error" aria-hidden="true"></i> {{ "vaultCustomTimeoutMinimum" | i18n }}
|
||||||
</small>
|
</small>
|
||||||
|
<small class="tw-text-danger" *ngIf="exceedsMaximumTimeout" id="maximum-error">
|
||||||
|
<i class="bwi bwi-error" aria-hidden="true"></i>
|
||||||
|
{{
|
||||||
|
"vaultTimeoutPolicyMaximumError" | i18n: vaultTimeoutPolicyHours : vaultTimeoutPolicyMinutes
|
||||||
|
}}
|
||||||
|
</small>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -55,16 +55,41 @@ type VaultTimeoutFormValue = VaultTimeoutForm["value"];
|
|||||||
export class VaultTimeoutInputComponent
|
export class VaultTimeoutInputComponent
|
||||||
implements ControlValueAccessor, Validator, OnInit, OnDestroy, OnChanges
|
implements ControlValueAccessor, Validator, OnInit, OnDestroy, OnChanges
|
||||||
{
|
{
|
||||||
|
protected readonly VaultTimeoutAction = VaultTimeoutAction;
|
||||||
|
|
||||||
get showCustom() {
|
get showCustom() {
|
||||||
return this.form.get("vaultTimeout").value === VaultTimeoutInputComponent.CUSTOM_VALUE;
|
return this.form.get("vaultTimeout").value === VaultTimeoutInputComponent.CUSTOM_VALUE;
|
||||||
}
|
}
|
||||||
|
|
||||||
get exceedsMinimumTimout(): boolean {
|
get exceedsMinimumTimeout(): boolean {
|
||||||
return (
|
return (
|
||||||
!this.showCustom || this.customTimeInMinutes() > VaultTimeoutInputComponent.MIN_CUSTOM_MINUTES
|
!this.showCustom || this.customTimeInMinutes() > VaultTimeoutInputComponent.MIN_CUSTOM_MINUTES
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get exceedsMaximumTimeout(): boolean {
|
||||||
|
return (
|
||||||
|
this.showCustom &&
|
||||||
|
this.customTimeInMinutes() >
|
||||||
|
this.vaultTimeoutPolicyMinutes + 60 * this.vaultTimeoutPolicyHours
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
get filteredVaultTimeoutOptions(): VaultTimeoutOption[] {
|
||||||
|
// by policy max value
|
||||||
|
if (this.vaultTimeoutPolicy == null || this.vaultTimeoutPolicy.data == null) {
|
||||||
|
return this.vaultTimeoutOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.vaultTimeoutOptions.filter((option) => {
|
||||||
|
if (typeof option.value === "number") {
|
||||||
|
return option.value <= this.vaultTimeoutPolicy.data.minutes;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
static CUSTOM_VALUE = -100;
|
static CUSTOM_VALUE = -100;
|
||||||
static MIN_CUSTOM_MINUTES = 0;
|
static MIN_CUSTOM_MINUTES = 0;
|
||||||
|
|
||||||
@@ -77,6 +102,7 @@ export class VaultTimeoutInputComponent
|
|||||||
});
|
});
|
||||||
|
|
||||||
@Input() vaultTimeoutOptions: VaultTimeoutOption[];
|
@Input() vaultTimeoutOptions: VaultTimeoutOption[];
|
||||||
|
|
||||||
vaultTimeoutPolicy: Policy;
|
vaultTimeoutPolicy: Policy;
|
||||||
vaultTimeoutPolicyHours: number;
|
vaultTimeoutPolicyHours: number;
|
||||||
vaultTimeoutPolicyMinutes: number;
|
vaultTimeoutPolicyMinutes: number;
|
||||||
@@ -207,7 +233,7 @@ export class VaultTimeoutInputComponent
|
|||||||
return { policyError: true };
|
return { policyError: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.exceedsMinimumTimout) {
|
if (!this.exceedsMinimumTimeout) {
|
||||||
return { minTimeoutError: true };
|
return { minTimeoutError: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user