From 18e96e2a47a48c5d79f98925f91f45a7cb7e2e11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Omar=20L=C3=B3pez?= Date: Mon, 10 Oct 2022 09:42:15 -0600 Subject: [PATCH] Refactors availabilities endpoints (#177) refs #175 --- lib/validations/availability.ts | 30 ++- pages/api/availabilities/[id].ts | 206 ------------------ .../availabilities/[id]/_auth-middleware.ts | 21 ++ pages/api/availabilities/[id]/_delete.ts | 39 ++++ pages/api/availabilities/[id]/_get.ts | 41 ++++ pages/api/availabilities/[id]/_patch.ts | 65 ++++++ pages/api/availabilities/[id]/index.ts | 16 ++ pages/api/availabilities/_get.ts | 58 +++++ pages/api/availabilities/_post.ts | 76 +++++++ pages/api/availabilities/index.ts | 144 +----------- 10 files changed, 336 insertions(+), 360 deletions(-) delete mode 100644 pages/api/availabilities/[id].ts create mode 100644 pages/api/availabilities/[id]/_auth-middleware.ts create mode 100644 pages/api/availabilities/[id]/_delete.ts create mode 100644 pages/api/availabilities/[id]/_get.ts create mode 100644 pages/api/availabilities/[id]/_patch.ts create mode 100644 pages/api/availabilities/[id]/index.ts create mode 100644 pages/api/availabilities/_get.ts create mode 100644 pages/api/availabilities/_post.ts diff --git a/lib/validations/availability.ts b/lib/validations/availability.ts index f55d481b2f..9d6fd20d1f 100644 --- a/lib/validations/availability.ts +++ b/lib/validations/availability.ts @@ -1,15 +1,14 @@ import { z } from "zod"; -import { _AvailabilityModel as Availability } from "@calcom/prisma/zod"; +import { _AvailabilityModel as Availability, _ScheduleModel as Schedule } from "@calcom/prisma/zod"; +import { denullishShape } from "@calcom/prisma/zod-utils"; -export const schemaAvailabilityBaseBodyParams = Availability.pick({ - startTime: true, - endTime: true, - date: true, - scheduleId: true, - days: true, - userId: true, -}).partial(); +export const schemaAvailabilityBaseBodyParams = /** We make all these properties required */ denullishShape( + Availability.pick({ + /** We need to pass the schedule where this availability belongs to */ + scheduleId: true, + }) +); export const schemaAvailabilityReadPublic = Availability.pick({ id: true, @@ -18,16 +17,15 @@ export const schemaAvailabilityReadPublic = Availability.pick({ date: true, scheduleId: true, days: true, - eventTypeId: true, - userId: true, -}).merge(z.object({ success: z.boolean().optional() })); + // eventTypeId: true /** @deprecated */, + // userId: true /** @deprecated */, +}).merge(z.object({ success: z.boolean().optional(), Schedule: Schedule.partial() }).partial()); const schemaAvailabilityCreateParams = z .object({ startTime: z.date().or(z.string()), endTime: z.date().or(z.string()), days: z.array(z.number()).optional(), - eventTypeId: z.number().optional(), }) .strict(); @@ -36,13 +34,11 @@ const schemaAvailabilityEditParams = z startTime: z.date().or(z.string()).optional(), endTime: z.date().or(z.string()).optional(), days: z.array(z.number()).optional(), - eventTypeId: z.number().optional(), }) .strict(); -export const schemaAvailabilityEditBodyParams = schemaAvailabilityBaseBodyParams.merge( - schemaAvailabilityEditParams -); +export const schemaAvailabilityEditBodyParams = schemaAvailabilityEditParams; + export const schemaAvailabilityCreateBodyParams = schemaAvailabilityBaseBodyParams.merge( schemaAvailabilityCreateParams ); diff --git a/pages/api/availabilities/[id].ts b/pages/api/availabilities/[id].ts deleted file mode 100644 index d95c10cc18..0000000000 --- a/pages/api/availabilities/[id].ts +++ /dev/null @@ -1,206 +0,0 @@ -import type { NextApiRequest, NextApiResponse } from "next"; - -import safeParseJSON from "@lib/helpers/safeParseJSON"; -import { withMiddleware } from "@lib/helpers/withMiddleware"; -import type { AvailabilityResponse } from "@lib/types"; -import { - schemaAvailabilityEditBodyParams, - schemaAvailabilityReadPublic, - schemaSingleAvailabilityReadBodyParams, -} from "@lib/validations/availability"; -import { - schemaQueryIdParseInt, - withValidQueryIdTransformParseInt, -} from "@lib/validations/shared/queryIdTransformParseInt"; - -export async function availabilityById( - { method, query, body, userId, isAdmin, prisma }: NextApiRequest, - res: NextApiResponse -) { - body = safeParseJSON(body); - if (body.success !== undefined && !body.success) { - res.status(400).json({ message: body.message }); - return; - } - - const safeQuery = schemaQueryIdParseInt.safeParse(query); - if (!safeQuery.success) { - res.status(400).json({ message: "Your query is invalid", error: safeQuery.error }); - return; - } - - const safe = schemaSingleAvailabilityReadBodyParams.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; - } - - const data = await prisma.schedule.findMany({ - where: { userId: safeBody.userId || userId }, - select: { - availability: true, - }, - }); - - const availabilitiesArray = data.flatMap((schedule) => schedule.availability); - - if (!availabilitiesArray.some((availability) => availability.id === safeQuery.data.id)) { - res.status(401).json({ message: "Unauthorized" }); - return; - } else { - switch (method) { - /** - * @swagger - * /availabilities/{id}: - * get: - * operationId: getAvailabilityById - * summary: Find an availability - * parameters: - * - in: path - * name: id - * schema: - * type: integer - * required: true - * description: ID of the availability to get - * tags: - * - availabilities - * externalDocs: - * url: https://docs.cal.com/availability - * responses: - * 200: - * description: OK - * 401: - * description: Unathorized - */ - case "GET": - await prisma.availability - .findUnique({ where: { id: safeQuery.data.id } }) - .then((data) => schemaAvailabilityReadPublic.parse(data)) - .then((availability) => res.status(200).json({ availability })) - .catch((error: Error) => - res.status(404).json({ message: `Availability with id: ${safeQuery.data.id} not found`, error }) - ); - break; - /** - * @swagger - * /availabilities/{id}: - * patch: - * operationId: editAvailabilityById - * summary: Edit an existing availability - * requestBody: - * description: Edit an existing availability related to one of your bookings - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * days: - * type: array - * example: email@example.com - * startTime: - * type: string - * example: 1970-01-01T17:00:00.000Z - * endTime: - * type: string - * example: 1970-01-01T17:00:00.000Z - * parameters: - * - in: path - * name: id - * schema: - * type: integer - * required: true - * description: ID of the availability to edit - * tags: - * - availabilities - * externalDocs: - * url: https://docs.cal.com/availability - * responses: - * 201: - * description: OK, availability edited successfuly - * 400: - * description: Bad request. Availability body is invalid. - * 401: - * description: Authorization information is missing or invalid. - */ - case "PATCH": - const safeBody = schemaAvailabilityEditBodyParams.safeParse(body); - if (!safeBody.success) { - console.log(safeBody.error); - res.status(400).json({ message: "Bad request" + safeBody.error, error: safeBody.error }); - return; - } - const userEventTypes = await prisma.eventType.findMany({ where: { userId } }); - const userEventTypesIds = userEventTypes.map((event) => event.id); - if (safeBody.data.eventTypeId && !userEventTypesIds.includes(safeBody.data.eventTypeId)) { - res.status(401).json({ - message: `Bad request. You're not the owner of eventTypeId: ${safeBody.data.eventTypeId}`, - }); - } - await prisma.availability - .update({ - where: { id: safeQuery.data.id }, - data: safeBody.data, - }) - .then((data) => schemaAvailabilityReadPublic.parse(data)) - .then((availability) => res.status(200).json({ availability })) - .catch((error: Error) => { - res.status(404).json({ - message: `Availability with id: ${safeQuery.data.id} not found`, - error, - }); - }); - break; - /** - * @swagger - * /availabilities/{id}: - * delete: - * operationId: removeAvailabilityById - * summary: Remove an existing availability - * parameters: - * - in: path - * name: id - * schema: - * type: integer - * required: true - * description: ID of the availability to delete - * tags: - * - availabilities - * externalDocs: - * url: https://docs.cal.com/availability - * responses: - * 201: - * description: OK, availability removed successfuly - * 400: - * description: Bad request. Availability id is invalid. - * 401: - * description: Authorization information is missing or invalid. - */ - case "DELETE": - await prisma.availability - .delete({ where: { id: safeQuery.data.id } }) - .then(() => - res - .status(200) - .json({ message: `Availability with id: ${safeQuery.data.id} deleted successfully` }) - ) - .catch((error: Error) => - res.status(404).json({ message: `Availability with id: ${safeQuery.data.id} not found`, error }) - ); - break; - - default: - res.status(405).json({ message: "Method not allowed" }); - break; - } - } -} - -export default withMiddleware("HTTP_GET_DELETE_PATCH")(withValidQueryIdTransformParseInt(availabilityById)); diff --git a/pages/api/availabilities/[id]/_auth-middleware.ts b/pages/api/availabilities/[id]/_auth-middleware.ts new file mode 100644 index 0000000000..44378f90a3 --- /dev/null +++ b/pages/api/availabilities/[id]/_auth-middleware.ts @@ -0,0 +1,21 @@ +import type { NextApiRequest } from "next"; + +import { defaultResponder } from "@calcom/lib/server"; + +import { schemaQueryIdParseInt } from "@lib/validations/shared/queryIdTransformParseInt"; + +export async function authMiddleware(req: NextApiRequest) { + const { userId, prisma, isAdmin, query } = req; + const { id } = schemaQueryIdParseInt.parse(query); + /** Admins can skip the ownership verification */ + if (isAdmin) return; + /** + * There's a caveat here. If the availability exists but the user doesn't own it, + * the user will see a 404 error which may or not be the desired behavior. + */ + await prisma.availability.findFirstOrThrow({ + where: { id, Schedule: { userId } }, + }); +} + +export default defaultResponder(authMiddleware); diff --git a/pages/api/availabilities/[id]/_delete.ts b/pages/api/availabilities/[id]/_delete.ts new file mode 100644 index 0000000000..a298642d6f --- /dev/null +++ b/pages/api/availabilities/[id]/_delete.ts @@ -0,0 +1,39 @@ +import type { NextApiRequest } from "next"; + +import { defaultResponder } from "@calcom/lib/server"; + +import { schemaQueryIdParseInt } from "@lib/validations/shared/queryIdTransformParseInt"; + +/** + * @swagger + * /availabilities/{id}: + * delete: + * operationId: removeAvailabilityById + * summary: Remove an existing availability + * parameters: + * - in: path + * name: id + * schema: + * type: integer + * required: true + * description: ID of the availability to delete + * tags: + * - availabilities + * externalDocs: + * url: https://docs.cal.com/availability + * responses: + * 201: + * description: OK, availability removed successfully + * 400: + * description: Bad request. Availability id is invalid. + * 401: + * description: Authorization information is missing or invalid. + */ +export async function deleteHandler(req: NextApiRequest) { + const { prisma, query } = req; + const { id } = schemaQueryIdParseInt.parse(query); + await prisma.availability.delete({ where: { id } }); + return { message: `Availability with id: ${id} deleted successfully` }; +} + +export default defaultResponder(deleteHandler); diff --git a/pages/api/availabilities/[id]/_get.ts b/pages/api/availabilities/[id]/_get.ts new file mode 100644 index 0000000000..3ea8064e05 --- /dev/null +++ b/pages/api/availabilities/[id]/_get.ts @@ -0,0 +1,41 @@ +import type { NextApiRequest } from "next"; + +import { defaultResponder } from "@calcom/lib/server"; + +import { schemaAvailabilityReadPublic } from "@lib/validations/availability"; +import { schemaQueryIdParseInt } from "@lib/validations/shared/queryIdTransformParseInt"; + +/** + * @swagger + * /availabilities/{id}: + * get: + * operationId: getAvailabilityById + * summary: Find an availability + * parameters: + * - in: path + * name: id + * schema: + * type: integer + * required: true + * description: ID of the availability to get + * tags: + * - availabilities + * externalDocs: + * url: https://docs.cal.com/availability + * responses: + * 200: + * description: OK + * 401: + * description: Unauthorized + */ +export async function getHandler(req: NextApiRequest) { + const { prisma, query } = req; + const { id } = schemaQueryIdParseInt.parse(query); + const availability = await prisma.availability.findUnique({ + where: { id }, + include: { Schedule: { select: { userId: true } } }, + }); + return { availability: schemaAvailabilityReadPublic.parse(availability) }; +} + +export default defaultResponder(getHandler); diff --git a/pages/api/availabilities/[id]/_patch.ts b/pages/api/availabilities/[id]/_patch.ts new file mode 100644 index 0000000000..9e7b0b5ebb --- /dev/null +++ b/pages/api/availabilities/[id]/_patch.ts @@ -0,0 +1,65 @@ +import type { NextApiRequest } from "next"; + +import { defaultResponder } from "@calcom/lib/server"; + +import { + schemaAvailabilityEditBodyParams, + schemaAvailabilityReadPublic, +} from "@lib/validations/availability"; +import { schemaQueryIdParseInt } from "@lib/validations/shared/queryIdTransformParseInt"; + +/** + * @swagger + * /availabilities/{id}: + * patch: + * operationId: editAvailabilityById + * summary: Edit an existing availability + * requestBody: + * description: Edit an existing availability related to one of your bookings + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * days: + * type: array + * example: email@example.com + * startTime: + * type: string + * example: 1970-01-01T17:00:00.000Z + * endTime: + * type: string + * example: 1970-01-01T17:00:00.000Z + * parameters: + * - in: path + * name: id + * schema: + * type: integer + * required: true + * description: ID of the availability to edit + * tags: + * - availabilities + * externalDocs: + * url: https://docs.cal.com/availability + * responses: + * 201: + * description: OK, availability edited successfully + * 400: + * description: Bad request. Availability body is invalid. + * 401: + * description: Authorization information is missing or invalid. + */ +export async function patchHandler(req: NextApiRequest) { + const { prisma, query, body } = req; + const { id } = schemaQueryIdParseInt.parse(query); + const data = schemaAvailabilityEditBodyParams.parse(body); + const availability = await prisma.availability.update({ + where: { id }, + data, + include: { Schedule: { select: { userId: true } } }, + }); + return { availability: schemaAvailabilityReadPublic.parse(availability) }; +} + +export default defaultResponder(patchHandler); diff --git a/pages/api/availabilities/[id]/index.ts b/pages/api/availabilities/[id]/index.ts new file mode 100644 index 0000000000..4b740bcbb5 --- /dev/null +++ b/pages/api/availabilities/[id]/index.ts @@ -0,0 +1,16 @@ +import { NextApiRequest, NextApiResponse } from "next"; + +import { defaultHandler } from "@calcom/lib/server"; + +import { withMiddleware } from "@lib/helpers/withMiddleware"; + +import authMiddleware from "./_auth-middleware"; + +export default withMiddleware("HTTP_GET_DELETE_PATCH")(async (req: NextApiRequest, res: NextApiResponse) => { + await authMiddleware(req, res); + return defaultHandler({ + GET: import("./_get"), + PATCH: import("./_patch"), + DELETE: import("./_delete"), + })(req, res); +}); diff --git a/pages/api/availabilities/_get.ts b/pages/api/availabilities/_get.ts new file mode 100644 index 0000000000..b0649b5b16 --- /dev/null +++ b/pages/api/availabilities/_get.ts @@ -0,0 +1,58 @@ +import type { NextApiRequest } from "next"; + +import { HttpError } from "@calcom/lib/http-error"; +import { defaultResponder } from "@calcom/lib/server"; + +import { schemaAvailabilityReadBodyParams } from "@lib/validations/availability"; + +/** + * @swagger + * /availabilities: + * get: + * operationId: listAvailabilities + * summary: Find all availabilities + * tags: + * - availabilities + * externalDocs: + * url: https://docs.cal.com/availability + * responses: + * 200: + * description: OK + * 401: + * description: Authorization information is missing or invalid. + * 404: + * description: No availabilities were found + */ +async function handler(req: NextApiRequest) { + const { userId, isAdmin, prisma } = req; + const body = schemaAvailabilityReadBodyParams.parse(req.body || {}); + + if (body.userId && !isAdmin) + throw new HttpError({ + statusCode: 401, + message: "Unauthorized: Only admins can request other users' availabilities", + }); + + const userIds = Array.isArray(body.userId) ? body.userId : [body.userId || userId]; + + let availabilities = await prisma.availability.findMany({ + where: { Schedule: { userId: { in: userIds } } }, + include: { Schedule: { select: { userId: true } } }, + ...(Array.isArray(body.userId) && { orderBy: { userId: "asc" } }), + }); + + /** + * IDK if this a special requirement but, since availabilities aren't directly related to a user. + * We manually override the `userId` field so the user doesn't need to query the scheduleId just + * to get the user that it belongs to... OR... we can just access `availability.Schedule.userId` instead + * but at this point I'm afraid to break something so we will have both for now... ¯\_(ツ)_/¯ + */ + availabilities = availabilities.map((a) => ({ ...a, userId: a.Schedule?.userId || null })); + + if (!availabilities.length) + throw new HttpError({ statusCode: 404, message: "No Availabilities were found" }); + + return { availabilities }; +} + +export default defaultResponder(handler); diff --git a/pages/api/availabilities/_post.ts b/pages/api/availabilities/_post.ts new file mode 100644 index 0000000000..aa4bac13fe --- /dev/null +++ b/pages/api/availabilities/_post.ts @@ -0,0 +1,76 @@ +import type { NextApiRequest } from "next"; + +import { HttpError } from "@calcom/lib/http-error"; +import { defaultResponder } from "@calcom/lib/server"; + +import { + schemaAvailabilityCreateBodyParams, + schemaAvailabilityReadPublic, +} from "@lib/validations/availability"; + +/** + * @swagger + * /availabilities: + * post: + * operationId: addAvailability + * summary: Creates a new availability + * requestBody: + * description: Edit an existing availability related to one of your bookings + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - scheduleId + * - startTime + * - endTime + * properties: + * days: + * type: array + * example: email@example.com + * startTime: + * type: string + * example: 1970-01-01T17:00:00.000Z + * endTime: + * type: string + * example: 1970-01-01T17:00:00.000Z + * tags: + * - availabilities + * externalDocs: + * url: https://docs.cal.com/availability + * responses: + * 201: + * description: OK, availability created + * 400: + * description: Bad request. Availability body is invalid. + * 401: + * description: Authorization information is missing or invalid. + */ +async function postHandler(req: NextApiRequest) { + const { prisma } = req; + const data = schemaAvailabilityCreateBodyParams.parse(req.body); + await checkPermissions(req); + const availability = await prisma.availability.create({ + data, + include: { Schedule: { select: { userId: true } } }, + }); + req.statusCode = 201; + return { + availability: schemaAvailabilityReadPublic.parse(availability), + message: "Availability created successfully", + }; +} + +async function checkPermissions(req: NextApiRequest) { + const { userId, prisma, isAdmin } = req; + if (isAdmin) return; + const data = schemaAvailabilityCreateBodyParams.parse(req.body); + const schedule = await prisma.schedule.findFirst({ + where: { userId, id: data.scheduleId }, + }); + if (!schedule) + throw new HttpError({ statusCode: 401, message: "You can't add availabilities to this schedule" }); +} + +export default defaultResponder(postHandler); diff --git a/pages/api/availabilities/index.ts b/pages/api/availabilities/index.ts index e3bc338f9f..c07846423f 100644 --- a/pages/api/availabilities/index.ts +++ b/pages/api/availabilities/index.ts @@ -1,140 +1,10 @@ -import type { NextApiRequest, NextApiResponse } from "next"; +import { defaultHandler } from "@calcom/lib/server"; -import safeParseJSON from "@lib/helpers/safeParseJSON"; import { withMiddleware } from "@lib/helpers/withMiddleware"; -import { AvailabilityResponse, AvailabilitiesResponse } from "@lib/types"; -import { - schemaAvailabilityCreateBodyParams, - schemaAvailabilityReadPublic, - schemaAvailabilityReadBodyParams, -} from "@lib/validations/availability"; -async function createOrlistAllAvailabilities( - { method, body, userId, isAdmin, prisma }: NextApiRequest, - res: NextApiResponse -) { - body = safeParseJSON(body); - if (body.success !== undefined && !body.success) { - res.status(400).json({ message: body.message }); - return; - } - - const safe = schemaAvailabilityReadBodyParams.safeParse(body); - - if (!safe.success) { - return res.status(400).json({ message: "Bad request" }); - } - - const safeBody = safe.data; - - if (safeBody.userId && !isAdmin) { - res.status(401).json({ message: "Unauthorized" }); - return; - } else { - if (method === "GET") { - /** - * @swagger - * /availabilities: - * get: - * operationId: listAvailabilities - * summary: Find all availabilities - * tags: - * - availabilities - * externalDocs: - * url: https://docs.cal.com/availability - * responses: - * 200: - * description: OK - * 401: - * description: Authorization information is missing or invalid. - * 404: - * description: No availabilities were found - */ - // const data = await prisma.availability.findMany({ where: { userId } }); - - const userIds = Array.isArray(safeBody.userId) ? safeBody.userId : [safeBody.userId || userId]; - - const schedules = await prisma.schedule.findMany({ - where: { - userId: { in: userIds }, - availability: { some: {} }, - }, - select: { - availability: true, - userId: true, - }, - ...(Array.isArray(body.userId) && { orderBy: { userId: "asc" } }), - }); - - const availabilities = schedules.flatMap((schedule) => { - return { ...schedule.availability[0], userId: schedule.userId }; - }); - - if (availabilities) res.status(200).json({ availabilities }); - else - (error: Error) => - res.status(404).json({ - message: "No Availabilities were found", - error, - }); - } else if (method === "POST") { - /** - * @swagger - * /availabilities: - * post: - * operationId: addAvailability - * summary: Creates a new availability - * requestBody: - * description: Edit an existing availability related to one of your bookings - * required: true - * content: - * application/json: - * schema: - * type: object - * required: - * - startTime - * - endTime - * properties: - * days: - * type: array - * example: email@example.com - * startTime: - * type: string - * example: 1970-01-01T17:00:00.000Z - * endTime: - * type: string - * example: 1970-01-01T17:00:00.000Z - * tags: - * - availabilities - * externalDocs: - * url: https://docs.cal.com/availability - * responses: - * 201: - * description: OK, availability created - * 400: - * description: Bad request. Availability body is invalid. - * 401: - * description: Authorization information is missing or invalid. - */ - const safe = schemaAvailabilityCreateBodyParams.safeParse(body); - if (!safe.success) { - res.status(400).json({ message: "Your request is invalid", error: safe.error }); - return; - } - // FIXME: check for eventTypeId ad scheduleId ownership if passed - - const data = await prisma.availability.create({ data: { ...safe.data, userId } }); - const availability = schemaAvailabilityReadPublic.parse(data); - - if (availability) res.status(201).json({ availability, message: "Availability created successfully" }); - else - (error: Error) => - res.status(400).json({ - message: "Could not create new availability", - error, - }); - } else res.status(405).json({ message: `Method ${method} not allowed` }); - } -} - -export default withMiddleware("HTTP_GET_OR_POST")(createOrlistAllAvailabilities); +export default withMiddleware("HTTP_GET_OR_POST")( + defaultHandler({ + GET: import("./_get"), + POST: import("./_post"), + }) +);