Add domain component to the site resource

This commit is contained in:
Owen
2026-04-11 17:19:18 -07:00
parent 9e50569c31
commit fc4633db91
4 changed files with 95 additions and 18 deletions

View File

@@ -249,7 +249,12 @@ export const siteResources = pgTable("siteResources", {
authDaemonPort: integer("authDaemonPort").default(22123), authDaemonPort: integer("authDaemonPort").default(22123),
authDaemonMode: varchar("authDaemonMode", { length: 32 }) authDaemonMode: varchar("authDaemonMode", { length: 32 })
.$type<"site" | "remote">() .$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", { export const clientSiteResources = pgTable("clientSiteResources", {

View File

@@ -277,7 +277,12 @@ export const siteResources = sqliteTable("siteResources", {
authDaemonPort: integer("authDaemonPort").default(22123), authDaemonPort: integer("authDaemonPort").default(22123),
authDaemonMode: text("authDaemonMode") authDaemonMode: text("authDaemonMode")
.$type<"site" | "remote">() .$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", { export const clientSiteResources = sqliteTable("clientSiteResources", {

View File

@@ -28,6 +28,7 @@ import { NextFunction, Request, Response } from "express";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
import { z } from "zod"; import { z } from "zod";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
import { validateAndConstructDomain } from "@server/lib/domainUtils";
const createSiteResourceParamsSchema = z.strictObject({ const createSiteResourceParamsSchema = z.strictObject({
orgId: z.string() orgId: z.string()
@@ -58,15 +59,14 @@ const createSiteResourceSchema = z
udpPortRangeString: portRangeStringSchema, udpPortRangeString: portRangeStringSchema,
disableIcmp: z.boolean().optional(), disableIcmp: z.boolean().optional(),
authDaemonPort: z.int().positive().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() .strict()
.refine( .refine(
(data) => { (data) => {
if ( if (data.mode === "host" || data.mode == "http") {
data.mode === "host" ||
data.mode == "http"
) {
if (data.mode == "host") { if (data.mode == "host") {
// Check if it's a valid IP address using zod (v4 or v6) // Check if it's a valid IP address using zod (v4 or v6)
const isValidIP = z const isValidIP = z
@@ -196,7 +196,9 @@ export async function createSiteResource(
udpPortRangeString, udpPortRangeString,
disableIcmp, disableIcmp,
authDaemonPort, authDaemonPort,
authDaemonMode authDaemonMode,
domainId,
subdomain
} = parsedBody.data; } = parsedBody.data;
// Verify the site exists and belongs to the org // 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 // make sure the alias is unique within the org if provided
if (alias) { if (finalAlias) {
const [conflict] = await db const [conflict] = await db
.select() .select()
.from(siteResources) .from(siteResources)
.where( .where(
and( and(
eq(siteResources.orgId, orgId), eq(siteResources.orgId, orgId),
eq(siteResources.alias, alias.trim()) eq(siteResources.alias, finalAlias.trim())
) )
) )
.limit(1); .limit(1);
@@ -296,11 +330,13 @@ export async function createSiteResource(
scheme, scheme,
destinationPort, destinationPort,
enabled, enabled,
alias, alias: finalAlias,
aliasAddress, aliasAddress,
tcpPortRangeString, tcpPortRangeString,
udpPortRangeString, udpPortRangeString,
disableIcmp disableIcmp,
domainId,
subdomain: finalSubdomain
}; };
if (isLicensedSshPam) { if (isLicensedSshPam) {
if (authDaemonPort !== undefined) if (authDaemonPort !== undefined)

View File

@@ -14,6 +14,7 @@ import {
userSiteResources userSiteResources
} from "@server/db"; } from "@server/db";
import { tierMatrix } from "@server/lib/billing/tierMatrix"; import { tierMatrix } from "@server/lib/billing/tierMatrix";
import { validateAndConstructDomain } from "@server/lib/domainUtils";
import { import {
generateAliasConfig, generateAliasConfig,
generateRemoteSubnets, generateRemoteSubnets,
@@ -72,7 +73,9 @@ const updateSiteResourceSchema = z
udpPortRangeString: portRangeStringSchema, udpPortRangeString: portRangeStringSchema,
disableIcmp: z.boolean().optional(), disableIcmp: z.boolean().optional(),
authDaemonPort: z.int().positive().nullish(), 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() .strict()
.refine( .refine(
@@ -212,7 +215,9 @@ export async function updateSiteResource(
udpPortRangeString, udpPortRangeString,
disableIcmp, disableIcmp,
authDaemonPort, authDaemonPort,
authDaemonMode authDaemonMode,
domainId,
subdomain
} = parsedBody.data; } = parsedBody.data;
const [site] = await db 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 // make sure the alias is unique within the org if provided
if (alias) { if (finalAlias) {
const [conflict] = await db const [conflict] = await db
.select() .select()
.from(siteResources) .from(siteResources)
.where( .where(
and( and(
eq(siteResources.orgId, existingSiteResource.orgId), eq(siteResources.orgId, existingSiteResource.orgId),
eq(siteResources.alias, alias.trim()), eq(siteResources.alias, finalAlias.trim()),
ne(siteResources.siteResourceId, siteResourceId) // exclude self ne(siteResources.siteResourceId, siteResourceId) // exclude self
) )
) )
@@ -378,10 +405,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 ...sshPamSet
}) })
.where( .where(
@@ -484,10 +513,12 @@ export async function updateSiteResource(
destination: destination, destination: destination,
destinationPort: destinationPort, destinationPort: destinationPort,
enabled: enabled, enabled: enabled,
alias: alias && alias.trim() ? alias : null, alias: finalAlias,
tcpPortRangeString: tcpPortRangeString, tcpPortRangeString: tcpPortRangeString,
udpPortRangeString: udpPortRangeString, udpPortRangeString: udpPortRangeString,
disableIcmp: disableIcmp, disableIcmp: disableIcmp,
domainId,
subdomain: finalSubdomain,
...sshPamSet ...sshPamSet
}) })
.where( .where(