fix: seated event type public id cancels/reschedule entire booking without auth (#11667)
Co-authored-by: Keith Williams <keithwillcode@gmail.com>pull/11740/head
parent
a90848edcb
commit
b4315b186a
|
@ -2,6 +2,7 @@ import type { GetServerSidePropsContext } from "next";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { Booker } from "@calcom/atoms";
|
import { Booker } from "@calcom/atoms";
|
||||||
|
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||||
import { getBookerWrapperClasses } from "@calcom/features/bookings/Booker/utils/getBookerWrapperClasses";
|
import { getBookerWrapperClasses } from "@calcom/features/bookings/Booker/utils/getBookerWrapperClasses";
|
||||||
import { BookerSeo } from "@calcom/features/bookings/components/BookerSeo";
|
import { BookerSeo } from "@calcom/features/bookings/components/BookerSeo";
|
||||||
import {
|
import {
|
||||||
|
@ -43,6 +44,7 @@ export default function Type({
|
||||||
hideBranding={isBrandingHidden}
|
hideBranding={isBrandingHidden}
|
||||||
isSEOIndexable={isSEOIndexable ?? true}
|
isSEOIndexable={isSEOIndexable ?? true}
|
||||||
entity={entity}
|
entity={entity}
|
||||||
|
bookingData={booking}
|
||||||
/>
|
/>
|
||||||
<Booker
|
<Booker
|
||||||
username={user}
|
username={user}
|
||||||
|
@ -61,6 +63,7 @@ Type.isBookingPage = true;
|
||||||
Type.PageWrapper = PageWrapper;
|
Type.PageWrapper = PageWrapper;
|
||||||
|
|
||||||
async function getDynamicGroupPageProps(context: GetServerSidePropsContext) {
|
async function getDynamicGroupPageProps(context: GetServerSidePropsContext) {
|
||||||
|
const session = await getServerSession(context);
|
||||||
const { user: usernames, type: slug } = paramsSchema.parse(context.params);
|
const { user: usernames, type: slug } = paramsSchema.parse(context.params);
|
||||||
const { rescheduleUid, bookingUid, duration: queryDuration } = context.query;
|
const { rescheduleUid, bookingUid, duration: queryDuration } = context.query;
|
||||||
|
|
||||||
|
@ -96,7 +99,7 @@ async function getDynamicGroupPageProps(context: GetServerSidePropsContext) {
|
||||||
|
|
||||||
let booking: GetBookingType | null = null;
|
let booking: GetBookingType | null = null;
|
||||||
if (rescheduleUid) {
|
if (rescheduleUid) {
|
||||||
booking = await getBookingForReschedule(`${rescheduleUid}`);
|
booking = await getBookingForReschedule(`${rescheduleUid}`, session?.user?.id);
|
||||||
} else if (bookingUid) {
|
} else if (bookingUid) {
|
||||||
booking = await getBookingForSeatedEvent(`${bookingUid}`);
|
booking = await getBookingForSeatedEvent(`${bookingUid}`);
|
||||||
}
|
}
|
||||||
|
@ -138,6 +141,7 @@ async function getDynamicGroupPageProps(context: GetServerSidePropsContext) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getUserPageProps(context: GetServerSidePropsContext) {
|
async function getUserPageProps(context: GetServerSidePropsContext) {
|
||||||
|
const session = await getServerSession(context);
|
||||||
const { user: usernames, type: slug } = paramsSchema.parse(context.params);
|
const { user: usernames, type: slug } = paramsSchema.parse(context.params);
|
||||||
const username = usernames[0];
|
const username = usernames[0];
|
||||||
const { rescheduleUid, bookingUid, duration: queryDuration } = context.query;
|
const { rescheduleUid, bookingUid, duration: queryDuration } = context.query;
|
||||||
|
@ -168,7 +172,7 @@ async function getUserPageProps(context: GetServerSidePropsContext) {
|
||||||
|
|
||||||
let booking: GetBookingType | null = null;
|
let booking: GetBookingType | null = null;
|
||||||
if (rescheduleUid) {
|
if (rescheduleUid) {
|
||||||
booking = await getBookingForReschedule(`${rescheduleUid}`);
|
booking = await getBookingForReschedule(`${rescheduleUid}`, session?.user?.id);
|
||||||
} else if (bookingUid) {
|
} else if (bookingUid) {
|
||||||
booking = await getBookingForSeatedEvent(`${bookingUid}`);
|
booking = await getBookingForSeatedEvent(`${bookingUid}`);
|
||||||
}
|
}
|
||||||
|
|
|
@ -146,7 +146,7 @@ export default function Success(props: SuccessProps) {
|
||||||
const shouldAlignCentrallyInEmbed = useEmbedNonStylesConfig("align") !== "left";
|
const shouldAlignCentrallyInEmbed = useEmbedNonStylesConfig("align") !== "left";
|
||||||
const shouldAlignCentrally = !isEmbed || shouldAlignCentrallyInEmbed;
|
const shouldAlignCentrally = !isEmbed || shouldAlignCentrallyInEmbed;
|
||||||
const [calculatedDuration, setCalculatedDuration] = useState<number | undefined>(undefined);
|
const [calculatedDuration, setCalculatedDuration] = useState<number | undefined>(undefined);
|
||||||
|
const { requiresLoginToUpdate } = props;
|
||||||
function setIsCancellationMode(value: boolean) {
|
function setIsCancellationMode(value: boolean) {
|
||||||
const _searchParams = new URLSearchParams(searchParams);
|
const _searchParams = new URLSearchParams(searchParams);
|
||||||
|
|
||||||
|
@ -532,7 +532,28 @@ export default function Success(props: SuccessProps) {
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{(!needsConfirmation || !userIsOwner) &&
|
{requiresLoginToUpdate && (
|
||||||
|
<>
|
||||||
|
<hr className="border-subtle mb-8" />
|
||||||
|
<div className="text-center">
|
||||||
|
<span className="text-emphasis ltr:mr-2 rtl:ml-2">{t("need_to_make_a_change")}</span>
|
||||||
|
{/* Login button but redirect to here */}
|
||||||
|
<span className="text-default inline">
|
||||||
|
<span className="underline" data-testid="reschedule-link">
|
||||||
|
<Link
|
||||||
|
href={`/auth/login?callbackUrl=${encodeURIComponent(
|
||||||
|
`/booking/${bookingInfo?.uid}`
|
||||||
|
)}`}
|
||||||
|
legacyBehavior>
|
||||||
|
{t("login")}
|
||||||
|
</Link>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{!requiresLoginToUpdate &&
|
||||||
|
(!needsConfirmation || !userIsOwner) &&
|
||||||
!isCancelled &&
|
!isCancelled &&
|
||||||
(!isCancellationMode ? (
|
(!isCancellationMode ? (
|
||||||
<>
|
<>
|
||||||
|
@ -540,28 +561,30 @@ export default function Success(props: SuccessProps) {
|
||||||
<div className="text-center last:pb-0">
|
<div className="text-center last:pb-0">
|
||||||
<span className="text-emphasis ltr:mr-2 rtl:ml-2">{t("need_to_make_a_change")}</span>
|
<span className="text-emphasis ltr:mr-2 rtl:ml-2">{t("need_to_make_a_change")}</span>
|
||||||
|
|
||||||
{!props.recurringBookings && (
|
<>
|
||||||
<span className="text-default inline">
|
{!props.recurringBookings && (
|
||||||
<span className="underline" data-testid="reschedule-link">
|
<span className="text-default inline">
|
||||||
<Link
|
<span className="underline" data-testid="reschedule-link">
|
||||||
href={`/reschedule/${seatReferenceUid || bookingInfo?.uid}`}
|
<Link
|
||||||
legacyBehavior>
|
href={`/reschedule/${seatReferenceUid || bookingInfo?.uid}`}
|
||||||
{t("reschedule")}
|
legacyBehavior>
|
||||||
</Link>
|
{t("reschedule")}
|
||||||
|
</Link>
|
||||||
|
</span>
|
||||||
|
<span className="mx-2">{t("or_lowercase")}</span>
|
||||||
</span>
|
</span>
|
||||||
<span className="mx-2">{t("or_lowercase")}</span>
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<button
|
|
||||||
data-testid="cancel"
|
|
||||||
className={classNames(
|
|
||||||
"text-default underline",
|
|
||||||
props.recurringBookings && "ltr:mr-2 rtl:ml-2"
|
|
||||||
)}
|
)}
|
||||||
onClick={() => setIsCancellationMode(true)}>
|
|
||||||
{t("cancel")}
|
<button
|
||||||
</button>
|
data-testid="cancel"
|
||||||
|
className={classNames(
|
||||||
|
"text-default underline",
|
||||||
|
props.recurringBookings && "ltr:mr-2 rtl:ml-2"
|
||||||
|
)}
|
||||||
|
onClick={() => setIsCancellationMode(true)}>
|
||||||
|
{t("cancel")}
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
@ -1010,7 +1033,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||||
const session = await getServerSession(context);
|
const session = await getServerSession(context);
|
||||||
let tz: string | null = null;
|
let tz: string | null = null;
|
||||||
let userTimeFormat: number | null = null;
|
let userTimeFormat: number | null = null;
|
||||||
|
let requiresLoginToUpdate = false;
|
||||||
if (session) {
|
if (session) {
|
||||||
const user = await ssr.viewer.me.fetch();
|
const user = await ssr.viewer.me.fetch();
|
||||||
tz = user.timeZone;
|
tz = user.timeZone;
|
||||||
|
@ -1022,9 +1045,10 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||||
if (!parsedQuery.success) return { notFound: true };
|
if (!parsedQuery.success) return { notFound: true };
|
||||||
const { uid, eventTypeSlug, seatReferenceUid } = parsedQuery.data;
|
const { uid, eventTypeSlug, seatReferenceUid } = parsedQuery.data;
|
||||||
|
|
||||||
|
const { uid: maybeUid } = await maybeGetBookingUidFromSeat(prisma, uid);
|
||||||
const bookingInfoRaw = await prisma.booking.findFirst({
|
const bookingInfoRaw = await prisma.booking.findFirst({
|
||||||
where: {
|
where: {
|
||||||
uid: await maybeGetBookingUidFromSeat(prisma, uid),
|
uid: maybeUid,
|
||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
title: true,
|
title: true,
|
||||||
|
@ -1088,6 +1112,10 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (eventTypeRaw.seatsPerTimeSlot && !seatReferenceUid && !session) {
|
||||||
|
requiresLoginToUpdate = true;
|
||||||
|
}
|
||||||
|
|
||||||
const bookingInfo = getBookingWithResponses(bookingInfoRaw);
|
const bookingInfo = getBookingWithResponses(bookingInfoRaw);
|
||||||
// @NOTE: had to do this because Server side cant return [Object objects]
|
// @NOTE: had to do this because Server side cant return [Object objects]
|
||||||
// probably fixable with json.stringify -> json.parse
|
// probably fixable with json.stringify -> json.parse
|
||||||
|
@ -1156,6 +1184,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||||
paymentStatus: payment,
|
paymentStatus: payment,
|
||||||
...(tz && { tz }),
|
...(tz && { tz }),
|
||||||
userTimeFormat,
|
userTimeFormat,
|
||||||
|
requiresLoginToUpdate,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ import type { GetServerSidePropsContext } from "next";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { Booker } from "@calcom/atoms";
|
import { Booker } from "@calcom/atoms";
|
||||||
|
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||||
import { getBookerWrapperClasses } from "@calcom/features/bookings/Booker/utils/getBookerWrapperClasses";
|
import { getBookerWrapperClasses } from "@calcom/features/bookings/Booker/utils/getBookerWrapperClasses";
|
||||||
import { BookerSeo } from "@calcom/features/bookings/components/BookerSeo";
|
import { BookerSeo } from "@calcom/features/bookings/components/BookerSeo";
|
||||||
import { getBookingForReschedule, getMultipleDurationValue } from "@calcom/features/bookings/lib/get-booking";
|
import { getBookingForReschedule, getMultipleDurationValue } from "@calcom/features/bookings/lib/get-booking";
|
||||||
|
@ -57,6 +58,7 @@ Type.PageWrapper = PageWrapper;
|
||||||
Type.isBookingPage = true;
|
Type.isBookingPage = true;
|
||||||
|
|
||||||
async function getUserPageProps(context: GetServerSidePropsContext) {
|
async function getUserPageProps(context: GetServerSidePropsContext) {
|
||||||
|
const session = await getServerSession(context);
|
||||||
const { link, slug } = paramsSchema.parse(context.params);
|
const { link, slug } = paramsSchema.parse(context.params);
|
||||||
const { rescheduleUid, duration: queryDuration } = context.query;
|
const { rescheduleUid, duration: queryDuration } = context.query;
|
||||||
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(context.req.headers.host ?? "");
|
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(context.req.headers.host ?? "");
|
||||||
|
@ -119,7 +121,7 @@ async function getUserPageProps(context: GetServerSidePropsContext) {
|
||||||
|
|
||||||
let booking: GetBookingType | null = null;
|
let booking: GetBookingType | null = null;
|
||||||
if (rescheduleUid) {
|
if (rescheduleUid) {
|
||||||
booking = await getBookingForReschedule(`${rescheduleUid}`);
|
booking = await getBookingForReschedule(`${rescheduleUid}`, session?.user?.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
const isTeamEvent = !!hashedLink.eventType?.team?.id;
|
const isTeamEvent = !!hashedLink.eventType?.team?.id;
|
||||||
|
|
|
@ -2,6 +2,7 @@ import type { GetServerSidePropsContext } from "next";
|
||||||
import { URLSearchParams } from "url";
|
import { URLSearchParams } from "url";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||||
import { getDefaultEvent } from "@calcom/lib/defaultEvents";
|
import { getDefaultEvent } from "@calcom/lib/defaultEvents";
|
||||||
import { maybeGetBookingUidFromSeat } from "@calcom/lib/server/maybeGetBookingUidFromSeat";
|
import { maybeGetBookingUidFromSeat } from "@calcom/lib/server/maybeGetBookingUidFromSeat";
|
||||||
import prisma, { bookingMinimalSelect } from "@calcom/prisma";
|
import prisma, { bookingMinimalSelect } from "@calcom/prisma";
|
||||||
|
@ -12,11 +13,16 @@ export default function Type() {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getServerSideProps(context: GetServerSidePropsContext) {
|
export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||||
const { uid: bookingId, seatReferenceUid } = z
|
const session = await getServerSession(context);
|
||||||
|
|
||||||
|
const { uid: bookingUid, seatReferenceUid } = z
|
||||||
.object({ uid: z.string(), seatReferenceUid: z.string().optional() })
|
.object({ uid: z.string(), seatReferenceUid: z.string().optional() })
|
||||||
.parse(context.query);
|
.parse(context.query);
|
||||||
|
|
||||||
const uid = await maybeGetBookingUidFromSeat(prisma, bookingId);
|
const { uid, seatReferenceUid: maybeSeatReferenceUid } = await maybeGetBookingUidFromSeat(
|
||||||
|
prisma,
|
||||||
|
bookingUid
|
||||||
|
);
|
||||||
const booking = await prisma.booking.findUnique({
|
const booking = await prisma.booking.findUnique({
|
||||||
where: {
|
where: {
|
||||||
uid,
|
uid,
|
||||||
|
@ -37,6 +43,21 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
seatsPerTimeSlot: true,
|
seatsPerTimeSlot: true,
|
||||||
|
userId: true,
|
||||||
|
owner: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
hosts: {
|
||||||
|
select: {
|
||||||
|
user: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
dynamicEventSlugRef: true,
|
dynamicEventSlugRef: true,
|
||||||
|
@ -53,7 +74,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!booking?.eventType && !booking?.dynamicEventSlugRef) {
|
if (!booking?.eventType && !booking?.dynamicEventSlugRef) {
|
||||||
// TODO: Show something in UI to let user know that this booking is not rescheduleable.
|
// TODO: Show something in UI to let user know that this booking is not rescheduleable
|
||||||
return {
|
return {
|
||||||
notFound: true,
|
notFound: true,
|
||||||
} as {
|
} as {
|
||||||
|
@ -61,6 +82,33 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if booking event type is for a seated event and no seat reference uid is provided, throw not found
|
||||||
|
if (booking?.eventType?.seatsPerTimeSlot && !maybeSeatReferenceUid) {
|
||||||
|
const userId = session?.user?.id;
|
||||||
|
|
||||||
|
if (!userId && !seatReferenceUid) {
|
||||||
|
return {
|
||||||
|
redirect: {
|
||||||
|
destination: `/auth/login?callbackUrl=/reschedule/${bookingUid}`,
|
||||||
|
permanent: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const userIsHost = booking?.eventType.hosts.find((host) => {
|
||||||
|
if (host.user.id === userId) return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
const userIsOwnerOfEventType = booking?.eventType.owner?.id === userId;
|
||||||
|
|
||||||
|
if (!userIsHost && !userIsOwnerOfEventType) {
|
||||||
|
return {
|
||||||
|
notFound: true,
|
||||||
|
} as {
|
||||||
|
notFound: true;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const eventType = booking.eventType ? booking.eventType : getDefaultEvent(dynamicEventSlugRef);
|
const eventType = booking.eventType ? booking.eventType : getDefaultEvent(dynamicEventSlugRef);
|
||||||
|
|
||||||
const eventPage = `${
|
const eventPage = `${
|
||||||
|
@ -72,7 +120,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||||
}/${eventType?.slug}`;
|
}/${eventType?.slug}`;
|
||||||
const destinationUrl = new URLSearchParams();
|
const destinationUrl = new URLSearchParams();
|
||||||
|
|
||||||
destinationUrl.set("rescheduleUid", seatReferenceUid || bookingId);
|
destinationUrl.set("rescheduleUid", seatReferenceUid || bookingUid);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
|
|
|
@ -2,6 +2,7 @@ import type { GetServerSidePropsContext } from "next";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { Booker } from "@calcom/atoms";
|
import { Booker } from "@calcom/atoms";
|
||||||
|
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||||
import { getBookerWrapperClasses } from "@calcom/features/bookings/Booker/utils/getBookerWrapperClasses";
|
import { getBookerWrapperClasses } from "@calcom/features/bookings/Booker/utils/getBookerWrapperClasses";
|
||||||
import { BookerSeo } from "@calcom/features/bookings/components/BookerSeo";
|
import { BookerSeo } from "@calcom/features/bookings/components/BookerSeo";
|
||||||
import { getBookingForReschedule, getMultipleDurationValue } from "@calcom/features/bookings/lib/get-booking";
|
import { getBookingForReschedule, getMultipleDurationValue } from "@calcom/features/bookings/lib/get-booking";
|
||||||
|
@ -37,6 +38,7 @@ export default function Type({
|
||||||
hideBranding={isBrandingHidden}
|
hideBranding={isBrandingHidden}
|
||||||
isTeamEvent
|
isTeamEvent
|
||||||
entity={entity}
|
entity={entity}
|
||||||
|
bookingData={booking}
|
||||||
/>
|
/>
|
||||||
<Booker
|
<Booker
|
||||||
username={user}
|
username={user}
|
||||||
|
@ -64,6 +66,7 @@ const paramsSchema = z.object({
|
||||||
// 1. Check if team exists, to show 404
|
// 1. Check if team exists, to show 404
|
||||||
// 2. If rescheduling, get the booking details
|
// 2. If rescheduling, get the booking details
|
||||||
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||||
|
const session = await getServerSession(context);
|
||||||
const { slug: teamSlug, type: meetingSlug } = paramsSchema.parse(context.params);
|
const { slug: teamSlug, type: meetingSlug } = paramsSchema.parse(context.params);
|
||||||
const { rescheduleUid, duration: queryDuration } = context.query;
|
const { rescheduleUid, duration: queryDuration } = context.query;
|
||||||
const { ssrInit } = await import("@server/lib/ssr");
|
const { ssrInit } = await import("@server/lib/ssr");
|
||||||
|
@ -92,7 +95,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||||
|
|
||||||
let booking: GetBookingType | null = null;
|
let booking: GetBookingType | null = null;
|
||||||
if (rescheduleUid) {
|
if (rescheduleUid) {
|
||||||
booking = await getBookingForReschedule(`${rescheduleUid}`);
|
booking = await getBookingForReschedule(`${rescheduleUid}`, session?.user?.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
const org = isValidOrgDomain ? currentOrgDomain : null;
|
const org = isValidOrgDomain ? currentOrgDomain : null;
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { expect } from "@playwright/test";
|
||||||
import { uuid } from "short-uuid";
|
import { uuid } from "short-uuid";
|
||||||
import { v4 as uuidv4 } from "uuid";
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
|
||||||
|
import { randomString } from "@calcom/lib/random";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
import { BookingStatus } from "@calcom/prisma/enums";
|
import { BookingStatus } from "@calcom/prisma/enums";
|
||||||
|
|
||||||
|
@ -96,7 +97,7 @@ test.describe("Booking with Seats", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test(`Attendees can cancel a seated event time slot`, async ({ page, users, bookings }) => {
|
test(`Attendees can cancel a seated event time slot`, async ({ page, users, bookings }) => {
|
||||||
const { booking } = await createUserWithSeatedEventAndAttendees({ users, bookings }, [
|
const { booking, user } = await createUserWithSeatedEventAndAttendees({ users, bookings }, [
|
||||||
{ name: "John First", email: "first+seats@cal.com", timeZone: "Europe/Berlin" },
|
{ name: "John First", email: "first+seats@cal.com", timeZone: "Europe/Berlin" },
|
||||||
{ name: "Jane Second", email: "second+seats@cal.com", timeZone: "Europe/Berlin" },
|
{ name: "Jane Second", email: "second+seats@cal.com", timeZone: "Europe/Berlin" },
|
||||||
{ name: "John Third", email: "third+seats@cal.com", timeZone: "Europe/Berlin" },
|
{ name: "John Third", email: "third+seats@cal.com", timeZone: "Europe/Berlin" },
|
||||||
|
@ -143,6 +144,19 @@ test.describe("Booking with Seats", () => {
|
||||||
expect(attendeeIds).not.toContain(bookingAttendees[0].id);
|
expect(attendeeIds).not.toContain(bookingAttendees[0].id);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await test.step("Attendee #2 shouldn't be able to cancel booking using only booking/uid", async () => {
|
||||||
|
await page.goto(`/booking/${booking.uid}`);
|
||||||
|
|
||||||
|
await expect(page.locator("[text=Cancel]")).toHaveCount(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step("Attendee #2 shouldn't be able to cancel booking using randomString for seatReferenceUId", async () => {
|
||||||
|
await page.goto(`/booking/${booking.uid}?seatReferenceUid=${randomString(10)}`);
|
||||||
|
|
||||||
|
// expect cancel button to don't be in the page
|
||||||
|
await expect(page.locator("[text=Cancel]")).toHaveCount(0);
|
||||||
|
});
|
||||||
|
|
||||||
await test.step("All attendees cancelling should delete the booking for the user", async () => {
|
await test.step("All attendees cancelling should delete the booking for the user", async () => {
|
||||||
// The remaining 2 attendees cancel
|
// The remaining 2 attendees cancel
|
||||||
for (let i = 1; i < bookingSeats.length; i++) {
|
for (let i = 1; i < bookingSeats.length; i++) {
|
||||||
|
@ -166,6 +180,47 @@ test.describe("Booking with Seats", () => {
|
||||||
expect(updatedBooking?.status).toBe(BookingStatus.CANCELLED);
|
expect(updatedBooking?.status).toBe(BookingStatus.CANCELLED);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("Owner shouldn't be able to cancel booking without login in", async ({ page, bookings, users }) => {
|
||||||
|
const { booking, user } = await createUserWithSeatedEventAndAttendees({ users, bookings }, [
|
||||||
|
{ name: "John First", email: "first+seats@cal.com", timeZone: "Europe/Berlin" },
|
||||||
|
{ name: "Jane Second", email: "second+seats@cal.com", timeZone: "Europe/Berlin" },
|
||||||
|
{ name: "John Third", email: "third+seats@cal.com", timeZone: "Europe/Berlin" },
|
||||||
|
]);
|
||||||
|
await page.goto(`/booking/${booking.uid}?cancel=true`);
|
||||||
|
await expect(page.locator("[text=Cancel]")).toHaveCount(0);
|
||||||
|
|
||||||
|
// expect login text to be in the page, not data-testid
|
||||||
|
await expect(page.locator("text=Login")).toHaveCount(1);
|
||||||
|
|
||||||
|
// click on login button text
|
||||||
|
await page.locator("text=Login").click();
|
||||||
|
|
||||||
|
// expect to be redirected to login page with query parameter callbackUrl
|
||||||
|
await expect(page).toHaveURL(/\/auth\/login\?callbackUrl=.*/);
|
||||||
|
|
||||||
|
await user.apiLogin();
|
||||||
|
|
||||||
|
// manual redirect to booking page
|
||||||
|
await page.goto(`/booking/${booking.uid}?cancel=true`);
|
||||||
|
|
||||||
|
// expect login button to don't be in the page
|
||||||
|
await expect(page.locator("text=Login")).toHaveCount(0);
|
||||||
|
|
||||||
|
// fill reason for cancellation
|
||||||
|
await page.fill('[data-testid="cancel_reason"]', "Double booked!");
|
||||||
|
|
||||||
|
// confirm cancellation
|
||||||
|
await page.locator('[data-testid="confirm_cancel"]').click();
|
||||||
|
await page.waitForLoadState("networkidle");
|
||||||
|
|
||||||
|
const updatedBooking = await prisma.booking.findFirst({
|
||||||
|
where: { id: booking.id },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(updatedBooking).not.toBeNull();
|
||||||
|
expect(updatedBooking?.status).toBe(BookingStatus.CANCELLED);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test.describe("Reschedule for booking with seats", () => {
|
test.describe("Reschedule for booking with seats", () => {
|
||||||
|
@ -543,4 +598,113 @@ test.describe("Reschedule for booking with seats", () => {
|
||||||
.first();
|
.first();
|
||||||
await expect(foundFirstAttendeeAgain).toHaveCount(1);
|
await expect(foundFirstAttendeeAgain).toHaveCount(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("Owner shouldn't be able to reschedule booking without login in", async ({
|
||||||
|
page,
|
||||||
|
bookings,
|
||||||
|
users,
|
||||||
|
}) => {
|
||||||
|
const { booking, user } = await createUserWithSeatedEventAndAttendees({ users, bookings }, [
|
||||||
|
{ name: "John First", email: "first+seats@cal.com", timeZone: "Europe/Berlin" },
|
||||||
|
{ name: "Jane Second", email: "second+seats@cal.com", timeZone: "Europe/Berlin" },
|
||||||
|
{ name: "John Third", email: "third+seats@cal.com", timeZone: "Europe/Berlin" },
|
||||||
|
]);
|
||||||
|
const getBooking = await booking.self();
|
||||||
|
|
||||||
|
await page.goto(`/booking/${booking.uid}`);
|
||||||
|
await expect(page.locator('[data-testid="reschedule"]')).toHaveCount(0);
|
||||||
|
|
||||||
|
// expect login text to be in the page, not data-testid
|
||||||
|
await expect(page.locator("text=Login")).toHaveCount(1);
|
||||||
|
|
||||||
|
// click on login button text
|
||||||
|
await page.locator("text=Login").click();
|
||||||
|
|
||||||
|
// expect to be redirected to login page with query parameter callbackUrl
|
||||||
|
await expect(page).toHaveURL(/\/auth\/login\?callbackUrl=.*/);
|
||||||
|
|
||||||
|
await user.apiLogin();
|
||||||
|
|
||||||
|
// manual redirect to booking page
|
||||||
|
await page.goto(`/booking/${booking.uid}`);
|
||||||
|
|
||||||
|
// expect login button to don't be in the page
|
||||||
|
await expect(page.locator("text=Login")).toHaveCount(0);
|
||||||
|
|
||||||
|
// reschedule-link click
|
||||||
|
await page.locator('[data-testid="reschedule-link"]').click();
|
||||||
|
|
||||||
|
await selectFirstAvailableTimeSlotNextMonth(page);
|
||||||
|
|
||||||
|
// data displayed in form should be user owner
|
||||||
|
const nameElement = await page.locator("input[name=name]");
|
||||||
|
const name = await nameElement.inputValue();
|
||||||
|
expect(name).toBe(user.username);
|
||||||
|
|
||||||
|
//same for email
|
||||||
|
const emailElement = await page.locator("input[name=email]");
|
||||||
|
const email = await emailElement.inputValue();
|
||||||
|
expect(email).toBe(user.email);
|
||||||
|
|
||||||
|
// reason to reschedule input should be visible textfield with name rescheduleReason
|
||||||
|
const reasonElement = await page.locator("textarea[name=rescheduleReason]");
|
||||||
|
await expect(reasonElement).toBeVisible();
|
||||||
|
|
||||||
|
// expect to be redirected to reschedule page
|
||||||
|
await page.locator('[data-testid="confirm-reschedule-button"]').click();
|
||||||
|
|
||||||
|
// should wait for URL but that path starts with booking/
|
||||||
|
await page.waitForURL(/\/booking\/.*/);
|
||||||
|
|
||||||
|
await expect(page).toHaveURL(/\/booking\/.*/);
|
||||||
|
|
||||||
|
await page.waitForLoadState("networkidle");
|
||||||
|
|
||||||
|
const updatedBooking = await prisma.booking.findFirst({
|
||||||
|
where: { id: booking.id },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(updatedBooking).not.toBeNull();
|
||||||
|
expect(getBooking?.startTime).not.toBe(updatedBooking?.startTime);
|
||||||
|
expect(getBooking?.endTime).not.toBe(updatedBooking?.endTime);
|
||||||
|
expect(updatedBooking?.status).toBe(BookingStatus.ACCEPTED);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Owner shouldn't be able to reschedule when going directly to booking/rescheduleUid", async ({
|
||||||
|
page,
|
||||||
|
bookings,
|
||||||
|
users,
|
||||||
|
}) => {
|
||||||
|
const { booking, user } = await createUserWithSeatedEventAndAttendees({ users, bookings }, [
|
||||||
|
{ name: "John First", email: "first+seats@cal.com", timeZone: "Europe/Berlin" },
|
||||||
|
{ name: "Jane Second", email: "second+seats@cal.com", timeZone: "Europe/Berlin" },
|
||||||
|
{ name: "John Third", email: "third+seats@cal.com", timeZone: "Europe/Berlin" },
|
||||||
|
]);
|
||||||
|
const getBooking = await booking.self();
|
||||||
|
|
||||||
|
await page.goto(`/${user.username}/seats?rescheduleUid=${getBooking?.uid}&bookingUid=null`);
|
||||||
|
|
||||||
|
await selectFirstAvailableTimeSlotNextMonth(page);
|
||||||
|
|
||||||
|
// expect textarea with name notes to be visible
|
||||||
|
const notesElement = await page.locator("textarea[name=notes]");
|
||||||
|
await expect(notesElement).toBeVisible();
|
||||||
|
|
||||||
|
// expect button confirm instead of reschedule
|
||||||
|
await expect(page.locator('[data-testid="confirm-book-button"]')).toHaveCount(1);
|
||||||
|
|
||||||
|
// now login and try again
|
||||||
|
await user.apiLogin();
|
||||||
|
|
||||||
|
await page.goto(`/${user.username}/seats?rescheduleUid=${getBooking?.uid}&bookingUid=null`);
|
||||||
|
|
||||||
|
await selectFirstAvailableTimeSlotNextMonth(page);
|
||||||
|
|
||||||
|
await expect(page).toHaveTitle(/(?!.*reschedule).*/);
|
||||||
|
|
||||||
|
// expect button reschedule
|
||||||
|
await expect(page.locator('[data-testid="confirm-reschedule-button"]')).toHaveCount(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
// @TODO: force 404 when rescheduleUid is not found
|
||||||
});
|
});
|
||||||
|
|
|
@ -375,6 +375,7 @@ export const BookEventFormChild = ({
|
||||||
fields={eventType.bookingFields}
|
fields={eventType.bookingFields}
|
||||||
locations={eventType.locations}
|
locations={eventType.locations}
|
||||||
rescheduleUid={rescheduleUid || undefined}
|
rescheduleUid={rescheduleUid || undefined}
|
||||||
|
bookingData={bookingData}
|
||||||
/>
|
/>
|
||||||
{(createBookingMutation.isError ||
|
{(createBookingMutation.isError ||
|
||||||
createRecurringBookingMutation.isError ||
|
createRecurringBookingMutation.isError ||
|
||||||
|
@ -404,8 +405,8 @@ export const BookEventFormChild = ({
|
||||||
type="submit"
|
type="submit"
|
||||||
color="primary"
|
color="primary"
|
||||||
loading={createBookingMutation.isLoading || createRecurringBookingMutation.isLoading}
|
loading={createBookingMutation.isLoading || createRecurringBookingMutation.isLoading}
|
||||||
data-testid={rescheduleUid ? "confirm-reschedule-button" : "confirm-book-button"}>
|
data-testid={rescheduleUid && bookingData ? "confirm-reschedule-button" : "confirm-book-button"}>
|
||||||
{rescheduleUid
|
{rescheduleUid && bookingData
|
||||||
? t("reschedule")
|
? t("reschedule")
|
||||||
: renderConfirmNotVerifyEmailButtonCond
|
: renderConfirmNotVerifyEmailButtonCond
|
||||||
? t("confirm")
|
? t("confirm")
|
||||||
|
@ -497,12 +498,18 @@ function useInitialFormValues({
|
||||||
});
|
});
|
||||||
|
|
||||||
const defaultUserValues = {
|
const defaultUserValues = {
|
||||||
email: rescheduleUid
|
email:
|
||||||
? bookingData?.attendees[0].email
|
rescheduleUid && bookingData && bookingData.attendees.length > 0
|
||||||
: parsedQuery["email"] || session.data?.user?.email || "",
|
? bookingData?.attendees[0].email
|
||||||
name: rescheduleUid
|
: !!parsedQuery["email"]
|
||||||
? bookingData?.attendees[0].name
|
? parsedQuery["email"]
|
||||||
: parsedQuery["name"] || session.data?.user?.name || "",
|
: session.data?.user?.email ?? "",
|
||||||
|
name:
|
||||||
|
rescheduleUid && bookingData && bookingData.attendees.length > 0
|
||||||
|
? bookingData?.attendees[0].name
|
||||||
|
: !!parsedQuery["name"]
|
||||||
|
? parsedQuery["name"]
|
||||||
|
: session.data?.user?.name ?? session.data?.user?.username ?? "",
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!isRescheduling) {
|
if (!isRescheduling) {
|
||||||
|
@ -526,14 +533,12 @@ function useInitialFormValues({
|
||||||
setDefaultValues(defaults);
|
setDefaultValues(defaults);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((!rescheduleUid && !bookingData) || !bookingData?.attendees.length) {
|
if (!rescheduleUid && !bookingData) {
|
||||||
return {};
|
|
||||||
}
|
|
||||||
const primaryAttendee = bookingData.attendees[0];
|
|
||||||
if (!primaryAttendee) {
|
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We should allow current session user as default values for booking form
|
||||||
|
|
||||||
const defaults = {
|
const defaults = {
|
||||||
responses: {} as Partial<z.infer<ReturnType<typeof getBookingResponsesSchema>>>,
|
responses: {} as Partial<z.infer<ReturnType<typeof getBookingResponsesSchema>>>,
|
||||||
};
|
};
|
||||||
|
@ -541,7 +546,7 @@ function useInitialFormValues({
|
||||||
const responses = eventType.bookingFields.reduce((responses, field) => {
|
const responses = eventType.bookingFields.reduce((responses, field) => {
|
||||||
return {
|
return {
|
||||||
...responses,
|
...responses,
|
||||||
[field.name]: bookingData.responses[field.name],
|
[field.name]: bookingData?.responses[field.name],
|
||||||
};
|
};
|
||||||
}, {});
|
}, {});
|
||||||
defaults.responses = {
|
defaults.responses = {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { useFormContext } from "react-hook-form";
|
import { useFormContext } from "react-hook-form";
|
||||||
|
|
||||||
import type { LocationObject } from "@calcom/app-store/locations";
|
import type { LocationObject } from "@calcom/app-store/locations";
|
||||||
|
import type { GetBookingType } from "@calcom/features/bookings/lib/get-booking";
|
||||||
import getLocationOptionsForSelect from "@calcom/features/bookings/lib/getLocationOptionsForSelect";
|
import getLocationOptionsForSelect from "@calcom/features/bookings/lib/getLocationOptionsForSelect";
|
||||||
import { FormBuilderField } from "@calcom/features/form-builder/FormBuilderField";
|
import { FormBuilderField } from "@calcom/features/form-builder/FormBuilderField";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
@ -13,10 +14,12 @@ export const BookingFields = ({
|
||||||
locations,
|
locations,
|
||||||
rescheduleUid,
|
rescheduleUid,
|
||||||
isDynamicGroupBooking,
|
isDynamicGroupBooking,
|
||||||
|
bookingData,
|
||||||
}: {
|
}: {
|
||||||
fields: NonNullable<RouterOutputs["viewer"]["public"]["event"]>["bookingFields"];
|
fields: NonNullable<RouterOutputs["viewer"]["public"]["event"]>["bookingFields"];
|
||||||
locations: LocationObject[];
|
locations: LocationObject[];
|
||||||
rescheduleUid?: string;
|
rescheduleUid?: string;
|
||||||
|
bookingData?: GetBookingType | null;
|
||||||
isDynamicGroupBooking: boolean;
|
isDynamicGroupBooking: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
|
@ -32,7 +35,9 @@ export const BookingFields = ({
|
||||||
// During reschedule by default all system fields are readOnly. Make them editable on case by case basis.
|
// During reschedule by default all system fields are readOnly. Make them editable on case by case basis.
|
||||||
// Allowing a system field to be edited might require sending emails to attendees, so we need to be careful
|
// Allowing a system field to be edited might require sending emails to attendees, so we need to be careful
|
||||||
let readOnly =
|
let readOnly =
|
||||||
(field.editable === "system" || field.editable === "system-but-optional") && !!rescheduleUid;
|
(field.editable === "system" || field.editable === "system-but-optional") &&
|
||||||
|
!!rescheduleUid &&
|
||||||
|
bookingData !== null;
|
||||||
|
|
||||||
let hidden = !!field.hidden;
|
let hidden = !!field.hidden;
|
||||||
const fieldViews = field.views;
|
const fieldViews = field.views;
|
||||||
|
@ -42,6 +47,9 @@ export const BookingFields = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
if (field.name === SystemField.Enum.rescheduleReason) {
|
if (field.name === SystemField.Enum.rescheduleReason) {
|
||||||
|
if (bookingData === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
// rescheduleReason is a reschedule specific field and thus should be editable during reschedule
|
// rescheduleReason is a reschedule specific field and thus should be editable during reschedule
|
||||||
readOnly = false;
|
readOnly = false;
|
||||||
}
|
}
|
||||||
|
@ -64,8 +72,8 @@ export const BookingFields = ({
|
||||||
hidden = isDynamicGroupBooking ? true : !!field.hidden;
|
hidden = isDynamicGroupBooking ? true : !!field.hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
// We don't show `notes` field during reschedule
|
// We don't show `notes` field during reschedule but since it's a query param we better valid if rescheduleUid brought any bookingData
|
||||||
if (field.name === SystemField.Enum.notes && !!rescheduleUid) {
|
if (field.name === SystemField.Enum.notes && bookingData !== null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import type { GetBookingType } from "@calcom/features/bookings/lib/get-booking";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import { HeadSeo } from "@calcom/ui";
|
import { HeadSeo } from "@calcom/ui";
|
||||||
|
@ -14,10 +15,20 @@ interface BookerSeoProps {
|
||||||
teamSlug?: string | null;
|
teamSlug?: string | null;
|
||||||
name?: string | null;
|
name?: string | null;
|
||||||
};
|
};
|
||||||
|
bookingData?: GetBookingType | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const BookerSeo = (props: BookerSeoProps) => {
|
export const BookerSeo = (props: BookerSeoProps) => {
|
||||||
const { eventSlug, username, rescheduleUid, hideBranding, isTeamEvent, entity, isSEOIndexable } = props;
|
const {
|
||||||
|
eventSlug,
|
||||||
|
username,
|
||||||
|
rescheduleUid,
|
||||||
|
hideBranding,
|
||||||
|
isTeamEvent,
|
||||||
|
entity,
|
||||||
|
isSEOIndexable,
|
||||||
|
bookingData,
|
||||||
|
} = props;
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const { data: event } = trpc.viewer.public.event.useQuery(
|
const { data: event } = trpc.viewer.public.event.useQuery(
|
||||||
{ username, eventSlug, isTeamEvent, org: entity.orgSlug ?? null },
|
{ username, eventSlug, isTeamEvent, org: entity.orgSlug ?? null },
|
||||||
|
@ -29,7 +40,7 @@ export const BookerSeo = (props: BookerSeoProps) => {
|
||||||
const title = event?.title ?? "";
|
const title = event?.title ?? "";
|
||||||
return (
|
return (
|
||||||
<HeadSeo
|
<HeadSeo
|
||||||
title={`${rescheduleUid ? t("reschedule") : ""} ${title} | ${profileName}`}
|
title={`${rescheduleUid && !!bookingData ? t("reschedule") : ""} ${title} | ${profileName}`}
|
||||||
description={`${rescheduleUid ? t("reschedule") : ""} ${title}`}
|
description={`${rescheduleUid ? t("reschedule") : ""} ${title}`}
|
||||||
meeting={{
|
meeting={{
|
||||||
title: title,
|
title: title,
|
||||||
|
|
|
@ -109,7 +109,7 @@ export const getBookingWithResponses = <
|
||||||
|
|
||||||
export default getBooking;
|
export default getBooking;
|
||||||
|
|
||||||
export const getBookingForReschedule = async (uid: string) => {
|
export const getBookingForReschedule = async (uid: string, userId?: number) => {
|
||||||
let rescheduleUid: string | null = null;
|
let rescheduleUid: string | null = null;
|
||||||
const theBooking = await prisma.booking.findFirst({
|
const theBooking = await prisma.booking.findFirst({
|
||||||
where: {
|
where: {
|
||||||
|
@ -117,8 +117,25 @@ export const getBookingForReschedule = async (uid: string) => {
|
||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
|
userId: true,
|
||||||
|
eventType: {
|
||||||
|
select: {
|
||||||
|
seatsPerTimeSlot: true,
|
||||||
|
hosts: {
|
||||||
|
select: {
|
||||||
|
userId: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
owner: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
let bookingSeatReferenceUid: number | null = null;
|
||||||
|
|
||||||
// If no booking is found via the uid, it's probably a booking seat
|
// If no booking is found via the uid, it's probably a booking seat
|
||||||
// that its being rescheduled, which we query next.
|
// that its being rescheduled, which we query next.
|
||||||
|
@ -144,11 +161,26 @@ export const getBookingForReschedule = async (uid: string) => {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (bookingSeat) {
|
if (bookingSeat) {
|
||||||
|
bookingSeatReferenceUid = bookingSeat.id;
|
||||||
rescheduleUid = bookingSeat.booking.uid;
|
rescheduleUid = bookingSeat.booking.uid;
|
||||||
attendeeEmail = bookingSeat.attendee.email;
|
attendeeEmail = bookingSeat.attendee.email;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
const isOwnerOfBooking = theBooking.userId === userId;
|
||||||
|
|
||||||
|
const isHostOfEventType = theBooking?.eventType?.hosts.some((host) => host.userId === userId);
|
||||||
|
|
||||||
|
const isUserIdInBooking = theBooking.userId === userId;
|
||||||
|
|
||||||
|
if (!isOwnerOfBooking && !isHostOfEventType && !isUserIdInBooking) return null;
|
||||||
|
hasOwnershipOnBooking = true;
|
||||||
|
}
|
||||||
|
|
||||||
// If we don't have a booking and no rescheduleUid, the ID is invalid,
|
// If we don't have a booking and no rescheduleUid, the ID is invalid,
|
||||||
// and we return null here.
|
// and we return null here.
|
||||||
if (!theBooking && !rescheduleUid) return null;
|
if (!theBooking && !rescheduleUid) return null;
|
||||||
|
@ -161,6 +193,8 @@ export const getBookingForReschedule = async (uid: string) => {
|
||||||
...booking,
|
...booking,
|
||||||
attendees: rescheduleUid
|
attendees: rescheduleUid
|
||||||
? booking.attendees.filter((attendee) => attendee.email === attendeeEmail)
|
? booking.attendees.filter((attendee) => attendee.email === attendeeEmail)
|
||||||
|
: hasOwnershipOnBooking
|
||||||
|
? []
|
||||||
: booking.attendees,
|
: booking.attendees,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -136,6 +136,19 @@ async function handler(req: CustomRequest) {
|
||||||
throw new HttpError({ statusCode: 400, message: "User not found" });
|
throw new HttpError({ statusCode: 400, message: "User not found" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If the booking is a seated event and there is no seatReferenceUid we should validate that logged in user is host
|
||||||
|
if (bookingToDelete.eventType?.seatsPerTimeSlot && !seatReferenceUid) {
|
||||||
|
const userIsHost = bookingToDelete.eventType.hosts.find((host) => {
|
||||||
|
if (host.user.id === userId) return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
const userIsOwnerOfEventType = bookingToDelete.eventType.owner?.id === userId;
|
||||||
|
|
||||||
|
if (!userIsHost && !userIsOwnerOfEventType) {
|
||||||
|
throw new HttpError({ statusCode: 401, message: "User not a host of this event" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// get webhooks
|
// get webhooks
|
||||||
const eventTrigger: WebhookTriggerEvents = "BOOKING_CANCELLED";
|
const eventTrigger: WebhookTriggerEvents = "BOOKING_CANCELLED";
|
||||||
|
|
||||||
|
|
|
@ -15,6 +15,6 @@ export async function maybeGetBookingUidFromSeat(prisma: PrismaClient, uid: stri
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (bookingSeat) return bookingSeat.booking.uid;
|
if (bookingSeat) return { uid: bookingSeat.booking.uid, seatReferenceUid: uid };
|
||||||
return uid;
|
return { uid };
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue