refactor: enhance captcha handling in login and register forms with reset functionality

This commit is contained in:
javayhu 2025-08-22 01:12:13 +08:00
parent a6a5d92dc1
commit 7f4a7a61a2
3 changed files with 77 additions and 38 deletions

View File

@ -24,7 +24,7 @@ import { zodResolver } from '@hookform/resolvers/zod';
import { EyeIcon, EyeOffIcon, Loader2Icon } from 'lucide-react';
import { useLocale, useTranslations } from 'next-intl';
import { useSearchParams } from 'next/navigation';
import { useState } from 'react';
import { useRef, useState } from 'react';
import { useForm, useWatch } from 'react-hook-form';
import * as z from 'zod';
import { Captcha } from '../shared/captcha';
@ -56,6 +56,7 @@ export const LoginForm = ({
const [success, setSuccess] = useState<string | undefined>('');
const [isPending, setIsPending] = useState(false);
const [showPassword, setShowPassword] = useState(false);
const captchaRef = useRef<any>(null);
// Check if credential login is enabled
const credentialLoginEnabled = websiteConfig.auth.enableCredentialLogin;
@ -92,6 +93,15 @@ export const LoginForm = ({
name: 'captchaToken',
});
// Function to reset captcha
const resetCaptcha = () => {
form.setValue('captchaToken', '');
// Try to reset the Turnstile widget if available
if (captchaRef.current && typeof captchaRef.current.reset === 'function') {
captchaRef.current.reset();
}
};
const onSubmit = async (values: z.infer<typeof LoginSchema>) => {
// Validate captcha token if turnstile is enabled and site key is available
if (captchaConfigured && values.captchaToken) {
@ -107,6 +117,8 @@ export const LoginForm = ({
console.error('login, captcha invalid:', values.captchaToken);
const errorMessage = captchaResult?.data?.error || t('captchaInvalid');
setError(errorMessage);
setIsPending(false);
resetCaptcha(); // Reset captcha on validation failure
return;
}
}
@ -139,6 +151,10 @@ export const LoginForm = ({
onError: (ctx) => {
console.error('login, error:', ctx.error);
setError(`${ctx.error.status}: ${ctx.error.message}`);
// Reset captcha on login error
if (captchaConfigured) {
resetCaptcha();
}
},
}
);
@ -237,6 +253,7 @@ export const LoginForm = ({
<FormSuccess message={success} />
{captchaConfigured && (
<Captcha
ref={captchaRef}
onSuccess={(token) => form.setValue('captchaToken', token)}
validationError={form.formState.errors.captchaToken?.message}
/>

View File

@ -22,7 +22,7 @@ import { zodResolver } from '@hookform/resolvers/zod';
import { EyeIcon, EyeOffIcon, Loader2Icon } from 'lucide-react';
import { useLocale, useTranslations } from 'next-intl';
import { useSearchParams } from 'next/navigation';
import { useState } from 'react';
import { useRef, useState } from 'react';
import { useForm, useWatch } from 'react-hook-form';
import * as z from 'zod';
import { Captcha } from '../shared/captcha';
@ -51,6 +51,7 @@ export const RegisterForm = ({
const [success, setSuccess] = useState<string | undefined>('');
const [isPending, setIsPending] = useState(false);
const [showPassword, setShowPassword] = useState(false);
const captchaRef = useRef<any>(null);
// Check if credential login is enabled
const credentialLoginEnabled = websiteConfig.auth.enableCredentialLogin;
@ -91,6 +92,15 @@ export const RegisterForm = ({
name: 'captchaToken',
});
// Function to reset captcha
const resetCaptcha = () => {
form.setValue('captchaToken', '');
// Try to reset the Turnstile widget if available
if (captchaRef.current && typeof captchaRef.current.reset === 'function') {
captchaRef.current.reset();
}
};
const onSubmit = async (values: z.infer<typeof RegisterSchema>) => {
// Validate captcha token if turnstile is enabled and site key is available
if (captchaConfigured && values.captchaToken) {
@ -106,6 +116,8 @@ export const RegisterForm = ({
console.error('register, captcha invalid:', values.captchaToken);
const errorMessage = captchaResult?.data?.error || t('captchaInvalid');
setError(errorMessage);
setIsPending(false);
resetCaptcha(); // Reset captcha on validation failure
return;
}
}
@ -148,6 +160,10 @@ export const RegisterForm = ({
// sign up fail, display the error message
console.error('register, error:', ctx.error);
setError(`${ctx.error.status}: ${ctx.error.message}`);
// Reset captcha on registration error
if (captchaConfigured) {
resetCaptcha();
}
},
}
);
@ -247,6 +263,7 @@ export const RegisterForm = ({
<FormSuccess message={success} />
{captchaConfigured && (
<Captcha
ref={captchaRef}
onSuccess={(token) => form.setValue('captchaToken', token)}
validationError={form.formState.errors.captchaToken?.message}
/>

View File

@ -5,7 +5,7 @@ import { websiteConfig } from '@/config/website';
import { useLocale } from 'next-intl';
import { useTheme } from 'next-themes';
import dynamic from 'next/dynamic';
import type { ComponentProps } from 'react';
import { type ComponentProps, forwardRef } from 'react';
const Turnstile = dynamic(
() => import('@marsidev/react-turnstile').then((mod) => mod.Turnstile),
@ -21,41 +21,46 @@ type Props = Omit<ComponentProps<typeof Turnstile>, 'siteKey'> & {
/**
* Captcha component for Cloudflare Turnstile
*/
export const Captcha = ({ validationError, ...props }: Props) => {
const turnstileEnabled = websiteConfig.features.enableTurnstileCaptcha;
const siteKey = process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY;
export const Captcha = forwardRef<any, Props>(
({ validationError, ...props }, ref) => {
const turnstileEnabled = websiteConfig.features.enableTurnstileCaptcha;
const siteKey = process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY;
// If turnstile is disabled in config, don't render anything
if (!turnstileEnabled) {
return null;
// If turnstile is disabled in config, don't render anything
if (!turnstileEnabled) {
return null;
}
// If turnstile is enabled but site key is missing, show error message
if (!siteKey) {
console.error('Captcha: NEXT_PUBLIC_TURNSTILE_SITE_KEY is not set');
return null;
}
const theme = useTheme();
const locale = useLocale();
return (
<>
<Turnstile
ref={ref}
options={{
size: 'flexible',
language: locale,
theme: theme.theme === 'dark' ? 'dark' : 'light',
}}
{...props}
siteKey={siteKey}
/>
{validationError && (
<FormMessage className="text-red-500 mt-2">
{validationError}
</FormMessage>
)}
</>
);
}
);
// If turnstile is enabled but site key is missing, show error message
if (!siteKey) {
console.error('Captcha: NEXT_PUBLIC_TURNSTILE_SITE_KEY is not set');
return null;
}
const theme = useTheme();
const locale = useLocale();
return (
<>
<Turnstile
options={{
size: 'flexible',
language: locale,
theme: theme.theme === 'dark' ? 'dark' : 'light',
}}
{...props}
siteKey={siteKey}
/>
{validationError && (
<FormMessage className="text-red-500 mt-2">
{validationError}
</FormMessage>
)}
</>
);
};
Captcha.displayName = 'Captcha';