feat: Support moving a user and it's teams to an org as temporary approach (#11892)

This commit is contained in:
Hariom Balhara 2023-10-17 08:36:46 +05:30 committed by GitHub
parent d46e80c2ac
commit 225055fb0c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 166 additions and 20 deletions

View File

@ -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;
};

View File

@ -23,7 +23,7 @@ import useTheme from "@calcom/lib/hooks/useTheme";
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML"; import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
import { stripMarkdown } from "@calcom/lib/stripMarkdown"; import { stripMarkdown } from "@calcom/lib/stripMarkdown";
import prisma from "@calcom/prisma"; 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 { baseEventTypeSelect } from "@calcom/prisma/selects";
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils"; import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import { HeadSeo, UnpublishedEntity } from "@calcom/ui"; import { HeadSeo, UnpublishedEntity } from "@calcom/ui";
@ -35,6 +35,8 @@ import PageWrapper from "@components/PageWrapper";
import { ssrInit } from "@server/lib/ssr"; import { ssrInit } from "@server/lib/ssr";
import { getTemporaryOrgRedirect } from "../lib/getTemporaryOrgRedirect";
export function UserPage(props: InferGetServerSidePropsType<typeof getServerSideProps>) { export function UserPage(props: InferGetServerSidePropsType<typeof getServerSideProps>) {
const { users, profile, eventTypes, markdownStrippedBio, entity } = props; const { users, profile, eventTypes, markdownStrippedBio, entity } = props;
@ -261,13 +263,14 @@ export const getServerSideProps: GetServerSideProps<UserPageProps> = async (cont
context.params?.orgSlug context.params?.orgSlug
); );
const usernameList = getUsernameList(context.query.user as string); const usernameList = getUsernameList(context.query.user as string);
const isOrgContext = isValidOrgDomain && currentOrgDomain;
const dataFetchStart = Date.now(); const dataFetchStart = Date.now();
const usersWithoutAvatar = await prisma.user.findMany({ const usersWithoutAvatar = await prisma.user.findMany({
where: { where: {
username: { username: {
in: usernameList, in: usernameList,
}, },
organization: isValidOrgDomain && currentOrgDomain ? getSlugOrRequestedSlug(currentOrgDomain) : null, organization: isOrgContext ? getSlugOrRequestedSlug(currentOrgDomain) : null,
}, },
select: { select: {
id: true, id: true,
@ -275,6 +278,7 @@ export const getServerSideProps: GetServerSideProps<UserPageProps> = async (cont
email: true, email: true,
name: true, name: true,
bio: true, bio: true,
metadata: true,
brandColor: true, brandColor: true,
darkBrandColor: true, darkBrandColor: true,
organizationId: true, organizationId: true,
@ -312,6 +316,18 @@ export const getServerSideProps: GetServerSideProps<UserPageProps> = async (cont
avatar: `/${user.username}/avatar.png`, 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))) { if (!users.length || (!isValidOrgDomain && !users.some((user) => user.organizationId === null))) {
return { return {
notFound: true, notFound: true,

View File

@ -15,12 +15,15 @@ import { orgDomainConfig, userOrgQuery } from "@calcom/features/ee/organizations
import { getUsernameList } from "@calcom/lib/defaultEvents"; import { getUsernameList } from "@calcom/lib/defaultEvents";
import slugify from "@calcom/lib/slugify"; import slugify from "@calcom/lib/slugify";
import prisma from "@calcom/prisma"; import prisma from "@calcom/prisma";
import { RedirectType } from "@calcom/prisma/client";
import type { inferSSRProps } from "@lib/types/inferSSRProps"; import type { inferSSRProps } from "@lib/types/inferSSRProps";
import type { EmbedProps } from "@lib/withEmbedSsr"; import type { EmbedProps } from "@lib/withEmbedSsr";
import PageWrapper from "@components/PageWrapper"; import PageWrapper from "@components/PageWrapper";
import { getTemporaryOrgRedirect } from "../../lib/getTemporaryOrgRedirect";
export type PageProps = inferSSRProps<typeof getServerSideProps> & EmbedProps; export type PageProps = inferSSRProps<typeof getServerSideProps> & EmbedProps;
export default function Type({ export default function Type({
@ -93,7 +96,7 @@ async function getDynamicGroupPageProps(context: GetServerSidePropsContext) {
if (!users.length) { if (!users.length) {
return { return {
notFound: true, notFound: true,
}; } as const;
} }
const org = isValidOrgDomain ? currentOrgDomain : null; const org = isValidOrgDomain ? currentOrgDomain : null;
@ -115,7 +118,7 @@ async function getDynamicGroupPageProps(context: GetServerSidePropsContext) {
if (!eventData) { if (!eventData) {
return { return {
notFound: true, notFound: true,
}; } as const;
} }
return { return {
@ -150,6 +153,20 @@ async function getUserPageProps(context: GetServerSidePropsContext) {
context.params?.orgSlug 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 { ssrInit } = await import("@server/lib/ssr");
const ssr = await ssrInit(context); const ssr = await ssrInit(context);
const user = await prisma.user.findFirst({ const user = await prisma.user.findFirst({
@ -167,7 +184,7 @@ async function getUserPageProps(context: GetServerSidePropsContext) {
if (!user) { if (!user) {
return { return {
notFound: true, notFound: true,
}; } as const;
} }
let booking: GetBookingType | null = null; let booking: GetBookingType | null = null;
@ -189,7 +206,7 @@ async function getUserPageProps(context: GetServerSidePropsContext) {
if (!eventData) { if (!eventData) {
return { return {
notFound: true, notFound: true,
}; } as const;
} }
return { return {

View File

@ -1,19 +1,7 @@
import type { GetServerSidePropsContext } from "next"; import withEmbedSsr from "@lib/withEmbedSsr";
import { getServerSideProps as _getServerSideProps } from "../[type]"; import { getServerSideProps as _getServerSideProps } from "../[type]";
export { default } from "../[type]"; export { default } from "../[type]";
export const getServerSideProps = async (context: GetServerSidePropsContext) => { export const getServerSideProps = withEmbedSsr(_getServerSideProps);
const ssrResponse = await _getServerSideProps(context);
if (ssrResponse.notFound) {
return ssrResponse;
}
return {
...ssrResponse,
props: {
...ssrResponse.props,
isEmbed: true,
},
};
};

View File

@ -18,6 +18,7 @@ import slugify from "@calcom/lib/slugify";
import { stripMarkdown } from "@calcom/lib/stripMarkdown"; import { stripMarkdown } from "@calcom/lib/stripMarkdown";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry"; import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
import prisma from "@calcom/prisma"; import prisma from "@calcom/prisma";
import { RedirectType } from "@calcom/prisma/client";
import { teamMetadataSchema } from "@calcom/prisma/zod-utils"; import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
import { Avatar, AvatarGroup, Button, HeadSeo, UnpublishedEntity } from "@calcom/ui"; import { Avatar, AvatarGroup, Button, HeadSeo, UnpublishedEntity } from "@calcom/ui";
import { ArrowRight } from "@calcom/ui/components/icon"; 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 { ssrInit } from "@server/lib/ssr";
import { getTemporaryOrgRedirect } from "../../lib/getTemporaryOrgRedirect";
export type PageProps = inferSSRProps<typeof getServerSideProps>; export type PageProps = inferSSRProps<typeof getServerSideProps>;
function TeamPage({ function TeamPage({
@ -272,6 +275,8 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
context.req.headers.host ?? "", context.req.headers.host ?? "",
context.params?.orgSlug context.params?.orgSlug
); );
const isOrgContext = isValidOrgDomain && currentOrgDomain;
const flags = await getFeatureFlagMap(prisma); const flags = await getFeatureFlagMap(prisma);
const team = await getTeamWithMembers({ const team = await getTeamWithMembers({
slug: slugify(slug ?? ""), slug: slugify(slug ?? ""),
@ -279,6 +284,19 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
isTeamView: true, isTeamView: true,
isOrgView: isValidOrgDomain && context.resolvedUrl === "/", 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 ssr = await ssrInit(context);
const metadata = teamMetadataSchema.parse(team?.metadata ?? {}); const metadata = teamMetadataSchema.parse(team?.metadata ?? {});
console.info("gSSP, team/[slug] - ", { console.info("gSSP, team/[slug] - ", {

View File

@ -11,12 +11,15 @@ import { getSlugOrRequestedSlug } from "@calcom/features/ee/organizations/lib/or
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains"; import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
import slugify from "@calcom/lib/slugify"; import slugify from "@calcom/lib/slugify";
import prisma from "@calcom/prisma"; import prisma from "@calcom/prisma";
import { RedirectType } from "@calcom/prisma/client";
import type { inferSSRProps } from "@lib/types/inferSSRProps"; import type { inferSSRProps } from "@lib/types/inferSSRProps";
import type { EmbedProps } from "@lib/withEmbedSsr"; import type { EmbedProps } from "@lib/withEmbedSsr";
import PageWrapper from "@components/PageWrapper"; import PageWrapper from "@components/PageWrapper";
import { getTemporaryOrgRedirect } from "../../../lib/getTemporaryOrgRedirect";
export type PageProps = inferSSRProps<typeof getServerSideProps> & EmbedProps; export type PageProps = inferSSRProps<typeof getServerSideProps> & EmbedProps;
export default function Type({ export default function Type({
@ -75,6 +78,19 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
context.req.headers.host ?? "", context.req.headers.host ?? "",
context.params?.orgSlug 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({ const team = await prisma.team.findFirst({
where: { where: {

View File

@ -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");

View File

@ -972,3 +972,24 @@ model CalendarCache {
@@id([credentialId, key]) @@id([credentialId, key])
@@unique([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])
}

View File

@ -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); const metadata = teamMetadataSchema.parse(membership?.team.metadata);
return { return {