Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions packages/features/availability/lib/getUserAvailability.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,12 @@ export type GetUserAvailabilityInitialData = {
})[];
busyTimesFromLimitsBookings?: EventBusyDetails[];
busyTimesFromLimits?: Map<number, EventBusyDetails[]>;
/**
* Busy times of guests (attendees who are not the host) from the booking being rescheduled.
* Injected by the slots service when `rescheduleUid` is present so that the availability
* calculation blocks any slot that conflicts with a guest's existing accepted booking.
*/
guestBusyTimes?: EventBusyDetails[];
eventTypeForLimits?: {
id: number;
bookingLimits?: unknown;
Expand Down Expand Up @@ -622,6 +628,8 @@ export class UserAvailabilityService {
})),
...busyTimesFromLimits,
...busyTimesFromTeamLimits,
// Guest busy times from the booking being rescheduled: prevents double-booking guests.
...(initialData?.guestBusyTimes ?? []),
];

log.debug(
Expand Down
41 changes: 41 additions & 0 deletions packages/features/bookings/repositories/BookingRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2220,4 +2220,45 @@ export class BookingRepository implements IBookingRepository {
},
});
}

/**
* Returns accepted bookings whose attendee list contains at least one of the given emails,
* within the provided date range and excluding the booking with `excludedUid` (the booking
* being rescheduled). Used to surface guest conflicts when a host reschedules.
*/
async getAcceptedBookingsByAttendeeEmails({
attendeeEmails,
startDate,
endDate,
excludedUid,
}: {
attendeeEmails: string[];
startDate: Date;
endDate: Date;
excludedUid?: string | null;
}) {
if (attendeeEmails.length === 0) return [];
return this.prismaClient.booking.findMany({
where: {
status: BookingStatus.ACCEPTED,
// Use overlap semantics: any booking whose time range intersects [startDate, endDate].
startTime: { lt: endDate },
endTime: { gt: startDate },
...(excludedUid ? { uid: { not: excludedUid } } : {}),
attendees: {
some: {
email: { in: attendeeEmails },
},
},
},
select: {
id: true,
startTime: true,
endTime: true,
title: true,
uid: true,
userId: true,
},
});
Comment on lines +2229 to +2262
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This new query method introduces important overlap/exclusion semantics but doesn't appear to be covered by repository tests. Since BookingRepository.integration-test.ts already sets up real bookings/attendees, adding coverage for overlap boundaries (touching endpoints), multiple emails, and excludedUid would help prevent regressions.

Copilot uses AI. Check for mistakes.
}
}
39 changes: 39 additions & 0 deletions packages/trpc/server/routers/viewer/slots/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -839,6 +839,44 @@ export class AvailableSlotsService {
const allUserIds = Array.from(userIdAndEmailMap.keys());

const bookingRepo = this.dependencies.bookingRepo;

// When rescheduling, collect accepted bookings of all guests (attendees who are not
// the host) so we can block slots that would double-book them.
let guestBusyTimes: EventBusyDetails[] = [];
if (input.rescheduleUid) {
const rescheduleBooking = await bookingRepo.findByUidIncludeEventTypeAttendeesAndUser({
bookingUid: input.rescheduleUid,
});
if (rescheduleBooking) {
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getSchedule is a public endpoint, and this code uses the client-provided rescheduleUid to look up a booking and then query other accepted bookings for the booking’s attendee emails. Without an explicit authorization/gating check (e.g., only run when the requester is the booking owner/host), this can become a privacy leak (inferring attendees’ busy times) if a rescheduleUid is disclosed. Consider gating guest-conflict lookups to authenticated host reschedules and/or verifying the requester is allowed to access the booking before querying attendee-based conflicts.

Suggested change
if (rescheduleBooking) {
// Only consider guest conflicts if the reschedule booking belongs to one of the
// current hosts/users participating in this scheduling flow. This prevents
// unauthorized callers from probing guest availability using arbitrary UIDs.
if (rescheduleBooking && rescheduleBooking.userId && allUserIds.includes(rescheduleBooking.userId)) {

Copilot uses AI. Check for mistakes.
// Host emails: the booking's user + any event-type host emails
const hostEmails = new Set<string>();
if (rescheduleBooking.user?.email) hostEmails.add(rescheduleBooking.user.email);
rescheduleBooking.eventType?.hosts?.forEach((h) => {
if (h.user.email) hostEmails.add(h.user.email);
});

// Guest emails = attendees that are NOT hosts
const guestEmails = rescheduleBooking.attendees
.map((a) => a.email)
.filter((email) => !hostEmails.has(email));

if (guestEmails.length > 0) {
const guestBookings = await bookingRepo.getAcceptedBookingsByAttendeeEmails({
attendeeEmails: guestEmails,
startDate: startTimeDate,
Comment on lines +858 to +866
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

guestEmails can contain duplicates (and potentially empty strings, depending on upstream data). Deduplicating/filtering before calling getAcceptedBookingsByAttendeeEmails will reduce the size of the SQL IN (...) list and avoid unnecessary DB work.

Copilot uses AI. Check for mistakes.
endDate: endTimeDate,
excludedUid: input.rescheduleUid,
});
guestBusyTimes = guestBookings.map((b) => ({
start: b.startTime.toISOString(),
end: b.endTime.toISOString(),
title: b.title ?? undefined,
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

guestBusyTimes is populated with title from bookings that belong to the guests. This will flow into getUserAvailability's busy output and debug logs, and may expose private meeting titles unrelated to the host/reschedule flow. Consider omitting the title (or replacing with a generic label) for guest-derived busy times.

Suggested change
title: b.title ?? undefined,
// Do not leak guest-owned meeting titles; use a generic label instead.
title: "Busy",

Copilot uses AI. Check for mistakes.
source: "guest",
}));
}
}
}

const [currentBookingsAllUsers, outOfOfficeDaysAllUsers] = await Promise.all([
bookingRepo.findAllExistingBookingsForEventTypeBetween({
startDate: startTimeDate,
Expand Down Expand Up @@ -962,6 +1000,7 @@ export class AvailableSlotsService {
eventTypeForLimits: eventType && (bookingLimits || durationLimits) ? eventType : null,
teamBookingLimits: teamBookingLimitsMap,
teamForBookingLimits: teamForBookingLimits,
guestBusyTimes,
},
});
/* We get all users working hours and busy slots */
Expand Down
Loading