diff --git a/apps/web/app/(booking-page-wrapper)/team/[slug]/[type]/pageWithCachedData.tsx b/apps/web/app/(booking-page-wrapper)/team/[slug]/[type]/pageWithCachedData.tsx index 56f7fc095f..0a15425b05 100644 --- a/apps/web/app/(booking-page-wrapper)/team/[slug]/[type]/pageWithCachedData.tsx +++ b/apps/web/app/(booking-page-wrapper)/team/[slug]/[type]/pageWithCachedData.tsx @@ -16,7 +16,7 @@ import slugify from "@calcom/lib/slugify"; import { BookingStatus, RedirectType } from "@calcom/prisma/enums"; import { buildLegacyCtx, buildLegacyRequest } from "@lib/buildLegacyCtx"; -import { getTemporaryOrgRedirect } from "@lib/getTemporaryOrgRedirect"; +import { handleOrgRedirect } from "@lib/handleOrgRedirect"; import CachedClientView, { type TeamBookingPageProps } from "~/team/type-view-cached"; @@ -124,19 +124,17 @@ export const generateMetadata = async ({ params, searchParams }: PageProps) => { const CachedTeamBooker = async ({ params, searchParams }: PageProps) => { const { currentOrgDomain, isValidOrgDomain, teamSlug, meetingSlug } = await _getOrgContext(await params); - const isOrgContext = currentOrgDomain && isValidOrgDomain; const legacyCtx = buildLegacyCtx(await headers(), await cookies(), await params, await searchParams); - // Handle org redirects for non-org contexts - if (!isOrgContext) { - const redirectResult = await getTemporaryOrgRedirect({ - slugs: teamSlug, - redirectType: RedirectType.Team, - eventTypeSlug: meetingSlug, - currentQuery: legacyCtx.query, - }); - if (redirectResult) return redirect(redirectResult.redirect.destination); - } + // Handle org redirects + const redirectResult = await handleOrgRedirect({ + slugs: [teamSlug], + redirectType: RedirectType.Team, + eventTypeSlug: meetingSlug, + context: legacyCtx, + currentOrgDomain: isValidOrgDomain ? currentOrgDomain : null, + }); + if (redirectResult) return redirect(redirectResult.redirect.destination); const teamData = await getCachedTeamData(teamSlug, currentOrgDomain); diff --git a/apps/web/lib/d/[link]/[slug]/getServerSideProps.tsx b/apps/web/lib/d/[link]/[slug]/getServerSideProps.tsx index e40ba6e826..16c05ffffc 100644 --- a/apps/web/lib/d/[link]/[slug]/getServerSideProps.tsx +++ b/apps/web/lib/d/[link]/[slug]/getServerSideProps.tsx @@ -15,7 +15,7 @@ import slugify from "@calcom/lib/slugify"; import prisma from "@calcom/prisma"; import { RedirectType } from "@calcom/prisma/enums"; -import { getTemporaryOrgRedirect } from "@lib/getTemporaryOrgRedirect"; +import { getRedirectWithOriginAndSearchString } from "@lib/handleOrgRedirect"; import type { inferSSRProps } from "@lib/types/inferSSRProps"; export type PageProps = inferSSRProps & EmbedProps; @@ -64,17 +64,26 @@ async function getUserPageProps(context: GetServerSidePropsContext) { return notFound; } - if (!org) { - const redirect = await getTemporaryOrgRedirect({ - slugs: [username], - redirectType: RedirectType.User, - eventTypeSlug: slug, - currentQuery: context.query, - }); + // Get just the origin and searchString from the redirect to ensure that we don't redirect to a URL that exposes the real path to book the user for any other events \ + // This is important for a private booking link + // e.g. http://app.cal.com/d/sgdthj8mu4nsLNTYi3fW2p/demo -> should redirect to -> http://acme.cal.com/d/sgdthj8mu4nsLNTYi3fW2p/demo and not to http://acme.cal.com/john/demo(which exposes the real path to book the user for any other events) + const redirectWithOriginAndSearchString = await getRedirectWithOriginAndSearchString({ + slugs: [username], + redirectType: RedirectType.User, + context, + currentOrgDomain: isValidOrgDomain ? currentOrgDomain : null, + }); - if (redirect) { - return redirect; - } + if (redirectWithOriginAndSearchString) { + return { + redirect: { + permanent: false, + // App Router doesn't have access to the current path directly, so we build it manually + destination: `${redirectWithOriginAndSearchString.origin ?? ""}/d/${link}/${slug}${ + redirectWithOriginAndSearchString.searchString + }`, + }, + }; } name = profileUsername || username; diff --git a/apps/web/lib/getTemporaryOrgRedirect.test.ts b/apps/web/lib/getTemporaryOrgRedirect.test.ts deleted file mode 100644 index d851115321..0000000000 --- a/apps/web/lib/getTemporaryOrgRedirect.test.ts +++ /dev/null @@ -1,333 +0,0 @@ -import prismaMock from "../../../tests/libs/__mocks__/prismaMock"; - -import { describe, it, expect, beforeEach } from "vitest"; - -import { RedirectType } from "@calcom/prisma/client"; - -import { getTemporaryOrgRedirect } from "./getTemporaryOrgRedirect"; - -const mockData = { - redirects: [] as { - toUrl: string; - from: string; - redirectType: RedirectType; - }[], -}; - -function mockARedirectInDB({ - toUrl, - slug, - redirectType, -}: { - toUrl: string; - slug: string; - redirectType: RedirectType; -}) { - mockData.redirects.push({ - toUrl, - from: slug, - redirectType, - }); - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - //@ts-ignore - prismaMock.tempOrgRedirect.findMany.mockImplementation(({ where }) => { - return new Promise((resolve) => { - const tempOrgRedirects: typeof mockData.redirects = []; - where.from.in.forEach((whereSlug: string) => { - const matchingRedirect = mockData.redirects.find((redirect) => { - return where.type === redirect.redirectType && whereSlug === redirect.from && where.fromOrgId === 0; - }); - if (matchingRedirect) { - tempOrgRedirects.push(matchingRedirect); - } - }); - resolve(tempOrgRedirects); - }); - }); -} - -beforeEach(() => { - mockData.redirects = []; -}); - -describe("getTemporaryOrgRedirect", () => { - it("should generate event-type URL without existing query params", async () => { - mockARedirectInDB({ slug: "slug", toUrl: "https://calcom.cal.com", redirectType: RedirectType.User }); - const redirect = await getTemporaryOrgRedirect({ - slugs: "slug", - redirectType: RedirectType.User, - eventTypeSlug: "30min", - currentQuery: {}, - }); - - expect(redirect).toEqual({ - redirect: { - permanent: false, - destination: "https://calcom.cal.com/30min?orgRedirection=true", - }, - }); - }); - - it("should generate event-type URL with existing query params", async () => { - mockARedirectInDB({ slug: "slug", toUrl: "https://calcom.cal.com", redirectType: RedirectType.User }); - - const redirect = await getTemporaryOrgRedirect({ - slugs: "slug", - redirectType: RedirectType.User, - eventTypeSlug: "30min", - currentQuery: { - abc: "1", - }, - }); - - expect(redirect).toEqual({ - redirect: { - permanent: false, - destination: "https://calcom.cal.com/30min?abc=1&orgRedirection=true", - }, - }); - }); - - it("should generate User URL with existing query params", async () => { - mockARedirectInDB({ slug: "slug", toUrl: "https://calcom.cal.com", redirectType: RedirectType.User }); - - const redirect = await getTemporaryOrgRedirect({ - slugs: "slug", - redirectType: RedirectType.User, - eventTypeSlug: null, - currentQuery: { - abc: "1", - }, - }); - - expect(redirect).toEqual({ - redirect: { - permanent: false, - destination: "https://calcom.cal.com?abc=1&orgRedirection=true", - }, - }); - }); - - it("should generate Team Profile URL with existing query params", async () => { - mockARedirectInDB({ - slug: "seeded-team", - toUrl: "https://calcom.cal.com", - redirectType: RedirectType.Team, - }); - - const redirect = await getTemporaryOrgRedirect({ - slugs: "seeded-team", - redirectType: RedirectType.Team, - eventTypeSlug: null, - currentQuery: { - abc: "1", - }, - }); - - expect(redirect).toEqual({ - redirect: { - permanent: false, - destination: "https://calcom.cal.com?abc=1&orgRedirection=true", - }, - }); - }); - - it("should generate Team Event URL with existing query params", async () => { - mockARedirectInDB({ - slug: "seeded-team", - toUrl: "https://calcom.cal.com", - redirectType: RedirectType.Team, - }); - - const redirect = await getTemporaryOrgRedirect({ - slugs: "seeded-team", - redirectType: RedirectType.Team, - eventTypeSlug: "30min", - currentQuery: { - abc: "1", - }, - }); - - expect(redirect).toEqual({ - redirect: { - permanent: false, - destination: "https://calcom.cal.com/30min?abc=1&orgRedirection=true", - }, - }); - }); - - it("should generate Team Event URL without query params", async () => { - mockARedirectInDB({ - slug: "seeded-team", - toUrl: "https://calcom.cal.com", - redirectType: RedirectType.Team, - }); - - const redirect = await getTemporaryOrgRedirect({ - slugs: "seeded-team", - redirectType: RedirectType.Team, - eventTypeSlug: "30min", - currentQuery: {}, - }); - - expect(redirect).toEqual({ - redirect: { - permanent: false, - destination: "https://calcom.cal.com/30min?orgRedirection=true", - }, - }); - }); - - it("should generate Dynamic Group Booking Profile Url", async () => { - mockARedirectInDB({ - slug: "first", - toUrl: "https://calcom.cal.com/first-in-org1", - redirectType: RedirectType.Team, - }); - mockARedirectInDB({ - slug: "second", - toUrl: "https://calcom.cal.com/second-in-org1", - redirectType: RedirectType.Team, - }); - - const redirect = await getTemporaryOrgRedirect({ - slugs: ["first", "second"], - redirectType: RedirectType.Team, - eventTypeSlug: null, - currentQuery: {}, - }); - - expect(redirect).toEqual({ - redirect: { - permanent: false, - destination: "https://calcom.cal.com/first-in-org1+second-in-org1?orgRedirection=true", - }, - }); - }); - - it("should generate Dynamic Group Booking Profile Url - same order", async () => { - mockARedirectInDB({ - slug: "second", - toUrl: "https://calcom.cal.com/second-in-org1", - redirectType: RedirectType.Team, - }); - mockARedirectInDB({ - slug: "first", - toUrl: "https://calcom.cal.com/first-in-org1", - redirectType: RedirectType.Team, - }); - - const redirect = await getTemporaryOrgRedirect({ - slugs: ["first", "second"], - redirectType: RedirectType.Team, - eventTypeSlug: null, - currentQuery: {}, - }); - - expect(redirect).toEqual({ - redirect: { - permanent: false, - destination: "https://calcom.cal.com/first-in-org1+second-in-org1?orgRedirection=true", - }, - }); - - const redirect1 = await getTemporaryOrgRedirect({ - slugs: ["second", "first"], - redirectType: RedirectType.Team, - eventTypeSlug: null, - currentQuery: {}, - }); - - expect(redirect1).toEqual({ - redirect: { - permanent: false, - destination: "https://calcom.cal.com/second-in-org1+first-in-org1?orgRedirection=true", - }, - }); - }); - - it("should generate Dynamic Group Booking Profile Url with query params", async () => { - mockARedirectInDB({ - slug: "first", - toUrl: "https://calcom.cal.com/first-in-org1", - redirectType: RedirectType.Team, - }); - mockARedirectInDB({ - slug: "second", - toUrl: "https://calcom.cal.com/second-in-org1", - redirectType: RedirectType.Team, - }); - - const redirect = await getTemporaryOrgRedirect({ - slugs: ["first", "second"], - redirectType: RedirectType.Team, - eventTypeSlug: null, - currentQuery: { - abc: "1", - }, - }); - - expect(redirect).toEqual({ - redirect: { - permanent: false, - destination: "https://calcom.cal.com/first-in-org1+second-in-org1?abc=1&orgRedirection=true", - }, - }); - }); - - it("should generate Dynamic Group Booking EventType Url", async () => { - mockARedirectInDB({ - slug: "first", - toUrl: "https://calcom.cal.com/first-in-org1", - redirectType: RedirectType.Team, - }); - mockARedirectInDB({ - slug: "second", - toUrl: "https://calcom.cal.com/second-in-org1", - redirectType: RedirectType.Team, - }); - - const redirect = await getTemporaryOrgRedirect({ - slugs: ["first", "second"], - redirectType: RedirectType.Team, - eventTypeSlug: "30min", - currentQuery: {}, - }); - - expect(redirect).toEqual({ - redirect: { - permanent: false, - destination: "https://calcom.cal.com/first-in-org1+second-in-org1/30min?orgRedirection=true", - }, - }); - }); - - it("should generate Dynamic Group Booking EventType Url with query params", async () => { - mockARedirectInDB({ - slug: "first", - toUrl: "https://calcom.cal.com/first-in-org1", - redirectType: RedirectType.Team, - }); - mockARedirectInDB({ - slug: "second", - toUrl: "https://calcom.cal.com/second-in-org1", - redirectType: RedirectType.Team, - }); - - const redirect = await getTemporaryOrgRedirect({ - slugs: ["first", "second"], - redirectType: RedirectType.Team, - eventTypeSlug: "30min", - currentQuery: { - abc: "1", - }, - }); - - expect(redirect).toEqual({ - redirect: { - permanent: false, - destination: "https://calcom.cal.com/first-in-org1+second-in-org1/30min?abc=1&orgRedirection=true", - }, - }); - }); -}); diff --git a/apps/web/lib/getTemporaryOrgRedirect.ts b/apps/web/lib/getTemporaryOrgRedirect.ts deleted file mode 100644 index f2d443f17a..0000000000 --- a/apps/web/lib/getTemporaryOrgRedirect.ts +++ /dev/null @@ -1,71 +0,0 @@ -import type { ParsedUrlQuery } from "querystring"; -import { stringify } from "querystring"; - -import logger from "@calcom/lib/logger"; -import { safeStringify } from "@calcom/lib/safeStringify"; -import type { RedirectType } from "@calcom/prisma/client"; - -const log = logger.getSubLogger({ prefix: ["lib", "getTemporaryOrgRedirect"] }); -export const getTemporaryOrgRedirect = async ({ - slugs, - redirectType, - eventTypeSlug, - currentQuery, -}: { - slugs: string[] | string; - redirectType: RedirectType; - eventTypeSlug: string | null; - currentQuery: ParsedUrlQuery; -}) => { - const prisma = (await import("@calcom/prisma")).default; - slugs = slugs instanceof Array ? slugs : [slugs]; - log.debug( - `Looking for redirect for`, - safeStringify({ - slugs, - redirectType, - eventTypeSlug, - }) - ); - - const redirects = await prisma.tempOrgRedirect.findMany({ - where: { - type: redirectType, - from: { - in: slugs, - }, - fromOrgId: 0, - }, - }); - - const currentQueryString = stringify(currentQuery); - if (!redirects.length) { - return null; - } - - // Use the first redirect origin as the new origin as we aren't supposed to handle different org usernames in a group - const newOrigin = new URL(redirects[0].toUrl).origin; - const query = currentQueryString ? `?${currentQueryString}&orgRedirection=true` : "?orgRedirection=true"; - // Use the same order as in input slugs - It is important from Dynamic Group perspective as the first user's settings are used for various things - const newSlugs = slugs.map((slug) => { - const redirect = redirects.find((redirect) => redirect.from === slug); - if (!redirect) { - return slug; - } - const newSlug = new URL(redirect.toUrl).pathname.slice(1); - return newSlug; - }); - - const newSlug = newSlugs.join("+"); - const newPath = newSlug ? `/${newSlug}` : ""; - - const newDestination = `${newOrigin}${newPath}${eventTypeSlug ? `/${eventTypeSlug}` : ""}${query}`; - log.debug(`Suggesting redirect from ${slugs} to ${newDestination}`); - - return { - redirect: { - permanent: false, - destination: newDestination, - }, - } as const; -}; diff --git a/apps/web/lib/handleOrgRedirect.test.ts b/apps/web/lib/handleOrgRedirect.test.ts new file mode 100644 index 0000000000..b38785c3e8 --- /dev/null +++ b/apps/web/lib/handleOrgRedirect.test.ts @@ -0,0 +1,704 @@ +import type { GetServerSidePropsContext } from "next"; +import type { ParsedUrlQuery } from "querystring"; +import { vi, describe, it, expect, beforeEach, afterEach } from "vitest"; + +import * as constants from "@calcom/lib/constants"; +import { RedirectType } from "@calcom/prisma/client"; + +import { handleOrgRedirect, getRedirectWithOriginAndSearchString } from "./handleOrgRedirect"; + +// Mock prisma for all tests +const prismaMock = vi.hoisted(() => ({ + tempOrgRedirect: { + findMany: vi.fn(), + }, +})); + +vi.mock("@calcom/prisma", () => ({ + default: prismaMock, +})); + +const createTestContext = (overrides?: { + host?: string; + query?: ParsedUrlQuery; +}): GetServerSidePropsContext => + ({ + req: { + headers: { + host: overrides?.host || "cal.local:3000", + }, + }, + query: overrides?.query || {}, + } as unknown as GetServerSidePropsContext); + +const createTestRedirectParams = (overrides?: { + slugs?: string[]; + redirectType?: RedirectType; + eventTypeSlug?: string | null; + context?: GetServerSidePropsContext; + currentOrgDomain?: string | null; +}) => ({ + slugs: overrides?.slugs || ["pro"], + redirectType: overrides?.redirectType || RedirectType.User, + eventTypeSlug: overrides?.eventTypeSlug ?? null, + context: overrides?.context || createTestContext(), + currentOrgDomain: overrides?.currentOrgDomain ?? null, +}); + +interface RedirectScenario { + redirects: Array<{ + from: string; + toUrl: string; + type?: RedirectType; + }>; +} + +const createRedirectScenario = (scenario: RedirectScenario) => { + // Create a map for quick lookup based on input parameters + const redirectMap = new Map(); + + scenario.redirects.forEach((redirect) => { + const key = `${redirect.type || RedirectType.User}-${redirect.from}`; + redirectMap.set(key, redirect); + }); + + // Return a mock implementation that returns data based on the actual query + prismaMock.tempOrgRedirect.findMany.mockImplementation(async (params) => { + const { type, from } = params?.where || {}; + const slugsToFind = from?.in || []; + + // Return redirects that match the requested type and slugs + const matchingRedirects = slugsToFind + .map((slug: string) => { + const key = `${type}-${slug}`; + const redirect = redirectMap.get(key); + if (redirect) { + return { + from: redirect.from, + toUrl: redirect.toUrl, + type: redirect.type || type, + fromOrgId: 0, + }; + } + return null; + }) + .filter(Boolean); + + return matchingRedirects; + }); +}; + +const expectRedirectTo = (result: any, expectedDestination: string) => { + expect(result).toEqual({ + redirect: { + permanent: false, + destination: expectedDestination, + }, + }); +}; + +const expectNoRedirect = (result: any) => { + expect(result).toBeNull(); +}; + +const expectPrismaCalledWith = (expectedParams: { type: RedirectType; slugs: string[] }) => { + expect(prismaMock.tempOrgRedirect.findMany).toHaveBeenCalledWith({ + where: { + type: expectedParams.type, + from: { + in: expectedParams.slugs, + }, + fromOrgId: 0, + }, + }); +}; + +const expectPrismaNotCalled = () => { + expect(prismaMock.tempOrgRedirect.findMany).not.toHaveBeenCalled(); +}; + +// Verify that the redirect was actually built from the data returned by Prisma +const expectRedirectUsesData = ( + result: any, + expectedSlug: string, + expectedOrigin = "https://acme.cal.local:3000" +) => { + if (!result || !result.redirect) { + throw new Error("Expected a redirect result but got null/undefined"); + } + const url = new URL(result.redirect.destination); + expect(url.origin).toBe(expectedOrigin); + expect(url.pathname).toContain(expectedSlug); +}; + +describe("handleOrgRedirect", () => { + beforeEach(() => { + vi.clearAllMocks(); + // Default to no redirects - tests will override with specific scenarios + createRedirectScenario({ redirects: [] }); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + describe("when not in organization context", () => { + it("should redirect to organization URL when redirect exists", async () => { + // Setup scenario with unique test data to verify correct usage + createRedirectScenario({ + redirects: [ + { + from: "pro", + toUrl: "https://acme.cal.local:3000/pro-example", + type: RedirectType.User, + }, + // Add a decoy redirect to ensure the code picks the right one + { + from: "other-user", + toUrl: "https://acme.cal.local:3000/wrong-redirect", + type: RedirectType.User, + }, + ], + }); + + const params = createTestRedirectParams({ + slugs: ["pro"], + }); + const result = await handleOrgRedirect(params); + + expectPrismaCalledWith({ type: RedirectType.User, slugs: ["pro"] }); + expectRedirectTo(result, "https://acme.cal.local:3000/pro-example?orgRedirection=true"); + expectRedirectUsesData(result, "pro-example"); + }); + + it("should return null when no redirect exists", async () => { + // Explicitly set up no redirects scenario + createRedirectScenario({ redirects: [] }); + + const params = createTestRedirectParams({ + slugs: ["pro"], + }); + const result = await handleOrgRedirect(params); + + expectPrismaCalledWith({ type: RedirectType.User, slugs: ["pro"] }); + expectNoRedirect(result); + }); + + it("should handle Team redirect type", async () => { + createRedirectScenario({ + redirects: [ + { + from: "sales-team", + toUrl: "https://acme.cal.local:3000/sales", + type: RedirectType.Team, + }, + // Add a User redirect with same slug to verify type filtering + { + from: "sales-team", + toUrl: "https://acme.cal.local:3000/wrong-user-redirect", + type: RedirectType.User, + }, + ], + }); + + const params = createTestRedirectParams({ + slugs: ["sales-team"], + redirectType: RedirectType.Team, + }); + const result = await handleOrgRedirect(params); + + expectPrismaCalledWith({ type: RedirectType.Team, slugs: ["sales-team"] }); + expectRedirectTo(result, "https://acme.cal.local:3000/sales?orgRedirection=true"); + expectRedirectUsesData(result, "sales"); + }); + }); + + describe("when SINGLE_ORG_SLUG mode is enabled", () => { + beforeEach(() => { + vi.spyOn(constants, "SINGLE_ORG_SLUG", "get").mockReturnValue("acme"); + }); + + it("should redirect using relative path when accessing from base domain", async () => { + createRedirectScenario({ + redirects: [ + { + from: "pro", + toUrl: "https://acme.cal.local:3000/pro-example", + type: RedirectType.User, + }, + ], + }); + + const params = createTestRedirectParams({ + context: createTestContext({ host: "cal.local:3000" }), + currentOrgDomain: "acme", + }); + const result = await handleOrgRedirect(params); + + expectPrismaCalledWith({ type: RedirectType.User, slugs: ["pro"] }); + // Should use relative path to stay on same domain + expectRedirectTo(result, "/pro-example?orgRedirection=true"); + }); + + it("should redirect john87 to john using relative path in single org mode", async () => { + createRedirectScenario({ + redirects: [ + { + from: "john87", + toUrl: "https://acme.cal.local:3000/john", + type: RedirectType.User, + }, + ], + }); + + const params = createTestRedirectParams({ + slugs: ["john87"], + context: createTestContext({ + host: "my-instance.com", + }), + currentOrgDomain: "acme", + }); + const result = await handleOrgRedirect(params); + + expectPrismaCalledWith({ type: RedirectType.User, slugs: ["john87"] }); + // Should redirect to relative path /john to stay on my-instance.com + expectRedirectTo(result, "/john?orgRedirection=true"); + }); + + describe("when orgRedirection parameter is present", () => { + it("should not redirect to prevent loops", async () => { + const params = createTestRedirectParams({ + context: createTestContext({ query: { orgRedirection: "true" } }), + currentOrgDomain: "acme", + }); + const result = await handleOrgRedirect(params); + + expectPrismaNotCalled(); + expectNoRedirect(result); + }); + }); + }); + + describe("when event type slug is provided", () => { + it("should include event type slug in redirect destination", async () => { + createRedirectScenario({ + redirects: [ + { + from: "pro", + toUrl: "https://acme.cal.local:3000/pro-example", + type: RedirectType.User, + }, + ], + }); + + const params = createTestRedirectParams({ + eventTypeSlug: "30min", + }); + const result = await handleOrgRedirect(params); + + expectPrismaCalledWith({ type: RedirectType.User, slugs: ["pro"] }); + expectRedirectTo(result, "https://acme.cal.local:3000/pro-example/30min?orgRedirection=true"); + // Verify the event type slug was properly appended + const url = new URL(result?.redirect.destination || ""); + expect(url.pathname).toBe("/pro-example/30min"); + }); + + it("should handle empty event type slug", async () => { + createRedirectScenario({ + redirects: [ + { + from: "pro", + toUrl: "https://acme.cal.local:3000/pro-example", + type: RedirectType.User, + }, + ], + }); + + const params = createTestRedirectParams({ + eventTypeSlug: null, + }); + const result = await handleOrgRedirect(params); + + expectRedirectTo(result, "https://acme.cal.local:3000/pro-example?orgRedirection=true"); + expectRedirectUsesData(result, "pro-example"); + }); + }); + + describe("when handling multiple slugs (group bookings)", () => { + it("should preserve order of slugs in redirect", async () => { + createRedirectScenario({ + redirects: [ + { + from: "user1", + toUrl: "https://acme.cal.local:3000/org-user1", + type: RedirectType.User, + }, + { + from: "user2", + toUrl: "https://acme.cal.local:3000/org-user2", + type: RedirectType.User, + }, + { + from: "user3", + toUrl: "https://acme.cal.local:3000/org-user3", + type: RedirectType.User, + }, + ], + }); + + const params = createTestRedirectParams({ + slugs: ["user1", "user2", "user3"], + }); + const result = await handleOrgRedirect(params); + + expectRedirectTo( + result, + "https://acme.cal.local:3000/org-user1+org-user2+org-user3?orgRedirection=true" + ); + // Verify order is preserved from input slugs + const url = new URL(result?.redirect.destination || ""); + expect(url.pathname).toBe("/org-user1+org-user2+org-user3"); + }); + + it("should handle partial redirects in group bookings", async () => { + createRedirectScenario({ + redirects: [ + { + from: "user1", + toUrl: "https://acme.cal.local:3000/org-user1", + type: RedirectType.User, + }, + // user2 has no redirect - it should stay as-is + { + from: "user3", + toUrl: "https://acme.cal.local:3000/org-user3", + type: RedirectType.User, + }, + ], + }); + + const params = createTestRedirectParams({ + slugs: ["user1", "user2", "user3"], + }); + const result = await handleOrgRedirect(params); + + expectRedirectTo(result, "https://acme.cal.local:3000/org-user1+user2+org-user3?orgRedirection=true"); + // Verify that user2 remains unchanged while others are redirected + const url = new URL(result?.redirect.destination || ""); + expect(url.pathname).toBe("/org-user1+user2+org-user3"); + }); + + it("should handle different origins in redirects", async () => { + createRedirectScenario({ + redirects: [ + { + from: "user1", + toUrl: "https://org1.cal.com/john", + type: RedirectType.User, + }, + { + from: "user2", + toUrl: "https://org1.cal.com/jane", + type: RedirectType.User, + }, + ], + }); + + const params = createTestRedirectParams({ + slugs: ["user1", "user2"], + }); + const result = await handleOrgRedirect(params); + + // Should use the first redirect's origin + expectRedirectTo(result, "https://org1.cal.com/john+jane?orgRedirection=true"); + expectRedirectUsesData(result, "john+jane", "https://org1.cal.com"); + }); + }); + + describe("when handling query parameters", () => { + it("should add orgRedirection=true to empty query", async () => { + createRedirectScenario({ + redirects: [ + { + from: "pro", + toUrl: "https://acme.cal.local:3000/pro-example", + type: RedirectType.User, + }, + ], + }); + + const params = createTestRedirectParams({ + context: createTestContext({ query: {} }), + }); + const result = await handleOrgRedirect(params); + + expectRedirectTo(result, "https://acme.cal.local:3000/pro-example?orgRedirection=true"); + expectRedirectUsesData(result, "pro-example"); + }); + + it("should preserve existing query parameters", async () => { + createRedirectScenario({ + redirects: [ + { + from: "pro", + toUrl: "https://acme.cal.local:3000/pro-example", + type: RedirectType.User, + }, + ], + }); + + const params = createTestRedirectParams({ + context: createTestContext({ + query: { abc: "1", xyz: "test" }, + }), + }); + const result = await handleOrgRedirect(params); + + expectRedirectTo(result, "https://acme.cal.local:3000/pro-example?abc=1&xyz=test&orgRedirection=true"); + // Verify query parameters are preserved + const url = new URL(result?.redirect.destination || ""); + expect(url.searchParams.get("abc")).toBe("1"); + expect(url.searchParams.get("xyz")).toBe("test"); + }); + + it("should not duplicate orgRedirection parameter", async () => { + createRedirectScenario({ + redirects: [ + { + from: "pro", + toUrl: "https://acme.cal.local:3000/pro-example", + type: RedirectType.User, + }, + ], + }); + + const params = createTestRedirectParams({ + context: createTestContext({ + query: { orgRedirection: "true", abc: "1" }, + }), + }); + const result = await handleOrgRedirect(params); + + expectRedirectTo(result, "https://acme.cal.local:3000/pro-example?abc=1&orgRedirection=true"); + + // Verify only one orgRedirection parameter exists + const url = new URL(result?.redirect.destination || ""); + const orgRedirectionParams = url.searchParams.getAll("orgRedirection"); + expect(orgRedirectionParams).toHaveLength(1); + expect(orgRedirectionParams[0]).toBe("true"); + }); + + it("should handle array query parameters", async () => { + createRedirectScenario({ + redirects: [ + { + from: "pro", + toUrl: "https://acme.cal.local:3000/pro-example", + type: RedirectType.User, + }, + ], + }); + + const params = createTestRedirectParams({ + context: createTestContext({ + query: { tags: ["tag1", "tag2"], filter: "active" }, + }), + }); + const result = await handleOrgRedirect(params); + + const url = new URL(result?.redirect.destination || ""); + expect(url.searchParams.getAll("tags")).toEqual(["tag1", "tag2"]); + expect(url.searchParams.get("filter")).toBe("active"); + expect(url.searchParams.get("orgRedirection")).toBe("true"); + }); + }); + + describe("edge cases and error scenarios", () => { + it("should handle empty slugs array", async () => { + createRedirectScenario({ redirects: [] }); + + const params = createTestRedirectParams({ + slugs: [], + }); + const result = await handleOrgRedirect(params); + + expectPrismaCalledWith({ type: RedirectType.User, slugs: [] }); + expectNoRedirect(result); + }); + + it("should handle redirects with trailing slashes", async () => { + createRedirectScenario({ + redirects: [ + { + from: "pro", + toUrl: "https://acme.cal.local:3000/pro-example/", + type: RedirectType.User, + }, + ], + }); + + const params = createTestRedirectParams(); + const result = await handleOrgRedirect(params); + + expectRedirectTo(result, "https://acme.cal.local:3000/pro-example/?orgRedirection=true"); + // Verify trailing slash is preserved + const url = new URL(result?.redirect.destination || ""); + expect(url.pathname).toBe("/pro-example/"); + }); + + it("should handle redirects to root path", async () => { + createRedirectScenario({ + redirects: [ + { + from: "pro", + toUrl: "https://acme.cal.local:3000", + type: RedirectType.User, + }, + ], + }); + + const params = createTestRedirectParams(); + const result = await handleOrgRedirect(params); + + expectRedirectTo(result, "https://acme.cal.local:3000?orgRedirection=true"); + // Verify root path redirect + const url = new URL(result?.redirect.destination || ""); + expect(url.pathname).toBe("/"); + }); + + it("should handle special characters in slugs", async () => { + createRedirectScenario({ + redirects: [ + { + from: "user-name.test", + toUrl: "https://acme.cal.local:3000/user_name_test", + type: RedirectType.User, + }, + ], + }); + + const params = createTestRedirectParams({ + slugs: ["user-name.test"], + }); + const result = await handleOrgRedirect(params); + + expectRedirectTo(result, "https://acme.cal.local:3000/user_name_test?orgRedirection=true"); + expectRedirectUsesData(result, "user_name_test"); + }); + }); + + describe("when SINGLE_ORG_SLUG is not set", () => { + beforeEach(() => { + vi.spyOn(constants, "SINGLE_ORG_SLUG", "get").mockReturnValue(undefined); + }); + + it("should not check for redirects when in org context", async () => { + const params = createTestRedirectParams({ + currentOrgDomain: "acme", + }); + const result = await handleOrgRedirect(params); + + expectPrismaNotCalled(); + expectNoRedirect(result); + }); + }); +}); + +describe("getRedirectWithOriginAndSearchString", () => { + beforeEach(() => { + vi.spyOn(constants, "SINGLE_ORG_SLUG", "get").mockReturnValue(undefined); + }); + + afterEach(() => { + prismaMock.tempOrgRedirect.findMany.mockReset(); + }); + + it("should return origin and search string for absolute URL redirects", async () => { + createRedirectScenario({ + redirects: [ + { + from: "john", + toUrl: "https://acme.cal.local:3000/john-org", + type: RedirectType.User, + }, + ], + }); + + const params = { + slugs: ["john"], + redirectType: RedirectType.User, + context: createTestContext(), + currentOrgDomain: null, + }; + const result = await getRedirectWithOriginAndSearchString(params); + + expect(result).not.toBeNull(); + expect(result?.origin).toBe("https://acme.cal.local:3000"); + expect(result?.searchString).toBe("?orgRedirection=true"); + }); + + it("should return null origin for relative path redirects", async () => { + // Enable SINGLE_ORG_SLUG for this test to get relative paths + vi.spyOn(constants, "SINGLE_ORG_SLUG", "get").mockReturnValue("acme"); + + createRedirectScenario({ + redirects: [ + { + from: "john", + toUrl: "https://acme.cal.local:3000/john-org", + type: RedirectType.User, + }, + ], + }); + + const params = { + slugs: ["john"], + redirectType: RedirectType.User, + context: createTestContext({ host: "my-instance.com" }), + currentOrgDomain: "acme", + }; + const result = await getRedirectWithOriginAndSearchString(params); + + expect(result).not.toBeNull(); + expect(result?.origin).toBeNull(); // Relative path has no origin + expect(result?.searchString).toBe("?orgRedirection=true"); + }); + + it("should preserve existing query parameters in search string", async () => { + createRedirectScenario({ + redirects: [ + { + from: "john", + toUrl: "https://acme.cal.local:3000/john-org", + type: RedirectType.User, + }, + ], + }); + + const params = { + slugs: ["john"], + redirectType: RedirectType.User, + context: createTestContext({ query: { foo: "bar", baz: "qux" } }), + currentOrgDomain: null, + }; + const result = await getRedirectWithOriginAndSearchString(params); + + expect(result).not.toBeNull(); + expect(result?.origin).toBe("https://acme.cal.local:3000"); + expect(result?.searchString).toBe("?foo=bar&baz=qux&orgRedirection=true"); + }); + + it("should return null when no redirect exists", async () => { + createRedirectScenario({ redirects: [] }); + + const params = { + slugs: ["john"], + redirectType: RedirectType.User, + context: createTestContext(), + currentOrgDomain: null, + }; + const result = await getRedirectWithOriginAndSearchString(params); + + expect(result).toBeNull(); + }); +}); diff --git a/apps/web/lib/handleOrgRedirect.ts b/apps/web/lib/handleOrgRedirect.ts new file mode 100644 index 0000000000..b5bed03f1a --- /dev/null +++ b/apps/web/lib/handleOrgRedirect.ts @@ -0,0 +1,203 @@ +import type { ParsedUrlQuery } from "querystring"; +import { stringify } from "querystring"; + +import { SINGLE_ORG_SLUG } from "@calcom/lib/constants"; +import logger from "@calcom/lib/logger"; +import { safeStringify } from "@calcom/lib/safeStringify"; +import type { RedirectType } from "@calcom/prisma/client"; + +const log = logger.getSubLogger({ prefix: ["lib", "handleOrgRedirect"] }); +type NextJsRedirect = { + redirect: { + permanent: false; + /** + * It could be a full URL or a relative path + */ + destination: string; + }; +}; + +function getSearchString(relativeOrAbsoluteUrl: string) { + const url = new URL(relativeOrAbsoluteUrl, "http://localhost"); + return url.search; +} + +const getTemporaryOrgRedirect = async ({ + slugs, + redirectType, + eventTypeSlug, + currentQuery, + useRelativePath = false, +}: { + slugs: string[]; + redirectType: RedirectType; + eventTypeSlug: string | null; + currentQuery: ParsedUrlQuery; + useRelativePath?: boolean; +}): Promise => { + const prisma = (await import("@calcom/prisma")).default; + log.debug( + `Looking for redirect for`, + safeStringify({ + slugs, + redirectType, + eventTypeSlug, + }) + ); + + const redirects = await prisma.tempOrgRedirect.findMany({ + where: { + type: redirectType, + from: { + in: slugs, + }, + fromOrgId: 0, + }, + }); + + if (!redirects.length) { + return null; + } + + // Use the first redirect origin as the new origin as we aren't supposed to handle different org usernames in a group + const newOrigin = new URL(redirects[0].toUrl).origin; + + // Filter out any existing orgRedirection parameter to avoid duplicates + // querystring.stringify transforms undefined values to empty strings, so we need to filter those out as well initially + const filteredQuery = Object.entries(currentQuery).reduce((acc, [key, value]) => { + if (key !== "orgRedirection" && value !== undefined) { + acc[key] = value; + } + return acc; + }, {} as ParsedUrlQuery); + + const currentQueryString = stringify(filteredQuery); + const query = currentQueryString ? `?${currentQueryString}&orgRedirection=true` : "?orgRedirection=true"; + // Use the same order as in input slugs - It is important from Dynamic Group perspective as the first user's settings are used for various things + const newSlugs = slugs.map((slug) => { + const redirect = redirects.find((redirect) => redirect.from === slug); + if (!redirect) { + return slug; + } + const newSlug = new URL(redirect.toUrl).pathname.slice(1); + return newSlug; + }); + + const newSlug = newSlugs.join("+"); + const newPath = newSlug ? `/${newSlug}` : ""; + + // When in single org mode and not on actual org subdomain, use relative path to stay on same domain + const newDestination = useRelativePath + ? `${newPath}${eventTypeSlug ? `/${eventTypeSlug}` : ""}${query}` + : `${newOrigin}${newPath}${eventTypeSlug ? `/${eventTypeSlug}` : ""}${query}`; + log.debug(`Suggesting redirect from ${slugs} to ${newDestination}`); + + return { + redirect: { + permanent: false, + destination: newDestination, + }, + } as const; +}; + +interface HandleOrgRedirectParams { + slugs: string[]; + redirectType: RedirectType; + eventTypeSlug: string | null; + context: { + query: ParsedUrlQuery; + }; + currentOrgDomain: string | null; +} + +/** + * Handles organization redirects for both regular org context and SINGLE_ORG_SLUG mode + * The redirect is required for all existing user links and team links to keep working when a user/team is moved to an organization + * Example: + * - User "john87" is added to organization "acme" and his username in the organization is "john". So, cal.com/john87 is redirected to cal.com/john + * - Team "acme-sales" is added to organization "acme" and its slug in the organization is "sales". So, cal.com/acme-sales is redirected to cal.com/sales + * + * Returns a redirect object if a redirect is needed, null otherwise + */ +export async function handleOrgRedirect({ + slugs, + redirectType, + eventTypeSlug, + context, + currentOrgDomain, +}: HandleOrgRedirectParams) { + const isOrgContext = !!currentOrgDomain; + const isARedirectFromNonOrgLink = context.query.orgRedirection === "true"; + + // If we're not in an org context, the request could clearly be eligible for a redirect + if (!isOrgContext) { + const nextJsRedirect = await getTemporaryOrgRedirect({ + slugs, + redirectType, + eventTypeSlug, + currentQuery: context.query, + }); + + if (nextJsRedirect) { + return nextJsRedirect; + } + } + + const isSingleOrgMode = SINGLE_ORG_SLUG && isOrgContext; + + // When SINGLE_ORG_SLUG is set(which is possible in Self-Hosted instances), isOrgContext could be true even when the current domain is not an org subdomain + // Example: my-instance.com could technically mean acme.my-instance.com where acme is the org slug + // In such a case, the existing links which were on my-instance.com e.g. my-instance.com/john87 should be redirected to my-instance.com/john where john is the new username in Organization + // This is why we need to follow redirect even when in Org Context + if (!isSingleOrgMode) { + return null; + } + + // If already redirected from a non-org link, we shouldn't redirect again. Protects against infinite redirects + if (isARedirectFromNonOrgLink) { + return null; + } + + // We're in single org mode but not on an actual org subdomain + // Check if there's a redirect for this username + // Use relative path since we want to stay on the same domain (my-instance.com) + const nextJsRedirect = await getTemporaryOrgRedirect({ + slugs, + redirectType, + eventTypeSlug, + currentQuery: context.query, + useRelativePath: true, + }); + + return nextJsRedirect; +} + +export async function getRedirectWithOriginAndSearchString({ + slugs, + redirectType, + context, + currentOrgDomain, +}: Omit) { + const nextJsRedirect = await handleOrgRedirect({ + slugs, + redirectType, + eventTypeSlug: null, + context, + currentOrgDomain, + }); + if (!nextJsRedirect) { + return null; + } + + const newDestination = nextJsRedirect.redirect.destination; + // If there is an origin use it, otherwise mark no new Origin and it remains the same domain + const newOrigin = + newDestination.startsWith("http://") || newDestination.startsWith("https://") + ? new URL(newDestination).origin + : null; + + return { + origin: newOrigin, + searchString: getSearchString(newDestination), + }; +} diff --git a/apps/web/lib/org/[orgSlug]/[user]/getServerSideProps.tsx b/apps/web/lib/org/[orgSlug]/[user]/getServerSideProps.tsx index 6b5c35d4a4..5615092fb2 100644 --- a/apps/web/lib/org/[orgSlug]/[user]/getServerSideProps.tsx +++ b/apps/web/lib/org/[orgSlug]/[user]/getServerSideProps.tsx @@ -23,15 +23,18 @@ export const getServerSideProps = async (ctx: GetServerSidePropsContext) => { if (team) { return GSSTeamPage({ ...ctx, - query: { slug: ctx.query.user, orgRedirection: ctx.query.orgRedirection }, + query: { + slug: ctx.query.user, + ...(ctx.query.orgRedirection !== undefined && { orgRedirection: ctx.query.orgRedirection }), + }, }); } return GSSUserPage({ ...ctx, query: { user: ctx.query.user, - redirect: ctx.query.redirect, - orgRedirection: ctx.query.orgRedirection, + ...(ctx.query.redirect !== undefined && { redirect: ctx.query.redirect }), + ...(ctx.query.orgRedirection !== undefined && { orgRedirection: ctx.query.orgRedirection }), }, }); }; diff --git a/apps/web/lib/team/[slug]/[type]/getServerSideProps.tsx b/apps/web/lib/team/[slug]/[type]/getServerSideProps.tsx index d1719158b0..a050a86511 100644 --- a/apps/web/lib/team/[slug]/[type]/getServerSideProps.tsx +++ b/apps/web/lib/team/[slug]/[type]/getServerSideProps.tsx @@ -15,7 +15,7 @@ import type { User } from "@calcom/prisma/client"; import { BookingStatus, RedirectType } from "@calcom/prisma/client"; import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils"; -import { getTemporaryOrgRedirect } from "@lib/getTemporaryOrgRedirect"; +import { handleOrgRedirect } from "@lib/handleOrgRedirect"; const paramsSchema = z.object({ type: z.string().transform((s) => slugify(s)), @@ -33,19 +33,17 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => const { rescheduleUid, isInstantMeeting: queryIsInstantMeeting, email } = query; const allowRescheduleForCancelledBooking = query.allowRescheduleForCancelledBooking === "true"; const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(req, params?.orgSlug); - const isOrgContext = currentOrgDomain && isValidOrgDomain; - if (!isOrgContext) { - const redirect = await getTemporaryOrgRedirect({ - slugs: teamSlug, - redirectType: RedirectType.Team, - eventTypeSlug: meetingSlug, - currentQuery: context.query, - }); + const redirect = await handleOrgRedirect({ + slugs: [teamSlug], + redirectType: RedirectType.Team, + eventTypeSlug: meetingSlug, + context, + currentOrgDomain: isValidOrgDomain ? currentOrgDomain : null, + }); - if (redirect) { - return redirect; - } + if (redirect) { + return redirect; } const team = await getTeamWithEventsData(teamSlug, meetingSlug, isValidOrgDomain, currentOrgDomain); diff --git a/apps/web/lib/team/[slug]/getServerSideProps.tsx b/apps/web/lib/team/[slug]/getServerSideProps.tsx index 4acfba0e3a..9f71ad2b48 100644 --- a/apps/web/lib/team/[slug]/getServerSideProps.tsx +++ b/apps/web/lib/team/[slug]/getServerSideProps.tsx @@ -19,7 +19,7 @@ import type { Team, OrganizationSettings } from "@calcom/prisma/client"; import { RedirectType } from "@calcom/prisma/client"; import { teamMetadataSchema } from "@calcom/prisma/zod-utils"; -import { getTemporaryOrgRedirect } from "@lib/getTemporaryOrgRedirect"; +import { handleOrgRedirect } from "@lib/handleOrgRedirect"; const log = logger.getSubLogger({ prefix: ["team/[slug]"] }); @@ -67,7 +67,6 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => context.req, context.params?.orgSlug ?? context.query?.orgSlug ); - const isOrgContext = isValidOrgDomain && currentOrgDomain; // Provided by Rewrite from next.config.js const isOrgProfile = context.query?.isOrgProfile === "1"; @@ -89,12 +88,13 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => isOrgView: isValidOrgDomain && isOrgProfile, }); - if (!isOrgContext && slug) { - const redirect = await getTemporaryOrgRedirect({ - slugs: slug, + if (slug) { + const redirect = await handleOrgRedirect({ + slugs: [slug], redirectType: RedirectType.Team, eventTypeSlug: null, - currentQuery: context.query, + context, + currentOrgDomain: isValidOrgDomain ? currentOrgDomain : null, }); if (redirect) { diff --git a/apps/web/server/lib/[user]/[type]/getServerSideProps.ts b/apps/web/server/lib/[user]/[type]/getServerSideProps.ts index 5ce7977b43..9846073240 100644 --- a/apps/web/server/lib/[user]/[type]/getServerSideProps.ts +++ b/apps/web/server/lib/[user]/[type]/getServerSideProps.ts @@ -15,7 +15,7 @@ import slugify from "@calcom/lib/slugify"; import prisma from "@calcom/prisma"; import { BookingStatus, RedirectType } from "@calcom/prisma/client"; -import { getTemporaryOrgRedirect } from "@lib/getTemporaryOrgRedirect"; +import { handleOrgRedirect } from "@lib/handleOrgRedirect"; import { getUsersInOrgContext } from "@server/lib/[user]/getServerSideProps"; @@ -122,17 +122,17 @@ async function getDynamicGroupPageProps(context: GetServerSidePropsContext) { const allowRescheduleForCancelledBooking = context.query.allowRescheduleForCancelledBooking === "true"; const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(context.req, context.params?.orgSlug); const org = isValidOrgDomain ? currentOrgDomain : null; - if (!org) { - const redirect = await getTemporaryOrgRedirect({ - slugs: usernames, - redirectType: RedirectType.User, - eventTypeSlug: slug, - currentQuery: context.query, - }); - if (redirect) { - return redirect; - } + const redirect = await handleOrgRedirect({ + slugs: usernames, + redirectType: RedirectType.User, + eventTypeSlug: slug, + context, + currentOrgDomain: org, + }); + + if (redirect) { + return redirect; } const userRepo = new UserRepository(prisma); @@ -220,18 +220,16 @@ async function getUserPageProps(context: GetServerSidePropsContext) { const allowRescheduleForCancelledBooking = context.query.allowRescheduleForCancelledBooking === "true"; const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(context.req, context.params?.orgSlug); - const isOrgContext = currentOrgDomain && isValidOrgDomain; - if (!isOrgContext) { - const redirect = await getTemporaryOrgRedirect({ - slugs: usernames, - redirectType: RedirectType.User, - eventTypeSlug: slug, - currentQuery: context.query, - }); + const redirect = await handleOrgRedirect({ + slugs: usernames, + redirectType: RedirectType.User, + eventTypeSlug: slug, + context, + currentOrgDomain: isValidOrgDomain ? currentOrgDomain : null, + }); - if (redirect) { - return redirect; - } + if (redirect) { + return redirect; } const [user] = await getUsersInOrgContext([username], isValidOrgDomain ? currentOrgDomain : null); diff --git a/apps/web/server/lib/[user]/getServerSideProps.ts b/apps/web/server/lib/[user]/getServerSideProps.ts index a41a603ded..48ffa0357d 100644 --- a/apps/web/server/lib/[user]/getServerSideProps.ts +++ b/apps/web/server/lib/[user]/getServerSideProps.ts @@ -18,7 +18,7 @@ import { RedirectType, type EventType, type User } from "@calcom/prisma/client"; import type { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils"; import type { UserProfile } from "@calcom/types/UserProfile"; -import { getTemporaryOrgRedirect } from "@lib/getTemporaryOrgRedirect"; +import { handleOrgRedirect } from "@lib/handleOrgRedirect"; const log = logger.getSubLogger({ prefix: ["[[pages/[user]]]"] }); type UserPageProps = { @@ -73,25 +73,20 @@ type UserPageProps = { export const getServerSideProps: GetServerSideProps = async (context) => { const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(context.req, context.params?.orgSlug); - const usernameList = getUsernameList(context.query.user as string); const isARedirectFromNonOrgLink = context.query.orgRedirection === "true"; - const isOrgContext = isValidOrgDomain && !!currentOrgDomain; - const dataFetchStart = Date.now(); - if (!isOrgContext) { - // If there is no org context, see if some redirect is setup due to org migration - const redirect = await getTemporaryOrgRedirect({ - slugs: usernameList, - redirectType: RedirectType.User, - eventTypeSlug: null, - currentQuery: context.query, - }); + const redirect = await handleOrgRedirect({ + slugs: usernameList, + redirectType: RedirectType.User, + eventTypeSlug: null, + context, + currentOrgDomain: isValidOrgDomain ? currentOrgDomain : null, + }); - if (redirect) { - return redirect; - } + if (redirect) { + return redirect; } const usersInOrgContext = await getUsersInOrgContext(