From e2414b174acdd99e7c860e8d82337f4952745c01 Mon Sep 17 00:00:00 2001 From: Hariom Balhara Date: Fri, 20 Oct 2023 00:05:34 +0530 Subject: [PATCH] Handle non-org team with same slug as the organizations requestedSlug (#11996) --- apps/web/next.config.js | 2 +- apps/web/pages/team/[slug].tsx | 31 +++++++++++++------ packages/lib/server/queries/teams/index.ts | 24 +++++++++++++- .../viewer/organizations/create.handler.ts | 12 +++---- 4 files changed, 52 insertions(+), 17 deletions(-) diff --git a/apps/web/next.config.js b/apps/web/next.config.js index 262e1f6e5a..22da1946e5 100644 --- a/apps/web/next.config.js +++ b/apps/web/next.config.js @@ -235,7 +235,7 @@ const nextConfig = { ? [ { ...matcherConfigRootPath, - destination: "/team/:orgSlug", + destination: "/team/:orgSlug?isOrgProfile=1", }, { ...matcherConfigUserRoute, diff --git a/apps/web/pages/team/[slug].tsx b/apps/web/pages/team/[slug].tsx index 3d6c4bc7bb..73c69acd2f 100644 --- a/apps/web/pages/team/[slug].tsx +++ b/apps/web/pages/team/[slug].tsx @@ -1,3 +1,9 @@ +// This route is reachable by +// 1. /team/[slug] +// 2. / (when on org domain e.g. http://calcom.cal.com/. This is through a rewrite from next.config.js) +// Also the getServerSideProps and default export are reused by +// 1. org/[orgSlug]/team/[slug] +// 2. org/[orgSlug]/[user]/[type] import classNames from "classnames"; import type { GetServerSidePropsContext } from "next"; import Link from "next/link"; @@ -12,6 +18,7 @@ import { WEBAPP_URL } from "@calcom/lib/constants"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery"; import useTheme from "@calcom/lib/hooks/useTheme"; +import logger from "@calcom/lib/logger"; import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML"; import { getTeamWithMembers } from "@calcom/lib/server/queries/teams"; import slugify from "@calcom/lib/slugify"; @@ -34,7 +41,7 @@ import { ssrInit } from "@server/lib/ssr"; import { getTemporaryOrgRedirect } from "../../lib/getTemporaryOrgRedirect"; export type PageProps = inferSSRProps; - +const log = logger.getSubLogger({ prefix: ["team/[slug]"] }); function TeamPage({ team, isUnpublished, @@ -277,12 +284,23 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => ); const isOrgContext = isValidOrgDomain && currentOrgDomain; + // Provided by Rewrite from next.config.js + const isOrgProfile = context.query?.isOrgProfile === "1"; const flags = await getFeatureFlagMap(prisma); + const isOrganizationFeatureEnabled = flags["organizations"]; + + log.debug("getServerSideProps", { + isOrgProfile, + isOrganizationFeatureEnabled, + isValidOrgDomain, + currentOrgDomain, + }); + const team = await getTeamWithMembers({ slug: slugify(slug ?? ""), orgSlug: currentOrgDomain, isTeamView: true, - isOrgView: isValidOrgDomain && context.resolvedUrl === "/", + isOrgView: isValidOrgDomain && isOrgProfile, }); if (!isOrgContext && slug) { @@ -299,17 +317,12 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => const ssr = await ssrInit(context); const metadata = teamMetadataSchema.parse(team?.metadata ?? {}); - console.info("gSSP, team/[slug] - ", { - isValidOrgDomain, - currentOrgDomain, - ALLOWED_HOSTNAMES: process.env.ALLOWED_HOSTNAMES, - flags: JSON.stringify(flags), - }); + // Taking care of sub-teams and orgs if ( (!isValidOrgDomain && team?.parent) || (!isValidOrgDomain && !!metadata?.isOrganization) || - flags["organizations"] !== true + !isOrganizationFeatureEnabled ) { return { notFound: true } as const; } diff --git a/packages/lib/server/queries/teams/index.ts b/packages/lib/server/queries/teams/index.ts index 6cf5e6f0d7..2d1fe4189b 100644 --- a/packages/lib/server/queries/teams/index.ts +++ b/packages/lib/server/queries/teams/index.ts @@ -7,6 +7,7 @@ import { SchedulingType } from "@calcom/prisma/enums"; import { EventTypeMetaDataSchema, teamMetadataSchema } from "@calcom/prisma/zod-utils"; import { WEBAPP_URL } from "../../../constants"; +import logger from "../../../logger"; export type TeamWithMembers = Awaited>; @@ -17,6 +18,9 @@ export async function getTeamWithMembers(args: { orgSlug?: string | null; includeTeamLogo?: boolean; isTeamView?: boolean; + /** + * If true, means that you are fetching an organization and not a team + */ isOrgView?: boolean; }) { const { id, slug, userId, orgSlug, isTeamView, isOrgView, includeTeamLogo } = args; @@ -120,12 +124,30 @@ export async function getTeamWithMembers(args: { } if (id) where.id = id; if (slug) where.slug = slug; + if (isOrgView) { + // We must fetch only the organization here. + // Note that an organization and a team that doesn't belong to an organization, both have parentId null + // If the organization has null slug(but requestedSlug is 'test') and the team also has slug 'test', we can't distinguish them without explicitly checking the metadata.isOrganization + // Note that, this isn't possible now to have same requestedSlug as the slug of a team not part of an organization. This is legacy teams handling mostly. But it is still safer to be sure that you are fetching an Organization only in case of isOrgView + where.metadata = { + path: ["isOrganization"], + equals: true, + }; + } - const team = await prisma.team.findFirst({ + const teams = await prisma.team.findMany({ where, select: teamSelect, }); + if (teams.length > 1) { + logger.error("Found more than one team/Org. We should be doing something wrong.", { + where, + teams: teams.map((team) => ({ id: team.id, slug: team.slug })), + }); + } + + const team = teams[0]; if (!team) return null; // This should improve performance saving already app data found. diff --git a/packages/trpc/server/routers/viewer/organizations/create.handler.ts b/packages/trpc/server/routers/viewer/organizations/create.handler.ts index 743805fd11..1e0f9a2e03 100644 --- a/packages/trpc/server/routers/viewer/organizations/create.handler.ts +++ b/packages/trpc/server/routers/viewer/organizations/create.handler.ts @@ -72,17 +72,17 @@ export const createHandler = async ({ input, ctx }: CreateOptions) => { }, }); - const slugCollisions = await prisma.team.findFirst({ + // An org doesn't have a parentId. A team that isn't part of an org also doesn't have a parentId. + // So, an org can't have the same slug as a non-org team. + // There is a unique index on [slug, parentId] in Team because we don't add the slug to the team always. We only add metadata.requestedSlug in some cases. So, DB won't prevent creation of such an organization. + const hasANonOrgTeamOrOrgWithSameSlug = await prisma.team.findFirst({ where: { slug: slug, - metadata: { - path: ["isOrganization"], - equals: true, - }, + parentId: null, }, }); - if (slugCollisions || RESERVED_SUBDOMAINS.includes(slug)) + if (hasANonOrgTeamOrOrgWithSameSlug || RESERVED_SUBDOMAINS.includes(slug)) throw new TRPCError({ code: "BAD_REQUEST", message: "organization_url_taken" }); if (userCollisions) throw new TRPCError({ code: "BAD_REQUEST", message: "admin_email_taken" });