From 084c91f6c16e6feb272eb5797def53baeabd632c Mon Sep 17 00:00:00 2001 From: Agusti Fernandez Pardo Date: Fri, 25 Mar 2022 23:26:22 +0100 Subject: [PATCH] feat: improve api key endpoints, add users endpoint --- lib/validations/apiKey.ts | 2 +- lib/validations/queryIdString.ts | 2 +- lib/validations/user.ts | 62 ++++++++++++++++++++++++++++++++ pages/api/api-keys/index.ts | 2 +- pages/api/api-keys/new.ts | 13 +++++-- pages/api/event-types/index.ts | 2 +- pages/api/users/[id]/delete.ts | 36 +++++++++++++++++++ pages/api/users/[id]/edit.ts | 37 +++++++++++++++++++ pages/api/users/[id]/index.ts | 31 ++++++++++++++++ pages/api/users/index.ts | 19 ++++++++++ pages/api/users/new.ts | 29 +++++++++++++++ 11 files changed, 228 insertions(+), 7 deletions(-) create mode 100644 lib/validations/user.ts create mode 100644 pages/api/users/[id]/delete.ts create mode 100644 pages/api/users/[id]/edit.ts create mode 100644 pages/api/users/[id]/index.ts create mode 100644 pages/api/users/index.ts create mode 100644 pages/api/users/new.ts diff --git a/lib/validations/apiKey.ts b/lib/validations/apiKey.ts index 5ed6e12b76..ea0fbaec3a 100644 --- a/lib/validations/apiKey.ts +++ b/lib/validations/apiKey.ts @@ -3,7 +3,7 @@ import { z } from "zod"; const schemaApiKey = z .object({ - expiresAt: z.date().optional(), // default is 30 days + expiresAt: z.string().transform((date: string) => new Date(date)).optional(), // default is 30 days note: z.string().min(1).optional(), }) .strict(); // Adding strict so that we can disallow passing in extra fields diff --git a/lib/validations/queryIdString.ts b/lib/validations/queryIdString.ts index 02c1e6aee5..ef000f2014 100644 --- a/lib/validations/queryIdString.ts +++ b/lib/validations/queryIdString.ts @@ -7,7 +7,7 @@ const schemaQueryId = z .object({ // since nextjs parses query params as strings, // we need to cast them to numbers using z.transform() and parseInt() - id: z.string().uuid() + id: z.string() }) .strict(); diff --git a/lib/validations/user.ts b/lib/validations/user.ts new file mode 100644 index 0000000000..4b24b168af --- /dev/null +++ b/lib/validations/user.ts @@ -0,0 +1,62 @@ +import { withValidation } from "next-validations"; +import { schemaEventType } from "./eventType"; +// import { schemaCredential } from "./credential"; +// import { schemaMembership } from "./membership"; +// import { schemaBooking } from "./booking"; +// import { schemaSchedule } from "./schedule"; +// import { schemaSelectedCalendar } from "./selectedCalendar"; +// import { schemaAvailability } from "./availability"; +// import { schemaWebhook } from "./webhook"; + +import { z } from "zod"; +import { schemaApiKey } from "./apiKey"; + +const schemaUser = z + .object({ + username: z.string().min(3), + name: z.string().min(3), + email: z.string().email(), // max is a full day. + emailVerified: z.date().optional(), + password: z.string().optional(), + bio: z.string().min(3).optional(), + avatar: z.string().optional(), + timeZone: z.string().default("Europe/London"), + weekStart: z.string().default("Sunday"), + bufferTime: z.number().default(0), + hideBranding: z.boolean().default(false), + theme: z.string().optional(), + trialEndsAt: z.date().optional(), + eventTypes: z.array((schemaEventType)).optional(), + // credentials: z.array((schemaCredentials)).optional(), + // teams: z.array((schemaMembership)).optional(), + // bookings: z.array((schemaBooking)).optional(), + // schedules: z.array((schemaSchedule)).optional(), + defaultScheduleId: z.number().optional(), + // selectedCalendars: z.array((schemaSelectedCalendar)).optional(), + completedOnboarding: z.boolean().default(false), + locale: z.string().optional(), + timeFormat: z.number().optional().default(12), + twoFactorEnabled: z.boolean().default(false), + twoFactorSecret: z.string().optional(), + identityProvider: z.enum(["CAL", "SAML", "GOOGLE"]).optional().default("CAL"), + identityProviderId: z.string().optional(), + // availavility: z.array((schemaAvailavility)).optional(), + invitedTo: z.number().optional(), + plan: z.enum(['FREE', 'TRIAL', 'PRO']).default("TRIAL"), + // webhooks: z.array((schemaWebhook)).optional(), + brandColor: z.string().default("#292929"), + darkBrandColor: z.string().default("#fafafa"), + // destinationCalendar: z.instanceof(schemaEventType).optional(), // FIXME: instanceof doesnt work here + away: z.boolean().default(false), + metadata: z.object({}).optional(), + verified: z.boolean().default(false), + apiKeys: z.array((schemaApiKey)).optional(), + }) + .strict(); // Adding strict so that we can disallow passing in extra fields +const withValidUser = withValidation({ + schema: schemaUser, + type: "Zod", + mode: "body", +}); + +export { schemaUser, withValidUser }; diff --git a/pages/api/api-keys/index.ts b/pages/api/api-keys/index.ts index d6337207e2..ed0314a9fc 100644 --- a/pages/api/api-keys/index.ts +++ b/pages/api/api-keys/index.ts @@ -9,7 +9,7 @@ type ResponseData = { export default async function apiKey(req: NextApiRequest, res: NextApiResponse) { try { - const apiKeys = await prisma.apiKey.findMany({ where: { id: `${req.query.eventTypeId}` } }); + const apiKeys = await prisma.apiKey.findMany({}); res.status(200).json({ data: { ...apiKeys } }); } catch (error) { console.log(error); diff --git a/pages/api/api-keys/new.ts b/pages/api/api-keys/new.ts index 2e649321c0..4133ea80c2 100644 --- a/pages/api/api-keys/new.ts +++ b/pages/api/api-keys/new.ts @@ -16,10 +16,17 @@ async function createApiKey(req: NextApiRequest, res: NextApiResponse res.status(201).json({ data: apiKey })) - .catch((error) => res.status(400).json({ message: "Could not create apiKey", error: error })); + .catch((error) => { + console.log(error); + res.status(400).json({ message: "Could not create apiKey", error: error }) + } + ) } } else { // Reject any other HTTP method than POST diff --git a/pages/api/event-types/index.ts b/pages/api/event-types/index.ts index faad9e43ba..491989884f 100644 --- a/pages/api/event-types/index.ts +++ b/pages/api/event-types/index.ts @@ -9,7 +9,7 @@ type ResponseData = { export default async function eventType(req: NextApiRequest, res: NextApiResponse) { try { - const eventTypes = await prisma.eventType.findMany({ where: { id: Number(req.query.eventTypeId) } }); + const eventTypes = await prisma.eventType.findMany(); res.status(200).json({ data: { ...eventTypes } }); } catch (error) { console.log(error); diff --git a/pages/api/users/[id]/delete.ts b/pages/api/users/[id]/delete.ts new file mode 100644 index 0000000000..3c35fd5844 --- /dev/null +++ b/pages/api/users/[id]/delete.ts @@ -0,0 +1,36 @@ +import prisma from "@calcom/prisma"; + +import type { NextApiRequest, NextApiResponse } from "next"; + +import { schemaQueryId, withValidQueryIdTransformParseInt } from "@lib/validations/queryIdTransformParseInt"; + + +type ResponseData = { + message?: string; + error?: unknown; +}; + +export async function user(req: NextApiRequest, res: NextApiResponse) { + const { query, method } = req; + const safe = await schemaQueryId.safeParse(query); + if (safe.success) { + if (method === "DELETE") { + // DELETE WILL DELETE THE EVENT TYPE + prisma.user + .delete({ where: { id: safe.data.id } }) + .then(() => { + // We only remove the user type from the database if there's an existing resource. + res.status(200).json({ message: `user-type with id: ${safe.data.id} deleted successfully` }); + }) + .catch((error) => { + // This catches the error thrown by prisma.user.delete() if the resource is not found. + res.status(400).json({ message: `Resource with id:${safe.data.id} was not found`, error: error }); + }); + } else { + // Reject any other HTTP method than POST + res.status(405).json({ message: "Only DELETE Method allowed in /user-types/[id]/delete endpoint" }); + } + } +} + +export default withValidQueryIdTransformParseInt(user); diff --git a/pages/api/users/[id]/edit.ts b/pages/api/users/[id]/edit.ts new file mode 100644 index 0000000000..1332b3c686 --- /dev/null +++ b/pages/api/users/[id]/edit.ts @@ -0,0 +1,37 @@ +import prisma from "@calcom/prisma"; + +import { User } from "@calcom/prisma/client"; +import type { NextApiRequest, NextApiResponse } from "next"; + +import { schemaUser, withValidUser } from "@lib/validations/user"; +import { schemaQueryId, withValidQueryIdTransformParseInt } from "@lib/validations/queryIdTransformParseInt"; + +type ResponseData = { + data?: User; + message?: string; + error?: unknown; +}; + +export async function editUser(req: NextApiRequest, res: NextApiResponse) { + const { query, body, method } = req; + const safeQuery = await schemaQueryId.safeParse(query); + const safeBody = await schemaUser.safeParse(body); + + if (method === "PATCH") { + if (safeQuery.success && safeBody.success) { + await prisma.user.update({ + where: { id: safeQuery.data.id }, + data: safeBody.data, + }).then(user => { + res.status(200).json({ data: user }); + }).catch(error => { + res.status(404).json({ message: `Event type with ID ${safeQuery.data.id} not found and wasn't updated`, error }) + }); + } + } else { + // Reject any other HTTP method than POST + res.status(405).json({ message: "Only PATCH Method allowed for updating user-types" }); + } +} + +export default withValidQueryIdTransformParseInt(withValidUser(editUser)); diff --git a/pages/api/users/[id]/index.ts b/pages/api/users/[id]/index.ts new file mode 100644 index 0000000000..7b6c991f2b --- /dev/null +++ b/pages/api/users/[id]/index.ts @@ -0,0 +1,31 @@ +import prisma from "@calcom/prisma"; + +import { User } from "@calcom/prisma/client"; +import type { NextApiRequest, NextApiResponse } from "next"; + +import { schemaQueryId, withValidQueryIdTransformParseInt } from "@lib/validations/queryIdTransformParseInt"; + +type ResponseData = { + data?: User; + message?: string; + error?: unknown; +}; + +export async function user(req: NextApiRequest, res: NextApiResponse) { + const { query, method } = req; + const safe = await schemaQueryId.safeParse(query); + if (safe.success) { + if (method === "GET") { + const user = await prisma.user.findUnique({ where: { id: safe.data.id } }); + + if (user) res.status(200).json({ data: user }); + if (!user) res.status(404).json({ message: "Event type not found" }); + } else { + // Reject any other HTTP method than POST + res.status(405).json({ message: "Only GET Method allowed" }); + } + } +} + + +export default withValidQueryIdTransformParseInt(user); diff --git a/pages/api/users/index.ts b/pages/api/users/index.ts new file mode 100644 index 0000000000..3d03dcf205 --- /dev/null +++ b/pages/api/users/index.ts @@ -0,0 +1,19 @@ +import prisma from "@calcom/prisma"; + +import { User } from "@calcom/prisma/client";import type { NextApiRequest, NextApiResponse } from "next"; + +type ResponseData = { + data?: User[]; + error?: unknown; +}; + +export default async function user(req: NextApiRequest, res: NextApiResponse) { + try { + const users = await prisma.user.findMany(); + res.status(200).json({ data: { ...users } }); + } catch (error) { + console.log(error); + // FIXME: Add zod for validation/error handling + res.status(400).json({ error: error }); + } +} diff --git a/pages/api/users/new.ts b/pages/api/users/new.ts new file mode 100644 index 0000000000..4dee441ff5 --- /dev/null +++ b/pages/api/users/new.ts @@ -0,0 +1,29 @@ +import prisma from "@calcom/prisma"; + +import { User } from "@calcom/prisma/client";import type { NextApiRequest, NextApiResponse } from "next"; + +import { schemaUser, withValidUser } from "@lib/validations/user"; + +type ResponseData = { + data?: User; + message?: string; + error?: string; +}; + +async function createUser(req: NextApiRequest, res: NextApiResponse) { + const { body, method } = req; + if (method === "POST") { + const safe = schemaUser.safeParse(body); + if (safe.success && safe.data) { + await prisma.user + .create({ data: safe.data }) + .then((user) => res.status(201).json({ data: user })) + .catch((error) => res.status(400).json({ message: "Could not create user type", error: error })); + } + } else { + // Reject any other HTTP method than POST + res.status(405).json({ error: "Only POST Method allowed" }); + } +} + +export default withValidUser(createUser);