mirror of
https://github.com/bitwarden/browser
synced 2025-12-17 08:43:33 +00:00
* Use typescript-strict-plugin to iteratively turn on strict * Add strict testing to pipeline Can be executed locally through either `npm run test:types` for full type checking including spec files, or `npx tsc-strict` for only tsconfig.json included files. * turn on strict for scripts directory * Use plugin for all tsconfigs in monorepo vscode is capable of executing tsc with plugins, but uses the most relevant tsconfig to do so. If the plugin is not a part of that config, it is skipped and developers get no feedback of strict compile time issues. These updates remedy that at the cost of slightly more complex removal of the plugin when the time comes. * remove plugin from configs that extend one that already has it * Update workspace settings to honor strict plugin * Apply strict-plugin to native message test runner * Update vscode workspace to use root tsc version * `./node_modules/.bin/update-strict-comments` 🤖 This is a one-time operation. All future files should adhere to strict type checking. * Add fixme to `ts-strict-ignore` comments * `update-strict-comments` 🤖 repeated for new merge files
273 lines
8.2 KiB
TypeScript
273 lines
8.2 KiB
TypeScript
// FIXME: Update this file to be type safe and remove this and next line
|
|
// @ts-strict-ignore
|
|
import { coerceBooleanProperty } from "@angular/cdk/coercion";
|
|
import { Component, EventEmitter, Input, NgZone, OnDestroy, OnInit, Output } from "@angular/core";
|
|
import {
|
|
BehaviorSubject,
|
|
catchError,
|
|
distinctUntilChanged,
|
|
filter,
|
|
map,
|
|
ReplaySubject,
|
|
Subject,
|
|
switchMap,
|
|
takeUntil,
|
|
withLatestFrom,
|
|
} from "rxjs";
|
|
|
|
import { 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 { UserId } from "@bitwarden/common/types/guid";
|
|
import { ToastService } from "@bitwarden/components";
|
|
import { Option } from "@bitwarden/components/src/select/option";
|
|
import {
|
|
CredentialGeneratorService,
|
|
Generators,
|
|
GeneratedCredential,
|
|
CredentialAlgorithm,
|
|
isPasswordAlgorithm,
|
|
AlgorithmInfo,
|
|
isSameAlgorithm,
|
|
} from "@bitwarden/generator-core";
|
|
import { GeneratorHistoryService } from "@bitwarden/generator-history";
|
|
|
|
/** Options group for passwords */
|
|
@Component({
|
|
selector: "tools-password-generator",
|
|
templateUrl: "password-generator.component.html",
|
|
})
|
|
export class PasswordGeneratorComponent implements OnInit, OnDestroy {
|
|
constructor(
|
|
private generatorService: CredentialGeneratorService,
|
|
private generatorHistoryService: GeneratorHistoryService,
|
|
private toastService: ToastService,
|
|
private logService: LogService,
|
|
private i18nService: I18nService,
|
|
private accountService: AccountService,
|
|
private zone: NgZone,
|
|
) {}
|
|
|
|
/** 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;
|
|
|
|
/** Removes bottom margin, passed to downstream components */
|
|
@Input({ transform: coerceBooleanProperty }) disableMargin = false;
|
|
|
|
/** tracks the currently selected credential type */
|
|
protected credentialType$ = new BehaviorSubject<CredentialAlgorithm>(null);
|
|
|
|
/** 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<string>();
|
|
|
|
/** Request a new value from the generator
|
|
* @param requestor a label used to trace generation request
|
|
* origin in the debugger.
|
|
*/
|
|
protected async generate(requestor: string) {
|
|
this.generate$.next(requestor);
|
|
}
|
|
|
|
/** Tracks changes to the selected credential type
|
|
* @param type the new credential type
|
|
*/
|
|
protected onCredentialTypeChanged(type: CredentialAlgorithm) {
|
|
// break subscription cycle
|
|
if (this.credentialType$.value !== type) {
|
|
this.zone.run(() => {
|
|
this.credentialType$.next(type);
|
|
});
|
|
}
|
|
}
|
|
|
|
/** Emits credentials created from a generation request. */
|
|
@Output()
|
|
readonly onGenerated = new EventEmitter<GeneratedCredential>();
|
|
|
|
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.generatorService
|
|
.algorithms$("password", { userId$: this.userId$ })
|
|
.pipe(
|
|
map((algorithms) => this.toOptions(algorithms)),
|
|
takeUntil(this.destroyed),
|
|
)
|
|
.subscribe(this.passwordOptions$);
|
|
|
|
// wire up the generator
|
|
this.algorithm$
|
|
.pipe(
|
|
filter((algorithm) => !!algorithm),
|
|
switchMap((algorithm) => this.typeToGenerator$(algorithm.id)),
|
|
catchError((error: unknown, generator) => {
|
|
if (typeof error === "string") {
|
|
this.toastService.showToast({
|
|
message: error,
|
|
variant: "error",
|
|
title: "",
|
|
});
|
|
} else {
|
|
this.logService.error(error);
|
|
}
|
|
|
|
// continue with origin stream
|
|
return generator;
|
|
}),
|
|
withLatestFrom(this.userId$),
|
|
takeUntil(this.destroyed),
|
|
)
|
|
.subscribe(([generated, userId]) => {
|
|
this.generatorHistoryService
|
|
.track(userId, generated.credential, generated.category, generated.generationDate)
|
|
.catch((e: unknown) => {
|
|
this.logService.error(e);
|
|
});
|
|
|
|
// update subjects within the angular zone so that the
|
|
// template bindings refresh immediately
|
|
this.zone.run(() => {
|
|
this.onGenerated.next(generated);
|
|
this.value$.next(generated.credential);
|
|
});
|
|
});
|
|
|
|
// assume the last-visible generator algorithm is the user's preferred one
|
|
const preferences = await this.generatorService.preferences({ singleUserId$: this.userId$ });
|
|
this.credentialType$
|
|
.pipe(
|
|
filter((type) => !!type),
|
|
withLatestFrom(preferences),
|
|
takeUntil(this.destroyed),
|
|
)
|
|
.subscribe(([algorithm, preference]) => {
|
|
if (isPasswordAlgorithm(algorithm)) {
|
|
preference.password.algorithm = algorithm;
|
|
preference.password.updated = new Date();
|
|
} else {
|
|
return;
|
|
}
|
|
|
|
preferences.next(preference);
|
|
});
|
|
|
|
// update active algorithm
|
|
preferences
|
|
.pipe(
|
|
map(({ password }) => this.generatorService.algorithm(password.algorithm)),
|
|
distinctUntilChanged((prev, next) => isSameAlgorithm(prev?.id, next?.id)),
|
|
takeUntil(this.destroyed),
|
|
)
|
|
.subscribe((algorithm) => {
|
|
// update navigation
|
|
this.onCredentialTypeChanged(algorithm.id);
|
|
|
|
// update subjects within the angular zone so that the
|
|
// template bindings refresh immediately
|
|
this.zone.run(() => {
|
|
this.algorithm$.next(algorithm);
|
|
});
|
|
});
|
|
|
|
// generate on load unless the generator prohibits it
|
|
this.algorithm$.pipe(takeUntil(this.destroyed)).subscribe((a) => {
|
|
this.zone.run(() => {
|
|
if (!a || a.onlyOnRequest) {
|
|
this.value$.next("-");
|
|
} else {
|
|
this.generate("autogenerate").catch((e: unknown) => this.logService.error(e));
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
private typeToGenerator$(type: CredentialAlgorithm) {
|
|
const dependencies = {
|
|
on$: this.generate$,
|
|
userId$: this.userId$,
|
|
};
|
|
|
|
switch (type) {
|
|
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}"`);
|
|
}
|
|
}
|
|
|
|
/** Lists the credential types supported by the component. */
|
|
protected passwordOptions$ = new BehaviorSubject<Option<CredentialAlgorithm>[]>([]);
|
|
|
|
/** tracks the currently selected credential type */
|
|
protected algorithm$ = new ReplaySubject<AlgorithmInfo>(1);
|
|
|
|
/**
|
|
* Emits the copy button aria-label respective of the selected credential type
|
|
*/
|
|
protected credentialTypeCopyLabel$ = this.algorithm$.pipe(
|
|
filter((algorithm) => !!algorithm),
|
|
map(({ copy }) => copy),
|
|
);
|
|
|
|
/**
|
|
* Emits the generate button aria-label respective of the selected credential type
|
|
*/
|
|
protected credentialTypeGenerateLabel$ = this.algorithm$.pipe(
|
|
filter((algorithm) => !!algorithm),
|
|
map(({ generate }) => generate),
|
|
);
|
|
|
|
/**
|
|
* Emits the copy credential toast respective of the selected credential type
|
|
*/
|
|
protected credentialTypeLabel$ = this.algorithm$.pipe(
|
|
filter((algorithm) => !!algorithm),
|
|
map(({ generatedValue }) => generatedValue),
|
|
);
|
|
|
|
private toOptions(algorithms: AlgorithmInfo[]) {
|
|
const options: Option<CredentialAlgorithm>[] = algorithms.map((algorithm) => ({
|
|
value: algorithm.id,
|
|
label: algorithm.name,
|
|
}));
|
|
|
|
return options;
|
|
}
|
|
|
|
private readonly destroyed = new Subject<void>();
|
|
ngOnDestroy(): void {
|
|
// tear down subscriptions
|
|
this.destroyed.complete();
|
|
|
|
// finalize subjects
|
|
this.generate$.complete();
|
|
this.value$.complete();
|
|
|
|
// finalize component bindings
|
|
this.onGenerated.complete();
|
|
}
|
|
}
|