1
0
mirror of https://github.com/bitwarden/browser.git synced 2025-04-18 20:46:00 +02:00
bitwarden-browser/apps/web/src/app/auth/settings/webauthn-login-settings/create-credential-dialog/create-credential-dialog.component.ts

179 lines
5.7 KiB
TypeScript

import { DialogConfig, DialogRef } from "@angular/cdk/dialog";
import { Component, OnInit } from "@angular/core";
import { FormBuilder, Validators } from "@angular/forms";
import { firstValueFrom, map, Observable } from "rxjs";
import { VerificationType } from "@bitwarden/common/auth/enums/verification-type";
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
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 { DialogService } from "@bitwarden/components";
import { WebauthnLoginService } from "../../../core";
import { CredentialCreateOptionsView } from "../../../core/views/credential-create-options.view";
import { CreatePasskeyFailedIcon } from "./create-passkey-failed.icon";
import { CreatePasskeyIcon } from "./create-passkey.icon";
export enum CreateCredentialDialogResult {
Success,
}
type Step =
| "userVerification"
| "credentialCreation"
| "credentialCreationFailed"
| "credentialNaming";
@Component({
templateUrl: "create-credential-dialog.component.html",
})
export class CreateCredentialDialogComponent implements OnInit {
protected readonly NameMaxCharacters = 50;
protected readonly CreateCredentialDialogResult = CreateCredentialDialogResult;
protected readonly Icons = { CreatePasskeyIcon, CreatePasskeyFailedIcon };
protected currentStep: Step = "userVerification";
protected formGroup = this.formBuilder.group({
userVerification: this.formBuilder.group({
masterPassword: ["", [Validators.required]],
}),
credentialNaming: this.formBuilder.group({
name: ["", Validators.maxLength(50)],
}),
});
protected credentialOptions?: CredentialCreateOptionsView;
protected deviceResponse?: PublicKeyCredential;
protected hasPasskeys$?: Observable<boolean>;
constructor(
private formBuilder: FormBuilder,
private dialogRef: DialogRef,
private webauthnService: WebauthnLoginService,
private platformUtilsService: PlatformUtilsService,
private i18nService: I18nService,
private logService: LogService
) {}
ngOnInit(): void {
this.hasPasskeys$ = this.webauthnService.credentials$.pipe(
map((credentials) => credentials.length > 0)
);
}
protected submit = async () => {
this.dialogRef.disableClose = true;
try {
switch (this.currentStep) {
case "userVerification":
return await this.submitUserVerification();
case "credentialCreationFailed":
return await this.submitCredentialCreationFailed();
case "credentialCreation":
return await this.submitCredentialCreation();
case "credentialNaming":
return await this.submitCredentialNaming();
}
} finally {
this.dialogRef.disableClose = false;
}
};
protected async submitUserVerification() {
this.formGroup.controls.userVerification.markAllAsTouched();
if (this.formGroup.controls.userVerification.invalid) {
return;
}
try {
this.credentialOptions = await this.webauthnService.getCredentialCreateOptions({
type: VerificationType.MasterPassword,
secret: this.formGroup.value.userVerification.masterPassword,
});
} catch (error) {
if (error instanceof ErrorResponse && error.statusCode === 400) {
this.platformUtilsService.showToast(
"error",
this.i18nService.t("error"),
this.i18nService.t("invalidMasterPassword")
);
} else {
this.logService?.error(error);
this.platformUtilsService.showToast("error", null, this.i18nService.t("unexpectedError"));
}
return;
}
this.currentStep = "credentialCreation";
await this.submitCredentialCreation();
}
protected async submitCredentialCreation() {
this.deviceResponse = await this.webauthnService.createCredential(this.credentialOptions);
if (this.deviceResponse === undefined) {
this.currentStep = "credentialCreationFailed";
return;
}
this.currentStep = "credentialNaming";
}
protected async submitCredentialCreationFailed() {
this.currentStep = "credentialCreation";
await this.submitCredentialCreation();
}
protected async submitCredentialNaming() {
this.formGroup.controls.credentialNaming.markAllAsTouched();
if (this.formGroup.controls.credentialNaming.invalid) {
return;
}
const name = this.formGroup.value.credentialNaming.name;
try {
await this.webauthnService.saveCredential(
this.credentialOptions,
this.deviceResponse,
this.formGroup.value.credentialNaming.name
);
} catch (error) {
this.logService?.error(error);
this.platformUtilsService.showToast("error", null, this.i18nService.t("unexpectedError"));
return;
}
if (firstValueFrom(this.hasPasskeys$)) {
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t("passkeySaved", name)
);
} else {
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t("loginWithPasskeyEnabled")
);
}
this.dialogRef.close(CreateCredentialDialogResult.Success);
}
}
/**
* Strongly typed helper to open a CreateCredentialDialog
* @param dialogService Instance of the dialog service that will be used to open the dialog
* @param config Configuration for the dialog
*/
export const openCreateCredentialDialog = (
dialogService: DialogService,
config: DialogConfig<unknown>
) => {
return dialogService.open<CreateCredentialDialogResult | undefined, unknown>(
CreateCredentialDialogComponent,
config
);
};