Skip to content

Commit

Permalink
chore: Add guest email blacklist (calcom#15255)
Browse files Browse the repository at this point in the history
* chore: Add guest email blacklist

* types

* Added check to only log when we've removed one

* feat: add verification logic

* chore: use base exftract email

* Added toLowerCase for guest email checks

---------

Co-authored-by: Udit Takkar <[email protected]>
  • Loading branch information
keithwillcode and Udit-takkar authored May 30, 2024
1 parent 4cf89d2 commit 3b1de34
Show file tree
Hide file tree
Showing 8 changed files with 88 additions and 1 deletion.
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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({
Expand All @@ -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?.();

Expand All @@ -43,7 +58,8 @@ export const useVerifyEmail = ({
};

const renderConfirmNotVerifyEmailButtonCond =
!requiresBookerEmailVerification || (email && verifiedEmail && verifiedEmail === email);
(!requiresBookerEmailVerification && !isEmailVerificationRequired) ||
(email && verifiedEmail && verifiedEmail === email);

return {
handleVerifyEmail,
Expand Down
15 changes: 15 additions & 0 deletions packages/features/bookings/lib/handleNewBooking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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;
Expand All @@ -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));

Expand Down
6 changes: 6 additions & 0 deletions packages/lib/extract-base-email.ts
Original file line number Diff line number Diff line change
@@ -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}`;
};
11 changes: 11 additions & 0 deletions packages/trpc/server/routers/publicViewer/_router.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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);
}),
});
Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
@@ -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<typeof ZUserEmailVerificationRequiredSchema>;
1 change: 1 addition & 0 deletions turbo.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down

0 comments on commit 3b1de34

Please sign in to comment.