mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-21 18:11:12 -05:00
feat: Integrate CAPTCHA into password updates
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user