mirror of
https://github.com/bitwarden/browser
synced 2025-12-10 21:33:27 +00:00
moving ownership of Auth vault timeout input component to KM (#17180)
(cherry picked from commit 5e595dabf71cdb312ae9e1e3bcc3121b3aebf19f)
This commit is contained in:
@@ -43,9 +43,6 @@ export * from "./user-verification/user-verification-dialog.component";
|
||||
export * from "./user-verification/user-verification-dialog.types";
|
||||
export * from "./user-verification/user-verification-form-input.component";
|
||||
|
||||
// vault timeout
|
||||
export * from "./vault-timeout-input/vault-timeout-input.component";
|
||||
|
||||
// sso
|
||||
export * from "./sso/sso.component";
|
||||
export * from "./sso/sso-component.service";
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
<div [formGroup]="form" class="tw-mb-4">
|
||||
<bit-form-field [disableMargin]="!showCustom">
|
||||
<bit-label>{{ "vaultTimeout1" | i18n }}</bit-label>
|
||||
<bit-select formControlName="vaultTimeout">
|
||||
<bit-option
|
||||
*ngFor="let o of filteredVaultTimeoutOptions"
|
||||
[value]="o.value"
|
||||
[label]="o.name"
|
||||
></bit-option>
|
||||
</bit-select>
|
||||
</bit-form-field>
|
||||
<div class="tw-grid tw-grid-cols-12 tw-gap-4" *ngIf="showCustom" formGroupName="custom">
|
||||
<bit-form-field class="tw-col-span-6" disableMargin>
|
||||
<input
|
||||
bitInput
|
||||
type="number"
|
||||
min="0"
|
||||
formControlName="hours"
|
||||
aria-labelledby="maximum-error"
|
||||
/>
|
||||
<bit-label>{{ "hours" | i18n }}</bit-label>
|
||||
</bit-form-field>
|
||||
<bit-form-field class="tw-col-span-6 tw-self-end" disableMargin>
|
||||
<input
|
||||
bitInput
|
||||
type="number"
|
||||
min="0"
|
||||
name="minutes"
|
||||
formControlName="minutes"
|
||||
aria-labelledby="maximum-error"
|
||||
/>
|
||||
<bit-label>{{ "minutes" | i18n }}</bit-label>
|
||||
</bit-form-field>
|
||||
</div>
|
||||
<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 }}
|
||||
</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>
|
||||
@@ -1,90 +0,0 @@
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import {
|
||||
VaultTimeoutSettingsService,
|
||||
VaultTimeoutStringType,
|
||||
} from "@bitwarden/common/key-management/vault-timeout";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { mockAccountServiceWith } from "@bitwarden/common/spec";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
|
||||
import { VaultTimeoutInputComponent } from "./vault-timeout-input.component";
|
||||
|
||||
describe("VaultTimeoutInputComponent", () => {
|
||||
let component: VaultTimeoutInputComponent;
|
||||
let fixture: ComponentFixture<VaultTimeoutInputComponent>;
|
||||
const policiesByType$ = jest.fn().mockReturnValue(new BehaviorSubject({}));
|
||||
const availableVaultTimeoutActions$ = jest.fn().mockReturnValue(new BehaviorSubject([]));
|
||||
const mockUserId = Utils.newGuid() as UserId;
|
||||
const accountService = mockAccountServiceWith(mockUserId);
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [VaultTimeoutInputComponent],
|
||||
providers: [
|
||||
{ provide: PolicyService, useValue: { policiesByType$ } },
|
||||
{ provide: AccountService, useValue: accountService },
|
||||
{ provide: VaultTimeoutSettingsService, useValue: { availableVaultTimeoutActions$ } },
|
||||
{ provide: I18nService, useValue: { t: (key: string) => key } },
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(VaultTimeoutInputComponent);
|
||||
component = fixture.componentInstance;
|
||||
component.vaultTimeoutOptions = [
|
||||
{ name: "oneMinute", value: 1 },
|
||||
{ name: "fiveMinutes", value: 5 },
|
||||
{ name: "fifteenMinutes", value: 15 },
|
||||
{ name: "thirtyMinutes", value: 30 },
|
||||
{ name: "oneHour", value: 60 },
|
||||
{ name: "fourHours", value: 240 },
|
||||
{ name: "onRefresh", value: VaultTimeoutStringType.OnRestart },
|
||||
];
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
describe("form", () => {
|
||||
beforeEach(async () => {
|
||||
await component.ngOnInit();
|
||||
});
|
||||
|
||||
it("invokes the onChange associated with `ControlValueAccessor`", () => {
|
||||
const onChange = jest.fn();
|
||||
component.registerOnChange(onChange);
|
||||
|
||||
component.form.controls.vaultTimeout.setValue(VaultTimeoutStringType.OnRestart);
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith(VaultTimeoutStringType.OnRestart);
|
||||
});
|
||||
|
||||
it("updates custom value to match preset option", () => {
|
||||
// 1 hour
|
||||
component.form.controls.vaultTimeout.setValue(60);
|
||||
|
||||
expect(component.form.value.custom).toEqual({ hours: 1, minutes: 0 });
|
||||
|
||||
// 17 minutes
|
||||
component.form.controls.vaultTimeout.setValue(17);
|
||||
|
||||
expect(component.form.value.custom).toEqual({ hours: 0, minutes: 17 });
|
||||
|
||||
// 2.25 hours
|
||||
component.form.controls.vaultTimeout.setValue(135);
|
||||
|
||||
expect(component.form.value.custom).toEqual({ hours: 2, minutes: 15 });
|
||||
});
|
||||
|
||||
it("sets custom timeout to 0 when a preset string option is selected", () => {
|
||||
// Set custom value to random values
|
||||
component.form.controls.custom.setValue({ hours: 1, minutes: 1 });
|
||||
|
||||
component.form.controls.vaultTimeout.setValue(VaultTimeoutStringType.OnLocked);
|
||||
|
||||
expect(component.form.value.custom).toEqual({ hours: 0, minutes: 0 });
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,292 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, Input, OnChanges, OnDestroy, OnInit } from "@angular/core";
|
||||
import {
|
||||
AbstractControl,
|
||||
ControlValueAccessor,
|
||||
FormBuilder,
|
||||
FormControl,
|
||||
FormGroup,
|
||||
NG_VALIDATORS,
|
||||
NG_VALUE_ACCESSOR,
|
||||
ReactiveFormsModule,
|
||||
ValidationErrors,
|
||||
Validator,
|
||||
} from "@angular/forms";
|
||||
import { filter, map, Observable, Subject, switchMap, takeUntil } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
||||
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 {
|
||||
VaultTimeout,
|
||||
VaultTimeoutAction,
|
||||
VaultTimeoutOption,
|
||||
VaultTimeoutSettingsService,
|
||||
} from "@bitwarden/common/key-management/vault-timeout";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { FormFieldModule, SelectModule } from "@bitwarden/components";
|
||||
|
||||
type VaultTimeoutForm = FormGroup<{
|
||||
vaultTimeout: FormControl<VaultTimeout | null>;
|
||||
custom: FormGroup<{
|
||||
hours: FormControl<number | null>;
|
||||
minutes: FormControl<number | null>;
|
||||
}>;
|
||||
}>;
|
||||
|
||||
type VaultTimeoutFormValue = VaultTimeoutForm["value"];
|
||||
|
||||
// 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: "auth-vault-timeout-input",
|
||||
templateUrl: "vault-timeout-input.component.html",
|
||||
imports: [CommonModule, JslibModule, ReactiveFormsModule, FormFieldModule, SelectModule],
|
||||
providers: [
|
||||
{
|
||||
provide: NG_VALUE_ACCESSOR,
|
||||
multi: true,
|
||||
useExisting: VaultTimeoutInputComponent,
|
||||
},
|
||||
{
|
||||
provide: NG_VALIDATORS,
|
||||
multi: true,
|
||||
useExisting: VaultTimeoutInputComponent,
|
||||
},
|
||||
],
|
||||
})
|
||||
export class VaultTimeoutInputComponent
|
||||
implements ControlValueAccessor, Validator, OnInit, OnDestroy, OnChanges
|
||||
{
|
||||
protected readonly VaultTimeoutAction = VaultTimeoutAction;
|
||||
|
||||
get showCustom() {
|
||||
return this.form.get("vaultTimeout").value === VaultTimeoutInputComponent.CUSTOM_VALUE;
|
||||
}
|
||||
|
||||
get exceedsMinimumTimeout(): boolean {
|
||||
return (
|
||||
!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 MIN_CUSTOM_MINUTES = 0;
|
||||
|
||||
form: VaultTimeoutForm = this.formBuilder.group({
|
||||
vaultTimeout: [null],
|
||||
custom: this.formBuilder.group({
|
||||
hours: [null],
|
||||
minutes: [null],
|
||||
}),
|
||||
});
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@Input() vaultTimeoutOptions: VaultTimeoutOption[];
|
||||
|
||||
vaultTimeoutPolicy: Policy;
|
||||
vaultTimeoutPolicyHours: number;
|
||||
vaultTimeoutPolicyMinutes: number;
|
||||
|
||||
protected canLockVault$: Observable<boolean>;
|
||||
|
||||
private onChange: (vaultTimeout: VaultTimeout) => void;
|
||||
private validatorChange: () => void;
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
constructor(
|
||||
private formBuilder: FormBuilder,
|
||||
private policyService: PolicyService,
|
||||
private vaultTimeoutSettingsService: VaultTimeoutSettingsService,
|
||||
private i18nService: I18nService,
|
||||
private accountService: AccountService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
this.accountService.activeAccount$
|
||||
.pipe(
|
||||
getUserId,
|
||||
switchMap((userId) =>
|
||||
this.policyService.policiesByType$(PolicyType.MaximumVaultTimeout, userId),
|
||||
),
|
||||
getFirstPolicy,
|
||||
filter((policy) => policy != null),
|
||||
takeUntil(this.destroy$),
|
||||
)
|
||||
.subscribe((policy) => {
|
||||
this.vaultTimeoutPolicy = policy;
|
||||
this.applyVaultTimeoutPolicy();
|
||||
});
|
||||
this.form.valueChanges
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe((value: VaultTimeoutFormValue) => {
|
||||
if (this.onChange) {
|
||||
this.onChange(this.getVaultTimeout(value));
|
||||
}
|
||||
});
|
||||
|
||||
// Assign the current value to the custom fields
|
||||
// so that if the user goes from a numeric value to custom
|
||||
// we can initialize the custom fields with the current value
|
||||
// ex: user picks 5 min, goes to custom, we want to show 0 hr, 5 min in the custom fields
|
||||
this.form.controls.vaultTimeout.valueChanges
|
||||
.pipe(
|
||||
filter((value) => value !== VaultTimeoutInputComponent.CUSTOM_VALUE),
|
||||
takeUntil(this.destroy$),
|
||||
)
|
||||
.subscribe((value) => {
|
||||
const current = typeof value === "string" ? 0 : Math.max(value, 0);
|
||||
|
||||
// This cannot emit an event b/c it would cause form.valueChanges to fire again
|
||||
// and we are already handling that above so just silently update
|
||||
// custom fields when vaultTimeout changes to a non-custom value
|
||||
this.form.patchValue(
|
||||
{
|
||||
custom: {
|
||||
hours: Math.floor(current / 60),
|
||||
minutes: current % 60,
|
||||
},
|
||||
},
|
||||
{ emitEvent: false },
|
||||
);
|
||||
});
|
||||
|
||||
this.canLockVault$ = this.vaultTimeoutSettingsService
|
||||
.availableVaultTimeoutActions$()
|
||||
.pipe(map((actions) => actions.includes(VaultTimeoutAction.Lock)));
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
ngOnChanges() {
|
||||
if (
|
||||
!this.vaultTimeoutOptions.find((p) => p.value === VaultTimeoutInputComponent.CUSTOM_VALUE)
|
||||
) {
|
||||
this.vaultTimeoutOptions.push({
|
||||
name: this.i18nService.t("custom"),
|
||||
value: VaultTimeoutInputComponent.CUSTOM_VALUE,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
getVaultTimeout(value: VaultTimeoutFormValue) {
|
||||
if (value.vaultTimeout !== VaultTimeoutInputComponent.CUSTOM_VALUE) {
|
||||
return value.vaultTimeout;
|
||||
}
|
||||
|
||||
return value.custom.hours * 60 + value.custom.minutes;
|
||||
}
|
||||
|
||||
writeValue(value: number): void {
|
||||
if (value == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.vaultTimeoutOptions.every((p) => p.value !== value)) {
|
||||
this.form.setValue({
|
||||
vaultTimeout: VaultTimeoutInputComponent.CUSTOM_VALUE,
|
||||
custom: {
|
||||
hours: Math.floor(value / 60),
|
||||
minutes: value % 60,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.form.patchValue({
|
||||
vaultTimeout: value,
|
||||
});
|
||||
}
|
||||
|
||||
registerOnChange(onChange: any): void {
|
||||
this.onChange = onChange;
|
||||
}
|
||||
|
||||
registerOnTouched(onTouched: any): void {
|
||||
// Empty
|
||||
}
|
||||
|
||||
setDisabledState?(isDisabled: boolean): void {
|
||||
// Empty
|
||||
}
|
||||
|
||||
validate(control: AbstractControl): ValidationErrors {
|
||||
if (this.vaultTimeoutPolicy && this.vaultTimeoutPolicy?.data?.minutes < control.value) {
|
||||
return { policyError: true };
|
||||
}
|
||||
|
||||
if (!this.exceedsMinimumTimeout) {
|
||||
return { minTimeoutError: true };
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
registerOnValidatorChange(fn: () => void): void {
|
||||
this.validatorChange = fn;
|
||||
}
|
||||
|
||||
private customTimeInMinutes() {
|
||||
return this.form.value.custom.hours * 60 + this.form.value.custom.minutes;
|
||||
}
|
||||
|
||||
private applyVaultTimeoutPolicy() {
|
||||
this.vaultTimeoutPolicyHours = Math.floor(this.vaultTimeoutPolicy.data.minutes / 60);
|
||||
this.vaultTimeoutPolicyMinutes = this.vaultTimeoutPolicy.data.minutes % 60;
|
||||
|
||||
this.vaultTimeoutOptions = this.vaultTimeoutOptions.filter((vaultTimeoutOption) => {
|
||||
// Always include the custom option
|
||||
if (vaultTimeoutOption.value === VaultTimeoutInputComponent.CUSTOM_VALUE) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (typeof vaultTimeoutOption.value === "number") {
|
||||
// Include numeric values that are less than or equal to the policy minutes
|
||||
return vaultTimeoutOption.value <= this.vaultTimeoutPolicy.data.minutes;
|
||||
}
|
||||
|
||||
// Exclude all string cases when there's a numeric policy defined
|
||||
return false;
|
||||
});
|
||||
|
||||
// Only call validator change if it's been set
|
||||
if (this.validatorChange) {
|
||||
this.validatorChange();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user