1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-24 16:43:27 +00:00
Files
browser/apps/desktop/src/autofill/modal/credentials/fido2-create.component.ts
Will Martin de2f4a04fc [CL-82] rename bit-icon to bit-svg; create new bit-icon component for font icons (#18584)
* rename bit-icon to bit-svg; create new bit-icon for font icons

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* find and replace current usage

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* add custom eslint warning

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* fix incorrect usage

* fix tests

* fix tests

* Update libs/components/src/svg/index.ts

Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>

* Update libs/eslint/components/no-bwi-class-usage.spec.mjs

Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>

* update component api

* update class name

* use icon type in iconButton component

* update type Icon --> BitSvg

* fix bad renames

* fix more renames

* fix bad input

* revert iconButton type

* fix lint

* fix more inputs

* misc fixes

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* fix test

* add eslint ignore

* fix lint

* add comparison story

---------

Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
2026-02-17 09:39:25 -08:00

220 lines
6.9 KiB
TypeScript

import { CommonModule } from "@angular/common";
import { ChangeDetectionStrategy, Component, OnInit, OnDestroy } from "@angular/core";
import { RouterModule, Router } from "@angular/router";
import { combineLatest, map, Observable, Subject, switchMap } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { BitwardenShield, NoResults } from "@bitwarden/assets/svg";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
import { Fido2Utils } from "@bitwarden/common/platform/services/fido2/fido2-utils";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherType } from "@bitwarden/common/vault/enums/cipher-type";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import {
DialogService,
BadgeModule,
ButtonModule,
DialogModule,
SvgModule,
ItemModule,
SectionComponent,
TableModule,
SectionHeaderComponent,
BitIconButtonComponent,
SimpleDialogOptions,
} from "@bitwarden/components";
import { PasswordRepromptService } from "@bitwarden/vault";
import { DesktopAutofillService } from "../../../autofill/services/desktop-autofill.service";
import { DesktopSettingsService } from "../../../platform/services/desktop-settings.service";
import {
DesktopFido2UserInterfaceService,
DesktopFido2UserInterfaceSession,
} from "../../services/desktop-fido2-user-interface.service";
@Component({
standalone: true,
imports: [
CommonModule,
RouterModule,
SectionHeaderComponent,
BitIconButtonComponent,
TableModule,
JslibModule,
SvgModule,
ButtonModule,
DialogModule,
SectionComponent,
ItemModule,
BadgeModule,
],
templateUrl: "fido2-create.component.html",
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class Fido2CreateComponent implements OnInit, OnDestroy {
session?: DesktopFido2UserInterfaceSession = null;
ciphers$: Observable<CipherView[]>;
private destroy$ = new Subject<void>();
readonly Icons = { BitwardenShield, NoResults };
private get DIALOG_MESSAGES() {
return {
unexpectedErrorShort: {
title: { key: "unexpectedErrorShort" },
content: { key: "closeThisBitwardenWindow" },
type: "danger",
acceptButtonText: { key: "closeThisWindow" },
cancelButtonText: null as null,
acceptAction: async () => this.dialogService.closeAll(),
},
unableToSavePasskey: {
title: { key: "unableToSavePasskey" },
content: { key: "closeThisBitwardenWindow" },
type: "danger",
acceptButtonText: { key: "closeThisWindow" },
cancelButtonText: null as null,
acceptAction: async () => this.dialogService.closeAll(),
},
overwritePasskey: {
title: { key: "overwritePasskey" },
content: { key: "alreadyContainsPasskey" },
type: "warning",
},
} as const satisfies Record<string, SimpleDialogOptions>;
}
constructor(
private readonly desktopSettingsService: DesktopSettingsService,
private readonly fido2UserInterfaceService: DesktopFido2UserInterfaceService,
private readonly accountService: AccountService,
private readonly cipherService: CipherService,
private readonly desktopAutofillService: DesktopAutofillService,
private readonly dialogService: DialogService,
private readonly domainSettingsService: DomainSettingsService,
private readonly passwordRepromptService: PasswordRepromptService,
private readonly router: Router,
) {}
async ngOnInit(): Promise<void> {
this.session = this.fido2UserInterfaceService.getCurrentSession();
if (this.session) {
const rpid = await this.session.getRpId();
this.initializeCiphersObservable(rpid);
} else {
await this.showErrorDialog(this.DIALOG_MESSAGES.unableToSavePasskey);
}
}
async ngOnDestroy(): Promise<void> {
this.destroy$.next();
this.destroy$.complete();
await this.closeModal();
}
async addCredentialToCipher(cipher: CipherView): Promise<void> {
const isConfirmed = await this.validateCipherAccess(cipher);
try {
if (!this.session) {
throw new Error("Missing session");
}
this.session.notifyConfirmCreateCredential(isConfirmed, cipher);
} catch {
await this.showErrorDialog(this.DIALOG_MESSAGES.unableToSavePasskey);
return;
}
await this.closeModal();
}
async confirmPasskey(): Promise<void> {
try {
if (!this.session) {
throw new Error("Missing session");
}
this.session.notifyConfirmCreateCredential(true);
} catch {
await this.showErrorDialog(this.DIALOG_MESSAGES.unableToSavePasskey);
}
await this.closeModal();
}
async closeModal(): Promise<void> {
await this.desktopSettingsService.setModalMode(false);
await this.accountService.setShowHeader(true);
if (this.session) {
this.session.notifyConfirmCreateCredential(false);
this.session.confirmChosenCipher(null);
}
await this.router.navigate(["/"]);
}
private initializeCiphersObservable(rpid: string): void {
const lastRegistrationRequest = this.desktopAutofillService.lastRegistrationRequest;
if (!lastRegistrationRequest || !rpid) {
return;
}
const userHandle = Fido2Utils.bufferToString(
new Uint8Array(lastRegistrationRequest.userHandle),
);
this.ciphers$ = combineLatest([
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
this.domainSettingsService.getUrlEquivalentDomains(rpid),
]).pipe(
switchMap(async ([activeUserId, equivalentDomains]) => {
if (!activeUserId) {
return [];
}
try {
const allCiphers = await this.cipherService.getAllDecrypted(activeUserId);
return allCiphers.filter(
(cipher) =>
cipher != null &&
cipher.type == CipherType.Login &&
cipher.login?.matchesUri(rpid, equivalentDomains) &&
Fido2Utils.cipherHasNoOtherPasskeys(cipher, userHandle) &&
!cipher.deletedDate,
);
} catch {
await this.showErrorDialog(this.DIALOG_MESSAGES.unexpectedErrorShort);
return [];
}
}),
);
}
private async validateCipherAccess(cipher: CipherView): Promise<boolean> {
if (cipher.login.hasFido2Credentials) {
const overwriteConfirmed = await this.dialogService.openSimpleDialog(
this.DIALOG_MESSAGES.overwritePasskey,
);
if (!overwriteConfirmed) {
return false;
}
}
if (cipher.reprompt) {
return this.passwordRepromptService.showPasswordPrompt();
}
return true;
}
private async showErrorDialog(config: SimpleDialogOptions): Promise<void> {
await this.dialogService.openSimpleDialog(config);
await this.closeModal();
}
}