import { CanActivate, ExecutionContext, Injectable, HttpException, Logger } from '@nestjs/common'; // Cloudflare Turnstile verification endpoint const TURNSTILE_VERIFY_URL = 'https://challenges.cloudflare.com/turnstile/v0/siteverify'; @Injectable() export class CaptchaGuard implements CanActivate { private readonly logger = new Logger(CaptchaGuard.name); private readonly secretKey: string; constructor() { this.secretKey = process.env.RECAPTCHA_SECRET_KEY ?? ''; } async canActivate(context: ExecutionContext): Promise { if (!this.secretKey) { this.logger.warn('RECAPTCHA_SECRET_KEY not set — captcha disabled'); return true; } const request = context.switchToHttp().getRequest(); const token = request.body?.captchaToken; if (!token) throw new HttpException('Captcha token required', 400); try { const res = await fetch(TURNSTILE_VERIFY_URL, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ secret: this.secretKey, response: token }), }); const data = await res.json(); if (!data.success) { this.logger.warn(`Captcha failed: success=${data.success} errors=${JSON.stringify(data['error-codes'] ?? [])}`); throw new HttpException('Captcha verification failed', 403); } return true; } catch (err: any) { if (err instanceof HttpException) throw err; this.logger.error(`Captcha verification error: ${err.message}`); return true; } } }