[PM-10439] Vault generator components (#11350)
* add password/username generator strings to web app
* remove JSLibServicesModule from generator dependencies
* integrate username and password generators into browser extension
* integrate username and password generators into web app
* move Jslib order back
* remove margin from within the generator components
* add alternative background to web generator dialog
* remove unneeded margin from username generator
* remove disable margin from div
* Revert "remove unneeded margin from username generator"
This reverts commit ae3067e3f2
.
* remove CSS to alter internal margins of the generator components
This commit is contained in:
parent
d4767bf172
commit
02993f7018
|
@ -1,4 +1,4 @@
|
|||
<bit-dialog dialogSize="default">
|
||||
<bit-dialog dialogSize="default" background="alt">
|
||||
<span bitDialogTitle>
|
||||
{{ title }}
|
||||
</span>
|
||||
|
|
|
@ -1,15 +1,11 @@
|
|||
import { DIALOG_DATA, DialogRef } from "@angular/cdk/dialog";
|
||||
import { DialogRef, DIALOG_DATA } from "@angular/cdk/dialog";
|
||||
import { Component, EventEmitter, Input, Output } from "@angular/core";
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { NoopAnimationsModule } from "@angular/platform-browser/animations";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import {
|
||||
PasswordGenerationServiceAbstraction,
|
||||
UsernameGenerationServiceAbstraction,
|
||||
} from "@bitwarden/generator-legacy";
|
||||
import { CipherFormGeneratorComponent } from "@bitwarden/vault";
|
||||
|
||||
import {
|
||||
|
@ -18,32 +14,26 @@ import {
|
|||
WebVaultGeneratorDialogParams,
|
||||
} from "./web-generator-dialog.component";
|
||||
|
||||
@Component({
|
||||
selector: "vault-cipher-form-generator",
|
||||
template: "",
|
||||
standalone: true,
|
||||
})
|
||||
class MockCipherFormGenerator {
|
||||
@Input() type: "password" | "username";
|
||||
@Output() valueGenerated = new EventEmitter<string>();
|
||||
}
|
||||
|
||||
describe("WebVaultGeneratorDialogComponent", () => {
|
||||
let component: WebVaultGeneratorDialogComponent;
|
||||
let fixture: ComponentFixture<WebVaultGeneratorDialogComponent>;
|
||||
|
||||
let dialogRef: MockProxy<DialogRef<any>>;
|
||||
let mockI18nService: MockProxy<I18nService>;
|
||||
let passwordOptionsSubject: BehaviorSubject<any>;
|
||||
let usernameOptionsSubject: BehaviorSubject<any>;
|
||||
let mockPasswordGenerationService: MockProxy<PasswordGenerationServiceAbstraction>;
|
||||
let mockUsernameGenerationService: MockProxy<UsernameGenerationServiceAbstraction>;
|
||||
|
||||
beforeEach(async () => {
|
||||
dialogRef = mock<DialogRef<any>>();
|
||||
mockI18nService = mock<I18nService>();
|
||||
passwordOptionsSubject = new BehaviorSubject([{ type: "password" }]);
|
||||
usernameOptionsSubject = new BehaviorSubject([{ type: "username" }]);
|
||||
|
||||
mockPasswordGenerationService = mock<PasswordGenerationServiceAbstraction>();
|
||||
mockPasswordGenerationService.getOptions$.mockReturnValue(
|
||||
passwordOptionsSubject.asObservable(),
|
||||
);
|
||||
|
||||
mockUsernameGenerationService = mock<UsernameGenerationServiceAbstraction>();
|
||||
mockUsernameGenerationService.getOptions$.mockReturnValue(
|
||||
usernameOptionsSubject.asObservable(),
|
||||
);
|
||||
|
||||
const mockDialogData: WebVaultGeneratorDialogParams = { type: "password" };
|
||||
|
||||
|
@ -66,23 +56,13 @@ describe("WebVaultGeneratorDialogComponent", () => {
|
|||
provide: PlatformUtilsService,
|
||||
useValue: mock<PlatformUtilsService>(),
|
||||
},
|
||||
{
|
||||
provide: PasswordGenerationServiceAbstraction,
|
||||
useValue: mockPasswordGenerationService,
|
||||
},
|
||||
{
|
||||
provide: UsernameGenerationServiceAbstraction,
|
||||
useValue: mockUsernameGenerationService,
|
||||
},
|
||||
{
|
||||
provide: CipherFormGeneratorComponent,
|
||||
useValue: {
|
||||
passwordOptions$: passwordOptionsSubject.asObservable(),
|
||||
usernameOptions$: usernameOptionsSubject.asObservable(),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compileComponents();
|
||||
})
|
||||
.overrideComponent(WebVaultGeneratorDialogComponent, {
|
||||
remove: { imports: [CipherFormGeneratorComponent] },
|
||||
add: { imports: [MockCipherFormGenerator] },
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(WebVaultGeneratorDialogComponent);
|
||||
component = fixture.componentInstance;
|
||||
|
|
|
@ -9266,6 +9266,42 @@
|
|||
"editAccess": {
|
||||
"message": "Edit access"
|
||||
},
|
||||
"uppercaseDescription": {
|
||||
"message": "Include uppercase characters",
|
||||
"description": "Tooltip for the password generator uppercase character checkbox"
|
||||
},
|
||||
"uppercaseLabel": {
|
||||
"message": "A-Z",
|
||||
"description": "Label for the password generator uppercase character checkbox"
|
||||
},
|
||||
"lowercaseDescription": {
|
||||
"message": "Include lowercase characters",
|
||||
"description": "Full description for the password generator lowercase character checkbox"
|
||||
},
|
||||
"lowercaseLabel": {
|
||||
"message": "a-z",
|
||||
"description": "Label for the password generator lowercase character checkbox"
|
||||
},
|
||||
"numbersDescription": {
|
||||
"message": "Include numbers",
|
||||
"description": "Full description for the password generator numbers checkbox"
|
||||
},
|
||||
"numbersLabel": {
|
||||
"message": "0-9",
|
||||
"description": "Label for the password generator numbers checkbox"
|
||||
},
|
||||
"specialCharactersDescription": {
|
||||
"message": "Include special characters",
|
||||
"description": "Full description for the password generator special characters checkbox"
|
||||
},
|
||||
"specialCharactersLabel": {
|
||||
"message": "!@#$%^&*",
|
||||
"description": "Label for the password generator special characters checkbox"
|
||||
},
|
||||
"avoidAmbiguous": {
|
||||
"message": "Avoid ambiguous characters",
|
||||
"description": "Label for the avoid ambiguous characters checkbox."
|
||||
},
|
||||
"addAttachment": {
|
||||
"message": "Add attachment"
|
||||
},
|
||||
|
|
|
@ -5,7 +5,6 @@ import { ReactiveFormsModule } from "@angular/forms";
|
|||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { safeProvider } from "@bitwarden/angular/platform/utils/safe-provider";
|
||||
import { SafeInjectionToken } from "@bitwarden/angular/services/injection-tokens";
|
||||
import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||
import { StateProvider } from "@bitwarden/common/platform/state";
|
||||
|
@ -43,7 +42,6 @@ const RANDOMIZER = new SafeInjectionToken<Randomizer>("Randomizer");
|
|||
InputModule,
|
||||
ItemModule,
|
||||
JslibModule,
|
||||
JslibServicesModule,
|
||||
ReactiveFormsModule,
|
||||
SectionComponent,
|
||||
SectionHeaderComponent,
|
||||
|
|
|
@ -1,62 +1,8 @@
|
|||
<bit-section>
|
||||
<!-- Password/Passphrase Toggle -->
|
||||
<bit-toggle-group
|
||||
*ngIf="isPassword"
|
||||
class="tw-w-full tw-justify-center tw-mt-3 tw-mb-5"
|
||||
(selectedChange)="updatePasswordType($event)"
|
||||
[selected]="passwordType$ | async"
|
||||
>
|
||||
<bit-toggle [value]="'password'">
|
||||
{{ "password" | i18n }}
|
||||
</bit-toggle>
|
||||
<bit-toggle [value]="'passphrase'">
|
||||
{{ "passphrase" | i18n }}
|
||||
</bit-toggle>
|
||||
</bit-toggle-group>
|
||||
|
||||
<!-- Generated Password/Passphrase/Username -->
|
||||
<bit-item>
|
||||
<bit-item-content>
|
||||
<bit-color-password [password]="generatedValue"></bit-color-password>
|
||||
</bit-item-content>
|
||||
<ng-container slot="end">
|
||||
<bit-item-action>
|
||||
<button
|
||||
type="button"
|
||||
bitIconButton="bwi-clone"
|
||||
size="small"
|
||||
[appCopyClick]="generatedValue"
|
||||
showToast
|
||||
[appA11yTitle]="'copyValue' | i18n"
|
||||
data-testid="copy-value-button"
|
||||
></button>
|
||||
</bit-item-action>
|
||||
<bit-item-action>
|
||||
<button
|
||||
type="button"
|
||||
bitIconButton="bwi-generate"
|
||||
size="small"
|
||||
(click)="regenerate$.next()"
|
||||
[appA11yTitle]="regenerateButtonTitle"
|
||||
data-testid="regenerate-button"
|
||||
></button>
|
||||
</bit-item-action>
|
||||
</ng-container>
|
||||
</bit-item>
|
||||
</bit-section>
|
||||
|
||||
<!-- Generator Options -->
|
||||
<!-- TODO: Replace with Generator Options Component(s) when available
|
||||
It is expected that the generator options component(s) will internally update the options stored in state
|
||||
which will trigger regeneration automatically in this dialog.
|
||||
-->
|
||||
<bit-section>
|
||||
<bit-section-header>
|
||||
<h2 bitTypography="h5">{{ "options" | i18n }}</h2>
|
||||
</bit-section-header>
|
||||
<bit-card>
|
||||
<em bitTypography="body2"
|
||||
>Placeholder: Replace with Generator Options Component(s) when available</em
|
||||
>
|
||||
</bit-card>
|
||||
</bit-section>
|
||||
<tools-password-generator
|
||||
*ngIf="type === 'password'"
|
||||
(onGenerated)="onCredentialGenerated($event)"
|
||||
></tools-password-generator>
|
||||
<tools-username-generator
|
||||
*ngIf="type === 'username'"
|
||||
(onGenerated)="onCredentialGenerated($event)"
|
||||
></tools-username-generator>
|
||||
|
|
|
@ -1,217 +1,103 @@
|
|||
import { ComponentFixture, fakeAsync, TestBed, tick } from "@angular/core/testing";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
import { Component, EventEmitter, Output } from "@angular/core";
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { By } from "@angular/platform-browser";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import {
|
||||
PasswordGenerationServiceAbstraction,
|
||||
PasswordGeneratorOptions,
|
||||
UsernameGenerationServiceAbstraction,
|
||||
UsernameGeneratorOptions,
|
||||
} from "@bitwarden/generator-legacy";
|
||||
PasswordGeneratorComponent,
|
||||
UsernameGeneratorComponent,
|
||||
} from "@bitwarden/generator-components";
|
||||
import { CipherFormGeneratorComponent } from "@bitwarden/vault";
|
||||
|
||||
@Component({
|
||||
selector: "tools-password-generator",
|
||||
template: `<ng-content></ng-content>`,
|
||||
standalone: true,
|
||||
})
|
||||
class MockPasswordGeneratorComponent {
|
||||
@Output() onGenerated = new EventEmitter();
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: "tools-username-generator",
|
||||
template: `<ng-content></ng-content>`,
|
||||
standalone: true,
|
||||
})
|
||||
class MockUsernameGeneratorComponent {
|
||||
@Output() onGenerated = new EventEmitter();
|
||||
}
|
||||
|
||||
describe("CipherFormGeneratorComponent", () => {
|
||||
let component: CipherFormGeneratorComponent;
|
||||
let fixture: ComponentFixture<CipherFormGeneratorComponent>;
|
||||
|
||||
let mockLegacyPasswordGenerationService: MockProxy<PasswordGenerationServiceAbstraction>;
|
||||
let mockLegacyUsernameGenerationService: MockProxy<UsernameGenerationServiceAbstraction>;
|
||||
let mockPlatformUtilsService: MockProxy<PlatformUtilsService>;
|
||||
|
||||
let passwordOptions$: BehaviorSubject<any>;
|
||||
let usernameOptions$: BehaviorSubject<any>;
|
||||
|
||||
beforeEach(async () => {
|
||||
passwordOptions$ = new BehaviorSubject([
|
||||
{
|
||||
type: "password",
|
||||
},
|
||||
] as [PasswordGeneratorOptions]);
|
||||
usernameOptions$ = new BehaviorSubject([
|
||||
{
|
||||
type: "word",
|
||||
},
|
||||
] as [UsernameGeneratorOptions]);
|
||||
|
||||
mockPlatformUtilsService = mock<PlatformUtilsService>();
|
||||
|
||||
mockLegacyPasswordGenerationService = mock<PasswordGenerationServiceAbstraction>();
|
||||
mockLegacyPasswordGenerationService.getOptions$.mockReturnValue(passwordOptions$);
|
||||
|
||||
mockLegacyUsernameGenerationService = mock<UsernameGenerationServiceAbstraction>();
|
||||
mockLegacyUsernameGenerationService.getOptions$.mockReturnValue(usernameOptions$);
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [CipherFormGeneratorComponent],
|
||||
providers: [
|
||||
{ provide: I18nService, useValue: { t: (key: string) => key } },
|
||||
{
|
||||
provide: PasswordGenerationServiceAbstraction,
|
||||
useValue: mockLegacyPasswordGenerationService,
|
||||
},
|
||||
{
|
||||
provide: UsernameGenerationServiceAbstraction,
|
||||
useValue: mockLegacyUsernameGenerationService,
|
||||
},
|
||||
{ provide: PlatformUtilsService, useValue: mockPlatformUtilsService },
|
||||
],
|
||||
}).compileComponents();
|
||||
providers: [{ provide: I18nService, useValue: { t: (key: string) => key } }],
|
||||
})
|
||||
.overrideComponent(CipherFormGeneratorComponent, {
|
||||
remove: { imports: [PasswordGeneratorComponent, UsernameGeneratorComponent] },
|
||||
add: { imports: [MockPasswordGeneratorComponent, MockUsernameGeneratorComponent] },
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(CipherFormGeneratorComponent);
|
||||
component = fixture.componentInstance;
|
||||
});
|
||||
|
||||
it("should create", () => {
|
||||
fixture.detectChanges();
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should use the appropriate text based on generator type", () => {
|
||||
component.type = "password";
|
||||
component.ngOnChanges();
|
||||
expect(component["regenerateButtonTitle"]).toBe("regeneratePassword");
|
||||
|
||||
component.type = "username";
|
||||
component.ngOnChanges();
|
||||
expect(component["regenerateButtonTitle"]).toBe("regenerateUsername");
|
||||
});
|
||||
|
||||
it("should emit regenerate$ when user clicks the regenerate button", fakeAsync(() => {
|
||||
const regenerateSpy = jest.spyOn(component["regenerate$"], "next");
|
||||
|
||||
fixture.nativeElement.querySelector("button[data-testid='regenerate-button']").click();
|
||||
|
||||
expect(regenerateSpy).toHaveBeenCalled();
|
||||
}));
|
||||
|
||||
it("should emit valueGenerated whenever a new value is generated", fakeAsync(() => {
|
||||
const valueGeneratedSpy = jest.spyOn(component.valueGenerated, "emit");
|
||||
|
||||
mockLegacyPasswordGenerationService.generatePassword.mockResolvedValue("generated-password");
|
||||
component.type = "password";
|
||||
|
||||
component.ngOnChanges();
|
||||
tick();
|
||||
|
||||
expect(valueGeneratedSpy).toHaveBeenCalledWith("generated-password");
|
||||
}));
|
||||
|
||||
describe("password generation", () => {
|
||||
let passwordGenerator: MockPasswordGeneratorComponent;
|
||||
|
||||
beforeEach(() => {
|
||||
component.type = "password";
|
||||
});
|
||||
|
||||
it("should update the generated value when the password options change", fakeAsync(() => {
|
||||
mockLegacyPasswordGenerationService.generatePassword
|
||||
.mockResolvedValueOnce("first-password")
|
||||
.mockResolvedValueOnce("second-password");
|
||||
|
||||
component.ngOnChanges();
|
||||
tick();
|
||||
|
||||
expect(component["generatedValue"]).toBe("first-password");
|
||||
|
||||
passwordOptions$.next([{ type: "password" }]);
|
||||
tick();
|
||||
|
||||
expect(component["generatedValue"]).toBe("second-password");
|
||||
expect(mockLegacyPasswordGenerationService.generatePassword).toHaveBeenCalledTimes(2);
|
||||
}));
|
||||
|
||||
it("should show password type toggle when the generator type is password", () => {
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(fixture.nativeElement.querySelector("bit-toggle-group")).toBeTruthy();
|
||||
passwordGenerator = fixture.debugElement.query(
|
||||
By.directive(MockPasswordGeneratorComponent),
|
||||
).componentInstance;
|
||||
});
|
||||
|
||||
it("should update the generated value when the password type is updated", fakeAsync(async () => {
|
||||
mockLegacyPasswordGenerationService.generatePassword
|
||||
.mockResolvedValueOnce("first-password")
|
||||
.mockResolvedValueOnce("second-password");
|
||||
it("only shows `PasswordGeneratorComponent`", () => {
|
||||
expect(passwordGenerator).toBeTruthy();
|
||||
expect(fixture.debugElement.query(By.directive(MockUsernameGeneratorComponent))).toBeNull();
|
||||
});
|
||||
|
||||
component.ngOnChanges();
|
||||
tick();
|
||||
it("invokes `valueGenerated` with the generated credential", () => {
|
||||
jest.spyOn(component.valueGenerated, "emit");
|
||||
|
||||
expect(component["generatedValue"]).toBe("first-password");
|
||||
passwordGenerator.onGenerated.emit({ credential: "new-cred-password!" });
|
||||
|
||||
await component["updatePasswordType"]("passphrase");
|
||||
tick();
|
||||
|
||||
expect(component["generatedValue"]).toBe("second-password");
|
||||
expect(mockLegacyPasswordGenerationService.generatePassword).toHaveBeenCalledTimes(2);
|
||||
}));
|
||||
|
||||
it("should update the password history when a new password is generated", fakeAsync(() => {
|
||||
mockLegacyPasswordGenerationService.generatePassword.mockResolvedValue("new-password");
|
||||
|
||||
component.ngOnChanges();
|
||||
tick();
|
||||
|
||||
expect(mockLegacyPasswordGenerationService.generatePassword).toHaveBeenCalledTimes(1);
|
||||
expect(mockLegacyPasswordGenerationService.addHistory).toHaveBeenCalledWith("new-password");
|
||||
expect(component["generatedValue"]).toBe("new-password");
|
||||
}));
|
||||
|
||||
it("should regenerate the password when regenerate$ emits", fakeAsync(() => {
|
||||
mockLegacyPasswordGenerationService.generatePassword
|
||||
.mockResolvedValueOnce("first-password")
|
||||
.mockResolvedValueOnce("second-password");
|
||||
|
||||
component.ngOnChanges();
|
||||
tick();
|
||||
|
||||
expect(component["generatedValue"]).toBe("first-password");
|
||||
|
||||
component["regenerate$"].next();
|
||||
tick();
|
||||
|
||||
expect(component["generatedValue"]).toBe("second-password");
|
||||
}));
|
||||
expect(component.valueGenerated.emit).toHaveBeenCalledTimes(1);
|
||||
expect(component.valueGenerated.emit).toHaveBeenCalledWith("new-cred-password!");
|
||||
});
|
||||
});
|
||||
|
||||
describe("username generation", () => {
|
||||
let usernameGenerator: MockUsernameGeneratorComponent;
|
||||
|
||||
beforeEach(() => {
|
||||
component.type = "username";
|
||||
});
|
||||
|
||||
it("should update the generated value when the username options change", fakeAsync(() => {
|
||||
mockLegacyUsernameGenerationService.generateUsername
|
||||
.mockResolvedValueOnce("first-username")
|
||||
.mockResolvedValueOnce("second-username");
|
||||
|
||||
component.ngOnChanges();
|
||||
tick();
|
||||
|
||||
expect(component["generatedValue"]).toBe("first-username");
|
||||
|
||||
usernameOptions$.next([{ type: "word" }]);
|
||||
tick();
|
||||
|
||||
expect(component["generatedValue"]).toBe("second-username");
|
||||
}));
|
||||
|
||||
it("should regenerate the username when regenerate$ emits", fakeAsync(() => {
|
||||
mockLegacyUsernameGenerationService.generateUsername
|
||||
.mockResolvedValueOnce("first-username")
|
||||
.mockResolvedValueOnce("second-username");
|
||||
|
||||
component.ngOnChanges();
|
||||
tick();
|
||||
|
||||
expect(component["generatedValue"]).toBe("first-username");
|
||||
|
||||
component["regenerate$"].next();
|
||||
tick();
|
||||
|
||||
expect(component["generatedValue"]).toBe("second-username");
|
||||
}));
|
||||
|
||||
it("should not show password type toggle when the generator type is username", () => {
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(fixture.nativeElement.querySelector("bit-toggle-group")).toBeNull();
|
||||
usernameGenerator = fixture.debugElement.query(
|
||||
By.directive(MockUsernameGeneratorComponent),
|
||||
).componentInstance;
|
||||
});
|
||||
|
||||
it("only shows `UsernameGeneratorComponent`", () => {
|
||||
expect(usernameGenerator).toBeTruthy();
|
||||
expect(fixture.debugElement.query(By.directive(MockPasswordGeneratorComponent))).toBeNull();
|
||||
});
|
||||
|
||||
it("invokes `valueGenerated` with the generated credential", () => {
|
||||
jest.spyOn(component.valueGenerated, "emit");
|
||||
|
||||
usernameGenerator.onGenerated.emit({ credential: "new-cred-username!" });
|
||||
|
||||
expect(component.valueGenerated.emit).toHaveBeenCalledTimes(1);
|
||||
expect(component.valueGenerated.emit).toHaveBeenCalledWith("new-cred-username!");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,36 +1,12 @@
|
|||
import { CommonModule } from "@angular/common";
|
||||
import { Component, DestroyRef, EventEmitter, Input, OnChanges, Output } from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import {
|
||||
combineLatest,
|
||||
map,
|
||||
merge,
|
||||
shareReplay,
|
||||
startWith,
|
||||
Subject,
|
||||
Subscription,
|
||||
switchMap,
|
||||
take,
|
||||
tap,
|
||||
} from "rxjs";
|
||||
import { Component, EventEmitter, Input, Output } from "@angular/core";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { SectionComponent } from "@bitwarden/components";
|
||||
import {
|
||||
CardComponent,
|
||||
ColorPasswordModule,
|
||||
IconButtonModule,
|
||||
ItemModule,
|
||||
SectionComponent,
|
||||
SectionHeaderComponent,
|
||||
ToggleGroupModule,
|
||||
TypographyModule,
|
||||
} from "@bitwarden/components";
|
||||
import { GeneratorType } from "@bitwarden/generator-core";
|
||||
import {
|
||||
PasswordGenerationServiceAbstraction,
|
||||
UsernameGenerationServiceAbstraction,
|
||||
} from "@bitwarden/generator-legacy";
|
||||
PasswordGeneratorComponent,
|
||||
UsernameGeneratorComponent,
|
||||
} from "@bitwarden/generator-components";
|
||||
import { GeneratedCredential } from "@bitwarden/generator-core";
|
||||
|
||||
/**
|
||||
* Renders a password or username generator UI and emits the most recently generated value.
|
||||
|
@ -40,20 +16,9 @@ import {
|
|||
selector: "vault-cipher-form-generator",
|
||||
templateUrl: "./cipher-form-generator.component.html",
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
CardComponent,
|
||||
SectionComponent,
|
||||
ToggleGroupModule,
|
||||
JslibModule,
|
||||
ItemModule,
|
||||
ColorPasswordModule,
|
||||
IconButtonModule,
|
||||
SectionHeaderComponent,
|
||||
TypographyModule,
|
||||
],
|
||||
imports: [CommonModule, SectionComponent, PasswordGeneratorComponent, UsernameGeneratorComponent],
|
||||
})
|
||||
export class CipherFormGeneratorComponent implements OnChanges {
|
||||
export class CipherFormGeneratorComponent {
|
||||
/**
|
||||
* The type of generator form to show.
|
||||
*/
|
||||
|
@ -66,118 +31,8 @@ export class CipherFormGeneratorComponent implements OnChanges {
|
|||
@Output()
|
||||
valueGenerated = new EventEmitter<string>();
|
||||
|
||||
protected get isPassword() {
|
||||
return this.type === "password";
|
||||
}
|
||||
|
||||
protected regenerateButtonTitle: string;
|
||||
protected regenerate$ = new Subject<void>();
|
||||
protected passwordTypeSubject$ = new Subject<GeneratorType>();
|
||||
/**
|
||||
* The currently generated value displayed to the user.
|
||||
* @protected
|
||||
*/
|
||||
protected generatedValue: string = "";
|
||||
|
||||
/**
|
||||
* The current username generation options.
|
||||
* @private
|
||||
*/
|
||||
private usernameOptions$ = this.legacyUsernameGenerationService.getOptions$();
|
||||
|
||||
/**
|
||||
* The current password type selected in the UI. Starts with the saved value from the service.
|
||||
* @protected
|
||||
*/
|
||||
protected passwordType$ = merge(
|
||||
this.legacyPasswordGenerationService.getOptions$().pipe(
|
||||
take(1),
|
||||
map(([options]) => options.type),
|
||||
),
|
||||
this.passwordTypeSubject$,
|
||||
).pipe(shareReplay({ bufferSize: 1, refCount: false }));
|
||||
|
||||
/**
|
||||
* The current password generation options.
|
||||
* @private
|
||||
*/
|
||||
private passwordOptions$ = combineLatest([
|
||||
this.legacyPasswordGenerationService.getOptions$(),
|
||||
this.passwordType$,
|
||||
]).pipe(
|
||||
map(([[options], type]) => {
|
||||
options.type = type;
|
||||
return options;
|
||||
}),
|
||||
);
|
||||
|
||||
/**
|
||||
* Tracks the regenerate$ subscription
|
||||
* @private
|
||||
*/
|
||||
private subscription: Subscription | null;
|
||||
|
||||
constructor(
|
||||
private i18nService: I18nService,
|
||||
private legacyPasswordGenerationService: PasswordGenerationServiceAbstraction,
|
||||
private legacyUsernameGenerationService: UsernameGenerationServiceAbstraction,
|
||||
private destroyRef: DestroyRef,
|
||||
) {}
|
||||
|
||||
ngOnChanges() {
|
||||
this.regenerateButtonTitle = this.i18nService.t(
|
||||
this.isPassword ? "regeneratePassword" : "regenerateUsername",
|
||||
);
|
||||
|
||||
// If we have a previous subscription, clear it
|
||||
if (this.subscription) {
|
||||
this.subscription.unsubscribe();
|
||||
this.subscription = null;
|
||||
}
|
||||
|
||||
if (this.isPassword) {
|
||||
this.setupPasswordGeneration();
|
||||
} else {
|
||||
this.setupUsernameGeneration();
|
||||
}
|
||||
}
|
||||
|
||||
private setupPasswordGeneration() {
|
||||
this.subscription = this.regenerate$
|
||||
.pipe(
|
||||
startWith(null),
|
||||
switchMap(() => this.passwordOptions$),
|
||||
switchMap((options) => this.legacyPasswordGenerationService.generatePassword(options)),
|
||||
tap(async (password) => {
|
||||
await this.legacyPasswordGenerationService.addHistory(password);
|
||||
}),
|
||||
takeUntilDestroyed(this.destroyRef),
|
||||
)
|
||||
.subscribe((password) => {
|
||||
this.generatedValue = password;
|
||||
this.valueGenerated.emit(password);
|
||||
});
|
||||
}
|
||||
|
||||
private setupUsernameGeneration() {
|
||||
this.subscription = this.regenerate$
|
||||
.pipe(
|
||||
startWith(null),
|
||||
switchMap(() => this.usernameOptions$),
|
||||
switchMap((options) => this.legacyUsernameGenerationService.generateUsername(options)),
|
||||
takeUntilDestroyed(this.destroyRef),
|
||||
)
|
||||
.subscribe((username) => {
|
||||
this.generatedValue = username;
|
||||
this.valueGenerated.emit(username);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch the password generation type.
|
||||
* @param value The new password generation type.
|
||||
*/
|
||||
protected updatePasswordType = async (value: GeneratorType) => {
|
||||
this.passwordTypeSubject$.next(value);
|
||||
/** Event handler for both generation components */
|
||||
onCredentialGenerated = (generatedCred: GeneratedCredential) => {
|
||||
this.valueGenerated.emit(generatedCred.credential);
|
||||
};
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue