All files / app/identity/login login.component.ts

100% Statements 22/22
100% Branches 4/4
100% Functions 2/2
100% Lines 22/22

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                                                                              1x                               14x   14x         14x       14x     14x   14x 14x 14x   14x         14x             3x     3x 1x     2x 2x   2x 2x 1x   1x 1x 1x        
import {
  ChangeDetectionStrategy,
  Component,
  inject,
  input,
  signal,
} from '@angular/core';
import type { InputSignal, Signal, WritableSignal } from '@angular/core';
import { Auth, signInWithEmailAndPassword } from '@angular/fire/auth';
import { FormGroup, ReactiveFormsModule } from '@angular/forms';
import type { FormControl, ValidationErrors } from '@angular/forms';
import { Router, RouterLink } from '@angular/router';
 
import { SpinnerComponent } from '~/app/shared/spinner/spinner.component';
 
import { AuthErrorMessagesComponent } from '../auth-error-messages/auth-error-messages.component';
import { getErrorCode } from '../error-code';
import { createEmailControl, createPasswordControl, PASSWORDS } from '../identity-forms';
 
/** Email & password credentials for Authentication */
type LoginFormGroup = FormGroup<{
  email: FormControl<string | null>;
  password: FormControl<string | null>;
}>;
 
/**
 * Email and password login form.
 */
@Component({
  selector: 'app-login',
  imports: [
    AuthErrorMessagesComponent,
    ReactiveFormsModule,
    RouterLink,
    SpinnerComponent,
  ],
  templateUrl: './login.component.html',
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class LoginComponent {
  /** Errors specific to the email field. */
  public readonly $emailCntrlErrors: Signal<ValidationErrors | undefined>;
  /** Aria-invalid attribute for email field. */
  public readonly $emailCntrlInvalid: Signal<boolean>;
  /** Login form error response code. */
  public readonly $errorCode: WritableSignal<string>;
  /** Errors specific to the password field. */
  public readonly $passwordCntrlErrors: Signal<ValidationErrors | undefined>;
  /** Aria-invalid attribute for password field. */
  public readonly $passwordCntrlInvalid: Signal<boolean>;
  /** Toggle Login form and spinner. */
  public readonly $showForm: WritableSignal<boolean>;
  public readonly emailCntrl: FormControl<string | null>;
  public readonly loginForm: LoginFormGroup;
  /** 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;
  /**
   * Navigate to root to allow default redirectTo Route to decide initial destination unless the
   * `next` query parameter is set.
   */
  public readonly next: InputSignal<string> = input<string>('/');
  public readonly passwordCntrl: FormControl<string | null>;
 
  private readonly _auth: Auth;
  private readonly _router: Router = inject(Router);
 
  constructor() {
    this._auth = inject(Auth);
 
    ({ $errors: this.$emailCntrlErrors, $invalid: this.$emailCntrlInvalid, control: this.emailCntrl } = createEmailControl());
    ({ $errors: this.$passwordCntrlErrors, $invalid: this.$passwordCntrlInvalid, control: this.passwordCntrl } = createPasswordControl());
    this.$showForm = signal<boolean>(true);
 
    this.loginForm = new FormGroup({
      email: this.emailCntrl,
      password: this.passwordCntrl,
    });
 
    this.$errorCode = signal<string>('');
  }
 
  /**
   * Login using credentials and then redirect to next view.
   */
  public async onSubmit(): Promise<void> {
    const { email, password } = this.loginForm.value;
 
    // Validators prevent email or password being falsy, but TypeScript doesn't know that.
    if (this.loginForm.invalid || !email || !password) {
      throw new Error('Invalid form submitted');
    }
 
    this.$errorCode.set(''); // Clear out any existing errors
    this.$showForm.set(false);
 
    try {
      await signInWithEmailAndPassword(this._auth, email, password);
      await this._router.navigateByUrl(this.next());
    } catch (err: unknown) {
      const code = getErrorCode(err);
      this.$errorCode.set(code);
      this.$showForm.set(true);
    }
  }
}