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:
@@ -30,6 +30,9 @@
|
||||
<link rel="apple-touch-startup-image" href="/splash/iphone-8.png"
|
||||
media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2)" />
|
||||
|
||||
<!-- Cloudflare Turnstile -->
|
||||
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
|
||||
|
||||
<title>spotlight.cam - Dance Event Video Exchange</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState } from 'react';
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Send, Mail, User, MessageSquare } from 'lucide-react';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
@@ -11,6 +11,8 @@ export default function ContactPage() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [success, setSuccess] = useState(false);
|
||||
const [turnstileToken, setTurnstileToken] = useState('');
|
||||
const turnstileRef = useRef(null);
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
firstName: '',
|
||||
@@ -20,6 +22,56 @@ export default function ContactPage() {
|
||||
message: '',
|
||||
});
|
||||
|
||||
// Setup Turnstile callbacks and render widget
|
||||
useEffect(() => {
|
||||
// Callback when Turnstile verification is successful
|
||||
window.onTurnstileSuccess = (token) => {
|
||||
setTurnstileToken(token);
|
||||
};
|
||||
|
||||
// Callback when Turnstile encounters an error
|
||||
window.onTurnstileError = () => {
|
||||
setError('CAPTCHA verification failed. Please try again.');
|
||||
setTurnstileToken('');
|
||||
};
|
||||
|
||||
// Wait for Turnstile script to load and render widget
|
||||
const renderTurnstile = () => {
|
||||
if (window.turnstile && turnstileRef.current && !turnstileRef.current.hasChildNodes()) {
|
||||
window.turnstile.render(turnstileRef.current, {
|
||||
sitekey: import.meta.env.VITE_TURNSTILE_SITE_KEY,
|
||||
callback: window.onTurnstileSuccess,
|
||||
'error-callback': window.onTurnstileError,
|
||||
theme: 'light',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Check if script is already loaded
|
||||
if (window.turnstile) {
|
||||
renderTurnstile();
|
||||
} else {
|
||||
// Wait for script to load
|
||||
const checkTurnstile = setInterval(() => {
|
||||
if (window.turnstile) {
|
||||
renderTurnstile();
|
||||
clearInterval(checkTurnstile);
|
||||
}
|
||||
}, 100);
|
||||
|
||||
return () => {
|
||||
clearInterval(checkTurnstile);
|
||||
delete window.onTurnstileSuccess;
|
||||
delete window.onTurnstileError;
|
||||
};
|
||||
}
|
||||
|
||||
return () => {
|
||||
delete window.onTurnstileSuccess;
|
||||
delete window.onTurnstileError;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleChange = (e) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData(prev => ({ ...prev, [name]: value }));
|
||||
@@ -28,6 +80,13 @@ export default function ContactPage() {
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
setError('');
|
||||
|
||||
// Validate Turnstile token
|
||||
if (!turnstileToken) {
|
||||
setError('Please complete the CAPTCHA verification');
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
@@ -36,6 +95,7 @@ export default function ContactPage() {
|
||||
email: formData.email,
|
||||
subject: formData.subject,
|
||||
message: formData.message,
|
||||
turnstileToken,
|
||||
};
|
||||
|
||||
// Add firstName and lastName only for non-logged-in users
|
||||
@@ -53,6 +113,11 @@ export default function ContactPage() {
|
||||
}, 3000);
|
||||
} catch (err) {
|
||||
setError(err.data?.error || 'Failed to submit contact form. Please try again.');
|
||||
// Reset Turnstile on error
|
||||
if (window.turnstile && turnstileRef.current) {
|
||||
window.turnstile.reset(turnstileRef.current);
|
||||
setTurnstileToken('');
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -208,11 +273,9 @@ export default function ContactPage() {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* TODO: CAPTCHA will go here */}
|
||||
<div className="bg-yellow-50 border border-yellow-200 rounded-md p-4">
|
||||
<p className="text-sm text-yellow-800">
|
||||
CAPTCHA verification coming soon
|
||||
</p>
|
||||
{/* Cloudflare Turnstile CAPTCHA */}
|
||||
<div>
|
||||
<div ref={turnstileRef}></div>
|
||||
</div>
|
||||
|
||||
{/* Error Message */}
|
||||
|
||||
Reference in New Issue
Block a user