From ee52ea196a199b117cfdc19e5d6396b9359c1bbb Mon Sep 17 00:00:00 2001 From: James Perkins Date: Mon, 24 Nov 2025 13:09:50 -0500 Subject: [PATCH 1/3] feat: integrate Cloudflare Turnstile CAPTCHA for authentication security This commit implements comprehensive CAPTCHA protection using Cloudflare Turnstile across all authentication flows to enhance security against automated attacks and bot abuse. Key Changes: - Add Turnstile verification component with dark theme and loading states - Integrate CAPTCHA challenges into sign-in and sign-up flows - Update authentication hooks (useSignIn, useSignUp) to handle CAPTCHA tokens - Modify auth actions to validate Turnstile tokens on the server side - Enhance WorkOS and local authentication providers with CAPTCHA support - Update auth types to include CAPTCHA token handling - Add environment configuration for Turnstile site key - Improve loading states and user experience during verification - Add proper error handling for CAPTCHA failures - Update invitation acceptance flows with CAPTCHA protection Technical Implementation: - Uses @marsidev/react-turnstile for React integration - Implements token validation in server-side auth actions - Adds proper TypeScript types for CAPTCHA-enabled auth flows - Maintains backward compatibility with existing auth flows - Includes cumulative layout shift (CLS) optimizations for better UX Security Benefits: - Protects against automated sign-up abuse - Prevents credential stuffing attacks on sign-in - Reduces spam account creation - Maintains legitimate user accessibility with minimal friction Files Modified: - Authentication components and hooks (29 files) - Server-side auth actions and routes - Type definitions and environment configuration - Package dependencies and build configuration --- .../app/(app)/api/auth/refresh/route.ts | 1 - .../app/api/auth/accept-invitation/route.ts | 11 +- .../app/api/auth/invitation/route.ts | 5 +- apps/dashboard/app/auth/actions.ts | 155 +++++++---- apps/dashboard/app/auth/hooks/useSignIn.ts | 72 ++++- apps/dashboard/app/auth/hooks/useSignUp.ts | 101 +++++-- .../app/auth/sign-in/[[...sign-in]]/page.tsx | 11 +- .../dashboard/app/auth/sign-in/email-code.tsx | 11 +- .../app/auth/sign-in/email-signin.tsx | 83 +++++- .../app/auth/sign-in/oauth-signin.tsx | 3 +- .../app/auth/sign-up/[[...sign-up]]/page.tsx | 3 +- .../dashboard/app/auth/sign-up/email-code.tsx | 27 +- .../app/auth/sign-up/email-signup.tsx | 150 +++++++++-- .../app/auth/sign-up/oauth-signup.tsx | 3 +- .../auth/post-auth-invitation-handler.tsx | 6 - .../components/auth/turnstile-challenge.tsx | 71 +++++ apps/dashboard/lib/auth/base-provider.ts | 19 +- apps/dashboard/lib/auth/cookies.ts | 5 +- apps/dashboard/lib/auth/get-auth.ts | 3 +- apps/dashboard/lib/auth/local.ts | 7 +- apps/dashboard/lib/auth/sessions.ts | 25 +- apps/dashboard/lib/auth/types.ts | 17 +- apps/dashboard/lib/auth/workos.ts | 254 +++++++++--------- apps/dashboard/lib/env.ts | 3 + apps/dashboard/package.json | 3 +- .../components/search/llm-search.examples.tsx | 2 +- .../design/components/toaster.example.tsx | 6 +- pnpm-lock.yaml | 19 +- turbo.json | 5 +- 29 files changed, 770 insertions(+), 311 deletions(-) create mode 100644 apps/dashboard/components/auth/turnstile-challenge.tsx diff --git a/apps/dashboard/app/(app)/api/auth/refresh/route.ts b/apps/dashboard/app/(app)/api/auth/refresh/route.ts index 6d109a5de4..c31f705363 100644 --- a/apps/dashboard/app/(app)/api/auth/refresh/route.ts +++ b/apps/dashboard/app/(app)/api/auth/refresh/route.ts @@ -8,7 +8,6 @@ export async function POST(request: Request) { // Get the current token from the request const currentToken = request.headers.get("x-current-token"); if (!currentToken) { - console.error("Session refresh failed: no current token"); return Response.json({ success: false, error: "Failed to refresh session" }, { status: 401 }); } // Call refreshSession logic here and get new token diff --git a/apps/dashboard/app/api/auth/accept-invitation/route.ts b/apps/dashboard/app/api/auth/accept-invitation/route.ts index b085c799eb..47ab3efe4e 100644 --- a/apps/dashboard/app/api/auth/accept-invitation/route.ts +++ b/apps/dashboard/app/api/auth/accept-invitation/route.ts @@ -38,10 +38,7 @@ export async function POST(request: NextRequest) { let invitation: Invitation | null; try { invitation = await auth.getInvitation(invitationToken); - } catch (error) { - console.error("Failed to retrieve invitation:", { - error: error instanceof Error ? error.message : "Unknown error", - }); + } catch (_error) { return NextResponse.json( { success: false, error: "Invalid or expired invitation token" }, { status: 400 }, @@ -96,9 +93,6 @@ export async function POST(request: NextRequest) { return response; } catch (error) { - console.error("Failed to accept invitation:", { - error: error instanceof Error ? error.message : "Unknown error", - }); return NextResponse.json( { success: false, @@ -107,8 +101,7 @@ export async function POST(request: NextRequest) { { status: 500 }, ); } - } catch (error) { - console.error("Error in accept invitation API:", error); + } catch (_error) { return NextResponse.json({ success: false, error: "Internal server error" }, { status: 500 }); } } diff --git a/apps/dashboard/app/api/auth/invitation/route.ts b/apps/dashboard/app/api/auth/invitation/route.ts index 295b21c437..5bdf1f8099 100644 --- a/apps/dashboard/app/api/auth/invitation/route.ts +++ b/apps/dashboard/app/api/auth/invitation/route.ts @@ -48,10 +48,7 @@ export async function POST(request: NextRequest) { success: true, organizationId: result.organizationId, }); - } catch (error) { - console.error("Error processing invitation:", { - error: error instanceof Error ? error.message : "Unknown error", - }); + } catch (_error) { return NextResponse.json( { success: false, error: "Internal server error" }, { status: 500, headers: { "Cache-Control": "no-store" } }, diff --git a/apps/dashboard/app/auth/actions.ts b/apps/dashboard/app/auth/actions.ts index 8e95952f02..27d339397d 100644 --- a/apps/dashboard/app/auth/actions.ts +++ b/apps/dashboard/app/auth/actions.ts @@ -15,6 +15,7 @@ import { type NavigationResponse, type OAuthResult, PENDING_SESSION_COOKIE, + type PendingTurnstileResponse, type SignInViaOAuthOptions, type UserData, type VerificationResult, @@ -38,6 +39,39 @@ function getRequestMetadata() { return { ipAddress, userAgent }; } +// Turnstile verification helper +async function verifyTurnstileToken(token: string): Promise { + const environment = env(); + const secretKey = environment.CLOUDFLARE_TURNSTILE_SECRET_KEY; + + if (!secretKey) { + return false; + } + + try { + const response = await fetch("https://challenges.cloudflare.com/turnstile/v0/siteverify", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + secret: secretKey, + response: token, + }), + }); + + if (!response.ok) { + return false; + } + + const data = await response.json(); + + return data.success === true; + } catch (_error) { + return false; + } +} + // Authentication Actions export async function signUpViaEmail(params: UserData): Promise { const metadata = getRequestMetadata(); @@ -99,9 +133,8 @@ export async function verifyAuthCode(params: { }); redirectUrl = `/join/success?${params.toString()}`; } - } catch (error) { + } catch (_error) { // Don't fail the redirect if we can't get org name - console.warn("Could not fetch organization name for success page:", error); } return { @@ -112,10 +145,7 @@ export async function verifyAuthCode(params: { } } } - } catch (error) { - console.error("Failed to auto-select invited organization:", { - error: error instanceof Error ? error.message : "Unknown error", - }); + } catch (_error) { // Fall through to return the original result if auto-selection fails } } @@ -137,12 +167,8 @@ export async function verifyAuthCode(params: { if (invitation.state === "pending") { try { await auth.acceptInvitation(invitation.id); - } catch (acceptError) { - // Log but don't fail - invitation might already be accepted - console.warn("Could not accept invitation (might already be accepted):", { - invitationId: invitation.id, - error: acceptError instanceof Error ? acceptError.message : "Unknown error", - }); + } catch (_acceptError) { + // Don't fail - invitation might already be accepted } } @@ -160,9 +186,8 @@ export async function verifyAuthCode(params: { const params = new URLSearchParams({ from_invite: "true" }); redirectUrl = `/join/success?${params.toString()}`; } - } catch (error) { + } catch (_error) { // Don't fail if we can't get org name, just use join success without org name - console.warn("Could not fetch organization name for new user success page:", error); const params = new URLSearchParams({ from_invite: "true" }); redirectUrl = `/join/success?${params.toString()}`; } @@ -173,24 +198,13 @@ export async function verifyAuthCode(params: { cookies: [], }; } - console.warn("Invalid invitation or missing organization ID:", { - hasInvitation: !!invitation, - organizationId: invitation?.organizationId, - }); - } catch (error) { - console.error("Failed to process invitation for new user:", { - error: error instanceof Error ? error.message : "Unknown error", - invitationToken: `${invitationToken.substring(0, 10)}...`, - }); + } catch (_error) { // Fall through to return original result } } return result; - } catch (error) { - console.error("Failed to verify auth code:", { - error: error instanceof Error ? error.message : "Unknown error", - }); + } catch (_error) { return { success: false, code: AuthErrorCode.UNKNOWN_ERROR, @@ -206,7 +220,6 @@ export async function verifyEmail(code: string): Promise { const token = await getCookie(PENDING_SESSION_COOKIE); if (!token) { - console.error("Pending auth token missing or expired"); return { success: false, code: AuthErrorCode.UNKNOWN_ERROR, @@ -221,10 +234,7 @@ export async function verifyEmail(code: string): Promise { } return result; - } catch (error) { - console.error("Failed to verify email:", { - error: error instanceof Error ? error.message : "Unknown error", - }); + } catch (_error) { return { success: false, code: AuthErrorCode.UNKNOWN_ERROR, @@ -237,7 +247,6 @@ export async function resendAuthCode(email: string): Promise { const envVars = env(); const unkeyRootKey = envVars.UNKEY_ROOT_KEY; if (!unkeyRootKey) { - console.error("UNKEY_ROOT_KEY environment variable is not set"); return { success: false, code: AuthErrorCode.UNKNOWN_ERROR, @@ -250,8 +259,7 @@ export async function resendAuthCode(email: string): Promise { duration: "5m", limit: 5, rootKey: unkeyRootKey, - onError: (err: Error) => { - console.error("Rate limiting error:", err.message); + onError: (_err: Error) => { return { success: true, limit: 0, remaining: 1, reset: 1 }; }, }); @@ -360,11 +368,8 @@ export async function completeOrgSelection( // Store the last used organization ID in a cookie for auto-selection on next login try { await setLastUsedOrgCookie({ orgId }); - } catch (error) { - console.error("Failed to set last used org cookie in completeOrgSelection:", { - orgId, - error: error instanceof Error ? error.message : "Unknown error", - }); + } catch (_error) { + // Ignore cookie setting errors } } @@ -384,18 +389,12 @@ export async function switchOrg(orgId: string): Promise<{ success: boolean; erro // Store the last used organization ID in a cookie for auto-selection on next login try { await setLastUsedOrgCookie({ orgId }); - } catch (error) { - console.error("Failed to set last used org cookie in switchOrg:", { - orgId, - error: error instanceof Error ? error.message : "Unknown error", - }); + } catch (_error) { + // Ignore cookie setting errors } return { success: true }; } catch (error) { - console.error("Organization switch failed:", { - error: error instanceof Error ? error.message : "Unknown error", - }); return { success: false, error: error instanceof Error ? error.message : "Failed to switch organization", @@ -426,18 +425,12 @@ export async function acceptInvitationAndJoin( await setSessionCookie({ token: newToken, expiresAt }); try { await setLastUsedOrgCookie({ orgId: organizationId }); - } catch (error) { - console.error("Failed to set last used org cookie in acceptInvitationAndJoin:", { - orgId: organizationId, - error: error instanceof Error ? error.message : "Unknown error", - }); + } catch (_error) { + // Ignore cookie setting errors } return { success: true }; } catch (error) { - console.error("Failed to accept invitation and join organization:", { - error: error instanceof Error ? error.message : "Unknown error", - }); return { success: false, error: error instanceof Error ? error.message : "Failed to join organization", @@ -445,6 +438,56 @@ export async function acceptInvitationAndJoin( } } +/** + * Verify Turnstile token and retry original auth operation + */ +export async function verifyTurnstileAndRetry(params: { + turnstileToken: string; + email: string; + challengeParams: PendingTurnstileResponse["challengeParams"]; + userData?: { firstName: string; lastName: string }; // Only for sign-up +}): Promise { + const { turnstileToken, email, challengeParams, userData } = params; + + // Verify Turnstile token + const isValidToken = await verifyTurnstileToken(turnstileToken); + + if (!isValidToken) { + return { + success: false, + code: AuthErrorCode.UNKNOWN_ERROR, + message: "Verification failed. Please try again.", + }; + } + + // Retry original auth operation based on action + const metadata = getRequestMetadata(); + + if (challengeParams.action === "sign-up" && userData) { + // For sign-up, we need the user data + return await auth.signUpViaEmail({ + ...userData, + email, + ...metadata, + bypassRadar: true, + }); + } + if (challengeParams.action === "sign-in") { + // For sign-in, we just need the email + return await auth.signInViaEmail({ + email, + ...metadata, + bypassRadar: true, + }); + } + + return { + success: false, + code: AuthErrorCode.UNKNOWN_ERROR, + message: "Invalid challenge parameters.", + }; +} + /** * Check if a pending session exists (for workspace selection flow) * This is needed because PENDING_SESSION_COOKIE is HttpOnly and not accessible from client diff --git a/apps/dashboard/app/auth/hooks/useSignIn.ts b/apps/dashboard/app/auth/hooks/useSignIn.ts index 92e0acc0dc..8ec8ce546c 100644 --- a/apps/dashboard/app/auth/hooks/useSignIn.ts +++ b/apps/dashboard/app/auth/hooks/useSignIn.ts @@ -2,16 +2,23 @@ import { getCookie } from "@/lib/auth/cookies"; import { AuthErrorCode, type AuthErrorResponse, + type EmailAuthResult, type Organization, PENDING_SESSION_COOKIE, type PendingOrgSelectionResponse, + type PendingTurnstileResponse, SIGN_IN_URL, type VerificationResult, errorMessages, } from "@/lib/auth/types"; import { useSearchParams } from "next/navigation"; import { useContext, useEffect, useState } from "react"; -import { resendAuthCode, signInViaEmail, verifyAuthCode } from "../actions"; +import { + resendAuthCode, + signInViaEmail, + verifyAuthCode, + verifyTurnstileAndRetry, +} from "../actions"; import { SignInContext } from "../context/signin-context"; function isAuthErrorResponse(result: VerificationResult): result is AuthErrorResponse { @@ -27,6 +34,15 @@ function isPendingOrgSelection(result: VerificationResult): result is PendingOrg ); } +function isPendingTurnstileChallenge(result: EmailAuthResult): result is PendingTurnstileResponse { + return ( + !result.success && + result.code === AuthErrorCode.RADAR_CHALLENGE_REQUIRED && + "email" in result && + "challengeParams" in result + ); +} + export function useSignIn() { const context = useContext(SignInContext); if (!context) { @@ -51,8 +67,7 @@ export function useSignIn() { try { parsedOrgs = JSON.parse(decodeURIComponent(orgsParam)); setOrgs(parsedOrgs); - } catch (err) { - console.error(err); + } catch (_err) { setError("Failed to load organizations"); } } @@ -60,8 +75,8 @@ export function useSignIn() { // Check for pending session cookie const hasTempSession = await getCookie(PENDING_SESSION_COOKIE); setHasPendingAuth(Boolean(parsedOrgs.length && hasTempSession)); - } catch (err) { - console.error("Error checking auth status:", err); + } catch (_err) { + // Ignore auth status check errors } finally { setLoading(false); } @@ -76,10 +91,15 @@ export function useSignIn() { setError(null); const result = await signInViaEmail(email); + // Return result for Turnstile challenge handling + if (isPendingTurnstileChallenge(result)) { + return result; + } + // Check if the operation was successful if (result.success) { setIsVerifying(true); - return; + return result; } // Handle error case - only set error message if we have an error response @@ -93,9 +113,10 @@ export function useSignIn() { } else { setError(errorMessages[AuthErrorCode.UNKNOWN_ERROR]); } + + return result; } catch (error) { // This catches any unexpected errors that weren't handled by the API - console.error("Unexpected error during sign in:", error); setError(errorMessages[AuthErrorCode.UNKNOWN_ERROR]); throw error; } @@ -161,11 +182,48 @@ export function useSignIn() { } }; + const handleTurnstileVerification = async ( + turnstileToken: string, + challengeData: PendingTurnstileResponse, + userData?: { firstName: string; lastName: string }, + ): Promise => { + try { + setError(null); + const result = await verifyTurnstileAndRetry({ + turnstileToken, + email: challengeData.email, + challengeParams: challengeData.challengeParams, + userData, + }); + + if (result.success) { + setIsVerifying(true); + return; + } + + if (isAuthErrorResponse(result)) { + if (result.code === AuthErrorCode.ACCOUNT_NOT_FOUND) { + setAccountNotFound(true); + setEmail(challengeData.email); + } else { + setError(result.message); + } + } else { + setError(errorMessages[AuthErrorCode.UNKNOWN_ERROR]); + } + } catch (error) { + setError(errorMessages[AuthErrorCode.UNKNOWN_ERROR]); + throw error; + } + }; + return { ...context, handleSignInViaEmail, handleVerification, handleResendCode, + handleTurnstileVerification, + isPendingTurnstileChallenge, orgs, loading, hasPendingAuth, diff --git a/apps/dashboard/app/auth/hooks/useSignUp.ts b/apps/dashboard/app/auth/hooks/useSignUp.ts index b608394a3d..576971b333 100644 --- a/apps/dashboard/app/auth/hooks/useSignUp.ts +++ b/apps/dashboard/app/auth/hooks/useSignUp.ts @@ -1,46 +1,95 @@ "use client"; -import type { UserData } from "@/lib/auth/types"; -import { resendAuthCode, signUpViaEmail, verifyAuthCode, verifyEmail } from "../actions"; +import type { + EmailAuthResult, + PendingTurnstileResponse, + UserData, + VerificationResult, +} from "@/lib/auth/types"; +import { AuthErrorCode } from "@/lib/auth/types"; +import { + resendAuthCode, + signUpViaEmail, + verifyAuthCode, + verifyEmail, + verifyTurnstileAndRetry, +} from "../actions"; import { useSignUpContext } from "../context/signup-context"; export function useSignUp() { const { userData, updateUserData, clearUserData } = useSignUpContext(); - const handleSignUpViaEmail = async ({ firstName, lastName, email }: UserData): Promise => { + const isPendingTurnstileChallenge = ( + result: EmailAuthResult, + ): result is PendingTurnstileResponse => { + return ( + !result.success && + result.code === AuthErrorCode.RADAR_CHALLENGE_REQUIRED && + "email" in result && + "challengeParams" in result + ); + }; + + const handleSignUpViaEmail = async ({ + firstName, + lastName, + email, + }: UserData): Promise => { updateUserData({ email, firstName, lastName }); - try { - await signUpViaEmail({ email, firstName, lastName }); - } catch (error) { - console.error("Sign up failed:", error); - throw error; - } + const result = await signUpViaEmail({ email, firstName, lastName }); + return result; }; - const handleCodeVerification = async (code: string, invitationToken?: string): Promise => { - try { - await verifyAuthCode({ - email: userData.email, - code, - invitationToken, - }); - } catch (error) { - console.error("OTP Verification error:", error); + const handleTurnstileVerification = async ( + turnstileToken: string, + challengeData: PendingTurnstileResponse, + ): Promise => { + // Validate userData exists and has required properties + if (!userData || !userData.firstName || !userData.lastName) { + throw new Error("User data is incomplete. First name and last name are required."); } + + const result = await verifyTurnstileAndRetry({ + turnstileToken, + email: challengeData.email, + challengeParams: challengeData.challengeParams, + userData: { + firstName: userData.firstName, + lastName: userData.lastName, + }, + }); + return result; }; - const handleEmailVerification = async (code: string): Promise => { - try { - await verifyEmail(code); - } catch (error) { - console.error("Email verification error:", error); + const handleCodeVerification = async ( + code: string, + invitationToken?: string, + ): Promise => { + // Validate userData exists and has email + if (!userData || !userData.email) { + throw new Error("User email is required for code verification."); } + + return await verifyAuthCode({ + email: userData.email, + code, + invitationToken, + }); }; - const handleResendCode = async (): Promise => { + const handleEmailVerification = async (code: string): Promise => { + return await verifyEmail(code); + }; + + const handleResendCode = async (): Promise => { + // Validate userData exists and has email + if (!userData || !userData.email) { + throw new Error("User email is required to resend authentication code."); + } + try { - await resendAuthCode(userData.email); + return await resendAuthCode(userData.email); } catch (error) { throw new Error( `Failed to resend authentication code to ${userData.email}: ${ @@ -58,5 +107,7 @@ export function useSignUp() { handleEmailVerification, handleResendCode, handleSignUpViaEmail, + handleTurnstileVerification, + isPendingTurnstileChallenge, }; } diff --git a/apps/dashboard/app/auth/sign-in/[[...sign-in]]/page.tsx b/apps/dashboard/app/auth/sign-in/[[...sign-in]]/page.tsx index ebce8f2d56..ed3c301b02 100644 --- a/apps/dashboard/app/auth/sign-in/[[...sign-in]]/page.tsx +++ b/apps/dashboard/app/auth/sign-in/[[...sign-in]]/page.tsx @@ -50,8 +50,8 @@ function SignInContent() { setLastUsedOrgId(value); } }) - .catch((error) => { - console.error("Failed to read last used org cookie:", error); + .catch((_error) => { + // Ignore cookie read errors }) .finally(() => { setClientReady(true); @@ -85,8 +85,7 @@ function SignInContent() { // On success, redirect to the dashboard router.push(result.redirectTo); }) - .catch((err) => { - console.error("Auto org selection failed:", err); + .catch((_err) => { setError("Failed to automatically sign in. Please select your workspace."); setIsLoading(false); setIsAutoSelecting(false); @@ -123,8 +122,8 @@ function SignInContent() { try { // Attempt sign-in with the provided email await handleSignInViaEmail(invitationEmail); - } catch (err) { - console.error("Auto sign-in failed:", err); + } catch (_err) { + // Ignore auto sign-in errors } finally { // Reset loading state setIsLoading(false); diff --git a/apps/dashboard/app/auth/sign-in/email-code.tsx b/apps/dashboard/app/auth/sign-in/email-code.tsx index 6d8243d0a4..4ebec94f48 100644 --- a/apps/dashboard/app/auth/sign-in/email-code.tsx +++ b/apps/dashboard/app/auth/sign-in/email-code.tsx @@ -89,9 +89,8 @@ export function EmailCode({ invitationToken }: { invitationToken?: string }) { success: "A new code has been sent to your email", }); await p; - } catch (error) { + } catch (_error) { setIsLoading(false); - console.error(error); } }; @@ -114,7 +113,13 @@ export function EmailCode({ invitationToken }: { invitationToken?: string }) {

)} -
verifyCode(otp)}> + { + e.preventDefault(); + verifyCode(otp); + }} + > ( + null, + ); + const [isTurnstileLoading, setIsTurnstileLoading] = useState(false); + const [currentEmail, setCurrentEmail] = useState(email || ""); // Set clientReady to true after hydration is complete useEffect(() => { setClientReady(true); }, []); + // Validate email format + const isValidEmail = (email: string) => { + return email.length > 0 && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email); + }; + + const isFormValid = isValidEmail(currentEmail); + const handleSubmit = async (e: FormEvent) => { e.preventDefault(); const formEmail = new FormData(e.currentTarget).get("email"); @@ -22,11 +37,66 @@ export function EmailSignIn() { } setIsLoading(true); - await handleSignInViaEmail(formEmail); - setLastUsed("email"); - setIsLoading(false); + try { + const result = await handleSignInViaEmail(formEmail); + + // Check if we got a Turnstile challenge + if (result && isPendingTurnstileChallenge(result)) { + setTurnstileChallenge(result); + setIsLoading(false); + return; + } + + setLastUsed("email"); + } catch (_error) { + // Error handling is done in the hook + } finally { + setIsLoading(false); + } + }; + + const handleTurnstileSuccess = async (token: string) => { + if (!turnstileChallenge) { + return; + } + + setIsTurnstileLoading(true); + try { + await handleTurnstileVerification(token, turnstileChallenge); + setTurnstileChallenge(null); + setLastUsed("email"); + } catch (_error) { + // Error handling is done in the hook + } finally { + setIsTurnstileLoading(false); + } }; + const handleTurnstileError = () => { + setTurnstileChallenge(null); + }; + + // Show Turnstile challenge if needed + if (turnstileChallenge) { + return ( +
+ + +
+ ); + } + return (
@@ -41,13 +111,14 @@ export function EmailSignIn() { autoComplete="email" autoCorrect="off" className="h-10 dark !bg-black w-full" + onChange={(e) => setCurrentEmail(e.target.value)} />
+ + ); + } + return (
+ {validationError && ( +
+ {validationError} +
+ )}
= ({ setVerification }) => { autoCapitalize="none" autoCorrect="off" className="h-10 dark !bg-black" + onChange={(e) => { + setFirstName(e.target.value); + validationError && setValidationError(""); + }} />
@@ -76,6 +184,10 @@ export const EmailSignUp: React.FC = ({ setVerification }) => { autoCapitalize="none" autoCorrect="off" className="h-10 dark !bg-black" + onChange={(e) => { + setLastName(e.target.value); + validationError && setValidationError(""); + }} />
@@ -90,13 +202,17 @@ export const EmailSignUp: React.FC = ({ setVerification }) => { autoComplete="email" autoCorrect="off" className="h-10 dark !bg-black w-full" + onChange={(e) => { + setEmail(e.target.value); + validationError && setValidationError(""); + }} />