diff --git a/apps/web/components/booking/BookingListItem.tsx b/apps/web/components/booking/BookingListItem.tsx index 0d72d910c7..5d6bb9272c 100644 --- a/apps/web/components/booking/BookingListItem.tsx +++ b/apps/web/components/booking/BookingListItem.tsx @@ -598,9 +598,7 @@ function BookingListItem(booking: BookingItemProps) { {isUpcoming && !isCancelled ? ( <> - {isPending && (userId === booking.user?.id || booking.isUserTeamAdminOrOwner) && ( - - )} + {isPending && } {isConfirmed && } {isRejected &&
{t("rejected")}
} diff --git a/packages/trpc/server/routers/viewer/bookings/confirm.handler.ts b/packages/trpc/server/routers/viewer/bookings/confirm.handler.ts index 1bd0203b64..a34f1946cf 100644 --- a/packages/trpc/server/routers/viewer/bookings/confirm.handler.ts +++ b/packages/trpc/server/routers/viewer/bookings/confirm.handler.ts @@ -17,7 +17,12 @@ import { getTranslation } from "@calcom/lib/server"; import { getUsersCredentials } from "@calcom/lib/server/getUsersCredentials"; import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat"; import { prisma } from "@calcom/prisma"; -import { BookingStatus, MembershipRole, SchedulingType, WebhookTriggerEvents } from "@calcom/prisma/enums"; +import { + BookingStatus, + MembershipRole, + WebhookTriggerEvents, + UserPermissionRole, +} from "@calcom/prisma/enums"; import type { EventTypeMetadata } from "@calcom/prisma/zod-utils"; import type { CalendarEvent } from "@calcom/types/Calendar"; import type { IAbstractPaymentService, PaymentApp } from "@calcom/types/PaymentService"; @@ -77,7 +82,6 @@ export const confirmHandler = async ({ ctx, input }: ConfirmOptions) => { id: true, name: true, parentId: true, - members: true, }, }, workflows: { @@ -109,30 +113,13 @@ export const confirmHandler = async ({ ctx, input }: ConfirmOptions) => { }, }); - if (booking.userId !== user.id && booking.eventTypeId) { - // Only query database when it is explicitly required. - const eventType = await prisma.eventType.findFirst({ - where: { - id: booking.eventTypeId, - schedulingType: SchedulingType.COLLECTIVE, - }, - select: { - users: { - select: { - id: true, - }, - }, - }, - }); - - const membership = booking.eventType?.team?.members.find((membership) => membership.userId === user.id); - const isTeamAdminOrOwner = - membership?.role === MembershipRole.OWNER || membership?.role === MembershipRole.ADMIN; - - if (eventType && !eventType.users.find((user) => booking.userId === user.id) && !isTeamAdminOrOwner) { - throw new TRPCError({ code: "UNAUTHORIZED", message: "UNAUTHORIZED" }); - } - } + await checkIfUserIsAuthorizedToConfirmBooking({ + eventTypeId: booking.eventTypeId, + loggedInUserId: user.id, + teamId: booking.eventType?.teamId, + bookingUserId: booking.userId, + userRole: user.role, + }); // Do not move this before authorization check. // This is done to avoid exposing extra information to the requester. @@ -425,3 +412,61 @@ export const confirmHandler = async ({ ctx, input }: ConfirmOptions) => { return { message, status }; }; + +const checkIfUserIsAuthorizedToConfirmBooking = async ({ + eventTypeId, + loggedInUserId, + teamId, + bookingUserId, + userRole, +}: { + eventTypeId: number | null; + loggedInUserId: number; + teamId?: number | null; + bookingUserId: number | null; + userRole: string; +}): Promise => { + // check system wide admin + if (userRole === UserPermissionRole.ADMIN) return; + + // Check if the user is the owner of the event type + if (bookingUserId === loggedInUserId) return; + + // Check if user is associated with the event type + if (eventTypeId) { + const [loggedInUserAsHostOfEventType, loggedInUserAsUserOfEventType] = await Promise.all([ + prisma.eventType.findUnique({ + where: { + id: eventTypeId, + hosts: { some: { userId: loggedInUserId } }, + }, + select: { id: true }, + }), + prisma.eventType.findUnique({ + where: { + id: eventTypeId, + users: { some: { id: loggedInUserId } }, + }, + select: { id: true }, + }), + ]); + + if (loggedInUserAsHostOfEventType || loggedInUserAsUserOfEventType) return; + } + + // Check if the user is an admin/owner of the team the booking belongs to + if (teamId) { + const membership = await prisma.membership.findFirst({ + where: { + userId: loggedInUserId, + teamId: teamId, + role: { + in: [MembershipRole.OWNER, MembershipRole.ADMIN], + }, + }, + }); + if (membership) return; + } + + throw new TRPCError({ code: "UNAUTHORIZED", message: "User is not authorized to confirm this booking" }); +}; diff --git a/packages/trpc/server/routers/viewer/bookings/get.handler.ts b/packages/trpc/server/routers/viewer/bookings/get.handler.ts index a0cae887fc..ac00188940 100644 --- a/packages/trpc/server/routers/viewer/bookings/get.handler.ts +++ b/packages/trpc/server/routers/viewer/bookings/get.handler.ts @@ -3,7 +3,7 @@ import getAllUserBookings from "@calcom/lib/bookings/getAllUserBookings"; import type { PrismaClient } from "@calcom/prisma"; import { bookingMinimalSelect } from "@calcom/prisma"; import type { Prisma } from "@calcom/prisma/client"; -import { MembershipRole, type BookingStatus } from "@calcom/prisma/enums"; +import { type BookingStatus } from "@calcom/prisma/enums"; import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils"; import type { TrpcSessionUser } from "../../../trpc"; @@ -229,7 +229,6 @@ export async function getBookings({ select: { id: true, name: true, - members: true, }, }, }, @@ -427,41 +426,39 @@ export async function getBookings({ // Now enrich bookings with relation data. We could have queried the relation data along with the bookings, but that would cause unnecessary queries to the database. // Because Prisma is also going to query the select relation data sequentially, we are fine querying it separately here as it would be just 1 query instead of 4 - const bookings = ( - await prisma.booking.findMany({ - where: { - id: { - in: plainBookings.map((booking) => booking.id), + + const bookings = await Promise.all( + ( + await prisma.booking.findMany({ + where: { + id: { + in: plainBookings.map((booking) => booking.id), + }, }, - }, - select: bookingSelect, - // We need to get the sorted bookings here as well because plainBookings array is not correctly sorted - orderBy, + select: bookingSelect, + // We need to get the sorted bookings here as well because plainBookings array is not correctly sorted + orderBy, + }) + ).map(async (booking) => { + // If seats are enabled and the event is not set to show attendees, filter out attendees that are not the current user + if (booking.seatsReferences.length && !booking.eventType?.seatsShowAttendees) { + booking.attendees = booking.attendees.filter((attendee) => attendee.email === user.email); + } + + return { + ...booking, + eventType: { + ...booking.eventType, + recurringEvent: parseRecurringEvent(booking.eventType?.recurringEvent), + eventTypeColor: parseEventTypeColor(booking.eventType?.eventTypeColor), + price: booking.eventType?.price || 0, + currency: booking.eventType?.currency || "usd", + metadata: EventTypeMetaDataSchema.parse(booking.eventType?.metadata || {}), + }, + startTime: booking.startTime.toISOString(), + endTime: booking.endTime.toISOString(), + }; }) - ).map((booking) => { - // If seats are enabled and the event is not set to show attendees, filter out attendees that are not the current user - if (booking.seatsReferences.length && !booking.eventType?.seatsShowAttendees) { - booking.attendees = booking.attendees.filter((attendee) => attendee.email === user.email); - } - - const membership = booking.eventType?.team?.members.find((membership) => membership.userId === user.id); - const isUserTeamAdminOrOwner = - membership?.role === MembershipRole.OWNER || membership?.role === MembershipRole.ADMIN; - - return { - ...booking, - eventType: { - ...booking.eventType, - recurringEvent: parseRecurringEvent(booking.eventType?.recurringEvent), - eventTypeColor: parseEventTypeColor(booking.eventType?.eventTypeColor), - price: booking.eventType?.price || 0, - currency: booking.eventType?.currency || "usd", - metadata: EventTypeMetaDataSchema.parse(booking.eventType?.metadata || {}), - }, - startTime: booking.startTime.toISOString(), - endTime: booking.endTime.toISOString(), - isUserTeamAdminOrOwner, - }; - }); + ); return { bookings, recurringInfo }; }