From f66ed50ecb307e2a60b41b88aa9211f3ce5d599e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Omar=20L=C3=B3pez?= Date: Thu, 20 Oct 2022 11:35:02 -0600 Subject: [PATCH] Selected Calendars endpoints refactor (#193) refs #175 --- lib/validations/selected-calendar.ts | 57 +++-- package.json | 2 +- pages/api/selected-calendars/[id].ts | 210 ------------------ .../[id]/_auth-middleware.ts | 16 ++ pages/api/selected-calendars/[id]/_delete.ts | 49 ++++ pages/api/selected-calendars/[id]/_get.ts | 51 +++++ pages/api/selected-calendars/[id]/_patch.ts | 66 ++++++ pages/api/selected-calendars/[id]/index.ts | 18 ++ pages/api/selected-calendars/_get.ts | 44 ++++ pages/api/selected-calendars/_post.ts | 60 +++++ pages/api/selected-calendars/index.ts | 96 +------- 11 files changed, 352 insertions(+), 317 deletions(-) delete mode 100644 pages/api/selected-calendars/[id].ts create mode 100644 pages/api/selected-calendars/[id]/_auth-middleware.ts create mode 100644 pages/api/selected-calendars/[id]/_delete.ts create mode 100644 pages/api/selected-calendars/[id]/_get.ts create mode 100644 pages/api/selected-calendars/[id]/_patch.ts create mode 100644 pages/api/selected-calendars/[id]/index.ts create mode 100644 pages/api/selected-calendars/_get.ts create mode 100644 pages/api/selected-calendars/_post.ts diff --git a/lib/validations/selected-calendar.ts b/lib/validations/selected-calendar.ts index ffdc54b96a..e1b2138e51 100644 --- a/lib/validations/selected-calendar.ts +++ b/lib/validations/selected-calendar.ts @@ -1,25 +1,48 @@ -import { z } from "zod"; +import z from "zod"; import { _SelectedCalendarModel as SelectedCalendar } from "@calcom/prisma/zod"; -export const schemaSelectedCalendarBaseBodyParams = SelectedCalendar.omit({ userId: true }).partial(); +import { schemaQueryIdAsString } from "./shared/queryIdString"; +import { schemaQueryIdParseInt } from "./shared/queryIdTransformParseInt"; + +export const schemaSelectedCalendarBaseBodyParams = SelectedCalendar; export const schemaSelectedCalendarPublic = SelectedCalendar.omit({}); -const schemaSelectedCalendarRequiredParams = z.object({ - externalId: z.string(), - integration: z.string(), - user: z.object({ - connect: z.object({ - id: z.number().optional(), - username: z.string().optional(), - email: z.string().optional(), - }), - // FIXME: Provide valid UserModel schema here, but not sure how yet. - create: z.any(), - }), +export const schemaSelectedCalendarBodyParams = schemaSelectedCalendarBaseBodyParams.partial({ + userId: true, }); -export const schemaSelectedCalendarBodyParams = schemaSelectedCalendarBaseBodyParams.merge( - schemaSelectedCalendarRequiredParams -); +export const schemaSelectedCalendarUpdateBodyParams = schemaSelectedCalendarBaseBodyParams.partial(); + +export const selectedCalendarIdSchema = schemaQueryIdAsString.transform((v, ctx) => { + /** We can assume the first part is the userId since it's an integer */ + const [userIdStr, ...rest] = v.id.split("_"); + /** We can assume that the remainder is both the integration type and external id combined */ + const integration_externalId = rest.join("_"); + /** + * Since we only handle calendars here we can split by `_calendar_` and re add it later on. + * This handle special cases like `google_calendar_c_blabla@group.calendar.google.com` and + * `hubspot_other_calendar`. + **/ + const [_integration, externalId] = integration_externalId.split("_calendar_"); + const userIdInt = schemaQueryIdParseInt.safeParse({ id: userIdStr }); + if (!userIdInt.success) { + ctx.addIssue({ code: z.ZodIssueCode.custom, message: "userId is not a number" }); + return z.NEVER; + } + if (!_integration) { + ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Missing integration" }); + return z.NEVER; + } + if (!externalId) { + ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Missing externalId" }); + return z.NEVER; + } + return { + userId: userIdInt.data.id, + /** We re-add the split `_calendar` string */ + integration: `${_integration}_calendar`, + externalId, + }; +}); diff --git a/package.json b/package.json index 0d6430d98c..1484a4d8e8 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,6 @@ "typescript": "^4.7.4", "tzdata": "^1.0.30", "uuid": "^8.3.2", - "zod": "^3.18.0" + "zod": "^3.19.1" } } diff --git a/pages/api/selected-calendars/[id].ts b/pages/api/selected-calendars/[id].ts deleted file mode 100644 index ea2037fba3..0000000000 --- a/pages/api/selected-calendars/[id].ts +++ /dev/null @@ -1,210 +0,0 @@ -import type { NextApiRequest, NextApiResponse } from "next"; - -import { withMiddleware } from "@lib/helpers/withMiddleware"; -import type { SelectedCalendarResponse } from "@lib/types"; -import { - schemaSelectedCalendarBodyParams, - schemaSelectedCalendarPublic, -} from "@lib/validations/selected-calendar"; -import { schemaQueryIdAsString, withValidQueryIdString } from "@lib/validations/shared/queryIdString"; - -export async function selectedCalendarById( - { method, query, body, userId, prisma }: NextApiRequest, - res: NextApiResponse -) { - const safeQuery = schemaQueryIdAsString.safeParse(query); - const safeBody = schemaSelectedCalendarBodyParams.safeParse(body); - if (!safeQuery.success) { - res.status(400).json({ message: "Your query was invalid" }); - return; - } - // This is how we set the userId and externalId in the query for managing compoundId. - const [paramUserId, integration, externalId] = safeQuery.data.id.split("_"); - if (userId !== parseInt(paramUserId)) res.status(401).json({ message: "Unauthorized" }); - else { - switch (method) { - /** - * @swagger - * /selected-calendars/{userId}_{integration}_{externalId}: - * get: - * operationId: getSelectedCalendarById - * summary: Find a selected calendar by providing the compoundId(userId_integration_externalId) separated by `_` - * parameters: - * - in: path - * name: userId - * schema: - * type: integer - * required: true - * description: userId of the selected calendar to get - * - in: path - * name: externalId - * schema: - * type: string - * required: true - * description: externalId of the selected calendar to get - * - in: path - * name: integration - * schema: - * type: string - * required: true - * description: integration of the selected calendar to get - * tags: - * - selected-calendars - * responses: - * 200: - * description: OK - * 401: - * description: Authorization information is missing or invalid. - * 404: - * description: SelectedCalendar was not found - */ - case "GET": - await prisma.selectedCalendar - .findUnique({ - where: { - userId_integration_externalId: { - userId: userId, - integration: integration, - externalId: externalId, - }, - }, - }) - .then((selectedCalendar) => schemaSelectedCalendarPublic.parse(selectedCalendar)) - .then((selected_calendar) => res.status(200).json({ selected_calendar })) - .catch((error: Error) => - res.status(404).json({ - message: `SelectedCalendar with id: ${safeQuery.data.id} not found`, - error, - }) - ); - break; - - /** - * @swagger - * /selected-calendars/{userId}_{integration}_{externalId}: - * patch: - * operationId: editSelectedCalendarById - * summary: Edit a selected calendar - * parameters: - * - in: path - * name: userId - * schema: - * type: integer - * required: true - * description: userId of the selected calendar to get - * - in: path - * name: externalId - * schema: - * type: string - * required: true - * description: externalId of the selected calendar to get - * - in: path - * name: integration - * schema: - * type: string - * required: true - * description: integration of the selected calendar to get - * tags: - * - selected-calendars - * responses: - * 201: - * description: OK, selected-calendar edited successfuly - * 400: - * description: Bad request. SelectedCalendar body is invalid. - * 401: - * description: Authorization information is missing or invalid. - */ - case "PATCH": - if (!safeBody.success) { - { - res.status(400).json({ message: "Invalid request body" }); - return; - } - } - await prisma.selectedCalendar - .update({ - where: { - userId_integration_externalId: { - userId: userId, - integration: integration, - externalId: externalId, - }, - }, - data: safeBody.data, - }) - .then((selectedCalendar) => schemaSelectedCalendarPublic.parse(selectedCalendar)) - .then((selected_calendar) => res.status(200).json({ selected_calendar })) - .catch((error: Error) => - res.status(404).json({ - message: `SelectedCalendar with id: ${safeQuery.data.id} not found`, - error, - }) - ); - break; - /** - * @swagger - * /selected-calendars/{userId}_{integration}_{externalId}: - * delete: - * operationId: removeSelectedCalendarById - * summary: Remove a selected calendar - * parameters: - * - in: path - * name: userId - * schema: - * type: integer - * required: true - * description: userId of the selected calendar to get - * - in: path - * name: externalId - * schema: - * type: integer - * required: true - * description: externalId of the selected-calendar to get - * - in: path - * name: integration - * schema: - * type: string - * required: true - * description: integration of the selected calendar to get - * tags: - * - selected-calendars - * responses: - * 201: - * description: OK, selected-calendar removed successfuly - * 400: - * description: Bad request. SelectedCalendar id is invalid. - * 401: - * description: Authorization information is missing or invalid. - */ - case "DELETE": - await prisma.selectedCalendar - .delete({ - where: { - userId_integration_externalId: { - userId: userId, - integration: integration, - externalId: externalId, - }, - }, - }) - .then(() => - res.status(200).json({ - message: `SelectedCalendar with id: ${safeQuery.data.id} deleted successfully`, - }) - ) - .catch((error: Error) => - res.status(404).json({ - message: `SelectedCalendar 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")(withValidQueryIdString(selectedCalendarById)); diff --git a/pages/api/selected-calendars/[id]/_auth-middleware.ts b/pages/api/selected-calendars/[id]/_auth-middleware.ts new file mode 100644 index 0000000000..1a9c74f305 --- /dev/null +++ b/pages/api/selected-calendars/[id]/_auth-middleware.ts @@ -0,0 +1,16 @@ +import type { NextApiRequest } from "next"; + +import { HttpError } from "@calcom/lib/http-error"; + +import { selectedCalendarIdSchema } from "@lib/validations/selected-calendar"; + +async function authMiddleware(req: NextApiRequest) { + const { userId, isAdmin } = req; + const { userId: queryUserId } = selectedCalendarIdSchema.parse(req.query); + // Admins can just skip this check + if (isAdmin) return; + // Check if the current user requesting is the same as the one being requested + if (userId !== queryUserId) throw new HttpError({ statusCode: 403, message: "Forbidden" }); +} + +export default authMiddleware; diff --git a/pages/api/selected-calendars/[id]/_delete.ts b/pages/api/selected-calendars/[id]/_delete.ts new file mode 100644 index 0000000000..c187adebbf --- /dev/null +++ b/pages/api/selected-calendars/[id]/_delete.ts @@ -0,0 +1,49 @@ +import type { NextApiRequest } from "next"; + +import { defaultResponder } from "@calcom/lib/server"; + +import { selectedCalendarIdSchema } from "@lib/validations/selected-calendar"; + +/** + * @swagger + * /selected-calendars/{userId}_{integration}_{externalId}: + * delete: + * operationId: removeSelectedCalendarById + * summary: Remove a selected calendar + * parameters: + * - in: path + * name: userId + * schema: + * type: integer + * required: true + * description: userId of the selected calendar to get + * - in: path + * name: externalId + * schema: + * type: integer + * required: true + * description: externalId of the selected-calendar to get + * - in: path + * name: integration + * schema: + * type: string + * required: true + * description: integration of the selected calendar to get + * tags: + * - selected-calendars + * responses: + * 201: + * description: OK, selected-calendar removed successfully + * 400: + * description: Bad request. SelectedCalendar id is invalid. + * 401: + * description: Authorization information is missing or invalid. + */ +export async function deleteHandler(req: NextApiRequest) { + const { prisma, query } = req; + const userId_integration_externalId = selectedCalendarIdSchema.parse(query); + await prisma.selectedCalendar.delete({ where: { userId_integration_externalId } }); + return { message: `Selected Calendar with id: ${query.id} deleted successfully` }; +} + +export default defaultResponder(deleteHandler); diff --git a/pages/api/selected-calendars/[id]/_get.ts b/pages/api/selected-calendars/[id]/_get.ts new file mode 100644 index 0000000000..7dd8d9e055 --- /dev/null +++ b/pages/api/selected-calendars/[id]/_get.ts @@ -0,0 +1,51 @@ +import type { NextApiRequest } from "next"; + +import { defaultResponder } from "@calcom/lib/server"; + +import { schemaSelectedCalendarPublic, selectedCalendarIdSchema } from "@lib/validations/selected-calendar"; + +/** + * @swagger + * /selected-calendars/{userId}_{integration}_{externalId}: + * get: + * operationId: getSelectedCalendarById + * summary: Find a selected calendar by providing the compoundId(userId_integration_externalId) separated by `_` + * parameters: + * - in: path + * name: userId + * schema: + * type: integer + * required: true + * description: userId of the selected calendar to get + * - in: path + * name: externalId + * schema: + * type: string + * required: true + * description: externalId of the selected calendar to get + * - in: path + * name: integration + * schema: + * type: string + * required: true + * description: integration of the selected calendar to get + * tags: + * - selected-calendars + * responses: + * 200: + * description: OK + * 401: + * description: Authorization information is missing or invalid. + * 404: + * description: SelectedCalendar was not found + */ +export async function getHandler(req: NextApiRequest) { + const { prisma, query } = req; + const userId_integration_externalId = selectedCalendarIdSchema.parse(query); + const data = await prisma.selectedCalendar.findUniqueOrThrow({ + where: { userId_integration_externalId }, + }); + return { selected_calendar: schemaSelectedCalendarPublic.parse(data) }; +} + +export default defaultResponder(getHandler); diff --git a/pages/api/selected-calendars/[id]/_patch.ts b/pages/api/selected-calendars/[id]/_patch.ts new file mode 100644 index 0000000000..064bee1dbc --- /dev/null +++ b/pages/api/selected-calendars/[id]/_patch.ts @@ -0,0 +1,66 @@ +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 { + schemaSelectedCalendarPublic, + schemaSelectedCalendarUpdateBodyParams, + selectedCalendarIdSchema, +} from "@lib/validations/selected-calendar"; + +/** + * @swagger + * /selected-calendars/{userId}_{integration}_{externalId}: + * patch: + * operationId: editSelectedCalendarById + * summary: Edit a selected calendar + * parameters: + * - in: path + * name: userId + * schema: + * type: integer + * required: true + * description: userId of the selected calendar to get + * - in: path + * name: externalId + * schema: + * type: string + * required: true + * description: externalId of the selected calendar to get + * - in: path + * name: integration + * schema: + * type: string + * required: true + * description: integration of the selected calendar to get + * tags: + * - selected-calendars + * responses: + * 201: + * description: OK, selected-calendar edited successfully + * 400: + * description: Bad request. SelectedCalendar body is invalid. + * 401: + * description: Authorization information is missing or invalid. + */ +export async function patchHandler(req: NextApiRequest) { + const { prisma, query, userId, isAdmin } = req; + const userId_integration_externalId = selectedCalendarIdSchema.parse(query); + const { userId: bodyUserId, ...data } = schemaSelectedCalendarUpdateBodyParams.parse(req.body); + const args: Prisma.SelectedCalendarUpdateArgs = { where: { userId_integration_externalId }, data }; + + if (!isAdmin && bodyUserId) throw new HttpError({ statusCode: 403, message: `ADMIN required for userId` }); + + if (isAdmin && bodyUserId) { + const where: Prisma.UserWhereInput = { id: userId }; + await prisma.user.findFirstOrThrow({ where }); + args.data.userId = userId; + } + + const result = await prisma.selectedCalendar.update(args); + return { selected_calendar: schemaSelectedCalendarPublic.parse(result) }; +} + +export default defaultResponder(patchHandler); diff --git a/pages/api/selected-calendars/[id]/index.ts b/pages/api/selected-calendars/[id]/index.ts new file mode 100644 index 0000000000..cbff13f006 --- /dev/null +++ b/pages/api/selected-calendars/[id]/index.ts @@ -0,0 +1,18 @@ +import type { 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/selected-calendars/_get.ts b/pages/api/selected-calendars/_get.ts new file mode 100644 index 0000000000..ebbbe19b5c --- /dev/null +++ b/pages/api/selected-calendars/_get.ts @@ -0,0 +1,44 @@ +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 { schemaSelectedCalendarPublic } from "@lib/validations/selected-calendar"; +import { schemaQuerySingleOrMultipleUserIds } from "@lib/validations/shared/queryUserId"; + +/** + * @swagger + * /selected-calendars: + * get: + * operationId: listSelectedCalendars + * summary: Find all selected calendars + * tags: + * - selected-calendars + * responses: + * 200: + * description: OK + * 401: + * description: Authorization information is missing or invalid. + * 404: + * description: No selected calendars were found + */ +async function getHandler(req: NextApiRequest) { + const { userId, isAdmin, prisma } = req; + /* Admin gets all selected calendar by default, otherwise only the user's ones */ + const args: Prisma.SelectedCalendarFindManyArgs = isAdmin ? {} : { where: { userId } }; + + /** Only admins can query other users */ + if (!isAdmin && req.query.userId) throw new HttpError({ statusCode: 403, message: "ADMIN required" }); + if (isAdmin && req.query.userId) { + const query = schemaQuerySingleOrMultipleUserIds.parse(req.query); + const userIds = Array.isArray(query.userId) ? query.userId : [query.userId || userId]; + args.where = { userId: { in: userIds } }; + if (Array.isArray(query.userId)) args.orderBy = { userId: "asc" }; + } + + const data = await prisma.selectedCalendar.findMany(args); + return { selected_calendars: data.map((v) => schemaSelectedCalendarPublic.parse(v)) }; +} + +export default defaultResponder(getHandler); diff --git a/pages/api/selected-calendars/_post.ts b/pages/api/selected-calendars/_post.ts new file mode 100644 index 0000000000..2118c5348f --- /dev/null +++ b/pages/api/selected-calendars/_post.ts @@ -0,0 +1,60 @@ +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 { + schemaSelectedCalendarBodyParams, + schemaSelectedCalendarPublic, +} from "@lib/validations/selected-calendar"; + +/** + * @swagger + * /selected-calendars: + * get: + * operationId: addSelectedCalendars + * summary: Find all selected calendars + * tags: + * - selected-calendars + * responses: + * 200: + * description: OK + * 401: + * description: Authorization information is missing or invalid. + * 404: + * description: No selected calendars were found + * post: + * summary: Creates a new selected calendar + * tags: + * - selected-calendars + * responses: + * 201: + * description: OK, selected calendar created + * 400: + * description: Bad request. SelectedCalendar body is invalid. + * 401: + * description: Authorization information is missing or invalid. + */ +async function postHandler(req: NextApiRequest) { + const { userId, isAdmin, prisma } = req; + const { userId: bodyUserId, ...body } = schemaSelectedCalendarBodyParams.parse(req.body); + const args: Prisma.SelectedCalendarCreateArgs = { data: { ...body, userId } }; + + if (!isAdmin && bodyUserId) throw new HttpError({ statusCode: 403, message: `ADMIN required for userId` }); + + if (isAdmin && bodyUserId) { + const where: Prisma.UserWhereInput = { id: bodyUserId }; + await prisma.user.findFirstOrThrow({ where }); + args.data.userId = userId; + } + + const data = await prisma.selectedCalendar.create(args); + + return { + selected_calendar: schemaSelectedCalendarPublic.parse(data), + message: "Selected Calendar created successfully", + }; +} + +export default defaultResponder(postHandler); diff --git a/pages/api/selected-calendars/index.ts b/pages/api/selected-calendars/index.ts index 3ab69494a8..c07846423f 100644 --- a/pages/api/selected-calendars/index.ts +++ b/pages/api/selected-calendars/index.ts @@ -1,92 +1,10 @@ -import type { NextApiRequest, NextApiResponse } from "next"; +import { defaultHandler } from "@calcom/lib/server"; import { withMiddleware } from "@lib/helpers/withMiddleware"; -import { SelectedCalendarResponse, SelectedCalendarsResponse } from "@lib/types"; -import { - schemaSelectedCalendarBodyParams, - schemaSelectedCalendarPublic, -} from "@lib/validations/selected-calendar"; -async function createOrlistAllSelectedCalendars( - { method, body, userId, prisma }: NextApiRequest, - res: NextApiResponse -) { - if (method === "GET") { - /** - * @swagger - * /selected-calendars: - * get: - * operationId: listSelectedCalendars - * summary: Find all selected calendars - * tags: - * - selected-calendars - * responses: - * 200: - * description: OK - * 401: - * description: Authorization information is missing or invalid. - * 404: - * description: No selected calendars were found - */ - const data = await prisma.selectedCalendar.findMany({ where: { userId } }); - const selected_calendars = data.map((selected_calendar) => - schemaSelectedCalendarPublic.parse(selected_calendar) - ); - if (selected_calendars) res.status(200).json({ selected_calendars }); - else - (error: Error) => - res.status(404).json({ - message: "No SelectedCalendars were found", - error, - }); - } else if (method === "POST") { - /** - * @swagger - * /selected-calendars: - * get: - * operationId: addSelectedCalendars - * summary: Find all selected calendars - * tags: - * - selected-calendars - * responses: - * 200: - * description: OK - * 401: - * description: Authorization information is missing or invalid. - * 404: - * description: No selected calendars were found - * post: - * summary: Creates a new selected calendar - * tags: - * - selected-calendars - * responses: - * 201: - * description: OK, selected calendar created - * 400: - * description: Bad request. SelectedCalendar body is invalid. - * 401: - * description: Authorization information is missing or invalid. - */ - const safe = schemaSelectedCalendarBodyParams.safeParse(body); - if (!safe.success) { - res.status(400).json({ message: "Invalid request body" }); - return; - } - // Create new selectedCalendar connecting it to current userId - const data = await prisma.selectedCalendar.create({ - data: { ...safe.data, user: { connect: { id: userId } } }, - }); - const selected_calendar = schemaSelectedCalendarPublic.parse(data); - - if (selected_calendar) - res.status(201).json({ selected_calendar, message: "SelectedCalendar created successfully" }); - else - (error: Error) => - res.status(400).json({ - message: "Could not create new selected calendar", - error, - }); - } else res.status(405).json({ message: `Method ${method} not allowed` }); -} - -export default withMiddleware("HTTP_GET_OR_POST")(createOrlistAllSelectedCalendars); +export default withMiddleware("HTTP_GET_OR_POST")( + defaultHandler({ + GET: import("./_get"), + POST: import("./_post"), + }) +);