diff --git a/lib/validations/availability.ts b/lib/validations/availability.ts index d55cfb74fa..20e3c61c94 100644 --- a/lib/validations/availability.ts +++ b/lib/validations/availability.ts @@ -3,13 +3,21 @@ import { z } from "zod"; import { _AvailabilityModel as Availability } from "@calcom/prisma/zod"; -export const schemaAvailabilityBaseBodyParams = Availability.omit({ id: true }).partial(); +export const schemaAvailabilityBaseBodyParams = Availability.pick({ + startTime: true, + endTime: true, + date: true, + scheduleId: true, + days: true, +}).partial(); export const schemaAvailabilityPublic = Availability.omit({}); const schemaAvailabilityRequiredParams = z.object({ - startTime: z.string(), - endTime: z.string(), + startTime: z.date().or(z.string()).optional(), + endTime: z.date().or(z.string()).optional(), + days: z.array(z.number()).optional(), + eventTypeId: z.number().optional(), }); export const schemaAvailabilityBodyParams = schemaAvailabilityBaseBodyParams.merge( diff --git a/lib/validations/user.ts b/lib/validations/user.ts index ec3f7a2247..7452c6ad44 100644 --- a/lib/validations/user.ts +++ b/lib/validations/user.ts @@ -1,36 +1,128 @@ import { withValidation } from "next-validations"; +import * as tzdb from "tzdata"; import { z } from "zod"; import { _UserModel as User } from "@calcom/prisma/zod"; -export const schemaUserBaseBodyParams = User.omit({ - id: true, - createdAt: true, - password: true, - twoFactorEnabled: true, - twoFactorSecret: true, +// @note: These are the ONLY values allowed as weekStart. So user don't introduce bad data. +enum weekdays { + MONDAY = "Monday", + TUESDAY = "Tuesday", + WEDNESDAY = "Wednesday", + THURSDAY = "Thursday", + FRIDAY = "Friday", + SATURDAY = "Saturday", + SUNDAY = "Sunday", +} + +// @note: extracted from apps/web/next-i18next.config.js, update if new locales. +enum locales { + EN = "en", + FR = "fr", + IT = "it", + RU = "ru", + ES = "es", + DE = "de", + PT = "pt", + RO = "ro", + NL = "nl", + PT_BR = "pt-BR", + ES_419 = "es-419", + KO = "ko", + JA = "ja", + PL = "pl", + AR = "ar", + IW = "iw", + ZH_CN = "zh-CN", + ZH_TW = "zh-TW", + CS = "cs", + SR = "sr", + SV = "sv", + VI = "vi", +} +enum theme { + DARK = "dark", + LIGHT = "light", +} + +enum timeFormat { + TWELVE = 12, + TWENTY_FOUR = 24, +} + +// @note: These are the values that are editable via PATCH method on the user Model +export const schemaUserBaseBodyParams = User.pick({ + name: true, + bio: true, + timeZone: true, + weekStart: true, + endTime: true, + bufferTime: true, + theme: true, + defaultScheduleId: true, + locale: true, + timeFormat: true, + brandColor: true, + darkBrandColor: true, + allowDynamicBooking: true, + away: true, + // @note: disallowing avatar changes via API for now. We can add it later if needed. User should upload image via UI. + // avatar: true, }).partial(); +// @note: partial() is used to allow for the user to edit only the fields they want to edit making all optional, +// if want to make any required do it in the schemaRequiredParams +// Here we can both require or not (adding optional or nullish) and also rewrite validations for any value +// for example making weekStart only accept weekdays as input const schemaUserRequiredParams = z.object({ - email: z.string().email(), + weekStart: z.nativeEnum(weekdays).optional(), + brandColor: z.string().min(4).max(9).regex(/^#/).optional(), + timeZone: z + .string() + // @note: This is a custom validation that checks if the timezone is valid and exists in the tzdb library + .refine((tz: string) => Object.keys(tzdb.zones).includes(tz)) + .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(), + timeFormat: z.nativeEnum(timeFormat).optional(), + defaultScheduleId: z + .number() + .refine((id: number) => id > 0) + .optional(), + locale: z.nativeEnum(locales), }); -export const schemaUserBodyParams = schemaUserBaseBodyParams.merge(schemaUserRequiredParams); +// @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(schemaUserRequiredParams).omit({}); -export const schemaUserPublic = User.omit({ - identityProvider: true, - identityProviderId: true, - plan: true, - metadata: true, - password: true, - twoFactorEnabled: true, - twoFactorSecret: true, - trialEndsAt: true, - completedOnboarding: true, +// @note: These are the values that are always returned when reading a user +export const schemaUserReadPublic = User.pick({ + id: true, + username: true, + name: true, + email: true, + emailVerified: true, + bio: true, + avatar: true, + timeZone: true, + weekStart: true, + endTime: true, + bufferTime: true, + theme: true, + defaultScheduleId: true, + locale: true, + timeFormat: true, + brandColor: true, + darkBrandColor: true, + allowDynamicBooking: true, + away: true, + createdDate: true, + verified: true, + invitedTo: true, }); -export const withValidUser = withValidation({ - schema: schemaUserBodyParams, - type: "Zod", - mode: "body", -}); +// @note: This is the validation for the PATCH method on the user Model. Not used for now. +export const withValidUser = withValidation({ schema: schemaUserEditBodyParams, type: "Zod", mode: "body" }); diff --git a/package.json b/package.json index 5739292e02..0c1cb9556e 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "next-swagger-doc": "^0.2.1", "next-transpile-modules": "^9.0.0", "next-validations": "^0.1.11", - "typescript": "^4.6.3" + "typescript": "^4.6.3", + "tzdata": "^1.0.30" } } diff --git a/pages/api/attendees/[id].ts b/pages/api/attendees/[id].ts index 6931c5d6df..0c456ea6e4 100644 --- a/pages/api/attendees/[id].ts +++ b/pages/api/attendees/[id].ts @@ -86,13 +86,13 @@ import { * description: Authorization information is missing or invalid. */ export async function attendeeById(req: NextApiRequest, res: NextApiResponse) { - const { method, query, body } = req; + const { method, query, body, userId } = req; const safeQuery = schemaQueryIdParseInt.safeParse(query); const safeBody = schemaAttendeeBodyParams.safeParse(body); if (!safeQuery.success) { + res.status(400).json({ error: safeQuery.error }); throw new Error("Invalid request query", safeQuery.error); } - const userId = req.userId; const userBookings = await prisma.booking.findMany({ where: { userId }, include: { attendees: true }, @@ -118,6 +118,7 @@ export async function attendeeById(req: NextApiRequest, res: NextApiResponse) { const { method, query, body } = req; - const safeQuery = schemaQueryIdParseInt.safeParse(query); const safeBody = schemaAvailabilityBodyParams.safeParse(body); + const safeQuery = schemaQueryIdParseInt.safeParse(query); if (!safeQuery.success) throw new Error("Invalid request query", safeQuery.error); const userId = req.userId; const data = await prisma.availability.findMany({ where: { userId } }); @@ -113,7 +113,15 @@ export async function availabilityById(req: NextApiRequest, res: NextApiResponse break; case "PATCH": + if (!safeBody.success) throw new Error("Invalid request body"); + const userEventTypes = await prisma.eventType.findMany({ where: { userId } }); + const userEventTypesIds = userEventTypes.map((event) => event.id); + if (safeBody.data.eventTypeId && !userEventTypesIds.includes(safeBody.data.eventTypeId)) { + res.status(401).json({ + message: `Bad request. You're not the owner of eventTypeId: ${safeBody.data.eventTypeId}`, + }); + } await prisma.availability .update({ where: { id: safeQuery.data.id }, @@ -121,9 +129,13 @@ export async function availabilityById(req: NextApiRequest, res: NextApiResponse }) .then((data) => schemaAvailabilityPublic.parse(data)) .then((availability) => res.status(200).json({ availability })) - .catch((error: Error) => - res.status(404).json({ message: `Availability with id: ${safeQuery.data.id} not found`, error }) - ); + .catch((error: Error) => { + console.log(error); + res.status(404).json({ + message: `Availability with id: ${safeQuery.data.id} not found`, + error, + }); + }); break; case "DELETE": diff --git a/pages/api/availabilities/index.ts b/pages/api/availabilities/index.ts index a6310059f7..40923606e6 100644 --- a/pages/api/availabilities/index.ts +++ b/pages/api/availabilities/index.ts @@ -59,10 +59,12 @@ async function createOrlistAllAvailabilities( error, }); } else if (method === "POST") { + console.log(req.body); const safe = schemaAvailabilityBodyParams.safeParse(req.body); if (!safe.success) throw new Error("Invalid request body"); const data = await prisma.availability.create({ data: { ...safe.data, userId } }); + console.log(data); const availability = schemaAvailabilityPublic.parse(data); if (availability) res.status(201).json({ availability, message: "Availability created successfully" }); diff --git a/pages/api/schedules/index.ts b/pages/api/schedules/index.ts index bbdd1c8dbc..bd9a38a9ef 100644 --- a/pages/api/schedules/index.ts +++ b/pages/api/schedules/index.ts @@ -70,4 +70,4 @@ async function createOrlistAllSchedules( } else res.status(405).json({ message: `Method ${method} not allowed` }); } -export default withMiddleware("HTTP_GET_OR_POST")(withValidSchedule(createOrlistAllSchedules)); +export default withMiddleware("HTTP_GET_OR_POST")(createOrlistAllSchedules); diff --git a/pages/api/users/[id].ts b/pages/api/users/[id].ts index 58fb826521..e7e35d5e6e 100644 --- a/pages/api/users/[id].ts +++ b/pages/api/users/[id].ts @@ -8,7 +8,7 @@ import { schemaQueryIdParseInt, withValidQueryIdTransformParseInt, } from "@lib/validations/shared/queryIdTransformParseInt"; -import { schemaUserBodyParams, schemaUserPublic } from "@lib/validations/user"; +import { schemaUserEditBodyParams, schemaUserReadPublic, withValidUser } from "@lib/validations/user"; /** * @swagger @@ -39,12 +39,10 @@ import { schemaUserBodyParams, schemaUserPublic } from "@lib/validations/user"; * - application/json * parameters: * - in: body - * name: user - * description: The user to edit + * name: name + * description: The users full name * schema: - * type: object - * $ref: '#/components/schemas/User' - * required: true + * type: string * - in: path * name: id * schema: @@ -85,19 +83,18 @@ import { schemaUserBodyParams, schemaUserPublic } from "@lib/validations/user"; * 401: * description: Authorization information is missing or invalid. */ -export async function userById(req: NextApiRequest, res: NextApiResponse) { - const { method, query, body } = req; +export async function userById(req: NextApiRequest, res: NextApiResponse) { + const { method, query, body, userId } = req; const safeQuery = schemaQueryIdParseInt.safeParse(query); - const safeBody = schemaUserBodyParams.safeParse(body); + console.log(body); if (!safeQuery.success) throw new Error("Invalid request query", safeQuery.error); - const userId = req.userId; if (safeQuery.data.id !== userId) res.status(401).json({ message: "Unauthorized" }); else { switch (method) { case "GET": await prisma.user .findUnique({ where: { id: safeQuery.data.id } }) - .then((data) => schemaUserPublic.parse(data)) + .then((data) => schemaUserReadPublic.parse(data)) .then((user) => res.status(200).json({ user })) .catch((error: Error) => res.status(404).json({ message: `User with id: ${safeQuery.data.id} not found`, error }) @@ -105,13 +102,29 @@ export async function userById(req: NextApiRequest, res: NextApiResponse schedule.id); + // @note: here we make sure user can only make as default his own scheudles + if (!userSchedulesIds.includes(Number(safeBody?.data?.defaultScheduleId))) { + res.status(400).json({ + message: "Bad request", + error: "Invalid default schedule id", + }); + throw new Error("Invalid request body value: defaultScheduleId"); + } await prisma.user .update({ - where: { id: safeQuery.data.id }, + where: { id: userId }, data: safeBody.data, }) - .then((data) => schemaUserPublic.parse(data)) + .then((data) => schemaUserReadPublic.parse(data)) .then((user) => res.status(200).json({ user })) .catch((error: Error) => res.status(404).json({ message: `User with id: ${safeQuery.data.id} not found`, error }) diff --git a/pages/api/users/index.ts b/pages/api/users/index.ts index 73f2a46eb6..dfea9f1ff6 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 { schemaUserPublic } from "@lib/validations/user"; +import { schemaUserReadPublic } from "@lib/validations/user"; /** * @swagger @@ -30,7 +30,7 @@ async function allUsers(req: NextApiRequest, res: NextApiResponse id: userId, }, }); - const users = data.map((user) => schemaUserPublic.parse(user)); + const users = data.map((user) => schemaUserReadPublic.parse(user)); if (users) res.status(200).json({ users }); else (error: Error) =>