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 | 1x 1x 16x 16x 16x 16x 16x 3x 3x 1x 1x 2x 2x 16x 1x 1x 12x 12x 12x 12x 16x 2x 2x 12x 12x 12x 12x 12x 12x 12x 11x 16x 8x 8x 8x 8x 8x 8x 3x 3x 3x 3x 3x 16x 1x 1x 1x 16x 8x 8x 1x 1x 7x 7x 7x 7x 7x 7x 7x 7x 7x 8x 8x 1x 1x 1x | /**
* CAPTCHA Verification using Cloudflare Turnstile
*
* Prevents abuse of OTP and authentication endpoints
*/
const TURNSTILE_SECRET_KEY = process.env.TURNSTILE_SECRET_KEY;
const TURNSTILE_VERIFY_URL = 'https://challenges.cloudflare.com/turnstile/v0/siteverify';
export interface CaptchaVerifyResult {
success: boolean;
error?: string;
challenge_ts?: string;
hostname?: string;
}
/**
* Verify Cloudflare Turnstile token
*/
export async function verifyCaptcha(
token: string,
ipAddress?: string
): Promise<CaptchaVerifyResult> {
if (!TURNSTILE_SECRET_KEY) {
console.warn('CAPTCHA: Turnstile secret key not configured, skipping verification');
// In development, allow without CAPTCHA
if (process.env.NODE_ENV === 'development') {
return { success: true };
}
return { success: false, error: 'CAPTCHA not configured' };
}
if (!token) {
return { success: false, error: 'CAPTCHA token is required' };
}
try {
const formData = new URLSearchParams();
formData.append('secret', TURNSTILE_SECRET_KEY);
formData.append('response', token);
if (ipAddress) {
formData.append('remoteip', ipAddress);
}
const response = await fetch(TURNSTILE_VERIFY_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: formData.toString(),
});
const data = await response.json();
if (!data.success) {
console.error('CAPTCHA verification failed:', data['error-codes']);
return {
success: false,
error: getCaptchaErrorMessage(data['error-codes']),
};
}
return {
success: true,
challenge_ts: data.challenge_ts,
hostname: data.hostname,
};
} catch (error) {
console.error('CAPTCHA verification error:', error);
return { success: false, error: 'Failed to verify CAPTCHA' };
}
}
/**
* Get human-readable error message for CAPTCHA errors
*/
function getCaptchaErrorMessage(errorCodes: string[]): string {
if (!errorCodes || errorCodes.length === 0) {
return 'CAPTCHA verification failed';
}
const errorMessages: Record<string, string> = {
'missing-input-secret': 'CAPTCHA configuration error',
'invalid-input-secret': 'CAPTCHA configuration error',
'missing-input-response': 'Please complete the CAPTCHA',
'invalid-input-response': 'Invalid CAPTCHA. Please try again.',
'bad-request': 'Invalid CAPTCHA request',
'timeout-or-duplicate': 'CAPTCHA expired. Please try again.',
'internal-error': 'CAPTCHA service error. Please try again.',
};
return errorMessages[errorCodes[0]] || 'CAPTCHA verification failed';
}
/**
* Check if CAPTCHA is enabled
*/
export function isCaptchaEnabled(): boolean {
return !!TURNSTILE_SECRET_KEY;
}
|