Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weโ€™ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[PM-13876] replace angular validation with html constraints validation #11816

Merged
merged 19 commits into from
Nov 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
98fef39
rough-in passphrase validation failure handling
audreyality Oct 29, 2024
1ad143f
trigger valid change from settings
audreyality Oct 30, 2024
4f76d0f
Merge branch 'main' into tools/PM-13876/prevent-generate-until-valid
audreyality Oct 30, 2024
3ee034a
fix `max` constraint enforcement
audreyality Oct 30, 2024
42be5db
add taps for generator validation monitoring/debugging
audreyality Oct 31, 2024
6832b43
HTML constraints validation rises like a phoenix
audreyality Oct 31, 2024
b3accc3
remove min/max boundaries to fix chrome display issue
audreyality Nov 1, 2024
dd23ce0
bind settings components as view children of options components
audreyality Nov 1, 2024
9774e8a
remove defunct `okSettings$`
audreyality Nov 1, 2024
9b79bdb
extend validationless generator to passwords
audreyality Nov 1, 2024
1942a92
extend validationless generator to catchall emails
audreyality Nov 1, 2024
bd3457c
extend validationless generator to forwarder emails
audreyality Nov 1, 2024
e8f6e15
extend validationless generator to subaddress emails
audreyality Nov 1, 2024
aec68c3
extend validationless generator to usernames
audreyality Nov 1, 2024
34a05b8
Merge branch 'main' into tools/PM-13876/remove-validation
audreyality Nov 1, 2024
e02e08c
fix observable cycle
audreyality Nov 1, 2024
703b310
disable generate button when no algorithm is selected
audreyality Nov 1, 2024
c75909b
prevent duplicate algorithm emissions
audreyality Nov 1, 2024
c356c7a
add constraints that assign email address defaults
audreyality Nov 1, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions libs/common/src/tools/state/object-key.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export type ObjectKey<State, Secret = State, Disclosed = Record<string, never>>
classifier: Classifier<State, Disclosed, Secret>;
format: "plain" | "classified";
options: UserKeyDefinitionOptions<State>;
initial?: State;
};

export function isObjectKey(key: any): key is ObjectKey<unknown> {
Expand Down
7 changes: 4 additions & 3 deletions libs/common/src/tools/state/user-state-subject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -254,17 +254,18 @@ export class UserStateSubject<
withConstraints,
map(([loadedState, constraints]) => {
// bypass nulls
if (!loadedState) {
if (!loadedState && !this.objectKey?.initial) {
return {
constraints: {} as Constraints<State>,
state: null,
} satisfies Constrained<State>;
}

const unconstrained = loadedState ?? structuredClone(this.objectKey.initial);
const calibration = isDynamic(constraints)
? constraints.calibrate(loadedState)
? constraints.calibrate(unconstrained)
: constraints;
const adjusted = calibration.adjust(loadedState);
const adjusted = calibration.adjust(unconstrained);

return {
constraints: calibration.constraints,
Expand Down
Original file line number Diff line number Diff line change
@@ -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>
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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>.+)");

Comment on lines +15 to +19
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

โš ๏ธ I don't see this being used anywhere. Possibly a relict before moving it to catchall-constraints.ts.

/** Options group for catchall emails */
@Component({
selector: "tools-catchall-settings",
Expand Down Expand Up @@ -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$() {
Expand All @@ -78,6 +95,7 @@ export class CatchallSettingsComponent implements OnInit, OnDestroy {

private readonly destroyed$ = new Subject<void>();
ngOnDestroy(): void {
this.destroyed$.next();
this.destroyed$.complete();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
buttonType="main"
(click)="generate('user request')"
[appA11yTitle]="credentialTypeGenerateLabel$ | async"
[disabled]="!(algorithm$ | async)"
>
{{ credentialTypeGenerateLabel$ | async }}
</button>
Expand All @@ -33,16 +34,19 @@
[appA11yTitle]="credentialTypeCopyLabel$ | async"
[appCopyClick]="value$ | async"
[valueLabel]="credentialTypeLabel$ | async"
[disabled]="!(algorithm$ | async)"
></button>
</div>
</bit-card>
<tools-password-settings
#passwordSettings
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

โš ๏ธโ›๏ธ The usage of viewChild got introduced with dd23ce0 but got removed with e02e08c, so there's no need to keep the reference within the markup.

This applies to credential-generator.component.html, password-generator.component.html and username-generator.component.html

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"
Expand Down Expand Up @@ -80,21 +84,25 @@ <h2 bitTypography="h6">{{ "options" | i18n }}</h2>
</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')"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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>();
Expand Down Expand Up @@ -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));
}
});
});
Expand Down Expand Up @@ -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);
}

Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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>
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import {
skip,
Subject,
switchAll,
switchMap,
takeUntil,
withLatestFrom,
} from "rxjs";
Expand All @@ -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",
Expand Down Expand Up @@ -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$$
Expand All @@ -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) {
Expand Down Expand Up @@ -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();
}
}
2 changes: 2 additions & 0 deletions libs/tools/generator/components/src/generator.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -79,6 +80,7 @@ const RANDOMIZER = new SafeInjectionToken<Randomizer>("Randomizer");
I18nService,
EncryptService,
KeyService,
AccountService,
],
}),
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,13 @@ <h6 bitTypography="h6">{{ "options" | i18n }}</h6>
<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>
Expand All @@ -16,14 +22,33 @@ <h6 bitTypography="h6">{{ "options" | i18n }}</h6>
<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>
Expand Down
Loading
Loading