diff --git a/apps/remix/app/components/forms/signin.tsx b/apps/remix/app/components/forms/signin.tsx index 0d2e7ff21..18c381c3a 100644 --- a/apps/remix/app/components/forms/signin.tsx +++ b/apps/remix/app/components/forms/signin.tsx @@ -89,7 +89,6 @@ export const SignInForm = ({ const turnstileSiteKey = env('NEXT_PUBLIC_TURNSTILE_SITE_KEY'); const turnstileRef = useRef(null); const twoFactorTurnstileRef = useRef(null); - const [captchaToken, setCaptchaToken] = useState(null); const [isPasskeyLoading, setIsPasskeyLoading] = useState(false); @@ -197,13 +196,31 @@ export const SignInForm = ({ }; const onFormSubmit = async ({ email, password, totpCode, backupCode }: TSignInFormSchema) => { + const $turnstile = isTwoFactorAuthenticationDialogOpen ? twoFactorTurnstileRef.current : turnstileRef.current; + try { + let token: string | undefined; + + if (turnstileSiteKey) { + token = await $turnstile?.getResponsePromise(3000).catch((_err) => undefined); + + if (!token) { + toast({ + title: _(msg`Human verification required`), + description: _(msg`Please complete the CAPTCHA challenge before signing in.`), + variant: 'destructive', + }); + + return; + } + } + await authClient.emailPassword.signIn({ email, password, totpCode, backupCode, - captchaToken: captchaToken ?? undefined, + captchaToken: token ?? undefined, redirectPath, }); } catch (err) { @@ -214,10 +231,6 @@ export const SignInForm = ({ if (error.code === 'TWO_FACTOR_MISSING_CREDENTIALS') { setIsTwoFactorAuthenticationDialogOpen(true); - // Turnstile tokens are single-use. Clear the consumed one so the - // dialog's fresh widget mounts cleanly and the dialog can't be - // submitted with the stale token before a new one is issued. - setCaptchaToken(null); return; } @@ -247,8 +260,7 @@ export const SignInForm = ({ variant: 'destructive', }); - turnstileRef.current?.reset(); - setCaptchaToken(null); + $turnstile?.reset(); } }; @@ -358,8 +370,6 @@ export const SignInForm = ({ setCaptchaToken(null)} options={{ size: 'flexible', appearance: 'interaction-only', @@ -499,8 +509,6 @@ export const SignInForm = ({ setCaptchaToken(null)} options={{ size: 'flexible', appearance: 'interaction-only', @@ -518,7 +526,7 @@ export const SignInForm = ({ )} - diff --git a/apps/remix/app/components/forms/signup.tsx b/apps/remix/app/components/forms/signup.tsx index dea82be17..7c47f5253 100644 --- a/apps/remix/app/components/forms/signup.tsx +++ b/apps/remix/app/components/forms/signup.tsx @@ -20,7 +20,7 @@ import { useLingui } from '@lingui/react'; import { Trans } from '@lingui/react/macro'; import type { TurnstileInstance } from '@marsidev/react-turnstile'; import { Turnstile } from '@marsidev/react-turnstile'; -import { useEffect, useRef, useState } from 'react'; +import { useEffect, useRef } from 'react'; import { useForm } from 'react-hook-form'; import { FaIdCardClip } from 'react-icons/fa6'; import { FcGoogle } from 'react-icons/fc'; @@ -86,8 +86,6 @@ export const SignUpForm = ({ const turnstileSiteKey = env('NEXT_PUBLIC_TURNSTILE_SITE_KEY'); const turnstileRef = useRef(null); - const [captchaToken, setCaptchaToken] = useState(null); - const hasSocialAuthEnabled = isGoogleSignupEnabled || isMicrosoftSignupEnabled || isOidcSignupEnabled; const form = useForm({ @@ -105,12 +103,28 @@ export const SignUpForm = ({ const onFormSubmit = async ({ name, email, password, signature }: TSignUpFormSchema) => { try { + let token: string | undefined; + + if (turnstileSiteKey) { + token = await turnstileRef.current?.getResponsePromise(3000).catch((_err) => undefined); + + if (!token) { + toast({ + title: _(msg`Human verification required`), + description: _(msg`Please complete the CAPTCHA challenge before signing in.`), + variant: 'destructive', + }); + + return; + } + } + await authClient.emailPassword.signUp({ name, email, password, signature, - captchaToken: captchaToken ?? undefined, + captchaToken: token ?? undefined, }); await navigate(returnTo ? returnTo : '/unverified-account'); @@ -140,7 +154,6 @@ export const SignUpForm = ({ }); turnstileRef.current?.reset(); - setCaptchaToken(null); } }; @@ -316,8 +329,6 @@ export const SignUpForm = ({ setCaptchaToken(null)} options={{ size: 'flexible', appearance: 'interaction-only', diff --git a/apps/remix/app/components/general/claim-account.tsx b/apps/remix/app/components/general/claim-account.tsx index d1c1654e9..d08068a9a 100644 --- a/apps/remix/app/components/general/claim-account.tsx +++ b/apps/remix/app/components/general/claim-account.tsx @@ -1,6 +1,7 @@ import { authClient } from '@documenso/auth/client'; import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics'; import { AppError } from '@documenso/lib/errors/app-error'; +import { env } from '@documenso/lib/utils/env'; import { zEmail } from '@documenso/lib/utils/zod'; import { ZPasswordSchema } from '@documenso/trpc/server/auth-router/schema'; import { Button } from '@documenso/ui/primitives/button'; @@ -12,6 +13,9 @@ import { zodResolver } from '@hookform/resolvers/zod'; import { msg } from '@lingui/core/macro'; import { useLingui } from '@lingui/react'; import { Trans } from '@lingui/react/macro'; +import type { TurnstileInstance } from '@marsidev/react-turnstile'; +import { Turnstile } from '@marsidev/react-turnstile'; +import { useRef } from 'react'; import { useForm } from 'react-hook-form'; import { useNavigate } from 'react-router'; import { z } from 'zod'; @@ -50,6 +54,9 @@ export const ClaimAccount = ({ defaultName, defaultEmail }: ClaimAccountProps) = const analytics = useAnalytics(); const navigate = useNavigate(); + const turnstileSiteKey = env('NEXT_PUBLIC_TURNSTILE_SITE_KEY'); + const turnstileRef = useRef(null); + const form = useForm({ values: { name: defaultName ?? '', @@ -61,7 +68,28 @@ export const ClaimAccount = ({ defaultName, defaultEmail }: ClaimAccountProps) = const onFormSubmit = async ({ name, email, password }: TClaimAccountFormSchema) => { try { - await authClient.emailPassword.signUp({ name, email, password }); + let token: string | undefined; + + if (turnstileSiteKey) { + token = await turnstileRef.current?.getResponsePromise(3000).catch((_err) => undefined); + + if (!token) { + toast({ + title: _(msg`Human verification required`), + description: _(msg`Please complete the CAPTCHA challenge before signing in.`), + variant: 'destructive', + }); + + return; + } + } + + await authClient.emailPassword.signUp({ + name, + email, + password, + captchaToken: token ?? undefined, + }); await navigate(`/unverified-account`); @@ -87,6 +115,8 @@ export const ClaimAccount = ({ defaultName, defaultEmail }: ClaimAccountProps) = description: _(errorMessage), variant: 'destructive', }); + + turnstileRef.current?.reset(); } }; @@ -141,6 +171,19 @@ export const ClaimAccount = ({ defaultName, defaultEmail }: ClaimAccountProps) = )} /> + {turnstileSiteKey && ( +
+ +
+ )} +