1
0
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:
✨ Audrey ✨
2024-05-20 13:08:49 -04:00
committed by GitHub
parent 97c7ef3f21
commit a16dc84a0a
59 changed files with 1995 additions and 399 deletions

View File

@@ -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,

View File

@@ -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));
}
}

View File

@@ -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) {