feat(security): implement Cloudflare Turnstile CAPTCHA on contact form
- Add Turnstile script to frontend/index.html - Implement programmatic widget rendering in ContactPage - Add backend verification via Cloudflare API - Include client IP in verification request - Update CSP headers to allow Cloudflare resources - Add environment variable configuration for site and secret keys - Pass VITE_TURNSTILE_SITE_KEY to frontend container - Add validation and error handling for CAPTCHA tokens
This commit is contained in:
@@ -58,3 +58,7 @@ ENABLE_SCHEDULER=false
|
||||
SCHEDULER_INTERVAL_SEC=300
|
||||
# Per-event minimum time between runs in seconds (default 60s)
|
||||
MATCHING_MIN_INTERVAL_SEC=60
|
||||
|
||||
# Cloudflare Turnstile (CAPTCHA)
|
||||
# Get your secret key from: https://dash.cloudflare.com/
|
||||
TURNSTILE_SECRET_KEY=your-secret-key-here
|
||||
|
||||
@@ -58,3 +58,7 @@ ENABLE_SCHEDULER=false
|
||||
SCHEDULER_INTERVAL_SEC=300
|
||||
# Per-event minimum time between runs in seconds to avoid thrashing
|
||||
MATCHING_MIN_INTERVAL_SEC=120
|
||||
|
||||
# Cloudflare Turnstile (CAPTCHA)
|
||||
# Get your secret key from: https://dash.cloudflare.com/
|
||||
TURNSTILE_SECRET_KEY=your-production-secret-key-here
|
||||
|
||||
@@ -57,10 +57,10 @@ router.post('/contact', [
|
||||
body('email').isEmail().normalizeEmail().withMessage('Valid email is required'),
|
||||
body('subject').trim().isLength({ min: 3, max: 255 }).withMessage('Subject must be between 3 and 255 characters'),
|
||||
body('message').trim().isLength({ min: 10, max: 5000 }).withMessage('Message must be between 10 and 5000 characters'),
|
||||
body('turnstileToken').notEmpty().withMessage('CAPTCHA verification is required'),
|
||||
// For non-logged-in users
|
||||
body('firstName').optional().trim().isLength({ min: 1, max: 100 }).withMessage('First name must be between 1 and 100 characters'),
|
||||
body('lastName').optional().trim().isLength({ min: 1, max: 100 }).withMessage('Last name must be between 1 and 100 characters'),
|
||||
// TODO: Add CAPTCHA validation here
|
||||
], async (req, res) => {
|
||||
try {
|
||||
// Validate request
|
||||
@@ -73,7 +73,41 @@ router.post('/contact', [
|
||||
});
|
||||
}
|
||||
|
||||
const { email, subject, message, firstName, lastName } = req.body;
|
||||
const { email, subject, message, firstName, lastName, turnstileToken } = req.body;
|
||||
|
||||
// Verify Turnstile token
|
||||
const turnstileSecret = process.env.TURNSTILE_SECRET_KEY;
|
||||
const turnstileVerifyUrl = 'https://challenges.cloudflare.com/turnstile/v0/siteverify';
|
||||
|
||||
console.log('[Turnstile] Verifying token, secret present:', !!turnstileSecret);
|
||||
|
||||
try {
|
||||
const turnstileResponse = await fetch(turnstileVerifyUrl, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
secret: turnstileSecret,
|
||||
response: turnstileToken,
|
||||
remoteip: getClientIP(req),
|
||||
}),
|
||||
});
|
||||
|
||||
const turnstileResult = await turnstileResponse.json();
|
||||
console.log('[Turnstile] Verification result:', JSON.stringify(turnstileResult));
|
||||
|
||||
if (!turnstileResult.success) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'CAPTCHA verification failed. Please try again.',
|
||||
});
|
||||
}
|
||||
} catch (turnstileError) {
|
||||
console.error('Turnstile verification error:', turnstileError);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
error: 'CAPTCHA verification failed. Please try again.',
|
||||
});
|
||||
}
|
||||
|
||||
// Check if user is authenticated
|
||||
const userId = req.user?.id || null;
|
||||
|
||||
Reference in New Issue
Block a user