From 78661799f237dce2a6451b1039629addd4f80c7c Mon Sep 17 00:00:00 2001 From: Owen Date: Mon, 14 Jul 2025 15:36:15 -0700 Subject: [PATCH] Resources working with new picker --- messages/en-US.json | 3 +- server/routers/resource/createResource.ts | 106 ++++++---- server/routers/resource/updateResource.ts | 186 ++++++++++-------- .../settings/resources/ResourcesTable.tsx | 30 ++- .../resources/[resourceId]/proxy/page.tsx | 50 +++-- .../settings/resources/create/page.tsx | 3 +- 6 files changed, 218 insertions(+), 160 deletions(-) diff --git a/messages/en-US.json b/messages/en-US.json index da83348e..01c242b6 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -1213,5 +1213,6 @@ "domainNotFoundDescription": "This resource is disabled because the domain no longer exists our system. Please set a new domain for this resource.", "failed": "Failed", "createNewOrgDescription": "Create a new organization", - "organization": "Organization" + "organization": "Organization", + "port": "Port" } diff --git a/server/routers/resource/createResource.ts b/server/routers/resource/createResource.ts index ba9e2462..784dc639 100644 --- a/server/routers/resource/createResource.ts +++ b/server/routers/resource/createResource.ts @@ -21,6 +21,7 @@ import logger from "@server/logger"; import { subdomainSchema } from "@server/lib/schemas"; import config from "@server/lib/config"; import { OpenAPITags, registry } from "@server/openApi"; +import { build } from "@server/build"; const createResourceParamsSchema = z .object({ @@ -36,7 +37,6 @@ const createHttpResourceSchema = z .string() .optional() .transform((val) => val?.toLowerCase()), - isBaseDomain: z.boolean().optional(), siteId: z.number(), http: z.boolean(), protocol: z.enum(["tcp", "udp"]), @@ -52,19 +52,6 @@ const createHttpResourceSchema = z }, { message: "Invalid subdomain" } ) - .refine( - (data) => { - if (!config.getRawConfig().flags?.allow_base_domain_resources) { - if (data.isBaseDomain) { - return false; - } - } - return true; - }, - { - message: "Base domain resources are not allowed" - } - ); const createRawResourceSchema = z .object({ @@ -101,9 +88,12 @@ registry.registerPath({ body: { content: { "application/json": { - schema: createHttpResourceSchema.or( - createRawResourceSchema - ) + schema: + build == "oss" + ? createHttpResourceSchema.or( + createRawResourceSchema + ) + : createHttpResourceSchema } } } @@ -166,7 +156,7 @@ export async function createResource( { siteId, orgId } ); } else { - if (!config.getRawConfig().flags?.allow_raw_resources) { + if (!config.getRawConfig().flags?.allow_raw_resources && build == "oss") { return next( createHttpError( HttpCode.BAD_REQUEST, @@ -211,35 +201,81 @@ async function createHttpResource( ); } - const { name, subdomain, isBaseDomain, http, protocol, domainId } = - parsedBody.data; + const { name, subdomain, domainId } = parsedBody.data; - const [orgDomain] = await db + const [domainRes] = await db .select() - .from(orgDomains) - .where( + .from(domains) + .where(eq(domains.domainId, domainId)) + .leftJoin( + orgDomains, and(eq(orgDomains.orgId, orgId), eq(orgDomains.domainId, domainId)) - ) - .leftJoin(domains, eq(orgDomains.domainId, domains.domainId)); + ); - if (!orgDomain || !orgDomain.domains) { + if (!domainRes || !domainRes.domains) { return next( createHttpError( HttpCode.NOT_FOUND, - `Domain with ID ${parsedBody.data.domainId} not found` + `Domain with ID ${domainId} not found` ) ); } - const domain = orgDomain.domains; + 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( + createHttpError( + HttpCode.BAD_REQUEST, + `Domain with ID ${domainRes.domains.domainId} is not verified` + ) + ); + } let fullDomain = ""; - if (isBaseDomain) { - fullDomain = domain.baseDomain; - } else { - fullDomain = `${subdomain}.${domain.baseDomain}`; + 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() + ) + ); + } + if (parsedSubdomain.data.includes(".")) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "Subdomain cannot contain a dot when using wildcard domains" + ) + ); + } + fullDomain = `${subdomain}.${domainRes.domains.baseDomain}`; + } else { + fullDomain = domainRes.domains.baseDomain; + } } + fullDomain = fullDomain.toLowerCase(); + logger.debug(`Full domain: ${fullDomain}`); // make sure the full domain is unique @@ -269,10 +305,10 @@ async function createHttpResource( orgId, name, subdomain, - http, - protocol, + http: true, + protocol: "tcp", ssl: true, - isBaseDomain + isBaseDomain: false }) .returning(); diff --git a/server/routers/resource/updateResource.ts b/server/routers/resource/updateResource.ts index 68e38a3e..9f623702 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 { build } from "@server/build"; const updateResourceParamsSchema = z .object({ @@ -40,7 +41,6 @@ const updateHttpResourceBodySchema = z sso: z.boolean().optional(), blockAccess: z.boolean().optional(), emailWhitelistEnabled: z.boolean().optional(), - isBaseDomain: z.boolean().optional(), applyRules: z.boolean().optional(), domainId: z.string().optional(), enabled: z.boolean().optional(), @@ -61,19 +61,6 @@ const updateHttpResourceBodySchema = z }, { message: "Invalid subdomain" } ) - .refine( - (data) => { - if (!config.getRawConfig().flags?.allow_base_domain_resources) { - if (data.isBaseDomain) { - return false; - } - } - return true; - }, - { - message: "Base domain resources are not allowed" - } - ) .refine( (data) => { if (data.tlsServerName) { @@ -134,9 +121,12 @@ registry.registerPath({ body: { content: { "application/json": { - schema: updateHttpResourceBodySchema.and( - updateRawResourceBodySchema - ) + schema: + build == "oss" + ? updateHttpResourceBodySchema.and( + updateRawResourceBodySchema + ) + : updateHttpResourceBodySchema } } } @@ -242,86 +232,120 @@ async function updateHttpResource( const updateData = parsedBody.data; if (updateData.domainId) { - const [existingDomain] = await db - .select() - .from(orgDomains) - .where( - and( - eq(orgDomains.orgId, org.orgId), - eq(orgDomains.domainId, updateData.domainId) - ) - ) - .leftJoin(domains, eq(orgDomains.domainId, domains.domainId)); + const domainId = updateData.domainId; - if (!existingDomain) { + 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 not found`) + createHttpError( + HttpCode.NOT_FOUND, + `Domain with ID ${updateData.domainId} not found` + ) ); } - } - - const domainId = updateData.domainId || resource.domainId!; - const subdomain = updateData.subdomain || resource.subdomain; - - const [domain] = await db - .select() - .from(domains) - .where(eq(domains.domainId, domainId)); - - const isBaseDomain = - updateData.isBaseDomain !== undefined - ? updateData.isBaseDomain - : resource.isBaseDomain; - - let fullDomain: string | null = null; - if (isBaseDomain) { - fullDomain = domain.baseDomain; - } else if (subdomain && domain) { - fullDomain = `${subdomain}.${domain.baseDomain}`; - } - - if (fullDomain) { - const [existingDomain] = await db - .select() - .from(resources) - .where(eq(resources.fullDomain, fullDomain)); if ( - existingDomain && - existingDomain.resourceId !== resource.resourceId + domainRes.orgDomains && + domainRes.orgDomains.orgId !== resource.orgId ) { return next( createHttpError( - HttpCode.CONFLICT, - "Resource with that domain already exists" + HttpCode.FORBIDDEN, + `You do not have permission to use domain with ID ${updateData.domainId}` ) ); } - } - const updatePayload = { - ...updateData, - fullDomain - }; + if (!domainRes.domains.verified) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + `Domain with ID ${updateData.domainId} is not verified` + ) + ); + } + + 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) { + // 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() + ) + ); + } + if (parsedSubdomain.data.includes(".")) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "Subdomain cannot contain a dot when using wildcard domains" + ) + ); + } + fullDomain = `${updateData.subdomain}.${domainRes.domains.baseDomain}`; + } else { + fullDomain = domainRes.domains.baseDomain; + } + } + + fullDomain = fullDomain.toLowerCase(); + + logger.debug(`Full domain: ${fullDomain}`); + + if (fullDomain) { + const [existingDomain] = await db + .select() + .from(resources) + .where(eq(resources.fullDomain, fullDomain)); + + if ( + existingDomain && + existingDomain.resourceId !== resource.resourceId + ) { + return next( + createHttpError( + HttpCode.CONFLICT, + "Resource with that domain already exists" + ) + ); + } + } + + // update the full domain if it has changed + if (fullDomain && fullDomain !== resource.fullDomain) { + await db + .update(resources) + .set({ fullDomain }) + .where(eq(resources.resourceId, resource.resourceId)); + } + } const updatedResource = await db .update(resources) - .set({ - name: updatePayload.name, - subdomain: updatePayload.subdomain, - ssl: updatePayload.ssl, - sso: updatePayload.sso, - blockAccess: updatePayload.blockAccess, - emailWhitelistEnabled: updatePayload.emailWhitelistEnabled, - isBaseDomain: updatePayload.isBaseDomain, - applyRules: updatePayload.applyRules, - domainId: updatePayload.domainId, - enabled: updatePayload.enabled, - stickySession: updatePayload.stickySession, - tlsServerName: updatePayload.tlsServerName, - setHostHeader: updatePayload.setHostHeader, - fullDomain: updatePayload.fullDomain - }) + .set(updateData) .where(eq(resources.resourceId, resource.resourceId)) .returning(); diff --git a/src/app/[orgId]/settings/resources/ResourcesTable.tsx b/src/app/[orgId]/settings/resources/ResourcesTable.tsx index 3d2d8710..e64fb4e3 100644 --- a/src/app/[orgId]/settings/resources/ResourcesTable.tsx +++ b/src/app/[orgId]/settings/resources/ResourcesTable.tsx @@ -162,25 +162,21 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) { const resourceRow = row.original; return (
- {!resourceRow.domainId ? ( + {!resourceRow.http ? ( + + ) : !resourceRow.domainId ? ( ) : ( -
- {!resourceRow.http ? ( - - ) : ( - - )} -
+ )}
); @@ -228,9 +224,11 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) { cell: ({ row }) => ( toggleResourceEnabled(val, row.original.id) } diff --git a/src/app/[orgId]/settings/resources/[resourceId]/proxy/page.tsx b/src/app/[orgId]/settings/resources/[resourceId]/proxy/page.tsx index 8d40344e..6a1380f2 100644 --- a/src/app/[orgId]/settings/resources/[resourceId]/proxy/page.tsx +++ b/src/app/[orgId]/settings/resources/[resourceId]/proxy/page.tsx @@ -339,34 +339,32 @@ export default function ReverseProxyTargets(props: { await api.delete(`/target/${targetId}`); } - // Save sticky session setting - const stickySessionData = targetsSettingsForm.getValues(); - await api.post(`/resource/${params.resourceId}`, { - stickySession: stickySessionData.stickySession - }); - updateResource({ stickySession: stickySessionData.stickySession }); + if (resource.http) { + // Gather all settings + const stickySessionData = targetsSettingsForm.getValues(); + const tlsData = tlsSettingsForm.getValues(); + const proxyData = proxySettingsForm.getValues(); - // Save TLS settings - const tlsData = tlsSettingsForm.getValues(); - await api.post(`/resource/${params.resourceId}`, { - ssl: tlsData.ssl, - tlsServerName: tlsData.tlsServerName || null - }); - updateResource({ - ...resource, - ssl: tlsData.ssl, - tlsServerName: tlsData.tlsServerName || null - }); + // Combine into one payload + const payload = { + stickySession: stickySessionData.stickySession, + ssl: tlsData.ssl, + tlsServerName: tlsData.tlsServerName || null, + setHostHeader: proxyData.setHostHeader || null + }; - // Save proxy settings - const proxyData = proxySettingsForm.getValues(); - await api.post(`/resource/${params.resourceId}`, { - setHostHeader: proxyData.setHostHeader || null - }); - updateResource({ - ...resource, - setHostHeader: proxyData.setHostHeader || null - }); + // Single API call to update all settings + await api.post(`/resource/${params.resourceId}`, payload); + + // Update local resource context + updateResource({ + ...resource, + stickySession: stickySessionData.stickySession, + ssl: tlsData.ssl, + tlsServerName: tlsData.tlsServerName || null, + setHostHeader: proxyData.setHostHeader || null + }); + } toast({ title: t("settingsUpdated"), diff --git a/src/app/[orgId]/settings/resources/create/page.tsx b/src/app/[orgId]/settings/resources/create/page.tsx index b357e03f..22e9d90c 100644 --- a/src/app/[orgId]/settings/resources/create/page.tsx +++ b/src/app/[orgId]/settings/resources/create/page.tsx @@ -165,7 +165,8 @@ export default function Page() { const httpData = httpForm.getValues(); Object.assign(payload, { subdomain: httpData.subdomain, - domainId: httpData.domainId + domainId: httpData.domainId, + protocol: "tcp", }); } else { const tcpUdpData = tcpUdpForm.getValues();