feat: Redirect when event type does not match booking type (#14460)
* feat: Redirect when event type does not match booking type * Favour early return * If multiple bookingUid/rescheduleUid given, use first * rescheduleUid/bookingUid behaviour is required
This commit is contained in:
@@ -1,13 +1,17 @@
|
||||
import type { DehydratedState } from "@tanstack/react-query";
|
||||
import { type GetServerSidePropsContext } from "next";
|
||||
import type { Session } from "next-auth";
|
||||
import { z } from "zod";
|
||||
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { getBookingForReschedule, getBookingForSeatedEvent } from "@calcom/features/bookings/lib/get-booking";
|
||||
import type { GetBookingType } from "@calcom/features/bookings/lib/get-booking";
|
||||
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import type { getPublicEvent } from "@calcom/features/eventtypes/lib/getPublicEvent";
|
||||
import { getUsernameList } from "@calcom/lib/defaultEvents";
|
||||
import { UserRepository } from "@calcom/lib/server/repository/user";
|
||||
import slugify from "@calcom/lib/slugify";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { RedirectType } from "@calcom/prisma/client";
|
||||
|
||||
import { getTemporaryOrgRedirect } from "@lib/getTemporaryOrgRedirect";
|
||||
@@ -16,6 +20,74 @@ import { type EmbedProps } from "@lib/withEmbedSsr";
|
||||
|
||||
export type PageProps = inferSSRProps<typeof getServerSideProps> & EmbedProps;
|
||||
|
||||
type Props = {
|
||||
eventData: Pick<
|
||||
NonNullable<Awaited<ReturnType<typeof getPublicEvent>>>,
|
||||
"id" | "length" | "metadata" | "entity"
|
||||
>;
|
||||
booking?: GetBookingType;
|
||||
rescheduleUid: string | null;
|
||||
bookingUid: string | null;
|
||||
user: string;
|
||||
slug: string;
|
||||
trpcState: DehydratedState;
|
||||
isBrandingHidden: boolean;
|
||||
isSEOIndexable: boolean | null;
|
||||
themeBasis: null | string;
|
||||
orgBannerUrl: null;
|
||||
};
|
||||
|
||||
async function processReschedule({
|
||||
props,
|
||||
rescheduleUid,
|
||||
session,
|
||||
}: {
|
||||
props: Props;
|
||||
session: Session | null;
|
||||
rescheduleUid: string | string[] | undefined;
|
||||
}) {
|
||||
if (!rescheduleUid) return;
|
||||
const booking = await getBookingForReschedule(`${rescheduleUid}`, session?.user?.id);
|
||||
// if no booking found, no eventTypeId (dynamic) or it matches this eventData - return void (success).
|
||||
if (booking === null || !booking.eventTypeId || booking?.eventTypeId === props.eventData?.id) {
|
||||
props.booking = booking;
|
||||
props.rescheduleUid = Array.isArray(rescheduleUid) ? rescheduleUid[0] : rescheduleUid;
|
||||
return;
|
||||
}
|
||||
// handle redirect response
|
||||
const redirectEventTypeTarget = await prisma.eventType.findUnique({
|
||||
where: {
|
||||
id: booking.eventTypeId,
|
||||
},
|
||||
select: {
|
||||
slug: true,
|
||||
},
|
||||
});
|
||||
if (!redirectEventTypeTarget) {
|
||||
return {
|
||||
notFound: true,
|
||||
} as const;
|
||||
}
|
||||
return {
|
||||
redirect: {
|
||||
permanent: false,
|
||||
destination: redirectEventTypeTarget.slug,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function processSeatedEvent({
|
||||
props,
|
||||
bookingUid,
|
||||
}: {
|
||||
props: Props;
|
||||
bookingUid: string | string[] | undefined;
|
||||
}) {
|
||||
if (!bookingUid) return;
|
||||
props.booking = await getBookingForSeatedEvent(`${bookingUid}`);
|
||||
props.bookingUid = Array.isArray(bookingUid) ? bookingUid[0] : bookingUid;
|
||||
}
|
||||
|
||||
async function getDynamicGroupPageProps(context: GetServerSidePropsContext) {
|
||||
const session = await getServerSession(context);
|
||||
const { user: usernames, type: slug } = paramsSchema.parse(context.params);
|
||||
@@ -51,13 +123,6 @@ async function getDynamicGroupPageProps(context: GetServerSidePropsContext) {
|
||||
} as const;
|
||||
}
|
||||
|
||||
let booking: GetBookingType | null = null;
|
||||
if (rescheduleUid) {
|
||||
booking = await getBookingForReschedule(`${rescheduleUid}`, session?.user?.id);
|
||||
} else if (bookingUid) {
|
||||
booking = await getBookingForSeatedEvent(`${bookingUid}`);
|
||||
}
|
||||
|
||||
// We use this to both prefetch the query on the server,
|
||||
// as well as to check if the event exist, so we c an show a 404 otherwise.
|
||||
const eventData = await ssr.viewer.public.event.fetch({
|
||||
@@ -73,27 +138,38 @@ async function getDynamicGroupPageProps(context: GetServerSidePropsContext) {
|
||||
} as const;
|
||||
}
|
||||
|
||||
return {
|
||||
props: {
|
||||
eventData: {
|
||||
entity: eventData.entity,
|
||||
length: eventData.length,
|
||||
metadata: {
|
||||
...eventData.metadata,
|
||||
multipleDuration: [15, 30, 60],
|
||||
},
|
||||
const props: Props = {
|
||||
eventData: {
|
||||
id: eventData.id,
|
||||
entity: eventData.entity,
|
||||
length: eventData.length,
|
||||
metadata: {
|
||||
...eventData.metadata,
|
||||
multipleDuration: [15, 30, 60],
|
||||
},
|
||||
booking,
|
||||
user: usernames.join("+"),
|
||||
slug,
|
||||
trpcState: ssr.dehydrate(),
|
||||
isBrandingHidden: false,
|
||||
isSEOIndexable: true,
|
||||
themeBasis: null,
|
||||
bookingUid: bookingUid ? `${bookingUid}` : null,
|
||||
rescheduleUid: rescheduleUid ? `${rescheduleUid}` : null,
|
||||
orgBannerUrl: null,
|
||||
},
|
||||
user: usernames.join("+"),
|
||||
slug,
|
||||
trpcState: ssr.dehydrate(),
|
||||
isBrandingHidden: false,
|
||||
isSEOIndexable: true,
|
||||
themeBasis: null,
|
||||
bookingUid: bookingUid ? `${bookingUid}` : null,
|
||||
rescheduleUid: null,
|
||||
orgBannerUrl: null,
|
||||
};
|
||||
|
||||
if (rescheduleUid) {
|
||||
const processRescheduleResult = await processReschedule({ props, rescheduleUid, session });
|
||||
if (processRescheduleResult) {
|
||||
return processRescheduleResult;
|
||||
}
|
||||
} else if (bookingUid) {
|
||||
await processSeatedEvent({ props, bookingUid });
|
||||
}
|
||||
|
||||
return {
|
||||
props,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -131,13 +207,6 @@ async function getUserPageProps(context: GetServerSidePropsContext) {
|
||||
} as const;
|
||||
}
|
||||
|
||||
let booking: GetBookingType | null = null;
|
||||
if (rescheduleUid) {
|
||||
booking = await getBookingForReschedule(`${rescheduleUid}`, session?.user?.id);
|
||||
} else if (bookingUid) {
|
||||
booking = await getBookingForSeatedEvent(`${bookingUid}`);
|
||||
}
|
||||
|
||||
const org = isValidOrgDomain ? currentOrgDomain : null;
|
||||
// We use this to both prefetch the query on the server,
|
||||
// as well as to check if the event exist, so we can show a 404 otherwise.
|
||||
@@ -154,24 +223,35 @@ async function getUserPageProps(context: GetServerSidePropsContext) {
|
||||
} as const;
|
||||
}
|
||||
|
||||
return {
|
||||
props: {
|
||||
booking,
|
||||
eventData: {
|
||||
entity: eventData.entity,
|
||||
length: eventData.length,
|
||||
metadata: eventData.metadata,
|
||||
},
|
||||
user: username,
|
||||
slug,
|
||||
trpcState: ssr.dehydrate(),
|
||||
isBrandingHidden: user?.hideBranding,
|
||||
isSEOIndexable: user?.allowSEOIndexing,
|
||||
themeBasis: username,
|
||||
bookingUid: bookingUid ? `${bookingUid}` : null,
|
||||
rescheduleUid: rescheduleUid ? `${rescheduleUid}` : null,
|
||||
orgBannerUrl: eventData?.owner?.profile?.organization?.bannerUrl ?? null,
|
||||
const props: Props = {
|
||||
eventData: {
|
||||
id: eventData.id,
|
||||
entity: eventData.entity,
|
||||
length: eventData.length,
|
||||
metadata: eventData.metadata,
|
||||
},
|
||||
user: username,
|
||||
slug,
|
||||
trpcState: ssr.dehydrate(),
|
||||
isBrandingHidden: user?.hideBranding,
|
||||
isSEOIndexable: user?.allowSEOIndexing,
|
||||
themeBasis: username,
|
||||
bookingUid: bookingUid ? `${bookingUid}` : null,
|
||||
rescheduleUid: null,
|
||||
orgBannerUrl: eventData?.owner?.profile?.organization?.bannerUrl ?? null,
|
||||
};
|
||||
|
||||
if (rescheduleUid) {
|
||||
const processRescheduleResult = await processReschedule({ props, rescheduleUid, session });
|
||||
if (processRescheduleResult) {
|
||||
return processRescheduleResult;
|
||||
}
|
||||
} else if (bookingUid) {
|
||||
await processSeatedEvent({ props, bookingUid });
|
||||
}
|
||||
|
||||
return {
|
||||
props,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -126,6 +126,21 @@ testBothFutureAndLegacyRoutes.describe("pro user", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("it redirects when a rescheduleUid does not match the current event type", async ({
|
||||
page,
|
||||
users,
|
||||
bookings,
|
||||
}) => {
|
||||
const [pro] = users.get();
|
||||
const [eventType] = pro.eventTypes;
|
||||
const bookingFixture = await bookings.create(pro.id, pro.username, eventType.id);
|
||||
|
||||
// open the wrong eventType (rescheduleUid created for /30min event)
|
||||
await page.goto(`${pro.username}/${pro.eventTypes[1].slug}?rescheduleUid=${bookingFixture.uid}`);
|
||||
|
||||
await expect(page).toHaveURL(new RegExp(`${pro.username}/${eventType.slug}`));
|
||||
});
|
||||
|
||||
test("Can cancel the recently created booking and rebook the same timeslot", async ({
|
||||
page,
|
||||
users,
|
||||
|
||||
@@ -201,7 +201,7 @@ async function handler(req: CustomRequest) {
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
username:true,
|
||||
username: true,
|
||||
name: true,
|
||||
email: true,
|
||||
timeZone: true,
|
||||
@@ -276,8 +276,8 @@ async function handler(req: CustomRequest) {
|
||||
destinationCalendar: bookingToDelete?.destinationCalendar
|
||||
? [bookingToDelete?.destinationCalendar]
|
||||
: bookingToDelete?.user.destinationCalendar
|
||||
? [bookingToDelete?.user.destinationCalendar]
|
||||
: [],
|
||||
? [bookingToDelete?.user.destinationCalendar]
|
||||
: [],
|
||||
cancellationReason: cancellationReason,
|
||||
...(teamMembers && {
|
||||
team: { name: bookingToDelete?.eventType?.team?.name || "Nameless", members: teamMembers, id: teamId! },
|
||||
|
||||
@@ -41,7 +41,9 @@ const LicenseRequired = ({ children, as = "", ...rest }: LicenseRequiredProps) =
|
||||
title={
|
||||
<>
|
||||
{t("enterprise_license_locally")} {t("enterprise_license_sales")}{" "}
|
||||
<a className="underline" href="https://cal.com/sales">{t("contact_sales")}</a>
|
||||
<a className="underline" href="https://cal.com/sales">
|
||||
{t("contact_sales")}
|
||||
</a>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -125,7 +125,7 @@ model EventType {
|
||||
metadata Json?
|
||||
/// @zod.custom(imports.successRedirectUrl)
|
||||
successRedirectUrl String?
|
||||
forwardParamsSuccessRedirect Boolean? @default(true)
|
||||
forwardParamsSuccessRedirect Boolean? @default(true)
|
||||
workflows WorkflowsOnEventTypes[]
|
||||
/// @zod.custom(imports.intervalLimitsType)
|
||||
bookingLimits Json?
|
||||
|
||||
Reference in New Issue
Block a user