fix: fix email exclusion validation for full emails and and @-prefixed domains (#26727)
* fix(bookings): fix email exclusion validation for full emails and @-prefixed domains * NIT * make case insensitive * fix: make domain matching case-insensitive in doesEmailMatchEntry Co-Authored-By: unknown <> --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
unknown
Devin AI
parent
5fa76f943b
commit
ce997447a4
@@ -1536,6 +1536,126 @@ describe("excluded email/domain validation", () => {
|
||||
email: "blocked.com@allowed.com",
|
||||
});
|
||||
});
|
||||
|
||||
test("should block full email match when exact email is excluded", async () => {
|
||||
const excludedEmails = "anik@cal.com";
|
||||
|
||||
const schema = getBookingResponsesSchema({
|
||||
bookingFields: [
|
||||
{
|
||||
name: "name",
|
||||
type: "name",
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: "email",
|
||||
type: "email",
|
||||
required: true,
|
||||
excludeEmails: excludedEmails,
|
||||
},
|
||||
] as z.infer<typeof eventTypeBookingFields> & z.BRAND<"HAS_SYSTEM_FIELDS">,
|
||||
view: "ALL_VIEWS",
|
||||
});
|
||||
|
||||
// anik@cal.com should be blocked
|
||||
const parsedResponses = await schema.safeParseAsync({
|
||||
name: "test",
|
||||
email: "anik@cal.com",
|
||||
});
|
||||
|
||||
expect(parsedResponses.success).toBe(false);
|
||||
|
||||
if (parsedResponses.success) {
|
||||
throw new Error("Should not reach here");
|
||||
}
|
||||
|
||||
expect(parsedResponses.error.issues[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
code: "custom",
|
||||
message: `{email}${CUSTOM_EMAIL_EXCLUDED_ERROR_MSG}`,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test("should block emails with domain when domain starts with @", async () => {
|
||||
const excludedEmails = "@gmail.com";
|
||||
|
||||
const schema = getBookingResponsesSchema({
|
||||
bookingFields: [
|
||||
{
|
||||
name: "name",
|
||||
type: "name",
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: "email",
|
||||
type: "email",
|
||||
required: true,
|
||||
excludeEmails: excludedEmails,
|
||||
},
|
||||
] as z.infer<typeof eventTypeBookingFields> & z.BRAND<"HAS_SYSTEM_FIELDS">,
|
||||
view: "ALL_VIEWS",
|
||||
});
|
||||
|
||||
// anik@gmail.com should be blocked when @gmail.com is excluded
|
||||
const parsedResponses = await schema.safeParseAsync({
|
||||
name: "test",
|
||||
email: "anik@gmail.com",
|
||||
});
|
||||
|
||||
expect(parsedResponses.success).toBe(false);
|
||||
|
||||
if (parsedResponses.success) {
|
||||
throw new Error("Should not reach here");
|
||||
}
|
||||
|
||||
expect(parsedResponses.error.issues[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
code: "custom",
|
||||
message: `{email}${CUSTOM_EMAIL_EXCLUDED_ERROR_MSG}`,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test("should block emails with domain when excluded email is just the domain without @", async () => {
|
||||
const excludedEmails = "gmail.com";
|
||||
|
||||
const schema = getBookingResponsesSchema({
|
||||
bookingFields: [
|
||||
{
|
||||
name: "name",
|
||||
type: "name",
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: "email",
|
||||
type: "email",
|
||||
required: true,
|
||||
excludeEmails: excludedEmails,
|
||||
},
|
||||
] as z.infer<typeof eventTypeBookingFields> & z.BRAND<"HAS_SYSTEM_FIELDS">,
|
||||
view: "ALL_VIEWS",
|
||||
});
|
||||
|
||||
// test@gmail.com should be blocked when gmail.com is excluded
|
||||
const parsedResponses = await schema.safeParseAsync({
|
||||
name: "test",
|
||||
email: "test@gmail.com",
|
||||
});
|
||||
|
||||
expect(parsedResponses.success).toBe(false);
|
||||
|
||||
if (parsedResponses.success) {
|
||||
throw new Error("Should not reach here");
|
||||
}
|
||||
|
||||
expect(parsedResponses.error.issues[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
code: "custom",
|
||||
message: `{email}${CUSTOM_EMAIL_EXCLUDED_ERROR_MSG}`,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("require email/domain validation", () => {
|
||||
|
||||
@@ -24,6 +24,28 @@ const ensureValidPhoneNumber = (value: string) => {
|
||||
// Replace the space(s) in the beginning with + as it is supposed to be provided in the beginning only
|
||||
return value.replace(/^ +/, "+");
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if a booker email matches an email/domain entry.
|
||||
* Supports three formats:
|
||||
* - Full email: "user@example.com" - matches exactly
|
||||
* - Domain with @ prefix: "@example.com" - matches any email ending with "@example.com"
|
||||
* - Domain without @ prefix: "example.com" - matches any email ending with "@example.com"
|
||||
*/
|
||||
const doesEmailMatchEntry = (bookerEmail: string, entry: string): boolean => {
|
||||
const bookerEmailLower = bookerEmail.toLowerCase();
|
||||
|
||||
if (entry.startsWith("@")) {
|
||||
const domain = entry.slice(1).toLowerCase();
|
||||
return bookerEmailLower.endsWith("@" + domain);
|
||||
}
|
||||
|
||||
if (entry.includes("@")) {
|
||||
return bookerEmailLower === entry.toLowerCase();
|
||||
}
|
||||
|
||||
return bookerEmailLower.endsWith("@" + entry.toLowerCase());
|
||||
};
|
||||
export const getBookingResponsesPartialSchema = ({ bookingFields, view, translateFn }: CommonParams) => {
|
||||
const schema = bookingResponses.unwrap().partial().and(catchAllSchema);
|
||||
|
||||
@@ -208,7 +230,7 @@ function preprocess<T extends z.ZodType>({
|
||||
const excludedEmails =
|
||||
bookingField.excludeEmails?.split(",").map((domain) => domain.trim()) || [];
|
||||
|
||||
const match = excludedEmails.find((email) => bookerEmail.endsWith("@" + email));
|
||||
const match = excludedEmails.find((excludedEntry) => doesEmailMatchEntry(bookerEmail, excludedEntry));
|
||||
if (match) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
@@ -220,7 +242,9 @@ function preprocess<T extends z.ZodType>({
|
||||
?.split(",")
|
||||
.map((domain) => domain.trim())
|
||||
.filter(Boolean) || [];
|
||||
const requiredEmailsMatch = requiredEmails.find((email) => bookerEmail.endsWith("@" + email));
|
||||
const requiredEmailsMatch = requiredEmails.find((requiredEntry) =>
|
||||
doesEmailMatchEntry(bookerEmail, requiredEntry)
|
||||
);
|
||||
if (requiredEmails.length > 0 && !requiredEmailsMatch) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
|
||||
Reference in New Issue
Block a user