diff --git a/packages/features/ee/organizations/lib/service/AdminOrganizationUpdateService.ts b/packages/features/ee/organizations/lib/service/AdminOrganizationUpdateService.ts new file mode 100644 index 0000000000..90dcd0084f --- /dev/null +++ b/packages/features/ee/organizations/lib/service/AdminOrganizationUpdateService.ts @@ -0,0 +1,146 @@ +import { getOrgFullOrigin } from "@calcom/ee/organizations/lib/orgDomains"; +import type { OrganizationRepository } from "@calcom/features/ee/organizations/repositories/OrganizationRepository"; +import { TeamRepository } from "@calcom/features/ee/teams/repositories/TeamRepository"; +import { renameDomain } from "@calcom/lib/domainManager/organization"; +import { getMetadataHelpers } from "@calcom/lib/getMetadataHelpers"; +import { HttpError } from "@calcom/lib/http-error"; +import type { Prisma, PrismaClient } from "@calcom/prisma/client"; +import { teamMetadataStrictSchema } from "@calcom/prisma/zod-utils"; +import type { TAdminUpdate } from "@calcom/trpc/server/routers/viewer/organizations/adminUpdate.schema"; + +type AdminOrganizationUpdateServiceDeps = { + prismaClient: PrismaClient; + organizationRepository: OrganizationRepository; +}; + +export class AdminOrganizationUpdateService { + constructor(private readonly deps: AdminOrganizationUpdateServiceDeps) {} + + async updateOrganization(input: TAdminUpdate) { + const { id, organizationSettings, ...restInput } = input; + const { organizationRepository } = this.deps; + + const existingOrg = await organizationRepository.findByIdIncludeOrganizationSettings({ id }); + + if (!existingOrg) { + throw new HttpError({ + message: "Organization not found", + statusCode: 404, + }); + } + + const { mergeMetadata } = getMetadataHelpers( + teamMetadataStrictSchema.unwrap(), + existingOrg.metadata || {} + ); + + const data: Prisma.TeamUpdateArgs["data"] = restInput; + const oldSlug = existingOrg.slug; + const newSlug = restInput.slug; + + if (newSlug) { + await throwIfSlugConflicts({ + id, + slug: newSlug, + teamRepository: new TeamRepository(this.deps.prismaClient), + }); + const isSlugChanged = newSlug !== oldSlug; + if (isSlugChanged) { + // If slug is changed, we need to rename the domain first + // If renaming fails, we don't want to update the new slug in DB + await renameDomain(oldSlug, newSlug); + } + data.slug = newSlug; + data.metadata = mergeMetadata({ + // If we save slug, we don't need the requestedSlug anymore + requestedSlug: undefined, + }); + } + + const updatedOrganization = await this.deps.prismaClient.$transaction(async (tx) => { + const updatedOrganization = await tx.team.update({ + where: { id }, + data, + }); + + // Update all TempOrgRedirect records that point to the old org URL to use the new org URL + if (newSlug && oldSlug && newSlug !== oldSlug) { + const oldOrgUrlPrefix = getOrgFullOrigin(oldSlug); + const newOrgUrlPrefix = getOrgFullOrigin(newSlug); + + const redirectsToUpdate = await tx.tempOrgRedirect.findMany({ + where: { + toUrl: { + startsWith: oldOrgUrlPrefix, + }, + }, + select: { + id: true, + toUrl: true, + }, + }); + + for (const redirect of redirectsToUpdate) { + const newToUrl = redirect.toUrl.replace(oldOrgUrlPrefix, newOrgUrlPrefix); + await tx.tempOrgRedirect.update({ + where: { + id: redirect.id, + }, + data: { + toUrl: newToUrl, + }, + }); + } + } + + if (organizationSettings || existingOrg.organizationSettings) { + await tx.organizationSettings.update({ + where: { + organizationId: updatedOrganization.id, + }, + data: { + isOrganizationConfigured: + organizationSettings?.isOrganizationConfigured || + existingOrg.organizationSettings?.isOrganizationConfigured, + isOrganizationVerified: + organizationSettings?.isOrganizationVerified || + existingOrg.organizationSettings?.isOrganizationVerified, + isAdminReviewed: organizationSettings?.isAdminReviewed, + orgAutoAcceptEmail: + organizationSettings?.orgAutoAcceptEmail || + existingOrg.organizationSettings?.orgAutoAcceptEmail, + isAdminAPIEnabled: !!( + organizationSettings?.isAdminAPIEnabled ?? existingOrg.organizationSettings?.isAdminAPIEnabled + ), + }, + }); + } + return updatedOrganization; + }); + + return updatedOrganization; + } +} + +async function throwIfSlugConflicts({ + id, + slug, + teamRepository, +}: { + id: number; + slug: string; + teamRepository: TeamRepository; +}) { + const isSlugAvailable = await teamRepository.isSlugAvailableForUpdate({ + slug, + teamId: id, + parentId: null, + }); + + if (!isSlugAvailable) { + throw new HttpError({ + message: "Organization or a Team with same slug already exists", + statusCode: 400, + }); + } +} diff --git a/packages/features/ee/organizations/repositories/OrganizationRepository.ts b/packages/features/ee/organizations/repositories/OrganizationRepository.ts index a1715968d2..e547d9e7a1 100644 --- a/packages/features/ee/organizations/repositories/OrganizationRepository.ts +++ b/packages/features/ee/organizations/repositories/OrganizationRepository.ts @@ -6,7 +6,7 @@ import { createAProfileForAnExistingUser } from "@calcom/features/profile/lib/cr import { UserRepository } from "@calcom/features/users/repositories/UserRepository"; import logger from "@calcom/lib/logger"; import { safeStringify } from "@calcom/lib/safeStringify"; -import type { PrismaClient } from "@calcom/prisma"; +import type { PrismaClient } from "@calcom/prisma/client"; import { MembershipRole } from "@calcom/prisma/enums"; import type { CreationSource } from "@calcom/prisma/enums"; import type { teamMetadataStrictSchema } from "@calcom/prisma/zod-utils"; @@ -227,6 +227,7 @@ export class OrganizationRepository { }, select: { ...orgSelect, + metadata: true, organizationSettings: true, }, }); diff --git a/packages/trpc/server/routers/viewer/organizations/adminUpdate.handler.ts b/packages/trpc/server/routers/viewer/organizations/adminUpdate.handler.ts index adc7c82f7a..d9bc7a644b 100644 --- a/packages/trpc/server/routers/viewer/organizations/adminUpdate.handler.ts +++ b/packages/trpc/server/routers/viewer/organizations/adminUpdate.handler.ts @@ -1,9 +1,6 @@ -import { renameDomain } from "@calcom/lib/domainManager/organization"; -import { getMetadataHelpers } from "@calcom/lib/getMetadataHelpers"; -import { HttpError } from "@calcom/lib/http-error"; +import { getOrganizationRepository } from "@calcom/features/ee/organizations/di/OrganizationRepository.container"; +import { AdminOrganizationUpdateService } from "@calcom/features/ee/organizations/lib/service/AdminOrganizationUpdateService"; import { prisma } from "@calcom/prisma"; -import type { Prisma } from "@calcom/prisma/client"; -import { teamMetadataStrictSchema } from "@calcom/prisma/zod-utils"; import type { TrpcSessionUser } from "../../../types"; import type { TAdminUpdate } from "./adminUpdate.schema"; @@ -16,103 +13,13 @@ type AdminUpdateOptions = { }; export const adminUpdateHandler = async ({ input }: AdminUpdateOptions) => { - const { id, organizationSettings, ...restInput } = input; - const existingOrg = await prisma.team.findUnique({ - where: { - id: id, - }, - include: { - organizationSettings: true, - }, + const organizationRepository = getOrganizationRepository(); + const adminOrganizationUpdateService = new AdminOrganizationUpdateService({ + prismaClient: prisma, + organizationRepository, }); - if (!existingOrg) { - throw new HttpError({ - message: "Organization not found", - statusCode: 404, - }); - } - - const { mergeMetadata } = getMetadataHelpers(teamMetadataStrictSchema.unwrap(), existingOrg.metadata || {}); - - const data: Prisma.TeamUpdateArgs["data"] = restInput; - - if (restInput.slug) { - await throwIfSlugConflicts({ id, slug: restInput.slug }); - const isSlugChanged = restInput.slug !== existingOrg.slug; - if (isSlugChanged) { - // If slug is changed, we need to rename the domain first - // If renaming fails, we don't want to update the new slug in DB - await renameDomain(existingOrg.slug, restInput.slug); - } - data.slug = input.slug; - data.metadata = mergeMetadata({ - // If we save slug, we don't need the requestedSlug anymore - requestedSlug: undefined, - }); - } - - const updatedOrganization = await prisma.$transaction(async (tx) => { - const updatedOrganization = await tx.team.update({ - where: { id }, - data, - }); - - if (organizationSettings || existingOrg.organizationSettings) { - await tx.organizationSettings.update({ - where: { - organizationId: updatedOrganization.id, - }, - data: { - isOrganizationConfigured: - organizationSettings?.isOrganizationConfigured || - existingOrg.organizationSettings?.isOrganizationConfigured, - isOrganizationVerified: - organizationSettings?.isOrganizationVerified || - existingOrg.organizationSettings?.isOrganizationVerified, - isAdminReviewed: organizationSettings?.isAdminReviewed, - orgAutoAcceptEmail: - organizationSettings?.orgAutoAcceptEmail || existingOrg.organizationSettings?.orgAutoAcceptEmail, - isAdminAPIEnabled: !!( - organizationSettings?.isAdminAPIEnabled ?? existingOrg.organizationSettings?.isAdminAPIEnabled - ), - }, - }); - } - return updatedOrganization; - }); - - return updatedOrganization; + return adminOrganizationUpdateService.updateOrganization(input); }; export default adminUpdateHandler; - -async function throwIfSlugConflicts({ id, slug }: { id: number; slug: string }) { - const organizationsWithSameSlug = await prisma.team.findMany({ - where: { - slug: slug, - parentId: null, - }, - }); - - if (organizationsWithSameSlug.length > 1) { - throw new HttpError({ - message: "There can only be one organization with a given slug", - statusCode: 400, - }); - } - - const foundOrg = organizationsWithSameSlug[0]; - if (!foundOrg) { - // No org with same slug found - So, no conflict. - return; - } - - // If foundOrg isn't same as the org being edited - if (foundOrg.id !== id) { - throw new HttpError({ - message: "Organization or a Team with same slug already exists", - statusCode: 400, - }); - } -}