mirror of
https://github.com/bitwarden/browser
synced 2025-12-15 07:43:35 +00:00
[PM-13876] replace angular validation with html constraints validation (#11816)
* rough-in passphrase validation failure handling * trigger valid change from settings * fix `max` constraint enforcement * add taps for generator validation monitoring/debugging * HTML constraints validation rises like a phoenix * remove min/max boundaries to fix chrome display issue * bind settings components as view children of options components * remove defunct `okSettings$` * extend validationless generator to passwords * extend validationless generator to catchall emails * extend validationless generator to forwarder emails * extend validationless generator to subaddress emails * extend validationless generator to usernames * fix observable cycle * disable generate button when no algorithm is selected * prevent duplicate algorithm emissions * add constraints that assign email address defaults
This commit is contained in:
@@ -1,6 +1,11 @@
|
||||
<form class="box" [formGroup]="settings" class="tw-container">
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "domainName" | i18n }}</bit-label>
|
||||
<input bitInput formControlName="catchallDomain" type="text" />
|
||||
<input
|
||||
bitInput
|
||||
formControlName="catchallDomain"
|
||||
type="text"
|
||||
(change)="save('catchallDomain')"
|
||||
/>
|
||||
</bit-form-field>
|
||||
</form>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core";
|
||||
import { FormBuilder } from "@angular/forms";
|
||||
import { BehaviorSubject, skip, Subject, takeUntil } from "rxjs";
|
||||
import { BehaviorSubject, map, skip, Subject, takeUntil, withLatestFrom } from "rxjs";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
@@ -12,6 +12,11 @@ import {
|
||||
|
||||
import { completeOnAccountSwitch } from "./util";
|
||||
|
||||
/** Splits an email into a username, subaddress, and domain named group.
|
||||
* Subaddress is optional.
|
||||
*/
|
||||
export const DOMAIN_PARSER = new RegExp("[^@]+@(?<domain>.+)");
|
||||
|
||||
/** Options group for catchall emails */
|
||||
@Component({
|
||||
selector: "tools-catchall-settings",
|
||||
@@ -60,7 +65,19 @@ export class CatchallSettingsComponent implements OnInit, OnDestroy {
|
||||
// the first emission is the current value; subsequent emissions are updates
|
||||
settings.pipe(skip(1), takeUntil(this.destroyed$)).subscribe(this.onUpdated);
|
||||
|
||||
this.settings.valueChanges.pipe(takeUntil(this.destroyed$)).subscribe(settings);
|
||||
// now that outputs are set up, connect inputs
|
||||
this.saveSettings
|
||||
.pipe(
|
||||
withLatestFrom(this.settings.valueChanges),
|
||||
map(([, settings]) => settings),
|
||||
takeUntil(this.destroyed$),
|
||||
)
|
||||
.subscribe(settings);
|
||||
}
|
||||
|
||||
private saveSettings = new Subject<string>();
|
||||
save(site: string = "component api call") {
|
||||
this.saveSettings.next(site);
|
||||
}
|
||||
|
||||
private singleUserId$() {
|
||||
@@ -78,6 +95,7 @@ export class CatchallSettingsComponent implements OnInit, OnDestroy {
|
||||
|
||||
private readonly destroyed$ = new Subject<void>();
|
||||
ngOnDestroy(): void {
|
||||
this.destroyed$.next();
|
||||
this.destroyed$.complete();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
buttonType="main"
|
||||
(click)="generate('user request')"
|
||||
[appA11yTitle]="credentialTypeGenerateLabel$ | async"
|
||||
[disabled]="!(algorithm$ | async)"
|
||||
>
|
||||
{{ credentialTypeGenerateLabel$ | async }}
|
||||
</button>
|
||||
@@ -33,16 +34,19 @@
|
||||
[appA11yTitle]="credentialTypeCopyLabel$ | async"
|
||||
[appCopyClick]="value$ | async"
|
||||
[valueLabel]="credentialTypeLabel$ | async"
|
||||
[disabled]="!(algorithm$ | async)"
|
||||
></button>
|
||||
</div>
|
||||
</bit-card>
|
||||
<tools-password-settings
|
||||
#passwordSettings
|
||||
class="tw-mt-6"
|
||||
*ngIf="(showAlgorithm$ | async)?.id === 'password'"
|
||||
[userId]="userId$ | async"
|
||||
(onUpdated)="generate('password settings')"
|
||||
/>
|
||||
<tools-passphrase-settings
|
||||
#passphraseSettings
|
||||
class="tw-mt-6"
|
||||
*ngIf="(showAlgorithm$ | async)?.id === 'passphrase'"
|
||||
[userId]="userId$ | async"
|
||||
@@ -80,21 +84,25 @@
|
||||
</bit-form-field>
|
||||
</form>
|
||||
<tools-catchall-settings
|
||||
#catchallSettings
|
||||
*ngIf="(showAlgorithm$ | async)?.id === 'catchall'"
|
||||
[userId]="userId$ | async"
|
||||
(onUpdated)="generate('catchall settings')"
|
||||
/>
|
||||
<tools-forwarder-settings
|
||||
#forwarderSettings
|
||||
*ngIf="!!(forwarderId$ | async)"
|
||||
[forwarder]="forwarderId$ | async"
|
||||
[userId]="this.userId$ | async"
|
||||
/>
|
||||
<tools-subaddress-settings
|
||||
#subaddressSettings
|
||||
*ngIf="(showAlgorithm$ | async)?.id === 'subaddress'"
|
||||
[userId]="userId$ | async"
|
||||
(onUpdated)="generate('subaddress settings')"
|
||||
/>
|
||||
<tools-username-settings
|
||||
#usernameSettings
|
||||
*ngIf="(showAlgorithm$ | async)?.id === 'username'"
|
||||
[userId]="userId$ | async"
|
||||
(onUpdated)="generate('username settings')"
|
||||
|
||||
@@ -202,9 +202,8 @@ export class CredentialGeneratorComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
});
|
||||
|
||||
// normalize cascade selections; introduce subjects to allow changes
|
||||
// from user selections and changes from preference updates to
|
||||
// update the template
|
||||
// these subjects normalize cascade selections to ensure the current
|
||||
// cascade is always well-known.
|
||||
type CascadeValue = { nav: string; algorithm?: CredentialAlgorithm };
|
||||
const activeRoot$ = new Subject<CascadeValue>();
|
||||
const activeIdentifier$ = new Subject<CascadeValue>();
|
||||
@@ -385,7 +384,7 @@ export class CredentialGeneratorComponent implements OnInit, OnDestroy {
|
||||
if (!a || a.onlyOnRequest) {
|
||||
this.value$.next("-");
|
||||
} else {
|
||||
this.generate("autogenerate");
|
||||
this.generate("autogenerate").catch((e: unknown) => this.logService.error(e));
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -495,7 +494,7 @@ export class CredentialGeneratorComponent implements OnInit, OnDestroy {
|
||||
* @param requestor a label used to trace generation request
|
||||
* origin in the debugger.
|
||||
*/
|
||||
protected generate(requestor: string) {
|
||||
protected async generate(requestor: string) {
|
||||
this.generate$.next(requestor);
|
||||
}
|
||||
|
||||
@@ -510,6 +509,7 @@ export class CredentialGeneratorComponent implements OnInit, OnDestroy {
|
||||
|
||||
private readonly destroyed = new Subject<void>();
|
||||
ngOnDestroy() {
|
||||
this.destroyed.next();
|
||||
this.destroyed.complete();
|
||||
|
||||
// finalize subjects
|
||||
|
||||
@@ -1,16 +1,28 @@
|
||||
<form class="box" [formGroup]="settings" class="tw-container">
|
||||
<bit-form-field *ngIf="displayDomain">
|
||||
<bit-label>{{ "forwarderDomainName" | i18n }}</bit-label>
|
||||
<input bitInput formControlName="domain" type="text" placeholder="example.com" />
|
||||
<input
|
||||
bitInput
|
||||
formControlName="domain"
|
||||
type="text"
|
||||
placeholder="example.com"
|
||||
(change)="save('domain')"
|
||||
/>
|
||||
<bit-hint>{{ "forwarderDomainNameHint" | i18n }}</bit-hint>
|
||||
</bit-form-field>
|
||||
<bit-form-field *ngIf="displayToken">
|
||||
<bit-label>{{ "apiKey" | i18n }}</bit-label>
|
||||
<input bitInput formControlName="token" type="password" />
|
||||
<button type="button" bitIconButton bitSuffix bitPasswordInputToggle></button>
|
||||
<button
|
||||
type="button"
|
||||
bitIconButton
|
||||
bitSuffix
|
||||
bitPasswordInputToggle
|
||||
(change)="save('token')"
|
||||
></button>
|
||||
</bit-form-field>
|
||||
<bit-form-field *ngIf="displayBaseUrl" disableMargin>
|
||||
<bit-label>{{ "selfHostBaseUrl" | i18n }}</bit-label>
|
||||
<input bitInput formControlName="baseUrl" type="text" />
|
||||
<input bitInput formControlName="baseUrl" type="text" (change)="save('baseUrl')" />
|
||||
</bit-form-field>
|
||||
</form>
|
||||
|
||||
@@ -17,7 +17,6 @@ import {
|
||||
skip,
|
||||
Subject,
|
||||
switchAll,
|
||||
switchMap,
|
||||
takeUntil,
|
||||
withLatestFrom,
|
||||
} from "rxjs";
|
||||
@@ -33,7 +32,7 @@ import {
|
||||
toCredentialGeneratorConfiguration,
|
||||
} from "@bitwarden/generator-core";
|
||||
|
||||
import { completeOnAccountSwitch, toValidators } from "./util";
|
||||
import { completeOnAccountSwitch } from "./util";
|
||||
|
||||
const Controls = Object.freeze({
|
||||
domain: "domain",
|
||||
@@ -117,35 +116,17 @@ export class ForwarderSettingsComponent implements OnInit, OnChanges, OnDestroy
|
||||
this.settings.patchValue(settings as any, { emitEvent: false });
|
||||
});
|
||||
|
||||
// bind policy to the reactive form
|
||||
forwarder$
|
||||
.pipe(
|
||||
switchMap((forwarder) => {
|
||||
const constraints$ = this.generatorService
|
||||
.policy$(forwarder, { userId$: singleUserId$ })
|
||||
.pipe(map(({ constraints }) => [constraints, forwarder] as const));
|
||||
|
||||
return constraints$;
|
||||
}),
|
||||
takeUntil(this.destroyed$),
|
||||
)
|
||||
.subscribe(([constraints, forwarder]) => {
|
||||
for (const name in Controls) {
|
||||
const control = this.settings.get(name);
|
||||
if (forwarder.request.includes(name as any)) {
|
||||
control.enable({ emitEvent: false });
|
||||
control.setValidators(
|
||||
// the configuration's type erasure affects `toValidators` as well
|
||||
toValidators(name, forwarder, constraints),
|
||||
);
|
||||
} else {
|
||||
control.disable({ emitEvent: false });
|
||||
control.clearValidators();
|
||||
}
|
||||
// enable requested forwarder inputs
|
||||
forwarder$.pipe(takeUntil(this.destroyed$)).subscribe((forwarder) => {
|
||||
for (const name in Controls) {
|
||||
const control = this.settings.get(name);
|
||||
if (forwarder.request.includes(name as any)) {
|
||||
control.enable({ emitEvent: false });
|
||||
} else {
|
||||
control.disable({ emitEvent: false });
|
||||
}
|
||||
|
||||
this.settings.updateValueAndValidity({ emitEvent: false });
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// the first emission is the current value; subsequent emissions are updates
|
||||
settings$$
|
||||
@@ -157,13 +138,18 @@ export class ForwarderSettingsComponent implements OnInit, OnChanges, OnDestroy
|
||||
.subscribe(this.onUpdated);
|
||||
|
||||
// now that outputs are set up, connect inputs
|
||||
this.settings.valueChanges
|
||||
.pipe(withLatestFrom(settings$$), takeUntil(this.destroyed$))
|
||||
.subscribe(([value, settings]) => {
|
||||
this.saveSettings
|
||||
.pipe(withLatestFrom(this.settings.valueChanges, settings$$), takeUntil(this.destroyed$))
|
||||
.subscribe(([, value, settings]) => {
|
||||
settings.next(value);
|
||||
});
|
||||
}
|
||||
|
||||
private saveSettings = new Subject<string>();
|
||||
save(site: string = "component api call") {
|
||||
this.saveSettings.next(site);
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
this.refresh$.complete();
|
||||
if ("forwarder" in changes) {
|
||||
@@ -192,6 +178,7 @@ export class ForwarderSettingsComponent implements OnInit, OnChanges, OnDestroy
|
||||
|
||||
private readonly destroyed$ = new Subject<void>();
|
||||
ngOnDestroy(): void {
|
||||
this.destroyed$.next();
|
||||
this.destroyed$.complete();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { safeProvider } from "@bitwarden/angular/platform/utils/safe-provider";
|
||||
import { SafeInjectionToken } from "@bitwarden/angular/services/injection-tokens";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { StateProvider } from "@bitwarden/common/platform/state";
|
||||
@@ -79,6 +80,7 @@ const RANDOMIZER = new SafeInjectionToken<Randomizer>("Randomizer");
|
||||
I18nService,
|
||||
EncryptService,
|
||||
KeyService,
|
||||
AccountService,
|
||||
],
|
||||
}),
|
||||
],
|
||||
|
||||
@@ -7,7 +7,13 @@
|
||||
<bit-card>
|
||||
<bit-form-field disableMargin>
|
||||
<bit-label>{{ "numWords" | i18n }}</bit-label>
|
||||
<input bitInput formControlName="numWords" id="num-words" type="number" />
|
||||
<input
|
||||
bitInput
|
||||
formControlName="numWords"
|
||||
id="num-words"
|
||||
type="number"
|
||||
(change)="save('numWords')"
|
||||
/>
|
||||
<bit-hint>{{ numWordsBoundariesHint$ | async }}</bit-hint>
|
||||
</bit-form-field>
|
||||
</bit-card>
|
||||
@@ -16,14 +22,33 @@
|
||||
<bit-card>
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "wordSeparator" | i18n }}</bit-label>
|
||||
<input bitInput formControlName="wordSeparator" id="word-separator" type="text" />
|
||||
<input
|
||||
bitInput
|
||||
formControlName="wordSeparator"
|
||||
id="word-separator"
|
||||
type="text"
|
||||
[maxlength]="wordSeparatorMaxLength"
|
||||
(change)="save('wordSeparator')"
|
||||
/>
|
||||
</bit-form-field>
|
||||
<bit-form-control>
|
||||
<input bitCheckbox formControlName="capitalize" id="capitalize" type="checkbox" />
|
||||
<input
|
||||
bitCheckbox
|
||||
formControlName="capitalize"
|
||||
id="capitalize"
|
||||
type="checkbox"
|
||||
(change)="save('capitalize')"
|
||||
/>
|
||||
<bit-label>{{ "capitalize" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
<bit-form-control [disableMargin]="!policyInEffect">
|
||||
<input bitCheckbox formControlName="includeNumber" id="include-number" type="checkbox" />
|
||||
<input
|
||||
bitCheckbox
|
||||
formControlName="includeNumber"
|
||||
id="include-number"
|
||||
type="checkbox"
|
||||
(change)="save('includeNumber')"
|
||||
/>
|
||||
<bit-label>{{ "includeNumber" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
<p *ngIf="policyInEffect" bitTypography="helper">{{ "generatorPolicyInEffect" | i18n }}</p>
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
import { coerceBooleanProperty } from "@angular/cdk/coercion";
|
||||
import { OnInit, Input, Output, EventEmitter, Component, OnDestroy } from "@angular/core";
|
||||
import { FormBuilder } from "@angular/forms";
|
||||
import { BehaviorSubject, skip, takeUntil, Subject, ReplaySubject } from "rxjs";
|
||||
import {
|
||||
BehaviorSubject,
|
||||
skip,
|
||||
takeUntil,
|
||||
Subject,
|
||||
map,
|
||||
withLatestFrom,
|
||||
ReplaySubject,
|
||||
} from "rxjs";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
@@ -12,7 +20,7 @@ import {
|
||||
PassphraseGenerationOptions,
|
||||
} from "@bitwarden/generator-core";
|
||||
|
||||
import { completeOnAccountSwitch, toValidators } from "./util";
|
||||
import { completeOnAccountSwitch } from "./util";
|
||||
|
||||
const Controls = Object.freeze({
|
||||
numWords: "numWords",
|
||||
@@ -81,21 +89,12 @@ export class PassphraseSettingsComponent implements OnInit, OnDestroy {
|
||||
// the first emission is the current value; subsequent emissions are updates
|
||||
settings.pipe(skip(1), takeUntil(this.destroyed$)).subscribe(this.onUpdated);
|
||||
|
||||
// dynamic policy enforcement
|
||||
// explain policy & disable policy-overridden fields
|
||||
this.generatorService
|
||||
.policy$(Generators.passphrase, { userId$: singleUserId$ })
|
||||
.pipe(takeUntil(this.destroyed$))
|
||||
.subscribe(({ constraints }) => {
|
||||
this.settings
|
||||
.get(Controls.numWords)
|
||||
.setValidators(toValidators(Controls.numWords, Generators.passphrase, constraints));
|
||||
|
||||
this.settings
|
||||
.get(Controls.wordSeparator)
|
||||
.setValidators(toValidators(Controls.wordSeparator, Generators.passphrase, constraints));
|
||||
|
||||
this.settings.updateValueAndValidity({ emitEvent: false });
|
||||
|
||||
this.wordSeparatorMaxLength = constraints.wordSeparator.maxLength;
|
||||
this.policyInEffect = constraints.policyInEffect;
|
||||
|
||||
this.toggleEnabled(Controls.capitalize, !constraints.capitalize?.readonly);
|
||||
@@ -110,7 +109,21 @@ export class PassphraseSettingsComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
|
||||
// now that outputs are set up, connect inputs
|
||||
this.settings.valueChanges.pipe(takeUntil(this.destroyed$)).subscribe(settings);
|
||||
this.saveSettings
|
||||
.pipe(
|
||||
withLatestFrom(this.settings.valueChanges),
|
||||
map(([, settings]) => settings),
|
||||
takeUntil(this.destroyed$),
|
||||
)
|
||||
.subscribe(settings);
|
||||
}
|
||||
|
||||
/** attribute binding for wordSeparator[maxlength] */
|
||||
protected wordSeparatorMaxLength: number;
|
||||
|
||||
private saveSettings = new Subject<string>();
|
||||
save(site: string = "component api call") {
|
||||
this.saveSettings.next(site);
|
||||
}
|
||||
|
||||
/** display binding for enterprise policy notice */
|
||||
@@ -144,6 +157,7 @@ export class PassphraseSettingsComponent implements OnInit, OnDestroy {
|
||||
|
||||
private readonly destroyed$ = new Subject<void>();
|
||||
ngOnDestroy(): void {
|
||||
this.destroyed$.next();
|
||||
this.destroyed$.complete();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
buttonType="main"
|
||||
(click)="generate('user request')"
|
||||
[appA11yTitle]="credentialTypeGenerateLabel$ | async"
|
||||
[disabled]="!(algorithm$ | async)"
|
||||
>
|
||||
{{ credentialTypeGenerateLabel$ | async }}
|
||||
</button>
|
||||
@@ -31,10 +32,12 @@
|
||||
[appA11yTitle]="credentialTypeCopyLabel$ | async"
|
||||
[appCopyClick]="value$ | async"
|
||||
[valueLabel]="credentialTypeLabel$ | async"
|
||||
[disabled]="!(algorithm$ | async)"
|
||||
></button>
|
||||
</div>
|
||||
</bit-card>
|
||||
<tools-password-settings
|
||||
#passwordSettings
|
||||
class="tw-mt-6"
|
||||
*ngIf="(algorithm$ | async)?.id === 'password'"
|
||||
[userId]="this.userId$ | async"
|
||||
@@ -42,6 +45,7 @@
|
||||
(onUpdated)="generate('password settings')"
|
||||
/>
|
||||
<tools-passphrase-settings
|
||||
#passphraseSettings
|
||||
class="tw-mt-6"
|
||||
*ngIf="(algorithm$ | async)?.id === 'passphrase'"
|
||||
[userId]="this.userId$ | async"
|
||||
|
||||
@@ -22,11 +22,11 @@ import { Option } from "@bitwarden/components/src/select/option";
|
||||
import {
|
||||
CredentialGeneratorService,
|
||||
Generators,
|
||||
PasswordAlgorithm,
|
||||
GeneratedCredential,
|
||||
CredentialAlgorithm,
|
||||
isPasswordAlgorithm,
|
||||
AlgorithmInfo,
|
||||
isSameAlgorithm,
|
||||
} from "@bitwarden/generator-core";
|
||||
import { GeneratorHistoryService } from "@bitwarden/generator-history";
|
||||
|
||||
@@ -57,7 +57,7 @@ export class PasswordGeneratorComponent implements OnInit, OnDestroy {
|
||||
@Input({ transform: coerceBooleanProperty }) disableMargin = false;
|
||||
|
||||
/** tracks the currently selected credential type */
|
||||
protected credentialType$ = new BehaviorSubject<PasswordAlgorithm>(null);
|
||||
protected credentialType$ = new BehaviorSubject<CredentialAlgorithm>(null);
|
||||
|
||||
/** Emits the last generated value. */
|
||||
protected readonly value$ = new BehaviorSubject<string>("");
|
||||
@@ -72,14 +72,14 @@ export class PasswordGeneratorComponent implements OnInit, OnDestroy {
|
||||
* @param requestor a label used to trace generation request
|
||||
* origin in the debugger.
|
||||
*/
|
||||
protected generate(requestor: string) {
|
||||
protected async generate(requestor: string) {
|
||||
this.generate$.next(requestor);
|
||||
}
|
||||
|
||||
/** Tracks changes to the selected credential type
|
||||
* @param type the new credential type
|
||||
*/
|
||||
protected onCredentialTypeChanged(type: PasswordAlgorithm) {
|
||||
protected onCredentialTypeChanged(type: CredentialAlgorithm) {
|
||||
// break subscription cycle
|
||||
if (this.credentialType$.value !== type) {
|
||||
this.zone.run(() => {
|
||||
@@ -169,29 +169,34 @@ export class PasswordGeneratorComponent implements OnInit, OnDestroy {
|
||||
preferences.next(preference);
|
||||
});
|
||||
|
||||
// populate the form with the user's preferences to kick off interactivity
|
||||
preferences.pipe(takeUntil(this.destroyed)).subscribe(({ password }) => {
|
||||
// update navigation
|
||||
this.onCredentialTypeChanged(password.algorithm);
|
||||
|
||||
// load algorithm metadata
|
||||
const algorithm = this.generatorService.algorithm(password.algorithm);
|
||||
|
||||
// update subjects within the angular zone so that the
|
||||
// template bindings refresh immediately
|
||||
this.zone.run(() => {
|
||||
this.algorithm$.next(algorithm);
|
||||
});
|
||||
});
|
||||
|
||||
// generate on load unless the generator prohibits it
|
||||
this.algorithm$
|
||||
// update active algorithm
|
||||
preferences
|
||||
.pipe(
|
||||
distinctUntilChanged((prev, next) => prev.id === next.id),
|
||||
filter((a) => !a.onlyOnRequest),
|
||||
map(({ password }) => this.generatorService.algorithm(password.algorithm)),
|
||||
distinctUntilChanged((prev, next) => isSameAlgorithm(prev?.id, next?.id)),
|
||||
takeUntil(this.destroyed),
|
||||
)
|
||||
.subscribe(() => this.generate("autogenerate"));
|
||||
.subscribe((algorithm) => {
|
||||
// update navigation
|
||||
this.onCredentialTypeChanged(algorithm.id);
|
||||
|
||||
// update subjects within the angular zone so that the
|
||||
// template bindings refresh immediately
|
||||
this.zone.run(() => {
|
||||
this.algorithm$.next(algorithm);
|
||||
});
|
||||
});
|
||||
|
||||
// generate on load unless the generator prohibits it
|
||||
this.algorithm$.pipe(takeUntil(this.destroyed)).subscribe((a) => {
|
||||
this.zone.run(() => {
|
||||
if (!a || a.onlyOnRequest) {
|
||||
this.value$.next("-");
|
||||
} else {
|
||||
this.generate("autogenerate").catch((e: unknown) => this.logService.error(e));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private typeToGenerator$(type: CredentialAlgorithm) {
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<bit-card>
|
||||
<bit-form-field disableMargin>
|
||||
<bit-label>{{ "length" | i18n }}</bit-label>
|
||||
<input bitInput formControlName="length" type="number" />
|
||||
<input bitInput formControlName="length" type="number" (change)="save('length')" />
|
||||
<bit-hint>{{ lengthBoundariesHint$ | async }}</bit-hint>
|
||||
</bit-form-field>
|
||||
</bit-card>
|
||||
@@ -21,7 +21,12 @@
|
||||
attr.aria-description="{{ 'uppercaseDescription' | i18n }}"
|
||||
title="{{ 'uppercaseDescription' | i18n }}"
|
||||
>
|
||||
<input bitCheckbox type="checkbox" formControlName="uppercase" />
|
||||
<input
|
||||
bitCheckbox
|
||||
type="checkbox"
|
||||
formControlName="uppercase"
|
||||
(change)="save('uppercase')"
|
||||
/>
|
||||
<bit-label>{{ "uppercaseLabel" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
<bit-form-control
|
||||
@@ -29,7 +34,12 @@
|
||||
attr.aria-description="{{ 'lowercaseDescription' | i18n }}"
|
||||
title="{{ 'lowercaseDescription' | i18n }}"
|
||||
>
|
||||
<input bitCheckbox type="checkbox" formControlName="lowercase" />
|
||||
<input
|
||||
bitCheckbox
|
||||
type="checkbox"
|
||||
formControlName="lowercase"
|
||||
(change)="save('lowercase')"
|
||||
/>
|
||||
<bit-label>{{ "lowercaseLabel" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
<bit-form-control
|
||||
@@ -37,7 +47,7 @@
|
||||
attr.aria-description="{{ 'numbersDescription' | i18n }}"
|
||||
title="{{ 'numbersDescription' | i18n }}"
|
||||
>
|
||||
<input bitCheckbox type="checkbox" formControlName="number" />
|
||||
<input bitCheckbox type="checkbox" formControlName="number" (change)="save('number')" />
|
||||
<bit-label>{{ "numbersLabel" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
<bit-form-control
|
||||
@@ -45,22 +55,42 @@
|
||||
attr.aria-description="{{ 'specialCharactersDescription' | i18n }}"
|
||||
title="{{ 'specialCharactersDescription' | i18n }}"
|
||||
>
|
||||
<input bitCheckbox type="checkbox" formControlName="special" />
|
||||
<input
|
||||
bitCheckbox
|
||||
type="checkbox"
|
||||
formControlName="special"
|
||||
(change)="save('special')"
|
||||
/>
|
||||
<bit-label>{{ "specialCharactersLabel" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
</div>
|
||||
<div class="tw-flex">
|
||||
<bit-form-field class="tw-w-full tw-basis-1/2 tw-mr-4">
|
||||
<bit-label>{{ "minNumbers" | i18n }}</bit-label>
|
||||
<input bitInput type="number" formControlName="minNumber" />
|
||||
<input
|
||||
bitInput
|
||||
type="number"
|
||||
formControlName="minNumber"
|
||||
(change)="save('minNumbers')"
|
||||
/>
|
||||
</bit-form-field>
|
||||
<bit-form-field class="tw-w-full tw-basis-1/2">
|
||||
<bit-label>{{ "minSpecial" | i18n }}</bit-label>
|
||||
<input bitInput type="number" formControlName="minSpecial" />
|
||||
<input
|
||||
bitInput
|
||||
type="number"
|
||||
formControlName="minSpecial"
|
||||
(change)="save('minSpecial')"
|
||||
/>
|
||||
</bit-form-field>
|
||||
</div>
|
||||
<bit-form-control [disableMargin]="!policyInEffect">
|
||||
<input bitCheckbox type="checkbox" formControlName="avoidAmbiguous" />
|
||||
<input
|
||||
bitCheckbox
|
||||
type="checkbox"
|
||||
formControlName="avoidAmbiguous"
|
||||
(change)="save('avoidAmbiguous')"
|
||||
/>
|
||||
<bit-label>{{ "avoidAmbiguous" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
<p *ngIf="policyInEffect" bitTypography="helper">{{ "generatorPolicyInEffect" | i18n }}</p>
|
||||
|
||||
@@ -1,7 +1,17 @@
|
||||
import { coerceBooleanProperty } from "@angular/cdk/coercion";
|
||||
import { OnInit, Input, Output, EventEmitter, Component, OnDestroy } from "@angular/core";
|
||||
import { FormBuilder } from "@angular/forms";
|
||||
import { BehaviorSubject, takeUntil, Subject, map, filter, tap, skip, ReplaySubject } from "rxjs";
|
||||
import {
|
||||
BehaviorSubject,
|
||||
takeUntil,
|
||||
Subject,
|
||||
map,
|
||||
filter,
|
||||
tap,
|
||||
skip,
|
||||
ReplaySubject,
|
||||
withLatestFrom,
|
||||
} from "rxjs";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
@@ -12,7 +22,7 @@ import {
|
||||
PasswordGenerationOptions,
|
||||
} from "@bitwarden/generator-core";
|
||||
|
||||
import { completeOnAccountSwitch, toValidators } from "./util";
|
||||
import { completeOnAccountSwitch } from "./util";
|
||||
|
||||
const Controls = Object.freeze({
|
||||
length: "length",
|
||||
@@ -118,23 +128,11 @@ export class PasswordSettingsComponent implements OnInit, OnDestroy {
|
||||
this.settings.patchValue(s, { emitEvent: false });
|
||||
});
|
||||
|
||||
// bind policy to the template
|
||||
// explain policy & disable policy-overridden fields
|
||||
this.generatorService
|
||||
.policy$(Generators.password, { userId$: singleUserId$ })
|
||||
.pipe(takeUntil(this.destroyed$))
|
||||
.subscribe(({ constraints }) => {
|
||||
this.settings
|
||||
.get(Controls.length)
|
||||
.setValidators(toValidators(Controls.length, Generators.password, constraints));
|
||||
|
||||
this.minNumber.setValidators(
|
||||
toValidators(Controls.minNumber, Generators.password, constraints),
|
||||
);
|
||||
|
||||
this.minSpecial.setValidators(
|
||||
toValidators(Controls.minSpecial, Generators.password, constraints),
|
||||
);
|
||||
|
||||
this.policyInEffect = constraints.policyInEffect;
|
||||
|
||||
const toggles = [
|
||||
@@ -153,8 +151,8 @@ export class PasswordSettingsComponent implements OnInit, OnDestroy {
|
||||
|
||||
const boundariesHint = this.i18nService.t(
|
||||
"generatorBoundariesHint",
|
||||
constraints.length.min,
|
||||
constraints.length.max,
|
||||
constraints.length.min?.toString(),
|
||||
constraints.length.max?.toString(),
|
||||
);
|
||||
this.lengthBoundariesHint.next(boundariesHint);
|
||||
});
|
||||
@@ -201,9 +199,10 @@ export class PasswordSettingsComponent implements OnInit, OnDestroy {
|
||||
settings.pipe(skip(1), takeUntil(this.destroyed$)).subscribe(this.onUpdated);
|
||||
|
||||
// now that outputs are set up, connect inputs
|
||||
this.settings.valueChanges
|
||||
this.saveSettings
|
||||
.pipe(
|
||||
map((settings) => {
|
||||
withLatestFrom(this.settings.valueChanges),
|
||||
map(([, settings]) => {
|
||||
// interface is "avoid" while storage is "include"
|
||||
const s: any = { ...settings };
|
||||
s.ambiguous = s.avoidAmbiguous;
|
||||
@@ -215,6 +214,11 @@ export class PasswordSettingsComponent implements OnInit, OnDestroy {
|
||||
.subscribe(settings);
|
||||
}
|
||||
|
||||
private saveSettings = new Subject<string>();
|
||||
save(site: string = "component api call") {
|
||||
this.saveSettings.next(site);
|
||||
}
|
||||
|
||||
/** display binding for enterprise policy notice */
|
||||
protected policyInEffect: boolean;
|
||||
|
||||
@@ -246,6 +250,7 @@ export class PasswordSettingsComponent implements OnInit, OnDestroy {
|
||||
|
||||
private readonly destroyed$ = new Subject<void>();
|
||||
ngOnDestroy(): void {
|
||||
this.destroyed$.next();
|
||||
this.destroyed$.complete();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
<form class="box" [formGroup]="settings" class="tw-container">
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "email" | i18n }}</bit-label>
|
||||
<input bitInput formControlName="subaddressEmail" type="text" />
|
||||
<input
|
||||
bitInput
|
||||
formControlName="subaddressEmail"
|
||||
type="text"
|
||||
(change)="save('subaddressEmail')"
|
||||
/>
|
||||
</bit-form-field>
|
||||
</form>
|
||||
|
||||
@@ -53,28 +53,25 @@ export class SubaddressSettingsComponent implements OnInit, OnDestroy {
|
||||
const singleUserId$ = this.singleUserId$();
|
||||
const settings = await this.generatorService.settings(Generators.subaddress, { singleUserId$ });
|
||||
|
||||
settings
|
||||
.pipe(
|
||||
withLatestFrom(this.accountService.activeAccount$),
|
||||
map(([settings, activeAccount]) => {
|
||||
// if the subaddress isn't specified, copy it from
|
||||
// the user's settings
|
||||
if ((settings.subaddressEmail ?? "").length < 1) {
|
||||
settings.subaddressEmail = activeAccount.email;
|
||||
}
|
||||
|
||||
return settings;
|
||||
}),
|
||||
takeUntil(this.destroyed$),
|
||||
)
|
||||
.subscribe((s) => {
|
||||
this.settings.patchValue(s, { emitEvent: false });
|
||||
});
|
||||
settings.pipe(takeUntil(this.destroyed$)).subscribe((s) => {
|
||||
this.settings.patchValue(s, { emitEvent: false });
|
||||
});
|
||||
|
||||
// the first emission is the current value; subsequent emissions are updates
|
||||
settings.pipe(skip(1), takeUntil(this.destroyed$)).subscribe(this.onUpdated);
|
||||
|
||||
this.settings.valueChanges.pipe(takeUntil(this.destroyed$)).subscribe(settings);
|
||||
this.saveSettings
|
||||
.pipe(
|
||||
withLatestFrom(this.settings.valueChanges),
|
||||
map(([, settings]) => settings),
|
||||
takeUntil(this.destroyed$),
|
||||
)
|
||||
.subscribe(settings);
|
||||
}
|
||||
|
||||
private saveSettings = new Subject<string>();
|
||||
save(site: string = "component api call") {
|
||||
this.saveSettings.next(site);
|
||||
}
|
||||
|
||||
private singleUserId$() {
|
||||
@@ -92,6 +89,7 @@ export class SubaddressSettingsComponent implements OnInit, OnDestroy {
|
||||
|
||||
private readonly destroyed$ = new Subject<void>();
|
||||
ngOnDestroy(): void {
|
||||
this.destroyed$.next();
|
||||
this.destroyed$.complete();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
buttonType="main"
|
||||
(click)="generate('user request')"
|
||||
[appA11yTitle]="credentialTypeGenerateLabel$ | async"
|
||||
[disabled]="!(algorithm$ | async)"
|
||||
>
|
||||
{{ credentialTypeGenerateLabel$ | async }}
|
||||
</button>
|
||||
@@ -20,6 +21,7 @@
|
||||
[appA11yTitle]="credentialTypeCopyLabel$ | async"
|
||||
[appCopyClick]="value$ | async"
|
||||
[valueLabel]="credentialTypeLabel$ | async"
|
||||
[disabled]="!(algorithm$ | async)"
|
||||
>
|
||||
{{ credentialTypeCopyLabel$ | async }}
|
||||
</button>
|
||||
@@ -57,21 +59,25 @@
|
||||
</bit-form-field>
|
||||
</form>
|
||||
<tools-catchall-settings
|
||||
#catchallSettings
|
||||
*ngIf="(algorithm$ | async)?.id === 'catchall'"
|
||||
[userId]="this.userId$ | async"
|
||||
(onUpdated)="generate('catchall settings')"
|
||||
/>
|
||||
<tools-forwarder-settings
|
||||
#forwarderSettings
|
||||
*ngIf="!!(forwarderId$ | async)"
|
||||
[forwarder]="forwarderId$ | async"
|
||||
[userId]="this.userId$ | async"
|
||||
/>
|
||||
<tools-subaddress-settings
|
||||
#subaddressSettings
|
||||
*ngIf="(algorithm$ | async)?.id === 'subaddress'"
|
||||
[userId]="this.userId$ | async"
|
||||
(onUpdated)="generate('subaddress settings')"
|
||||
/>
|
||||
<tools-username-settings
|
||||
#usernameSettings
|
||||
*ngIf="(algorithm$ | async)?.id === 'username'"
|
||||
[userId]="this.userId$ | async"
|
||||
(onUpdated)="generate('username settings')"
|
||||
|
||||
@@ -322,7 +322,7 @@ export class UsernameGeneratorComponent implements OnInit, OnDestroy {
|
||||
if (!a || a.onlyOnRequest) {
|
||||
this.value$.next("-");
|
||||
} else {
|
||||
this.generate("autogenerate");
|
||||
this.generate("autogenerate").catch((e: unknown) => this.logService.error(e));
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -414,7 +414,7 @@ export class UsernameGeneratorComponent implements OnInit, OnDestroy {
|
||||
* @param requestor a label used to trace generation request
|
||||
* origin in the debugger.
|
||||
*/
|
||||
protected generate(requestor: string) {
|
||||
protected async generate(requestor: string) {
|
||||
this.generate$.next(requestor);
|
||||
}
|
||||
|
||||
@@ -429,6 +429,7 @@ export class UsernameGeneratorComponent implements OnInit, OnDestroy {
|
||||
|
||||
private readonly destroyed = new Subject<void>();
|
||||
ngOnDestroy() {
|
||||
this.destroyed.next();
|
||||
this.destroyed.complete();
|
||||
|
||||
// finalize subjects
|
||||
|
||||
@@ -1,10 +1,20 @@
|
||||
<form class="box" [formGroup]="settings" class="tw-container">
|
||||
<bit-form-control>
|
||||
<input bitCheckbox formControlName="wordCapitalize" type="checkbox" />
|
||||
<input
|
||||
bitCheckbox
|
||||
formControlName="wordCapitalize"
|
||||
type="checkbox"
|
||||
(change)="save('wordCapitalize')"
|
||||
/>
|
||||
<bit-label>{{ "capitalize" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
<bit-form-control>
|
||||
<input bitCheckbox formControlName="wordIncludeNumber" type="checkbox" />
|
||||
<input
|
||||
bitCheckbox
|
||||
formControlName="wordIncludeNumber"
|
||||
type="checkbox"
|
||||
(change)="save('wordIncludeNumber')"
|
||||
/>
|
||||
<bit-label>{{ "includeNumber" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
</form>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core";
|
||||
import { FormBuilder } from "@angular/forms";
|
||||
import { BehaviorSubject, skip, Subject, takeUntil } from "rxjs";
|
||||
import { BehaviorSubject, map, skip, Subject, takeUntil, withLatestFrom } from "rxjs";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
@@ -61,7 +61,18 @@ export class UsernameSettingsComponent implements OnInit, OnDestroy {
|
||||
// the first emission is the current value; subsequent emissions are updates
|
||||
settings.pipe(skip(1), takeUntil(this.destroyed$)).subscribe(this.onUpdated);
|
||||
|
||||
this.settings.valueChanges.pipe(takeUntil(this.destroyed$)).subscribe(settings);
|
||||
this.saveSettings
|
||||
.pipe(
|
||||
withLatestFrom(this.settings.valueChanges),
|
||||
map(([, settings]) => settings),
|
||||
takeUntil(this.destroyed$),
|
||||
)
|
||||
.subscribe(settings);
|
||||
}
|
||||
|
||||
private saveSettings = new Subject<string>();
|
||||
save(site: string = "component api call") {
|
||||
this.saveSettings.next(site);
|
||||
}
|
||||
|
||||
private singleUserId$() {
|
||||
@@ -79,6 +90,7 @@ export class UsernameSettingsComponent implements OnInit, OnDestroy {
|
||||
|
||||
private readonly destroyed$ = new Subject<void>();
|
||||
ngOnDestroy(): void {
|
||||
this.destroyed$.next();
|
||||
this.destroyed$.complete();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ export function toValidators<Policy, Settings>(
|
||||
}
|
||||
|
||||
const max = getConstraint("max", config, runtime);
|
||||
if (max === undefined) {
|
||||
if (max !== undefined) {
|
||||
validators.push(Validators.max(max));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user