import dayjs from "@calcom/dayjs"; import { ErrorCode } from "@calcom/lib/errorCodes"; export type EventTypeForTimezone = { userId?: number | null; teamId?: number | null; hosts?: Array<{ user: { timeZone: string | null; } | null; }> | null; owner?: { timeZone: string | null; } | null; team?: { members?: Array<{ user?: { timeZone: string | null; } | null; }> | null; } | null; }; export type HashedLinkData = { id: number; expiresAt: Date | null; maxUsageCount: number | null; usageCount: number; eventType: EventTypeForTimezone; }; /** * Extracts the host timezone from event type data * @param eventType - Event type data with potential timezone information * @returns Host timezone string or null if not found */ export function extractHostTimezone(eventType: EventTypeForTimezone): string { if (eventType?.userId && eventType?.owner?.timeZone) { // Personal event type - use owner's timezone return eventType.owner.timeZone; } else if (eventType?.teamId) { // Team event type - try hosts first, then team members if (eventType.hosts && eventType.hosts.length > 0 && eventType.hosts[0]?.user?.timeZone) { return eventType.hosts[0].user.timeZone; } else if ( eventType.team?.members && eventType.team.members.length > 0 && eventType.team.members[0]?.user?.timeZone ) { return eventType.team.members[0]?.user?.timeZone; } } return dayjs.tz.guess(); } /** * Core time-based expiration logic * @param expiresAt - The expiration date * @param timezone - Optional timezone for comparison * @returns true if expired, false if valid */ function hasExpiryTimePassed(expiresAt: Date | string | null, timezone?: string | null): boolean { if (!expiresAt) return false; if (timezone) { const now = dayjs().tz(timezone); const expiration = dayjs(expiresAt).tz(timezone); return expiration.isBefore(now); } else { const now = dayjs(); const expiration = dayjs(expiresAt); return expiration.isBefore(now); } } /** * Validates if a hashed link has expired based on time * @param expiresAt - The expiration date * @param eventType - Event type data for timezone extraction * @returns true if expired, false if valid */ export function isTimeBasedExpired(expiresAt: Date | null, eventType: EventTypeForTimezone): boolean { const hostTimezone = extractHostTimezone(eventType); return hasExpiryTimePassed(expiresAt, hostTimezone); } /** * Validates if a hashed link has exceeded its usage limit * @param usageCount - Current usage count * @param maxUsageCount - Maximum allowed usage count * @returns true if usage exceeded, false if valid */ export function isUsageBasedExpired(usageCount: number, maxUsageCount?: number | null): boolean { if (!maxUsageCount || maxUsageCount <= 0) return false; return usageCount >= maxUsageCount; } /** * Validates hashed link data for expiration * @param linkData - The hashed link data to validate * @throws Error with ErrorCode.PrivateLinkExpired if link is expired */ export function validateHashedLinkData(linkData: HashedLinkData): void { if (isTimeBasedExpired(linkData.expiresAt, linkData.eventType)) { throw new Error(ErrorCode.PrivateLinkExpired); } if (isUsageBasedExpired(linkData.usageCount, linkData.maxUsageCount)) { throw new Error(ErrorCode.PrivateLinkExpired); } } /** * Checks if a link has expired * @param link - Link object with expiration and usage data * @param timezone - Optional timezone for time-based expiration * @returns true if expired, false if valid */ export function isLinkExpired( link: { expiresAt?: Date | string | null; maxUsageCount?: number | null; usageCount?: number | null; }, timezone?: string | null ): boolean { if (link.expiresAt) return hasExpiryTimePassed(link.expiresAt, timezone); return isUsageBasedExpired(link.usageCount || 0, link.maxUsageCount); } /** * filters out expired links * @param links - Array of links to filter * @param timezone - Optional timezone for time-based expiration * @returns Array of active (non-expired) links */ export function filterActiveLinks< T extends { expiresAt?: Date | null; maxUsageCount?: number | null; usageCount?: number | null; } >(links: T[], timezone?: string | null): T[] { return links.filter((link) => !isLinkExpired(link, timezone)); }