* init * fix type * fix a re-render infinite loop because of missing readOnly (╯°□°)╯︵ ┻━┻) * further fixes * improvement * fix expiry datetime check * remove unnecessary prismaMock def * revert * fix test * add test ids * remove unit tests in favor of e2e * e2e test update * fix e2e * fix e2e * remove unnecessary change * abstract into injectable object * further improvements * fix label not selecting radio * fix type * code improvement * DI implementation * fix type * fix quick copy * code improvement and a few fixes * further improvements and NITS * further into DI * select * improve link list sorting * prep for easier conflict resolution * add back translations * using useCopy instead * improvement * add index to update salt and have different hash generation * fix private link description * fix increment regression in expiry logic * fixes * address feedback * use extractHostTimezone in event type listing * remove unused function * remove translationBundler * -_- * address feedback * further changes * address more feedback * NIT * address improvement suggestions * use extractHostTimezone * remove console log * pre update * code improvement * further fixes * cleanup * -_-
146 lines
4.3 KiB
TypeScript
146 lines
4.3 KiB
TypeScript
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));
|
|
}
|