From b220420630da5714983791bc9dc75968db63f714 Mon Sep 17 00:00:00 2001 From: Dhairyashil Shinde <93669429+dhairyashiil@users.noreply.github.com> Date: Thu, 11 Dec 2025 16:45:01 +0530 Subject: [PATCH] fix: reset Cloudflare Turnstile token on signup errors (#24927) * fix: cloudflare turnstile token reset * fix: silently reset turnstile on invalid token error * refactor: remove unused forwardRef logic from Turnstile component - Remove forwardRef, useImperativeHandle, and useRef imports - Remove unused TurnstileInstance type export - Simplify to a plain function component - The ref-based reset was replaced by key-based remount in signup-view * refactor: remove redundant cfToken validation check The submit button is already disabled when cfToken is missing, making this defensive check unreachable during normal form flow. * revert prettier formatiing * chore: revert yarn.lock changes * refactor(auth): use shared constant for cloudflare token error message Replace hardcoded "Invalid cloudflare token" string with an exported constant to prevent silent breakage if the error message changes. --- apps/web/modules/signup-view.tsx | 23 +++++++++++++++----- packages/lib/server/checkCfTurnstileToken.ts | 4 +++- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/apps/web/modules/signup-view.tsx b/apps/web/modules/signup-view.tsx index 9875f1717e..10f33c8a44 100644 --- a/apps/web/modules/signup-view.tsx +++ b/apps/web/modules/signup-view.tsx @@ -35,6 +35,7 @@ import { pushGTMEvent } from "@calcom/lib/gtm"; import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams"; import { useDebounce } from "@calcom/lib/hooks/useDebounce"; import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { INVALID_CLOUDFLARE_TOKEN_ERROR } from "@calcom/lib/server/checkCfTurnstileToken"; import { IS_EUROPE } from "@calcom/lib/timezoneConstants"; import { signupSchema as apiSignupSchema } from "@calcom/prisma/zod-utils"; import type { inferSSRProps } from "@calcom/types/inferSSRProps"; @@ -191,6 +192,7 @@ export default function Signup({ const [usernameTaken, setUsernameTaken] = useState(false); const [isGoogleLoading, setIsGoogleLoading] = useState(false); const [displayEmailForm, setDisplayEmailForm] = useState(token); + const [turnstileKey, setTurnstileKey] = useState(0); const searchParams = useCompatSearchParams(); const { t, i18n } = useLocale(); const router = useRouter(); @@ -207,7 +209,6 @@ export default function Signup({ useEffect(() => { if (redirectUrl) { - // eslint-disable-next-line @calcom/eslint/avoid-web-storage localStorage.setItem("onBoardingRedirect", redirectUrl); } }, [redirectUrl]); @@ -306,6 +307,13 @@ export default function Signup({ }); }) .catch((err) => { + setTurnstileKey((k) => k + 1); + formMethods.setValue("cfToken", undefined); + + if (err.message === INVALID_CLOUDFLARE_TOKEN_ERROR) { + return; + } + posthog.capture("signup_form_submit_error", { has_token: !!token, is_org_invite: isOrgInviteByLink, @@ -512,10 +520,17 @@ export default function Signup({ {/* Cloudflare Turnstile Captcha */} {CLOUDFLARE_SITE_ID ? ( { formMethods.setValue("cfToken", token); }} + onExpire={() => { + formMethods.setValue("cfToken", undefined); + }} + onError={() => { + formMethods.setValue("cfToken", undefined); + }} /> ) : null} @@ -558,7 +573,6 @@ export default function Signup({ org_slug: orgSlug, }); - // eslint-disable-next-line @calcom/eslint/avoid-web-storage localStorage.setItem("username", username); const sp = new URLSearchParams(); // @NOTE: don't remove username query param as it's required right now for stripe payment page @@ -588,9 +602,7 @@ export default function Signup({ !!formMethods.formState.errors.email || !formMethods.getValues("email") || !formMethods.getValues("password") || - (CLOUDFLARE_SITE_ID && - !process.env.NEXT_PUBLIC_IS_E2E && - !formMethods.getValues("cfToken")) || + (CLOUDFLARE_SITE_ID && !process.env.NEXT_PUBLIC_IS_E2E && !watch("cfToken")) || isSubmitting || usernameTaken }> @@ -640,7 +652,6 @@ export default function Signup({ if (prepopulateFormValues?.username) { // If username is present we save it in query params to check for premium searchQueryParams.set("username", prepopulateFormValues.username); - // eslint-disable-next-line @calcom/eslint/avoid-web-storage localStorage.setItem("username", prepopulateFormValues.username); } if (token) { diff --git a/packages/lib/server/checkCfTurnstileToken.ts b/packages/lib/server/checkCfTurnstileToken.ts index 7985dfa7d9..2fb0440545 100644 --- a/packages/lib/server/checkCfTurnstileToken.ts +++ b/packages/lib/server/checkCfTurnstileToken.ts @@ -2,6 +2,8 @@ import { HttpError } from "../http-error"; const TURNSTILE_SECRET_ID = process.env.CLOUDFLARE_TURNSTILE_SECRET; +export const INVALID_CLOUDFLARE_TOKEN_ERROR = "Invalid cloudflare token"; + export async function checkCfTurnstileToken({ token, remoteIp }: { token?: string; remoteIp: string }) { // This means the instance doesn't have turnstile enabled - we skip the check and just return success. // OR the instance is running in CI so we skip these checks also @@ -28,7 +30,7 @@ export async function checkCfTurnstileToken({ token, remoteIp }: { token?: strin const data = await result.json(); if (!data["success"]) { - throw new HttpError({ statusCode: 401, message: "Invalid cloudflare token" }); + throw new HttpError({ statusCode: 401, message: INVALID_CLOUDFLARE_TOKEN_ERROR }); } return data;