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.
This commit is contained in:
Dhairyashil Shinde
2025-12-11 16:45:01 +05:30
committed by GitHub
parent 7d5e9a4e0e
commit b220420630
2 changed files with 20 additions and 7 deletions
+17 -6
View File
@@ -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 ? (
<TurnstileCaptcha
key={turnstileKey}
appearance="interaction-only"
onVerify={(token) => {
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) {
+3 -1
View File
@@ -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;