From fc2978a61bdbe5b2ca26c96e4284cdfd88978fa8 Mon Sep 17 00:00:00 2001 From: Agusti Fernandez Pardo Date: Thu, 7 Apr 2022 03:29:53 +0200 Subject: [PATCH 1/6] feat: initial work unifying new endpoint and generating api key --- .github/workflows/type-check.yml | 26 ------------ lib/helpers/httpMethods.ts | 5 ++- lib/helpers/withMiddleware.ts | 10 ++++- lib/validations/payment.ts | 4 +- pages/api/attendees/index.ts | 57 ++++++++++++++++++++------ pages/api/attendees/new.ts | 41 ------------------- pages/api/payments/[id].ts | 16 ++++---- templates/newIndex.ts | 68 ++++++++++++++++++++++++++++++++ 8 files changed, 135 insertions(+), 92 deletions(-) delete mode 100644 .github/workflows/type-check.yml delete mode 100644 pages/api/attendees/new.ts create mode 100644 templates/newIndex.ts diff --git a/.github/workflows/type-check.yml b/.github/workflows/type-check.yml deleted file mode 100644 index a8c933d87e..0000000000 --- a/.github/workflows/type-check.yml +++ /dev/null @@ -1,26 +0,0 @@ -name: Check types -on: - pull_request: - branches: - - main -jobs: - types: - name: Check types - strategy: - matrix: - node: ["14.x"] - os: [ubuntu-latest] - runs-on: ${{ matrix.os }} - - steps: - - name: Checkout repo - uses: actions/checkout@v2 - with: - fetch-depth: 0 - - - name: Use Node ${{ matrix.node }} - uses: actions/setup-node@v2 - with: - node-version: ${{ matrix.node }} - - run: yarn - - run: yarn type-check diff --git a/lib/helpers/httpMethods.ts b/lib/helpers/httpMethods.ts index 1454355f3a..0617208a90 100644 --- a/lib/helpers/httpMethods.ts +++ b/lib/helpers/httpMethods.ts @@ -10,7 +10,9 @@ export const httpMethod = (allowedHttpMethod: "GET" | "POST" | "PATCH" | "DELETE } }; }; - +// Made this so we can support several HTTP Methods in one route and use it there. +// Could be further extracted into a third function or refactored into one. +// that checks if it's just a string or an array and apply the correct logic to both cases. export const httpMethods = (allowedHttpMethod: string[]): NextMiddleware => { return async function (req, res, next) { if (allowedHttpMethod.map((method) => method === req.method)) { @@ -27,3 +29,4 @@ export const HTTP_GET = httpMethod("GET"); export const HTTP_PATCH = httpMethod("PATCH"); export const HTTP_DELETE = httpMethod("DELETE"); export const HTTP_GET_DELETE_PATCH = httpMethods(["GET", "DELETE", "PATCH"]); +export const HTTP_GET_OR_POST = httpMethods(["GET", "POST"]); diff --git a/lib/helpers/withMiddleware.ts b/lib/helpers/withMiddleware.ts index 4171fa774f..c926eff4ec 100644 --- a/lib/helpers/withMiddleware.ts +++ b/lib/helpers/withMiddleware.ts @@ -2,11 +2,19 @@ import { label } from "next-api-middleware"; import { addRequestId } from "./addRequestid"; import { captureErrors } from "./captureErrors"; -import { HTTP_POST, HTTP_DELETE, HTTP_PATCH, HTTP_GET, HTTP_GET_DELETE_PATCH } from "./httpMethods"; +import { + HTTP_POST, + HTTP_DELETE, + HTTP_PATCH, + HTTP_GET, + HTTP_GET_OR_POST, + HTTP_GET_DELETE_PATCH, +} from "./httpMethods"; import { verifyApiKey } from "./verifyApiKey"; const withMiddleware = label( { + HTTP_GET_OR_POST, HTTP_GET_DELETE_PATCH, HTTP_GET, HTTP_PATCH, diff --git a/lib/validations/payment.ts b/lib/validations/payment.ts index 7a9a138a13..5789d4a092 100644 --- a/lib/validations/payment.ts +++ b/lib/validations/payment.ts @@ -2,9 +2,9 @@ import { withValidation } from "next-validations"; import { _PaymentModel as Payment } from "@calcom/prisma/zod"; +// FIXME: Payment seems a delicate endpoint, do we need to remove anything here? export const schemaPaymentBodyParams = Payment.omit({ id: true }); - -export const schemaPaymentPublic = Payment.omit({}); +export const schemaPaymentPublic = Payment.omit({ externalId: true }); export const withValidPayment = withValidation({ schema: schemaPaymentBodyParams, diff --git a/pages/api/attendees/index.ts b/pages/api/attendees/index.ts index 4c94c6ed7d..60f6a35ebc 100644 --- a/pages/api/attendees/index.ts +++ b/pages/api/attendees/index.ts @@ -3,8 +3,8 @@ import type { NextApiRequest, NextApiResponse } from "next"; import prisma from "@calcom/prisma"; import { withMiddleware } from "@lib/helpers/withMiddleware"; -import { AttendeesResponse } from "@lib/types"; -import { schemaAttendeePublic } from "@lib/validations/attendee"; +import { AttendeeResponse, AttendeesResponse } from "@lib/types"; +import { schemaAttendeeBodyParams, schemaAttendeePublic, withValidAttendee } from "@lib/validations/attendee"; /** * @swagger @@ -20,18 +20,49 @@ import { schemaAttendeePublic } from "@lib/validations/attendee"; * description: Authorization information is missing or invalid. * 404: * description: No attendees were found + * post: + * summary: Creates a new attendee + * tags: + * - attendees + * responses: + * 201: + * description: OK, attendee created + * model: Attendee + * 400: + * description: Bad request. Attendee body is invalid. + * 401: + * description: Authorization information is missing or invalid. */ -async function allAttendees(_: NextApiRequest, res: NextApiResponse) { - const attendees = await prisma.attendee.findMany(); - const data = attendees.map((attendee) => schemaAttendeePublic.parse(attendee)); +async function createOrlistAllAttendees( + req: NextApiRequest, + res: NextApiResponse +) { + const { method } = req; + if (method === "GET") { + const attendees = await prisma.attendee.findMany(); + const data = attendees.map((attendee) => schemaAttendeePublic.parse(attendee)); + if (data) res.status(200).json({ data }); + else + (error: Error) => + res.status(404).json({ + message: "No Attendees were found", + error, + }); + } else if (method === "POST") { + const safe = schemaAttendeeBodyParams.safeParse(req.body); + if (!safe.success) throw new Error("Invalid request body", safe.error); - if (data) res.status(200).json({ data }); - else - (error: Error) => - res.status(404).json({ - message: "No Attendees were found", - error, - }); + const attendee = await prisma.attendee.create({ data: safe.data }); + const data = schemaAttendeePublic.parse(attendee); + + if (data) res.status(201).json({ data, message: "Attendee created successfully" }); + else + (error: Error) => + res.status(400).json({ + message: "Could not create new attendee", + error, + }); + } else res.status(405).json({ message: `Method ${method} not allowed` }); } -export default withMiddleware("HTTP_GET")(allAttendees); +export default withMiddleware("HTTP_GET_OR_POST")(withValidAttendee(createOrlistAllAttendees)); diff --git a/pages/api/attendees/new.ts b/pages/api/attendees/new.ts deleted file mode 100644 index b934e5b62d..0000000000 --- a/pages/api/attendees/new.ts +++ /dev/null @@ -1,41 +0,0 @@ -import type { NextApiRequest, NextApiResponse } from "next"; - -import prisma from "@calcom/prisma"; - -import { withMiddleware } from "@lib/helpers/withMiddleware"; -import type { AttendeeResponse } from "@lib/types"; -import { schemaAttendeeBodyParams, schemaAttendeePublic, withValidAttendee } from "@lib/validations/attendee"; - -/** - * @swagger - * /api/attendees/new: - * post: - * summary: Creates a new attendee - * tags: - * - attendees - * responses: - * 201: - * description: OK, attendee created - * model: Attendee - * 400: - * description: Bad request. Attendee body is invalid. - * 401: - * description: Authorization information is missing or invalid. - */ -async function createAttendee(req: NextApiRequest, res: NextApiResponse) { - const safe = schemaAttendeeBodyParams.safeParse(req.body); - if (!safe.success) throw new Error("Invalid request body", safe.error); - - const attendee = await prisma.attendee.create({ data: safe.data }); - const data = schemaAttendeePublic.parse(attendee); - - if (data) res.status(201).json({ data, message: "Attendee created successfully" }); - else - (error: Error) => - res.status(400).json({ - message: "Could not create new attendee", - error, - }); -} - -export default withMiddleware("HTTP_POST")(withValidAttendee(createAttendee)); diff --git a/pages/api/payments/[id].ts b/pages/api/payments/[id].ts index 14b106ca43..7637bb724d 100644 --- a/pages/api/payments/[id].ts +++ b/pages/api/payments/[id].ts @@ -5,13 +5,16 @@ import prisma from "@calcom/prisma"; import { withMiddleware } from "@lib/helpers/withMiddleware"; import type { PaymentResponse } from "@lib/types"; import { schemaPaymentBodyParams, schemaPaymentPublic } from "@lib/validations/payment"; -import { schemaQueryIdAsString, withValidQueryIdString } from "@lib/validations/shared/queryIdString"; +import { + schemaQueryIdParseInt, + withValidQueryIdTransformParseInt, +} from "@lib/validations/shared/queryIdTransformParseInt"; /** * @swagger * /api/payments/{id}: * get: - * summary: Get a payment by ID + * summary: Get an payment by ID * parameters: * - in: path * name: id @@ -78,17 +81,14 @@ import { schemaQueryIdAsString, withValidQueryIdString } from "@lib/validations/ */ export async function paymentById(req: NextApiRequest, res: NextApiResponse) { const { method, query, body } = req; - const safeQuery = await schemaQueryIdAsString.safeParse(query); + const safeQuery = await schemaQueryIdParseInt.safeParse(query); const safeBody = await schemaPaymentBodyParams.safeParse(body); if (!safeQuery.success) throw new Error("Invalid request query", safeQuery.error); - const [userId, teamId] = safeQuery.data.id.split("_"); switch (method) { case "GET": await prisma.payment - .findUnique({ - where: { userId_teamId: { userId: parseInt(userId), teamId: parseInt(teamId) } }, - }) + .findUnique({ where: { id: safeQuery.data.id } }) .then((payment) => schemaPaymentPublic.parse(payment)) .then((data) => res.status(200).json({ data })) .catch((error: Error) => @@ -127,4 +127,4 @@ export async function paymentById(req: NextApiRequest, res: NextApiResponse +) { + const { method } = req; + if (method === "GET") { + const payments = await prisma.payment.findMany(); + const data = payments.map((payment) => schemaPaymentPublic.parse(payment)); + if (data) res.status(200).json({ data }); + else + (error: Error) => + res.status(404).json({ + message: "No Payments were found", + error, + }); + } else if (method === "POST") { + const safe = schemaPaymentBodyParams.safeParse(req.body); + if (!safe.success) throw new Error("Invalid request body"); + + const payment = await prisma.payment.create({ data: safe.data }); + const data = schemaPaymentPublic.parse(payment); + + if (data) res.status(201).json({ data, message: "Payment created successfully" }); + else + (error: Error) => + res.status(400).json({ + message: "Could not create new payment", + error, + }); + } else res.status(405).json({ message: `Method ${method} not allowed` }); +} + +export default withMiddleware("HTTP_GET_OR_POST")(withValidPayment(createOrlistAllPayments)); From 325b19ba3f335baa6384280bfc5a29f926e1ba49 Mon Sep 17 00:00:00 2001 From: Agusti Fernandez Pardo Date: Thu, 7 Apr 2022 21:55:43 +0200 Subject: [PATCH 2/6] Add userId check in users getAll and id endpoints --- lib/helpers/verifyApiKey.ts | 4 +- lib/utils/getUserFromHeader.ts | 3 + lib/validations/shared/baseApiParams.ts | 2 +- .../api/reminder-mails}/newIndex.ts | 0 pages/api/schedules/newIndex.ts | 68 ++++++++++++++++ pages/api/selected-calendars/newIndex.ts | 68 ++++++++++++++++ pages/api/users/[id].ts | 81 ++++++++++--------- pages/api/users/index.ts | 14 +++- pages/api/users/new.ts | 51 ------------ templates/endpoints/newIndex.ts | 68 ++++++++++++++++ 10 files changed, 263 insertions(+), 96 deletions(-) create mode 100644 lib/utils/getUserFromHeader.ts rename {templates => pages/api/reminder-mails}/newIndex.ts (100%) create mode 100644 pages/api/schedules/newIndex.ts create mode 100644 pages/api/selected-calendars/newIndex.ts delete mode 100644 pages/api/users/new.ts create mode 100644 templates/endpoints/newIndex.ts diff --git a/lib/helpers/verifyApiKey.ts b/lib/helpers/verifyApiKey.ts index 55ee111451..740c4a2de4 100644 --- a/lib/helpers/verifyApiKey.ts +++ b/lib/helpers/verifyApiKey.ts @@ -17,8 +17,10 @@ export const verifyApiKey: NextMiddleware = async (req, res, next) => { res.status(401).json({ error: "Your api key is not valid" }); throw new Error("No api key found"); } + if (apiKey.userId) { + res.setHeader("X-Calcom-User-ID", apiKey.userId); + } if (apiKey.expiresAt && apiKey.userId && dateInPast(today, apiKey.expiresAt)) { - res.setHeader("Calcom-User-ID", apiKey.userId); await next(); } else res.status(401).json({ error: "Your api key is not valid" }); }; diff --git a/lib/utils/getUserFromHeader.ts b/lib/utils/getUserFromHeader.ts new file mode 100644 index 0000000000..fc2e524954 --- /dev/null +++ b/lib/utils/getUserFromHeader.ts @@ -0,0 +1,3 @@ +import { NextApiResponse } from "next"; + +export const getCalcomUserId = (res: NextApiResponse) => res.getHeader("x-calcom-user-id") as number; diff --git a/lib/validations/shared/baseApiParams.ts b/lib/validations/shared/baseApiParams.ts index a7791a5b9d..b670be62d3 100644 --- a/lib/validations/shared/baseApiParams.ts +++ b/lib/validations/shared/baseApiParams.ts @@ -6,7 +6,7 @@ export const baseApiParams = z .object({ // since we added apiKey as query param this is required by next-validations helper // for query params to work properly and not fail. - apiKey: z.string().cuid().optional(), + apiKey: z.string().optional(), // version required for supporting /v1/ redirect to query in api as *?version=1 version: z.string().optional(), }) diff --git a/templates/newIndex.ts b/pages/api/reminder-mails/newIndex.ts similarity index 100% rename from templates/newIndex.ts rename to pages/api/reminder-mails/newIndex.ts diff --git a/pages/api/schedules/newIndex.ts b/pages/api/schedules/newIndex.ts new file mode 100644 index 0000000000..732eb013b2 --- /dev/null +++ b/pages/api/schedules/newIndex.ts @@ -0,0 +1,68 @@ +import type { NextApiRequest, NextApiResponse } from "next"; + +import prisma from "@calcom/prisma"; + +import { withMiddleware } from "@lib/helpers/withMiddleware"; +import { PaymentResponse, PaymentsResponse } from "@lib/types"; +import { schemaPaymentBodyParams, schemaPaymentPublic, withValidPayment } from "@lib/validations/payment"; + +/** + * @swagger + * /api/payments: + * get: + * summary: Get all payments + * tags: + * - payments + * responses: + * 200: + * description: OK + * 401: + * description: Authorization information is missing or invalid. + * 404: + * description: No payments were found + * post: + * summary: Creates a new payment + * tags: + * - payments + * responses: + * 201: + * description: OK, payment created + * model: Payment + * 400: + * description: Bad request. Payment body is invalid. + * 401: + * description: Authorization information is missing or invalid. + */ +async function createOrlistAllPayments( + req: NextApiRequest, + res: NextApiResponse +) { + const { method } = req; + if (method === "GET") { + const payments = await prisma.payment.findMany(); + const data = payments.map((payment) => schemaPaymentPublic.parse(payment)); + if (data) res.status(200).json({ data }); + else + (error: Error) => + res.status(404).json({ + message: "No Payments were found", + error, + }); + } else if (method === "POST") { + const safe = schemaPaymentBodyParams.safeParse(req.body); + if (!safe.success) throw new Error("Invalid request body"); + + const payment = await prisma.payment.create({ data: safe.data }); + const data = schemaPaymentPublic.parse(payment); + + if (data) res.status(201).json({ data, message: "Payment created successfully" }); + else + (error: Error) => + res.status(400).json({ + message: "Could not create new payment", + error, + }); + } else res.status(405).json({ message: `Method ${method} not allowed` }); +} + +export default withMiddleware("HTTP_GET_OR_POST")(withValidPayment(createOrlistAllPayments)); diff --git a/pages/api/selected-calendars/newIndex.ts b/pages/api/selected-calendars/newIndex.ts new file mode 100644 index 0000000000..732eb013b2 --- /dev/null +++ b/pages/api/selected-calendars/newIndex.ts @@ -0,0 +1,68 @@ +import type { NextApiRequest, NextApiResponse } from "next"; + +import prisma from "@calcom/prisma"; + +import { withMiddleware } from "@lib/helpers/withMiddleware"; +import { PaymentResponse, PaymentsResponse } from "@lib/types"; +import { schemaPaymentBodyParams, schemaPaymentPublic, withValidPayment } from "@lib/validations/payment"; + +/** + * @swagger + * /api/payments: + * get: + * summary: Get all payments + * tags: + * - payments + * responses: + * 200: + * description: OK + * 401: + * description: Authorization information is missing or invalid. + * 404: + * description: No payments were found + * post: + * summary: Creates a new payment + * tags: + * - payments + * responses: + * 201: + * description: OK, payment created + * model: Payment + * 400: + * description: Bad request. Payment body is invalid. + * 401: + * description: Authorization information is missing or invalid. + */ +async function createOrlistAllPayments( + req: NextApiRequest, + res: NextApiResponse +) { + const { method } = req; + if (method === "GET") { + const payments = await prisma.payment.findMany(); + const data = payments.map((payment) => schemaPaymentPublic.parse(payment)); + if (data) res.status(200).json({ data }); + else + (error: Error) => + res.status(404).json({ + message: "No Payments were found", + error, + }); + } else if (method === "POST") { + const safe = schemaPaymentBodyParams.safeParse(req.body); + if (!safe.success) throw new Error("Invalid request body"); + + const payment = await prisma.payment.create({ data: safe.data }); + const data = schemaPaymentPublic.parse(payment); + + if (data) res.status(201).json({ data, message: "Payment created successfully" }); + else + (error: Error) => + res.status(400).json({ + message: "Could not create new payment", + error, + }); + } else res.status(405).json({ message: `Method ${method} not allowed` }); +} + +export default withMiddleware("HTTP_GET_OR_POST")(withValidPayment(createOrlistAllPayments)); diff --git a/pages/api/users/[id].ts b/pages/api/users/[id].ts index fa0b4c7255..e309288253 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 { getCalcomUserId } from "@lib/utils/getUserFromHeader"; import { schemaQueryIdParseInt, withValidQueryIdTransformParseInt, @@ -14,7 +15,7 @@ import { schemaUserBodyParams, schemaUserPublic } from "@lib/validations/user"; * @swagger * /api/users/{id}: * get: - * summary: Get a user by ID + * summary: Get a user by ID, returns your user if regular user. * parameters: * - in: path * name: id @@ -84,47 +85,49 @@ export async function userById(req: NextApiRequest, res: NextApiResponse schemaUserPublic.parse(user)) + .then((data) => res.status(200).json({ data })) + .catch((error: Error) => + res.status(404).json({ message: `User with id: ${safeQuery.data.id} not found`, error }) + ); + break; - switch (method) { - case "GET": - await prisma.user - .findUnique({ where: { id: safeQuery.data.id } }) - .then((user) => schemaUserPublic.parse(user)) - .then((data) => res.status(200).json({ data })) - .catch((error: Error) => - res.status(404).json({ message: `User with id: ${safeQuery.data.id} not found`, error }) - ); - break; + case "PATCH": + if (!safeBody.success) throw new Error("Invalid request body"); + await prisma.user + .update({ + where: { id: safeQuery.data.id }, + data: safeBody.data, + }) + .then((user) => schemaUserPublic.parse(user)) + .then((data) => res.status(200).json({ data })) + .catch((error: Error) => + res.status(404).json({ message: `User with id: ${safeQuery.data.id} not found`, error }) + ); + break; - case "PATCH": - if (!safeBody.success) throw new Error("Invalid request body"); - await prisma.user - .update({ - where: { id: safeQuery.data.id }, - data: safeBody.data, - }) - .then((user) => schemaUserPublic.parse(user)) - .then((data) => res.status(200).json({ data })) - .catch((error: Error) => - res.status(404).json({ message: `User with id: ${safeQuery.data.id} not found`, error }) - ); - break; + 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; - 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; - } + default: + res.status(405).json({ message: "Method not allowed" }); + break; + } + } else res.status(401).json({ message: "Unauthorized" }); } export default withMiddleware("HTTP_GET_DELETE_PATCH")(withValidQueryIdTransformParseInt(userById)); diff --git a/pages/api/users/index.ts b/pages/api/users/index.ts index 6bb3763a88..23ec86eba0 100644 --- a/pages/api/users/index.ts +++ b/pages/api/users/index.ts @@ -4,13 +4,14 @@ import prisma from "@calcom/prisma"; import { withMiddleware } from "@lib/helpers/withMiddleware"; import { UsersResponse } from "@lib/types"; +import { getCalcomUserId } from "@lib/utils/getUserFromHeader"; import { schemaUserPublic } from "@lib/validations/user"; /** * @swagger * /api/users: * get: - * summary: Get all users + * summary: Get all users (admin only), returns your user if regular user. * tags: * - users * responses: @@ -21,10 +22,14 @@ import { schemaUserPublic } from "@lib/validations/user"; * 404: * description: No users were found */ -async function allUsers(_: NextApiRequest, res: NextApiResponse) { - const users = await prisma.user.findMany(); +async function allUsers(req: NextApiRequest, res: NextApiResponse) { + const userId = getCalcomUserId(res); + const users = await prisma.user.findMany({ + where: { + id: userId, + }, + }); const data = users.map((user) => schemaUserPublic.parse(user)); - if (data) res.status(200).json({ data }); else (error: Error) => @@ -33,5 +38,6 @@ async function allUsers(_: NextApiRequest, res: NextApiResponse) error, }); } +// No POST endpoint for users for now as a regular user you're expected to signup. export default withMiddleware("HTTP_GET")(allUsers); diff --git a/pages/api/users/new.ts b/pages/api/users/new.ts deleted file mode 100644 index df3310c09c..0000000000 --- a/pages/api/users/new.ts +++ /dev/null @@ -1,51 +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 { schemaUserBodyParams, schemaUserPublic, withValidUser } from "@lib/validations/user"; - -/** - * @swagger - * /api/users/new: - * post: - * summary: Add a new user - * consumes: - * - application/json - * parameters: - * - in: body - * name: user - * description: The user to edit - * schema: - * type: object - * $ref: '#/components/schemas/User' - * required: true - * tags: - * - users - * responses: - * 201: - * description: OK, user created - * model: User - * 400: - * description: Bad request. User body is invalid. - * 401: - * description: Authorization information is missing or invalid. - */ -async function createUser(req: NextApiRequest, res: NextApiResponse) { - const safe = schemaUserBodyParams.safeParse(req.body); - if (!safe.success) throw new Error("Invalid request body", safe.error); - - const user = await prisma.user.create({ data: safe.data }); - const data = schemaUserPublic.parse(user); - - if (data) res.status(201).json({ data, message: "User created successfully" }); - else - (error: Error) => - res.status(400).json({ - message: "Could not create new user", - error, - }); -} - -export default withMiddleware("HTTP_POST")(withValidUser(createUser)); diff --git a/templates/endpoints/newIndex.ts b/templates/endpoints/newIndex.ts new file mode 100644 index 0000000000..732eb013b2 --- /dev/null +++ b/templates/endpoints/newIndex.ts @@ -0,0 +1,68 @@ +import type { NextApiRequest, NextApiResponse } from "next"; + +import prisma from "@calcom/prisma"; + +import { withMiddleware } from "@lib/helpers/withMiddleware"; +import { PaymentResponse, PaymentsResponse } from "@lib/types"; +import { schemaPaymentBodyParams, schemaPaymentPublic, withValidPayment } from "@lib/validations/payment"; + +/** + * @swagger + * /api/payments: + * get: + * summary: Get all payments + * tags: + * - payments + * responses: + * 200: + * description: OK + * 401: + * description: Authorization information is missing or invalid. + * 404: + * description: No payments were found + * post: + * summary: Creates a new payment + * tags: + * - payments + * responses: + * 201: + * description: OK, payment created + * model: Payment + * 400: + * description: Bad request. Payment body is invalid. + * 401: + * description: Authorization information is missing or invalid. + */ +async function createOrlistAllPayments( + req: NextApiRequest, + res: NextApiResponse +) { + const { method } = req; + if (method === "GET") { + const payments = await prisma.payment.findMany(); + const data = payments.map((payment) => schemaPaymentPublic.parse(payment)); + if (data) res.status(200).json({ data }); + else + (error: Error) => + res.status(404).json({ + message: "No Payments were found", + error, + }); + } else if (method === "POST") { + const safe = schemaPaymentBodyParams.safeParse(req.body); + if (!safe.success) throw new Error("Invalid request body"); + + const payment = await prisma.payment.create({ data: safe.data }); + const data = schemaPaymentPublic.parse(payment); + + if (data) res.status(201).json({ data, message: "Payment created successfully" }); + else + (error: Error) => + res.status(400).json({ + message: "Could not create new payment", + error, + }); + } else res.status(405).json({ message: `Method ${method} not allowed` }); +} + +export default withMiddleware("HTTP_GET_OR_POST")(withValidPayment(createOrlistAllPayments)); From 3d917e4dd486453da56d6241b308fab9c98a73c2 Mon Sep 17 00:00:00 2001 From: Agusti Fernandez Pardo Date: Fri, 8 Apr 2022 01:59:04 +0200 Subject: [PATCH 3/6] make verifyApiKey check for hashed, need @calcom/ee in transpile modules next.config --- lib/helpers/verifyApiKey.ts | 5 ++++- next.config.js | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/helpers/verifyApiKey.ts b/lib/helpers/verifyApiKey.ts index 740c4a2de4..275457d12c 100644 --- a/lib/helpers/verifyApiKey.ts +++ b/lib/helpers/verifyApiKey.ts @@ -1,5 +1,6 @@ import { NextMiddleware } from "next-api-middleware"; +import { hashAPIKey } from "@calcom/ee/lib/api/apiKeys"; // import { nanoid } from "nanoid"; import prisma from "@calcom/prisma"; @@ -12,7 +13,9 @@ const today = new Date(); export const verifyApiKey: NextMiddleware = async (req, res, next) => { if (!req.query.apiKey) res.status(401).json({ message: "No API key provided" }); - const apiKey = await prisma.apiKey.findUnique({ where: { id: req.query.apiKey as string } }); + const strippedApiKey = `${req.query.apiKey}`.replace("cal_", ""); + const hashedKey = hashAPIKey(strippedApiKey); + const apiKey = await prisma.apiKey.findUnique({ where: { hashedKey } }); if (!apiKey) { res.status(401).json({ error: "Your api key is not valid" }); throw new Error("No api key found"); diff --git a/next.config.js b/next.config.js index 1962fe9a8e..c6f75ad063 100644 --- a/next.config.js +++ b/next.config.js @@ -1,6 +1,6 @@ // https://www.npmjs.com/package/next-transpile-modules // This makes our @calcom/prisma package from the monorepo to be transpiled and usable by API -const withTM = require("next-transpile-modules")(["@calcom/prisma", "@calcom/lib"]); +const withTM = require("next-transpile-modules")(["@calcom/prisma", "@calcom/lib", "@calcom/ee"]); // use something like withPlugins([withTM], {}) if more plugins added later. From fc3677631fd2f809034bcfe9296184fc4ca322a9 Mon Sep 17 00:00:00 2001 From: Agusti Fernandez Pardo Date: Fri, 8 Apr 2022 03:16:53 +0200 Subject: [PATCH 4/6] attendes of users bookings only --- lib/types.ts | 2 ++ lib/validations/attendee.ts | 1 + pages/api/attendees/index.ts | 31 ++++++++++++++++++++++++------- 3 files changed, 27 insertions(+), 7 deletions(-) diff --git a/lib/types.ts b/lib/types.ts index 55c36392bb..cffd7c9841 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -62,6 +62,8 @@ export type SelectedCalendarsResponse = BaseResponse & { export type AttendeeResponse = BaseResponse & { data?: Partial; }; +// Grouping attendees in booking arrays for now, +// later might remove endpoint and move to booking endpoint altogether. export type AttendeesResponse = BaseResponse & { data?: Partial[]; }; diff --git a/lib/validations/attendee.ts b/lib/validations/attendee.ts index 856ab2bcae..290ea16adf 100644 --- a/lib/validations/attendee.ts +++ b/lib/validations/attendee.ts @@ -8,6 +8,7 @@ export const schemaAttendeeBaseBodyParams = Attendee.omit({ id: true }).partial( export const schemaAttendeePublic = Attendee.omit({}); const schemaAttendeeRequiredParams = z.object({ + bookingId: z.any(), email: z.string().email(), name: z.string(), timeZone: z.string(), diff --git a/pages/api/attendees/index.ts b/pages/api/attendees/index.ts index 60f6a35ebc..644bba575c 100644 --- a/pages/api/attendees/index.ts +++ b/pages/api/attendees/index.ts @@ -4,6 +4,7 @@ import prisma from "@calcom/prisma"; import { withMiddleware } from "@lib/helpers/withMiddleware"; import { AttendeeResponse, AttendeesResponse } from "@lib/types"; +import { getCalcomUserId } from "@lib/utils/getUserFromHeader"; import { schemaAttendeeBodyParams, schemaAttendeePublic, withValidAttendee } from "@lib/validations/attendee"; /** @@ -38,10 +39,19 @@ async function createOrlistAllAttendees( res: NextApiResponse ) { const { method } = req; + const userId = getCalcomUserId(res); + // Here we make sure to only return attendee's of the user's own bookings. + const userBookings = await prisma.booking.findMany({ + where: { + userId, + }, + include: { + attendees: true, + }, + }); + const userBookingsAttendees = userBookings.map((booking) => booking.attendees).flat(); if (method === "GET") { - const attendees = await prisma.attendee.findMany(); - const data = attendees.map((attendee) => schemaAttendeePublic.parse(attendee)); - if (data) res.status(200).json({ data }); + if (userBookingsAttendees) res.status(200).json({ data: userBookingsAttendees }); else (error: Error) => res.status(404).json({ @@ -50,9 +60,16 @@ async function createOrlistAllAttendees( }); } else if (method === "POST") { const safe = schemaAttendeeBodyParams.safeParse(req.body); - if (!safe.success) throw new Error("Invalid request body", safe.error); - - const attendee = await prisma.attendee.create({ data: safe.data }); + if (!safe.success) { + console.log(safe.error); + throw new Error("Invalid request body", safe.error); + } + const bookingId = safe.data.bookingId; + delete safe.data.bookingId; + const noBookingId = safe.data; + const attendee = await prisma.attendee.create({ + data: { ...noBookingId, booking: { connect: { id: parseInt(bookingId as string) } } }, + }); const data = schemaAttendeePublic.parse(attendee); if (data) res.status(201).json({ data, message: "Attendee created successfully" }); @@ -65,4 +82,4 @@ async function createOrlistAllAttendees( } else res.status(405).json({ message: `Method ${method} not allowed` }); } -export default withMiddleware("HTTP_GET_OR_POST")(withValidAttendee(createOrlistAllAttendees)); +export default withMiddleware("HTTP_GET_OR_POST")(createOrlistAllAttendees); From 9edc1dbd29aea3ec9ed7d34d4483fb61db1f9a23 Mon Sep 17 00:00:00 2001 From: Agusti Fernandez Pardo Date: Fri, 8 Apr 2022 18:08:26 +0200 Subject: [PATCH 5/6] some fixes on helpers/middlewares --- .env.example | 1 + lib/helpers/addRequestid.ts | 2 +- lib/helpers/captureErrors.ts | 9 ++++----- lib/helpers/verifyApiKey.ts | 33 +++++++++++++++++++-------------- lib/helpers/withCost.ts | 23 ----------------------- lib/utils/getCalcomUserId.ts | 3 +++ lib/utils/getUserFromHeader.ts | 3 --- pages/api/attendees/index.ts | 2 +- 8 files changed, 29 insertions(+), 47 deletions(-) create mode 100644 .env.example delete mode 100644 lib/helpers/withCost.ts create mode 100644 lib/utils/getCalcomUserId.ts delete mode 100644 lib/utils/getUserFromHeader.ts diff --git a/.env.example b/.env.example new file mode 100644 index 0000000000..f3271e560c --- /dev/null +++ b/.env.example @@ -0,0 +1 @@ +API_KEY_PREFIX=cal_ \ No newline at end of file diff --git a/lib/helpers/addRequestid.ts b/lib/helpers/addRequestid.ts index af151e00cc..263c7aacc1 100644 --- a/lib/helpers/addRequestid.ts +++ b/lib/helpers/addRequestid.ts @@ -2,7 +2,7 @@ import { nanoid } from "nanoid"; import { NextMiddleware } from "next-api-middleware"; export const addRequestId: NextMiddleware = async (_req, res, next) => { - // Apply header + // Apply header with unique ID to every request res.setHeader("Calcom-Response-ID", nanoid()); // Let remaining middleware and API route execute await next(); diff --git a/lib/helpers/captureErrors.ts b/lib/helpers/captureErrors.ts index 109e4e2254..ff35ee3c89 100644 --- a/lib/helpers/captureErrors.ts +++ b/lib/helpers/captureErrors.ts @@ -6,10 +6,9 @@ export const captureErrors: NextMiddleware = async (_req, res, next) => { // Catch any errors that are thrown in remaining // middleware and the API route handler await next(); - } catch (err) { - Sentry.captureException(err); - console.log(err); - res.status(400).json({ message: "Something went wrong", error: err }); - // res.json({ error: err }); + } catch (error) { + Sentry.captureException(error); + console.log(error); + res.status(400).json({ message: "Something went wrong", error }); } }; diff --git a/lib/helpers/verifyApiKey.ts b/lib/helpers/verifyApiKey.ts index 275457d12c..694de73593 100644 --- a/lib/helpers/verifyApiKey.ts +++ b/lib/helpers/verifyApiKey.ts @@ -1,29 +1,34 @@ import { NextMiddleware } from "next-api-middleware"; import { hashAPIKey } from "@calcom/ee/lib/api/apiKeys"; -// import { nanoid } from "nanoid"; import prisma from "@calcom/prisma"; -const dateInPast = function (firstDate: Date, secondDate: Date) { +// Used to check if the API key is not expired, could be extracted if reused. but not for now. +export const dateInPast = function (firstDate: Date, secondDate: Date) { if (firstDate.setHours(0, 0, 0, 0) <= secondDate.setHours(0, 0, 0, 0)) { return true; } }; const today = new Date(); +// This verifies the API key and sets the user if it is valid. export const verifyApiKey: NextMiddleware = async (req, res, next) => { if (!req.query.apiKey) res.status(401).json({ message: "No API key provided" }); - const strippedApiKey = `${req.query.apiKey}`.replace("cal_", ""); + + const strippedApiKey = `${req.query.apiKey}`.replace(process.env.API_KEY_PREFIX || "cal_", ""); const hashedKey = hashAPIKey(strippedApiKey); - const apiKey = await prisma.apiKey.findUnique({ where: { hashedKey } }); - if (!apiKey) { - res.status(401).json({ error: "Your api key is not valid" }); - throw new Error("No api key found"); - } - if (apiKey.userId) { - res.setHeader("X-Calcom-User-ID", apiKey.userId); - } - if (apiKey.expiresAt && apiKey.userId && dateInPast(today, apiKey.expiresAt)) { - await next(); - } else res.status(401).json({ error: "Your api key is not valid" }); + + await prisma.apiKey + .findUnique({ where: { hashedKey } }) + .then(async (apiKey) => { + if (!apiKey) { + res.status(401).json({ error: "You did not provide an api key" }); + throw new Error("No api key found"); + } + if (apiKey.userId) res.setHeader("X-Calcom-User-ID", apiKey?.userId); + if (apiKey.expiresAt && apiKey.userId && dateInPast(today, apiKey.expiresAt)) await next(); + }) + .catch((error) => { + res.status(401).json({ error: "Your api key is not valid" }); + }); }; diff --git a/lib/helpers/withCost.ts b/lib/helpers/withCost.ts deleted file mode 100644 index d462536956..0000000000 --- a/lib/helpers/withCost.ts +++ /dev/null @@ -1,23 +0,0 @@ -// Make a middleware that adds a cost to running the request -// by calling stripeSubscription addCost() * pricePerBooking -// Initially to test out 0,5 cent per booking via API call -// withCost(5)(endpoint) -// Should add a charge of 0.5 cent per booking to the subscription of the user making the request -import { NextMiddleware } from "next-api-middleware"; - -export const withCost = (priceInCents: number): NextMiddleware => { - return async function (req, res, next) { - console.log(req.headers); - if ( - priceInCents > 0 - // && stripeCustomerId && stripeSubscriptionId - ) { - console.log(priceInCents); - // if (req.method === allowedHttpMethod || req.method == "OPTIONS") { - await next(); - } else { - res.status(405).json({ message: `We weren't able to process the payment for this transaction` }); - res.end(); - } - }; -}; diff --git a/lib/utils/getCalcomUserId.ts b/lib/utils/getCalcomUserId.ts new file mode 100644 index 0000000000..8670bc8759 --- /dev/null +++ b/lib/utils/getCalcomUserId.ts @@ -0,0 +1,3 @@ +import { NextApiResponse } from "next"; + +export const getCalcomUserId = (res: NextApiResponse): number => res.getHeader("x-calcom-user-id") as number; diff --git a/lib/utils/getUserFromHeader.ts b/lib/utils/getUserFromHeader.ts deleted file mode 100644 index fc2e524954..0000000000 --- a/lib/utils/getUserFromHeader.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { NextApiResponse } from "next"; - -export const getCalcomUserId = (res: NextApiResponse) => res.getHeader("x-calcom-user-id") as number; diff --git a/pages/api/attendees/index.ts b/pages/api/attendees/index.ts index 644bba575c..e15a3af0e0 100644 --- a/pages/api/attendees/index.ts +++ b/pages/api/attendees/index.ts @@ -4,7 +4,7 @@ import prisma from "@calcom/prisma"; import { withMiddleware } from "@lib/helpers/withMiddleware"; import { AttendeeResponse, AttendeesResponse } from "@lib/types"; -import { getCalcomUserId } from "@lib/utils/getUserFromHeader"; +import { getCalcomUserId } from "@lib/utils/getCalcomUserId"; import { schemaAttendeeBodyParams, schemaAttendeePublic, withValidAttendee } from "@lib/validations/attendee"; /** From 1f561e70be027a8b14b6949dc6d387feff79f632 Mon Sep 17 00:00:00 2001 From: Agusti Fernandez Pardo Date: Fri, 8 Apr 2022 20:54:48 +0200 Subject: [PATCH 6/6] reanem getCalcomUserId util --- pages/api/users/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/api/users/index.ts b/pages/api/users/index.ts index 23ec86eba0..17c975c32d 100644 --- a/pages/api/users/index.ts +++ b/pages/api/users/index.ts @@ -4,7 +4,7 @@ import prisma from "@calcom/prisma"; import { withMiddleware } from "@lib/helpers/withMiddleware"; import { UsersResponse } from "@lib/types"; -import { getCalcomUserId } from "@lib/utils/getUserFromHeader"; +import { getCalcomUserId } from "@lib/utils/getCalcomUserId"; import { schemaUserPublic } from "@lib/validations/user"; /**