mirror of
https://github.com/fosrl/pangolin.git
synced 2026-02-26 23:06:37 +00:00
Pull up downstream changes
This commit is contained in:
252
server/routers/domain/createOrgDomain.ts
Normal file
252
server/routers/domain/createOrgDomain.ts
Normal file
@@ -0,0 +1,252 @@
|
||||
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";
|
||||
|
||||
const paramsSchema = z
|
||||
.object({
|
||||
orgId: z.string()
|
||||
})
|
||||
.strict();
|
||||
|
||||
const bodySchema = z
|
||||
.object({
|
||||
type: z.enum(["ns", "cname"]),
|
||||
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;
|
||||
|
||||
// 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.`
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const domainId = generateId(15);
|
||||
|
||||
const [insertedDomain] = await trx
|
||||
.insert(domains)
|
||||
.values({
|
||||
domainId,
|
||||
baseDomain,
|
||||
type
|
||||
})
|
||||
.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}`
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
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")
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user