diff --git a/.env.example b/.env.example index dcc04ef5d7..fe36f11116 100644 --- a/.env.example +++ b/.env.example @@ -359,3 +359,6 @@ UNKEY_ROOT_KEY= # Used for Cal.ai Enterprise Voice AI Agents # https://retellai.com RETELL_AI_KEY= + +# Used to disallow emails as being added as guests on bookings +BLACKLISTED_GUEST_EMAILS= diff --git a/packages/features/bookings/Booker/components/hooks/useVerifyEmail.ts b/packages/features/bookings/Booker/components/hooks/useVerifyEmail.ts index 81bb7ece31..8887335193 100644 --- a/packages/features/bookings/Booker/components/hooks/useVerifyEmail.ts +++ b/packages/features/bookings/Booker/components/hooks/useVerifyEmail.ts @@ -1,6 +1,8 @@ +import { useSession } from "next-auth/react"; import { useState } from "react"; import { useBookerStore } from "@calcom/features/bookings/Booker/store"; +import { useDebounce } from "@calcom/lib/hooks/useDebounce"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { trpc } from "@calcom/trpc"; import { showToast } from "@calcom/ui"; @@ -21,6 +23,8 @@ export const useVerifyEmail = ({ const [isEmailVerificationModalVisible, setEmailVerificationModalVisible] = useState(false); const verifiedEmail = useBookerStore((state) => state.verifiedEmail); const setVerifiedEmail = useBookerStore((state) => state.setVerifiedEmail); + const debouncedEmail = useDebounce(email, 600); + const { data: session } = useSession(); const { t } = useLocale(); const sendEmailVerificationByCodeMutation = trpc.viewer.auth.sendVerifyEmailCode.useMutation({ @@ -32,6 +36,17 @@ export const useVerifyEmail = ({ }, }); + const { data: isEmailVerificationRequired } = + trpc.viewer.public.checkIfUserEmailVerificationRequired.useQuery( + { + userSessionEmail: session?.user.email || "", + email: debouncedEmail, + }, + { + enabled: !!debouncedEmail, + } + ); + const handleVerifyEmail = () => { onVerifyEmail?.(); @@ -43,7 +58,8 @@ export const useVerifyEmail = ({ }; const renderConfirmNotVerifyEmailButtonCond = - !requiresBookerEmailVerification || (email && verifiedEmail && verifiedEmail === email); + (!requiresBookerEmailVerification && !isEmailVerificationRequired) || + (email && verifiedEmail && verifiedEmail === email); return { handleVerifyEmail, diff --git a/packages/features/bookings/lib/handleNewBooking.ts b/packages/features/bookings/lib/handleNewBooking.ts index b4bd389fc2..37751adbce 100644 --- a/packages/features/bookings/lib/handleNewBooking.ts +++ b/packages/features/bookings/lib/handleNewBooking.ts @@ -68,6 +68,7 @@ import { getUTCOffsetByTimezone } from "@calcom/lib/date-fns"; import { getDefaultEvent, getUsernameList } from "@calcom/lib/defaultEvents"; import { ErrorCode } from "@calcom/lib/errorCodes"; import { getErrorFromUnknown } from "@calcom/lib/errors"; +import { extractBaseEmail } from "@calcom/lib/extract-base-email"; import { getBookerBaseUrl } from "@calcom/lib/getBookerUrl/server"; import getPaymentAppData from "@calcom/lib/getPaymentAppData"; import { getTeamIdFromEventType } from "@calcom/lib/getTeamIdFromEventType"; @@ -1398,7 +1399,17 @@ async function handler( }, ]; + const blacklistedGuestEmails = process.env.BLACKLISTED_GUEST_EMAILS + ? process.env.BLACKLISTED_GUEST_EMAILS.split(",") + : []; + + const guestsRemoved: string[] = []; const guests = (reqGuests || []).reduce((guestArray, guest) => { + const baseGuestEmail = extractBaseEmail(guest).toLowerCase(); + if (blacklistedGuestEmails.some((e) => e.toLowerCase() === baseGuestEmail)) { + guestsRemoved.push(guest); + return guestArray; + } // If it's a team event, remove the team member from guests if (isTeamEventType && users.some((user) => user.email === guest)) { return guestArray; @@ -1414,6 +1425,10 @@ async function handler( return guestArray; }, [] as Invitee); + if (guestsRemoved.length > 0) { + log.info("Removed guests from the booking", guestsRemoved); + } + const seed = `${organizerUser.username}:${dayjs(reqBody.start).utc().format()}:${new Date().getTime()}`; const uid = translator.fromUUID(uuidv5(seed, uuidv5.URL)); diff --git a/packages/lib/extract-base-email.ts b/packages/lib/extract-base-email.ts new file mode 100644 index 0000000000..a687011000 --- /dev/null +++ b/packages/lib/extract-base-email.ts @@ -0,0 +1,6 @@ +// Function to extract base email +export const extractBaseEmail = (email: string): string => { + const [localPart, domain] = email.split("@"); + const baseLocalPart = localPart.split("+")[0]; + return `${baseLocalPart}@${domain}`; +}; diff --git a/packages/trpc/server/routers/publicViewer/_router.tsx b/packages/trpc/server/routers/publicViewer/_router.tsx index 417a37d94c..501663e47c 100644 --- a/packages/trpc/server/routers/publicViewer/_router.tsx +++ b/packages/trpc/server/routers/publicViewer/_router.tsx @@ -1,6 +1,7 @@ import publicProcedure from "../../procedures/publicProcedure"; import { importHandler, router } from "../../trpc"; import { slotsRouter } from "../viewer/slots/_router"; +import { ZUserEmailVerificationRequiredSchema } from "./checkIfUserEmailVerificationRequired.schema"; import { i18nInputSchema } from "./i18n.schema"; import { ZNoShowInputSchema } from "./noShow.schema"; import { event } from "./procedures/event"; @@ -56,4 +57,14 @@ export const publicViewerRouter = router({ ); return handler(); }), + + checkIfUserEmailVerificationRequired: publicProcedure + .input(ZUserEmailVerificationRequiredSchema) + .query(async (opts) => { + const handler = await importHandler( + namespaced("checkIfUserEmailVerificationRequired"), + () => import("./checkIfUserEmailVerificationRequired.handler") + ); + return handler(opts); + }), }); diff --git a/packages/trpc/server/routers/publicViewer/checkIfUserEmailVerificationRequired.handler.ts b/packages/trpc/server/routers/publicViewer/checkIfUserEmailVerificationRequired.handler.ts new file mode 100644 index 0000000000..0279035e86 --- /dev/null +++ b/packages/trpc/server/routers/publicViewer/checkIfUserEmailVerificationRequired.handler.ts @@ -0,0 +1,27 @@ +import { extractBaseEmail } from "@calcom/lib/extract-base-email"; +import logger from "@calcom/lib/logger"; + +import type { TUserEmailVerificationRequiredSchema } from "./checkIfUserEmailVerificationRequired.schema"; + +const log = logger.getSubLogger({ prefix: ["checkIfUserEmailVerificationRequired"] }); + +export const userWithEmailHandler = async ({ input }: { input: TUserEmailVerificationRequiredSchema }) => { + const { userSessionEmail, email } = input; + const baseEmail = extractBaseEmail(email); + + const blacklistedGuestEmails = process.env.BLACKLISTED_GUEST_EMAILS + ? process.env.BLACKLISTED_GUEST_EMAILS.split(",") + : []; + + const blacklistedEmail = blacklistedGuestEmails.find( + (guestEmail: string) => guestEmail.toLowerCase() === baseEmail.toLowerCase() + ); + + if (!!blacklistedEmail && blacklistedEmail !== userSessionEmail) { + log.warn(`blacklistedEmail: ${blacklistedEmail}`); + return true; + } + return false; +}; + +export default userWithEmailHandler; diff --git a/packages/trpc/server/routers/publicViewer/checkIfUserEmailVerificationRequired.schema.ts b/packages/trpc/server/routers/publicViewer/checkIfUserEmailVerificationRequired.schema.ts new file mode 100644 index 0000000000..1a6a6687ed --- /dev/null +++ b/packages/trpc/server/routers/publicViewer/checkIfUserEmailVerificationRequired.schema.ts @@ -0,0 +1,8 @@ +import { z } from "zod"; + +export const ZUserEmailVerificationRequiredSchema = z.object({ + userSessionEmail: z.string().optional(), + email: z.string(), +}); + +export type TUserEmailVerificationRequiredSchema = z.infer; diff --git a/turbo.json b/turbo.json index e5baaa1cfe..4354d7bcc2 100644 --- a/turbo.json +++ b/turbo.json @@ -233,6 +233,7 @@ "BASECAMP3_CLIENT_ID", "BASECAMP3_CLIENT_SECRET", "BASECAMP3_USER_AGENT", + "BLACKLISTED_GUEST_EMAILS", "AUTH_BEARER_TOKEN_VERCEL", "BUILD_ID", "CALCOM_APP_CREDENTIAL_ENCRYPTION_KEY",