mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-24 05:31:13 -05:00
feat: Integrate Cloudflare Turnstile CAPTCHA
This commit is contained in:
138
src/components/auth/TurnstileCaptcha.tsx
Normal file
138
src/components/auth/TurnstileCaptcha.tsx
Normal file
@@ -0,0 +1,138 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { Turnstile } from '@marsidev/react-turnstile';
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||
import { AlertCircle, RefreshCw } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
interface TurnstileCaptchaProps {
|
||||
onSuccess: (token: string) => void;
|
||||
onError?: (error: string) => void;
|
||||
onExpire?: () => void;
|
||||
siteKey?: string;
|
||||
theme?: 'light' | 'dark' | 'auto';
|
||||
size?: 'normal' | 'compact';
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function TurnstileCaptcha({
|
||||
onSuccess,
|
||||
onError,
|
||||
onExpire,
|
||||
siteKey = "0x4AAAAAAAk8oZ8Z8Z8Z8Z8Z", // Default test key - replace in production
|
||||
theme = 'auto',
|
||||
size = 'normal',
|
||||
className = ''
|
||||
}: TurnstileCaptchaProps) {
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [key, setKey] = useState(0);
|
||||
const turnstileRef = useRef<any>(null);
|
||||
|
||||
const handleSuccess = (token: string) => {
|
||||
setError(null);
|
||||
setLoading(false);
|
||||
onSuccess(token);
|
||||
};
|
||||
|
||||
const handleError = (errorCode: string) => {
|
||||
setLoading(false);
|
||||
const errorMessage = getErrorMessage(errorCode);
|
||||
setError(errorMessage);
|
||||
onError?.(errorMessage);
|
||||
};
|
||||
|
||||
const handleExpire = () => {
|
||||
setError('CAPTCHA expired. Please try again.');
|
||||
onExpire?.();
|
||||
};
|
||||
|
||||
const handleLoad = () => {
|
||||
setLoading(false);
|
||||
setError(null);
|
||||
};
|
||||
|
||||
const resetCaptcha = () => {
|
||||
setKey(prev => prev + 1);
|
||||
setError(null);
|
||||
setLoading(true);
|
||||
};
|
||||
|
||||
const getErrorMessage = (errorCode: string): string => {
|
||||
switch (errorCode) {
|
||||
case 'network-error':
|
||||
return 'Network error. Please check your connection and try again.';
|
||||
case 'timeout':
|
||||
return 'CAPTCHA timed out. Please try again.';
|
||||
case 'invalid-sitekey':
|
||||
return 'Invalid site configuration. Please contact support.';
|
||||
case 'token-already-spent':
|
||||
return 'CAPTCHA token already used. Please refresh and try again.';
|
||||
default:
|
||||
return 'CAPTCHA verification failed. Please try again.';
|
||||
}
|
||||
};
|
||||
|
||||
// Auto-reset on theme changes
|
||||
useEffect(() => {
|
||||
resetCaptcha();
|
||||
}, [theme]);
|
||||
|
||||
if (!siteKey || siteKey === "0x4AAAAAAAk8oZ8Z8Z8Z8Z8Z") {
|
||||
return (
|
||||
<Alert className="border-yellow-200 bg-yellow-50 dark:border-yellow-800 dark:bg-yellow-950">
|
||||
<AlertCircle className="h-4 w-4 text-yellow-600 dark:text-yellow-400" />
|
||||
<AlertDescription className="text-yellow-800 dark:text-yellow-200">
|
||||
CAPTCHA is using test keys. Configure VITE_TURNSTILE_SITE_KEY for production.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`space-y-3 ${className}`}>
|
||||
<div className="flex flex-col items-center">
|
||||
{loading && (
|
||||
<div className="flex items-center justify-center p-4 border border-dashed border-muted-foreground/30 rounded-lg bg-muted/20">
|
||||
<RefreshCw className="w-4 h-4 animate-spin mr-2 text-muted-foreground" />
|
||||
<span className="text-sm text-muted-foreground">Loading CAPTCHA...</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={loading ? 'opacity-0 absolute' : 'opacity-100'}>
|
||||
<Turnstile
|
||||
key={key}
|
||||
ref={turnstileRef}
|
||||
siteKey={siteKey}
|
||||
onSuccess={handleSuccess}
|
||||
onError={handleError}
|
||||
onExpire={handleExpire}
|
||||
onLoad={handleLoad}
|
||||
options={{
|
||||
theme,
|
||||
size,
|
||||
tabIndex: 0
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<Alert variant="destructive">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription className="flex items-center justify-between">
|
||||
<span>{error}</span>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={resetCaptcha}
|
||||
className="ml-2 h-6 px-2 text-xs"
|
||||
>
|
||||
<RefreshCw className="w-3 h-3 mr-1" />
|
||||
Retry
|
||||
</Button>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user