mirror of
https://github.com/bitwarden/browser
synced 2025-12-11 13:53:34 +00:00
[PM-12303] fix password state spurious emissions (#11670)
* trace generation requests * eliminate spurious save caused by validator changes * fix emissions caused by setting bounds attrbutes --------- Co-authored-by: Daniel James Smith <djsmith85@users.noreply.github.com>
This commit is contained in:
@@ -20,9 +20,11 @@
|
|||||||
type="button"
|
type="button"
|
||||||
bitIconButton="bwi-generate"
|
bitIconButton="bwi-generate"
|
||||||
buttonType="main"
|
buttonType="main"
|
||||||
(click)="generate$.next()"
|
(click)="generate('user request')"
|
||||||
[appA11yTitle]="credentialTypeGenerateLabel$ | async"
|
[appA11yTitle]="credentialTypeGenerateLabel$ | async"
|
||||||
></button>
|
>
|
||||||
|
{{ credentialTypeGenerateLabel$ | async }}
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
bitIconButton="bwi-clone"
|
bitIconButton="bwi-clone"
|
||||||
@@ -37,13 +39,13 @@
|
|||||||
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$.next()"
|
(onUpdated)="generate('password settings')"
|
||||||
/>
|
/>
|
||||||
<tools-passphrase-settings
|
<tools-passphrase-settings
|
||||||
class="tw-mt-6"
|
class="tw-mt-6"
|
||||||
*ngIf="(showAlgorithm$ | async)?.id === 'passphrase'"
|
*ngIf="(showAlgorithm$ | async)?.id === 'passphrase'"
|
||||||
[userId]="userId$ | async"
|
[userId]="userId$ | async"
|
||||||
(onUpdated)="generate$.next()"
|
(onUpdated)="generate('passphrase settings')"
|
||||||
/>
|
/>
|
||||||
<bit-section *ngIf="(category$ | async) !== 'password'">
|
<bit-section *ngIf="(category$ | async) !== 'password'">
|
||||||
<bit-section-header>
|
<bit-section-header>
|
||||||
@@ -69,7 +71,7 @@
|
|||||||
<tools-catchall-settings
|
<tools-catchall-settings
|
||||||
*ngIf="(showAlgorithm$ | async)?.id === 'catchall'"
|
*ngIf="(showAlgorithm$ | async)?.id === 'catchall'"
|
||||||
[userId]="userId$ | async"
|
[userId]="userId$ | async"
|
||||||
(onUpdated)="generate$.next()"
|
(onUpdated)="generate('catchall settings')"
|
||||||
/>
|
/>
|
||||||
<tools-forwarder-settings
|
<tools-forwarder-settings
|
||||||
*ngIf="!!(forwarderId$ | async)"
|
*ngIf="!!(forwarderId$ | async)"
|
||||||
@@ -79,12 +81,12 @@
|
|||||||
<tools-subaddress-settings
|
<tools-subaddress-settings
|
||||||
*ngIf="(showAlgorithm$ | async)?.id === 'subaddress'"
|
*ngIf="(showAlgorithm$ | async)?.id === 'subaddress'"
|
||||||
[userId]="userId$ | async"
|
[userId]="userId$ | async"
|
||||||
(onUpdated)="generate$.next()"
|
(onUpdated)="generate('subaddress settings')"
|
||||||
/>
|
/>
|
||||||
<tools-username-settings
|
<tools-username-settings
|
||||||
*ngIf="(showAlgorithm$ | async)?.id === 'username'"
|
*ngIf="(showAlgorithm$ | async)?.id === 'username'"
|
||||||
[userId]="userId$ | async"
|
[userId]="userId$ | async"
|
||||||
(onUpdated)="generate$.next()"
|
(onUpdated)="generate('username settings')"
|
||||||
/>
|
/>
|
||||||
</bit-card>
|
</bit-card>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -376,7 +376,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$.next();
|
this.generate("autogenerate");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -472,7 +472,15 @@ export class CredentialGeneratorComponent implements OnInit, OnDestroy {
|
|||||||
protected readonly userId$ = new BehaviorSubject<UserId>(null);
|
protected readonly userId$ = new BehaviorSubject<UserId>(null);
|
||||||
|
|
||||||
/** Emits when a new credential is requested */
|
/** Emits when a new credential is requested */
|
||||||
protected readonly generate$ = new Subject<void>();
|
private readonly generate$ = new Subject<string>();
|
||||||
|
|
||||||
|
/** Request a new value from the generator
|
||||||
|
* @param requestor a label used to trace generation request
|
||||||
|
* origin in the debugger.
|
||||||
|
*/
|
||||||
|
protected generate(requestor: string) {
|
||||||
|
this.generate$.next(requestor);
|
||||||
|
}
|
||||||
|
|
||||||
private toOptions(algorithms: AlgorithmInfo[]) {
|
private toOptions(algorithms: AlgorithmInfo[]) {
|
||||||
const options: Option<string>[] = algorithms.map((algorithm) => ({
|
const options: Option<string>[] = algorithms.map((algorithm) => ({
|
||||||
|
|||||||
@@ -143,6 +143,8 @@ export class ForwarderSettingsComponent implements OnInit, OnChanges, OnDestroy
|
|||||||
control.clearValidators();
|
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
|
||||||
|
|||||||
@@ -7,14 +7,7 @@
|
|||||||
<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
|
<input bitInput formControlName="numWords" id="num-words" type="number" />
|
||||||
bitInput
|
|
||||||
formControlName="numWords"
|
|
||||||
id="num-words"
|
|
||||||
type="number"
|
|
||||||
[min]="minNumWords"
|
|
||||||
[max]="maxNumWords"
|
|
||||||
/>
|
|
||||||
</bit-form-field>
|
</bit-form-field>
|
||||||
</bit-card>
|
</bit-card>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -91,9 +91,8 @@ export class PassphraseSettingsComponent implements OnInit, OnDestroy {
|
|||||||
.get(Controls.wordSeparator)
|
.get(Controls.wordSeparator)
|
||||||
.setValidators(toValidators(Controls.wordSeparator, Generators.passphrase, constraints));
|
.setValidators(toValidators(Controls.wordSeparator, Generators.passphrase, constraints));
|
||||||
|
|
||||||
// forward word boundaries to the template (can't do it through the rx form)
|
this.settings.updateValueAndValidity({ emitEvent: false });
|
||||||
this.minNumWords = constraints.numWords.min;
|
|
||||||
this.maxNumWords = constraints.numWords.max;
|
|
||||||
this.policyInEffect = constraints.policyInEffect;
|
this.policyInEffect = constraints.policyInEffect;
|
||||||
|
|
||||||
this.toggleEnabled(Controls.capitalize, !constraints.capitalize?.readonly);
|
this.toggleEnabled(Controls.capitalize, !constraints.capitalize?.readonly);
|
||||||
@@ -104,12 +103,6 @@ export class PassphraseSettingsComponent implements OnInit, OnDestroy {
|
|||||||
this.settings.valueChanges.pipe(takeUntil(this.destroyed$)).subscribe(settings);
|
this.settings.valueChanges.pipe(takeUntil(this.destroyed$)).subscribe(settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** attribute binding for numWords[min] */
|
|
||||||
protected minNumWords: number;
|
|
||||||
|
|
||||||
/** attribute binding for numWords[max] */
|
|
||||||
protected maxNumWords: number;
|
|
||||||
|
|
||||||
/** display binding for enterprise policy notice */
|
/** display binding for enterprise policy notice */
|
||||||
protected policyInEffect: boolean;
|
protected policyInEffect: boolean;
|
||||||
|
|
||||||
|
|||||||
@@ -18,9 +18,11 @@
|
|||||||
type="button"
|
type="button"
|
||||||
bitIconButton="bwi-generate"
|
bitIconButton="bwi-generate"
|
||||||
buttonType="main"
|
buttonType="main"
|
||||||
(click)="generate$.next()"
|
(click)="generate('user request')"
|
||||||
[appA11yTitle]="credentialTypeGenerateLabel$ | async"
|
[appA11yTitle]="credentialTypeGenerateLabel$ | async"
|
||||||
></button>
|
>
|
||||||
|
{{ credentialTypeGenerateLabel$ | async }}
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
bitIconButton="bwi-clone"
|
bitIconButton="bwi-clone"
|
||||||
@@ -36,12 +38,12 @@
|
|||||||
*ngIf="(algorithm$ | async)?.id === 'password'"
|
*ngIf="(algorithm$ | async)?.id === 'password'"
|
||||||
[userId]="this.userId$ | async"
|
[userId]="this.userId$ | async"
|
||||||
[disableMargin]="disableMargin"
|
[disableMargin]="disableMargin"
|
||||||
(onUpdated)="generate$.next()"
|
(onUpdated)="generate('password settings')"
|
||||||
/>
|
/>
|
||||||
<tools-passphrase-settings
|
<tools-passphrase-settings
|
||||||
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"
|
||||||
(onUpdated)="generate$.next()"
|
(onUpdated)="generate('passphrase settings')"
|
||||||
[disableMargin]="disableMargin"
|
[disableMargin]="disableMargin"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -59,7 +59,15 @@ export class PasswordGeneratorComponent implements OnInit, OnDestroy {
|
|||||||
protected readonly userId$ = new BehaviorSubject<UserId>(null);
|
protected readonly userId$ = new BehaviorSubject<UserId>(null);
|
||||||
|
|
||||||
/** Emits when a new credential is requested */
|
/** Emits when a new credential is requested */
|
||||||
protected readonly generate$ = new Subject<void>();
|
private readonly generate$ = new Subject<string>();
|
||||||
|
|
||||||
|
/** Request a new value from the generator
|
||||||
|
* @param requestor a label used to trace generation request
|
||||||
|
* origin in the debugger.
|
||||||
|
*/
|
||||||
|
protected generate(requestor: string) {
|
||||||
|
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
|
||||||
@@ -154,7 +162,7 @@ export class PasswordGeneratorComponent implements OnInit, OnDestroy {
|
|||||||
filter((a) => !a.onlyOnRequest),
|
filter((a) => !a.onlyOnRequest),
|
||||||
takeUntil(this.destroyed),
|
takeUntil(this.destroyed),
|
||||||
)
|
)
|
||||||
.subscribe(() => this.generate$.next());
|
.subscribe(() => this.generate("autogenerate"));
|
||||||
}
|
}
|
||||||
|
|
||||||
private typeToGenerator$(type: CredentialAlgorithm) {
|
private typeToGenerator$(type: CredentialAlgorithm) {
|
||||||
|
|||||||
@@ -7,13 +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
|
<input bitInput formControlName="length" type="number" />
|
||||||
bitInput
|
|
||||||
formControlName="length"
|
|
||||||
type="number"
|
|
||||||
[min]="minLength"
|
|
||||||
[max]="maxLength"
|
|
||||||
/>
|
|
||||||
</bit-form-field>
|
</bit-form-field>
|
||||||
</bit-card>
|
</bit-card>
|
||||||
</div>
|
</div>
|
||||||
@@ -57,23 +51,11 @@
|
|||||||
<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
|
<input bitInput type="number" formControlName="minNumber" />
|
||||||
bitInput
|
|
||||||
type="number"
|
|
||||||
[min]="minMinNumber"
|
|
||||||
[max]="maxMinNumber"
|
|
||||||
formControlName="minNumber"
|
|
||||||
/>
|
|
||||||
</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
|
<input bitInput type="number" formControlName="minSpecial" />
|
||||||
bitInput
|
|
||||||
type="number"
|
|
||||||
[min]="minMinSpecial"
|
|
||||||
[max]="maxMinSpecial"
|
|
||||||
formControlName="minSpecial"
|
|
||||||
/>
|
|
||||||
</bit-form-field>
|
</bit-form-field>
|
||||||
</div>
|
</div>
|
||||||
<bit-form-control [disableMargin]="!policyInEffect">
|
<bit-form-control [disableMargin]="!policyInEffect">
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
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, debounceTime, skip } from "rxjs";
|
import { BehaviorSubject, takeUntil, Subject, map, filter, tap, skip } 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";
|
||||||
@@ -132,14 +132,6 @@ export class PasswordSettingsComponent implements OnInit, OnDestroy {
|
|||||||
toValidators(Controls.minSpecial, Generators.password, constraints),
|
toValidators(Controls.minSpecial, Generators.password, constraints),
|
||||||
);
|
);
|
||||||
|
|
||||||
// forward word boundaries to the template (can't do it through the rx form)
|
|
||||||
this.minLength = constraints.length.min;
|
|
||||||
this.maxLength = constraints.length.max;
|
|
||||||
this.minMinNumber = constraints.minNumber.min;
|
|
||||||
this.maxMinNumber = constraints.minNumber.max;
|
|
||||||
this.minMinSpecial = constraints.minSpecial.min;
|
|
||||||
this.maxMinSpecial = constraints.minSpecial.max;
|
|
||||||
|
|
||||||
this.policyInEffect = constraints.policyInEffect;
|
this.policyInEffect = constraints.policyInEffect;
|
||||||
|
|
||||||
const toggles = [
|
const toggles = [
|
||||||
@@ -201,9 +193,6 @@ export class PasswordSettingsComponent implements OnInit, OnDestroy {
|
|||||||
// now that outputs are set up, connect inputs
|
// now that outputs are set up, connect inputs
|
||||||
this.settings.valueChanges
|
this.settings.valueChanges
|
||||||
.pipe(
|
.pipe(
|
||||||
// debounce ensures rapid edits to a field, such as partial edits to a
|
|
||||||
// spinbox or rapid button clicks don't emit spurious generator updates
|
|
||||||
debounceTime(this.waitMs),
|
|
||||||
map((settings) => {
|
map((settings) => {
|
||||||
// interface is "avoid" while storage is "include"
|
// interface is "avoid" while storage is "include"
|
||||||
const s: any = { ...settings };
|
const s: any = { ...settings };
|
||||||
@@ -216,24 +205,6 @@ export class PasswordSettingsComponent implements OnInit, OnDestroy {
|
|||||||
.subscribe(settings);
|
.subscribe(settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** attribute binding for length[min] */
|
|
||||||
protected minLength: number;
|
|
||||||
|
|
||||||
/** attribute binding for length[max] */
|
|
||||||
protected maxLength: number;
|
|
||||||
|
|
||||||
/** attribute binding for minNumber[min] */
|
|
||||||
protected minMinNumber: number;
|
|
||||||
|
|
||||||
/** attribute binding for minNumber[max] */
|
|
||||||
protected maxMinNumber: number;
|
|
||||||
|
|
||||||
/** attribute binding for minSpecial[min] */
|
|
||||||
protected minMinSpecial: number;
|
|
||||||
|
|
||||||
/** attribute binding for minSpecial[max] */
|
|
||||||
protected maxMinSpecial: number;
|
|
||||||
|
|
||||||
/** display binding for enterprise policy notice */
|
/** display binding for enterprise policy notice */
|
||||||
protected policyInEffect: boolean;
|
protected policyInEffect: boolean;
|
||||||
|
|
||||||
|
|||||||
@@ -7,9 +7,11 @@
|
|||||||
type="button"
|
type="button"
|
||||||
bitIconButton="bwi-generate"
|
bitIconButton="bwi-generate"
|
||||||
buttonType="main"
|
buttonType="main"
|
||||||
(click)="generate$.next()"
|
(click)="generate('user request')"
|
||||||
[appA11yTitle]="credentialTypeGenerateLabel$ | async"
|
[appA11yTitle]="credentialTypeGenerateLabel$ | async"
|
||||||
></button>
|
>
|
||||||
|
{{ credentialTypeGenerateLabel$ | async }}
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
bitIconButton="bwi-clone"
|
bitIconButton="bwi-clone"
|
||||||
@@ -17,7 +19,9 @@
|
|||||||
showToast
|
showToast
|
||||||
[appA11yTitle]="credentialTypeCopyLabel$ | async"
|
[appA11yTitle]="credentialTypeCopyLabel$ | async"
|
||||||
[appCopyClick]="value$ | async"
|
[appCopyClick]="value$ | async"
|
||||||
></button>
|
>
|
||||||
|
{{ credentialTypeCopyLabel$ | async }}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</bit-card>
|
</bit-card>
|
||||||
<bit-section [disableMargin]="disableMargin">
|
<bit-section [disableMargin]="disableMargin">
|
||||||
@@ -44,7 +48,7 @@
|
|||||||
<tools-catchall-settings
|
<tools-catchall-settings
|
||||||
*ngIf="(algorithm$ | async)?.id === 'catchall'"
|
*ngIf="(algorithm$ | async)?.id === 'catchall'"
|
||||||
[userId]="this.userId$ | async"
|
[userId]="this.userId$ | async"
|
||||||
(onUpdated)="generate$.next()"
|
(onUpdated)="generate('catchall settings')"
|
||||||
/>
|
/>
|
||||||
<tools-forwarder-settings
|
<tools-forwarder-settings
|
||||||
*ngIf="!!(forwarderId$ | async)"
|
*ngIf="!!(forwarderId$ | async)"
|
||||||
@@ -54,12 +58,12 @@
|
|||||||
<tools-subaddress-settings
|
<tools-subaddress-settings
|
||||||
*ngIf="(algorithm$ | async)?.id === 'subaddress'"
|
*ngIf="(algorithm$ | async)?.id === 'subaddress'"
|
||||||
[userId]="this.userId$ | async"
|
[userId]="this.userId$ | async"
|
||||||
(onUpdated)="generate$.next()"
|
(onUpdated)="generate('subaddress settings')"
|
||||||
/>
|
/>
|
||||||
<tools-username-settings
|
<tools-username-settings
|
||||||
*ngIf="(algorithm$ | async)?.id === 'username'"
|
*ngIf="(algorithm$ | async)?.id === 'username'"
|
||||||
[userId]="this.userId$ | async"
|
[userId]="this.userId$ | async"
|
||||||
(onUpdated)="generate$.next()"
|
(onUpdated)="generate('username settings')"
|
||||||
/>
|
/>
|
||||||
</bit-card>
|
</bit-card>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -313,7 +313,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$.next();
|
this.generate("autogenerate");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -391,7 +391,15 @@ export class UsernameGeneratorComponent implements OnInit, OnDestroy {
|
|||||||
protected readonly userId$ = new BehaviorSubject<UserId>(null);
|
protected readonly userId$ = new BehaviorSubject<UserId>(null);
|
||||||
|
|
||||||
/** Emits when a new credential is requested */
|
/** Emits when a new credential is requested */
|
||||||
protected readonly generate$ = new Subject<void>();
|
private readonly generate$ = new Subject<string>();
|
||||||
|
|
||||||
|
/** Request a new value from the generator
|
||||||
|
* @param requestor a label used to trace generation request
|
||||||
|
* origin in the debugger.
|
||||||
|
*/
|
||||||
|
protected generate(requestor: string) {
|
||||||
|
this.generate$.next(requestor);
|
||||||
|
}
|
||||||
|
|
||||||
private toOptions(algorithms: AlgorithmInfo[]) {
|
private toOptions(algorithms: AlgorithmInfo[]) {
|
||||||
const options: Option<string>[] = algorithms.map((algorithm) => ({
|
const options: Option<string>[] = algorithms.map((algorithm) => ({
|
||||||
|
|||||||
Reference in New Issue
Block a user