cal.pub0.org/packages/trpc/server/routers/viewer/organizations/create.handler.ts

201 lines
6.1 KiB
TypeScript

import { createHash } from "crypto";
import { lookup } from "dns";
import { totp } from "otplib";
import { sendOrganizationEmailVerification } from "@calcom/emails";
import { sendAdminOrganizationNotification } from "@calcom/emails";
import { hashPassword } from "@calcom/features/auth/lib/hashPassword";
import { subdomainSuffix } from "@calcom/features/ee/organizations/lib/orgDomains";
import { DEFAULT_SCHEDULE, getAvailabilityFromSchedule } from "@calcom/lib/availability";
import {
IS_TEAM_BILLING_ENABLED,
RESERVED_SUBDOMAINS,
IS_PRODUCTION,
WEBAPP_URL,
} from "@calcom/lib/constants";
import { getTranslation } from "@calcom/lib/server/i18n";
import { prisma } from "@calcom/prisma";
import { MembershipRole, UserPermissionRole } from "@calcom/prisma/enums";
import { TRPCError } from "@trpc/server";
import type { TrpcSessionUser } from "../../../trpc";
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);
});
});
};
const vercelCreateDomain = async (domain: string) => {
const response = await fetch(
`https://api.vercel.com/v9/projects/${process.env.PROJECT_ID_VERCEL}/domains?teamId=${process.env.TEAM_ID_VERCEL}`,
{
body: JSON.stringify({ name: `${domain}.${subdomainSuffix()}` }),
headers: {
Authorization: `Bearer ${process.env.AUTH_BEARER_TOKEN_VERCEL}`,
"Content-Type": "application/json",
},
method: "POST",
}
);
const data = await response.json();
// Domain is already owned by another team but you can request delegation to access it
if (data.error?.code === "forbidden") return false;
// Domain is already being used by a different project
if (data.error?.code === "domain_taken") return false;
return true;
};
export const createHandler = async ({ input, ctx }: CreateOptions) => {
const { slug, name, adminEmail, adminUsername, check } = input;
const userCollisions = await prisma.user.findUnique({
where: {
email: adminEmail,
},
});
const slugCollisions = await prisma.team.findFirst({
where: {
slug: slug,
metadata: {
path: ["isOrganization"],
equals: true,
},
},
});
if (slugCollisions || RESERVED_SUBDOMAINS.includes(slug))
throw new TRPCError({ code: "BAD_REQUEST", message: "organization_url_taken" });
if (userCollisions) throw new TRPCError({ code: "BAD_REQUEST", message: "admin_email_taken" });
const password = createHash("md5")
.update(`${adminEmail}${process.env.CALENDSO_ENCRYPTION_KEY}`)
.digest("hex");
const hashedPassword = await hashPassword(password);
const emailDomain = adminEmail.split("@")[1];
const t = await getTranslation(ctx.user.locale ?? "en", "common");
const availability = getAvailabilityFromSchedule(DEFAULT_SCHEDULE);
let isOrganizationConfigured = false;
if (check === false) {
// eslint-disable-next-line turbo/no-undeclared-env-vars
if (process.env.VERCEL) {
// We only want to proceed to register the subdomain for the org in Vercel
// within a Vercel context
isOrganizationConfigured = await vercelCreateDomain(slug);
} else {
// 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: adminEmail,
webappIPAddress: await getIPAddress(
WEBAPP_URL.replace("https://", "")?.replace("http://", "").replace(/(:.*)/, "")
),
t,
});
} else {
console.warn("Organization created: subdomain not configured and couldn't notify adminnistrators");
}
}
const createOwnerOrg = await prisma.user.create({
data: {
username: adminUsername,
email: adminEmail,
emailVerified: new Date(),
password: hashedPassword,
// Default schedule
schedules: {
create: {
name: t("default_schedule_name"),
availability: {
createMany: {
data: availability.map((schedule) => ({
days: schedule.days,
startTime: schedule.startTime,
endTime: schedule.endTime,
})),
},
},
},
},
organization: {
create: {
name,
...(!IS_TEAM_BILLING_ENABLED && { slug }),
metadata: {
...(IS_TEAM_BILLING_ENABLED && { requestedSlug: slug }),
isOrganization: true,
isOrganizationVerified: false,
isOrganizationConfigured,
orgAutoAcceptEmail: emailDomain,
},
},
},
},
});
await prisma.membership.create({
data: {
userId: createOwnerOrg.id,
role: MembershipRole.OWNER,
accepted: true,
teamId: createOwnerOrg.organizationId!,
},
});
return { user: { ...createOwnerOrg, password } };
} else {
if (!IS_PRODUCTION) return { checked: true };
const language = await getTranslation(input.language ?? "en", "common");
const secret = createHash("md5")
.update(adminEmail + process.env.CALENDSO_ENCRYPTION_KEY)
.digest("hex");
totp.options = { step: 900 };
const code = totp.generate(secret);
await sendOrganizationEmailVerification({
user: {
email: adminEmail,
},
code,
language,
});
}
// Sync Services: Close.com
//closeComUpsertOrganizationUser(createTeam, ctx.user, MembershipRole.OWNER);
return { checked: true };
};
export default createHandler;