diff --git a/apps/web/lib/orgMigration.test.ts b/apps/web/lib/orgMigration.test.ts index a107a8012a..080ac4131d 100644 --- a/apps/web/lib/orgMigration.test.ts +++ b/apps/web/lib/orgMigration.test.ts @@ -696,7 +696,7 @@ describe("orgMigration", () => { }); describe("when user email matches orgAutoAcceptEmail", () => { - const orgSettings = { + const orgMetadata = { orgAutoAcceptEmail: "org1.com", } as const; @@ -725,10 +725,8 @@ describe("orgMigration", () => { const dbOrg = await createOrg({ slug: data.targetOrg.slug, name: data.targetOrg.name, - organizationSettings: { - create: { - ...orgSettings, - }, + metadata: { + ...orgMetadata, }, }); @@ -786,10 +784,8 @@ describe("orgMigration", () => { const dbOrg = await createOrg({ slug: data.targetOrg.slug, name: data.targetOrg.name, - organizationSettings: { - create: { - ...orgSettings, - }, + metadata: { + ...orgMetadata, }, }); @@ -841,11 +837,7 @@ describe("orgMigration", () => { name: data.targetOrg.name, metadata: { requestedSlug: data.targetOrg.requestedSlug, - }, - organizationSettings: { - create: { - ...orgSettings, - }, + ...orgMetadata, }, }); @@ -909,7 +901,9 @@ describe("orgMigration", () => { id: data.targetOrg.id, slug: data.targetOrg.slug, name: data.targetOrg.name, - isOrganization: true, + metadata: { + isOrganization: true, + }, }, }); @@ -1570,9 +1564,9 @@ async function createOrg( return await prismock.team.create({ data: { ...data, - isOrganization: true, metadata: { ...(data.metadata || {}), + isOrganization: true, }, }, }); diff --git a/apps/web/lib/orgMigration.ts b/apps/web/lib/orgMigration.ts index ef1921d840..c09304c315 100644 --- a/apps/web/lib/orgMigration.ts +++ b/apps/web/lib/orgMigration.ts @@ -1,6 +1,5 @@ import { getOrgUsernameFromEmail } from "@calcom/features/auth/signup/utils/getOrgUsernameFromEmail"; import { getOrgFullOrigin } from "@calcom/features/ee/organizations/lib/orgDomains"; -import { isOrganization } from "@calcom/lib/entityPermissionUtils"; import { HttpError } from "@calcom/lib/http-error"; import logger from "@calcom/lib/logger"; import { safeStringify } from "@calcom/lib/safeStringify"; @@ -47,7 +46,7 @@ export async function moveUserToOrg({ const teamMetadata = teamMetadataSchema.parse(team?.metadata); - if (!isOrganization({ team })) { + if (!teamMetadata?.isOrganization) { throw new Error(`Team with ID:${targetOrgId} is not an Org`); } @@ -61,7 +60,7 @@ export async function moveUserToOrg({ if (!targetOrgUsername) { targetOrgUsername = getOrgUsernameFromEmail( userToMoveToOrg.email, - team.organizationSettings?.orgAutoAcceptEmail || "" + targetOrganization.metadata.orgAutoAcceptEmail || "" ); } @@ -196,7 +195,7 @@ export async function moveTeamToOrg({ const teamMetadata = teamMetadataSchema.parse(possibleOrg?.metadata); - if (!isOrganization({ team: possibleOrg })) { + if (!teamMetadata?.isOrganization) { throw new Error(`${targetOrg.id} is not an Org`); } @@ -205,7 +204,7 @@ export async function moveTeamToOrg({ await addTeamRedirect({ oldTeamSlug, teamSlug: updatedTeam.slug, - orgSlug: targetOrganization.slug || orgMetadata?.requestedSlug || null, + orgSlug: targetOrganization.slug || orgMetadata.requestedSlug || null, }); await setOrgSlugIfNotSet({ slug: targetOrganization.slug }, orgMetadata, targetOrg.id); if (moveMembers) { @@ -329,14 +328,13 @@ async function setOrgSlugIfNotSet( }, orgMetadata: { requestedSlug?: string | undefined; - } | null, + }, targetOrgId: number ) { if (targetOrganization.slug) { return; } - - if (!orgMetadata?.requestedSlug) { + if (!orgMetadata.requestedSlug) { throw new HttpError({ statusCode: 400, message: `Org with id: ${targetOrgId} doesn't have a slug. Tried using requestedSlug but that's also not present. So, all migration done but failed to set the Organization slug. Please set it manually`, @@ -373,9 +371,6 @@ async function getTeamOrThrowError(targetOrgId: number) { where: { id: targetOrgId, }, - include: { - organizationSettings: true, - }, }); if (!team) { @@ -625,7 +620,6 @@ async function moveTeamsWithoutMembersToOrg({ id: true, slug: true, metadata: true, - isOrganization: true, }, }); @@ -637,7 +631,7 @@ async function moveTeamsWithoutMembersToOrg({ }; }) // Remove Orgs from the list - .filter((team) => !isOrganization({ team })); + .filter((team) => !team.metadata?.isOrganization); const teamIdsToBeMovedToOrg = teamsToBeMovedToOrg.map((t) => t.id); @@ -790,7 +784,6 @@ async function removeTeamsWithoutItsMemberFromOrg({ userToRemoveFromOrg }: { use id: true, slug: true, metadata: true, - isOrganization: true, }, }); @@ -802,7 +795,7 @@ async function removeTeamsWithoutItsMemberFromOrg({ userToRemoveFromOrg }: { use }; }) // Remove Orgs from the list - .filter((team) => !isOrganization({ team })); + .filter((team) => !team.metadata?.isOrganization); const teamIdsToBeRemovedFromOrg = teamsToBeRemovedFromOrg.map((t) => t.id); diff --git a/apps/web/pages/signup.tsx b/apps/web/pages/signup.tsx index 50b4138c7f..88298c4908 100644 --- a/apps/web/pages/signup.tsx +++ b/apps/web/pages/signup.tsx @@ -610,16 +610,14 @@ export const getServerSideProps = async (ctx: GetServerSidePropsContext) => { team: { select: { metadata: true, - isOrganization: true, parentId: true, parent: { select: { slug: true, - organizationSettings: true, + metadata: true, }, }, slug: true, - organizationSettings: true, }, }, }, @@ -668,8 +666,8 @@ export const getServerSideProps = async (ctx: GetServerSidePropsContext) => { }; const isATeamInOrganization = tokenTeam?.parentId !== null; + const isOrganization = tokenTeam.metadata?.isOrganization; // Detect if the team is an org by either the metadata flag or if it has a parent team - const isOrganization = tokenTeam.isOrganization; const isOrganizationOrATeamInOrganization = isOrganization || isATeamInOrganization; // If we are dealing with an org, the slug may come from the team itself or its parent const orgSlug = isOrganizationOrATeamInOrganization @@ -686,7 +684,9 @@ export const getServerSideProps = async (ctx: GetServerSidePropsContext) => { const isValidEmail = checkValidEmail(verificationToken.identifier); const isOrgInviteByLink = isOrganizationOrATeamInOrganization && !isValidEmail; - const parentOrgSettings = tokenTeam?.parent?.organizationSettings ?? null; + const parentMetaDataForSubteam = tokenTeam?.parent?.metadata + ? teamMetadataSchema.parse(tokenTeam.parent.metadata) + : null; return { props: { @@ -699,15 +699,15 @@ export const getServerSideProps = async (ctx: GetServerSidePropsContext) => { ? getOrgUsernameFromEmail( verificationToken.identifier, (isOrganization - ? tokenTeam.organizationSettings?.orgAutoAcceptEmail - : parentOrgSettings?.orgAutoAcceptEmail) || "" + ? tokenTeam.metadata?.orgAutoAcceptEmail + : parentMetaDataForSubteam?.orgAutoAcceptEmail) || "" ) : slugify(username), } : null, orgSlug, orgAutoAcceptEmail: isOrgInviteByLink - ? tokenTeam?.organizationSettings?.orgAutoAcceptEmail ?? parentOrgSettings?.orgAutoAcceptEmail ?? null + ? tokenTeam?.metadata?.orgAutoAcceptEmail ?? parentMetaDataForSubteam?.orgAutoAcceptEmail ?? null : null, }, }; diff --git a/apps/web/pages/team/[slug].tsx b/apps/web/pages/team/[slug].tsx index 830acfb6ef..6b80836974 100644 --- a/apps/web/pages/team/[slug].tsx +++ b/apps/web/pages/team/[slug].tsx @@ -74,7 +74,7 @@ function TeamPage({ return (
@@ -178,7 +178,7 @@ function TeamPage({ title: markdownStrippedBio, profile: { name: `${team.name}`, - image: `${WEBAPP_URL}/${team.isOrganization ? "org" : "team"}/${team.slug}/avatar.png`, + image: `${WEBAPP_URL}/${team.metadata?.isOrganization ? "org" : "team"}/${team.slug}/avatar.png`, }, }} /> @@ -190,7 +190,7 @@ function TeamPage({ imageSrc={ isValidOrgDomain ? `/org/${currentOrgDomain}/avatar.png` - : `${WEBAPP_URL}/${team.isOrganization ? "org" : "team"}/${team.slug}/avatar.png` + : `${WEBAPP_URL}/${team.metadata?.isOrganization ? "org" : "team"}/${team.slug}/avatar.png` } size="lg" /> @@ -208,7 +208,7 @@ function TeamPage({ )} - {team?.isOrganization ? ( + {metadata?.isOrganization ? ( ) : ( <> @@ -311,7 +311,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => // Taking care of sub-teams and orgs if ( (!isValidOrgDomain && team?.parent) || - (!isValidOrgDomain && !!team?.isOrganization) || + (!isValidOrgDomain && !!metadata?.isOrganization) || !isOrganizationFeatureEnabled ) { return { notFound: true } as const; diff --git a/apps/web/playwright/fixtures/orgs.ts b/apps/web/playwright/fixtures/orgs.ts index ea67350304..265fcd73a1 100644 --- a/apps/web/playwright/fixtures/orgs.ts +++ b/apps/web/playwright/fixtures/orgs.ts @@ -43,8 +43,8 @@ async function createOrgInDb({ data: { name: name, slug: slug, - isOrganization: true, metadata: { + isOrganization: true, ...(requestedSlug ? { requestedSlug, diff --git a/apps/web/playwright/fixtures/users.ts b/apps/web/playwright/fixtures/users.ts index c14e0e58f9..9ef4085fd4 100644 --- a/apps/web/playwright/fixtures/users.ts +++ b/apps/web/playwright/fixtures/users.ts @@ -118,21 +118,18 @@ const createTeamAndAddUser = async ( const slug = `${isOrg ? "org" : "team"}-${workerInfo.workerIndex}-${Date.now()}`; const data: PrismaType.TeamCreateInput = { name: `user-id-${user.id}'s ${isOrg ? "Org" : "Team"}`, - isOrganization: isOrg, }; data.metadata = { ...(isUnpublished ? { requestedSlug: slug } : {}), + ...(isOrg + ? { + isOrganization: true, + isOrganizationVerified: !!isOrgVerified, + orgAutoAcceptEmail: user.email.split("@")[1], + isOrganizationConfigured: false, + } + : {}), }; - if (isOrg) { - data.organizationSettings = { - create: { - isOrganizationVerified: !!isOrgVerified, - orgAutoAcceptEmail: user.email.split("@")[1], - isOrganizationConfigured: false, - }, - }; - } - data.slug = !isUnpublished ? slug : undefined; if (isOrg && hasSubteam) { const team = await createTeamAndAddUser({ user }, workerInfo); @@ -537,7 +534,7 @@ const createUserFixture = (user: UserWithIncludes, page: Page) => { }, }; }) - .find((membership) => !membership.team.isOrganization); + .find((membership) => !membership.team?.metadata?.isOrganization); if (!membership) { throw new Error("No team found for user"); } @@ -548,7 +545,10 @@ const createUserFixture = (user: UserWithIncludes, page: Page) => { where: { userId: user.id, team: { - isOrganization: true, + metadata: { + path: ["isOrganization"], + equals: true, + }, }, }, include: { team: { include: { children: true } } }, diff --git a/packages/features/auth/lib/next-auth-options.ts b/packages/features/auth/lib/next-auth-options.ts index 113bb510b8..0cf2530174 100644 --- a/packages/features/auth/lib/next-auth-options.ts +++ b/packages/features/auth/lib/next-auth-options.ts @@ -61,10 +61,20 @@ const checkIfUserShouldBelongToOrg = async (idP: IdentityProvider, email: string if (!ORGANIZATIONS_AUTOLINK || idP !== "GOOGLE") return { orgUsername, orgId: undefined }; const existingOrg = await prisma.team.findFirst({ where: { - organizationSettings: { - isOrganizationVerified: true, - orgAutoAcceptEmail: apexDomain, - }, + AND: [ + { + metadata: { + path: ["isOrganizationVerified"], + equals: true, + }, + }, + { + metadata: { + path: ["orgAutoAcceptEmail"], + equals: apexDomain, + }, + }, + ], }, select: { id: true, diff --git a/packages/features/auth/signup/handlers/calcomHandler.ts b/packages/features/auth/signup/handlers/calcomHandler.ts index d95f1d5352..47e86e779a 100644 --- a/packages/features/auth/signup/handlers/calcomHandler.ts +++ b/packages/features/auth/signup/handlers/calcomHandler.ts @@ -14,7 +14,7 @@ import { closeComUpsertTeamUser } from "@calcom/lib/sync/SyncServiceManager"; import { validateAndGetCorrectedUsernameAndEmail } from "@calcom/lib/validateUsername"; import { prisma } from "@calcom/prisma"; import { IdentityProvider } from "@calcom/prisma/enums"; -import { signupSchema } from "@calcom/prisma/zod-utils"; +import { signupSchema, teamMetadataSchema } from "@calcom/prisma/zod-utils"; import { joinAnyChildTeamOnOrgInvite } from "../utils/organization"; import { @@ -129,6 +129,8 @@ async function handler(req: RequestWithUsernameStatus, res: NextApiResponse) { }, }); if (team) { + const teamMetadata = teamMetadataSchema.parse(team?.metadata); + const user = await prisma.user.upsert({ where: { email }, update: { @@ -147,6 +149,7 @@ async function handler(req: RequestWithUsernameStatus, res: NextApiResponse) { // Wrapping in a transaction as if one fails we want to rollback the whole thing to preventa any data inconsistencies const { membership } = await createOrUpdateMemberships({ + teamMetadata, user, team, }); diff --git a/packages/features/auth/signup/handlers/selfHostedHandler.ts b/packages/features/auth/signup/handlers/selfHostedHandler.ts index 3ccb2fc498..60e1111432 100644 --- a/packages/features/auth/signup/handlers/selfHostedHandler.ts +++ b/packages/features/auth/signup/handlers/selfHostedHandler.ts @@ -12,6 +12,7 @@ import { validateAndGetCorrectedUsernameAndEmail } from "@calcom/lib/validateUse import prisma from "@calcom/prisma"; import { IdentityProvider } from "@calcom/prisma/enums"; import { signupSchema } from "@calcom/prisma/zod-utils"; +import { teamMetadataSchema } from "@calcom/prisma/zod-utils"; import { joinAnyChildTeamOnOrgInvite } from "../utils/organization"; import { @@ -68,6 +69,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) }, }); if (team) { + const teamMetadata = teamMetadataSchema.parse(team?.metadata); + const user = await prisma.user.upsert({ where: { email: userEmail }, update: { @@ -85,6 +88,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) }); const { membership } = await createOrUpdateMemberships({ + teamMetadata, user, team, }); diff --git a/packages/features/auth/signup/utils/createOrUpdateMemberships.ts b/packages/features/auth/signup/utils/createOrUpdateMemberships.ts index 2380da13a6..593fb50ebd 100644 --- a/packages/features/auth/signup/utils/createOrUpdateMemberships.ts +++ b/packages/features/auth/signup/utils/createOrUpdateMemberships.ts @@ -1,16 +1,21 @@ +import type z from "zod"; + import { prisma } from "@calcom/prisma"; import type { Team, User } from "@calcom/prisma/client"; import { MembershipRole } from "@calcom/prisma/enums"; +import type { teamMetadataSchema } from "@calcom/prisma/zod-utils"; export const createOrUpdateMemberships = async ({ + teamMetadata, user, team, }: { user: Pick; - team: Pick; + team: Pick; + teamMetadata: z.infer; }) => { return await prisma.$transaction(async (tx) => { - if (team.isOrganization) { + if (teamMetadata?.isOrganization) { await tx.user.update({ where: { id: user.id, diff --git a/packages/features/ee/organizations/api/subteams.ts b/packages/features/ee/organizations/api/subteams.ts index ad935527c2..352b922bb4 100644 --- a/packages/features/ee/organizations/api/subteams.ts +++ b/packages/features/ee/organizations/api/subteams.ts @@ -4,6 +4,7 @@ import z from "zod"; import { HttpError } from "@calcom/lib/http-error"; import { defaultHandler, defaultResponder } from "@calcom/lib/server"; import prisma from "@calcom/prisma"; +import { teamMetadataSchema } from "@calcom/prisma/zod-utils"; const querySchema = z.object({ org: z.string({ required_error: "org slug is required" }), @@ -19,16 +20,13 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { } = parsedQuery; if (!slug) return res.status(400).json({ message: "Org is needed" }); - const org = await prisma.team.findFirst({ - where: { slug }, - select: { children: true, isOrganization: true }, - }); + const org = await prisma.team.findFirst({ where: { slug }, select: { children: true, metadata: true } }); if (!org) return res.status(400).json({ message: "Org doesn't exist" }); - const isOrganization = org.isOrganization; + const metadata = teamMetadataSchema.parse(org?.metadata); - if (!isOrganization) return res.status(400).json({ message: "Team is not an org" }); + if (!metadata?.isOrganization) return res.status(400).json({ message: "Team is not an org" }); return res.status(200).json({ slugs: org.children.map((ch) => ch.slug) }); } diff --git a/packages/features/ee/organizations/lib/orgDomains.ts b/packages/features/ee/organizations/lib/orgDomains.ts index 6a2cbc1050..ebb0c9d73e 100644 --- a/packages/features/ee/organizations/lib/orgDomains.ts +++ b/packages/features/ee/organizations/lib/orgDomains.ts @@ -139,7 +139,10 @@ export function whereClauseForOrgWithSlugOrRequestedSlug(slug: string) { }, }, ], - isOrganization: true, + metadata: { + path: ["isOrganization"], + equals: true, + }, } satisfies Prisma.TeamWhereInput; } diff --git a/packages/features/ee/organizations/pages/settings/admin/AdminOrgEditPage.tsx b/packages/features/ee/organizations/pages/settings/admin/AdminOrgEditPage.tsx index 4dd43bb817..620210633e 100644 --- a/packages/features/ee/organizations/pages/settings/admin/AdminOrgEditPage.tsx +++ b/packages/features/ee/organizations/pages/settings/admin/AdminOrgEditPage.tsx @@ -6,7 +6,7 @@ import { z } from "zod"; import NoSSR from "@calcom/core/components/NoSSR"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useParamsWithFallback } from "@calcom/lib/hooks/useParamsWithFallback"; -import type { orgSettingsSchema } from "@calcom/prisma/zod-utils"; +import type { teamMetadataSchema } from "@calcom/prisma/zod-utils"; import { trpc } from "@calcom/trpc/react"; import { Button, Form, Meta, TextField, showToast } from "@calcom/ui"; @@ -43,7 +43,7 @@ const OrgEditView = ({ orgId }: { orgId: number }) => { type FormValues = { name: Team["name"]; slug: Team["slug"]; - organizationSettings: z.infer; + metadata: z.infer; }; const OrgForm = ({ @@ -81,10 +81,6 @@ const OrgForm = ({ mutation.mutate({ id: org.id, ...values, - organizationSettings: { - ...org.organizationSettings, - orgAutoAcceptEmail: values.organizationSettings?.orgAutoAcceptEmail, - }, }); }; @@ -100,7 +96,7 @@ const OrgForm = ({ label="Domain for which invitations are auto-accepted" placeholder="abc.com" required - {...form.register("organizationSettings.orgAutoAcceptEmail")} + {...form.register("metadata.orgAutoAcceptEmail")} />