fix: Hotfix in change from @spammer.com to spammer.com (#24479)

* fix: Hotfix in change from @spammer.com to spammer.com

* Test fixes + string

* Removed redundant test, fixed error message

* Comment fixup
This commit is contained in:
Alex van Andel
2025-10-15 15:38:14 +01:00
committed by GitHub
parent bb9d99c3e1
commit 56ef5d667c
5 changed files with 57 additions and 48 deletions
@@ -2,7 +2,7 @@
import { Controller, useForm } from "react-hook-form";
import { domainWithAtRegex, emailRegex } from "@calcom/lib/emailSchema";
import { domainRegex, emailRegex } from "@calcom/lib/emailSchema";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { WatchlistType } from "@calcom/prisma/enums";
import { trpc } from "@calcom/trpc/react";
@@ -71,7 +71,7 @@ export function CreateBlocklistEntryModal({ isOpen, onClose }: CreateBlocklistEn
return t("invalid_email_address");
}
} else if (watchType === WatchlistType.DOMAIN) {
if (!domainWithAtRegex.test(value)) {
if (!domainRegex.test(value)) {
return t("invalid_domain_format");
}
}
@@ -130,7 +130,7 @@ export function CreateBlocklistEntryModal({ isOpen, onClose }: CreateBlocklistEn
render={({ field }) => (
<Input
{...field}
placeholder={watchType === WatchlistType.EMAIL ? "user@example.com" : "@spammer.com"}
placeholder={watchType === WatchlistType.EMAIL ? "user@example.com" : "spammer.com"}
/>
)}
/>
@@ -3834,7 +3834,7 @@
"no_description_provided": "No description provided",
"organization_blocklist": "Organization Blocklist",
"manage_blocked_emails_and_domains": "Manage blocked emails and domains for your organization",
"invalid_domain_format": "Invalid domain format. Example: @example.com",
"invalid_domain_format": "Invalid domain format. Example: example.com",
"invalid_email_address": "Invalid email address. Example: user@example.com",
"reason_for_adding_to_blocklist": "Reason for adding to blocklist",
"what_would_you_like_to_block": "What would you like to block?",
+6 -4
View File
@@ -1,16 +1,18 @@
import { z } from "zod";
/** @see https://github.com/colinhacks/zod/issues/3155#issuecomment-2060045794 */
export const emailRegex =
/* eslint-disable-next-line no-useless-escape */
/^(?!\.)(?!.*\.\.)([A-Z0-9_+-\.']*)[A-Z0-9_+'-]@([A-Z0-9][A-Z0-9\-]*\.)+[A-Z]{2,}$/i;
/**
* Domain regex for watchlist entries - requires @ prefix
* Domain regex for watchlist entries
* Supports international domains with Unicode characters
* Examples: @example.com, @münchen.de, @example.co.uk
* Examples: example.com, münchen.de, example.co.uk
*/
export const domainWithAtRegex =
/^@[a-zA-Z0-9\u00a1-\uffff]([a-zA-Z0-9\u00a1-\uffff-]*[a-zA-Z0-9\u00a1-\uffff])?(\.[a-zA-Z0-9\u00a1-\uffff]([a-zA-Z0-9\u00a1-\uffff-]*[a-zA-Z0-9\u00a1-\uffff])?)*$/;
export const domainRegex =
/^[a-zA-Z0-9\u00a1-\uffff]([a-zA-Z0-9\u00a1-\uffff-]*[a-zA-Z0-9\u00a1-\uffff])?(\.[a-zA-Z0-9\u00a1-\uffff]([a-zA-Z0-9\u00a1-\uffff-]*[a-zA-Z0-9\u00a1-\uffff])?)*$/;
/**
* RFC 5321 Section 4.5.3.1.3 specifies:
@@ -1,9 +1,12 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { describe, it, expect, vi, beforeEach } from "vitest";
import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service";
import { WatchlistRepository } from "@calcom/lib/server/repository/watchlist.repository";
import { WatchlistType, WatchlistAction, MembershipRole } from "@calcom/prisma/enums";
import { TRPCError } from "@trpc/server";
import { createWatchlistEntryHandler } from "./createWatchlistEntry.handler";
vi.mock("@calcom/features/pbac/services/permission-check.service");
@@ -35,7 +38,7 @@ describe("createWatchlistEntryHandler", () => {
it("should throw FORBIDDEN when user is not part of an organization", async () => {
await expect(
createWatchlistEntryHandler({
ctx: { user: { ...mockUser, organizationId: undefined, profile: null } },
ctx: { user: { ...(mockUser as any), organizationId: undefined, profile: null } },
input: {
type: WatchlistType.EMAIL,
value: "spam@example.com",
@@ -54,7 +57,7 @@ describe("createWatchlistEntryHandler", () => {
await expect(
createWatchlistEntryHandler({
ctx: { user: mockUser },
ctx: { user: mockUser as any },
input: {
type: WatchlistType.EMAIL,
value: "spam@example.com",
@@ -73,7 +76,7 @@ describe("createWatchlistEntryHandler", () => {
mockWatchlistRepo.createEntry.mockResolvedValue({ id: "watchlist-1" });
await createWatchlistEntryHandler({
ctx: { user: mockUser },
ctx: { user: mockUser as any },
input: {
type: WatchlistType.EMAIL,
value: "test@example.com",
@@ -97,7 +100,7 @@ describe("createWatchlistEntryHandler", () => {
it("should throw BAD_REQUEST for invalid email format", async () => {
await expect(
createWatchlistEntryHandler({
ctx: { user: mockUser },
ctx: { user: mockUser as any },
input: {
type: WatchlistType.EMAIL,
value: "invalid-email",
@@ -114,7 +117,7 @@ describe("createWatchlistEntryHandler", () => {
it("should throw BAD_REQUEST for email missing @ symbol", async () => {
await expect(
createWatchlistEntryHandler({
ctx: { user: mockUser },
ctx: { user: mockUser as any },
input: {
type: WatchlistType.EMAIL,
value: "notanemail.com",
@@ -126,43 +129,43 @@ describe("createWatchlistEntryHandler", () => {
});
});
it("should throw BAD_REQUEST for invalid domain format without @", async () => {
await expect(
createWatchlistEntryHandler({
ctx: { user: mockUser },
input: {
type: WatchlistType.DOMAIN,
value: "example.com",
},
})
).rejects.toMatchObject({
code: "BAD_REQUEST",
message: "Invalid domain format. Domain must start with @ (e.g., @example.com)",
it("should accept valid domain format without @", async () => {
mockWatchlistRepo.createEntry.mockResolvedValue({ id: "watchlist-1" });
const result = await createWatchlistEntryHandler({
ctx: { user: mockUser as any },
input: {
type: WatchlistType.DOMAIN,
value: "example.com",
},
});
expect(mockWatchlistRepo.createEntry).not.toHaveBeenCalled();
expect(result.success).toBe(true);
expect(mockWatchlistRepo.createEntry).toHaveBeenCalled();
});
it("should throw BAD_REQUEST for domain with invalid format", async () => {
await expect(
createWatchlistEntryHandler({
ctx: { user: mockUser },
ctx: { user: mockUser as any },
input: {
type: WatchlistType.DOMAIN,
value: "not-a-domain",
value: "invalid..domain",
},
})
).rejects.toMatchObject({
code: "BAD_REQUEST",
message: "Invalid domain format. Domain must start with @ (e.g., @example.com)",
});
).rejects.toStrictEqual(
new TRPCError({
code: "BAD_REQUEST",
message: "Invalid domain format (e.g., example.com)",
})
);
});
it("should accept valid email format", async () => {
mockWatchlistRepo.createEntry.mockResolvedValue({ id: "watchlist-1" });
const result = await createWatchlistEntryHandler({
ctx: { user: mockUser },
ctx: { user: mockUser as any },
input: {
type: WatchlistType.EMAIL,
value: "valid@example.com",
@@ -173,19 +176,23 @@ describe("createWatchlistEntryHandler", () => {
expect(mockWatchlistRepo.createEntry).toHaveBeenCalled();
});
it("should accept valid domain format with @", async () => {
it("should accept valid domain format and normalize it", async () => {
mockWatchlistRepo.createEntry.mockResolvedValue({ id: "watchlist-1" });
const result = await createWatchlistEntryHandler({
ctx: { user: mockUser },
ctx: { user: mockUser as any },
input: {
type: WatchlistType.DOMAIN,
value: "@example.com",
value: "example.com",
},
});
expect(result.success).toBe(true);
expect(mockWatchlistRepo.createEntry).toHaveBeenCalled();
expect(mockWatchlistRepo.createEntry).toHaveBeenCalledWith(
expect.objectContaining({
value: "example.com",
})
);
});
});
@@ -205,7 +212,7 @@ describe("createWatchlistEntryHandler", () => {
mockWatchlistRepo.createEntry.mockResolvedValue(mockEntry);
const result = await createWatchlistEntryHandler({
ctx: { user: mockUser },
ctx: { user: mockUser as any },
input: {
type: WatchlistType.EMAIL,
value: "spam@example.com",
@@ -224,21 +231,21 @@ describe("createWatchlistEntryHandler", () => {
});
});
it("should create watchlist entry for DOMAIN type", async () => {
it("should create watchlist entry for DOMAIN type and normalize it", async () => {
const mockEntry = {
id: "watchlist-2",
type: WatchlistType.DOMAIN,
value: "@spammer.com",
value: "spammer.com",
organizationId: 100,
action: WatchlistAction.BLOCK,
};
mockWatchlistRepo.createEntry.mockResolvedValue(mockEntry);
const result = await createWatchlistEntryHandler({
ctx: { user: mockUser },
ctx: { user: mockUser as any },
input: {
type: WatchlistType.DOMAIN,
value: "@spammer.com",
value: "spammer.com",
},
});
@@ -246,7 +253,7 @@ describe("createWatchlistEntryHandler", () => {
expect(result.entry).toEqual(mockEntry);
expect(mockWatchlistRepo.createEntry).toHaveBeenCalledWith({
type: WatchlistType.DOMAIN,
value: "@spammer.com",
value: "spammer.com",
organizationId: 100,
action: WatchlistAction.BLOCK,
description: undefined,
@@ -258,7 +265,7 @@ describe("createWatchlistEntryHandler", () => {
mockWatchlistRepo.createEntry.mockResolvedValue({ id: "watchlist-4" });
await createWatchlistEntryHandler({
ctx: { user: mockUser },
ctx: { user: mockUser as any },
input: {
type: WatchlistType.EMAIL,
value: "SPAM@EXAMPLE.COM",
@@ -279,7 +286,7 @@ describe("createWatchlistEntryHandler", () => {
mockWatchlistRepo.createEntry.mockResolvedValue({ id: "watchlist-5" });
await createWatchlistEntryHandler({
ctx: { user: mockUser },
ctx: { user: mockUser as any },
input: {
type: WatchlistType.EMAIL,
value: "test@example.com",
@@ -1,5 +1,5 @@
import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service";
import { domainWithAtRegex, emailRegex } from "@calcom/lib/emailSchema";
import { domainRegex, emailRegex } from "@calcom/lib/emailSchema";
import { WatchlistRepository } from "@calcom/lib/server/repository/watchlist.repository";
import { prisma } from "@calcom/prisma";
import { MembershipRole, WatchlistAction } from "@calcom/prisma/enums";
@@ -51,10 +51,10 @@ export const createWatchlistEntryHandler = async ({ ctx, input }: CreateWatchlis
});
}
if (input.type === "DOMAIN" && !domainWithAtRegex.test(input.value)) {
if (input.type === "DOMAIN" && !domainRegex.test(input.value)) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Invalid domain format. Domain must start with @ (e.g., @example.com)",
message: "Invalid domain format (e.g., example.com)",
});
}