All files / app/identity/change-password change-password.component.ts

100% Statements 23/23
100% Branches 5/5
100% Functions 2/2
100% Lines 23/23

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 135 136 137 138                                                                                          1x                                           21x   21x           21x         21x         21x           21x                 21x   21x 21x     21x               4x       4x 1x     3x 3x   3x 3x 3x 2x   2x 2x     3x      
import { AsyncPipe } from '@angular/common';
import {
  ChangeDetectionStrategy,
  Component,
  inject,
  signal,
} from '@angular/core';
import type { Signal, WritableSignal } from '@angular/core';
import { EmailAuthProvider, reauthenticateWithCredential, updatePassword } 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 { 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';
import { confirmMatch, confirmMatchFormErrors } from '../validators/confirm-match';
 
/**
 * Collects the User's current password and their new password with confirmation.
 */
type ChangePasswordFormGroup = FormGroup<{
  currentPw: FormControl<string | null>;
  password1: FormControl<string | null>;
  password2: FormControl<string | null>;
}>;
 
/**
 * Form to change User's password using the current password.
 */
@Component({
  changeDetection: ChangeDetectionStrategy.OnPush,
  imports: [
    AsyncPipe,
    AuthErrorMessagesComponent,
    ReactiveFormsModule,
    SpinnerComponent,
  ],
  selector: 'app-change-password',
  templateUrl: './change-password.component.html',
})
export class ChangePasswordComponent {
  /** Errors specifically for the current password field. */
  public readonly $currentPwCntrlErrors: Signal<ValidationErrors | undefined>;
  /** Aria-invalid attribute for the current password field. */
  public readonly $currentPwCntrlInvalid: Signal<boolean>;
  /** Firebase response error code. */
  public readonly $errorCode: WritableSignal<string>;
  /** Aria-invalid attribute for the form. */
  public readonly $formPasswordsInvalid: Signal<boolean>;
  /** Errors specifically for the first new password field. */
  public readonly $password1CntrlErrors: Signal<ValidationErrors | undefined>;
  /** Aria-invalid attribute for the first new password field. */
  public readonly $password1CntrlInvalid: Signal<boolean>;
  /** Errors specifically for the second new password field. */
  public readonly $password2CntrlErrors: Signal<ValidationErrors | undefined>;
  /** Aria-invalid attribute for the second new password field. */
  public readonly $password2CntrlInvalid: Signal<boolean>;
  /** Toggle showing the form and spinner */
  public readonly $showForm: WritableSignal<boolean>;
  public readonly changePasswordForm: ChangePasswordFormGroup;
  public readonly currentPwCntrl: FormControl<string | null>;
  /** 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 password1Cntrl: FormControl<string | null>;
  public readonly password2Cntrl: FormControl<string | null>;
  public readonly user$: MaybeUser$;
 
  constructor() {
    ({
      $errors: this.$currentPwCntrlErrors,
      $invalid: this.$currentPwCntrlInvalid,
      control: this.currentPwCntrl,
    } = createPasswordControl());
    ({
      $errors: this.$password1CntrlErrors,
      $invalid: this.$password1CntrlInvalid,
      control: this.password1Cntrl,
    } = createPasswordControl(true));
    ({
      $errors: this.$password2CntrlErrors,
      $invalid: this.$password2CntrlInvalid,
      control: this.password2Cntrl,
    } = createPasswordControl());
 
    this.changePasswordForm = new FormGroup(
      {
        currentPw: this.currentPwCntrl,
        password1: this.password1Cntrl,
        password2: this.password2Cntrl,
      },
      confirmMatch('password1', 'password2'),
    );
 
    this.$formPasswordsInvalid = confirmMatchFormErrors(this.changePasswordForm, this.password1Cntrl, this.password2Cntrl);
 
    this.$errorCode = signal<string>('');
    this.$showForm = signal<boolean>(true);
 
    // Not handling non-logged in users because the Route guards should.
    this.user$ = inject(USER$);
  }
 
  /**
   * Re-authenticates use using the submitted current password, and then updates the password using
   * the new password from the form.
   */
  public async onSubmit(user: User): Promise<void> {
    const { currentPw, password1 } = this.changePasswordForm.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.changePasswordForm.invalid || !currentPw || !password1 || !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, currentPw);
      const credentials = await reauthenticateWithCredential(user, emailCreds);
      await updatePassword(credentials.user, password1);
    } catch (err: unknown) {
      const code = getErrorCode(err);
      this.$errorCode.set(code);
    }
 
    this.$showForm.set(true);
  }
}