1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-07 20:24:01 +00:00
Files
browser/libs/auth/src/angular/user-verification/user-verification-dialog.component.ts
Oscar Hinton 8f74eaea1c Remove standalone true from auth (#15035)
Remove standalone: true from every instance since it's the default as of Angular 19.
2025-06-04 09:22:37 -04:00

301 lines
9.6 KiB
TypeScript

// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { CommonModule } from "@angular/common";
import { Component, Inject } from "@angular/core";
import { FormBuilder, ReactiveFormsModule } from "@angular/forms";
import { firstValueFrom } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { VerificationWithSecret } from "@bitwarden/common/auth/types/verification";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
// eslint-disable-next-line no-restricted-imports
import {
DIALOG_DATA,
DialogRef,
AsyncActionsModule,
ButtonModule,
CalloutModule,
DialogModule,
DialogService,
ToastService,
} from "@bitwarden/components";
import { ActiveClientVerificationOption } from "./active-client-verification-option.enum";
import {
UserVerificationDialogOptions,
UserVerificationDialogResult,
} from "./user-verification-dialog.types";
import { UserVerificationFormInputComponent } from "./user-verification-form-input.component";
@Component({
templateUrl: "user-verification-dialog.component.html",
imports: [
CommonModule,
ReactiveFormsModule,
JslibModule,
ButtonModule,
DialogModule,
AsyncActionsModule,
UserVerificationFormInputComponent,
CalloutModule,
],
})
export class UserVerificationDialogComponent {
verificationForm = this.formBuilder.group({
secret: this.formBuilder.control<VerificationWithSecret | null>(null),
});
get secret() {
return this.verificationForm.controls.secret;
}
invalidSecret = false;
activeClientVerificationOption: ActiveClientVerificationOption;
readonly ActiveClientVerificationOption = ActiveClientVerificationOption;
constructor(
@Inject(DIALOG_DATA) public dialogOptions: UserVerificationDialogOptions,
private dialogRef: DialogRef<UserVerificationDialogResult | string>,
private formBuilder: FormBuilder,
private userVerificationService: UserVerificationService,
private platformUtilsService: PlatformUtilsService,
private i18nService: I18nService,
private toastService: ToastService,
) {}
/**
* Opens the user verification dialog.
*
* @param {DialogService} dialogService - The service used to open the dialog.
* @param {UserVerificationDialogOptions} data - Parameters for configuring the dialog.
* @returns {Promise<UserVerificationDialogResult>} A promise that resolves to the result of the user verification process.
*
* @example
* // Example 1: Default, simple scenario
* const result = await UserVerificationDialogComponent.open(
* this.dialogService,
* {}
* );
*
* // Handle the result of the dialog based on user action and verification success
* if (result.userAction === 'cancel') {
* // User cancelled the dialog
* return;
* }
*
* // User confirmed the dialog so check verification success
* if (!result.verificationSuccess) {
* // verification failed
* return;
* }
*
* ----------------------------------------------------------
*
* @example
* // Example 2: Custom scenario
* const result = await UserVerificationDialogComponent.open(
* this.dialogService,
* {
* title: 'customTitle',
* bodyText: 'customBodyText',
* calloutOptions: {
* text: 'customCalloutText',
* type: 'warning',
* },
* confirmButtonOptions: {
* text: 'customConfirmButtonText',
* type: 'danger',
* }
* }
* );
*
* // Handle the result of the dialog based on user action and verification success
* if (result.userAction === 'cancel') {
* // User cancelled the dialog
* return;
* }
*
* // User confirmed the dialog so check verification success
* if (!result.verificationSuccess) {
* // verification failed
* return;
* }
*
* ----------------------------------------------------------
*
* @example
* // Example 3: Client side verification scenario only
* const result = await UserVerificationDialogComponent.open(
* this.dialogService,
* { clientSideOnlyVerification: true }
* );
*
* // Handle the result of the dialog based on user action and verification success
* if (result.userAction === 'cancel') {
* // User cancelled the dialog
* return;
* }
*
* // User confirmed the dialog so check verification success
* if (!result.verificationSuccess) {
* if (result.noAvailableClientVerificationMethods) {
* // No client-side verification methods are available
* // Could send user to configure a verification method like PIN or biometrics
* }
* return;
* }
*
* ----------------------------------------------------------
*
* @example
* // Example 4: Custom user verification validation
*
* const result = await UserVerificationDialogComponent.open(dialogService, {
* verificationType: {
* type: "custom",
* // Pass in a function that will be used to validate the input of the
* // verification dialog, returning true when finished.
* verificationFn: async (secret: VerificationWithSecret) => {
* const request = await userVerificationService.buildRequest<CustomRequestType>(secret);
*
* // ... Do something with the custom request type
*
* await someServicer.sendMyRequestThatVerfiesUserIdentity(
* // ... Some other data
* request,
* );
* return true;
* },
* },
* });
*
* // ... Evaluate the result as usual
*/
static async open(
dialogService: DialogService,
data: UserVerificationDialogOptions,
): Promise<UserVerificationDialogResult> {
const dialogRef = dialogService.open<UserVerificationDialogResult | string>(
UserVerificationDialogComponent,
{
data,
},
);
const dialogResult = await firstValueFrom(dialogRef.closed);
// An empty string is returned when the user hits the x to close the dialog.
// Undefined is returned when the users hits the escape key to close the dialog.
if (typeof dialogResult === "string" || dialogResult === undefined) {
// User used x to close dialog
return {
userAction: "cancel",
verificationSuccess: false,
};
} else {
return dialogResult;
}
}
handleActiveClientVerificationOptionChange(
activeClientVerificationOption: ActiveClientVerificationOption,
) {
this.activeClientVerificationOption = activeClientVerificationOption;
}
handleBiometricsVerificationResultChange(biometricsVerificationResult: boolean) {
if (biometricsVerificationResult) {
this.close({
userAction: "confirm",
verificationSuccess: true,
noAvailableClientVerificationMethods: false,
});
}
}
submit = async () => {
if (this.activeClientVerificationOption === ActiveClientVerificationOption.None) {
this.close({
userAction: "confirm",
verificationSuccess: false,
noAvailableClientVerificationMethods: true,
});
return;
}
this.verificationForm.markAllAsTouched();
if (this.verificationForm.invalid) {
return;
}
try {
if (
typeof this.dialogOptions.verificationType === "object" &&
this.dialogOptions.verificationType.type === "custom"
) {
const success = await this.dialogOptions.verificationType.verificationFn(this.secret.value);
this.close({
userAction: "confirm",
verificationSuccess: success,
});
return;
}
// TODO: once we migrate all user verification scenarios to use this new implementation,
// we should consider refactoring the user verification service handling of the
// OTP and MP flows to not throw errors on verification failure.
const verificationResult = await this.userVerificationService.verifyUser(this.secret.value);
if (verificationResult) {
this.invalidSecret = false;
this.close({
userAction: "confirm",
verificationSuccess: true,
noAvailableClientVerificationMethods: false,
});
} else {
this.invalidSecret = true;
// Only pin should ever get here, but added this check to be safe.
if (this.activeClientVerificationOption === ActiveClientVerificationOption.Pin) {
this.toastService.showToast({
variant: "error",
title: this.i18nService.t("error"),
message: this.i18nService.t("invalidPin"),
});
} else {
this.toastService.showToast({
variant: "error",
title: null,
message: this.i18nService.t("unexpectedError"),
});
}
}
} catch (e) {
// Catch handles OTP and MP verification scenarios as those throw errors on verification failure instead of returning false like PIN and biometrics.
this.invalidSecret = true;
this.toastService.showToast({
variant: "error",
title: this.i18nService.t("error"),
message: e.message,
});
return;
}
};
cancel() {
this.close({
userAction: "cancel",
verificationSuccess: false,
});
}
close(dialogResult: UserVerificationDialogResult) {
this.dialogRef.close(dialogResult);
}
}