From cd03f5a821e4271df3291b4f1603d1574de9f269 Mon Sep 17 00:00:00 2001 From: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com> Date: Wed, 8 Jun 2022 13:06:28 +0530 Subject: [PATCH 01/70] Adds team event type check --- pages/api/hooks/[id].ts | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/pages/api/hooks/[id].ts b/pages/api/hooks/[id].ts index 32a81e7b12..3a14efa592 100644 --- a/pages/api/hooks/[id].ts +++ b/pages/api/hooks/[id].ts @@ -95,6 +95,27 @@ export async function WebhookById( return; } } + if (safeBody.data.eventTypeId) { + const team = await ctx.prisma.team.findFirst({ + where: { + eventTypes: { + some: { + id: safeBody.data.eventTypeId, + }, + }, + }, + include: { + members: true, + }, + }); + + // Team should be available and the user should be a member of the team + if (!team?.members.some((membership) => membership.userId === userId)) { + throw new TRPCError({ + code: "UNAUTHORIZED", + }); + } + } await prisma.webhook .update({ where: { id: safeQuery.data.id }, data: safeBody.data }) .then((data) => schemaWebhookReadPublic.parse(data)) From bae84f2ce4e7daa9fcc3c8984a18997946cdab20 Mon Sep 17 00:00:00 2001 From: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com> Date: Wed, 8 Jun 2022 13:12:05 +0530 Subject: [PATCH 02/70] Added team event check to POST --- pages/api/hooks/index.ts | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/pages/api/hooks/index.ts b/pages/api/hooks/index.ts index 422a30e419..e92a681080 100644 --- a/pages/api/hooks/index.ts +++ b/pages/api/hooks/index.ts @@ -61,6 +61,27 @@ async function createOrlistAllWebhooks( res.status(400).json({ message: "Invalid request body" }); return; } + if (safe.data.eventTypeId) { + const team = await ctx.prisma.team.findFirst({ + where: { + eventTypes: { + some: { + id: safe.data.eventTypeId, + }, + }, + }, + include: { + members: true, + }, + }); + + // Team should be available and the user should be a member of the team + if (!team?.members.some((membership) => membership.userId === userId)) { + throw new TRPCError({ + code: "UNAUTHORIZED", + }); + } + } const data = await prisma.webhook.create({ data: { id: uuidv4(), ...safe.data, userId } }); if (data) res.status(201).json({ webhook: data, message: "Webhook created successfully" }); else From ef3838b4764cf57d9928fa6581f814652627248b Mon Sep 17 00:00:00 2001 From: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com> Date: Wed, 8 Jun 2022 13:20:09 +0530 Subject: [PATCH 03/70] hotfix ctx removal --- pages/api/hooks/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/api/hooks/index.ts b/pages/api/hooks/index.ts index e92a681080..f3f3257499 100644 --- a/pages/api/hooks/index.ts +++ b/pages/api/hooks/index.ts @@ -62,7 +62,7 @@ async function createOrlistAllWebhooks( return; } if (safe.data.eventTypeId) { - const team = await ctx.prisma.team.findFirst({ + const team = await prisma.team.findFirst({ where: { eventTypes: { some: { From b881312f1038ea9b67afa52c6a01a9c3c07f8028 Mon Sep 17 00:00:00 2001 From: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com> Date: Wed, 8 Jun 2022 13:20:44 +0530 Subject: [PATCH 04/70] hotfix ctx removal --- pages/api/hooks/[id].ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/api/hooks/[id].ts b/pages/api/hooks/[id].ts index 3a14efa592..7debc7ac8d 100644 --- a/pages/api/hooks/[id].ts +++ b/pages/api/hooks/[id].ts @@ -96,7 +96,7 @@ export async function WebhookById( } } if (safeBody.data.eventTypeId) { - const team = await ctx.prisma.team.findFirst({ + const team = await prisma.team.findFirst({ where: { eventTypes: { some: { From 9b1dffbdb9e64969c79d87c892e4a0083e8e64f1 Mon Sep 17 00:00:00 2001 From: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com> Date: Wed, 8 Jun 2022 13:27:48 +0530 Subject: [PATCH 05/70] 401 return fix --- pages/api/hooks/index.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pages/api/hooks/index.ts b/pages/api/hooks/index.ts index f3f3257499..ec85ab13df 100644 --- a/pages/api/hooks/index.ts +++ b/pages/api/hooks/index.ts @@ -77,9 +77,8 @@ async function createOrlistAllWebhooks( // Team should be available and the user should be a member of the team if (!team?.members.some((membership) => membership.userId === userId)) { - throw new TRPCError({ - code: "UNAUTHORIZED", - }); + res.status(401).json({ message: "Unauthorized" }); + return; } } const data = await prisma.webhook.create({ data: { id: uuidv4(), ...safe.data, userId } }); From db6f5cad1512ac6d6efc8b4c9368634ebcde348b Mon Sep 17 00:00:00 2001 From: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com> Date: Wed, 8 Jun 2022 13:28:22 +0530 Subject: [PATCH 06/70] 401 message fix --- pages/api/hooks/[id].ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pages/api/hooks/[id].ts b/pages/api/hooks/[id].ts index 7debc7ac8d..8f81fc62d1 100644 --- a/pages/api/hooks/[id].ts +++ b/pages/api/hooks/[id].ts @@ -111,9 +111,8 @@ export async function WebhookById( // Team should be available and the user should be a member of the team if (!team?.members.some((membership) => membership.userId === userId)) { - throw new TRPCError({ - code: "UNAUTHORIZED", - }); + res.status(401).json({ message: "Unauthorized" }); + return; } } await prisma.webhook From d569296654495621b0475a4636fa9ac937cccd56 Mon Sep 17 00:00:00 2001 From: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com> Date: Thu, 9 Jun 2022 17:09:20 +0530 Subject: [PATCH 07/70] Add webhook trigger --init --- pages/api/bookings/index.ts | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/pages/api/bookings/index.ts b/pages/api/bookings/index.ts index 40f24283ee..1951e4475d 100644 --- a/pages/api/bookings/index.ts +++ b/pages/api/bookings/index.ts @@ -81,7 +81,31 @@ async function createOrlistAllBookings( const data = await prisma.booking.create({ data: { ...safe.data } }); const booking = schemaBookingReadPublic.parse(data); - if (booking) res.status(201).json({ booking, message: "Booking created successfully" }); + if (booking) { + res.status(201).json({ booking, message: "Booking created successfully" }); + + // Send Webhook call if hooked to BOOKING_CREATED & BOOKING_RESCHEDULED +// const eventTrigger: WebhookTriggerEvents = rescheduleUid ? "BOOKING_RESCHEDULED" : "BOOKING_CREATED"; +// const subscriberOptions = { +// userId: user.id, +// eventTypeId, +// triggerEvent: eventTrigger, +// }; + +// const subscribers = await getSubscribers(subscriberOptions); +// const bookingId = booking?.id; +// const promises = subscribers.map((sub) => +// sendPayload(eventTrigger, new Date().toISOString(), sub, { +// ...evt, +// bookingId, +// rescheduleUid, +// metadata: reqBody.metadata, +// }).catch((e) => { +// console.error(`Error executing webhook for event: ${eventTrigger}, URL: ${sub.subscriberUrl}`, e); +// }) +// ); +// await Promise.all(promises); + } else (error: Error) => { console.log(error); From cd482cd91a44a4631bb0be0bce4b82968c4c5068 Mon Sep 17 00:00:00 2001 From: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com> Date: Fri, 10 Jun 2022 11:40:38 +0530 Subject: [PATCH 08/70] Add getWebhook function --- lib/utils/webhookSubscriptions.ts | 42 +++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 lib/utils/webhookSubscriptions.ts diff --git a/lib/utils/webhookSubscriptions.ts b/lib/utils/webhookSubscriptions.ts new file mode 100644 index 0000000000..bdf9d95576 --- /dev/null +++ b/lib/utils/webhookSubscriptions.ts @@ -0,0 +1,42 @@ +import { WebhookTriggerEvents } from "@prisma/client"; + +import prisma from "@calcom/prisma"; + +export type GetSubscriberOptions = { + userId: number; + eventTypeId: number; + triggerEvent: WebhookTriggerEvents; +}; + +const getWebhooks = async (options: GetSubscriberOptions) => { + const { userId, eventTypeId } = options; + const allWebhooks = await prisma.webhook.findMany({ + where: { + OR: [ + { + userId, + }, + { + eventTypeId, + }, + ], + AND: { + eventTriggers: { + has: options.triggerEvent, + }, + active: { + equals: true, + }, + }, + }, + select: { + subscriberUrl: true, + payloadTemplate: true, + appId: true, + }, + }); + + return allWebhooks; +}; + +export default getWebhooks; From 664e697d40bc24232102221ed52443562f0b01b5 Mon Sep 17 00:00:00 2001 From: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com> Date: Fri, 10 Jun 2022 11:42:01 +0530 Subject: [PATCH 09/70] add sendPayload file --- lib/utils/sendPayload.ts | 77 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 lib/utils/sendPayload.ts diff --git a/lib/utils/sendPayload.ts b/lib/utils/sendPayload.ts new file mode 100644 index 0000000000..62cc3f96d2 --- /dev/null +++ b/lib/utils/sendPayload.ts @@ -0,0 +1,77 @@ +import { Webhook } from "@prisma/client"; +import { compile } from "handlebars"; + +import type { CalendarEvent } from "@calcom/types/Calendar"; + +type ContentType = "application/json" | "application/x-www-form-urlencoded"; + +function applyTemplate(template: string, data: CalendarEvent, contentType: ContentType) { + const compiled = compile(template)(data); + if (contentType === "application/json") { + return JSON.stringify(jsonParse(compiled)); + } + return compiled; +} + +function jsonParse(jsonString: string) { + try { + return JSON.parse(jsonString); + } catch (e) { + // don't do anything. + } + return false; +} + +const sendPayload = async ( + triggerEvent: string, + createdAt: string, + webhook: Pick, + data: CalendarEvent & { + metadata?: { [key: string]: string }; + rescheduleUid?: string; + bookingId?: number; + } +) => { + const { subscriberUrl, appId, payloadTemplate: template } = webhook; + if (!subscriberUrl || !data) { + throw new Error("Missing required elements to send webhook payload."); + } + + const contentType = + !template || jsonParse(template) ? "application/json" : "application/x-www-form-urlencoded"; + + data.description = data.description || data.additionalNotes; + + let body; + + /* Zapier id is hardcoded in the DB, we send the raw data for this case */ + if (appId === "zapier") { + body = JSON.stringify(data); + } else if (template) { + body = applyTemplate(template, data, contentType); + } else { + body = JSON.stringify({ + triggerEvent: triggerEvent, + createdAt: createdAt, + payload: data, + }); + } + + const response = await fetch(subscriberUrl, { + method: "POST", + headers: { + "Content-Type": contentType, + }, + body, + }); + + const text = await response.text(); + + return { + ok: response.ok, + status: response.status, + message: text, + }; +}; + +export default sendPayload; From 542beb007938450ab0365aa4c6367a27cf6b5386 Mon Sep 17 00:00:00 2001 From: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com> Date: Fri, 10 Jun 2022 11:46:51 +0530 Subject: [PATCH 10/70] adjust booking file --- pages/api/bookings/index.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pages/api/bookings/index.ts b/pages/api/bookings/index.ts index 1951e4475d..2ee5c2da1c 100644 --- a/pages/api/bookings/index.ts +++ b/pages/api/bookings/index.ts @@ -84,15 +84,17 @@ async function createOrlistAllBookings( if (booking) { res.status(201).json({ booking, message: "Booking created successfully" }); - // Send Webhook call if hooked to BOOKING_CREATED & BOOKING_RESCHEDULED -// const eventTrigger: WebhookTriggerEvents = rescheduleUid ? "BOOKING_RESCHEDULED" : "BOOKING_CREATED"; +// Create Calendar Event for webhook payload + +// Send Webhook call if hooked to BOOKING_CREATED & BOOKING_RESCHEDULED +// const eventTrigger = WebhookTriggerEvents.BOOKING_CREATED; // const subscriberOptions = { // userId: user.id, // eventTypeId, // triggerEvent: eventTrigger, // }; -// const subscribers = await getSubscribers(subscriberOptions); +// const subscribers = await getWebhooks(subscriberOptions); // const bookingId = booking?.id; // const promises = subscribers.map((sub) => // sendPayload(eventTrigger, new Date().toISOString(), sub, { From 804de678680f2fa991c904e1f0fb62f96a53bee5 Mon Sep 17 00:00:00 2001 From: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com> Date: Fri, 10 Jun 2022 12:17:00 +0530 Subject: [PATCH 11/70] Add eventName file --- lib/utils/eventName.ts | 62 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 lib/utils/eventName.ts diff --git a/lib/utils/eventName.ts b/lib/utils/eventName.ts new file mode 100644 index 0000000000..5d7865fe94 --- /dev/null +++ b/lib/utils/eventName.ts @@ -0,0 +1,62 @@ +import { TFunction } from "next-i18next"; + +type EventNameObjectType = { + attendeeName: string; + eventType: string; + eventName?: string | null; + host: string; + location?: string; + t: TFunction; +}; + +export function getEventName(eventNameObj: EventNameObjectType, forAttendeeView = false) { + if (!eventNameObj.eventName) + return eventNameObj.t("event_between_users", { + eventName: eventNameObj.eventType, + host: eventNameObj.host, + attendeeName: eventNameObj.attendeeName, + }); + + let eventName = eventNameObj.eventName; + let locationString = ""; + + if (eventNameObj.eventName.includes("{LOCATION}")) { + switch (eventNameObj.location) { + case "inPerson": + locationString = "In Person"; + break; + case "userPhone": + case "phone": + locationString = "Phone"; + break; + case "integrations:daily": + locationString = "Cal Video"; + break; + case "integrations:zoom": + locationString = "Zoom"; + break; + case "integrations:huddle01": + locationString = "Huddle01"; + break; + case "integrations:tandem": + locationString = "Tandem"; + break; + case "integrations:office365_video": + locationString = "MS Teams"; + break; + case "integrations:jitsi": + locationString = "Jitsi"; + break; + } + eventName = eventName.replace("{LOCATION}", locationString); + } + + return ( + eventName + // Need this for compatibility with older event names + .replace("{USER}", eventNameObj.attendeeName) + .replace("{ATTENDEE}", eventNameObj.attendeeName) + .replace("{HOST}", eventNameObj.host) + .replace("{HOST/ATTENDEE}", forAttendeeView ? eventNameObj.host : eventNameObj.attendeeName) + ); +} From e3822c50d0998312929a3c6b58e9c728c16a58ed Mon Sep 17 00:00:00 2001 From: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com> Date: Fri, 10 Jun 2022 12:43:41 +0530 Subject: [PATCH 12/70] Update booking file with webhook payload --- pages/api/bookings/index.ts | 78 +++++++++++++++++++++++++++---------- 1 file changed, 57 insertions(+), 21 deletions(-) diff --git a/pages/api/bookings/index.ts b/pages/api/bookings/index.ts index 2ee5c2da1c..f3b9e8c027 100644 --- a/pages/api/bookings/index.ts +++ b/pages/api/bookings/index.ts @@ -2,9 +2,14 @@ import type { NextApiRequest, NextApiResponse } from "next"; import prisma from "@calcom/prisma"; +import { WebhookTriggerEvents } from "@prisma/client"; + import { withMiddleware } from "@lib/helpers/withMiddleware"; +import sendPayload from "@lib/utils/sendPayload"; +import getWebhooks from "@lib/utils/webhookSubscriptions"; import { BookingResponse, BookingsResponse } from "@lib/types"; import { schemaBookingCreateBodyParams, schemaBookingReadPublic } from "@lib/validations/booking"; +import { schemaEventTypeReadPublic } from "@lib/validations/event-type"; async function createOrlistAllBookings( { method, body, userId }: NextApiRequest, @@ -84,29 +89,60 @@ async function createOrlistAllBookings( if (booking) { res.status(201).json({ booking, message: "Booking created successfully" }); -// Create Calendar Event for webhook payload + // Create Calendar Event for webhook payload + const eventType = await prisma.eventType + .findUnique({ where: { id: booking?.eventTypeId } }) + .then((data) => schemaEventTypeReadPublic.parse(data)) + .then((event_type) => res.status(200).json({ event_type })) + .catch((error: Error) => + res.status(404).json({ + message: `EventType with id: ${booking?.eventTypeId} not found`, + error, + }) + ); + const evt = { + type: eventType.title, + title: booking.title, + description: "", + additionalNotes: "", + customInputs: {}, + startTime: booking.startTime, + endTime: booking.endTime, + organizer: { + name: "", + email: "", + timezone: "", + language: { + locale: "en" + } + }, + attendees: [], + location: "", + destinationCalendar: null, + hideCalendar: false, + uid: booking.uid, + metadata: {} + }; -// Send Webhook call if hooked to BOOKING_CREATED & BOOKING_RESCHEDULED -// const eventTrigger = WebhookTriggerEvents.BOOKING_CREATED; -// const subscriberOptions = { -// userId: user.id, -// eventTypeId, -// triggerEvent: eventTrigger, -// }; + // Send Webhook call if hooked to BOOKING_CREATED + const triggerEvent = WebhookTriggerEvents.BOOKING_CREATED; + const subscriberOptions = { + userId, + eventTypeId: eventType.id, + triggerEvent, + }; -// const subscribers = await getWebhooks(subscriberOptions); -// const bookingId = booking?.id; -// const promises = subscribers.map((sub) => -// sendPayload(eventTrigger, new Date().toISOString(), sub, { -// ...evt, -// bookingId, -// rescheduleUid, -// metadata: reqBody.metadata, -// }).catch((e) => { -// console.error(`Error executing webhook for event: ${eventTrigger}, URL: ${sub.subscriberUrl}`, e); -// }) -// ); -// await Promise.all(promises); + const subscribers = await getWebhooks(subscriberOptions); + const bookingId = booking?.id; + const promises = subscribers.map((sub) => + sendPayload(eventTrigger, new Date().toISOString(), sub, { + ...evt, + bookingId, + }).catch((e) => { + console.error(`Error executing webhook for event: ${eventTrigger}, URL: ${sub.subscriberUrl}`, e); + }) + ); + await Promise.all(promises); } else (error: Error) => { From 4f3668100691dfb0f4fe8c622fd8bce513eed679 Mon Sep 17 00:00:00 2001 From: Syed Ali Shahbaz Date: Fri, 10 Jun 2022 12:51:24 +0530 Subject: [PATCH 13/70] --fix --- pages/api/bookings/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pages/api/bookings/index.ts b/pages/api/bookings/index.ts index f3b9e8c027..2a11672e32 100644 --- a/pages/api/bookings/index.ts +++ b/pages/api/bookings/index.ts @@ -135,11 +135,11 @@ async function createOrlistAllBookings( const subscribers = await getWebhooks(subscriberOptions); const bookingId = booking?.id; const promises = subscribers.map((sub) => - sendPayload(eventTrigger, new Date().toISOString(), sub, { + sendPayload(triggerEvent, new Date().toISOString(), sub, { ...evt, bookingId, }).catch((e) => { - console.error(`Error executing webhook for event: ${eventTrigger}, URL: ${sub.subscriberUrl}`, e); + console.error(`Error executing webhook for event: ${triggerEvent}, URL: ${sub.subscriberUrl}`, e); }) ); await Promise.all(promises); From f9c20bc8ce3544508585d8bb0fece077994da833 Mon Sep 17 00:00:00 2001 From: Syed Ali Shahbaz Date: Fri, 10 Jun 2022 12:53:59 +0530 Subject: [PATCH 14/70] --fix null estimated value --- pages/api/bookings/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/api/bookings/index.ts b/pages/api/bookings/index.ts index 2a11672e32..73c4e8d12b 100644 --- a/pages/api/bookings/index.ts +++ b/pages/api/bookings/index.ts @@ -91,7 +91,7 @@ async function createOrlistAllBookings( // Create Calendar Event for webhook payload const eventType = await prisma.eventType - .findUnique({ where: { id: booking?.eventTypeId } }) + .findUnique({ where: { id: booking.eventTypeId as number } }) .then((data) => schemaEventTypeReadPublic.parse(data)) .then((event_type) => res.status(200).json({ event_type })) .catch((error: Error) => From 4431142d4e540b8d0135241cff16d0c7d3fbff61 Mon Sep 17 00:00:00 2001 From: Syed Ali Shahbaz Date: Fri, 10 Jun 2022 12:58:04 +0530 Subject: [PATCH 15/70] remove unnecessary return --- pages/api/bookings/index.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pages/api/bookings/index.ts b/pages/api/bookings/index.ts index 73c4e8d12b..322543078f 100644 --- a/pages/api/bookings/index.ts +++ b/pages/api/bookings/index.ts @@ -93,10 +93,9 @@ async function createOrlistAllBookings( const eventType = await prisma.eventType .findUnique({ where: { id: booking.eventTypeId as number } }) .then((data) => schemaEventTypeReadPublic.parse(data)) - .then((event_type) => res.status(200).json({ event_type })) .catch((error: Error) => res.status(404).json({ - message: `EventType with id: ${booking?.eventTypeId} not found`, + message: `EventType with id: ${booking.eventTypeId} not found`, error, }) ); From adab79040f37e83e8ddc5aa4b59f079a21cacbc3 Mon Sep 17 00:00:00 2001 From: Syed Ali Shahbaz Date: Fri, 10 Jun 2022 13:06:22 +0530 Subject: [PATCH 16/70] --fix --- pages/api/bookings/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/api/bookings/index.ts b/pages/api/bookings/index.ts index 322543078f..2db7abac29 100644 --- a/pages/api/bookings/index.ts +++ b/pages/api/bookings/index.ts @@ -100,7 +100,7 @@ async function createOrlistAllBookings( }) ); const evt = { - type: eventType.title, + type: eventType?.title || booking.title, title: booking.title, description: "", additionalNotes: "", From cd3a80006c6eb3b44a17273439f729051406f613 Mon Sep 17 00:00:00 2001 From: Syed Ali Shahbaz Date: Fri, 10 Jun 2022 13:10:16 +0530 Subject: [PATCH 17/70] --fix --- pages/api/bookings/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/api/bookings/index.ts b/pages/api/bookings/index.ts index 2db7abac29..1b23a5e3f3 100644 --- a/pages/api/bookings/index.ts +++ b/pages/api/bookings/index.ts @@ -127,7 +127,7 @@ async function createOrlistAllBookings( const triggerEvent = WebhookTriggerEvents.BOOKING_CREATED; const subscriberOptions = { userId, - eventTypeId: eventType.id, + eventTypeId: booking.eventTypeId as number, triggerEvent, }; From a8ad052a037246219eb15cdb82d8dee798b8b057 Mon Sep 17 00:00:00 2001 From: Syed Ali Shahbaz Date: Fri, 10 Jun 2022 13:16:58 +0530 Subject: [PATCH 18/70] --convert date to string in calendar event --- pages/api/bookings/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pages/api/bookings/index.ts b/pages/api/bookings/index.ts index 1b23a5e3f3..0a421298e1 100644 --- a/pages/api/bookings/index.ts +++ b/pages/api/bookings/index.ts @@ -105,8 +105,8 @@ async function createOrlistAllBookings( description: "", additionalNotes: "", customInputs: {}, - startTime: booking.startTime, - endTime: booking.endTime, + startTime: booking.startTime.toISOString(), + endTime: booking.endTime.toISOString(), organizer: { name: "", email: "", From ef5bbfe4f96c65480beb7bc88519464368700cdf Mon Sep 17 00:00:00 2001 From: Syed Ali Shahbaz Date: Fri, 10 Jun 2022 13:22:06 +0530 Subject: [PATCH 19/70] fixed timeZone key --- pages/api/bookings/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/api/bookings/index.ts b/pages/api/bookings/index.ts index 0a421298e1..a8eaf90662 100644 --- a/pages/api/bookings/index.ts +++ b/pages/api/bookings/index.ts @@ -110,7 +110,7 @@ async function createOrlistAllBookings( organizer: { name: "", email: "", - timezone: "", + timeZone: "", language: { locale: "en" } From f1c98181d2daee95d3e16ba25406010914aa8e78 Mon Sep 17 00:00:00 2001 From: Syed Ali Shahbaz Date: Fri, 10 Jun 2022 13:30:43 +0530 Subject: [PATCH 20/70] tfunction added --- pages/api/bookings/index.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pages/api/bookings/index.ts b/pages/api/bookings/index.ts index a8eaf90662..f5748daf67 100644 --- a/pages/api/bookings/index.ts +++ b/pages/api/bookings/index.ts @@ -1,5 +1,6 @@ -import type { NextApiRequest, NextApiResponse } from "next"; +import { getTranslation } from "@calcom/lib/server/i18n"; +import type { NextApiRequest, NextApiResponse } from "next"; import prisma from "@calcom/prisma"; import { WebhookTriggerEvents } from "@prisma/client"; @@ -99,6 +100,7 @@ async function createOrlistAllBookings( error, }) ); + const fallbackTfunction = await getTranslation("en", "common"); const evt = { type: eventType?.title || booking.title, title: booking.title, @@ -112,6 +114,7 @@ async function createOrlistAllBookings( email: "", timeZone: "", language: { + translate: fallbackTfunction, locale: "en" } }, From 54353f99d139395826a270843ad57abd73c38d1d Mon Sep 17 00:00:00 2001 From: Syed Ali Shahbaz Date: Fri, 10 Jun 2022 13:32:47 +0530 Subject: [PATCH 21/70] Calendar event changed to any for loose allowance --- lib/utils/sendPayload.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/utils/sendPayload.ts b/lib/utils/sendPayload.ts index 62cc3f96d2..925be7486a 100644 --- a/lib/utils/sendPayload.ts +++ b/lib/utils/sendPayload.ts @@ -1,11 +1,11 @@ import { Webhook } from "@prisma/client"; import { compile } from "handlebars"; -import type { CalendarEvent } from "@calcom/types/Calendar"; +// import type { CalendarEvent } from "@calcom/types/Calendar"; Add this to make it strict, change data: any to CalendarEvent type type ContentType = "application/json" | "application/x-www-form-urlencoded"; -function applyTemplate(template: string, data: CalendarEvent, contentType: ContentType) { +function applyTemplate(template: string, data: any, contentType: ContentType) { const compiled = compile(template)(data); if (contentType === "application/json") { return JSON.stringify(jsonParse(compiled)); From 8339d339a1ac3f149045e7fa0a180107ea585ce7 Mon Sep 17 00:00:00 2001 From: Syed Ali Shahbaz Date: Fri, 10 Jun 2022 13:38:45 +0530 Subject: [PATCH 22/70] prettier and payload fix --- lib/utils/sendPayload.ts | 2 +- pages/api/bookings/index.ts | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/utils/sendPayload.ts b/lib/utils/sendPayload.ts index 925be7486a..f40c0ad743 100644 --- a/lib/utils/sendPayload.ts +++ b/lib/utils/sendPayload.ts @@ -26,7 +26,7 @@ const sendPayload = async ( triggerEvent: string, createdAt: string, webhook: Pick, - data: CalendarEvent & { + data: any & { metadata?: { [key: string]: string }; rescheduleUid?: string; bookingId?: number; diff --git a/pages/api/bookings/index.ts b/pages/api/bookings/index.ts index f5748daf67..7abd8ae5e7 100644 --- a/pages/api/bookings/index.ts +++ b/pages/api/bookings/index.ts @@ -1,14 +1,14 @@ -import { getTranslation } from "@calcom/lib/server/i18n"; - -import type { NextApiRequest, NextApiResponse } from "next"; import prisma from "@calcom/prisma"; import { WebhookTriggerEvents } from "@prisma/client"; +import { getTranslation } from "@calcom/lib/server/i18n"; + +import type { NextApiRequest, NextApiResponse } from "next"; import { withMiddleware } from "@lib/helpers/withMiddleware"; +import { BookingResponse, BookingsResponse } from "@lib/types"; import sendPayload from "@lib/utils/sendPayload"; import getWebhooks from "@lib/utils/webhookSubscriptions"; -import { BookingResponse, BookingsResponse } from "@lib/types"; import { schemaBookingCreateBodyParams, schemaBookingReadPublic } from "@lib/validations/booking"; import { schemaEventTypeReadPublic } from "@lib/validations/event-type"; @@ -115,7 +115,7 @@ async function createOrlistAllBookings( timeZone: "", language: { translate: fallbackTfunction, - locale: "en" + locale: "en", } }, attendees: [], From 5453af6d6ee189185b0c467157013d821825d7cd Mon Sep 17 00:00:00 2001 From: Syed Ali Shahbaz Date: Fri, 10 Jun 2022 15:57:31 +0530 Subject: [PATCH 23/70] prettier fix attempt --- pages/api/bookings/index.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/pages/api/bookings/index.ts b/pages/api/bookings/index.ts index 7abd8ae5e7..4db3f3051a 100644 --- a/pages/api/bookings/index.ts +++ b/pages/api/bookings/index.ts @@ -1,10 +1,9 @@ -import prisma from "@calcom/prisma"; - import { WebhookTriggerEvents } from "@prisma/client"; -import { getTranslation } from "@calcom/lib/server/i18n"; - import type { NextApiRequest, NextApiResponse } from "next"; +import { getTranslation } from "@calcom/lib/server/i18n"; +import prisma from "@calcom/prisma"; + import { withMiddleware } from "@lib/helpers/withMiddleware"; import { BookingResponse, BookingsResponse } from "@lib/types"; import sendPayload from "@lib/utils/sendPayload"; From 301c132815bd011f47efae13f21b053704664158 Mon Sep 17 00:00:00 2001 From: Syed Ali Shahbaz Date: Fri, 10 Jun 2022 16:21:55 +0530 Subject: [PATCH 24/70] prettier fix attempt-2 --- pages/api/bookings/index.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/pages/api/bookings/index.ts b/pages/api/bookings/index.ts index 4db3f3051a..b8ef582dc1 100644 --- a/pages/api/bookings/index.ts +++ b/pages/api/bookings/index.ts @@ -88,8 +88,7 @@ async function createOrlistAllBookings( if (booking) { res.status(201).json({ booking, message: "Booking created successfully" }); - - // Create Calendar Event for webhook payload + // Create Calendar Event for webhook payload const eventType = await prisma.eventType .findUnique({ where: { id: booking.eventTypeId as number } }) .then((data) => schemaEventTypeReadPublic.parse(data)) @@ -115,24 +114,24 @@ async function createOrlistAllBookings( language: { translate: fallbackTfunction, locale: "en", - } + }, }, attendees: [], location: "", destinationCalendar: null, hideCalendar: false, uid: booking.uid, - metadata: {} + metadata: {}, }; - - // Send Webhook call if hooked to BOOKING_CREATED + + // Send Webhook call if hooked to BOOKING_CREATED const triggerEvent = WebhookTriggerEvents.BOOKING_CREATED; const subscriberOptions = { userId, eventTypeId: booking.eventTypeId as number, triggerEvent, }; - + const subscribers = await getWebhooks(subscriberOptions); const bookingId = booking?.id; const promises = subscribers.map((sub) => From 99e58c414c0419b96d9a0fe0c6e992c62307788f Mon Sep 17 00:00:00 2001 From: Syed Ali Shahbaz Date: Fri, 10 Jun 2022 16:36:28 +0530 Subject: [PATCH 25/70] prettier fix -3 --- pages/api/bookings/index.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pages/api/bookings/index.ts b/pages/api/bookings/index.ts index b8ef582dc1..90f9cf7f22 100644 --- a/pages/api/bookings/index.ts +++ b/pages/api/bookings/index.ts @@ -143,8 +143,7 @@ async function createOrlistAllBookings( }) ); await Promise.all(promises); - } - else + } else (error: Error) => { console.log(error); res.status(400).json({ From 110edd7dcce0ae07554857c7a3c18395f3440b5a Mon Sep 17 00:00:00 2001 From: Syed Ali Shahbaz Date: Sat, 11 Jun 2022 16:54:30 +0530 Subject: [PATCH 26/70] removed 18next rel --- pages/api/bookings/index.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/pages/api/bookings/index.ts b/pages/api/bookings/index.ts index 90f9cf7f22..6f8f2ceadc 100644 --- a/pages/api/bookings/index.ts +++ b/pages/api/bookings/index.ts @@ -1,7 +1,6 @@ import { WebhookTriggerEvents } from "@prisma/client"; import type { NextApiRequest, NextApiResponse } from "next"; -import { getTranslation } from "@calcom/lib/server/i18n"; import prisma from "@calcom/prisma"; import { withMiddleware } from "@lib/helpers/withMiddleware"; @@ -98,7 +97,6 @@ async function createOrlistAllBookings( error, }) ); - const fallbackTfunction = await getTranslation("en", "common"); const evt = { type: eventType?.title || booking.title, title: booking.title, @@ -112,7 +110,6 @@ async function createOrlistAllBookings( email: "", timeZone: "", language: { - translate: fallbackTfunction, locale: "en", }, }, From 61e742ed5ef42d9d2584a92d06c9ec59e2785e5b Mon Sep 17 00:00:00 2001 From: Syed Ali Shahbaz Date: Sat, 11 Jun 2022 16:57:35 +0530 Subject: [PATCH 27/70] uuid automate in booking API --- lib/validations/booking.ts | 1 - pages/api/bookings/index.ts | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/validations/booking.ts b/lib/validations/booking.ts index 67b4f1b35a..b61d6913fd 100644 --- a/lib/validations/booking.ts +++ b/lib/validations/booking.ts @@ -13,7 +13,6 @@ const schemaBookingBaseBodyParams = Booking.pick({ const schemaBookingCreateParams = z .object({ - uid: z.string(), eventTypeId: z.number(), title: z.string(), startTime: z.date().or(z.string()), diff --git a/pages/api/bookings/index.ts b/pages/api/bookings/index.ts index 6f8f2ceadc..37fbd94262 100644 --- a/pages/api/bookings/index.ts +++ b/pages/api/bookings/index.ts @@ -1,5 +1,6 @@ import { WebhookTriggerEvents } from "@prisma/client"; import type { NextApiRequest, NextApiResponse } from "next"; +import { v4 as uuidv4 } from "uuid"; import prisma from "@calcom/prisma"; @@ -82,7 +83,7 @@ async function createOrlistAllBookings( return; } safe.data.userId = userId; - const data = await prisma.booking.create({ data: { ...safe.data } }); + const data = await prisma.booking.create({ data: { id: uuidv4(), ...safe.data } }); const booking = schemaBookingReadPublic.parse(data); if (booking) { From 3c98c5cefc4db970d674e54b14846504a2c1bf3f Mon Sep 17 00:00:00 2001 From: Syed Ali Shahbaz Date: Sat, 11 Jun 2022 16:58:21 +0530 Subject: [PATCH 28/70] fixing id to uid in create body --- pages/api/bookings/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/api/bookings/index.ts b/pages/api/bookings/index.ts index 37fbd94262..6bd220aaf5 100644 --- a/pages/api/bookings/index.ts +++ b/pages/api/bookings/index.ts @@ -83,7 +83,7 @@ async function createOrlistAllBookings( return; } safe.data.userId = userId; - const data = await prisma.booking.create({ data: { id: uuidv4(), ...safe.data } }); + const data = await prisma.booking.create({ data: { uid: uuidv4(), ...safe.data } }); const booking = schemaBookingReadPublic.parse(data); if (booking) { From 719e21497b58e4a57b6914a6c4000e9443fa5188 Mon Sep 17 00:00:00 2001 From: Syed Ali Shahbaz Date: Sat, 11 Jun 2022 17:03:11 +0530 Subject: [PATCH 29/70] removed invalid response --- pages/api/bookings/index.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/pages/api/bookings/index.ts b/pages/api/bookings/index.ts index 6bd220aaf5..af20e851fe 100644 --- a/pages/api/bookings/index.ts +++ b/pages/api/bookings/index.ts @@ -92,12 +92,9 @@ async function createOrlistAllBookings( const eventType = await prisma.eventType .findUnique({ where: { id: booking.eventTypeId as number } }) .then((data) => schemaEventTypeReadPublic.parse(data)) - .catch((error: Error) => - res.status(404).json({ - message: `EventType with id: ${booking.eventTypeId} not found`, - error, - }) - ); + .catch((e: Error) => { + console.error(`Event type with ID: ${booking.eventTypeId} not found`, e); + }); const evt = { type: eventType?.title || booking.title, title: booking.title, From 910a0f386a116b57284b6b6a99e74ed30c9df16b Mon Sep 17 00:00:00 2001 From: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com> Date: Sat, 11 Jun 2022 22:39:03 +0530 Subject: [PATCH 30/70] Debug booking API --- pages/api/bookings/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/pages/api/bookings/index.ts b/pages/api/bookings/index.ts index af20e851fe..fc0030b651 100644 --- a/pages/api/bookings/index.ts +++ b/pages/api/bookings/index.ts @@ -35,6 +35,7 @@ async function createOrlistAllBookings( */ const data = await prisma.booking.findMany({ where: { userId } }); const bookings = data.map((booking) => schemaBookingReadPublic.parse(booking)); + console.log(`Bookings requested by ${userId}`); if (bookings) res.status(200).json({ bookings }); else (error: Error) => From 470c43befe7f940802fb471f38b7d296c4662446 Mon Sep 17 00:00:00 2001 From: Syed Ali Shahbaz Date: Sun, 12 Jun 2022 11:16:16 +0530 Subject: [PATCH 31/70] call webhook before sending booking response --- pages/api/bookings/index.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pages/api/bookings/index.ts b/pages/api/bookings/index.ts index fc0030b651..b99b3510d5 100644 --- a/pages/api/bookings/index.ts +++ b/pages/api/bookings/index.ts @@ -88,7 +88,6 @@ async function createOrlistAllBookings( const booking = schemaBookingReadPublic.parse(data); if (booking) { - res.status(201).json({ booking, message: "Booking created successfully" }); // Create Calendar Event for webhook payload const eventType = await prisma.eventType .findUnique({ where: { id: booking.eventTypeId as number } }) @@ -139,6 +138,9 @@ async function createOrlistAllBookings( }) ); await Promise.all(promises); + + res.status(201).json({ booking, message: "Booking created successfully" }); + } else (error: Error) => { console.log(error); From bd88bec1d2015a83261ed5cdf14802b057475539 Mon Sep 17 00:00:00 2001 From: Syed Ali Shahbaz Date: Sun, 12 Jun 2022 11:24:38 +0530 Subject: [PATCH 32/70] fix prettier --- pages/api/bookings/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/pages/api/bookings/index.ts b/pages/api/bookings/index.ts index b99b3510d5..1bea4249b2 100644 --- a/pages/api/bookings/index.ts +++ b/pages/api/bookings/index.ts @@ -140,7 +140,6 @@ async function createOrlistAllBookings( await Promise.all(promises); res.status(201).json({ booking, message: "Booking created successfully" }); - } else (error: Error) => { console.log(error); From 935473cfcd040a178d25b7f813a5ce5acbb076d4 Mon Sep 17 00:00:00 2001 From: Syed Ali Shahbaz Date: Sun, 12 Jun 2022 11:25:53 +0530 Subject: [PATCH 33/70] removed comment --- pages/api/bookings/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/pages/api/bookings/index.ts b/pages/api/bookings/index.ts index 1bea4249b2..3336098d57 100644 --- a/pages/api/bookings/index.ts +++ b/pages/api/bookings/index.ts @@ -88,7 +88,6 @@ async function createOrlistAllBookings( const booking = schemaBookingReadPublic.parse(data); if (booking) { - // Create Calendar Event for webhook payload const eventType = await prisma.eventType .findUnique({ where: { id: booking.eventTypeId as number } }) .then((data) => schemaEventTypeReadPublic.parse(data)) From ecce02ebbf60e6b215ed980f876cb760581609c4 Mon Sep 17 00:00:00 2001 From: Syed Ali Shahbaz Date: Sun, 12 Jun 2022 11:43:26 +0530 Subject: [PATCH 34/70] debug logs added --- lib/utils/sendPayload.ts | 1 + pages/api/bookings/index.ts | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/lib/utils/sendPayload.ts b/lib/utils/sendPayload.ts index f40c0ad743..a6234a1d37 100644 --- a/lib/utils/sendPayload.ts +++ b/lib/utils/sendPayload.ts @@ -18,6 +18,7 @@ function jsonParse(jsonString: string) { return JSON.parse(jsonString); } catch (e) { // don't do anything. + console.error(`error jsonParsing in sendPayload`); } return false; } diff --git a/pages/api/bookings/index.ts b/pages/api/bookings/index.ts index 3336098d57..e31a017801 100644 --- a/pages/api/bookings/index.ts +++ b/pages/api/bookings/index.ts @@ -94,6 +94,7 @@ async function createOrlistAllBookings( .catch((e: Error) => { console.error(`Event type with ID: ${booking.eventTypeId} not found`, e); }); + console.log(`eventType: ${eventType}`); const evt = { type: eventType?.title || booking.title, title: booking.title, @@ -117,16 +118,20 @@ async function createOrlistAllBookings( uid: booking.uid, metadata: {}, }; + console.log(`evt: ${evt}`); // Send Webhook call if hooked to BOOKING_CREATED const triggerEvent = WebhookTriggerEvents.BOOKING_CREATED; + console.log(`Trigger Event: ${triggerEvent}`); const subscriberOptions = { userId, eventTypeId: booking.eventTypeId as number, triggerEvent, }; + console.log(`subscriberOptions: ${subscriberOptions}`); const subscribers = await getWebhooks(subscriberOptions); + console.log(`subscribers: ${subscribers}`); const bookingId = booking?.id; const promises = subscribers.map((sub) => sendPayload(triggerEvent, new Date().toISOString(), sub, { From b385d3547bf487c341ab20c9a372fdf84af1da5c Mon Sep 17 00:00:00 2001 From: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com> Date: Mon, 13 Jun 2022 13:24:21 +0530 Subject: [PATCH 35/70] Adds debug console.log --- pages/api/bookings/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/api/bookings/index.ts b/pages/api/bookings/index.ts index e31a017801..ab85b339cd 100644 --- a/pages/api/bookings/index.ts +++ b/pages/api/bookings/index.ts @@ -142,7 +142,7 @@ async function createOrlistAllBookings( }) ); await Promise.all(promises); - + console.log("All promises resolved! About to send the response"); res.status(201).json({ booking, message: "Booking created successfully" }); } else (error: Error) => { From b1dd1b85727d3e0b9b8badfe151c513e7b48eb75 Mon Sep 17 00:00:00 2001 From: Agusti Fernandez Pardo Date: Mon, 13 Jun 2022 23:58:15 +0200 Subject: [PATCH 36/70] fix: make jsonSchema.optional() --- lib/validations/user.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/validations/user.ts b/lib/validations/user.ts index 528bc2e292..2385224f5a 100644 --- a/lib/validations/user.ts +++ b/lib/validations/user.ts @@ -93,7 +93,7 @@ const schemaUserEditParams = z.object({ .optional() .nullable(), locale: z.nativeEnum(locales).optional().nullable(), - metadata: jsonSchema, + metadata: jsonSchema.optional(), }); // @note: These are the values that are editable via PATCH method on the user Model, From 49087d858e4d7c7d6166bd8b53895b618ac1da2f Mon Sep 17 00:00:00 2001 From: zomars Date: Tue, 14 Jun 2022 14:07:23 -0600 Subject: [PATCH 37/70] 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 38/70] 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 39/70] 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 40/70] 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 41/70] 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 42/70] 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 43/70] 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); From 3cb2a8b86607072eedcc4b09edb488879bf127b5 Mon Sep 17 00:00:00 2001 From: zomars Date: Tue, 14 Jun 2022 15:13:59 -0600 Subject: [PATCH 44/70] Matches transpilling modules to web --- next.config.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/next.config.js b/next.config.js index 62b15643fa..22ff466595 100644 --- a/next.config.js +++ b/next.config.js @@ -2,9 +2,15 @@ // This makes our @calcom/prisma package from the monorepo to be transpiled and usable by API const withTM = require("next-transpile-modules")([ "@calcom/app-store", - "@calcom/prisma", - "@calcom/lib", + "@calcom/core", "@calcom/ee", + "@calcom/lib", + "@calcom/prisma", + "@calcom/stripe", + "@calcom/ui", + "@calcom/emails", + "@calcom/embed-core", + "@calcom/embed-snippet", ]); module.exports = withTM({ From e1bcd38e61c412cc7500ab8eb453dd8c5ae1825c Mon Sep 17 00:00:00 2001 From: zomars Date: Tue, 14 Jun 2022 15:17:09 -0600 Subject: [PATCH 45/70] Adds basic availability endpoint --- pages/api/availability/_get.ts | 29 +++++++++++++++++++++++++++++ pages/api/availability/index.ts | 5 +++++ 2 files changed, 34 insertions(+) create mode 100644 pages/api/availability/_get.ts create mode 100644 pages/api/availability/index.ts diff --git a/pages/api/availability/_get.ts b/pages/api/availability/_get.ts new file mode 100644 index 0000000000..21450f005e --- /dev/null +++ b/pages/api/availability/_get.ts @@ -0,0 +1,29 @@ +import type { NextApiRequest } from "next"; +import { z } from "zod"; + +import { getUserAvailability } from "@calcom/core/getUserAvailability"; +import { defaultResponder } from "@calcom/lib/server"; +import { stringOrNumber } from "@calcom/prisma/zod-utils"; + +const availabilitySchema = z + .object({ + userId: 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."); + +async function handler(req: NextApiRequest) { + const { username, userId, eventTypeId, dateTo, dateFrom } = availabilitySchema.parse(req.query); + return getUserAvailability({ + username, + dateFrom, + dateTo, + eventTypeId, + userId, + }); +} + +export default defaultResponder(handler); diff --git a/pages/api/availability/index.ts b/pages/api/availability/index.ts new file mode 100644 index 0000000000..0fe7836697 --- /dev/null +++ b/pages/api/availability/index.ts @@ -0,0 +1,5 @@ +import { defaultHandler } from "@calcom/lib/server"; + +export default defaultHandler({ + GET: import("./_get"), +}); From ba37ae78d91598f9a6c2f7650cd3f87a02570150 Mon Sep 17 00:00:00 2001 From: Agusti Fernandez Pardo Date: Tue, 14 Jun 2022 23:41:33 +0200 Subject: [PATCH 46/70] fix: adds middleware, makes userId not optional --- pages/api/availability/_get.ts | 2 +- pages/api/availability/index.ts | 10 +++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/pages/api/availability/_get.ts b/pages/api/availability/_get.ts index 21450f005e..5289190c03 100644 --- a/pages/api/availability/_get.ts +++ b/pages/api/availability/_get.ts @@ -7,7 +7,7 @@ import { stringOrNumber } from "@calcom/prisma/zod-utils"; const availabilitySchema = z .object({ - userId: stringOrNumber.optional(), + userId: stringOrNumber, username: z.string().optional(), dateFrom: z.string(), dateTo: z.string(), diff --git a/pages/api/availability/index.ts b/pages/api/availability/index.ts index 0fe7836697..b0c4e223d5 100644 --- a/pages/api/availability/index.ts +++ b/pages/api/availability/index.ts @@ -1,5 +1,9 @@ import { defaultHandler } from "@calcom/lib/server"; -export default defaultHandler({ - GET: import("./_get"), -}); +import { withMiddleware } from "@lib/helpers/withMiddleware"; + +export default withMiddleware("HTTP_GET_OR_POST")( + defaultHandler({ + GET: import("./_get"), + }) +); From 372b20a4c937a5b3014febaf61836656e507a3fd Mon Sep 17 00:00:00 2001 From: Agusti Fernandez Pardo Date: Tue, 14 Jun 2022 23:42:51 +0200 Subject: [PATCH 47/70] fix: no post for now --- pages/api/availability/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/api/availability/index.ts b/pages/api/availability/index.ts index b0c4e223d5..fe6357f6bc 100644 --- a/pages/api/availability/index.ts +++ b/pages/api/availability/index.ts @@ -2,7 +2,7 @@ import { defaultHandler } from "@calcom/lib/server"; import { withMiddleware } from "@lib/helpers/withMiddleware"; -export default withMiddleware("HTTP_GET_OR_POST")( +export default withMiddleware("HTTP_GET")( defaultHandler({ GET: import("./_get"), }) From 1bde5d88c105cf0357ce708a52f725dc37072be1 Mon Sep 17 00:00:00 2001 From: Agusti Fernandez Pardo Date: Tue, 14 Jun 2022 23:43:53 +0200 Subject: [PATCH 48/70] fix: add back optional --- 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 5289190c03..21450f005e 100644 --- a/pages/api/availability/_get.ts +++ b/pages/api/availability/_get.ts @@ -7,7 +7,7 @@ import { stringOrNumber } from "@calcom/prisma/zod-utils"; const availabilitySchema = z .object({ - userId: stringOrNumber, + userId: stringOrNumber.optional(), username: z.string().optional(), dateFrom: z.string(), dateTo: z.string(), From 1e80a83b1f5519fc76fdbab53456ef0fc4310cfd Mon Sep 17 00:00:00 2001 From: Agusti Fernandez Pardo Date: Tue, 14 Jun 2022 23:57:38 +0200 Subject: [PATCH 49/70] add session to req --- lib/helpers/verifyApiKey.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/helpers/verifyApiKey.ts b/lib/helpers/verifyApiKey.ts index c45f3d3c3f..481b006849 100644 --- a/lib/helpers/verifyApiKey.ts +++ b/lib/helpers/verifyApiKey.ts @@ -11,6 +11,8 @@ declare module "next" { userId: number; method: string; query: { [key: string]: string | string[] }; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + session: any; } } From ab26f35520c3c564ad39f4b9ccf7f0521caef3c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Omar=20L=C3=B3pez?= Date: Tue, 14 Jun 2022 16:04:40 -0600 Subject: [PATCH 50/70] Update package.json --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 48cf7b6ef9..493a5929fd 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,8 @@ "author": "Cal.com Inc.", "private": true, "scripts": { - "build": "next build", + "app-store:generate": "yarn workspace @calcom/app-store-cli generate", + "build": "yarn app-store:generate && next build", "clean": "rm -rf .turbo && rm -rf node_modules && rm -rf .next", "dev": "PORT=3002 next dev", "lint-fix": "next lint --fix && prettier --write .", From 5559e63be10e0812043d7e2e90ff54fa0f9ac97d Mon Sep 17 00:00:00 2001 From: Agusti Fernandez Pardo Date: Wed, 15 Jun 2022 00:19:39 +0200 Subject: [PATCH 51/70] fix add app-store to deps --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 493a5929fd..6384aaa8ca 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ }, "dependencies": { "@calcom/prisma": "*", + "@calcom/app-store": "*", "@sentry/nextjs": "^6.19.7", "modify-response-middleware": "^1.1.0", "next": "^12.1.6", From ff701ffec68b4216ccbdadefe2db84c11500400b Mon Sep 17 00:00:00 2001 From: Agusti Fernandez Pardo Date: Wed, 15 Jun 2022 00:21:09 +0200 Subject: [PATCH 52/70] fix: no need for app store deps --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index 6384aaa8ca..493a5929fd 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,6 @@ }, "dependencies": { "@calcom/prisma": "*", - "@calcom/app-store": "*", "@sentry/nextjs": "^6.19.7", "modify-response-middleware": "^1.1.0", "next": "^12.1.6", From ed1bc8015acf4a02fded22856a9fd82eaade1462 Mon Sep 17 00:00:00 2001 From: Agusti Fernandez Pardo Date: Wed, 15 Jun 2022 20:43:35 +0200 Subject: [PATCH 53/70] feat: initial isAdmin work for events and attendees --- lib/helpers/verifyApiKey.ts | 6 +++ lib/validations/booking.ts | 1 - pages/api/attendees/[id].ts | 10 +++-- pages/api/attendees/index.ts | 79 ++++++++++++++++++++++------------ pages/api/bookings/index.ts | 2 +- pages/api/event-types/[id].ts | 4 +- pages/api/event-types/index.ts | 4 +- pages/api/users/[id].ts | 4 +- pages/api/users/index.ts | 4 +- 9 files changed, 69 insertions(+), 45 deletions(-) diff --git a/lib/helpers/verifyApiKey.ts b/lib/helpers/verifyApiKey.ts index c45f3d3c3f..c3977ca94f 100644 --- a/lib/helpers/verifyApiKey.ts +++ b/lib/helpers/verifyApiKey.ts @@ -4,12 +4,15 @@ import { NextMiddleware } from "next-api-middleware"; import { hashAPIKey } from "@calcom/ee/lib/api/apiKeys"; import prisma from "@calcom/prisma"; +import { isAdminGuard } from "@lib/utils/isAdmin"; + /** @todo figure how to use the one from `@calcom/types`fi */ /** @todo: remove once `@calcom/types` is updated with it.*/ declare module "next" { export interface NextApiRequest extends IncomingMessage { userId: number; method: string; + isAdmin: boolean; query: { [key: string]: string | string[] }; } } @@ -39,5 +42,8 @@ export const verifyApiKey: NextMiddleware = async (req, res, next) => { if (!apiKey.userId) return res.status(404).json({ error: "No user found for this apiKey" }); /* We save the user id in the request for later use */ req.userId = apiKey.userId; + /* We save the isAdmin boolean here for later use */ + req.isAdmin = await isAdminGuard(req.userId); + await next(); }; diff --git a/lib/validations/booking.ts b/lib/validations/booking.ts index b61d6913fd..a8a4b52c37 100644 --- a/lib/validations/booking.ts +++ b/lib/validations/booking.ts @@ -24,7 +24,6 @@ export const schemaBookingCreateBodyParams = schemaBookingBaseBodyParams.merge(s const schemaBookingEditParams = z .object({ - uid: z.string().optional(), title: z.string().optional(), startTime: z.date().optional(), endTime: z.date().optional(), diff --git a/pages/api/attendees/[id].ts b/pages/api/attendees/[id].ts index a1674d87a4..a72dbf5812 100644 --- a/pages/api/attendees/[id].ts +++ b/pages/api/attendees/[id].ts @@ -11,7 +11,7 @@ import { } from "@lib/validations/shared/queryIdTransformParseInt"; export async function attendeeById( - { method, query, body, userId }: NextApiRequest, + { method, query, body, userId, isAdmin }: NextApiRequest, res: NextApiResponse ) { const safeQuery = schemaQueryIdParseInt.safeParse(query); @@ -33,9 +33,11 @@ export async function attendeeById( .flat() .map((attendee) => attendee.id) ); - // @note: Here we make sure to only return attendee's of the user's own bookings. - if (!userBookingsAttendeeIds.includes(safeQuery.data.id)) res.status(401).json({ message: "Unauthorized" }); - else { + // @note: Here we make sure to only return attendee's of the user's own bookings if the user is not an admin. + if (!isAdmin) { + if (!userBookingsAttendeeIds.includes(safeQuery.data.id)) + res.status(401).json({ message: "Unauthorized" }); + } else { switch (method) { /** * @swagger diff --git a/pages/api/attendees/index.ts b/pages/api/attendees/index.ts index eaae0b28ff..6b2c4fc415 100644 --- a/pages/api/attendees/index.ts +++ b/pages/api/attendees/index.ts @@ -1,24 +1,30 @@ import type { NextApiRequest, NextApiResponse } from "next"; -import db from "@calcom/prisma"; +import prisma from "@calcom/prisma"; import { withMiddleware } from "@lib/helpers/withMiddleware"; import { AttendeeResponse, AttendeesResponse } from "@lib/types"; import { schemaAttendeeCreateBodyParams, schemaAttendeeReadPublic } from "@lib/validations/attendee"; async function createOrlistAllAttendees( - { method, userId, body }: NextApiRequest, + { method, userId, body, isAdmin }: NextApiRequest, res: NextApiResponse ) { - const userBookings = await db.booking.findMany({ - where: { - userId, - }, - include: { - attendees: true, - }, - }); - const attendees = userBookings.map((booking) => booking.attendees).flat(); + let attendees; + if (!isAdmin) { + const userBookings = await prisma.booking.findMany({ + where: { + userId, + }, + include: { + attendees: true, + }, + }); + attendees = userBookings.map((booking) => booking.attendees).flat(); + } else { + const data = await prisma.attendee.findMany(); + attendees = data.map((attendee) => schemaAttendeeReadPublic.parse(attendee)); + } if (method === "GET") { /** * @swagger @@ -37,12 +43,7 @@ async function createOrlistAllAttendees( * description: No attendees were found */ if (attendees) res.status(200).json({ attendees }); - else - (error: Error) => - res.status(404).json({ - message: "No Attendees were found", - error, - }); + else (error: Error) => res.status(400).json({ error }); } else if (method === "POST") { /** * @swagger @@ -90,16 +91,40 @@ async function createOrlistAllAttendees( res.status(400).json({ message: "Invalid request body", error: safePost.error }); return; } - const userWithBookings = await db.user.findUnique({ where: { id: userId }, include: { bookings: true } }); - if (!userWithBookings) { - res.status(404).json({ message: "User not found" }); - return; - } - const userBookingIds = userWithBookings.bookings.map((booking: { id: number }) => booking.id).flat(); - // Here we make sure to only return attendee's of the user's own bookings. - if (!userBookingIds.includes(safePost.data.bookingId)) res.status(401).json({ message: "Unauthorized" }); - else { - const data = await db.attendee.create({ + if (!isAdmin) { + const userWithBookings = await prisma.user.findUnique({ + where: { id: userId }, + include: { bookings: true }, + }); + if (!userWithBookings) { + res.status(404).json({ message: "User not found" }); + return; + } + const userBookingIds = userWithBookings.bookings.map((booking: { id: number }) => booking.id).flat(); + // Here we make sure to only return attendee's of the user's own bookings. + if (!userBookingIds.includes(safePost.data.bookingId)) + res.status(401).json({ message: "Unauthorized" }); + else { + const data = await prisma.attendee.create({ + data: { + email: safePost.data.email, + name: safePost.data.name, + timeZone: safePost.data.timeZone, + booking: { connect: { id: safePost.data.bookingId } }, + }, + }); + const attendee = schemaAttendeeReadPublic.parse(data); + + if (attendee) { + res.status(201).json({ + attendee, + message: "Attendee created successfully", + }); + } else (error: Error) => res.status(400).json({ error }); + } + } else { + // @todo: check real availability times before booking + const data = await prisma.attendee.create({ data: { email: safePost.data.email, name: safePost.data.name, diff --git a/pages/api/bookings/index.ts b/pages/api/bookings/index.ts index ab85b339cd..47d9b80da3 100644 --- a/pages/api/bookings/index.ts +++ b/pages/api/bookings/index.ts @@ -12,7 +12,7 @@ import { schemaBookingCreateBodyParams, schemaBookingReadPublic } from "@lib/val import { schemaEventTypeReadPublic } from "@lib/validations/event-type"; async function createOrlistAllBookings( - { method, body, userId }: NextApiRequest, + { method, body, userId, isAdmin }: NextApiRequest, res: NextApiResponse ) { console.log("userIduserId", userId); diff --git a/pages/api/event-types/[id].ts b/pages/api/event-types/[id].ts index 19435a0bc6..013f9909be 100644 --- a/pages/api/event-types/[id].ts +++ b/pages/api/event-types/[id].ts @@ -4,7 +4,6 @@ import prisma from "@calcom/prisma"; import { withMiddleware } from "@lib/helpers/withMiddleware"; import type { EventTypeResponse } from "@lib/types"; -import { isAdminGuard } from "@lib/utils/isAdmin"; import { schemaEventTypeEditBodyParams, schemaEventTypeReadPublic } from "@lib/validations/event-type"; import { schemaQueryIdParseInt, @@ -12,10 +11,9 @@ import { } from "@lib/validations/shared/queryIdTransformParseInt"; export async function eventTypeById( - { method, query, body, userId }: NextApiRequest, + { method, query, body, userId, isAdmin }: NextApiRequest, res: NextApiResponse ) { - const isAdmin = await isAdminGuard(userId); const safeQuery = schemaQueryIdParseInt.safeParse(query); if (!safeQuery.success) { res.status(400).json({ message: "Your query was invalid" }); diff --git a/pages/api/event-types/index.ts b/pages/api/event-types/index.ts index bfe8c3e7ee..dc6844e039 100644 --- a/pages/api/event-types/index.ts +++ b/pages/api/event-types/index.ts @@ -4,14 +4,12 @@ import prisma from "@calcom/prisma"; import { withMiddleware } from "@lib/helpers/withMiddleware"; import { EventTypeResponse, EventTypesResponse } from "@lib/types"; -import { isAdminGuard } from "@lib/utils/isAdmin"; import { schemaEventTypeCreateBodyParams, schemaEventTypeReadPublic } from "@lib/validations/event-type"; async function createOrlistAllEventTypes( - { method, body, userId }: NextApiRequest, + { method, body, userId, isAdmin }: NextApiRequest, res: NextApiResponse ) { - const isAdmin = await isAdminGuard(userId); if (method === "GET") { /** * @swagger diff --git a/pages/api/users/[id].ts b/pages/api/users/[id].ts index 65b864c652..bf70a7a785 100644 --- a/pages/api/users/[id].ts +++ b/pages/api/users/[id].ts @@ -4,7 +4,6 @@ import prisma from "@calcom/prisma"; import { withMiddleware } from "@lib/helpers/withMiddleware"; import type { UserResponse } from "@lib/types"; -import { isAdminGuard } from "@lib/utils/isAdmin"; import { schemaQueryIdParseInt, withValidQueryIdTransformParseInt, @@ -12,7 +11,7 @@ import { import { schemaUserEditBodyParams, schemaUserReadPublic } from "@lib/validations/user"; export async function userById( - { method, query, body, userId }: NextApiRequest, + { method, query, body, userId, isAdmin }: NextApiRequest, res: NextApiResponse ) { const safeQuery = schemaQueryIdParseInt.safeParse(query); @@ -21,7 +20,6 @@ export async function userById( 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" }); diff --git a/pages/api/users/index.ts b/pages/api/users/index.ts index 3839ae21b6..d94f198ad8 100644 --- a/pages/api/users/index.ts +++ b/pages/api/users/index.ts @@ -4,7 +4,6 @@ import prisma from "@calcom/prisma"; 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"; /** @@ -24,10 +23,9 @@ import { schemaUserReadPublic, schemaUserCreateBodyParams } from "@lib/validatio * description: No users were found */ async function getAllorCreateUser( - { userId, method, body }: NextApiRequest, + { userId, method, body, isAdmin }: NextApiRequest, res: NextApiResponse ) { - const isAdmin = await isAdminGuard(userId); if (method === "GET") { if (!isAdmin) { // If user is not ADMIN, return only his data. From 1d2e5f07ef6243f445d320c897584ecb762f338b Mon Sep 17 00:00:00 2001 From: Agusti Fernandez Pardo Date: Wed, 15 Jun 2022 21:10:19 +0200 Subject: [PATCH 54/70] feat: admin endpoints for bookings --- lib/validations/booking.ts | 1 - pages/api/bookings/[id].ts | 8 +++++--- pages/api/bookings/index.ts | 37 ++++++++++++++++++++++++++----------- 3 files changed, 31 insertions(+), 15 deletions(-) diff --git a/lib/validations/booking.ts b/lib/validations/booking.ts index a8a4b52c37..a7a8eab730 100644 --- a/lib/validations/booking.ts +++ b/lib/validations/booking.ts @@ -3,7 +3,6 @@ import { z } from "zod"; import { _BookingModel as Booking } from "@calcom/prisma/zod"; const schemaBookingBaseBodyParams = Booking.pick({ - uid: true, userId: true, eventTypeId: true, title: true, diff --git a/pages/api/bookings/[id].ts b/pages/api/bookings/[id].ts index 7e17c9613a..8603af73c8 100644 --- a/pages/api/bookings/[id].ts +++ b/pages/api/bookings/[id].ts @@ -11,7 +11,7 @@ import { } from "@lib/validations/shared/queryIdTransformParseInt"; export async function bookingById( - { method, query, body, userId }: NextApiRequest, + { method, query, body, userId, isAdmin }: NextApiRequest, res: NextApiResponse ) { const safeQuery = schemaQueryIdParseInt.safeParse(query); @@ -25,8 +25,10 @@ export async function bookingById( }); if (!userWithBookings) throw new Error("User not found"); const userBookingIds = userWithBookings.bookings.map((booking: { id: number }) => booking.id).flat(); - if (!userBookingIds.includes(safeQuery.data.id)) res.status(401).json({ message: "Unauthorized" }); - else { + + if (!isAdmin) { + if (!userBookingIds.includes(safeQuery.data.id)) res.status(401).json({ message: "Unauthorized" }); + } else { switch (method) { /** * @swagger diff --git a/pages/api/bookings/index.ts b/pages/api/bookings/index.ts index 47d9b80da3..560bca7b8e 100644 --- a/pages/api/bookings/index.ts +++ b/pages/api/bookings/index.ts @@ -33,16 +33,29 @@ async function createOrlistAllBookings( * 404: * description: No bookings were found */ - const data = await prisma.booking.findMany({ where: { userId } }); - const bookings = data.map((booking) => schemaBookingReadPublic.parse(booking)); - console.log(`Bookings requested by ${userId}`); - if (bookings) res.status(200).json({ bookings }); - else - (error: Error) => - res.status(404).json({ - message: "No Bookings were found", - error, - }); + if (!isAdmin) { + const data = await prisma.booking.findMany({ where: { userId } }); + const bookings = data.map((booking) => schemaBookingReadPublic.parse(booking)); + if (bookings) res.status(200).json({ bookings }); + else { + (error: Error) => + res.status(404).json({ + message: "No Bookings were found", + error, + }); + } + } else { + const data = await prisma.booking.findMany(); + const bookings = data.map((booking) => schemaBookingReadPublic.parse(booking)); + if (bookings) res.status(200).json({ bookings }); + else { + (error: Error) => + res.status(404).json({ + message: "No Bookings were found", + error, + }); + } + } } else if (method === "POST") { /** * @swagger @@ -83,7 +96,9 @@ async function createOrlistAllBookings( res.status(400).json({ message: "Bad request. Booking body is invalid." }); return; } - safe.data.userId = userId; + if (!isAdmin) { + safe.data.userId = userId; + } const data = await prisma.booking.create({ data: { uid: uuidv4(), ...safe.data } }); const booking = schemaBookingReadPublic.parse(data); From 8c3774e10018bd92596f5e81ca038d9670e4675a Mon Sep 17 00:00:00 2001 From: Agusti Fernandez Pardo Date: Wed, 15 Jun 2022 21:14:35 +0200 Subject: [PATCH 55/70] fix: remove comment --- pages/api/attendees/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/pages/api/attendees/index.ts b/pages/api/attendees/index.ts index 6b2c4fc415..e4f3e54f79 100644 --- a/pages/api/attendees/index.ts +++ b/pages/api/attendees/index.ts @@ -123,7 +123,6 @@ async function createOrlistAllAttendees( } else (error: Error) => res.status(400).json({ error }); } } else { - // @todo: check real availability times before booking const data = await prisma.attendee.create({ data: { email: safePost.data.email, From 26b9e9456824f514d2e451b33cbd434ab455d02f Mon Sep 17 00:00:00 2001 From: Agusti Fernandez Pardo Date: Wed, 15 Jun 2022 21:16:46 +0200 Subject: [PATCH 56/70] fix: undo changes in users --- pages/api/users/[id].ts | 4 +++- pages/api/users/index.ts | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/pages/api/users/[id].ts b/pages/api/users/[id].ts index bf70a7a785..65b864c652 100644 --- a/pages/api/users/[id].ts +++ b/pages/api/users/[id].ts @@ -4,6 +4,7 @@ import prisma from "@calcom/prisma"; import { withMiddleware } from "@lib/helpers/withMiddleware"; import type { UserResponse } from "@lib/types"; +import { isAdminGuard } from "@lib/utils/isAdmin"; import { schemaQueryIdParseInt, withValidQueryIdTransformParseInt, @@ -11,7 +12,7 @@ import { import { schemaUserEditBodyParams, schemaUserReadPublic } from "@lib/validations/user"; export async function userById( - { method, query, body, userId, isAdmin }: NextApiRequest, + { method, query, body, userId }: NextApiRequest, res: NextApiResponse ) { const safeQuery = schemaQueryIdParseInt.safeParse(query); @@ -20,6 +21,7 @@ export async function userById( 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" }); diff --git a/pages/api/users/index.ts b/pages/api/users/index.ts index d94f198ad8..3839ae21b6 100644 --- a/pages/api/users/index.ts +++ b/pages/api/users/index.ts @@ -4,6 +4,7 @@ import prisma from "@calcom/prisma"; 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"; /** @@ -23,9 +24,10 @@ import { schemaUserReadPublic, schemaUserCreateBodyParams } from "@lib/validatio * description: No users were found */ async function getAllorCreateUser( - { userId, method, body, isAdmin }: NextApiRequest, + { 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. From aadde45bb7da48ea99865df92f976432f79b8c67 Mon Sep 17 00:00:00 2001 From: Agusti Fernandez Pardo Date: Sat, 4 Jun 2022 01:17:01 +0200 Subject: [PATCH 57/70] fix: add event-types admin endpoints --- pages/api/event-types/index.ts | 58 +++++++++++++++++++--------------- 1 file changed, 33 insertions(+), 25 deletions(-) diff --git a/pages/api/event-types/index.ts b/pages/api/event-types/index.ts index 0e2467692f..bfe8c3e7ee 100644 --- a/pages/api/event-types/index.ts +++ b/pages/api/event-types/index.ts @@ -4,12 +4,14 @@ import prisma from "@calcom/prisma"; import { withMiddleware } from "@lib/helpers/withMiddleware"; import { EventTypeResponse, EventTypesResponse } from "@lib/types"; +import { isAdminGuard } from "@lib/utils/isAdmin"; import { schemaEventTypeCreateBodyParams, schemaEventTypeReadPublic } from "@lib/validations/event-type"; async function createOrlistAllEventTypes( { method, body, userId }: NextApiRequest, res: NextApiResponse ) { + const isAdmin = await isAdminGuard(userId); if (method === "GET") { /** * @swagger @@ -29,20 +31,27 @@ async function createOrlistAllEventTypes( * 404: * description: No event types were found */ - const data = await prisma.user - .findUnique({ - where: { id: userId }, - rejectOnNotFound: true, - select: { eventTypes: true }, - }) - .catch((error) => res.status(404).json({ message: "No event types were found", error })); - if (data) res.status(200).json({ event_types: data.eventTypes }); - else - (error: Error) => - res.status(404).json({ - message: "No EventTypes were found", - error, - }); + if (!isAdmin) { + const data = await prisma.user + .findUnique({ + where: { id: userId }, + rejectOnNotFound: true, + select: { eventTypes: true }, + }) + .catch((error) => res.status(404).json({ message: "No event types were found", error })); + // @todo: add validations back schemaReadEventType.parse + if (data) res.status(200).json({ event_types: data.eventTypes }); + else + (error: Error) => + res.status(404).json({ + message: "No EventTypes were found", + error, + }); + } else { + const data = await prisma.eventType.findMany({}); + const event_types = data.map((eventType) => schemaEventTypeReadPublic.parse(eventType)); + if (event_types) res.status(200).json({ event_types }); + } } else if (method === "POST") { /** * @swagger @@ -92,17 +101,16 @@ async function createOrlistAllEventTypes( res.status(400).json({ message: "Invalid request body", error: safe.error }); return; } - - const data = await prisma.eventType.create({ data: { ...safe.data, userId } }); - const event_type = schemaEventTypeReadPublic.parse(data); - - if (data) res.status(201).json({ event_type, message: "EventType created successfully" }); - else - (error: Error) => - res.status(400).json({ - message: "Could not create new event type", - error, - }); + if (!isAdmin) { + const data = await prisma.eventType.create({ data: { ...safe.data, userId } }); + const event_type = schemaEventTypeReadPublic.parse(data); + if (data) res.status(201).json({ event_type, message: "EventType created successfully" }); + } else { + // if admin don't re-set userId from input + const data = await prisma.eventType.create({ data: { ...safe.data } }); + const event_type = schemaEventTypeReadPublic.parse(data); + if (data) res.status(201).json({ event_type, message: "EventType created successfully" }); + } } else res.status(405).json({ message: `Method ${method} not allowed` }); } From 514a98f9e042a79169252d227373e9dcd1d72d35 Mon Sep 17 00:00:00 2001 From: Agusti Fernandez Pardo Date: Sat, 4 Jun 2022 01:32:05 +0200 Subject: [PATCH 58/70] feat: add admin endpoint support for event-types id --- pages/api/event-types/[id].ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pages/api/event-types/[id].ts b/pages/api/event-types/[id].ts index 20ad84d776..81cefb3ab7 100644 --- a/pages/api/event-types/[id].ts +++ b/pages/api/event-types/[id].ts @@ -4,6 +4,7 @@ import prisma from "@calcom/prisma"; import { withMiddleware } from "@lib/helpers/withMiddleware"; import type { EventTypeResponse } from "@lib/types"; +import { isAdminGuard } from "@lib/utils/isAdmin"; import { schemaEventTypeEditBodyParams, schemaEventTypeReadPublic } from "@lib/validations/event-type"; import { schemaQueryIdParseInt, @@ -14,19 +15,21 @@ export async function eventTypeById( { method, query, body, userId }: NextApiRequest, res: NextApiResponse ) { + const isAdmin = await isAdminGuard(userId); const safeQuery = schemaQueryIdParseInt.safeParse(query); if (!safeQuery.success) { res.status(400).json({ message: "Your query was invalid" }); return; } - const data = await await prisma.user.findUnique({ + const data = await prisma.user.findUnique({ where: { id: userId }, rejectOnNotFound: true, select: { eventTypes: true }, }); const userEventTypes = data.eventTypes.map((eventType) => eventType.id); - if (!userEventTypes.includes(safeQuery.data.id)) res.status(401).json({ message: "Unauthorized" }); + if (!isAdmin || !userEventTypes.includes(safeQuery.data.id)) + res.status(401).json({ message: "Unauthorized" }); else { switch (method) { /** From d8d0d42374fff2c5cbe03182967a60f8a1841a9b Mon Sep 17 00:00:00 2001 From: Agusti Fernandez Pardo Date: Sat, 4 Jun 2022 02:26:16 +0200 Subject: [PATCH 59/70] fix: only check event type ownership if not admin --- pages/api/event-types/[id].ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/pages/api/event-types/[id].ts b/pages/api/event-types/[id].ts index 81cefb3ab7..19435a0bc6 100644 --- a/pages/api/event-types/[id].ts +++ b/pages/api/event-types/[id].ts @@ -27,10 +27,9 @@ export async function eventTypeById( select: { eventTypes: true }, }); const userEventTypes = data.eventTypes.map((eventType) => eventType.id); - - if (!isAdmin || !userEventTypes.includes(safeQuery.data.id)) - res.status(401).json({ message: "Unauthorized" }); - else { + if (!isAdmin) { + if (!userEventTypes.includes(safeQuery.data.id)) res.status(401).json({ message: "Unauthorized" }); + } else { switch (method) { /** * @swagger From dd94df8c97be8eb550f356c3cdce5080030fb7f9 Mon Sep 17 00:00:00 2001 From: Agusti Fernandez Pardo Date: Wed, 15 Jun 2022 22:59:26 +0200 Subject: [PATCH 60/70] merging main to prod fixing conflicts in users --- lib/helpers/verifyApiKey.ts | 6 ++ lib/validations/booking.ts | 1 - pages/api/attendees/[id].ts | 10 +- pages/api/attendees/index.ts | 79 +++++++++----- pages/api/bookings/index.ts | 2 +- pages/api/event-types/[id].ts | 4 +- pages/api/event-types/index.ts | 4 +- pages/api/users/[id].ts | 184 +++++++++++++++++++++++++++++++++ 8 files changed, 251 insertions(+), 39 deletions(-) create mode 100644 pages/api/users/[id].ts diff --git a/lib/helpers/verifyApiKey.ts b/lib/helpers/verifyApiKey.ts index 481b006849..08c0216737 100644 --- a/lib/helpers/verifyApiKey.ts +++ b/lib/helpers/verifyApiKey.ts @@ -4,12 +4,15 @@ import { NextMiddleware } from "next-api-middleware"; import { hashAPIKey } from "@calcom/ee/lib/api/apiKeys"; import prisma from "@calcom/prisma"; +import { isAdminGuard } from "@lib/utils/isAdmin"; + /** @todo figure how to use the one from `@calcom/types`fi */ /** @todo: remove once `@calcom/types` is updated with it.*/ declare module "next" { export interface NextApiRequest extends IncomingMessage { userId: number; method: string; + isAdmin: boolean; query: { [key: string]: string | string[] }; // eslint-disable-next-line @typescript-eslint/no-explicit-any session: any; @@ -41,5 +44,8 @@ export const verifyApiKey: NextMiddleware = async (req, res, next) => { if (!apiKey.userId) return res.status(404).json({ error: "No user found for this apiKey" }); /* We save the user id in the request for later use */ req.userId = apiKey.userId; + /* We save the isAdmin boolean here for later use */ + req.isAdmin = await isAdminGuard(req.userId); + await next(); }; diff --git a/lib/validations/booking.ts b/lib/validations/booking.ts index b61d6913fd..a8a4b52c37 100644 --- a/lib/validations/booking.ts +++ b/lib/validations/booking.ts @@ -24,7 +24,6 @@ export const schemaBookingCreateBodyParams = schemaBookingBaseBodyParams.merge(s const schemaBookingEditParams = z .object({ - uid: z.string().optional(), title: z.string().optional(), startTime: z.date().optional(), endTime: z.date().optional(), diff --git a/pages/api/attendees/[id].ts b/pages/api/attendees/[id].ts index a1674d87a4..a72dbf5812 100644 --- a/pages/api/attendees/[id].ts +++ b/pages/api/attendees/[id].ts @@ -11,7 +11,7 @@ import { } from "@lib/validations/shared/queryIdTransformParseInt"; export async function attendeeById( - { method, query, body, userId }: NextApiRequest, + { method, query, body, userId, isAdmin }: NextApiRequest, res: NextApiResponse ) { const safeQuery = schemaQueryIdParseInt.safeParse(query); @@ -33,9 +33,11 @@ export async function attendeeById( .flat() .map((attendee) => attendee.id) ); - // @note: Here we make sure to only return attendee's of the user's own bookings. - if (!userBookingsAttendeeIds.includes(safeQuery.data.id)) res.status(401).json({ message: "Unauthorized" }); - else { + // @note: Here we make sure to only return attendee's of the user's own bookings if the user is not an admin. + if (!isAdmin) { + if (!userBookingsAttendeeIds.includes(safeQuery.data.id)) + res.status(401).json({ message: "Unauthorized" }); + } else { switch (method) { /** * @swagger diff --git a/pages/api/attendees/index.ts b/pages/api/attendees/index.ts index eaae0b28ff..6b2c4fc415 100644 --- a/pages/api/attendees/index.ts +++ b/pages/api/attendees/index.ts @@ -1,24 +1,30 @@ import type { NextApiRequest, NextApiResponse } from "next"; -import db from "@calcom/prisma"; +import prisma from "@calcom/prisma"; import { withMiddleware } from "@lib/helpers/withMiddleware"; import { AttendeeResponse, AttendeesResponse } from "@lib/types"; import { schemaAttendeeCreateBodyParams, schemaAttendeeReadPublic } from "@lib/validations/attendee"; async function createOrlistAllAttendees( - { method, userId, body }: NextApiRequest, + { method, userId, body, isAdmin }: NextApiRequest, res: NextApiResponse ) { - const userBookings = await db.booking.findMany({ - where: { - userId, - }, - include: { - attendees: true, - }, - }); - const attendees = userBookings.map((booking) => booking.attendees).flat(); + let attendees; + if (!isAdmin) { + const userBookings = await prisma.booking.findMany({ + where: { + userId, + }, + include: { + attendees: true, + }, + }); + attendees = userBookings.map((booking) => booking.attendees).flat(); + } else { + const data = await prisma.attendee.findMany(); + attendees = data.map((attendee) => schemaAttendeeReadPublic.parse(attendee)); + } if (method === "GET") { /** * @swagger @@ -37,12 +43,7 @@ async function createOrlistAllAttendees( * description: No attendees were found */ if (attendees) res.status(200).json({ attendees }); - else - (error: Error) => - res.status(404).json({ - message: "No Attendees were found", - error, - }); + else (error: Error) => res.status(400).json({ error }); } else if (method === "POST") { /** * @swagger @@ -90,16 +91,40 @@ async function createOrlistAllAttendees( res.status(400).json({ message: "Invalid request body", error: safePost.error }); return; } - const userWithBookings = await db.user.findUnique({ where: { id: userId }, include: { bookings: true } }); - if (!userWithBookings) { - res.status(404).json({ message: "User not found" }); - return; - } - const userBookingIds = userWithBookings.bookings.map((booking: { id: number }) => booking.id).flat(); - // Here we make sure to only return attendee's of the user's own bookings. - if (!userBookingIds.includes(safePost.data.bookingId)) res.status(401).json({ message: "Unauthorized" }); - else { - const data = await db.attendee.create({ + if (!isAdmin) { + const userWithBookings = await prisma.user.findUnique({ + where: { id: userId }, + include: { bookings: true }, + }); + if (!userWithBookings) { + res.status(404).json({ message: "User not found" }); + return; + } + const userBookingIds = userWithBookings.bookings.map((booking: { id: number }) => booking.id).flat(); + // Here we make sure to only return attendee's of the user's own bookings. + if (!userBookingIds.includes(safePost.data.bookingId)) + res.status(401).json({ message: "Unauthorized" }); + else { + const data = await prisma.attendee.create({ + data: { + email: safePost.data.email, + name: safePost.data.name, + timeZone: safePost.data.timeZone, + booking: { connect: { id: safePost.data.bookingId } }, + }, + }); + const attendee = schemaAttendeeReadPublic.parse(data); + + if (attendee) { + res.status(201).json({ + attendee, + message: "Attendee created successfully", + }); + } else (error: Error) => res.status(400).json({ error }); + } + } else { + // @todo: check real availability times before booking + const data = await prisma.attendee.create({ data: { email: safePost.data.email, name: safePost.data.name, diff --git a/pages/api/bookings/index.ts b/pages/api/bookings/index.ts index ab85b339cd..47d9b80da3 100644 --- a/pages/api/bookings/index.ts +++ b/pages/api/bookings/index.ts @@ -12,7 +12,7 @@ import { schemaBookingCreateBodyParams, schemaBookingReadPublic } from "@lib/val import { schemaEventTypeReadPublic } from "@lib/validations/event-type"; async function createOrlistAllBookings( - { method, body, userId }: NextApiRequest, + { method, body, userId, isAdmin }: NextApiRequest, res: NextApiResponse ) { console.log("userIduserId", userId); diff --git a/pages/api/event-types/[id].ts b/pages/api/event-types/[id].ts index 19435a0bc6..013f9909be 100644 --- a/pages/api/event-types/[id].ts +++ b/pages/api/event-types/[id].ts @@ -4,7 +4,6 @@ import prisma from "@calcom/prisma"; import { withMiddleware } from "@lib/helpers/withMiddleware"; import type { EventTypeResponse } from "@lib/types"; -import { isAdminGuard } from "@lib/utils/isAdmin"; import { schemaEventTypeEditBodyParams, schemaEventTypeReadPublic } from "@lib/validations/event-type"; import { schemaQueryIdParseInt, @@ -12,10 +11,9 @@ import { } from "@lib/validations/shared/queryIdTransformParseInt"; export async function eventTypeById( - { method, query, body, userId }: NextApiRequest, + { method, query, body, userId, isAdmin }: NextApiRequest, res: NextApiResponse ) { - const isAdmin = await isAdminGuard(userId); const safeQuery = schemaQueryIdParseInt.safeParse(query); if (!safeQuery.success) { res.status(400).json({ message: "Your query was invalid" }); diff --git a/pages/api/event-types/index.ts b/pages/api/event-types/index.ts index bfe8c3e7ee..dc6844e039 100644 --- a/pages/api/event-types/index.ts +++ b/pages/api/event-types/index.ts @@ -4,14 +4,12 @@ import prisma from "@calcom/prisma"; import { withMiddleware } from "@lib/helpers/withMiddleware"; import { EventTypeResponse, EventTypesResponse } from "@lib/types"; -import { isAdminGuard } from "@lib/utils/isAdmin"; import { schemaEventTypeCreateBodyParams, schemaEventTypeReadPublic } from "@lib/validations/event-type"; async function createOrlistAllEventTypes( - { method, body, userId }: NextApiRequest, + { method, body, userId, isAdmin }: NextApiRequest, res: NextApiResponse ) { - const isAdmin = await isAdminGuard(userId); if (method === "GET") { /** * @swagger diff --git a/pages/api/users/[id].ts b/pages/api/users/[id].ts new file mode 100644 index 0000000000..bf70a7a785 --- /dev/null +++ b/pages/api/users/[id].ts @@ -0,0 +1,184 @@ +import type { NextApiRequest, NextApiResponse } from "next"; + +import prisma from "@calcom/prisma"; + +import { withMiddleware } from "@lib/helpers/withMiddleware"; +import type { UserResponse } from "@lib/types"; +import { + schemaQueryIdParseInt, + withValidQueryIdTransformParseInt, +} from "@lib/validations/shared/queryIdTransformParseInt"; +import { schemaUserEditBodyParams, schemaUserReadPublic } from "@lib/validations/user"; + +export async function userById( + { method, query, body, userId, isAdmin }: NextApiRequest, + res: NextApiResponse +) { + const safeQuery = schemaQueryIdParseInt.safeParse(query); + console.log(body); + if (!safeQuery.success) { + res.status(400).json({ message: "Your query was invalid" }); + return; + } + // 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)); From 6349491ae7d711a9134c66e4e17f92fab96cbe3f Mon Sep 17 00:00:00 2001 From: Agusti Fernandez Pardo Date: Wed, 15 Jun 2022 21:10:19 +0200 Subject: [PATCH 61/70] feat: admin endpoints for bookings --- lib/validations/booking.ts | 1 - pages/api/bookings/[id].ts | 8 +++++--- pages/api/bookings/index.ts | 37 ++++++++++++++++++++++++++----------- 3 files changed, 31 insertions(+), 15 deletions(-) diff --git a/lib/validations/booking.ts b/lib/validations/booking.ts index a8a4b52c37..a7a8eab730 100644 --- a/lib/validations/booking.ts +++ b/lib/validations/booking.ts @@ -3,7 +3,6 @@ import { z } from "zod"; import { _BookingModel as Booking } from "@calcom/prisma/zod"; const schemaBookingBaseBodyParams = Booking.pick({ - uid: true, userId: true, eventTypeId: true, title: true, diff --git a/pages/api/bookings/[id].ts b/pages/api/bookings/[id].ts index 7e17c9613a..8603af73c8 100644 --- a/pages/api/bookings/[id].ts +++ b/pages/api/bookings/[id].ts @@ -11,7 +11,7 @@ import { } from "@lib/validations/shared/queryIdTransformParseInt"; export async function bookingById( - { method, query, body, userId }: NextApiRequest, + { method, query, body, userId, isAdmin }: NextApiRequest, res: NextApiResponse ) { const safeQuery = schemaQueryIdParseInt.safeParse(query); @@ -25,8 +25,10 @@ export async function bookingById( }); if (!userWithBookings) throw new Error("User not found"); const userBookingIds = userWithBookings.bookings.map((booking: { id: number }) => booking.id).flat(); - if (!userBookingIds.includes(safeQuery.data.id)) res.status(401).json({ message: "Unauthorized" }); - else { + + if (!isAdmin) { + if (!userBookingIds.includes(safeQuery.data.id)) res.status(401).json({ message: "Unauthorized" }); + } else { switch (method) { /** * @swagger diff --git a/pages/api/bookings/index.ts b/pages/api/bookings/index.ts index 47d9b80da3..560bca7b8e 100644 --- a/pages/api/bookings/index.ts +++ b/pages/api/bookings/index.ts @@ -33,16 +33,29 @@ async function createOrlistAllBookings( * 404: * description: No bookings were found */ - const data = await prisma.booking.findMany({ where: { userId } }); - const bookings = data.map((booking) => schemaBookingReadPublic.parse(booking)); - console.log(`Bookings requested by ${userId}`); - if (bookings) res.status(200).json({ bookings }); - else - (error: Error) => - res.status(404).json({ - message: "No Bookings were found", - error, - }); + if (!isAdmin) { + const data = await prisma.booking.findMany({ where: { userId } }); + const bookings = data.map((booking) => schemaBookingReadPublic.parse(booking)); + if (bookings) res.status(200).json({ bookings }); + else { + (error: Error) => + res.status(404).json({ + message: "No Bookings were found", + error, + }); + } + } else { + const data = await prisma.booking.findMany(); + const bookings = data.map((booking) => schemaBookingReadPublic.parse(booking)); + if (bookings) res.status(200).json({ bookings }); + else { + (error: Error) => + res.status(404).json({ + message: "No Bookings were found", + error, + }); + } + } } else if (method === "POST") { /** * @swagger @@ -83,7 +96,9 @@ async function createOrlistAllBookings( res.status(400).json({ message: "Bad request. Booking body is invalid." }); return; } - safe.data.userId = userId; + if (!isAdmin) { + safe.data.userId = userId; + } const data = await prisma.booking.create({ data: { uid: uuidv4(), ...safe.data } }); const booking = schemaBookingReadPublic.parse(data); From fe53fd92486657f3a06dadfabded89856bf4beed Mon Sep 17 00:00:00 2001 From: Agusti Fernandez Pardo Date: Wed, 15 Jun 2022 21:14:35 +0200 Subject: [PATCH 62/70] fix: remove comment --- pages/api/attendees/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/pages/api/attendees/index.ts b/pages/api/attendees/index.ts index 6b2c4fc415..e4f3e54f79 100644 --- a/pages/api/attendees/index.ts +++ b/pages/api/attendees/index.ts @@ -123,7 +123,6 @@ async function createOrlistAllAttendees( } else (error: Error) => res.status(400).json({ error }); } } else { - // @todo: check real availability times before booking const data = await prisma.attendee.create({ data: { email: safePost.data.email, From 3310b9307de602198e2df64e8e56631ce553c794 Mon Sep 17 00:00:00 2001 From: Agusti Fernandez Pardo Date: Wed, 15 Jun 2022 21:16:46 +0200 Subject: [PATCH 63/70] fix: undo changes in users --- pages/api/users/[id].ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pages/api/users/[id].ts b/pages/api/users/[id].ts index bf70a7a785..65b864c652 100644 --- a/pages/api/users/[id].ts +++ b/pages/api/users/[id].ts @@ -4,6 +4,7 @@ import prisma from "@calcom/prisma"; import { withMiddleware } from "@lib/helpers/withMiddleware"; import type { UserResponse } from "@lib/types"; +import { isAdminGuard } from "@lib/utils/isAdmin"; import { schemaQueryIdParseInt, withValidQueryIdTransformParseInt, @@ -11,7 +12,7 @@ import { import { schemaUserEditBodyParams, schemaUserReadPublic } from "@lib/validations/user"; export async function userById( - { method, query, body, userId, isAdmin }: NextApiRequest, + { method, query, body, userId }: NextApiRequest, res: NextApiResponse ) { const safeQuery = schemaQueryIdParseInt.safeParse(query); @@ -20,6 +21,7 @@ export async function userById( 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" }); From 998970c4cbe527f3c875418bd680403fac826708 Mon Sep 17 00:00:00 2001 From: Agusti Fernandez Pardo Date: Wed, 15 Jun 2022 23:06:30 +0200 Subject: [PATCH 64/70] fix: remove duplicate users/id --- pages/api/users/[id].ts | 186 ---------------------------------------- 1 file changed, 186 deletions(-) delete mode 100644 pages/api/users/[id].ts diff --git a/pages/api/users/[id].ts b/pages/api/users/[id].ts deleted file mode 100644 index 65b864c652..0000000000 --- a/pages/api/users/[id].ts +++ /dev/null @@ -1,186 +0,0 @@ -import type { NextApiRequest, NextApiResponse } from "next"; - -import prisma from "@calcom/prisma"; - -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"; - -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)); From 4389288e699796c67e51dd2706ea003ff6161bed Mon Sep 17 00:00:00 2001 From: Agusti Fernandez Pardo Date: Thu, 16 Jun 2022 00:04:04 +0200 Subject: [PATCH 65/70] feat: adds users/id/availability endpoint --- pages/api/users/[id]/availability/_get.ts | 29 ++++++++++++++++++++++ pages/api/users/[id]/availability/index.ts | 9 +++++++ 2 files changed, 38 insertions(+) create mode 100644 pages/api/users/[id]/availability/_get.ts create mode 100644 pages/api/users/[id]/availability/index.ts diff --git a/pages/api/users/[id]/availability/_get.ts b/pages/api/users/[id]/availability/_get.ts new file mode 100644 index 0000000000..11c9b3d586 --- /dev/null +++ b/pages/api/users/[id]/availability/_get.ts @@ -0,0 +1,29 @@ +import type { NextApiRequest } from "next"; +import { z } from "zod"; + +import { getUserAvailability } from "@calcom/core/getUserAvailability"; +import { defaultResponder } from "@calcom/lib/server"; +import { stringOrNumber } from "@calcom/prisma/zod-utils"; + +const availabilitySchema = z + .object({ + id: stringOrNumber, + username: z.string().optional(), + dateFrom: z.string(), + dateTo: z.string(), + eventTypeId: stringOrNumber.optional(), + }) + .refine((data) => !!data.username || !!data.id, "Either username or userId should be filled in."); + +async function handler(req: NextApiRequest) { + const { username, id, eventTypeId, dateTo, dateFrom } = availabilitySchema.parse(req.query); + return getUserAvailability({ + username, + dateFrom, + dateTo, + eventTypeId, + userId: id, + }); +} + +export default defaultResponder(handler); diff --git a/pages/api/users/[id]/availability/index.ts b/pages/api/users/[id]/availability/index.ts new file mode 100644 index 0000000000..fe6357f6bc --- /dev/null +++ b/pages/api/users/[id]/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("./_get"), + }) +); From 11f3d411b3fbfdecf7a17e32a0e783e7d49c3042 Mon Sep 17 00:00:00 2001 From: Agusti Fernandez Pardo Date: Thu, 16 Jun 2022 00:18:40 +0200 Subject: [PATCH 66/70] fix: rename users id to users userId to easier availability sharing _get endpoint --- lib/validations/shared/queryUserId.ts | 15 ++++++++++ pages/api/availability/_get.ts | 2 +- pages/api/users/[id]/availability/_get.ts | 29 ------------------- pages/api/users/{[id] => [userId]}/_delete.ts | 7 +++-- pages/api/users/{[id] => [userId]}/_get.ts | 9 +++--- pages/api/users/{[id] => [userId]}/_patch.ts | 7 +++-- .../{[id] => [userId]}/availability/index.ts | 2 +- pages/api/users/{[id] => [userId]}/index.ts | 0 8 files changed, 30 insertions(+), 41 deletions(-) create mode 100644 lib/validations/shared/queryUserId.ts delete mode 100644 pages/api/users/[id]/availability/_get.ts rename pages/api/users/{[id] => [userId]}/_delete.ts (84%) rename pages/api/users/{[id] => [userId]}/_get.ts (82%) rename pages/api/users/{[id] => [userId]}/_patch.ts (90%) rename pages/api/users/{[id] => [userId]}/availability/index.ts (81%) rename pages/api/users/{[id] => [userId]}/index.ts (100%) diff --git a/lib/validations/shared/queryUserId.ts b/lib/validations/shared/queryUserId.ts new file mode 100644 index 0000000000..cbe6e282d2 --- /dev/null +++ b/lib/validations/shared/queryUserId.ts @@ -0,0 +1,15 @@ +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 schemaQueryUserId = baseApiParams + .extend({ + userId: z + .string() + .regex(/^\d+$/) + .transform((id) => parseInt(id)), + }) + .strict(); diff --git a/pages/api/availability/_get.ts b/pages/api/availability/_get.ts index 21450f005e..5289190c03 100644 --- a/pages/api/availability/_get.ts +++ b/pages/api/availability/_get.ts @@ -7,7 +7,7 @@ import { stringOrNumber } from "@calcom/prisma/zod-utils"; const availabilitySchema = z .object({ - userId: stringOrNumber.optional(), + userId: stringOrNumber, username: z.string().optional(), dateFrom: z.string(), dateTo: z.string(), diff --git a/pages/api/users/[id]/availability/_get.ts b/pages/api/users/[id]/availability/_get.ts deleted file mode 100644 index 11c9b3d586..0000000000 --- a/pages/api/users/[id]/availability/_get.ts +++ /dev/null @@ -1,29 +0,0 @@ -import type { NextApiRequest } from "next"; -import { z } from "zod"; - -import { getUserAvailability } from "@calcom/core/getUserAvailability"; -import { defaultResponder } from "@calcom/lib/server"; -import { stringOrNumber } from "@calcom/prisma/zod-utils"; - -const availabilitySchema = z - .object({ - id: stringOrNumber, - username: z.string().optional(), - dateFrom: z.string(), - dateTo: z.string(), - eventTypeId: stringOrNumber.optional(), - }) - .refine((data) => !!data.username || !!data.id, "Either username or userId should be filled in."); - -async function handler(req: NextApiRequest) { - const { username, id, eventTypeId, dateTo, dateFrom } = availabilitySchema.parse(req.query); - return getUserAvailability({ - username, - dateFrom, - dateTo, - eventTypeId, - userId: id, - }); -} - -export default defaultResponder(handler); diff --git a/pages/api/users/[id]/_delete.ts b/pages/api/users/[userId]/_delete.ts similarity index 84% rename from pages/api/users/[id]/_delete.ts rename to pages/api/users/[userId]/_delete.ts index 7d38d07372..3478c44789 100644 --- a/pages/api/users/[id]/_delete.ts +++ b/pages/api/users/[userId]/_delete.ts @@ -5,7 +5,7 @@ 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 { schemaQueryUserId } from "@lib/validations/shared/queryUserId"; /** * @swagger @@ -32,10 +32,11 @@ import { schemaQueryIdParseInt } from "@lib/validations/shared/queryIdTransformP * description: Authorization information is missing or invalid. */ export async function deleteHandler(req: NextApiRequest) { - const query = schemaQueryIdParseInt.parse(req.query); + const query = schemaQueryUserId.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" }); + if (!isAdmin && query.userId !== req.userId) + throw new HttpError({ statusCode: 401, message: "Unauthorized" }); const user = await prisma.user.findUnique({ where: { id: query.id } }); if (!user) throw new HttpError({ statusCode: 404, message: "User not found" }); diff --git a/pages/api/users/[id]/_get.ts b/pages/api/users/[userId]/_get.ts similarity index 82% rename from pages/api/users/[id]/_get.ts rename to pages/api/users/[userId]/_get.ts index 443b935e9c..5c76c4e315 100644 --- a/pages/api/users/[id]/_get.ts +++ b/pages/api/users/[userId]/_get.ts @@ -5,7 +5,7 @@ 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 { schemaQueryUserId } from "@lib/validations/shared/queryUserId"; import { schemaUserReadPublic } from "@lib/validations/user"; /** @@ -33,11 +33,12 @@ import { schemaUserReadPublic } from "@lib/validations/user"; * description: User was not found */ export async function getHandler(req: NextApiRequest) { - const query = schemaQueryIdParseInt.parse(req.query); + const query = schemaQueryUserId.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 } }); + if (!isAdmin && query.userId !== req.userId) + throw new HttpError({ statusCode: 401, message: "Unauthorized" }); + const data = await prisma.user.findUnique({ where: { id: query.userId } }); const user = schemaUserReadPublic.parse(data); return { user }; } diff --git a/pages/api/users/[id]/_patch.ts b/pages/api/users/[userId]/_patch.ts similarity index 90% rename from pages/api/users/[id]/_patch.ts rename to pages/api/users/[userId]/_patch.ts index a6551609e7..9f32e920c4 100644 --- a/pages/api/users/[id]/_patch.ts +++ b/pages/api/users/[userId]/_patch.ts @@ -5,7 +5,7 @@ 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 { schemaQueryUserId } from "@lib/validations/shared/queryUserId"; import { schemaUserEditBodyParams, schemaUserReadPublic } from "@lib/validations/user"; /** @@ -54,10 +54,11 @@ import { schemaUserEditBodyParams, schemaUserReadPublic } from "@lib/validations * description: Authorization information is missing or invalid. */ export async function patchHandler(req: NextApiRequest) { - const query = schemaQueryIdParseInt.parse(req.query); + const query = schemaQueryUserId.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" }); + if (!isAdmin && query.userId !== req.userId) + throw new HttpError({ statusCode: 401, message: "Unauthorized" }); const body = schemaUserEditBodyParams.parse(req.body); const userSchedules = await prisma.schedule.findMany({ diff --git a/pages/api/users/[id]/availability/index.ts b/pages/api/users/[userId]/availability/index.ts similarity index 81% rename from pages/api/users/[id]/availability/index.ts rename to pages/api/users/[userId]/availability/index.ts index fe6357f6bc..1a27360f81 100644 --- a/pages/api/users/[id]/availability/index.ts +++ b/pages/api/users/[userId]/availability/index.ts @@ -4,6 +4,6 @@ import { withMiddleware } from "@lib/helpers/withMiddleware"; export default withMiddleware("HTTP_GET")( defaultHandler({ - GET: import("./_get"), + GET: import("@api/availability/_get"), }) ); diff --git a/pages/api/users/[id]/index.ts b/pages/api/users/[userId]/index.ts similarity index 100% rename from pages/api/users/[id]/index.ts rename to pages/api/users/[userId]/index.ts From 1889ececf8a7e42eddb1370df18b3ecc7cbfc671 Mon Sep 17 00:00:00 2001 From: Agusti Fernandez Pardo Date: Thu, 16 Jun 2022 01:27:26 +0200 Subject: [PATCH 67/70] feat: improve deployment docs for api --- README.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/README.md b/README.md index 8c39f8457f..ad6c3a78ab 100644 --- a/README.md +++ b/README.md @@ -178,3 +178,20 @@ We make sure of this by not using next in dev, but next build && next start, if See . Here in dev mode OPTIONS method is hardcoded to return only GET and OPTIONS as allowed method. Running in Production mode would cause this file to be not used. This is hot-reloading logic only. To remove this limitation, we need to ensure that on local endpoints are requested by swagger at /api/v1 and not /v1 + + +## Deployment + +We recommend deploying API in vercel. + +There's some settings that you'll need to setup. + +Under Vercel > Your API Deployment > Settings + +In General > Build & Development Settings +BUILD COMMAND: `yarn turbo run build --scope=@calcom/api --include-dependencies --no-deps` +OUTPUT DIRECTORY: `apps/api/.next` + +See `scripts/vercel-deploy.sh` for more info on how the deployment is done. +## Environment variables +Lastly API requires an env var for `DATABASE_URL` \ No newline at end of file From 8b87cde733acef43660e7105e5f67eb4f2d86d20 Mon Sep 17 00:00:00 2001 From: Agusti Fernandez Pardo Date: Thu, 16 Jun 2022 01:48:29 +0200 Subject: [PATCH 68/70] fix: _delete missing userId rename --- pages/api/users/[userId]/_delete.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/api/users/[userId]/_delete.ts b/pages/api/users/[userId]/_delete.ts index 3478c44789..c2d8d5db29 100644 --- a/pages/api/users/[userId]/_delete.ts +++ b/pages/api/users/[userId]/_delete.ts @@ -38,7 +38,7 @@ export async function deleteHandler(req: NextApiRequest) { if (!isAdmin && query.userId !== req.userId) throw new HttpError({ statusCode: 401, message: "Unauthorized" }); - const user = await prisma.user.findUnique({ where: { id: query.id } }); + const user = await prisma.user.findUnique({ where: { id: query.userId } }); if (!user) throw new HttpError({ statusCode: 404, message: "User not found" }); await prisma.user.delete({ where: { id: user.id } }); From 29be8f52667576b5090e58e84b9be08b3688f72a Mon Sep 17 00:00:00 2001 From: Agusti Fernandez Pardo Date: Thu, 16 Jun 2022 21:50:41 +0200 Subject: [PATCH 69/70] fix: makes id optional in availability --- 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 5289190c03..21450f005e 100644 --- a/pages/api/availability/_get.ts +++ b/pages/api/availability/_get.ts @@ -7,7 +7,7 @@ import { stringOrNumber } from "@calcom/prisma/zod-utils"; const availabilitySchema = z .object({ - userId: stringOrNumber, + userId: stringOrNumber.optional(), username: z.string().optional(), dateFrom: z.string(), dateTo: z.string(), From 2682db8fcd0239868a11bfbc34cfd95bb615e6f0 Mon Sep 17 00:00:00 2001 From: Agusti Fernandez Pardo Date: Thu, 16 Jun 2022 23:38:36 +0200 Subject: [PATCH 70/70] fix: remove user metadta from validations --- lib/validations/user.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/lib/validations/user.ts b/lib/validations/user.ts index 1a69eb5c7c..dcaafdf8d2 100644 --- a/lib/validations/user.ts +++ b/lib/validations/user.ts @@ -59,7 +59,6 @@ export const schemaUserBaseBodyParams = User.pick({ timeZone: true, weekStart: true, endTime: true, - metadata: true, bufferTime: true, theme: true, defaultScheduleId: true, @@ -93,7 +92,6 @@ const schemaUserEditParams = z.object({ .optional() .nullable(), locale: z.nativeEnum(locales).optional().nullable(), - metadata: jsonSchema.optional(), }); // @note: These are the values that are editable via PATCH method on the user Model, @@ -116,7 +114,6 @@ const schemaUserCreateParams = z.object({ .optional() .nullable(), locale: z.nativeEnum(locales).optional(), - metadata: jsonSchema, createdDate: z.string().or(z.date()).optional(), }); @@ -141,7 +138,6 @@ export const schemaUserReadPublic = User.pick({ emailVerified: true, bio: true, avatar: true, - metadata: true, timeZone: true, weekStart: true, endTime: true,