From 9459b9048db6adeeb7b83f2948915300553d4469 Mon Sep 17 00:00:00 2001 From: Agusti Fernandez Pardo Date: Tue, 31 May 2022 18:33:01 +0200 Subject: [PATCH 1/4] feat: Admin API users manegement endpoints --- lib/utils/isAdmin.ts | 6 +++++ lib/validations/user.ts | 32 ++++++++++++++++++++--- pages/api/users/[id].ts | 8 ++++-- pages/api/users/index.ts | 56 +++++++++++++++++++++------------------- 4 files changed, 70 insertions(+), 32 deletions(-) create mode 100644 lib/utils/isAdmin.ts diff --git a/lib/utils/isAdmin.ts b/lib/utils/isAdmin.ts new file mode 100644 index 0000000000..1026b8deb1 --- /dev/null +++ b/lib/utils/isAdmin.ts @@ -0,0 +1,6 @@ +import prisma from "@calcom/prisma"; + +export const isAdminGuard = async (userId: number) => { + const user = await prisma.user.findUnique({ where: { id: userId } }); + return user?.role === "ADMIN"; +}; diff --git a/lib/validations/user.ts b/lib/validations/user.ts index d78d8e23b2..3955caabd4 100644 --- a/lib/validations/user.ts +++ b/lib/validations/user.ts @@ -85,19 +85,45 @@ const schemaUserEditParams = z.object({ bufferTime: z.number().min(0).max(86400).optional(), startTime: z.number().min(0).max(86400).optional(), endTime: z.number().min(0).max(86400).optional(), - theme: z.nativeEnum(theme).optional(), + theme: z.nativeEnum(theme).optional().nullable(), timeFormat: z.nativeEnum(timeFormat).optional(), defaultScheduleId: z .number() .refine((id: number) => id > 0) - .optional(), + .optional() + .nullable(), + locale: z.nativeEnum(locales).optional().nullable(), + metadata: jsonSchema.or(z.null()), +}); + +// @note: These are the values that are editable via PATCH method on the user Model, +// merging both BaseBodyParams with RequiredParams, and omiting whatever we want at the end. + +const schemaUserCreateParams = z.object({ + email: z.string().email(), + weekStart: z.nativeEnum(weekdays).optional(), + brandColor: z.string().min(4).max(9).regex(/^#/).optional(), + darkBrandColor: z.string().min(4).max(9).regex(/^#/).optional(), + timeZone: timeZone.optional(), + bufferTime: z.number().min(0).max(86400).optional(), + startTime: z.number().min(0).max(86400).optional(), + endTime: z.number().min(0).max(86400).optional(), + theme: z.nativeEnum(theme).optional().nullable(), + timeFormat: z.nativeEnum(timeFormat).optional(), + defaultScheduleId: z + .number() + .refine((id: number) => id > 0) + .optional() + .nullable(), locale: z.nativeEnum(locales).optional(), metadata: jsonSchema, + createdDate: z.string().or(z.date()).optional(), }); // @note: These are the values that are editable via PATCH method on the user Model, // merging both BaseBodyParams with RequiredParams, and omiting whatever we want at the end. export const schemaUserEditBodyParams = schemaUserBaseBodyParams.merge(schemaUserEditParams).omit({}); +export const schemaUserCreateBodyParams = schemaUserBaseBodyParams.merge(schemaUserCreateParams).omit({}); // @note: These are the values that are always returned when reading a user export const schemaUserReadPublic = User.pick({ @@ -124,4 +150,4 @@ export const schemaUserReadPublic = User.pick({ createdDate: true, verified: true, invitedTo: true, -}); +}).merge(schemaUserEditBodyParams); diff --git a/pages/api/users/[id].ts b/pages/api/users/[id].ts index 7539d513b8..65b864c652 100644 --- a/pages/api/users/[id].ts +++ b/pages/api/users/[id].ts @@ -4,6 +4,7 @@ import prisma from "@calcom/prisma"; import { withMiddleware } from "@lib/helpers/withMiddleware"; import type { UserResponse } from "@lib/types"; +import { isAdminGuard } from "@lib/utils/isAdmin"; import { schemaQueryIdParseInt, withValidQueryIdTransformParseInt, @@ -20,8 +21,11 @@ export async function userById( res.status(400).json({ message: "Your query was invalid" }); return; } - if (safeQuery.data.id !== userId) res.status(401).json({ message: "Unauthorized" }); - else { + const isAdmin = await isAdminGuard(userId); + // Here we only check for ownership of the user if the user is not admin, otherwise we let ADMIN's edit any user + if (!isAdmin) { + if (safeQuery.data.id !== userId) res.status(401).json({ message: "Unauthorized" }); + } else { switch (method) { case "GET": /** diff --git a/pages/api/users/index.ts b/pages/api/users/index.ts index 9f80a421d2..122892f2e1 100644 --- a/pages/api/users/index.ts +++ b/pages/api/users/index.ts @@ -4,7 +4,8 @@ import prisma from "@calcom/prisma"; import { withMiddleware } from "@lib/helpers/withMiddleware"; import { UserResponse, UsersResponse } from "@lib/types"; -import { schemaUserReadPublic } from "@lib/validations/user"; +import { isAdminGuard } from "@lib/utils/isAdmin"; +import { schemaUserReadPublic, schemaUserCreateBodyParams } from "@lib/validations/user"; /** * @swagger @@ -26,34 +27,35 @@ async function getAllorCreateUser( { userId, method, body }: NextApiRequest, res: NextApiResponse ) { + const isAdmin = await isAdminGuard(userId); if (method === "GET") { - 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 - (error: Error) => - res.status(404).json({ - message: "No Users were found", - error, - }); + if (!isAdmin) { + 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 { + 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 (!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 }); + } } - // else if (method === "POST") { - // const isAdmin = await prisma.user - // .findUnique({ where: { id: userId } }) - // .then((user) => user?.role === "ADMIN"); - // if (!isAdmin) res.status(401).json({ message: "You are not authorized" }); - // else { - // const user = await prisma.user.create({ - // data: schemaUserReadPublic.parse(body), - // }); - // res.status(201).json({ user }); - // } - // } } -// No POST endpoint for users for now as a regular user you're expected to signup. export default withMiddleware("HTTP_GET_OR_POST")(getAllorCreateUser); From 4eccc8a74befcc0daf05f5148be3141f3f419541 Mon Sep 17 00:00:00 2001 From: Agusti Fernandez Pardo Date: Tue, 31 May 2022 18:42:20 +0200 Subject: [PATCH 2/4] fix: build error jsonSchema metadata not nullable --- lib/validations/user.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/validations/user.ts b/lib/validations/user.ts index 3955caabd4..528bc2e292 100644 --- a/lib/validations/user.ts +++ b/lib/validations/user.ts @@ -93,7 +93,7 @@ const schemaUserEditParams = z.object({ .optional() .nullable(), locale: z.nativeEnum(locales).optional().nullable(), - metadata: jsonSchema.or(z.null()), + metadata: jsonSchema, }); // @note: These are the values that are editable via PATCH method on the user Model, From 187d5f2b1003f222d4224db1381e51e6da59dd5e Mon Sep 17 00:00:00 2001 From: Agusti Fernandez Pardo Date: Tue, 31 May 2022 18:53:41 +0200 Subject: [PATCH 3/4] docs: add some comments --- pages/api/users/index.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/pages/api/users/index.ts b/pages/api/users/index.ts index 122892f2e1..3839ae21b6 100644 --- a/pages/api/users/index.ts +++ b/pages/api/users/index.ts @@ -30,19 +30,18 @@ async function getAllorCreateUser( const isAdmin = await isAdminGuard(userId); if (method === "GET") { if (!isAdmin) { - const data = await prisma.user.findMany({ - where: { - id: userId, - }, - }); + // 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); From 7f42cc84793fd59808a030aedcc44079c1315565 Mon Sep 17 00:00:00 2001 From: Agusti Fernandez Pardo Date: Tue, 31 May 2022 20:46:09 +0200 Subject: [PATCH 4/4] fix: improve admin check using enum instead of string --- lib/utils/isAdmin.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/utils/isAdmin.ts b/lib/utils/isAdmin.ts index 1026b8deb1..c97628b46d 100644 --- a/lib/utils/isAdmin.ts +++ b/lib/utils/isAdmin.ts @@ -1,6 +1,7 @@ import prisma from "@calcom/prisma"; +import { UserPermissionRole } from "@calcom/prisma/client"; export const isAdminGuard = async (userId: number) => { const user = await prisma.user.findUnique({ where: { id: userId } }); - return user?.role === "ADMIN"; + return user?.role === UserPermissionRole.ADMIN; };