1
0
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:
Shane Melton
2024-08-07 12:02:33 -07:00
committed by GitHub
parent 6179397ba9
commit 041cd87e7e
11 changed files with 720 additions and 1 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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