add common domain validation func

This commit is contained in:
miloschwartz
2025-09-08 17:25:01 -07:00
parent 45cb1562e5
commit 06055ff62b
4 changed files with 131 additions and 136 deletions

112
server/lib/domainUtils.ts Normal file
View File

@@ -0,0 +1,112 @@
import { db } from "@server/db";
import { domains, orgDomains } from "@server/db";
import { eq, and } from "drizzle-orm";
import { subdomainSchema } from "@server/lib/schemas";
import { fromError } from "zod-validation-error";
export type DomainValidationResult = {
success: true;
fullDomain: string;
subdomain: string | null;
} | {
success: false;
error: string;
};
/**
* Validates a domain and constructs the full domain based on domain type and subdomain.
*
* @param domainId - The ID of the domain to validate
* @param orgId - The organization ID to check domain access
* @param subdomain - Optional subdomain to append (for ns and wildcard domains)
* @returns DomainValidationResult with success status and either fullDomain/subdomain or error message
*/
export async function validateAndConstructDomain(
domainId: string,
orgId: string,
subdomain?: string | null
): Promise<DomainValidationResult> {
try {
// Query domain with organization access check
const [domainRes] = await db
.select()
.from(domains)
.where(eq(domains.domainId, domainId))
.leftJoin(
orgDomains,
and(eq(orgDomains.orgId, orgId), eq(orgDomains.domainId, domainId))
);
// Check if domain exists
if (!domainRes || !domainRes.domains) {
return {
success: false,
error: `Domain with ID ${domainId} not found`
};
}
// Check if organization has access to domain
if (domainRes.orgDomains && domainRes.orgDomains.orgId !== orgId) {
return {
success: false,
error: `Organization does not have access to domain with ID ${domainId}`
};
}
// Check if domain is verified
if (!domainRes.domains.verified) {
return {
success: false,
error: `Domain with ID ${domainId} is not verified`
};
}
// Construct full domain based on domain type
let fullDomain = "";
let finalSubdomain = subdomain;
if (domainRes.domains.type === "ns") {
if (subdomain) {
fullDomain = `${subdomain}.${domainRes.domains.baseDomain}`;
} else {
fullDomain = domainRes.domains.baseDomain;
}
} else if (domainRes.domains.type === "cname") {
fullDomain = domainRes.domains.baseDomain;
finalSubdomain = null; // CNAME domains don't use subdomains
} else if (domainRes.domains.type === "wildcard") {
if (subdomain !== undefined && subdomain !== null) {
// Validate subdomain format for wildcard domains
const parsedSubdomain = subdomainSchema.safeParse(subdomain);
if (!parsedSubdomain.success) {
return {
success: false,
error: fromError(parsedSubdomain.error).toString()
};
}
fullDomain = `${subdomain}.${domainRes.domains.baseDomain}`;
} else {
fullDomain = domainRes.domains.baseDomain;
}
}
// If the full domain equals the base domain, set subdomain to null
if (fullDomain === domainRes.domains.baseDomain) {
finalSubdomain = null;
}
// Convert to lowercase
fullDomain = fullDomain.toLowerCase();
return {
success: true,
fullDomain,
subdomain: finalSubdomain ?? null
};
} catch (error) {
return {
success: false,
error: `An error occurred while validating domain: ${error instanceof Error ? error.message : 'Unknown error'}`
};
}
}

View File

@@ -21,6 +21,7 @@ import { subdomainSchema } from "@server/lib/schemas";
import config from "@server/lib/config"; import config from "@server/lib/config";
import { OpenAPITags, registry } from "@server/openApi"; import { OpenAPITags, registry } from "@server/openApi";
import { build } from "@server/build"; import { build } from "@server/build";
import { validateAndConstructDomain } from "@server/lib/domainUtils";
const createResourceParamsSchema = z const createResourceParamsSchema = z
.object({ .object({
@@ -193,76 +194,21 @@ async function createHttpResource(
} }
const { name, domainId } = parsedBody.data; const { name, domainId } = parsedBody.data;
let subdomain = parsedBody.data.subdomain; const subdomain = parsedBody.data.subdomain;
const [domainRes] = await db // Validate domain and construct full domain
.select() const domainResult = await validateAndConstructDomain(domainId, orgId, subdomain);
.from(domains)
.where(eq(domains.domainId, domainId))
.leftJoin(
orgDomains,
and(eq(orgDomains.orgId, orgId), eq(orgDomains.domainId, domainId))
);
if (!domainRes || !domainRes.domains) { if (!domainResult.success) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
`Domain with ID ${domainId} not found`
)
);
}
if (domainRes.orgDomains && domainRes.orgDomains.orgId !== orgId) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
`Organization does not have access to domain with ID ${domainId}`
)
);
}
if (!domainRes.domains.verified) {
return next( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,
`Domain with ID ${domainRes.domains.domainId} is not verified` domainResult.error
) )
); );
} }
let fullDomain = ""; const { fullDomain, subdomain: finalSubdomain } = domainResult;
if (domainRes.domains.type == "ns") {
if (subdomain) {
fullDomain = `${subdomain}.${domainRes.domains.baseDomain}`;
} else {
fullDomain = domainRes.domains.baseDomain;
}
} else if (domainRes.domains.type == "cname") {
fullDomain = domainRes.domains.baseDomain;
} else if (domainRes.domains.type == "wildcard") {
if (subdomain) {
// the subdomain cant have a dot in it
const parsedSubdomain = subdomainSchema.safeParse(subdomain);
if (!parsedSubdomain.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedSubdomain.error).toString()
)
);
}
fullDomain = `${subdomain}.${domainRes.domains.baseDomain}`;
} else {
fullDomain = domainRes.domains.baseDomain;
}
}
if (fullDomain === domainRes.domains.baseDomain) {
subdomain = null;
}
fullDomain = fullDomain.toLowerCase();
logger.debug(`Full domain: ${fullDomain}`); logger.debug(`Full domain: ${fullDomain}`);
@@ -291,7 +237,7 @@ async function createHttpResource(
domainId, domainId,
orgId, orgId,
name, name,
subdomain, subdomain: finalSubdomain,
http: true, http: true,
protocol: "tcp", protocol: "tcp",
ssl: true ssl: true

View File

@@ -20,6 +20,7 @@ import { tlsNameSchema } from "@server/lib/schemas";
import { subdomainSchema } from "@server/lib/schemas"; import { subdomainSchema } from "@server/lib/schemas";
import { registry } from "@server/openApi"; import { registry } from "@server/openApi";
import { OpenAPITags } from "@server/openApi"; import { OpenAPITags } from "@server/openApi";
import { validateAndConstructDomain } from "@server/lib/domainUtils";
const updateResourceParamsSchema = z const updateResourceParamsSchema = z
.object({ .object({
@@ -230,78 +231,19 @@ async function updateHttpResource(
if (updateData.domainId) { if (updateData.domainId) {
const domainId = updateData.domainId; const domainId = updateData.domainId;
const [domainRes] = await db // Validate domain and construct full domain
.select() const domainResult = await validateAndConstructDomain(domainId, resource.orgId, updateData.subdomain);
.from(domains)
.where(eq(domains.domainId, domainId))
.leftJoin(
orgDomains,
and(
eq(orgDomains.orgId, resource.orgId),
eq(orgDomains.domainId, domainId)
)
);
if (!domainRes || !domainRes.domains) { if (!domainResult.success) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
`Domain with ID ${updateData.domainId} not found`
)
);
}
if (
domainRes.orgDomains &&
domainRes.orgDomains.orgId !== resource.orgId
) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
`You do not have permission to use domain with ID ${updateData.domainId}`
)
);
}
if (!domainRes.domains.verified) {
return next( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,
`Domain with ID ${updateData.domainId} is not verified` domainResult.error
) )
); );
} }
let fullDomain = ""; const { fullDomain, subdomain: finalSubdomain } = domainResult;
if (domainRes.domains.type == "ns") {
if (updateData.subdomain) {
fullDomain = `${updateData.subdomain}.${domainRes.domains.baseDomain}`;
} else {
fullDomain = domainRes.domains.baseDomain;
}
} else if (domainRes.domains.type == "cname") {
fullDomain = domainRes.domains.baseDomain;
} else if (domainRes.domains.type == "wildcard") {
if (updateData.subdomain !== undefined) {
// the subdomain cant have a dot in it
const parsedSubdomain = subdomainSchema.safeParse(
updateData.subdomain
);
if (!parsedSubdomain.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedSubdomain.error).toString()
)
);
}
fullDomain = `${updateData.subdomain}.${domainRes.domains.baseDomain}`;
} else {
fullDomain = domainRes.domains.baseDomain;
}
}
fullDomain = fullDomain.toLowerCase();
logger.debug(`Full domain: ${fullDomain}`); logger.debug(`Full domain: ${fullDomain}`);
@@ -332,9 +274,8 @@ async function updateHttpResource(
.where(eq(resources.resourceId, resource.resourceId)); .where(eq(resources.resourceId, resource.resourceId));
} }
if (fullDomain === domainRes.domains.baseDomain) { // Update the subdomain in the update data
updateData.subdomain = null; updateData.subdomain = finalSubdomain;
}
} }
const updatedResource = await db const updatedResource = await db

View File

@@ -471,15 +471,11 @@ export default function GeneralForm() {
? `${sanitizedSubdomain}.${selectedDomain.baseDomain}` ? `${sanitizedSubdomain}.${selectedDomain.baseDomain}`
: selectedDomain.baseDomain; : selectedDomain.baseDomain;
setResourceFullDomain(sanitizedFullDomain); setResourceFullDomain(`${resource.ssl ? "https" : "http"}://${sanitizedFullDomain}`);
form.setValue("domainId", selectedDomain.domainId); form.setValue("domainId", selectedDomain.domainId);
form.setValue("subdomain", sanitizedSubdomain); form.setValue("subdomain", sanitizedSubdomain);
setEditDomainOpen(false); setEditDomainOpen(false);
toast({
description: `Final domain: ${sanitizedFullDomain}`,
});
} }
}} }}
> >