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
class="tw-mb-4"
[selected]="(root$ | async).nav"
(selectedChange)="onRootChanged($event)"
(selectedChange)="onRootChanged({ nav: $event })"
attr.aria-label="{{ 'type' | i18n }}"
>
<bit-toggle *ngFor="let option of rootOptions$ | async" [value]="option.value">
@ -57,6 +57,12 @@
}}</bit-hint>
</bit-form-field>
</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
*ngIf="(algorithm$ | async)?.id === 'catchall'"
[userId]="userId$ | async"

View File

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

View File

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

View File

@ -2,9 +2,11 @@ import { Component, EventEmitter, Input, NgZone, OnDestroy, OnInit, Output } fro
import { FormBuilder } from "@angular/forms";
import {
BehaviorSubject,
concat,
distinctUntilChanged,
filter,
map,
of,
ReplaySubject,
Subject,
switchMap,
@ -20,6 +22,8 @@ import {
AlgorithmInfo,
CredentialAlgorithm,
CredentialGeneratorService,
EmailAlgorithm,
ForwarderIntegration,
GeneratedCredential,
Generators,
getForwarderConfiguration,
@ -27,8 +31,15 @@ import {
isForwarderIntegration,
isUsernameAlgorithm,
toCredentialGeneratorConfiguration,
UsernameAlgorithm,
} 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({
selector: "tools-username-generator",
@ -61,8 +72,12 @@ export class UsernameGeneratorComponent implements OnInit, OnDestroy {
readonly onGenerated = new EventEmitter<GeneratedCredential>();
/** Tracks the selected generation algorithm */
protected credential = this.formBuilder.group({
type: [null as CredentialAlgorithm],
protected username = this.formBuilder.group({
nav: [null as UsernameNavValue],
});
protected forwarder = this.formBuilder.group({
nav: [null as ForwarderNavValue],
});
async ngOnInit() {
@ -81,18 +96,22 @@ export class UsernameGeneratorComponent implements OnInit, OnDestroy {
this.generatorService
.algorithms$(["email", "username"], { userId$: this.userId$ })
.pipe(
map(
(algorithms) =>
[
this.toOptions(algorithms.filter((a) => !isForwarderIntegration(a.id))),
this.toOptions(algorithms.filter((a) => isForwarderIntegration(a.id))),
] as const,
),
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),
)
.subscribe(([type, forwarder]) => {
this.typeOptions$.next(type);
this.forwarderOptions$.next(forwarder);
.subscribe(([usernames, forwarders]) => {
this.typeOptions$.next(usernames);
this.forwarderOptions$.next(forwarders);
});
this.algorithm$
@ -125,18 +144,32 @@ export class UsernameGeneratorComponent implements OnInit, OnDestroy {
// assume the last-visible generator algorithm is the user's preferred one
const preferences = await this.generatorService.preferences({ singleUserId$: this.userId$ });
this.credential.valueChanges
this.username.valueChanges
.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),
takeUntil(this.destroyed),
)
.subscribe(([{ type }, preference]) => {
if (isEmailAlgorithm(type)) {
preference.email.algorithm = type;
.subscribe(([{ nav: algorithm }, preference]) => {
if (isEmailAlgorithm(algorithm)) {
preference.email.algorithm = algorithm;
preference.email.updated = new Date();
} else if (isUsernameAlgorithm(type)) {
preference.username.algorithm = type;
} else if (isUsernameAlgorithm(algorithm)) {
preference.username.algorithm = algorithm;
preference.username.updated = new Date();
} else {
return;
@ -147,14 +180,23 @@ export class UsernameGeneratorComponent implements OnInit, OnDestroy {
// populate the form with the user's preferences to kick off interactivity
preferences.pipe(takeUntil(this.destroyed)).subscribe(({ email, username }) => {
// this generator supports email & username; the last preference
// set by the user "wins"
const preference = email.updated > username.updated ? email.algorithm : username.algorithm;
// the last preference set by the user "wins"
const forwarderPref = isForwarderIntegration(email.algorithm) ? email : null;
const usernamePref = email.updated > username.updated ? email : username;
// break subscription loop
this.credential.setValue({ type: preference }, { emitEvent: false });
// inject drilldown flags
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
// template bindings refresh immediately
this.zone.run(() => {
@ -199,10 +241,10 @@ export class UsernameGeneratorComponent implements OnInit, OnDestroy {
}
/** 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. */
protected forwarderOptions$ = new BehaviorSubject<Option<CredentialAlgorithm>[]>([]);
protected forwarderOptions$ = new BehaviorSubject<Option<ForwarderNavValue>[]>([]);
/** tracks the currently selected credential type */
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 | CredentialCategory[]): AlgorithmInfo[] {
const categories = Array.isArray(category) ? category : [category];
const categories: CredentialCategory[] = Array.isArray(category) ? category : [category];
const algorithms = categories
.flatMap((c) => CredentialCategories[c])
.flatMap((c) => CredentialCategories[c] as CredentialAlgorithm[])
.map((id) => this.algorithm(id))
.filter((info) => info !== null);

View File

@ -36,7 +36,7 @@ export const CredentialCategories = Object.freeze({
username: UsernameAlgorithms as Readonly<UsernameAlgorithm[]>,
/** 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. */
@ -55,7 +55,7 @@ export function isUsernameAlgorithm(
/** Returns true when the input algorithm is an email algorithm. */
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. */

View File

@ -1,4 +1,4 @@
import { CredentialAlgorithm, PasswordAlgorithm } from "./generator-type";
import { EmailAlgorithm, PasswordAlgorithm, UsernameAlgorithm } from "./generator-type";
export * from "./boundary";
export * from "./catchall-generator-options";
@ -22,7 +22,7 @@ export * from "./word-options";
/** Provided for backwards compatibility only.
* @deprecated Use one of the Algorithm types instead.
*/
export type GeneratorType = CredentialAlgorithm;
export type GeneratorType = PasswordAlgorithm | UsernameAlgorithm | EmailAlgorithm;
/** Provided for backwards compatibility only.
* @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 { 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 { 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 {
createRandomizer,
@ -32,7 +34,7 @@ const RANDOMIZER = new SafeInjectionToken<Randomizer>("Randomizer");
safeProvider({
useClass: CredentialGeneratorService,
provide: CredentialGeneratorService,
deps: [RANDOMIZER, StateProvider, PolicyService],
deps: [RANDOMIZER, StateProvider, PolicyService, ApiService, I18nService],
}),
],
exports: [SendFormComponent],