1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-16 16:23:44 +00:00

[PM-23524] Port desktop settings to CL vault timeout, and drop old non-CL vault timeout components (#15513)

* Remove unused old vault timeout component

* Drop desktop specific vault timeout component and replace it with shared CL implementation

* Fix tests

* Fix test

* Fix build on desktop

* Fix tests

* Fix margin
This commit is contained in:
Bernd Schoolmann
2025-07-28 16:17:53 +02:00
committed by GitHub
parent b0ffaf0b18
commit 38d5edc2c5
10 changed files with 205 additions and 299 deletions

View File

@@ -1,44 +0,0 @@
<div [formGroup]="form">
<div class="box-content-row last display-block" appBoxRow>
<label for="vaultTimeout">{{ "vaultTimeout" | i18n }}</label>
<select
id="vaultTimeout"
name="VaultTimeout"
formControlName="vaultTimeout"
class="form-control"
>
<option *ngFor="let o of vaultTimeoutOptions" [ngValue]="o.value">{{ o.name }}</option>
</select>
</div>
<div class="box-content-row last" *ngIf="showCustom">
<div formGroupName="custom" class="row">
<div class="col">
<div class="display-block" appBoxRow>
<label for="customVaultTimeout">{{ "hours" | i18n }}</label>
<input
id="hours"
class="form-control"
type="number"
min="0"
name="hours"
formControlName="hours"
/>
</div>
</div>
<div class="col">
<div class="display-block" appBoxRow>
<label for="customVaultTimeout">{{ "minutes" | i18n }}</label>
<input
id="minutes"
class="form-control"
type="number"
min="0"
max="59"
name="minutes"
formControlName="minutes"
/>
</div>
</div>
</div>
</div>
</div>

View File

@@ -1,23 +0,0 @@
import { Component } from "@angular/core";
import { NG_VALIDATORS, NG_VALUE_ACCESSOR } from "@angular/forms";
import { VaultTimeoutInputComponent as VaultTimeoutInputComponentBase } from "@bitwarden/auth/angular";
@Component({
selector: "app-vault-timeout-input",
templateUrl: "vault-timeout-input.component.html",
providers: [
{
provide: NG_VALUE_ACCESSOR,
multi: true,
useExisting: VaultTimeoutInputComponent,
},
{
provide: NG_VALIDATORS,
multi: true,
useExisting: VaultTimeoutInputComponent,
},
],
standalone: false,
})
export class VaultTimeoutInputComponent extends VaultTimeoutInputComponentBase {}

View File

@@ -27,7 +27,6 @@ import {
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";
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 { AutofillComponent } from "../autofill/popup/settings/autofill.component"; import { AutofillComponent } from "../autofill/popup/settings/autofill.component";
import { NotificationsSettingsComponent } from "../autofill/popup/settings/notifications.component"; import { NotificationsSettingsComponent } from "../autofill/popup/settings/notifications.component";
import { RemovePasswordComponent } from "../key-management/key-connector/remove-password.component"; import { RemovePasswordComponent } from "../key-management/key-connector/remove-password.component";
@@ -96,7 +95,6 @@ import "../platform/popup/locales";
ColorPasswordCountPipe, ColorPasswordCountPipe,
TabsV2Component, TabsV2Component,
UserVerificationComponent, UserVerificationComponent,
VaultTimeoutInputComponent,
RemovePasswordComponent, RemovePasswordComponent,
EnvironmentSelectorComponent, EnvironmentSelectorComponent,
], ],

View File

@@ -30,88 +30,39 @@
</button> </button>
</h2> </h2>
<ng-container *ngIf="showSecurity"> <ng-container *ngIf="showSecurity">
<bit-callout type="info" *ngIf="vaultTimeoutPolicyCallout | async as policy"> <bit-section disableMargin>
<span *ngIf="policy.timeout && policy.action"> <bit-section-header>
{{ <h2 bitTypography="h6">{{ "vaultTimeoutHeader" | i18n }}</h2>
"vaultTimeoutPolicyWithActionInEffect" </bit-section-header>
| i18n: policy.timeout.hours : policy.timeout.minutes : (policy.action | i18n)
}} <auth-vault-timeout-input
</span> [vaultTimeoutOptions]="vaultTimeoutOptions"
<span *ngIf="policy.timeout && !policy.action"> [formControl]="form.controls.vaultTimeout"
{{ ngDefaultControl
"vaultTimeoutPolicyInEffect"
| i18n: policy.timeout.hours : policy.timeout.minutes
}}
</span>
<span *ngIf="!policy.timeout && policy.action">
{{ "vaultTimeoutActionPolicyInEffect" | i18n: (policy.action | i18n) }}
</span>
</bit-callout>
<app-vault-timeout-input
[vaultTimeoutOptions]="vaultTimeoutOptions"
[formControl]="form.controls.vaultTimeout"
ngDefaultControl
></app-vault-timeout-input>
<ng-container
*ngIf="availableVaultTimeoutActions$ | async as availableVaultTimeoutActions"
>
<div class="form-group">
<label>{{ "vaultTimeoutAction" | i18n }}</label>
<div class="radio radio-mt-2">
<label for="vaultTimeoutActionLock">
<!--
Using [attr.disabled] because reactive forms don't support disabling individual radio buttons, see:
https://github.com/angular/angular/issues/11763
-->
<input
type="radio"
id="vaultTimeoutActionLock"
value="{{ VaultTimeoutAction.Lock }}"
aria-describedby="vaultTimeoutActionLockHelp"
formControlName="vaultTimeoutAction"
[attr.disabled]="
!availableVaultTimeoutActions.includes(VaultTimeoutAction.Lock)
? true
: null
"
/>
{{ "lock" | i18n }}
</label>
</div>
<small id="vaultTimeoutActionLockHelp" class="help-block">{{
"vaultTimeoutActionLockDesc" | i18n
}}</small>
<div class="radio">
<label for="vaultTimeoutActionLogOut">
<input
type="radio"
id="vaultTimeoutActionLogOut"
value="{{ VaultTimeoutAction.LogOut }}"
aria-describedby="vaultTimeoutActionLogOutHelp"
formControlName="vaultTimeoutAction"
[attr.disabled]="
!availableVaultTimeoutActions.includes(VaultTimeoutAction.LogOut)
? true
: null
"
/>
{{ "logOut" | i18n }}
</label>
</div>
<small id="vaultTimeoutActionLogOutHelp" class="help-block">{{
"vaultTimeoutActionLogOutDesc" | i18n
}}</small>
</div>
<div
*ngIf="!availableVaultTimeoutActions.includes(VaultTimeoutAction.Lock)"
class="form-group"
> >
<small id="unlockMethodNeededToChangeTimeoutActionHelp" class="help-block">{{ </auth-vault-timeout-input>
"unlockMethodNeededToChangeTimeoutActionDesc" | i18n
}}</small> <bit-form-field disableMargin>
</div> <bit-label for="vaultTimeoutAction">{{ "vaultTimeoutAction1" | i18n }}</bit-label>
</ng-container> <bit-select id="vaultTimeoutAction" formControlName="vaultTimeoutAction">
<div class="form-group" *ngIf="(pinEnabled$ | async) || this.form.value.pin"> <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-section>
<div class="form-group tw-mt-4" *ngIf="(pinEnabled$ | async) || this.form.value.pin">
<div class="checkbox"> <div class="checkbox">
<label for="pin"> <label for="pin">
<input id="pin" type="checkbox" formControlName="pin" /> <input id="pin" type="checkbox" formControlName="pin" />

View File

@@ -4,7 +4,6 @@ import { By } from "@angular/platform-browser";
import { mock } from "jest-mock-extended"; import { mock } from "jest-mock-extended";
import { firstValueFrom, of } from "rxjs"; import { firstValueFrom, of } from "rxjs";
import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe";
import { PinServiceAbstraction } from "@bitwarden/auth/common"; import { PinServiceAbstraction } from "@bitwarden/auth/common";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { PolicyType } from "@bitwarden/common/admin-console/enums";
@@ -33,7 +32,7 @@ import { Utils } from "@bitwarden/common/platform/misc/utils";
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec";
import { UserId } from "@bitwarden/common/types/guid"; import { UserId } from "@bitwarden/common/types/guid";
import { DialogRef, DialogService } from "@bitwarden/components"; import { DialogRef, DialogService, ToastService } from "@bitwarden/components";
import { BiometricStateService, BiometricsStatus, KeyService } from "@bitwarden/key-management"; import { BiometricStateService, BiometricsStatus, KeyService } from "@bitwarden/key-management";
import { SetPinComponent } from "../../auth/components/set-pin.component"; import { SetPinComponent } from "../../auth/components/set-pin.component";
@@ -92,7 +91,7 @@ describe("SettingsComponent", () => {
i18nService.supportedTranslationLocales = []; i18nService.supportedTranslationLocales = [];
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
declarations: [SettingsComponent, I18nPipe], imports: [],
providers: [ providers: [
{ {
provide: AutofillSettingsServiceAbstraction, provide: AutofillSettingsServiceAbstraction,
@@ -126,11 +125,26 @@ describe("SettingsComponent", () => {
{ provide: VaultTimeoutSettingsService, useValue: vaultTimeoutSettingsService }, { provide: VaultTimeoutSettingsService, useValue: vaultTimeoutSettingsService },
{ provide: ValidationService, useValue: validationService }, { provide: ValidationService, useValue: validationService },
{ provide: MessagingService, useValue: messagingService }, { provide: MessagingService, useValue: messagingService },
{ provide: ToastService, useValue: mock<ToastService>() },
{ provide: DesktopAutotypeService, useValue: desktopAutotypeService }, { provide: DesktopAutotypeService, useValue: desktopAutotypeService },
], ],
schemas: [NO_ERRORS_SCHEMA], schemas: [NO_ERRORS_SCHEMA],
}).compileComponents(); }).compileComponents();
TestBed.overrideComponent(SettingsComponent, {
add: {
providers: [
{
provide: DialogService,
useValue: dialogService,
},
],
},
remove: {
providers: [DialogService],
},
});
fixture = TestBed.createComponent(SettingsComponent); fixture = TestBed.createComponent(SettingsComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
fixture.detectChanges(); fixture.detectChanges();
@@ -163,6 +177,7 @@ describe("SettingsComponent", () => {
themeStateService.selectedTheme$ = of(ThemeType.System); themeStateService.selectedTheme$ = of(ThemeType.System);
i18nService.userSetLocale$ = of("en"); i18nService.userSetLocale$ = of("en");
pinServiceAbstraction.isPinSet.mockResolvedValue(false); pinServiceAbstraction.isPinSet.mockResolvedValue(false);
policyService.policiesByType$.mockReturnValue(of([null]));
desktopAutotypeService.autotypeEnabled$ = of(false); desktopAutotypeService.autotypeEnabled$ = of(false);
}); });
@@ -580,7 +595,6 @@ describe("SettingsComponent", () => {
component["form"].controls.vaultTimeoutAction.setValue(DEFAULT_VAULT_TIMEOUT_ACTION, { component["form"].controls.vaultTimeoutAction.setValue(DEFAULT_VAULT_TIMEOUT_ACTION, {
emitEvent: false, emitEvent: false,
}); });
component["previousVaultTimeout"] = DEFAULT_VAULT_TIMEOUT;
}); });
it.each([ it.each([
@@ -594,14 +608,13 @@ describe("SettingsComponent", () => {
])("should save vault timeout", async (vaultTimeout: VaultTimeout) => { ])("should save vault timeout", async (vaultTimeout: VaultTimeout) => {
dialogService.openSimpleDialog.mockResolvedValue(true); dialogService.openSimpleDialog.mockResolvedValue(true);
await component.saveVaultTimeout(vaultTimeout); await component.saveVaultTimeout(DEFAULT_VAULT_TIMEOUT, vaultTimeout);
expect(vaultTimeoutSettingsService.setVaultTimeoutOptions).toHaveBeenCalledWith( expect(vaultTimeoutSettingsService.setVaultTimeoutOptions).toHaveBeenCalledWith(
mockUserId, mockUserId,
vaultTimeout, vaultTimeout,
DEFAULT_VAULT_TIMEOUT_ACTION, DEFAULT_VAULT_TIMEOUT_ACTION,
); );
expect(component["previousVaultTimeout"]).toEqual(DEFAULT_VAULT_TIMEOUT);
}); });
it("should save vault timeout when vault timeout action is disabled", async () => { it("should save vault timeout when vault timeout action is disabled", async () => {
@@ -610,24 +623,25 @@ describe("SettingsComponent", () => {
}); });
component["form"].controls.vaultTimeoutAction.disable({ emitEvent: false }); component["form"].controls.vaultTimeoutAction.disable({ emitEvent: false });
await component.saveVaultTimeout(DEFAULT_VAULT_TIMEOUT); await component.saveVaultTimeout(DEFAULT_VAULT_TIMEOUT, DEFAULT_VAULT_TIMEOUT);
expect(vaultTimeoutSettingsService.setVaultTimeoutOptions).toHaveBeenCalledWith( expect(vaultTimeoutSettingsService.setVaultTimeoutOptions).toHaveBeenCalledWith(
mockUserId, mockUserId,
DEFAULT_VAULT_TIMEOUT, DEFAULT_VAULT_TIMEOUT,
VaultTimeoutAction.LogOut, VaultTimeoutAction.LogOut,
); );
expect(component["previousVaultTimeout"]).toEqual(DEFAULT_VAULT_TIMEOUT);
}); });
it("should not save vault timeout when vault timeout is 'never' and dialog is cancelled", async () => { it("should not save vault timeout when vault timeout is 'never' and dialog is cancelled", async () => {
dialogService.openSimpleDialog.mockResolvedValue(false); dialogService.openSimpleDialog.mockResolvedValue(false);
await component.saveVaultTimeout(VaultTimeoutStringType.Never); await component.saveVaultTimeout(DEFAULT_VAULT_TIMEOUT, VaultTimeoutStringType.Never);
expect(vaultTimeoutSettingsService.setVaultTimeoutOptions).not.toHaveBeenCalled(); expect(vaultTimeoutSettingsService.setVaultTimeoutOptions).not.toHaveBeenCalledWith(
expect(component["form"].getRawValue().vaultTimeout).toEqual(DEFAULT_VAULT_TIMEOUT); mockUserId,
expect(component["previousVaultTimeout"]).toEqual(DEFAULT_VAULT_TIMEOUT); VaultTimeoutStringType.Never,
DEFAULT_VAULT_TIMEOUT_ACTION,
);
expect(dialogService.openSimpleDialog).toHaveBeenCalledWith({ expect(dialogService.openSimpleDialog).toHaveBeenCalledWith({
title: { key: "warning" }, title: { key: "warning" },
content: { key: "neverLockWarning" }, content: { key: "neverLockWarning" },
@@ -637,21 +651,27 @@ describe("SettingsComponent", () => {
it("should not save vault timeout when vault timeout is 0", async () => { it("should not save vault timeout when vault timeout is 0", async () => {
component["form"].controls.vaultTimeout.setValue(0, { emitEvent: false }); component["form"].controls.vaultTimeout.setValue(0, { emitEvent: false });
await component.saveVaultTimeout(0); await component.saveVaultTimeout(DEFAULT_VAULT_TIMEOUT, 0);
expect(vaultTimeoutSettingsService.setVaultTimeoutOptions).not.toHaveBeenCalled(); expect(vaultTimeoutSettingsService.setVaultTimeoutOptions).not.toHaveBeenCalledWith(
mockUserId,
0,
DEFAULT_VAULT_TIMEOUT_ACTION,
);
expect(component["form"].getRawValue().vaultTimeout).toEqual(0); expect(component["form"].getRawValue().vaultTimeout).toEqual(0);
expect(component["previousVaultTimeout"]).toEqual(DEFAULT_VAULT_TIMEOUT);
}); });
it("should not save vault timeout when vault timeout is invalid", async () => { it("should not save vault timeout when vault timeout is invalid", async () => {
i18nService.t.mockReturnValue("Number too large test error"); i18nService.t.mockReturnValue("Number too large test error");
component["form"].controls.vaultTimeout.setErrors({}, { emitEvent: false }); component["form"].controls.vaultTimeout.setErrors({}, { emitEvent: false });
await component.saveVaultTimeout(999_999_999); await component.saveVaultTimeout(DEFAULT_VAULT_TIMEOUT, 999_999_999);
expect(vaultTimeoutSettingsService.setVaultTimeoutOptions).not.toHaveBeenCalled(); expect(vaultTimeoutSettingsService.setVaultTimeoutOptions).not.toHaveBeenCalledWith(
mockUserId,
999_999_999,
DEFAULT_VAULT_TIMEOUT_ACTION,
);
expect(component["form"].getRawValue().vaultTimeout).toEqual(DEFAULT_VAULT_TIMEOUT); expect(component["form"].getRawValue().vaultTimeout).toEqual(DEFAULT_VAULT_TIMEOUT);
expect(component["previousVaultTimeout"]).toEqual(DEFAULT_VAULT_TIMEOUT);
expect(platformUtilsService.showToast).toHaveBeenCalledWith( expect(platformUtilsService.showToast).toHaveBeenCalledWith(
"error", "error",
null, null,

View File

@@ -1,19 +1,14 @@
// FIXME: Update this file to be type safe and remove this and next line // FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore // @ts-strict-ignore
import { Component, OnDestroy, OnInit } from "@angular/core"; import { CommonModule } from "@angular/common";
import { FormBuilder } from "@angular/forms"; import { ChangeDetectorRef, Component, OnDestroy, OnInit } from "@angular/core";
import { BehaviorSubject, Observable, Subject, firstValueFrom, of } from "rxjs"; import { FormBuilder, FormsModule, ReactiveFormsModule } from "@angular/forms";
import { import { RouterModule } from "@angular/router";
concatMap, import { BehaviorSubject, Observable, Subject, combineLatest, firstValueFrom, of } from "rxjs";
debounceTime, import { concatMap, map, pairwise, startWith, switchMap, takeUntil, timeout } from "rxjs/operators";
filter,
map,
switchMap,
takeUntil,
tap,
timeout,
} from "rxjs/operators";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { VaultTimeoutInputComponent } from "@bitwarden/auth/angular";
import { PinServiceAbstraction } from "@bitwarden/auth/common"; import { PinServiceAbstraction } from "@bitwarden/auth/common";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { PolicyType } from "@bitwarden/common/admin-console/enums";
@@ -43,7 +38,19 @@ import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.e
import { Utils } from "@bitwarden/common/platform/misc/utils"; import { Utils } from "@bitwarden/common/platform/misc/utils";
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
import { UserId } from "@bitwarden/common/types/guid"; import { UserId } from "@bitwarden/common/types/guid";
import { DialogService } from "@bitwarden/components"; import {
CheckboxModule,
DialogService,
FormFieldModule,
IconButtonModule,
ItemModule,
LinkModule,
SectionComponent,
SectionHeaderComponent,
SelectModule,
ToastService,
TypographyModule,
} from "@bitwarden/components";
import { KeyService, BiometricStateService, BiometricsStatus } from "@bitwarden/key-management"; import { KeyService, BiometricStateService, BiometricsStatus } from "@bitwarden/key-management";
import { SetPinComponent } from "../../auth/components/set-pin.component"; import { SetPinComponent } from "../../auth/components/set-pin.component";
@@ -57,14 +64,30 @@ import { NativeMessagingManifestService } from "../services/native-messaging-man
@Component({ @Component({
selector: "app-settings", selector: "app-settings",
templateUrl: "settings.component.html", templateUrl: "settings.component.html",
standalone: false, standalone: true,
imports: [
CheckboxModule,
CommonModule,
FormFieldModule,
FormsModule,
ReactiveFormsModule,
IconButtonModule,
ItemModule,
JslibModule,
LinkModule,
RouterModule,
SectionComponent,
SectionHeaderComponent,
SelectModule,
TypographyModule,
VaultTimeoutInputComponent,
],
}) })
export class SettingsComponent implements OnInit, OnDestroy { export class SettingsComponent implements OnInit, OnDestroy {
// For use in template // For use in template
protected readonly VaultTimeoutAction = VaultTimeoutAction; protected readonly VaultTimeoutAction = VaultTimeoutAction;
showMinToTray = false; showMinToTray = false;
vaultTimeoutOptions: VaultTimeoutOption[] = [];
localeOptions: any[]; localeOptions: any[];
themeOptions: any[]; themeOptions: any[];
clearClipboardOptions: any[]; clearClipboardOptions: any[];
@@ -96,12 +119,9 @@ export class SettingsComponent implements OnInit, OnDestroy {
currentUserEmail: string; currentUserEmail: string;
currentUserId: UserId; currentUserId: UserId;
availableVaultTimeoutActions$: Observable<VaultTimeoutAction[]>; availableVaultTimeoutActions: VaultTimeoutAction[] = [];
vaultTimeoutPolicyCallout: Observable<{ vaultTimeoutOptions: VaultTimeoutOption[] = [];
timeout: { hours: number; minutes: number }; hasVaultTimeoutPolicy = false;
action: "lock" | "logOut";
}>;
previousVaultTimeout: VaultTimeout = null;
userHasMasterPassword: boolean; userHasMasterPassword: boolean;
userHasPinSet: boolean; userHasPinSet: boolean;
@@ -170,6 +190,8 @@ export class SettingsComponent implements OnInit, OnDestroy {
private nativeMessagingManifestService: NativeMessagingManifestService, private nativeMessagingManifestService: NativeMessagingManifestService,
private configService: ConfigService, private configService: ConfigService,
private validationService: ValidationService, private validationService: ValidationService,
private changeDetectorRef: ChangeDetectorRef,
private toastService: ToastService,
) { ) {
this.isMac = this.platformUtilsService.getDevice() === DeviceType.MacOsDesktop; this.isMac = this.platformUtilsService.getDevice() === DeviceType.MacOsDesktop;
this.isLinux = this.platformUtilsService.getDevice() === DeviceType.LinuxDesktop; this.isLinux = this.platformUtilsService.getDevice() === DeviceType.LinuxDesktop;
@@ -255,38 +277,54 @@ export class SettingsComponent implements OnInit, OnDestroy {
this.currentUserEmail = activeAccount.email; this.currentUserEmail = activeAccount.email;
this.currentUserId = activeAccount.id; this.currentUserId = activeAccount.id;
this.availableVaultTimeoutActions$ = this.refreshTimeoutSettings$.pipe( const maximumVaultTimeoutPolicy = this.accountService.activeAccount$.pipe(
switchMap(() =>
this.vaultTimeoutSettingsService.availableVaultTimeoutActions$(activeAccount.id),
),
);
// Load timeout policy
this.vaultTimeoutPolicyCallout = this.accountService.activeAccount$.pipe(
getUserId, getUserId,
switchMap((userId) => switchMap((userId) =>
this.policyService.policiesByType$(PolicyType.MaximumVaultTimeout, userId), this.policyService.policiesByType$(PolicyType.MaximumVaultTimeout, userId),
), ),
getFirstPolicy, getFirstPolicy,
filter((policy) => policy != null), );
map((policy) => { if ((await firstValueFrom(maximumVaultTimeoutPolicy)) != null) {
let timeout; this.hasVaultTimeoutPolicy = true;
if (policy.data?.minutes) { }
timeout = {
hours: Math.floor(policy.data?.minutes / 60), this.refreshTimeoutSettings$
minutes: policy.data?.minutes % 60, .pipe(
}; switchMap(() =>
} combineLatest([
return { timeout: timeout, action: policy.data?.action }; this.vaultTimeoutSettingsService.availableVaultTimeoutActions$(),
}), this.vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$(activeAccount.id),
tap((policy) => { ]),
if (policy.action) { ),
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 }); this.form.controls.vaultTimeoutAction.disable({ emitEvent: false });
} else { } else {
this.form.controls.vaultTimeoutAction.enable({ emitEvent: false }); this.form.controls.vaultTimeoutAction.enable({ emitEvent: false });
} }
}), });
);
// Load initial values // Load initial values
this.userHasPinSet = await this.pinService.isPinSet(activeAccount.id); this.userHasPinSet = await this.pinService.isPinSet(activeAccount.id);
@@ -354,7 +392,6 @@ export class SettingsComponent implements OnInit, OnDestroy {
// Non-form values // Non-form values
this.showMinToTray = this.platformUtilsService.getDevice() !== DeviceType.LinuxDesktop; this.showMinToTray = this.platformUtilsService.getDevice() !== DeviceType.LinuxDesktop;
this.showAlwaysShowDock = this.platformUtilsService.getDevice() === DeviceType.MacOsDesktop; this.showAlwaysShowDock = this.platformUtilsService.getDevice() === DeviceType.MacOsDesktop;
this.previousVaultTimeout = this.form.value.vaultTimeout;
this.refreshTimeoutSettings$ this.refreshTimeoutSettings$
.pipe( .pipe(
@@ -370,9 +407,10 @@ export class SettingsComponent implements OnInit, OnDestroy {
// Form events // Form events
this.form.controls.vaultTimeout.valueChanges this.form.controls.vaultTimeout.valueChanges
.pipe( .pipe(
debounceTime(500), startWith(initialValues.vaultTimeout), // emit to init pairwise
concatMap(async (value) => { pairwise(),
await this.saveVaultTimeout(value); concatMap(async ([previousValue, newValue]) => {
await this.saveVaultTimeout(previousValue, newValue);
}), }),
takeUntil(this.destroy$), takeUntil(this.destroy$),
) )
@@ -423,7 +461,7 @@ export class SettingsComponent implements OnInit, OnDestroy {
}, 1000); }, 1000);
} }
async saveVaultTimeout(newValue: VaultTimeout) { async saveVaultTimeout(previousValue: VaultTimeout, newValue: VaultTimeout) {
if (newValue === VaultTimeoutStringType.Never) { if (newValue === VaultTimeoutStringType.Never) {
const confirmed = await this.dialogService.openSimpleDialog({ const confirmed = await this.dialogService.openSimpleDialog({
title: { key: "warning" }, title: { key: "warning" },
@@ -432,7 +470,7 @@ export class SettingsComponent implements OnInit, OnDestroy {
}); });
if (!confirmed) { if (!confirmed) {
this.form.controls.vaultTimeout.setValue(this.previousVaultTimeout); this.form.controls.vaultTimeout.setValue(previousValue, { emitEvent: false });
return; return;
} }
} }
@@ -451,8 +489,6 @@ export class SettingsComponent implements OnInit, OnDestroy {
return; return;
} }
this.previousVaultTimeout = this.form.value.vaultTimeout;
const activeAccount = await firstValueFrom(this.accountService.activeAccount$); const activeAccount = await firstValueFrom(this.accountService.activeAccount$);
await this.vaultTimeoutSettingsService.setVaultTimeoutOptions( await this.vaultTimeoutSettingsService.setVaultTimeoutOptions(
@@ -460,10 +496,11 @@ export class SettingsComponent implements OnInit, OnDestroy {
newValue, newValue,
this.form.getRawValue().vaultTimeoutAction, this.form.getRawValue().vaultTimeoutAction,
); );
this.refreshTimeoutSettings$.next();
} }
async saveVaultTimeoutAction(newValue: VaultTimeoutAction) { async saveVaultTimeoutAction(value: VaultTimeoutAction) {
if (newValue === "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" },
@@ -471,7 +508,7 @@ export class SettingsComponent implements OnInit, OnDestroy {
}); });
if (!confirmed) { if (!confirmed) {
this.form.controls.vaultTimeoutAction.patchValue(VaultTimeoutAction.Lock, { this.form.controls.vaultTimeoutAction.setValue(VaultTimeoutAction.Lock, {
emitEvent: false, emitEvent: false,
}); });
return; return;
@@ -479,11 +516,11 @@ export class SettingsComponent implements OnInit, OnDestroy {
} }
if (this.form.controls.vaultTimeout.hasError("policyError")) { if (this.form.controls.vaultTimeout.hasError("policyError")) {
this.platformUtilsService.showToast( this.toastService.showToast({
"error", variant: "error",
null, title: null,
this.i18nService.t("vaultTimeoutTooLarge"), message: this.i18nService.t("vaultTimeoutTooLarge"),
); });
return; return;
} }
@@ -492,8 +529,9 @@ export class SettingsComponent 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();
} }
async updatePinHandler(value: boolean) { async updatePinHandler(value: boolean) {

View File

@@ -1,42 +0,0 @@
<div [formGroup]="form">
<div class="form-group">
<label for="vaultTimeout">{{ "vaultTimeout" | i18n }}</label>
<select
id="vaultTimeout"
name="VaultTimeout"
aria-describedby="vaultTimeoutHelp"
formControlName="vaultTimeout"
class="form-control"
>
<option *ngFor="let o of vaultTimeoutOptions" [ngValue]="o.value">{{ o.name }}</option>
</select>
<small id="vaultTimeoutHelp" class="help-block">{{ "vaultTimeoutDesc" | i18n }}</small>
</div>
<div class="form-group row" *ngIf="showCustom" formGroupName="custom">
<div class="col">
<label for="hours">{{ "hours" | i18n }}</label>
<input
id="hours"
class="form-control"
type="number"
min="0"
name="hours"
formControlName="hours"
/>
</div>
<div class="col">
<label for="minutes">{{ "minutes" | i18n }}</label>
<input
id="minutes"
class="form-control"
type="number"
min="0"
max="59"
name="minutes"
formControlName="minutes"
/>
</div>
</div>
<div class="form-group"></div>
<!-- Styling fix -->
</div>

View File

@@ -1,23 +0,0 @@
import { Component } from "@angular/core";
import { NG_VALIDATORS, NG_VALUE_ACCESSOR } from "@angular/forms";
import { VaultTimeoutInputComponent as VaultTimeoutInputComponentBase } from "@bitwarden/auth/angular";
@Component({
selector: "app-vault-timeout-input",
templateUrl: "vault-timeout-input.component.html",
providers: [
{
provide: NG_VALUE_ACCESSOR,
multi: true,
useExisting: VaultTimeoutInputComponent,
},
{
provide: NG_VALIDATORS,
multi: true,
useExisting: VaultTimeoutInputComponent,
},
],
standalone: false,
})
export class VaultTimeoutInputComponent extends VaultTimeoutInputComponentBase {}

View File

@@ -19,8 +19,6 @@ import { RemovePasswordComponent } from "../key-management/key-connector/remove-
import { VaultFilterModule } from "../vault/app/vault/vault-filter/vault-filter.module"; import { VaultFilterModule } from "../vault/app/vault/vault-filter/vault-filter.module";
import { VaultV2Component } from "../vault/app/vault/vault-v2.component"; import { VaultV2Component } from "../vault/app/vault/vault-v2.component";
import { SettingsComponent } from "./accounts/settings.component";
import { VaultTimeoutInputComponent } from "./accounts/vault-timeout-input.component";
import { AppRoutingModule } from "./app-routing.module"; import { AppRoutingModule } from "./app-routing.module";
import { AppComponent } from "./app.component"; import { AppComponent } from "./app.component";
import { UserVerificationComponent } from "./components/user-verification.component"; import { UserVerificationComponent } from "./components/user-verification.component";
@@ -55,8 +53,6 @@ import { SharedModule } from "./shared/shared.module";
PremiumComponent, PremiumComponent,
RemovePasswordComponent, RemovePasswordComponent,
SearchComponent, SearchComponent,
SettingsComponent,
VaultTimeoutInputComponent,
], ],
providers: [SshAgentService], providers: [SshAgentService],
bootstrap: [AppComponent], bootstrap: [AppComponent],

View File

@@ -1225,12 +1225,18 @@
"twoStepLogin": { "twoStepLogin": {
"message": "Two-step login" "message": "Two-step login"
}, },
"vaultTimeoutHeader": {
"message": "Vault timeout"
},
"vaultTimeout": { "vaultTimeout": {
"message": "Vault timeout" "message": "Vault timeout"
}, },
"vaultTimeout1": { "vaultTimeout1": {
"message": "Timeout" "message": "Timeout"
}, },
"vaultTimeoutAction1": {
"message": "Timeout action"
},
"vaultTimeoutDesc": { "vaultTimeoutDesc": {
"message": "Choose when your vault will take the vault timeout action." "message": "Choose when your vault will take the vault timeout action."
}, },
@@ -2511,6 +2517,35 @@
"vaultTimeoutTooLarge": { "vaultTimeoutTooLarge": {
"message": "Your vault timeout exceeds the restrictions set by your organization." "message": "Your vault timeout exceeds the restrictions set by your organization."
}, },
"vaultTimeoutPolicyAffectingOptions": {
"message": "Enterprise policy requirements have been applied to your timeout options"
},
"vaultTimeoutPolicyInEffect": {
"message": "Your organization policies have set your maximum allowed vault timeout to $HOURS$ hour(s) and $MINUTES$ minute(s).",
"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"
}
}
},
"inviteAccepted": { "inviteAccepted": {
"message": "Invitation accepted" "message": "Invitation accepted"
}, },