refactor: enhance captcha handling in login and register forms with reset functionality
This commit is contained in:
parent
a6a5d92dc1
commit
7f4a7a61a2
@ -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}
|
||||
/>
|
||||
|
@ -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}
|
||||
/>
|
||||
|
@ -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';
|
||||
|
Loading…
Reference in New Issue
Block a user