From fc4633db918bf93cc998833c8faad34868572008 Mon Sep 17 00:00:00 2001 From: Owen Date: Sat, 11 Apr 2026 17:19:18 -0700 Subject: [PATCH] Add domain component to the site resource --- server/db/pg/schema/schema.ts | 7 ++- server/db/sqlite/schema/schema.ts | 7 ++- .../siteResource/createSiteResource.ts | 56 +++++++++++++++---- .../siteResource/updateSiteResource.ts | 43 ++++++++++++-- 4 files changed, 95 insertions(+), 18 deletions(-) diff --git a/server/db/pg/schema/schema.ts b/server/db/pg/schema/schema.ts index 4885eec98..aac86c1b9 100644 --- a/server/db/pg/schema/schema.ts +++ b/server/db/pg/schema/schema.ts @@ -249,7 +249,12 @@ export const siteResources = pgTable("siteResources", { authDaemonPort: integer("authDaemonPort").default(22123), authDaemonMode: varchar("authDaemonMode", { length: 32 }) .$type<"site" | "remote">() - .default("site") + .default("site"), + domainId: varchar("domainId").references(() => domains.domainId, { + onDelete: "set null" + }), + subdomain: varchar("subdomain"), + fullDomain: varchar("fullDomain") }); export const clientSiteResources = pgTable("clientSiteResources", { diff --git a/server/db/sqlite/schema/schema.ts b/server/db/sqlite/schema/schema.ts index 7b31460f6..e58601dc3 100644 --- a/server/db/sqlite/schema/schema.ts +++ b/server/db/sqlite/schema/schema.ts @@ -277,7 +277,12 @@ export const siteResources = sqliteTable("siteResources", { authDaemonPort: integer("authDaemonPort").default(22123), authDaemonMode: text("authDaemonMode") .$type<"site" | "remote">() - .default("site") + .default("site"), + domainId: text("domainId").references(() => domains.domainId, { + onDelete: "set null" + }), + subdomain: text("subdomain"), + fullDomain: text("fullDomain"), }); export const clientSiteResources = sqliteTable("clientSiteResources", { diff --git a/server/routers/siteResource/createSiteResource.ts b/server/routers/siteResource/createSiteResource.ts index 6fbe50d59..d51bf54db 100644 --- a/server/routers/siteResource/createSiteResource.ts +++ b/server/routers/siteResource/createSiteResource.ts @@ -28,6 +28,7 @@ import { NextFunction, Request, Response } from "express"; import createHttpError from "http-errors"; import { z } from "zod"; import { fromError } from "zod-validation-error"; +import { validateAndConstructDomain } from "@server/lib/domainUtils"; const createSiteResourceParamsSchema = z.strictObject({ orgId: z.string() @@ -58,15 +59,14 @@ const createSiteResourceSchema = z udpPortRangeString: portRangeStringSchema, disableIcmp: z.boolean().optional(), authDaemonPort: z.int().positive().optional(), - authDaemonMode: z.enum(["site", "remote"]).optional() + authDaemonMode: z.enum(["site", "remote"]).optional(), + domainId: z.string().optional(), // only used for http mode, we need this to verify the alias is unique within the org + subdomain: z.string().optional() // only used for http mode, we need this to verify the alias is unique within the org }) .strict() .refine( (data) => { - if ( - data.mode === "host" || - data.mode == "http" - ) { + if (data.mode === "host" || data.mode == "http") { if (data.mode == "host") { // Check if it's a valid IP address using zod (v4 or v6) const isValidIP = z @@ -196,7 +196,9 @@ export async function createSiteResource( udpPortRangeString, disableIcmp, authDaemonPort, - authDaemonMode + authDaemonMode, + domainId, + subdomain } = parsedBody.data; // Verify the site exists and belongs to the org @@ -248,15 +250,47 @@ export async function createSiteResource( ); } + if (domainId && alias) { + // throw an error because we can only have one or the other + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "Alias and domain cannot both be set. Please choose one or the other." + ) + ); + } + + let fullDomain: string | null = null; + let finalSubdomain: string | null = null; + let finalAlias = alias ? alias.trim() : null; + if (domainId && subdomain) { + // Validate domain and construct full domain + const domainResult = await validateAndConstructDomain( + domainId, + orgId, + subdomain + ); + + if (!domainResult.success) { + return next( + createHttpError(HttpCode.BAD_REQUEST, domainResult.error) + ); + } + + fullDomain = domainResult.fullDomain; + finalSubdomain = domainResult.subdomain; + finalAlias = fullDomain; // we will use the full domain as the alias for uniqueness checks and routing + } + // make sure the alias is unique within the org if provided - if (alias) { + if (finalAlias) { const [conflict] = await db .select() .from(siteResources) .where( and( eq(siteResources.orgId, orgId), - eq(siteResources.alias, alias.trim()) + eq(siteResources.alias, finalAlias.trim()) ) ) .limit(1); @@ -296,11 +330,13 @@ export async function createSiteResource( scheme, destinationPort, enabled, - alias, + alias: finalAlias, aliasAddress, tcpPortRangeString, udpPortRangeString, - disableIcmp + disableIcmp, + domainId, + subdomain: finalSubdomain }; if (isLicensedSshPam) { if (authDaemonPort !== undefined) diff --git a/server/routers/siteResource/updateSiteResource.ts b/server/routers/siteResource/updateSiteResource.ts index 5b1fac861..b66792c75 100644 --- a/server/routers/siteResource/updateSiteResource.ts +++ b/server/routers/siteResource/updateSiteResource.ts @@ -14,6 +14,7 @@ import { userSiteResources } from "@server/db"; import { tierMatrix } from "@server/lib/billing/tierMatrix"; +import { validateAndConstructDomain } from "@server/lib/domainUtils"; import { generateAliasConfig, generateRemoteSubnets, @@ -72,7 +73,9 @@ const updateSiteResourceSchema = z udpPortRangeString: portRangeStringSchema, disableIcmp: z.boolean().optional(), authDaemonPort: z.int().positive().nullish(), - authDaemonMode: z.enum(["site", "remote"]).optional() + authDaemonMode: z.enum(["site", "remote"]).optional(), + domainId: z.string().optional(), + subdomain: z.string().optional() }) .strict() .refine( @@ -212,7 +215,9 @@ export async function updateSiteResource( udpPortRangeString, disableIcmp, authDaemonPort, - authDaemonMode + authDaemonMode, + domainId, + subdomain } = parsedBody.data; const [site] = await db @@ -302,15 +307,37 @@ export async function updateSiteResource( } } + let fullDomain: string | null = null; + let finalSubdomain: string | null = null; + let finalAlias = alias ? alias.trim() : null; + if (domainId && subdomain) { + // Validate domain and construct full domain + const domainResult = await validateAndConstructDomain( + domainId, + org.orgId, + subdomain + ); + + if (!domainResult.success) { + return next( + createHttpError(HttpCode.BAD_REQUEST, domainResult.error) + ); + } + + fullDomain = domainResult.fullDomain; + finalSubdomain = domainResult.subdomain; + finalAlias = fullDomain; // we will use the full domain as the alias for uniqueness checks and routing + } + // make sure the alias is unique within the org if provided - if (alias) { + if (finalAlias) { const [conflict] = await db .select() .from(siteResources) .where( and( eq(siteResources.orgId, existingSiteResource.orgId), - eq(siteResources.alias, alias.trim()), + eq(siteResources.alias, finalAlias.trim()), ne(siteResources.siteResourceId, siteResourceId) // exclude self ) ) @@ -378,10 +405,12 @@ export async function updateSiteResource( destination, destinationPort, enabled, - alias: alias && alias.trim() ? alias : null, + alias: finalAlias, tcpPortRangeString, udpPortRangeString, disableIcmp, + domainId, + subdomain: finalSubdomain, ...sshPamSet }) .where( @@ -484,10 +513,12 @@ export async function updateSiteResource( destination: destination, destinationPort: destinationPort, enabled: enabled, - alias: alias && alias.trim() ? alias : null, + alias: finalAlias, tcpPortRangeString: tcpPortRangeString, udpPortRangeString: udpPortRangeString, disableIcmp: disableIcmp, + domainId, + subdomain: finalSubdomain, ...sshPamSet }) .where(