From be2647790c39ec95280f5cd489dd8e139e879b0d Mon Sep 17 00:00:00 2001 From: Agusti Fernandez Pardo Date: Thu, 30 Jun 2022 00:01:14 +0200 Subject: [PATCH] feat: refactor teams and add team availability --- lib/validations/shared/queryTeamId.ts | 21 +++ lib/validations/team.ts | 2 +- pages/api/teams/[id].ts | 146 ------------------ pages/api/teams/[teamId]/_delete.ts | 57 +++++++ pages/api/teams/[teamId]/_get.ts | 48 ++++++ pages/api/teams/[teamId]/_patch.ts | 62 ++++++++ .../api/teams/[teamId]/availability/index.ts | 9 ++ pages/api/teams/[teamId]/index.ts | 14 ++ pages/api/teams/_get.ts | 43 ++++++ pages/api/teams/_post.ts | 47 ++++++ pages/api/teams/index.ts | 91 +---------- pages/api/users/_post.ts | 16 ++ 12 files changed, 325 insertions(+), 231 deletions(-) create mode 100644 lib/validations/shared/queryTeamId.ts delete mode 100644 pages/api/teams/[id].ts create mode 100644 pages/api/teams/[teamId]/_delete.ts create mode 100644 pages/api/teams/[teamId]/_get.ts create mode 100644 pages/api/teams/[teamId]/_patch.ts create mode 100644 pages/api/teams/[teamId]/availability/index.ts create mode 100644 pages/api/teams/[teamId]/index.ts create mode 100644 pages/api/teams/_get.ts create mode 100644 pages/api/teams/_post.ts diff --git a/lib/validations/shared/queryTeamId.ts b/lib/validations/shared/queryTeamId.ts new file mode 100644 index 0000000000..f2a80361eb --- /dev/null +++ b/lib/validations/shared/queryTeamId.ts @@ -0,0 +1,21 @@ +import { withValidation } from "next-validations"; +import { z } from "zod"; + +import { baseApiParams } from "./baseApiParams"; + +// Extracted out as utility function so can be reused +// at different endpoints that require this validation. +export const schemaQueryTeamId = baseApiParams + .extend({ + teamId: z + .string() + .regex(/^\d+$/) + .transform((id) => parseInt(id)), + }) + .strict(); + +export const withValidQueryTeamId = withValidation({ + schema: schemaQueryTeamId, + type: "Zod", + mode: "query", +}); diff --git a/lib/validations/team.ts b/lib/validations/team.ts index d980607f87..1efa3e84df 100644 --- a/lib/validations/team.ts +++ b/lib/validations/team.ts @@ -8,4 +8,4 @@ const schemaTeamRequiredParams = z.object({}); export const schemaTeamBodyParams = schemaTeamBaseBodyParams.merge(schemaTeamRequiredParams); -export const schemaTeamPublic = Team.omit({}); +export const schemaTeamReadPublic = Team.omit({}); diff --git a/pages/api/teams/[id].ts b/pages/api/teams/[id].ts deleted file mode 100644 index 65dd8d116e..0000000000 --- a/pages/api/teams/[id].ts +++ /dev/null @@ -1,146 +0,0 @@ -import type { NextApiRequest, NextApiResponse } from "next"; - -import { withMiddleware } from "@lib/helpers/withMiddleware"; -import type { TeamResponse } from "@lib/types"; -import { - schemaQueryIdParseInt, - withValidQueryIdTransformParseInt, -} from "@lib/validations/shared/queryIdTransformParseInt"; -import { schemaTeamBodyParams, schemaTeamPublic } from "@lib/validations/team"; - -/** - * @swagger - * /teams/{id}: - * get: - * operationId: getTeamById - * summary: Find a team - * parameters: - * - in: path - * name: id - * schema: - * type: integer - * required: true - * description: ID of the team to get - * tags: - * - teams - * responses: - * 200: - * description: OK - * 401: - * description: Authorization information is missing or invalid. - * 404: - * description: Team was not found - * patch: - * operationId: editTeamById - * summary: Edit an existing team - * parameters: - * - in: path - * name: id - * schema: - * type: integer - * required: true - * description: ID of the team to edit - * tags: - * - teams - * responses: - * 201: - * description: OK, team edited successfuly - * 400: - * description: Bad request. Team body is invalid. - * 401: - * description: Authorization information is missing or invalid. - * delete: - * operationId: removeTeamById - * summary: Remove an existing team - * parameters: - * - in: path - * name: id - * schema: - * type: integer - * required: true - * description: ID of the team to delete - * tags: - * - teams - * responses: - * 201: - * description: OK, team removed successfuly - * 400: - * description: Bad request. Team id is invalid. - * 401: - * description: Authorization information is missing or invalid. - */ -export async function teamById( - { method, query, body, userId, prisma }: NextApiRequest, - res: NextApiResponse -) { - const safeQuery = schemaQueryIdParseInt.safeParse(query); - const safeBody = schemaTeamBodyParams.safeParse(body); - if (!safeQuery.success) { - res.status(400).json({ message: "Your query was invalid" }); - return; - } - const userWithMemberships = await prisma.membership.findMany({ - where: { userId: userId }, - }); - //FIXME: This is a hack to get the teamId from the user's membership - console.log(userWithMemberships); - const userTeamIds = userWithMemberships.map((membership) => membership.teamId); - if (!userTeamIds.includes(safeQuery.data.id)) res.status(401).json({ message: "Unauthorized" }); - else { - switch (method) { - case "GET": - await prisma.team - .findUnique({ where: { id: safeQuery.data.id } }) - .then((data) => schemaTeamPublic.parse(data)) - .then((team) => res.status(200).json({ team })) - .catch((error: Error) => - res.status(404).json({ - message: `Team with id: ${safeQuery.data.id} not found`, - error, - }) - ); - break; - - case "PATCH": - if (!safeBody.success) { - { - res.status(400).json({ message: "Invalid request body" }); - return; - } - } - await prisma.team - .update({ where: { id: safeQuery.data.id }, data: safeBody.data }) - .then((team) => schemaTeamPublic.parse(team)) - .then((team) => res.status(200).json({ team })) - .catch((error: Error) => - res.status(404).json({ - message: `Team with id: ${safeQuery.data.id} not found`, - error, - }) - ); - break; - - case "DELETE": - await prisma.team - .delete({ where: { id: safeQuery.data.id } }) - .then(() => - res.status(200).json({ - message: `Team with id: ${safeQuery.data.id} deleted successfully`, - }) - ) - .catch((error: Error) => - res.status(404).json({ - message: `Team 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(teamById)); diff --git a/pages/api/teams/[teamId]/_delete.ts b/pages/api/teams/[teamId]/_delete.ts new file mode 100644 index 0000000000..39b982caff --- /dev/null +++ b/pages/api/teams/[teamId]/_delete.ts @@ -0,0 +1,57 @@ +import type { NextApiRequest, NextApiResponse } from "next"; + +import { HttpError } from "@calcom/lib/http-error"; +import { defaultResponder } from "@calcom/lib/server"; + +import { schemaQueryTeamId } from "@lib/validations/shared/queryTeamId"; + +/** + * @swagger + * /users/{teamId}: + * delete: + * operationId: removeTeamById + * summary: Remove an existing team + * parameters: + * - in: path + * name: teamId + * schema: + * type: integer + * required: true + * description: ID of the team to delete + * tags: + * - teams + * responses: + * 201: + * description: OK, team removed successfuly + * 400: + * description: Bad request. Team id is invalid. + * 401: + * description: Authorization information is missing or invalid. + */ +export async function deleteHandler(req: NextApiRequest, res: NextApiResponse) { + const { prisma, isAdmin, userId } = req; + + const query = schemaQueryTeamId.parse(req.query); + const userWithMemberships = await prisma.membership.findMany({ + where: { userId: userId }, + }); + const userTeamIds = userWithMemberships.map((membership) => membership.teamId); + // 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 && !userTeamIds.includes(query.teamId)) + throw new HttpError({ statusCode: 401, message: "Unauthorized" }); + await prisma.team + .delete({ where: { id: query.teamId } }) + .then(() => + res.status(200).json({ + message: `Team with id: ${query.teamId} deleted successfully`, + }) + ) + .catch((error: Error) => + res.status(404).json({ + message: `Team with id: ${query.teamId} not found`, + error, + }) + ); +} + +export default defaultResponder(deleteHandler); diff --git a/pages/api/teams/[teamId]/_get.ts b/pages/api/teams/[teamId]/_get.ts new file mode 100644 index 0000000000..b2295d40fa --- /dev/null +++ b/pages/api/teams/[teamId]/_get.ts @@ -0,0 +1,48 @@ +import type { NextApiRequest } from "next"; + +import { HttpError } from "@calcom/lib/http-error"; +import { defaultResponder } from "@calcom/lib/server"; + +import { schemaQueryTeamId } from "@lib/validations/shared/queryTeamId"; +import { schemaTeamReadPublic } from "@lib/validations/team"; + +/** + * @swagger + * /teams/{teamId}: + * get: + * operationId: getTeamById + * summary: Find a team + * parameters: + * - in: path + * name: teamId + * schema: + * type: integer + * required: true + * description: ID of the team to get + * tags: + * - teams + * responses: + * 200: + * description: OK + * 401: + * description: Authorization information is missing or invalid. + * 404: + * description: Team was not found + */ +export async function getHandler(req: NextApiRequest) { + const { prisma, isAdmin, userId } = req; + + const query = schemaQueryTeamId.parse(req.query); + const userWithMemberships = await prisma.membership.findMany({ + where: { userId: userId }, + }); + const userTeamIds = userWithMemberships.map((membership) => membership.teamId); + // 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 && !userTeamIds.includes(query.teamId)) + throw new HttpError({ statusCode: 401, message: "Unauthorized" }); + const data = await prisma.team.findUnique({ where: { id: query.teamId } }); + const team = schemaTeamReadPublic.parse(data); + return { team }; +} + +export default defaultResponder(getHandler); diff --git a/pages/api/teams/[teamId]/_patch.ts b/pages/api/teams/[teamId]/_patch.ts new file mode 100644 index 0000000000..7550cd49b5 --- /dev/null +++ b/pages/api/teams/[teamId]/_patch.ts @@ -0,0 +1,62 @@ +import type { NextApiRequest, NextApiResponse } from "next"; + +import { HttpError } from "@calcom/lib/http-error"; +import { defaultResponder } from "@calcom/lib/server"; + +import { schemaQueryTeamId } from "@lib/validations/shared/queryTeamId"; +import { schemaTeamBodyParams, schemaTeamReadPublic } from "@lib/validations/team"; + +/** + * @swagger + * /teams/{teamId}: + * patch: + * operationId: editTeamById + * summary: Edit an existing team + * parameters: + * - in: path + * name: teamId + * schema: + * type: integer + * required: true + * description: ID of the team to edit + * tags: + * - teams + * responses: + * 201: + * description: OK, team edited successfuly + * 400: + * description: Bad request. Team body is invalid. + * 401: + * description: Authorization information is missing or invalid. + */ +export async function patchHandler(req: NextApiRequest, res: NextApiResponse) { + const { prisma, isAdmin, userId, body } = req; + const safeBody = schemaTeamBodyParams.safeParse(body); + + const query = schemaQueryTeamId.parse(req.query); + const userWithMemberships = await prisma.membership.findMany({ + where: { userId: userId }, + }); + const userTeamIds = userWithMemberships.map((membership) => membership.teamId); + // 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 && !userTeamIds.includes(query.teamId)) + throw new HttpError({ statusCode: 401, message: "Unauthorized" }); + if (!safeBody.success) { + { + res.status(400).json({ message: "Invalid request body" }); + return; + } + } + await prisma.team + .update({ where: { id: query.teamId }, data: safeBody.data }) + .then((team) => schemaTeamReadPublic.parse(team)) + .then((team) => res.status(200).json({ team })) + .catch((error: Error) => + res.status(404).json({ + message: `Team with id: ${query.teamId} not found`, + error, + }) + ); +} + +export default defaultResponder(patchHandler); diff --git a/pages/api/teams/[teamId]/availability/index.ts b/pages/api/teams/[teamId]/availability/index.ts new file mode 100644 index 0000000000..1a27360f81 --- /dev/null +++ b/pages/api/teams/[teamId]/availability/index.ts @@ -0,0 +1,9 @@ +import { defaultHandler } from "@calcom/lib/server"; + +import { withMiddleware } from "@lib/helpers/withMiddleware"; + +export default withMiddleware("HTTP_GET")( + defaultHandler({ + GET: import("@api/availability/_get"), + }) +); diff --git a/pages/api/teams/[teamId]/index.ts b/pages/api/teams/[teamId]/index.ts new file mode 100644 index 0000000000..ed397498d4 --- /dev/null +++ b/pages/api/teams/[teamId]/index.ts @@ -0,0 +1,14 @@ +import { defaultHandler } from "@calcom/lib/server"; + +import { withMiddleware } from "@lib/helpers/withMiddleware"; +import { withValidQueryTeamId } from "@lib/validations/shared/queryTeamId"; + +export default withMiddleware("HTTP_GET_DELETE_PATCH")( + withValidQueryTeamId( + defaultHandler({ + GET: import("./_get"), + PATCH: import("./_patch"), + DELETE: import("./_delete"), + }) + ) +); diff --git a/pages/api/teams/_get.ts b/pages/api/teams/_get.ts new file mode 100644 index 0000000000..4a175c899b --- /dev/null +++ b/pages/api/teams/_get.ts @@ -0,0 +1,43 @@ +import type { NextApiRequest } from "next"; + +import { defaultResponder } from "@calcom/lib/server"; + +import { schemaTeamReadPublic } from "@lib/validations/team"; + +import { Prisma } from ".prisma/client"; + +/** + * @swagger + * /teams: + * get: + * operationId: listTeams + * summary: Find all teams + * tags: + * - teams + * responses: + * 200: + * description: OK + * 401: + * description: Authorization information is missing or invalid. + * 404: + * description: No teams were found + */ +async function getHandler(req: NextApiRequest) { + const { userId, prisma, isAdmin } = req; + const membershipWhere: Prisma.MembershipWhereInput = {}; + // If user is not ADMIN, return only his data. + if (!isAdmin) membershipWhere.userId = userId; + const userWithMemberships = await prisma.membership.findMany({ + where: membershipWhere, + }); + const teamIds = userWithMemberships.map((membership) => membership.teamId); + const teamWhere: Prisma.TeamWhereInput = {}; + + if (!isAdmin) teamWhere.id = { in: teamIds }; + + const data = await prisma.team.findMany({ where: teamWhere }); + const teams = schemaTeamReadPublic.parse(data); + return { teams }; +} + +export default defaultResponder(getHandler); diff --git a/pages/api/teams/_post.ts b/pages/api/teams/_post.ts new file mode 100644 index 0000000000..53f66d7746 --- /dev/null +++ b/pages/api/teams/_post.ts @@ -0,0 +1,47 @@ +import type { NextApiRequest } from "next"; + +import { HttpError } from "@calcom/lib/http-error"; +import { defaultResponder } from "@calcom/lib/server"; + +import { schemaMembershipPublic } from "@lib/validations/membership"; +import { schemaTeamBodyParams, schemaTeamReadPublic } from "@lib/validations/team"; + +/** + * @swagger + * /teams: + * post: + * operationId: addTeam + * summary: Creates a new team + * tags: + * - teams + * responses: + * 201: + * description: OK, team created + * 400: + * description: Bad request. Team body is invalid. + * 401: + * description: Authorization information is missing or invalid. + */ +async function postHandler(req: NextApiRequest) { + const { prisma, body, userId } = req; + const safe = schemaTeamBodyParams.safeParse(body); + if (!safe.success) throw new HttpError({ statusCode: 400, message: "Invalid request body" }); + const data = await prisma.team.create({ data: safe.data }); + // We're also creating the relation membership of team ownership in this call. + const owner = await prisma.membership + .create({ + data: { userId, teamId: data.id, role: "OWNER", accepted: true }, + }) + .then((owner) => schemaMembershipPublic.parse(owner)); + const team = schemaTeamReadPublic.parse(data); + if (!team) throw new HttpError({ statusCode: 400, message: "We were not able to create your team" }); + req.statusCode = 201; + // We are also returning the new ownership relation as owner besides team. + return { + team, + owner, + message: "Team created successfully, we also made you the owner of this team", + }; +} + +export default defaultResponder(postHandler); diff --git a/pages/api/teams/index.ts b/pages/api/teams/index.ts index 1dcc046f6e..c07846423f 100644 --- a/pages/api/teams/index.ts +++ b/pages/api/teams/index.ts @@ -1,87 +1,10 @@ -import type { NextApiRequest, NextApiResponse } from "next"; +import { defaultHandler } from "@calcom/lib/server"; import { withMiddleware } from "@lib/helpers/withMiddleware"; -import { TeamResponse, TeamsResponse } from "@lib/types"; -import { schemaMembershipPublic } from "@lib/validations/membership"; -import { schemaTeamBodyParams, schemaTeamPublic } from "@lib/validations/team"; -async function createOrlistAllTeams( - { method, body, userId, prisma }: NextApiRequest, - res: NextApiResponse -) { - if (method === "GET") { - /** - * @swagger - * /teams: - * get: - * operationId: listTeams - * summary: Find all teams - * tags: - * - teams - * responses: - * 200: - * description: OK - * 401: - * description: Authorization information is missing or invalid. - * 404: - * description: No teams were found - */ - const userWithMemberships = await prisma.membership.findMany({ - where: { userId: userId }, - }); - const teamIds = userWithMemberships.map((membership) => membership.teamId); - const teams = await prisma.team.findMany({ where: { id: { in: teamIds } } }); - if (teams) res.status(200).json({ teams }); - else - (error: Error) => - res.status(404).json({ - message: "No Teams were found", - error, - }); - } else if (method === "POST") { - /** - * @swagger - * /teams: - * post: - * operationId: addTeam - * summary: Creates a new team - * tags: - * - teams - * responses: - * 201: - * description: OK, team created - * 400: - * description: Bad request. Team body is invalid. - * 401: - * description: Authorization information is missing or invalid. - */ - const safe = schemaTeamBodyParams.safeParse(body); - if (!safe.success) { - res.status(400).json({ message: "Invalid request body" }); - return; - } - const team = await prisma.team.create({ data: safe.data }); - // We're also creating the relation membership of team ownership in this call. - const membership = await prisma.membership - .create({ - data: { userId, teamId: team.id, role: "OWNER", accepted: true }, - }) - .then((membership) => schemaMembershipPublic.parse(membership)); - const data = schemaTeamPublic.parse(team); - // We are also returning the new ownership relation as owner besides team. - if (data) - res.status(201).json({ - team: data, - owner: membership, - message: "Team created successfully, we also made you the owner of this team", - }); - else - (error: Error) => - res.status(400).json({ - message: "Could not create new team", - error, - }); - } else res.status(405).json({ message: `Method ${method} not allowed` }); -} - -export default withMiddleware("HTTP_GET_OR_POST")(createOrlistAllTeams); +export default withMiddleware("HTTP_GET_OR_POST")( + defaultHandler({ + GET: import("./_get"), + POST: import("./_post"), + }) +); diff --git a/pages/api/users/_post.ts b/pages/api/users/_post.ts index 03ab696e96..268931b122 100644 --- a/pages/api/users/_post.ts +++ b/pages/api/users/_post.ts @@ -5,6 +5,22 @@ import { defaultResponder } from "@calcom/lib/server"; import { schemaUserCreateBodyParams } from "@lib/validations/user"; +/** + * @swagger + * /users: + * post: + * operationId: addUser + * summary: Creates a new user + * tags: + * - users + * responses: + * 201: + * description: OK, user created + * 400: + * description: Bad request. user body is invalid. + * 401: + * description: Authorization information is missing or invalid. + */ async function postHandler(req: NextApiRequest) { const { prisma, isAdmin } = req; // If user is not ADMIN, return unauthorized.