From be2647790c39ec95280f5cd489dd8e139e879b0d Mon Sep 17 00:00:00 2001 From: Agusti Fernandez Pardo Date: Thu, 30 Jun 2022 00:01:14 +0200 Subject: [PATCH 01/19] 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. From a63b623a7da87a66acbc2b870dff2bfbe0e29b0f Mon Sep 17 00:00:00 2001 From: Agusti Fernandez Pardo Date: Tue, 5 Jul 2022 20:12:14 +0200 Subject: [PATCH 02/19] feat: teamId availability in API --- lib/helpers/extendRequest.ts | 5 +++++ lib/helpers/withMiddleware.ts | 3 +++ lib/helpers/withPagination.ts | 12 +++++++++++ lib/validations/user.ts | 1 + next.config.js | 1 + pages/api/availability/_get.ts | 39 ++++++++++++++++++++++++++-------- pages/api/users/_get.ts | 14 +++++++++--- 7 files changed, 63 insertions(+), 12 deletions(-) create mode 100644 lib/helpers/withPagination.ts diff --git a/lib/helpers/extendRequest.ts b/lib/helpers/extendRequest.ts index 8ed3ee0571..14de404418 100644 --- a/lib/helpers/extendRequest.ts +++ b/lib/helpers/extendRequest.ts @@ -15,8 +15,13 @@ declare module "next" { session: { user: { id: number } }; query: { [key: string]: string | string[] }; isAdmin: boolean; + pagination: { take: number; skip: number }; } } export const extendRequest: NextMiddleware = async (req, res, next) => { + req.pagination = { + take: 100, + skip: 0, + }; await next(); }; diff --git a/lib/helpers/withMiddleware.ts b/lib/helpers/withMiddleware.ts index ab6d5cb79e..9ce21233fb 100644 --- a/lib/helpers/withMiddleware.ts +++ b/lib/helpers/withMiddleware.ts @@ -1,4 +1,5 @@ import { label } from "next-api-middleware"; +import PagesManifestPlugin from "next/dist/build/webpack/plugins/pages-manifest-plugin"; import { addRequestId } from "./addRequestid"; import { captureErrors } from "./captureErrors"; @@ -13,6 +14,7 @@ import { HTTP_GET_DELETE_PATCH, } from "./httpMethods"; import { verifyApiKey } from "./verifyApiKey"; +import { withPagination } from "./withPagination"; const withMiddleware = label( { @@ -26,6 +28,7 @@ const withMiddleware = label( verifyApiKey, customPrismaClient, extendRequest, + pagination: withPagination, sentry: captureErrors, }, // The order here, determines the order of execution, put customPrismaClient before verifyApiKey always. diff --git a/lib/helpers/withPagination.ts b/lib/helpers/withPagination.ts new file mode 100644 index 0000000000..6d76c680f1 --- /dev/null +++ b/lib/helpers/withPagination.ts @@ -0,0 +1,12 @@ +import { NextMiddleware } from "next-api-middleware"; + +export const withPagination: NextMiddleware = async (req, res, next) => { + const { page } = req.query; + const pageNumber = parseInt(page as string); + const skip = pageNumber * 10; + req.pagination = { + take: 10, + skip: skip || 0, + }; + await next(); +}; diff --git a/lib/validations/user.ts b/lib/validations/user.ts index dcaafdf8d2..15a66ed371 100644 --- a/lib/validations/user.ts +++ b/lib/validations/user.ts @@ -153,6 +153,7 @@ export const schemaUserReadPublic = User.pick({ createdDate: true, verified: true, invitedTo: true, + role: true, }); export const schemaUsersReadPublic = z.array(schemaUserReadPublic); diff --git a/next.config.js b/next.config.js index 22ff466595..344d6fdef8 100644 --- a/next.config.js +++ b/next.config.js @@ -10,6 +10,7 @@ const withTM = require("next-transpile-modules")([ "@calcom/ui", "@calcom/emails", "@calcom/embed-core", + "@calcom/dayjs", "@calcom/embed-snippet", ]); diff --git a/pages/api/availability/_get.ts b/pages/api/availability/_get.ts index 21450f005e..c3edc816c4 100644 --- a/pages/api/availability/_get.ts +++ b/pages/api/availability/_get.ts @@ -1,3 +1,4 @@ +import { HttpError } from "@/../../packages/lib/http-error"; import type { NextApiRequest } from "next"; import { z } from "zod"; @@ -8,22 +9,42 @@ import { stringOrNumber } from "@calcom/prisma/zod-utils"; const availabilitySchema = z .object({ userId: stringOrNumber.optional(), + teamId: stringOrNumber.optional(), username: z.string().optional(), dateFrom: z.string(), dateTo: z.string(), eventTypeId: stringOrNumber.optional(), }) - .refine((data) => !!data.username || !!data.userId, "Either username or userId should be filled in."); + .refine( + (data) => !!data.username || !!data.userId || !!data.teamId, + "Either username or userId or teamId should be filled in." + ); async function handler(req: NextApiRequest) { - const { username, userId, eventTypeId, dateTo, dateFrom } = availabilitySchema.parse(req.query); - return getUserAvailability({ - username, - dateFrom, - dateTo, - eventTypeId, - userId, - }); + const { prisma, isAdmin } = req; + const { username, userId, eventTypeId, dateTo, dateFrom, teamId } = availabilitySchema.parse(req.query); + if (!teamId) + return getUserAvailability({ + username, + dateFrom, + dateTo, + eventTypeId, + userId, + }); + const team = await prisma.team.findUnique({ where: { id: teamId }, select: { members: true } }); + if (!team) throw new HttpError({ statusCode: 404, message: "teamId not found" }); + if (!team.members) throw new HttpError({ statusCode: 404, message: "teamId not found" }); + if (!isAdmin) throw new HttpError({ statusCode: 401, message: "Unauthorized" }); + return team.members.map( + async (user) => + await getUserAvailability({ + username, + dateFrom, + dateTo, + eventTypeId, + userId: user.userId, + }) + ); } export default defaultResponder(handler); diff --git a/pages/api/users/_get.ts b/pages/api/users/_get.ts index 67bb53e2d1..90a03e11d0 100644 --- a/pages/api/users/_get.ts +++ b/pages/api/users/_get.ts @@ -2,6 +2,7 @@ import type { NextApiRequest } from "next"; import { defaultResponder } from "@calcom/lib/server"; +import { withMiddleware } from "@lib/helpers/withMiddleware"; import { schemaUsersReadPublic } from "@lib/validations/user"; import { Prisma } from ".prisma/client"; @@ -23,13 +24,20 @@ import { Prisma } from ".prisma/client"; * description: No users were found */ async function getHandler(req: NextApiRequest) { - const { userId, prisma, isAdmin } = req; + const { + userId, + prisma, + isAdmin, + pagination: { take, skip }, + } = req; 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 data = await prisma.user.findMany({ where, take, skip }); + console.log(data); const users = schemaUsersReadPublic.parse(data); + console.log(users); return { users }; } -export default defaultResponder(getHandler); +export default withMiddleware("pagination")(defaultResponder(getHandler)); From 9b5c7c51209997ad39300900568055e5c6ed441a Mon Sep 17 00:00:00 2001 From: Agusti Fernandez Pardo Date: Tue, 5 Jul 2022 20:13:13 +0200 Subject: [PATCH 03/19] fix: remove unused import --- lib/helpers/withMiddleware.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/helpers/withMiddleware.ts b/lib/helpers/withMiddleware.ts index 9ce21233fb..6f7d7927c0 100644 --- a/lib/helpers/withMiddleware.ts +++ b/lib/helpers/withMiddleware.ts @@ -1,5 +1,4 @@ import { label } from "next-api-middleware"; -import PagesManifestPlugin from "next/dist/build/webpack/plugins/pages-manifest-plugin"; import { addRequestId } from "./addRequestid"; import { captureErrors } from "./captureErrors"; From 5dbb2066e02df2b3e4917058ae6ad118d7efb88d Mon Sep 17 00:00:00 2001 From: Agusti Fernandez Pardo <6601142+agustif@users.noreply.github.com> Date: Tue, 5 Jul 2022 20:34:53 +0200 Subject: [PATCH 04/19] Update pages/api/availability/_get.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit add initialData: {user} Co-authored-by: Omar López --- pages/api/availability/_get.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/api/availability/_get.ts b/pages/api/availability/_get.ts index c3edc816c4..4d6d3559b1 100644 --- a/pages/api/availability/_get.ts +++ b/pages/api/availability/_get.ts @@ -43,7 +43,7 @@ async function handler(req: NextApiRequest) { dateTo, eventTypeId, userId: user.userId, - }) + }, {initialData: { user }}) ); } From 2263e042173ed47cac05181619cd3196e513af79 Mon Sep 17 00:00:00 2001 From: Agusti Fernandez Pardo <6601142+agustif@users.noreply.github.com> Date: Tue, 5 Jul 2022 20:35:30 +0200 Subject: [PATCH 05/19] Update pages/api/availability/_get.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit add select: availabilitUserSelect Co-authored-by: Omar López --- pages/api/availability/_get.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/api/availability/_get.ts b/pages/api/availability/_get.ts index 4d6d3559b1..85e198feb0 100644 --- a/pages/api/availability/_get.ts +++ b/pages/api/availability/_get.ts @@ -31,7 +31,7 @@ async function handler(req: NextApiRequest) { eventTypeId, userId, }); - const team = await prisma.team.findUnique({ where: { id: teamId }, select: { members: true } }); + const team = await prisma.team.findUnique({ where: { id: teamId }, select: { members: { select: availabilityUserSelect } } }); if (!team) throw new HttpError({ statusCode: 404, message: "teamId not found" }); if (!team.members) throw new HttpError({ statusCode: 404, message: "teamId not found" }); if (!isAdmin) throw new HttpError({ statusCode: 401, message: "Unauthorized" }); From f1ee03f297b8e113f22459d96b09d56f432507f1 Mon Sep 17 00:00:00 2001 From: Agusti Fernandez Pardo Date: Tue, 5 Jul 2022 20:58:26 +0200 Subject: [PATCH 06/19] fix: add teams memberships users --- pages/api/availability/_get.ts | 32 ++++++++++++++++++++++---------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/pages/api/availability/_get.ts b/pages/api/availability/_get.ts index 85e198feb0..1a461c0039 100644 --- a/pages/api/availability/_get.ts +++ b/pages/api/availability/_get.ts @@ -1,11 +1,14 @@ -import { HttpError } from "@/../../packages/lib/http-error"; import type { NextApiRequest } from "next"; import { z } from "zod"; import { getUserAvailability } from "@calcom/core/getUserAvailability"; +import { HttpError } from "@calcom/lib/http-error"; import { defaultResponder } from "@calcom/lib/server"; +import { availabilityUserSelect } from "@calcom/prisma"; import { stringOrNumber } from "@calcom/prisma/zod-utils"; +import { User } from ".prisma/client"; + const availabilitySchema = z .object({ userId: stringOrNumber.optional(), @@ -31,19 +34,28 @@ async function handler(req: NextApiRequest) { eventTypeId, userId, }); - const team = await prisma.team.findUnique({ where: { id: teamId }, select: { members: { select: availabilityUserSelect } } }); + const team = await prisma.team.findUnique({ + where: { id: teamId }, + select: { members: true }, + }); if (!team) throw new HttpError({ statusCode: 404, message: "teamId not found" }); if (!team.members) throw new HttpError({ statusCode: 404, message: "teamId not found" }); + const allMemberIds = team.members.map((membership) => membership.userId); + const members = await prisma.user.findMany({ + where: { id: { in: allMemberIds } }, + select: availabilityUserSelect, + }); if (!isAdmin) throw new HttpError({ statusCode: 401, message: "Unauthorized" }); - return team.members.map( + return members.map( async (user) => - await getUserAvailability({ - username, - dateFrom, - dateTo, - eventTypeId, - userId: user.userId, - }, {initialData: { user }}) + await getUserAvailability( + { + dateFrom, + dateTo, + eventTypeId, + }, + { user } + ) ); } From a6e199be670fbc6252399133e3620996ebf3055b Mon Sep 17 00:00:00 2001 From: Agusti Fernandez Pardo Date: Tue, 5 Jul 2022 23:05:29 +0200 Subject: [PATCH 07/19] fix: make availability for teamId work --- pages/api/availability/_get.ts | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/pages/api/availability/_get.ts b/pages/api/availability/_get.ts index 1a461c0039..0d9afa6100 100644 --- a/pages/api/availability/_get.ts +++ b/pages/api/availability/_get.ts @@ -7,8 +7,6 @@ import { defaultResponder } from "@calcom/lib/server"; import { availabilityUserSelect } from "@calcom/prisma"; import { stringOrNumber } from "@calcom/prisma/zod-utils"; -import { User } from ".prisma/client"; - const availabilitySchema = z .object({ userId: stringOrNumber.optional(), @@ -46,17 +44,27 @@ async function handler(req: NextApiRequest) { select: availabilityUserSelect, }); if (!isAdmin) throw new HttpError({ statusCode: 401, message: "Unauthorized" }); - return members.map( - async (user) => - await getUserAvailability( + const availabilities = members.map(async (user) => { + return { + userId: user.id, + availability: await getUserAvailability( { + userId: user.id, dateFrom, dateTo, eventTypeId, }, { user } - ) - ); + ), + }; + }); + const settled = await Promise.all(availabilities); + if (!settled) + throw new HttpError({ + statusCode: 401, + message: "We had an issue retrieving all your members availabilities", + }); + if (settled) return settled; } export default defaultResponder(handler); From bd485b7b77898d28d840d0d07deb360c88f1b83a Mon Sep 17 00:00:00 2001 From: Agusti Fernandez Pardo <6601142+agustif@users.noreply.github.com> Date: Fri, 8 Jul 2022 19:33:00 +0200 Subject: [PATCH 08/19] Update pages/api/teams/_get.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fix prisma import Co-authored-by: Omar López --- pages/api/teams/_get.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/api/teams/_get.ts b/pages/api/teams/_get.ts index 4a175c899b..e961e59bb3 100644 --- a/pages/api/teams/_get.ts +++ b/pages/api/teams/_get.ts @@ -4,7 +4,7 @@ import { defaultResponder } from "@calcom/lib/server"; import { schemaTeamReadPublic } from "@lib/validations/team"; -import { Prisma } from ".prisma/client"; +import { Prisma } from "@prisma/client"; /** * @swagger From f6faa8bc4670f7c604f7c78de6f798e1e35e22bd Mon Sep 17 00:00:00 2001 From: Agusti Fernandez Pardo <6601142+agustif@users.noreply.github.com> Date: Fri, 8 Jul 2022 19:33:17 +0200 Subject: [PATCH 09/19] Update pages/api/users/_get.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit remove console log Co-authored-by: Omar López --- pages/api/users/_get.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/pages/api/users/_get.ts b/pages/api/users/_get.ts index 90a03e11d0..26a6c1925b 100644 --- a/pages/api/users/_get.ts +++ b/pages/api/users/_get.ts @@ -34,7 +34,6 @@ async function getHandler(req: NextApiRequest) { // If user is not ADMIN, return only his data. if (!isAdmin) where.id = userId; const data = await prisma.user.findMany({ where, take, skip }); - console.log(data); const users = schemaUsersReadPublic.parse(data); console.log(users); return { users }; From a72fbfa0933d9f90951a66aa8d75dc636c880007 Mon Sep 17 00:00:00 2001 From: Agusti Fernandez Pardo <6601142+agustif@users.noreply.github.com> Date: Fri, 8 Jul 2022 19:33:37 +0200 Subject: [PATCH 10/19] Update pages/api/availability/_get.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit improve error message team no members Co-authored-by: Omar López --- pages/api/availability/_get.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/api/availability/_get.ts b/pages/api/availability/_get.ts index 0d9afa6100..894df6b8cb 100644 --- a/pages/api/availability/_get.ts +++ b/pages/api/availability/_get.ts @@ -37,7 +37,7 @@ async function handler(req: NextApiRequest) { select: { members: true }, }); if (!team) throw new HttpError({ statusCode: 404, message: "teamId not found" }); - if (!team.members) throw new HttpError({ statusCode: 404, message: "teamId not found" }); + if (!team.members) throw new HttpError({ statusCode: 404, message: "team has no members" }); const allMemberIds = team.members.map((membership) => membership.userId); const members = await prisma.user.findMany({ where: { id: { in: allMemberIds } }, From 80e414d4f55d20c2135b4c3b8e5b7eacebd3eb79 Mon Sep 17 00:00:00 2001 From: Agusti Fernandez Pardo <6601142+agustif@users.noreply.github.com> Date: Fri, 8 Jul 2022 19:33:50 +0200 Subject: [PATCH 11/19] Update pages/api/availability/_get.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit no if just return Co-authored-by: Omar López --- pages/api/availability/_get.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/api/availability/_get.ts b/pages/api/availability/_get.ts index 894df6b8cb..6b6eecfe0b 100644 --- a/pages/api/availability/_get.ts +++ b/pages/api/availability/_get.ts @@ -64,7 +64,7 @@ async function handler(req: NextApiRequest) { statusCode: 401, message: "We had an issue retrieving all your members availabilities", }); - if (settled) return settled; +return settled; } export default defaultResponder(handler); From 76d0b0c1ee5281439f828f57aace8b332e9803df Mon Sep 17 00:00:00 2001 From: Agusti Fernandez Pardo <6601142+agustif@users.noreply.github.com> Date: Fri, 8 Jul 2022 19:34:26 +0200 Subject: [PATCH 12/19] Update pages/api/users/_get.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit remove console log statement Co-authored-by: Omar López --- pages/api/users/_get.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/pages/api/users/_get.ts b/pages/api/users/_get.ts index 26a6c1925b..853eef938b 100644 --- a/pages/api/users/_get.ts +++ b/pages/api/users/_get.ts @@ -35,7 +35,6 @@ async function getHandler(req: NextApiRequest) { if (!isAdmin) where.id = userId; const data = await prisma.user.findMany({ where, take, skip }); const users = schemaUsersReadPublic.parse(data); - console.log(users); return { users }; } From 530752147e560bc6ca87d3dfa94b41cd546c843c Mon Sep 17 00:00:00 2001 From: Agusti Fernandez Pardo Date: Fri, 8 Jul 2022 20:15:25 +0200 Subject: [PATCH 13/19] fix: use zod schema for parsing page query param --- lib/helpers/withPagination.ts | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/lib/helpers/withPagination.ts b/lib/helpers/withPagination.ts index 6d76c680f1..2bcd219b33 100644 --- a/lib/helpers/withPagination.ts +++ b/lib/helpers/withPagination.ts @@ -1,11 +1,25 @@ import { NextMiddleware } from "next-api-middleware"; +import z from "zod"; -export const withPagination: NextMiddleware = async (req, res, next) => { - const { page } = req.query; - const pageNumber = parseInt(page as string); - const skip = pageNumber * 10; +const withPage = z.object({ + pageNumber: z + .string() + .min(1) + .default("1") + .transform((n) => parseInt(n)), + take: z + .string() + .min(10) + .max(100) + .default("10") + .transform((n) => parseInt(n)), +}); + +export const withPagination: NextMiddleware = async (req, _, next) => { + const { pageNumber, take } = withPage.parse(req.query); + const skip = pageNumber * take; req.pagination = { - take: 10, + take: take || 10, skip: skip || 0, }; await next(); From c8829fc08bf4da949ccf2a91ae8525bb20ca7b44 Mon Sep 17 00:00:00 2001 From: Agusti Fernandez Pardo Date: Fri, 8 Jul 2022 20:19:51 +0200 Subject: [PATCH 14/19] fix: rename pageNumber to page --- lib/helpers/withPagination.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/helpers/withPagination.ts b/lib/helpers/withPagination.ts index 2bcd219b33..d8c68fdd3b 100644 --- a/lib/helpers/withPagination.ts +++ b/lib/helpers/withPagination.ts @@ -2,7 +2,7 @@ import { NextMiddleware } from "next-api-middleware"; import z from "zod"; const withPage = z.object({ - pageNumber: z + page: z .string() .min(1) .default("1") @@ -16,8 +16,8 @@ const withPage = z.object({ }); export const withPagination: NextMiddleware = async (req, _, next) => { - const { pageNumber, take } = withPage.parse(req.query); - const skip = pageNumber * take; + const { page, take } = withPage.parse(req.query); + const skip = page * take; req.pagination = { take: take || 10, skip: skip || 0, From 6971ba249f0b3ca3289035a5c3fe519261e2f05b Mon Sep 17 00:00:00 2001 From: Agusti Fernandez Pardo Date: Fri, 8 Jul 2022 21:47:31 +0200 Subject: [PATCH 15/19] fix: add optional to pagination query's --- lib/helpers/withPagination.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/helpers/withPagination.ts b/lib/helpers/withPagination.ts index d8c68fdd3b..8e2da388f5 100644 --- a/lib/helpers/withPagination.ts +++ b/lib/helpers/withPagination.ts @@ -5,22 +5,24 @@ const withPage = z.object({ page: z .string() .min(1) + .optional() .default("1") .transform((n) => parseInt(n)), take: z .string() .min(10) .max(100) + .optional() .default("10") .transform((n) => parseInt(n)), }); export const withPagination: NextMiddleware = async (req, _, next) => { const { page, take } = withPage.parse(req.query); - const skip = page * take; + const skip = page * take || 0; req.pagination = { - take: take || 10, - skip: skip || 0, + take, + skip, }; await next(); }; From 3f6e7904cbbee8182fcda06240ca52e441c02ecd Mon Sep 17 00:00:00 2001 From: Agusti Fernandez Pardo Date: Fri, 8 Jul 2022 21:49:14 +0200 Subject: [PATCH 16/19] fix: no http_method middleware needed as already handled by defaultHandler --- pages/api/teams/[teamId]/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/api/teams/[teamId]/index.ts b/pages/api/teams/[teamId]/index.ts index ed397498d4..3644bbfd61 100644 --- a/pages/api/teams/[teamId]/index.ts +++ b/pages/api/teams/[teamId]/index.ts @@ -3,7 +3,7 @@ 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")( +export default withMiddleware()( withValidQueryTeamId( defaultHandler({ GET: import("./_get"), From f985bbd1ff4100e15a11a1197621f86f93c85fb7 Mon Sep 17 00:00:00 2001 From: Agusti Fernandez Pardo Date: Tue, 12 Jul 2022 20:01:25 +0200 Subject: [PATCH 17/19] fix: move patch to no then/catch --- pages/api/teams/[teamId]/_patch.ts | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/pages/api/teams/[teamId]/_patch.ts b/pages/api/teams/[teamId]/_patch.ts index 7550cd49b5..f395ca7414 100644 --- a/pages/api/teams/[teamId]/_patch.ts +++ b/pages/api/teams/[teamId]/_patch.ts @@ -39,7 +39,7 @@ export async function patchHandler(req: NextApiRequest, res: NextApiResponse) { }); 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)) + if (!isAdmin || !userTeamIds.includes(query.teamId)) throw new HttpError({ statusCode: 401, message: "Unauthorized" }); if (!safeBody.success) { { @@ -47,16 +47,11 @@ export async function patchHandler(req: NextApiRequest, res: NextApiResponse) { 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, - }) - ); + const data = await prisma.team.update({ where: { id: query.teamId }, data: safeBody.data }); + if (!data) throw new HttpError({ statusCode: 404, message: `Team with id: ${query.teamId} not found` }); + const team = schemaTeamReadPublic.parse(data); + if (!team) throw new HttpError({ statusCode: 401, message: `Your request body wasn't valid` }); + return { team }; } export default defaultResponder(patchHandler); From f87eb7d18e19be8fe24e0e1e1e2014707c947572 Mon Sep 17 00:00:00 2001 From: Agusti Fernandez Pardo Date: Tue, 12 Jul 2022 20:01:51 +0200 Subject: [PATCH 18/19] fix: lint add spaces --- pages/api/availability/_get.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/api/availability/_get.ts b/pages/api/availability/_get.ts index 6b6eecfe0b..590970fb70 100644 --- a/pages/api/availability/_get.ts +++ b/pages/api/availability/_get.ts @@ -64,7 +64,7 @@ async function handler(req: NextApiRequest) { statusCode: 401, message: "We had an issue retrieving all your members availabilities", }); -return settled; + return settled; } export default defaultResponder(handler); From 67fe26ec699dda3524b4b50cb3e6f21b24117a6f Mon Sep 17 00:00:00 2001 From: Agusti Fernandez Pardo Date: Tue, 12 Jul 2022 20:09:54 +0200 Subject: [PATCH 19/19] fix: lint issue import order --- pages/api/teams/_get.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pages/api/teams/_get.ts b/pages/api/teams/_get.ts index e961e59bb3..a716c2ec67 100644 --- a/pages/api/teams/_get.ts +++ b/pages/api/teams/_get.ts @@ -1,11 +1,10 @@ +import { Prisma } from "@prisma/client"; import type { NextApiRequest } from "next"; import { defaultResponder } from "@calcom/lib/server"; import { schemaTeamReadPublic } from "@lib/validations/team"; -import { Prisma } from "@prisma/client"; - /** * @swagger * /teams: