From c44c1a551877a655e4e296970deb9fb4a89a89be Mon Sep 17 00:00:00 2001 From: Owen Date: Mon, 15 Dec 2025 21:02:04 -0500 Subject: [PATCH] Add UI, update API, send to newt --- messages/en-US.json | 9 +- server/db/pg/schema/schema.ts | 4 +- server/db/sqlite/schema/schema.ts | 4 +- server/lib/ip.ts | 136 ++++++++- .../siteResource/createSiteResource.ts | 14 +- .../siteResource/listAllSiteResourcesByOrg.ts | 2 + .../siteResource/updateSiteResource.ts | 15 +- .../settings/resources/client/page.tsx | 4 +- src/components/ClientResourcesTable.tsx | 2 + .../CreateInternalResourceDialog.tsx | 242 +++++++++++++++ src/components/EditInternalResourceDialog.tsx | 279 ++++++++++++++++++ 11 files changed, 689 insertions(+), 22 deletions(-) diff --git a/messages/en-US.json b/messages/en-US.json index ff316a98..ee26c280 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -2275,5 +2275,12 @@ "agent": "Agent", "personalUseOnly": "Personal Use Only", "loginPageLicenseWatermark": "This instance is licensed for personal use only.", - "instanceIsUnlicensed": "This instance is unlicensed." + "instanceIsUnlicensed": "This instance is unlicensed.", + "portRestrictions": "Port Restrictions", + "allPorts": "All", + "custom": "Custom", + "allPortsAllowed": "All Ports Allowed", + "allPortsBlocked": "All Ports Blocked", + "tcpPortsDescription": "Specify which TCP ports are allowed for this resource. Use '*' for all ports, leave empty to block all, or enter a comma-separated list of ports and ranges (e.g., 80,443,8000-9000).", + "udpPortsDescription": "Specify which UDP ports are allowed for this resource. Use '*' for all ports, leave empty to block all, or enter a comma-separated list of ports and ranges (e.g., 53,123,500-600)." } diff --git a/server/db/pg/schema/schema.ts b/server/db/pg/schema/schema.ts index 71877f2f..34562e7d 100644 --- a/server/db/pg/schema/schema.ts +++ b/server/db/pg/schema/schema.ts @@ -213,7 +213,9 @@ export const siteResources = pgTable("siteResources", { destination: varchar("destination").notNull(), // ip, cidr, hostname; validate against the mode enabled: boolean("enabled").notNull().default(true), alias: varchar("alias"), - aliasAddress: varchar("aliasAddress") + aliasAddress: varchar("aliasAddress"), + tcpPortRangeString: varchar("tcpPortRangeString"), + udpPortRangeString: varchar("udpPortRangeString") }); export const clientSiteResources = pgTable("clientSiteResources", { diff --git a/server/db/sqlite/schema/schema.ts b/server/db/sqlite/schema/schema.ts index 6e17cac4..ab754bc9 100644 --- a/server/db/sqlite/schema/schema.ts +++ b/server/db/sqlite/schema/schema.ts @@ -234,7 +234,9 @@ export const siteResources = sqliteTable("siteResources", { destination: text("destination").notNull(), // ip, cidr, hostname enabled: integer("enabled", { mode: "boolean" }).notNull().default(true), alias: text("alias"), - aliasAddress: text("aliasAddress") + aliasAddress: text("aliasAddress"), + tcpPortRangeString: text("tcpPortRangeString"), + udpPortRangeString: text("udpPortRangeString") }); export const clientSiteResources = sqliteTable("clientSiteResources", { diff --git a/server/lib/ip.ts b/server/lib/ip.ts index 9c412801..2bf3e0e8 100644 --- a/server/lib/ip.ts +++ b/server/lib/ip.ts @@ -1,10 +1,4 @@ -import { - clientSitesAssociationsCache, - db, - SiteResource, - siteResources, - Transaction -} from "@server/db"; +import { db, SiteResource, siteResources, Transaction } from "@server/db"; import { clients, orgs, sites } from "@server/db"; import { and, eq, isNotNull } from "drizzle-orm"; import config from "@server/lib/config"; @@ -476,6 +470,7 @@ export type SubnetProxyTarget = { portRange?: { min: number; max: number; + protocol: "tcp" | "udp"; }[]; }; @@ -505,6 +500,10 @@ export function generateSubnetProxyTargets( } const clientPrefix = `${clientSite.subnet.split("/")[0]}/32`; + const portRange = [ + ...parsePortRangeString(siteResource.tcpPortRangeString, "tcp"), + ...parsePortRangeString(siteResource.udpPortRangeString, "udp") + ]; if (siteResource.mode == "host") { let destination = siteResource.destination; @@ -515,7 +514,8 @@ export function generateSubnetProxyTargets( targets.push({ sourcePrefix: clientPrefix, - destPrefix: destination + destPrefix: destination, + portRange }); } @@ -524,13 +524,15 @@ export function generateSubnetProxyTargets( targets.push({ sourcePrefix: clientPrefix, destPrefix: `${siteResource.aliasAddress}/32`, - rewriteTo: destination + rewriteTo: destination, + portRange }); } } else if (siteResource.mode == "cidr") { targets.push({ sourcePrefix: clientPrefix, - destPrefix: siteResource.destination + destPrefix: siteResource.destination, + portRange }); } } @@ -542,3 +544,117 @@ export function generateSubnetProxyTargets( return targets; } + +// Custom schema for validating port range strings +// Format: "80,443,8000-9000" or "*" for all ports, or empty string +export const portRangeStringSchema = z + .string() + .optional() + .refine( + (val) => { + if (!val || val.trim() === "" || val.trim() === "*") { + return true; + } + + // Split by comma and validate each part + const parts = val.split(",").map((p) => p.trim()); + + for (const part of parts) { + if (part === "") { + return false; // empty parts not allowed + } + + // Check if it's a range (contains dash) + if (part.includes("-")) { + const [start, end] = part.split("-").map((p) => p.trim()); + + // Both parts must be present + if (!start || !end) { + return false; + } + + const startPort = parseInt(start, 10); + const endPort = parseInt(end, 10); + + // Must be valid numbers + if (isNaN(startPort) || isNaN(endPort)) { + return false; + } + + // Must be valid port range (1-65535) + if ( + startPort < 1 || + startPort > 65535 || + endPort < 1 || + endPort > 65535 + ) { + return false; + } + + // Start must be <= end + if (startPort > endPort) { + return false; + } + } else { + // Single port + const port = parseInt(part, 10); + + // Must be a valid number + if (isNaN(port)) { + return false; + } + + // Must be valid port range (1-65535) + if (port < 1 || port > 65535) { + return false; + } + } + } + + return true; + }, + { + message: + 'Port range must be "*" for all ports, or a comma-separated list of ports and ranges (e.g., "80,443,8000-9000"). Ports must be between 1 and 65535, and ranges must have start <= end.' + } + ); + +/** + * Parses a port range string into an array of port range objects + * @param portRangeStr - Port range string (e.g., "80,443,8000-9000", "*", or "") + * @param protocol - Protocol to use for all ranges (default: "tcp") + * @returns Array of port range objects with min, max, and protocol fields + */ +export function parsePortRangeString( + portRangeStr: string | undefined | null, + protocol: "tcp" | "udp" = "tcp" +): { min: number; max: number; protocol: "tcp" | "udp" }[] { + // Handle undefined or empty string - insert dummy value with port 0 + if (!portRangeStr || portRangeStr.trim() === "") { + return [{ min: 0, max: 0, protocol }]; + } + + // Handle wildcard - return empty array (all ports allowed) + if (portRangeStr.trim() === "*") { + return []; + } + + const result: { min: number; max: number; protocol: "tcp" | "udp" }[] = []; + const parts = portRangeStr.split(",").map((p) => p.trim()); + + for (const part of parts) { + if (part.includes("-")) { + // Range + const [start, end] = part.split("-").map((p) => p.trim()); + const startPort = parseInt(start, 10); + const endPort = parseInt(end, 10); + result.push({ min: startPort, max: endPort, protocol }); + } else { + // Single port + const port = parseInt(part, 10); + result.push({ min: port, max: port, protocol }); + } + } + + return result; +} diff --git a/server/routers/siteResource/createSiteResource.ts b/server/routers/siteResource/createSiteResource.ts index e9ce8e04..370037cb 100644 --- a/server/routers/siteResource/createSiteResource.ts +++ b/server/routers/siteResource/createSiteResource.ts @@ -10,7 +10,7 @@ import { userSiteResources } from "@server/db"; import { getUniqueSiteResourceName } from "@server/db/names"; -import { getNextAvailableAliasAddress } from "@server/lib/ip"; +import { getNextAvailableAliasAddress, portRangeStringSchema } from "@server/lib/ip"; import { rebuildClientAssociationsFromSiteResource } from "@server/lib/rebuildClientAssociations"; import response from "@server/lib/response"; import logger from "@server/logger"; @@ -45,7 +45,9 @@ const createSiteResourceSchema = z .optional(), userIds: z.array(z.string()), roleIds: z.array(z.int()), - clientIds: z.array(z.int()) + clientIds: z.array(z.int()), + tcpPortRangeString: portRangeStringSchema, + udpPortRangeString: portRangeStringSchema }) .strict() .refine( @@ -154,7 +156,9 @@ export async function createSiteResource( alias, userIds, roleIds, - clientIds + clientIds, + tcpPortRangeString, + udpPortRangeString } = parsedBody.data; // Verify the site exists and belongs to the org @@ -239,7 +243,9 @@ export async function createSiteResource( destination, enabled, alias, - aliasAddress + aliasAddress, + tcpPortRangeString, + udpPortRangeString }) .returning(); diff --git a/server/routers/siteResource/listAllSiteResourcesByOrg.ts b/server/routers/siteResource/listAllSiteResourcesByOrg.ts index f6975cd2..4fc96533 100644 --- a/server/routers/siteResource/listAllSiteResourcesByOrg.ts +++ b/server/routers/siteResource/listAllSiteResourcesByOrg.ts @@ -97,6 +97,8 @@ export async function listAllSiteResourcesByOrg( destination: siteResources.destination, enabled: siteResources.enabled, alias: siteResources.alias, + tcpPortRangeString: siteResources.tcpPortRangeString, + udpPortRangeString: siteResources.udpPortRangeString, siteName: sites.name, siteNiceId: sites.niceId, siteAddress: sites.address diff --git a/server/routers/siteResource/updateSiteResource.ts b/server/routers/siteResource/updateSiteResource.ts index 92704adb..8ecd8bf0 100644 --- a/server/routers/siteResource/updateSiteResource.ts +++ b/server/routers/siteResource/updateSiteResource.ts @@ -23,7 +23,8 @@ import { updatePeerData, updateTargets } from "@server/routers/client/targets"; import { generateAliasConfig, generateRemoteSubnets, - generateSubnetProxyTargets + generateSubnetProxyTargets, + portRangeStringSchema } from "@server/lib/ip"; import { getClientSiteResourceAccess, @@ -55,7 +56,9 @@ const updateSiteResourceSchema = z .nullish(), userIds: z.array(z.string()), roleIds: z.array(z.int()), - clientIds: z.array(z.int()) + clientIds: z.array(z.int()), + tcpPortRangeString: portRangeStringSchema, + udpPortRangeString: portRangeStringSchema }) .strict() .refine( @@ -160,7 +163,9 @@ export async function updateSiteResource( enabled, userIds, roleIds, - clientIds + clientIds, + tcpPortRangeString, + udpPortRangeString } = parsedBody.data; const [site] = await db @@ -226,7 +231,9 @@ export async function updateSiteResource( mode: mode, destination: destination, enabled: enabled, - alias: alias && alias.trim() ? alias : null + alias: alias && alias.trim() ? alias : null, + tcpPortRangeString: tcpPortRangeString, + udpPortRangeString: udpPortRangeString }) .where( and( diff --git a/src/app/[orgId]/settings/resources/client/page.tsx b/src/app/[orgId]/settings/resources/client/page.tsx index 49ccb97f..1628e689 100644 --- a/src/app/[orgId]/settings/resources/client/page.tsx +++ b/src/app/[orgId]/settings/resources/client/page.tsx @@ -67,7 +67,9 @@ export default async function ClientResourcesPage( // destinationPort: siteResource.destinationPort, alias: siteResource.alias || null, siteNiceId: siteResource.siteNiceId, - niceId: siteResource.niceId + niceId: siteResource.niceId, + tcpPortRangeString: siteResource.tcpPortRangeString || null, + udpPortRangeString: siteResource.udpPortRangeString || null }; } ); diff --git a/src/components/ClientResourcesTable.tsx b/src/components/ClientResourcesTable.tsx index 3f65c762..8ca1cc21 100644 --- a/src/components/ClientResourcesTable.tsx +++ b/src/components/ClientResourcesTable.tsx @@ -41,6 +41,8 @@ export type InternalResourceRow = { // destinationPort: number | null; alias: string | null; niceId: string; + tcpPortRangeString: string | null; + udpPortRangeString: string | null; }; type ClientResourcesTableProps = { diff --git a/src/components/CreateInternalResourceDialog.tsx b/src/components/CreateInternalResourceDialog.tsx index 91ef26da..6aad4123 100644 --- a/src/components/CreateInternalResourceDialog.tsx +++ b/src/components/CreateInternalResourceDialog.tsx @@ -59,6 +59,82 @@ import { useTranslations } from "next-intl"; import { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; import { z } from "zod"; +import { InfoPopup } from "@app/components/ui/info-popup"; + +// Helper to validate port range string format +const isValidPortRangeString = (val: string | undefined | null): boolean => { + if (!val || val.trim() === "" || val.trim() === "*") { + return true; + } + + const parts = val.split(",").map((p) => p.trim()); + + for (const part of parts) { + if (part === "") { + return false; + } + + if (part.includes("-")) { + const [start, end] = part.split("-").map((p) => p.trim()); + if (!start || !end) { + return false; + } + + const startPort = parseInt(start, 10); + const endPort = parseInt(end, 10); + + if (isNaN(startPort) || isNaN(endPort)) { + return false; + } + + if (startPort < 1 || startPort > 65535 || endPort < 1 || endPort > 65535) { + return false; + } + + if (startPort > endPort) { + return false; + } + } else { + const port = parseInt(part, 10); + if (isNaN(port)) { + return false; + } + if (port < 1 || port > 65535) { + return false; + } + } + } + + return true; +}; + +// Port range string schema for client-side validation +const portRangeStringSchema = z + .string() + .optional() + .nullable() + .refine( + (val) => isValidPortRangeString(val), + { + message: + 'Port range must be "*" for all ports, or a comma-separated list of ports and ranges (e.g., "80,443,8000-9000"). Ports must be between 1 and 65535.' + } + ); + +// Helper to determine the port mode from a port range string +type PortMode = "all" | "blocked" | "custom"; +const getPortModeFromString = (val: string | undefined | null): PortMode => { + if (val === "*") return "all"; + if (!val || val.trim() === "") return "blocked"; + return "custom"; +}; + +// Helper to get the port string for API from mode and custom value +const getPortStringFromMode = (mode: PortMode, customValue: string): string | undefined => { + if (mode === "all") return "*"; + if (mode === "blocked") return ""; + return customValue; +}; type Site = ListSitesResponse["sites"][0]; @@ -103,6 +179,8 @@ export default function CreateInternalResourceDialog({ // .max(65535, t("createInternalResourceDialogDestinationPortMax")) // .nullish(), alias: z.string().nullish(), + tcpPortRangeString: portRangeStringSchema, + udpPortRangeString: portRangeStringSchema, roles: z .array( z.object({ @@ -209,6 +287,12 @@ export default function CreateInternalResourceDialog({ number | null >(null); + // Port restriction UI state - default to "all" (*) for new resources + const [tcpPortMode, setTcpPortMode] = useState("all"); + const [udpPortMode, setUdpPortMode] = useState("all"); + const [tcpCustomPorts, setTcpCustomPorts] = useState(""); + const [udpCustomPorts, setUdpCustomPorts] = useState(""); + const availableSites = sites.filter( (site) => site.type === "newt" && site.subnet ); @@ -224,6 +308,8 @@ export default function CreateInternalResourceDialog({ destination: "", // destinationPort: undefined, alias: "", + tcpPortRangeString: "*", + udpPortRangeString: "*", roles: [], users: [], clients: [] @@ -232,6 +318,17 @@ export default function CreateInternalResourceDialog({ const mode = form.watch("mode"); + // Update form values when port mode or custom ports change + useEffect(() => { + const tcpValue = getPortStringFromMode(tcpPortMode, tcpCustomPorts); + form.setValue("tcpPortRangeString", tcpValue); + }, [tcpPortMode, tcpCustomPorts, form]); + + useEffect(() => { + const udpValue = getPortStringFromMode(udpPortMode, udpCustomPorts); + form.setValue("udpPortRangeString", udpValue); + }, [udpPortMode, udpCustomPorts, form]); + // Helper function to check if destination contains letters (hostname vs IP) const isHostname = (destination: string): boolean => { return /[a-zA-Z]/.test(destination); @@ -258,10 +355,17 @@ export default function CreateInternalResourceDialog({ destination: "", // destinationPort: undefined, alias: "", + tcpPortRangeString: "*", + udpPortRangeString: "*", roles: [], users: [], clients: [] }); + // Reset port mode state + setTcpPortMode("all"); + setUdpPortMode("all"); + setTcpCustomPorts(""); + setUdpCustomPorts(""); } }, [open]); @@ -304,6 +408,8 @@ export default function CreateInternalResourceDialog({ data.alias.trim() ? data.alias : undefined, + tcpPortRangeString: data.tcpPortRangeString, + udpPortRangeString: data.udpPortRangeString, roleIds: data.roles ? data.roles.map((r) => parseInt(r.id)) : [], @@ -727,6 +833,142 @@ export default function CreateInternalResourceDialog({ )} + {/* Port Restrictions Section */} +
+

+ {t("portRestrictions")} +

+
+ {/* TCP Ports */} + ( + +
+ + TCP + + +
+
+ + {tcpPortMode === "custom" ? ( + + + setTcpCustomPorts(e.target.value) + } + className="flex-1" + /> + + ) : ( + + )} +
+ +
+ )} + /> + + {/* UDP Ports */} + ( + +
+ + UDP + + +
+
+ + {udpPortMode === "custom" ? ( + + + setUdpCustomPorts(e.target.value) + } + className="flex-1" + /> + + ) : ( + + )} +
+ +
+ )} + /> +
+
+ {/* Access Control Section */}

diff --git a/src/components/EditInternalResourceDialog.tsx b/src/components/EditInternalResourceDialog.tsx index f793d147..cfa60463 100644 --- a/src/components/EditInternalResourceDialog.tsx +++ b/src/components/EditInternalResourceDialog.tsx @@ -47,6 +47,82 @@ import { AxiosResponse } from "axios"; import { UserType } from "@server/types/UserTypes"; import { useQueries, useQuery, useQueryClient } from "@tanstack/react-query"; import { orgQueries, resourceQueries } from "@app/lib/queries"; +import { InfoPopup } from "@app/components/ui/info-popup"; + +// Helper to validate port range string format +const isValidPortRangeString = (val: string | undefined | null): boolean => { + if (!val || val.trim() === "" || val.trim() === "*") { + return true; + } + + const parts = val.split(",").map((p) => p.trim()); + + for (const part of parts) { + if (part === "") { + return false; + } + + if (part.includes("-")) { + const [start, end] = part.split("-").map((p) => p.trim()); + if (!start || !end) { + return false; + } + + const startPort = parseInt(start, 10); + const endPort = parseInt(end, 10); + + if (isNaN(startPort) || isNaN(endPort)) { + return false; + } + + if (startPort < 1 || startPort > 65535 || endPort < 1 || endPort > 65535) { + return false; + } + + if (startPort > endPort) { + return false; + } + } else { + const port = parseInt(part, 10); + if (isNaN(port)) { + return false; + } + if (port < 1 || port > 65535) { + return false; + } + } + } + + return true; +}; + +// Port range string schema for client-side validation +const portRangeStringSchema = z + .string() + .optional() + .nullable() + .refine( + (val) => isValidPortRangeString(val), + { + message: + 'Port range must be "*" for all ports, or a comma-separated list of ports and ranges (e.g., "80,443,8000-9000"). Ports must be between 1 and 65535.' + } + ); + +// Helper to determine the port mode from a port range string +type PortMode = "all" | "blocked" | "custom"; +const getPortModeFromString = (val: string | undefined | null): PortMode => { + if (val === "*") return "all"; + if (!val || val.trim() === "") return "blocked"; + return "custom"; +}; + +// Helper to get the port string for API from mode and custom value +const getPortStringFromMode = (mode: PortMode, customValue: string): string | undefined => { + if (mode === "all") return "*"; + if (mode === "blocked") return ""; + return customValue; +}; type InternalResourceData = { id: number; @@ -61,6 +137,8 @@ type InternalResourceData = { destination: string; // destinationPort?: number | null; alias?: string | null; + tcpPortRangeString?: string | null; + udpPortRangeString?: string | null; }; type EditInternalResourceDialogProps = { @@ -94,6 +172,8 @@ export default function EditInternalResourceDialog({ destination: z.string().min(1), // destinationPort: z.int().positive().min(1, t("editInternalResourceDialogDestinationPortMin")).max(65535, t("editInternalResourceDialogDestinationPortMax")).nullish(), alias: z.string().nullish(), + tcpPortRangeString: portRangeStringSchema, + udpPortRangeString: portRangeStringSchema, roles: z .array( z.object({ @@ -255,6 +335,24 @@ export default function EditInternalResourceDialog({ number | null >(null); + // Port restriction UI state + const [tcpPortMode, setTcpPortMode] = useState( + getPortModeFromString(resource.tcpPortRangeString) + ); + const [udpPortMode, setUdpPortMode] = useState( + getPortModeFromString(resource.udpPortRangeString) + ); + const [tcpCustomPorts, setTcpCustomPorts] = useState( + resource.tcpPortRangeString && resource.tcpPortRangeString !== "*" + ? resource.tcpPortRangeString + : "" + ); + const [udpCustomPorts, setUdpCustomPorts] = useState( + resource.udpPortRangeString && resource.udpPortRangeString !== "*" + ? resource.udpPortRangeString + : "" + ); + const form = useForm({ resolver: zodResolver(formSchema), defaultValues: { @@ -265,6 +363,8 @@ export default function EditInternalResourceDialog({ destination: resource.destination || "", // destinationPort: resource.destinationPort ?? undefined, alias: resource.alias ?? null, + tcpPortRangeString: resource.tcpPortRangeString ?? "*", + udpPortRangeString: resource.udpPortRangeString ?? "*", roles: [], users: [], clients: [] @@ -273,6 +373,17 @@ export default function EditInternalResourceDialog({ const mode = form.watch("mode"); + // Update form values when port mode or custom ports change + useEffect(() => { + const tcpValue = getPortStringFromMode(tcpPortMode, tcpCustomPorts); + form.setValue("tcpPortRangeString", tcpValue); + }, [tcpPortMode, tcpCustomPorts, form]); + + useEffect(() => { + const udpValue = getPortStringFromMode(udpPortMode, udpCustomPorts); + form.setValue("udpPortRangeString", udpValue); + }, [udpPortMode, udpCustomPorts, form]); + // Helper function to check if destination contains letters (hostname vs IP) const isHostname = (destination: string): boolean => { return /[a-zA-Z]/.test(destination); @@ -327,6 +438,8 @@ export default function EditInternalResourceDialog({ data.alias.trim() ? data.alias : null, + tcpPortRangeString: data.tcpPortRangeString, + udpPortRangeString: data.udpPortRangeString, roleIds: (data.roles || []).map((r) => parseInt(r.id)), userIds: (data.users || []).map((u) => u.id), clientIds: (data.clients || []).map((c) => parseInt(c.id)) @@ -396,10 +509,25 @@ export default function EditInternalResourceDialog({ mode: resource.mode || "host", destination: resource.destination || "", alias: resource.alias ?? null, + tcpPortRangeString: resource.tcpPortRangeString ?? "*", + udpPortRangeString: resource.udpPortRangeString ?? "*", roles: [], users: [], clients: [] }); + // Reset port mode state + setTcpPortMode(getPortModeFromString(resource.tcpPortRangeString)); + setUdpPortMode(getPortModeFromString(resource.udpPortRangeString)); + setTcpCustomPorts( + resource.tcpPortRangeString && resource.tcpPortRangeString !== "*" + ? resource.tcpPortRangeString + : "" + ); + setUdpCustomPorts( + resource.udpPortRangeString && resource.udpPortRangeString !== "*" + ? resource.udpPortRangeString + : "" + ); previousResourceId.current = resource.id; } @@ -438,10 +566,25 @@ export default function EditInternalResourceDialog({ destination: resource.destination || "", // destinationPort: resource.destinationPort ?? undefined, alias: resource.alias ?? null, + tcpPortRangeString: resource.tcpPortRangeString ?? "*", + udpPortRangeString: resource.udpPortRangeString ?? "*", roles: [], users: [], clients: [] }); + // Reset port mode state + setTcpPortMode(getPortModeFromString(resource.tcpPortRangeString)); + setUdpPortMode(getPortModeFromString(resource.udpPortRangeString)); + setTcpCustomPorts( + resource.tcpPortRangeString && resource.tcpPortRangeString !== "*" + ? resource.tcpPortRangeString + : "" + ); + setUdpCustomPorts( + resource.udpPortRangeString && resource.udpPortRangeString !== "*" + ? resource.udpPortRangeString + : "" + ); // Reset previous resource ID to ensure clean state on next open previousResourceId.current = null; } @@ -674,6 +817,142 @@ export default function EditInternalResourceDialog({

)} + {/* Port Restrictions Section */} +
+

+ {t("portRestrictions")} +

+
+ {/* TCP Ports */} + ( + +
+ + TCP + + +
+
+ + {tcpPortMode === "custom" ? ( + + + setTcpCustomPorts(e.target.value) + } + className="flex-1" + /> + + ) : ( + + )} +
+ +
+ )} + /> + + {/* UDP Ports */} + ( + +
+ + UDP + + +
+
+ + {udpPortMode === "custom" ? ( + + + setUdpCustomPorts(e.target.value) + } + className="flex-1" + /> + + ) : ( + + )} +
+ +
+ )} + /> +
+
+ {/* Access Control Section */}