mirror of
https://github.com/bitwarden/browser
synced 2025-12-14 23:33:31 +00:00
[PM-16792] [PM-16822] Encapsulate encryptor and state provision within UserStateSubject (#13195)
This commit is contained in:
@@ -1,25 +1,31 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core";
|
||||
import {
|
||||
Component,
|
||||
EventEmitter,
|
||||
Input,
|
||||
OnChanges,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
Output,
|
||||
SimpleChanges,
|
||||
} from "@angular/core";
|
||||
import { FormBuilder } from "@angular/forms";
|
||||
import { BehaviorSubject, map, skip, Subject, takeUntil, withLatestFrom } from "rxjs";
|
||||
import { map, ReplaySubject, skip, Subject, takeUntil, withLatestFrom } from "rxjs";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { Account } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import {
|
||||
CatchallGenerationOptions,
|
||||
CredentialGeneratorService,
|
||||
Generators,
|
||||
} from "@bitwarden/generator-core";
|
||||
|
||||
import { completeOnAccountSwitch } from "./util";
|
||||
|
||||
/** Options group for catchall emails */
|
||||
@Component({
|
||||
selector: "tools-catchall-settings",
|
||||
templateUrl: "catchall-settings.component.html",
|
||||
})
|
||||
export class CatchallSettingsComponent implements OnInit, OnDestroy {
|
||||
export class CatchallSettingsComponent implements OnInit, OnDestroy, OnChanges {
|
||||
/** Instantiates the component
|
||||
* @param accountService queries user availability
|
||||
* @param generatorService settings and policy logic
|
||||
@@ -28,15 +34,14 @@ export class CatchallSettingsComponent implements OnInit, OnDestroy {
|
||||
constructor(
|
||||
private formBuilder: FormBuilder,
|
||||
private generatorService: CredentialGeneratorService,
|
||||
private accountService: AccountService,
|
||||
) {}
|
||||
|
||||
/** Binds the component to a specific user's settings.
|
||||
* When this input is not provided, the form binds to the active
|
||||
* user
|
||||
*/
|
||||
@Input()
|
||||
userId: UserId | null;
|
||||
@Input({ required: true })
|
||||
account: Account;
|
||||
|
||||
private account$ = new ReplaySubject<Account>(1);
|
||||
|
||||
/** Emits settings updates and completes if the settings become unavailable.
|
||||
* @remarks this does not emit the initial settings. If you would like
|
||||
@@ -51,9 +56,16 @@ export class CatchallSettingsComponent implements OnInit, OnDestroy {
|
||||
catchallDomain: [Generators.catchall.settings.initial.catchallDomain],
|
||||
});
|
||||
|
||||
async ngOnChanges(changes: SimpleChanges) {
|
||||
if ("account" in changes && changes.account) {
|
||||
this.account$.next(this.account);
|
||||
}
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
const singleUserId$ = this.singleUserId$();
|
||||
const settings = await this.generatorService.settings(Generators.catchall, { singleUserId$ });
|
||||
const settings = await this.generatorService.settings(Generators.catchall, {
|
||||
account$: this.account$,
|
||||
});
|
||||
|
||||
settings.pipe(takeUntil(this.destroyed$)).subscribe((s) => {
|
||||
this.settings.patchValue(s, { emitEvent: false });
|
||||
@@ -77,21 +89,9 @@ export class CatchallSettingsComponent implements OnInit, OnDestroy {
|
||||
this.saveSettings.next(site);
|
||||
}
|
||||
|
||||
private singleUserId$() {
|
||||
// FIXME: this branch should probably scan for the user and make sure
|
||||
// the account is unlocked
|
||||
if (this.userId) {
|
||||
return new BehaviorSubject(this.userId as UserId).asObservable();
|
||||
}
|
||||
|
||||
return this.accountService.activeAccount$.pipe(
|
||||
completeOnAccountSwitch(),
|
||||
takeUntil(this.destroyed$),
|
||||
);
|
||||
}
|
||||
|
||||
private readonly destroyed$ = new Subject<void>();
|
||||
ngOnDestroy(): void {
|
||||
this.account$.complete();
|
||||
this.destroyed$.next();
|
||||
this.destroyed$.complete();
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<span bitDialogTitle>{{ "generatorHistory" | i18n }}</span>
|
||||
<ng-container bitDialogContent>
|
||||
<bit-empty-credential-history *ngIf="!(hasHistory$ | async)" style="display: contents" />
|
||||
<bit-credential-generator-history *ngIf="hasHistory$ | async" />
|
||||
<bit-credential-generator-history [account]="account$ | async" *ngIf="hasHistory$ | async" />
|
||||
</ng-container>
|
||||
<ng-container bitDialogFooter>
|
||||
<button
|
||||
|
||||
@@ -1,12 +1,25 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component } from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { BehaviorSubject, distinctUntilChanged, firstValueFrom, map, switchMap } from "rxjs";
|
||||
import { Component, Input, OnChanges, SimpleChanges, OnInit, OnDestroy } from "@angular/core";
|
||||
import {
|
||||
BehaviorSubject,
|
||||
ReplaySubject,
|
||||
Subject,
|
||||
firstValueFrom,
|
||||
map,
|
||||
switchMap,
|
||||
takeUntil,
|
||||
} from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import {
|
||||
SemanticLogger,
|
||||
disabledSemanticLoggerProvider,
|
||||
ifEnabledSemanticLoggerProvider,
|
||||
} from "@bitwarden/common/tools/log";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { ButtonModule, DialogModule, DialogService } from "@bitwarden/components";
|
||||
import { GeneratorHistoryService } from "@bitwarden/generator-history";
|
||||
@@ -26,28 +39,66 @@ import { EmptyCredentialHistoryComponent } from "./empty-credential-history.comp
|
||||
EmptyCredentialHistoryComponent,
|
||||
],
|
||||
})
|
||||
export class CredentialGeneratorHistoryDialogComponent {
|
||||
export class CredentialGeneratorHistoryDialogComponent implements OnChanges, OnInit, OnDestroy {
|
||||
private readonly destroyed = new Subject<void>();
|
||||
protected readonly hasHistory$ = new BehaviorSubject<boolean>(false);
|
||||
protected readonly userId$ = new BehaviorSubject<UserId>(null);
|
||||
|
||||
constructor(
|
||||
private accountService: AccountService,
|
||||
private history: GeneratorHistoryService,
|
||||
private dialogService: DialogService,
|
||||
) {
|
||||
this.accountService.activeAccount$
|
||||
.pipe(
|
||||
takeUntilDestroyed(),
|
||||
map(({ id }) => id),
|
||||
distinctUntilChanged(),
|
||||
)
|
||||
.subscribe(this.userId$);
|
||||
private logService: LogService,
|
||||
) {}
|
||||
|
||||
this.userId$
|
||||
@Input()
|
||||
account: Account | null;
|
||||
|
||||
protected account$ = new ReplaySubject<Account>(1);
|
||||
|
||||
/** Send structured debug logs from the credential generator component
|
||||
* to the debugger console.
|
||||
*
|
||||
* @warning this may reveal sensitive information in plaintext.
|
||||
*/
|
||||
@Input()
|
||||
debug: boolean = false;
|
||||
|
||||
// this `log` initializer is overridden in `ngOnInit`
|
||||
private log: SemanticLogger = disabledSemanticLoggerProvider({});
|
||||
|
||||
ngOnChanges(changes: SimpleChanges) {
|
||||
const account = changes?.account;
|
||||
if (account?.previousValue?.id !== account?.currentValue?.id) {
|
||||
this.log.debug(
|
||||
{
|
||||
previousUserId: account?.previousValue?.id as UserId,
|
||||
currentUserId: account?.currentValue?.id as UserId,
|
||||
},
|
||||
"account input change detected",
|
||||
);
|
||||
this.account$.next(account.currentValue ?? this.account);
|
||||
}
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
this.log = ifEnabledSemanticLoggerProvider(this.debug, this.logService, {
|
||||
type: "CredentialGeneratorComponent",
|
||||
});
|
||||
|
||||
if (!this.account) {
|
||||
this.account = await firstValueFrom(this.accountService.activeAccount$);
|
||||
this.log.info(
|
||||
{ userId: this.account.id },
|
||||
"account not specified; using active account settings",
|
||||
);
|
||||
this.account$.next(this.account);
|
||||
}
|
||||
|
||||
this.account$
|
||||
.pipe(
|
||||
takeUntilDestroyed(),
|
||||
switchMap((id) => id && this.history.credentials$(id)),
|
||||
switchMap((account) => account.id && this.history.credentials$(account.id)),
|
||||
map((credentials) => credentials.length > 0),
|
||||
takeUntil(this.destroyed),
|
||||
)
|
||||
.subscribe(this.hasHistory$);
|
||||
}
|
||||
@@ -63,7 +114,14 @@ export class CredentialGeneratorHistoryDialogComponent {
|
||||
});
|
||||
|
||||
if (confirmed) {
|
||||
await this.history.clear(await firstValueFrom(this.userId$));
|
||||
await this.history.clear((await firstValueFrom(this.account$)).id);
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.destroyed.next();
|
||||
this.destroyed.complete();
|
||||
|
||||
this.log.debug("component destroyed");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,23 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component } from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { RouterLink } from "@angular/router";
|
||||
import { BehaviorSubject, distinctUntilChanged, map, switchMap } from "rxjs";
|
||||
import { Component, Input, OnChanges, SimpleChanges, OnInit, OnDestroy } from "@angular/core";
|
||||
import { BehaviorSubject, ReplaySubject, Subject, map, switchMap, takeUntil, tap } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { Account } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import {
|
||||
SemanticLogger,
|
||||
disabledSemanticLoggerProvider,
|
||||
ifEnabledSemanticLoggerProvider,
|
||||
} from "@bitwarden/common/tools/log";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import {
|
||||
ColorPasswordModule,
|
||||
IconButtonModule,
|
||||
ItemModule,
|
||||
NoItemsModule,
|
||||
SectionComponent,
|
||||
SectionHeaderComponent,
|
||||
} from "@bitwarden/components";
|
||||
import { CredentialGeneratorService } from "@bitwarden/generator-core";
|
||||
import { GeneratedCredential, GeneratorHistoryService } from "@bitwarden/generator-history";
|
||||
@@ -32,35 +34,61 @@ import { GeneratorModule } from "./generator.module";
|
||||
IconButtonModule,
|
||||
NoItemsModule,
|
||||
JslibModule,
|
||||
RouterLink,
|
||||
ItemModule,
|
||||
SectionComponent,
|
||||
SectionHeaderComponent,
|
||||
GeneratorModule,
|
||||
],
|
||||
})
|
||||
export class CredentialGeneratorHistoryComponent {
|
||||
protected readonly userId$ = new BehaviorSubject<UserId>(null);
|
||||
export class CredentialGeneratorHistoryComponent implements OnChanges, OnInit, OnDestroy {
|
||||
private readonly destroyed = new Subject<void>();
|
||||
protected readonly credentials$ = new BehaviorSubject<GeneratedCredential[]>([]);
|
||||
|
||||
constructor(
|
||||
private accountService: AccountService,
|
||||
private generatorService: CredentialGeneratorService,
|
||||
private history: GeneratorHistoryService,
|
||||
) {
|
||||
this.accountService.activeAccount$
|
||||
.pipe(
|
||||
takeUntilDestroyed(),
|
||||
map(({ id }) => id),
|
||||
distinctUntilChanged(),
|
||||
)
|
||||
.subscribe(this.userId$);
|
||||
private logService: LogService,
|
||||
) {}
|
||||
|
||||
this.userId$
|
||||
@Input({ required: true })
|
||||
account: Account;
|
||||
|
||||
protected account$ = new ReplaySubject<Account>(1);
|
||||
|
||||
/** Send structured debug logs from the credential generator component
|
||||
* to the debugger console.
|
||||
*
|
||||
* @warning this may reveal sensitive information in plaintext.
|
||||
*/
|
||||
@Input()
|
||||
debug: boolean = false;
|
||||
|
||||
// this `log` initializer is overridden in `ngOnInit`
|
||||
private log: SemanticLogger = disabledSemanticLoggerProvider({});
|
||||
|
||||
async ngOnChanges(changes: SimpleChanges) {
|
||||
const account = changes?.account;
|
||||
if (account?.previousValue?.id !== account?.currentValue?.id) {
|
||||
this.log.debug(
|
||||
{
|
||||
previousUserId: account?.previousValue?.id as UserId,
|
||||
currentUserId: account?.currentValue?.id as UserId,
|
||||
},
|
||||
"account input change detected",
|
||||
);
|
||||
this.account$.next(account.currentValue ?? this.account);
|
||||
}
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.log = ifEnabledSemanticLoggerProvider(this.debug, this.logService, {
|
||||
type: "CredentialGeneratorComponent",
|
||||
});
|
||||
|
||||
this.account$
|
||||
.pipe(
|
||||
takeUntilDestroyed(),
|
||||
switchMap((id) => id && this.history.credentials$(id)),
|
||||
tap((account) => this.log.info({ accountId: account.id }, "loading credential history")),
|
||||
switchMap((account) => this.history.credentials$(account.id)),
|
||||
map((credentials) => credentials.filter((c) => (c.credential ?? "") !== "")),
|
||||
takeUntil(this.destroyed),
|
||||
)
|
||||
.subscribe(this.credentials$);
|
||||
}
|
||||
@@ -74,4 +102,11 @@ export class CredentialGeneratorHistoryComponent {
|
||||
const info = this.generatorService.algorithm(credential.category);
|
||||
return info.credentialType;
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.destroyed.next();
|
||||
this.destroyed.complete();
|
||||
|
||||
this.log.debug("component destroyed");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,13 +41,13 @@
|
||||
<tools-password-settings
|
||||
class="tw-mt-6"
|
||||
*ngIf="(showAlgorithm$ | async)?.id === 'password'"
|
||||
[userId]="userId$ | async"
|
||||
[account]="account$ | async"
|
||||
(onUpdated)="generate('password settings')"
|
||||
/>
|
||||
<tools-passphrase-settings
|
||||
class="tw-mt-6"
|
||||
*ngIf="(showAlgorithm$ | async)?.id === 'passphrase'"
|
||||
[userId]="userId$ | async"
|
||||
[account]="account$ | async"
|
||||
(onUpdated)="generate('passphrase settings')"
|
||||
/>
|
||||
<bit-section *ngIf="(category$ | async) !== 'password'">
|
||||
@@ -83,22 +83,22 @@
|
||||
</form>
|
||||
<tools-catchall-settings
|
||||
*ngIf="(showAlgorithm$ | async)?.id === 'catchall'"
|
||||
[userId]="userId$ | async"
|
||||
[account]="account$ | async"
|
||||
(onUpdated)="generate('catchall settings')"
|
||||
/>
|
||||
<tools-forwarder-settings
|
||||
*ngIf="!!(forwarderId$ | async)"
|
||||
[account]="account$ | async"
|
||||
[forwarder]="forwarderId$ | async"
|
||||
[userId]="this.userId$ | async"
|
||||
/>
|
||||
<tools-subaddress-settings
|
||||
*ngIf="(showAlgorithm$ | async)?.id === 'subaddress'"
|
||||
[userId]="userId$ | async"
|
||||
[account]="account$ | async"
|
||||
(onUpdated)="generate('subaddress settings')"
|
||||
/>
|
||||
<tools-username-settings
|
||||
*ngIf="(showAlgorithm$ | async)?.id === 'username'"
|
||||
[userId]="userId$ | async"
|
||||
[account]="account$ | async"
|
||||
(onUpdated)="generate('username settings')"
|
||||
/>
|
||||
</bit-card>
|
||||
|
||||
@@ -1,7 +1,17 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { LiveAnnouncer } from "@angular/cdk/a11y";
|
||||
import { Component, EventEmitter, Input, NgZone, OnDestroy, OnInit, Output } from "@angular/core";
|
||||
import {
|
||||
Component,
|
||||
EventEmitter,
|
||||
Input,
|
||||
NgZone,
|
||||
OnChanges,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
Output,
|
||||
SimpleChanges,
|
||||
} from "@angular/core";
|
||||
import { FormBuilder } from "@angular/forms";
|
||||
import {
|
||||
BehaviorSubject,
|
||||
@@ -10,6 +20,7 @@ import {
|
||||
combineLatestWith,
|
||||
distinctUntilChanged,
|
||||
filter,
|
||||
firstValueFrom,
|
||||
map,
|
||||
ReplaySubject,
|
||||
Subject,
|
||||
@@ -18,10 +29,15 @@ import {
|
||||
withLatestFrom,
|
||||
} from "rxjs";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { IntegrationId } from "@bitwarden/common/tools/integration";
|
||||
import {
|
||||
SemanticLogger,
|
||||
disabledSemanticLoggerProvider,
|
||||
ifEnabledSemanticLoggerProvider,
|
||||
} from "@bitwarden/common/tools/log";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { ToastService, Option } from "@bitwarden/components";
|
||||
import {
|
||||
@@ -52,7 +68,9 @@ const NONE_SELECTED = "none";
|
||||
selector: "tools-credential-generator",
|
||||
templateUrl: "credential-generator.component.html",
|
||||
})
|
||||
export class CredentialGeneratorComponent implements OnInit, OnDestroy {
|
||||
export class CredentialGeneratorComponent implements OnInit, OnChanges, OnDestroy {
|
||||
private readonly destroyed = new Subject<void>();
|
||||
|
||||
constructor(
|
||||
private generatorService: CredentialGeneratorService,
|
||||
private generatorHistoryService: GeneratorHistoryService,
|
||||
@@ -69,7 +87,34 @@ export class CredentialGeneratorComponent implements OnInit, OnDestroy {
|
||||
* the form binds to the active user
|
||||
*/
|
||||
@Input()
|
||||
userId: UserId | null;
|
||||
account: Account | null;
|
||||
|
||||
/** Send structured debug logs from the credential generator component
|
||||
* to the debugger console.
|
||||
*
|
||||
* @warning this may reveal sensitive information in plaintext.
|
||||
*/
|
||||
@Input()
|
||||
debug: boolean = false;
|
||||
|
||||
// this `log` initializer is overridden in `ngOnInit`
|
||||
private log: SemanticLogger = disabledSemanticLoggerProvider({});
|
||||
|
||||
protected account$ = new ReplaySubject<Account>(1);
|
||||
|
||||
async ngOnChanges(changes: SimpleChanges) {
|
||||
const account = changes?.account;
|
||||
if (account?.previousValue?.id !== account?.currentValue?.id) {
|
||||
this.log.debug(
|
||||
{
|
||||
previousUserId: account?.previousValue?.id as UserId,
|
||||
currentUserId: account?.currentValue?.id as UserId,
|
||||
},
|
||||
"account input change detected",
|
||||
);
|
||||
this.account$.next(account.currentValue ?? this.account);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The website associated with the credential generation request.
|
||||
@@ -103,20 +148,21 @@ export class CredentialGeneratorComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
|
||||
async ngOnInit() {
|
||||
if (this.userId) {
|
||||
this.userId$.next(this.userId);
|
||||
} else {
|
||||
this.accountService.activeAccount$
|
||||
.pipe(
|
||||
map((acct) => acct.id),
|
||||
distinctUntilChanged(),
|
||||
takeUntil(this.destroyed),
|
||||
)
|
||||
.subscribe(this.userId$);
|
||||
this.log = ifEnabledSemanticLoggerProvider(this.debug, this.logService, {
|
||||
type: "CredentialGeneratorComponent",
|
||||
});
|
||||
|
||||
if (!this.account) {
|
||||
this.account = await firstValueFrom(this.accountService.activeAccount$);
|
||||
this.log.info(
|
||||
{ userId: this.account.id },
|
||||
"account not specified; using active account settings",
|
||||
);
|
||||
this.account$.next(this.account);
|
||||
}
|
||||
|
||||
this.generatorService
|
||||
.algorithms$(["email", "username"], { userId$: this.userId$ })
|
||||
.algorithms$(["email", "username"], { account$: this.account$ })
|
||||
.pipe(
|
||||
map((algorithms) => {
|
||||
const usernames = algorithms.filter((a) => !isForwarderIntegration(a.id));
|
||||
@@ -137,7 +183,7 @@ export class CredentialGeneratorComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
|
||||
this.generatorService
|
||||
.algorithms$("password", { userId$: this.userId$ })
|
||||
.algorithms$("password", { account$: this.account$ })
|
||||
.pipe(
|
||||
map((algorithms) => {
|
||||
const options = this.toOptions(algorithms);
|
||||
@@ -194,12 +240,14 @@ export class CredentialGeneratorComponent implements OnInit, OnDestroy {
|
||||
// continue with origin stream
|
||||
return generator;
|
||||
}),
|
||||
withLatestFrom(this.userId$, this.algorithm$),
|
||||
withLatestFrom(this.account$, this.algorithm$),
|
||||
takeUntil(this.destroyed),
|
||||
)
|
||||
.subscribe(([generated, userId, algorithm]) => {
|
||||
.subscribe(([generated, account, algorithm]) => {
|
||||
this.log.debug({ source: generated.source }, "credential generated");
|
||||
|
||||
this.generatorHistoryService
|
||||
.track(userId, generated.credential, generated.category, generated.generationDate)
|
||||
.track(account.id, generated.credential, generated.category, generated.generationDate)
|
||||
.catch((e: unknown) => {
|
||||
this.logService.error(e);
|
||||
});
|
||||
@@ -274,6 +322,8 @@ export class CredentialGeneratorComponent implements OnInit, OnDestroy {
|
||||
takeUntil(this.destroyed),
|
||||
)
|
||||
.subscribe(([showForwarder, forwarderId]) => {
|
||||
this.log.debug({ forwarderId, showForwarder }, "forwarder visibility updated");
|
||||
|
||||
// update subjects within the angular zone so that the
|
||||
// template bindings refresh immediately
|
||||
this.zone.run(() => {
|
||||
@@ -297,6 +347,8 @@ export class CredentialGeneratorComponent implements OnInit, OnDestroy {
|
||||
takeUntil(this.destroyed),
|
||||
)
|
||||
.subscribe((algorithm) => {
|
||||
this.log.debug(algorithm, "algorithm selected");
|
||||
|
||||
// update subjects within the angular zone so that the
|
||||
// template bindings refresh immediately
|
||||
this.zone.run(() => {
|
||||
@@ -305,7 +357,7 @@ export class CredentialGeneratorComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
|
||||
// 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({ account$: this.account$ });
|
||||
this.algorithm$
|
||||
.pipe(
|
||||
filter((algorithm) => !!algorithm),
|
||||
@@ -313,19 +365,21 @@ export class CredentialGeneratorComponent implements OnInit, OnDestroy {
|
||||
takeUntil(this.destroyed),
|
||||
)
|
||||
.subscribe(([algorithm, preference]) => {
|
||||
function setPreference(category: CredentialCategory) {
|
||||
function setPreference(category: CredentialCategory, log: SemanticLogger) {
|
||||
const p = preference[category];
|
||||
p.algorithm = algorithm.id;
|
||||
p.updated = new Date();
|
||||
|
||||
log.info({ algorithm, category }, "algorithm preferences updated");
|
||||
}
|
||||
|
||||
// `is*Algorithm` decides `algorithm`'s type, which flows into `setPreference`
|
||||
if (isEmailAlgorithm(algorithm.id)) {
|
||||
setPreference("email");
|
||||
setPreference("email", this.log);
|
||||
} else if (isUsernameAlgorithm(algorithm.id)) {
|
||||
setPreference("username");
|
||||
setPreference("username", this.log);
|
||||
} else if (isPasswordAlgorithm(algorithm.id)) {
|
||||
setPreference("password");
|
||||
setPreference("password", this.log);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
@@ -396,25 +450,33 @@ export class CredentialGeneratorComponent implements OnInit, OnDestroy {
|
||||
this.algorithm$.pipe(takeUntil(this.destroyed)).subscribe((a) => {
|
||||
this.zone.run(() => {
|
||||
if (!a || a.onlyOnRequest) {
|
||||
this.log.debug("autogeneration disabled; clearing generated credential");
|
||||
this.generatedCredential$.next(null);
|
||||
} else {
|
||||
this.generate("autogenerate").catch((e: unknown) => this.logService.error(e));
|
||||
this.log.debug("autogeneration enabled");
|
||||
this.generate("autogenerate").catch((e: unknown) => {
|
||||
this.log.error(e as object, "a failure occurred during autogeneration");
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
this.log.debug("component initialized");
|
||||
}
|
||||
|
||||
private announce(message: string) {
|
||||
this.ariaLive.announce(message).catch((e) => this.logService.error(e));
|
||||
}
|
||||
|
||||
private typeToGenerator$(type: CredentialAlgorithm) {
|
||||
private typeToGenerator$(algorithm: CredentialAlgorithm) {
|
||||
const dependencies = {
|
||||
on$: this.generate$,
|
||||
userId$: this.userId$,
|
||||
account$: this.account$,
|
||||
};
|
||||
|
||||
switch (type) {
|
||||
this.log.debug({ algorithm }, "constructing generation stream");
|
||||
|
||||
switch (algorithm) {
|
||||
case "catchall":
|
||||
return this.generatorService.generate$(Generators.catchall, dependencies);
|
||||
|
||||
@@ -431,14 +493,14 @@ export class CredentialGeneratorComponent implements OnInit, OnDestroy {
|
||||
return this.generatorService.generate$(Generators.passphrase, dependencies);
|
||||
}
|
||||
|
||||
if (isForwarderIntegration(type)) {
|
||||
const forwarder = getForwarderConfiguration(type.forwarder);
|
||||
if (isForwarderIntegration(algorithm)) {
|
||||
const forwarder = getForwarderConfiguration(algorithm.forwarder);
|
||||
const configuration = toCredentialGeneratorConfiguration(forwarder);
|
||||
const generator = this.generatorService.generate$(configuration, dependencies);
|
||||
return generator;
|
||||
}
|
||||
|
||||
throw new Error(`Invalid generator type: "${type}"`);
|
||||
this.log.panic({ algorithm }, `Invalid generator type: "${algorithm}"`);
|
||||
}
|
||||
|
||||
/** Lists the top-level credential types supported by the component.
|
||||
@@ -506,9 +568,6 @@ export class CredentialGeneratorComponent implements OnInit, OnDestroy {
|
||||
map((generated) => generated?.credential ?? "-"),
|
||||
);
|
||||
|
||||
/** Emits when the userId changes */
|
||||
protected readonly userId$ = new BehaviorSubject<UserId>(null);
|
||||
|
||||
/** Identifies generator requests that were requested by the user */
|
||||
protected readonly USER_REQUEST = "user request";
|
||||
|
||||
@@ -516,11 +575,13 @@ export class CredentialGeneratorComponent implements OnInit, OnDestroy {
|
||||
private readonly generate$ = new Subject<GenerateRequest>();
|
||||
|
||||
/** Request a new value from the generator
|
||||
* @param requestor a label used to trace generation request
|
||||
* @param source a label used to trace generation request
|
||||
* origin in the debugger.
|
||||
*/
|
||||
protected async generate(requestor: string) {
|
||||
this.generate$.next({ source: requestor, website: this.website });
|
||||
protected async generate(source: string) {
|
||||
const request = { source, website: this.website };
|
||||
this.log.debug(request, "generation requested");
|
||||
this.generate$.next(request);
|
||||
}
|
||||
|
||||
private toOptions(algorithms: AlgorithmInfo[]) {
|
||||
@@ -532,7 +593,6 @@ export class CredentialGeneratorComponent implements OnInit, OnDestroy {
|
||||
return options;
|
||||
}
|
||||
|
||||
private readonly destroyed = new Subject<void>();
|
||||
ngOnDestroy() {
|
||||
this.destroyed.next();
|
||||
this.destroyed.complete();
|
||||
@@ -543,5 +603,7 @@ export class CredentialGeneratorComponent implements OnInit, OnDestroy {
|
||||
|
||||
// finalize component bindings
|
||||
this.onGenerated.complete();
|
||||
|
||||
this.log.debug("component destroyed");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,21 +11,10 @@ import {
|
||||
SimpleChanges,
|
||||
} from "@angular/core";
|
||||
import { FormBuilder } from "@angular/forms";
|
||||
import {
|
||||
BehaviorSubject,
|
||||
concatMap,
|
||||
map,
|
||||
ReplaySubject,
|
||||
skip,
|
||||
Subject,
|
||||
switchAll,
|
||||
takeUntil,
|
||||
withLatestFrom,
|
||||
} from "rxjs";
|
||||
import { map, ReplaySubject, skip, Subject, switchAll, takeUntil, withLatestFrom } from "rxjs";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { Account } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { IntegrationId } from "@bitwarden/common/tools/integration";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import {
|
||||
CredentialGeneratorConfiguration,
|
||||
CredentialGeneratorService,
|
||||
@@ -34,8 +23,6 @@ import {
|
||||
toCredentialGeneratorConfiguration,
|
||||
} from "@bitwarden/generator-core";
|
||||
|
||||
import { completeOnAccountSwitch } from "./util";
|
||||
|
||||
const Controls = Object.freeze({
|
||||
domain: "domain",
|
||||
token: "token",
|
||||
@@ -56,15 +43,14 @@ export class ForwarderSettingsComponent implements OnInit, OnChanges, OnDestroy
|
||||
constructor(
|
||||
private formBuilder: FormBuilder,
|
||||
private generatorService: CredentialGeneratorService,
|
||||
private accountService: AccountService,
|
||||
) {}
|
||||
|
||||
/** Binds the component to a specific user's settings.
|
||||
* When this input is not provided, the form binds to the active
|
||||
* user
|
||||
*/
|
||||
@Input()
|
||||
userId: UserId | null;
|
||||
@Input({ required: true })
|
||||
account: Account;
|
||||
|
||||
protected account$ = new ReplaySubject<Account>(1);
|
||||
|
||||
@Input({ required: true })
|
||||
forwarder: IntegrationId;
|
||||
@@ -87,8 +73,6 @@ export class ForwarderSettingsComponent implements OnInit, OnChanges, OnDestroy
|
||||
private forwarderId$ = new ReplaySubject<IntegrationId>(1);
|
||||
|
||||
async ngOnInit() {
|
||||
const singleUserId$ = this.singleUserId$();
|
||||
|
||||
const forwarder$ = new ReplaySubject<CredentialGeneratorConfiguration<any, NoPolicy>>(1);
|
||||
this.forwarderId$
|
||||
.pipe(
|
||||
@@ -108,12 +92,12 @@ export class ForwarderSettingsComponent implements OnInit, OnChanges, OnDestroy
|
||||
forwarder$.next(forwarder);
|
||||
});
|
||||
|
||||
const settings$$ = forwarder$.pipe(
|
||||
concatMap((forwarder) => this.generatorService.settings(forwarder, { singleUserId$ })),
|
||||
const settings$ = forwarder$.pipe(
|
||||
map((forwarder) => this.generatorService.settings(forwarder, { account$: this.account$ })),
|
||||
);
|
||||
|
||||
// bind settings to the reactive form
|
||||
settings$$.pipe(switchAll(), takeUntil(this.destroyed$)).subscribe((settings) => {
|
||||
settings$.pipe(switchAll(), takeUntil(this.destroyed$)).subscribe((settings) => {
|
||||
// skips reactive event emissions to break a subscription cycle
|
||||
this.settings.patchValue(settings as any, { emitEvent: false });
|
||||
});
|
||||
@@ -131,7 +115,7 @@ export class ForwarderSettingsComponent implements OnInit, OnChanges, OnDestroy
|
||||
});
|
||||
|
||||
// the first emission is the current value; subsequent emissions are updates
|
||||
settings$$
|
||||
settings$
|
||||
.pipe(
|
||||
map((settings$) => settings$.pipe(skip(1))),
|
||||
switchAll(),
|
||||
@@ -141,7 +125,7 @@ export class ForwarderSettingsComponent implements OnInit, OnChanges, OnDestroy
|
||||
|
||||
// now that outputs are set up, connect inputs
|
||||
this.saveSettings
|
||||
.pipe(withLatestFrom(this.settings.valueChanges, settings$$), takeUntil(this.destroyed$))
|
||||
.pipe(withLatestFrom(this.settings.valueChanges, settings$), takeUntil(this.destroyed$))
|
||||
.subscribe(([, value, settings]) => {
|
||||
settings.next(value);
|
||||
});
|
||||
@@ -152,30 +136,21 @@ export class ForwarderSettingsComponent implements OnInit, OnChanges, OnDestroy
|
||||
this.saveSettings.next(site);
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
async ngOnChanges(changes: SimpleChanges) {
|
||||
this.refresh$.complete();
|
||||
if ("forwarder" in changes) {
|
||||
this.forwarderId$.next(this.forwarder);
|
||||
}
|
||||
|
||||
if ("account" in changes) {
|
||||
this.account$.next(this.account);
|
||||
}
|
||||
}
|
||||
|
||||
protected displayDomain: boolean;
|
||||
protected displayToken: boolean;
|
||||
protected displayBaseUrl: boolean;
|
||||
|
||||
private singleUserId$() {
|
||||
// FIXME: this branch should probably scan for the user and make sure
|
||||
// the account is unlocked
|
||||
if (this.userId) {
|
||||
return new BehaviorSubject(this.userId as UserId).asObservable();
|
||||
}
|
||||
|
||||
return this.accountService.activeAccount$.pipe(
|
||||
completeOnAccountSwitch(),
|
||||
takeUntil(this.destroyed$),
|
||||
);
|
||||
}
|
||||
|
||||
private readonly refresh$ = new Subject<void>();
|
||||
|
||||
private readonly destroyed$ = new Subject<void>();
|
||||
|
||||
@@ -5,12 +5,13 @@ 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 { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { StateProvider } from "@bitwarden/common/platform/state";
|
||||
import { KeyServiceLegacyEncryptorProvider } from "@bitwarden/common/tools/cryptography/key-service-legacy-encryptor-provider";
|
||||
import { LegacyEncryptorProvider } from "@bitwarden/common/tools/cryptography/legacy-encryptor-provider";
|
||||
import { disabledSemanticLoggerProvider } from "@bitwarden/common/tools/log";
|
||||
import { UserStateSubjectDependencyProvider } from "@bitwarden/common/tools/state/user-state-subject-dependency-provider";
|
||||
import {
|
||||
createRandomizer,
|
||||
CredentialGeneratorService,
|
||||
@@ -34,17 +35,25 @@ export const RANDOMIZER = new SafeInjectionToken<Randomizer>("Randomizer");
|
||||
useClass: KeyServiceLegacyEncryptorProvider,
|
||||
deps: [EncryptService, KeyService],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: UserStateSubjectDependencyProvider,
|
||||
useFactory: (encryptor: LegacyEncryptorProvider, state: StateProvider) =>
|
||||
Object.freeze({
|
||||
encryptor,
|
||||
state,
|
||||
log: disabledSemanticLoggerProvider,
|
||||
}),
|
||||
deps: [LegacyEncryptorProvider, StateProvider],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: CredentialGeneratorService,
|
||||
useClass: CredentialGeneratorService,
|
||||
deps: [
|
||||
RANDOMIZER,
|
||||
StateProvider,
|
||||
PolicyService,
|
||||
ApiService,
|
||||
I18nService,
|
||||
LegacyEncryptorProvider,
|
||||
AccountService,
|
||||
UserStateSubjectDependencyProvider,
|
||||
],
|
||||
}),
|
||||
],
|
||||
|
||||
@@ -1,29 +1,27 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { coerceBooleanProperty } from "@angular/cdk/coercion";
|
||||
import { OnInit, Input, Output, EventEmitter, Component, OnDestroy } from "@angular/core";
|
||||
import { FormBuilder } from "@angular/forms";
|
||||
import {
|
||||
BehaviorSubject,
|
||||
skip,
|
||||
takeUntil,
|
||||
Subject,
|
||||
map,
|
||||
withLatestFrom,
|
||||
ReplaySubject,
|
||||
} from "rxjs";
|
||||
OnInit,
|
||||
Input,
|
||||
Output,
|
||||
EventEmitter,
|
||||
Component,
|
||||
OnDestroy,
|
||||
SimpleChanges,
|
||||
OnChanges,
|
||||
} from "@angular/core";
|
||||
import { FormBuilder } from "@angular/forms";
|
||||
import { skip, takeUntil, Subject, map, withLatestFrom, ReplaySubject } from "rxjs";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { Account } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import {
|
||||
Generators,
|
||||
CredentialGeneratorService,
|
||||
PassphraseGenerationOptions,
|
||||
} from "@bitwarden/generator-core";
|
||||
|
||||
import { completeOnAccountSwitch } from "./util";
|
||||
|
||||
const Controls = Object.freeze({
|
||||
numWords: "numWords",
|
||||
includeNumber: "includeNumber",
|
||||
@@ -36,9 +34,8 @@ const Controls = Object.freeze({
|
||||
selector: "tools-passphrase-settings",
|
||||
templateUrl: "passphrase-settings.component.html",
|
||||
})
|
||||
export class PassphraseSettingsComponent implements OnInit, OnDestroy {
|
||||
export class PassphraseSettingsComponent implements OnInit, OnChanges, OnDestroy {
|
||||
/** Instantiates the component
|
||||
* @param accountService queries user availability
|
||||
* @param generatorService settings and policy logic
|
||||
* @param i18nService localize hints
|
||||
* @param formBuilder reactive form controls
|
||||
@@ -47,15 +44,20 @@ export class PassphraseSettingsComponent implements OnInit, OnDestroy {
|
||||
private formBuilder: FormBuilder,
|
||||
private generatorService: CredentialGeneratorService,
|
||||
private i18nService: I18nService,
|
||||
private accountService: AccountService,
|
||||
) {}
|
||||
|
||||
/** Binds the component to a specific user's settings.
|
||||
* When this input is not provided, the form binds to the active
|
||||
* user
|
||||
*/
|
||||
@Input()
|
||||
userId: UserId | null;
|
||||
@Input({ required: true })
|
||||
account: Account;
|
||||
|
||||
protected account$ = new ReplaySubject<Account>(1);
|
||||
|
||||
async ngOnChanges(changes: SimpleChanges) {
|
||||
if ("account" in changes && changes.account) {
|
||||
this.account$.next(this.account);
|
||||
}
|
||||
}
|
||||
|
||||
/** When `true`, an options header is displayed by the component. Otherwise, the header is hidden. */
|
||||
@Input()
|
||||
@@ -80,8 +82,9 @@ export class PassphraseSettingsComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
|
||||
async ngOnInit() {
|
||||
const singleUserId$ = this.singleUserId$();
|
||||
const settings = await this.generatorService.settings(Generators.passphrase, { singleUserId$ });
|
||||
const settings = await this.generatorService.settings(Generators.passphrase, {
|
||||
account$: this.account$,
|
||||
});
|
||||
|
||||
// skips reactive event emissions to break a subscription cycle
|
||||
settings.withConstraints$
|
||||
@@ -108,7 +111,7 @@ export class PassphraseSettingsComponent implements OnInit, OnDestroy {
|
||||
|
||||
// explain policy & disable policy-overridden fields
|
||||
this.generatorService
|
||||
.policy$(Generators.passphrase, { userId$: singleUserId$ })
|
||||
.policy$(Generators.passphrase, { account$: this.account$ })
|
||||
.pipe(takeUntil(this.destroyed$))
|
||||
.subscribe(({ constraints }) => {
|
||||
this.wordSeparatorMaxLength = constraints.wordSeparator.maxLength;
|
||||
@@ -152,19 +155,6 @@ export class PassphraseSettingsComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
private singleUserId$() {
|
||||
// FIXME: this branch should probably scan for the user and make sure
|
||||
// the account is unlocked
|
||||
if (this.userId) {
|
||||
return new BehaviorSubject(this.userId as UserId).asObservable();
|
||||
}
|
||||
|
||||
return this.accountService.activeAccount$.pipe(
|
||||
completeOnAccountSwitch(),
|
||||
takeUntil(this.destroyed$),
|
||||
);
|
||||
}
|
||||
|
||||
private readonly destroyed$ = new Subject<void>();
|
||||
ngOnDestroy(): void {
|
||||
this.destroyed$.next();
|
||||
|
||||
@@ -39,14 +39,14 @@
|
||||
<tools-password-settings
|
||||
class="tw-mt-6"
|
||||
*ngIf="(algorithm$ | async)?.id === 'password'"
|
||||
[userId]="this.userId$ | async"
|
||||
[account]="account$ | async"
|
||||
[disableMargin]="disableMargin"
|
||||
(onUpdated)="generate('password settings')"
|
||||
/>
|
||||
<tools-passphrase-settings
|
||||
class="tw-mt-6"
|
||||
*ngIf="(algorithm$ | async)?.id === 'passphrase'"
|
||||
[userId]="this.userId$ | async"
|
||||
[account]="account$ | async"
|
||||
(onUpdated)="generate('passphrase settings')"
|
||||
[disableMargin]="disableMargin"
|
||||
/>
|
||||
|
||||
@@ -2,12 +2,23 @@
|
||||
// @ts-strict-ignore
|
||||
import { LiveAnnouncer } from "@angular/cdk/a11y";
|
||||
import { coerceBooleanProperty } from "@angular/cdk/coercion";
|
||||
import { Component, EventEmitter, Input, NgZone, OnDestroy, OnInit, Output } from "@angular/core";
|
||||
import {
|
||||
Component,
|
||||
EventEmitter,
|
||||
Input,
|
||||
NgZone,
|
||||
OnChanges,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
Output,
|
||||
SimpleChanges,
|
||||
} from "@angular/core";
|
||||
import {
|
||||
BehaviorSubject,
|
||||
catchError,
|
||||
distinctUntilChanged,
|
||||
filter,
|
||||
firstValueFrom,
|
||||
map,
|
||||
ReplaySubject,
|
||||
Subject,
|
||||
@@ -16,8 +27,13 @@ import {
|
||||
withLatestFrom,
|
||||
} from "rxjs";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import {
|
||||
SemanticLogger,
|
||||
disabledSemanticLoggerProvider,
|
||||
ifEnabledSemanticLoggerProvider,
|
||||
} from "@bitwarden/common/tools/log";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { ToastService, Option } from "@bitwarden/components";
|
||||
import {
|
||||
@@ -29,6 +45,7 @@ import {
|
||||
AlgorithmInfo,
|
||||
isSameAlgorithm,
|
||||
GenerateRequest,
|
||||
CredentialCategories,
|
||||
} from "@bitwarden/generator-core";
|
||||
import { GeneratorHistoryService } from "@bitwarden/generator-history";
|
||||
|
||||
@@ -37,7 +54,7 @@ import { GeneratorHistoryService } from "@bitwarden/generator-history";
|
||||
selector: "tools-password-generator",
|
||||
templateUrl: "password-generator.component.html",
|
||||
})
|
||||
export class PasswordGeneratorComponent implements OnInit, OnDestroy {
|
||||
export class PasswordGeneratorComponent implements OnInit, OnChanges, OnDestroy {
|
||||
constructor(
|
||||
private generatorService: CredentialGeneratorService,
|
||||
private generatorHistoryService: GeneratorHistoryService,
|
||||
@@ -48,12 +65,38 @@ export class PasswordGeneratorComponent implements OnInit, OnDestroy {
|
||||
private ariaLive: LiveAnnouncer,
|
||||
) {}
|
||||
|
||||
/** Binds the component to a specific user's settings.
|
||||
* When this input is not provided, the form binds to the active
|
||||
* user
|
||||
/** Binds the component to a specific user's settings. When this input is not provided,
|
||||
* the form binds to the active user
|
||||
*/
|
||||
@Input()
|
||||
userId: UserId | null;
|
||||
account: Account | null;
|
||||
|
||||
protected account$ = new ReplaySubject<Account>(1);
|
||||
|
||||
/** Send structured debug logs from the credential generator component
|
||||
* to the debugger console.
|
||||
*
|
||||
* @warning this may reveal sensitive information in plaintext.
|
||||
*/
|
||||
@Input()
|
||||
debug: boolean = false;
|
||||
|
||||
// this `log` initializer is overridden in `ngOnInit`
|
||||
private log: SemanticLogger = disabledSemanticLoggerProvider({});
|
||||
|
||||
async ngOnChanges(changes: SimpleChanges) {
|
||||
const account = changes?.account;
|
||||
if (account?.previousValue?.id !== account?.currentValue?.id) {
|
||||
this.log.debug(
|
||||
{
|
||||
previousUserId: account?.previousValue?.id as UserId,
|
||||
currentUserId: account?.currentValue?.id as UserId,
|
||||
},
|
||||
"account input change detected",
|
||||
);
|
||||
this.account$.next(this.account);
|
||||
}
|
||||
}
|
||||
|
||||
/** Removes bottom margin, passed to downstream components */
|
||||
@Input({ transform: coerceBooleanProperty }) disableMargin = false;
|
||||
@@ -64,9 +107,6 @@ export class PasswordGeneratorComponent implements OnInit, OnDestroy {
|
||||
/** Emits the last generated value. */
|
||||
protected readonly value$ = new BehaviorSubject<string>("");
|
||||
|
||||
/** Emits when the userId changes */
|
||||
protected readonly userId$ = new BehaviorSubject<UserId>(null);
|
||||
|
||||
/** Emits when a new credential is requested */
|
||||
private readonly generate$ = new Subject<GenerateRequest>();
|
||||
|
||||
@@ -77,8 +117,10 @@ export class PasswordGeneratorComponent implements OnInit, OnDestroy {
|
||||
* @param requestor a label used to trace generation request
|
||||
* origin in the debugger.
|
||||
*/
|
||||
protected async generate(requestor: string) {
|
||||
this.generate$.next({ source: requestor });
|
||||
protected async generate(source: string) {
|
||||
this.log.debug({ source }, "generation requested");
|
||||
|
||||
this.generate$.next({ source });
|
||||
}
|
||||
|
||||
/** Tracks changes to the selected credential type
|
||||
@@ -102,20 +144,21 @@ export class PasswordGeneratorComponent implements OnInit, OnDestroy {
|
||||
readonly onAlgorithm = new EventEmitter<AlgorithmInfo>();
|
||||
|
||||
async ngOnInit() {
|
||||
if (this.userId) {
|
||||
this.userId$.next(this.userId);
|
||||
} else {
|
||||
this.accountService.activeAccount$
|
||||
.pipe(
|
||||
map((acct) => acct.id),
|
||||
distinctUntilChanged(),
|
||||
takeUntil(this.destroyed),
|
||||
)
|
||||
.subscribe(this.userId$);
|
||||
this.log = ifEnabledSemanticLoggerProvider(this.debug, this.logService, {
|
||||
type: "UsernameGeneratorComponent",
|
||||
});
|
||||
|
||||
if (!this.account) {
|
||||
this.account = await firstValueFrom(this.accountService.activeAccount$);
|
||||
this.log.info(
|
||||
{ userId: this.account.id },
|
||||
"account not specified; using active account settings",
|
||||
);
|
||||
this.account$.next(this.account);
|
||||
}
|
||||
|
||||
this.generatorService
|
||||
.algorithms$("password", { userId$: this.userId$ })
|
||||
.algorithms$("password", { account$: this.account$ })
|
||||
.pipe(
|
||||
map((algorithms) => this.toOptions(algorithms)),
|
||||
takeUntil(this.destroyed),
|
||||
@@ -141,12 +184,14 @@ export class PasswordGeneratorComponent implements OnInit, OnDestroy {
|
||||
// continue with origin stream
|
||||
return generator;
|
||||
}),
|
||||
withLatestFrom(this.userId$, this.algorithm$),
|
||||
withLatestFrom(this.account$, this.algorithm$),
|
||||
takeUntil(this.destroyed),
|
||||
)
|
||||
.subscribe(([generated, userId, algorithm]) => {
|
||||
.subscribe(([generated, account, algorithm]) => {
|
||||
this.log.debug({ source: generated.source }, "credential generated");
|
||||
|
||||
this.generatorHistoryService
|
||||
.track(userId, generated.credential, generated.category, generated.generationDate)
|
||||
.track(account.id, generated.credential, generated.category, generated.generationDate)
|
||||
.catch((e: unknown) => {
|
||||
this.logService.error(e);
|
||||
});
|
||||
@@ -164,7 +209,7 @@ export class PasswordGeneratorComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
|
||||
// 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({ account$: this.account$ });
|
||||
this.credentialType$
|
||||
.pipe(
|
||||
filter((type) => !!type),
|
||||
@@ -173,6 +218,10 @@ export class PasswordGeneratorComponent implements OnInit, OnDestroy {
|
||||
)
|
||||
.subscribe(([algorithm, preference]) => {
|
||||
if (isPasswordAlgorithm(algorithm)) {
|
||||
this.log.info(
|
||||
{ algorithm, category: CredentialCategories.password },
|
||||
"algorithm preferences updated",
|
||||
);
|
||||
preference.password.algorithm = algorithm;
|
||||
preference.password.updated = new Date();
|
||||
} else {
|
||||
@@ -190,6 +239,8 @@ export class PasswordGeneratorComponent implements OnInit, OnDestroy {
|
||||
takeUntil(this.destroyed),
|
||||
)
|
||||
.subscribe((algorithm) => {
|
||||
this.log.debug(algorithm, "algorithm selected");
|
||||
|
||||
// update navigation
|
||||
this.onCredentialTypeChanged(algorithm.id);
|
||||
|
||||
@@ -205,32 +256,40 @@ export class PasswordGeneratorComponent implements OnInit, OnDestroy {
|
||||
this.algorithm$.pipe(takeUntil(this.destroyed)).subscribe((a) => {
|
||||
this.zone.run(() => {
|
||||
if (!a || a.onlyOnRequest) {
|
||||
this.log.debug("autogeneration disabled; clearing generated credential");
|
||||
this.value$.next("-");
|
||||
} else {
|
||||
this.generate("autogenerate").catch((e: unknown) => this.logService.error(e));
|
||||
this.log.debug("autogeneration enabled");
|
||||
this.generate("autogenerate").catch((e: unknown) => {
|
||||
this.log.error(e as object, "a failure occurred during autogeneration");
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
this.log.debug("component initialized");
|
||||
}
|
||||
|
||||
private announce(message: string) {
|
||||
this.ariaLive.announce(message).catch((e) => this.logService.error(e));
|
||||
}
|
||||
|
||||
private typeToGenerator$(type: CredentialAlgorithm) {
|
||||
private typeToGenerator$(algorithm: CredentialAlgorithm) {
|
||||
const dependencies = {
|
||||
on$: this.generate$,
|
||||
userId$: this.userId$,
|
||||
account$: this.account$,
|
||||
};
|
||||
|
||||
switch (type) {
|
||||
this.log.debug({ algorithm }, "constructing generation stream");
|
||||
|
||||
switch (algorithm) {
|
||||
case "password":
|
||||
return this.generatorService.generate$(Generators.password, dependencies);
|
||||
|
||||
case "passphrase":
|
||||
return this.generatorService.generate$(Generators.passphrase, dependencies);
|
||||
default:
|
||||
throw new Error(`Invalid generator type: "${type}"`);
|
||||
this.log.panic({ algorithm }, `Invalid generator type: "${algorithm}"`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,31 +1,27 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { coerceBooleanProperty } from "@angular/cdk/coercion";
|
||||
import { OnInit, Input, Output, EventEmitter, Component, OnDestroy } from "@angular/core";
|
||||
import { FormBuilder } from "@angular/forms";
|
||||
import {
|
||||
BehaviorSubject,
|
||||
takeUntil,
|
||||
Subject,
|
||||
map,
|
||||
filter,
|
||||
tap,
|
||||
skip,
|
||||
ReplaySubject,
|
||||
withLatestFrom,
|
||||
} from "rxjs";
|
||||
OnInit,
|
||||
Input,
|
||||
Output,
|
||||
EventEmitter,
|
||||
Component,
|
||||
OnDestroy,
|
||||
SimpleChanges,
|
||||
OnChanges,
|
||||
} from "@angular/core";
|
||||
import { FormBuilder } from "@angular/forms";
|
||||
import { takeUntil, Subject, map, filter, tap, skip, ReplaySubject, withLatestFrom } from "rxjs";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { Account } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import {
|
||||
Generators,
|
||||
CredentialGeneratorService,
|
||||
PasswordGenerationOptions,
|
||||
} from "@bitwarden/generator-core";
|
||||
|
||||
import { completeOnAccountSwitch } from "./util";
|
||||
|
||||
const Controls = Object.freeze({
|
||||
length: "length",
|
||||
uppercase: "uppercase",
|
||||
@@ -42,9 +38,8 @@ const Controls = Object.freeze({
|
||||
selector: "tools-password-settings",
|
||||
templateUrl: "password-settings.component.html",
|
||||
})
|
||||
export class PasswordSettingsComponent implements OnInit, OnDestroy {
|
||||
export class PasswordSettingsComponent implements OnInit, OnChanges, OnDestroy {
|
||||
/** Instantiates the component
|
||||
* @param accountService queries user availability
|
||||
* @param generatorService settings and policy logic
|
||||
* @param i18nService localize hints
|
||||
* @param formBuilder reactive form controls
|
||||
@@ -53,15 +48,20 @@ export class PasswordSettingsComponent implements OnInit, OnDestroy {
|
||||
private formBuilder: FormBuilder,
|
||||
private generatorService: CredentialGeneratorService,
|
||||
private i18nService: I18nService,
|
||||
private accountService: AccountService,
|
||||
) {}
|
||||
|
||||
/** Binds the password component to a specific user's settings.
|
||||
* When this input is not provided, the form binds to the active
|
||||
* user
|
||||
/** Binds the component to a specific user's settings.
|
||||
*/
|
||||
@Input()
|
||||
userId: UserId | null;
|
||||
@Input({ required: true })
|
||||
account: Account;
|
||||
|
||||
protected account$ = new ReplaySubject<Account>(1);
|
||||
|
||||
async ngOnChanges(changes: SimpleChanges) {
|
||||
if ("account" in changes && changes.account) {
|
||||
this.account$.next(this.account);
|
||||
}
|
||||
}
|
||||
|
||||
/** When `true`, an options header is displayed by the component. Otherwise, the header is hidden. */
|
||||
@Input()
|
||||
@@ -110,8 +110,9 @@ export class PasswordSettingsComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
const singleUserId$ = this.singleUserId$();
|
||||
const settings = await this.generatorService.settings(Generators.password, { singleUserId$ });
|
||||
const settings = await this.generatorService.settings(Generators.password, {
|
||||
account$: this.account$,
|
||||
});
|
||||
|
||||
// bind settings to the UI
|
||||
settings.withConstraints$
|
||||
@@ -145,7 +146,7 @@ export class PasswordSettingsComponent implements OnInit, OnDestroy {
|
||||
|
||||
// explain policy & disable policy-overridden fields
|
||||
this.generatorService
|
||||
.policy$(Generators.password, { userId$: singleUserId$ })
|
||||
.policy$(Generators.password, { account$: this.account$ })
|
||||
.pipe(takeUntil(this.destroyed$))
|
||||
.subscribe(({ constraints }) => {
|
||||
this.policyInEffect = constraints.policyInEffect;
|
||||
@@ -243,19 +244,6 @@ export class PasswordSettingsComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
private singleUserId$() {
|
||||
// FIXME: this branch should probably scan for the user and make sure
|
||||
// the account is unlocked
|
||||
if (this.userId) {
|
||||
return new BehaviorSubject(this.userId as UserId).asObservable();
|
||||
}
|
||||
|
||||
return this.accountService.activeAccount$.pipe(
|
||||
completeOnAccountSwitch(),
|
||||
takeUntil(this.destroyed$),
|
||||
);
|
||||
}
|
||||
|
||||
private readonly destroyed$ = new Subject<void>();
|
||||
ngOnDestroy(): void {
|
||||
this.destroyed$.next();
|
||||
|
||||
@@ -1,25 +1,31 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core";
|
||||
import {
|
||||
Component,
|
||||
EventEmitter,
|
||||
Input,
|
||||
OnChanges,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
Output,
|
||||
SimpleChanges,
|
||||
} from "@angular/core";
|
||||
import { FormBuilder } from "@angular/forms";
|
||||
import { BehaviorSubject, map, skip, Subject, takeUntil, withLatestFrom } from "rxjs";
|
||||
import { map, ReplaySubject, skip, Subject, takeUntil, withLatestFrom } from "rxjs";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import {
|
||||
CredentialGeneratorService,
|
||||
Generators,
|
||||
SubaddressGenerationOptions,
|
||||
} from "@bitwarden/generator-core";
|
||||
|
||||
import { completeOnAccountSwitch } from "./util";
|
||||
|
||||
/** Options group for plus-addressed emails */
|
||||
@Component({
|
||||
selector: "tools-subaddress-settings",
|
||||
templateUrl: "subaddress-settings.component.html",
|
||||
})
|
||||
export class SubaddressSettingsComponent implements OnInit, OnDestroy {
|
||||
export class SubaddressSettingsComponent implements OnInit, OnChanges, OnDestroy {
|
||||
/** Instantiates the component
|
||||
* @param accountService queries user availability
|
||||
* @param generatorService settings and policy logic
|
||||
@@ -32,11 +38,17 @@ export class SubaddressSettingsComponent implements OnInit, OnDestroy {
|
||||
) {}
|
||||
|
||||
/** Binds the component to a specific user's settings.
|
||||
* When this input is not provided, the form binds to the active
|
||||
* user
|
||||
*/
|
||||
@Input()
|
||||
userId: UserId | null;
|
||||
@Input({ required: true })
|
||||
account: Account;
|
||||
|
||||
protected account$ = new ReplaySubject<Account>(1);
|
||||
|
||||
async ngOnChanges(changes: SimpleChanges) {
|
||||
if ("account" in changes && changes.account) {
|
||||
this.account$.next(this.account);
|
||||
}
|
||||
}
|
||||
|
||||
/** Emits settings updates and completes if the settings become unavailable.
|
||||
* @remarks this does not emit the initial settings. If you would like
|
||||
@@ -52,8 +64,9 @@ export class SubaddressSettingsComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
|
||||
async ngOnInit() {
|
||||
const singleUserId$ = this.singleUserId$();
|
||||
const settings = await this.generatorService.settings(Generators.subaddress, { singleUserId$ });
|
||||
const settings = await this.generatorService.settings(Generators.subaddress, {
|
||||
account$: this.account$,
|
||||
});
|
||||
|
||||
settings.pipe(takeUntil(this.destroyed$)).subscribe((s) => {
|
||||
this.settings.patchValue(s, { emitEvent: false });
|
||||
@@ -76,19 +89,6 @@ export class SubaddressSettingsComponent implements OnInit, OnDestroy {
|
||||
this.saveSettings.next(site);
|
||||
}
|
||||
|
||||
private singleUserId$() {
|
||||
// FIXME: this branch should probably scan for the user and make sure
|
||||
// the account is unlocked
|
||||
if (this.userId) {
|
||||
return new BehaviorSubject(this.userId as UserId).asObservable();
|
||||
}
|
||||
|
||||
return this.accountService.activeAccount$.pipe(
|
||||
completeOnAccountSwitch(),
|
||||
takeUntil(this.destroyed$),
|
||||
);
|
||||
}
|
||||
|
||||
private readonly destroyed$ = new Subject<void>();
|
||||
ngOnDestroy(): void {
|
||||
this.destroyed$.next();
|
||||
|
||||
@@ -60,22 +60,22 @@
|
||||
</form>
|
||||
<tools-catchall-settings
|
||||
*ngIf="(algorithm$ | async)?.id === 'catchall'"
|
||||
[userId]="this.userId$ | async"
|
||||
[account]="account$ | async"
|
||||
(onUpdated)="generate('catchall settings')"
|
||||
/>
|
||||
<tools-forwarder-settings
|
||||
*ngIf="!!(forwarderId$ | async)"
|
||||
[forwarder]="forwarderId$ | async"
|
||||
[userId]="this.userId$ | async"
|
||||
[account]="account$ | async"
|
||||
/>
|
||||
<tools-subaddress-settings
|
||||
*ngIf="(algorithm$ | async)?.id === 'subaddress'"
|
||||
[userId]="this.userId$ | async"
|
||||
[account]="account$ | async"
|
||||
(onUpdated)="generate('subaddress settings')"
|
||||
/>
|
||||
<tools-username-settings
|
||||
*ngIf="(algorithm$ | async)?.id === 'username'"
|
||||
[userId]="this.userId$ | async"
|
||||
[account]="account$ | async"
|
||||
(onUpdated)="generate('username settings')"
|
||||
/>
|
||||
</bit-card>
|
||||
|
||||
@@ -2,7 +2,17 @@
|
||||
// @ts-strict-ignore
|
||||
import { LiveAnnouncer } from "@angular/cdk/a11y";
|
||||
import { coerceBooleanProperty } from "@angular/cdk/coercion";
|
||||
import { Component, EventEmitter, Input, NgZone, OnDestroy, OnInit, Output } from "@angular/core";
|
||||
import {
|
||||
Component,
|
||||
EventEmitter,
|
||||
Input,
|
||||
NgZone,
|
||||
OnChanges,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
Output,
|
||||
SimpleChanges,
|
||||
} from "@angular/core";
|
||||
import { FormBuilder } from "@angular/forms";
|
||||
import {
|
||||
BehaviorSubject,
|
||||
@@ -11,6 +21,7 @@ import {
|
||||
combineLatestWith,
|
||||
distinctUntilChanged,
|
||||
filter,
|
||||
firstValueFrom,
|
||||
map,
|
||||
ReplaySubject,
|
||||
Subject,
|
||||
@@ -19,15 +30,21 @@ import {
|
||||
withLatestFrom,
|
||||
} from "rxjs";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { IntegrationId } from "@bitwarden/common/tools/integration";
|
||||
import {
|
||||
SemanticLogger,
|
||||
disabledSemanticLoggerProvider,
|
||||
ifEnabledSemanticLoggerProvider,
|
||||
} from "@bitwarden/common/tools/log";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { ToastService, Option } from "@bitwarden/components";
|
||||
import {
|
||||
AlgorithmInfo,
|
||||
CredentialAlgorithm,
|
||||
CredentialCategories,
|
||||
CredentialGeneratorService,
|
||||
GenerateRequest,
|
||||
GeneratedCredential,
|
||||
@@ -51,7 +68,7 @@ const NONE_SELECTED = "none";
|
||||
selector: "tools-username-generator",
|
||||
templateUrl: "username-generator.component.html",
|
||||
})
|
||||
export class UsernameGeneratorComponent implements OnInit, OnDestroy {
|
||||
export class UsernameGeneratorComponent implements OnInit, OnChanges, OnDestroy {
|
||||
/** Instantiates the username generator
|
||||
* @param generatorService generates credentials; stores preferences
|
||||
* @param i18nService localizes generator algorithm descriptions
|
||||
@@ -75,7 +92,34 @@ export class UsernameGeneratorComponent implements OnInit, OnDestroy {
|
||||
* the form binds to the active user
|
||||
*/
|
||||
@Input()
|
||||
userId: UserId | null;
|
||||
account: Account | null;
|
||||
|
||||
protected account$ = new ReplaySubject<Account>(1);
|
||||
|
||||
/** Send structured debug logs from the credential generator component
|
||||
* to the debugger console.
|
||||
*
|
||||
* @warning this may reveal sensitive information in plaintext.
|
||||
*/
|
||||
@Input()
|
||||
debug: boolean = false;
|
||||
|
||||
// this `log` initializer is overridden in `ngOnInit`
|
||||
private log: SemanticLogger = disabledSemanticLoggerProvider({});
|
||||
|
||||
async ngOnChanges(changes: SimpleChanges) {
|
||||
const account = changes?.account;
|
||||
if (account?.previousValue?.id !== account?.currentValue?.id) {
|
||||
this.log.debug(
|
||||
{
|
||||
previousUserId: account?.previousValue?.id as UserId,
|
||||
currentUserId: account?.currentValue?.id as UserId,
|
||||
},
|
||||
"account input change detected",
|
||||
);
|
||||
this.account$.next(this.account);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The website associated with the credential generation request.
|
||||
@@ -104,20 +148,21 @@ export class UsernameGeneratorComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
|
||||
async ngOnInit() {
|
||||
if (this.userId) {
|
||||
this.userId$.next(this.userId);
|
||||
} else {
|
||||
this.accountService.activeAccount$
|
||||
.pipe(
|
||||
map((acct) => acct.id),
|
||||
distinctUntilChanged(),
|
||||
takeUntil(this.destroyed),
|
||||
)
|
||||
.subscribe(this.userId$);
|
||||
this.log = ifEnabledSemanticLoggerProvider(this.debug, this.logService, {
|
||||
type: "UsernameGeneratorComponent",
|
||||
});
|
||||
|
||||
if (!this.account) {
|
||||
this.account = await firstValueFrom(this.accountService.activeAccount$);
|
||||
this.log.info(
|
||||
{ userId: this.account.id },
|
||||
"account not specified; using active account settings",
|
||||
);
|
||||
this.account$.next(this.account);
|
||||
}
|
||||
|
||||
this.generatorService
|
||||
.algorithms$(["email", "username"], { userId$: this.userId$ })
|
||||
.algorithms$(["email", "username"], { account$: this.account$ })
|
||||
.pipe(
|
||||
map((algorithms) => {
|
||||
const usernames = algorithms.filter((a) => !isForwarderIntegration(a.id));
|
||||
@@ -169,12 +214,14 @@ export class UsernameGeneratorComponent implements OnInit, OnDestroy {
|
||||
// continue with origin stream
|
||||
return generator;
|
||||
}),
|
||||
withLatestFrom(this.userId$, this.algorithm$),
|
||||
withLatestFrom(this.account$, this.algorithm$),
|
||||
takeUntil(this.destroyed),
|
||||
)
|
||||
.subscribe(([generated, userId, algorithm]) => {
|
||||
.subscribe(([generated, account, algorithm]) => {
|
||||
this.log.debug({ source: generated.source }, "credential generated");
|
||||
|
||||
this.generatorHistoryService
|
||||
.track(userId, generated.credential, generated.category, generated.generationDate)
|
||||
.track(account.id, generated.credential, generated.category, generated.generationDate)
|
||||
.catch((e: unknown) => {
|
||||
this.logService.error(e);
|
||||
});
|
||||
@@ -237,6 +284,8 @@ export class UsernameGeneratorComponent implements OnInit, OnDestroy {
|
||||
takeUntil(this.destroyed),
|
||||
)
|
||||
.subscribe(([showForwarder, forwarderId]) => {
|
||||
this.log.debug({ forwarderId, showForwarder }, "forwarder visibility updated");
|
||||
|
||||
// update subjects within the angular zone so that the
|
||||
// template bindings refresh immediately
|
||||
this.zone.run(() => {
|
||||
@@ -260,6 +309,8 @@ export class UsernameGeneratorComponent implements OnInit, OnDestroy {
|
||||
takeUntil(this.destroyed),
|
||||
)
|
||||
.subscribe((algorithm) => {
|
||||
this.log.debug(algorithm, "algorithm selected");
|
||||
|
||||
// update subjects within the angular zone so that the
|
||||
// template bindings refresh immediately
|
||||
this.zone.run(() => {
|
||||
@@ -269,7 +320,7 @@ 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$ });
|
||||
const preferences = await this.generatorService.preferences({ account$: this.account$ });
|
||||
this.algorithm$
|
||||
.pipe(
|
||||
filter((algorithm) => !!algorithm),
|
||||
@@ -278,9 +329,17 @@ export class UsernameGeneratorComponent implements OnInit, OnDestroy {
|
||||
)
|
||||
.subscribe(([algorithm, preference]) => {
|
||||
if (isEmailAlgorithm(algorithm.id)) {
|
||||
this.log.info(
|
||||
{ algorithm, category: CredentialCategories.email },
|
||||
"algorithm preferences updated",
|
||||
);
|
||||
preference.email.algorithm = algorithm.id;
|
||||
preference.email.updated = new Date();
|
||||
} else if (isUsernameAlgorithm(algorithm.id)) {
|
||||
this.log.info(
|
||||
{ algorithm, category: CredentialCategories.username },
|
||||
"algorithm preferences updated",
|
||||
);
|
||||
preference.username.algorithm = algorithm.id;
|
||||
preference.username.updated = new Date();
|
||||
} else {
|
||||
@@ -339,21 +398,30 @@ export class UsernameGeneratorComponent implements OnInit, OnDestroy {
|
||||
this.algorithm$.pipe(takeUntil(this.destroyed)).subscribe((a) => {
|
||||
this.zone.run(() => {
|
||||
if (!a || a.onlyOnRequest) {
|
||||
this.log.debug("autogeneration disabled; clearing generated credential");
|
||||
this.value$.next("-");
|
||||
} else {
|
||||
this.generate("autogenerate").catch((e: unknown) => this.logService.error(e));
|
||||
this.log.debug("autogeneration enabled");
|
||||
|
||||
this.generate("autogenerate").catch((e: unknown) => {
|
||||
this.log.error(e as object, "a failure occurred during autogeneration");
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
this.log.debug("component initialized");
|
||||
}
|
||||
|
||||
private typeToGenerator$(type: CredentialAlgorithm) {
|
||||
private typeToGenerator$(algorithm: CredentialAlgorithm) {
|
||||
const dependencies = {
|
||||
on$: this.generate$,
|
||||
userId$: this.userId$,
|
||||
account$: this.account$,
|
||||
};
|
||||
|
||||
switch (type) {
|
||||
this.log.debug({ algorithm }, "constructing generation stream");
|
||||
|
||||
switch (algorithm) {
|
||||
case "catchall":
|
||||
return this.generatorService.generate$(Generators.catchall, dependencies);
|
||||
|
||||
@@ -364,13 +432,13 @@ export class UsernameGeneratorComponent implements OnInit, OnDestroy {
|
||||
return this.generatorService.generate$(Generators.username, dependencies);
|
||||
}
|
||||
|
||||
if (isForwarderIntegration(type)) {
|
||||
const forwarder = getForwarderConfiguration(type.forwarder);
|
||||
if (isForwarderIntegration(algorithm)) {
|
||||
const forwarder = getForwarderConfiguration(algorithm.forwarder);
|
||||
const configuration = toCredentialGeneratorConfiguration(forwarder);
|
||||
return this.generatorService.generate$(configuration, dependencies);
|
||||
}
|
||||
|
||||
throw new Error(`Invalid generator type: "${type}"`);
|
||||
this.log.panic({ algorithm }, `Invalid generator type: "${algorithm}"`);
|
||||
}
|
||||
|
||||
private announce(message: string) {
|
||||
@@ -398,9 +466,6 @@ export class UsernameGeneratorComponent implements OnInit, OnDestroy {
|
||||
/** Emits the last generated value. */
|
||||
protected readonly value$ = new BehaviorSubject<string>("");
|
||||
|
||||
/** Emits when the userId changes */
|
||||
protected readonly userId$ = new BehaviorSubject<UserId>(null);
|
||||
|
||||
/** Emits when a new credential is requested */
|
||||
private readonly generate$ = new Subject<GenerateRequest>();
|
||||
|
||||
@@ -437,11 +502,13 @@ export class UsernameGeneratorComponent implements OnInit, OnDestroy {
|
||||
protected readonly USER_REQUEST = "user request";
|
||||
|
||||
/** Request a new value from the generator
|
||||
* @param requestor a label used to trace generation request
|
||||
* @param source a label used to trace generation request
|
||||
* origin in the debugger.
|
||||
*/
|
||||
protected async generate(requestor: string) {
|
||||
this.generate$.next({ source: requestor, website: this.website });
|
||||
protected async generate(source: string) {
|
||||
const request = { source, website: this.website };
|
||||
this.log.debug(request, "generation requested");
|
||||
this.generate$.next(request);
|
||||
}
|
||||
|
||||
private toOptions(algorithms: AlgorithmInfo[]) {
|
||||
|
||||
@@ -1,25 +1,31 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core";
|
||||
import {
|
||||
Component,
|
||||
EventEmitter,
|
||||
Input,
|
||||
OnChanges,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
Output,
|
||||
SimpleChanges,
|
||||
} from "@angular/core";
|
||||
import { FormBuilder } from "@angular/forms";
|
||||
import { BehaviorSubject, map, skip, Subject, takeUntil, withLatestFrom } from "rxjs";
|
||||
import { map, ReplaySubject, skip, Subject, takeUntil, withLatestFrom } from "rxjs";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { Account } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import {
|
||||
CredentialGeneratorService,
|
||||
EffUsernameGenerationOptions,
|
||||
Generators,
|
||||
} from "@bitwarden/generator-core";
|
||||
|
||||
import { completeOnAccountSwitch } from "./util";
|
||||
|
||||
/** Options group for usernames */
|
||||
@Component({
|
||||
selector: "tools-username-settings",
|
||||
templateUrl: "username-settings.component.html",
|
||||
})
|
||||
export class UsernameSettingsComponent implements OnInit, OnDestroy {
|
||||
export class UsernameSettingsComponent implements OnInit, OnChanges, OnDestroy {
|
||||
/** Instantiates the component
|
||||
* @param accountService queries user availability
|
||||
* @param generatorService settings and policy logic
|
||||
@@ -28,15 +34,20 @@ export class UsernameSettingsComponent implements OnInit, OnDestroy {
|
||||
constructor(
|
||||
private formBuilder: FormBuilder,
|
||||
private generatorService: CredentialGeneratorService,
|
||||
private accountService: AccountService,
|
||||
) {}
|
||||
|
||||
/** Binds the component to a specific user's settings.
|
||||
* When this input is not provided, the form binds to the active
|
||||
* user
|
||||
*/
|
||||
@Input()
|
||||
userId: UserId | null;
|
||||
@Input({ required: true })
|
||||
account: Account;
|
||||
|
||||
protected account$ = new ReplaySubject<Account>(1);
|
||||
|
||||
async ngOnChanges(changes: SimpleChanges) {
|
||||
if ("account" in changes) {
|
||||
this.account$.next(this.account);
|
||||
}
|
||||
}
|
||||
|
||||
/** Emits settings updates and completes if the settings become unavailable.
|
||||
* @remarks this does not emit the initial settings. If you would like
|
||||
@@ -53,8 +64,9 @@ export class UsernameSettingsComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
|
||||
async ngOnInit() {
|
||||
const singleUserId$ = this.singleUserId$();
|
||||
const settings = await this.generatorService.settings(Generators.username, { singleUserId$ });
|
||||
const settings = await this.generatorService.settings(Generators.username, {
|
||||
account$: this.account$,
|
||||
});
|
||||
|
||||
settings.pipe(takeUntil(this.destroyed$)).subscribe((s) => {
|
||||
this.settings.patchValue(s, { emitEvent: false });
|
||||
@@ -77,19 +89,6 @@ export class UsernameSettingsComponent implements OnInit, OnDestroy {
|
||||
this.saveSettings.next(site);
|
||||
}
|
||||
|
||||
private singleUserId$() {
|
||||
// FIXME: this branch should probably scan for the user and make sure
|
||||
// the account is unlocked
|
||||
if (this.userId) {
|
||||
return new BehaviorSubject(this.userId as UserId).asObservable();
|
||||
}
|
||||
|
||||
return this.accountService.activeAccount$.pipe(
|
||||
completeOnAccountSwitch(),
|
||||
takeUntil(this.destroyed$),
|
||||
);
|
||||
}
|
||||
|
||||
private readonly destroyed$ = new Subject<void>();
|
||||
ngOnDestroy(): void {
|
||||
this.destroyed$.next();
|
||||
|
||||
Reference in New Issue
Block a user