Press n or j to go to the next uncovered block, b, p or k for the previous block.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 | 1x 15x 15x 15x 15x 15x 15x 15x 15x 15x 1x 1x 4x 4x 1x 3x 3x 3x 3x 3x 1x 1x 2x 2x 3x 2x 2x 2x | import { AsyncPipe } from '@angular/common'; import { ChangeDetectionStrategy, Component, inject, signal, viewChild, } from '@angular/core'; import type { ElementRef, Signal, WritableSignal } from '@angular/core'; import { deleteUser, EmailAuthProvider, reauthenticateWithCredential } from '@angular/fire/auth'; import type { User } from '@angular/fire/auth'; import { FormGroup, ReactiveFormsModule } from '@angular/forms'; import type { FormControl, ValidationErrors } from '@angular/forms'; import { Router } from '@angular/router'; import { USER$ } from '@app/core/user.token'; import type { MaybeUser$ } from '@app/core/user.token'; import { SpinnerComponent } from '@app/shared/spinner/spinner.component'; import { AuthErrorMessagesComponent } from '../auth-error-messages/auth-error-messages.component'; import { getErrorCode } from '../error-code'; import { createPasswordControl, PASSWORDS } from '../identity-forms'; /** Deleting a Firebase User requires a recent authentication. */ type DeleteAccountFormGroup = FormGroup<{ password: FormControl<string | null>; }>; /** Template reference to HTML dialog element. */ type DialogRef = ElementRef<HTMLDialogElement>; /** * Two step process to delete a User's account in Firebase Authentication. First button opens a * dialog where the User enters their password and confirms account deletion. */ @Component({ changeDetection: ChangeDetectionStrategy.OnPush, imports: [ AsyncPipe, AuthErrorMessagesComponent, ReactiveFormsModule, SpinnerComponent, ], selector: 'app-delete-account', templateUrl: './delete-account.component.html', }) export class DeleteAccountComponent { /** Errors from Firebase, displayed after the dialog is closed. */ public readonly $errorCode: WritableSignal<string>; /** Errors specific to the password field. */ public readonly $passwordCntrlErrors: Signal<ValidationErrors | undefined>; /** Aria-invalid attribute for the password field. */ public readonly $passwordCntrlInvalid: Signal<boolean>; /** Toggle showing view and the spinner. */ public readonly $showForm: WritableSignal<boolean>; public readonly deleteAccountForm: DeleteAccountFormGroup; /** Used in error message for password maximum length. */ public readonly maxPasswordLength: number = PASSWORDS.maxLength; /** Used in error message for password minimum length. */ public readonly minPasswordLength: number = PASSWORDS.minLength; public readonly passwordCntrl: FormControl<string | null>; public readonly user$: MaybeUser$; private readonly _$confirmDialog: Signal<DialogRef> = viewChild.required<DialogRef>('confirmDialog'); private readonly _router: Router; constructor() { this._router = inject(Router); ({ $errors: this.$passwordCntrlErrors, $invalid: this.$passwordCntrlInvalid, control: this.passwordCntrl, } = createPasswordControl()); this.deleteAccountForm = new FormGroup({ password: this.passwordCntrl, }); this.$errorCode = signal<string>(''); this.$showForm = signal<boolean>(true); // Not handling non-logged in users because the Route guards should. this.user$ = inject(USER$); } /** * Closes the HTML Dialog element without deleting the account. */ public closeDialog(): void { const dialogEl = this._$confirmDialog(); dialogEl.nativeElement.close(); } /** * Re-authenticates the User using their password from the form, and then deletes the User in * Firebase Authentication. */ public async deleteAcount(user: User): Promise<void> { // The dialog automatically closes on submit. event.preventDefault() and event.stopPropagation() do not prevent that. const { password } = this.deleteAccountForm.value; // Validators prevent email1 or password being falsy, but TypeScript doesn't know that. // Additionally, all users are expected to have an email address. if (this.deleteAccountForm.invalid || !password || !user.email) { throw new Error('Invalid form submitted'); } this.$showForm.set(false); this.$errorCode.set(''); // Clear out any existing errors try { const emailCreds = EmailAuthProvider.credential(user.email, password); const credentials = await reauthenticateWithCredential(user, emailCreds); await deleteUser(credentials.user); await this._router.navigateByUrl('/'); } catch (err: unknown) { const code = getErrorCode(err); this.$errorCode.set(code); } this.$showForm.set(true); } /** * Opens the HTML Dialog containing the delete account form. */ public openDialog(): void { const dialogEl = this._$confirmDialog(); this.$errorCode.set(''); // Clear out any existing errors dialogEl.nativeElement.showModal(); } } |