1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-06 00:13:28 +00:00

[PM-26056] Consolidated session timeout component (#16988)

* consolidated session timeout settings component

* rename preferences to appearance

* race condition bug on computed signal

* outdated header for browser

* unnecessary padding

* remove required on action, fix build

* rename localization key

* missing user id

* required

* cleanup task

* eslint fix signals rollback

* takeUntilDestroyed, null checks

* move browser specific logic outside shared component

* explicit input type

* input name

* takeUntilDestroyed, no toast

* unit tests

* cleanup

* cleanup, correct link to deprecation jira

* tech debt todo with jira

* missing web localization key when policy is on

* relative import

* extracting timeout options to component service

* duplicate localization key

* fix failing test

* subsequent timeout action selecting opening without dialog on first dialog cancellation

* default locale can be null

* unit tests failing

* rename, simplifications

* one if else feature flag

* timeout input component rendering before async pipe completion
This commit is contained in:
Maciej Zieniuk
2025-11-11 15:15:36 +01:00
committed by GitHub
parent a66227638e
commit 021d3e53aa
38 changed files with 1660 additions and 80 deletions

View File

@@ -796,6 +796,12 @@
"onLocked": {
"message": "On system lock"
},
"onIdle": {
"message": "On system idle"
},
"onSleep": {
"message": "On system sleep"
},
"onRestart": {
"message": "On browser restart"
},
@@ -5809,5 +5815,8 @@
},
"cardNumberLabel": {
"message": "Card number"
},
"sessionTimeoutSettingsAction": {
"message": "Timeout action"
}
}

View File

@@ -20,9 +20,9 @@
<bit-card>
<bit-form-control [disableMargin]="!((pinEnabled$ | async) || this.form.value.pin)">
<input bitCheckbox id="biometric" type="checkbox" formControlName="biometric" />
<bit-label for="biometric" class="tw-whitespace-normal">{{
"unlockWithBiometrics" | i18n
}}</bit-label>
<bit-label for="biometric" class="tw-whitespace-normal">
{{ "unlockWithBiometrics" | i18n }}
</bit-label>
<bit-hint *ngIf="biometricUnavailabilityReason">
{{ biometricUnavailabilityReason }}
</bit-hint>
@@ -38,9 +38,9 @@
type="checkbox"
formControlName="enableAutoBiometricsPrompt"
/>
<bit-label for="autoBiometricsPrompt" class="tw-whitespace-normal">{{
"enableAutoBiometricsPrompt" | i18n
}}</bit-label>
<bit-label for="autoBiometricsPrompt" class="tw-whitespace-normal">
{{ "enableAutoBiometricsPrompt" | i18n }}
</bit-label>
</bit-form-control>
<bit-form-control
[disableMargin]="!(this.form.value.pin && showMasterPasswordOnClientRestartOption)"
@@ -60,46 +60,60 @@
type="checkbox"
formControlName="pinLockWithMasterPassword"
/>
<bit-label for="pinEphemeral" class="tw-whitespace-normal">{{
"lockWithMasterPassOnRestart1" | i18n
}}</bit-label>
<bit-label for="pinEphemeral" class="tw-whitespace-normal">
{{ "lockWithMasterPassOnRestart1" | i18n }}
</bit-label>
</bit-form-control>
</bit-card>
</bit-section>
<bit-section>
<bit-section-header>
<h2 bitTypography="h6">{{ "vaultTimeoutHeader" | i18n }}</h2>
</bit-section-header>
@if (consolidatedSessionTimeoutComponent$ | async) {
<bit-section-header>
<h2 bitTypography="h6">
{{ "sessionTimeoutHeader" | i18n }}
</h2>
</bit-section-header>
<bit-card>
<auth-vault-timeout-input
[vaultTimeoutOptions]="vaultTimeoutOptions"
[formControl]="form.controls.vaultTimeout"
ngDefaultControl
>
</auth-vault-timeout-input>
<bit-card>
<bit-session-timeout-settings [refreshTimeoutActionSettings]="refreshTimeoutSettings$" />
</bit-card>
} @else {
<bit-section-header>
<h2 bitTypography="h6">
{{ "vaultTimeoutHeader" | i18n }}
</h2>
</bit-section-header>
<bit-form-field disableMargin>
<bit-label for="vaultTimeoutAction">{{ "vaultTimeoutAction1" | i18n }}</bit-label>
<bit-select id="vaultTimeoutAction" formControlName="vaultTimeoutAction">
<bit-option
*ngFor="let action of availableVaultTimeoutActions"
[value]="action"
[label]="action | i18n"
>
</bit-option>
</bit-select>
<bit-card>
<auth-vault-timeout-input
[vaultTimeoutOptions]="vaultTimeoutOptions"
[formControl]="form.controls.vaultTimeout"
ngDefaultControl
>
</auth-vault-timeout-input>
<bit-hint *ngIf="!availableVaultTimeoutActions.includes(VaultTimeoutAction.Lock)">
{{ "unlockMethodNeededToChangeTimeoutActionDesc" | i18n }}<br />
<bit-form-field disableMargin>
<bit-label for="vaultTimeoutAction">{{ "vaultTimeoutAction1" | i18n }}</bit-label>
<bit-select id="vaultTimeoutAction" formControlName="vaultTimeoutAction">
<bit-option
*ngFor="let action of availableVaultTimeoutActions"
[value]="action"
[label]="action | i18n"
>
</bit-option>
</bit-select>
<bit-hint *ngIf="!availableVaultTimeoutActions.includes(VaultTimeoutAction.Lock)">
{{ "unlockMethodNeededToChangeTimeoutActionDesc" | i18n }}<br />
</bit-hint>
</bit-form-field>
<bit-hint *ngIf="hasVaultTimeoutPolicy" class="tw-mt-4">
{{ "vaultTimeoutPolicyAffectingOptions" | i18n }}
</bit-hint>
</bit-form-field>
<bit-hint *ngIf="hasVaultTimeoutPolicy" class="tw-mt-4">
{{ "vaultTimeoutPolicyAffectingOptions" | i18n }}
</bit-hint>
</bit-card>
</bit-card>
}
</bit-section>
<bit-section>

View File

@@ -20,6 +20,7 @@ import {
VaultTimeoutStringType,
VaultTimeoutAction,
} from "@bitwarden/common/key-management/vault-timeout";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
@@ -64,6 +65,7 @@ describe("AccountSecurityComponent", () => {
const dialogService = mock<DialogService>();
const platformUtilsService = mock<PlatformUtilsService>();
const lockService = mock<LockService>();
const configService = mock<ConfigService>();
beforeEach(async () => {
await TestBed.configureTestingModule({
@@ -93,6 +95,7 @@ describe("AccountSecurityComponent", () => {
{ provide: CollectionService, useValue: mock<CollectionService>() },
{ provide: ValidationService, useValue: validationService },
{ provide: LockService, useValue: lockService },
{ provide: ConfigService, useValue: configService },
],
})
.overrideComponent(AccountSecurityComponent, {

View File

@@ -32,6 +32,7 @@ import { getFirstPolicy } from "@bitwarden/common/admin-console/services/policy/
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction";
import {
VaultTimeout,
@@ -40,6 +41,7 @@ import {
VaultTimeoutSettingsService,
VaultTimeoutStringType,
} from "@bitwarden/common/key-management/vault-timeout";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
@@ -67,6 +69,7 @@ import {
BiometricStateService,
BiometricsStatus,
} from "@bitwarden/key-management";
import { SessionTimeoutSettingsComponent } from "@bitwarden/key-management-ui";
import { BiometricErrors, BiometricErrorTypes } from "../../../models/biometricErrors";
import { BrowserApi } from "../../../platform/browser/browser-api";
@@ -100,6 +103,7 @@ import { AwaitDesktopDialogComponent } from "./await-desktop-dialog.component";
SectionComponent,
SectionHeaderComponent,
SelectModule,
SessionTimeoutSettingsComponent,
SpotlightComponent,
TypographyModule,
VaultTimeoutInputComponent,
@@ -133,11 +137,14 @@ export class AccountSecurityComponent implements OnInit, OnDestroy {
),
);
private refreshTimeoutSettings$ = new BehaviorSubject<void>(undefined);
protected readonly consolidatedSessionTimeoutComponent$: Observable<boolean>;
protected refreshTimeoutSettings$ = new BehaviorSubject<void>(undefined);
private destroy$ = new Subject<void>();
constructor(
private accountService: AccountService,
private configService: ConfigService,
private pinService: PinServiceAbstraction,
private policyService: PolicyService,
private formBuilder: FormBuilder,
@@ -157,7 +164,11 @@ export class AccountSecurityComponent implements OnInit, OnDestroy {
private vaultNudgesService: NudgesService,
private validationService: ValidationService,
private logService: LogService,
) {}
) {
this.consolidatedSessionTimeoutComponent$ = this.configService.getFeatureFlag$(
FeatureFlag.ConsolidatedSessionTimeoutComponent,
);
}
async ngOnInit() {
const hasMasterPassword = await this.userVerificationService.hasMasterPassword();
@@ -173,6 +184,7 @@ export class AccountSecurityComponent implements OnInit, OnDestroy {
this.hasVaultTimeoutPolicy = true;
}
// Determine platform-specific timeout options
const showOnLocked =
!this.platformUtilsService.isFirefox() &&
!this.platformUtilsService.isSafari() &&

View File

@@ -0,0 +1,58 @@
import { defer, Observable, of } from "rxjs";
import {
VaultTimeout,
VaultTimeoutOption,
VaultTimeoutStringType,
} from "@bitwarden/common/key-management/vault-timeout";
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 { SessionTimeoutSettingsComponentService } from "@bitwarden/key-management-ui";
export class BrowserSessionTimeoutSettingsComponentService
implements SessionTimeoutSettingsComponentService
{
availableTimeoutOptions$: Observable<VaultTimeoutOption[]> = defer(() => {
const options: VaultTimeoutOption[] = [
{ 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 },
];
const showOnLocked =
!this.platformUtilsService.isFirefox() &&
!this.platformUtilsService.isSafari() &&
!(this.platformUtilsService.isOpera() && navigator.platform === "MacIntel");
if (showOnLocked) {
options.push({
name: this.i18nService.t("onLocked"),
value: VaultTimeoutStringType.OnLocked,
});
}
options.push(
{ name: this.i18nService.t("onRestart"), value: VaultTimeoutStringType.OnRestart },
{ name: this.i18nService.t("never"), value: VaultTimeoutStringType.Never },
);
return of(options);
});
constructor(
private readonly i18nService: I18nService,
private readonly platformUtilsService: PlatformUtilsService,
private readonly messagingService: MessagingService,
) {}
onTimeoutSave(timeout: VaultTimeout): void {
if (timeout === VaultTimeoutStringType.Never) {
this.messagingService.send("bgReseedStorage");
}
}
}

View File

@@ -141,7 +141,10 @@ import {
KdfConfigService,
KeyService,
} from "@bitwarden/key-management";
import { LockComponentService } from "@bitwarden/key-management-ui";
import {
LockComponentService,
SessionTimeoutSettingsComponentService,
} from "@bitwarden/key-management-ui";
import { DerivedStateProvider, GlobalStateProvider, StateProvider } from "@bitwarden/state";
import { InlineDerivedStateProvider } from "@bitwarden/state-internal";
import {
@@ -165,6 +168,7 @@ import AutofillService from "../../autofill/services/autofill.service";
import { InlineMenuFieldQualificationService } from "../../autofill/services/inline-menu-field-qualification.service";
import { ForegroundBrowserBiometricsService } from "../../key-management/biometrics/foreground-browser-biometrics";
import { ExtensionLockComponentService } from "../../key-management/lock/services/extension-lock-component.service";
import { BrowserSessionTimeoutSettingsComponentService } from "../../key-management/session-timeout/services/browser-session-timeout-settings-component.service";
import { ForegroundVaultTimeoutService } from "../../key-management/vault-timeout/foreground-vault-timeout.service";
import { BrowserActionsService } from "../../platform/actions/browser-actions.service";
import { BrowserApi } from "../../platform/browser/browser-api";
@@ -713,6 +717,11 @@ const safeProviders: SafeProvider[] = [
useClass: ExtensionNewDeviceVerificationComponentService,
deps: [],
}),
safeProvider({
provide: SessionTimeoutSettingsComponentService,
useClass: BrowserSessionTimeoutSettingsComponentService,
deps: [I18nServiceAbstraction, PlatformUtilsService, MessagingServiceAbstraction],
}),
];
@NgModule({