mirror of
https://github.com/bitwarden/browser
synced 2025-12-12 06:13:38 +00:00
* 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
279 lines
9.2 KiB
TypeScript
279 lines
9.2 KiB
TypeScript
import { CommonModule } from "@angular/common";
|
|
import { Component, DestroyRef, input, OnInit, signal } from "@angular/core";
|
|
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
|
import {
|
|
FormControl,
|
|
FormGroup,
|
|
FormsModule,
|
|
ReactiveFormsModule,
|
|
Validators,
|
|
} from "@angular/forms";
|
|
import { RouterModule } from "@angular/router";
|
|
import {
|
|
BehaviorSubject,
|
|
combineLatest,
|
|
concatMap,
|
|
distinctUntilChanged,
|
|
filter,
|
|
firstValueFrom,
|
|
map,
|
|
Observable,
|
|
of,
|
|
pairwise,
|
|
startWith,
|
|
switchMap,
|
|
} from "rxjs";
|
|
|
|
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
|
import { VaultTimeoutInputComponent } from "@bitwarden/auth/angular";
|
|
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
|
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
|
import { getFirstPolicy } from "@bitwarden/common/admin-console/services/policy/default-policy.service";
|
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
|
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
|
import {
|
|
MaximumVaultTimeoutPolicyData,
|
|
VaultTimeout,
|
|
VaultTimeoutAction,
|
|
VaultTimeoutOption,
|
|
VaultTimeoutSettingsService,
|
|
VaultTimeoutStringType,
|
|
} from "@bitwarden/common/key-management/vault-timeout";
|
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
|
import { UserId } from "@bitwarden/common/types/guid";
|
|
import {
|
|
CheckboxModule,
|
|
DialogService,
|
|
FormFieldModule,
|
|
IconButtonModule,
|
|
ItemModule,
|
|
LinkModule,
|
|
SelectModule,
|
|
ToastService,
|
|
TypographyModule,
|
|
} from "@bitwarden/components";
|
|
import { LogService } from "@bitwarden/logging";
|
|
|
|
import { SessionTimeoutSettingsComponentService } from "../services/session-timeout-settings-component.service";
|
|
|
|
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
|
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
|
@Component({
|
|
selector: "bit-session-timeout-settings",
|
|
templateUrl: "session-timeout-settings.component.html",
|
|
imports: [
|
|
CheckboxModule,
|
|
CommonModule,
|
|
FormFieldModule,
|
|
FormsModule,
|
|
ReactiveFormsModule,
|
|
IconButtonModule,
|
|
ItemModule,
|
|
JslibModule,
|
|
LinkModule,
|
|
RouterModule,
|
|
SelectModule,
|
|
TypographyModule,
|
|
VaultTimeoutInputComponent,
|
|
],
|
|
})
|
|
export class SessionTimeoutSettingsComponent implements OnInit {
|
|
// TODO remove once https://bitwarden.atlassian.net/browse/PM-27283 is completed
|
|
// This is because vaultTimeoutSettingsService.availableVaultTimeoutActions$ is not reactive, hence the change detection
|
|
// needs to be manually triggered to refresh available timeout actions
|
|
readonly refreshTimeoutActionSettings = input<Observable<void>>(
|
|
new BehaviorSubject<void>(undefined),
|
|
);
|
|
|
|
formGroup = new FormGroup({
|
|
timeout: new FormControl<VaultTimeout | null>(null, [Validators.required]),
|
|
timeoutAction: new FormControl<VaultTimeoutAction>(VaultTimeoutAction.Lock, [
|
|
Validators.required,
|
|
]),
|
|
});
|
|
protected readonly availableTimeoutActions = signal<VaultTimeoutAction[]>([]);
|
|
protected readonly availableTimeoutOptions$ =
|
|
this.sessionTimeoutSettingsComponentService.availableTimeoutOptions$.pipe(
|
|
startWith([] as VaultTimeoutOption[]),
|
|
);
|
|
protected hasVaultTimeoutPolicy$: Observable<boolean> = of(false);
|
|
|
|
private userId!: UserId;
|
|
|
|
constructor(
|
|
private readonly vaultTimeoutSettingsService: VaultTimeoutSettingsService,
|
|
private readonly sessionTimeoutSettingsComponentService: SessionTimeoutSettingsComponentService,
|
|
private readonly i18nService: I18nService,
|
|
private readonly toastService: ToastService,
|
|
private readonly policyService: PolicyService,
|
|
private readonly accountService: AccountService,
|
|
private readonly dialogService: DialogService,
|
|
private readonly logService: LogService,
|
|
private readonly destroyRef: DestroyRef,
|
|
) {}
|
|
|
|
get canLock() {
|
|
return this.availableTimeoutActions().includes(VaultTimeoutAction.Lock);
|
|
}
|
|
|
|
async ngOnInit(): Promise<void> {
|
|
const availableTimeoutOptions = await firstValueFrom(
|
|
this.sessionTimeoutSettingsComponentService.availableTimeoutOptions$,
|
|
);
|
|
|
|
this.logService.debug(
|
|
"[SessionTimeoutSettings] Available timeout options",
|
|
availableTimeoutOptions,
|
|
);
|
|
|
|
this.userId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
|
|
|
|
const maximumVaultTimeoutPolicy$ = this.policyService
|
|
.policiesByType$(PolicyType.MaximumVaultTimeout, this.userId)
|
|
.pipe(getFirstPolicy);
|
|
|
|
this.hasVaultTimeoutPolicy$ = maximumVaultTimeoutPolicy$.pipe(map((policy) => policy != null));
|
|
|
|
let timeout = await firstValueFrom(
|
|
this.vaultTimeoutSettingsService.getVaultTimeoutByUserId$(this.userId),
|
|
);
|
|
|
|
// Fallback if current timeout option is not available on this platform
|
|
// Only applies to string-based timeout types, not numeric values
|
|
const hasCurrentOption = availableTimeoutOptions.some((opt) => opt.value === timeout);
|
|
if (!hasCurrentOption && typeof timeout !== "number") {
|
|
this.logService.debug(
|
|
"[SessionTimeoutSettings] Current timeout option not available, falling back from",
|
|
{ timeout },
|
|
);
|
|
timeout = VaultTimeoutStringType.OnRestart;
|
|
}
|
|
|
|
this.formGroup.patchValue(
|
|
{
|
|
timeout: timeout,
|
|
timeoutAction: await firstValueFrom(
|
|
this.vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$(this.userId),
|
|
),
|
|
},
|
|
{ emitEvent: false },
|
|
);
|
|
|
|
this.refreshTimeoutActionSettings()
|
|
.pipe(
|
|
startWith(undefined),
|
|
switchMap(() =>
|
|
combineLatest([
|
|
this.vaultTimeoutSettingsService.availableVaultTimeoutActions$(this.userId),
|
|
this.vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$(this.userId),
|
|
maximumVaultTimeoutPolicy$,
|
|
]),
|
|
),
|
|
takeUntilDestroyed(this.destroyRef),
|
|
)
|
|
.subscribe(([availableActions, action, policy]) => {
|
|
this.availableTimeoutActions.set(availableActions);
|
|
this.formGroup.controls.timeoutAction.setValue(action, { emitEvent: false });
|
|
|
|
const policyData = policy?.data as MaximumVaultTimeoutPolicyData | undefined;
|
|
|
|
// Enable/disable the action control based on policy or available actions
|
|
if (policyData?.action != null || availableActions.length <= 1) {
|
|
this.formGroup.controls.timeoutAction.disable({ emitEvent: false });
|
|
} else {
|
|
this.formGroup.controls.timeoutAction.enable({ emitEvent: false });
|
|
}
|
|
});
|
|
|
|
this.formGroup.controls.timeout.valueChanges
|
|
.pipe(
|
|
startWith(timeout), // emit to init pairwise
|
|
filter((value) => value != null),
|
|
distinctUntilChanged(),
|
|
pairwise(),
|
|
concatMap(async ([previousValue, newValue]) => {
|
|
await this.saveTimeout(previousValue, newValue);
|
|
}),
|
|
takeUntilDestroyed(this.destroyRef),
|
|
)
|
|
.subscribe();
|
|
|
|
this.formGroup.controls.timeoutAction.valueChanges
|
|
.pipe(
|
|
filter((value) => value != null),
|
|
map(async (value) => {
|
|
await this.saveTimeoutAction(value);
|
|
}),
|
|
takeUntilDestroyed(this.destroyRef),
|
|
)
|
|
.subscribe();
|
|
}
|
|
|
|
async saveTimeout(previousValue: VaultTimeout, newValue: VaultTimeout) {
|
|
this.formGroup.controls.timeout.markAllAsTouched();
|
|
if (this.formGroup.controls.timeout.invalid) {
|
|
return;
|
|
}
|
|
|
|
this.logService.debug("[SessionTimeoutSettings] Saving timeout", { previousValue, newValue });
|
|
|
|
if (newValue === VaultTimeoutStringType.Never) {
|
|
const confirmed = await this.dialogService.openSimpleDialog({
|
|
title: { key: "warning" },
|
|
content: { key: "neverLockWarning" },
|
|
type: "warning",
|
|
});
|
|
|
|
if (!confirmed) {
|
|
this.formGroup.controls.timeout.setValue(previousValue, { emitEvent: false });
|
|
return;
|
|
}
|
|
}
|
|
|
|
const vaultTimeoutAction = await firstValueFrom(
|
|
this.vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$(this.userId),
|
|
);
|
|
|
|
await this.vaultTimeoutSettingsService.setVaultTimeoutOptions(
|
|
this.userId,
|
|
newValue,
|
|
vaultTimeoutAction,
|
|
);
|
|
|
|
this.sessionTimeoutSettingsComponentService.onTimeoutSave(newValue);
|
|
}
|
|
|
|
async saveTimeoutAction(value: VaultTimeoutAction) {
|
|
this.logService.debug("[SessionTimeoutSettings] Saving timeout action", value);
|
|
|
|
if (value === VaultTimeoutAction.LogOut) {
|
|
const confirmed = await this.dialogService.openSimpleDialog({
|
|
title: { key: "vaultTimeoutLogOutConfirmationTitle" },
|
|
content: { key: "vaultTimeoutLogOutConfirmation" },
|
|
type: "warning",
|
|
});
|
|
|
|
if (!confirmed) {
|
|
this.formGroup.controls.timeoutAction.setValue(VaultTimeoutAction.Lock, {
|
|
emitEvent: false,
|
|
});
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (this.formGroup.controls.timeout.hasError("policyError")) {
|
|
this.toastService.showToast({
|
|
variant: "error",
|
|
message: this.i18nService.t("vaultTimeoutTooLarge"),
|
|
});
|
|
return;
|
|
}
|
|
|
|
await this.vaultTimeoutSettingsService.setVaultTimeoutOptions(
|
|
this.userId,
|
|
this.formGroup.controls.timeout.value!,
|
|
value,
|
|
);
|
|
}
|
|
}
|