From da618415254dbe35165559a53a7684dc9f135306 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Omar=20L=C3=B3pez?= Date: Tue, 11 Oct 2022 08:25:57 -0600 Subject: [PATCH] Refactors booking references endpoints (#180) refs #175 --- lib/validations/booking-reference.ts | 33 +--- pages/api/booking-references/[id].ts | 181 ------------------ .../[id]/_auth-middleware.ts | 19 ++ pages/api/booking-references/[id]/_delete.ts | 37 ++++ pages/api/booking-references/[id]/_get.ts | 38 ++++ pages/api/booking-references/[id]/_patch.ts | 69 +++++++ pages/api/booking-references/[id]/index.ts | 18 ++ pages/api/booking-references/_get.ts | 31 +++ pages/api/booking-references/_post.ts | 80 ++++++++ pages/api/booking-references/index.ts | 125 +----------- 10 files changed, 303 insertions(+), 328 deletions(-) delete mode 100644 pages/api/booking-references/[id].ts create mode 100644 pages/api/booking-references/[id]/_auth-middleware.ts create mode 100644 pages/api/booking-references/[id]/_delete.ts create mode 100644 pages/api/booking-references/[id]/_get.ts create mode 100644 pages/api/booking-references/[id]/_patch.ts create mode 100644 pages/api/booking-references/[id]/index.ts create mode 100644 pages/api/booking-references/_get.ts create mode 100644 pages/api/booking-references/_post.ts diff --git a/lib/validations/booking-reference.ts b/lib/validations/booking-reference.ts index e6f90a0450..662285b690 100644 --- a/lib/validations/booking-reference.ts +++ b/lib/validations/booking-reference.ts @@ -1,6 +1,5 @@ -import { z } from "zod"; - import { _BookingReferenceModel as BookingReference } from "@calcom/prisma/zod"; +import { denullishShape } from "@calcom/prisma/zod-utils"; export const schemaBookingReferenceBaseBodyParams = BookingReference.pick({ type: true, @@ -23,31 +22,7 @@ export const schemaBookingReferenceReadPublic = BookingReference.pick({ deleted: true, }); -const schemaBookingReferenceCreateParams = z - .object({ - type: z.string(), - uid: z.string(), - meetingId: z.string(), - meetingPassword: z.string().optional(), - meetingUrl: z.string().optional(), - deleted: z.boolean(), - }) +export const schemaBookingCreateBodyParams = BookingReference.omit({ id: true, bookingId: true }) + .merge(denullishShape(BookingReference.pick({ bookingId: true }))) .strict(); - -const schemaBookingReferenceEditParams = z - .object({ - type: z.string().optional(), - uid: z.string().optional(), - meetingId: z.string().optional(), - meetingPassword: z.string().optional(), - meetingUrl: z.string().optional(), - deleted: z.boolean().optional(), - }) - .strict(); - -export const schemaBookingCreateBodyParams = schemaBookingReferenceBaseBodyParams.merge( - schemaBookingReferenceCreateParams -); -export const schemaBookingEditBodyParams = schemaBookingReferenceBaseBodyParams.merge( - schemaBookingReferenceEditParams -); +export const schemaBookingEditBodyParams = schemaBookingCreateBodyParams.partial(); diff --git a/pages/api/booking-references/[id].ts b/pages/api/booking-references/[id].ts deleted file mode 100644 index 17e444d717..0000000000 --- a/pages/api/booking-references/[id].ts +++ /dev/null @@ -1,181 +0,0 @@ -import type { NextApiRequest, NextApiResponse } from "next"; - -import { withMiddleware } from "@lib/helpers/withMiddleware"; -import type { BookingReferenceResponse } from "@lib/types"; -import { - schemaBookingEditBodyParams, - schemaBookingReferenceReadPublic, -} from "@lib/validations/booking-reference"; -import { - schemaQueryIdParseInt, - withValidQueryIdTransformParseInt, -} from "@lib/validations/shared/queryIdTransformParseInt"; - -export async function bookingReferenceById( - { method, query, body, userId, prisma }: NextApiRequest, - res: NextApiResponse -) { - const safeQuery = schemaQueryIdParseInt.safeParse(query); - if (!safeQuery.success) { - res.status(400).json({ message: "Your query was invalid" }); - return; - } - const userWithBookings = await prisma.user.findUnique({ - where: { id: userId }, - include: { bookings: true }, - }); - if (!userWithBookings) throw new Error("User not found"); - const userBookingIds = userWithBookings.bookings.map((booking: { id: number }) => booking.id).flat(); - const bookingReferences = await prisma.bookingReference - .findMany({ where: { id: { in: userBookingIds } } }) - .then((bookingReferences) => bookingReferences.map((bookingReference) => bookingReference.id)); - - if (!bookingReferences?.includes(safeQuery.data.id)) res.status(401).json({ message: "Unauthorized" }); - else { - switch (method) { - case "GET": - /** - * @swagger - * /booking-references/{id}: - * get: - * operationId: getBookingReferenceById - * summary: Find a booking reference - * parameters: - * - in: path - * name: id - * schema: - * type: integer - * required: true - * description: ID of the booking reference to get - * tags: - * - booking-references - * responses: - * 200: - * description: OK - * 401: - * description: Authorization information is missing or invalid. - * 404: - * description: BookingReference was not found - */ - await prisma.bookingReference - .findFirst({ where: { id: safeQuery.data.id } }) - .then((data) => schemaBookingReferenceReadPublic.parse(data)) - .then((booking_reference) => res.status(200).json({ booking_reference })) - .catch((error: Error) => { - res.status(404).json({ - message: `BookingReference with id: ${safeQuery.data.id} not found`, - error, - }); - }); - - break; - case "PATCH": - /** - * @swagger - * /booking-references/{id}: - * patch: - * operationId: editBookingReferenceById - * summary: Edit an existing booking reference - * requestBody: - * description: Edit an existing booking reference 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 booking reference to edit - * tags: - * - booking-references - * responses: - * 201: - * description: OK, safeBody.data edited successfuly - * 400: - * description: Bad request. BookingReference body is invalid. - * 401: - * description: Authorization information is missing or invalid. - */ - - const safeBody = schemaBookingEditBodyParams.safeParse(body); - if (!safeBody.success) { - console.log(safeBody.error); - res.status(400).json({ message: "Invalid request body", error: safeBody.error }); - return; - } - await prisma.bookingReference - .update({ where: { id: safeQuery.data.id }, data: safeBody.data }) - .then((data) => schemaBookingReferenceReadPublic.parse(data)) - .then((booking_reference) => res.status(200).json({ booking_reference })) - .catch((error: Error) => - res.status(404).json({ - message: `BookingReference with id: ${safeQuery.data.id} not found`, - error, - }) - ); - break; - case "DELETE": - /** - * @swagger - * /booking-references/{id}: - * delete: - * operationId: removeBookingReferenceById - * summary: Remove an existing booking reference - * parameters: - * - in: path - * name: id - * schema: - * type: integer - * required: true - * description: ID of the booking reference to delete - * tags: - * - booking-references - * responses: - * 201: - * description: OK, bookingReference removed successfuly - * 400: - * description: Bad request. BookingReference id is invalid. - * 401: - * description: Authorization information is missing or invalid. - */ - await prisma.bookingReference - .delete({ - where: { id: safeQuery.data.id }, - }) - .then(() => - res.status(200).json({ - message: `BookingReference with id: ${safeQuery.data.id} deleted`, - }) - ) - .catch((error: Error) => - res.status(404).json({ - message: `BookingReference 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(bookingReferenceById) -); diff --git a/pages/api/booking-references/[id]/_auth-middleware.ts b/pages/api/booking-references/[id]/_auth-middleware.ts new file mode 100644 index 0000000000..f8a83b3bd2 --- /dev/null +++ b/pages/api/booking-references/[id]/_auth-middleware.ts @@ -0,0 +1,19 @@ +import type { NextApiRequest } from "next"; + +import { HttpError } from "@calcom/lib/http-error"; + +import { schemaQueryIdParseInt } from "@lib/validations/shared/queryIdTransformParseInt"; + +async function authMiddleware(req: NextApiRequest) { + const { userId, isAdmin, prisma } = req; + const { id } = schemaQueryIdParseInt.parse(req.query); + // Here we make sure to only return references of the user's own bookings if the user is not an admin. + if (isAdmin) return; + // Find all references where the user has bookings + const bookingReference = await prisma.bookingReference.findFirst({ + where: { id, booking: { userId } }, + }); + if (!bookingReference) throw new HttpError({ statusCode: 401, message: "Unauthorized" }); +} + +export default authMiddleware; diff --git a/pages/api/booking-references/[id]/_delete.ts b/pages/api/booking-references/[id]/_delete.ts new file mode 100644 index 0000000000..7bc90b18b3 --- /dev/null +++ b/pages/api/booking-references/[id]/_delete.ts @@ -0,0 +1,37 @@ +import type { NextApiRequest } from "next"; + +import { defaultResponder } from "@calcom/lib/server"; + +import { schemaQueryIdParseInt } from "@lib/validations/shared/queryIdTransformParseInt"; + +/** + * @swagger + * /booking-references/{id}: + * delete: + * operationId: removeBookingReferenceById + * summary: Remove an existing booking reference + * parameters: + * - in: path + * name: id + * schema: + * type: integer + * required: true + * description: ID of the booking reference to delete + * tags: + * - booking-references + * responses: + * 201: + * description: OK, bookingReference removed successfully + * 400: + * description: Bad request. BookingReference 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.bookingReference.delete({ where: { id } }); + return { message: `BookingReference with id: ${id} deleted` }; +} + +export default defaultResponder(deleteHandler); diff --git a/pages/api/booking-references/[id]/_get.ts b/pages/api/booking-references/[id]/_get.ts new file mode 100644 index 0000000000..c78b40222a --- /dev/null +++ b/pages/api/booking-references/[id]/_get.ts @@ -0,0 +1,38 @@ +import type { NextApiRequest } from "next"; + +import { defaultResponder } from "@calcom/lib/server"; + +import { schemaBookingReferenceReadPublic } from "@lib/validations/booking-reference"; +import { schemaQueryIdParseInt } from "@lib/validations/shared/queryIdTransformParseInt"; + +/** + * @swagger + * /booking-references/{id}: + * get: + * operationId: getBookingReferenceById + * summary: Find a booking reference + * parameters: + * - in: path + * name: id + * schema: + * type: integer + * required: true + * description: ID of the booking reference to get + * tags: + * - booking-references + * responses: + * 200: + * description: OK + * 401: + * description: Authorization information is missing or invalid. + * 404: + * description: BookingReference was not found + */ +export async function getHandler(req: NextApiRequest) { + const { prisma, query } = req; + const { id } = schemaQueryIdParseInt.parse(query); + const booking_reference = await prisma.bookingReference.findUniqueOrThrow({ where: { id } }); + return { booking_reference: schemaBookingReferenceReadPublic.parse(booking_reference) }; +} + +export default defaultResponder(getHandler); diff --git a/pages/api/booking-references/[id]/_patch.ts b/pages/api/booking-references/[id]/_patch.ts new file mode 100644 index 0000000000..ccaa22000d --- /dev/null +++ b/pages/api/booking-references/[id]/_patch.ts @@ -0,0 +1,69 @@ +import type { Prisma } from "@prisma/client"; +import type { NextApiRequest } from "next"; + +import { defaultResponder } from "@calcom/lib/server"; + +import { + schemaBookingEditBodyParams, + schemaBookingReferenceReadPublic, +} from "@lib/validations/booking-reference"; +import { schemaQueryIdParseInt } from "@lib/validations/shared/queryIdTransformParseInt"; + +/** + * @swagger + * /booking-references/{id}: + * patch: + * operationId: editBookingReferenceById + * summary: Edit an existing booking reference + * requestBody: + * description: Edit an existing booking reference 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 booking reference to edit + * tags: + * - booking-references + * responses: + * 201: + * description: OK, safeBody.data edited successfully + * 400: + * description: Bad request. BookingReference body is invalid. + * 401: + * description: Authorization information is missing or invalid. + */ +export async function patchHandler(req: NextApiRequest) { + const { prisma, query, body, isAdmin, userId } = req; + const { id } = schemaQueryIdParseInt.parse(query); + const data = schemaBookingEditBodyParams.parse(body); + /* If user tries to update bookingId, we run extra checks */ + if (data.bookingId) { + const args: Prisma.BookingFindFirstOrThrowArgs = isAdmin + ? /* If admin, we only check that the booking exists */ + { where: { id: data.bookingId } } + : /* For non-admins we make sure the booking belongs to the user */ + { where: { id: data.bookingId, userId } }; + await prisma.booking.findFirstOrThrow(args); + } + const booking_reference = await prisma.bookingReference.update({ where: { id }, data }); + return { booking_reference: schemaBookingReferenceReadPublic.parse(booking_reference) }; +} + +export default defaultResponder(patchHandler); diff --git a/pages/api/booking-references/[id]/index.ts b/pages/api/booking-references/[id]/index.ts new file mode 100644 index 0000000000..7d92ee2906 --- /dev/null +++ b/pages/api/booking-references/[id]/index.ts @@ -0,0 +1,18 @@ +import { NextApiRequest, NextApiResponse } from "next"; + +import { defaultHandler, defaultResponder } from "@calcom/lib/server"; + +import { withMiddleware } from "@lib/helpers/withMiddleware"; + +import authMiddleware from "./_auth-middleware"; + +export default withMiddleware("HTTP_GET_DELETE_PATCH")( + defaultResponder(async (req: NextApiRequest, res: NextApiResponse) => { + await authMiddleware(req); + return defaultHandler({ + GET: import("./_get"), + PATCH: import("./_patch"), + DELETE: import("./_delete"), + })(req, res); + }) +); diff --git a/pages/api/booking-references/_get.ts b/pages/api/booking-references/_get.ts new file mode 100644 index 0000000000..0bf3b93b4a --- /dev/null +++ b/pages/api/booking-references/_get.ts @@ -0,0 +1,31 @@ +import type { Prisma } from "@prisma/client"; +import type { NextApiRequest } from "next"; + +import { defaultResponder } from "@calcom/lib/server"; + +import { schemaBookingReferenceReadPublic } from "@lib/validations/booking-reference"; + +/** + * @swagger + * /booking-references: + * get: + * operationId: listBookingReferences + * summary: Find all booking references + * tags: + * - booking-references + * responses: + * 200: + * description: OK + * 401: + * description: Authorization information is missing or invalid. + * 404: + * description: No booking references were found + */ +async function getHandler(req: NextApiRequest) { + const { userId, isAdmin, prisma } = req; + const args: Prisma.BookingReferenceFindManyArgs = isAdmin ? {} : { where: { booking: { userId } } }; + const data = await prisma.bookingReference.findMany(args); + return { booking_references: data.map((br) => schemaBookingReferenceReadPublic.parse(br)) }; +} + +export default defaultResponder(getHandler); diff --git a/pages/api/booking-references/_post.ts b/pages/api/booking-references/_post.ts new file mode 100644 index 0000000000..b5ed53b79d --- /dev/null +++ b/pages/api/booking-references/_post.ts @@ -0,0 +1,80 @@ +import type { Prisma } from "@prisma/client"; +import type { NextApiRequest } from "next"; + +import { HttpError } from "@calcom/lib/http-error"; +import { defaultResponder } from "@calcom/lib/server"; + +import { + schemaBookingCreateBodyParams, + schemaBookingReferenceReadPublic, +} from "@lib/validations/booking-reference"; + +/** + * @swagger + * /booking-references: + * post: + * operationId: addBookingReference + * summary: Creates a new booking reference + * requestBody: + * description: Create a new booking reference related to one of your bookings + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - type + * - uid + * - meetingId + * - bookingId + * - deleted + * properties: + * deleted: + * type: boolean + * example: false + * uid: + * type: string + * example: '123456789' + * type: + * type: string + * example: email@example.com + * bookingId: + * type: number + * example: 1 + * meetingId: + * type: string + * example: 'meeting-id' + * tags: + * - booking-references + * responses: + * 201: + * description: OK, booking reference created + * 400: + * description: Bad request. BookingReference body is invalid. + * 401: + * description: Authorization information is missing or invalid. + */ +async function postHandler(req: NextApiRequest) { + const { userId, isAdmin, prisma } = req; + const body = schemaBookingCreateBodyParams.parse(req.body); + const args: Prisma.BookingFindFirstOrThrowArgs = isAdmin + ? /* If admin, we only check that the booking exists */ + { where: { id: body.bookingId } } + : /* For non-admins we make sure the booking belongs to the user */ + { where: { id: body.bookingId, userId } }; + await prisma.booking.findFirstOrThrow(args); + + const data = await prisma.bookingReference.create({ + data: { + ...body, + bookingId: body.bookingId, + }, + }); + + return { + booking_reference: schemaBookingReferenceReadPublic.parse(data), + message: "Booking reference created successfully", + }; +} + +export default defaultResponder(postHandler); diff --git a/pages/api/booking-references/index.ts b/pages/api/booking-references/index.ts index 5c63a605b7..c07846423f 100644 --- a/pages/api/booking-references/index.ts +++ b/pages/api/booking-references/index.ts @@ -1,121 +1,10 @@ -import type { NextApiRequest, NextApiResponse } from "next"; +import { defaultHandler } from "@calcom/lib/server"; import { withMiddleware } from "@lib/helpers/withMiddleware"; -import { BookingReferenceResponse, BookingReferencesResponse } from "@lib/types"; -import { - schemaBookingCreateBodyParams, - schemaBookingReferenceReadPublic, -} from "@lib/validations/booking-reference"; -async function createOrlistAllBookingReferences( - { method, userId, body, prisma }: NextApiRequest, - res: NextApiResponse -) { - const userWithBookings = await prisma.user.findUnique({ - where: { id: userId }, - include: { bookings: true }, - }); - if (!userWithBookings) throw new Error("User not found"); - const userBookingIds = userWithBookings.bookings.map((booking: { id: number }) => booking.id).flat(); - if (method === "GET") { - /** - * @swagger - * /booking-references: - * get: - * operationId: listBookingReferences - * summary: Find all booking references - * tags: - * - booking-references - * responses: - * 200: - * description: OK - * 401: - * description: Authorization information is missing or invalid. - * 404: - * description: No booking references were found - */ - const data = await prisma.bookingReference.findMany({ where: { id: { in: userBookingIds } } }); - const booking_references = data.map((bookingReference) => - schemaBookingReferenceReadPublic.parse(bookingReference) - ); - if (booking_references) res.status(200).json({ booking_references }); - else - (error: Error) => - res.status(404).json({ - message: "No BookingReferences were found", - error, - }); - } else if (method === "POST") { - /** - * @swagger - * /booking-references: - * post: - * operationId: addBookingReference - * summary: Creates a new booking reference - * requestBody: - * description: Create a new booking reference related to one of your bookings - * required: true - * content: - * application/json: - * schema: - * type: object - * required: - * - type - * - uid - * - meetindId - * - bookingId - * - deleted - * properties: - * deleted: - * type: boolean - * example: false - * uid: - * type: string - * example: '123456789' - * type: - * type: string - * example: email@example.com - * bookingId: - * type: number - * example: 1 - * meetingId: - * type: string - * example: 'meeting-id' - * tags: - * - booking-references - * responses: - * 201: - * description: OK, booking reference created - * 400: - * description: Bad request. BookingReference body is invalid. - * 401: - * description: Authorization information is missing or invalid. - */ - const safe = schemaBookingCreateBodyParams.safeParse(body); - if (!safe.success) { - res.status(400).json({ message: "Bad request. BookingReference body is invalid", error: safe.error }); - return; - } - if (!safe.data.bookingId) throw new Error("BookingReference: bookingId not found"); - if (!userBookingIds.includes(safe.data.bookingId)) res.status(401).json({ message: "Unauthorized" }); - else { - const booking_reference = await prisma.bookingReference.create({ - data: { ...safe.data }, - }); - if (booking_reference) { - res.status(201).json({ - booking_reference, - message: "BookingReference created successfully", - }); - } else { - (error: Error) => - res.status(400).json({ - message: "Could not create new booking reference", - error, - }); - } - } - } else res.status(405).json({ message: `Method ${method} not allowed` }); -} - -export default withMiddleware("HTTP_GET_OR_POST")(createOrlistAllBookingReferences); +export default withMiddleware("HTTP_GET_OR_POST")( + defaultHandler({ + GET: import("./_get"), + POST: import("./_post"), + }) +);