From 28f8b05dbcc847036c20e61f09279b72117c5129 Mon Sep 17 00:00:00 2001 From: Owen Date: Sun, 27 Jul 2025 10:21:27 -0700 Subject: [PATCH] Basic clients working --- messages/en-US.json | 7 +- server/db/pg/schema.ts | 3 +- server/db/sqlite/schema.ts | 3 +- server/routers/client/updateClient.ts | 3 +- server/routers/newt/handleGetConfigMessage.ts | 100 ++++++++++++++- server/routers/newt/targets.ts | 42 +++++-- .../routers/olm/handleOlmRegisterMessage.ts | 15 +-- server/routers/olm/peers.ts | 8 +- server/routers/resource/createResource.ts | 10 +- server/routers/resource/deleteResource.ts | 3 +- server/routers/resource/transferResource.ts | 6 +- server/routers/resource/updateResource.ts | 3 +- server/routers/target/createTarget.ts | 2 +- server/routers/target/deleteTarget.ts | 2 +- server/routers/target/updateTarget.ts | 2 +- server/routers/traefik/getTraefikConfig.ts | 7 +- server/setup/scriptsSqlite/1.8.0.ts | 29 +++++ src/app/[orgId]/settings/clients/page.tsx | 2 +- .../[resourceId]/ResourceInfoBox.tsx | 37 ++++-- .../resources/[resourceId]/general/page.tsx | 118 +++++++++++++----- .../settings/resources/create/page.tsx | 72 +++++++++-- 21 files changed, 387 insertions(+), 87 deletions(-) create mode 100644 server/setup/scriptsSqlite/1.8.0.ts diff --git a/messages/en-US.json b/messages/en-US.json index 8e78c3d2..4ff1b866 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -1093,7 +1093,7 @@ "sidebarAllUsers": "All Users", "sidebarIdentityProviders": "Identity Providers", "sidebarLicense": "License", - "sidebarClients": "Clients", + "sidebarClients": "Clients (beta)", "sidebarDomains": "Domains", "enableDockerSocket": "Enable Docker Socket", "enableDockerSocketDescription": "Enable Docker Socket discovery for populating container information. Socket path must be provided to Newt.", @@ -1315,5 +1315,8 @@ "olmErrorFetchLatest": "An error occurred while fetching the latest Olm release.", "remoteSubnets": "Remote Subnets", "enterCidrRange": "Enter CIDR range", - "remoteSubnetsDescription": "Add CIDR ranges that can access this site remotely. Use format like 10.0.0.0/24 or 192.168.1.0/24." + "remoteSubnetsDescription": "Add CIDR ranges that can access this site remotely. Use format like 10.0.0.0/24 or 192.168.1.0/24.", + "resourceEnableProxy": "Enable Public Proxy", + "resourceEnableProxyDescription": "Enable public proxying to this resource. This allows access to the resource from outside the network through the cloud on an open port. Requires Traefik config.", + "externalProxyEnabled": "External Proxy Enabled" } \ No newline at end of file diff --git a/server/db/pg/schema.ts b/server/db/pg/schema.ts index d774a985..5709c9f8 100644 --- a/server/db/pg/schema.ts +++ b/server/db/pg/schema.ts @@ -94,7 +94,8 @@ export const resources = pgTable("resources", { enabled: boolean("enabled").notNull().default(true), stickySession: boolean("stickySession").notNull().default(false), tlsServerName: varchar("tlsServerName"), - setHostHeader: varchar("setHostHeader") + setHostHeader: varchar("setHostHeader"), + enableProxy: boolean("enableProxy").notNull().default(true), }); export const targets = pgTable("targets", { diff --git a/server/db/sqlite/schema.ts b/server/db/sqlite/schema.ts index d372856d..974faa67 100644 --- a/server/db/sqlite/schema.ts +++ b/server/db/sqlite/schema.ts @@ -106,7 +106,8 @@ export const resources = sqliteTable("resources", { .notNull() .default(false), tlsServerName: text("tlsServerName"), - setHostHeader: text("setHostHeader") + setHostHeader: text("setHostHeader"), + enableProxy: integer("enableProxy", { mode: "boolean" }).default(true), }); export const targets = sqliteTable("targets", { diff --git a/server/routers/client/updateClient.ts b/server/routers/client/updateClient.ts index 87bb3c47..73c67d53 100644 --- a/server/routers/client/updateClient.ts +++ b/server/routers/client/updateClient.ts @@ -147,7 +147,8 @@ export async function updateClient( endpoint: site.endpoint, publicKey: site.publicKey, serverIP: site.address, - serverPort: site.listenPort + serverPort: site.listenPort, + remoteSubnets: site.remoteSubnets }); } diff --git a/server/routers/newt/handleGetConfigMessage.ts b/server/routers/newt/handleGetConfigMessage.ts index ce887b98..2d6ed98b 100644 --- a/server/routers/newt/handleGetConfigMessage.ts +++ b/server/routers/newt/handleGetConfigMessage.ts @@ -2,9 +2,16 @@ import { z } from "zod"; import { MessageHandler } from "../ws"; import logger from "@server/logger"; import { fromError } from "zod-validation-error"; -import { db, ExitNode, exitNodes } from "@server/db"; +import { + db, + ExitNode, + exitNodes, + resources, + Target, + targets +} from "@server/db"; import { clients, clientSites, Newt, sites } from "@server/db"; -import { eq } from "drizzle-orm"; +import { eq, and, inArray } from "drizzle-orm"; import { updatePeer } from "../olm/peers"; import axios from "axios"; @@ -191,7 +198,8 @@ export const handleGetConfigMessage: MessageHandler = async (context) => { endpoint: endpoint, publicKey: site.publicKey, serverIP: site.address, - serverPort: site.listenPort + serverPort: site.listenPort, + remoteSubnets: site.remoteSubnets }); } catch (error) { logger.error( @@ -212,14 +220,96 @@ export const handleGetConfigMessage: MessageHandler = async (context) => { // Filter out any null values from peers that didn't have an olm const validPeers = peers.filter((peer) => peer !== null); + // Improved version + const allResources = await db.transaction(async (tx) => { + // First get all resources for the site + const resourcesList = await tx + .select({ + resourceId: resources.resourceId, + subdomain: resources.subdomain, + fullDomain: resources.fullDomain, + ssl: resources.ssl, + blockAccess: resources.blockAccess, + sso: resources.sso, + emailWhitelistEnabled: resources.emailWhitelistEnabled, + http: resources.http, + proxyPort: resources.proxyPort, + protocol: resources.protocol + }) + .from(resources) + .where(and(eq(resources.siteId, siteId), eq(resources.http, false))); + + // Get all enabled targets for these resources in a single query + const resourceIds = resourcesList.map((r) => r.resourceId); + const allTargets = + resourceIds.length > 0 + ? await tx + .select({ + resourceId: targets.resourceId, + targetId: targets.targetId, + ip: targets.ip, + method: targets.method, + port: targets.port, + internalPort: targets.internalPort, + enabled: targets.enabled, + }) + .from(targets) + .where( + and( + inArray(targets.resourceId, resourceIds), + eq(targets.enabled, true) + ) + ) + : []; + + // Combine the data in JS instead of using SQL for the JSON + return resourcesList.map((resource) => ({ + ...resource, + targets: allTargets.filter( + (target) => target.resourceId === resource.resourceId + ) + })); + }); + + const { tcpTargets, udpTargets } = allResources.reduce( + (acc, resource) => { + // Skip resources with no targets + if (!resource.targets?.length) return acc; + + // Format valid targets into strings + const formattedTargets = resource.targets + .filter( + (target: Target) => + resource.proxyPort && target?.ip && target?.port + ) + .map( + (target: Target) => + `${resource.proxyPort}:${target.ip}:${target.port}` + ); + + // Add to the appropriate protocol array + if (resource.protocol === "tcp") { + acc.tcpTargets.push(...formattedTargets); + } else { + acc.udpTargets.push(...formattedTargets); + } + + return acc; + }, + { tcpTargets: [] as string[], udpTargets: [] as string[] } + ); + // Build the configuration response const configResponse = { ipAddress: site.address, - peers: validPeers + peers: validPeers, + targets: { + udp: udpTargets, + tcp: tcpTargets + } }; logger.debug("Sending config: ", configResponse); - return { message: { type: "newt/wg/receive-config", diff --git a/server/routers/newt/targets.ts b/server/routers/newt/targets.ts index d3c541a6..642fc2df 100644 --- a/server/routers/newt/targets.ts +++ b/server/routers/newt/targets.ts @@ -4,7 +4,8 @@ import { sendToClient } from "../ws"; export function addTargets( newtId: string, targets: Target[], - protocol: string + protocol: string, + port: number | null = null ) { //create a list of udp and tcp targets const payloadTargets = targets.map((target) => { @@ -13,19 +14,32 @@ export function addTargets( }:${target.port}`; }); - const payload = { + sendToClient(newtId, { type: `newt/${protocol}/add`, data: { targets: payloadTargets } - }; - sendToClient(newtId, payload); + }); + + const payloadTargetsResources = targets.map((target) => { + return `${port ? port + ":" : ""}${ + target.ip + }:${target.port}`; + }); + + sendToClient(newtId, { + type: `newt/wg/${protocol}/add`, + data: { + targets: [payloadTargetsResources[0]] // We can only use one target for WireGuard right now + } + }); } export function removeTargets( newtId: string, targets: Target[], - protocol: string + protocol: string, + port: number | null = null ) { //create a list of udp and tcp targets const payloadTargets = targets.map((target) => { @@ -34,11 +48,23 @@ export function removeTargets( }:${target.port}`; }); - const payload = { + sendToClient(newtId, { type: `newt/${protocol}/remove`, data: { targets: payloadTargets } - }; - sendToClient(newtId, payload); + }); + + const payloadTargetsResources = targets.map((target) => { + return `${port ? port + ":" : ""}${ + target.ip + }:${target.port}`; + }); + + sendToClient(newtId, { + type: `newt/wg/${protocol}/remove`, + data: { + targets: [payloadTargetsResources[0]] // We can only use one target for WireGuard right now + } + }); } diff --git a/server/routers/olm/handleOlmRegisterMessage.ts b/server/routers/olm/handleOlmRegisterMessage.ts index f504ecd7..8a73daff 100644 --- a/server/routers/olm/handleOlmRegisterMessage.ts +++ b/server/routers/olm/handleOlmRegisterMessage.ts @@ -119,12 +119,12 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => { continue; } - if (site.lastHolePunch && now - site.lastHolePunch > 6 && relay) { - logger.warn( - `Site ${site.siteId} last hole punch is too old, skipping` - ); - continue; - } + // if (site.lastHolePunch && now - site.lastHolePunch > 6 && relay) { + // logger.warn( + // `Site ${site.siteId} last hole punch is too old, skipping` + // ); + // continue; + // } // If public key changed, delete old peer from this site if (client.pubKey && client.pubKey != publicKey) { @@ -175,7 +175,8 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => { endpoint: endpoint, publicKey: site.publicKey, serverIP: site.address, - serverPort: site.listenPort + serverPort: site.listenPort, + remoteSubnets: site.remoteSubnets }); } diff --git a/server/routers/olm/peers.ts b/server/routers/olm/peers.ts index 48a915aa..c47c84a8 100644 --- a/server/routers/olm/peers.ts +++ b/server/routers/olm/peers.ts @@ -12,6 +12,7 @@ export async function addPeer( endpoint: string; serverIP: string | null; serverPort: number | null; + remoteSubnets: string | null; // optional, comma-separated list of subnets that this site can access } ) { const [olm] = await db @@ -30,7 +31,8 @@ export async function addPeer( publicKey: peer.publicKey, endpoint: peer.endpoint, serverIP: peer.serverIP, - serverPort: peer.serverPort + serverPort: peer.serverPort, + remoteSubnets: peer.remoteSubnets // optional, comma-separated list of subnets that this site can access } }); @@ -66,6 +68,7 @@ export async function updatePeer( endpoint: string; serverIP: string | null; serverPort: number | null; + remoteSubnets?: string | null; // optional, comma-separated list of subnets that } ) { const [olm] = await db @@ -84,7 +87,8 @@ export async function updatePeer( publicKey: peer.publicKey, endpoint: peer.endpoint, serverIP: peer.serverIP, - serverPort: peer.serverPort + serverPort: peer.serverPort, + remoteSubnets: peer.remoteSubnets } }); diff --git a/server/routers/resource/createResource.ts b/server/routers/resource/createResource.ts index 8f16b198..dfbb7617 100644 --- a/server/routers/resource/createResource.ts +++ b/server/routers/resource/createResource.ts @@ -40,7 +40,7 @@ const createHttpResourceSchema = z siteId: z.number(), http: z.boolean(), protocol: z.enum(["tcp", "udp"]), - domainId: z.string() + domainId: z.string(), }) .strict() .refine( @@ -59,7 +59,8 @@ const createRawResourceSchema = z siteId: z.number(), http: z.boolean(), protocol: z.enum(["tcp", "udp"]), - proxyPort: z.number().int().min(1).max(65535) + proxyPort: z.number().int().min(1).max(65535), + enableProxy: z.boolean().default(true) }) .strict() .refine( @@ -378,7 +379,7 @@ async function createRawResource( ); } - const { name, http, protocol, proxyPort } = parsedBody.data; + const { name, http, protocol, proxyPort, enableProxy } = parsedBody.data; // if http is false check to see if there is already a resource with the same port and protocol const existingResource = await db @@ -411,7 +412,8 @@ async function createRawResource( name, http, protocol, - proxyPort + proxyPort, + enableProxy }) .returning(); diff --git a/server/routers/resource/deleteResource.ts b/server/routers/resource/deleteResource.ts index bb9a6f32..99adc5f7 100644 --- a/server/routers/resource/deleteResource.ts +++ b/server/routers/resource/deleteResource.ts @@ -103,7 +103,8 @@ export async function deleteResource( removeTargets( newt.newtId, targetsToBeRemoved, - deletedResource.protocol + deletedResource.protocol, + deletedResource.proxyPort ); } } diff --git a/server/routers/resource/transferResource.ts b/server/routers/resource/transferResource.ts index e0fce278..a99405df 100644 --- a/server/routers/resource/transferResource.ts +++ b/server/routers/resource/transferResource.ts @@ -168,7 +168,8 @@ export async function transferResource( removeTargets( newt.newtId, resourceTargets, - updatedResource.protocol + updatedResource.protocol, + updatedResource.proxyPort ); } } @@ -190,7 +191,8 @@ export async function transferResource( addTargets( newt.newtId, resourceTargets, - updatedResource.protocol + updatedResource.protocol, + updatedResource.proxyPort ); } } diff --git a/server/routers/resource/updateResource.ts b/server/routers/resource/updateResource.ts index a20a7024..e99c6e8b 100644 --- a/server/routers/resource/updateResource.ts +++ b/server/routers/resource/updateResource.ts @@ -93,7 +93,8 @@ const updateRawResourceBodySchema = z name: z.string().min(1).max(255).optional(), proxyPort: z.number().int().min(1).max(65535).optional(), stickySession: z.boolean().optional(), - enabled: z.boolean().optional() + enabled: z.boolean().optional(), + enableProxy: z.boolean().optional(), }) .strict() .refine((data) => Object.keys(data).length > 0, { diff --git a/server/routers/target/createTarget.ts b/server/routers/target/createTarget.ts index 52bd0417..ffea1571 100644 --- a/server/routers/target/createTarget.ts +++ b/server/routers/target/createTarget.ts @@ -173,7 +173,7 @@ export async function createTarget( .where(eq(newts.siteId, site.siteId)) .limit(1); - addTargets(newt.newtId, newTarget, resource.protocol); + addTargets(newt.newtId, newTarget, resource.protocol, resource.proxyPort); } } } diff --git a/server/routers/target/deleteTarget.ts b/server/routers/target/deleteTarget.ts index 17a9c5ee..6eadeccd 100644 --- a/server/routers/target/deleteTarget.ts +++ b/server/routers/target/deleteTarget.ts @@ -105,7 +105,7 @@ export async function deleteTarget( .where(eq(newts.siteId, site.siteId)) .limit(1); - removeTargets(newt.newtId, [deletedTarget], resource.protocol); + removeTargets(newt.newtId, [deletedTarget], resource.protocol, resource.proxyPort); } } diff --git a/server/routers/target/updateTarget.ts b/server/routers/target/updateTarget.ts index 0138520b..0b7c4692 100644 --- a/server/routers/target/updateTarget.ts +++ b/server/routers/target/updateTarget.ts @@ -157,7 +157,7 @@ export async function updateTarget( .where(eq(newts.siteId, site.siteId)) .limit(1); - addTargets(newt.newtId, [updatedTarget], resource.protocol); + addTargets(newt.newtId, [updatedTarget], resource.protocol, resource.proxyPort); } } return response(res, { diff --git a/server/routers/traefik/getTraefikConfig.ts b/server/routers/traefik/getTraefikConfig.ts index c876de22..882a296a 100644 --- a/server/routers/traefik/getTraefikConfig.ts +++ b/server/routers/traefik/getTraefikConfig.ts @@ -66,7 +66,8 @@ export async function traefikConfigProvider( enabled: resources.enabled, stickySession: resources.stickySession, tlsServerName: resources.tlsServerName, - setHostHeader: resources.setHostHeader + setHostHeader: resources.setHostHeader, + enableProxy: resources.enableProxy }) .from(resources) .innerJoin(sites, eq(sites.siteId, resources.siteId)) @@ -365,6 +366,10 @@ export async function traefikConfigProvider( } } else { // Non-HTTP (TCP/UDP) configuration + if (!resource.enableProxy) { + continue; + } + const protocol = resource.protocol.toLowerCase(); const port = resource.proxyPort; diff --git a/server/setup/scriptsSqlite/1.8.0.ts b/server/setup/scriptsSqlite/1.8.0.ts new file mode 100644 index 00000000..efb4f68a --- /dev/null +++ b/server/setup/scriptsSqlite/1.8.0.ts @@ -0,0 +1,29 @@ +import { APP_PATH } from "@server/lib/consts"; +import Database from "better-sqlite3"; +import path from "path"; + +const version = "1.8.0"; + +export default async function migration() { + console.log("Running setup script ${version}..."); + + const location = path.join(APP_PATH, "db", "db.sqlite"); + const db = new Database(location); + + try { + db.transaction(() => { + db.exec(` + ALTER TABLE 'user' ADD 'termsAcceptedTimestamp' text; + ALTER TABLE 'user' ADD 'termsVersion' text; + ALTER TABLE 'sites' ADD 'remoteSubnets' text; + `); + })(); + + console.log("Migrated database schema"); + } catch (e) { + console.log("Unable to migrate database schema"); + throw e; + } + + console.log(`${version} migration complete`); +} diff --git a/src/app/[orgId]/settings/clients/page.tsx b/src/app/[orgId]/settings/clients/page.tsx index b798bf93..83cc11e3 100644 --- a/src/app/[orgId]/settings/clients/page.tsx +++ b/src/app/[orgId]/settings/clients/page.tsx @@ -48,7 +48,7 @@ export default async function ClientsPage(props: ClientsPageProps) { return ( <> diff --git a/src/app/[orgId]/settings/resources/[resourceId]/ResourceInfoBox.tsx b/src/app/[orgId]/settings/resources/[resourceId]/ResourceInfoBox.tsx index 717e4d49..cc4408b2 100644 --- a/src/app/[orgId]/settings/resources/[resourceId]/ResourceInfoBox.tsx +++ b/src/app/[orgId]/settings/resources/[resourceId]/ResourceInfoBox.tsx @@ -18,6 +18,7 @@ import { useEffect, useState } from "react"; import { Button } from "@/components/ui/button"; import { RotateCw } from "lucide-react"; import { createApiClient } from "@app/lib/api"; +import { build } from "@server/build"; type ResourceInfoBoxType = {}; @@ -34,7 +35,7 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) { - {t('resourceInfo')} + {t("resourceInfo")} @@ -42,7 +43,7 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) { <> - {t('authentication')} + {t("authentication")} {authInfo.password || @@ -51,12 +52,12 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) { authInfo.whitelist ? (
- {t('protected')} + {t("protected")}
) : (
- {t('notProtected')} + {t("notProtected")}
)}
@@ -71,7 +72,7 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) {
- {t('site')} + {t("site")} {resource.siteName} @@ -98,7 +99,9 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) { ) : ( <> - {t('protocol')} + + {t("protocol")} + {resource.protocol.toUpperCase()} @@ -106,7 +109,7 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) { - {t('port')} + {t("port")} + {build == "oss" && ( + + + {t("externalProxyEnabled")} + + + + {resource.enableProxy + ? t("enabled") + : t("disabled")} + + + + )} )} - {t('visibility')} + {t("visibility")} - {resource.enabled ? t('enabled') : t('disabled')} + {resource.enabled + ? t("enabled") + : t("disabled")} diff --git a/src/app/[orgId]/settings/resources/[resourceId]/general/page.tsx b/src/app/[orgId]/settings/resources/[resourceId]/general/page.tsx index efda61c3..266911a6 100644 --- a/src/app/[orgId]/settings/resources/[resourceId]/general/page.tsx +++ b/src/app/[orgId]/settings/resources/[resourceId]/general/page.tsx @@ -66,6 +66,7 @@ import { } from "@server/routers/resource"; import { SwitchInput } from "@app/components/SwitchInput"; import { useTranslations } from "next-intl"; +import { Checkbox } from "@app/components/ui/checkbox"; import { Credenza, CredenzaBody, @@ -78,6 +79,7 @@ import { } from "@app/components/Credenza"; import DomainPicker from "@app/components/DomainPicker"; import { Globe } from "lucide-react"; +import { build } from "@server/build"; const TransferFormSchema = z.object({ siteId: z.number() @@ -118,25 +120,31 @@ export default function GeneralForm() { fullDomain: string; } | null>(null); - const GeneralFormSchema = z.object({ - enabled: z.boolean(), - subdomain: z.string().optional(), - name: z.string().min(1).max(255), - domainId: z.string().optional(), - proxyPort: z.number().int().min(1).max(65535).optional() - }).refine((data) => { - // For non-HTTP resources, proxyPort should be defined - if (!resource.http) { - return data.proxyPort !== undefined; - } - // For HTTP resources, proxyPort should be undefined - return data.proxyPort === undefined; - }, { - message: !resource.http - ? "Port number is required for non-HTTP resources" - : "Port number should not be set for HTTP resources", - path: ["proxyPort"] - }); + const GeneralFormSchema = z + .object({ + enabled: z.boolean(), + subdomain: z.string().optional(), + name: z.string().min(1).max(255), + domainId: z.string().optional(), + proxyPort: z.number().int().min(1).max(65535).optional(), + enableProxy: z.boolean().optional() + }) + .refine( + (data) => { + // For non-HTTP resources, proxyPort should be defined + if (!resource.http) { + return data.proxyPort !== undefined; + } + // For HTTP resources, proxyPort should be undefined + return data.proxyPort === undefined; + }, + { + message: !resource.http + ? "Port number is required for non-HTTP resources" + : "Port number should not be set for HTTP resources", + path: ["proxyPort"] + } + ); type GeneralFormValues = z.infer; @@ -147,7 +155,8 @@ export default function GeneralForm() { name: resource.name, subdomain: resource.subdomain ? resource.subdomain : undefined, domainId: resource.domainId || undefined, - proxyPort: resource.proxyPort || undefined + proxyPort: resource.proxyPort || undefined, + enableProxy: resource.enableProxy || false }, mode: "onChange" }); @@ -211,7 +220,8 @@ export default function GeneralForm() { name: data.name, subdomain: data.subdomain, domainId: data.domainId, - proxyPort: data.proxyPort + proxyPort: data.proxyPort, + enableProxy: data.enableProxy } ) .catch((e) => { @@ -238,7 +248,8 @@ export default function GeneralForm() { name: data.name, subdomain: data.subdomain, fullDomain: resource.fullDomain, - proxyPort: data.proxyPort + proxyPort: data.proxyPort, + enableProxy: data.enableProxy }); router.refresh(); @@ -357,16 +368,29 @@ export default function GeneralForm() { render={({ field }) => ( - {t("resourcePortNumber")} + {t( + "resourcePortNumber" + )} + value={ + field.value ?? + "" + } + onChange={( + e + ) => field.onChange( - e.target.value - ? parseInt(e.target.value) + e + .target + .value + ? parseInt( + e + .target + .value + ) : undefined ) } @@ -374,11 +398,49 @@ export default function GeneralForm() { - {t("resourcePortNumberDescription")} + {t( + "resourcePortNumberDescription" + )} )} /> + + {build == "oss" && ( + ( + + + + +
+ + {t( + "resourceEnableProxy" + )} + + + {t( + "resourceEnableProxyDescription" + )} + +
+
+ )} + /> + )} )} diff --git a/src/app/[orgId]/settings/resources/create/page.tsx b/src/app/[orgId]/settings/resources/create/page.tsx index 22e9d90c..a916a700 100644 --- a/src/app/[orgId]/settings/resources/create/page.tsx +++ b/src/app/[orgId]/settings/resources/create/page.tsx @@ -25,6 +25,7 @@ import { Controller, useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { Input } from "@app/components/ui/input"; import { Button } from "@app/components/ui/button"; +import { Checkbox } from "@app/components/ui/checkbox"; import { useParams, useRouter } from "next/navigation"; import { ListSitesResponse } from "@server/routers/site"; import { formatAxiosError } from "@app/lib/api"; @@ -64,6 +65,7 @@ import CopyTextBox from "@app/components/CopyTextBox"; import Link from "next/link"; import { useTranslations } from "next-intl"; import DomainPicker from "@app/components/DomainPicker"; +import { build } from "@server/build"; const baseResourceFormSchema = z.object({ name: z.string().min(1).max(255), @@ -78,7 +80,8 @@ const httpResourceFormSchema = z.object({ const tcpUdpResourceFormSchema = z.object({ protocol: z.string(), - proxyPort: z.number().int().min(1).max(65535) + proxyPort: z.number().int().min(1).max(65535), + enableProxy: z.boolean().default(false) }); type BaseResourceFormValues = z.infer; @@ -144,7 +147,8 @@ export default function Page() { resolver: zodResolver(tcpUdpResourceFormSchema), defaultValues: { protocol: "tcp", - proxyPort: undefined + proxyPort: undefined, + enableProxy: false } }); @@ -163,16 +167,17 @@ export default function Page() { if (isHttp) { const httpData = httpForm.getValues(); - Object.assign(payload, { - subdomain: httpData.subdomain, - domainId: httpData.domainId, - protocol: "tcp", - }); + Object.assign(payload, { + subdomain: httpData.subdomain, + domainId: httpData.domainId, + protocol: "tcp" + }); } else { const tcpUdpData = tcpUdpForm.getValues(); Object.assign(payload, { protocol: tcpUdpData.protocol, - proxyPort: tcpUdpData.proxyPort + proxyPort: tcpUdpData.proxyPort, + enableProxy: tcpUdpData.enableProxy }); } @@ -198,8 +203,15 @@ export default function Page() { if (isHttp) { router.push(`/${orgId}/settings/resources/${id}`); } else { - setShowSnippets(true); - router.refresh(); + const tcpUdpData = tcpUdpForm.getValues(); + // Only show config snippets if enableProxy is explicitly true + if (tcpUdpData.enableProxy === true) { + setShowSnippets(true); + router.refresh(); + } else { + // If enableProxy is false or undefined, go directly to resource page + router.push(`/${orgId}/settings/resources/${id}`); + } } } } catch (e) { @@ -603,6 +615,46 @@ export default function Page() { )} /> + + {build == "oss" && ( + ( + + + + +
+ + {t( + "resourceEnableProxy" + )} + + + {t( + "resourceEnableProxyDescription" + )} + +
+
+ )} + /> + )}