All files / src/app/extensions/captcha/shared/captcha-v3 captcha-v3.component.ts

95.34% Statements 41/43
83.33% Branches 5/6
90.9% Functions 10/11
95% Lines 38/40

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  2x                     2x 2x 2x 2x 2x 2x   2x   2x                               2x     9x   9x     9x 9x       8x       8x       8x 8x 4x           4x   10x   5x 5x 5x   5x       5x 5x         10x 5x 5x     5x 5x       5x             5x 5x 5x 5x                             2x  
// eslint-disable-next-line max-classes-per-file
import {
  AfterViewInit,
  ChangeDetectionStrategy,
  Component,
  DestroyRef,
  ElementRef,
  Input,
  NgModule,
  OnInit,
  inject,
} from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { FormGroup, Validators } from '@angular/forms';
import { TranslatePipe } from '@ngx-translate/core';
import { RECAPTCHA_V3_SITE_KEY, ReCaptchaV3Service, RecaptchaV3Module } from 'ng-recaptcha-2';
import { fromEvent, race, timer } from 'rxjs';
import { exhaustMap, filter, map, tap } from 'rxjs/operators';
 
import { DirectivesModule } from 'ish-core/directives.module';
 
import {
  SitekeyProviderService,
  getSynchronizedSiteKey,
} from '../../exports/sitekey-provider/sitekey-provider.service';
 
/**
 * The Captcha V3 Component
 *
 * Displays a captcha widget (V3) and saves the response token in the given form. It should only be used by {@link CaptchaComponent}
 */
@Component({
  selector: 'ish-captcha-v3',
  standalone: false,
  templateUrl: './captcha-v3.component.html',
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CaptchaV3Component implements OnInit, AfterViewInit {
  @Input({ required: true }) parentForm: FormGroup;
 
  private tokenReady = false;
 
  private destroyRef = inject(DestroyRef);
 
  constructor(
    private elementRef: ElementRef<HTMLElement>,
    private recaptchaV3Service: ReCaptchaV3Service
  ) {}
 
  ngOnInit() {
    this.parentForm.get('captchaAction').setValidators([Validators.required]);
  }
 
  ngAfterViewInit() {
    Iif (SSR) {
      return;
    }
 
    const formElement = this.elementRef.nativeElement.closest('form');
    if (!formElement) {
      return;
    }
 
    // Intercept form submit in capture phase: block until a fresh reCAPTCHA token is obtained,
    // then re-trigger the submit so Angular's (ngSubmit) fires with the token in place.
    let pendingSubmitter: HTMLElement;
    fromEvent<SubmitEvent>(formElement, 'submit', { capture: true })
      .pipe(
        filter(event => this.interceptSubmit(event)),
        tap(event => {
          pendingSubmitter = event.submitter;
          event.preventDefault();
          event.stopPropagation();
        }),
        exhaustMap(() => this.requestToken()),
        takeUntilDestroyed(this.destroyRef)
      )
      .subscribe(token => {
        this.applyTokenAndResubmit(token, formElement, pendingSubmitter);
        pendingSubmitter = undefined;
      });
  }
 
  private interceptSubmit(_event: SubmitEvent): boolean {
    if (this.tokenReady) {
      this.tokenReady = false;
      return false;
    }
    // Clear previous captcha errors so they don't block a retry
    this.parentForm.get('captcha').setErrors(undefined);
    return this.parentForm.valid;
  }
 
  private requestToken() {
    return race(
      this.recaptchaV3Service.execute(this.parentForm.get('captchaAction').value),
      timer(2000).pipe(map(() => ''))
    );
  }
 
  private applyTokenAndResubmit(token: string, formElement: HTMLFormElement, submitter: HTMLElement) {
    this.parentForm.get('captcha').setValue(token);
    this.parentForm.get('captcha').updateValueAndValidity();
    this.tokenReady = true;
    formElement.requestSubmit(submitter);
  }
}
 
@NgModule({
  declarations: [CaptchaV3Component],
  imports: [DirectivesModule, RecaptchaV3Module, TranslatePipe],
  providers: [
    {
      provide: RECAPTCHA_V3_SITE_KEY,
      useFactory: getSynchronizedSiteKey,
      deps: [SitekeyProviderService],
    },
  ],
})
export class CaptchaV3ComponentModule {}