diff --git a/libs/tools/generator/components/src/credential-generator.component.html b/libs/tools/generator/components/src/credential-generator.component.html index 91a7c12210..74229815c3 100644 --- a/libs/tools/generator/components/src/credential-generator.component.html +++ b/libs/tools/generator/components/src/credential-generator.component.html @@ -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 }}" > @@ -57,6 +57,12 @@ }} +
+ + {{ "forwarder" | i18n }} + + +
{ - 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[]; + usernameOptions.push({ value: FORWARDER, label: this.i18nService.t("forwarder") }); + + const forwarders = algorithms.filter((a) => isForwarderIntegration(a.id)); + const forwarderOptions = this.toOptions(forwarders) as Option[]; + 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[]>([]); - /** Lists the top-level credential types supported by the component. */ protected rootOptions$ = new BehaviorSubject[]>([]); + /** Lists the credential types of the username algorithm box. */ + protected usernameOptions$ = new BehaviorSubject[]>([]); + + /** Lists the credential types of the username algorithm box. */ + protected forwarderOptions$ = new BehaviorSubject[]>([]); + /** tracks the currently selected credential type */ protected algorithm$ = new ReplaySubject(1); diff --git a/libs/tools/generator/components/src/username-generator.component.html b/libs/tools/generator/components/src/username-generator.component.html index b0e235febf..a4afd04844 100644 --- a/libs/tools/generator/components/src/username-generator.component.html +++ b/libs/tools/generator/components/src/username-generator.component.html @@ -23,20 +23,19 @@
-
+ {{ "type" | i18n }} - + {{ credentialTypeHint$ | async }} +
+
{{ "forwarder" | i18n }} - - {{ - forwarderTypeHint$ | async - }} +
(); /** 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[]; + usernameOptions.push({ value: FORWARDER, label: this.i18nService.t("forwarder") }); + + const forwarders = algorithms.filter((a) => isForwarderIntegration(a.id)); + const forwarderOptions = this.toOptions(forwarders) as Option[]; + 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[]>([]); + protected typeOptions$ = new BehaviorSubject[]>([]); /** Lists the credential types supported by the component. */ - protected forwarderOptions$ = new BehaviorSubject[]>([]); + protected forwarderOptions$ = new BehaviorSubject[]>([]); /** tracks the currently selected credential type */ protected algorithm$ = new ReplaySubject(1); diff --git a/libs/tools/generator/core/src/services/credential-generator.service.ts b/libs/tools/generator/core/src/services/credential-generator.service.ts index b170cf4c97..c85027d6b6 100644 --- a/libs/tools/generator/core/src/services/credential-generator.service.ts +++ b/libs/tools/generator/core/src/services/credential-generator.service.ts @@ -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); diff --git a/libs/tools/generator/core/src/types/generator-type.ts b/libs/tools/generator/core/src/types/generator-type.ts index 2ea11577b6..ed1a649013 100644 --- a/libs/tools/generator/core/src/types/generator-type.ts +++ b/libs/tools/generator/core/src/types/generator-type.ts @@ -36,7 +36,7 @@ export const CredentialCategories = Object.freeze({ username: UsernameAlgorithms as Readonly, /** Lists algorithms in the "email" credential category */ - email: EmailAlgorithms as Readonly, + 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. */ diff --git a/libs/tools/generator/core/src/types/index.ts b/libs/tools/generator/core/src/types/index.ts index 884d976007..48272cbf60 100644 --- a/libs/tools/generator/core/src/types/index.ts +++ b/libs/tools/generator/core/src/types/index.ts @@ -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. diff --git a/libs/tools/send/send-ui/src/send-form/send-form.module.ts b/libs/tools/send/send-ui/src/send-form/send-form.module.ts index 99db65807a..9a8550bbd0 100644 --- a/libs/tools/send/send-ui/src/send-form/send-form.module.ts +++ b/libs/tools/send/send-ui/src/send-form/send-form.module.ts @@ -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"); safeProvider({ useClass: CredentialGeneratorService, provide: CredentialGeneratorService, - deps: [RANDOMIZER, StateProvider, PolicyService], + deps: [RANDOMIZER, StateProvider, PolicyService, ApiService, I18nService], }), ], exports: [SendFormComponent],