From 3eb5490df956ce7fb5ff8e796bec6b2f3f0b9d91 Mon Sep 17 00:00:00 2001 From: Keith Williams Date: Tue, 21 Jan 2025 02:57:56 -0300 Subject: [PATCH] feat: Use Cloudflare Turnstile in booker (#18755) * feat: Use Cloudflare Turnstile in booker * move files arround * render turnstile widget and add token to store * use token from body instead of headers * watch cf token to re-render disabled state * Update packages/features/bookings/Booker/components/BookEventForm/BookEventForm.tsx * Update packages/features/bookings/lib/create-booking.ts * ensure bool * add to recuring event * fix recuring events --------- Co-authored-by: sean-brydon Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com> Co-authored-by: Benny Joo --- .env.example | 1 + apps/web/modules/signup-view.tsx | 2 +- apps/web/pages/api/book/event.ts | 8 +++++ apps/web/pages/api/book/recurring-event.ts | 8 +++++ .../features}/auth/Turnstile.tsx | 0 packages/features/bookings/Booker/Booker.tsx | 2 ++ .../BookEventForm/BookEventForm.tsx | 30 ++++++++++++++++++- .../Booker/components/hooks/useBookingForm.ts | 1 + packages/features/bookings/Booker/types.ts | 1 + packages/lib/constants.ts | 1 + packages/lib/server/checkCfTurnstileToken.ts | 2 +- .../atoms/booker/BookerWebWrapper.tsx | 1 + packages/prisma/zod-utils.ts | 1 + turbo.json | 1 + 14 files changed, 56 insertions(+), 3 deletions(-) rename {apps/web/components => packages/features}/auth/Turnstile.tsx (100%) diff --git a/.env.example b/.env.example index 3a67898313..00feb50d8b 100644 --- a/.env.example +++ b/.env.example @@ -242,6 +242,7 @@ E2E_TEST_MAILHOG_ENABLED= # Cloudflare Turnstile NEXT_PUBLIC_CLOUDFLARE_SITEKEY= +NEXT_PUBLIC_CLOUDFLARE_USE_TURNSTILE_IN_BOOKER= CLOUDFLARE_TURNSTILE_SECRET= # Set the following value to true if you wish to enable Team Impersonation diff --git a/apps/web/modules/signup-view.tsx b/apps/web/modules/signup-view.tsx index a573bb3ce3..84e2abca13 100644 --- a/apps/web/modules/signup-view.tsx +++ b/apps/web/modules/signup-view.tsx @@ -58,7 +58,7 @@ const signupSchema = apiSignupSchema.extend({ cfToken: z.string().optional(), }); -const TurnstileCaptcha = dynamic(() => import("@components/auth/Turnstile"), { ssr: false }); +const TurnstileCaptcha = dynamic(() => import("@calcom/features/auth/Turnstile"), { ssr: false }); type FormValues = z.infer; diff --git a/apps/web/pages/api/book/event.ts b/apps/web/pages/api/book/event.ts index 21654b543e..222e675fa7 100644 --- a/apps/web/pages/api/book/event.ts +++ b/apps/web/pages/api/book/event.ts @@ -6,10 +6,18 @@ import handleNewBooking from "@calcom/features/bookings/lib/handleNewBooking"; import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError"; import getIP from "@calcom/lib/getIP"; import { defaultResponder } from "@calcom/lib/server"; +import { checkCfTurnstileToken } from "@calcom/lib/server/checkCfTurnstileToken"; async function handler(req: NextApiRequest & { userId?: number }, res: NextApiResponse) { const userIp = getIP(req); + if (process.env.NEXT_PUBLIC_CLOUDFLARE_USE_TURNSTILE_IN_BOOKER === "1") { + await checkCfTurnstileToken({ + token: req.body["cfToken"] as string, + remoteIp: userIp, + }); + } + await checkRateLimitAndThrowError({ rateLimitingType: "core", identifier: userIp, diff --git a/apps/web/pages/api/book/recurring-event.ts b/apps/web/pages/api/book/recurring-event.ts index 196c773d48..3cdc3e28c3 100644 --- a/apps/web/pages/api/book/recurring-event.ts +++ b/apps/web/pages/api/book/recurring-event.ts @@ -6,12 +6,20 @@ import type { BookingResponse } from "@calcom/features/bookings/types"; import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError"; import getIP from "@calcom/lib/getIP"; import { defaultResponder } from "@calcom/lib/server"; +import { checkCfTurnstileToken } from "@calcom/lib/server/checkCfTurnstileToken"; // @TODO: Didn't look at the contents of this function in order to not break old booking page. async function handler(req: NextApiRequest & { userId?: number }, res: NextApiResponse) { const userIp = getIP(req); + if (process.env.NEXT_PUBLIC_CLOUDFLARE_USE_TURNSTILE_IN_BOOKER === "1") { + await checkCfTurnstileToken({ + token: req.body[0]["cfToken"] as string, + remoteIp: userIp, + }); + } + await checkRateLimitAndThrowError({ rateLimitingType: "core", identifier: userIp, diff --git a/apps/web/components/auth/Turnstile.tsx b/packages/features/auth/Turnstile.tsx similarity index 100% rename from apps/web/components/auth/Turnstile.tsx rename to packages/features/auth/Turnstile.tsx diff --git a/packages/features/bookings/Booker/Booker.tsx b/packages/features/bookings/Booker/Booker.tsx index 4cd6355e6e..726fcd840d 100644 --- a/packages/features/bookings/Booker/Booker.tsx +++ b/packages/features/bookings/Booker/Booker.tsx @@ -72,6 +72,7 @@ const BookerComponent = ({ areInstantMeetingParametersSet = false, userLocale, hasValidLicense, + renderCaptcha, }: BookerProps & WrappedBookerProps) => { const searchParams = useCompatSearchParams(); const isPlatformBookerEmbed = useIsPlatformBookerEmbed(); @@ -166,6 +167,7 @@ const BookerComponent = ({ return bookerState === "booking" ? ( { setSelectedTimeslot(null); if (seatedEventData.bookingUid) { diff --git a/packages/features/bookings/Booker/components/BookEventForm/BookEventForm.tsx b/packages/features/bookings/Booker/components/BookEventForm/BookEventForm.tsx index fca672531d..612ac7c712 100644 --- a/packages/features/bookings/Booker/components/BookEventForm/BookEventForm.tsx +++ b/packages/features/bookings/Booker/components/BookEventForm/BookEventForm.tsx @@ -1,12 +1,18 @@ import type { TFunction } from "next-i18next"; import { Trans } from "next-i18next"; +import dynamic from "next/dynamic"; import Link from "next/link"; import { useMemo, useState } from "react"; import type { FieldError } from "react-hook-form"; import { useIsPlatformBookerEmbed } from "@calcom/atoms/monorepo"; import type { BookerEvent } from "@calcom/features/bookings/types"; -import { WEBSITE_PRIVACY_POLICY_URL, WEBSITE_TERMS_URL } from "@calcom/lib/constants"; +import { + WEBSITE_PRIVACY_POLICY_URL, + WEBSITE_TERMS_URL, + CLOUDFLARE_SITE_ID, + CLOUDFLARE_USE_TURNSTILE_IN_BOOKER, +} from "@calcom/lib/constants"; import { getPaymentAppData } from "@calcom/lib/getPaymentAppData"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { Alert, Button, EmptyScreen, Form } from "@calcom/ui"; @@ -17,6 +23,8 @@ import type { IUseBookingErrors, IUseBookingLoadingStates } from "../hooks/useBo import { BookingFields } from "./BookingFields"; import { FormSkeleton } from "./Skeleton"; +const TurnstileCaptcha = dynamic(() => import("@calcom/features/auth/Turnstile"), { ssr: false }); + type BookEventFormProps = { onCancel?: () => void; onSubmit: () => void; @@ -29,6 +37,7 @@ type BookEventFormProps = { extraOptions: Record; isPlatform?: boolean; isVerificationCodeSending: boolean; + renderCaptcha?: boolean; }; export const BookEventForm = ({ @@ -45,6 +54,7 @@ export const BookEventForm = ({ extraOptions, isVerificationCodeSending, isPlatform = false, + renderCaptcha, }: Omit & { eventQuery: { isError: boolean; @@ -61,6 +71,10 @@ export const BookEventForm = ({ const isInstantMeeting = useBookerStore((state) => state.isInstantMeeting); const isPlatformBookerEmbed = useIsPlatformBookerEmbed(); + // Cloudflare Turnstile Captcha + const shouldRenderCaptcha = + renderCaptcha && CLOUDFLARE_SITE_ID && CLOUDFLARE_USE_TURNSTILE_IN_BOOKER === "1"; + const [responseVercelIdHeader] = useState(null); const { t } = useLocale(); @@ -88,6 +102,8 @@ export const BookEventForm = ({ return ; } + const watchedCfToken = bookingForm.watch("cfToken"); + return (
)} + {/* Cloudflare Turnstile Captcha */} + {shouldRenderCaptcha ? ( + { + bookingForm.setValue("cfToken", token); + }} + /> + ) : null} {!isPlatform && (
)} + {isPlatformBookerEmbed && (
{t("proceeding_agreement")}{" "} @@ -176,9 +202,11 @@ export const BookEventForm = ({ {t("back")} )} +