mirror of
https://github.com/bitwarden/browser.git
synced 2024-12-21 16:18:28 +01: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:
parent
a9595b4d14
commit
414bdde232
@ -22,6 +22,7 @@ export type ObjectKey<State, Secret = State, Disclosed = Record<string, never>>
|
|||||||
classifier: Classifier<State, Disclosed, Secret>;
|
classifier: Classifier<State, Disclosed, Secret>;
|
||||||
format: "plain" | "classified";
|
format: "plain" | "classified";
|
||||||
options: UserKeyDefinitionOptions<State>;
|
options: UserKeyDefinitionOptions<State>;
|
||||||
|
initial?: State;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function isObjectKey(key: any): key is ObjectKey<unknown> {
|
export function isObjectKey(key: any): key is ObjectKey<unknown> {
|
||||||
|
@ -254,17 +254,18 @@ export class UserStateSubject<
|
|||||||
withConstraints,
|
withConstraints,
|
||||||
map(([loadedState, constraints]) => {
|
map(([loadedState, constraints]) => {
|
||||||
// bypass nulls
|
// bypass nulls
|
||||||
if (!loadedState) {
|
if (!loadedState && !this.objectKey?.initial) {
|
||||||
return {
|
return {
|
||||||
constraints: {} as Constraints<State>,
|
constraints: {} as Constraints<State>,
|
||||||
state: null,
|
state: null,
|
||||||
} satisfies Constrained<State>;
|
} satisfies Constrained<State>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const unconstrained = loadedState ?? structuredClone(this.objectKey.initial);
|
||||||
const calibration = isDynamic(constraints)
|
const calibration = isDynamic(constraints)
|
||||||
? constraints.calibrate(loadedState)
|
? constraints.calibrate(unconstrained)
|
||||||
: constraints;
|
: constraints;
|
||||||
const adjusted = calibration.adjust(loadedState);
|
const adjusted = calibration.adjust(unconstrained);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
constraints: calibration.constraints,
|
constraints: calibration.constraints,
|
||||||
|
@ -1,6 +1,11 @@
|
|||||||
<form class="box" [formGroup]="settings" class="tw-container">
|
<form class="box" [formGroup]="settings" class="tw-container">
|
||||||
<bit-form-field>
|
<bit-form-field>
|
||||||
<bit-label>{{ "domainName" | i18n }}</bit-label>
|
<bit-label>{{ "domainName" | i18n }}</bit-label>
|
||||||
<input bitInput formControlName="catchallDomain" type="text" />
|
<input
|
||||||
|
bitInput
|
||||||
|
formControlName="catchallDomain"
|
||||||
|
type="text"
|
||||||
|
(change)="save('catchallDomain')"
|
||||||
|
/>
|
||||||
</bit-form-field>
|
</bit-form-field>
|
||||||
</form>
|
</form>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core";
|
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core";
|
||||||
import { FormBuilder } from "@angular/forms";
|
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 { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
import { UserId } from "@bitwarden/common/types/guid";
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
@ -12,6 +12,11 @@ import {
|
|||||||
|
|
||||||
import { completeOnAccountSwitch } from "./util";
|
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 */
|
/** Options group for catchall emails */
|
||||||
@Component({
|
@Component({
|
||||||
selector: "tools-catchall-settings",
|
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
|
// the first emission is the current value; subsequent emissions are updates
|
||||||
settings.pipe(skip(1), takeUntil(this.destroyed$)).subscribe(this.onUpdated);
|
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$() {
|
private singleUserId$() {
|
||||||
@ -78,6 +95,7 @@ export class CatchallSettingsComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
private readonly destroyed$ = new Subject<void>();
|
private readonly destroyed$ = new Subject<void>();
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
|
this.destroyed$.next();
|
||||||
this.destroyed$.complete();
|
this.destroyed$.complete();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -22,6 +22,7 @@
|
|||||||
buttonType="main"
|
buttonType="main"
|
||||||
(click)="generate('user request')"
|
(click)="generate('user request')"
|
||||||
[appA11yTitle]="credentialTypeGenerateLabel$ | async"
|
[appA11yTitle]="credentialTypeGenerateLabel$ | async"
|
||||||
|
[disabled]="!(algorithm$ | async)"
|
||||||
>
|
>
|
||||||
{{ credentialTypeGenerateLabel$ | async }}
|
{{ credentialTypeGenerateLabel$ | async }}
|
||||||
</button>
|
</button>
|
||||||
@ -33,16 +34,19 @@
|
|||||||
[appA11yTitle]="credentialTypeCopyLabel$ | async"
|
[appA11yTitle]="credentialTypeCopyLabel$ | async"
|
||||||
[appCopyClick]="value$ | async"
|
[appCopyClick]="value$ | async"
|
||||||
[valueLabel]="credentialTypeLabel$ | async"
|
[valueLabel]="credentialTypeLabel$ | async"
|
||||||
|
[disabled]="!(algorithm$ | async)"
|
||||||
></button>
|
></button>
|
||||||
</div>
|
</div>
|
||||||
</bit-card>
|
</bit-card>
|
||||||
<tools-password-settings
|
<tools-password-settings
|
||||||
|
#passwordSettings
|
||||||
class="tw-mt-6"
|
class="tw-mt-6"
|
||||||
*ngIf="(showAlgorithm$ | async)?.id === 'password'"
|
*ngIf="(showAlgorithm$ | async)?.id === 'password'"
|
||||||
[userId]="userId$ | async"
|
[userId]="userId$ | async"
|
||||||
(onUpdated)="generate('password settings')"
|
(onUpdated)="generate('password settings')"
|
||||||
/>
|
/>
|
||||||
<tools-passphrase-settings
|
<tools-passphrase-settings
|
||||||
|
#passphraseSettings
|
||||||
class="tw-mt-6"
|
class="tw-mt-6"
|
||||||
*ngIf="(showAlgorithm$ | async)?.id === 'passphrase'"
|
*ngIf="(showAlgorithm$ | async)?.id === 'passphrase'"
|
||||||
[userId]="userId$ | async"
|
[userId]="userId$ | async"
|
||||||
@ -80,21 +84,25 @@
|
|||||||
</bit-form-field>
|
</bit-form-field>
|
||||||
</form>
|
</form>
|
||||||
<tools-catchall-settings
|
<tools-catchall-settings
|
||||||
|
#catchallSettings
|
||||||
*ngIf="(showAlgorithm$ | async)?.id === 'catchall'"
|
*ngIf="(showAlgorithm$ | async)?.id === 'catchall'"
|
||||||
[userId]="userId$ | async"
|
[userId]="userId$ | async"
|
||||||
(onUpdated)="generate('catchall settings')"
|
(onUpdated)="generate('catchall settings')"
|
||||||
/>
|
/>
|
||||||
<tools-forwarder-settings
|
<tools-forwarder-settings
|
||||||
|
#forwarderSettings
|
||||||
*ngIf="!!(forwarderId$ | async)"
|
*ngIf="!!(forwarderId$ | async)"
|
||||||
[forwarder]="forwarderId$ | async"
|
[forwarder]="forwarderId$ | async"
|
||||||
[userId]="this.userId$ | async"
|
[userId]="this.userId$ | async"
|
||||||
/>
|
/>
|
||||||
<tools-subaddress-settings
|
<tools-subaddress-settings
|
||||||
|
#subaddressSettings
|
||||||
*ngIf="(showAlgorithm$ | async)?.id === 'subaddress'"
|
*ngIf="(showAlgorithm$ | async)?.id === 'subaddress'"
|
||||||
[userId]="userId$ | async"
|
[userId]="userId$ | async"
|
||||||
(onUpdated)="generate('subaddress settings')"
|
(onUpdated)="generate('subaddress settings')"
|
||||||
/>
|
/>
|
||||||
<tools-username-settings
|
<tools-username-settings
|
||||||
|
#usernameSettings
|
||||||
*ngIf="(showAlgorithm$ | async)?.id === 'username'"
|
*ngIf="(showAlgorithm$ | async)?.id === 'username'"
|
||||||
[userId]="userId$ | async"
|
[userId]="userId$ | async"
|
||||||
(onUpdated)="generate('username settings')"
|
(onUpdated)="generate('username settings')"
|
||||||
|
@ -202,9 +202,8 @@ export class CredentialGeneratorComponent implements OnInit, OnDestroy {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// normalize cascade selections; introduce subjects to allow changes
|
// these subjects normalize cascade selections to ensure the current
|
||||||
// from user selections and changes from preference updates to
|
// cascade is always well-known.
|
||||||
// update the template
|
|
||||||
type CascadeValue = { nav: string; algorithm?: CredentialAlgorithm };
|
type CascadeValue = { nav: string; algorithm?: CredentialAlgorithm };
|
||||||
const activeRoot$ = new Subject<CascadeValue>();
|
const activeRoot$ = new Subject<CascadeValue>();
|
||||||
const activeIdentifier$ = new Subject<CascadeValue>();
|
const activeIdentifier$ = new Subject<CascadeValue>();
|
||||||
@ -385,7 +384,7 @@ export class CredentialGeneratorComponent implements OnInit, OnDestroy {
|
|||||||
if (!a || a.onlyOnRequest) {
|
if (!a || a.onlyOnRequest) {
|
||||||
this.value$.next("-");
|
this.value$.next("-");
|
||||||
} else {
|
} 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
|
* @param requestor a label used to trace generation request
|
||||||
* origin in the debugger.
|
* origin in the debugger.
|
||||||
*/
|
*/
|
||||||
protected generate(requestor: string) {
|
protected async generate(requestor: string) {
|
||||||
this.generate$.next(requestor);
|
this.generate$.next(requestor);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -510,6 +509,7 @@ export class CredentialGeneratorComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
private readonly destroyed = new Subject<void>();
|
private readonly destroyed = new Subject<void>();
|
||||||
ngOnDestroy() {
|
ngOnDestroy() {
|
||||||
|
this.destroyed.next();
|
||||||
this.destroyed.complete();
|
this.destroyed.complete();
|
||||||
|
|
||||||
// finalize subjects
|
// finalize subjects
|
||||||
|
@ -1,16 +1,28 @@
|
|||||||
<form class="box" [formGroup]="settings" class="tw-container">
|
<form class="box" [formGroup]="settings" class="tw-container">
|
||||||
<bit-form-field *ngIf="displayDomain">
|
<bit-form-field *ngIf="displayDomain">
|
||||||
<bit-label>{{ "forwarderDomainName" | i18n }}</bit-label>
|
<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-hint>{{ "forwarderDomainNameHint" | i18n }}</bit-hint>
|
||||||
</bit-form-field>
|
</bit-form-field>
|
||||||
<bit-form-field *ngIf="displayToken">
|
<bit-form-field *ngIf="displayToken">
|
||||||
<bit-label>{{ "apiKey" | i18n }}</bit-label>
|
<bit-label>{{ "apiKey" | i18n }}</bit-label>
|
||||||
<input bitInput formControlName="token" type="password" />
|
<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>
|
||||||
<bit-form-field *ngIf="displayBaseUrl" disableMargin>
|
<bit-form-field *ngIf="displayBaseUrl" disableMargin>
|
||||||
<bit-label>{{ "selfHostBaseUrl" | i18n }}</bit-label>
|
<bit-label>{{ "selfHostBaseUrl" | i18n }}</bit-label>
|
||||||
<input bitInput formControlName="baseUrl" type="text" />
|
<input bitInput formControlName="baseUrl" type="text" (change)="save('baseUrl')" />
|
||||||
</bit-form-field>
|
</bit-form-field>
|
||||||
</form>
|
</form>
|
||||||
|
@ -17,7 +17,6 @@ import {
|
|||||||
skip,
|
skip,
|
||||||
Subject,
|
Subject,
|
||||||
switchAll,
|
switchAll,
|
||||||
switchMap,
|
|
||||||
takeUntil,
|
takeUntil,
|
||||||
withLatestFrom,
|
withLatestFrom,
|
||||||
} from "rxjs";
|
} from "rxjs";
|
||||||
@ -33,7 +32,7 @@ import {
|
|||||||
toCredentialGeneratorConfiguration,
|
toCredentialGeneratorConfiguration,
|
||||||
} from "@bitwarden/generator-core";
|
} from "@bitwarden/generator-core";
|
||||||
|
|
||||||
import { completeOnAccountSwitch, toValidators } from "./util";
|
import { completeOnAccountSwitch } from "./util";
|
||||||
|
|
||||||
const Controls = Object.freeze({
|
const Controls = Object.freeze({
|
||||||
domain: "domain",
|
domain: "domain",
|
||||||
@ -117,35 +116,17 @@ export class ForwarderSettingsComponent implements OnInit, OnChanges, OnDestroy
|
|||||||
this.settings.patchValue(settings as any, { emitEvent: false });
|
this.settings.patchValue(settings as any, { emitEvent: false });
|
||||||
});
|
});
|
||||||
|
|
||||||
// bind policy to the reactive form
|
// enable requested forwarder inputs
|
||||||
forwarder$
|
forwarder$.pipe(takeUntil(this.destroyed$)).subscribe((forwarder) => {
|
||||||
.pipe(
|
for (const name in Controls) {
|
||||||
switchMap((forwarder) => {
|
const control = this.settings.get(name);
|
||||||
const constraints$ = this.generatorService
|
if (forwarder.request.includes(name as any)) {
|
||||||
.policy$(forwarder, { userId$: singleUserId$ })
|
control.enable({ emitEvent: false });
|
||||||
.pipe(map(({ constraints }) => [constraints, forwarder] as const));
|
} else {
|
||||||
|
control.disable({ emitEvent: false });
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
this.settings.updateValueAndValidity({ emitEvent: false });
|
});
|
||||||
});
|
|
||||||
|
|
||||||
// the first emission is the current value; subsequent emissions are updates
|
// the first emission is the current value; subsequent emissions are updates
|
||||||
settings$$
|
settings$$
|
||||||
@ -157,13 +138,18 @@ export class ForwarderSettingsComponent implements OnInit, OnChanges, OnDestroy
|
|||||||
.subscribe(this.onUpdated);
|
.subscribe(this.onUpdated);
|
||||||
|
|
||||||
// now that outputs are set up, connect inputs
|
// now that outputs are set up, connect inputs
|
||||||
this.settings.valueChanges
|
this.saveSettings
|
||||||
.pipe(withLatestFrom(settings$$), takeUntil(this.destroyed$))
|
.pipe(withLatestFrom(this.settings.valueChanges, settings$$), takeUntil(this.destroyed$))
|
||||||
.subscribe(([value, settings]) => {
|
.subscribe(([, value, settings]) => {
|
||||||
settings.next(value);
|
settings.next(value);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private saveSettings = new Subject<string>();
|
||||||
|
save(site: string = "component api call") {
|
||||||
|
this.saveSettings.next(site);
|
||||||
|
}
|
||||||
|
|
||||||
ngOnChanges(changes: SimpleChanges): void {
|
ngOnChanges(changes: SimpleChanges): void {
|
||||||
this.refresh$.complete();
|
this.refresh$.complete();
|
||||||
if ("forwarder" in changes) {
|
if ("forwarder" in changes) {
|
||||||
@ -192,6 +178,7 @@ export class ForwarderSettingsComponent implements OnInit, OnChanges, OnDestroy
|
|||||||
|
|
||||||
private readonly destroyed$ = new Subject<void>();
|
private readonly destroyed$ = new Subject<void>();
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
|
this.destroyed$.next();
|
||||||
this.destroyed$.complete();
|
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 { SafeInjectionToken } from "@bitwarden/angular/services/injection-tokens";
|
||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
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 { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { StateProvider } from "@bitwarden/common/platform/state";
|
import { StateProvider } from "@bitwarden/common/platform/state";
|
||||||
@ -79,6 +80,7 @@ const RANDOMIZER = new SafeInjectionToken<Randomizer>("Randomizer");
|
|||||||
I18nService,
|
I18nService,
|
||||||
EncryptService,
|
EncryptService,
|
||||||
KeyService,
|
KeyService,
|
||||||
|
AccountService,
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
@ -7,7 +7,13 @@
|
|||||||
<bit-card>
|
<bit-card>
|
||||||
<bit-form-field disableMargin>
|
<bit-form-field disableMargin>
|
||||||
<bit-label>{{ "numWords" | i18n }}</bit-label>
|
<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-hint>{{ numWordsBoundariesHint$ | async }}</bit-hint>
|
||||||
</bit-form-field>
|
</bit-form-field>
|
||||||
</bit-card>
|
</bit-card>
|
||||||
@ -16,14 +22,33 @@
|
|||||||
<bit-card>
|
<bit-card>
|
||||||
<bit-form-field>
|
<bit-form-field>
|
||||||
<bit-label>{{ "wordSeparator" | i18n }}</bit-label>
|
<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-field>
|
||||||
<bit-form-control>
|
<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-label>{{ "capitalize" | i18n }}</bit-label>
|
||||||
</bit-form-control>
|
</bit-form-control>
|
||||||
<bit-form-control [disableMargin]="!policyInEffect">
|
<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-label>{{ "includeNumber" | i18n }}</bit-label>
|
||||||
</bit-form-control>
|
</bit-form-control>
|
||||||
<p *ngIf="policyInEffect" bitTypography="helper">{{ "generatorPolicyInEffect" | i18n }}</p>
|
<p *ngIf="policyInEffect" bitTypography="helper">{{ "generatorPolicyInEffect" | i18n }}</p>
|
||||||
|
@ -1,7 +1,15 @@
|
|||||||
import { coerceBooleanProperty } from "@angular/cdk/coercion";
|
import { coerceBooleanProperty } from "@angular/cdk/coercion";
|
||||||
import { OnInit, Input, Output, EventEmitter, Component, OnDestroy } from "@angular/core";
|
import { OnInit, Input, Output, EventEmitter, Component, OnDestroy } from "@angular/core";
|
||||||
import { FormBuilder } from "@angular/forms";
|
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 { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
@ -12,7 +20,7 @@ import {
|
|||||||
PassphraseGenerationOptions,
|
PassphraseGenerationOptions,
|
||||||
} from "@bitwarden/generator-core";
|
} from "@bitwarden/generator-core";
|
||||||
|
|
||||||
import { completeOnAccountSwitch, toValidators } from "./util";
|
import { completeOnAccountSwitch } from "./util";
|
||||||
|
|
||||||
const Controls = Object.freeze({
|
const Controls = Object.freeze({
|
||||||
numWords: "numWords",
|
numWords: "numWords",
|
||||||
@ -81,21 +89,12 @@ export class PassphraseSettingsComponent implements OnInit, OnDestroy {
|
|||||||
// the first emission is the current value; subsequent emissions are updates
|
// the first emission is the current value; subsequent emissions are updates
|
||||||
settings.pipe(skip(1), takeUntil(this.destroyed$)).subscribe(this.onUpdated);
|
settings.pipe(skip(1), takeUntil(this.destroyed$)).subscribe(this.onUpdated);
|
||||||
|
|
||||||
// dynamic policy enforcement
|
// explain policy & disable policy-overridden fields
|
||||||
this.generatorService
|
this.generatorService
|
||||||
.policy$(Generators.passphrase, { userId$: singleUserId$ })
|
.policy$(Generators.passphrase, { userId$: singleUserId$ })
|
||||||
.pipe(takeUntil(this.destroyed$))
|
.pipe(takeUntil(this.destroyed$))
|
||||||
.subscribe(({ constraints }) => {
|
.subscribe(({ constraints }) => {
|
||||||
this.settings
|
this.wordSeparatorMaxLength = constraints.wordSeparator.maxLength;
|
||||||
.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.policyInEffect = constraints.policyInEffect;
|
this.policyInEffect = constraints.policyInEffect;
|
||||||
|
|
||||||
this.toggleEnabled(Controls.capitalize, !constraints.capitalize?.readonly);
|
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
|
// 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 */
|
/** display binding for enterprise policy notice */
|
||||||
@ -144,6 +157,7 @@ export class PassphraseSettingsComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
private readonly destroyed$ = new Subject<void>();
|
private readonly destroyed$ = new Subject<void>();
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
|
this.destroyed$.next();
|
||||||
this.destroyed$.complete();
|
this.destroyed$.complete();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,7 @@
|
|||||||
buttonType="main"
|
buttonType="main"
|
||||||
(click)="generate('user request')"
|
(click)="generate('user request')"
|
||||||
[appA11yTitle]="credentialTypeGenerateLabel$ | async"
|
[appA11yTitle]="credentialTypeGenerateLabel$ | async"
|
||||||
|
[disabled]="!(algorithm$ | async)"
|
||||||
>
|
>
|
||||||
{{ credentialTypeGenerateLabel$ | async }}
|
{{ credentialTypeGenerateLabel$ | async }}
|
||||||
</button>
|
</button>
|
||||||
@ -31,10 +32,12 @@
|
|||||||
[appA11yTitle]="credentialTypeCopyLabel$ | async"
|
[appA11yTitle]="credentialTypeCopyLabel$ | async"
|
||||||
[appCopyClick]="value$ | async"
|
[appCopyClick]="value$ | async"
|
||||||
[valueLabel]="credentialTypeLabel$ | async"
|
[valueLabel]="credentialTypeLabel$ | async"
|
||||||
|
[disabled]="!(algorithm$ | async)"
|
||||||
></button>
|
></button>
|
||||||
</div>
|
</div>
|
||||||
</bit-card>
|
</bit-card>
|
||||||
<tools-password-settings
|
<tools-password-settings
|
||||||
|
#passwordSettings
|
||||||
class="tw-mt-6"
|
class="tw-mt-6"
|
||||||
*ngIf="(algorithm$ | async)?.id === 'password'"
|
*ngIf="(algorithm$ | async)?.id === 'password'"
|
||||||
[userId]="this.userId$ | async"
|
[userId]="this.userId$ | async"
|
||||||
@ -42,6 +45,7 @@
|
|||||||
(onUpdated)="generate('password settings')"
|
(onUpdated)="generate('password settings')"
|
||||||
/>
|
/>
|
||||||
<tools-passphrase-settings
|
<tools-passphrase-settings
|
||||||
|
#passphraseSettings
|
||||||
class="tw-mt-6"
|
class="tw-mt-6"
|
||||||
*ngIf="(algorithm$ | async)?.id === 'passphrase'"
|
*ngIf="(algorithm$ | async)?.id === 'passphrase'"
|
||||||
[userId]="this.userId$ | async"
|
[userId]="this.userId$ | async"
|
||||||
|
@ -22,11 +22,11 @@ import { Option } from "@bitwarden/components/src/select/option";
|
|||||||
import {
|
import {
|
||||||
CredentialGeneratorService,
|
CredentialGeneratorService,
|
||||||
Generators,
|
Generators,
|
||||||
PasswordAlgorithm,
|
|
||||||
GeneratedCredential,
|
GeneratedCredential,
|
||||||
CredentialAlgorithm,
|
CredentialAlgorithm,
|
||||||
isPasswordAlgorithm,
|
isPasswordAlgorithm,
|
||||||
AlgorithmInfo,
|
AlgorithmInfo,
|
||||||
|
isSameAlgorithm,
|
||||||
} from "@bitwarden/generator-core";
|
} from "@bitwarden/generator-core";
|
||||||
import { GeneratorHistoryService } from "@bitwarden/generator-history";
|
import { GeneratorHistoryService } from "@bitwarden/generator-history";
|
||||||
|
|
||||||
@ -57,7 +57,7 @@ export class PasswordGeneratorComponent implements OnInit, OnDestroy {
|
|||||||
@Input({ transform: coerceBooleanProperty }) disableMargin = false;
|
@Input({ transform: coerceBooleanProperty }) disableMargin = false;
|
||||||
|
|
||||||
/** tracks the currently selected credential type */
|
/** tracks the currently selected credential type */
|
||||||
protected credentialType$ = new BehaviorSubject<PasswordAlgorithm>(null);
|
protected credentialType$ = new BehaviorSubject<CredentialAlgorithm>(null);
|
||||||
|
|
||||||
/** Emits the last generated value. */
|
/** Emits the last generated value. */
|
||||||
protected readonly value$ = new BehaviorSubject<string>("");
|
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
|
* @param requestor a label used to trace generation request
|
||||||
* origin in the debugger.
|
* origin in the debugger.
|
||||||
*/
|
*/
|
||||||
protected generate(requestor: string) {
|
protected async generate(requestor: string) {
|
||||||
this.generate$.next(requestor);
|
this.generate$.next(requestor);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Tracks changes to the selected credential type
|
/** Tracks changes to the selected credential type
|
||||||
* @param type the new credential type
|
* @param type the new credential type
|
||||||
*/
|
*/
|
||||||
protected onCredentialTypeChanged(type: PasswordAlgorithm) {
|
protected onCredentialTypeChanged(type: CredentialAlgorithm) {
|
||||||
// break subscription cycle
|
// break subscription cycle
|
||||||
if (this.credentialType$.value !== type) {
|
if (this.credentialType$.value !== type) {
|
||||||
this.zone.run(() => {
|
this.zone.run(() => {
|
||||||
@ -169,29 +169,34 @@ export class PasswordGeneratorComponent implements OnInit, OnDestroy {
|
|||||||
preferences.next(preference);
|
preferences.next(preference);
|
||||||
});
|
});
|
||||||
|
|
||||||
// populate the form with the user's preferences to kick off interactivity
|
// update active algorithm
|
||||||
preferences.pipe(takeUntil(this.destroyed)).subscribe(({ password }) => {
|
preferences
|
||||||
// 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$
|
|
||||||
.pipe(
|
.pipe(
|
||||||
distinctUntilChanged((prev, next) => prev.id === next.id),
|
map(({ password }) => this.generatorService.algorithm(password.algorithm)),
|
||||||
filter((a) => !a.onlyOnRequest),
|
distinctUntilChanged((prev, next) => isSameAlgorithm(prev?.id, next?.id)),
|
||||||
takeUntil(this.destroyed),
|
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) {
|
private typeToGenerator$(type: CredentialAlgorithm) {
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
<bit-card>
|
<bit-card>
|
||||||
<bit-form-field disableMargin>
|
<bit-form-field disableMargin>
|
||||||
<bit-label>{{ "length" | i18n }}</bit-label>
|
<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-hint>{{ lengthBoundariesHint$ | async }}</bit-hint>
|
||||||
</bit-form-field>
|
</bit-form-field>
|
||||||
</bit-card>
|
</bit-card>
|
||||||
@ -21,7 +21,12 @@
|
|||||||
attr.aria-description="{{ 'uppercaseDescription' | i18n }}"
|
attr.aria-description="{{ 'uppercaseDescription' | i18n }}"
|
||||||
title="{{ '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-label>{{ "uppercaseLabel" | i18n }}</bit-label>
|
||||||
</bit-form-control>
|
</bit-form-control>
|
||||||
<bit-form-control
|
<bit-form-control
|
||||||
@ -29,7 +34,12 @@
|
|||||||
attr.aria-description="{{ 'lowercaseDescription' | i18n }}"
|
attr.aria-description="{{ 'lowercaseDescription' | i18n }}"
|
||||||
title="{{ '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-label>{{ "lowercaseLabel" | i18n }}</bit-label>
|
||||||
</bit-form-control>
|
</bit-form-control>
|
||||||
<bit-form-control
|
<bit-form-control
|
||||||
@ -37,7 +47,7 @@
|
|||||||
attr.aria-description="{{ 'numbersDescription' | i18n }}"
|
attr.aria-description="{{ 'numbersDescription' | i18n }}"
|
||||||
title="{{ '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-label>{{ "numbersLabel" | i18n }}</bit-label>
|
||||||
</bit-form-control>
|
</bit-form-control>
|
||||||
<bit-form-control
|
<bit-form-control
|
||||||
@ -45,22 +55,42 @@
|
|||||||
attr.aria-description="{{ 'specialCharactersDescription' | i18n }}"
|
attr.aria-description="{{ 'specialCharactersDescription' | i18n }}"
|
||||||
title="{{ '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-label>{{ "specialCharactersLabel" | i18n }}</bit-label>
|
||||||
</bit-form-control>
|
</bit-form-control>
|
||||||
</div>
|
</div>
|
||||||
<div class="tw-flex">
|
<div class="tw-flex">
|
||||||
<bit-form-field class="tw-w-full tw-basis-1/2 tw-mr-4">
|
<bit-form-field class="tw-w-full tw-basis-1/2 tw-mr-4">
|
||||||
<bit-label>{{ "minNumbers" | i18n }}</bit-label>
|
<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>
|
||||||
<bit-form-field class="tw-w-full tw-basis-1/2">
|
<bit-form-field class="tw-w-full tw-basis-1/2">
|
||||||
<bit-label>{{ "minSpecial" | i18n }}</bit-label>
|
<bit-label>{{ "minSpecial" | i18n }}</bit-label>
|
||||||
<input bitInput type="number" formControlName="minSpecial" />
|
<input
|
||||||
|
bitInput
|
||||||
|
type="number"
|
||||||
|
formControlName="minSpecial"
|
||||||
|
(change)="save('minSpecial')"
|
||||||
|
/>
|
||||||
</bit-form-field>
|
</bit-form-field>
|
||||||
</div>
|
</div>
|
||||||
<bit-form-control [disableMargin]="!policyInEffect">
|
<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-label>{{ "avoidAmbiguous" | i18n }}</bit-label>
|
||||||
</bit-form-control>
|
</bit-form-control>
|
||||||
<p *ngIf="policyInEffect" bitTypography="helper">{{ "generatorPolicyInEffect" | i18n }}</p>
|
<p *ngIf="policyInEffect" bitTypography="helper">{{ "generatorPolicyInEffect" | i18n }}</p>
|
||||||
|
@ -1,7 +1,17 @@
|
|||||||
import { coerceBooleanProperty } from "@angular/cdk/coercion";
|
import { coerceBooleanProperty } from "@angular/cdk/coercion";
|
||||||
import { OnInit, Input, Output, EventEmitter, Component, OnDestroy } from "@angular/core";
|
import { OnInit, Input, Output, EventEmitter, Component, OnDestroy } from "@angular/core";
|
||||||
import { FormBuilder } from "@angular/forms";
|
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 { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
@ -12,7 +22,7 @@ import {
|
|||||||
PasswordGenerationOptions,
|
PasswordGenerationOptions,
|
||||||
} from "@bitwarden/generator-core";
|
} from "@bitwarden/generator-core";
|
||||||
|
|
||||||
import { completeOnAccountSwitch, toValidators } from "./util";
|
import { completeOnAccountSwitch } from "./util";
|
||||||
|
|
||||||
const Controls = Object.freeze({
|
const Controls = Object.freeze({
|
||||||
length: "length",
|
length: "length",
|
||||||
@ -118,23 +128,11 @@ export class PasswordSettingsComponent implements OnInit, OnDestroy {
|
|||||||
this.settings.patchValue(s, { emitEvent: false });
|
this.settings.patchValue(s, { emitEvent: false });
|
||||||
});
|
});
|
||||||
|
|
||||||
// bind policy to the template
|
// explain policy & disable policy-overridden fields
|
||||||
this.generatorService
|
this.generatorService
|
||||||
.policy$(Generators.password, { userId$: singleUserId$ })
|
.policy$(Generators.password, { userId$: singleUserId$ })
|
||||||
.pipe(takeUntil(this.destroyed$))
|
.pipe(takeUntil(this.destroyed$))
|
||||||
.subscribe(({ constraints }) => {
|
.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;
|
this.policyInEffect = constraints.policyInEffect;
|
||||||
|
|
||||||
const toggles = [
|
const toggles = [
|
||||||
@ -153,8 +151,8 @@ export class PasswordSettingsComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
const boundariesHint = this.i18nService.t(
|
const boundariesHint = this.i18nService.t(
|
||||||
"generatorBoundariesHint",
|
"generatorBoundariesHint",
|
||||||
constraints.length.min,
|
constraints.length.min?.toString(),
|
||||||
constraints.length.max,
|
constraints.length.max?.toString(),
|
||||||
);
|
);
|
||||||
this.lengthBoundariesHint.next(boundariesHint);
|
this.lengthBoundariesHint.next(boundariesHint);
|
||||||
});
|
});
|
||||||
@ -201,9 +199,10 @@ export class PasswordSettingsComponent implements OnInit, OnDestroy {
|
|||||||
settings.pipe(skip(1), takeUntil(this.destroyed$)).subscribe(this.onUpdated);
|
settings.pipe(skip(1), takeUntil(this.destroyed$)).subscribe(this.onUpdated);
|
||||||
|
|
||||||
// now that outputs are set up, connect inputs
|
// now that outputs are set up, connect inputs
|
||||||
this.settings.valueChanges
|
this.saveSettings
|
||||||
.pipe(
|
.pipe(
|
||||||
map((settings) => {
|
withLatestFrom(this.settings.valueChanges),
|
||||||
|
map(([, settings]) => {
|
||||||
// interface is "avoid" while storage is "include"
|
// interface is "avoid" while storage is "include"
|
||||||
const s: any = { ...settings };
|
const s: any = { ...settings };
|
||||||
s.ambiguous = s.avoidAmbiguous;
|
s.ambiguous = s.avoidAmbiguous;
|
||||||
@ -215,6 +214,11 @@ export class PasswordSettingsComponent implements OnInit, OnDestroy {
|
|||||||
.subscribe(settings);
|
.subscribe(settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private saveSettings = new Subject<string>();
|
||||||
|
save(site: string = "component api call") {
|
||||||
|
this.saveSettings.next(site);
|
||||||
|
}
|
||||||
|
|
||||||
/** display binding for enterprise policy notice */
|
/** display binding for enterprise policy notice */
|
||||||
protected policyInEffect: boolean;
|
protected policyInEffect: boolean;
|
||||||
|
|
||||||
@ -246,6 +250,7 @@ export class PasswordSettingsComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
private readonly destroyed$ = new Subject<void>();
|
private readonly destroyed$ = new Subject<void>();
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
|
this.destroyed$.next();
|
||||||
this.destroyed$.complete();
|
this.destroyed$.complete();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,11 @@
|
|||||||
<form class="box" [formGroup]="settings" class="tw-container">
|
<form class="box" [formGroup]="settings" class="tw-container">
|
||||||
<bit-form-field>
|
<bit-form-field>
|
||||||
<bit-label>{{ "email" | i18n }}</bit-label>
|
<bit-label>{{ "email" | i18n }}</bit-label>
|
||||||
<input bitInput formControlName="subaddressEmail" type="text" />
|
<input
|
||||||
|
bitInput
|
||||||
|
formControlName="subaddressEmail"
|
||||||
|
type="text"
|
||||||
|
(change)="save('subaddressEmail')"
|
||||||
|
/>
|
||||||
</bit-form-field>
|
</bit-form-field>
|
||||||
</form>
|
</form>
|
||||||
|
@ -53,28 +53,25 @@ export class SubaddressSettingsComponent implements OnInit, OnDestroy {
|
|||||||
const singleUserId$ = this.singleUserId$();
|
const singleUserId$ = this.singleUserId$();
|
||||||
const settings = await this.generatorService.settings(Generators.subaddress, { singleUserId$ });
|
const settings = await this.generatorService.settings(Generators.subaddress, { singleUserId$ });
|
||||||
|
|
||||||
settings
|
settings.pipe(takeUntil(this.destroyed$)).subscribe((s) => {
|
||||||
.pipe(
|
this.settings.patchValue(s, { emitEvent: false });
|
||||||
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 });
|
|
||||||
});
|
|
||||||
|
|
||||||
// the first emission is the current value; subsequent emissions are updates
|
// the first emission is the current value; subsequent emissions are updates
|
||||||
settings.pipe(skip(1), takeUntil(this.destroyed$)).subscribe(this.onUpdated);
|
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$() {
|
private singleUserId$() {
|
||||||
@ -92,6 +89,7 @@ export class SubaddressSettingsComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
private readonly destroyed$ = new Subject<void>();
|
private readonly destroyed$ = new Subject<void>();
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
|
this.destroyed$.next();
|
||||||
this.destroyed$.complete();
|
this.destroyed$.complete();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,7 @@
|
|||||||
buttonType="main"
|
buttonType="main"
|
||||||
(click)="generate('user request')"
|
(click)="generate('user request')"
|
||||||
[appA11yTitle]="credentialTypeGenerateLabel$ | async"
|
[appA11yTitle]="credentialTypeGenerateLabel$ | async"
|
||||||
|
[disabled]="!(algorithm$ | async)"
|
||||||
>
|
>
|
||||||
{{ credentialTypeGenerateLabel$ | async }}
|
{{ credentialTypeGenerateLabel$ | async }}
|
||||||
</button>
|
</button>
|
||||||
@ -20,6 +21,7 @@
|
|||||||
[appA11yTitle]="credentialTypeCopyLabel$ | async"
|
[appA11yTitle]="credentialTypeCopyLabel$ | async"
|
||||||
[appCopyClick]="value$ | async"
|
[appCopyClick]="value$ | async"
|
||||||
[valueLabel]="credentialTypeLabel$ | async"
|
[valueLabel]="credentialTypeLabel$ | async"
|
||||||
|
[disabled]="!(algorithm$ | async)"
|
||||||
>
|
>
|
||||||
{{ credentialTypeCopyLabel$ | async }}
|
{{ credentialTypeCopyLabel$ | async }}
|
||||||
</button>
|
</button>
|
||||||
@ -57,21 +59,25 @@
|
|||||||
</bit-form-field>
|
</bit-form-field>
|
||||||
</form>
|
</form>
|
||||||
<tools-catchall-settings
|
<tools-catchall-settings
|
||||||
|
#catchallSettings
|
||||||
*ngIf="(algorithm$ | async)?.id === 'catchall'"
|
*ngIf="(algorithm$ | async)?.id === 'catchall'"
|
||||||
[userId]="this.userId$ | async"
|
[userId]="this.userId$ | async"
|
||||||
(onUpdated)="generate('catchall settings')"
|
(onUpdated)="generate('catchall settings')"
|
||||||
/>
|
/>
|
||||||
<tools-forwarder-settings
|
<tools-forwarder-settings
|
||||||
|
#forwarderSettings
|
||||||
*ngIf="!!(forwarderId$ | async)"
|
*ngIf="!!(forwarderId$ | async)"
|
||||||
[forwarder]="forwarderId$ | async"
|
[forwarder]="forwarderId$ | async"
|
||||||
[userId]="this.userId$ | async"
|
[userId]="this.userId$ | async"
|
||||||
/>
|
/>
|
||||||
<tools-subaddress-settings
|
<tools-subaddress-settings
|
||||||
|
#subaddressSettings
|
||||||
*ngIf="(algorithm$ | async)?.id === 'subaddress'"
|
*ngIf="(algorithm$ | async)?.id === 'subaddress'"
|
||||||
[userId]="this.userId$ | async"
|
[userId]="this.userId$ | async"
|
||||||
(onUpdated)="generate('subaddress settings')"
|
(onUpdated)="generate('subaddress settings')"
|
||||||
/>
|
/>
|
||||||
<tools-username-settings
|
<tools-username-settings
|
||||||
|
#usernameSettings
|
||||||
*ngIf="(algorithm$ | async)?.id === 'username'"
|
*ngIf="(algorithm$ | async)?.id === 'username'"
|
||||||
[userId]="this.userId$ | async"
|
[userId]="this.userId$ | async"
|
||||||
(onUpdated)="generate('username settings')"
|
(onUpdated)="generate('username settings')"
|
||||||
|
@ -322,7 +322,7 @@ export class UsernameGeneratorComponent implements OnInit, OnDestroy {
|
|||||||
if (!a || a.onlyOnRequest) {
|
if (!a || a.onlyOnRequest) {
|
||||||
this.value$.next("-");
|
this.value$.next("-");
|
||||||
} else {
|
} 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
|
* @param requestor a label used to trace generation request
|
||||||
* origin in the debugger.
|
* origin in the debugger.
|
||||||
*/
|
*/
|
||||||
protected generate(requestor: string) {
|
protected async generate(requestor: string) {
|
||||||
this.generate$.next(requestor);
|
this.generate$.next(requestor);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -429,6 +429,7 @@ export class UsernameGeneratorComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
private readonly destroyed = new Subject<void>();
|
private readonly destroyed = new Subject<void>();
|
||||||
ngOnDestroy() {
|
ngOnDestroy() {
|
||||||
|
this.destroyed.next();
|
||||||
this.destroyed.complete();
|
this.destroyed.complete();
|
||||||
|
|
||||||
// finalize subjects
|
// finalize subjects
|
||||||
|
@ -1,10 +1,20 @@
|
|||||||
<form class="box" [formGroup]="settings" class="tw-container">
|
<form class="box" [formGroup]="settings" class="tw-container">
|
||||||
<bit-form-control>
|
<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-label>{{ "capitalize" | i18n }}</bit-label>
|
||||||
</bit-form-control>
|
</bit-form-control>
|
||||||
<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-label>{{ "includeNumber" | i18n }}</bit-label>
|
||||||
</bit-form-control>
|
</bit-form-control>
|
||||||
</form>
|
</form>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core";
|
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core";
|
||||||
import { FormBuilder } from "@angular/forms";
|
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 { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
import { UserId } from "@bitwarden/common/types/guid";
|
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
|
// the first emission is the current value; subsequent emissions are updates
|
||||||
settings.pipe(skip(1), takeUntil(this.destroyed$)).subscribe(this.onUpdated);
|
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$() {
|
private singleUserId$() {
|
||||||
@ -79,6 +90,7 @@ export class UsernameSettingsComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
private readonly destroyed$ = new Subject<void>();
|
private readonly destroyed$ = new Subject<void>();
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
|
this.destroyed$.next();
|
||||||
this.destroyed$.complete();
|
this.destroyed$.complete();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -49,7 +49,7 @@ export function toValidators<Policy, Settings>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const max = getConstraint("max", config, runtime);
|
const max = getConstraint("max", config, runtime);
|
||||||
if (max === undefined) {
|
if (max !== undefined) {
|
||||||
validators.push(Validators.max(max));
|
validators.push(Validators.max(max));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||||
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
||||||
|
import { GENERATOR_DISK } from "@bitwarden/common/platform/state";
|
||||||
import { ApiSettings } from "@bitwarden/common/tools/integration/rpc";
|
import { ApiSettings } from "@bitwarden/common/tools/integration/rpc";
|
||||||
|
import { PublicClassifier } from "@bitwarden/common/tools/public-classifier";
|
||||||
import { IdentityConstraint } from "@bitwarden/common/tools/state/identity-state-constraint";
|
import { IdentityConstraint } from "@bitwarden/common/tools/state/identity-state-constraint";
|
||||||
|
import { ObjectKey } from "@bitwarden/common/tools/state/object-key";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
EmailRandomizer,
|
EmailRandomizer,
|
||||||
@ -19,12 +22,12 @@ import {
|
|||||||
PasswordGeneratorOptionsEvaluator,
|
PasswordGeneratorOptionsEvaluator,
|
||||||
passwordLeastPrivilege,
|
passwordLeastPrivilege,
|
||||||
} from "../policies";
|
} from "../policies";
|
||||||
|
import { CatchallConstraints } from "../policies/catchall-constraints";
|
||||||
|
import { SubaddressConstraints } from "../policies/subaddress-constraints";
|
||||||
import {
|
import {
|
||||||
CATCHALL_SETTINGS,
|
|
||||||
EFF_USERNAME_SETTINGS,
|
EFF_USERNAME_SETTINGS,
|
||||||
PASSPHRASE_SETTINGS,
|
PASSPHRASE_SETTINGS,
|
||||||
PASSWORD_SETTINGS,
|
PASSWORD_SETTINGS,
|
||||||
SUBADDRESS_SETTINGS,
|
|
||||||
} from "../strategies/storage";
|
} from "../strategies/storage";
|
||||||
import {
|
import {
|
||||||
CatchallGenerationOptions,
|
CatchallGenerationOptions,
|
||||||
@ -178,79 +181,115 @@ const USERNAME = Object.freeze({
|
|||||||
},
|
},
|
||||||
} satisfies CredentialGeneratorConfiguration<EffUsernameGenerationOptions, NoPolicy>);
|
} satisfies CredentialGeneratorConfiguration<EffUsernameGenerationOptions, NoPolicy>);
|
||||||
|
|
||||||
const CATCHALL = Object.freeze({
|
const CATCHALL: CredentialGeneratorConfiguration<CatchallGenerationOptions, NoPolicy> =
|
||||||
id: "catchall",
|
Object.freeze({
|
||||||
category: "email",
|
id: "catchall",
|
||||||
nameKey: "catchallEmail",
|
category: "email",
|
||||||
descriptionKey: "catchallEmailDesc",
|
nameKey: "catchallEmail",
|
||||||
generateKey: "generateEmail",
|
descriptionKey: "catchallEmailDesc",
|
||||||
generatedValueKey: "email",
|
generateKey: "generateEmail",
|
||||||
copyKey: "copyEmail",
|
generatedValueKey: "email",
|
||||||
onlyOnRequest: false,
|
copyKey: "copyEmail",
|
||||||
request: [],
|
onlyOnRequest: false,
|
||||||
engine: {
|
request: [],
|
||||||
create(
|
engine: {
|
||||||
dependencies: GeneratorDependencyProvider,
|
create(
|
||||||
): CredentialGenerator<CatchallGenerationOptions> {
|
dependencies: GeneratorDependencyProvider,
|
||||||
return new EmailRandomizer(dependencies.randomizer);
|
): CredentialGenerator<CatchallGenerationOptions> {
|
||||||
|
return new EmailRandomizer(dependencies.randomizer);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
settings: {
|
||||||
settings: {
|
initial: DefaultCatchallOptions,
|
||||||
initial: DefaultCatchallOptions,
|
constraints: { catchallDomain: { minLength: 1 } },
|
||||||
constraints: { catchallDomain: { minLength: 1 } },
|
account: {
|
||||||
account: CATCHALL_SETTINGS,
|
key: "catchallGeneratorSettings",
|
||||||
},
|
target: "object",
|
||||||
policy: {
|
format: "plain",
|
||||||
type: PolicyType.PasswordGenerator,
|
classifier: new PublicClassifier<CatchallGenerationOptions>([
|
||||||
disabledValue: {},
|
"catchallType",
|
||||||
combine(_acc: NoPolicy, _policy: Policy) {
|
"catchallDomain",
|
||||||
return {};
|
]),
|
||||||
|
state: GENERATOR_DISK,
|
||||||
|
initial: {
|
||||||
|
catchallType: "random",
|
||||||
|
catchallDomain: "",
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
deserializer: (value) => value,
|
||||||
|
clearOn: ["logout"],
|
||||||
|
},
|
||||||
|
} satisfies ObjectKey<CatchallGenerationOptions>,
|
||||||
},
|
},
|
||||||
createEvaluator(_policy: NoPolicy) {
|
policy: {
|
||||||
return new DefaultPolicyEvaluator<CatchallGenerationOptions>();
|
type: PolicyType.PasswordGenerator,
|
||||||
|
disabledValue: {},
|
||||||
|
combine(_acc: NoPolicy, _policy: Policy) {
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
createEvaluator(_policy: NoPolicy) {
|
||||||
|
return new DefaultPolicyEvaluator<CatchallGenerationOptions>();
|
||||||
|
},
|
||||||
|
toConstraints(_policy: NoPolicy, email: string) {
|
||||||
|
return new CatchallConstraints(email);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
toConstraints(_policy: NoPolicy) {
|
});
|
||||||
return new IdentityConstraint<CatchallGenerationOptions>();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} satisfies CredentialGeneratorConfiguration<CatchallGenerationOptions, NoPolicy>);
|
|
||||||
|
|
||||||
const SUBADDRESS = Object.freeze({
|
const SUBADDRESS: CredentialGeneratorConfiguration<SubaddressGenerationOptions, NoPolicy> =
|
||||||
id: "subaddress",
|
Object.freeze({
|
||||||
category: "email",
|
id: "subaddress",
|
||||||
nameKey: "plusAddressedEmail",
|
category: "email",
|
||||||
descriptionKey: "plusAddressedEmailDesc",
|
nameKey: "plusAddressedEmail",
|
||||||
generateKey: "generateEmail",
|
descriptionKey: "plusAddressedEmailDesc",
|
||||||
generatedValueKey: "email",
|
generateKey: "generateEmail",
|
||||||
copyKey: "copyEmail",
|
generatedValueKey: "email",
|
||||||
onlyOnRequest: false,
|
copyKey: "copyEmail",
|
||||||
request: [],
|
onlyOnRequest: false,
|
||||||
engine: {
|
request: [],
|
||||||
create(
|
engine: {
|
||||||
dependencies: GeneratorDependencyProvider,
|
create(
|
||||||
): CredentialGenerator<SubaddressGenerationOptions> {
|
dependencies: GeneratorDependencyProvider,
|
||||||
return new EmailRandomizer(dependencies.randomizer);
|
): CredentialGenerator<SubaddressGenerationOptions> {
|
||||||
|
return new EmailRandomizer(dependencies.randomizer);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
settings: {
|
||||||
settings: {
|
initial: DefaultSubaddressOptions,
|
||||||
initial: DefaultSubaddressOptions,
|
constraints: {},
|
||||||
constraints: {},
|
account: {
|
||||||
account: SUBADDRESS_SETTINGS,
|
key: "subaddressGeneratorSettings",
|
||||||
},
|
target: "object",
|
||||||
policy: {
|
format: "plain",
|
||||||
type: PolicyType.PasswordGenerator,
|
classifier: new PublicClassifier<SubaddressGenerationOptions>([
|
||||||
disabledValue: {},
|
"subaddressType",
|
||||||
combine(_acc: NoPolicy, _policy: Policy) {
|
"subaddressEmail",
|
||||||
return {};
|
]),
|
||||||
|
state: GENERATOR_DISK,
|
||||||
|
initial: {
|
||||||
|
subaddressType: "random",
|
||||||
|
subaddressEmail: "",
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
deserializer: (value) => value,
|
||||||
|
clearOn: ["logout"],
|
||||||
|
},
|
||||||
|
} satisfies ObjectKey<SubaddressGenerationOptions>,
|
||||||
},
|
},
|
||||||
createEvaluator(_policy: NoPolicy) {
|
policy: {
|
||||||
return new DefaultPolicyEvaluator<SubaddressGenerationOptions>();
|
type: PolicyType.PasswordGenerator,
|
||||||
|
disabledValue: {},
|
||||||
|
combine(_acc: NoPolicy, _policy: Policy) {
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
createEvaluator(_policy: NoPolicy) {
|
||||||
|
return new DefaultPolicyEvaluator<SubaddressGenerationOptions>();
|
||||||
|
},
|
||||||
|
toConstraints(_policy: NoPolicy, email: string) {
|
||||||
|
return new SubaddressConstraints(email);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
toConstraints(_policy: NoPolicy) {
|
});
|
||||||
return new IdentityConstraint<SubaddressGenerationOptions>();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} satisfies CredentialGeneratorConfiguration<SubaddressGenerationOptions, NoPolicy>);
|
|
||||||
|
|
||||||
export function toCredentialGeneratorConfiguration<Settings extends ApiSettings = ApiSettings>(
|
export function toCredentialGeneratorConfiguration<Settings extends ApiSettings = ApiSettings>(
|
||||||
configuration: ForwarderConfiguration<Settings>,
|
configuration: ForwarderConfiguration<Settings>,
|
||||||
|
@ -0,0 +1,45 @@
|
|||||||
|
import { Constraints, StateConstraints } from "@bitwarden/common/tools/types";
|
||||||
|
|
||||||
|
import { CatchallGenerationOptions } from "../types";
|
||||||
|
|
||||||
|
/** Parses the domain part of an email address
|
||||||
|
*/
|
||||||
|
const DOMAIN_PARSER = new RegExp("[^@]+@(?<domain>.+)");
|
||||||
|
|
||||||
|
/** A constraint that sets the catchall domain using a fixed email address */
|
||||||
|
export class CatchallConstraints implements StateConstraints<CatchallGenerationOptions> {
|
||||||
|
/** Creates a catchall constraints
|
||||||
|
* @param email - the email address containing the domain.
|
||||||
|
*/
|
||||||
|
constructor(email: string) {
|
||||||
|
if (!email) {
|
||||||
|
this.domain = "";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = DOMAIN_PARSER.exec(email);
|
||||||
|
if (parsed && parsed.groups?.domain) {
|
||||||
|
this.domain = parsed.groups.domain;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private domain: string;
|
||||||
|
|
||||||
|
constraints: Readonly<Constraints<CatchallGenerationOptions>> = {};
|
||||||
|
|
||||||
|
adjust(state: CatchallGenerationOptions) {
|
||||||
|
const currentDomain = (state.catchallDomain ?? "").trim();
|
||||||
|
|
||||||
|
if (currentDomain !== "") {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
const options = { ...state };
|
||||||
|
options.catchallDomain = this.domain;
|
||||||
|
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
fix(state: CatchallGenerationOptions) {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,34 @@
|
|||||||
|
import { Constraints, StateConstraints } from "@bitwarden/common/tools/types";
|
||||||
|
|
||||||
|
import { SubaddressGenerationOptions } from "../types";
|
||||||
|
|
||||||
|
/** A constraint that sets the subaddress email using a fixed email address */
|
||||||
|
export class SubaddressConstraints implements StateConstraints<SubaddressGenerationOptions> {
|
||||||
|
/** Creates a catchall constraints
|
||||||
|
* @param email - the email address containing the domain.
|
||||||
|
*/
|
||||||
|
constructor(readonly email: string) {
|
||||||
|
if (!email) {
|
||||||
|
this.email = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
constraints: Readonly<Constraints<SubaddressGenerationOptions>> = {};
|
||||||
|
|
||||||
|
adjust(state: SubaddressGenerationOptions) {
|
||||||
|
const currentDomain = (state.subaddressEmail ?? "").trim();
|
||||||
|
|
||||||
|
if (currentDomain !== "") {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
const options = { ...state };
|
||||||
|
options.subaddressEmail = this.email;
|
||||||
|
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
fix(state: SubaddressGenerationOptions) {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
@ -23,11 +23,12 @@ export function mapPolicyToEvaluator<Policy, Evaluator>(
|
|||||||
*/
|
*/
|
||||||
export function mapPolicyToConstraints<Policy, Evaluator>(
|
export function mapPolicyToConstraints<Policy, Evaluator>(
|
||||||
configuration: PolicyConfiguration<Policy, Evaluator>,
|
configuration: PolicyConfiguration<Policy, Evaluator>,
|
||||||
|
email: string,
|
||||||
) {
|
) {
|
||||||
return pipe(
|
return pipe(
|
||||||
reduceCollection(configuration.combine, configuration.disabledValue),
|
reduceCollection(configuration.combine, configuration.disabledValue),
|
||||||
distinctIfShallowMatch(),
|
distinctIfShallowMatch(),
|
||||||
map(configuration.toConstraints),
|
map((policy) => configuration.toConstraints(policy, email)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -202,6 +202,7 @@ describe("CredentialGeneratorService", () => {
|
|||||||
i18nService,
|
i18nService,
|
||||||
encryptService,
|
encryptService,
|
||||||
keyService,
|
keyService,
|
||||||
|
accountService,
|
||||||
);
|
);
|
||||||
const generated = new ObservableTracker(generator.generate$(SomeConfiguration));
|
const generated = new ObservableTracker(generator.generate$(SomeConfiguration));
|
||||||
|
|
||||||
@ -223,6 +224,7 @@ describe("CredentialGeneratorService", () => {
|
|||||||
i18nService,
|
i18nService,
|
||||||
encryptService,
|
encryptService,
|
||||||
keyService,
|
keyService,
|
||||||
|
accountService,
|
||||||
);
|
);
|
||||||
const generated = new ObservableTracker(generator.generate$(SomeConfiguration));
|
const generated = new ObservableTracker(generator.generate$(SomeConfiguration));
|
||||||
|
|
||||||
@ -248,6 +250,7 @@ describe("CredentialGeneratorService", () => {
|
|||||||
i18nService,
|
i18nService,
|
||||||
encryptService,
|
encryptService,
|
||||||
keyService,
|
keyService,
|
||||||
|
accountService,
|
||||||
);
|
);
|
||||||
const generated = new ObservableTracker(generator.generate$(SomeConfiguration));
|
const generated = new ObservableTracker(generator.generate$(SomeConfiguration));
|
||||||
|
|
||||||
@ -276,6 +279,7 @@ describe("CredentialGeneratorService", () => {
|
|||||||
i18nService,
|
i18nService,
|
||||||
encryptService,
|
encryptService,
|
||||||
keyService,
|
keyService,
|
||||||
|
accountService,
|
||||||
);
|
);
|
||||||
const website$ = new BehaviorSubject("some website");
|
const website$ = new BehaviorSubject("some website");
|
||||||
const generated = new ObservableTracker(generator.generate$(SomeConfiguration, { website$ }));
|
const generated = new ObservableTracker(generator.generate$(SomeConfiguration, { website$ }));
|
||||||
@ -297,6 +301,7 @@ describe("CredentialGeneratorService", () => {
|
|||||||
i18nService,
|
i18nService,
|
||||||
encryptService,
|
encryptService,
|
||||||
keyService,
|
keyService,
|
||||||
|
accountService,
|
||||||
);
|
);
|
||||||
const website$ = new BehaviorSubject("some website");
|
const website$ = new BehaviorSubject("some website");
|
||||||
let error = null;
|
let error = null;
|
||||||
@ -322,6 +327,7 @@ describe("CredentialGeneratorService", () => {
|
|||||||
i18nService,
|
i18nService,
|
||||||
encryptService,
|
encryptService,
|
||||||
keyService,
|
keyService,
|
||||||
|
accountService,
|
||||||
);
|
);
|
||||||
const website$ = new BehaviorSubject("some website");
|
const website$ = new BehaviorSubject("some website");
|
||||||
let completed = false;
|
let completed = false;
|
||||||
@ -348,6 +354,7 @@ describe("CredentialGeneratorService", () => {
|
|||||||
i18nService,
|
i18nService,
|
||||||
encryptService,
|
encryptService,
|
||||||
keyService,
|
keyService,
|
||||||
|
accountService,
|
||||||
);
|
);
|
||||||
const userId$ = new BehaviorSubject(AnotherUser).asObservable();
|
const userId$ = new BehaviorSubject(AnotherUser).asObservable();
|
||||||
const generated = new ObservableTracker(generator.generate$(SomeConfiguration, { userId$ }));
|
const generated = new ObservableTracker(generator.generate$(SomeConfiguration, { userId$ }));
|
||||||
@ -368,6 +375,7 @@ describe("CredentialGeneratorService", () => {
|
|||||||
i18nService,
|
i18nService,
|
||||||
encryptService,
|
encryptService,
|
||||||
keyService,
|
keyService,
|
||||||
|
accountService,
|
||||||
);
|
);
|
||||||
const userId = new BehaviorSubject(SomeUser);
|
const userId = new BehaviorSubject(SomeUser);
|
||||||
const userId$ = userId.pipe(filter((u) => !!u));
|
const userId$ = userId.pipe(filter((u) => !!u));
|
||||||
@ -392,6 +400,7 @@ describe("CredentialGeneratorService", () => {
|
|||||||
i18nService,
|
i18nService,
|
||||||
encryptService,
|
encryptService,
|
||||||
keyService,
|
keyService,
|
||||||
|
accountService,
|
||||||
);
|
);
|
||||||
const userId$ = new BehaviorSubject(SomeUser);
|
const userId$ = new BehaviorSubject(SomeUser);
|
||||||
let error = null;
|
let error = null;
|
||||||
@ -417,6 +426,7 @@ describe("CredentialGeneratorService", () => {
|
|||||||
i18nService,
|
i18nService,
|
||||||
encryptService,
|
encryptService,
|
||||||
keyService,
|
keyService,
|
||||||
|
accountService,
|
||||||
);
|
);
|
||||||
const userId$ = new BehaviorSubject(SomeUser);
|
const userId$ = new BehaviorSubject(SomeUser);
|
||||||
let completed = false;
|
let completed = false;
|
||||||
@ -443,6 +453,7 @@ describe("CredentialGeneratorService", () => {
|
|||||||
i18nService,
|
i18nService,
|
||||||
encryptService,
|
encryptService,
|
||||||
keyService,
|
keyService,
|
||||||
|
accountService,
|
||||||
);
|
);
|
||||||
const on$ = new Subject<void>();
|
const on$ = new Subject<void>();
|
||||||
const results: any[] = [];
|
const results: any[] = [];
|
||||||
@ -485,6 +496,7 @@ describe("CredentialGeneratorService", () => {
|
|||||||
i18nService,
|
i18nService,
|
||||||
encryptService,
|
encryptService,
|
||||||
keyService,
|
keyService,
|
||||||
|
accountService,
|
||||||
);
|
);
|
||||||
const on$ = new Subject<void>();
|
const on$ = new Subject<void>();
|
||||||
let error: any = null;
|
let error: any = null;
|
||||||
@ -511,6 +523,7 @@ describe("CredentialGeneratorService", () => {
|
|||||||
i18nService,
|
i18nService,
|
||||||
encryptService,
|
encryptService,
|
||||||
keyService,
|
keyService,
|
||||||
|
accountService,
|
||||||
);
|
);
|
||||||
const on$ = new Subject<void>();
|
const on$ = new Subject<void>();
|
||||||
let complete = false;
|
let complete = false;
|
||||||
@ -542,6 +555,7 @@ describe("CredentialGeneratorService", () => {
|
|||||||
i18nService,
|
i18nService,
|
||||||
encryptService,
|
encryptService,
|
||||||
keyService,
|
keyService,
|
||||||
|
accountService,
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = generator.algorithms("password");
|
const result = generator.algorithms("password");
|
||||||
@ -563,6 +577,7 @@ describe("CredentialGeneratorService", () => {
|
|||||||
i18nService,
|
i18nService,
|
||||||
encryptService,
|
encryptService,
|
||||||
keyService,
|
keyService,
|
||||||
|
accountService,
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = generator.algorithms("username");
|
const result = generator.algorithms("username");
|
||||||
@ -583,6 +598,7 @@ describe("CredentialGeneratorService", () => {
|
|||||||
i18nService,
|
i18nService,
|
||||||
encryptService,
|
encryptService,
|
||||||
keyService,
|
keyService,
|
||||||
|
accountService,
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = generator.algorithms("email");
|
const result = generator.algorithms("email");
|
||||||
@ -604,6 +620,7 @@ describe("CredentialGeneratorService", () => {
|
|||||||
i18nService,
|
i18nService,
|
||||||
encryptService,
|
encryptService,
|
||||||
keyService,
|
keyService,
|
||||||
|
accountService,
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = generator.algorithms(["username", "email"]);
|
const result = generator.algorithms(["username", "email"]);
|
||||||
@ -629,6 +646,7 @@ describe("CredentialGeneratorService", () => {
|
|||||||
i18nService,
|
i18nService,
|
||||||
encryptService,
|
encryptService,
|
||||||
keyService,
|
keyService,
|
||||||
|
accountService,
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = await firstValueFrom(generator.algorithms$("password"));
|
const result = await firstValueFrom(generator.algorithms$("password"));
|
||||||
@ -646,6 +664,7 @@ describe("CredentialGeneratorService", () => {
|
|||||||
i18nService,
|
i18nService,
|
||||||
encryptService,
|
encryptService,
|
||||||
keyService,
|
keyService,
|
||||||
|
accountService,
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = await firstValueFrom(generator.algorithms$("username"));
|
const result = await firstValueFrom(generator.algorithms$("username"));
|
||||||
@ -662,6 +681,7 @@ describe("CredentialGeneratorService", () => {
|
|||||||
i18nService,
|
i18nService,
|
||||||
encryptService,
|
encryptService,
|
||||||
keyService,
|
keyService,
|
||||||
|
accountService,
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = await firstValueFrom(generator.algorithms$("email"));
|
const result = await firstValueFrom(generator.algorithms$("email"));
|
||||||
@ -679,6 +699,7 @@ describe("CredentialGeneratorService", () => {
|
|||||||
i18nService,
|
i18nService,
|
||||||
encryptService,
|
encryptService,
|
||||||
keyService,
|
keyService,
|
||||||
|
accountService,
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = await firstValueFrom(generator.algorithms$(["username", "email"]));
|
const result = await firstValueFrom(generator.algorithms$(["username", "email"]));
|
||||||
@ -701,6 +722,7 @@ describe("CredentialGeneratorService", () => {
|
|||||||
i18nService,
|
i18nService,
|
||||||
encryptService,
|
encryptService,
|
||||||
keyService,
|
keyService,
|
||||||
|
accountService,
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = await firstValueFrom(generator.algorithms$(["password"]));
|
const result = await firstValueFrom(generator.algorithms$(["password"]));
|
||||||
@ -726,6 +748,7 @@ describe("CredentialGeneratorService", () => {
|
|||||||
i18nService,
|
i18nService,
|
||||||
encryptService,
|
encryptService,
|
||||||
keyService,
|
keyService,
|
||||||
|
accountService,
|
||||||
);
|
);
|
||||||
const results: any = [];
|
const results: any = [];
|
||||||
const sub = generator.algorithms$("password").subscribe((r) => results.push(r));
|
const sub = generator.algorithms$("password").subscribe((r) => results.push(r));
|
||||||
@ -763,6 +786,7 @@ describe("CredentialGeneratorService", () => {
|
|||||||
i18nService,
|
i18nService,
|
||||||
encryptService,
|
encryptService,
|
||||||
keyService,
|
keyService,
|
||||||
|
accountService,
|
||||||
);
|
);
|
||||||
const userId$ = new BehaviorSubject(AnotherUser).asObservable();
|
const userId$ = new BehaviorSubject(AnotherUser).asObservable();
|
||||||
|
|
||||||
@ -784,6 +808,7 @@ describe("CredentialGeneratorService", () => {
|
|||||||
i18nService,
|
i18nService,
|
||||||
encryptService,
|
encryptService,
|
||||||
keyService,
|
keyService,
|
||||||
|
accountService,
|
||||||
);
|
);
|
||||||
const userId = new BehaviorSubject(SomeUser);
|
const userId = new BehaviorSubject(SomeUser);
|
||||||
const userId$ = userId.asObservable();
|
const userId$ = userId.asObservable();
|
||||||
@ -814,6 +839,7 @@ describe("CredentialGeneratorService", () => {
|
|||||||
i18nService,
|
i18nService,
|
||||||
encryptService,
|
encryptService,
|
||||||
keyService,
|
keyService,
|
||||||
|
accountService,
|
||||||
);
|
);
|
||||||
const userId = new BehaviorSubject(SomeUser);
|
const userId = new BehaviorSubject(SomeUser);
|
||||||
const userId$ = userId.asObservable();
|
const userId$ = userId.asObservable();
|
||||||
@ -840,6 +866,7 @@ describe("CredentialGeneratorService", () => {
|
|||||||
i18nService,
|
i18nService,
|
||||||
encryptService,
|
encryptService,
|
||||||
keyService,
|
keyService,
|
||||||
|
accountService,
|
||||||
);
|
);
|
||||||
const userId = new BehaviorSubject(SomeUser);
|
const userId = new BehaviorSubject(SomeUser);
|
||||||
const userId$ = userId.asObservable();
|
const userId$ = userId.asObservable();
|
||||||
@ -866,6 +893,7 @@ describe("CredentialGeneratorService", () => {
|
|||||||
i18nService,
|
i18nService,
|
||||||
encryptService,
|
encryptService,
|
||||||
keyService,
|
keyService,
|
||||||
|
accountService,
|
||||||
);
|
);
|
||||||
const userId = new BehaviorSubject(SomeUser);
|
const userId = new BehaviorSubject(SomeUser);
|
||||||
const userId$ = userId.asObservable();
|
const userId$ = userId.asObservable();
|
||||||
@ -898,6 +926,7 @@ describe("CredentialGeneratorService", () => {
|
|||||||
i18nService,
|
i18nService,
|
||||||
encryptService,
|
encryptService,
|
||||||
keyService,
|
keyService,
|
||||||
|
accountService,
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = await firstValueFrom(generator.settings$(SomeConfiguration));
|
const result = await firstValueFrom(generator.settings$(SomeConfiguration));
|
||||||
@ -916,6 +945,7 @@ describe("CredentialGeneratorService", () => {
|
|||||||
i18nService,
|
i18nService,
|
||||||
encryptService,
|
encryptService,
|
||||||
keyService,
|
keyService,
|
||||||
|
accountService,
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = await firstValueFrom(generator.settings$(SomeConfiguration));
|
const result = await firstValueFrom(generator.settings$(SomeConfiguration));
|
||||||
@ -936,6 +966,7 @@ describe("CredentialGeneratorService", () => {
|
|||||||
i18nService,
|
i18nService,
|
||||||
encryptService,
|
encryptService,
|
||||||
keyService,
|
keyService,
|
||||||
|
accountService,
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = await firstValueFrom(generator.settings$(SomeConfiguration));
|
const result = await firstValueFrom(generator.settings$(SomeConfiguration));
|
||||||
@ -961,6 +992,7 @@ describe("CredentialGeneratorService", () => {
|
|||||||
i18nService,
|
i18nService,
|
||||||
encryptService,
|
encryptService,
|
||||||
keyService,
|
keyService,
|
||||||
|
accountService,
|
||||||
);
|
);
|
||||||
const results: any = [];
|
const results: any = [];
|
||||||
const sub = generator.settings$(SomeConfiguration).subscribe((r) => results.push(r));
|
const sub = generator.settings$(SomeConfiguration).subscribe((r) => results.push(r));
|
||||||
@ -986,6 +1018,7 @@ describe("CredentialGeneratorService", () => {
|
|||||||
i18nService,
|
i18nService,
|
||||||
encryptService,
|
encryptService,
|
||||||
keyService,
|
keyService,
|
||||||
|
accountService,
|
||||||
);
|
);
|
||||||
const userId$ = new BehaviorSubject(AnotherUser).asObservable();
|
const userId$ = new BehaviorSubject(AnotherUser).asObservable();
|
||||||
|
|
||||||
@ -1007,6 +1040,7 @@ describe("CredentialGeneratorService", () => {
|
|||||||
i18nService,
|
i18nService,
|
||||||
encryptService,
|
encryptService,
|
||||||
keyService,
|
keyService,
|
||||||
|
accountService,
|
||||||
);
|
);
|
||||||
const userId = new BehaviorSubject(SomeUser);
|
const userId = new BehaviorSubject(SomeUser);
|
||||||
const userId$ = userId.asObservable();
|
const userId$ = userId.asObservable();
|
||||||
@ -1034,6 +1068,7 @@ describe("CredentialGeneratorService", () => {
|
|||||||
i18nService,
|
i18nService,
|
||||||
encryptService,
|
encryptService,
|
||||||
keyService,
|
keyService,
|
||||||
|
accountService,
|
||||||
);
|
);
|
||||||
const userId = new BehaviorSubject(SomeUser);
|
const userId = new BehaviorSubject(SomeUser);
|
||||||
const userId$ = userId.asObservable();
|
const userId$ = userId.asObservable();
|
||||||
@ -1060,6 +1095,7 @@ describe("CredentialGeneratorService", () => {
|
|||||||
i18nService,
|
i18nService,
|
||||||
encryptService,
|
encryptService,
|
||||||
keyService,
|
keyService,
|
||||||
|
accountService,
|
||||||
);
|
);
|
||||||
const userId = new BehaviorSubject(SomeUser);
|
const userId = new BehaviorSubject(SomeUser);
|
||||||
const userId$ = userId.asObservable();
|
const userId$ = userId.asObservable();
|
||||||
@ -1086,6 +1122,7 @@ describe("CredentialGeneratorService", () => {
|
|||||||
i18nService,
|
i18nService,
|
||||||
encryptService,
|
encryptService,
|
||||||
keyService,
|
keyService,
|
||||||
|
accountService,
|
||||||
);
|
);
|
||||||
const userId = new BehaviorSubject(SomeUser);
|
const userId = new BehaviorSubject(SomeUser);
|
||||||
const userId$ = userId.asObservable();
|
const userId$ = userId.asObservable();
|
||||||
@ -1118,6 +1155,7 @@ describe("CredentialGeneratorService", () => {
|
|||||||
i18nService,
|
i18nService,
|
||||||
encryptService,
|
encryptService,
|
||||||
keyService,
|
keyService,
|
||||||
|
accountService,
|
||||||
);
|
);
|
||||||
const subject = await generator.settings(SomeConfiguration, { singleUserId$ });
|
const subject = await generator.settings(SomeConfiguration, { singleUserId$ });
|
||||||
|
|
||||||
@ -1139,6 +1177,7 @@ describe("CredentialGeneratorService", () => {
|
|||||||
i18nService,
|
i18nService,
|
||||||
encryptService,
|
encryptService,
|
||||||
keyService,
|
keyService,
|
||||||
|
accountService,
|
||||||
);
|
);
|
||||||
|
|
||||||
let completed = false;
|
let completed = false;
|
||||||
@ -1165,6 +1204,7 @@ describe("CredentialGeneratorService", () => {
|
|||||||
i18nService,
|
i18nService,
|
||||||
encryptService,
|
encryptService,
|
||||||
keyService,
|
keyService,
|
||||||
|
accountService,
|
||||||
);
|
);
|
||||||
const userId$ = new BehaviorSubject(SomeUser).asObservable();
|
const userId$ = new BehaviorSubject(SomeUser).asObservable();
|
||||||
|
|
||||||
@ -1182,6 +1222,7 @@ describe("CredentialGeneratorService", () => {
|
|||||||
i18nService,
|
i18nService,
|
||||||
encryptService,
|
encryptService,
|
||||||
keyService,
|
keyService,
|
||||||
|
accountService,
|
||||||
);
|
);
|
||||||
const userId$ = new BehaviorSubject(SomeUser).asObservable();
|
const userId$ = new BehaviorSubject(SomeUser).asObservable();
|
||||||
const policy$ = new BehaviorSubject([somePolicy]);
|
const policy$ = new BehaviorSubject([somePolicy]);
|
||||||
@ -1201,6 +1242,7 @@ describe("CredentialGeneratorService", () => {
|
|||||||
i18nService,
|
i18nService,
|
||||||
encryptService,
|
encryptService,
|
||||||
keyService,
|
keyService,
|
||||||
|
accountService,
|
||||||
);
|
);
|
||||||
const userId = new BehaviorSubject(SomeUser);
|
const userId = new BehaviorSubject(SomeUser);
|
||||||
const userId$ = userId.asObservable();
|
const userId$ = userId.asObservable();
|
||||||
@ -1230,6 +1272,7 @@ describe("CredentialGeneratorService", () => {
|
|||||||
i18nService,
|
i18nService,
|
||||||
encryptService,
|
encryptService,
|
||||||
keyService,
|
keyService,
|
||||||
|
accountService,
|
||||||
);
|
);
|
||||||
const userId = new BehaviorSubject(SomeUser);
|
const userId = new BehaviorSubject(SomeUser);
|
||||||
const userId$ = userId.asObservable();
|
const userId$ = userId.asObservable();
|
||||||
@ -1260,6 +1303,7 @@ describe("CredentialGeneratorService", () => {
|
|||||||
i18nService,
|
i18nService,
|
||||||
encryptService,
|
encryptService,
|
||||||
keyService,
|
keyService,
|
||||||
|
accountService,
|
||||||
);
|
);
|
||||||
const userId = new BehaviorSubject(SomeUser);
|
const userId = new BehaviorSubject(SomeUser);
|
||||||
const userId$ = userId.asObservable();
|
const userId$ = userId.asObservable();
|
||||||
@ -1286,6 +1330,7 @@ describe("CredentialGeneratorService", () => {
|
|||||||
i18nService,
|
i18nService,
|
||||||
encryptService,
|
encryptService,
|
||||||
keyService,
|
keyService,
|
||||||
|
accountService,
|
||||||
);
|
);
|
||||||
const userId = new BehaviorSubject(SomeUser);
|
const userId = new BehaviorSubject(SomeUser);
|
||||||
const userId$ = userId.asObservable();
|
const userId$ = userId.asObservable();
|
||||||
|
@ -23,6 +23,7 @@ import { Simplify } from "type-fest";
|
|||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
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";
|
||||||
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { StateProvider } from "@bitwarden/common/platform/state";
|
import { StateProvider } from "@bitwarden/common/platform/state";
|
||||||
@ -98,6 +99,7 @@ export class CredentialGeneratorService {
|
|||||||
private readonly i18nService: I18nService,
|
private readonly i18nService: I18nService,
|
||||||
private readonly encryptService: EncryptService,
|
private readonly encryptService: EncryptService,
|
||||||
private readonly keyService: KeyService,
|
private readonly keyService: KeyService,
|
||||||
|
private readonly accountService: AccountService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
private getDependencyProvider(): GeneratorDependencyProvider {
|
private getDependencyProvider(): GeneratorDependencyProvider {
|
||||||
@ -380,17 +382,30 @@ export class CredentialGeneratorService {
|
|||||||
configuration: Configuration<Settings, Policy>,
|
configuration: Configuration<Settings, Policy>,
|
||||||
dependencies: Policy$Dependencies,
|
dependencies: Policy$Dependencies,
|
||||||
): Observable<GeneratorConstraints<Settings>> {
|
): Observable<GeneratorConstraints<Settings>> {
|
||||||
const completion$ = dependencies.userId$.pipe(ignoreElements(), endWith(true));
|
const email$ = dependencies.userId$.pipe(
|
||||||
|
distinctUntilChanged(),
|
||||||
|
withLatestFrom(this.accountService.accounts$),
|
||||||
|
filter((accounts) => !!accounts),
|
||||||
|
map(([userId, accounts]) => {
|
||||||
|
if (userId in accounts) {
|
||||||
|
return { userId, email: accounts[userId].email };
|
||||||
|
}
|
||||||
|
|
||||||
const constraints$ = dependencies.userId$.pipe(
|
return { userId, email: null };
|
||||||
switchMap((userId) => {
|
}),
|
||||||
// complete policy emissions otherwise `mergeMap` holds `policies$` open indefinitely
|
);
|
||||||
|
|
||||||
|
const constraints$ = email$.pipe(
|
||||||
|
switchMap(({ userId, email }) => {
|
||||||
|
// complete policy emissions otherwise `switchMap` holds `policies$` open indefinitely
|
||||||
const policies$ = this.policyService
|
const policies$ = this.policyService
|
||||||
.getAll$(configuration.policy.type, userId)
|
.getAll$(configuration.policy.type, userId)
|
||||||
.pipe(takeUntil(completion$));
|
.pipe(
|
||||||
|
mapPolicyToConstraints(configuration.policy, email),
|
||||||
|
takeUntil(anyComplete(email$)),
|
||||||
|
);
|
||||||
return policies$;
|
return policies$;
|
||||||
}),
|
}),
|
||||||
mapPolicyToConstraints(configuration.policy),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return constraints$;
|
return constraints$;
|
||||||
|
@ -24,9 +24,13 @@ export type PolicyConfiguration<Policy, Settings> = {
|
|||||||
createEvaluator: (policy: Policy) => PolicyEvaluator<Policy, Settings>;
|
createEvaluator: (policy: Policy) => PolicyEvaluator<Policy, Settings>;
|
||||||
|
|
||||||
/** Converts policy service data into actionable policy constraints.
|
/** Converts policy service data into actionable policy constraints.
|
||||||
|
*
|
||||||
|
* @param policy - the policy to map into policy constraints.
|
||||||
|
* @param email - the default email to extend.
|
||||||
|
*
|
||||||
* @remarks this version includes constraints needed for the reactive forms;
|
* @remarks this version includes constraints needed for the reactive forms;
|
||||||
* it was introduced so that the constraints can be incrementally introduced
|
* it was introduced so that the constraints can be incrementally introduced
|
||||||
* as the new UI is built.
|
* as the new UI is built.
|
||||||
*/
|
*/
|
||||||
toConstraints: (policy: Policy) => GeneratorConstraints<Settings>;
|
toConstraints: (policy: Policy, email: string) => GeneratorConstraints<Settings>;
|
||||||
};
|
};
|
||||||
|
@ -4,6 +4,7 @@ import { safeProvider } from "@bitwarden/angular/platform/utils/safe-provider";
|
|||||||
import { SafeInjectionToken } from "@bitwarden/angular/services/injection-tokens";
|
import { SafeInjectionToken } from "@bitwarden/angular/services/injection-tokens";
|
||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
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 { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { StateProvider } from "@bitwarden/common/platform/state";
|
import { StateProvider } from "@bitwarden/common/platform/state";
|
||||||
@ -43,6 +44,7 @@ const RANDOMIZER = new SafeInjectionToken<Randomizer>("Randomizer");
|
|||||||
I18nService,
|
I18nService,
|
||||||
EncryptService,
|
EncryptService,
|
||||||
KeyService,
|
KeyService,
|
||||||
|
AccountService,
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
Loading…
Reference in New Issue
Block a user