diff --git a/lib/validations/team.ts b/lib/validations/team.ts index 1efa3e84df..352b2ff431 100644 --- a/lib/validations/team.ts +++ b/lib/validations/team.ts @@ -2,10 +2,14 @@ import { z } from "zod"; import { _TeamModel as Team } from "@calcom/prisma/zod"; -export const schemaTeamBaseBodyParams = Team.omit({ id: true }).partial(); +export const schemaTeamBaseBodyParams = Team.omit({ id: true }).partial({ hideBranding: true }); const schemaTeamRequiredParams = z.object({}); -export const schemaTeamBodyParams = schemaTeamBaseBodyParams.merge(schemaTeamRequiredParams); +export const schemaTeamBodyParams = schemaTeamBaseBodyParams.merge(schemaTeamRequiredParams).strict(); + +export const schemaTeamUpdateBodyParams = schemaTeamBodyParams.partial(); export const schemaTeamReadPublic = Team.omit({}); + +export const schemaTeamsReadPublic = z.array(schemaTeamReadPublic); diff --git a/pages/api/teams/[teamId]/_auth-middleware.ts b/pages/api/teams/[teamId]/_auth-middleware.ts new file mode 100644 index 0000000000..347c3e3075 --- /dev/null +++ b/pages/api/teams/[teamId]/_auth-middleware.ts @@ -0,0 +1,16 @@ +import type { NextApiRequest } from "next"; + +import { schemaQueryTeamId } from "@lib/validations/shared/queryTeamId"; + +async function authMiddleware(req: NextApiRequest) { + const { userId, prisma, isAdmin } = req; + const { teamId } = schemaQueryTeamId.parse(req.query); + /** Admins can skip the ownership verification */ + if (isAdmin) return; + /** Non-members will see a 404 error which may or not be the desired behavior. */ + await prisma.team.findFirstOrThrow({ + where: { id: teamId, members: { some: { userId } } }, + }); +} + +export default authMiddleware; diff --git a/pages/api/teams/[teamId]/_delete.ts b/pages/api/teams/[teamId]/_delete.ts index 39b982caff..c372e5dd79 100644 --- a/pages/api/teams/[teamId]/_delete.ts +++ b/pages/api/teams/[teamId]/_delete.ts @@ -1,4 +1,4 @@ -import type { NextApiRequest, NextApiResponse } from "next"; +import type { NextApiRequest } from "next"; import { HttpError } from "@calcom/lib/http-error"; import { defaultResponder } from "@calcom/lib/server"; @@ -22,36 +22,22 @@ import { schemaQueryTeamId } from "@lib/validations/shared/queryTeamId"; * - teams * responses: * 201: - * description: OK, team removed successfuly + * description: OK, team removed successfully * 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 }, +export async function deleteHandler(req: NextApiRequest) { + const { prisma, query, userId } = req; + const { teamId } = schemaQueryTeamId.parse(query); + /** Only OWNERS can delete teams */ + const _team = await prisma.team.findFirst({ + where: { id: teamId, members: { some: { userId, role: "OWNER" } } }, }); - 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, - }) - ); + if (!_team) throw new HttpError({ statusCode: 401, message: "Unauthorized: OWNER required" }); + await prisma.team.delete({ where: { id: teamId } }); + return { message: `Team with id: ${teamId} deleted successfully` }; } export default defaultResponder(deleteHandler); diff --git a/pages/api/teams/[teamId]/_get.ts b/pages/api/teams/[teamId]/_get.ts index b2295d40fa..34b70e6e24 100644 --- a/pages/api/teams/[teamId]/_get.ts +++ b/pages/api/teams/[teamId]/_get.ts @@ -1,6 +1,6 @@ +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 { schemaQueryTeamId } from "@lib/validations/shared/queryTeamId"; @@ -31,18 +31,12 @@ import { schemaTeamReadPublic } from "@lib/validations/team"; */ 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 }; + const { teamId } = schemaQueryTeamId.parse(req.query); + const where: Prisma.TeamWhereInput = { id: teamId }; + // Non-admins can only query the teams they're part of + if (!isAdmin) where.members = { some: { userId } }; + const data = await prisma.team.findFirstOrThrow({ where }); + return { team: schemaTeamReadPublic.parse(data) }; } export default defaultResponder(getHandler); diff --git a/pages/api/teams/[teamId]/_patch.ts b/pages/api/teams/[teamId]/_patch.ts index f395ca7414..a8782d5a4c 100644 --- a/pages/api/teams/[teamId]/_patch.ts +++ b/pages/api/teams/[teamId]/_patch.ts @@ -1,10 +1,10 @@ -import type { NextApiRequest, NextApiResponse } from "next"; +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 { schemaTeamBodyParams, schemaTeamReadPublic } from "@lib/validations/team"; +import { schemaTeamReadPublic, schemaTeamUpdateBodyParams } from "@lib/validations/team"; /** * @swagger @@ -23,35 +23,23 @@ import { schemaTeamBodyParams, schemaTeamReadPublic } from "@lib/validations/tea * - teams * responses: * 201: - * description: OK, team edited successfuly + * description: OK, team edited successfully * 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 }, +export async function patchHandler(req: NextApiRequest) { + const { prisma, body, userId } = req; + const data = schemaTeamUpdateBodyParams.parse(body); + const { teamId } = schemaQueryTeamId.parse(req.query); + /** Only OWNERS and ADMINS can edit teams */ + const _team = await prisma.team.findFirst({ + where: { id: teamId, members: { some: { userId, role: { in: ["OWNER", "ADMIN"] } } } }, }); - 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; - } - } - 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 }; + if (!_team) throw new HttpError({ statusCode: 401, message: "Unauthorized: OWNER or ADMIN required" }); + const team = await prisma.team.update({ where: { id: teamId }, data }); + return { team: schemaTeamReadPublic.parse(team) }; } export default defaultResponder(patchHandler); diff --git a/pages/api/teams/[teamId]/index.ts b/pages/api/teams/[teamId]/index.ts index 3644bbfd61..7d92ee2906 100644 --- a/pages/api/teams/[teamId]/index.ts +++ b/pages/api/teams/[teamId]/index.ts @@ -1,14 +1,18 @@ -import { defaultHandler } from "@calcom/lib/server"; +import { NextApiRequest, NextApiResponse } from "next"; + +import { defaultHandler, defaultResponder } from "@calcom/lib/server"; import { withMiddleware } from "@lib/helpers/withMiddleware"; -import { withValidQueryTeamId } from "@lib/validations/shared/queryTeamId"; -export default withMiddleware()( - withValidQueryTeamId( - defaultHandler({ +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/teams/_get.ts b/pages/api/teams/_get.ts index a716c2ec67..9e9adebcdb 100644 --- a/pages/api/teams/_get.ts +++ b/pages/api/teams/_get.ts @@ -3,7 +3,7 @@ import type { NextApiRequest } from "next"; import { defaultResponder } from "@calcom/lib/server"; -import { schemaTeamReadPublic } from "@lib/validations/team"; +import { schemaTeamsReadPublic } from "@lib/validations/team"; /** * @swagger @@ -23,20 +23,11 @@ import { schemaTeamReadPublic } from "@lib/validations/team"; */ async function getHandler(req: NextApiRequest) { const { userId, prisma, isAdmin } = req; - const membershipWhere: Prisma.MembershipWhereInput = {}; + const where: Prisma.TeamWhereInput = {}; // 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 }; + if (!isAdmin) where.members = { some: { userId } }; + const data = await prisma.team.findMany({ where }); + return { teams: schemaTeamsReadPublic.parse(data) }; } export default defaultResponder(getHandler); diff --git a/pages/api/teams/_post.ts b/pages/api/teams/_post.ts index 53f66d7746..54b8706d55 100644 --- a/pages/api/teams/_post.ts +++ b/pages/api/teams/_post.ts @@ -1,6 +1,5 @@ import type { NextApiRequest } from "next"; -import { HttpError } from "@calcom/lib/http-error"; import { defaultResponder } from "@calcom/lib/server"; import { schemaMembershipPublic } from "@lib/validations/membership"; @@ -24,22 +23,22 @@ import { schemaTeamBodyParams, schemaTeamReadPublic } from "@lib/validations/tea */ 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" }); + const data = schemaTeamBodyParams.parse(body); + const team = await prisma.team.create({ + data: { + ...data, + members: { + // We're also creating the relation membership of team ownership in this call. + create: { userId, role: "OWNER", accepted: true }, + }, + }, + include: { members: true }, + }); req.statusCode = 201; // We are also returning the new ownership relation as owner besides team. return { - team, - owner, + team: schemaTeamReadPublic.parse(team), + owner: schemaMembershipPublic.parse(team.members[0]), message: "Team created successfully, we also made you the owner of this team", }; }