2
0
Files
2024-08-09 00:39:27 +02:00

263 lines
8.4 KiB
TypeScript

import { lookup } from "dns";
import { getOrgFullOrigin } from "@calcom/ee/organizations/lib/orgDomains";
import { sendAdminOrganizationNotification, sendOrganizationCreationEmail } from "@calcom/emails";
import { sendEmailVerification } from "@calcom/features/auth/lib/verifyEmail";
import { DEFAULT_SCHEDULE, getAvailabilityFromSchedule } from "@calcom/lib/availability";
import {
RESERVED_SUBDOMAINS,
ORG_SELF_SERVE_ENABLED,
ORG_MINIMUM_PUBLISHED_TEAMS_SELF_SERVE,
WEBAPP_URL,
} from "@calcom/lib/constants";
import { createDomain } from "@calcom/lib/domainManager/organization";
import { getTranslation } from "@calcom/lib/server/i18n";
import { OrganizationRepository } from "@calcom/lib/server/repository/organization";
import { UserRepository } from "@calcom/lib/server/repository/user";
import { prisma } from "@calcom/prisma";
import { UserPermissionRole } from "@calcom/prisma/enums";
import { TRPCError } from "@trpc/server";
import type { TrpcSessionUser } from "../../../trpc";
import { BillingPeriod } from "./create.schema";
import type { TCreateInputSchema } from "./create.schema";
type CreateOptions = {
ctx: {
user: NonNullable<TrpcSessionUser>;
};
input: TCreateInputSchema;
};
const getIPAddress = async (url: string): Promise<string> => {
return new Promise((resolve, reject) => {
lookup(url, (err, address) => {
if (err) reject(err);
resolve(address);
});
});
};
export const createHandler = async ({ input, ctx }: CreateOptions) => {
const {
slug,
name,
orgOwnerEmail,
seats,
pricePerSeat,
isPlatform,
billingPeriod: billingPeriodRaw,
} = input;
const loggedInUser = await prisma.user.findUnique({
where: {
id: ctx.user.id,
},
select: {
id: true,
role: true,
email: true,
teams: {
select: {
team: {
select: {
slug: true,
},
},
},
},
},
});
if (!loggedInUser) throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized." });
const IS_USER_ADMIN = loggedInUser.role === UserPermissionRole.ADMIN;
// We only allow creating an annual billing period if you are a system admin
const billingPeriod = (IS_USER_ADMIN ? billingPeriodRaw : BillingPeriod.MONTHLY) ?? BillingPeriod.MONTHLY;
if (!ORG_SELF_SERVE_ENABLED && !IS_USER_ADMIN && !isPlatform) {
throw new TRPCError({ code: "FORBIDDEN", message: "Only admins can create organizations" });
}
if (!IS_USER_ADMIN && loggedInUser.email !== orgOwnerEmail && !isPlatform) {
throw new TRPCError({
code: "FORBIDDEN",
message: "You can only create organization where you are the owner",
});
}
const publishedTeams = loggedInUser.teams.filter((team) => !!team.team.slug);
if (!IS_USER_ADMIN && publishedTeams.length < ORG_MINIMUM_PUBLISHED_TEAMS_SELF_SERVE && !isPlatform) {
throw new TRPCError({ code: "FORBIDDEN", message: "You need to have minimum published teams." });
}
let orgOwner = await prisma.user.findUnique({
where: {
email: orgOwnerEmail,
},
});
const hasAnOrgWithSameSlug = await prisma.team.findFirst({
where: {
slug: slug,
parentId: null,
isOrganization: true,
},
});
// Allow creating an organization with same requestedSlug as a non-org Team's slug
// It is needed so that later we can migrate the non-org Team(with the conflicting slug) to the newly created org
// Publishing the organization would fail if the team with the same slug is not migrated first
if (hasAnOrgWithSameSlug || RESERVED_SUBDOMAINS.includes(slug))
throw new TRPCError({ code: "BAD_REQUEST", message: "organization_url_taken" });
const availability = getAvailabilityFromSchedule(DEFAULT_SCHEDULE);
const isOrganizationConfigured = isPlatform ? true : await createDomain(slug);
const loggedInUserTranslation = await getTranslation(ctx.user.locale, "common");
const inputLanguageTranslation = await getTranslation(input.language ?? "en", "common");
if (!isOrganizationConfigured) {
// Otherwise, we proceed to send an administrative email to admins regarding
// the need to configure DNS registry to support the newly created org
const instanceAdmins = await prisma.user.findMany({
where: { role: UserPermissionRole.ADMIN },
select: { email: true },
});
if (instanceAdmins.length) {
await sendAdminOrganizationNotification({
instanceAdmins,
orgSlug: slug,
ownerEmail: orgOwnerEmail,
webappIPAddress: await getIPAddress(
WEBAPP_URL.replace("https://", "")?.replace("http://", "").replace(/(:.*)/, "")
),
t: loggedInUserTranslation,
});
} else {
console.warn("Organization created: subdomain not configured and couldn't notify adminnistrators");
}
}
const autoAcceptEmail = orgOwnerEmail.split("@")[1];
const orgData = {
name,
slug,
isOrganizationConfigured,
isOrganizationAdminReviewed: IS_USER_ADMIN,
autoAcceptEmail,
seats: seats ?? null,
pricePerSeat: pricePerSeat ?? null,
isPlatform,
billingPeriod,
};
// Create a new user and invite them as the owner of the organization
if (!orgOwner) {
const data = await OrganizationRepository.createWithNonExistentOwner({
orgData,
owner: {
email: orgOwnerEmail,
},
});
orgOwner = data.orgOwner;
const { organization, ownerProfile } = data;
const translation = await getTranslation(input.language ?? "en", "common");
await sendEmailVerification({
email: orgOwnerEmail,
language: ctx.user.locale,
username: ownerProfile.username || "",
isPlatform: isPlatform,
});
if (!isPlatform) {
await sendOrganizationCreationEmail({
language: translation,
from: ctx.user.name ?? `${organization.name}'s admin`,
to: orgOwnerEmail,
ownerNewUsername: ownerProfile.username,
ownerOldUsername: null,
orgDomain: getOrgFullOrigin(slug, { protocol: false }),
orgName: organization.name,
prevLink: null,
newLink: `${getOrgFullOrigin(slug, { protocol: true })}/${ownerProfile.username}`,
});
}
const user = await UserRepository.enrichUserWithItsProfile({
user: { ...orgOwner, organizationId: organization.id },
});
return {
userId: user.id,
email: user.email,
organizationId: user.organizationId,
upId: user.profile.upId,
};
} else {
// If we are making the loggedIn user the owner of the organization and he is already a part of an organization, we don't allow it because multi-org is not supported yet
const isLoggedInUserOrgOwner = orgOwner.id === loggedInUser.id;
if (ctx.user.profile.organizationId && isLoggedInUserOrgOwner) {
throw new TRPCError({ code: "FORBIDDEN", message: "User is part of an organization already" });
}
const nonOrgUsernameForOwner = orgOwner.username || "";
const { organization, ownerProfile } = await OrganizationRepository.createWithExistingUserAsOwner({
orgData,
owner: {
id: orgOwner.id,
email: orgOwnerEmail,
nonOrgUsername: nonOrgUsernameForOwner,
},
});
if (!isPlatform) {
await sendOrganizationCreationEmail({
language: inputLanguageTranslation,
from: ctx.user.name ?? `${organization.name}'s admin`,
to: orgOwnerEmail,
ownerNewUsername: ownerProfile.username,
ownerOldUsername: nonOrgUsernameForOwner,
orgDomain: getOrgFullOrigin(slug, { protocol: false }),
orgName: organization.name,
prevLink: `${getOrgFullOrigin("", { protocol: true })}/${nonOrgUsernameForOwner}`,
newLink: `${getOrgFullOrigin(slug, { protocol: true })}/${ownerProfile.username}`,
});
}
if (!organization.id) throw Error("User not created");
const user = await UserRepository.enrichUserWithItsProfile({
user: { ...orgOwner, organizationId: organization.id },
});
await prisma.availability.createMany({
data: availability.map((schedule) => ({
days: schedule.days,
startTime: schedule.startTime,
endTime: schedule.endTime,
userId: user.id,
})),
});
return {
userId: user.id,
email: user.email,
organizationId: user.organizationId,
upId: user.profile.upId,
};
}
// Sync Services: Close.com
//closeComUpsertOrganizationUser(createTeam, ctx.user, MembershipRole.OWNER);
};
export default createHandler;