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