Skip to content

Commit b220420

Browse files
authored
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.
1 parent 7d5e9a4 commit b220420

File tree

2 files changed

+20
-7
lines changed

2 files changed

+20
-7
lines changed

apps/web/modules/signup-view.tsx

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import { pushGTMEvent } from "@calcom/lib/gtm";
3535
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
3636
import { useDebounce } from "@calcom/lib/hooks/useDebounce";
3737
import { useLocale } from "@calcom/lib/hooks/useLocale";
38+
import { INVALID_CLOUDFLARE_TOKEN_ERROR } from "@calcom/lib/server/checkCfTurnstileToken";
3839
import { IS_EUROPE } from "@calcom/lib/timezoneConstants";
3940
import { signupSchema as apiSignupSchema } from "@calcom/prisma/zod-utils";
4041
import type { inferSSRProps } from "@calcom/types/inferSSRProps";
@@ -191,6 +192,7 @@ export default function Signup({
191192
const [usernameTaken, setUsernameTaken] = useState(false);
192193
const [isGoogleLoading, setIsGoogleLoading] = useState(false);
193194
const [displayEmailForm, setDisplayEmailForm] = useState(token);
195+
const [turnstileKey, setTurnstileKey] = useState(0);
194196
const searchParams = useCompatSearchParams();
195197
const { t, i18n } = useLocale();
196198
const router = useRouter();
@@ -207,7 +209,6 @@ export default function Signup({
207209

208210
useEffect(() => {
209211
if (redirectUrl) {
210-
// eslint-disable-next-line @calcom/eslint/avoid-web-storage
211212
localStorage.setItem("onBoardingRedirect", redirectUrl);
212213
}
213214
}, [redirectUrl]);
@@ -306,6 +307,13 @@ export default function Signup({
306307
});
307308
})
308309
.catch((err) => {
310+
setTurnstileKey((k) => k + 1);
311+
formMethods.setValue("cfToken", undefined);
312+
313+
if (err.message === INVALID_CLOUDFLARE_TOKEN_ERROR) {
314+
return;
315+
}
316+
309317
posthog.capture("signup_form_submit_error", {
310318
has_token: !!token,
311319
is_org_invite: isOrgInviteByLink,
@@ -512,10 +520,17 @@ export default function Signup({
512520
{/* Cloudflare Turnstile Captcha */}
513521
{CLOUDFLARE_SITE_ID ? (
514522
<TurnstileCaptcha
523+
key={turnstileKey}
515524
appearance="interaction-only"
516525
onVerify={(token) => {
517526
formMethods.setValue("cfToken", token);
518527
}}
528+
onExpire={() => {
529+
formMethods.setValue("cfToken", undefined);
530+
}}
531+
onError={() => {
532+
formMethods.setValue("cfToken", undefined);
533+
}}
519534
/>
520535
) : null}
521536

@@ -558,7 +573,6 @@ export default function Signup({
558573
org_slug: orgSlug,
559574
});
560575

561-
// eslint-disable-next-line @calcom/eslint/avoid-web-storage
562576
localStorage.setItem("username", username);
563577
const sp = new URLSearchParams();
564578
// @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({
588602
!!formMethods.formState.errors.email ||
589603
!formMethods.getValues("email") ||
590604
!formMethods.getValues("password") ||
591-
(CLOUDFLARE_SITE_ID &&
592-
!process.env.NEXT_PUBLIC_IS_E2E &&
593-
!formMethods.getValues("cfToken")) ||
605+
(CLOUDFLARE_SITE_ID && !process.env.NEXT_PUBLIC_IS_E2E && !watch("cfToken")) ||
594606
isSubmitting ||
595607
usernameTaken
596608
}>
@@ -640,7 +652,6 @@ export default function Signup({
640652
if (prepopulateFormValues?.username) {
641653
// If username is present we save it in query params to check for premium
642654
searchQueryParams.set("username", prepopulateFormValues.username);
643-
// eslint-disable-next-line @calcom/eslint/avoid-web-storage
644655
localStorage.setItem("username", prepopulateFormValues.username);
645656
}
646657
if (token) {

packages/lib/server/checkCfTurnstileToken.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { HttpError } from "../http-error";
22

33
const TURNSTILE_SECRET_ID = process.env.CLOUDFLARE_TURNSTILE_SECRET;
44

5+
export const INVALID_CLOUDFLARE_TOKEN_ERROR = "Invalid cloudflare token";
6+
57
export async function checkCfTurnstileToken({ token, remoteIp }: { token?: string; remoteIp: string }) {
68
// This means the instance doesn't have turnstile enabled - we skip the check and just return success.
79
// 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
2830
const data = await result.json();
2931

3032
if (!data["success"]) {
31-
throw new HttpError({ statusCode: 401, message: "Invalid cloudflare token" });
33+
throw new HttpError({ statusCode: 401, message: INVALID_CLOUDFLARE_TOKEN_ERROR });
3234
}
3335

3436
return data;

0 commit comments

Comments
 (0)