diff --git a/apps/api/v1/pages/api/bookings/[id]/_delete.ts b/apps/api/v1/pages/api/bookings/[id]/_delete.ts index 1756c29467..b7321824ca 100644 --- a/apps/api/v1/pages/api/bookings/[id]/_delete.ts +++ b/apps/api/v1/pages/api/bookings/[id]/_delete.ts @@ -82,6 +82,7 @@ async function handler(req: NextApiRequest) { return await handleCancelBooking({ bookingData: { id, allRemainingBookings, cancellationReason, cancelledBy }, userId: req.userId, + impersonatedByUserUuid: null, actionSource: "API_V1", }); } diff --git a/apps/api/v1/pages/api/bookings/_post.ts b/apps/api/v1/pages/api/bookings/_post.ts index 3b1c1c57f2..a1ab66c754 100644 --- a/apps/api/v1/pages/api/bookings/_post.ts +++ b/apps/api/v1/pages/api/bookings/_post.ts @@ -247,6 +247,7 @@ async function handler(req: NextApiRequest) { userId, hostname: req.headers.host || "", forcedSlug: req.headers["x-cal-force-slug"] as string | undefined, + impersonatedByUserUuid: null, }, bookingDataSchemaGetter: getBookingDataSchemaForApi, }); 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 47a8bd5390..45c8dceac4 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 @@ -223,6 +223,7 @@ export class BookingsController_2024_04_15 { platformBookingUrl: bookingRequest.platformBookingUrl, platformBookingLocation: bookingRequest.platformBookingLocation, areCalendarEventsEnabled: bookingRequest.areCalendarEventsEnabled, + impersonatedByUserUuid: null, }, }); if (booking.userId && booking.uid && booking.startTime && booking.user?.isPlatformManaged) { @@ -279,6 +280,7 @@ export class BookingsController_2024_04_15 { platformCancelUrl: bookingRequest.platformCancelUrl, platformRescheduleUrl: bookingRequest.platformRescheduleUrl, platformBookingUrl: bookingRequest.platformBookingUrl, + impersonatedByUserUuid: null, actionSource: "API_V2", }); if (!res.onlyRemovedAttendee && res.isPlatformManagedUserBooking) { @@ -317,6 +319,7 @@ export class BookingsController_2024_04_15 { userId: user.id, actor: makeUserActor(user.uuid), actionSource: "API_V2", + impersonatedByUserUuid: null, }); return { status: SUCCESS_STATUS, data: markNoShowResponse }; @@ -354,6 +357,7 @@ export class BookingsController_2024_04_15 { platformBookingUrl: bookingRequest.platformBookingUrl, platformBookingLocation: bookingRequest.platformBookingLocation, noEmail: bookingRequest.body.noEmail, + impersonatedByUserUuid: null, }, creationSource: "API_V2", }); diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/booking-guests.service.ts b/apps/api/v2/src/ee/bookings/2024-08-13/services/booking-guests.service.ts index c434cfae1b..d5b7c483f5 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/services/booking-guests.service.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/services/booking-guests.service.ts @@ -48,6 +48,7 @@ export class BookingGuestsService_2024_08_13 { input: { bookingId: booking.id, guests: input.guests }, emailsEnabled, actionSource: "API_V2", + impersonatedByUserUuid: null, }); if (res.message === "Guests added") { return await this.bookingsService.getBooking(bookingUid, user); 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 c7bf7f40cc..a47ed5826e 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 @@ -468,6 +468,7 @@ export class BookingsService_2024_08_13 { platformBookingLocation: bookingRequest.platformBookingLocation, noEmail: bookingRequest.noEmail, areCalendarEventsEnabled: bookingRequest.areCalendarEventsEnabled, + impersonatedByUserUuid: null, }, creationSource: "API_V2", }); @@ -497,6 +498,7 @@ export class BookingsService_2024_08_13 { platformBookingUrl: bookingRequest.platformBookingUrl, platformBookingLocation: bookingRequest.platformBookingLocation, areCalendarEventsEnabled: bookingRequest.areCalendarEventsEnabled, + impersonatedByUserUuid: null, }, creationSource: "API_V2", }); @@ -527,6 +529,7 @@ export class BookingsService_2024_08_13 { platformBookingUrl: bookingRequest.platformBookingUrl, platformBookingLocation: bookingRequest.platformBookingLocation, areCalendarEventsEnabled: bookingRequest.areCalendarEventsEnabled, + impersonatedByUserUuid: null, }, }); @@ -569,6 +572,7 @@ export class BookingsService_2024_08_13 { platformBookingUrl: bookingRequest.platformBookingUrl, platformBookingLocation: bookingRequest.platformBookingLocation, areCalendarEventsEnabled: bookingRequest.areCalendarEventsEnabled, + impersonatedByUserUuid: null, }, }); @@ -828,6 +832,7 @@ export class BookingsService_2024_08_13 { platformBookingUrl: bookingRequest.platformBookingUrl, platformBookingLocation: bookingRequest.platformBookingLocation, areCalendarEventsEnabled: bookingRequest.areCalendarEventsEnabled, + impersonatedByUserUuid: null, }, }); if (!booking.uid) { @@ -976,6 +981,7 @@ export class BookingsService_2024_08_13 { platformCancelUrl: bookingRequest.platformCancelUrl, platformRescheduleUrl: bookingRequest.platformRescheduleUrl, platformBookingUrl: bookingRequest.platformBookingUrl, + impersonatedByUserUuid: null, }); if (!res.onlyRemovedAttendee && res.isPlatformManagedUserBooking) { @@ -1036,6 +1042,7 @@ export class BookingsService_2024_08_13 { platformClientParams, actor: makeUserActor(userUuid), actionSource: "API_V2", + impersonatedByUserUuid: null, }); const booking = await this.bookingsRepository.getByUidWithAttendeesAndUserAndEvent(bookingUid); @@ -1242,6 +1249,7 @@ export class BookingsService_2024_08_13 { platformClientParams, actionSource: "API_V2", actor: makeUserActor(requestUser.uuid), + impersonatedByUserUuid: null, }, }); @@ -1277,6 +1285,7 @@ export class BookingsService_2024_08_13 { platformClientParams, actionSource: "API_V2", actor: makeUserActor(requestUser.uuid), + impersonatedByUserUuid: null, }, }); diff --git a/apps/web/app/api/cancel/route.ts b/apps/web/app/api/cancel/route.ts index f16ad65ae1..7a2dcfebfa 100644 --- a/apps/web/app/api/cancel/route.ts +++ b/apps/web/app/api/cancel/route.ts @@ -43,6 +43,7 @@ async function handler(req: NextRequest) { userId: session?.user?.id || -1, userUuid: session?.user?.uuid, actionSource: "WEBAPP", + impersonatedByUserUuid: session?.user?.impersonatedBy?.uuid ?? null, }); // const bookingCancelService = getBookingCancelService(); diff --git a/apps/web/app/api/link/route.ts b/apps/web/app/api/link/route.ts index 34c400fa62..2ff82732fa 100644 --- a/apps/web/app/api/link/route.ts +++ b/apps/web/app/api/link/route.ts @@ -94,6 +94,7 @@ async function handler(request: NextRequest) { : undefined, actionSource: "MAGIC_LINK", actor: makeUserActor(user.uuid), + impersonatedByUserUuid: null, }, }); } catch (e) { diff --git a/apps/web/app/api/verify-booking-token/route.ts b/apps/web/app/api/verify-booking-token/route.ts index d8a77175ca..8c6bb944c5 100644 --- a/apps/web/app/api/verify-booking-token/route.ts +++ b/apps/web/app/api/verify-booking-token/route.ts @@ -108,6 +108,7 @@ async function handleBookingAction( emailsEnabled: true, actionSource: "MAGIC_LINK", actor: makeUserActor(user.uuid), + impersonatedByUserUuid: null, }, }); } catch (e) { diff --git a/apps/web/pages/api/book/event.ts b/apps/web/pages/api/book/event.ts index 5f744934a2..20eccb3981 100644 --- a/apps/web/pages/api/book/event.ts +++ b/apps/web/pages/api/book/event.ts @@ -54,7 +54,7 @@ async function handler(req: NextApiRequest & { userId?: number; traceContext: Tr hostname: req.headers.host || "", forcedSlug: req.headers["x-cal-force-slug"] as string | undefined, traceContext: req.traceContext, - impersonatedByUserUuid: session?.user?.impersonatedBy?.uuid, + impersonatedByUserUuid: session?.user?.impersonatedBy?.uuid ?? null, }, }); diff --git a/apps/web/pages/api/book/recurring-event.ts b/apps/web/pages/api/book/recurring-event.ts index 70481c8f5a..55ab38baec 100644 --- a/apps/web/pages/api/book/recurring-event.ts +++ b/apps/web/pages/api/book/recurring-event.ts @@ -53,6 +53,7 @@ async function handler(req: NextApiRequest & RequestMeta) { platformRescheduleUrl: req.platformRescheduleUrl, platformBookingLocation: req.platformBookingLocation, noEmail: req.noEmail, + impersonatedByUserUuid: session?.user?.impersonatedBy?.uuid ?? null, }, creationSource: "WEBAPP", }); diff --git a/apps/web/test/handlers/requestReschedule.test.ts b/apps/web/test/handlers/requestReschedule.test.ts index 9b4ca962db..4ddf98cdf1 100644 --- a/apps/web/test/handlers/requestReschedule.test.ts +++ b/apps/web/test/handlers/requestReschedule.test.ts @@ -580,5 +580,6 @@ function getTrpcHandlerData({ }, input: input, source: "WEBAPP" as const, + impersonatedByUserUuid: null, }; } diff --git a/apps/web/test/lib/confirm.handler.test.ts b/apps/web/test/lib/confirm.handler.test.ts index 4e7c3fab75..fc2ea6723a 100644 --- a/apps/web/test/lib/confirm.handler.test.ts +++ b/apps/web/test/lib/confirm.handler.test.ts @@ -131,6 +131,7 @@ describe("confirmHandler", () => { emailsEnabled: true, actor: makeUserActor(ctx.user.uuid), actionSource: "WEBAPP", + impersonatedByUserUuid: null, }, }); @@ -230,6 +231,7 @@ describe("confirmHandler", () => { emailsEnabled: true, actor, actionSource: "WEBAPP", + impersonatedByUserUuid: null, }, }); @@ -357,6 +359,7 @@ describe("confirmHandler", () => { emailsEnabled: true, actor, actionSource: "WEBAPP", + impersonatedByUserUuid: null, }, }); @@ -467,6 +470,7 @@ describe("confirmHandler", () => { emailsEnabled: false, actor, actionSource: "WEBAPP", + impersonatedByUserUuid: null, }, }); @@ -592,6 +596,7 @@ describe("confirmHandler", () => { emailsEnabled: false, actor, actionSource: "WEBAPP", + impersonatedByUserUuid: null, }, }); diff --git a/packages/app-store/_utils/payments/handlePaymentSuccess.ts b/packages/app-store/_utils/payments/handlePaymentSuccess.ts index c3fc6e33c0..913e236000 100644 --- a/packages/app-store/_utils/payments/handlePaymentSuccess.ts +++ b/packages/app-store/_utils/payments/handlePaymentSuccess.ts @@ -251,6 +251,7 @@ export async function handlePaymentSuccess(params: { platformClientParams, traceContext: updatedTraceContext, actionSource: "WEBHOOK", + impersonatedByUserUuid: null, actor, }); } else { diff --git a/packages/features/booking-audit/lib/service/BookingAuditProducerService.interface.ts b/packages/features/booking-audit/lib/service/BookingAuditProducerService.interface.ts index 1a4aa0c232..de98656c1f 100644 --- a/packages/features/booking-audit/lib/service/BookingAuditProducerService.interface.ts +++ b/packages/features/booking-audit/lib/service/BookingAuditProducerService.interface.ts @@ -208,6 +208,7 @@ export interface BookingAuditProducerService { organizationId: number | null; source: ActionSource; operationId?: string | null; + context?: BookingAuditContext; isBookingAuditEnabled: boolean; }): Promise; @@ -220,6 +221,7 @@ export interface BookingAuditProducerService { organizationId: number | null; source: ActionSource; operationId?: string | null; + context?: BookingAuditContext; isBookingAuditEnabled: boolean; }): Promise; diff --git a/packages/features/booking-audit/lib/service/BookingAuditTaskerProducerService.ts b/packages/features/booking-audit/lib/service/BookingAuditTaskerProducerService.ts index 54d29b5bb8..bb98a1fbe7 100644 --- a/packages/features/booking-audit/lib/service/BookingAuditTaskerProducerService.ts +++ b/packages/features/booking-audit/lib/service/BookingAuditTaskerProducerService.ts @@ -438,6 +438,7 @@ export class BookingAuditTaskerProducerService implements BookingAuditProducerSe organizationId: number | null; source: ActionSource; operationId?: string | null; + context?: BookingAuditContext; isBookingAuditEnabled: boolean; }): Promise { await this.queueBulkTask({ @@ -455,6 +456,7 @@ export class BookingAuditTaskerProducerService implements BookingAuditProducerSe organizationId: number | null; source: ActionSource; operationId?: string | null; + context?: BookingAuditContext; isBookingAuditEnabled: boolean; }): Promise { await this.queueBulkTask({ diff --git a/packages/features/bookings/lib/dto/BookingCancel.d.ts b/packages/features/bookings/lib/dto/BookingCancel.d.ts index 5632fae3a4..313c5a25ef 100644 --- a/packages/features/bookings/lib/dto/BookingCancel.d.ts +++ b/packages/features/bookings/lib/dto/BookingCancel.d.ts @@ -21,6 +21,7 @@ export type CancelBookingMeta = { platformCancelUrl?: string; platformBookingUrl?: string; arePlatformEmailsEnabled?: boolean; + impersonatedByUserUuid: string | null; }; /** diff --git a/packages/features/bookings/lib/dto/types.d.ts b/packages/features/bookings/lib/dto/types.d.ts index d3ed978b24..68df9d4c36 100644 --- a/packages/features/bookings/lib/dto/types.d.ts +++ b/packages/features/bookings/lib/dto/types.d.ts @@ -40,7 +40,7 @@ export type CreateBookingMeta = { forcedSlug?: string; noEmail?: boolean; traceContext?: TraceContext; - impersonatedByUserUuid?: string; + impersonatedByUserUuid: string | null; } & PlatformParams; export type BookingHandlerInput = { diff --git a/packages/features/bookings/lib/handleCancelBooking.ts b/packages/features/bookings/lib/handleCancelBooking.ts index d186daf858..bd4fcec0be 100644 --- a/packages/features/bookings/lib/handleCancelBooking.ts +++ b/packages/features/bookings/lib/handleCancelBooking.ts @@ -88,6 +88,7 @@ export type CancelBookingInput = { userUuid?: string; bookingData: z.infer; actionSource: ValidActionSource; + impersonatedByUserUuid: string | null; } & PlatformParams; type Dependencies = { @@ -179,6 +180,9 @@ async function handler(input: CancelBookingInput, dependencies?: Dependencies) { } = input; const userUuid = input.userUuid ?? null; + const impersonatedByUserUuid = input.impersonatedByUserUuid ?? null; + + const auditContext = impersonatedByUserUuid ? { impersonatedBy: impersonatedByUserUuid } : undefined; const actionSource = input.actionSource; @@ -557,6 +561,7 @@ async function handler(input: CancelBookingInput, dependencies?: Dependencies) { organizationId: orgId ?? null, operationId, source: actionSource, + context: auditContext, isBookingAuditEnabled, }); } else { @@ -592,6 +597,7 @@ async function handler(input: CancelBookingInput, dependencies?: Dependencies) { new: BookingStatus.CANCELLED, }, }, + context: auditContext, isBookingAuditEnabled, }); @@ -756,6 +762,7 @@ export class BookingCancelService implements IBookingCancelService { const cancelBookingInput: CancelBookingInput = { bookingData: input.bookingData, ...(input.bookingMeta || {}), + impersonatedByUserUuid: input.bookingMeta?.impersonatedByUserUuid ?? null, actionSource: input.actionSource, }; diff --git a/packages/features/bookings/lib/handleCancelBooking/test/handleCancelBooking.test.ts b/packages/features/bookings/lib/handleCancelBooking/test/handleCancelBooking.test.ts index 16a34ae261..8bf539d366 100644 --- a/packages/features/bookings/lib/handleCancelBooking/test/handleCancelBooking.test.ts +++ b/packages/features/bookings/lib/handleCancelBooking/test/handleCancelBooking.test.ts @@ -134,6 +134,7 @@ describe("Cancel Booking", () => { cancelledBy: organizer.email, cancellationReason: "No reason", }, + impersonatedByUserUuid: null, actionSource: "WEBAPP", }); @@ -265,6 +266,7 @@ describe("Cancel Booking", () => { cancelledBy: organizer.email, cancellationReason: "No reason", }, + impersonatedByUserUuid: null, actionSource: "WEBAPP", }); @@ -413,6 +415,7 @@ describe("Cancel Booking", () => { cancelledBy: organizer.email, cancellationReason: "Testing round robin cancellation with host as attendee", }, + impersonatedByUserUuid: null, actionSource: "WEBAPP", }); @@ -566,6 +569,7 @@ describe("Cancel Booking", () => { cancelledBy: primaryHost.email, cancellationReason: "Testing EMAIL_HOST workflow sends to secondary host in round robin", }, + impersonatedByUserUuid: null, actionSource: "WEBAPP", }); @@ -644,6 +648,7 @@ describe("Cancel Booking", () => { cancelledBy: organizer.email, cancellationReason: "Testing past booking cancellation", }, + impersonatedByUserUuid: null, actionSource: "WEBAPP", }) ).rejects.toThrow("Cannot cancel a booking that has already ended"); @@ -713,6 +718,7 @@ describe("Cancel Booking", () => { uid: uidOfBookingToBeCancelled, cancelledBy: organizer.email, }, + impersonatedByUserUuid: null, actionSource: "WEBAPP", }) ).rejects.toThrow("Cancellation reason is required"); @@ -825,6 +831,7 @@ describe("Cancel Booking", () => { cancelledBy: organizer.email, cancellationReason: "No reason", }, + impersonatedByUserUuid: null, actionSource: "WEBAPP", }); @@ -952,6 +959,7 @@ describe("Cancel Booking", () => { cancelledBy: organizer.email, cancellationReason: "No reason", }, + impersonatedByUserUuid: null, actionSource: "WEBAPP", }); @@ -1070,6 +1078,7 @@ describe("Cancel Booking", () => { cancellationReason: "Attendee cancelled within time threshold", }, userId: 999, + impersonatedByUserUuid: null, actionSource: "WEBAPP", }); @@ -1179,6 +1188,7 @@ describe("Cancel Booking", () => { cancelledBy: organizer.email, cancellationReason: "Organization booking cancellation test", }, + impersonatedByUserUuid: null, actionSource: "WEBAPP", }); @@ -1302,6 +1312,7 @@ describe("Cancel Booking", () => { cancellationReason: "Cancelling seated event", }, userId: organizer.id, + impersonatedByUserUuid: null, actionSource: "WEBAPP", }); @@ -1424,6 +1435,7 @@ describe("Cancel Booking", () => { allRemainingBookings: true, }, userId: organizer.id, + impersonatedByUserUuid: null, actionSource: "WEBAPP", }); @@ -1546,6 +1558,7 @@ describe("Cancel Booking", () => { cancelSubsequentBookings: true, }, userId: organizer.id, + impersonatedByUserUuid: null, actionSource: "WEBAPP", }); @@ -1660,6 +1673,7 @@ describe("Cancel Booking", () => { cancellationReason: "Testing booking reference cleanup", }, userId: organizer.id, + impersonatedByUserUuid: null, actionSource: "WEBAPP", }); diff --git a/packages/features/bookings/lib/handleConfirmation.ts b/packages/features/bookings/lib/handleConfirmation.ts index 8712660321..46d88d3896 100644 --- a/packages/features/bookings/lib/handleConfirmation.ts +++ b/packages/features/bookings/lib/handleConfirmation.ts @@ -45,6 +45,7 @@ async function fireBookingAcceptedEvent({ acceptedBookings, isBookingAuditEnabled, tracingLogger, + impersonatedByUserUuid, }: { actor: Actor; organizationId: number | null; @@ -55,9 +56,11 @@ async function fireBookingAcceptedEvent({ }[]; isBookingAuditEnabled: boolean; tracingLogger: ISimpleLogger; + impersonatedByUserUuid: string | null; }) { try { const bookingEventHandlerService = getBookingEventHandlerService(); + const context = impersonatedByUserUuid ? { impersonatedBy: impersonatedByUserUuid } : undefined; if (acceptedBookings.length > 1) { const operationId = uuidv4(); await bookingEventHandlerService.onBulkBookingsAccepted({ @@ -71,6 +74,7 @@ async function fireBookingAcceptedEvent({ organizationId, operationId, source: actionSource, + context, isBookingAuditEnabled, }); } else if (acceptedBookings.length === 1) { @@ -83,6 +87,7 @@ async function fireBookingAcceptedEvent({ status: { old: acceptedBooking.oldStatus, new: BookingStatus.ACCEPTED }, }, source: actionSource, + context, isBookingAuditEnabled, }); } @@ -135,6 +140,7 @@ export async function handleConfirmation(args: { traceContext: TraceContext; actionSource: ValidActionSource; actor: Actor; + impersonatedByUserUuid: string | null; }) { const { user, @@ -149,6 +155,7 @@ export async function handleConfirmation(args: { traceContext, actionSource, actor, + impersonatedByUserUuid, } = args; const eventType = booking.eventType; const eventTypeMetadata = EventTypeMetaDataSchema.parse(eventType?.metadata || {}); @@ -428,6 +435,7 @@ export async function handleConfirmation(args: { actionSource, isBookingAuditEnabled, tracingLogger, + impersonatedByUserUuid, }); //Workflows - set reminders for confirmed events diff --git a/packages/features/bookings/lib/handleSeats/handleSeats.ts b/packages/features/bookings/lib/handleSeats/handleSeats.ts index 0d02d9d69b..2db2319afd 100644 --- a/packages/features/bookings/lib/handleSeats/handleSeats.ts +++ b/packages/features/bookings/lib/handleSeats/handleSeats.ts @@ -29,6 +29,7 @@ const fireBookingEvents = async ({ rescheduledBy, actionSource, actorUserUuid, + impersonatedByUserUuid, isBookingAuditEnabled, deps, }: { @@ -42,6 +43,7 @@ const fireBookingEvents = async ({ rescheduledBy: string | undefined; actionSource: ActionSource; actorUserUuid: string | null; + impersonatedByUserUuid: string | null; isBookingAuditEnabled: boolean; deps: { bookingEventHandler: BookingEventHandlerService; @@ -75,6 +77,8 @@ const fireBookingEvents = async ({ return; } + const auditContext = impersonatedByUserUuid ? { impersonatedBy: impersonatedByUserUuid } : undefined; + if (rescheduleUid && originalRescheduledBooking) { const movedToDifferentBooking = newBooking.uid && newBooking.uid !== previousSeatedBooking.uid; const newBookingStartTimeMs = @@ -107,6 +111,7 @@ const fireBookingEvents = async ({ }, }, source: actionSource, + context: auditContext, isBookingAuditEnabled, }); } else { @@ -122,6 +127,7 @@ const fireBookingEvents = async ({ endTime: previousSeatedBooking.endTime.getTime(), }, source: actionSource, + context: auditContext, isBookingAuditEnabled, }); } @@ -159,6 +165,7 @@ const handleSeats = async ( fullName, traceContext, actionSource, + impersonatedByUserUuid, deps: { bookingEventHandler }, } = newSeatedBookingObject; // TODO: We could allow doing more things to support good dry run for seats @@ -244,6 +251,7 @@ const handleSeats = async ( rescheduledBy, actionSource, actorUserUuid: reqUserUuid ?? null, + impersonatedByUserUuid, isBookingAuditEnabled, deps: { bookingEventHandler, logger: loggerWithEventDetails }, }); diff --git a/packages/features/bookings/lib/handleSeats/test/handleSeats.test.ts b/packages/features/bookings/lib/handleSeats/test/handleSeats.test.ts index 961cbfce43..c00b555570 100644 --- a/packages/features/bookings/lib/handleSeats/test/handleSeats.test.ts +++ b/packages/features/bookings/lib/handleSeats/test/handleSeats.test.ts @@ -1976,6 +1976,7 @@ describe("handleSeats", () => { cancellationReason: "test cancellation reason", }, userId: organizer.id, + impersonatedByUserUuid: null, actionSource: "WEBAPP", }); @@ -2116,6 +2117,7 @@ describe("handleSeats", () => { cancellationReason: "test cancellation reason", }, userId: organizer.id, + impersonatedByUserUuid: null, actionSource: "WEBAPP", }); @@ -2969,6 +2971,7 @@ describe("handleSeats", () => { cancellationReason: "test cancellation reason", }, userId: organizer.id, + impersonatedByUserUuid: null, actionSource: "WEBAPP", }); diff --git a/packages/features/bookings/lib/handleSeats/types.d.ts b/packages/features/bookings/lib/handleSeats/types.d.ts index ecf319660f..8c13bb5fc5 100644 --- a/packages/features/bookings/lib/handleSeats/types.d.ts +++ b/packages/features/bookings/lib/handleSeats/types.d.ts @@ -65,6 +65,7 @@ export type NewSeatedBookingObject = { organizationId?: number | null; actionSource: ActionSource; traceContext: TraceContext; + impersonatedByUserUuid: string | null; deps: { bookingEventHandler: BookingEventHandlerService; } diff --git a/packages/features/bookings/lib/onBookingEvents/BookingEventHandlerService.ts b/packages/features/bookings/lib/onBookingEvents/BookingEventHandlerService.ts index 10d54a7843..37744c15a9 100644 --- a/packages/features/bookings/lib/onBookingEvents/BookingEventHandlerService.ts +++ b/packages/features/bookings/lib/onBookingEvents/BookingEventHandlerService.ts @@ -466,9 +466,10 @@ export class BookingEventHandlerService { organizationId: number | null; operationId?: string | null; source: ActionSource; + context?: BookingAuditContext; isBookingAuditEnabled: boolean; }) { - const { bookings, actor, organizationId, operationId, source, isBookingAuditEnabled } = params; + const { bookings, actor, organizationId, operationId, source, context, isBookingAuditEnabled } = params; await this.bookingAuditProducerService.queueBulkCreatedAudit({ bookings: bookings.map((booking) => ({ bookingUid: booking.bookingUid, @@ -478,6 +479,7 @@ export class BookingEventHandlerService { organizationId, source, operationId, + context, isBookingAuditEnabled, }); } @@ -491,9 +493,10 @@ export class BookingEventHandlerService { organizationId: number | null; operationId?: string | null; source: ActionSource; + context?: BookingAuditContext; isBookingAuditEnabled: boolean; }) { - const { bookings, actor, organizationId, operationId, source, isBookingAuditEnabled } = params; + const { bookings, actor, organizationId, operationId, source, context, isBookingAuditEnabled } = params; await this.bookingAuditProducerService.queueBulkRescheduledAudit({ bookings: bookings.map((booking) => ({ bookingUid: booking.bookingUid, @@ -503,6 +506,7 @@ export class BookingEventHandlerService { organizationId, source, operationId, + context, isBookingAuditEnabled, }); } diff --git a/packages/features/bookings/lib/service/RecurringBookingService.ts b/packages/features/bookings/lib/service/RecurringBookingService.ts index 3fcf92aef8..610839949c 100644 --- a/packages/features/bookings/lib/service/RecurringBookingService.ts +++ b/packages/features/bookings/lib/service/RecurringBookingService.ts @@ -77,6 +77,7 @@ export const handleNewRecurringBooking = async function ( bookingMeta: { hostname: input.hostname || "", forcedSlug: input.forcedSlug as string | undefined, + impersonatedByUserUuid: input.impersonatedByUserUuid ?? null, ...handleBookingMeta, }, }); @@ -120,11 +121,12 @@ export const handleNewRecurringBooking = async function ( bookingMeta: { hostname: input.hostname || "", forcedSlug: input.forcedSlug as string | undefined, + impersonatedByUserUuid: input.impersonatedByUserUuid ?? null, ...handleBookingMeta, }, }); - const eachRecurringBooking = await promiseEachRecurringBooking; + const eachRecurringBooking= await promiseEachRecurringBooking; createdBookings.push(eachRecurringBooking); @@ -148,6 +150,7 @@ export const handleNewRecurringBooking = async function ( actorUserUuid: input.userUuid ?? null, rescheduledBy: firstBooking.rescheduledBy ?? null, creationSource, + impersonatedByUserUuid: input.impersonatedByUserUuid ?? null, }); } @@ -164,7 +167,7 @@ export interface IRecurringBookingServiceDependencies { * Recurring Booking Service takes care of creating/rescheduling recurring bookings. */ export class RecurringBookingService implements IBookingService { - constructor(private readonly deps: IRecurringBookingServiceDependencies) {} + constructor(private readonly deps: IRecurringBookingServiceDependencies) { } async fireBookingEvents({ createdBookings, @@ -173,6 +176,7 @@ export class RecurringBookingService implements IBookingService { actorUserUuid, rescheduledBy, creationSource, + impersonatedByUserUuid, }: { createdBookings: BookingResponse[]; eventTypeId: number; @@ -180,6 +184,7 @@ export class RecurringBookingService implements IBookingService { actorUserUuid: string | null; rescheduledBy: string | null; creationSource: CreationSource | undefined; + impersonatedByUserUuid: string | null; }) { try { type ValidBooking = BookingResponse & { @@ -245,6 +250,8 @@ export class RecurringBookingService implements IBookingService { ); }; + const auditContext = impersonatedByUserUuid ? { impersonatedBy: impersonatedByUserUuid } : undefined; + if (isReschedule) { const bulkRescheduledBookings = createdBookings.filter(isValidRescheduledBooking).map((booking) => ({ bookingUid: booking.previousBooking.uid, @@ -261,11 +268,11 @@ export class RecurringBookingService implements IBookingService { organizationId: eventOrganizationId, operationId, source: actionSource, + context: auditContext, isBookingAuditEnabled, }); } } else { - // For new bookings const bulkCreatedBookings = createdBookings.filter(isValidBooking).map((booking) => ({ bookingUid: booking.uid, auditData: buildBookingCreatedAuditData({ booking, attendeeSeatId: null }), @@ -278,6 +285,7 @@ export class RecurringBookingService implements IBookingService { organizationId: eventOrganizationId, operationId, source: actionSource, + context: auditContext, isBookingAuditEnabled, }); } @@ -292,7 +300,11 @@ export class RecurringBookingService implements IBookingService { bookingMeta?: CreateBookingMeta; creationSource: CreationSource; }): Promise { - const handlerInput = { bookingData: input.bookingData, ...(input.bookingMeta || {}) }; + const handlerInput = { + bookingData: input.bookingData, + ...(input.bookingMeta || {}), + impersonatedByUserUuid: input.bookingMeta?.impersonatedByUserUuid ?? null, + }; return handleNewRecurringBooking.bind(this)({ input: handlerInput, deps: this.deps, @@ -305,7 +317,11 @@ export class RecurringBookingService implements IBookingService { bookingMeta?: CreateBookingMeta; creationSource: CreationSource; }): Promise { - const handlerInput = { bookingData: input.bookingData, ...(input.bookingMeta || {}) }; + const handlerInput = { + bookingData: input.bookingData, + ...(input.bookingMeta || {}), + impersonatedByUserUuid: input.bookingMeta?.impersonatedByUserUuid ?? null, + }; return handleNewRecurringBooking.bind(this)({ input: handlerInput, deps: this.deps, diff --git a/packages/features/bookings/lib/service/RegularBookingService.ts b/packages/features/bookings/lib/service/RegularBookingService.ts index 9b95eec6bc..33a66db6f3 100644 --- a/packages/features/bookings/lib/service/RegularBookingService.ts +++ b/packages/features/bookings/lib/service/RegularBookingService.ts @@ -603,6 +603,7 @@ async function handler( skipEventLimitsCheck = false, skipCalendarSyncTaskCreation = false, traceContext: passedTraceContext, + impersonatedByUserUuid, } = input; let bookingEmailsAndSmsTaskerAction: BookingActionType = BookingActionMap.requested; @@ -1832,6 +1833,7 @@ async function handler( organizationId: eventOrganizationId, actionSource, traceContext, + impersonatedByUserUuid: impersonatedByUserUuid ?? null, deps, }, deps.featuresRepository @@ -2518,10 +2520,11 @@ async function handler( isRecurringBooking: !!input.bookingData.allRecurringDates, attendeeSeatId: evt.attendeeSeatId ?? null, tracingLogger, + impersonatedByUserUuid: impersonatedByUserUuid ?? null, isBookingAuditEnabled, }); - const webhookLocation = metadata?.videoCallUrl || evt.location; + const webhookLocation= metadata?.videoCallUrl || evt.location; const { assignmentReason: _emailAssignmentReason, ...evtWithoutAssignmentReason } = evt; const webhookData: EventPayloadType = { @@ -2961,6 +2964,7 @@ export class RegularBookingService implements IBookingService { isRecurringBooking, attendeeSeatId, tracingLogger, + impersonatedByUserUuid, isBookingAuditEnabled, }: { booking: { @@ -2987,6 +2991,7 @@ export class RegularBookingService implements IBookingService { isRecurringBooking: boolean; tracingLogger: ReturnType; attendeeSeatId: string | null; + impersonatedByUserUuid: string | null; isBookingAuditEnabled: boolean; }) { try { @@ -3020,6 +3025,8 @@ export class RegularBookingService implements IBookingService { logger: tracingLogger, }); + const auditContext = impersonatedByUserUuid ? { impersonatedBy: impersonatedByUserUuid } : undefined; + // For recurring bookings we fire the events in the RecurringBookingService if (!isRecurringBooking) { if (originalRescheduledBooking) { @@ -3040,6 +3047,7 @@ export class RegularBookingService implements IBookingService { }), source: actionSource, operationId: null, + context: auditContext, isBookingAuditEnabled, }); } else { @@ -3049,6 +3057,7 @@ export class RegularBookingService implements IBookingService { auditData: buildBookingCreatedAuditData({ booking, attendeeSeatId }), source: actionSource, operationId: null, + context: auditContext, isBookingAuditEnabled, }); } @@ -3059,11 +3068,25 @@ export class RegularBookingService implements IBookingService { } async createBooking(input: { bookingData: CreateRegularBookingData; bookingMeta?: CreateBookingMeta }) { - return handler.bind(this)({ bookingData: input.bookingData, ...input.bookingMeta }, this.deps); + return handler.bind(this)( + { + bookingData: input.bookingData, + ...input.bookingMeta, + impersonatedByUserUuid: input.bookingMeta?.impersonatedByUserUuid ?? null, + }, + this.deps + ); } async rescheduleBooking(input: { bookingData: CreateRegularBookingData; bookingMeta?: CreateBookingMeta }) { - return handler.bind(this)({ bookingData: input.bookingData, ...input.bookingMeta }, this.deps); + return handler.bind(this)( + { + bookingData: input.bookingData, + ...input.bookingMeta, + impersonatedByUserUuid: input.bookingMeta?.impersonatedByUserUuid ?? null, + }, + this.deps + ); } /** @@ -3074,11 +3097,11 @@ export class RegularBookingService implements IBookingService { bookingMeta?: CreateBookingMeta; bookingDataSchemaGetter: BookingDataSchemaGetter; }) { - const bookingMeta = input.bookingMeta ?? {}; return handler.bind(this)( { bookingData: input.bookingData, - ...bookingMeta, + ...input.bookingMeta, + impersonatedByUserUuid: input.bookingMeta?.impersonatedByUserUuid ?? null, }, this.deps, input.bookingDataSchemaGetter diff --git a/packages/features/calendar-subscription/lib/sync/CalendarSyncService.ts b/packages/features/calendar-subscription/lib/sync/CalendarSyncService.ts index 73139a53d7..d5ddb5f972 100644 --- a/packages/features/calendar-subscription/lib/sync/CalendarSyncService.ts +++ b/packages/features/calendar-subscription/lib/sync/CalendarSyncService.ts @@ -119,6 +119,7 @@ export class CalendarSyncService { await handleCancelBooking({ userId: booking.userId, actionSource: "SYSTEM", + impersonatedByUserUuid: null, bookingData: { uid: booking.uid, cancellationReason: "Cancelled on user's calendar", @@ -210,6 +211,7 @@ export class CalendarSyncService { skipCalendarSyncTaskCreation: true, skipAvailabilityCheck: true, skipEventLimitsCheck: true, + impersonatedByUserUuid: null, }, }); log.info("Successfully rescheduled booking from calendar sync", { bookingUid }); diff --git a/packages/features/calendar-subscription/lib/sync/__tests__/CalendarSyncService.integration.test.ts b/packages/features/calendar-subscription/lib/sync/__tests__/CalendarSyncService.integration.test.ts index ccf2b6d03b..f11633f9a7 100644 --- a/packages/features/calendar-subscription/lib/sync/__tests__/CalendarSyncService.integration.test.ts +++ b/packages/features/calendar-subscription/lib/sync/__tests__/CalendarSyncService.integration.test.ts @@ -921,6 +921,7 @@ describe("CalendarSyncService - error isolation", () => { expect(mockHandleCancelBooking).toHaveBeenCalledWith({ userId: 99, actionSource: "SYSTEM", + impersonatedByUserUuid: null, bookingData: { uid: "my-booking-uid", cancellationReason: "Cancelled on user's calendar", @@ -947,6 +948,7 @@ describe("CalendarSyncService - error isolation", () => { skipCalendarSyncTaskCreation: true, skipAvailabilityCheck: true, skipEventLimitsCheck: true, + impersonatedByUserUuid: null, }, }) ); diff --git a/packages/features/calendar-subscription/lib/sync/__tests__/CalendarSyncService.test.ts b/packages/features/calendar-subscription/lib/sync/__tests__/CalendarSyncService.test.ts index ea3b46cbee..da14565605 100644 --- a/packages/features/calendar-subscription/lib/sync/__tests__/CalendarSyncService.test.ts +++ b/packages/features/calendar-subscription/lib/sync/__tests__/CalendarSyncService.test.ts @@ -232,6 +232,7 @@ describe("CalendarSyncService", () => { expect(mockHandleCancelBooking).toHaveBeenCalledWith({ userId: mockBooking.userId, actionSource: "SYSTEM", + impersonatedByUserUuid: null, bookingData: { uid: mockBooking.uid, cancellationReason: "Cancelled on user's calendar", @@ -345,6 +346,7 @@ describe("CalendarSyncService", () => { skipCalendarSyncTaskCreation: true, skipAvailabilityCheck: true, skipEventLimitsCheck: true, + impersonatedByUserUuid: null, }, }); const lastCall = mockCreateBooking.mock.calls.at(-1)!; @@ -437,6 +439,7 @@ describe("CalendarSyncService", () => { skipCalendarSyncTaskCreation: true, skipAvailabilityCheck: true, skipEventLimitsCheck: true, + impersonatedByUserUuid: null, }, }); const baseCall = mockCreateBooking.mock.calls.at(-1)!; diff --git a/packages/features/ee/payments/api/webhook.ts b/packages/features/ee/payments/api/webhook.ts index 2c3979b53c..c3c8af5b67 100644 --- a/packages/features/ee/payments/api/webhook.ts +++ b/packages/features/ee/payments/api/webhook.ts @@ -146,6 +146,7 @@ const handleSetupSuccess = async (event: Stripe.Event, traceContext: TraceContex traceContext: updatedTraceContext, actionSource: "WEBHOOK", actor, + impersonatedByUserUuid: null, }); } else if (areEmailsEnabled) { await sendOrganizerRequestEmail({ ...evt }, eventType.metadata); diff --git a/packages/features/handleMarkNoShow.ts b/packages/features/handleMarkNoShow.ts index 1ca9bb67d6..017a74f36b 100644 --- a/packages/features/handleMarkNoShow.ts +++ b/packages/features/handleMarkNoShow.ts @@ -81,6 +81,7 @@ type HandleMarkHostNoShowArgs = { actionSource: ValidActionSource; locale?: string; platformClientParams?: PlatformClientParams; + impersonatedByUserUuid: string | null; }; type HandleMarkAttendeesAndHostNoShowArgs = { @@ -92,6 +93,7 @@ type HandleMarkAttendeesAndHostNoShowArgs = { actionSource: ValidActionSource; locale?: string; platformClientParams?: PlatformClientParams; + impersonatedByUserUuid: string | null; }; type HandleMarkNoShowArgs = { @@ -104,6 +106,7 @@ type HandleMarkNoShowArgs = { locale?: string; platformClientParams?: PlatformClientParams; actor: Actor; + impersonatedByUserUuid: string | null; }; const buildResultPayload = async ({ @@ -201,6 +204,7 @@ async function fireNoShowUpdated({ actor, orgId, actionSource, + impersonatedByUserUuid, }: { updatedNoShowHost?: boolean; hostUserUuid?: string; @@ -213,6 +217,7 @@ async function fireNoShowUpdated({ actor: Actor; orgId: number | null; actionSource: ValidActionSource; + impersonatedByUserUuid: string | null; }): Promise { const auditData: { host?: { userUuid: string; noShow: { old: boolean | null; new: boolean } }; @@ -248,12 +253,14 @@ async function fireNoShowUpdated({ const isSomethingChanged = auditData.host || (auditData.attendeesNoShow && auditData.attendeesNoShow.length > 0); if (isSomethingChanged) { + const auditContext = impersonatedByUserUuid ? { impersonatedBy: impersonatedByUserUuid } : undefined; await bookingEventHandlerService.onNoShowUpdated({ bookingUid: booking.uid, actor, organizationId: orgId ?? null, source: actionSource, auditData, + context: auditContext, isBookingAuditEnabled, }); } @@ -268,6 +275,7 @@ const handleMarkNoShow = async ({ locale, platformClientParams, actionSource, + impersonatedByUserUuid, }: HandleMarkNoShowArgs): Promise => { const responsePayload = new ResponsePayload(); const t = await getTranslation(locale ?? "en", "common"); @@ -457,6 +465,7 @@ const handleMarkNoShow = async ({ actor, orgId: orgId ?? null, actionSource, + impersonatedByUserUuid, }); return responsePayload.getPayload(); @@ -539,6 +548,7 @@ export const handleMarkHostNoShow = async ({ actionSource, locale, platformClientParams, + impersonatedByUserUuid, }: HandleMarkHostNoShowArgs): Promise => { const actorEmail = buildActorEmail({ identifier: getUniqueIdentifier({ prefix: "attendee" }), @@ -555,6 +565,7 @@ export const handleMarkHostNoShow = async ({ actionSource, locale, platformClientParams, + impersonatedByUserUuid, }); }; @@ -572,6 +583,7 @@ export const handleMarkAttendeesAndHostNoShow = async ({ actionSource, locale, platformClientParams, + impersonatedByUserUuid, }: HandleMarkAttendeesAndHostNoShowArgs): Promise => { const actor = makeUserActor(userUuid); @@ -584,6 +596,7 @@ export const handleMarkAttendeesAndHostNoShow = async ({ actionSource, locale, platformClientParams, + impersonatedByUserUuid, }); }; diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma index 230c00df8a..86a550d252 100644 --- a/packages/prisma/schema.prisma +++ b/packages/prisma/schema.prisma @@ -1,5 +1,5 @@ // This is your Prisma Schema file -// learn more about it in the docs: https://pris.ly/d/prisma-schema +// Learn more about it in the docs: https://pris.ly/d/prisma-schema datasource db { provider = "postgresql" diff --git a/packages/trpc/server/routers/loggedInViewer/markNoShow.handler.ts b/packages/trpc/server/routers/loggedInViewer/markNoShow.handler.ts index df8b941972..823f4f536a 100644 --- a/packages/trpc/server/routers/loggedInViewer/markNoShow.handler.ts +++ b/packages/trpc/server/routers/loggedInViewer/markNoShow.handler.ts @@ -1,3 +1,5 @@ +import type { Session } from "next-auth"; + import { handleMarkAttendeesAndHostNoShow } from "@calcom/features/handleMarkNoShow"; import type { TrpcSessionUser } from "@calcom/trpc/server/types"; @@ -7,11 +9,13 @@ type NoShowOptions = { input: TNoShowInputSchema; ctx: { user: NonNullable; + session: Session; }; }; export const markNoShow = async ({ ctx, input }: NoShowOptions) => { const { bookingUid, attendees, noShowHost } = input; + const impersonatedByUserUuid = ctx.session.user?.impersonatedBy?.uuid ?? null; return handleMarkAttendeesAndHostNoShow({ bookingUid, @@ -21,5 +25,6 @@ export const markNoShow = async ({ ctx, input }: NoShowOptions) => { userUuid: ctx.user.uuid, locale: ctx.user.locale, actionSource: "WEBAPP", + impersonatedByUserUuid, }); }; diff --git a/packages/trpc/server/routers/publicViewer/markHostAsNoShow.handler.ts b/packages/trpc/server/routers/publicViewer/markHostAsNoShow.handler.ts index 7b58db949d..758c4ef19d 100644 --- a/packages/trpc/server/routers/publicViewer/markHostAsNoShow.handler.ts +++ b/packages/trpc/server/routers/publicViewer/markHostAsNoShow.handler.ts @@ -15,6 +15,7 @@ export const noShowHandler = async ({ input }: NoShowOptions) => { bookingUid, noShowHost, actionSource: "WEBAPP", + impersonatedByUserUuid: null, }); }; diff --git a/packages/trpc/server/routers/viewer/bookings/_router.tsx b/packages/trpc/server/routers/viewer/bookings/_router.tsx index 960d181543..de8e5f7c6f 100644 --- a/packages/trpc/server/routers/viewer/bookings/_router.tsx +++ b/packages/trpc/server/routers/viewer/bookings/_router.tsx @@ -1,5 +1,5 @@ import { makeUserActor } from "@calcom/features/booking-audit/lib/makeActor"; -import type { ActionSource } from "@calcom/features/booking-audit/lib/types/actionSource"; + import { MembershipRole } from "@calcom/prisma/enums"; import authedProcedure from "../../../procedures/authedProcedure"; import { createTeamPbacProcedure } from "../../../procedures/pbacProcedures"; @@ -39,30 +39,33 @@ export const bookingsRouter = router({ ctx, input, source: "WEBAPP", + impersonatedByUserUuid: ctx.session?.user?.impersonatedBy?.uuid ?? null, }); }), - editLocation: bookingsProcedure.input(ZEditLocationInputSchema).mutation(async ({ input, ctx }) => { + editLocation:bookingsProcedure.input(ZEditLocationInputSchema).mutation(async ({ input, ctx }) => { const { editLocationHandler } = await import("./editLocation.handler"); return editLocationHandler({ ctx, input, actionSource: "WEBAPP", + impersonatedByUserUuid: ctx.session?.user?.impersonatedBy?.uuid ?? null, }); }), - addGuests: authedProcedure.input(ZAddGuestsInputSchema).mutation(async ({ input, ctx }) => { + addGuests:authedProcedure.input(ZAddGuestsInputSchema).mutation(async ({ input, ctx }) => { const { addGuestsHandler } = await import("./addGuests.handler"); return addGuestsHandler({ ctx, input, actionSource: "WEBAPP", + impersonatedByUserUuid: ctx.session?.user?.impersonatedBy?.uuid ?? null, }); }), - confirm: authedProcedure.input(ZConfirmInputSchema).mutation(async ({ input, ctx }) => { + confirm:authedProcedure.input(ZConfirmInputSchema).mutation(async ({ input, ctx }) => { const { confirmHandler } = await import("./confirm.handler"); return confirmHandler({ @@ -71,6 +74,7 @@ export const bookingsRouter = router({ ...input, actor: makeUserActor(ctx.user.uuid), actionSource: "WEBAPP", + impersonatedByUserUuid: ctx.session?.user?.impersonatedBy?.uuid ?? null, }, }); }), @@ -121,6 +125,7 @@ export const bookingsRouter = router({ return reportBookingHandler({ ctx, input, + impersonatedByUserUuid: ctx.session?.user?.impersonatedBy?.uuid ?? null, actionSource: "WEBAPP", }); }), diff --git a/packages/trpc/server/routers/viewer/bookings/addGuests.handler.ts b/packages/trpc/server/routers/viewer/bookings/addGuests.handler.ts index d99f95ef9c..a63ce9a8e9 100644 --- a/packages/trpc/server/routers/viewer/bookings/addGuests.handler.ts +++ b/packages/trpc/server/routers/viewer/bookings/addGuests.handler.ts @@ -37,6 +37,7 @@ type AddGuestsOptions = { input: TAddGuestsInputSchema; emailsEnabled?: boolean; actionSource: ValidActionSource; + impersonatedByUserUuid: string | null; }; export type Booking = NonNullable< @@ -49,6 +50,7 @@ export const addGuestsHandler = async ({ input, emailsEnabled = true, actionSource, + impersonatedByUserUuid, }: AddGuestsOptions) => { const { user } = ctx; const { bookingId, guests } = input; @@ -93,6 +95,7 @@ export const addGuestsHandler = async ({ } const bookingEventHandlerService = getBookingEventHandlerService(); + const context = impersonatedByUserUuid ? { impersonatedBy: impersonatedByUserUuid } : undefined; const featuresRepository = getFeaturesRepository(); const organizationId = user.organizationId ?? null; const isBookingAuditEnabled = organizationId @@ -107,6 +110,7 @@ export const addGuestsHandler = async ({ auditData: { added: uniqueGuestEmails, }, + context, isBookingAuditEnabled, }); diff --git a/packages/trpc/server/routers/viewer/bookings/confirm.handler.ts b/packages/trpc/server/routers/viewer/bookings/confirm.handler.ts index d253e7dcbe..8b94cf0485 100644 --- a/packages/trpc/server/routers/viewer/bookings/confirm.handler.ts +++ b/packages/trpc/server/routers/viewer/bookings/confirm.handler.ts @@ -52,7 +52,7 @@ type ConfirmOptions = { >; traceContext: TraceContext; }; - input: TConfirmInputSchema & { actionSource: ValidActionSource; actor: Actor }; + input: TConfirmInputSchema & { actionSource: ValidActionSource; actor: Actor; impersonatedByUserUuid: string | null }; }; async function fireRejectionEvent({ @@ -63,6 +63,7 @@ async function fireRejectionEvent({ rejectionReason, isBookingAuditEnabled, tracingLogger, + impersonatedByUserUuid, }: { actor: Actor; organizationId: number | null; @@ -74,11 +75,13 @@ async function fireRejectionEvent({ }[]; isBookingAuditEnabled: boolean; tracingLogger: ISimpleLogger; + impersonatedByUserUuid: string | null; }): Promise { try { const bookingEventHandlerService = getBookingEventHandlerService(); if (rejectedBookings.length > 1) { const operationId = uuidv4(); + const context = impersonatedByUserUuid ? { impersonatedBy: impersonatedByUserUuid } : undefined; await bookingEventHandlerService.onBulkBookingsRejected({ bookings: rejectedBookings.map((booking) => ({ bookingUid: booking.uid, @@ -91,10 +94,12 @@ async function fireRejectionEvent({ organizationId, operationId, source: actionSource, + context, isBookingAuditEnabled, }); } else if (rejectedBookings.length === 1) { const booking = rejectedBookings[0]; + const context = impersonatedByUserUuid ? { impersonatedBy: impersonatedByUserUuid } : undefined; await bookingEventHandlerService.onBookingRejected({ bookingUid: booking.uid, actor, @@ -104,6 +109,7 @@ async function fireRejectionEvent({ status: { old: booking.oldStatus, new: BookingStatus.REJECTED }, }, source: actionSource, + context, isBookingAuditEnabled, }); } @@ -125,6 +131,7 @@ export const confirmHandler = async ({ ctx, input }: ConfirmOptions) => { platformClientParams, actionSource, actor, + impersonatedByUserUuid, } = input; const booking = await prisma.booking.findUniqueOrThrow({ @@ -450,6 +457,7 @@ export const confirmHandler = async ({ ctx, input }: ConfirmOptions) => { traceContext, actionSource, actor, + impersonatedByUserUuid, }); } else { evt.rejectionReason = rejectionReason; @@ -542,6 +550,7 @@ export const confirmHandler = async ({ ctx, input }: ConfirmOptions) => { rejectedBookings, isBookingAuditEnabled, tracingLogger: log, + impersonatedByUserUuid, }); // send BOOKING_REJECTED webhooks diff --git a/packages/trpc/server/routers/viewer/bookings/editLocation.handler.ts b/packages/trpc/server/routers/viewer/bookings/editLocation.handler.ts index 54cc38eb7a..091d11257e 100644 --- a/packages/trpc/server/routers/viewer/bookings/editLocation.handler.ts +++ b/packages/trpc/server/routers/viewer/bookings/editLocation.handler.ts @@ -36,6 +36,7 @@ type EditLocationOptions = { } & BookingsProcedureContext; input: TEditLocationInputSchema; actionSource: ValidActionSource; + impersonatedByUserUuid: string | null; }; type UserMetadata = z.infer; @@ -262,7 +263,7 @@ export function getLocationForOrganizerDefaultConferencingAppInEvtFormat({ return appLink; } -export async function editLocationHandler({ ctx, input, actionSource }: EditLocationOptions) { +export async function editLocationHandler({ ctx, input, actionSource, impersonatedByUserUuid }: EditLocationOptions) { const { newLocation, credentialId: conferenceCredentialId } = input; const { booking, user: loggedInUser } = ctx; @@ -318,6 +319,7 @@ export async function editLocationHandler({ ctx, input, actionSource }: EditLoca } const bookingEventHandlerService = getBookingEventHandlerService(); + const context = impersonatedByUserUuid ? { impersonatedBy: impersonatedByUserUuid } : undefined; const featuresRepository = getFeaturesRepository(); const isBookingAuditEnabled = organizationId ? await featuresRepository.checkIfTeamHasFeature(organizationId, "booking-audit") @@ -334,6 +336,7 @@ export async function editLocationHandler({ ctx, input, actionSource }: EditLoca new: updatedLocation, }, }, + context, isBookingAuditEnabled, }); diff --git a/packages/trpc/server/routers/viewer/bookings/reportBooking.handler.test.ts b/packages/trpc/server/routers/viewer/bookings/reportBooking.handler.test.ts index 924a028302..8bfe861b95 100644 --- a/packages/trpc/server/routers/viewer/bookings/reportBooking.handler.test.ts +++ b/packages/trpc/server/routers/viewer/bookings/reportBooking.handler.test.ts @@ -81,6 +81,7 @@ describe("reportBookingHandler", () => { bookingUid: "test-booking-uid", reason: BookingReportReason.SPAM, }, + impersonatedByUserUuid: null, }) ).rejects.toThrow(TRPCError); @@ -91,6 +92,7 @@ describe("reportBookingHandler", () => { bookingUid: "test-booking-uid", reason: BookingReportReason.SPAM, }, + impersonatedByUserUuid: null, }) ).rejects.toMatchObject({ code: "FORBIDDEN", @@ -109,6 +111,7 @@ describe("reportBookingHandler", () => { bookingUid: "test-booking-uid", reason: BookingReportReason.SPAM, }, + impersonatedByUserUuid: null, }) ).rejects.toMatchObject({ code: "NOT_FOUND", @@ -132,6 +135,7 @@ describe("reportBookingHandler", () => { bookingUid: "test-booking-uid", reason: BookingReportReason.SPAM, }, + impersonatedByUserUuid: null, }) ).rejects.toMatchObject({ code: "BAD_REQUEST", @@ -161,6 +165,7 @@ describe("reportBookingHandler", () => { reason: BookingReportReason.SPAM, description: "This is spam", }, + impersonatedByUserUuid: null, }); expect(result.success).toBe(true); @@ -190,6 +195,7 @@ describe("reportBookingHandler", () => { bookingUid: "test-booking-uid", reason: BookingReportReason.DONT_KNOW_PERSON, }, + impersonatedByUserUuid: null, }); expect(result.message).toBe("Booking reported successfully"); @@ -226,6 +232,7 @@ describe("reportBookingHandler", () => { reason: BookingReportReason.SPAM, description: "Spam booking", }, + impersonatedByUserUuid: null, }); expect(result.message).toBe("Booking reported and cancelled successfully"); @@ -236,6 +243,7 @@ describe("reportBookingHandler", () => { skipCancellationReasonValidation: true, }, userId: mockUser.id, + impersonatedByUserUuid: null, }); }); @@ -252,6 +260,7 @@ describe("reportBookingHandler", () => { bookingUid: "test-booking-uid", reason: BookingReportReason.SPAM, }, + impersonatedByUserUuid: null, }); expect(handleCancelBooking).not.toHaveBeenCalled(); @@ -272,6 +281,7 @@ describe("reportBookingHandler", () => { bookingUid: "test-booking-uid", reason: BookingReportReason.SPAM, }, + impersonatedByUserUuid: null, }); expect(result.success).toBe(true); @@ -304,6 +314,7 @@ describe("reportBookingHandler", () => { bookingUid: "test-booking-uid", reason: BookingReportReason.SPAM, }, + impersonatedByUserUuid: null, }); expect(handleCancelBooking).toHaveBeenCalledWith({ @@ -311,6 +322,7 @@ describe("reportBookingHandler", () => { cancelSubsequentBookings: true, }), userId: mockUser.id, + impersonatedByUserUuid: null, }); }); @@ -337,6 +349,7 @@ describe("reportBookingHandler", () => { bookingUid: "test-booking-uid", reason: BookingReportReason.SPAM, }, + impersonatedByUserUuid: null, }); expect(handleCancelBooking).toHaveBeenCalledWith({ @@ -344,6 +357,7 @@ describe("reportBookingHandler", () => { seatReferenceUid: "seat-123", }), userId: mockUser.id, + impersonatedByUserUuid: null, }); }); }); diff --git a/packages/trpc/server/routers/viewer/bookings/reportBooking.handler.ts b/packages/trpc/server/routers/viewer/bookings/reportBooking.handler.ts index 401b9be929..28090dd80a 100644 --- a/packages/trpc/server/routers/viewer/bookings/reportBooking.handler.ts +++ b/packages/trpc/server/routers/viewer/bookings/reportBooking.handler.ts @@ -17,12 +17,13 @@ type ReportBookingOptions = { user: NonNullable; }; input: TReportBookingInputSchema; + impersonatedByUserUuid: string | null; actionSource: ValidActionSource; }; const log = logger.getSubLogger({ prefix: ["reportBookingHandler"] }); -export const reportBookingHandler = async ({ ctx, input, actionSource }: ReportBookingOptions) => { +export const reportBookingHandler = async ({ ctx, input, impersonatedByUserUuid, actionSource }: ReportBookingOptions) => { const { user } = ctx; const { bookingUid, reason, description } = input; @@ -92,6 +93,7 @@ export const reportBookingHandler = async ({ ctx, input, actionSource }: ReportB ...(seatReferenceUid ? { seatReferenceUid } : {}), }, userId: user.id, + impersonatedByUserUuid, actionSource, }); didCancel = true; diff --git a/packages/trpc/server/routers/viewer/bookings/requestReschedule.handler.ts b/packages/trpc/server/routers/viewer/bookings/requestReschedule.handler.ts index 9c5ca97107..02d677b354 100644 --- a/packages/trpc/server/routers/viewer/bookings/requestReschedule.handler.ts +++ b/packages/trpc/server/routers/viewer/bookings/requestReschedule.handler.ts @@ -48,9 +48,10 @@ type RequestRescheduleOptions = { }; input: TRequestRescheduleInputSchema; source: ValidActionSource; + impersonatedByUserUuid: string | null; }; const log = logger.getSubLogger({ prefix: ["requestRescheduleHandler"] }); -export const requestRescheduleHandler = async ({ ctx, input, source }: RequestRescheduleOptions) => { +export const requestRescheduleHandler = async ({ ctx, input, source, impersonatedByUserUuid }: RequestRescheduleOptions) => { const { user } = ctx; const { bookingUid, rescheduleReason: cancellationReason } = input; log.debug("Started", safeStringify({ bookingUid })); @@ -316,6 +317,7 @@ export const requestRescheduleHandler = async ({ ctx, input, source }: RequestRe await Promise.all(promises); const bookingEventHandlerService = getBookingEventHandlerService(); + const context = impersonatedByUserUuid ? { impersonatedBy: impersonatedByUserUuid } : undefined; const featuresRepository = getFeaturesRepository(); const isBookingAuditEnabled = orgId ? await featuresRepository.checkIfTeamHasFeature(orgId, "booking-audit") @@ -330,6 +332,7 @@ export const requestRescheduleHandler = async ({ ctx, input, source }: RequestRe rescheduleReason: cancellationReason ?? null, rescheduledRequestedBy: user.email, }, + context, isBookingAuditEnabled, }); };