diff --git a/lib/helpers/safeParseJSON.ts b/lib/helpers/safeParseJSON.ts new file mode 100644 index 0000000000..3681e42273 --- /dev/null +++ b/lib/helpers/safeParseJSON.ts @@ -0,0 +1,14 @@ +export default function parseJSONSafely(str: string) { + try { + return JSON.parse(str); + } catch (e) { + console.error((e as Error).message); + if ((e as Error).message.includes("Unexpected token")) { + return { + success: false, + message: `Invalid JSON in the body: ${(e as Error).message}`, + }; + } + return {}; + } +} diff --git a/lib/validations/schedule.ts b/lib/validations/schedule.ts index 176d972da1..229ab5a14b 100644 --- a/lib/validations/schedule.ts +++ b/lib/validations/schedule.ts @@ -1,14 +1,31 @@ import { z } from "zod"; -import { _ScheduleModel as Schedule } from "@calcom/prisma/zod"; +import { _ScheduleModel as Schedule, _AvailabilityModel as Availability } from "@calcom/prisma/zod"; const schemaScheduleBaseBodyParams = Schedule.omit({ id: true }).partial(); const schemaScheduleRequiredParams = z.object({ - userId: z.number(), - name: z.string(), + name: z.string().optional(), + userId: z.union([z.number(), z.array(z.number())]).optional(), }); export const schemaScheduleBodyParams = schemaScheduleBaseBodyParams.merge(schemaScheduleRequiredParams); -export const schemaSchedulePublic = Schedule.omit({}); +export const schemaSingleScheduleBodyParams = schemaScheduleBaseBodyParams.merge( + z.object({ userId: z.number().optional() }) +); + +export const schemaCreateScheduleBodyParams = schemaScheduleBaseBodyParams.merge( + z.object({ userId: z.number().optional(), name: z.string() }) +); + +export const schemaSchedulePublic = z + .object({ id: z.number() }) + .merge(Schedule) + .merge( + z.object({ + availability: z + .array(Availability.pick({ id: true, eventTypeId: true, days: true, startTime: true, endTime: true })) + .optional(), + }) + ); diff --git a/pages/api/schedules/[id].ts b/pages/api/schedules/[id].ts index 593f9d7846..59c78ea12b 100644 --- a/pages/api/schedules/[id].ts +++ b/pages/api/schedules/[id].ts @@ -1,27 +1,44 @@ import type { NextApiRequest, NextApiResponse } from "next"; +import safeParseJSON from "@lib/helpers/safeParseJSON"; import { withMiddleware } from "@lib/helpers/withMiddleware"; import type { ScheduleResponse } from "@lib/types"; -import { schemaScheduleBodyParams, schemaSchedulePublic } from "@lib/validations/schedule"; +import { schemaSingleScheduleBodyParams, schemaSchedulePublic } from "@lib/validations/schedule"; import { schemaQueryIdParseInt, withValidQueryIdTransformParseInt, } from "@lib/validations/shared/queryIdTransformParseInt"; export async function scheduleById( - { method, query, body, userId, prisma }: NextApiRequest, + { method, query, body, userId, isAdmin, prisma }: NextApiRequest, res: NextApiResponse ) { + body = safeParseJSON(body); + if (!body.success) { + res.status(400).json({ message: body.message }); + } + + const safeBody = schemaSingleScheduleBodyParams.safeParse(body); + if (!safeBody.success) { + res.status(400).json({ message: "Bad request" }); + return; + } + const safeQuery = schemaQueryIdParseInt.safeParse(query); - const safeBody = schemaScheduleBodyParams.safeParse(body); + if (safeBody.data.userId && !isAdmin) { + res.status(401).json({ message: "Unauthorized" }); + return; + } if (!safeQuery.success) { res.status(400).json({ message: "Your query was invalid" }); return; } - const userSchedules = await prisma.schedule.findMany({ where: { userId } }); + const userSchedules = await prisma.schedule.findMany({ where: { userId: safeBody.data.userId || userId } }); const userScheduleIds = userSchedules.map((schedule) => schedule.id); - if (!userScheduleIds.includes(safeQuery.data.id)) res.status(401).json({ message: "Unauthorized" }); - else { + if (!userScheduleIds.includes(safeQuery.data.id)) { + res.status(401).json({ message: "Unauthorized" }); + return; + } else { switch (method) { /** * @swagger @@ -48,7 +65,10 @@ export async function scheduleById( */ case "GET": await prisma.schedule - .findUnique({ where: { id: safeQuery.data.id } }) + .findUnique({ + where: { id: safeQuery.data.id }, + include: { availability: true }, + }) .then((data) => schemaSchedulePublic.parse(data)) .then((schedule) => res.status(200).json({ schedule })) .catch((error: Error) => @@ -89,6 +109,9 @@ export async function scheduleById( return; } } + + delete safeBody.data.userId; + await prisma.schedule .update({ where: { id: safeQuery.data.id }, data: safeBody.data }) .then((data) => schemaSchedulePublic.parse(data)) diff --git a/pages/api/schedules/index.ts b/pages/api/schedules/index.ts index 81964f4030..86faad4181 100644 --- a/pages/api/schedules/index.ts +++ b/pages/api/schedules/index.ts @@ -1,72 +1,128 @@ import type { NextApiRequest, NextApiResponse } from "next"; +import { getAvailabilityFromSchedule, DEFAULT_SCHEDULE } from "@calcom/lib/availability"; + +import safeParseJSON from "@lib/helpers/safeParseJSON"; import { withMiddleware } from "@lib/helpers/withMiddleware"; import { ScheduleResponse, SchedulesResponse } from "@lib/types"; -import { schemaScheduleBodyParams, schemaSchedulePublic } from "@lib/validations/schedule"; +import { + schemaScheduleBodyParams, + schemaSchedulePublic, + schemaCreateScheduleBodyParams, +} from "@lib/validations/schedule"; async function createOrlistAllSchedules( - { method, body, userId, prisma }: NextApiRequest, + { method, body, userId, isAdmin, prisma }: NextApiRequest, res: NextApiResponse ) { - if (method === "GET") { - /** - * @swagger - * /schedules: - * get: - * operationId: listSchedules - * summary: Find all schedules - * tags: - * - schedules - * responses: - * 200: - * description: OK - * 401: - * description: Authorization information is missing or invalid. - * 404: - * description: No schedules were found - */ - const data = await prisma.schedule.findMany({ where: { userId } }); - const schedules = data.map((schedule) => schemaSchedulePublic.parse(schedule)); - if (schedules) res.status(200).json({ schedules }); - else - (error: Error) => - res.status(404).json({ - message: "No Schedules were found", - error, - }); - } else if (method === "POST") { - /** - * @swagger - * /schedules: - * post: - * operationId: addSchedule - * summary: Creates a new schedule - * tags: - * - schedules - * responses: - * 201: - * description: OK, schedule created - * 400: - * description: Bad request. Schedule body is invalid. - * 401: - * description: Authorization information is missing or invalid. - */ - const safe = schemaScheduleBodyParams.safeParse(body); - if (!safe.success) { - res.status(400).json({ message: "Invalid request body" }); - return; - } - const data = await prisma.schedule.create({ data: { ...safe.data, userId } }); - const schedule = schemaSchedulePublic.parse(data); + body = safeParseJSON(body); + if (!body.success) { + res.status(400).json({ message: body.message }); + return; + } - if (schedule) res.status(201).json({ schedule, message: "Schedule created successfully" }); - else - (error: Error) => - res.status(400).json({ - message: "Could not create new schedule", - error, - }); - } else res.status(405).json({ message: `Method ${method} not allowed` }); + const safe = schemaScheduleBodyParams.safeParse(body); + + if (!safe.success) { + res.status(400).json({ message: "Bad request" }); + return; + } + + const safeBody = safe.data; + + if (safeBody.userId && !isAdmin) { + res.status(401).json({ message: "Unauthorized" }); + return; + } else { + if (method === "GET") { + /** + * @swagger + * /schedules: + * get: + * operationId: listSchedules + * summary: Find all schedules + * tags: + * - schedules + * responses: + * 200: + * description: OK + * 401: + * description: Authorization information is missing or invalid. + * 404: + * description: No schedules were found + */ + + const userIds = Array.isArray(safeBody.userId) ? safeBody.userId : [safeBody.userId || userId]; + + const data = await prisma.schedule.findMany({ + where: { + userId: { in: userIds }, + }, + include: { availability: true }, + ...(Array.isArray(body.userId) && { orderBy: { userId: "asc" } }), + }); + const schedules = data.map((schedule) => schemaSchedulePublic.parse(schedule)); + if (schedules) res.status(200).json({ schedules }); + else + (error: Error) => + res.status(404).json({ + message: "No Schedules were found", + error, + }); + } else if (method === "POST") { + /** + * @swagger + * /schedules: + * post: + * operationId: addSchedule + * summary: Creates a new schedule + * tags: + * - schedules + * responses: + * 201: + * description: OK, schedule created + * 400: + * description: Bad request. Schedule body is invalid. + * 401: + * description: Authorization information is missing or invalid. + */ + const safe = schemaCreateScheduleBodyParams.safeParse(body); + if (body.userId && !isAdmin) { + res.status(401).json({ message: "Unauthorized" }); + return; + } + + if (!safe.success) { + res.status(400).json({ message: "Invalid request body" }); + return; + } + + const data = await prisma.schedule.create({ + data: { + ...safe.data, + userId: safe.data.userId || userId, + availability: { + createMany: { + data: getAvailabilityFromSchedule(DEFAULT_SCHEDULE).map((schedule) => ({ + days: schedule.days, + startTime: schedule.startTime, + endTime: schedule.endTime, + })), + }, + }, + }, + }); + const schedule = schemaSchedulePublic.parse(data); + + if (schedule) res.status(201).json({ schedule, message: "Schedule created successfully" }); + else + (error: Error) => + res.status(400).json({ + message: "Could not create new schedule", + error, + }); + } else res.status(405).json({ message: `Method ${method} not allowed` }); + } } export default withMiddleware("HTTP_GET_OR_POST")(createOrlistAllSchedules);