mirror of
https://github.com/bitwarden/browser
synced 2025-12-15 15:53:27 +00:00
[PM-6819] Credential generator MV3 integration (#8998)
* replace `PasswordGeneratorService` with `legacyPasswordGenerationServiceFactory` * replace `UsernameGeneratorService` with `legacyUsernameGenerationServiceFactory` * migrate generator options and history * apply policy immediately once available * suppress duplicate policy emissions * run password generation response code in `ngZone`
This commit is contained in:
@@ -193,13 +193,11 @@ import { SearchService } from "@bitwarden/common/services/search.service";
|
||||
import { VaultTimeoutSettingsService } from "@bitwarden/common/services/vault-timeout/vault-timeout-settings.service";
|
||||
import { VaultTimeoutService } from "@bitwarden/common/services/vault-timeout/vault-timeout.service";
|
||||
import {
|
||||
PasswordGenerationService,
|
||||
PasswordGenerationServiceAbstraction,
|
||||
} from "@bitwarden/common/tools/generator/password";
|
||||
import {
|
||||
UsernameGenerationService,
|
||||
UsernameGenerationServiceAbstraction,
|
||||
} from "@bitwarden/common/tools/generator/username";
|
||||
legacyPasswordGenerationServiceFactory,
|
||||
legacyUsernameGenerationServiceFactory,
|
||||
} from "@bitwarden/common/tools/generator";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
|
||||
import { UsernameGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/username";
|
||||
import {
|
||||
PasswordStrengthService,
|
||||
PasswordStrengthServiceAbstraction,
|
||||
@@ -559,13 +557,27 @@ const safeProviders: SafeProvider[] = [
|
||||
}),
|
||||
safeProvider({
|
||||
provide: PasswordGenerationServiceAbstraction,
|
||||
useClass: PasswordGenerationService,
|
||||
deps: [CryptoServiceAbstraction, PolicyServiceAbstraction, StateServiceAbstraction],
|
||||
useFactory: legacyPasswordGenerationServiceFactory,
|
||||
deps: [
|
||||
EncryptService,
|
||||
CryptoServiceAbstraction,
|
||||
PolicyServiceAbstraction,
|
||||
AccountServiceAbstraction,
|
||||
StateProvider,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: UsernameGenerationServiceAbstraction,
|
||||
useClass: UsernameGenerationService,
|
||||
deps: [CryptoServiceAbstraction, StateServiceAbstraction, ApiServiceAbstraction],
|
||||
useFactory: legacyUsernameGenerationServiceFactory,
|
||||
deps: [
|
||||
ApiServiceAbstraction,
|
||||
I18nServiceAbstraction,
|
||||
CryptoServiceAbstraction,
|
||||
EncryptService,
|
||||
PolicyServiceAbstraction,
|
||||
AccountServiceAbstraction,
|
||||
StateProvider,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: ApiServiceAbstraction,
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import { Directive, EventEmitter, Input, OnInit, Output } from "@angular/core";
|
||||
import { Directive, EventEmitter, Input, NgZone, OnDestroy, OnInit, Output } from "@angular/core";
|
||||
import { ActivatedRoute } from "@angular/router";
|
||||
import { BehaviorSubject, firstValueFrom } from "rxjs";
|
||||
import { debounceTime, first, map } from "rxjs/operators";
|
||||
import { BehaviorSubject, combineLatest, firstValueFrom, Subject } from "rxjs";
|
||||
import { debounceTime, first, map, skipWhile, takeUntil } from "rxjs/operators";
|
||||
|
||||
import { PasswordGeneratorPolicyOptions } from "@bitwarden/common/admin-console/models/domain/password-generator-policy-options";
|
||||
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 { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { GeneratorOptions } from "@bitwarden/common/tools/generator/generator-options";
|
||||
import { GeneratorType } from "@bitwarden/common/tools/generator/generator-type";
|
||||
import {
|
||||
PasswordGenerationServiceAbstraction,
|
||||
PasswordGeneratorOptions,
|
||||
@@ -22,9 +21,9 @@ import {
|
||||
import { EmailForwarderOptions } from "@bitwarden/common/tools/models/domain/email-forwarder-options";
|
||||
|
||||
@Directive()
|
||||
export class GeneratorComponent implements OnInit {
|
||||
export class GeneratorComponent implements OnInit, OnDestroy {
|
||||
@Input() comingFromAddEdit = false;
|
||||
@Input() type: string;
|
||||
@Input() type: GeneratorType | "";
|
||||
@Output() onSelected = new EventEmitter<string>();
|
||||
|
||||
usernameGeneratingPromise: Promise<string>;
|
||||
@@ -43,6 +42,9 @@ export class GeneratorComponent implements OnInit {
|
||||
enforcedPasswordPolicyOptions: PasswordGeneratorPolicyOptions;
|
||||
usernameWebsite: string = null;
|
||||
|
||||
private destroy$ = new Subject<void>();
|
||||
private isInitialized$ = new BehaviorSubject(false);
|
||||
|
||||
// update screen reader minimum password length with 500ms debounce
|
||||
// so that the user isn't flooded with status updates
|
||||
private _passwordOptionsMinLengthForReader = new BehaviorSubject<number>(
|
||||
@@ -53,15 +55,17 @@ export class GeneratorComponent implements OnInit {
|
||||
debounceTime(500),
|
||||
);
|
||||
|
||||
private _password = new BehaviorSubject<string>("-");
|
||||
|
||||
constructor(
|
||||
protected passwordGenerationService: PasswordGenerationServiceAbstraction,
|
||||
protected usernameGenerationService: UsernameGenerationServiceAbstraction,
|
||||
protected platformUtilsService: PlatformUtilsService,
|
||||
protected stateService: StateService,
|
||||
protected accountService: AccountService,
|
||||
protected i18nService: I18nService,
|
||||
protected logService: LogService,
|
||||
protected route: ActivatedRoute,
|
||||
protected accountService: AccountService,
|
||||
protected ngZone: NgZone,
|
||||
private win: Window,
|
||||
) {
|
||||
this.typeOptions = [
|
||||
@@ -92,61 +96,115 @@ export class GeneratorComponent implements OnInit {
|
||||
];
|
||||
this.subaddressOptions = [{ name: i18nService.t("random"), value: "random" }];
|
||||
this.catchallOptions = [{ name: i18nService.t("random"), value: "random" }];
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.initForwardOptions();
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
// eslint-disable-next-line rxjs/no-async-subscribe
|
||||
this.route.queryParams.pipe(first()).subscribe(async (qParams) => {
|
||||
const passwordOptionsResponse = await this.passwordGenerationService.getOptions();
|
||||
this.passwordOptions = passwordOptionsResponse[0];
|
||||
this.enforcedPasswordPolicyOptions = passwordOptionsResponse[1];
|
||||
this.avoidAmbiguous = !this.passwordOptions.ambiguous;
|
||||
this.passwordOptions.type =
|
||||
this.passwordOptions.type === "passphrase" ? "passphrase" : "password";
|
||||
this.forwardOptions = [
|
||||
{ name: "", value: "", validForSelfHosted: false },
|
||||
{ name: "addy.io", value: "anonaddy", validForSelfHosted: true },
|
||||
{ name: "DuckDuckGo", value: "duckduckgo", validForSelfHosted: false },
|
||||
{ name: "Fastmail", value: "fastmail", validForSelfHosted: true },
|
||||
{ name: "Firefox Relay", value: "firefoxrelay", validForSelfHosted: false },
|
||||
{ name: "SimpleLogin", value: "simplelogin", validForSelfHosted: true },
|
||||
{ name: "Forward Email", value: "forwardemail", validForSelfHosted: true },
|
||||
].sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
this.usernameOptions = await this.usernameGenerationService.getOptions();
|
||||
if (this.usernameOptions.type == null) {
|
||||
this.usernameOptions.type = "word";
|
||||
}
|
||||
if (
|
||||
this.usernameOptions.subaddressEmail == null ||
|
||||
this.usernameOptions.subaddressEmail === ""
|
||||
) {
|
||||
this.usernameOptions.subaddressEmail = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.email)),
|
||||
);
|
||||
}
|
||||
if (this.usernameWebsite == null) {
|
||||
this.usernameOptions.subaddressType = this.usernameOptions.catchallType = "random";
|
||||
} else {
|
||||
this.usernameOptions.website = this.usernameWebsite;
|
||||
const websiteOption = { name: this.i18nService.t("websiteName"), value: "website-name" };
|
||||
this.subaddressOptions.push(websiteOption);
|
||||
this.catchallOptions.push(websiteOption);
|
||||
}
|
||||
|
||||
if (this.type !== "username" && this.type !== "password") {
|
||||
if (qParams.type === "username" || qParams.type === "password") {
|
||||
this.type = qParams.type;
|
||||
} else {
|
||||
const generatorOptions = await this.stateService.getGeneratorOptions();
|
||||
this.type = generatorOptions?.type ?? "password";
|
||||
}
|
||||
}
|
||||
if (this.regenerateWithoutButtonPress()) {
|
||||
await this.regenerate();
|
||||
}
|
||||
this._password.pipe(debounceTime(250)).subscribe((password) => {
|
||||
ngZone.run(() => {
|
||||
this.password = password;
|
||||
});
|
||||
this.passwordGenerationService.addHistory(this.password).catch((e) => {
|
||||
this.logService.error(e);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async typeChanged() {
|
||||
await this.stateService.setGeneratorOptions({ type: this.type } as GeneratorOptions);
|
||||
if (this.regenerateWithoutButtonPress()) {
|
||||
await this.regenerate();
|
||||
cascadeOptions(navigationType: GeneratorType = undefined, accountEmail: string) {
|
||||
this.avoidAmbiguous = !this.passwordOptions.ambiguous;
|
||||
|
||||
if (!this.type) {
|
||||
if (navigationType) {
|
||||
this.type = navigationType;
|
||||
} else {
|
||||
this.type = this.passwordOptions.type === "username" ? "username" : "password";
|
||||
}
|
||||
}
|
||||
|
||||
this.passwordOptions.type =
|
||||
this.passwordOptions.type === "passphrase" ? "passphrase" : "password";
|
||||
|
||||
if (this.usernameOptions.type == null) {
|
||||
this.usernameOptions.type = "word";
|
||||
}
|
||||
if (
|
||||
this.usernameOptions.subaddressEmail == null ||
|
||||
this.usernameOptions.subaddressEmail === ""
|
||||
) {
|
||||
this.usernameOptions.subaddressEmail = accountEmail;
|
||||
}
|
||||
if (this.usernameWebsite == null) {
|
||||
this.usernameOptions.subaddressType = this.usernameOptions.catchallType = "random";
|
||||
} else {
|
||||
this.usernameOptions.website = this.usernameWebsite;
|
||||
const websiteOption = { name: this.i18nService.t("websiteName"), value: "website-name" };
|
||||
this.subaddressOptions.push(websiteOption);
|
||||
this.catchallOptions.push(websiteOption);
|
||||
}
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
combineLatest([
|
||||
this.route.queryParams.pipe(first()),
|
||||
this.accountService.activeAccount$.pipe(first()),
|
||||
this.passwordGenerationService.getOptions$(),
|
||||
this.usernameGenerationService.getOptions$(),
|
||||
])
|
||||
.pipe(
|
||||
map(([qParams, account, [passwordOptions, passwordPolicy], usernameOptions]) => ({
|
||||
navigationType: qParams.type as GeneratorType,
|
||||
accountEmail: account.email,
|
||||
passwordOptions,
|
||||
passwordPolicy,
|
||||
usernameOptions,
|
||||
})),
|
||||
takeUntil(this.destroy$),
|
||||
)
|
||||
.subscribe((options) => {
|
||||
this.passwordOptions = options.passwordOptions;
|
||||
this.enforcedPasswordPolicyOptions = options.passwordPolicy;
|
||||
this.usernameOptions = options.usernameOptions;
|
||||
|
||||
this.cascadeOptions(options.navigationType, options.accountEmail);
|
||||
this._passwordOptionsMinLengthForReader.next(this.passwordOptions.minLength);
|
||||
|
||||
if (this.regenerateWithoutButtonPress()) {
|
||||
this.regenerate().catch((e) => {
|
||||
this.logService.error(e);
|
||||
});
|
||||
}
|
||||
|
||||
this.isInitialized$.next(true);
|
||||
});
|
||||
|
||||
// once initialization is complete, `ngOnInit` should return.
|
||||
//
|
||||
// FIXME(#6944): if a sync is in progress, wait to complete until after
|
||||
// the sync completes.
|
||||
await firstValueFrom(
|
||||
this.isInitialized$.pipe(
|
||||
skipWhile((initialized) => !initialized),
|
||||
takeUntil(this.destroy$),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
this.isInitialized$.complete();
|
||||
this._passwordOptionsMinLengthForReader.complete();
|
||||
}
|
||||
|
||||
async typeChanged() {
|
||||
await this.savePasswordOptions();
|
||||
}
|
||||
|
||||
async regenerate() {
|
||||
@@ -160,7 +218,7 @@ export class GeneratorComponent implements OnInit {
|
||||
async sliderChanged() {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.savePasswordOptions(false);
|
||||
this.savePasswordOptions();
|
||||
await this.passwordGenerationService.addHistory(this.password);
|
||||
}
|
||||
|
||||
@@ -204,31 +262,34 @@ export class GeneratorComponent implements OnInit {
|
||||
|
||||
async sliderInput() {
|
||||
await this.normalizePasswordOptions();
|
||||
this.password = await this.passwordGenerationService.generatePassword(this.passwordOptions);
|
||||
}
|
||||
|
||||
async savePasswordOptions(regenerate = true) {
|
||||
async savePasswordOptions() {
|
||||
// map navigation state into generator type
|
||||
const restoreType = this.passwordOptions.type;
|
||||
if (this.type === "username") {
|
||||
this.passwordOptions.type = this.type;
|
||||
}
|
||||
|
||||
// save options
|
||||
await this.normalizePasswordOptions();
|
||||
await this.passwordGenerationService.saveOptions(this.passwordOptions);
|
||||
|
||||
if (regenerate && this.regenerateWithoutButtonPress()) {
|
||||
await this.regeneratePassword();
|
||||
}
|
||||
// restore the original format
|
||||
this.passwordOptions.type = restoreType;
|
||||
}
|
||||
|
||||
async saveUsernameOptions(regenerate = true) {
|
||||
async saveUsernameOptions() {
|
||||
await this.usernameGenerationService.saveOptions(this.usernameOptions);
|
||||
if (this.usernameOptions.type === "forwarded") {
|
||||
this.username = "-";
|
||||
}
|
||||
if (regenerate && this.regenerateWithoutButtonPress()) {
|
||||
await this.regenerateUsername();
|
||||
}
|
||||
}
|
||||
|
||||
async regeneratePassword() {
|
||||
this.password = await this.passwordGenerationService.generatePassword(this.passwordOptions);
|
||||
await this.passwordGenerationService.addHistory(this.password);
|
||||
this._password.next(
|
||||
await this.passwordGenerationService.generatePassword(this.passwordOptions),
|
||||
);
|
||||
}
|
||||
|
||||
regenerateUsername() {
|
||||
@@ -297,28 +358,5 @@ export class GeneratorComponent implements OnInit {
|
||||
await this.passwordGenerationService.enforcePasswordGeneratorPoliciesOnOptions(
|
||||
this.passwordOptions,
|
||||
);
|
||||
|
||||
this._passwordOptionsMinLengthForReader.next(this.passwordOptions.minLength);
|
||||
}
|
||||
|
||||
private async initForwardOptions() {
|
||||
this.forwardOptions = [
|
||||
{ name: "addy.io", value: "anonaddy", validForSelfHosted: true },
|
||||
{ name: "DuckDuckGo", value: "duckduckgo", validForSelfHosted: false },
|
||||
{ name: "Fastmail", value: "fastmail", validForSelfHosted: true },
|
||||
{ name: "Firefox Relay", value: "firefoxrelay", validForSelfHosted: false },
|
||||
{ name: "SimpleLogin", value: "simplelogin", validForSelfHosted: true },
|
||||
{ name: "Forward Email", value: "forwardemail", validForSelfHosted: true },
|
||||
];
|
||||
|
||||
this.usernameOptions = await this.usernameGenerationService.getOptions();
|
||||
if (
|
||||
this.usernameOptions.forwardedService == null ||
|
||||
this.usernameOptions.forwardedService === ""
|
||||
) {
|
||||
this.forwardOptions.push({ name: "", value: null, validForSelfHosted: false });
|
||||
}
|
||||
|
||||
this.forwardOptions = this.forwardOptions.sort((a, b) => a.name.localeCompare(b.name));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,8 +23,7 @@ export class PasswordGeneratorHistoryComponent implements OnInit {
|
||||
}
|
||||
|
||||
clear = async () => {
|
||||
this.history = [];
|
||||
await this.passwordGenerationService.clear();
|
||||
this.history = await this.passwordGenerationService.clear();
|
||||
};
|
||||
|
||||
copy(password: string) {
|
||||
|
||||
Reference in New Issue
Block a user