mirror of
https://github.com/fosrl/pangolin.git
synced 2025-07-24 20:55:04 +02:00
287 lines
9.7 KiB
TypeScript
287 lines
9.7 KiB
TypeScript
import { Request, Response, NextFunction } from "express";
|
|
import { z } from "zod";
|
|
import { db, Domain, domains, OrgDomains, orgDomains } 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 { fromError } from "zod-validation-error";
|
|
import { subdomainSchema } from "@server/lib/schemas";
|
|
import { generateId } from "@server/auth/sessions/app";
|
|
import { eq, and } from "drizzle-orm";
|
|
import { isValidDomain } from "@server/lib/validators";
|
|
import { build } from "@server/build";
|
|
|
|
const paramsSchema = z
|
|
.object({
|
|
orgId: z.string()
|
|
})
|
|
.strict();
|
|
|
|
const bodySchema = z
|
|
.object({
|
|
type: z.enum(["ns", "cname", "wildcard"]),
|
|
baseDomain: subdomainSchema
|
|
})
|
|
.strict();
|
|
|
|
export type CreateDomainResponse = {
|
|
domainId: string;
|
|
nsRecords?: string[];
|
|
cnameRecords?: { baseDomain: string; value: string }[];
|
|
txtRecords?: { baseDomain: string; value: string }[];
|
|
};
|
|
|
|
// Helper to check if a domain is a subdomain or equal to another domain
|
|
function isSubdomainOrEqual(a: string, b: string): boolean {
|
|
const aParts = a.toLowerCase().split(".");
|
|
const bParts = b.toLowerCase().split(".");
|
|
if (aParts.length < bParts.length) return false;
|
|
return aParts.slice(-bParts.length).join(".") === bParts.join(".");
|
|
}
|
|
|
|
export async function createOrgDomain(
|
|
req: Request,
|
|
res: Response,
|
|
next: NextFunction
|
|
): Promise<any> {
|
|
try {
|
|
const parsedBody = bodySchema.safeParse(req.body);
|
|
if (!parsedBody.success) {
|
|
return next(
|
|
createHttpError(
|
|
HttpCode.BAD_REQUEST,
|
|
fromError(parsedBody.error).toString()
|
|
)
|
|
);
|
|
}
|
|
|
|
const parsedParams = paramsSchema.safeParse(req.params);
|
|
if (!parsedParams.success) {
|
|
return next(
|
|
createHttpError(
|
|
HttpCode.BAD_REQUEST,
|
|
fromError(parsedParams.error).toString()
|
|
)
|
|
);
|
|
}
|
|
|
|
const { orgId } = parsedParams.data;
|
|
const { type, baseDomain } = parsedBody.data;
|
|
|
|
if (build == "oss") {
|
|
if (type !== "wildcard") {
|
|
return next(
|
|
createHttpError(
|
|
HttpCode.NOT_IMPLEMENTED,
|
|
"Creating NS or CNAME records is not supported"
|
|
)
|
|
);
|
|
}
|
|
} else if (build == "enterprise" || build == "saas") {
|
|
if (type !== "ns" && type !== "cname") {
|
|
return next(
|
|
createHttpError(
|
|
HttpCode.BAD_REQUEST,
|
|
"Invalid domain type. Only NS, CNAME are allowed."
|
|
)
|
|
);
|
|
}
|
|
}
|
|
|
|
// Validate organization exists
|
|
if (!isValidDomain(baseDomain)) {
|
|
return next(
|
|
createHttpError(HttpCode.BAD_REQUEST, "Invalid domain format")
|
|
);
|
|
}
|
|
|
|
let numOrgDomains: OrgDomains[] | undefined;
|
|
let cnameRecords: CreateDomainResponse["cnameRecords"];
|
|
let txtRecords: CreateDomainResponse["txtRecords"];
|
|
let nsRecords: CreateDomainResponse["nsRecords"];
|
|
let returned: Domain | undefined;
|
|
|
|
await db.transaction(async (trx) => {
|
|
const [existing] = await trx
|
|
.select()
|
|
.from(domains)
|
|
.where(
|
|
and(
|
|
eq(domains.baseDomain, baseDomain),
|
|
eq(domains.type, type)
|
|
)
|
|
)
|
|
.leftJoin(
|
|
orgDomains,
|
|
eq(orgDomains.domainId, domains.domainId)
|
|
);
|
|
|
|
if (existing) {
|
|
const {
|
|
domains: existingDomain,
|
|
orgDomains: existingOrgDomain
|
|
} = existing;
|
|
|
|
// user alrady added domain to this account
|
|
// always reject
|
|
if (existingOrgDomain?.orgId === orgId) {
|
|
return next(
|
|
createHttpError(
|
|
HttpCode.BAD_REQUEST,
|
|
"Domain is already added to this org"
|
|
)
|
|
);
|
|
}
|
|
|
|
// domain already exists elsewhere
|
|
// check if it's already fully verified
|
|
if (existingDomain.verified) {
|
|
return next(
|
|
createHttpError(
|
|
HttpCode.BAD_REQUEST,
|
|
"Domain is already verified to an org"
|
|
)
|
|
);
|
|
}
|
|
}
|
|
|
|
// --- Domain overlap logic ---
|
|
// Only consider existing verified domains
|
|
const verifiedDomains = await trx
|
|
.select()
|
|
.from(domains)
|
|
.where(eq(domains.verified, true));
|
|
|
|
if (type == "cname") {
|
|
// Block if a verified CNAME exists at the same name
|
|
const cnameExists = verifiedDomains.some(
|
|
(d) => d.type === "cname" && d.baseDomain === baseDomain
|
|
);
|
|
if (cnameExists) {
|
|
return next(
|
|
createHttpError(
|
|
HttpCode.BAD_REQUEST,
|
|
`A CNAME record already exists for ${baseDomain}. Only one CNAME record is allowed per domain.`
|
|
)
|
|
);
|
|
}
|
|
// Block if a verified NS exists at or below (same or subdomain)
|
|
const nsAtOrBelow = verifiedDomains.some(
|
|
(d) =>
|
|
d.type === "ns" &&
|
|
(isSubdomainOrEqual(baseDomain, d.baseDomain) ||
|
|
baseDomain === d.baseDomain)
|
|
);
|
|
if (nsAtOrBelow) {
|
|
return next(
|
|
createHttpError(
|
|
HttpCode.BAD_REQUEST,
|
|
`A nameserver (NS) record exists at or below ${baseDomain}. You cannot create a CNAME record here.`
|
|
)
|
|
);
|
|
}
|
|
} else if (type == "ns") {
|
|
// Block if a verified NS exists at or below (same or subdomain)
|
|
const nsAtOrBelow = verifiedDomains.some(
|
|
(d) =>
|
|
d.type === "ns" &&
|
|
(isSubdomainOrEqual(baseDomain, d.baseDomain) ||
|
|
baseDomain === d.baseDomain)
|
|
);
|
|
if (nsAtOrBelow) {
|
|
return next(
|
|
createHttpError(
|
|
HttpCode.BAD_REQUEST,
|
|
`A nameserver (NS) record already exists at or below ${baseDomain}. You cannot create another NS record here.`
|
|
)
|
|
);
|
|
}
|
|
} else if (type == "wildcard") {
|
|
// TODO: Figure out how to handle wildcards
|
|
}
|
|
|
|
const domainId = generateId(15);
|
|
|
|
const [insertedDomain] = await trx
|
|
.insert(domains)
|
|
.values({
|
|
domainId,
|
|
baseDomain,
|
|
type,
|
|
verified: build == "oss" ? true : false
|
|
})
|
|
.returning();
|
|
|
|
returned = insertedDomain;
|
|
|
|
// add domain to account
|
|
await trx
|
|
.insert(orgDomains)
|
|
.values({
|
|
orgId,
|
|
domainId
|
|
})
|
|
.returning();
|
|
|
|
// TODO: This needs to be cross region and not hardcoded
|
|
if (type === "ns") {
|
|
nsRecords = ["ns-east.fossorial.io", "ns-west.fossorial.io"];
|
|
} else if (type === "cname") {
|
|
cnameRecords = [
|
|
{
|
|
value: `${domainId}.cname.fossorial.io`,
|
|
baseDomain: baseDomain
|
|
},
|
|
{
|
|
value: `_acme-challenge.${domainId}.cname.fossorial.io`,
|
|
baseDomain: `_acme-challenge.${baseDomain}`
|
|
}
|
|
];
|
|
} else if (type === "wildcard") {
|
|
cnameRecords = [
|
|
{
|
|
value: `Server IP Address`,
|
|
baseDomain: `*.${baseDomain}`
|
|
},
|
|
{
|
|
value: `Server IP Address`,
|
|
baseDomain: `${baseDomain}`
|
|
}
|
|
];
|
|
}
|
|
|
|
numOrgDomains = await trx
|
|
.select()
|
|
.from(orgDomains)
|
|
.where(eq(orgDomains.orgId, orgId));
|
|
});
|
|
|
|
if (!returned) {
|
|
return next(
|
|
createHttpError(
|
|
HttpCode.INTERNAL_SERVER_ERROR,
|
|
"Failed to create domain"
|
|
)
|
|
);
|
|
}
|
|
|
|
return response<CreateDomainResponse>(res, {
|
|
data: {
|
|
domainId: returned.domainId,
|
|
cnameRecords,
|
|
txtRecords,
|
|
nsRecords
|
|
},
|
|
success: true,
|
|
error: false,
|
|
message: "Domain created successfully",
|
|
status: HttpCode.CREATED
|
|
});
|
|
} catch (error) {
|
|
logger.error(error);
|
|
return next(
|
|
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
|
);
|
|
}
|
|
}
|