Files
calendar/packages/features/watchlist/lib/service/OrganizationWatchlistOperationsService.ts
Benny JooGitHubbenny@cal.com <sldisek783@gmail.com>Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
ab21c7f805 refactor: Cal.diy (#28903)
* feat: Cal.diy — community-driven MIT-licensed fork of Cal.com

This squashed commit contains all Cal.diy changes applied on top of calcom/cal.com main:

- Rebrand Cal.com to Cal.diy across the entire codebase
- Remove Enterprise Edition (EE) features, license checks, and AGPL restrictions
- Switch license from AGPL-3.0 to MIT
- Remove docs/ directory (migrated to Nextra at cal.diy)
- Remove dead code: org tests, EE tips, platform nav, premium username, SAML/SSO, etc.
- Clean up .env.example for self-hosted Cal.diy
- Update Docker image references to calcom/cal.diy
- Update README, CONTRIBUTING.md, and issue templates for Cal.diy community fork
- Add PR welcome bot for Cal.diy contributors
- Fix API v2 breaking changes oasdiff ignore entries
- Replace Blacksmith CI runners with default GitHub Actions

3893 files changed, 20789 insertions(+), 411020 deletions(-)

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* refactor: remove org-specific /organizations/:orgId endpoints from API v2 atoms controllers (#1701)

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix: revert Cal.diy Inc to Cal.com, Inc. in license files, copyright notices, and package metadata (#1702)

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* rip out org related comments in api v2

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-04-15 09:52:36 -03:00

145 lines
5.0 KiB
TypeScript

import type { PrismaBookingReportRepository } from "@calcom/features/bookingReport/repositories/PrismaBookingReportRepository";
import type { WatchlistRepository } from "@calcom/features/watchlist/lib/repository/WatchlistRepository";
import { BookingReportStatus, MembershipRole, WatchlistType } from "@calcom/prisma/enums";
import { WatchlistErrors } from "../errors/WatchlistErrors";
import { extractDomainFromEmail, normalizeEmail } from "../utils/normalization";
import type {
AddReportsToWatchlistResult,
CreateWatchlistEntryInput,
CreateWatchlistEntryResult,
DeleteWatchlistEntryInput,
DeleteWatchlistEntryResult,
WatchlistOperationsScope,
} from "./WatchlistOperationsService";
import { WatchlistOperationsService } from "./WatchlistOperationsService";
type PermissionString = string;
class PermissionCheckService {
constructor(_prisma?: unknown) {}
async checkPermission(..._args: unknown[]) { return true; }
async hasPermission(..._args: unknown[]) { return true; }
async getTeamIdsWithPermission(..._args: unknown[]): Promise<number[]> { return []; }
}
type Deps = {
watchlistRepo: WatchlistRepository;
bookingReportRepo: PrismaBookingReportRepository;
permissionCheckService: PermissionCheckService;
organizationId: number;
};
export class OrganizationWatchlistOperationsService extends WatchlistOperationsService {
private readonly permissionCheckService: PermissionCheckService;
private readonly organizationId: number;
constructor(deps: Deps) {
super(deps);
this.permissionCheckService = deps.permissionCheckService;
this.organizationId = deps.organizationId;
}
protected getScope(): WatchlistOperationsScope {
return {
organizationId: this.organizationId,
isGlobal: false,
};
}
private async checkPermission(userId: number, permission: PermissionString): Promise<void> {
const hasPermission = await this.permissionCheckService.checkPermission({
userId,
teamId: this.organizationId,
permission,
fallbackRoles: [MembershipRole.OWNER, MembershipRole.ADMIN],
});
if (!hasPermission) {
throw WatchlistErrors.permissionDenied(
`You are not authorized to ${permission.split(".")[1]} watchlist entries`
);
}
}
async createWatchlistEntry(input: CreateWatchlistEntryInput): Promise<CreateWatchlistEntryResult> {
await this.checkPermission(input.userId, "watchlist.create");
return super.createWatchlistEntry(input);
}
async deleteWatchlistEntry(input: DeleteWatchlistEntryInput): Promise<DeleteWatchlistEntryResult> {
await this.checkPermission(input.userId, "watchlist.delete");
return super.deleteWatchlistEntry(input);
}
async addToWatchlistByEmail(input: {
email: string;
type: WatchlistType;
description?: string;
userId: number;
}): Promise<AddReportsToWatchlistResult> {
await this.checkPermission(input.userId, "watchlist.create");
const scope = this.getScope();
let watchlistValue: string;
let reports: Array<{ id: string; bookerEmail: string; watchlistId: string | null }>;
if (input.type === WatchlistType.EMAIL) {
watchlistValue = normalizeEmail(input.email);
reports = await this.deps.bookingReportRepo.findPendingReportsByEmail({
email: watchlistValue,
organizationId: this.organizationId,
});
} else {
watchlistValue = extractDomainFromEmail(input.email);
reports = await this.deps.bookingReportRepo.findPendingReportsByDomain({
domain: watchlistValue,
organizationId: this.organizationId,
});
}
const { watchlistEntry } = await this.deps.watchlistRepo.createEntryFromReport({
type: input.type,
value: watchlistValue,
organizationId: scope.organizationId,
isGlobal: scope.isGlobal,
userId: input.userId,
description: input.description,
});
if (reports.length > 0) {
await this.deps.bookingReportRepo.bulkLinkWatchlistWithStatus({
links: reports.map((r) => ({ reportId: r.id, watchlistId: watchlistEntry.id })),
status: BookingReportStatus.BLOCKED,
});
}
return {
success: true,
message: `Successfully added ${input.type === WatchlistType.EMAIL ? "email" : "domain"} to blocklist`,
addedCount: reports.length,
skippedCount: 0,
results: reports.map((r) => ({ reportId: r.id, watchlistId: watchlistEntry.id })),
};
}
async dismissReportByEmail(input: {
email: string;
userId: number;
}): Promise<{ success: boolean; count: number }> {
await this.checkPermission(input.userId, "watchlist.update");
const normalizedEmail = normalizeEmail(input.email);
const result = await this.deps.bookingReportRepo.dismissReportsByEmail({
email: normalizedEmail,
status: BookingReportStatus.DISMISSED,
organizationId: this.organizationId,
});
if (result.count === 0) {
throw WatchlistErrors.notFound("No pending reports found for this email");
}
return { success: true, count: result.count };
}
}