fix: filter slots by requested date range to prevent date override leakage (#25822)
* fix: filter slots by requested date range to prevent date override leakage Co-Authored-By: morgan@cal.com <morgan@cal.com> * refactor: move filterSlotsByRequestedDateRange to private method Co-Authored-By: morgan@cal.com <morgan@cal.com> --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
morgan@cal.com
Devin AI
parent
e46c1bb40c
commit
5a3f15357c
@@ -272,6 +272,61 @@ export class AvailableSlotsService {
|
||||
return periodType === PeriodType.ROLLING_WINDOW || periodType === PeriodType.ROLLING;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters slots to only include dates within the requested range.
|
||||
* This is necessary because buildDateRanges uses a ±1 day buffer when checking
|
||||
* if date overrides should be included (to handle timezone edge cases), which can
|
||||
* cause slots from adjacent days to leak into the response.
|
||||
*/
|
||||
private _filterSlotsByRequestedDateRange<
|
||||
T extends Record<string, { time: string; attendees?: number; bookingUid?: string }[]>,
|
||||
>({
|
||||
slotsMappedToDate,
|
||||
startTime,
|
||||
endTime,
|
||||
timeZone,
|
||||
}: {
|
||||
slotsMappedToDate: T;
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
timeZone: string | undefined;
|
||||
}): T {
|
||||
if (!timeZone) {
|
||||
return slotsMappedToDate;
|
||||
}
|
||||
const inputStartTime = dayjs(startTime).tz(timeZone);
|
||||
const inputEndTime = dayjs(endTime).tz(timeZone);
|
||||
|
||||
// fr-CA uses YYYY-MM-DD format
|
||||
const formatter = new Intl.DateTimeFormat("fr-CA", {
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
timeZone: timeZone,
|
||||
});
|
||||
|
||||
const allowedDates = new Set<string>();
|
||||
for (
|
||||
let d = inputStartTime.startOf("day");
|
||||
!d.isAfter(inputEndTime, "day");
|
||||
d = d.add(1, "day")
|
||||
) {
|
||||
allowedDates.add(formatter.format(d.toDate()));
|
||||
}
|
||||
|
||||
const filtered = {} as T;
|
||||
for (const [date, slots] of Object.entries(slotsMappedToDate)) {
|
||||
if (allowedDates.has(date)) {
|
||||
(filtered as Record<string, typeof slots>)[date] = slots;
|
||||
}
|
||||
}
|
||||
return filtered;
|
||||
}
|
||||
private filterSlotsByRequestedDateRange = withReporting(
|
||||
this._filterSlotsByRequestedDateRange.bind(this),
|
||||
"filterSlotsByRequestedDateRange"
|
||||
);
|
||||
|
||||
private _getAllDatesWithBookabilityStatus(availableDates: string[]) {
|
||||
const availableDatesSet = new Set(availableDates);
|
||||
const firstDate = dayjs(availableDates[0]);
|
||||
@@ -1431,8 +1486,15 @@ export class AvailableSlotsService {
|
||||
);
|
||||
const withinBoundsSlotsMappedToDate = mapWithinBoundsSlotsToDate();
|
||||
|
||||
const filteredSlotsMappedToDate = this.filterSlotsByRequestedDateRange({
|
||||
slotsMappedToDate: withinBoundsSlotsMappedToDate,
|
||||
startTime: input.startTime,
|
||||
endTime: input.endTime,
|
||||
timeZone: input.timeZone,
|
||||
});
|
||||
|
||||
// We only want to run this on single targeted events and not dynamic
|
||||
if (!Object.keys(withinBoundsSlotsMappedToDate).length && input.usernameList?.length === 1) {
|
||||
if (!Object.keys(filteredSlotsMappedToDate).length && input.usernameList?.length === 1) {
|
||||
try {
|
||||
await this.dependencies.noSlotsNotificationService.handleNotificationWhenNoSlots({
|
||||
eventDetails: {
|
||||
@@ -1474,7 +1536,7 @@ export class AvailableSlotsService {
|
||||
: null;
|
||||
|
||||
return {
|
||||
slots: withinBoundsSlotsMappedToDate,
|
||||
slots: filteredSlotsMappedToDate,
|
||||
...troubleshooterData,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user