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:
Alex van Andel
2024-05-08 10:33:22 -03:00
committed by GitHub
parent 5d7b2a12a7
commit c5bd45fdfb
5 changed files with 152 additions and 55 deletions
@@ -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,
};
}
+15
View File
@@ -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>
</>
}
/>
+1 -1
View File
@@ -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?