chore: add captcha validation to login form
This commit is contained in:
parent
bd029eac2a
commit
716eac324f
@ -219,7 +219,9 @@
|
|||||||
"hidePassword": "Hide password",
|
"hidePassword": "Hide password",
|
||||||
"or": "Or continue with",
|
"or": "Or continue with",
|
||||||
"emailRequired": "Please enter your email",
|
"emailRequired": "Please enter your email",
|
||||||
"passwordRequired": "Please enter your password"
|
"passwordRequired": "Please enter your password",
|
||||||
|
"captchaInvalid": "Captcha verification failed",
|
||||||
|
"captchaError": "Captcha verification error"
|
||||||
},
|
},
|
||||||
"register": {
|
"register": {
|
||||||
"title": "Register",
|
"title": "Register",
|
||||||
|
@ -219,7 +219,9 @@
|
|||||||
"hidePassword": "隐藏密码",
|
"hidePassword": "隐藏密码",
|
||||||
"or": "或以社媒账号登录",
|
"or": "或以社媒账号登录",
|
||||||
"emailRequired": "请输入邮箱",
|
"emailRequired": "请输入邮箱",
|
||||||
"passwordRequired": "请输入密码"
|
"passwordRequired": "请输入密码",
|
||||||
|
"captchaInvalid": "验证码验证失败",
|
||||||
|
"captchaError": "验证码验证出错"
|
||||||
},
|
},
|
||||||
"register": {
|
"register": {
|
||||||
"title": "注册",
|
"title": "注册",
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import { validateCaptchaAction } from '@/actions/validate-captcha';
|
||||||
import { AuthCard } from '@/components/auth/auth-card';
|
import { AuthCard } from '@/components/auth/auth-card';
|
||||||
import { FormError } from '@/components/shared/form-error';
|
import { FormError } from '@/components/shared/form-error';
|
||||||
import { FormSuccess } from '@/components/shared/form-success';
|
import { FormSuccess } from '@/components/shared/form-success';
|
||||||
@ -13,6 +14,7 @@ import {
|
|||||||
FormMessage,
|
FormMessage,
|
||||||
} from '@/components/ui/form';
|
} from '@/components/ui/form';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { websiteConfig } from '@/config/website';
|
||||||
import { LocaleLink } from '@/i18n/navigation';
|
import { LocaleLink } from '@/i18n/navigation';
|
||||||
import { authClient } from '@/lib/auth-client';
|
import { authClient } from '@/lib/auth-client';
|
||||||
import { getUrlWithLocaleInCallbackUrl } from '@/lib/urls/urls';
|
import { getUrlWithLocaleInCallbackUrl } from '@/lib/urls/urls';
|
||||||
@ -23,8 +25,9 @@ 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 { useState } from 'react';
|
||||||
import { useForm } 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 { SocialLoginButton } from './social-login-button';
|
import { SocialLoginButton } from './social-login-button';
|
||||||
|
|
||||||
export interface LoginFormProps {
|
export interface LoginFormProps {
|
||||||
@ -54,6 +57,14 @@ export const LoginForm = ({
|
|||||||
const [isPending, setIsPending] = useState(false);
|
const [isPending, setIsPending] = useState(false);
|
||||||
const [showPassword, setShowPassword] = useState(false);
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
|
|
||||||
|
// turnstile captcha schema
|
||||||
|
const turnstileEnabled = websiteConfig.features.enableTurnstileCaptcha;
|
||||||
|
const captchaSiteKey = process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY;
|
||||||
|
const captchaConfigured = turnstileEnabled && !!captchaSiteKey;
|
||||||
|
const captchaSchema = captchaConfigured
|
||||||
|
? z.string().min(1, 'Please complete the captcha')
|
||||||
|
: z.string().optional();
|
||||||
|
|
||||||
const LoginSchema = z.object({
|
const LoginSchema = z.object({
|
||||||
email: z.string().email({
|
email: z.string().email({
|
||||||
message: t('emailRequired'),
|
message: t('emailRequired'),
|
||||||
@ -61,6 +72,7 @@ export const LoginForm = ({
|
|||||||
password: z.string().min(1, {
|
password: z.string().min(1, {
|
||||||
message: t('passwordRequired'),
|
message: t('passwordRequired'),
|
||||||
}),
|
}),
|
||||||
|
captchaToken: captchaSchema,
|
||||||
});
|
});
|
||||||
|
|
||||||
const form = useForm<z.infer<typeof LoginSchema>>({
|
const form = useForm<z.infer<typeof LoginSchema>>({
|
||||||
@ -68,10 +80,30 @@ export const LoginForm = ({
|
|||||||
defaultValues: {
|
defaultValues: {
|
||||||
email: '',
|
email: '',
|
||||||
password: '',
|
password: '',
|
||||||
|
captchaToken: '',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const captchaToken = useWatch({
|
||||||
|
control: form.control,
|
||||||
|
name: 'captchaToken',
|
||||||
|
});
|
||||||
|
|
||||||
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
|
||||||
|
if (captchaConfigured && values.captchaToken) {
|
||||||
|
const captchaResult = await validateCaptchaAction({
|
||||||
|
captchaToken: values.captchaToken,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!captchaResult?.data?.success || !captchaResult?.data?.valid) {
|
||||||
|
console.error('login, captcha invalid:', values.captchaToken);
|
||||||
|
const errorMessage = captchaResult?.data?.error || t('captchaInvalid');
|
||||||
|
setError(errorMessage);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 1. if callbackUrl is provided, user will be redirected to the callbackURL after login successfully.
|
// 1. if callbackUrl is provided, user will be redirected to the callbackURL after login successfully.
|
||||||
// if user email is not verified, a new verification email will be sent to the user with the callbackURL.
|
// if user email is not verified, a new verification email will be sent to the user with the callbackURL.
|
||||||
// 2. if callbackUrl is not provided, we should redirect manually in the onSuccess callback.
|
// 2. if callbackUrl is not provided, we should redirect manually in the onSuccess callback.
|
||||||
@ -193,8 +225,14 @@ export const LoginForm = ({
|
|||||||
</div>
|
</div>
|
||||||
<FormError message={error || urlError || undefined} />
|
<FormError message={error || urlError || undefined} />
|
||||||
<FormSuccess message={success} />
|
<FormSuccess message={success} />
|
||||||
|
{captchaConfigured && (
|
||||||
|
<Captcha
|
||||||
|
onSuccess={(token) => form.setValue('captchaToken', token)}
|
||||||
|
validationError={form.formState.errors.captchaToken?.message}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<Button
|
<Button
|
||||||
disabled={isPending}
|
disabled={isPending || (captchaConfigured && !captchaToken)}
|
||||||
size="lg"
|
size="lg"
|
||||||
type="submit"
|
type="submit"
|
||||||
className="w-full flex items-center justify-center gap-2 cursor-pointer"
|
className="w-full flex items-center justify-center gap-2 cursor-pointer"
|
||||||
|
Loading…
Reference in New Issue
Block a user