From 06055ff62bc2425cddca957969bd9095d6d77f2f Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Mon, 8 Sep 2025 17:25:01 -0700 Subject: [PATCH] add common domain validation func --- server/lib/domainUtils.ts | 112 ++++++++++++++++++ server/routers/resource/createResource.ts | 72 ++--------- server/routers/resource/updateResource.ts | 77 ++---------- .../resources/[resourceId]/general/page.tsx | 6 +- 4 files changed, 131 insertions(+), 136 deletions(-) create mode 100644 server/lib/domainUtils.ts diff --git a/server/lib/domainUtils.ts b/server/lib/domainUtils.ts new file mode 100644 index 00000000..d043ca51 --- /dev/null +++ b/server/lib/domainUtils.ts @@ -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 { + 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'}` + }; + } +} diff --git a/server/routers/resource/createResource.ts b/server/routers/resource/createResource.ts index e3e431ec..fa170ef5 100644 --- a/server/routers/resource/createResource.ts +++ b/server/routers/resource/createResource.ts @@ -21,6 +21,7 @@ import { subdomainSchema } from "@server/lib/schemas"; import config from "@server/lib/config"; import { OpenAPITags, registry } from "@server/openApi"; import { build } from "@server/build"; +import { validateAndConstructDomain } from "@server/lib/domainUtils"; const createResourceParamsSchema = z .object({ @@ -193,76 +194,21 @@ async function createHttpResource( } const { name, domainId } = parsedBody.data; - let subdomain = parsedBody.data.subdomain; + const subdomain = parsedBody.data.subdomain; - const [domainRes] = await db - .select() - .from(domains) - .where(eq(domains.domainId, domainId)) - .leftJoin( - orgDomains, - and(eq(orgDomains.orgId, orgId), eq(orgDomains.domainId, domainId)) - ); - - if (!domainRes || !domainRes.domains) { - 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) { + // Validate domain and construct full domain + const domainResult = await validateAndConstructDomain(domainId, orgId, subdomain); + + if (!domainResult.success) { return next( createHttpError( HttpCode.BAD_REQUEST, - `Domain with ID ${domainRes.domains.domainId} is not verified` + domainResult.error ) ); } - let fullDomain = ""; - 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(); + const { fullDomain, subdomain: finalSubdomain } = domainResult; logger.debug(`Full domain: ${fullDomain}`); @@ -291,7 +237,7 @@ async function createHttpResource( domainId, orgId, name, - subdomain, + subdomain: finalSubdomain, http: true, protocol: "tcp", ssl: true diff --git a/server/routers/resource/updateResource.ts b/server/routers/resource/updateResource.ts index 30acc0c1..05f1cf66 100644 --- a/server/routers/resource/updateResource.ts +++ b/server/routers/resource/updateResource.ts @@ -20,6 +20,7 @@ import { tlsNameSchema } from "@server/lib/schemas"; import { subdomainSchema } from "@server/lib/schemas"; import { registry } from "@server/openApi"; import { OpenAPITags } from "@server/openApi"; +import { validateAndConstructDomain } from "@server/lib/domainUtils"; const updateResourceParamsSchema = z .object({ @@ -230,78 +231,19 @@ async function updateHttpResource( if (updateData.domainId) { const domainId = updateData.domainId; - const [domainRes] = await db - .select() - .from(domains) - .where(eq(domains.domainId, domainId)) - .leftJoin( - orgDomains, - and( - eq(orgDomains.orgId, resource.orgId), - eq(orgDomains.domainId, domainId) - ) - ); - - if (!domainRes || !domainRes.domains) { - 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) { + // Validate domain and construct full domain + const domainResult = await validateAndConstructDomain(domainId, resource.orgId, updateData.subdomain); + + if (!domainResult.success) { return next( createHttpError( HttpCode.BAD_REQUEST, - `Domain with ID ${updateData.domainId} is not verified` + domainResult.error ) ); } - let fullDomain = ""; - 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(); + const { fullDomain, subdomain: finalSubdomain } = domainResult; logger.debug(`Full domain: ${fullDomain}`); @@ -332,9 +274,8 @@ async function updateHttpResource( .where(eq(resources.resourceId, resource.resourceId)); } - if (fullDomain === domainRes.domains.baseDomain) { - updateData.subdomain = null; - } + // Update the subdomain in the update data + updateData.subdomain = finalSubdomain; } const updatedResource = await db diff --git a/src/app/[orgId]/settings/resources/[resourceId]/general/page.tsx b/src/app/[orgId]/settings/resources/[resourceId]/general/page.tsx index e2bab1cb..0f201a1a 100644 --- a/src/app/[orgId]/settings/resources/[resourceId]/general/page.tsx +++ b/src/app/[orgId]/settings/resources/[resourceId]/general/page.tsx @@ -471,15 +471,11 @@ export default function GeneralForm() { ? `${sanitizedSubdomain}.${selectedDomain.baseDomain}` : selectedDomain.baseDomain; - setResourceFullDomain(sanitizedFullDomain); + setResourceFullDomain(`${resource.ssl ? "https" : "http"}://${sanitizedFullDomain}`); form.setValue("domainId", selectedDomain.domainId); form.setValue("subdomain", sanitizedSubdomain); setEditDomainOpen(false); - - toast({ - description: `Final domain: ${sanitizedFullDomain}`, - }); } }} >