diff --git a/packages/trpc/server/routers/viewer/slots/types.ts b/packages/trpc/server/routers/viewer/slots/types.ts index d9a78e7ede..78afb85cd9 100644 --- a/packages/trpc/server/routers/viewer/slots/types.ts +++ b/packages/trpc/server/routers/viewer/slots/types.ts @@ -3,40 +3,45 @@ import { z } from "zod"; import { timeZoneSchema } from "@calcom/lib/dayjs/timeZone.schema"; -export const getScheduleSchema = z - .object({ - // startTime ISOString - startTime: z.string(), - // endTime ISOString - endTime: z.string(), - // Event type ID - eventTypeId: z.coerce.number().int().optional(), - // Event type slug - eventTypeSlug: z.string().optional(), - // invitee timezone - timeZone: timeZoneSchema.optional(), - // or list of users (for dynamic events) - usernameList: z.array(z.string()).min(1).optional(), - debug: z.boolean().optional(), - // to handle event types with multiple duration options - duration: z - .string() - .optional() - .transform((val) => val && parseInt(val)), - rescheduleUid: z.string().nullish(), - // whether to do team event or user event - isTeamEvent: z.boolean().optional().default(false), - orgSlug: z.string().nullish(), - teamMemberEmail: z.string().nullish(), - routedTeamMemberIds: z.array(z.number()).nullish(), - skipContactOwner: z.boolean().nullish(), - _enableTroubleshooter: z.boolean().optional(), - _bypassCalendarBusyTimes: z.boolean().optional(), - _shouldServeCache: z.boolean().optional(), - routingFormResponseId: z.number().optional(), - queuedFormResponseId: z.string().nullish(), - email: z.string().nullish(), - }) +const isValidDateString = (val: string) => !isNaN(Date.parse(val)); + +export const getScheduleSchemaObject = z.object({ + startTime: z.string().refine(isValidDateString, { + message: "startTime must be a valid date string", + }), + endTime: z.string().refine(isValidDateString, { + message: "endTime must be a valid date string", + }), + // Event type ID + eventTypeId: z.coerce.number().int().optional(), + // Event type slug + eventTypeSlug: z.string().optional(), + // invitee timezone + timeZone: timeZoneSchema.optional(), + // or list of users (for dynamic events) + usernameList: z.array(z.string()).min(1).optional(), + debug: z.boolean().optional(), + // to handle event types with multiple duration options + duration: z + .string() + .optional() + .transform((val) => val && parseInt(val)), + rescheduleUid: z.string().nullish(), + // whether to do team event or user event + isTeamEvent: z.boolean().optional().default(false), + orgSlug: z.string().nullish(), + teamMemberEmail: z.string().nullish(), + routedTeamMemberIds: z.array(z.number()).nullish(), + skipContactOwner: z.boolean().nullish(), + _enableTroubleshooter: z.boolean().optional(), + _bypassCalendarBusyTimes: z.boolean().optional(), + _shouldServeCache: z.boolean().optional(), + routingFormResponseId: z.number().optional(), + queuedFormResponseId: z.string().nullish(), + email: z.string().nullish(), +}); + +export const getScheduleSchema = getScheduleSchemaObject .transform((val) => { // Need this so we can pass a single username in the query string form public API if (val.usernameList) { @@ -50,7 +55,11 @@ export const getScheduleSchema = z .refine( (data) => !!data.eventTypeId || (!!data.usernameList && !!data.eventTypeSlug), "You need to either pass an eventTypeId OR an usernameList/eventTypeSlug combination" - ); + ) + .refine(({ startTime, endTime }) => new Date(endTime).getTime() > new Date(startTime).getTime(), { + message: "endTime must be after startTime", + path: ["endTime"], + }); export const reserveSlotSchema = z .object({ @@ -82,7 +91,7 @@ export interface ContextForGetSchedule extends Record { req?: (IncomingMessage & { cookies: Partial<{ [key: string]: string }> }) | undefined; } -export type TGetScheduleInputSchema = z.infer; +export type TGetScheduleInputSchema = z.infer; export const ZGetScheduleInputSchema = getScheduleSchema; export type GetScheduleOptions = { diff --git a/packages/trpc/server/routers/viewer/slots/util.ts b/packages/trpc/server/routers/viewer/slots/util.ts index 2489a84e0a..e14b6771b2 100644 --- a/packages/trpc/server/routers/viewer/slots/util.ts +++ b/packages/trpc/server/routers/viewer/slots/util.ts @@ -186,10 +186,8 @@ export class AvailableSlotsService { const { currentOrgDomain, isValidOrgDomain } = organizationDetails; // For dynamic booking, we need to get and update user credentials, schedule and availability in the eventTypeObject as they're required in the new availability logic if (!input.eventTypeSlug) { - throw new TRPCError({ - message: "eventTypeSlug is required for dynamic booking", - code: "BAD_REQUEST", - }); + // never happens as it's guarded by our Zod Schema refine, but for clear type safety we throw an Error if the eventTypeSlug isn't given. + throw new Error("Event type slug is required in dynamic booking."); } const dynamicEventType = getDefaultEvent(input.eventTypeSlug); @@ -634,19 +632,6 @@ export class AvailableSlotsService { limitManager.mergeBusyTimes(globalLimitManager); - const bookingLimitsParams = { - bookings: userBusyTimes, - bookingLimits, - dateFrom, - dateTo, - limitManager, - rescheduleUid, - teamId, - user, - includeManagedEvents, - timeZone, - }; - for (const key of descendingLimitKeys) { const limit = bookingLimits?.[key]; if (!limit) continue; @@ -1011,10 +996,6 @@ export class AvailableSlotsService { ); const endTime = input.timeZone === "Etc/GMT" ? dayjs.utc(input.endTime) : dayjs(input.endTime).utc().tz(input.timeZone); - - if (!startTime.isValid() || !endTime.isValid()) { - throw new TRPCError({ message: "Invalid time range given.", code: "BAD_REQUEST" }); - } // when an empty array is given we should prefer to have it handled as if this wasn't given at all // we don't want to return no availability in this case. const routedTeamMemberIds = input.routedTeamMemberIds ?? []; @@ -1164,11 +1145,9 @@ export class AvailableSlotsService { scheduleId: eventType.restrictionScheduleId, }); if (restrictionSchedule) { + // runtime error preventing misconfiguration when restrictionSchedule timeZone must be used. if (!eventType.useBookerTimezone && !restrictionSchedule.timeZone) { - throw new TRPCError({ - message: "No timezone is set for the restricted schedule", - code: "BAD_REQUEST", - }); + throw new Error("No timezone is set for the restricted schedule"); } const restrictionTimezone = eventType.useBookerTimezone