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 { EyeIcon, EyeOffIcon, Loader2Icon } from 'lucide-react';
|
||||||
import { useLocale, useTranslations } from 'next-intl';
|
import { useLocale, useTranslations } from 'next-intl';
|
||||||
import { useSearchParams } from 'next/navigation';
|
import { useSearchParams } from 'next/navigation';
|
||||||
import { useState } from 'react';
|
import { useRef, useState } from 'react';
|
||||||
import { useForm, useWatch } from 'react-hook-form';
|
import { useForm, useWatch } from 'react-hook-form';
|
||||||
import * as z from 'zod';
|
import * as z from 'zod';
|
||||||
import { Captcha } from '../shared/captcha';
|
import { Captcha } from '../shared/captcha';
|
||||||
@ -56,6 +56,7 @@ export const LoginForm = ({
|
|||||||
const [success, setSuccess] = useState<string | undefined>('');
|
const [success, setSuccess] = useState<string | undefined>('');
|
||||||
const [isPending, setIsPending] = useState(false);
|
const [isPending, setIsPending] = useState(false);
|
||||||
const [showPassword, setShowPassword] = useState(false);
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
|
const captchaRef = useRef<any>(null);
|
||||||
|
|
||||||
// Check if credential login is enabled
|
// Check if credential login is enabled
|
||||||
const credentialLoginEnabled = websiteConfig.auth.enableCredentialLogin;
|
const credentialLoginEnabled = websiteConfig.auth.enableCredentialLogin;
|
||||||
@ -92,6 +93,15 @@ export const LoginForm = ({
|
|||||||
name: 'captchaToken',
|
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>) => {
|
const onSubmit = async (values: z.infer<typeof LoginSchema>) => {
|
||||||
// Validate captcha token if turnstile is enabled and site key is available
|
// Validate captcha token if turnstile is enabled and site key is available
|
||||||
if (captchaConfigured && values.captchaToken) {
|
if (captchaConfigured && values.captchaToken) {
|
||||||
@ -107,6 +117,8 @@ export const LoginForm = ({
|
|||||||
console.error('login, captcha invalid:', values.captchaToken);
|
console.error('login, captcha invalid:', values.captchaToken);
|
||||||
const errorMessage = captchaResult?.data?.error || t('captchaInvalid');
|
const errorMessage = captchaResult?.data?.error || t('captchaInvalid');
|
||||||
setError(errorMessage);
|
setError(errorMessage);
|
||||||
|
setIsPending(false);
|
||||||
|
resetCaptcha(); // Reset captcha on validation failure
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -139,6 +151,10 @@ export const LoginForm = ({
|
|||||||
onError: (ctx) => {
|
onError: (ctx) => {
|
||||||
console.error('login, error:', ctx.error);
|
console.error('login, error:', ctx.error);
|
||||||
setError(`${ctx.error.status}: ${ctx.error.message}`);
|
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} />
|
<FormSuccess message={success} />
|
||||||
{captchaConfigured && (
|
{captchaConfigured && (
|
||||||
<Captcha
|
<Captcha
|
||||||
|
ref={captchaRef}
|
||||||
onSuccess={(token) => form.setValue('captchaToken', token)}
|
onSuccess={(token) => form.setValue('captchaToken', token)}
|
||||||
validationError={form.formState.errors.captchaToken?.message}
|
validationError={form.formState.errors.captchaToken?.message}
|
||||||
/>
|
/>
|
||||||
|
@ -22,7 +22,7 @@ import { zodResolver } from '@hookform/resolvers/zod';
|
|||||||
import { EyeIcon, EyeOffIcon, Loader2Icon } from 'lucide-react';
|
import { EyeIcon, EyeOffIcon, Loader2Icon } from 'lucide-react';
|
||||||
import { useLocale, useTranslations } from 'next-intl';
|
import { useLocale, useTranslations } from 'next-intl';
|
||||||
import { useSearchParams } from 'next/navigation';
|
import { useSearchParams } from 'next/navigation';
|
||||||
import { useState } from 'react';
|
import { useRef, useState } from 'react';
|
||||||
import { useForm, useWatch } from 'react-hook-form';
|
import { useForm, useWatch } from 'react-hook-form';
|
||||||
import * as z from 'zod';
|
import * as z from 'zod';
|
||||||
import { Captcha } from '../shared/captcha';
|
import { Captcha } from '../shared/captcha';
|
||||||
@ -51,6 +51,7 @@ export const RegisterForm = ({
|
|||||||
const [success, setSuccess] = useState<string | undefined>('');
|
const [success, setSuccess] = useState<string | undefined>('');
|
||||||
const [isPending, setIsPending] = useState(false);
|
const [isPending, setIsPending] = useState(false);
|
||||||
const [showPassword, setShowPassword] = useState(false);
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
|
const captchaRef = useRef<any>(null);
|
||||||
|
|
||||||
// Check if credential login is enabled
|
// Check if credential login is enabled
|
||||||
const credentialLoginEnabled = websiteConfig.auth.enableCredentialLogin;
|
const credentialLoginEnabled = websiteConfig.auth.enableCredentialLogin;
|
||||||
@ -91,6 +92,15 @@ export const RegisterForm = ({
|
|||||||
name: 'captchaToken',
|
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>) => {
|
const onSubmit = async (values: z.infer<typeof RegisterSchema>) => {
|
||||||
// Validate captcha token if turnstile is enabled and site key is available
|
// Validate captcha token if turnstile is enabled and site key is available
|
||||||
if (captchaConfigured && values.captchaToken) {
|
if (captchaConfigured && values.captchaToken) {
|
||||||
@ -106,6 +116,8 @@ export const RegisterForm = ({
|
|||||||
console.error('register, captcha invalid:', values.captchaToken);
|
console.error('register, captcha invalid:', values.captchaToken);
|
||||||
const errorMessage = captchaResult?.data?.error || t('captchaInvalid');
|
const errorMessage = captchaResult?.data?.error || t('captchaInvalid');
|
||||||
setError(errorMessage);
|
setError(errorMessage);
|
||||||
|
setIsPending(false);
|
||||||
|
resetCaptcha(); // Reset captcha on validation failure
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -148,6 +160,10 @@ export const RegisterForm = ({
|
|||||||
// sign up fail, display the error message
|
// sign up fail, display the error message
|
||||||
console.error('register, error:', ctx.error);
|
console.error('register, error:', ctx.error);
|
||||||
setError(`${ctx.error.status}: ${ctx.error.message}`);
|
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} />
|
<FormSuccess message={success} />
|
||||||
{captchaConfigured && (
|
{captchaConfigured && (
|
||||||
<Captcha
|
<Captcha
|
||||||
|
ref={captchaRef}
|
||||||
onSuccess={(token) => form.setValue('captchaToken', token)}
|
onSuccess={(token) => form.setValue('captchaToken', token)}
|
||||||
validationError={form.formState.errors.captchaToken?.message}
|
validationError={form.formState.errors.captchaToken?.message}
|
||||||
/>
|
/>
|
||||||
|
@ -5,7 +5,7 @@ import { websiteConfig } from '@/config/website';
|
|||||||
import { useLocale } from 'next-intl';
|
import { useLocale } from 'next-intl';
|
||||||
import { useTheme } from 'next-themes';
|
import { useTheme } from 'next-themes';
|
||||||
import dynamic from 'next/dynamic';
|
import dynamic from 'next/dynamic';
|
||||||
import type { ComponentProps } from 'react';
|
import { type ComponentProps, forwardRef } from 'react';
|
||||||
|
|
||||||
const Turnstile = dynamic(
|
const Turnstile = dynamic(
|
||||||
() => import('@marsidev/react-turnstile').then((mod) => mod.Turnstile),
|
() => import('@marsidev/react-turnstile').then((mod) => mod.Turnstile),
|
||||||
@ -21,41 +21,46 @@ type Props = Omit<ComponentProps<typeof Turnstile>, 'siteKey'> & {
|
|||||||
/**
|
/**
|
||||||
* Captcha component for Cloudflare Turnstile
|
* Captcha component for Cloudflare Turnstile
|
||||||
*/
|
*/
|
||||||
export const Captcha = ({ validationError, ...props }: Props) => {
|
export const Captcha = forwardRef<any, Props>(
|
||||||
const turnstileEnabled = websiteConfig.features.enableTurnstileCaptcha;
|
({ validationError, ...props }, ref) => {
|
||||||
const siteKey = process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY;
|
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 turnstile is disabled in config, don't render anything
|
||||||
if (!turnstileEnabled) {
|
if (!turnstileEnabled) {
|
||||||
return null;
|
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
|
Captcha.displayName = 'Captcha';
|
||||||
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>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
Loading…
Reference in New Issue
Block a user