fix: Booker reschedule behaviour for org admin (#26530)
* fix: reschedule behaviour for org admin * fix: use pbac for permissions check --------- Co-authored-by: Peer Richelsen <peeroke@gmail.com> Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
This commit is contained in:
co-authored by
Peer Richelsen
Morgan
parent
36cf6e3703
commit
f32ea8b4c5
@@ -1,8 +1,10 @@
|
||||
import { bookingResponsesDbSchema } from "@calcom/features/bookings/lib/getBookingResponsesSchema";
|
||||
import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service";
|
||||
import slugify from "@calcom/lib/slugify";
|
||||
import type { PrismaClient } from "@calcom/prisma";
|
||||
import prisma from "@calcom/prisma";
|
||||
import type { Prisma } from "@calcom/prisma/client";
|
||||
import { MembershipRole } from "@calcom/prisma/enums";
|
||||
|
||||
type BookingSelect = {
|
||||
description: true;
|
||||
@@ -24,7 +26,8 @@ function getResponsesFromOldBooking(
|
||||
) {
|
||||
const customInputs = rawBooking.customInputs || {};
|
||||
const responses = Object.keys(customInputs).reduce((acc, label) => {
|
||||
acc[slugify(label) as keyof typeof acc] = customInputs[label as keyof typeof customInputs];
|
||||
acc[slugify(label) as keyof typeof acc] =
|
||||
customInputs[label as keyof typeof customInputs];
|
||||
return acc;
|
||||
}, {});
|
||||
return {
|
||||
@@ -43,7 +46,11 @@ function getResponsesFromOldBooking(
|
||||
};
|
||||
}
|
||||
|
||||
async function getBooking(prisma: PrismaClient, uid: string, isSeatedEvent?: boolean) {
|
||||
async function getBooking(
|
||||
prisma: PrismaClient,
|
||||
uid: string,
|
||||
isSeatedEvent?: boolean
|
||||
) {
|
||||
const rawBooking = await prisma.booking.findUnique({
|
||||
where: {
|
||||
uid,
|
||||
@@ -80,6 +87,7 @@ async function getBooking(prisma: PrismaClient, uid: string, isSeatedEvent?: boo
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -94,8 +102,12 @@ async function getBooking(prisma: PrismaClient, uid: string, isSeatedEvent?: boo
|
||||
if (booking) {
|
||||
// @NOTE: had to do this because Server side cant return [Object objects]
|
||||
// probably fixable with json.stringify -> json.parse
|
||||
booking["startTime"] = (booking?.startTime as Date)?.toISOString() as unknown as Date;
|
||||
booking["endTime"] = (booking?.endTime as Date)?.toISOString() as unknown as Date;
|
||||
booking["startTime"] = (
|
||||
booking?.startTime as Date
|
||||
)?.toISOString() as unknown as Date;
|
||||
booking["endTime"] = (
|
||||
booking?.endTime as Date
|
||||
)?.toISOString() as unknown as Date;
|
||||
}
|
||||
|
||||
return booking;
|
||||
@@ -115,7 +127,9 @@ export const getBookingWithResponses = <
|
||||
) => {
|
||||
return {
|
||||
...booking,
|
||||
responses: isSeatedEvent ? booking.responses : booking.responses || getResponsesFromOldBooking(booking),
|
||||
responses: isSeatedEvent
|
||||
? booking.responses
|
||||
: booking.responses || getResponsesFromOldBooking(booking),
|
||||
} as Omit<T, "responses"> & { responses: Record<string, any> };
|
||||
};
|
||||
|
||||
@@ -130,9 +144,15 @@ export const getBookingForReschedule = async (uid: string, userId?: number) => {
|
||||
select: {
|
||||
id: true,
|
||||
userId: true,
|
||||
user: {
|
||||
select: {
|
||||
organizationId: true,
|
||||
},
|
||||
},
|
||||
eventType: {
|
||||
select: {
|
||||
seatsPerTimeSlot: true,
|
||||
teamId: true,
|
||||
hosts: {
|
||||
select: {
|
||||
userId: true,
|
||||
@@ -152,7 +172,10 @@ export const getBookingForReschedule = async (uid: string, userId?: number) => {
|
||||
// If no booking is found via the uid, it's probably a booking seat
|
||||
// that its being rescheduled, which we query next.
|
||||
let attendeeEmail: string | null = null;
|
||||
let bookingSeatData: { description?: string; responses: Prisma.JsonValue } | null = null;
|
||||
let bookingSeatData: {
|
||||
description?: string;
|
||||
responses: Prisma.JsonValue;
|
||||
} | null = null;
|
||||
if (!theBooking) {
|
||||
const bookingSeat = await prisma.bookingSeat.findFirst({
|
||||
where: {
|
||||
@@ -175,7 +198,10 @@ export const getBookingForReschedule = async (uid: string, userId?: number) => {
|
||||
},
|
||||
});
|
||||
if (bookingSeat) {
|
||||
bookingSeatData = bookingSeat.data as unknown as { description?: string; responses: Prisma.JsonValue };
|
||||
bookingSeatData = bookingSeat.data as unknown as {
|
||||
description?: string;
|
||||
responses: Prisma.JsonValue;
|
||||
};
|
||||
bookingSeatReferenceUid = bookingSeat.id;
|
||||
rescheduleUid = bookingSeat.booking.uid;
|
||||
attendeeEmail = bookingSeat.attendee.email;
|
||||
@@ -185,14 +211,37 @@ export const getBookingForReschedule = async (uid: string, userId?: number) => {
|
||||
// If we have the booking and not bookingSeat, we need to make sure the booking belongs to the userLoggedIn
|
||||
// Otherwise, we return null here.
|
||||
let hasOwnershipOnBooking = false;
|
||||
if (theBooking && theBooking?.eventType?.seatsPerTimeSlot && bookingSeatReferenceUid === null) {
|
||||
if (
|
||||
theBooking &&
|
||||
theBooking?.eventType?.seatsPerTimeSlot &&
|
||||
bookingSeatReferenceUid === null
|
||||
) {
|
||||
const isOwnerOfBooking = theBooking.userId === userId;
|
||||
|
||||
const isHostOfEventType = theBooking?.eventType?.hosts.some((host) => host.userId === userId);
|
||||
const isHostOfEventType = theBooking?.eventType?.hosts.some(
|
||||
(host) => host.userId === userId
|
||||
);
|
||||
|
||||
const isUserIdInBooking = theBooking.userId === userId;
|
||||
|
||||
if (!isOwnerOfBooking && !isHostOfEventType && !isUserIdInBooking) return null;
|
||||
let hasOrgAccess = false;
|
||||
if (userId && theBooking.user?.organizationId) {
|
||||
const permissionCheckService = new PermissionCheckService();
|
||||
hasOrgAccess = await permissionCheckService.checkPermission({
|
||||
userId,
|
||||
teamId: theBooking.user.organizationId,
|
||||
permission: "booking.readOrgBookings",
|
||||
fallbackRoles: [MembershipRole.OWNER, MembershipRole.ADMIN],
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
!isOwnerOfBooking &&
|
||||
!isHostOfEventType &&
|
||||
!isUserIdInBooking &&
|
||||
!hasOrgAccess
|
||||
)
|
||||
return null;
|
||||
hasOwnershipOnBooking = true;
|
||||
}
|
||||
|
||||
@@ -200,13 +249,19 @@ export const getBookingForReschedule = async (uid: string, userId?: number) => {
|
||||
// and we return null here.
|
||||
if (!theBooking && !rescheduleUid) return null;
|
||||
|
||||
const booking = await getBooking(prisma, rescheduleUid || uid, bookingSeatReferenceUid ? true : false);
|
||||
const booking = await getBooking(
|
||||
prisma,
|
||||
rescheduleUid || uid,
|
||||
bookingSeatReferenceUid ? true : false
|
||||
);
|
||||
|
||||
if (!booking) return null;
|
||||
|
||||
if (bookingSeatReferenceUid) {
|
||||
booking["description"] = bookingSeatData?.description ?? null;
|
||||
booking["responses"] = bookingResponsesDbSchema.parse(bookingSeatData?.responses ?? {});
|
||||
booking["responses"] = bookingResponsesDbSchema.parse(
|
||||
bookingSeatData?.responses ?? {}
|
||||
);
|
||||
}
|
||||
return {
|
||||
...booking,
|
||||
@@ -244,6 +299,7 @@ export const getBookingForSeatedEvent = async (uid: string) => {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -293,6 +349,7 @@ export const getMultipleDurationValue = (
|
||||
defaultValue: number
|
||||
) => {
|
||||
if (!multipleDurationConfig) return null;
|
||||
if (multipleDurationConfig.includes(Number(queryDuration))) return Number(queryDuration);
|
||||
if (multipleDurationConfig.includes(Number(queryDuration)))
|
||||
return Number(queryDuration);
|
||||
return defaultValue;
|
||||
};
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import debounce from "lodash/debounce";
|
||||
import { useMemo, useEffect, useCallback, useState, useRef, useContext } from "react";
|
||||
import {
|
||||
useMemo,
|
||||
useEffect,
|
||||
useCallback,
|
||||
useState,
|
||||
useRef,
|
||||
useContext,
|
||||
} from "react";
|
||||
import { shallow } from "zustand/shallow";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
@@ -49,7 +56,9 @@ import type {
|
||||
} from "./types";
|
||||
|
||||
const BookerPlatformWrapperComponent = (
|
||||
props: BookerPlatformWrapperAtomPropsForIndividual | BookerPlatformWrapperAtomPropsForTeam
|
||||
props:
|
||||
| BookerPlatformWrapperAtomPropsForIndividual
|
||||
| BookerPlatformWrapperAtomPropsForTeam
|
||||
) => {
|
||||
const {
|
||||
view = "MONTH_VIEW",
|
||||
@@ -75,19 +84,29 @@ const BookerPlatformWrapperComponent = (
|
||||
const layout = BookerLayouts[view];
|
||||
|
||||
const { clientId } = useAtomsContext();
|
||||
const teamId: number | undefined = props.isTeamEvent ? props.teamId : undefined;
|
||||
const teamId: number | undefined = props.isTeamEvent
|
||||
? props.teamId
|
||||
: undefined;
|
||||
const [_bookerState, setBookerState] = useBookerStoreContext(
|
||||
(state) => [state.state, state.setState],
|
||||
shallow
|
||||
);
|
||||
const setSelectedDate = useBookerStoreContext((state) => state.setSelectedDate);
|
||||
const setSelectedDuration = useBookerStoreContext((state) => state.setSelectedDuration);
|
||||
const setSelectedDate = useBookerStoreContext(
|
||||
(state) => state.setSelectedDate
|
||||
);
|
||||
const setSelectedDuration = useBookerStoreContext(
|
||||
(state) => state.setSelectedDuration
|
||||
);
|
||||
const setBookingData = useBookerStoreContext((state) => state.setBookingData);
|
||||
const setOrg = useBookerStoreContext((state) => state.setOrg);
|
||||
const bookingData = useBookerStoreContext((state) => state.bookingData);
|
||||
const setSelectedTimeslot = useBookerStoreContext((state) => state.setSelectedTimeslot);
|
||||
const setSelectedTimeslot = useBookerStoreContext(
|
||||
(state) => state.setSelectedTimeslot
|
||||
);
|
||||
const setSelectedMonth = useBookerStoreContext((state) => state.setMonth);
|
||||
const selectedDuration = useBookerStoreContext((state) => state.selectedDuration);
|
||||
const selectedDuration = useBookerStoreContext(
|
||||
(state) => state.selectedDuration
|
||||
);
|
||||
|
||||
const [isOverlayCalendarEnabled, setIsOverlayCalendarEnabled] = useState(
|
||||
Boolean(localStorage?.getItem?.("overlayCalendarSwitchDefault"))
|
||||
@@ -102,9 +121,14 @@ const BookerPlatformWrapperComponent = (
|
||||
}, []);
|
||||
const debouncedStateChange = useMemo(() => {
|
||||
return debounce(
|
||||
(currentStateValues: BookerStoreValues, callback: (values: BookerStoreValues) => void) => {
|
||||
(
|
||||
currentStateValues: BookerStoreValues,
|
||||
callback: (values: BookerStoreValues) => void
|
||||
) => {
|
||||
const prevState = prevStateRef.current;
|
||||
const stateChanged = !prevState || JSON.stringify(prevState) !== JSON.stringify(currentStateValues);
|
||||
const stateChanged =
|
||||
!prevState ||
|
||||
JSON.stringify(prevState) !== JSON.stringify(currentStateValues);
|
||||
|
||||
if (stateChanged) {
|
||||
callback(currentStateValues);
|
||||
@@ -132,7 +156,12 @@ const BookerPlatformWrapperComponent = (
|
||||
unsubscribe();
|
||||
debouncedStateChange.cancel();
|
||||
};
|
||||
}, [onBookerStateChange, getStateValues, debouncedStateChange, bookerStoreContext]);
|
||||
}, [
|
||||
onBookerStateChange,
|
||||
getStateValues,
|
||||
debouncedStateChange,
|
||||
bookerStoreContext,
|
||||
]);
|
||||
|
||||
useGetBookingForReschedule({
|
||||
uid: props.rescheduleUid ?? props.bookingUid ?? "",
|
||||
@@ -141,12 +170,18 @@ const BookerPlatformWrapperComponent = (
|
||||
},
|
||||
});
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const username = useMemo(() => {
|
||||
// when rescheduling, prefer the booking host's username from bookingData
|
||||
// this ensures we fetch the correct event type even when an org admin reschedules
|
||||
if (bookingData?.user?.username) {
|
||||
return formatUsername(bookingData.user.username);
|
||||
}
|
||||
if (props.username) {
|
||||
return formatUsername(props.username);
|
||||
}
|
||||
return "";
|
||||
}, [props.username]);
|
||||
}, [props.username, bookingData?.user?.username]);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedDuration(props.duration ?? null);
|
||||
@@ -205,7 +240,10 @@ const BookerPlatformWrapperComponent = (
|
||||
allowUpdatingUrlParams,
|
||||
defaultPhoneCountry,
|
||||
});
|
||||
const [dayCount] = useBookerStoreContext((state) => [state.dayCount, state.setDayCount], shallow);
|
||||
const [dayCount] = useBookerStoreContext(
|
||||
(state) => [state.dayCount, state.setDayCount],
|
||||
shallow
|
||||
);
|
||||
const selectedDate = useBookerStoreContext((state) => state.selectedDate);
|
||||
|
||||
const month = useBookerStoreContext((state) => state.month);
|
||||
@@ -213,7 +251,11 @@ const BookerPlatformWrapperComponent = (
|
||||
|
||||
const { data: session } = useMe();
|
||||
const hasSession = !!session;
|
||||
const { name: defaultName, guests: defaultGuests, ...restFormValues } = props.defaultFormValues ?? {};
|
||||
const {
|
||||
name: defaultName,
|
||||
guests: defaultGuests,
|
||||
...restFormValues
|
||||
} = props.defaultFormValues ?? {};
|
||||
|
||||
const prefillFormParamName = useMemo(() => {
|
||||
if (defaultName) {
|
||||
@@ -247,7 +289,8 @@ const BookerPlatformWrapperComponent = (
|
||||
});
|
||||
|
||||
const startTime =
|
||||
customStartTime && dayjs(customStartTime).isAfter(dayjs(calculatedStartTime))
|
||||
customStartTime &&
|
||||
dayjs(customStartTime).isAfter(dayjs(calculatedStartTime))
|
||||
? dayjs(customStartTime).toISOString()
|
||||
: calculatedStartTime;
|
||||
const endTime = calculatedEndTime;
|
||||
@@ -263,8 +306,10 @@ const BookerPlatformWrapperComponent = (
|
||||
? new URLSearchParams(routingFormSearchParams)
|
||||
: new URLSearchParams(window.location.search);
|
||||
|
||||
const routedTeamMemberIds = getRoutedTeamMemberIdsFromSearchParams(searchParams);
|
||||
const skipContactOwner = searchParams.get("cal.skipContactOwner") === "true";
|
||||
const routedTeamMemberIds =
|
||||
getRoutedTeamMemberIdsFromSearchParams(searchParams);
|
||||
const skipContactOwner =
|
||||
searchParams.get("cal.skipContactOwner") === "true";
|
||||
|
||||
const isBookingDryRun =
|
||||
searchParams?.get("cal.isBookingDryRun")?.toLowerCase() === "true" ||
|
||||
@@ -304,7 +349,12 @@ const BookerPlatformWrapperComponent = (
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (schedule.data && !schedule.isPending && !schedule.error && onTimeslotsLoaded) {
|
||||
if (
|
||||
schedule.data &&
|
||||
!schedule.isPending &&
|
||||
!schedule.error &&
|
||||
onTimeslotsLoaded
|
||||
) {
|
||||
onTimeslotsLoaded(schedule.data.slots);
|
||||
}
|
||||
}, [schedule.data, schedule.isPending, schedule.error, onTimeslotsLoaded]);
|
||||
@@ -379,14 +429,17 @@ const BookerPlatformWrapperComponent = (
|
||||
onReserveSlotError: props.onReserveSlotError,
|
||||
onDeleteSlotSuccess: props.onDeleteSlotSuccess,
|
||||
onDeleteSlotError: props.onDeleteSlotError,
|
||||
isBookingDryRun: props.isBookingDryRun ? props.isBookingDryRun : routingParams?.isBookingDryRun,
|
||||
isBookingDryRun: props.isBookingDryRun
|
||||
? props.isBookingDryRun
|
||||
: routingParams?.isBookingDryRun,
|
||||
handleSlotReservation,
|
||||
});
|
||||
|
||||
const verifyEmail = useVerifyEmail({
|
||||
email: bookerForm.formEmail,
|
||||
name: bookerForm.formName,
|
||||
requiresBookerEmailVerification: event?.data?.requiresBookerEmailVerification,
|
||||
requiresBookerEmailVerification:
|
||||
event?.data?.requiresBookerEmailVerification,
|
||||
onVerifyEmail: bookerForm.beforeVerifyEmail,
|
||||
});
|
||||
|
||||
@@ -400,9 +453,10 @@ const BookerPlatformWrapperComponent = (
|
||||
},
|
||||
});
|
||||
|
||||
const { data: connectedCalendars, isPending: fetchingConnectedCalendars } = useConnectedCalendars({
|
||||
enabled: hasSession,
|
||||
});
|
||||
const { data: connectedCalendars, isPending: fetchingConnectedCalendars } =
|
||||
useConnectedCalendars({
|
||||
enabled: hasSession,
|
||||
});
|
||||
const calendars = connectedCalendars as ConnectedDestinationCalendars;
|
||||
|
||||
const { set, clearSet } = useLocalSet<{
|
||||
@@ -423,7 +477,11 @@ const BookerPlatformWrapperComponent = (
|
||||
onError: () => {
|
||||
clearSet();
|
||||
},
|
||||
enabled: Boolean(hasSession && isOverlayCalendarEnabled && latestCalendarsToLoad?.length > 0),
|
||||
enabled: Boolean(
|
||||
hasSession &&
|
||||
isOverlayCalendarEnabled &&
|
||||
latestCalendarsToLoad?.length > 0
|
||||
),
|
||||
});
|
||||
|
||||
const handleBookEvent = useHandleBookEvent({
|
||||
@@ -490,7 +548,9 @@ const BookerPlatformWrapperComponent = (
|
||||
if (isOverlayCalendarEnabled && view === "MONTH_VIEW") {
|
||||
localStorage?.removeItem("overlayCalendarSwitchDefault");
|
||||
}
|
||||
setIsOverlayCalendarEnabled(Boolean(localStorage?.getItem?.("overlayCalendarSwitchDefault")));
|
||||
setIsOverlayCalendarEnabled(
|
||||
Boolean(localStorage?.getItem?.("overlayCalendarSwitchDefault"))
|
||||
);
|
||||
}, [view, isOverlayCalendarEnabled]);
|
||||
|
||||
return (
|
||||
@@ -542,8 +602,14 @@ const BookerPlatformWrapperComponent = (
|
||||
bookingForm: bookerForm.bookingForm,
|
||||
bookerFormErrorRef: bookerForm.bookerFormErrorRef,
|
||||
errors: {
|
||||
hasDataErrors: isCreateBookingError || isCreateRecBookingError || isCreateInstantBookingError,
|
||||
dataErrors: createBookingError || createRecBookingError || createInstantBookingError,
|
||||
hasDataErrors:
|
||||
isCreateBookingError ||
|
||||
isCreateRecBookingError ||
|
||||
isCreateInstantBookingError,
|
||||
dataErrors:
|
||||
createBookingError ||
|
||||
createRecBookingError ||
|
||||
createInstantBookingError,
|
||||
},
|
||||
loadingStates: {
|
||||
creatingBooking: creatingBooking,
|
||||
@@ -569,7 +635,10 @@ const BookerPlatformWrapperComponent = (
|
||||
event={event}
|
||||
schedule={schedule}
|
||||
orgBannerUrl={bannerUrl ?? event.data?.bannerUrl}
|
||||
bookerLayout={{ ...bookerLayout, hideEventTypeDetails: hideEventMetadata }}
|
||||
bookerLayout={{
|
||||
...bookerLayout,
|
||||
hideEventTypeDetails: hideEventMetadata,
|
||||
}}
|
||||
verifyCode={verifyCode}
|
||||
isPlatform
|
||||
hasValidLicense={true}
|
||||
@@ -582,7 +651,9 @@ const BookerPlatformWrapperComponent = (
|
||||
};
|
||||
|
||||
export const BookerPlatformWrapper = (
|
||||
props: BookerPlatformWrapperAtomPropsForIndividual | BookerPlatformWrapperAtomPropsForTeam
|
||||
props:
|
||||
| BookerPlatformWrapperAtomPropsForIndividual
|
||||
| BookerPlatformWrapperAtomPropsForTeam
|
||||
) => {
|
||||
return (
|
||||
<BookerStoreProvider>
|
||||
|
||||
Reference in New Issue
Block a user