feat: Integrate CAPTCHA into password updates

This commit is contained in:
gpt-engineer-app[bot]
2025-10-01 15:16:25 +00:00
parent ab5961a33b
commit 9ae7e2908e

View File

@@ -17,6 +17,8 @@ import { useToast } from '@/hooks/use-toast';
import { supabase } from '@/integrations/supabase/client'; import { supabase } from '@/integrations/supabase/client';
import { Loader2, Shield, CheckCircle2 } from 'lucide-react'; import { Loader2, Shield, CheckCircle2 } from 'lucide-react';
import { InputOTP, InputOTPGroup, InputOTPSlot } from '@/components/ui/input-otp'; import { InputOTP, InputOTPGroup, InputOTPSlot } from '@/components/ui/input-otp';
import { TurnstileCaptcha } from '@/components/auth/TurnstileCaptcha';
import { useTheme } from '@/components/theme/ThemeProvider';
const passwordSchema = z.object({ const passwordSchema = z.object({
currentPassword: z.string().min(1, 'Current password is required'), currentPassword: z.string().min(1, 'Current password is required'),
@@ -39,12 +41,15 @@ type Step = 'password' | 'mfa' | 'success';
export function PasswordUpdateDialog({ open, onOpenChange, onSuccess }: PasswordUpdateDialogProps) { export function PasswordUpdateDialog({ open, onOpenChange, onSuccess }: PasswordUpdateDialogProps) {
const { toast } = useToast(); const { toast } = useToast();
const { theme } = useTheme();
const [step, setStep] = useState<Step>('password'); const [step, setStep] = useState<Step>('password');
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [nonce, setNonce] = useState<string>(''); const [nonce, setNonce] = useState<string>('');
const [newPassword, setNewPassword] = useState<string>(''); const [newPassword, setNewPassword] = useState<string>('');
const [hasMFA, setHasMFA] = useState(false); const [hasMFA, setHasMFA] = useState(false);
const [totpCode, setTotpCode] = useState(''); const [totpCode, setTotpCode] = useState('');
const [captchaToken, setCaptchaToken] = useState<string>('');
const [captchaKey, setCaptchaKey] = useState(0);
const form = useForm<PasswordFormData>({ const form = useForm<PasswordFormData>({
resolver: zodResolver(passwordSchema), resolver: zodResolver(passwordSchema),
@@ -75,15 +80,32 @@ export function PasswordUpdateDialog({ open, onOpenChange, onSuccess }: Password
}; };
const onSubmit = async (data: PasswordFormData) => { const onSubmit = async (data: PasswordFormData) => {
if (!captchaToken) {
toast({
title: 'CAPTCHA Required',
description: 'Please complete the CAPTCHA verification.',
variant: 'destructive'
});
return;
}
setLoading(true); setLoading(true);
try { try {
// Step 1: Reauthenticate with current password to get a nonce // Step 1: Reauthenticate with current password to get a nonce
const { error: signInError } = await supabase.auth.signInWithPassword({ const { error: signInError } = await supabase.auth.signInWithPassword({
email: (await supabase.auth.getUser()).data.user?.email || '', email: (await supabase.auth.getUser()).data.user?.email || '',
password: data.currentPassword password: data.currentPassword,
options: {
captchaToken
}
}); });
if (signInError) throw signInError; if (signInError) {
// Reset CAPTCHA on authentication failure
setCaptchaToken('');
setCaptchaKey(prev => prev + 1);
throw signInError;
}
// Step 2: Generate nonce for secure password update // Step 2: Generate nonce for secure password update
const { data: sessionData } = await supabase.auth.getSession(); const { data: sessionData } = await supabase.auth.getSession();
@@ -215,6 +237,8 @@ export function PasswordUpdateDialog({ open, onOpenChange, onSuccess }: Password
setStep('password'); setStep('password');
form.reset(); form.reset();
setTotpCode(''); setTotpCode('');
setCaptchaToken('');
setCaptchaKey(prev => prev + 1);
} }
}; };
@@ -279,11 +303,23 @@ export function PasswordUpdateDialog({ open, onOpenChange, onSuccess }: Password
)} )}
</div> </div>
<div className="space-y-2">
<TurnstileCaptcha
key={captchaKey}
onSuccess={setCaptchaToken}
onError={() => setCaptchaToken('')}
onExpire={() => setCaptchaToken('')}
siteKey={import.meta.env.VITE_TURNSTILE_SITE_KEY}
theme={theme === 'dark' ? 'dark' : 'light'}
size="normal"
/>
</div>
<DialogFooter> <DialogFooter>
<Button type="button" variant="outline" onClick={handleClose} disabled={loading}> <Button type="button" variant="outline" onClick={handleClose} disabled={loading}>
Cancel Cancel
</Button> </Button>
<Button type="submit" disabled={loading}> <Button type="submit" disabled={loading || !captchaToken}>
{loading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />} {loading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
{hasMFA ? 'Continue' : 'Update Password'} {hasMFA ? 'Continue' : 'Update Password'}
</Button> </Button>