From e407a16766996a1b00e5c1791c94e2fdd3e46ed7 Mon Sep 17 00:00:00 2001 From: Agusti Fernandez Pardo Date: Mon, 6 Jun 2022 18:17:10 +0200 Subject: [PATCH 01/20] feat: dynamic prisma --- jest.setup.ts | 2 +- lib/helpers/customApiEndpoints.ts | 20 ++++++ lib/helpers/extendRequest.ts | 19 +++++ lib/helpers/verifyApiKey.ts | 24 ++++--- lib/helpers/withMiddleware.ts | 6 +- lib/utils/isAdmin.ts | 2 +- pages/api/attendees/[id].ts | 13 ++-- pages/api/attendees/index.ts | 92 ++++++++++++++---------- pages/api/availabilities/[id].ts | 5 +- pages/api/availabilities/index.ts | 5 +- pages/api/booking-references/[id].ts | 5 +- pages/api/booking-references/index.ts | 4 +- pages/api/bookings/[id].ts | 13 ++-- pages/api/bookings/index.ts | 67 ++++++++++------- pages/api/custom-inputs/[id].ts | 4 +- pages/api/custom-inputs/index.ts | 4 +- pages/api/destination-calendars/[id].ts | 4 +- pages/api/destination-calendars/index.ts | 4 +- pages/api/event-references/[id].ts | 4 +- pages/api/event-references/index.ts | 4 +- pages/api/event-types/[id].ts | 4 +- pages/api/event-types/index.ts | 6 +- pages/api/hooks/[id].ts | 4 +- pages/api/hooks/index.ts | 4 +- pages/api/memberships/[id].ts | 4 +- pages/api/memberships/index.ts | 4 +- pages/api/payments/[id].ts | 4 +- pages/api/payments/index.ts | 4 +- pages/api/schedules/[id].ts | 4 +- pages/api/schedules/index.ts | 4 +- pages/api/selected-calendars/[id].ts | 4 +- pages/api/selected-calendars/index.ts | 4 +- pages/api/teams/[id].ts | 4 +- pages/api/teams/index.ts | 4 +- pages/api/users/[id].ts | 4 +- pages/api/users/index.ts | 4 +- 36 files changed, 201 insertions(+), 166 deletions(-) create mode 100644 lib/helpers/customApiEndpoints.ts create mode 100644 lib/helpers/extendRequest.ts diff --git a/jest.setup.ts b/jest.setup.ts index b764cee58f..d26c6b4183 100644 --- a/jest.setup.ts +++ b/jest.setup.ts @@ -1,6 +1,6 @@ import prisma from "@calcom/prisma"; afterEach((done) => { - prisma.$disconnect().then(); + prisma().$disconnect().then(); done(); }); diff --git a/lib/helpers/customApiEndpoints.ts b/lib/helpers/customApiEndpoints.ts new file mode 100644 index 0000000000..55d7dc78a2 --- /dev/null +++ b/lib/helpers/customApiEndpoints.ts @@ -0,0 +1,20 @@ +import { NextMiddleware } from "next-api-middleware"; + +import prisma from "@calcom/prisma"; + +// This replaces the prisma client for the cusotm one if the customApiId is valid +export const customApiEndpoints: NextMiddleware = async (req, res, next) => { + const { + query: { customApiId }, + } = req; + // If no custom api Id is provided, return the regular cal.com prisma client. + if (!customApiId) { + req.prisma = prisma(); + await next(); + } + // Hardcoded new database, this should be a dynamic call to console prisma/api? + const newDatabseUrl = process.env.DATABASE_PROD_URL; + req.prisma = prisma(newDatabseUrl); + + await next(); +}; diff --git a/lib/helpers/extendRequest.ts b/lib/helpers/extendRequest.ts new file mode 100644 index 0000000000..ac9e54d5ed --- /dev/null +++ b/lib/helpers/extendRequest.ts @@ -0,0 +1,19 @@ +import type { IncomingMessage } from "http"; +import { NextMiddleware } from "next-api-middleware"; + +import type { PrismaClient } from ".prisma/client"; + +/** @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; + prisma: PrismaClient; + body: any; + query: { [key: string]: string | string[] }; + } +} +export const extendRequest: NextMiddleware = async (req, res, next) => { + await next(); +}; diff --git a/lib/helpers/verifyApiKey.ts b/lib/helpers/verifyApiKey.ts index c45f3d3c3f..d350f4e1f3 100644 --- a/lib/helpers/verifyApiKey.ts +++ b/lib/helpers/verifyApiKey.ts @@ -1,18 +1,19 @@ -import type { IncomingMessage } from "http"; +// import type { IncomingMessage } from "http"; import { NextMiddleware } from "next-api-middleware"; import { hashAPIKey } from "@calcom/ee/lib/api/apiKeys"; -import prisma from "@calcom/prisma"; -/** @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; - query: { [key: string]: string | string[] }; - } -} +// // import prisma from "@calcom/prisma"; + +// /** @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; +// query: { [key: string]: string | string[] }; +// } +// } // Used to check if the apiKey is not expired, could be extracted if reused. but not for now. export const dateNotInPast = function (date: Date) { @@ -24,6 +25,7 @@ export const dateNotInPast = function (date: Date) { // This verifies the apiKey and sets the user if it is valid. export const verifyApiKey: NextMiddleware = async (req, res, next) => { + const { prisma } = req; if (!req.query.apiKey) return res.status(401).json({ message: "No apiKey provided" }); // We remove the prefix from the user provided api_key. If no env set default to "cal_" const strippedApiKey = `${req.query.apiKey}`.replace(process.env.API_KEY_PREFIX || "cal_", ""); diff --git a/lib/helpers/withMiddleware.ts b/lib/helpers/withMiddleware.ts index c926eff4ec..73eef171f9 100644 --- a/lib/helpers/withMiddleware.ts +++ b/lib/helpers/withMiddleware.ts @@ -2,6 +2,8 @@ import { label } from "next-api-middleware"; import { addRequestId } from "./addRequestid"; import { captureErrors } from "./captureErrors"; +import { customApiEndpoints } from "./customApiEndpoints"; +import { extendRequest } from "./extendRequest"; import { HTTP_POST, HTTP_DELETE, @@ -22,9 +24,11 @@ const withMiddleware = label( HTTP_DELETE, addRequestId, verifyApiKey, + customApiEndpoints, + extendRequest, sentry: captureErrors, }, - ["sentry", "verifyApiKey", "addRequestId"] // <-- Provide a list of middleware to call automatically + ["sentry", "customApiEndpoints", "verifyApiKey", "addRequestId", "extendRequest"] // <-- Provide a list of middleware to call automatically ); export { withMiddleware }; diff --git a/lib/utils/isAdmin.ts b/lib/utils/isAdmin.ts index 64af37367f..4991934509 100644 --- a/lib/utils/isAdmin.ts +++ b/lib/utils/isAdmin.ts @@ -3,6 +3,6 @@ import { UserPermissionRole } from "@prisma/client"; import prisma from "@calcom/prisma"; export const isAdminGuard = async (userId: number) => { - const user = await prisma.user.findUnique({ where: { id: userId } }); + const user = await prisma().user.findUnique({ where: { id: userId } }); return user?.role === UserPermissionRole.ADMIN; }; diff --git a/pages/api/attendees/[id].ts b/pages/api/attendees/[id].ts index a1674d87a4..c37e92b9ba 100644 --- a/pages/api/attendees/[id].ts +++ b/pages/api/attendees/[id].ts @@ -1,9 +1,9 @@ import type { NextApiRequest, NextApiResponse } from "next"; -import prisma from "@calcom/prisma"; - +// // import prisma from "@calcom/prisma"; import { withMiddleware } from "@lib/helpers/withMiddleware"; import type { AttendeeResponse } from "@lib/types"; +import { isAdminGuard } from "@lib/utils/isAdmin"; import { schemaAttendeeEditBodyParams, schemaAttendeeReadPublic } from "@lib/validations/attendee"; import { schemaQueryIdParseInt, @@ -11,9 +11,10 @@ import { } from "@lib/validations/shared/queryIdTransformParseInt"; export async function attendeeById( - { method, query, body, userId }: NextApiRequest, + { method, query, body, userId, prisma }: NextApiRequest, res: NextApiResponse ) { + const isAdmin = await isAdminGuard(userId); const safeQuery = schemaQueryIdParseInt.safeParse(query); if (!safeQuery.success) { res.status(400).json({ error: safeQuery.error }); @@ -34,8 +35,10 @@ export async function attendeeById( .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 { + 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..25f459823e 100644 --- a/pages/api/attendees/index.ts +++ b/pages/api/attendees/index.ts @@ -1,24 +1,31 @@ import type { NextApiRequest, NextApiResponse } from "next"; -import db from "@calcom/prisma"; - +// import db from "@calcom/prisma"; import { withMiddleware } from "@lib/helpers/withMiddleware"; -import { AttendeeResponse, AttendeesResponse } from "@lib/types"; +import type { AttendeeResponse, AttendeesResponse } from "@lib/types"; +import { isAdminGuard } from "@lib/utils/isAdmin"; import { schemaAttendeeCreateBodyParams, schemaAttendeeReadPublic } from "@lib/validations/attendee"; async function createOrlistAllAttendees( - { method, userId, body }: NextApiRequest, + { method, userId, body, prisma }: NextApiRequest, res: NextApiResponse ) { - const userBookings = await db.booking.findMany({ - where: { - userId, - }, - include: { - attendees: true, - }, - }); - const attendees = userBookings.map((booking) => booking.attendees).flat(); + const isAdmin = await isAdminGuard(userId); + 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 +44,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 +92,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, @@ -114,13 +140,7 @@ async function createOrlistAllAttendees( attendee, message: "Attendee created successfully", }); - } else { - (error: Error) => - res.status(400).json({ - message: "Could not create new attendee", - error, - }); - } + } else (error: Error) => res.status(400).json({ error }); } } else res.status(405).json({ message: `Method ${method} not allowed` }); } diff --git a/pages/api/availabilities/[id].ts b/pages/api/availabilities/[id].ts index 0b6a58c338..3dcbdbbca3 100644 --- a/pages/api/availabilities/[id].ts +++ b/pages/api/availabilities/[id].ts @@ -1,7 +1,6 @@ import type { NextApiRequest, NextApiResponse } from "next"; -import prisma from "@calcom/prisma"; - +// // import prisma from "@calcom/prisma"; import { withMiddleware } from "@lib/helpers/withMiddleware"; import type { AvailabilityResponse } from "@lib/types"; import { @@ -14,7 +13,7 @@ import { } from "@lib/validations/shared/queryIdTransformParseInt"; export async function availabilityById( - { method, query, body, userId }: NextApiRequest, + { method, query, body, userId, prisma }: NextApiRequest, res: NextApiResponse ) { const safeQuery = schemaQueryIdParseInt.safeParse(query); diff --git a/pages/api/availabilities/index.ts b/pages/api/availabilities/index.ts index bf17bd1ede..0e45564270 100644 --- a/pages/api/availabilities/index.ts +++ b/pages/api/availabilities/index.ts @@ -1,7 +1,6 @@ import type { NextApiRequest, NextApiResponse } from "next"; -import prisma from "@calcom/prisma"; - +// import prisma from "@calcom/prisma"; import { withMiddleware } from "@lib/helpers/withMiddleware"; import { AvailabilityResponse, AvailabilitiesResponse } from "@lib/types"; import { @@ -10,7 +9,7 @@ import { } from "@lib/validations/availability"; async function createOrlistAllAvailabilities( - { method, body, userId }: NextApiRequest, + { method, body, userId, prisma }: NextApiRequest, res: NextApiResponse ) { if (method === "GET") { diff --git a/pages/api/booking-references/[id].ts b/pages/api/booking-references/[id].ts index f7a14056eb..17e444d717 100644 --- a/pages/api/booking-references/[id].ts +++ b/pages/api/booking-references/[id].ts @@ -1,7 +1,5 @@ import type { NextApiRequest, NextApiResponse } from "next"; -import prisma from "@calcom/prisma"; - import { withMiddleware } from "@lib/helpers/withMiddleware"; import type { BookingReferenceResponse } from "@lib/types"; import { @@ -14,7 +12,7 @@ import { } from "@lib/validations/shared/queryIdTransformParseInt"; export async function bookingReferenceById( - { method, query, body, userId }: NextApiRequest, + { method, query, body, userId, prisma }: NextApiRequest, res: NextApiResponse ) { const safeQuery = schemaQueryIdParseInt.safeParse(query); @@ -64,7 +62,6 @@ export async function bookingReferenceById( .then((data) => schemaBookingReferenceReadPublic.parse(data)) .then((booking_reference) => res.status(200).json({ booking_reference })) .catch((error: Error) => { - console.log(error); res.status(404).json({ message: `BookingReference with id: ${safeQuery.data.id} not found`, error, diff --git a/pages/api/booking-references/index.ts b/pages/api/booking-references/index.ts index 58e45d5c9e..5c63a605b7 100644 --- a/pages/api/booking-references/index.ts +++ b/pages/api/booking-references/index.ts @@ -1,7 +1,5 @@ import type { NextApiRequest, NextApiResponse } from "next"; -import prisma from "@calcom/prisma"; - import { withMiddleware } from "@lib/helpers/withMiddleware"; import { BookingReferenceResponse, BookingReferencesResponse } from "@lib/types"; import { @@ -10,7 +8,7 @@ import { } from "@lib/validations/booking-reference"; async function createOrlistAllBookingReferences( - { method, userId, body }: NextApiRequest, + { method, userId, body, prisma }: NextApiRequest, res: NextApiResponse ) { const userWithBookings = await prisma.user.findUnique({ diff --git a/pages/api/bookings/[id].ts b/pages/api/bookings/[id].ts index 7e17c9613a..68540fb69b 100644 --- a/pages/api/bookings/[id].ts +++ b/pages/api/bookings/[id].ts @@ -1,9 +1,9 @@ import type { NextApiRequest, NextApiResponse } from "next"; -import prisma from "@calcom/prisma"; - +// import prisma from "@calcom/prisma"; import { withMiddleware } from "@lib/helpers/withMiddleware"; import type { BookingResponse } from "@lib/types"; +import { isAdminGuard } from "@lib/utils/isAdmin"; import { schemaBookingEditBodyParams, schemaBookingReadPublic } from "@lib/validations/booking"; import { schemaQueryIdParseInt, @@ -11,7 +11,7 @@ import { } from "@lib/validations/shared/queryIdTransformParseInt"; export async function bookingById( - { method, query, body, userId }: NextApiRequest, + { method, query, body, userId, prisma }: NextApiRequest, res: NextApiResponse ) { const safeQuery = schemaQueryIdParseInt.safeParse(query); @@ -25,8 +25,11 @@ 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 { + const isAdmin = await isAdminGuard(userId); + + 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 40f24283ee..f1befaeb23 100644 --- a/pages/api/bookings/index.ts +++ b/pages/api/bookings/index.ts @@ -1,16 +1,16 @@ import type { NextApiRequest, NextApiResponse } from "next"; -import prisma from "@calcom/prisma"; - +// import prisma from "@calcom/prisma"; import { withMiddleware } from "@lib/helpers/withMiddleware"; -import { BookingResponse, BookingsResponse } from "@lib/types"; +import type { BookingResponse, BookingsResponse } from "@lib/types"; +import { isAdminGuard } from "@lib/utils/isAdmin"; import { schemaBookingCreateBodyParams, schemaBookingReadPublic } from "@lib/validations/booking"; async function createOrlistAllBookings( - { method, body, userId }: NextApiRequest, + { method, body, userId, prisma }: NextApiRequest, res: NextApiResponse ) { - console.log("userIduserId", userId); + const isAdmin = await isAdminGuard(userId); if (method === "GET") { /** * @swagger @@ -28,15 +28,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)); - 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 @@ -77,19 +91,20 @@ async function createOrlistAllBookings( res.status(400).json({ message: "Bad request. Booking body is invalid." }); return; } - safe.data.userId = userId; - const data = await prisma.booking.create({ data: { ...safe.data } }); - const booking = schemaBookingReadPublic.parse(data); + if (!isAdmin) { + safe.data.userId = userId; + 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" }); - else - (error: Error) => { - console.log(error); - res.status(400).json({ - message: "Could not create new booking", - error, - }); - }; + if (booking) res.status(201).json({ booking, message: "Booking created successfully" }); + else (error: Error) => res.status(400).json({ error }); + } else { + 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" }); + else (error: Error) => res.status(400).json({ error }); + } } else res.status(405).json({ message: `Method ${method} not allowed` }); } diff --git a/pages/api/custom-inputs/[id].ts b/pages/api/custom-inputs/[id].ts index 7423a10b99..5b778714ad 100644 --- a/pages/api/custom-inputs/[id].ts +++ b/pages/api/custom-inputs/[id].ts @@ -1,7 +1,5 @@ import type { NextApiRequest, NextApiResponse } from "next"; -import prisma from "@calcom/prisma"; - import { withMiddleware } from "@lib/helpers/withMiddleware"; import type { EventTypeCustomInputResponse } from "@lib/types"; import { @@ -72,7 +70,7 @@ import { * description: Authorization information is missing or invalid. */ async function eventTypeById( - { method, query, body, userId }: NextApiRequest, + { method, query, body, userId, prisma }: NextApiRequest, res: NextApiResponse ) { const safeQuery = schemaQueryIdParseInt.safeParse(query); diff --git a/pages/api/custom-inputs/index.ts b/pages/api/custom-inputs/index.ts index e1c2d484ed..17500249aa 100644 --- a/pages/api/custom-inputs/index.ts +++ b/pages/api/custom-inputs/index.ts @@ -1,7 +1,5 @@ import type { NextApiRequest, NextApiResponse } from "next"; -import prisma from "@calcom/prisma"; - import { withMiddleware } from "@lib/helpers/withMiddleware"; import { EventTypeCustomInputResponse, EventTypeCustomInputsResponse } from "@lib/types"; import { @@ -10,7 +8,7 @@ import { } from "@lib/validations/event-type-custom-input"; async function createOrlistAllEventTypeCustomInputs( - { userId, method, body }: NextApiRequest, + { userId, method, body, prisma }: NextApiRequest, res: NextApiResponse ) { const data = await prisma.eventType.findMany({ where: { userId } }); diff --git a/pages/api/destination-calendars/[id].ts b/pages/api/destination-calendars/[id].ts index 528b9e5f2d..307e2634ba 100644 --- a/pages/api/destination-calendars/[id].ts +++ b/pages/api/destination-calendars/[id].ts @@ -1,7 +1,5 @@ import type { NextApiRequest, NextApiResponse } from "next"; -import prisma from "@calcom/prisma"; - import { withMiddleware } from "@lib/helpers/withMiddleware"; import type { DestinationCalendarResponse } from "@lib/types"; import { @@ -14,7 +12,7 @@ import { } from "@lib/validations/shared/queryIdTransformParseInt"; export async function destionationCalendarById( - { method, query, body, userId }: NextApiRequest, + { method, query, body, userId, prisma }: NextApiRequest, res: NextApiResponse ) { const safeQuery = schemaQueryIdParseInt.safeParse(query); diff --git a/pages/api/destination-calendars/index.ts b/pages/api/destination-calendars/index.ts index bfd7df6d89..5fced320d9 100644 --- a/pages/api/destination-calendars/index.ts +++ b/pages/api/destination-calendars/index.ts @@ -1,7 +1,5 @@ import type { NextApiRequest, NextApiResponse } from "next"; -import prisma from "@calcom/prisma"; - import { withMiddleware } from "@lib/helpers/withMiddleware"; import { DestinationCalendarResponse, DestinationCalendarsResponse } from "@lib/types"; import { @@ -10,7 +8,7 @@ import { } from "@lib/validations/destination-calendar"; async function createOrlistAllDestinationCalendars( - { method, body, userId }: NextApiRequest, + { method, body, userId, prisma }: NextApiRequest, res: NextApiResponse ) { if (method === "GET") { diff --git a/pages/api/event-references/[id].ts b/pages/api/event-references/[id].ts index 7a72fea4c7..58bc27599d 100644 --- a/pages/api/event-references/[id].ts +++ b/pages/api/event-references/[id].ts @@ -1,7 +1,5 @@ import type { NextApiRequest, NextApiResponse } from "next"; -import prisma from "@calcom/prisma"; - import { withMiddleware } from "@lib/helpers/withMiddleware"; import type { DailyEventReferenceResponse } from "@lib/types"; import { @@ -14,7 +12,7 @@ import { } from "@lib/validations/shared/queryIdTransformParseInt"; export async function dailyEventReferenceById( - { method, query, body, userId }: NextApiRequest, + { method, query, body, userId, prisma }: NextApiRequest, res: NextApiResponse ) { const safeQuery = schemaQueryIdParseInt.safeParse(query); diff --git a/pages/api/event-references/index.ts b/pages/api/event-references/index.ts index 48a438e321..449737cd06 100644 --- a/pages/api/event-references/index.ts +++ b/pages/api/event-references/index.ts @@ -1,7 +1,5 @@ import type { NextApiRequest, NextApiResponse } from "next"; -import prisma from "@calcom/prisma"; - import { withMiddleware } from "@lib/helpers/withMiddleware"; import { DailyEventReferenceResponse, DailyEventReferencesResponse } from "@lib/types"; import { @@ -10,7 +8,7 @@ import { } from "@lib/validations/event-reference"; async function createOrlistAllDailyEventReferences( - { method, body, userId }: NextApiRequest, + { method, body, userId, prisma }: NextApiRequest, res: NextApiResponse ) { const userBookings = await prisma.booking.findMany({ where: { userId } }); diff --git a/pages/api/event-types/[id].ts b/pages/api/event-types/[id].ts index 19435a0bc6..117ee7bf43 100644 --- a/pages/api/event-types/[id].ts +++ b/pages/api/event-types/[id].ts @@ -1,7 +1,5 @@ import type { NextApiRequest, NextApiResponse } from "next"; -import prisma from "@calcom/prisma"; - import { withMiddleware } from "@lib/helpers/withMiddleware"; import type { EventTypeResponse } from "@lib/types"; import { isAdminGuard } from "@lib/utils/isAdmin"; @@ -12,7 +10,7 @@ import { } from "@lib/validations/shared/queryIdTransformParseInt"; export async function eventTypeById( - { method, query, body, userId }: NextApiRequest, + { method, query, body, userId, prisma }: NextApiRequest, res: NextApiResponse ) { const isAdmin = await isAdminGuard(userId); diff --git a/pages/api/event-types/index.ts b/pages/api/event-types/index.ts index bfe8c3e7ee..b7939bd0c7 100644 --- a/pages/api/event-types/index.ts +++ b/pages/api/event-types/index.ts @@ -1,14 +1,12 @@ import type { NextApiRequest, NextApiResponse } from "next"; -import prisma from "@calcom/prisma"; - import { withMiddleware } from "@lib/helpers/withMiddleware"; -import { EventTypeResponse, EventTypesResponse } from "@lib/types"; +import type { 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, prisma }: NextApiRequest, res: NextApiResponse ) { const isAdmin = await isAdminGuard(userId); diff --git a/pages/api/hooks/[id].ts b/pages/api/hooks/[id].ts index 32a81e7b12..e88761736c 100644 --- a/pages/api/hooks/[id].ts +++ b/pages/api/hooks/[id].ts @@ -1,14 +1,12 @@ import type { NextApiRequest, NextApiResponse } from "next"; -import prisma from "@calcom/prisma"; - import { withMiddleware } from "@lib/helpers/withMiddleware"; import type { WebhookResponse } from "@lib/types"; import { schemaQueryIdAsString } from "@lib/validations/shared/queryIdString"; import { schemaWebhookEditBodyParams, schemaWebhookReadPublic } from "@lib/validations/webhook"; export async function WebhookById( - { method, query, body, userId }: NextApiRequest, + { method, query, body, userId, prisma }: NextApiRequest, res: NextApiResponse ) { const safeQuery = schemaQueryIdAsString.safeParse(query); diff --git a/pages/api/hooks/index.ts b/pages/api/hooks/index.ts index 422a30e419..3fb8380f4c 100644 --- a/pages/api/hooks/index.ts +++ b/pages/api/hooks/index.ts @@ -1,14 +1,12 @@ import type { NextApiRequest, NextApiResponse } from "next"; import { v4 as uuidv4 } from "uuid"; -import prisma from "@calcom/prisma"; - import { withMiddleware } from "@lib/helpers/withMiddleware"; import { WebhookResponse, WebhooksResponse } from "@lib/types"; import { schemaWebhookCreateBodyParams } from "@lib/validations/webhook"; async function createOrlistAllWebhooks( - { method, body, userId }: NextApiRequest, + { method, body, userId, prisma }: NextApiRequest, res: NextApiResponse ) { if (method === "GET") { diff --git a/pages/api/memberships/[id].ts b/pages/api/memberships/[id].ts index ea99860d14..b6050ebbe9 100644 --- a/pages/api/memberships/[id].ts +++ b/pages/api/memberships/[id].ts @@ -1,14 +1,12 @@ import type { NextApiRequest, NextApiResponse } from "next"; -import prisma from "@calcom/prisma"; - import { withMiddleware } from "@lib/helpers/withMiddleware"; import type { MembershipResponse } from "@lib/types"; import { schemaMembershipBodyParams, schemaMembershipPublic } from "@lib/validations/membership"; import { schemaQueryIdAsString, withValidQueryIdString } from "@lib/validations/shared/queryIdString"; export async function membershipById( - { method, query, body, userId }: NextApiRequest, + { method, query, body, userId, prisma }: NextApiRequest, res: NextApiResponse ) { const safeQuery = schemaQueryIdAsString.safeParse(query); diff --git a/pages/api/memberships/index.ts b/pages/api/memberships/index.ts index efd3fb278c..5aaa36f6db 100644 --- a/pages/api/memberships/index.ts +++ b/pages/api/memberships/index.ts @@ -1,13 +1,11 @@ import type { NextApiRequest, NextApiResponse } from "next"; -import prisma from "@calcom/prisma"; - import { withMiddleware } from "@lib/helpers/withMiddleware"; import { MembershipResponse, MembershipsResponse } from "@lib/types"; import { schemaMembershipBodyParams, schemaMembershipPublic } from "@lib/validations/membership"; async function createOrlistAllMemberships( - { method, body, userId }: NextApiRequest, + { method, body, userId, prisma }: NextApiRequest, res: NextApiResponse ) { if (method === "GET") { diff --git a/pages/api/payments/[id].ts b/pages/api/payments/[id].ts index 99d515fe45..26f2138536 100644 --- a/pages/api/payments/[id].ts +++ b/pages/api/payments/[id].ts @@ -1,7 +1,5 @@ import type { NextApiRequest, NextApiResponse } from "next"; -import prisma from "@calcom/prisma"; - import { withMiddleware } from "@lib/helpers/withMiddleware"; import type { PaymentResponse } from "@lib/types"; import { schemaPaymentPublic } from "@lib/validations/payment"; @@ -33,7 +31,7 @@ import { * description: Payment was not found */ export async function paymentById( - { method, query, userId }: NextApiRequest, + { method, query, userId, prisma }: NextApiRequest, res: NextApiResponse ) { const safeQuery = schemaQueryIdParseInt.safeParse(query); diff --git a/pages/api/payments/index.ts b/pages/api/payments/index.ts index d758421f46..829d24681d 100644 --- a/pages/api/payments/index.ts +++ b/pages/api/payments/index.ts @@ -1,7 +1,5 @@ import type { NextApiRequest, NextApiResponse } from "next"; -import prisma from "@calcom/prisma"; - import { withMiddleware } from "@lib/helpers/withMiddleware"; import { PaymentsResponse } from "@lib/types"; import { schemaPaymentPublic } from "@lib/validations/payment"; @@ -21,7 +19,7 @@ import { schemaPaymentPublic } from "@lib/validations/payment"; * 404: * description: No payments were found */ -async function allPayments({ userId }: NextApiRequest, res: NextApiResponse) { +async function allPayments({ userId, prisma }: NextApiRequest, res: NextApiResponse) { const userWithBookings = await prisma.user.findUnique({ where: { id: userId }, include: { bookings: true }, diff --git a/pages/api/schedules/[id].ts b/pages/api/schedules/[id].ts index 305cc5c51b..593f9d7846 100644 --- a/pages/api/schedules/[id].ts +++ b/pages/api/schedules/[id].ts @@ -1,7 +1,5 @@ import type { NextApiRequest, NextApiResponse } from "next"; -import prisma from "@calcom/prisma"; - import { withMiddleware } from "@lib/helpers/withMiddleware"; import type { ScheduleResponse } from "@lib/types"; import { schemaScheduleBodyParams, schemaSchedulePublic } from "@lib/validations/schedule"; @@ -11,7 +9,7 @@ import { } from "@lib/validations/shared/queryIdTransformParseInt"; export async function scheduleById( - { method, query, body, userId }: NextApiRequest, + { method, query, body, userId, prisma }: NextApiRequest, res: NextApiResponse ) { const safeQuery = schemaQueryIdParseInt.safeParse(query); diff --git a/pages/api/schedules/index.ts b/pages/api/schedules/index.ts index 4421844047..81964f4030 100644 --- a/pages/api/schedules/index.ts +++ b/pages/api/schedules/index.ts @@ -1,13 +1,11 @@ import type { NextApiRequest, NextApiResponse } from "next"; -import prisma from "@calcom/prisma"; - import { withMiddleware } from "@lib/helpers/withMiddleware"; import { ScheduleResponse, SchedulesResponse } from "@lib/types"; import { schemaScheduleBodyParams, schemaSchedulePublic } from "@lib/validations/schedule"; async function createOrlistAllSchedules( - { method, body, userId }: NextApiRequest, + { method, body, userId, prisma }: NextApiRequest, res: NextApiResponse ) { if (method === "GET") { diff --git a/pages/api/selected-calendars/[id].ts b/pages/api/selected-calendars/[id].ts index 88dca61806..ea2037fba3 100644 --- a/pages/api/selected-calendars/[id].ts +++ b/pages/api/selected-calendars/[id].ts @@ -1,7 +1,5 @@ import type { NextApiRequest, NextApiResponse } from "next"; -import prisma from "@calcom/prisma"; - import { withMiddleware } from "@lib/helpers/withMiddleware"; import type { SelectedCalendarResponse } from "@lib/types"; import { @@ -11,7 +9,7 @@ import { import { schemaQueryIdAsString, withValidQueryIdString } from "@lib/validations/shared/queryIdString"; export async function selectedCalendarById( - { method, query, body, userId }: NextApiRequest, + { method, query, body, userId, prisma }: NextApiRequest, res: NextApiResponse ) { const safeQuery = schemaQueryIdAsString.safeParse(query); diff --git a/pages/api/selected-calendars/index.ts b/pages/api/selected-calendars/index.ts index f0021f9f0e..3ab69494a8 100644 --- a/pages/api/selected-calendars/index.ts +++ b/pages/api/selected-calendars/index.ts @@ -1,7 +1,5 @@ import type { NextApiRequest, NextApiResponse } from "next"; -import prisma from "@calcom/prisma"; - import { withMiddleware } from "@lib/helpers/withMiddleware"; import { SelectedCalendarResponse, SelectedCalendarsResponse } from "@lib/types"; import { @@ -10,7 +8,7 @@ import { } from "@lib/validations/selected-calendar"; async function createOrlistAllSelectedCalendars( - { method, body, userId }: NextApiRequest, + { method, body, userId, prisma }: NextApiRequest, res: NextApiResponse ) { if (method === "GET") { diff --git a/pages/api/teams/[id].ts b/pages/api/teams/[id].ts index 2cfd371749..65dd8d116e 100644 --- a/pages/api/teams/[id].ts +++ b/pages/api/teams/[id].ts @@ -1,7 +1,5 @@ import type { NextApiRequest, NextApiResponse } from "next"; -import prisma from "@calcom/prisma"; - import { withMiddleware } from "@lib/helpers/withMiddleware"; import type { TeamResponse } from "@lib/types"; import { @@ -72,7 +70,7 @@ import { schemaTeamBodyParams, schemaTeamPublic } from "@lib/validations/team"; * description: Authorization information is missing or invalid. */ export async function teamById( - { method, query, body, userId }: NextApiRequest, + { method, query, body, userId, prisma }: NextApiRequest, res: NextApiResponse ) { const safeQuery = schemaQueryIdParseInt.safeParse(query); diff --git a/pages/api/teams/index.ts b/pages/api/teams/index.ts index e5f870cef6..1dcc046f6e 100644 --- a/pages/api/teams/index.ts +++ b/pages/api/teams/index.ts @@ -1,14 +1,12 @@ import type { NextApiRequest, NextApiResponse } from "next"; -import prisma from "@calcom/prisma"; - import { withMiddleware } from "@lib/helpers/withMiddleware"; import { TeamResponse, TeamsResponse } from "@lib/types"; import { schemaMembershipPublic } from "@lib/validations/membership"; import { schemaTeamBodyParams, schemaTeamPublic } from "@lib/validations/team"; async function createOrlistAllTeams( - { method, body, userId }: NextApiRequest, + { method, body, userId, prisma }: NextApiRequest, res: NextApiResponse ) { if (method === "GET") { diff --git a/pages/api/users/[id].ts b/pages/api/users/[id].ts index 65b864c652..276f015f9d 100644 --- a/pages/api/users/[id].ts +++ b/pages/api/users/[id].ts @@ -1,7 +1,5 @@ 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"; @@ -12,7 +10,7 @@ import { import { schemaUserEditBodyParams, schemaUserReadPublic } from "@lib/validations/user"; export async function userById( - { method, query, body, userId }: NextApiRequest, + { method, query, body, userId, prisma }: NextApiRequest, res: NextApiResponse ) { const safeQuery = schemaQueryIdParseInt.safeParse(query); diff --git a/pages/api/users/index.ts b/pages/api/users/index.ts index 3839ae21b6..cbb0eab69a 100644 --- a/pages/api/users/index.ts +++ b/pages/api/users/index.ts @@ -1,7 +1,5 @@ import type { NextApiRequest, NextApiResponse } from "next"; -import prisma from "@calcom/prisma"; - import { withMiddleware } from "@lib/helpers/withMiddleware"; import { UserResponse, UsersResponse } from "@lib/types"; import { isAdminGuard } from "@lib/utils/isAdmin"; @@ -24,7 +22,7 @@ import { schemaUserReadPublic, schemaUserCreateBodyParams } from "@lib/validatio * description: No users were found */ async function getAllorCreateUser( - { userId, method, body }: NextApiRequest, + { userId, method, body, prisma }: NextApiRequest, res: NextApiResponse ) { const isAdmin = await isAdminGuard(userId); From 180e925bed62e037f068dc9b513b38c8b4869c7a Mon Sep 17 00:00:00 2001 From: Agusti Fernandez Pardo Date: Wed, 8 Jun 2022 18:52:25 +0200 Subject: [PATCH 02/20] feat: dynamic prisma almost working --- README.md | 10 ++ jest.setup.ts | 2 +- lib/helpers/customApiEndpoints.ts | 20 ---- lib/helpers/customPrisma.ts | 45 +++++++++ lib/helpers/extendRequest.ts | 6 +- lib/helpers/verifyApiKey.ts | 18 ++-- lib/helpers/withMiddleware.ts | 7 +- lib/utils/isAdmin.ts | 2 +- next.config.js | 6 ++ package.json | 4 + pages/api/users/[id]/availability.ts | 144 +++++++++++++++++++++++++++ tsconfig.json | 1 + 12 files changed, 226 insertions(+), 39 deletions(-) delete mode 100644 lib/helpers/customApiEndpoints.ts create mode 100644 lib/helpers/customPrisma.ts create mode 100644 pages/api/users/[id]/availability.ts diff --git a/README.md b/README.md index 8c39f8457f..7b9f1c7472 100644 --- a/README.md +++ b/README.md @@ -178,3 +178,13 @@ 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 + + +## Hosted api through cal.com + +Go to console.com +Add a deployment or go to an existing one. +Activate API or Admin addon +Provide your DATABASE-URL +Store it safely, you'll get a customApiID, save it. +call api.cal.com?apiKey=your_cal_instance_apiKey&customApiId=cal_datasource_key \ No newline at end of file diff --git a/jest.setup.ts b/jest.setup.ts index d26c6b4183..b764cee58f 100644 --- a/jest.setup.ts +++ b/jest.setup.ts @@ -1,6 +1,6 @@ import prisma from "@calcom/prisma"; afterEach((done) => { - prisma().$disconnect().then(); + prisma.$disconnect().then(); done(); }); diff --git a/lib/helpers/customApiEndpoints.ts b/lib/helpers/customApiEndpoints.ts deleted file mode 100644 index 55d7dc78a2..0000000000 --- a/lib/helpers/customApiEndpoints.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { NextMiddleware } from "next-api-middleware"; - -import prisma from "@calcom/prisma"; - -// This replaces the prisma client for the cusotm one if the customApiId is valid -export const customApiEndpoints: NextMiddleware = async (req, res, next) => { - const { - query: { customApiId }, - } = req; - // If no custom api Id is provided, return the regular cal.com prisma client. - if (!customApiId) { - req.prisma = prisma(); - await next(); - } - // Hardcoded new database, this should be a dynamic call to console prisma/api? - const newDatabseUrl = process.env.DATABASE_PROD_URL; - req.prisma = prisma(newDatabseUrl); - - await next(); -}; diff --git a/lib/helpers/customPrisma.ts b/lib/helpers/customPrisma.ts new file mode 100644 index 0000000000..27bc3261e2 --- /dev/null +++ b/lib/helpers/customPrisma.ts @@ -0,0 +1,45 @@ +import { hash } from "bcryptjs"; +import cache from "memory-cache"; +import { NextMiddleware } from "next-api-middleware"; + +import { prismaAdmin } from "@calcom/console/modules/common/utils/prisma"; +import { asStringOrUndefined, asStringOrNull } from "@calcom/lib/asStringOrNull"; +import { PRISMA_CLIENT_CACHING_TIME } from "@calcom/lib/constants"; +import { prisma, customPrisma } from "@calcom/prisma"; + +// This replaces the prisma client for the cusotm one if the customCredentialsId is valid +export const customPrismaClient: NextMiddleware = async (req, res, next) => { + const { + query: { customCredentialsId }, + } = req; + // If no custom api Id is provided, attach to request the regular cal.com prisma client. + if (!customCredentialsId) { + req.prisma = prisma; + await next(); + } else { + const id = asStringOrUndefined(customCredentialsId); + + // If we have a customCredentialsId, we check if it is valid. + const dataCredentials = await prismaAdmin.dataCredentials.findUnique({ + where: { id }, + }); + if (!dataCredentials) { + res.status(400).json({ error: "Invalid custom credentials id" }); + return; + } + const credentials = dataCredentials?.credentials; + const hashedUrl = await hash(credentials, 12); + + const cachedPrisma = cache.get(hashedUrl); + + if (!cachedPrisma) { + cache.put( + hashedUrl, + customPrisma({ datasources: { db: { url: credentials } } }), + PRISMA_CLIENT_CACHING_TIME // Cache the prisma client for 24 hours + ); + } + req.prisma = cachedPrisma; + } + await next(); +}; diff --git a/lib/helpers/extendRequest.ts b/lib/helpers/extendRequest.ts index ac9e54d5ed..d0226a255c 100644 --- a/lib/helpers/extendRequest.ts +++ b/lib/helpers/extendRequest.ts @@ -3,14 +3,16 @@ import { NextMiddleware } from "next-api-middleware"; import type { PrismaClient } from ".prisma/client"; -/** @todo figure how to use the one from `@calcom/types`fi */ +/** @todo figure how to use the one from `@calcom/types` */ /** @todo: remove once `@calcom/types` is updated with it.*/ declare module "next" { export interface NextApiRequest extends IncomingMessage { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + body: any; userId: number; method: string; prisma: PrismaClient; - body: any; + session: { user: { id: number } }; query: { [key: string]: string | string[] }; } } diff --git a/lib/helpers/verifyApiKey.ts b/lib/helpers/verifyApiKey.ts index d350f4e1f3..9068f2be73 100644 --- a/lib/helpers/verifyApiKey.ts +++ b/lib/helpers/verifyApiKey.ts @@ -3,18 +3,6 @@ import { NextMiddleware } from "next-api-middleware"; import { hashAPIKey } from "@calcom/ee/lib/api/apiKeys"; -// // import prisma from "@calcom/prisma"; - -// /** @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; -// query: { [key: string]: string | string[] }; -// } -// } - // Used to check if the apiKey is not expired, could be extracted if reused. but not for now. export const dateNotInPast = function (date: Date) { const now = new Date(); @@ -27,12 +15,18 @@ export const dateNotInPast = function (date: Date) { export const verifyApiKey: NextMiddleware = async (req, res, next) => { const { prisma } = req; if (!req.query.apiKey) return res.status(401).json({ message: "No apiKey provided" }); + console.log("req.query.apiKey", req.query.apiKey); + // We remove the prefix from the user provided api_key. If no env set default to "cal_" const strippedApiKey = `${req.query.apiKey}`.replace(process.env.API_KEY_PREFIX || "cal_", ""); + console.log("strippedApiKey", strippedApiKey); + // Hash the key again before matching against the database records. const hashedKey = hashAPIKey(strippedApiKey); + console.log("hashedKey", hashedKey); // Check if the hashed api key exists in database. const apiKey = await prisma.apiKey.findUnique({ where: { hashedKey } }); + // console.log("apiKey", apiKey); // If we cannot find any api key. Throw a 401 Unauthorized. if (!apiKey) return res.status(401).json({ error: "Your apiKey is not valid" }); if (apiKey.expiresAt && dateNotInPast(apiKey.expiresAt)) { diff --git a/lib/helpers/withMiddleware.ts b/lib/helpers/withMiddleware.ts index 73eef171f9..05b21cbf0d 100644 --- a/lib/helpers/withMiddleware.ts +++ b/lib/helpers/withMiddleware.ts @@ -2,7 +2,7 @@ import { label } from "next-api-middleware"; import { addRequestId } from "./addRequestid"; import { captureErrors } from "./captureErrors"; -import { customApiEndpoints } from "./customApiEndpoints"; +import { customPrismaClient } from "./customPrisma"; import { extendRequest } from "./extendRequest"; import { HTTP_POST, @@ -24,11 +24,12 @@ const withMiddleware = label( HTTP_DELETE, addRequestId, verifyApiKey, - customApiEndpoints, + customPrismaClient, extendRequest, sentry: captureErrors, }, - ["sentry", "customApiEndpoints", "verifyApiKey", "addRequestId", "extendRequest"] // <-- Provide a list of middleware to call automatically + // The order here, determines the order of execution, put customPrismaClient before verifyApiKey always. + ["sentry", "customPrismaClient", "verifyApiKey", "addRequestId", "extendRequest"] // <-- Provide a list of middleware to call automatically ); export { withMiddleware }; diff --git a/lib/utils/isAdmin.ts b/lib/utils/isAdmin.ts index 4991934509..64af37367f 100644 --- a/lib/utils/isAdmin.ts +++ b/lib/utils/isAdmin.ts @@ -3,6 +3,6 @@ import { UserPermissionRole } from "@prisma/client"; import prisma from "@calcom/prisma"; export const isAdminGuard = async (userId: number) => { - const user = await prisma().user.findUnique({ where: { id: userId } }); + const user = await prisma.user.findUnique({ where: { id: userId } }); return user?.role === UserPermissionRole.ADMIN; }; diff --git a/next.config.js b/next.config.js index 5e40353edf..06f2023723 100644 --- a/next.config.js +++ b/next.config.js @@ -2,8 +2,14 @@ // 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/console", "@calcom/prisma", + "@calcom/types", + "@calcom/core", + "@calcom/stripe", + "@calcom/emails", "@calcom/lib", + "@calcom/ui", "@calcom/ee", ]); diff --git a/package.json b/package.json index 57da05e5e6..7475fcb6bc 100644 --- a/package.json +++ b/package.json @@ -20,13 +20,17 @@ }, "devDependencies": { "@calcom/tsconfig": "*", + "@calcom/types": "*", "babel-jest": "^28.1.0", "jest": "^28.1.0", "node-mocks-http": "^1.11.0" }, "dependencies": { + "@calcom/console": "*", "@calcom/prisma": "*", "@sentry/nextjs": "^6.19.7", + "bcryptjs": "^2.4.3", + "memory-cache": "^0.2.0", "modify-response-middleware": "^1.1.0", "next": "^12.1.6", "next-api-middleware": "^1.0.1", diff --git a/pages/api/users/[id]/availability.ts b/pages/api/users/[id]/availability.ts new file mode 100644 index 0000000000..ce57c24ea1 --- /dev/null +++ b/pages/api/users/[id]/availability.ts @@ -0,0 +1,144 @@ +// import availability from "@calcom/core/availability"; +// export default availability; +import { Prisma } from "@prisma/client"; +import dayjs from "dayjs"; +import timezone from "dayjs/plugin/timezone"; +import utc from "dayjs/plugin/utc"; +import type { NextApiRequest, NextApiResponse } from "next"; + +// import prisma from "@calcom/prisma"; +import { asNumberOrThrow, asStringOrNull } from "@calcom/lib/asStringOrNull"; +import { getWorkingHours } from "@calcom/lib/availability"; +import getBusyTimes from "@calcom/lib/getBusyTimes"; + +dayjs.extend(utc); +dayjs.extend(timezone); + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + const { prisma } = req; + const id = asNumberOrThrow(req.query.id); + const dateFrom = dayjs(asStringOrNull(req.query.dateFrom)); + const dateTo = dayjs(asStringOrNull(req.query.dateTo)); + const eventTypeId = typeof req.query.eventTypeId === "string" ? parseInt(req.query.eventTypeId) : undefined; + + if (!dateFrom.isValid() || !dateTo.isValid()) { + return res.status(400).json({ message: "Invalid time range given." }); + } + + const rawUser = await prisma.user.findUnique({ + where: { + id, + }, + select: { + credentials: true, + timeZone: true, + bufferTime: true, + availability: true, + id: true, + startTime: true, + endTime: true, + selectedCalendars: true, + schedules: { + select: { + availability: true, + timeZone: true, + id: true, + }, + }, + defaultScheduleId: true, + }, + }); + + const getEventType = (id: number) => + prisma.eventType.findUnique({ + where: { id }, + select: { + seatsPerTimeSlot: true, + timeZone: true, + schedule: { + select: { + availability: true, + timeZone: true, + }, + }, + availability: { + select: { + startTime: true, + endTime: true, + days: true, + }, + }, + }, + }); + + type EventType = Prisma.PromiseReturnType; + let eventType: EventType | null = null; + if (eventTypeId) eventType = await getEventType(eventTypeId); + + if (!rawUser) throw new Error("No user found"); + + const { selectedCalendars, ...currentUser } = rawUser; + + const busyTimes = await getBusyTimes({ + credentials: currentUser.credentials, + startTime: dateFrom.format(), + endTime: dateTo.format(), + eventTypeId, + userId: currentUser.id, + selectedCalendars, + }); + + const bufferedBusyTimes = busyTimes.map((a) => ({ + start: dayjs(a.start).subtract(currentUser.bufferTime, "minute"), + end: dayjs(a.end).add(currentUser.bufferTime, "minute"), + })); + + const schedule = eventType?.schedule + ? { ...eventType?.schedule } + : { + ...currentUser.schedules.filter( + (schedule) => !currentUser.defaultScheduleId || schedule.id === currentUser.defaultScheduleId + )[0], + }; + + const timeZone = schedule.timeZone || eventType?.timeZone || currentUser.timeZone; + + const workingHours = getWorkingHours( + { + timeZone, + }, + schedule.availability || + (eventType?.availability.length ? eventType.availability : currentUser.availability) + ); + + /* Current logic is if a booking is in a time slot mark it as busy, but seats can have more than one attendee so grab + current bookings with a seats event type and display them on the calendar, even if they are full */ + let currentSeats; + if (eventType?.seatsPerTimeSlot) { + currentSeats = await prisma.booking.findMany({ + where: { + eventTypeId: eventTypeId, + startTime: { + gte: dateFrom.format(), + lte: dateTo.format(), + }, + }, + select: { + uid: true, + startTime: true, + _count: { + select: { + attendees: true, + }, + }, + }, + }); + } + + res.status(200).json({ + busy: bufferedBusyTimes, + timeZone, + workingHours, + currentSeats, + }); +} diff --git a/tsconfig.json b/tsconfig.json index 2714b86c1c..ef00959c45 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,7 @@ { "extends": "@calcom/tsconfig/nextjs.json", "compilerOptions": { + "target": "es2017", "strict": true, "baseUrl": ".", "paths": { From ab6a99749f619df4423815244ff8785b453e6908 Mon Sep 17 00:00:00 2001 From: Agusti Fernandez Pardo Date: Tue, 14 Jun 2022 00:32:07 +0200 Subject: [PATCH 03/20] fix: linting --- lib/helpers/customPrisma.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/helpers/customPrisma.ts b/lib/helpers/customPrisma.ts index 27bc3261e2..27846f3148 100644 --- a/lib/helpers/customPrisma.ts +++ b/lib/helpers/customPrisma.ts @@ -3,7 +3,7 @@ import cache from "memory-cache"; import { NextMiddleware } from "next-api-middleware"; import { prismaAdmin } from "@calcom/console/modules/common/utils/prisma"; -import { asStringOrUndefined, asStringOrNull } from "@calcom/lib/asStringOrNull"; +import { asStringOrUndefined } from "@calcom/lib/asStringOrNull"; import { PRISMA_CLIENT_CACHING_TIME } from "@calcom/lib/constants"; import { prisma, customPrisma } from "@calcom/prisma"; From f3d7922efd3d8eca1816ac4e8237060fae997363 Mon Sep 17 00:00:00 2001 From: Agusti Fernandez Pardo Date: Tue, 14 Jun 2022 19:48:47 +0200 Subject: [PATCH 04/20] move caching time constant to API --- lib/constants.ts | 1 + lib/helpers/customPrisma.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 lib/constants.ts diff --git a/lib/constants.ts b/lib/constants.ts new file mode 100644 index 0000000000..52c7fe0a41 --- /dev/null +++ b/lib/constants.ts @@ -0,0 +1 @@ +export const PRISMA_CLIENT_CACHING_TIME = 1000 * 60 * 60 * 24; diff --git a/lib/helpers/customPrisma.ts b/lib/helpers/customPrisma.ts index 27846f3148..dc9bbeeaa3 100644 --- a/lib/helpers/customPrisma.ts +++ b/lib/helpers/customPrisma.ts @@ -2,9 +2,9 @@ import { hash } from "bcryptjs"; import cache from "memory-cache"; import { NextMiddleware } from "next-api-middleware"; +import { PRISMA_CLIENT_CACHING_TIME } from "@calcom/api/lib/constants"; import { prismaAdmin } from "@calcom/console/modules/common/utils/prisma"; import { asStringOrUndefined } from "@calcom/lib/asStringOrNull"; -import { PRISMA_CLIENT_CACHING_TIME } from "@calcom/lib/constants"; import { prisma, customPrisma } from "@calcom/prisma"; // This replaces the prisma client for the cusotm one if the customCredentialsId is valid From 374a6e087ae4b7c86659172c34601540b937f621 Mon Sep 17 00:00:00 2001 From: Agusti Fernandez Pardo Date: Fri, 17 Jun 2022 23:23:28 +0200 Subject: [PATCH 05/20] feat: remove datacredentials, move to deployment.databaseUrl --- lib/helpers/customPrisma.ts | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/lib/helpers/customPrisma.ts b/lib/helpers/customPrisma.ts index dc9bbeeaa3..f106ce36f2 100644 --- a/lib/helpers/customPrisma.ts +++ b/lib/helpers/customPrisma.ts @@ -7,27 +7,31 @@ import { prismaAdmin } from "@calcom/console/modules/common/utils/prisma"; import { asStringOrUndefined } from "@calcom/lib/asStringOrNull"; import { prisma, customPrisma } from "@calcom/prisma"; -// This replaces the prisma client for the cusotm one if the customCredentialsId is valid +// This replaces the prisma client for the cusotm one if the key is valid export const customPrismaClient: NextMiddleware = async (req, res, next) => { const { - query: { customCredentialsId }, - } = req; + query: { key }, + }: { query: { key?: string } } = req; // If no custom api Id is provided, attach to request the regular cal.com prisma client. - if (!customCredentialsId) { + if (!key) { req.prisma = prisma; await next(); } else { - const id = asStringOrUndefined(customCredentialsId); + const id = asStringOrUndefined(key); - // If we have a customCredentialsId, we check if it is valid. - const dataCredentials = await prismaAdmin.dataCredentials.findUnique({ - where: { id }, + // If we have a key, we check if it is valid. + const deployment = await prismaAdmin.deployment.findUnique({ + where: { key }, }); - if (!dataCredentials) { + if (!deployment) { res.status(400).json({ error: "Invalid custom credentials id" }); return; } - const credentials = dataCredentials?.credentials; + const credentials = deployment.databaseUrl; + if (!credentials) { + res.status(400).json({ error: "no databaseUrl set up at your instance yet" }); + return; + } const hashedUrl = await hash(credentials, 12); const cachedPrisma = cache.get(hashedUrl); From 5b28e9ec1c8ce6c53172f725a2da6f8d509e6adf Mon Sep 17 00:00:00 2001 From: Agusti Fernandez Pardo Date: Sat, 18 Jun 2022 02:16:15 +0200 Subject: [PATCH 06/20] fix: main merging issues --- lib/helpers/extendRequest.ts | 1 - lib/helpers/withMiddleware.ts | 2 +- next.config.js | 1 + pages/api/availabilities/[id].ts | 2 +- pages/api/users/index.ts | 58 -------------------------------- 5 files changed, 3 insertions(+), 61 deletions(-) diff --git a/lib/helpers/extendRequest.ts b/lib/helpers/extendRequest.ts index 52af6da544..8ed3ee0571 100644 --- a/lib/helpers/extendRequest.ts +++ b/lib/helpers/extendRequest.ts @@ -18,6 +18,5 @@ declare module "next" { } } export const extendRequest: NextMiddleware = async (req, res, next) => { - // @note: just an empty middleware, we care about extending the next api request types only. await next(); }; diff --git a/lib/helpers/withMiddleware.ts b/lib/helpers/withMiddleware.ts index 05b21cbf0d..ab6d5cb79e 100644 --- a/lib/helpers/withMiddleware.ts +++ b/lib/helpers/withMiddleware.ts @@ -29,7 +29,7 @@ const withMiddleware = label( sentry: captureErrors, }, // The order here, determines the order of execution, put customPrismaClient before verifyApiKey always. - ["sentry", "customPrismaClient", "verifyApiKey", "addRequestId", "extendRequest"] // <-- Provide a list of middleware to call automatically + ["extendRequest", "sentry", "customPrismaClient", "verifyApiKey", "addRequestId"] // <-- Provide a list of middleware to call automatically ); export { withMiddleware }; diff --git a/next.config.js b/next.config.js index 22ff466595..939eba2374 100644 --- a/next.config.js +++ b/next.config.js @@ -3,6 +3,7 @@ const withTM = require("next-transpile-modules")([ "@calcom/app-store", "@calcom/core", + "@calcom/console", "@calcom/ee", "@calcom/lib", "@calcom/prisma", diff --git a/pages/api/availabilities/[id].ts b/pages/api/availabilities/[id].ts index fe62182978..1d4b6ed4a1 100644 --- a/pages/api/availabilities/[id].ts +++ b/pages/api/availabilities/[id].ts @@ -1,6 +1,6 @@ import type { NextApiRequest, NextApiResponse } from "next"; -// import { withMiddleware } from "@lib/helpers/withMiddleware"; +import { withMiddleware } from "@lib/helpers/withMiddleware"; import type { AvailabilityResponse } from "@lib/types"; import { schemaAvailabilityEditBodyParams, diff --git a/pages/api/users/index.ts b/pages/api/users/index.ts index 28231b487a..c07846423f 100644 --- a/pages/api/users/index.ts +++ b/pages/api/users/index.ts @@ -1,68 +1,10 @@ -<<<<<<< HEAD -import type { NextApiRequest, NextApiResponse } from "next"; -======= import { defaultHandler } from "@calcom/lib/server"; ->>>>>>> main import { withMiddleware } from "@lib/helpers/withMiddleware"; -<<<<<<< HEAD -/** - * @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, prisma }: 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"), }) ); ->>>>>>> main From cc534a2914403f3a23475efdb1dc5d8c339bffb7 Mon Sep 17 00:00:00 2001 From: Agusti Fernandez Pardo Date: Sat, 18 Jun 2022 02:35:39 +0200 Subject: [PATCH 07/20] fix: remove console from api deps --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index dd21735779..94c4c7a2a2 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,6 @@ "node-mocks-http": "^1.11.0" }, "dependencies": { - "@calcom/console": "*", "@calcom/prisma": "*", "@sentry/nextjs": "^6.19.7", "bcryptjs": "^2.4.3", From 60688e2e9110cc3cc07028d2fc9e69d4d731678c Mon Sep 17 00:00:00 2001 From: Agusti Fernandez Pardo Date: Sat, 18 Jun 2022 03:03:13 +0200 Subject: [PATCH 08/20] fix: make isAdmin require req.prisma too --- lib/helpers/verifyApiKey.ts | 2 +- lib/utils/isAdmin.ts | 6 ++---- lib/utils/webhookSubscriptions.ts | 2 +- pages/api/users/[userId]/_delete.ts | 2 +- pages/api/users/[userId]/_get.ts | 2 +- pages/api/users/[userId]/_patch.ts | 2 +- pages/api/users/_get.ts | 4 ++-- pages/api/users/_post.ts | 2 +- 8 files changed, 10 insertions(+), 12 deletions(-) diff --git a/lib/helpers/verifyApiKey.ts b/lib/helpers/verifyApiKey.ts index ec6aaf1179..8c11cdb492 100644 --- a/lib/helpers/verifyApiKey.ts +++ b/lib/helpers/verifyApiKey.ts @@ -34,7 +34,7 @@ export const verifyApiKey: NextMiddleware = async (req, res, next) => { /* 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); + req.isAdmin = await isAdminGuard(req.userId, prisma); await next(); }; diff --git a/lib/utils/isAdmin.ts b/lib/utils/isAdmin.ts index 64af37367f..aab1454b62 100644 --- a/lib/utils/isAdmin.ts +++ b/lib/utils/isAdmin.ts @@ -1,8 +1,6 @@ -import { UserPermissionRole } from "@prisma/client"; +import { PrismaClient, UserPermissionRole } from "@prisma/client"; -import prisma from "@calcom/prisma"; - -export const isAdminGuard = async (userId: number) => { +export const isAdminGuard = async (userId: number, prisma: PrismaClient) => { const user = await prisma.user.findUnique({ where: { id: userId } }); return user?.role === UserPermissionRole.ADMIN; }; diff --git a/lib/utils/webhookSubscriptions.ts b/lib/utils/webhookSubscriptions.ts index bdf9d95576..e53b15e7c5 100644 --- a/lib/utils/webhookSubscriptions.ts +++ b/lib/utils/webhookSubscriptions.ts @@ -7,7 +7,7 @@ export type GetSubscriberOptions = { eventTypeId: number; triggerEvent: WebhookTriggerEvents; }; - +/** @note will this not work with custom prisma? since we're importing prisma directly and not passing it from request here **/ const getWebhooks = async (options: GetSubscriberOptions) => { const { userId, eventTypeId } = options; const allWebhooks = await prisma.webhook.findMany({ diff --git a/pages/api/users/[userId]/_delete.ts b/pages/api/users/[userId]/_delete.ts index c2d8d5db29..ac15975931 100644 --- a/pages/api/users/[userId]/_delete.ts +++ b/pages/api/users/[userId]/_delete.ts @@ -33,7 +33,7 @@ import { schemaQueryUserId } from "@lib/validations/shared/queryUserId"; */ export async function deleteHandler(req: NextApiRequest) { const query = schemaQueryUserId.parse(req.query); - const isAdmin = await isAdminGuard(req.userId); + const isAdmin = await isAdminGuard(req.userId, req.prisma); // 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.userId !== req.userId) throw new HttpError({ statusCode: 401, message: "Unauthorized" }); diff --git a/pages/api/users/[userId]/_get.ts b/pages/api/users/[userId]/_get.ts index 5c76c4e315..2fda8ddc91 100644 --- a/pages/api/users/[userId]/_get.ts +++ b/pages/api/users/[userId]/_get.ts @@ -34,7 +34,7 @@ import { schemaUserReadPublic } from "@lib/validations/user"; */ export async function getHandler(req: NextApiRequest) { const query = schemaQueryUserId.parse(req.query); - const isAdmin = await isAdminGuard(req.userId); + const isAdmin = await isAdminGuard(req.userId, req.prisma); // 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.userId !== req.userId) throw new HttpError({ statusCode: 401, message: "Unauthorized" }); diff --git a/pages/api/users/[userId]/_patch.ts b/pages/api/users/[userId]/_patch.ts index 9f32e920c4..58efa5773b 100644 --- a/pages/api/users/[userId]/_patch.ts +++ b/pages/api/users/[userId]/_patch.ts @@ -55,7 +55,7 @@ import { schemaUserEditBodyParams, schemaUserReadPublic } from "@lib/validations */ export async function patchHandler(req: NextApiRequest) { const query = schemaQueryUserId.parse(req.query); - const isAdmin = await isAdminGuard(req.userId); + const isAdmin = await isAdminGuard(req.userId, req.prisma); // 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.userId !== req.userId) throw new HttpError({ statusCode: 401, message: "Unauthorized" }); diff --git a/pages/api/users/_get.ts b/pages/api/users/_get.ts index 4c4aadaecd..7f90ba8a27 100644 --- a/pages/api/users/_get.ts +++ b/pages/api/users/_get.ts @@ -24,8 +24,8 @@ import { Prisma } from ".prisma/client"; * 404: * description: No users were found */ -async function getHandler({ userId }: NextApiRequest) { - const isAdmin = await isAdminGuard(userId); +async function getHandler({ userId, prisma }: NextApiRequest) { + const isAdmin = await isAdminGuard(userId, prisma); const where: Prisma.UserWhereInput = {}; // If user is not ADMIN, return only his data. if (!isAdmin) where.id = userId; diff --git a/pages/api/users/_post.ts b/pages/api/users/_post.ts index 908a9c4bcc..2217c49127 100644 --- a/pages/api/users/_post.ts +++ b/pages/api/users/_post.ts @@ -8,7 +8,7 @@ import { isAdminGuard } from "@lib/utils/isAdmin"; import { schemaUserCreateBodyParams } from "@lib/validations/user"; async function postHandler(req: NextApiRequest) { - const isAdmin = await isAdminGuard(req.userId); + const isAdmin = await isAdminGuard(req.userId, req.prisma); // 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); From 39ce6287fcb29543ab6288309e9d9ddc01c97659 Mon Sep 17 00:00:00 2001 From: Agusti Fernandez Pardo Date: Sat, 18 Jun 2022 22:30:27 +0200 Subject: [PATCH 09/20] fix: no need to target 2017 --- tsconfig.json | 1 - 1 file changed, 1 deletion(-) diff --git a/tsconfig.json b/tsconfig.json index ef00959c45..2714b86c1c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "@calcom/tsconfig/nextjs.json", "compilerOptions": { - "target": "es2017", "strict": true, "baseUrl": ".", "paths": { From c3bed7ba8fa4669f73c9bddbdfd19c672cc275c4 Mon Sep 17 00:00:00 2001 From: Agusti Fernandez Pardo Date: Mon, 20 Jun 2022 02:26:20 +0200 Subject: [PATCH 10/20] fix: add console to package --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 94c4c7a2a2..dd21735779 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "node-mocks-http": "^1.11.0" }, "dependencies": { + "@calcom/console": "*", "@calcom/prisma": "*", "@sentry/nextjs": "^6.19.7", "bcryptjs": "^2.4.3", From 4626b3dffa2b5deb9271e040033deb08c8ed6ebd Mon Sep 17 00:00:00 2001 From: Agusti Fernandez Pardo Date: Mon, 20 Jun 2022 02:29:18 +0200 Subject: [PATCH 11/20] fix: --- lib/helpers/customPrisma.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/helpers/customPrisma.ts b/lib/helpers/customPrisma.ts index f106ce36f2..98d1e3b2b2 100644 --- a/lib/helpers/customPrisma.ts +++ b/lib/helpers/customPrisma.ts @@ -3,7 +3,7 @@ import cache from "memory-cache"; import { NextMiddleware } from "next-api-middleware"; import { PRISMA_CLIENT_CACHING_TIME } from "@calcom/api/lib/constants"; -import { prismaAdmin } from "@calcom/console/modules/common/utils/prisma"; +import prismaAdmin from "@calcom/console/modules/common/utils/prisma"; import { asStringOrUndefined } from "@calcom/lib/asStringOrNull"; import { prisma, customPrisma } from "@calcom/prisma"; From e68e6d11652f291365b437c51bf11b45cf4df1d5 Mon Sep 17 00:00:00 2001 From: Agusti Fernandez Pardo Date: Mon, 20 Jun 2022 02:30:51 +0200 Subject: [PATCH 12/20] no console in package --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index dd21735779..94c4c7a2a2 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,6 @@ "node-mocks-http": "^1.11.0" }, "dependencies": { - "@calcom/console": "*", "@calcom/prisma": "*", "@sentry/nextjs": "^6.19.7", "bcryptjs": "^2.4.3", From 48675f4e7a3e51d6c775a1e08d3e813da546b9d6 Mon Sep 17 00:00:00 2001 From: Agusti Fernandez Pardo Date: Mon, 20 Jun 2022 03:10:10 +0200 Subject: [PATCH 13/20] fix add back console --- next.config.js | 2 +- package.json | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/next.config.js b/next.config.js index 939eba2374..f1d3bace86 100644 --- a/next.config.js +++ b/next.config.js @@ -3,7 +3,6 @@ const withTM = require("next-transpile-modules")([ "@calcom/app-store", "@calcom/core", - "@calcom/console", "@calcom/ee", "@calcom/lib", "@calcom/prisma", @@ -12,6 +11,7 @@ const withTM = require("next-transpile-modules")([ "@calcom/emails", "@calcom/embed-core", "@calcom/embed-snippet", + "@calcom/console", ]); module.exports = withTM({ diff --git a/package.json b/package.json index 94c4c7a2a2..e1fed1bdb9 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ }, "dependencies": { "@calcom/prisma": "*", + "@calcom/console": "*", "@sentry/nextjs": "^6.19.7", "bcryptjs": "^2.4.3", "memory-cache": "^0.2.0", From 312664d881220c981e2ff7500fc73f004c71869b Mon Sep 17 00:00:00 2001 From: Agusti Fernandez Pardo Date: Mon, 20 Jun 2022 03:13:03 +0200 Subject: [PATCH 14/20] fix generate schemas console --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e1fed1bdb9..627c6d4565 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "dev": "PORT=3002 next dev", "lint-fix": "next lint --fix && prettier --write .", "lint": "next lint", - "prebuild": "cd ../.. && yarn workspace @calcom/prisma generate-schemas", + "prebuild": "cd ../.. && yarn workspace @calcom/prisma generate-schemas && yarn workspace @calcom/console generate-schemas", "start": "PORT=3002 next start", "test": "jest --detectOpenHandles --passWithNoTests", "type-check": "tsc --pretty --noEmit" From f0f27857fa974fd6db9f10b71f1814acef15ed80 Mon Sep 17 00:00:00 2001 From: Agusti Fernandez Pardo Date: Mon, 20 Jun 2022 03:14:24 +0200 Subject: [PATCH 15/20] no package console --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index 627c6d4565..9a3ee1b877 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,6 @@ }, "dependencies": { "@calcom/prisma": "*", - "@calcom/console": "*", "@sentry/nextjs": "^6.19.7", "bcryptjs": "^2.4.3", "memory-cache": "^0.2.0", From 7558a73ab77b3b120a5e45fcf34698a5bf16fec6 Mon Sep 17 00:00:00 2001 From: Agusti Fernandez Pardo Date: Mon, 20 Jun 2022 03:18:13 +0200 Subject: [PATCH 16/20] fix: dont generate schemas for console --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9a3ee1b877..94c4c7a2a2 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "dev": "PORT=3002 next dev", "lint-fix": "next lint --fix && prettier --write .", "lint": "next lint", - "prebuild": "cd ../.. && yarn workspace @calcom/prisma generate-schemas && yarn workspace @calcom/console generate-schemas", + "prebuild": "cd ../.. && yarn workspace @calcom/prisma generate-schemas", "start": "PORT=3002 next start", "test": "jest --detectOpenHandles --passWithNoTests", "type-check": "tsc --pretty --noEmit" From 6e78bf15f0e8af3689eeef8be5f50ba54e84da78 Mon Sep 17 00:00:00 2001 From: Agusti Fernandez Pardo Date: Tue, 21 Jun 2022 23:02:43 +0200 Subject: [PATCH 17/20] feat: databaseUrl customPrisma --- lib/helpers/customPrisma.ts | 29 +++++++++++++++++------------ next.config.js | 1 - 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/lib/helpers/customPrisma.ts b/lib/helpers/customPrisma.ts index 98d1e3b2b2..f9da8ebe1e 100644 --- a/lib/helpers/customPrisma.ts +++ b/lib/helpers/customPrisma.ts @@ -3,7 +3,7 @@ import cache from "memory-cache"; import { NextMiddleware } from "next-api-middleware"; import { PRISMA_CLIENT_CACHING_TIME } from "@calcom/api/lib/constants"; -import prismaAdmin from "@calcom/console/modules/common/utils/prisma"; +// import prismaAdmin from "@calcom/console/modules/common/utils/prisma"; import { asStringOrUndefined } from "@calcom/lib/asStringOrNull"; import { prisma, customPrisma } from "@calcom/prisma"; @@ -20,26 +20,31 @@ export const customPrismaClient: NextMiddleware = async (req, res, next) => { const id = asStringOrUndefined(key); // If we have a key, we check if it is valid. - const deployment = await prismaAdmin.deployment.findUnique({ - where: { key }, - }); - if (!deployment) { - res.status(400).json({ error: "Invalid custom credentials id" }); - return; - } - const credentials = deployment.databaseUrl; - if (!credentials) { + // const deployment = await prismaAdmin.deployment.findUnique({ + // where: { key }, + // }); + const databaseUrl = await fetch(`https://console.cal.com/api/deployments/database?key=${id}`) + .then((res) => res.json()) + .then((res) => res.databaseUrl); + + console.log(databaseUrl); + // if (!databaseUrl) { + // res.status(400).json({ error: "Invalid custom credentials id" }); + // return; + // } + // const credentials = deployment.databaseUrl; + if (!databaseUrl) { res.status(400).json({ error: "no databaseUrl set up at your instance yet" }); return; } - const hashedUrl = await hash(credentials, 12); + const hashedUrl = await hash(databaseUrl, 12); const cachedPrisma = cache.get(hashedUrl); if (!cachedPrisma) { cache.put( hashedUrl, - customPrisma({ datasources: { db: { url: credentials } } }), + customPrisma({ datasources: { db: { url: databaseUrl } } }), PRISMA_CLIENT_CACHING_TIME // Cache the prisma client for 24 hours ); } diff --git a/next.config.js b/next.config.js index f1d3bace86..22ff466595 100644 --- a/next.config.js +++ b/next.config.js @@ -11,7 +11,6 @@ const withTM = require("next-transpile-modules")([ "@calcom/emails", "@calcom/embed-core", "@calcom/embed-snippet", - "@calcom/console", ]); module.exports = withTM({ From b6729d8b82d9ceb7988006e3a5c28fdcf56e29ea Mon Sep 17 00:00:00 2001 From: Agusti Fernandez Pardo Date: Wed, 22 Jun 2022 05:49:24 +0200 Subject: [PATCH 18/20] fix custom prisma works --- lib/helpers/customPrisma.ts | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/lib/helpers/customPrisma.ts b/lib/helpers/customPrisma.ts index f9da8ebe1e..5082de68da 100644 --- a/lib/helpers/customPrisma.ts +++ b/lib/helpers/customPrisma.ts @@ -5,6 +5,7 @@ import { NextMiddleware } from "next-api-middleware"; import { PRISMA_CLIENT_CACHING_TIME } from "@calcom/api/lib/constants"; // import prismaAdmin from "@calcom/console/modules/common/utils/prisma"; import { asStringOrUndefined } from "@calcom/lib/asStringOrNull"; +import { CONSOLE_URL } from "@calcom/lib/constants"; import { prisma, customPrisma } from "@calcom/prisma"; // This replaces the prisma client for the cusotm one if the key is valid @@ -23,16 +24,12 @@ export const customPrismaClient: NextMiddleware = async (req, res, next) => { // const deployment = await prismaAdmin.deployment.findUnique({ // where: { key }, // }); - const databaseUrl = await fetch(`https://console.cal.com/api/deployments/database?key=${id}`) + const databaseUrl = await fetch( + `${process.env.NEXT_PUBLIC_CONSOLE_URL || CONSOLE_URL}/api/deployments/database?key=${id}` + ) .then((res) => res.json()) .then((res) => res.databaseUrl); - console.log(databaseUrl); - // if (!databaseUrl) { - // res.status(400).json({ error: "Invalid custom credentials id" }); - // return; - // } - // const credentials = deployment.databaseUrl; if (!databaseUrl) { res.status(400).json({ error: "no databaseUrl set up at your instance yet" }); return; @@ -48,7 +45,7 @@ export const customPrismaClient: NextMiddleware = async (req, res, next) => { PRISMA_CLIENT_CACHING_TIME // Cache the prisma client for 24 hours ); } - req.prisma = cachedPrisma; + req.prisma = customPrisma({ datasources: { db: { url: databaseUrl } } }); } await next(); }; From c3996221e17e582b3ab532309a46e0510821e687 Mon Sep 17 00:00:00 2001 From: Agusti Fernandez Pardo Date: Thu, 23 Jun 2022 19:16:12 +0200 Subject: [PATCH 19/20] fix: don't require apiKey if already using customPrisma with calcom license key --- lib/helpers/customPrisma.ts | 20 +++++++++++--------- lib/helpers/verifyApiKey.ts | 16 ++++++++-------- 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/lib/helpers/customPrisma.ts b/lib/helpers/customPrisma.ts index 5082de68da..d5aeaf9749 100644 --- a/lib/helpers/customPrisma.ts +++ b/lib/helpers/customPrisma.ts @@ -4,7 +4,6 @@ import { NextMiddleware } from "next-api-middleware"; import { PRISMA_CLIENT_CACHING_TIME } from "@calcom/api/lib/constants"; // import prismaAdmin from "@calcom/console/modules/common/utils/prisma"; -import { asStringOrUndefined } from "@calcom/lib/asStringOrNull"; import { CONSOLE_URL } from "@calcom/lib/constants"; import { prisma, customPrisma } from "@calcom/prisma"; @@ -18,14 +17,9 @@ export const customPrismaClient: NextMiddleware = async (req, res, next) => { req.prisma = prisma; await next(); } else { - const id = asStringOrUndefined(key); - - // If we have a key, we check if it is valid. - // const deployment = await prismaAdmin.deployment.findUnique({ - // where: { key }, - // }); + // If we have a key, we check if the deployment matching the key, has a databaseUrl value set. const databaseUrl = await fetch( - `${process.env.NEXT_PUBLIC_CONSOLE_URL || CONSOLE_URL}/api/deployments/database?key=${id}` + `${process.env.NEXT_PUBLIC_CONSOLE_URL || CONSOLE_URL}/api/deployments/database?key=${key}` ) .then((res) => res.json()) .then((res) => res.databaseUrl); @@ -34,10 +28,11 @@ export const customPrismaClient: NextMiddleware = async (req, res, next) => { res.status(400).json({ error: "no databaseUrl set up at your instance yet" }); return; } + // FIXME: Add some checks for the databaseUrl to make sure it is valid. (e.g. not a localhost) const hashedUrl = await hash(databaseUrl, 12); const cachedPrisma = cache.get(hashedUrl); - + /* We cache each cusotm prisma client for 24h to avoid too many requests to the database. */ if (!cachedPrisma) { cache.put( hashedUrl, @@ -46,6 +41,13 @@ export const customPrismaClient: NextMiddleware = async (req, res, next) => { ); } req.prisma = customPrisma({ datasources: { db: { url: databaseUrl } } }); + /* @note: + In order to skip verifyApiKey for customPrisma requests, + we pass isAdmin true, and userId 0, if we detect them later, + we skip verifyApiKey logic and pass onto next middleware instead. + */ + req.isAdmin = true; + req.userId = 0; } await next(); }; diff --git a/lib/helpers/verifyApiKey.ts b/lib/helpers/verifyApiKey.ts index 8c11cdb492..3ee1fad862 100644 --- a/lib/helpers/verifyApiKey.ts +++ b/lib/helpers/verifyApiKey.ts @@ -14,26 +14,26 @@ export const dateNotInPast = function (date: Date) { // This verifies the apiKey and sets the user if it is valid. export const verifyApiKey: NextMiddleware = async (req, res, next) => { - const { prisma } = req; + const { prisma, userId, isAdmin } = req; + // If the user is an admin and using a license key (from customPrisma), skip the apiKey check. + if (userId === 0 && isAdmin) await next(); + // Check if the apiKey query param is provided. if (!req.query.apiKey) return res.status(401).json({ message: "No apiKey provided" }); - - // We remove the prefix from the user provided api_key. If no env set default to "cal_" + // remove the prefix from the user provided api_key. If no env set default to "cal_" const strippedApiKey = `${req.query.apiKey}`.replace(process.env.API_KEY_PREFIX || "cal_", ""); - // Hash the key again before matching against the database records. const hashedKey = hashAPIKey(strippedApiKey); // Check if the hashed api key exists in database. const apiKey = await prisma.apiKey.findUnique({ where: { hashedKey } }); - // console.log("apiKey", apiKey); - // If we cannot find any api key. Throw a 401 Unauthorized. + // If cannot find any api key. Throw a 401 Unauthorized. if (!apiKey) return res.status(401).json({ error: "Your apiKey is not valid" }); if (apiKey.expiresAt && dateNotInPast(apiKey.expiresAt)) { return res.status(401).json({ error: "This apiKey is expired" }); } 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 */ + // save the user id in the request for later use req.userId = apiKey.userId; - /* We save the isAdmin boolean here for later use */ + // save the isAdmin boolean here for later use req.isAdmin = await isAdminGuard(req.userId, prisma); await next(); From aa64f3237843cab292c843ac0471033934863b85 Mon Sep 17 00:00:00 2001 From: Agusti Fernandez Pardo Date: Thu, 23 Jun 2022 23:20:07 +0200 Subject: [PATCH 20/20] fix set headers error by adding missing return --- lib/helpers/verifyApiKey.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/helpers/verifyApiKey.ts b/lib/helpers/verifyApiKey.ts index 3ee1fad862..4a375aee26 100644 --- a/lib/helpers/verifyApiKey.ts +++ b/lib/helpers/verifyApiKey.ts @@ -16,7 +16,10 @@ export const dateNotInPast = function (date: Date) { export const verifyApiKey: NextMiddleware = async (req, res, next) => { const { prisma, userId, isAdmin } = req; // If the user is an admin and using a license key (from customPrisma), skip the apiKey check. - if (userId === 0 && isAdmin) await next(); + if (userId === 0 && isAdmin) { + await next(); + return; + } // Check if the apiKey query param is provided. if (!req.query.apiKey) return res.status(401).json({ message: "No apiKey provided" }); // remove the prefix from the user provided api_key. If no env set default to "cal_"