From c5bd45fdfb01167bed6d82cbca88a37ec66d9b71 Mon Sep 17 00:00:00 2001 From: Alex van Andel Date: Wed, 8 May 2024 14:33:22 +0100 Subject: [PATCH] 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 --- ...rs-type-public-view.getServerSideProps.tsx | 180 +++++++++++++----- apps/web/playwright/booking-pages.e2e.ts | 15 ++ .../bookings/lib/handleCancelBooking.ts | 6 +- .../ee/common/components/LicenseRequired.tsx | 4 +- packages/prisma/schema.prisma | 2 +- 5 files changed, 152 insertions(+), 55 deletions(-) diff --git a/apps/web/modules/users/views/users-type-public-view.getServerSideProps.tsx b/apps/web/modules/users/views/users-type-public-view.getServerSideProps.tsx index 42a03703f7..b032ec126d 100644 --- a/apps/web/modules/users/views/users-type-public-view.getServerSideProps.tsx +++ b/apps/web/modules/users/views/users-type-public-view.getServerSideProps.tsx @@ -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 & EmbedProps; +type Props = { + eventData: Pick< + NonNullable>>, + "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, }; } diff --git a/apps/web/playwright/booking-pages.e2e.ts b/apps/web/playwright/booking-pages.e2e.ts index 3a45dcb017..68763426ad 100644 --- a/apps/web/playwright/booking-pages.e2e.ts +++ b/apps/web/playwright/booking-pages.e2e.ts @@ -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, diff --git a/packages/features/bookings/lib/handleCancelBooking.ts b/packages/features/bookings/lib/handleCancelBooking.ts index 49d8ae7f5d..c7ca758fc5 100644 --- a/packages/features/bookings/lib/handleCancelBooking.ts +++ b/packages/features/bookings/lib/handleCancelBooking.ts @@ -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! }, diff --git a/packages/features/ee/common/components/LicenseRequired.tsx b/packages/features/ee/common/components/LicenseRequired.tsx index 98b8165adb..e48af64963 100644 --- a/packages/features/ee/common/components/LicenseRequired.tsx +++ b/packages/features/ee/common/components/LicenseRequired.tsx @@ -41,7 +41,9 @@ const LicenseRequired = ({ children, as = "", ...rest }: LicenseRequiredProps) = title={ <> {t("enterprise_license_locally")} {t("enterprise_license_sales")}{" "} - {t("contact_sales")} + + {t("contact_sales")} + } /> diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma index a5ca286b71..b409550b21 100644 --- a/packages/prisma/schema.prisma +++ b/packages/prisma/schema.prisma @@ -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?