From 225055fb0c96ab6af2810d93be874b20db52686c Mon Sep 17 00:00:00 2001 From: Hariom Balhara Date: Tue, 17 Oct 2023 08:36:46 +0530 Subject: [PATCH] feat: Support moving a user and it's teams to an org as temporary approach (#11892) --- apps/web/lib/getTemporaryOrgRedirect.ts | 44 +++++++++++++++++++ apps/web/pages/[user].tsx | 20 ++++++++- apps/web/pages/[user]/[type].tsx | 25 +++++++++-- .../org/[orgSlug]/[user]/[type]/embed.tsx | 16 +------ apps/web/pages/team/[slug].tsx | 18 ++++++++ apps/web/pages/team/[slug]/[type].tsx | 16 +++++++ .../migration.sql | 19 ++++++++ packages/prisma/schema.prisma | 21 +++++++++ .../viewer/organizations/list.handler.ts | 7 +++ 9 files changed, 166 insertions(+), 20 deletions(-) create mode 100644 apps/web/lib/getTemporaryOrgRedirect.ts create mode 100644 packages/prisma/migrations/20231014180034_add_temp_org_redirect/migration.sql diff --git a/apps/web/lib/getTemporaryOrgRedirect.ts b/apps/web/lib/getTemporaryOrgRedirect.ts new file mode 100644 index 0000000000..11c4578f5d --- /dev/null +++ b/apps/web/lib/getTemporaryOrgRedirect.ts @@ -0,0 +1,44 @@ +import logger from "@calcom/lib/logger"; +import { safeStringify } from "@calcom/lib/safeStringify"; +import type { RedirectType } from "@calcom/prisma/client"; + +const log = logger.getChildLogger({ prefix: ["lib", "getTemporaryOrgRedirect"] }); +export const getTemporaryOrgRedirect = async ({ + slug, + redirectType, + eventTypeSlug, +}: { + slug: string; + redirectType: RedirectType; + eventTypeSlug: string | null; +}) => { + const prisma = (await import("@calcom/prisma")).default; + log.debug( + `Looking for redirect for`, + safeStringify({ + slug, + redirectType, + eventTypeSlug, + }) + ); + const redirect = await prisma.tempOrgRedirect.findUnique({ + where: { + from_type_fromOrgId: { + type: redirectType, + from: slug, + fromOrgId: 0, + }, + }, + }); + + if (redirect) { + log.debug(`Redirecting ${slug} to ${redirect.toUrl}`); + return { + redirect: { + permanent: false, + destination: eventTypeSlug ? `${redirect.toUrl}/${eventTypeSlug}` : redirect.toUrl, + }, + } as const; + } + return null; +}; diff --git a/apps/web/pages/[user].tsx b/apps/web/pages/[user].tsx index 763cd86ccf..1392af4cfa 100644 --- a/apps/web/pages/[user].tsx +++ b/apps/web/pages/[user].tsx @@ -23,7 +23,7 @@ import useTheme from "@calcom/lib/hooks/useTheme"; import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML"; import { stripMarkdown } from "@calcom/lib/stripMarkdown"; import prisma from "@calcom/prisma"; -import type { EventType, User } from "@calcom/prisma/client"; +import { RedirectType, type EventType, type User } from "@calcom/prisma/client"; import { baseEventTypeSelect } from "@calcom/prisma/selects"; import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils"; import { HeadSeo, UnpublishedEntity } from "@calcom/ui"; @@ -35,6 +35,8 @@ import PageWrapper from "@components/PageWrapper"; import { ssrInit } from "@server/lib/ssr"; +import { getTemporaryOrgRedirect } from "../lib/getTemporaryOrgRedirect"; + export function UserPage(props: InferGetServerSidePropsType) { const { users, profile, eventTypes, markdownStrippedBio, entity } = props; @@ -261,13 +263,14 @@ export const getServerSideProps: GetServerSideProps = async (cont context.params?.orgSlug ); const usernameList = getUsernameList(context.query.user as string); + const isOrgContext = isValidOrgDomain && currentOrgDomain; const dataFetchStart = Date.now(); const usersWithoutAvatar = await prisma.user.findMany({ where: { username: { in: usernameList, }, - organization: isValidOrgDomain && currentOrgDomain ? getSlugOrRequestedSlug(currentOrgDomain) : null, + organization: isOrgContext ? getSlugOrRequestedSlug(currentOrgDomain) : null, }, select: { id: true, @@ -275,6 +278,7 @@ export const getServerSideProps: GetServerSideProps = async (cont email: true, name: true, bio: true, + metadata: true, brandColor: true, darkBrandColor: true, organizationId: true, @@ -312,6 +316,18 @@ export const getServerSideProps: GetServerSideProps = async (cont avatar: `/${user.username}/avatar.png`, })); + if (!isOrgContext) { + const redirect = await getTemporaryOrgRedirect({ + slug: usernameList[0], + redirectType: RedirectType.User, + eventTypeSlug: null, + }); + + if (redirect) { + return redirect; + } + } + if (!users.length || (!isValidOrgDomain && !users.some((user) => user.organizationId === null))) { return { notFound: true, diff --git a/apps/web/pages/[user]/[type].tsx b/apps/web/pages/[user]/[type].tsx index 4c568cc946..2b75259196 100644 --- a/apps/web/pages/[user]/[type].tsx +++ b/apps/web/pages/[user]/[type].tsx @@ -15,12 +15,15 @@ import { orgDomainConfig, userOrgQuery } from "@calcom/features/ee/organizations import { getUsernameList } from "@calcom/lib/defaultEvents"; import slugify from "@calcom/lib/slugify"; import prisma from "@calcom/prisma"; +import { RedirectType } from "@calcom/prisma/client"; import type { inferSSRProps } from "@lib/types/inferSSRProps"; import type { EmbedProps } from "@lib/withEmbedSsr"; import PageWrapper from "@components/PageWrapper"; +import { getTemporaryOrgRedirect } from "../../lib/getTemporaryOrgRedirect"; + export type PageProps = inferSSRProps & EmbedProps; export default function Type({ @@ -93,7 +96,7 @@ async function getDynamicGroupPageProps(context: GetServerSidePropsContext) { if (!users.length) { return { notFound: true, - }; + } as const; } const org = isValidOrgDomain ? currentOrgDomain : null; @@ -115,7 +118,7 @@ async function getDynamicGroupPageProps(context: GetServerSidePropsContext) { if (!eventData) { return { notFound: true, - }; + } as const; } return { @@ -150,6 +153,20 @@ async function getUserPageProps(context: GetServerSidePropsContext) { context.params?.orgSlug ); + const isOrgContext = currentOrgDomain && isValidOrgDomain; + + if (!isOrgContext) { + const redirect = await getTemporaryOrgRedirect({ + slug: usernames[0], + redirectType: RedirectType.User, + eventTypeSlug: slug, + }); + + if (redirect) { + return redirect; + } + } + const { ssrInit } = await import("@server/lib/ssr"); const ssr = await ssrInit(context); const user = await prisma.user.findFirst({ @@ -167,7 +184,7 @@ async function getUserPageProps(context: GetServerSidePropsContext) { if (!user) { return { notFound: true, - }; + } as const; } let booking: GetBookingType | null = null; @@ -189,7 +206,7 @@ async function getUserPageProps(context: GetServerSidePropsContext) { if (!eventData) { return { notFound: true, - }; + } as const; } return { diff --git a/apps/web/pages/org/[orgSlug]/[user]/[type]/embed.tsx b/apps/web/pages/org/[orgSlug]/[user]/[type]/embed.tsx index b9384e607e..5cfd1726ca 100644 --- a/apps/web/pages/org/[orgSlug]/[user]/[type]/embed.tsx +++ b/apps/web/pages/org/[orgSlug]/[user]/[type]/embed.tsx @@ -1,19 +1,7 @@ -import type { GetServerSidePropsContext } from "next"; +import withEmbedSsr from "@lib/withEmbedSsr"; import { getServerSideProps as _getServerSideProps } from "../[type]"; export { default } from "../[type]"; -export const getServerSideProps = async (context: GetServerSidePropsContext) => { - const ssrResponse = await _getServerSideProps(context); - if (ssrResponse.notFound) { - return ssrResponse; - } - return { - ...ssrResponse, - props: { - ...ssrResponse.props, - isEmbed: true, - }, - }; -}; +export const getServerSideProps = withEmbedSsr(_getServerSideProps); diff --git a/apps/web/pages/team/[slug].tsx b/apps/web/pages/team/[slug].tsx index c6e68d5310..3d6c4bc7bb 100644 --- a/apps/web/pages/team/[slug].tsx +++ b/apps/web/pages/team/[slug].tsx @@ -18,6 +18,7 @@ import slugify from "@calcom/lib/slugify"; import { stripMarkdown } from "@calcom/lib/stripMarkdown"; import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry"; import prisma from "@calcom/prisma"; +import { RedirectType } from "@calcom/prisma/client"; import { teamMetadataSchema } from "@calcom/prisma/zod-utils"; import { Avatar, AvatarGroup, Button, HeadSeo, UnpublishedEntity } from "@calcom/ui"; import { ArrowRight } from "@calcom/ui/components/icon"; @@ -30,6 +31,8 @@ import Team from "@components/team/screens/Team"; import { ssrInit } from "@server/lib/ssr"; +import { getTemporaryOrgRedirect } from "../../lib/getTemporaryOrgRedirect"; + export type PageProps = inferSSRProps; function TeamPage({ @@ -272,6 +275,8 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => context.req.headers.host ?? "", context.params?.orgSlug ); + const isOrgContext = isValidOrgDomain && currentOrgDomain; + const flags = await getFeatureFlagMap(prisma); const team = await getTeamWithMembers({ slug: slugify(slug ?? ""), @@ -279,6 +284,19 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => isTeamView: true, isOrgView: isValidOrgDomain && context.resolvedUrl === "/", }); + + if (!isOrgContext && slug) { + const redirect = await getTemporaryOrgRedirect({ + slug: slug, + redirectType: RedirectType.Team, + eventTypeSlug: null, + }); + + if (redirect) { + return redirect; + } + } + const ssr = await ssrInit(context); const metadata = teamMetadataSchema.parse(team?.metadata ?? {}); console.info("gSSP, team/[slug] - ", { diff --git a/apps/web/pages/team/[slug]/[type].tsx b/apps/web/pages/team/[slug]/[type].tsx index 668a82d6f8..a4204c6f03 100644 --- a/apps/web/pages/team/[slug]/[type].tsx +++ b/apps/web/pages/team/[slug]/[type].tsx @@ -11,12 +11,15 @@ import { getSlugOrRequestedSlug } from "@calcom/features/ee/organizations/lib/or import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains"; import slugify from "@calcom/lib/slugify"; import prisma from "@calcom/prisma"; +import { RedirectType } from "@calcom/prisma/client"; import type { inferSSRProps } from "@lib/types/inferSSRProps"; import type { EmbedProps } from "@lib/withEmbedSsr"; import PageWrapper from "@components/PageWrapper"; +import { getTemporaryOrgRedirect } from "../../../lib/getTemporaryOrgRedirect"; + export type PageProps = inferSSRProps & EmbedProps; export default function Type({ @@ -75,6 +78,19 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => context.req.headers.host ?? "", context.params?.orgSlug ); + const isOrgContext = currentOrgDomain && isValidOrgDomain; + + if (!isOrgContext) { + const redirect = await getTemporaryOrgRedirect({ + slug: teamSlug, + redirectType: RedirectType.Team, + eventTypeSlug: meetingSlug, + }); + + if (redirect) { + return redirect; + } + } const team = await prisma.team.findFirst({ where: { diff --git a/packages/prisma/migrations/20231014180034_add_temp_org_redirect/migration.sql b/packages/prisma/migrations/20231014180034_add_temp_org_redirect/migration.sql new file mode 100644 index 0000000000..be139f9032 --- /dev/null +++ b/packages/prisma/migrations/20231014180034_add_temp_org_redirect/migration.sql @@ -0,0 +1,19 @@ +-- CreateEnum +CREATE TYPE "RedirectType" AS ENUM ('user-event-type', 'team-event-type', 'user', 'team'); + +-- CreateTable +CREATE TABLE "TempOrgRedirect" ( + "id" SERIAL NOT NULL, + "from" TEXT NOT NULL, + "fromOrgId" INTEGER NOT NULL, + "type" "RedirectType" NOT NULL, + "toUrl" TEXT NOT NULL, + "enabled" BOOLEAN NOT NULL DEFAULT true, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "TempOrgRedirect_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "TempOrgRedirect_from_type_fromOrgId_key" ON "TempOrgRedirect"("from", "type", "fromOrgId"); diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma index bce9606ade..c74a8556c3 100644 --- a/packages/prisma/schema.prisma +++ b/packages/prisma/schema.prisma @@ -972,3 +972,24 @@ model CalendarCache { @@id([credentialId, key]) @@unique([credentialId, key]) } + +enum RedirectType { + UserEventType @map("user-event-type") + TeamEventType @map("team-event-type") + User @map("user") + Team @map("team") +} + +model TempOrgRedirect { + id Int @id @default(autoincrement()) + // Better would be to have fromOrgId and toOrgId as well and then we should have just to instead toUrl + from String + // 0 would mean it is non org + fromOrgId Int + type RedirectType + toUrl String + enabled Boolean @default(true) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + @@unique([from, type, fromOrgId]) +} \ No newline at end of file diff --git a/packages/trpc/server/routers/viewer/organizations/list.handler.ts b/packages/trpc/server/routers/viewer/organizations/list.handler.ts index d8f5bacd4d..6ee087f8c8 100644 --- a/packages/trpc/server/routers/viewer/organizations/list.handler.ts +++ b/packages/trpc/server/routers/viewer/organizations/list.handler.ts @@ -29,6 +29,13 @@ export const listHandler = async ({ ctx }: ListHandlerInput) => { }, }); + if (!membership) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "You do not have a membership to your organization", + }); + } + const metadata = teamMetadataSchema.parse(membership?.team.metadata); return {