From 3cab37ed8d86418dde500a205a4b7b8a41c8797d Mon Sep 17 00:00:00 2001 From: Alex van Andel Date: Tue, 15 Aug 2023 20:18:25 +0100 Subject: [PATCH] feat: api return schedule (#10769) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Leo Giovanetti Co-authored-by: Omar López --- apps/api/lib/validations/event-type.ts | 1 + apps/api/pages/api/event-types/[id]/_get.ts | 42 ++++++++- apps/api/pages/api/event-types/_get.ts | 89 +++++++++++++++---- .../api/teams/[teamId]/event-types/_get.ts | 2 +- 4 files changed, 112 insertions(+), 22 deletions(-) diff --git a/apps/api/lib/validations/event-type.ts b/apps/api/lib/validations/event-type.ts index 13c16022a2..ffe4cc4bed 100644 --- a/apps/api/lib/validations/event-type.ts +++ b/apps/api/lib/validations/event-type.ts @@ -98,6 +98,7 @@ export const schemaEventTypeReadPublic = EventType.pick({ position: true, userId: true, teamId: true, + scheduleId: true, eventName: true, timeZone: true, periodType: true, diff --git a/apps/api/pages/api/event-types/[id]/_get.ts b/apps/api/pages/api/event-types/[id]/_get.ts index 74d9d14456..59c3c06786 100644 --- a/apps/api/pages/api/event-types/[id]/_get.ts +++ b/apps/api/pages/api/event-types/[id]/_get.ts @@ -1,9 +1,11 @@ import type { NextApiRequest } from "next"; +import { HttpError } from "@calcom/lib/http-error"; import { defaultResponder } from "@calcom/lib/server"; import { schemaEventTypeReadPublic } from "~/lib/validations/event-type"; import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt"; +import { checkPermissions as canAccessTeamEventOrThrow } from "~/pages/api/teams/[teamId]/_auth-middleware"; import getCalLink from "../_utils/getCalLink"; @@ -42,7 +44,8 @@ import getCalLink from "../_utils/getCalLink"; export async function getHandler(req: NextApiRequest) { const { prisma, query } = req; const { id } = schemaQueryIdParseInt.parse(query); - const event_type = await prisma.eventType.findUnique({ + + const eventType = await prisma.eventType.findUnique({ where: { id }, include: { customInputs: true, @@ -51,10 +54,43 @@ export async function getHandler(req: NextApiRequest) { owner: { select: { username: true, id: true } }, }, }); + await checkPermissions(req, eventType); - const link = event_type ? getCalLink(event_type) : null; + const link = eventType ? getCalLink(eventType) : null; + // user.defaultScheduleId doesn't work the same for team events. + if (!eventType?.scheduleId && eventType?.userId && !eventType?.teamId) { + const user = await prisma.user.findUniqueOrThrow({ + where: { + id: eventType.userId, + }, + select: { + defaultScheduleId: true, + }, + }); + eventType.scheduleId = user.defaultScheduleId; + } - return { event_type: schemaEventTypeReadPublic.parse({ ...event_type, link }) }; + // TODO: eventType when not found should be a 404 + // but API consumers may depend on the {} behaviour. + return { event_type: schemaEventTypeReadPublic.parse({ ...eventType, link }) }; +} + +type BaseEventTypeCheckPermissions = { + userId: number | null; + teamId: number | null; +}; + +async function checkPermissions( + req: NextApiRequest, + eventType: (T & Partial>) | null +) { + if (req.isAdmin) return true; + if (eventType?.teamId) { + req.query.teamId = String(eventType.teamId); + await canAccessTeamEventOrThrow(req, "MEMBER"); + } + if (eventType?.userId === req.userId) return true; // is owner. + throw new HttpError({ statusCode: 403, message: "Forbidden" }); } export default defaultResponder(getHandler); diff --git a/apps/api/pages/api/event-types/_get.ts b/apps/api/pages/api/event-types/_get.ts index 4a274973a9..6b03ece7f4 100644 --- a/apps/api/pages/api/event-types/_get.ts +++ b/apps/api/pages/api/event-types/_get.ts @@ -1,8 +1,8 @@ -import type { Prisma } from "@prisma/client"; import type { NextApiRequest } from "next"; import { HttpError } from "@calcom/lib/http-error"; import { defaultResponder } from "@calcom/lib/server"; +import type { PrismaClient } from "@calcom/prisma/client"; import { schemaEventTypeReadPublic } from "~/lib/validations/event-type"; import { schemaQuerySingleOrMultipleUserIds } from "~/lib/validations/shared/queryUserId"; @@ -35,20 +35,12 @@ import getCalLink from "./_utils/getCalLink"; * description: No event types were found */ async function getHandler(req: NextApiRequest) { - const { userId, isAdmin, prisma } = req; - - const args: Prisma.EventTypeFindManyArgs = { - where: { userId }, - }; - /** Only admins can query other users */ - if (!isAdmin && req.query.userId) throw new HttpError({ statusCode: 401, message: "ADMIN required" }); - if (isAdmin && req.query.userId) { - const query = schemaQuerySingleOrMultipleUserIds.parse(req.query); - const userIds = Array.isArray(query.userId) ? query.userId : [query.userId || userId]; - args.where = { userId: { in: userIds } }; - } + const { userId, prisma } = req; + const userIds = req.query.userId ? extractUserIdsFromQuery(req) : [userId]; const data = await prisma.eventType.findMany({ - ...args, + where: { + userId: { in: userIds }, + }, include: { customInputs: true, team: { select: { slug: true } }, @@ -56,13 +48,74 @@ async function getHandler(req: NextApiRequest) { owner: { select: { username: true, id: true } }, }, }); + // this really should return [], but backwards compatibility.. if (data.length === 0) new HttpError({ statusCode: 404, message: "No event types were found" }); return { - event_types: data.map((eventType) => { - const link = getCalLink(eventType); - return schemaEventTypeReadPublic.parse({ ...eventType, link }); - }), + event_types: (await defaultScheduleId<(typeof data)[number]>({ eventTypes: data, prisma, userIds })).map( + (eventType) => { + const link = getCalLink(eventType); + return schemaEventTypeReadPublic.parse({ ...eventType, link }); + } + ), }; } +// TODO: Extract & reuse. +function extractUserIdsFromQuery({ isAdmin, query }: NextApiRequest) { + /** Guard: Only admins can query other users */ + if (!isAdmin) { + throw new HttpError({ statusCode: 401, message: "ADMIN required" }); + } + const { userId: userIdOrUserIds } = schemaQuerySingleOrMultipleUserIds.parse(query); + return Array.isArray(userIdOrUserIds) ? userIdOrUserIds : [userIdOrUserIds]; +} + +type DefaultScheduleIdEventTypeBase = { + scheduleId: number | null; + userId: number | null; +}; +// If an eventType is given w/o a scheduleId +// Then we associate the default user schedule id to the eventType +async function defaultScheduleId({ + prisma, + eventTypes, + userIds, +}: { + prisma: PrismaClient; + eventTypes: (T & Partial>)[]; + userIds: number[]; +}) { + // there is no event types without a scheduleId, skip the user query + if (eventTypes.every((eventType) => eventType.scheduleId)) return eventTypes; + + const users = await prisma.user.findMany({ + where: { + id: { + in: userIds, + }, + }, + select: { + id: true, + defaultScheduleId: true, + }, + }); + + if (!users.length) { + return eventTypes; + } + + const defaultScheduleIds = users.reduce((result, user) => { + result[user.id] = user.defaultScheduleId; + return result; + }, {} as { [x: number]: number | null }); + + return eventTypes.map((eventType) => { + // realistically never happens, userId should't be null on personal event types. + if (!eventType.userId) return eventType; + return { + ...eventType, + scheduleId: eventType.scheduleId || defaultScheduleIds[eventType.userId], + }; + }); +} export default defaultResponder(getHandler); diff --git a/apps/api/pages/api/teams/[teamId]/event-types/_get.ts b/apps/api/pages/api/teams/[teamId]/event-types/_get.ts index 38d1a6d425..8e75f132ab 100644 --- a/apps/api/pages/api/teams/[teamId]/event-types/_get.ts +++ b/apps/api/pages/api/teams/[teamId]/event-types/_get.ts @@ -59,7 +59,7 @@ async function getHandler(req: NextApiRequest) { }; const data = await prisma.eventType.findMany(args); - return { event_types: data.map((attendee) => schemaEventTypeReadPublic.parse(attendee)) }; + return { event_types: data.map((eventType) => schemaEventTypeReadPublic.parse(eventType)) }; } export default defaultResponder(getHandler);