From 49b0f48be09916043801629445e973e689a1021c Mon Sep 17 00:00:00 2001 From: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com> Date: Fri, 6 Oct 2023 17:58:51 +0400 Subject: [PATCH] feat: add parentid eventtype API along with children eventTypeIds (#11714) * add parentId * Allow POST parentId * adds eventtypeid of children * add doc * adds userId in children * adds validations * adds docstring * fix status codes * check fix for owners --- apps/api/lib/validations/event-type.ts | 17 +++++- apps/api/pages/api/event-types/[id]/_get.ts | 1 + apps/api/pages/api/event-types/_get.ts | 1 + apps/api/pages/api/event-types/_post.ts | 12 ++++- .../_utils/checkParentEventOwnership.ts | 52 +++++++++++++++++++ .../event-types/_utils/checkUserMembership.ts | 52 +++++++++++++++++++ 6 files changed, 133 insertions(+), 2 deletions(-) create mode 100644 apps/api/pages/api/event-types/_utils/checkParentEventOwnership.ts create mode 100644 apps/api/pages/api/event-types/_utils/checkUserMembership.ts diff --git a/apps/api/lib/validations/event-type.ts b/apps/api/lib/validations/event-type.ts index 295884ab46..70823213bf 100644 --- a/apps/api/lib/validations/event-type.ts +++ b/apps/api/lib/validations/event-type.ts @@ -24,6 +24,11 @@ const hostSchema = _HostModel.pick({ userId: true, }); +export const childrenSchema = z.object({ + id: z.number().int(), + userId: z.number().int(), +}); + export const schemaEventTypeBaseBodyParams = EventType.pick({ title: true, description: true, @@ -45,6 +50,7 @@ export const schemaEventTypeBaseBodyParams = EventType.pick({ disableGuests: true, hideCalendarNotes: true, minimumBookingNotice: true, + parentId: true, beforeEventBuffer: true, afterEventBuffer: true, teamId: true, @@ -56,7 +62,12 @@ export const schemaEventTypeBaseBodyParams = EventType.pick({ bookingLimits: true, durationLimits: true, }) - .merge(z.object({ hosts: z.array(hostSchema).optional().default([]) })) + .merge( + z.object({ + children: z.array(childrenSchema).optional().default([]), + hosts: z.array(hostSchema).optional().default([]), + }) + ) .partial() .strict(); @@ -73,6 +84,7 @@ const schemaEventTypeCreateParams = z seatsShowAvailabilityCount: z.boolean().optional(), bookingFields: eventTypeBookingFields.optional(), scheduleId: z.number().optional(), + parentId: z.number().optional(), }) .strict(); @@ -125,6 +137,7 @@ export const schemaEventTypeReadPublic = EventType.pick({ price: true, currency: true, slotInterval: true, + parentId: true, successRedirectUrl: true, description: true, locations: true, @@ -137,6 +150,8 @@ export const schemaEventTypeReadPublic = EventType.pick({ durationLimits: true, }).merge( z.object({ + children: z.array(childrenSchema).optional().default([]), + hosts: z.array(hostSchema).optional().default([]), locations: z .array( z.object({ diff --git a/apps/api/pages/api/event-types/[id]/_get.ts b/apps/api/pages/api/event-types/[id]/_get.ts index 59c3c06786..b4de59b51a 100644 --- a/apps/api/pages/api/event-types/[id]/_get.ts +++ b/apps/api/pages/api/event-types/[id]/_get.ts @@ -52,6 +52,7 @@ export async function getHandler(req: NextApiRequest) { team: { select: { slug: true } }, users: true, owner: { select: { username: true, id: true } }, + children: { select: { id: true, userId: true } }, }, }); await checkPermissions(req, eventType); diff --git a/apps/api/pages/api/event-types/_get.ts b/apps/api/pages/api/event-types/_get.ts index 66d84cf621..a58e6422ce 100644 --- a/apps/api/pages/api/event-types/_get.ts +++ b/apps/api/pages/api/event-types/_get.ts @@ -46,6 +46,7 @@ async function getHandler(req: NextApiRequest) { team: { select: { slug: true } }, users: true, owner: { select: { username: true, id: true } }, + children: { select: { id: true, userId: true } }, }, }); // this really should return [], but backwards compatibility.. diff --git a/apps/api/pages/api/event-types/_post.ts b/apps/api/pages/api/event-types/_post.ts index 852b20158f..075ed4c71a 100644 --- a/apps/api/pages/api/event-types/_post.ts +++ b/apps/api/pages/api/event-types/_post.ts @@ -6,7 +6,9 @@ import { defaultResponder } from "@calcom/lib/server"; import { schemaEventTypeCreateBodyParams, schemaEventTypeReadPublic } from "~/lib/validations/event-type"; +import checkParentEventOwnership from "./_utils/checkParentEventOwnership"; import checkTeamEventEditPermission from "./_utils/checkTeamEventEditPermission"; +import checkUserMembership from "./_utils/checkUserMembership"; import ensureOnlyMembersAsHosts from "./_utils/ensureOnlyMembersAsHosts"; /** @@ -118,10 +120,13 @@ import ensureOnlyMembersAsHosts from "./_utils/ensureOnlyMembersAsHosts"; * schedulingType: * type: string * description: The type of scheduling if a Team event. Required for team events only - * enum: [ROUND_ROBIN, COLLECTIVE] + * enum: [ROUND_ROBIN, COLLECTIVE, MANAGED] * price: * type: integer * description: Price of the event type booking + * parentId: + * type: integer + * description: EventTypeId of the parent managed event * currency: * type: string * description: Currency acronym. Eg- usd, eur, gbp, etc. @@ -276,6 +281,11 @@ async function postHandler(req: NextApiRequest) { await checkPermissions(req); + if (parsedBody.parentId) { + await checkParentEventOwnership(parsedBody.parentId, userId); + await checkUserMembership(parsedBody.parentId, parsedBody.userId); + } + if (isAdmin && parsedBody.userId) { data = { ...parsedBody, users: { connect: { id: parsedBody.userId } } }; } diff --git a/apps/api/pages/api/event-types/_utils/checkParentEventOwnership.ts b/apps/api/pages/api/event-types/_utils/checkParentEventOwnership.ts new file mode 100644 index 0000000000..15ada70097 --- /dev/null +++ b/apps/api/pages/api/event-types/_utils/checkParentEventOwnership.ts @@ -0,0 +1,52 @@ +import { HttpError } from "@calcom/lib/http-error"; + +/** + * Checks if a user, identified by the provided userId, has ownership (or admin rights) over + * the team associated with the event type identified by the parentId. + * + * @param parentId - The ID of the parent event type. + * @param userId - The ID of the user. + * + * @throws {HttpError} If the parent event type is not found, + * if the parent event type doesn't belong to any team, + * or if the user doesn't have ownership or admin rights to the associated team. + */ +export default async function checkParentEventOwnership(parentId: number, userId: number) { + const parentEventType = await prisma.eventType.findUnique({ + where: { + id: parentId, + }, + select: { + teamId: true, + }, + }); + + if (!parentEventType) { + throw new HttpError({ + statusCode: 404, + message: "Parent event type not found.", + }); + } + + if (!parentEventType.teamId) { + throw new HttpError({ + statusCode: 400, + message: "This event type is not capable of having children", + }); + } + + const teamMember = await prisma.membership.findFirst({ + where: { + teamId: parentEventType.teamId, + userId: userId, + OR: [{ role: "OWNER" }, { role: "ADMIN" }], + }, + }); + + if (!teamMember) { + throw new HttpError({ + statusCode: 403, + message: "User is not authorized to access the team to which the parent event type belongs.", + }); + } +} diff --git a/apps/api/pages/api/event-types/_utils/checkUserMembership.ts b/apps/api/pages/api/event-types/_utils/checkUserMembership.ts new file mode 100644 index 0000000000..df819bc95e --- /dev/null +++ b/apps/api/pages/api/event-types/_utils/checkUserMembership.ts @@ -0,0 +1,52 @@ +import { HttpError } from "@calcom/lib/http-error"; + +/** + * Checks if a user, identified by the provided userId, is a member of the team associated + * with the event type identified by the parentId. + * + * @param parentId - The ID of the event type. + * @param userId - The ID of the user. + * + * @throws {HttpError} If the event type is not found, + * if the event type doesn't belong to any team, + * or if the user isn't a member of the associated team. + */ +export default async function checkUserMembership(parentId: number, userId: number) { + const parentEventType = await prisma.eventType.findUnique({ + where: { + id: parentId, + }, + select: { + teamId: true, + }, + }); + + if (!parentEventType) { + throw new HttpError({ + statusCode: 404, + message: "Event type not found.", + }); + } + + if (!parentEventType.teamId) { + throw new HttpError({ + statusCode: 400, + message: "This event type is not capable of having children.", + }); + } + + const teamMember = await prisma.membership.findFirst({ + where: { + teamId: parentEventType.teamId, + userId: userId, + accepted: true, + }, + }); + + if (!teamMember) { + throw new HttpError({ + statusCode: 400, + message: "User is not a team member.", + }); + } +}