From 49087d858e4d7c7d6166bd8b53895b618ac1da2f Mon Sep 17 00:00:00 2001 From: zomars Date: Tue, 14 Jun 2022 14:07:23 -0600 Subject: [PATCH 1/7] Fixes hot-reload for dev --- next.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/next.config.js b/next.config.js index 5e40353edf..62b15643fa 100644 --- a/next.config.js +++ b/next.config.js @@ -10,7 +10,7 @@ const withTM = require("next-transpile-modules")([ module.exports = withTM({ async rewrites() { return { - beforeFiles: [ + afterFiles: [ // This redirects requests recieved at / the root to the /api/ folder. { source: "/v:version/:rest*", From 4d93b08e4c3e95b7f960c8fffc363aad1d660745 Mon Sep 17 00:00:00 2001 From: zomars Date: Tue, 14 Jun 2022 14:08:19 -0600 Subject: [PATCH 2/7] Scripts fixes --- package.json | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 57da05e5e6..48cf7b6ef9 100644 --- a/package.json +++ b/package.json @@ -8,9 +8,8 @@ "private": true, "scripts": { "build": "next build", - "clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist", - "dev-real": "PORT=3002 next dev", - "dev": "next build && PORT=3002 next start", + "clean": "rm -rf .turbo && rm -rf node_modules && rm -rf .next", + "dev": "PORT=3002 next dev", "lint-fix": "next lint --fix && prettier --write .", "lint": "next lint", "prebuild": "cd ../.. && yarn workspace @calcom/prisma generate-schemas", From 58e1ea9bf66bbf3f0b83ae6b0c614b798db17e73 Mon Sep 17 00:00:00 2001 From: zomars Date: Tue, 14 Jun 2022 14:08:58 -0600 Subject: [PATCH 3/7] User endpoint refactoring --- lib/validations/user.ts | 4 ++- pages/api/users/_get.ts | 37 +++++++++++++++++++++++ pages/api/users/_post.ts | 20 +++++++++++++ pages/api/users/index.ts | 64 +++++----------------------------------- 4 files changed, 67 insertions(+), 58 deletions(-) create mode 100644 pages/api/users/_get.ts create mode 100644 pages/api/users/_post.ts diff --git a/lib/validations/user.ts b/lib/validations/user.ts index 2385224f5a..c780dfbed8 100644 --- a/lib/validations/user.ts +++ b/lib/validations/user.ts @@ -150,4 +150,6 @@ export const schemaUserReadPublic = User.pick({ createdDate: true, verified: true, invitedTo: true, -}).merge(schemaUserEditBodyParams); +}); + +export const schemaUsersReadPublic = z.array(schemaUserReadPublic); diff --git a/pages/api/users/_get.ts b/pages/api/users/_get.ts new file mode 100644 index 0000000000..4c4aadaecd --- /dev/null +++ b/pages/api/users/_get.ts @@ -0,0 +1,37 @@ +import type { NextApiRequest } from "next"; + +import { defaultResponder } from "@calcom/lib/server"; +import prisma from "@calcom/prisma"; + +import { isAdminGuard } from "@lib/utils/isAdmin"; +import { schemaUsersReadPublic } from "@lib/validations/user"; + +import { Prisma } from ".prisma/client"; + +/** + * @swagger + * /users: + * get: + * operationId: listUsers + * summary: Find all users. + * tags: + * - users + * responses: + * 200: + * description: OK + * 401: + * description: Authorization information is missing or invalid. + * 404: + * description: No users were found + */ +async function getHandler({ userId }: NextApiRequest) { + const isAdmin = await isAdminGuard(userId); + const where: Prisma.UserWhereInput = {}; + // If user is not ADMIN, return only his data. + if (!isAdmin) where.id = userId; + const data = await prisma.user.findMany({ where }); + const users = schemaUsersReadPublic.parse(data); + return { users }; +} + +export default defaultResponder(getHandler); diff --git a/pages/api/users/_post.ts b/pages/api/users/_post.ts new file mode 100644 index 0000000000..908a9c4bcc --- /dev/null +++ b/pages/api/users/_post.ts @@ -0,0 +1,20 @@ +import { HttpError } from "@/../../packages/lib/http-error"; +import type { NextApiRequest } from "next"; + +import { defaultResponder } from "@calcom/lib/server"; +import prisma from "@calcom/prisma"; + +import { isAdminGuard } from "@lib/utils/isAdmin"; +import { schemaUserCreateBodyParams } from "@lib/validations/user"; + +async function postHandler(req: NextApiRequest) { + const isAdmin = await isAdminGuard(req.userId); + // If user is not ADMIN, return unauthorized. + if (!isAdmin) throw new HttpError({ statusCode: 401, message: "You are not authorized" }); + const data = schemaUserCreateBodyParams.parse(req.body); + const user = await prisma.user.create({ data }); + req.statusCode = 201; + return { user }; +} + +export default defaultResponder(postHandler); diff --git a/pages/api/users/index.ts b/pages/api/users/index.ts index 3839ae21b6..c07846423f 100644 --- a/pages/api/users/index.ts +++ b/pages/api/users/index.ts @@ -1,60 +1,10 @@ -import type { NextApiRequest, NextApiResponse } from "next"; - -import prisma from "@calcom/prisma"; +import { defaultHandler } from "@calcom/lib/server"; import { withMiddleware } from "@lib/helpers/withMiddleware"; -import { UserResponse, UsersResponse } from "@lib/types"; -import { isAdminGuard } from "@lib/utils/isAdmin"; -import { schemaUserReadPublic, schemaUserCreateBodyParams } from "@lib/validations/user"; -/** - * @swagger - * /users: - * get: - * operationId: listUsers - * summary: Find all users. - * tags: - * - users - * responses: - * 200: - * description: OK - * 401: - * description: Authorization information is missing or invalid. - * 404: - * description: No users were found - */ -async function getAllorCreateUser( - { userId, method, body }: NextApiRequest, - res: NextApiResponse -) { - const isAdmin = await isAdminGuard(userId); - if (method === "GET") { - if (!isAdmin) { - // If user is not ADMIN, return only his data. - const data = await prisma.user.findMany({ where: { id: userId } }); - const users = data.map((user) => schemaUserReadPublic.parse(user)); - if (users) res.status(200).json({ users }); - } else { - // If user is admin, return all users. - const data = await prisma.user.findMany({}); - const users = data.map((user) => schemaUserReadPublic.parse(user)); - if (users) res.status(200).json({ users }); - } - } else if (method === "POST") { - // If user is not ADMIN, return unauthorized. - if (!isAdmin) res.status(401).json({ message: "You are not authorized" }); - else { - const safeBody = schemaUserCreateBodyParams.safeParse(body); - if (!safeBody.success) { - res.status(400).json({ message: "Your body was invalid" }); - return; - } - const user = await prisma.user.create({ - data: safeBody.data, - }); - res.status(201).json({ user }); - } - } -} - -export default withMiddleware("HTTP_GET_OR_POST")(getAllorCreateUser); +export default withMiddleware("HTTP_GET_OR_POST")( + defaultHandler({ + GET: import("./_get"), + POST: import("./_post"), + }) +); From 7d0cef065f079c575ec8ee7c80f150f21a60cab7 Mon Sep 17 00:00:00 2001 From: zomars Date: Tue, 14 Jun 2022 14:10:59 -0600 Subject: [PATCH 4/7] Refactors user id endpoint --- pages/api/users/{[id].ts => [id]/index.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename pages/api/users/{[id].ts => [id]/index.ts} (100%) diff --git a/pages/api/users/[id].ts b/pages/api/users/[id]/index.ts similarity index 100% rename from pages/api/users/[id].ts rename to pages/api/users/[id]/index.ts From 0f72a9084a789c84cf7642d1f1932de4b057e2b1 Mon Sep 17 00:00:00 2001 From: zomars Date: Tue, 14 Jun 2022 14:35:15 -0600 Subject: [PATCH 5/7] Splits user endpoints by method --- pages/api/users/[id]/_delete.ts | 43 +++++++ pages/api/users/[id]/_get.ts | 45 ++++++++ pages/api/users/[id]/_patch.ts | 82 ++++++++++++++ pages/api/users/[id]/index.ts | 194 ++------------------------------ 4 files changed, 181 insertions(+), 183 deletions(-) create mode 100644 pages/api/users/[id]/_delete.ts create mode 100644 pages/api/users/[id]/_get.ts create mode 100644 pages/api/users/[id]/_patch.ts diff --git a/pages/api/users/[id]/_delete.ts b/pages/api/users/[id]/_delete.ts new file mode 100644 index 0000000000..ce04999eea --- /dev/null +++ b/pages/api/users/[id]/_delete.ts @@ -0,0 +1,43 @@ +import type { NextApiRequest } from "next"; + +import { HttpError } from "@calcom/lib/http-error"; +import { defaultResponder } from "@calcom/lib/server"; +import prisma from "@calcom/prisma"; + +import { isAdminGuard } from "@lib/utils/isAdmin"; +import { schemaQueryIdParseInt } from "@lib/validations/shared/queryIdTransformParseInt"; + +/** + * @swagger + * /users/{id}: + * delete: + * summary: Remove an existing user + * operationId: removeUserById + * parameters: + * - in: path + * name: id + * example: 1 + * schema: + * type: integer + * required: true + * description: ID of the user to delete + * tags: + * - users + * responses: + * 201: + * description: OK, user removed successfuly + * 400: + * description: Bad request. User id is invalid. + * 401: + * description: Authorization information is missing or invalid. + */ +export async function deleteHandler(req: NextApiRequest) { + const query = schemaQueryIdParseInt.parse(req.query); + const isAdmin = await isAdminGuard(req.userId); + // Here we only check for ownership of the user if the user is not admin, otherwise we let ADMIN's edit any user + if (!isAdmin && query.id !== req.userId) throw new HttpError({ statusCode: 401, message: "Unauthorized" }); + await prisma.user.delete({ where: { id: query.id } }); + return { message: `User with id: ${query.id} deleted successfully` }; +} + +export default defaultResponder(deleteHandler); diff --git a/pages/api/users/[id]/_get.ts b/pages/api/users/[id]/_get.ts new file mode 100644 index 0000000000..443b935e9c --- /dev/null +++ b/pages/api/users/[id]/_get.ts @@ -0,0 +1,45 @@ +import type { NextApiRequest } from "next"; + +import { HttpError } from "@calcom/lib/http-error"; +import { defaultResponder } from "@calcom/lib/server"; +import prisma from "@calcom/prisma"; + +import { isAdminGuard } from "@lib/utils/isAdmin"; +import { schemaQueryIdParseInt } from "@lib/validations/shared/queryIdTransformParseInt"; +import { schemaUserReadPublic } from "@lib/validations/user"; + +/** + * @swagger + * /users/{id}: + * get: + * summary: Find a user, returns your user if regular user. + * operationId: getUserById + * parameters: + * - in: path + * name: id + * example: 4 + * schema: + * type: integer + * required: true + * description: ID of the user to get + * tags: + * - users + * responses: + * 200: + * description: OK + * 401: + * description: Authorization information is missing or invalid. + * 404: + * description: User was not found + */ +export async function getHandler(req: NextApiRequest) { + const query = schemaQueryIdParseInt.parse(req.query); + const isAdmin = await isAdminGuard(req.userId); + // Here we only check for ownership of the user if the user is not admin, otherwise we let ADMIN's edit any user + if (!isAdmin && query.id !== req.userId) throw new HttpError({ statusCode: 401, message: "Unauthorized" }); + const data = await prisma.user.findUnique({ where: { id: query.id } }); + const user = schemaUserReadPublic.parse(data); + return { user }; +} + +export default defaultResponder(getHandler); diff --git a/pages/api/users/[id]/_patch.ts b/pages/api/users/[id]/_patch.ts new file mode 100644 index 0000000000..a6551609e7 --- /dev/null +++ b/pages/api/users/[id]/_patch.ts @@ -0,0 +1,82 @@ +import type { NextApiRequest } from "next"; + +import { HttpError } from "@calcom/lib/http-error"; +import { defaultResponder } from "@calcom/lib/server"; +import prisma from "@calcom/prisma"; + +import { isAdminGuard } from "@lib/utils/isAdmin"; +import { schemaQueryIdParseInt } from "@lib/validations/shared/queryIdTransformParseInt"; +import { schemaUserEditBodyParams, schemaUserReadPublic } from "@lib/validations/user"; + +/** + * @swagger + * /users/{id}: + * patch: + * summary: Edit an existing user + * operationId: editUserById + * requestBody: + * description: Edit an existing attendee related to one of your bookings + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * weekStart: + * type: string + * enum: [Monday, Sunday, Saturday] + * example: Monday + * brandColor: + * type: string + * example: "#FF000F" + * darkBrandColor: + * type: string + * example: "#000000" + * timeZone: + * type: string + * example: Europe/London + * parameters: + * - in: path + * name: id + * example: 4 + * schema: + * type: integer + * required: true + * description: ID of the user to edit + * tags: + * - users + * responses: + * 201: + * description: OK, user edited successfuly + * 400: + * description: Bad request. User body is invalid. + * 401: + * description: Authorization information is missing or invalid. + */ +export async function patchHandler(req: NextApiRequest) { + const query = schemaQueryIdParseInt.parse(req.query); + const isAdmin = await isAdminGuard(req.userId); + // Here we only check for ownership of the user if the user is not admin, otherwise we let ADMIN's edit any user + if (!isAdmin && query.id !== req.userId) throw new HttpError({ statusCode: 401, message: "Unauthorized" }); + + const body = schemaUserEditBodyParams.parse(req.body); + const userSchedules = await prisma.schedule.findMany({ + where: { userId: req.userId }, + }); + const userSchedulesIds = userSchedules.map((schedule) => schedule.id); + // @note: here we make sure user can only make as default his own scheudles + if (body.defaultScheduleId && !userSchedulesIds.includes(Number(body.defaultScheduleId))) { + throw new HttpError({ + statusCode: 400, + message: "Bad request: Invalid default schedule id", + }); + } + const data = await prisma.user.update({ + where: { id: req.userId }, + data: body, + }); + const user = schemaUserReadPublic.parse(data); + return { user }; +} + +export default defaultResponder(patchHandler); diff --git a/pages/api/users/[id]/index.ts b/pages/api/users/[id]/index.ts index 65b864c652..7d8887e5d1 100644 --- a/pages/api/users/[id]/index.ts +++ b/pages/api/users/[id]/index.ts @@ -1,186 +1,14 @@ -import type { NextApiRequest, NextApiResponse } from "next"; - -import prisma from "@calcom/prisma"; +import { defaultHandler } from "@/../../packages/lib/server"; import { withMiddleware } from "@lib/helpers/withMiddleware"; -import type { UserResponse } from "@lib/types"; -import { isAdminGuard } from "@lib/utils/isAdmin"; -import { - schemaQueryIdParseInt, - withValidQueryIdTransformParseInt, -} from "@lib/validations/shared/queryIdTransformParseInt"; -import { schemaUserEditBodyParams, schemaUserReadPublic } from "@lib/validations/user"; +import { withValidQueryIdTransformParseInt } from "@lib/validations/shared/queryIdTransformParseInt"; -export async function userById( - { method, query, body, userId }: NextApiRequest, - res: NextApiResponse -) { - const safeQuery = schemaQueryIdParseInt.safeParse(query); - console.log(body); - if (!safeQuery.success) { - res.status(400).json({ message: "Your query was invalid" }); - return; - } - const isAdmin = await isAdminGuard(userId); - // Here we only check for ownership of the user if the user is not admin, otherwise we let ADMIN's edit any user - if (!isAdmin) { - if (safeQuery.data.id !== userId) res.status(401).json({ message: "Unauthorized" }); - } else { - switch (method) { - case "GET": - /** - * @swagger - * /users/{id}: - * get: - * summary: Find a user, returns your user if regular user. - * operationId: getUserById - * parameters: - * - in: path - * name: id - * example: 4 - * schema: - * type: integer - * required: true - * description: ID of the user to get - * tags: - * - users - * responses: - * 200: - * description: OK - * 401: - * description: Authorization information is missing or invalid. - * 404: - * description: User was not found - */ - - await prisma.user - .findUnique({ where: { id: safeQuery.data.id } }) - .then((data) => schemaUserReadPublic.parse(data)) - .then((user) => res.status(200).json({ user })) - .catch((error: Error) => - res.status(404).json({ message: `User with id: ${safeQuery.data.id} not found`, error }) - ); - break; - case "PATCH": - /** - * @swagger - * /users/{id}: - * patch: - * summary: Edit an existing user - * operationId: editUserById - * requestBody: - * description: Edit an existing attendee related to one of your bookings - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * weekStart: - * type: string - * enum: [Monday, Sunday, Saturday] - * example: Monday - * brandColor: - * type: string - * example: "#FF000F" - * darkBrandColor: - * type: string - * example: "#000000" - * timeZone: - * type: string - * example: Europe/London - * parameters: - * - in: path - * name: id - * example: 4 - * schema: - * type: integer - * required: true - * description: ID of the user to edit - * tags: - * - users - * responses: - * 201: - * description: OK, user edited successfuly - * 400: - * description: Bad request. User body is invalid. - * 401: - * description: Authorization information is missing or invalid. - */ - - const safeBody = schemaUserEditBodyParams.safeParse(body); - if (!safeBody.success) { - res.status(400).json({ message: "Bad request", error: safeBody.error }); - - return; - } - const userSchedules = await prisma.schedule.findMany({ - where: { userId }, - }); - const userSchedulesIds = userSchedules.map((schedule) => schedule.id); - // @note: here we make sure user can only make as default his own scheudles - if ( - safeBody?.data?.defaultScheduleId && - !userSchedulesIds.includes(Number(safeBody?.data?.defaultScheduleId)) - ) { - res.status(400).json({ - message: "Bad request: Invalid default schedule id", - }); - return; - } - await prisma.user - .update({ - where: { id: userId }, - data: safeBody.data, - }) - .then((data) => schemaUserReadPublic.parse(data)) - .then((user) => res.status(200).json({ user })) - .catch((error: Error) => - res.status(404).json({ message: `User with id: ${safeQuery.data.id} not found`, error }) - ); - break; - - /** - * @swagger - * /users/{id}: - * delete: - * summary: Remove an existing user - * operationId: removeUserById - * parameters: - * - in: path - * name: id - * example: 1 - * schema: - * type: integer - * required: true - * description: ID of the user to delete - * tags: - * - users - * responses: - * 201: - * description: OK, user removed successfuly - * 400: - * description: Bad request. User id is invalid. - * 401: - * description: Authorization information is missing or invalid. - */ - - case "DELETE": - await prisma.user - .delete({ where: { id: safeQuery.data.id } }) - .then(() => - res.status(200).json({ message: `User with id: ${safeQuery.data.id} deleted successfully` }) - ) - .catch((error: Error) => - res.status(404).json({ message: `User 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(userById)); +export default withMiddleware("HTTP_GET_DELETE_PATCH")( + withValidQueryIdTransformParseInt( + defaultHandler({ + GET: import("./_get"), + PATCH: import("./_patch"), + DELETE: import("./_delete"), + }) + ) +); From ae9c33ddbe0d5ec5105f59f326bf6e02edf950f3 Mon Sep 17 00:00:00 2001 From: zomars Date: Tue, 14 Jun 2022 14:44:09 -0600 Subject: [PATCH 6/7] Makes user create/update body strict --- lib/validations/user.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/lib/validations/user.ts b/lib/validations/user.ts index c780dfbed8..1a69eb5c7c 100644 --- a/lib/validations/user.ts +++ b/lib/validations/user.ts @@ -122,8 +122,15 @@ const schemaUserCreateParams = z.object({ // @note: These are the values that are editable via PATCH method on the user Model, // merging both BaseBodyParams with RequiredParams, and omiting whatever we want at the end. -export const schemaUserEditBodyParams = schemaUserBaseBodyParams.merge(schemaUserEditParams).omit({}); -export const schemaUserCreateBodyParams = schemaUserBaseBodyParams.merge(schemaUserCreateParams).omit({}); +export const schemaUserEditBodyParams = schemaUserBaseBodyParams + .merge(schemaUserEditParams) + .omit({}) + .strict(); + +export const schemaUserCreateBodyParams = schemaUserBaseBodyParams + .merge(schemaUserCreateParams) + .omit({}) + .strict(); // @note: These are the values that are always returned when reading a user export const schemaUserReadPublic = User.pick({ From 1ab81fb8ee127d5fa1dce93ed1cda68ae2b651b5 Mon Sep 17 00:00:00 2001 From: zomars Date: Tue, 14 Jun 2022 14:51:02 -0600 Subject: [PATCH 7/7] Better error on user delete --- pages/api/users/[id]/_delete.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/pages/api/users/[id]/_delete.ts b/pages/api/users/[id]/_delete.ts index ce04999eea..7d38d07372 100644 --- a/pages/api/users/[id]/_delete.ts +++ b/pages/api/users/[id]/_delete.ts @@ -36,8 +36,12 @@ export async function deleteHandler(req: NextApiRequest) { const isAdmin = await isAdminGuard(req.userId); // Here we only check for ownership of the user if the user is not admin, otherwise we let ADMIN's edit any user if (!isAdmin && query.id !== req.userId) throw new HttpError({ statusCode: 401, message: "Unauthorized" }); - await prisma.user.delete({ where: { id: query.id } }); - return { message: `User with id: ${query.id} deleted successfully` }; + + const user = await prisma.user.findUnique({ where: { id: query.id } }); + if (!user) throw new HttpError({ statusCode: 404, message: "User not found" }); + + await prisma.user.delete({ where: { id: user.id } }); + return { message: `User with id: ${user.id} deleted successfully` }; } export default defaultResponder(deleteHandler);