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

finish forwarder drilldown

This commit is contained in:
✨ Audrey ✨ 2024-10-15 13:56:49 -04:00
parent c48ddd8ecd
commit 32aa0429bb
No known key found for this signature in database
GPG Key ID: 0CF8B4C0D9088B97
8 changed files with 155 additions and 59 deletions

View File

@ -3,7 +3,7 @@
fullWidth fullWidth
class="tw-mb-4" class="tw-mb-4"
[selected]="(root$ | async).nav" [selected]="(root$ | async).nav"
(selectedChange)="onRootChanged($event)" (selectedChange)="onRootChanged({ nav: $event })"
attr.aria-label="{{ 'type' | i18n }}" attr.aria-label="{{ 'type' | i18n }}"
> >
<bit-toggle *ngFor="let option of rootOptions$ | async" [value]="option.value"> <bit-toggle *ngFor="let option of rootOptions$ | async" [value]="option.value">
@ -57,6 +57,12 @@
}}</bit-hint> }}</bit-hint>
</bit-form-field> </bit-form-field>
</form> </form>
<form class="box" [formGroup]="forwarder" class="tw-container">
<bit-form-field>
<bit-label>{{ "forwarder" | i18n }}</bit-label>
<bit-select [items]="forwarderOptions$ | async" formControlName="forwarder"> </bit-select>
</bit-form-field>
</form>
<tools-catchall-settings <tools-catchall-settings
*ngIf="(algorithm$ | async)?.id === 'catchall'" *ngIf="(algorithm$ | async)?.id === 'catchall'"
[userId]="userId$ | async" [userId]="userId$ | async"

View File

@ -23,6 +23,8 @@ import {
CredentialAlgorithm, CredentialAlgorithm,
CredentialCategory, CredentialCategory,
CredentialGeneratorService, CredentialGeneratorService,
EmailAlgorithm,
ForwarderIntegration,
GeneratedCredential, GeneratedCredential,
Generators, Generators,
getForwarderConfiguration, getForwarderConfiguration,
@ -32,6 +34,7 @@ import {
isUsernameAlgorithm, isUsernameAlgorithm,
PasswordAlgorithm, PasswordAlgorithm,
toCredentialGeneratorConfiguration, toCredentialGeneratorConfiguration,
UsernameAlgorithm,
} from "@bitwarden/generator-core"; } from "@bitwarden/generator-core";
/** root category that drills into username and email categories */ /** root category that drills into username and email categories */
@ -39,6 +42,12 @@ const IDENTIFIER = "identifier";
/** options available for the top-level navigation */ /** options available for the top-level navigation */
type RootNavValue = PasswordAlgorithm | typeof IDENTIFIER; type RootNavValue = PasswordAlgorithm | typeof IDENTIFIER;
const FORWARDER = "forwarder";
type UsernameNavValue = UsernameAlgorithm | EmailAlgorithm | typeof FORWARDER;
const NONE_SELECTED = "none";
type ForwarderNavValue = ForwarderIntegration | typeof NONE_SELECTED;
@Component({ @Component({
selector: "tools-credential-generator", selector: "tools-credential-generator",
templateUrl: "credential-generator.component.html", templateUrl: "credential-generator.component.html",
@ -66,17 +75,21 @@ export class CredentialGeneratorComponent implements OnInit, OnDestroy {
nav: null, nav: null,
}); });
protected onRootChanged(nav: RootNavValue) { protected onRootChanged(value: { nav: RootNavValue }) {
// prevent subscription cycle // prevent subscription cycle
if (this.root$.value.nav !== nav) { if (this.root$.value.nav !== value.nav) {
this.zone.run(() => { this.zone.run(() => {
this.root$.next({ nav }); this.root$.next(value);
}); });
} }
} }
protected username = this.formBuilder.group({ protected username = this.formBuilder.group({
nav: [null as CredentialAlgorithm], nav: [null as UsernameNavValue],
});
protected forwarder = this.formBuilder.group({
nav: [null as ForwarderNavValue],
}); });
async ngOnInit() { async ngOnInit() {
@ -95,10 +108,23 @@ export class CredentialGeneratorComponent implements OnInit, OnDestroy {
this.generatorService this.generatorService
.algorithms$(["email", "username"], { userId$: this.userId$ }) .algorithms$(["email", "username"], { userId$: this.userId$ })
.pipe( .pipe(
map((algorithms) => this.toOptions(algorithms)), map((algorithms) => {
const usernames = algorithms.filter((a) => !isForwarderIntegration(a.id));
const usernameOptions = this.toOptions(usernames) as Option<UsernameNavValue>[];
usernameOptions.push({ value: FORWARDER, label: this.i18nService.t("forwarder") });
const forwarders = algorithms.filter((a) => isForwarderIntegration(a.id));
const forwarderOptions = this.toOptions(forwarders) as Option<ForwarderNavValue>[];
forwarderOptions.unshift({ value: NONE_SELECTED, label: this.i18nService.t("select") });
return [usernameOptions, forwarderOptions] as const;
}),
takeUntil(this.destroyed), takeUntil(this.destroyed),
) )
.subscribe(this.usernameOptions$); .subscribe(([usernames, forwarders]) => {
this.usernameOptions$.next(usernames);
this.forwarderOptions$.next(forwarders);
});
this.generatorService this.generatorService
.algorithms$("password", { userId$: this.userId$ }) .algorithms$("password", { userId$: this.userId$ })
@ -166,11 +192,18 @@ export class CredentialGeneratorComponent implements OnInit, OnDestroy {
return of(root as { nav: CredentialAlgorithm }); return of(root as { nav: CredentialAlgorithm });
} }
}), }),
switchMap((tier1) => { switchMap((username) => {
if (tier1.nav === FORWARDER) { if (username.nav === FORWARDER) {
return concat(of(this.forwarder.value), this.forwarder.valueChanges); return concat(of(this.forwarder.value), this.forwarder.valueChanges);
} else { } else {
return of(tier1 as { nav: CredentialAlgorithm }); return of(username as { nav: CredentialAlgorithm });
}
}),
map((forwarder) => {
if (forwarder.nav === NONE_SELECTED) {
return { nav: null };
} else {
return forwarder as { nav: CredentialAlgorithm };
} }
}), }),
filter(({ nav }) => !!nav), filter(({ nav }) => !!nav),
@ -201,16 +234,27 @@ export class CredentialGeneratorComponent implements OnInit, OnDestroy {
// populate the form with the user's preferences to kick off interactivity // populate the form with the user's preferences to kick off interactivity
preferences.pipe(takeUntil(this.destroyed)).subscribe(({ email, username, password }) => { preferences.pipe(takeUntil(this.destroyed)).subscribe(({ email, username, password }) => {
// the last preference set by the user "wins" // the last preference set by the user "wins"
const userNav = email.updated > username.updated ? email : username; const forwarderPref = isForwarderIntegration(email.algorithm) ? email : null;
const rootNav: any = userNav.updated > password.updated ? IDENTIFIER : password.algorithm; const usernamePref = email.updated > username.updated ? email : username;
const credentialType = rootNav === IDENTIFIER ? userNav.algorithm : password.algorithm; const rootPref = username.updated > password.updated ? username : password;
// inject drilldown flags
const forwarderNav = !forwarderPref
? NONE_SELECTED
: (forwarderPref.algorithm as ForwarderIntegration);
const userNav = forwarderPref ? FORWARDER : (usernamePref.algorithm as UsernameAlgorithm);
const rootNav =
rootPref.algorithm == usernamePref.algorithm
? IDENTIFIER
: (rootPref.algorithm as PasswordAlgorithm);
// update navigation; break subscription loop // update navigation; break subscription loop
this.onRootChanged(rootNav); this.onRootChanged({ nav: rootNav });
this.username.setValue({ nav: userNav.algorithm }, { emitEvent: false }); this.username.setValue({ nav: userNav }, { emitEvent: false });
this.forwarder.setValue({ nav: forwarderNav }, { emitEvent: false });
// load algorithm metadata // load algorithm metadata
const algorithm = this.generatorService.algorithm(credentialType); const algorithm = this.generatorService.algorithm(rootPref.algorithm);
// update subjects within the angular zone so that the // update subjects within the angular zone so that the
// template bindings refresh immediately // template bindings refresh immediately
@ -261,12 +305,15 @@ export class CredentialGeneratorComponent implements OnInit, OnDestroy {
throw new Error(`Invalid generator type: "${type}"`); throw new Error(`Invalid generator type: "${type}"`);
} }
/** Lists the credential types of the username algorithm box. */
protected usernameOptions$ = new BehaviorSubject<Option<CredentialAlgorithm>[]>([]);
/** Lists the top-level credential types supported by the component. */ /** Lists the top-level credential types supported by the component. */
protected rootOptions$ = new BehaviorSubject<Option<RootNavValue>[]>([]); protected rootOptions$ = new BehaviorSubject<Option<RootNavValue>[]>([]);
/** Lists the credential types of the username algorithm box. */
protected usernameOptions$ = new BehaviorSubject<Option<UsernameNavValue>[]>([]);
/** Lists the credential types of the username algorithm box. */
protected forwarderOptions$ = new BehaviorSubject<Option<ForwarderNavValue>[]>([]);
/** tracks the currently selected credential type */ /** tracks the currently selected credential type */
protected algorithm$ = new ReplaySubject<AlgorithmInfo>(1); protected algorithm$ = new ReplaySubject<AlgorithmInfo>(1);

View File

@ -23,20 +23,19 @@
</bit-section-header> </bit-section-header>
<div class="tw-mb-4"> <div class="tw-mb-4">
<bit-card> <bit-card>
<form class="box" [formGroup]="credential" class="tw-container"> <form class="box" [formGroup]="username" class="tw-container">
<bit-form-field> <bit-form-field>
<bit-label>{{ "type" | i18n }}</bit-label> <bit-label>{{ "type" | i18n }}</bit-label>
<bit-select [items]="typeOptions$ | async" formControlName="type"> </bit-select> <bit-select [items]="typeOptions$ | async" formControlName="nav"> </bit-select>
<bit-hint *ngIf="!!(credentialTypeHint$ | async)">{{ <bit-hint *ngIf="!!(credentialTypeHint$ | async)">{{
credentialTypeHint$ | async credentialTypeHint$ | async
}}</bit-hint> }}</bit-hint>
</bit-form-field> </bit-form-field>
</form>
<form class="box" [formGroup]="forwarder" class="tw-container">
<bit-form-field> <bit-form-field>
<bit-label>{{ "forwarder" | i18n }}</bit-label> <bit-label>{{ "forwarder" | i18n }}</bit-label>
<bit-select [items]="forwarderOptions$ | async" formControlName="forwarder"> </bit-select> <bit-select [items]="forwarderOptions$ | async" formControlName="nav"> </bit-select>
<bit-hint *ngIf="!!(forwarderTypeHint$ | async)">{{
forwarderTypeHint$ | async
}}</bit-hint>
</bit-form-field> </bit-form-field>
</form> </form>
<tools-catchall-settings <tools-catchall-settings

View File

@ -2,9 +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,
concat,
distinctUntilChanged, distinctUntilChanged,
filter, filter,
map, map,
of,
ReplaySubject, ReplaySubject,
Subject, Subject,
switchMap, switchMap,
@ -20,6 +22,8 @@ import {
AlgorithmInfo, AlgorithmInfo,
CredentialAlgorithm, CredentialAlgorithm,
CredentialGeneratorService, CredentialGeneratorService,
EmailAlgorithm,
ForwarderIntegration,
GeneratedCredential, GeneratedCredential,
Generators, Generators,
getForwarderConfiguration, getForwarderConfiguration,
@ -27,8 +31,15 @@ import {
isForwarderIntegration, isForwarderIntegration,
isUsernameAlgorithm, isUsernameAlgorithm,
toCredentialGeneratorConfiguration, toCredentialGeneratorConfiguration,
UsernameAlgorithm,
} from "@bitwarden/generator-core"; } from "@bitwarden/generator-core";
const FORWARDER = "forwarder";
type UsernameNavValue = UsernameAlgorithm | EmailAlgorithm | typeof FORWARDER;
const NONE_SELECTED = "none";
type ForwarderNavValue = ForwarderIntegration | typeof NONE_SELECTED;
/** Component that generates usernames and emails */ /** Component that generates usernames and emails */
@Component({ @Component({
selector: "tools-username-generator", selector: "tools-username-generator",
@ -61,8 +72,12 @@ export class UsernameGeneratorComponent implements OnInit, OnDestroy {
readonly onGenerated = new EventEmitter<GeneratedCredential>(); readonly onGenerated = new EventEmitter<GeneratedCredential>();
/** Tracks the selected generation algorithm */ /** Tracks the selected generation algorithm */
protected credential = this.formBuilder.group({ protected username = this.formBuilder.group({
type: [null as CredentialAlgorithm], nav: [null as UsernameNavValue],
});
protected forwarder = this.formBuilder.group({
nav: [null as ForwarderNavValue],
}); });
async ngOnInit() { async ngOnInit() {
@ -81,18 +96,22 @@ export class UsernameGeneratorComponent implements OnInit, OnDestroy {
this.generatorService this.generatorService
.algorithms$(["email", "username"], { userId$: this.userId$ }) .algorithms$(["email", "username"], { userId$: this.userId$ })
.pipe( .pipe(
map( map((algorithms) => {
(algorithms) => const usernames = algorithms.filter((a) => !isForwarderIntegration(a.id));
[ const usernameOptions = this.toOptions(usernames) as Option<UsernameNavValue>[];
this.toOptions(algorithms.filter((a) => !isForwarderIntegration(a.id))), usernameOptions.push({ value: FORWARDER, label: this.i18nService.t("forwarder") });
this.toOptions(algorithms.filter((a) => isForwarderIntegration(a.id))),
] as const, const forwarders = algorithms.filter((a) => isForwarderIntegration(a.id));
), const forwarderOptions = this.toOptions(forwarders) as Option<ForwarderNavValue>[];
forwarderOptions.unshift({ value: NONE_SELECTED, label: this.i18nService.t("select") });
return [usernameOptions, forwarderOptions] as const;
}),
takeUntil(this.destroyed), takeUntil(this.destroyed),
) )
.subscribe(([type, forwarder]) => { .subscribe(([usernames, forwarders]) => {
this.typeOptions$.next(type); this.typeOptions$.next(usernames);
this.forwarderOptions$.next(forwarder); this.forwarderOptions$.next(forwarders);
}); });
this.algorithm$ this.algorithm$
@ -125,18 +144,32 @@ export class UsernameGeneratorComponent implements OnInit, OnDestroy {
// assume the last-visible generator algorithm is the user's preferred one // assume the last-visible 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$ });
this.credential.valueChanges this.username.valueChanges
.pipe( .pipe(
filter(({ type }) => !!type), switchMap((username) => {
if (username.nav === FORWARDER) {
return concat(of(this.forwarder.value), this.forwarder.valueChanges);
} else {
return of(username as { nav: CredentialAlgorithm });
}
}),
map((forwarder) => {
if (forwarder.nav === NONE_SELECTED) {
return { nav: null };
} else {
return forwarder as { nav: CredentialAlgorithm };
}
}),
filter(({ nav }) => !!nav),
withLatestFrom(preferences), withLatestFrom(preferences),
takeUntil(this.destroyed), takeUntil(this.destroyed),
) )
.subscribe(([{ type }, preference]) => { .subscribe(([{ nav: algorithm }, preference]) => {
if (isEmailAlgorithm(type)) { if (isEmailAlgorithm(algorithm)) {
preference.email.algorithm = type; preference.email.algorithm = algorithm;
preference.email.updated = new Date(); preference.email.updated = new Date();
} else if (isUsernameAlgorithm(type)) { } else if (isUsernameAlgorithm(algorithm)) {
preference.username.algorithm = type; preference.username.algorithm = algorithm;
preference.username.updated = new Date(); preference.username.updated = new Date();
} else { } else {
return; return;
@ -147,14 +180,23 @@ export class UsernameGeneratorComponent implements OnInit, OnDestroy {
// populate the form with the user's preferences to kick off interactivity // populate the form with the user's preferences to kick off interactivity
preferences.pipe(takeUntil(this.destroyed)).subscribe(({ email, username }) => { preferences.pipe(takeUntil(this.destroyed)).subscribe(({ email, username }) => {
// this generator supports email & username; the last preference // the last preference set by the user "wins"
// set by the user "wins" const forwarderPref = isForwarderIntegration(email.algorithm) ? email : null;
const preference = email.updated > username.updated ? email.algorithm : username.algorithm; const usernamePref = email.updated > username.updated ? email : username;
// break subscription loop // inject drilldown flags
this.credential.setValue({ type: preference }, { emitEvent: false }); const forwarderNav = forwarderPref
? (forwarderPref.algorithm as ForwarderIntegration)
: NONE_SELECTED;
const userNav = forwarderPref ? FORWARDER : (usernamePref.algorithm as UsernameAlgorithm);
// update navigation; break subscription loop
this.username.setValue({ nav: userNav }, { emitEvent: false });
this.forwarder.setValue({ nav: forwarderNav }, { emitEvent: false });
// load selected algorithm metadata
const algorithm = this.generatorService.algorithm(usernamePref.algorithm);
const algorithm = this.generatorService.algorithm(preference);
// update subjects within the angular zone so that the // update subjects within the angular zone so that the
// template bindings refresh immediately // template bindings refresh immediately
this.zone.run(() => { this.zone.run(() => {
@ -199,10 +241,10 @@ export class UsernameGeneratorComponent implements OnInit, OnDestroy {
} }
/** Lists the credential types supported by the component. */ /** Lists the credential types supported by the component. */
protected typeOptions$ = new BehaviorSubject<Option<CredentialAlgorithm>[]>([]); protected typeOptions$ = new BehaviorSubject<Option<UsernameNavValue>[]>([]);
/** Lists the credential types supported by the component. */ /** Lists the credential types supported by the component. */
protected forwarderOptions$ = new BehaviorSubject<Option<CredentialAlgorithm>[]>([]); protected forwarderOptions$ = new BehaviorSubject<Option<ForwarderNavValue>[]>([]);
/** tracks the currently selected credential type */ /** tracks the currently selected credential type */
protected algorithm$ = new ReplaySubject<AlgorithmInfo>(1); protected algorithm$ = new ReplaySubject<AlgorithmInfo>(1);

View File

@ -201,9 +201,9 @@ export class CredentialGeneratorService {
algorithms(category: CredentialCategory): AlgorithmInfo[]; algorithms(category: CredentialCategory): AlgorithmInfo[];
algorithms(category: CredentialCategory[]): AlgorithmInfo[]; algorithms(category: CredentialCategory[]): AlgorithmInfo[];
algorithms(category: CredentialCategory | CredentialCategory[]): AlgorithmInfo[] { algorithms(category: CredentialCategory | CredentialCategory[]): AlgorithmInfo[] {
const categories = Array.isArray(category) ? category : [category]; const categories: CredentialCategory[] = Array.isArray(category) ? category : [category];
const algorithms = categories const algorithms = categories
.flatMap((c) => CredentialCategories[c]) .flatMap((c) => CredentialCategories[c] as CredentialAlgorithm[])
.map((id) => this.algorithm(id)) .map((id) => this.algorithm(id))
.filter((info) => info !== null); .filter((info) => info !== null);

View File

@ -36,7 +36,7 @@ export const CredentialCategories = Object.freeze({
username: UsernameAlgorithms as Readonly<UsernameAlgorithm[]>, username: UsernameAlgorithms as Readonly<UsernameAlgorithm[]>,
/** Lists algorithms in the "email" credential category */ /** Lists algorithms in the "email" credential category */
email: EmailAlgorithms as Readonly<EmailAlgorithm[]>, email: EmailAlgorithms as Readonly<(EmailAlgorithm | ForwarderIntegration)[]>,
}); });
/** Returns true when the input algorithm is a password algorithm. */ /** Returns true when the input algorithm is a password algorithm. */
@ -55,7 +55,7 @@ export function isUsernameAlgorithm(
/** Returns true when the input algorithm is an email algorithm. */ /** Returns true when the input algorithm is an email algorithm. */
export function isEmailAlgorithm(algorithm: CredentialAlgorithm): algorithm is EmailAlgorithm { export function isEmailAlgorithm(algorithm: CredentialAlgorithm): algorithm is EmailAlgorithm {
return EmailAlgorithms.includes(algorithm as any); return EmailAlgorithms.includes(algorithm as any) || isForwarderIntegration(algorithm);
} }
/** A type of compound credential that may be generated by the credential generator. */ /** A type of compound credential that may be generated by the credential generator. */

View File

@ -1,4 +1,4 @@
import { CredentialAlgorithm, PasswordAlgorithm } from "./generator-type"; import { EmailAlgorithm, PasswordAlgorithm, UsernameAlgorithm } from "./generator-type";
export * from "./boundary"; export * from "./boundary";
export * from "./catchall-generator-options"; export * from "./catchall-generator-options";
@ -22,7 +22,7 @@ export * from "./word-options";
/** Provided for backwards compatibility only. /** Provided for backwards compatibility only.
* @deprecated Use one of the Algorithm types instead. * @deprecated Use one of the Algorithm types instead.
*/ */
export type GeneratorType = CredentialAlgorithm; export type GeneratorType = PasswordAlgorithm | UsernameAlgorithm | EmailAlgorithm;
/** Provided for backwards compatibility only. /** Provided for backwards compatibility only.
* @deprecated Use one of the Algorithm types instead. * @deprecated Use one of the Algorithm types instead.

View File

@ -2,8 +2,10 @@ import { NgModule } from "@angular/core";
import { safeProvider } from "@bitwarden/angular/platform/utils/safe-provider"; 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 { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { StateProvider } from "@bitwarden/common/platform/state"; import { StateProvider } from "@bitwarden/common/platform/state";
import { import {
createRandomizer, createRandomizer,
@ -32,7 +34,7 @@ const RANDOMIZER = new SafeInjectionToken<Randomizer>("Randomizer");
safeProvider({ safeProvider({
useClass: CredentialGeneratorService, useClass: CredentialGeneratorService,
provide: CredentialGeneratorService, provide: CredentialGeneratorService,
deps: [RANDOMIZER, StateProvider, PolicyService], deps: [RANDOMIZER, StateProvider, PolicyService, ApiService, I18nService],
}), }),
], ],
exports: [SendFormComponent], exports: [SendFormComponent],