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:
parent
c48ddd8ecd
commit
32aa0429bb
@ -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"
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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. */
|
||||
|
@ -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.
|
||||
|
@ -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],
|
||||
|
Loading…
Reference in New Issue
Block a user