import { Request, Response, NextFunction } from "express"; import { z } from "zod"; import { db } from "@server/db"; import { eq } from "drizzle-orm"; import { domains, Org, orgDomains, orgs, roleActions, roles, userOrgs, users, actions } from "@server/db"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; import logger from "@server/logger"; import config from "@server/lib/config"; import { fromError } from "zod-validation-error"; import { defaultRoleAllowedActions } from "../role"; import { OpenAPITags, registry } from "@server/openApi"; import { isValidCIDR } from "@server/lib/validators"; import { createCustomer } from "#dynamic/lib/billing"; import { usageService } from "@server/lib/billing/usageService"; import { FeatureId } from "@server/lib/billing"; import { build } from "@server/build"; import { calculateUserClientsForOrgs } from "@server/lib/calculateUserClientsForOrgs"; import { doCidrsOverlap } from "@server/lib/ip"; const createOrgSchema = z.strictObject({ orgId: z.string(), name: z.string().min(1).max(255), subnet: z // .union([z.cidrv4(), z.cidrv6()]) .union([z.cidrv4()]) // for now lets just do ipv4 until we verify ipv6 works everywhere .refine((val) => isValidCIDR(val), { message: "Invalid subnet CIDR" }), utilitySubnet: z .union([z.cidrv4()]) // for now lets just do ipv4 until we verify ipv6 works everywhere .refine((val) => isValidCIDR(val), { message: "Invalid utility subnet CIDR" }) }); registry.registerPath({ method: "put", path: "/org", description: "Create a new organization", tags: [OpenAPITags.Org], request: { body: { content: { "application/json": { schema: createOrgSchema } } } }, responses: {} }); export async function createOrg( req: Request, res: Response, next: NextFunction ): Promise { try { // should this be in a middleware? if (config.getRawConfig().flags?.disable_user_create_org) { if (req.user && !req.user?.serverAdmin) { return next( createHttpError( HttpCode.FORBIDDEN, "Only server admins can create organizations" ) ); } } const parsedBody = createOrgSchema.safeParse(req.body); if (!parsedBody.success) { return next( createHttpError( HttpCode.BAD_REQUEST, fromError(parsedBody.error).toString() ) ); } const { orgId, name, subnet, utilitySubnet } = parsedBody.data; // TODO: for now we are making all of the orgs the same subnet // make sure the subnet is unique // const subnetExists = await db // .select() // .from(orgs) // .where(eq(orgs.subnet, subnet)) // .limit(1); // if (subnetExists.length > 0) { // return next( // createHttpError( // HttpCode.CONFLICT, // `Subnet ${subnet} already exists` // ) // ); // } // make sure the orgId is unique const orgExists = await db .select() .from(orgs) .where(eq(orgs.orgId, orgId)) .limit(1); if (orgExists.length > 0) { return next( createHttpError( HttpCode.CONFLICT, `Organization with ID ${orgId} already exists` ) ); } if (doCidrsOverlap(subnet, utilitySubnet)) { return next( createHttpError( HttpCode.BAD_REQUEST, `Subnet ${subnet} overlaps with utility subnet ${utilitySubnet}` ) ); } let error = ""; let org: Org | null = null; await db.transaction(async (trx) => { const allDomains = await trx .select() .from(domains) .where(eq(domains.configManaged, true)); const newOrg = await trx .insert(orgs) .values({ orgId, name, subnet, utilitySubnet, createdAt: new Date().toISOString() }) .returning(); if (newOrg.length === 0) { error = "Failed to create organization"; trx.rollback(); return; } org = newOrg[0]; // Create admin role within the same transaction const [insertedRole] = await trx .insert(roles) .values({ orgId: newOrg[0].orgId, isAdmin: true, name: "Admin", description: "Admin role with the most permissions" }) .returning({ roleId: roles.roleId }); if (!insertedRole || !insertedRole.roleId) { error = "Failed to create Admin role"; trx.rollback(); return; } const roleId = insertedRole.roleId; // Get all actions and create role actions const actionIds = await trx.select().from(actions).execute(); if (actionIds.length > 0) { await trx.insert(roleActions).values( actionIds.map((action) => ({ roleId, actionId: action.actionId, orgId: newOrg[0].orgId })) ); } if (allDomains.length) { await trx.insert(orgDomains).values( allDomains.map((domain) => ({ orgId: newOrg[0].orgId, domainId: domain.domainId })) ); } let ownerUserId: string | null = null; if (req.user) { await trx.insert(userOrgs).values({ userId: req.user!.userId, orgId: newOrg[0].orgId, roleId: roleId, isOwner: true }); ownerUserId = req.user!.userId; } else { // if org created by root api key, set the server admin as the owner const [serverAdmin] = await trx .select() .from(users) .where(eq(users.serverAdmin, true)); if (!serverAdmin) { error = "Server admin not found"; trx.rollback(); return; } await trx.insert(userOrgs).values({ userId: serverAdmin.userId, orgId: newOrg[0].orgId, roleId: roleId, isOwner: true }); ownerUserId = serverAdmin.userId; } const memberRole = await trx .insert(roles) .values({ name: "Member", description: "Members can only view resources", orgId }) .returning(); await trx.insert(roleActions).values( defaultRoleAllowedActions.map((action) => ({ roleId: memberRole[0].roleId, actionId: action, orgId })) ); await calculateUserClientsForOrgs(ownerUserId, trx); }); if (!org) { return next( createHttpError( HttpCode.INTERNAL_SERVER_ERROR, "Failed to create org" ) ); } if (error) { return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, error)); } if (build == "saas") { // make sure we have the stripe customer const customerId = await createCustomer(orgId, req.user?.email); if (customerId) { await usageService.updateDaily( orgId, FeatureId.USERS, 1, customerId ); // Only 1 because we are creating the org } } return response(res, { data: org, success: true, error: false, message: "Organization created successfully", status: HttpCode.CREATED }); } catch (error) { logger.error(error); return next( createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") ); } }