mirror of
https://github.com/bitwarden/browser
synced 2025-12-14 07:13:32 +00:00
[PM-9675] Browser Refresh Login - Generator dialog (#10352)
* [PM-9675] Introduce CipherFormGenerator component * [PM-9675] Introduce VaultGeneratorDialog component for Browser * [PM-9675] Introduce BrowserCipherFormGeneration Service * [PM-9675] Fix aria label on popup header * [PM-9675] Cleanup html * [PM-9675] Cleanup vault generator dialog spec file
This commit is contained in:
@@ -1803,6 +1803,18 @@
|
||||
"passwordGeneratorPolicyInEffect": {
|
||||
"message": "One or more organization policies are affecting your generator settings."
|
||||
},
|
||||
"passwordGenerator": {
|
||||
"message": "Password generator"
|
||||
},
|
||||
"usernameGenerator": {
|
||||
"message": "Username generator"
|
||||
},
|
||||
"useThisPassword": {
|
||||
"message": "Use this password"
|
||||
},
|
||||
"useThisUsername": {
|
||||
"message": "Use this username"
|
||||
},
|
||||
"vaultTimeoutAction": {
|
||||
"message": "Vault timeout action"
|
||||
},
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
type="button"
|
||||
*ngIf="showBackButton"
|
||||
[title]="'back' | i18n"
|
||||
[ariaLabel]="'back' | i18n"
|
||||
[attr.aria-label]="'back' | i18n"
|
||||
[bitAction]="backAction"
|
||||
></button>
|
||||
<h1 *ngIf="pageTitle" bitTypography="h3" class="!tw-mb-0.5 tw-text-headers">
|
||||
|
||||
@@ -14,6 +14,7 @@ import { AsyncActionsModule, ButtonModule, SearchModule } from "@bitwarden/compo
|
||||
import {
|
||||
CipherFormConfig,
|
||||
CipherFormConfigService,
|
||||
CipherFormGenerationService,
|
||||
CipherFormMode,
|
||||
CipherFormModule,
|
||||
DefaultCipherFormConfigService,
|
||||
@@ -27,6 +28,7 @@ import { PopupHeaderComponent } from "../../../../../platform/popup/layout/popup
|
||||
import { PopupPageComponent } from "../../../../../platform/popup/layout/popup-page.component";
|
||||
import { PopupCloseWarningService } from "../../../../../popup/services/popup-close-warning.service";
|
||||
import { BrowserFido2UserInterfaceSession } from "../../../../fido2/browser-fido2-user-interface.service";
|
||||
import { BrowserCipherFormGenerationService } from "../../../services/browser-cipher-form-generation.service";
|
||||
import { BrowserTotpCaptureService } from "../../../services/browser-totp-capture.service";
|
||||
import {
|
||||
fido2PopoutSessionData$,
|
||||
@@ -106,6 +108,7 @@ export type AddEditQueryParams = Partial<Record<keyof QueryParams, string>>;
|
||||
providers: [
|
||||
{ provide: CipherFormConfigService, useClass: DefaultCipherFormConfigService },
|
||||
{ provide: TotpCaptureService, useClass: BrowserTotpCaptureService },
|
||||
{ provide: CipherFormGenerationService, useClass: BrowserCipherFormGenerationService },
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
<popup-page @slideIn>
|
||||
<popup-header
|
||||
slot="header"
|
||||
[backAction]="close"
|
||||
showBackButton
|
||||
[pageTitle]="title"
|
||||
></popup-header>
|
||||
|
||||
<vault-cipher-form-generator
|
||||
[type]="params.type"
|
||||
(valueGenerated)="onValueGenerated($event)"
|
||||
></vault-cipher-form-generator>
|
||||
|
||||
<popup-footer slot="footer">
|
||||
<button
|
||||
type="button"
|
||||
bitButton
|
||||
buttonType="primary"
|
||||
(click)="selectValue()"
|
||||
data-testid="select-button"
|
||||
>
|
||||
{{ selectButtonText }}
|
||||
</button>
|
||||
</popup-footer>
|
||||
</popup-page>
|
||||
@@ -0,0 +1,82 @@
|
||||
import { DIALOG_DATA, DialogRef } 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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { CipherFormGeneratorComponent } from "@bitwarden/vault";
|
||||
|
||||
import {
|
||||
GeneratorDialogParams,
|
||||
GeneratorDialogResult,
|
||||
VaultGeneratorDialogComponent,
|
||||
} from "./vault-generator-dialog.component";
|
||||
|
||||
@Component({
|
||||
selector: "vault-cipher-form-generator",
|
||||
template: "",
|
||||
standalone: true,
|
||||
})
|
||||
class MockCipherFormGenerator {
|
||||
@Input() type: "password" | "username";
|
||||
@Output() valueGenerated = new EventEmitter<string>();
|
||||
}
|
||||
|
||||
describe("VaultGeneratorDialogComponent", () => {
|
||||
let component: VaultGeneratorDialogComponent;
|
||||
let fixture: ComponentFixture<VaultGeneratorDialogComponent>;
|
||||
let mockDialogRef: MockProxy<DialogRef<GeneratorDialogResult>>;
|
||||
let dialogData: GeneratorDialogParams;
|
||||
|
||||
beforeEach(async () => {
|
||||
mockDialogRef = mock<DialogRef<GeneratorDialogResult>>();
|
||||
dialogData = { type: "password" };
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [VaultGeneratorDialogComponent, NoopAnimationsModule],
|
||||
providers: [
|
||||
{ provide: I18nService, useValue: { t: (key: string) => key } },
|
||||
{ provide: DIALOG_DATA, useValue: dialogData },
|
||||
{ provide: DialogRef, useValue: mockDialogRef },
|
||||
],
|
||||
})
|
||||
.overrideComponent(VaultGeneratorDialogComponent, {
|
||||
remove: { imports: [CipherFormGeneratorComponent] },
|
||||
add: { imports: [MockCipherFormGenerator] },
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(VaultGeneratorDialogComponent);
|
||||
component = fixture.componentInstance;
|
||||
});
|
||||
|
||||
it("should create", () => {
|
||||
fixture.detectChanges();
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should use the appropriate text based on generator type", () => {
|
||||
expect(component["title"]).toBe("passwordGenerator");
|
||||
expect(component["selectButtonText"]).toBe("useThisPassword");
|
||||
|
||||
dialogData.type = "username";
|
||||
|
||||
fixture = TestBed.createComponent(VaultGeneratorDialogComponent);
|
||||
component = fixture.componentInstance;
|
||||
|
||||
expect(component["title"]).toBe("usernameGenerator");
|
||||
expect(component["selectButtonText"]).toBe("useThisUsername");
|
||||
});
|
||||
|
||||
it("should close the dialog with the generated value when the user selects it", () => {
|
||||
component["generatedValue"] = "generated-value";
|
||||
|
||||
fixture.nativeElement.querySelector("button[data-testid='select-button']").click();
|
||||
|
||||
expect(mockDialogRef.close).toHaveBeenCalledWith({
|
||||
action: "selected",
|
||||
generatedValue: "generated-value",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,120 @@
|
||||
import { animate, group, style, transition, trigger } from "@angular/animations";
|
||||
import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
|
||||
import { Overlay } from "@angular/cdk/overlay";
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, Inject } from "@angular/core";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { ButtonModule, DialogService } from "@bitwarden/components";
|
||||
import { CipherFormGeneratorComponent } from "@bitwarden/vault";
|
||||
|
||||
import { PopupFooterComponent } from "../../../../../platform/popup/layout/popup-footer.component";
|
||||
import { PopupHeaderComponent } from "../../../../../platform/popup/layout/popup-header.component";
|
||||
import { PopupPageComponent } from "../../../../../platform/popup/layout/popup-page.component";
|
||||
|
||||
export interface GeneratorDialogParams {
|
||||
type: "password" | "username";
|
||||
}
|
||||
|
||||
export interface GeneratorDialogResult {
|
||||
action: GeneratorDialogAction;
|
||||
generatedValue?: string;
|
||||
}
|
||||
|
||||
export enum GeneratorDialogAction {
|
||||
Selected = "selected",
|
||||
Canceled = "canceled",
|
||||
}
|
||||
|
||||
const slideIn = trigger("slideIn", [
|
||||
transition(":enter", [
|
||||
style({ opacity: 0, transform: "translateY(100vh)" }),
|
||||
group([
|
||||
animate("0.15s linear", style({ opacity: 1 })),
|
||||
animate("0.3s ease-out", style({ transform: "none" })),
|
||||
]),
|
||||
]),
|
||||
]);
|
||||
|
||||
@Component({
|
||||
selector: "app-vault-generator-dialog",
|
||||
templateUrl: "./vault-generator-dialog.component.html",
|
||||
standalone: true,
|
||||
imports: [
|
||||
PopupPageComponent,
|
||||
PopupHeaderComponent,
|
||||
PopupFooterComponent,
|
||||
CommonModule,
|
||||
CipherFormGeneratorComponent,
|
||||
ButtonModule,
|
||||
],
|
||||
animations: [slideIn],
|
||||
})
|
||||
export class VaultGeneratorDialogComponent {
|
||||
protected title = this.i18nService.t(this.isPassword ? "passwordGenerator" : "usernameGenerator");
|
||||
protected selectButtonText = this.i18nService.t(
|
||||
this.isPassword ? "useThisPassword" : "useThisUsername",
|
||||
);
|
||||
|
||||
/**
|
||||
* Whether the dialog is generating a password/passphrase. If false, it is generating a username.
|
||||
* @protected
|
||||
*/
|
||||
protected get isPassword() {
|
||||
return this.params.type === "password";
|
||||
}
|
||||
|
||||
/**
|
||||
* The currently generated value.
|
||||
* @protected
|
||||
*/
|
||||
protected generatedValue: string = "";
|
||||
|
||||
constructor(
|
||||
@Inject(DIALOG_DATA) protected params: GeneratorDialogParams,
|
||||
private dialogRef: DialogRef<GeneratorDialogResult>,
|
||||
private i18nService: I18nService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Close the dialog without selecting a value.
|
||||
*/
|
||||
protected close = () => {
|
||||
this.dialogRef.close({ action: GeneratorDialogAction.Canceled });
|
||||
};
|
||||
|
||||
/**
|
||||
* Close the dialog and select the currently generated value.
|
||||
*/
|
||||
protected selectValue = () => {
|
||||
this.dialogRef.close({
|
||||
action: GeneratorDialogAction.Selected,
|
||||
generatedValue: this.generatedValue,
|
||||
});
|
||||
};
|
||||
|
||||
onValueGenerated(value: string) {
|
||||
this.generatedValue = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the vault generator dialog in a full screen dialog.
|
||||
*/
|
||||
static open(
|
||||
dialogService: DialogService,
|
||||
overlay: Overlay,
|
||||
config: DialogConfig<GeneratorDialogParams>,
|
||||
) {
|
||||
const position = overlay.position().global();
|
||||
|
||||
return dialogService.open<GeneratorDialogResult, GeneratorDialogParams>(
|
||||
VaultGeneratorDialogComponent,
|
||||
{
|
||||
...config,
|
||||
positionStrategy: position,
|
||||
height: "100vh",
|
||||
width: "100vw",
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { Overlay } from "@angular/cdk/overlay";
|
||||
import { inject, Injectable } from "@angular/core";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
import { CipherFormGenerationService } from "@bitwarden/vault";
|
||||
|
||||
import { VaultGeneratorDialogComponent } from "../components/vault-v2/vault-generator-dialog/vault-generator-dialog.component";
|
||||
|
||||
@Injectable()
|
||||
export class BrowserCipherFormGenerationService implements CipherFormGenerationService {
|
||||
private dialogService = inject(DialogService);
|
||||
private overlay = inject(Overlay);
|
||||
|
||||
async generatePassword(): Promise<string> {
|
||||
const dialogRef = VaultGeneratorDialogComponent.open(this.dialogService, this.overlay, {
|
||||
data: { type: "password" },
|
||||
});
|
||||
|
||||
const result = await firstValueFrom(dialogRef.closed);
|
||||
|
||||
if (result == null || result.action === "canceled") {
|
||||
return null;
|
||||
}
|
||||
|
||||
return result.generatedValue;
|
||||
}
|
||||
|
||||
async generateUsername(): Promise<string> {
|
||||
const dialogRef = VaultGeneratorDialogComponent.open(this.dialogService, this.overlay, {
|
||||
data: { type: "username" },
|
||||
});
|
||||
|
||||
const result = await firstValueFrom(dialogRef.closed);
|
||||
|
||||
if (result == null || result.action === "canceled") {
|
||||
return null;
|
||||
}
|
||||
|
||||
return result.generatedValue;
|
||||
}
|
||||
async generateInitialPassword(): Promise<string> {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user