Files
calendar/packages/features/watchlist/lib/service/WatchlistOperationsService.ts
T
Alex van AndelGitHubalex@cal.com <me@alexvanandel.com>Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
5c2382d296 fix: allow wildcard prefix in domain validation (#27413)
* fix: allow wildcard prefix in normalizeDomain function

The normalizeDomain function now accepts domains with *. prefix
(e.g., *.cal.com) for wildcard domain matching in the watchlist.
The domain part after *. is validated using the existing regex.

Co-Authored-By: alex@cal.com <me@alexvanandel.com>

* fix: allow wildcard domains in validation checks

Update WatchlistOperationsService and CreateBlocklistEntryModal to
accept wildcard domain prefixes (*.) in domain validation. This ensures
wildcard domains like *.cal.com can be added through both the API and UI.

Co-Authored-By: alex@cal.com <me@alexvanandel.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-01-29 21:56:55 -03:00

97 lines
2.8 KiB
TypeScript

import type { PrismaBookingReportRepository } from "@calcom/features/bookingReport/repositories/PrismaBookingReportRepository";
import type { WatchlistEntry } from "@calcom/features/watchlist/lib/repository/IWatchlistRepository";
import type { WatchlistRepository } from "@calcom/features/watchlist/lib/repository/WatchlistRepository";
import { domainRegex, emailRegex } from "@calcom/lib/emailSchema";
import { WatchlistAction, WatchlistType } from "@calcom/prisma/enums";
import { WatchlistErrors } from "../errors/WatchlistErrors";
export interface AddReportsToWatchlistResult {
success: boolean;
message: string;
addedCount: number;
skippedCount: number;
results: Array<{ reportId: string; watchlistId: string }>;
}
export interface CreateWatchlistEntryInput {
type: WatchlistType;
value: string;
description?: string;
userId: number;
}
export interface CreateWatchlistEntryResult {
success: boolean;
entry: WatchlistEntry;
}
export interface DeleteWatchlistEntryInput {
entryId: string;
userId: number;
}
export interface DeleteWatchlistEntryResult {
success: boolean;
message: string;
}
export interface WatchlistOperationsScope {
organizationId: number | null;
isGlobal: boolean;
}
type Deps = {
watchlistRepo: WatchlistRepository;
bookingReportRepo: PrismaBookingReportRepository;
};
export abstract class WatchlistOperationsService {
constructor(protected readonly deps: Deps) {}
protected abstract getScope(): WatchlistOperationsScope;
validateEmailOrDomain(type: WatchlistType, value: string): void {
if (type === WatchlistType.EMAIL && !emailRegex.test(value)) {
throw WatchlistErrors.invalidEmail("Invalid email address format");
}
if (type === WatchlistType.DOMAIN) {
const domainToValidate = value.startsWith("*.") ? value.slice(2) : value;
if (!domainRegex.test(domainToValidate)) {
throw WatchlistErrors.invalidDomain("Invalid domain format (e.g., example.com or *.example.com)");
}
}
}
async createWatchlistEntry(input: CreateWatchlistEntryInput): Promise<CreateWatchlistEntryResult> {
const scope = this.getScope();
this.validateEmailOrDomain(input.type, input.value);
const entry = await this.deps.watchlistRepo.createEntryIfNotExists({
type: input.type,
value: input.value.toLowerCase(),
organizationId: scope.organizationId,
action: WatchlistAction.BLOCK,
description: input.description,
userId: input.userId,
isGlobal: scope.isGlobal,
});
return {
success: true,
entry,
};
}
async deleteWatchlistEntry(input: DeleteWatchlistEntryInput): Promise<DeleteWatchlistEntryResult> {
await this.deps.watchlistRepo.deleteEntry(input.entryId, input.userId);
return {
success: true,
message: "Entry deleted successfully",
};
}
}