1
0
mirror of https://github.com/bitwarden/browser.git synced 2024-10-22 07:50:04 +02:00

fix a bunch of forwarder cascade edge cases

This commit is contained in:
✨ Audrey ✨ 2024-10-18 17:45:31 -04:00
parent 11dc419780
commit b2f2da74ef
No known key found for this signature in database
GPG Key ID: 0CF8B4C0D9088B97
2 changed files with 162 additions and 94 deletions

View File

@ -2,12 +2,11 @@ import { Component, EventEmitter, Input, NgZone, OnDestroy, OnInit, Output } fro
import { FormBuilder } from "@angular/forms"; import { FormBuilder } from "@angular/forms";
import { import {
BehaviorSubject, BehaviorSubject,
combineLatest,
combineLatestWith, combineLatestWith,
concat,
distinctUntilChanged, distinctUntilChanged,
filter, filter,
map, map,
of,
ReplaySubject, ReplaySubject,
Subject, Subject,
switchMap, switchMap,
@ -31,6 +30,7 @@ import {
isEmailAlgorithm, isEmailAlgorithm,
isForwarderIntegration, isForwarderIntegration,
isPasswordAlgorithm, isPasswordAlgorithm,
isSameAlgorithm,
isUsernameAlgorithm, isUsernameAlgorithm,
toCredentialGeneratorConfiguration, toCredentialGeneratorConfiguration,
} from "@bitwarden/generator-core"; } from "@bitwarden/generator-core";
@ -173,59 +173,111 @@ export class CredentialGeneratorComponent implements OnInit, OnDestroy {
}); });
}); });
const username$ = new Subject<{ nav: string }>(); // normalize cascade selections; introduce subjects to allow changes
// from user selections and changes from preference updates to
// update the template
type CascadeValue = { nav: string; algorithm?: CredentialAlgorithm };
const activeRoot$ = new Subject<CascadeValue>();
const activeIdentifier$ = new Subject<CascadeValue>();
const activeForwarder$ = new Subject<CascadeValue>();
this.root$ this.root$
.pipe( .pipe(
filter(({ nav }) => !!nav), map(
switchMap((maybeAlgorithm) => { (root): CascadeValue =>
if (maybeAlgorithm.nav === IDENTIFIER) { root.nav === IDENTIFIER
return concat(of(this.username.value), this.username.valueChanges); ? { nav: root.nav }
: { nav: root.nav, algorithm: JSON.parse(root.nav) },
),
takeUntil(this.destroyed),
)
.subscribe(activeRoot$);
this.username.valueChanges
.pipe(
map(
(username): CascadeValue =>
username.nav === FORWARDER
? { nav: username.nav }
: { nav: username.nav, algorithm: JSON.parse(username.nav) },
),
takeUntil(this.destroyed),
)
.subscribe(activeIdentifier$);
this.forwarder.valueChanges
.pipe(
map(
(forwarder): CascadeValue =>
forwarder.nav === NONE_SELECTED
? { nav: forwarder.nav }
: { nav: forwarder.nav, algorithm: JSON.parse(forwarder.nav) },
),
takeUntil(this.destroyed),
)
.subscribe(activeForwarder$);
// update forwarder cascade visibility
combineLatest([activeRoot$, activeIdentifier$, activeForwarder$])
.pipe(takeUntil(this.destroyed))
.subscribe(([root, username, forwarder]) => {
const showForwarder = !root.algorithm && !username.algorithm;
const forwarderId =
showForwarder && isForwarderIntegration(forwarder.algorithm)
? forwarder.algorithm.forwarder
: null;
// update subjects within the angular zone so that the
// template bindings refresh immediately
this.zone.run(() => {
this.showForwarder$.next(showForwarder);
this.forwarderId$.next(forwarderId);
});
});
// update active algorithm
combineLatest([activeRoot$, activeIdentifier$, activeForwarder$])
.pipe(
map(([root, username, forwarder]) => {
const selection = root.algorithm ?? username.algorithm ?? forwarder.algorithm;
if (selection) {
return this.generatorService.algorithm(selection);
} else { } else {
return of(maybeAlgorithm); return null;
} }
}), }),
takeUntil(this.destroyed), takeUntil(this.destroyed),
) )
.subscribe(username$); .subscribe((algorithm) => {
// update subjects within the angular zone so that the
// template bindings refresh immediately
this.zone.run(() => {
this.algorithm$.next(algorithm);
});
});
// assume the last-visible generator algorithm is the user's preferred one // assume the last-selected generator algorithm is the user's preferred one
const preferences = await this.generatorService.preferences({ singleUserId$: this.userId$ }); const preferences = await this.generatorService.preferences({ singleUserId$: this.userId$ });
username$ this.algorithm$
.pipe( .pipe(
switchMap((maybeAlgorithm) => { filter((algorithm) => !!algorithm),
if (maybeAlgorithm.nav === FORWARDER) { distinctUntilChanged((prev, next) => isSameAlgorithm(prev.id, next.id)),
return concat(of(this.forwarder.value), this.forwarder.valueChanges);
} else {
return of(maybeAlgorithm);
}
}),
map((maybeAlgorithm) => {
if (maybeAlgorithm.nav === NONE_SELECTED) {
return { nav: null };
} else {
return maybeAlgorithm;
}
}),
filter(({ nav }) => !!nav),
withLatestFrom(preferences), withLatestFrom(preferences),
takeUntil(this.destroyed), takeUntil(this.destroyed),
) )
.subscribe(([{ nav }, preference]) => { .subscribe(([algorithm, preference]) => {
function setPreference(category: CredentialCategory) { function setPreference(category: CredentialCategory) {
const p = preference[category]; const p = preference[category];
p.algorithm = algorithm; p.algorithm = algorithm.id;
p.updated = new Date(); p.updated = new Date();
} }
const algorithm = JSON.parse(nav);
// `is*Algorithm` decides `algorithm`'s type, which flows into `setPreference` // `is*Algorithm` decides `algorithm`'s type, which flows into `setPreference`
if (isForwarderIntegration(algorithm) && algorithm.forwarder === null) { if (isEmailAlgorithm(algorithm.id)) {
return;
} else if (isEmailAlgorithm(algorithm)) {
setPreference("email"); setPreference("email");
} else if (isUsernameAlgorithm(algorithm)) { } else if (isUsernameAlgorithm(algorithm.id)) {
setPreference("username"); setPreference("username");
} else if (isPasswordAlgorithm(algorithm)) { } else if (isPasswordAlgorithm(algorithm.id)) {
setPreference("password"); setPreference("password");
} else { } else {
return; return;
@ -234,74 +286,80 @@ export class CredentialGeneratorComponent implements OnInit, OnDestroy {
preferences.next(preference); preferences.next(preference);
}); });
username$ // populate the form with the user's preferences to kick off interactivity
preferences
.pipe( .pipe(
map(({ nav }) => nav === FORWARDER), map(({ email, username, password }) => {
const forwarderPref = isForwarderIntegration(email.algorithm) ? email : null;
const usernamePref = email.updated > username.updated ? email : username;
// inject drilldown flags
const forwarderNav = !forwarderPref
? NONE_SELECTED
: JSON.stringify(forwarderPref.algorithm);
const userNav = forwarderPref ? FORWARDER : JSON.stringify(usernamePref.algorithm);
const rootNav =
usernamePref.updated > password.updated
? IDENTIFIER
: JSON.stringify(password.algorithm);
// construct cascade metadata
const cascade = {
root: {
selection: { nav: rootNav },
active: {
nav: rootNav,
algorithm: rootNav === IDENTIFIER ? null : password.algorithm,
} as CascadeValue,
},
username: {
selection: { nav: userNav },
active: {
nav: userNav,
algorithm: forwarderPref ? null : usernamePref.algorithm,
},
},
forwarder: {
selection: { nav: forwarderNav },
active: {
nav: forwarderNav,
algorithm: forwarderPref?.algorithm,
},
},
};
return cascade;
}),
takeUntil(this.destroyed), takeUntil(this.destroyed),
) )
.subscribe((showForwarder) => { .subscribe(({ root, username, forwarder }) => {
// update subjects within the angular zone so that the // update navigation; break subscription loop
// template bindings refresh immediately this.onRootChanged(root.selection);
this.zone.run(() => { this.username.setValue(username.selection, { emitEvent: false });
if (showForwarder) { this.forwarder.setValue(forwarder.selection, { emitEvent: false });
this.value$.next("-");
} // update cascade visibility
this.showForwarder$.next(showForwarder); activeRoot$.next(root.active);
}); activeIdentifier$.next(username.active);
activeForwarder$.next(forwarder.active);
}); });
// populate the form with the user's preferences to kick off interactivity // automatically regenerate when the algorithm switches if the algorithm
preferences.pipe(takeUntil(this.destroyed)).subscribe(({ email, username, password }) => { // allows it; otherwise set a placeholder
// the last preference set by the user "wins"
let forwarderPref = null;
let forwarderId: IntegrationId = null;
if (isForwarderIntegration(email.algorithm)) {
forwarderPref = email;
forwarderId = email.algorithm.forwarder;
}
const usernamePref = email.updated > username.updated ? email : username;
const rootPref = usernamePref.updated > password.updated ? usernamePref : password;
// inject drilldown flags
const forwarderNav = !forwarderPref ? NONE_SELECTED : JSON.stringify(forwarderPref.algorithm);
const userNav = forwarderPref ? FORWARDER : JSON.stringify(usernamePref.algorithm);
const rootNav =
rootPref.algorithm == usernamePref.algorithm
? IDENTIFIER
: JSON.stringify(rootPref.algorithm);
// update navigation; break subscription loop
this.onRootChanged({ nav: rootNav });
this.username.setValue({ nav: userNav }, { emitEvent: false });
this.forwarder.setValue({ nav: forwarderNav }, { emitEvent: false });
// load algorithm metadata
const algorithm = this.generatorService.algorithm(rootPref.algorithm);
// update subjects within the angular zone so that the
// template bindings refresh immediately
this.zone.run(() => {
this.algorithm$.next(algorithm);
const showForwarder = userNav === FORWARDER;
this.showForwarder$.next(showForwarder);
if (showForwarder && forwarderNav !== NONE_SELECTED) {
this.forwarderId$.next(forwarderId);
} else {
this.forwarderId$.next(null);
}
});
});
// generate on load unless the generator prohibits it
this.algorithm$ this.algorithm$
.pipe( .pipe(
distinctUntilChanged((prev, next) => prev.id === next.id), distinctUntilChanged((prev, next) => isSameAlgorithm(prev.id, next.id)),
filter((a) => !a.onlyOnRequest),
takeUntil(this.destroyed), takeUntil(this.destroyed),
) )
.subscribe(() => this.generate$.next()); .subscribe((a) => {
this.zone.run(() => {
if (!a || a.onlyOnRequest) {
this.value$.next("-");
} else {
this.generate$.next();
}
});
});
} }
private typeToGenerator$(type: CredentialAlgorithm) { private typeToGenerator$(type: CredentialAlgorithm) {

View File

@ -17,7 +17,17 @@ export type ForwarderIntegration = { forwarder: IntegrationId };
export function isForwarderIntegration( export function isForwarderIntegration(
algorithm: CredentialAlgorithm, algorithm: CredentialAlgorithm,
): algorithm is ForwarderIntegration { ): algorithm is ForwarderIntegration {
return typeof algorithm === "object" && "forwarder" in algorithm; return algorithm && typeof algorithm === "object" && "forwarder" in algorithm;
}
export function isSameAlgorithm(lhs: CredentialAlgorithm, rhs: CredentialAlgorithm) {
if (lhs === rhs) {
return true;
} else if (isForwarderIntegration(lhs) && isForwarderIntegration(rhs)) {
return lhs.forwarder === rhs.forwarder;
} else {
return false;
}
} }
/** A type of credential that may be generated by the credential generator. */ /** A type of credential that may be generated by the credential generator. */