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

100% Statements 26/26
100% Branches 9/9
100% Functions 4/4
100% Lines 25/25

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                                                            1x 8x 1x 1x   7x                               1x   8x   8x   8x   8x                           8x           8x           8x     8x 8x   8x 6x   6x   5x   1x     2x 1x   2x 1x           8x        
import {
  ChangeDetectionStrategy,
  Component,
  computed,
  effect,
  inject,
  input,
} from '@angular/core';
import type { InputSignal, Signal } from '@angular/core';
import { Router } from '@angular/router';
 
import { SpinnerComponent } from '@app/shared/spinner/spinner.component';
 
/**
 * https://firebase.google.com/docs/reference/js/auth.actioncodeurl
 */
export interface ActionCodeState {
  /** We may include a next url when verifying email. */
  readonly continueUrl: string | undefined;
  /** Currently not used, but the language code of the email sent to with the oobCode. */
  readonly lang: string | undefined;
  /** Action to be performed by the oobCode. */
  readonly mode: string | undefined;
  /** Out of Band Code to perform sensitive Authentication action. */
  readonly oobCode: string;
}
 
/**
 * Firebase Action continueUrl is fully qualified. If it has a value convert it into a relative URL.
 */
const cleanUrl = (continueUrl: string | undefined): string | undefined => {
  if (continueUrl) {
    const url = new URL(continueUrl);
    return `${url.pathname}${url.search}${url.hash}`;
  }
  return undefined;
};
 
/**
 * Self handle Firebase Authentication Actions
 * https://firebase.google.com/docs/auth/custom-email-handler
 *
 * Strips the query string parameters from the URL and stores them in the Router state for the
 * specific Components to handle.
 */
@Component({
  changeDetection: ChangeDetectionStrategy.OnPush,
  imports: [ SpinnerComponent ],
  selector: 'app-actions',
  template: '<app-spinner class="modal-block" />',
})
export class ActionsComponent {
  /** Query parameter from Firebase Authentication link. */
  public readonly continueUrl: InputSignal<string | undefined> = input<string>();
  /** Query parameter from Firebase Authentication link. */
  public readonly lang: InputSignal<string | undefined> = input<string>();
  /** Query parameter from Firebase Authentication link. */
  public readonly mode: InputSignal<string> = input.required<string>();
  /** Query parameter from Firebase Authentication link. */
  public readonly oobCode: InputSignal<string> = input.required<string>();
 
  private readonly _$actionState: Signal<Partial<ActionCodeState>>;
  private readonly _modePaths: Record<string, string>;
  private readonly _router: Router;
 
  /**
   * Collects the Firebase action codes from the URL query parameters and stores them in the router
   * state.
   * Maps Firebase action mode to our specific Components for handling the sensitive actions.
   * Replaces this URL in the history stack to prevent reverse navigation from attepting to apply
   * the code again.
   */
  constructor() {
    this._$actionState = computed((): Partial<ActionCodeState> => ({
      continueUrl: cleanUrl(this.continueUrl()),
      lang: this.lang(),
      mode: this.mode(),
      oobCode: this.oobCode(),
    }));
    this._modePaths = {
      recoverEmail: '/recover-email',
      resetPassword: '/reset-password',
      verifyAndChangeEmail: '/verify-email',
      verifyEmail: '/verify-email',
    };
    this._router = inject(Router);
 
    // eslint-disable-next-line @typescript-eslint/no-misused-promises -- This works, for now, but perhaps not in the future!
    effect(async (): Promise<void> => {
      const state = this._$actionState();
 
      if (state.mode && state.oobCode) {
        const path = this._modePaths[state.mode];
 
        if (path) {
          // If this promise is not awaited then test cases fail :-(
          await this._router.navigateByUrl(path, { replaceUrl: true, state });
        } else {
          console.error(`Unknown mode '${state.mode}'`);
        }
      } else {
        if (!state.mode) {
          console.error('Missing ActionCodeSettings#mode');
        }
        if (!state.oobCode) {
          console.error('Missing ActionCodeSettings#oobCode');
        }
      }
 
      // Something about this action is invalid.
      // Navigate to root to allow default redirectTo Route to decide initial destination.
      await this._router.navigateByUrl('/', { replaceUrl: true });
    });
  }
}