diff --git a/apps/web/src/app/auth/core/services/webauthn/webauthn.service.ts b/apps/web/src/app/auth/core/services/webauthn/webauthn.service.ts index f6b733609ef..f2051429323 100644 --- a/apps/web/src/app/auth/core/services/webauthn/webauthn.service.ts +++ b/apps/web/src/app/auth/core/services/webauthn/webauthn.service.ts @@ -1,5 +1,5 @@ import { Injectable, Optional } from "@angular/core"; -import { BehaviorSubject, from, map, Observable, shareReplay, switchMap, tap } from "rxjs"; +import { BehaviorSubject, filter, from, map, Observable, shareReplay, switchMap, tap } from "rxjs"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/abstractions/log.service"; @@ -105,6 +105,15 @@ export class WebauthnService { } } + getCredential$(credentialId: string): Observable { + return this.credentials$.pipe( + map( + (credentials) => credentials.find((c) => c.id === credentialId), + filter((c) => c !== undefined) + ) + ); + } + private getCredentials$(): Observable { return from(this.apiService.getCredentials()).pipe(map((response) => response.data)); } diff --git a/apps/web/src/app/auth/settings/fido2-login-settings/delete-credential-dialog/delete-credential-dialog.component.html b/apps/web/src/app/auth/settings/fido2-login-settings/delete-credential-dialog/delete-credential-dialog.component.html new file mode 100644 index 00000000000..047abac0563 --- /dev/null +++ b/apps/web/src/app/auth/settings/fido2-login-settings/delete-credential-dialog/delete-credential-dialog.component.html @@ -0,0 +1,34 @@ +
+ + {{ "removePasskey" | i18n }} + {{ + credential.name + }} + + + + + + + +

{{ "removePasskeyInfo" | i18n }}

+ + + {{ "masterPassword" | i18n }} + + + {{ "confirmIdentity" | i18n }} + +
+
+ + + + +
+
diff --git a/apps/web/src/app/auth/settings/fido2-login-settings/delete-credential-dialog/delete-credential-dialog.component.ts b/apps/web/src/app/auth/settings/fido2-login-settings/delete-credential-dialog/delete-credential-dialog.component.ts new file mode 100644 index 00000000000..53536406083 --- /dev/null +++ b/apps/web/src/app/auth/settings/fido2-login-settings/delete-credential-dialog/delete-credential-dialog.component.ts @@ -0,0 +1,60 @@ +import { DialogConfig, DialogRef, DIALOG_DATA } from "@angular/cdk/dialog"; +import { Component, Inject, OnDestroy, OnInit } from "@angular/core"; +import { FormBuilder, Validators } from "@angular/forms"; +import { Subject, takeUntil } from "rxjs"; + +import { DialogServiceAbstraction } from "@bitwarden/angular/services/dialog"; + +import { WebauthnService } from "../../../core"; +import { WebauthnCredentialView } from "../../../core/views/webauth-credential.view"; + +export interface DeleteCredentialDialogParams { + credentialId: string; +} + +@Component({ + templateUrl: "delete-credential-dialog.component.html", +}) +export class DeleteCredentialDialogComponent implements OnInit, OnDestroy { + private destroy$ = new Subject(); + + protected formGroup = this.formBuilder.group({ + masterPassword: ["", [Validators.required]], + }); + protected credential?: WebauthnCredentialView; + + constructor( + @Inject(DIALOG_DATA) private params: DeleteCredentialDialogParams, + private formBuilder: FormBuilder, + private dialogRef: DialogRef, + private webauthnService: WebauthnService + ) {} + + ngOnInit(): void { + this.webauthnService + .getCredential$(this.params.credentialId) + .pipe(takeUntil(this.destroy$)) + .subscribe((credential) => (this.credential = credential)); + } + + submit = async () => { + // empty + }; + + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + } +} + +/** + * Strongly typed helper to open a DeleteCredentialDialogComponent + * @param dialogService Instance of the dialog service that will be used to open the dialog + * @param config Configuration for the dialog + */ +export const openDeleteCredentialDialogComponent = ( + dialogService: DialogServiceAbstraction, + config: DialogConfig +) => { + return dialogService.open(DeleteCredentialDialogComponent, config); +}; diff --git a/apps/web/src/app/auth/settings/fido2-login-settings/fido2-login-settings.component.html b/apps/web/src/app/auth/settings/fido2-login-settings/fido2-login-settings.component.html index 0814ef80746..7f412d24264 100644 --- a/apps/web/src/app/auth/settings/fido2-login-settings/fido2-login-settings.component.html +++ b/apps/web/src/app/auth/settings/fido2-login-settings/fido2-login-settings.component.html @@ -29,6 +29,7 @@ bitLink [disabled]="loading" [attr.aria-label]="('remove' | i18n) + ' ' + credential.name" + (click)="deleteCredential(credential.id)" > {{ "remove" | i18n }} diff --git a/apps/web/src/app/auth/settings/fido2-login-settings/fido2-login-settings.component.ts b/apps/web/src/app/auth/settings/fido2-login-settings/fido2-login-settings.component.ts index 622576bbc16..975d50adc32 100644 --- a/apps/web/src/app/auth/settings/fido2-login-settings/fido2-login-settings.component.ts +++ b/apps/web/src/app/auth/settings/fido2-login-settings/fido2-login-settings.component.ts @@ -7,6 +7,7 @@ import { WebauthnService } from "../../core"; import { WebauthnCredentialView } from "../../core/views/webauth-credential.view"; import { openCreateCredentialDialog } from "./create-credential-dialog/create-credential-dialog.component"; +import { openDeleteCredentialDialogComponent } from "./delete-credential-dialog/delete-credential-dialog.component"; @Component({ selector: "app-fido2-login-settings", @@ -54,7 +55,11 @@ export class Fido2LoginSettingsComponent implements OnInit, OnDestroy { this.destroy$.complete(); } - protected async createCredential() { + protected createCredential() { openCreateCredentialDialog(this.dialogService, {}); } + + protected deleteCredential(credentialId: string) { + openDeleteCredentialDialogComponent(this.dialogService, { data: { credentialId } }); + } } diff --git a/apps/web/src/app/auth/settings/fido2-login-settings/fido2-login-settings.module.ts b/apps/web/src/app/auth/settings/fido2-login-settings/fido2-login-settings.module.ts index e6325415d9d..8fbdcdb9a9c 100644 --- a/apps/web/src/app/auth/settings/fido2-login-settings/fido2-login-settings.module.ts +++ b/apps/web/src/app/auth/settings/fido2-login-settings/fido2-login-settings.module.ts @@ -4,11 +4,16 @@ import { FormsModule, ReactiveFormsModule } from "@angular/forms"; import { SharedModule } from "../../../shared/shared.module"; import { CreateCredentialDialogComponent } from "./create-credential-dialog/create-credential-dialog.component"; +import { DeleteCredentialDialogComponent } from "./delete-credential-dialog/delete-credential-dialog.component"; import { Fido2LoginSettingsComponent } from "./fido2-login-settings.component"; @NgModule({ imports: [SharedModule, FormsModule, ReactiveFormsModule], - declarations: [Fido2LoginSettingsComponent, CreateCredentialDialogComponent], + declarations: [ + Fido2LoginSettingsComponent, + CreateCredentialDialogComponent, + DeleteCredentialDialogComponent, + ], exports: [Fido2LoginSettingsComponent], }) export class Fido2LoginSettingsModule {} diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index f831607c462..02f51596138 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -673,6 +673,12 @@ } } }, + "removePasskey": { + "message": "Remove passkey" + }, + "removePasskeyInfo": { + "message": "If all passkeys are removed, you will be unable to log into new devices without your master password." + }, "tryAgain": { "message": "Try again" },