diff --git a/apps/api/v2/src/ee/bookings/2024-04-15/controllers/bookings.controller.ts b/apps/api/v2/src/ee/bookings/2024-04-15/controllers/bookings.controller.ts index 5ebf59c850eee4..d88ecce3d4c61d 100644 --- a/apps/api/v2/src/ee/bookings/2024-04-15/controllers/bookings.controller.ts +++ b/apps/api/v2/src/ee/bookings/2024-04-15/controllers/bookings.controller.ts @@ -13,9 +13,14 @@ import { InstantBookingCreateService } from "@/lib/services/instant-booking-crea import { RecurringBookingService } from "@/lib/services/recurring-booking.service"; import { RegularBookingService } from "@/lib/services/regular-booking.service"; import { ApiKeysRepository } from "@/modules/api-keys/api-keys-repository"; +import { + AuthOptionalUser, + GetOptionalUser, +} from "@/modules/auth/decorators/get-optional-user/get-optional-user.decorator"; import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator"; import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator"; import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard"; +import { OptionalApiAuthGuard } from "@/modules/auth/guards/optional-api-auth/optional-api-auth.guard"; import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard"; import { BillingService } from "@/modules/billing/services/billing.service"; import { KyselyReadService } from "@/modules/kysely/kysely-read.service"; @@ -172,8 +177,12 @@ export class BookingsController_2024_04_15 { } @Get("/:bookingUid/reschedule") - async getBookingForReschedule(@Param("bookingUid") bookingUid: string): Promise> { - const booking = await getBookingForReschedule(bookingUid); + @UseGuards(OptionalApiAuthGuard) + async getBookingForReschedule( + @Param("bookingUid") bookingUid: string, + @GetOptionalUser() user: AuthOptionalUser + ): Promise> { + const booking = await getBookingForReschedule(bookingUid, user?.id); if (!booking) { throw new NotFoundException(`Booking with UID=${bookingUid} does not exist.`); diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.ts b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.ts index 5f9e2704e87e9b..7c15255c8b7ddc 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.ts @@ -290,7 +290,7 @@ export class BookingsController_2024_08_13 { { $ref: getSchemaPath(RescheduleSeatedBookingInput_2024_08_13) }, ], }, - description: `Accepts different types of reschedule booking input: Reschedule Booking (Option 1) or Reschedule Seated Booking (Option 2). + description: `Accepts different types of reschedule booking input: Reschedule Booking (Option 1) or Reschedule Seated Booking (Option 2). If you're rescheduling a seated booking as org admin of booking host, pass booking input for Reschedule Booking (Option 1) along with your access token in the request header. If you are rescheduling a seated booking for an event type with 'show attendees' disabled, then to retrieve attendees in the response either set 'show attendees' to true on event type level or you have to provide an authentication method of event type owner, host, team admin or owner or org admin or owner.`, @@ -328,7 +328,7 @@ export class BookingsController_2024_08_13 { \nCancelling seated bookings: It is possible to cancel specific seat within a booking as an attendee or all of the seats as the host. \n1. As an attendee - provide :bookingUid in the request URL \`/bookings/:bookingUid/cancel\` and seatUid in the request body \`{"seatUid": "123-123-123"}\` . This will remove this particular attendance from the booking. - \n2. As the host - host can cancel booking for all attendees aka for every seat. Provide :bookingUid in the request URL \`/bookings/:bookingUid/cancel\` and cancellationReason in the request body \`{"cancellationReason": "Will travel"}\` and \`Authorization: Bearer token\` request header where token is event type owner (host) credential. This will cancel the booking for all attendees. + \n2. As the host or org admin of host - host can cancel booking for all attendees aka for every seat, this also applies to org admins. Provide :bookingUid in the request URL \`/bookings/:bookingUid/cancel\` and cancellationReason in the request body \`{"cancellationReason": "Will travel"}\` and \`Authorization: Bearer token\` request header where token is event type owner (host) credential. This will cancel the booking for all attendees. \nCancelling recurring seated bookings: For recurring seated bookings it is not possible to cancel all of them with 1 call diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/e2e/managed-user-bookings.e2e-spec.ts b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/e2e/managed-user-bookings.e2e-spec.ts index 5647960365fd9c..d1ed2103f755e0 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/e2e/managed-user-bookings.e2e-spec.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/e2e/managed-user-bookings.e2e-spec.ts @@ -70,6 +70,7 @@ describe("Managed user bookings 2024-08-13", () => { let firstManagedUserEventTypeId: number; let eventTypeRequiresConfirmationId: number; + let seatedEventTypeId: number; const orgAdminManagedUserEmail = `managed-user-bookings-2024-08-13-org-admin-${randomString()}@api.com`; let orgAdminManagedUser: CreateManagedUserData; @@ -309,6 +310,20 @@ describe("Managed user bookings 2024-08-13", () => { eventTypeRequiresConfirmationId = eventTypeRequiresConfirmation.id; }); + it("should create a seated event type for first managed user", async () => { + const seatedEventType = await eventTypesRepositoryFixture.create( + { + title: `managed-user-bookings-seated-event-type-${randomString()}`, + slug: `managed-user-bookings-seated-event-type-${randomString()}`, + length: 30, + seatsPerTimeSlot: 5, + seatsShowAttendees: true, + }, + firstManagedUser.user.id + ); + seatedEventTypeId = seatedEventType.id; + }); + describe("bookings using original emails", () => { it("managed user should be booked by managed user attendee and booking shows up in both users' bookings", async () => { const body: CreateBookingInput_2024_08_13 = { @@ -773,6 +788,107 @@ describe("Managed user bookings 2024-08-13", () => { }); }); + describe("seated booking management by org admin", () => { + let seatedBookingUid: string; + + it("should create a seated booking with multiple attendees", async () => { + const bodyOne: CreateBookingInput_2024_08_13 = { + start: new Date(Date.UTC(2030, 0, 10, 10, 0, 0)).toISOString(), + eventTypeId: seatedEventTypeId, + attendee: { + name: secondManagedUser.user.name!, + email: secondManagedUser.user.email, + timeZone: secondManagedUser.user.timeZone, + language: secondManagedUser.user.locale, + }, + }; + + const responseOne = await request(app.getHttpServer()) + .post("/v2/bookings") + .send(bodyOne) + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .expect(201); + + const responseBodyForFirstAttendee: ApiSuccessResponse = responseOne.body; + expect(responseBodyForFirstAttendee.status).toEqual(SUCCESS_STATUS); + expect(responseBodyForFirstAttendee.data).toBeDefined(); + + const bookingDataForFirstAttendee = responseBodyForFirstAttendee.data as BookingOutput_2024_08_13; + seatedBookingUid = bookingDataForFirstAttendee.uid; + expect(bookingDataForFirstAttendee.attendees).toBeDefined(); + expect(bookingDataForFirstAttendee.attendees.length).toBeGreaterThan(0); + + const bodyTwo: CreateBookingInput_2024_08_13 = { + start: new Date(Date.UTC(2030, 0, 10, 10, 0, 0)).toISOString(), + eventTypeId: seatedEventTypeId, + attendee: { + name: thirdManagedUser.user.name!, + email: thirdManagedUser.user.email, + timeZone: thirdManagedUser.user.timeZone, + language: thirdManagedUser.user.locale, + }, + }; + + const responseTwo = await request(app.getHttpServer()) + .post("/v2/bookings") + .send(bodyTwo) + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .expect(201); + + const responseBodyForSecondAttendee: ApiSuccessResponse = responseTwo.body; + expect(responseBodyForSecondAttendee.status).toEqual(SUCCESS_STATUS); + expect(responseBodyForSecondAttendee.data).toBeDefined(); + + const bookingDataForSecondAttendee = responseBodyForSecondAttendee.data as BookingOutput_2024_08_13; + expect(bookingDataForSecondAttendee.attendees).toBeDefined(); + expect(bookingDataForSecondAttendee.attendees.length).toBeGreaterThan(0); + }); + + it("should allow org admin to reschedule all seats in a seated booking for a managed user", async () => { + const newStartTime = new Date(Date.UTC(2030, 0, 10, 11, 0, 0)).toISOString(); + + const response = await request(app.getHttpServer()) + .post(`/v2/bookings/${seatedBookingUid}/reschedule`) + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .set("Authorization", `Bearer ${orgAdminManagedUser.accessToken}`) + .send({ + start: newStartTime, + }) + .expect(201); + + const responseBody: ApiSuccessResponse = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data).toBeDefined(); + + const bookingData = responseBody.data as BookingOutput_2024_08_13; + expect(bookingData.start).toEqual(newStartTime); + + seatedBookingUid = bookingData.uid; + }); + + it("should allow org admin to cancel all seats in a seated booking for a managed user", async () => { + const response = await request(app.getHttpServer()) + .post(`/v2/bookings/${seatedBookingUid}/cancel`) + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .set("Authorization", `Bearer ${orgAdminManagedUser.accessToken}`) + .send({ + cancellationReason: "Org admin cancelled the booking", + }) + .expect(200); + + const responseBody: GetBookingOutput_2024_08_13 = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data).toBeDefined(); + + const bookingData = responseBody.data as BookingOutput_2024_08_13; + expect(bookingData.status).toEqual("cancelled"); + expect(bookingData.uid).toEqual(seatedBookingUid); + + const cancelledBooking = await bookingsRepositoryFixture.getByUid(seatedBookingUid); + expect(cancelledBooking?.status).toEqual("CANCELLED"); + }); + }); + describe("event type booking requires authentication", () => { let eventTypeRequiringAuthenticationId: number; diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/repositories/bookings.repository.ts b/apps/api/v2/src/ee/bookings/2024-08-13/repositories/bookings.repository.ts index 812f0b580c932d..4c6f74700e564a 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/repositories/bookings.repository.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/repositories/bookings.repository.ts @@ -50,6 +50,20 @@ export class BookingsRepository_2024_08_13 { }); } + async getByUidWithUserIdAndSeatsReferencesCount(bookingUid: string) { + return this.dbRead.prisma.booking.findUnique({ + where: { + uid: bookingUid, + }, + select: { + userId: true, + seatsReferences: { + take: 1, + }, + }, + }); + } + async getByUid(bookingUid: string) { return this.dbRead.prisma.booking.findUnique({ where: { diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts b/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts index de6660d4eff885..aa4c0c82fb0f7a 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts @@ -49,6 +49,7 @@ import { confirmBookingHandler, getCalendarLinks, } from "@calcom/platform-libraries"; +import { PrismaOrgMembershipRepository } from "@calcom/platform-libraries/bookings"; import { CreateBookingInput_2024_08_13, CreateBookingInput, @@ -64,6 +65,7 @@ import { RescheduleBookingInput, CancelBookingInput, } from "@calcom/platform-types"; +import type { RescheduleSeatedBookingInput_2024_08_13 } from "@calcom/platform-types"; import type { PrismaClient } from "@calcom/prisma"; import type { EventType, User, Team } from "@calcom/prisma/client"; @@ -762,16 +764,26 @@ export class BookingsService_2024_08_13 { authUser: AuthOptionalUser ) { try { - await this.canRescheduleBooking(bookingUid); + const isIndividualSeatRequest = this.isRescheduleSeatedBody(body); + const isIndividualSeatReschedule = await this.shouldRescheduleIndividualSeat( + bookingUid, + isIndividualSeatRequest, + authUser + ); + const bookingRequest = await this.inputService.createRescheduleBookingRequest( request, bookingUid, - body + body, + isIndividualSeatReschedule ); + + await this.canRescheduleBooking(bookingUid); + const booking = await this.regularBookingService.createBooking({ bookingData: bookingRequest.body, bookingMeta: { - userId: bookingRequest.userId, + userId: bookingRequest.userId ?? authUser?.id, hostname: bookingRequest.headers?.host || "", platformClientId: bookingRequest.platformClientId, platformRescheduleUrl: bookingRequest.platformRescheduleUrl, @@ -840,6 +852,57 @@ export class BookingsService_2024_08_13 { return booking; } + async shouldRescheduleIndividualSeat( + bookingUid: string, + isIndividualSeatReschedule: boolean, + authUser: AuthOptionalUser + ) { + const booking = await this.bookingsRepository.getByUidWithUserIdAndSeatsReferencesCount(bookingUid); + + if (!booking) { + throw new NotFoundException(`Booking with uid=${bookingUid} was not found in the database`); + } + + const hasSeatsPresent = booking.seatsReferences.length > 0; + + if (!hasSeatsPresent) return false; + + return await this.isIndividualSeatOrOrgAdminReschedule( + isIndividualSeatReschedule, + booking.userId, + authUser?.id + ); + } + + async isIndividualSeatOrOrgAdminReschedule( + isIndividualSeatReschedule: boolean, + bookingUserId: number | null, + authUserId?: number | null + ) { + if (isIndividualSeatReschedule) { + return true; + } + + if (!authUserId) { + throw new Error(`No auth user found`); + } + + if (!bookingUserId) { + throw new Error(`No user found for booking`); + } + + const isOrgAdmin = await PrismaOrgMembershipRepository.isLoggedInUserOrgAdminOfBookingHost( + authUserId, + bookingUserId + ); + + return isOrgAdmin; + } + + isRescheduleSeatedBody(body: RescheduleBookingInput): body is RescheduleSeatedBookingInput_2024_08_13 { + return "seatUid" in body; + } + async cancelBooking( request: Request, bookingUid: string, diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts b/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts index c308529f25a983..272aed172641ee 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts @@ -507,11 +507,13 @@ export class InputBookingsService_2024_08_13 { async createRescheduleBookingRequest( request: Request, bookingUid: string, - body: RescheduleBookingInput + body: RescheduleBookingInput, + isIndividualSeatReschedule: boolean ): Promise { - const bodyTransformed = this.isRescheduleSeatedBody(body) - ? await this.transformInputRescheduleSeatedBooking(bookingUid, body) - : await this.transformInputRescheduleBooking(bookingUid, body); + const bodyTransformed = + isIndividualSeatReschedule && "seatUid" in body + ? await this.transformInputRescheduleSeatedBooking(bookingUid, body) + : await this.transformInputRescheduleBooking(bookingUid, body, isIndividualSeatReschedule); const oAuthClientParams = await this.platformBookingsService.getOAuthClientParams( bodyTransformed.eventTypeId @@ -606,7 +608,11 @@ export class InputBookingsService_2024_08_13 { }; } - async transformInputRescheduleBooking(bookingUid: string, inputBooking: RescheduleBookingInput_2024_08_13) { + async transformInputRescheduleBooking( + bookingUid: string, + inputBooking: RescheduleBookingInput_2024_08_13, + isIndividualSeatReschedule: boolean + ) { const booking = await this.bookingsRepository.getByUidWithAttendeesAndUserAndEvent(bookingUid); if (!booking) { throw new NotFoundException(`Booking with uid=${bookingUid} not found`); @@ -618,7 +624,7 @@ export class InputBookingsService_2024_08_13 { if (!eventType) { throw new NotFoundException(`Event type with id=${booking.eventTypeId} not found`); } - if (eventType.seatsPerTimeSlot) { + if (eventType.seatsPerTimeSlot && !isIndividualSeatReschedule) { throw new BadRequestException( `Booking with uid=${bookingUid} is a seated booking which means you have to provide seatUid in the request body to specify which seat specifically you want to reschedule. First, fetch the booking using https://cal.com/docs/api-reference/v2/bookings/get-a-booking and then within the attendees array you will find the seatUid of the booking you want to reschedule. Second, provide the seatUid in the request body to specify which seat specifically you want to reschedule using the reschedule endpoint https://cal.com/docs/api-reference/v2/bookings/reschedule-a-booking#option-2` ); @@ -652,7 +658,6 @@ export class InputBookingsService_2024_08_13 { const startTime = DateTime.fromISO(inputBooking.start, { zone: "utc" }).setZone(attendee.timeZone); const endTime = startTime.plus({ minutes: eventType.length }); - return { start: startTime.toISO(), end: endTime.toISO(), diff --git a/packages/features/bookings/lib/handleCancelBooking.ts b/packages/features/bookings/lib/handleCancelBooking.ts index 6b24bb913c80b4..01b40ff05a2cf1 100644 --- a/packages/features/bookings/lib/handleCancelBooking.ts +++ b/packages/features/bookings/lib/handleCancelBooking.ts @@ -28,6 +28,7 @@ import { parseRecurringEvent } from "@calcom/lib/isRecurringEvent"; import logger from "@calcom/lib/logger"; import { safeStringify } from "@calcom/lib/safeStringify"; import { getTranslation } from "@calcom/lib/server/i18n"; +import { PrismaOrgMembershipRepository } from "@calcom/lib/server/repository/PrismaOrgMembershipRepository"; import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat"; // TODO: Prisma import would be used from DI in a followup PR when we remove `handler` export import prisma from "@calcom/prisma"; @@ -144,8 +145,18 @@ async function handler(input: CancelBookingInput) { const userIsOwnerOfEventType = bookingToDelete.eventType.owner?.id === userId; - if (!userIsHost && !userIsOwnerOfEventType) { - throw new HttpError({ statusCode: 401, message: "User not a host of this event" }); + const userIsOrgAdminOfBookingUser = + userId && + (await PrismaOrgMembershipRepository.isLoggedInUserOrgAdminOfBookingHost( + userId, + bookingToDelete.userId + )); + + if (!userIsHost && !userIsOwnerOfEventType && !userIsOrgAdminOfBookingUser) { + throw new HttpError({ + statusCode: 401, + message: "User not a host of this event or an admin of the booking user", + }); } } diff --git a/packages/features/bookings/lib/handleSeats/reschedule/rescheduleSeatedBooking.ts b/packages/features/bookings/lib/handleSeats/reschedule/rescheduleSeatedBooking.ts index a7c962a5aa7544..109ee3f33b4ed6 100644 --- a/packages/features/bookings/lib/handleSeats/reschedule/rescheduleSeatedBooking.ts +++ b/packages/features/bookings/lib/handleSeats/reschedule/rescheduleSeatedBooking.ts @@ -1,8 +1,8 @@ -// eslint-disable-next-line no-restricted-imports import dayjs from "@calcom/dayjs"; -import { refreshCredentials } from "@calcom/features/bookings/lib/getAllCredentialsForUsersOnEvent/refreshCredentials"; import EventManager from "@calcom/features/bookings/lib/EventManager"; +import { refreshCredentials } from "@calcom/features/bookings/lib/getAllCredentialsForUsersOnEvent/refreshCredentials"; import { HttpError } from "@calcom/lib/http-error"; +import { PrismaOrgMembershipRepository } from "@calcom/lib/server/repository/PrismaOrgMembershipRepository"; import prisma from "@calcom/prisma"; import { BookingStatus } from "@calcom/prisma/enums"; import type { Person } from "@calcom/types/Calendar"; @@ -98,9 +98,17 @@ const rescheduleSeatedBooking = async ( }; if (!bookingSeat) { + const isOrgAdmin = + reqUserId && + seatedBooking.user && + (await PrismaOrgMembershipRepository.isLoggedInUserOrgAdminOfBookingHost( + reqUserId, + seatedBooking.user?.id + )); // if no bookingSeat is given and the userId != owner, 401. + // if no bookingSeat is given, also check if the request user is an org admin of the booking user // TODO: Next step; Evaluate ownership, what about teams? - if (seatedBooking.user?.id !== reqUserId) { + if (seatedBooking.user?.id !== reqUserId && !isOrgAdmin) { throw new HttpError({ statusCode: 401 }); } diff --git a/packages/lib/server/repository/PrismaOrgMembershipRepository.ts b/packages/lib/server/repository/PrismaOrgMembershipRepository.ts new file mode 100644 index 00000000000000..f70ce8cb93a1ec --- /dev/null +++ b/packages/lib/server/repository/PrismaOrgMembershipRepository.ts @@ -0,0 +1,61 @@ +import prisma from "@calcom/prisma"; +import { MembershipRole } from "@calcom/prisma/enums"; + +export class PrismaOrgMembershipRepository { + static async getOrgIdsWhereAdmin(loggedInUserId: number) { + const loggedInUserOrgMemberships = await prisma.membership.findMany({ + where: { + userId: loggedInUserId, + role: { + in: [MembershipRole.OWNER, MembershipRole.ADMIN], + }, + team: { + parentId: null, + }, + }, + select: { + teamId: true, + }, + }); + + return loggedInUserOrgMemberships.map((m) => m.teamId); + } + + static async isLoggedInUserOrgAdminOfBookingHost(loggedInUserId: number, bookingUserId: number) { + const orgIdsWhereLoggedInUserAdmin = await this.getOrgIdsWhereAdmin(loggedInUserId); + + if (orgIdsWhereLoggedInUserAdmin.length === 0) { + return false; + } + + const bookingUserOrgMembership = await prisma.membership.findFirst({ + where: { + userId: bookingUserId, + teamId: { + in: orgIdsWhereLoggedInUserAdmin, + }, + team: { + parentId: null, + }, + }, + select: { + userId: true, + }, + }); + + if (bookingUserOrgMembership) return true; + + const bookingUserOrgTeamMembership = await prisma.membership.findFirst({ + where: { + userId: bookingUserId, + team: { + parentId: { + in: orgIdsWhereLoggedInUserAdmin, + }, + }, + }, + }); + + return !!bookingUserOrgTeamMembership; + } +} diff --git a/packages/platform/libraries/bookings.ts b/packages/platform/libraries/bookings.ts index eceb5447f66289..db2be613f10012 100644 --- a/packages/platform/libraries/bookings.ts +++ b/packages/platform/libraries/bookings.ts @@ -8,4 +8,5 @@ export type { InstantBookingCreateResult, RegularBookingCreateResult, } from "@calcom/features/bookings/lib/dto/types"; +export { PrismaOrgMembershipRepository } from "@calcom/lib/server/repository/PrismaOrgMembershipRepository"; export { addGuestsHandler } from "@calcom/trpc/server/routers/viewer/bookings/addGuests.handler"; diff --git a/packages/trpc/server/routers/viewer/bookings/confirm.handler.ts b/packages/trpc/server/routers/viewer/bookings/confirm.handler.ts index b1545acdd29bbd..2dadd50a4b7c25 100644 --- a/packages/trpc/server/routers/viewer/bookings/confirm.handler.ts +++ b/packages/trpc/server/routers/viewer/bookings/confirm.handler.ts @@ -17,6 +17,7 @@ import { getTeamIdFromEventType } from "@calcom/lib/getTeamIdFromEventType"; import { isPrismaObjOrUndefined } from "@calcom/lib/isPrismaObj"; import { parseRecurringEvent } from "@calcom/lib/isRecurringEvent"; import { getTranslation } from "@calcom/lib/server/i18n"; +import { PrismaOrgMembershipRepository } from "@calcom/lib/server/repository/PrismaOrgMembershipRepository"; import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat"; import { prisma } from "@calcom/prisma"; import { Prisma } from "@calcom/prisma/client"; @@ -411,7 +412,7 @@ export const confirmHandler = async ({ ctx, input }: ConfirmOptions) => { } } - const message = `Booking ${confirmed}` ? "confirmed" : "rejected"; + const message = confirmed ? "Booking confirmed" : "Booking rejected"; const status = confirmed ? BookingStatus.ACCEPTED : BookingStatus.REJECTED; return { message, status }; @@ -472,63 +473,12 @@ const checkIfUserIsAuthorizedToConfirmBooking = async ({ if (membership) return; } - if (bookingUserId && (await isLoggedInUserOrgAdminOfBookingUser(loggedInUserId, bookingUserId))) { + if ( + bookingUserId && + (await PrismaOrgMembershipRepository.isLoggedInUserOrgAdminOfBookingHost(loggedInUserId, bookingUserId)) + ) { return; } throw new TRPCError({ code: "UNAUTHORIZED", message: "User is not authorized to confirm this booking" }); }; - -async function isLoggedInUserOrgAdminOfBookingUser(loggedInUserId: number, bookingUserId: number) { - const orgIdsWhereLoggedInUserAdmin = await getOrgIdsWhereAdmin(loggedInUserId); - - if (orgIdsWhereLoggedInUserAdmin.length === 0) { - return false; - } - - const bookingUserOrgMembership = await prisma.membership.findFirst({ - where: { - userId: bookingUserId, - teamId: { - in: orgIdsWhereLoggedInUserAdmin, - }, - team: { - parentId: null, - }, - }, - }); - - if (bookingUserOrgMembership) return true; - - const bookingUserOrgTeamMembership = await prisma.membership.findFirst({ - where: { - userId: bookingUserId, - team: { - parentId: { - in: orgIdsWhereLoggedInUserAdmin, - }, - }, - }, - }); - - return !!bookingUserOrgTeamMembership; -} - -async function getOrgIdsWhereAdmin(loggedInUserId: number) { - const loggedInUserOrgMemberships = await prisma.membership.findMany({ - where: { - userId: loggedInUserId, - role: { - in: [MembershipRole.OWNER, MembershipRole.ADMIN], - }, - team: { - parentId: null, - }, - }, - select: { - teamId: true, - }, - }); - - return loggedInUserOrgMemberships.map((m) => m.teamId); -}