diff --git a/messages/en-US.json b/messages/en-US.json index f531c7f9..b0035f81 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -1545,6 +1545,17 @@ "editInternalResourceDialogInvalidIPAddressFormat": "Invalid IP address format", "editInternalResourceDialogDestinationPortMin": "Destination port must be at least 1", "editInternalResourceDialogDestinationPortMax": "Destination port must be less than 65536", + "editInternalResourceDialogPortModeRequired": "Protocol, proxy port, and destination port are required for port mode", + "editInternalResourceDialogMode": "Mode", + "editInternalResourceDialogModePort": "Port", + "editInternalResourceDialogModeHost": "Host", + "editInternalResourceDialogModeCidr": "CIDR", + "editInternalResourceDialogDestination": "Destination", + "editInternalResourceDialogDestinationHostDescription": "The IP address or hostname of the resource on the site's network.", + "editInternalResourceDialogDestinationIPDescription": "The IP or hostname address of the resource on the site's network.", + "editInternalResourceDialogDestinationCidrDescription": "The CIDR range of the resource on the site's network.", + "editInternalResourceDialogAlias": "Alias", + "editInternalResourceDialogAliasDescription": "An optional alias for this resource.", "createInternalResourceDialogNoSitesAvailable": "No Sites Available", "createInternalResourceDialogNoSitesAvailableDescription": "You need to have at least one Newt site with a subnet configured to create internal resources.", "createInternalResourceDialogClose": "Close", @@ -1578,6 +1589,16 @@ "createInternalResourceDialogInvalidIPAddressFormat": "Invalid IP address format", "createInternalResourceDialogDestinationPortMin": "Destination port must be at least 1", "createInternalResourceDialogDestinationPortMax": "Destination port must be less than 65536", + "createInternalResourceDialogPortModeRequired": "Protocol, proxy port, and destination port are required for port mode", + "createInternalResourceDialogMode": "Mode", + "createInternalResourceDialogModePort": "Port", + "createInternalResourceDialogModeHost": "Host", + "createInternalResourceDialogModeCidr": "CIDR", + "createInternalResourceDialogDestination": "Destination", + "createInternalResourceDialogDestinationHostDescription": "The IP address or hostname of the resource on the site's network.", + "createInternalResourceDialogDestinationCidrDescription": "The CIDR range of the resource on the site's network.", + "createInternalResourceDialogAlias": "Alias", + "createInternalResourceDialogAliasDescription": "An optional alias for this resource.", "siteConfiguration": "Configuration", "siteAcceptClientConnections": "Accept Client Connections", "siteAcceptClientConnectionsDescription": "Allow other devices to connect through this Newt instance as a gateway using clients.", diff --git a/server/db/pg/schema/schema.ts b/server/db/pg/schema/schema.ts index 7861f39b..a0333540 100644 --- a/server/db/pg/schema/schema.ts +++ b/server/db/pg/schema/schema.ts @@ -204,11 +204,13 @@ export const siteResources = pgTable("siteResources", { .references(() => orgs.orgId, { onDelete: "cascade" }), niceId: varchar("niceId").notNull(), name: varchar("name").notNull(), - protocol: varchar("protocol").notNull(), - proxyPort: integer("proxyPort").notNull(), - destinationPort: integer("destinationPort").notNull(), - destinationIp: varchar("destinationIp").notNull(), - enabled: boolean("enabled").notNull().default(true) + mode: varchar("mode").notNull(), // "host" | "cidr" | "port" + protocol: varchar("protocol"), // only for port mode + proxyPort: integer("proxyPort"), // only for port mode + destinationPort: integer("destinationPort"), // only for port mode + destination: varchar("destination").notNull(), // ip, cidr, hostname; validate against the mode + enabled: boolean("enabled").notNull().default(true), + alias: varchar("alias") }); export const roleSiteResources = pgTable("roleSiteResources", { diff --git a/server/db/sqlite/schema/schema.ts b/server/db/sqlite/schema/schema.ts index 495cf3dc..59ad76db 100644 --- a/server/db/sqlite/schema/schema.ts +++ b/server/db/sqlite/schema/schema.ts @@ -225,11 +225,13 @@ export const siteResources = sqliteTable("siteResources", { .references(() => orgs.orgId, { onDelete: "cascade" }), niceId: text("niceId").notNull(), name: text("name").notNull(), - protocol: text("protocol").notNull(), - proxyPort: integer("proxyPort").notNull(), - destinationPort: integer("destinationPort").notNull(), - destinationIp: text("destinationIp").notNull(), - enabled: integer("enabled", { mode: "boolean" }).notNull().default(true) + mode: text("mode").notNull(), // "host" | "cidr" | "port" + protocol: text("protocol"), // only for port mode + proxyPort: integer("proxyPort"), // only for port mode + destinationPort: integer("destinationPort"), // only for port mode + destination: text("destination").notNull(), // ip, cidr, hostname + enabled: integer("enabled", { mode: "boolean" }).notNull().default(true), + alias: text("alias") }); export const roleSiteResources = sqliteTable("roleSiteResources", { diff --git a/server/lib/blueprints/applyBlueprint.ts b/server/lib/blueprints/applyBlueprint.ts index 697e8093..e2116378 100644 --- a/server/lib/blueprints/applyBlueprint.ts +++ b/server/lib/blueprints/applyBlueprint.ts @@ -122,14 +122,14 @@ export async function applyBlueprint({ ) .limit(1); - if (site) { + if (site && result.resource.mode === "port" && result.resource.protocol && result.resource.proxyPort && result.resource.destinationPort) { logger.debug( `Updating client resource ${result.resource.siteResourceId} on site ${site.sites.siteId}` ); await addClientTargets( site.newt.newtId, - result.resource.destinationIp, + result.resource.destination, result.resource.destinationPort, result.resource.protocol, result.resource.proxyPort diff --git a/server/lib/blueprints/clientResources.ts b/server/lib/blueprints/clientResources.ts index 59bbc346..7b92ba21 100644 --- a/server/lib/blueprints/clientResources.ts +++ b/server/lib/blueprints/clientResources.ts @@ -75,8 +75,9 @@ export async function updateClientResources( .set({ name: resourceData.name || resourceNiceId, siteId: site.siteId, + mode: "port", proxyPort: resourceData["proxy-port"]!, - destinationIp: resourceData.hostname, + destination: resourceData.hostname, destinationPort: resourceData["internal-port"], protocol: resourceData.protocol }) @@ -98,8 +99,9 @@ export async function updateClientResources( siteId: site.siteId, niceId: resourceNiceId, name: resourceData.name || resourceNiceId, + mode: "port", proxyPort: resourceData["proxy-port"]!, - destinationIp: resourceData.hostname, + destination: resourceData.hostname, destinationPort: resourceData["internal-port"], protocol: resourceData.protocol }) diff --git a/server/middlewares/verifyOrgAccess.ts b/server/middlewares/verifyOrgAccess.ts index 441dd126..3acdad6c 100644 --- a/server/middlewares/verifyOrgAccess.ts +++ b/server/middlewares/verifyOrgAccess.ts @@ -53,8 +53,6 @@ export async function verifyOrgAccess( session: req.session }); - logger.debug("Org check policy result", { policyCheck }); - if (!policyCheck.allowed || policyCheck.error) { return next( createHttpError( diff --git a/server/routers/newt/handleGetConfigMessage.ts b/server/routers/newt/handleGetConfigMessage.ts index 3eba94b9..6766bea1 100644 --- a/server/routers/newt/handleGetConfigMessage.ts +++ b/server/routers/newt/handleGetConfigMessage.ts @@ -216,13 +216,18 @@ export const handleGetConfigMessage: MessageHandler = async (context) => { const { tcpTargets, udpTargets } = allSiteResources.reduce( (acc, resource) => { + // Only process port mode resources + if (resource.mode !== "port") { + return acc; + } + // Filter out invalid targets - if (!resource.proxyPort || !resource.destinationIp || !resource.destinationPort) { + if (!resource.proxyPort || !resource.destination || !resource.destinationPort || !resource.protocol) { return acc; } // Format target into string - const formattedTarget = `${resource.proxyPort}:${resource.destinationIp}:${resource.destinationPort}`; + const formattedTarget = `${resource.proxyPort}:${resource.destination}:${resource.destinationPort}`; // Add to the appropriate protocol array if (resource.protocol === "tcp") { diff --git a/server/routers/siteResource/createSiteResource.ts b/server/routers/siteResource/createSiteResource.ts index 48682ae3..e7f8bd75 100644 --- a/server/routers/siteResource/createSiteResource.ts +++ b/server/routers/siteResource/createSiteResource.ts @@ -22,13 +22,30 @@ const createSiteResourceParamsSchema = z const createSiteResourceSchema = z .object({ name: z.string().min(1).max(255), - protocol: z.enum(["tcp", "udp"]), - proxyPort: z.number().int().positive(), - destinationPort: z.number().int().positive(), - destinationIp: z.string(), - enabled: z.boolean().default(true) - }) - .strict(); + mode: z.enum(["host", "cidr", "port"]), + protocol: z.enum(["tcp", "udp"]).optional(), + proxyPort: z.number().int().positive().optional(), + destinationPort: z.number().int().positive().optional(), + destination: z.string().min(1), + enabled: z.boolean().default(true), + alias: z.string().optional() + }).strict() + .refine( + (data) => { + if (data.mode === "port") { + return ( + data.protocol !== undefined && + data.proxyPort !== undefined && + data.destinationPort !== undefined + ); + } + return true; + }, + { + message: + "Protocol, proxy port, and destination port are required for port mode" + } + ); export type CreateSiteResourceBody = z.infer; export type CreateSiteResourceResponse = SiteResource; @@ -82,11 +99,13 @@ export async function createSiteResource( const { siteId, orgId } = parsedParams.data; const { name, + mode, protocol, proxyPort, destinationPort, - destinationIp, - enabled + destination, + enabled, + alias } = parsedBody.data; // Verify the site exists and belongs to the org @@ -100,26 +119,28 @@ export async function createSiteResource( return next(createHttpError(HttpCode.NOT_FOUND, "Site not found")); } - // check if resource with same protocol and proxy port already exists - const [existingResource] = await db - .select() - .from(siteResources) - .where( - and( - eq(siteResources.siteId, siteId), - eq(siteResources.orgId, orgId), - eq(siteResources.protocol, protocol), - eq(siteResources.proxyPort, proxyPort) + // check if resource with same protocol and proxy port already exists (only for port mode) + if (mode === "port" && protocol && proxyPort) { + const [existingResource] = await db + .select() + .from(siteResources) + .where( + and( + eq(siteResources.siteId, siteId), + eq(siteResources.orgId, orgId), + eq(siteResources.protocol, protocol), + eq(siteResources.proxyPort, proxyPort) + ) ) - ) - .limit(1); - if (existingResource && existingResource.siteResourceId) { - return next( - createHttpError( - HttpCode.CONFLICT, - "A resource with the same protocol and proxy port already exists" - ) - ); + .limit(1); + if (existingResource && existingResource.siteResourceId) { + return next( + createHttpError( + HttpCode.CONFLICT, + "A resource with the same protocol and proxy port already exists" + ) + ); + } } const niceId = await getUniqueSiteResourceName(orgId); @@ -132,11 +153,13 @@ export async function createSiteResource( niceId, orgId, name, - protocol, - proxyPort, - destinationPort, - destinationIp, - enabled + mode, + protocol: mode === "port" ? protocol : null, + proxyPort: mode === "port" ? proxyPort : null, + destinationPort: mode === "port" ? destinationPort : null, + destination, + enabled, + alias: alias || null }) .returning(); @@ -157,24 +180,29 @@ export async function createSiteResource( siteResourceId: newSiteResource.siteResourceId }); - const [newt] = await db - .select() - .from(newts) - .where(eq(newts.siteId, site.siteId)) - .limit(1); + // Only add targets for port mode + if (mode === "port" && protocol && proxyPort && destinationPort) { + const [newt] = await db + .select() + .from(newts) + .where(eq(newts.siteId, site.siteId)) + .limit(1); - if (!newt) { - return next(createHttpError(HttpCode.NOT_FOUND, "Newt not found")); + if (!newt) { + return next( + createHttpError(HttpCode.NOT_FOUND, "Newt not found") + ); + } + + await addTargets( + newt.newtId, + destination, + destinationPort, + protocol, + proxyPort + ); } - await addTargets( - newt.newtId, - destinationIp, - destinationPort, - protocol, - proxyPort - ); - logger.info( `Created site resource ${newSiteResource.siteResourceId} for site ${siteId}` ); diff --git a/server/routers/siteResource/deleteSiteResource.ts b/server/routers/siteResource/deleteSiteResource.ts index 347d4b53..b43dcd27 100644 --- a/server/routers/siteResource/deleteSiteResource.ts +++ b/server/routers/siteResource/deleteSiteResource.ts @@ -91,24 +91,27 @@ export async function deleteSiteResource( eq(siteResources.orgId, orgId) )); - const [newt] = await db - .select() - .from(newts) - .where(eq(newts.siteId, site.siteId)) - .limit(1); + // Only remove targets for port mode + if (existingSiteResource.mode === "port" && existingSiteResource.protocol && existingSiteResource.proxyPort && existingSiteResource.destinationPort) { + const [newt] = await db + .select() + .from(newts) + .where(eq(newts.siteId, site.siteId)) + .limit(1); - if (!newt) { - return next(createHttpError(HttpCode.NOT_FOUND, "Newt not found")); + if (!newt) { + return next(createHttpError(HttpCode.NOT_FOUND, "Newt not found")); + } + + await removeTargets( + newt.newtId, + existingSiteResource.destination, + existingSiteResource.destinationPort, + existingSiteResource.protocol, + existingSiteResource.proxyPort + ); } - await removeTargets( - newt.newtId, - existingSiteResource.destinationIp, - existingSiteResource.destinationPort, - existingSiteResource.protocol, - existingSiteResource.proxyPort - ); - logger.info(`Deleted site resource ${siteResourceId} for site ${siteId}`); return response(res, { diff --git a/server/routers/siteResource/listAllSiteResourcesByOrg.ts b/server/routers/siteResource/listAllSiteResourcesByOrg.ts index 948fc2c2..a17d3b0a 100644 --- a/server/routers/siteResource/listAllSiteResourcesByOrg.ts +++ b/server/routers/siteResource/listAllSiteResourcesByOrg.ts @@ -32,7 +32,7 @@ const listAllSiteResourcesByOrgQuerySchema = z.object({ }); export type ListAllSiteResourcesByOrgResponse = { - siteResources: (SiteResource & { siteName: string, siteNiceId: string })[]; + siteResources: (SiteResource & { siteName: string, siteNiceId: string, siteAddress: string | null })[]; }; registry.registerPath({ @@ -82,14 +82,18 @@ export async function listAllSiteResourcesByOrg( siteResourceId: siteResources.siteResourceId, siteId: siteResources.siteId, orgId: siteResources.orgId, + niceId: siteResources.niceId, name: siteResources.name, + mode: siteResources.mode, protocol: siteResources.protocol, proxyPort: siteResources.proxyPort, destinationPort: siteResources.destinationPort, - destinationIp: siteResources.destinationIp, + destination: siteResources.destination, enabled: siteResources.enabled, + alias: siteResources.alias, siteName: sites.name, - siteNiceId: sites.niceId + siteNiceId: sites.niceId, + siteAddress: sites.address }) .from(siteResources) .innerJoin(sites, eq(siteResources.siteId, sites.siteId)) diff --git a/server/routers/siteResource/updateSiteResource.ts b/server/routers/siteResource/updateSiteResource.ts index f6f71124..11ed35c7 100644 --- a/server/routers/siteResource/updateSiteResource.ts +++ b/server/routers/siteResource/updateSiteResource.ts @@ -25,11 +25,13 @@ const updateSiteResourceParamsSchema = z const updateSiteResourceSchema = z .object({ name: z.string().min(1).max(255).optional(), - protocol: z.enum(["tcp", "udp"]).optional(), - proxyPort: z.number().int().positive().optional(), - destinationPort: z.number().int().positive().optional(), - destinationIp: z.string().optional(), - enabled: z.boolean().optional() + mode: z.enum(["host", "cidr", "port"]).optional(), + protocol: z.enum(["tcp", "udp"]).nullish(), + proxyPort: z.number().int().positive().nullish(), + destinationPort: z.number().int().positive().nullish(), + destination: z.string().min(1).optional(), + enabled: z.boolean().optional(), + alias: z.string().nullish() }) .strict(); @@ -114,39 +116,77 @@ export async function updateSiteResource( ); } - const protocol = updateData.protocol || existingSiteResource.protocol; - const proxyPort = - updateData.proxyPort || existingSiteResource.proxyPort; + // Determine the final mode and validate port mode requirements + const finalMode = updateData.mode || existingSiteResource.mode; + const finalProtocol = updateData.protocol !== undefined ? updateData.protocol : existingSiteResource.protocol; + const finalProxyPort = updateData.proxyPort !== undefined ? updateData.proxyPort : existingSiteResource.proxyPort; + const finalDestinationPort = updateData.destinationPort !== undefined ? updateData.destinationPort : existingSiteResource.destinationPort; - // check if resource with same protocol and proxy port already exists - const [existingResource] = await db - .select() - .from(siteResources) - .where( - and( - eq(siteResources.siteId, siteId), - eq(siteResources.orgId, orgId), - eq(siteResources.protocol, protocol), - eq(siteResources.proxyPort, proxyPort) + if (finalMode === "port") { + if (!finalProtocol || !finalProxyPort || !finalDestinationPort) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "Protocol, proxy port, and destination port are required for port mode" + ) + ); + } + + // check if resource with same protocol and proxy port already exists + const [existingResource] = await db + .select() + .from(siteResources) + .where( + and( + eq(siteResources.siteId, siteId), + eq(siteResources.orgId, orgId), + eq(siteResources.protocol, finalProtocol), + eq(siteResources.proxyPort, finalProxyPort) + ) ) - ) - .limit(1); - if ( - existingResource && - existingResource.siteResourceId !== siteResourceId - ) { - return next( - createHttpError( - HttpCode.CONFLICT, - "A resource with the same protocol and proxy port already exists" - ) - ); + .limit(1); + if ( + existingResource && + existingResource.siteResourceId !== siteResourceId + ) { + return next( + createHttpError( + HttpCode.CONFLICT, + "A resource with the same protocol and proxy port already exists" + ) + ); + } + } + + // Prepare update data + const updateValues: any = {}; + if (updateData.name !== undefined) updateValues.name = updateData.name; + if (updateData.mode !== undefined) updateValues.mode = updateData.mode; + if (updateData.destination !== undefined) updateValues.destination = updateData.destination; + if (updateData.enabled !== undefined) updateValues.enabled = updateData.enabled; + + // Handle nullish fields (can be undefined, null, or a value) + if (updateData.alias !== undefined) { + updateValues.alias = updateData.alias && updateData.alias.trim() ? updateData.alias : null; + } + + // Handle port mode fields - include in update if explicitly provided (null or value) or if mode changed + const isModeChangingFromPort = existingSiteResource.mode === "port" && updateData.mode && updateData.mode !== "port"; + + if (updateData.protocol !== undefined || isModeChangingFromPort) { + updateValues.protocol = finalMode === "port" ? finalProtocol : null; + } + if (updateData.proxyPort !== undefined || isModeChangingFromPort) { + updateValues.proxyPort = finalMode === "port" ? finalProxyPort : null; + } + if (updateData.destinationPort !== undefined || isModeChangingFromPort) { + updateValues.destinationPort = finalMode === "port" ? finalDestinationPort : null; } // Update the site resource const [updatedSiteResource] = await db .update(siteResources) - .set(updateData) + .set(updateValues) .where( and( eq(siteResources.siteResourceId, siteResourceId), @@ -156,24 +196,27 @@ export async function updateSiteResource( ) .returning(); - const [newt] = await db - .select() - .from(newts) - .where(eq(newts.siteId, site.siteId)) - .limit(1); + // Only add targets for port mode + if (updatedSiteResource.mode === "port" && updatedSiteResource.protocol && updatedSiteResource.proxyPort && updatedSiteResource.destinationPort) { + const [newt] = await db + .select() + .from(newts) + .where(eq(newts.siteId, site.siteId)) + .limit(1); - if (!newt) { - return next(createHttpError(HttpCode.NOT_FOUND, "Newt not found")); + if (!newt) { + return next(createHttpError(HttpCode.NOT_FOUND, "Newt not found")); + } + + await addTargets( + newt.newtId, + updatedSiteResource.destination, + updatedSiteResource.destinationPort, + updatedSiteResource.protocol, + updatedSiteResource.proxyPort + ); } - await addTargets( - newt.newtId, - updatedSiteResource.destinationIp, - updatedSiteResource.destinationPort, - updatedSiteResource.protocol, - updatedSiteResource.proxyPort - ); - logger.info( `Updated site resource ${siteResourceId} for site ${siteId}` ); diff --git a/src/app/[orgId]/settings/resources/page.tsx b/src/app/[orgId]/settings/resources/page.tsx index eadb19d4..f9a691c7 100644 --- a/src/app/[orgId]/settings/resources/page.tsx +++ b/src/app/[orgId]/settings/resources/page.tsx @@ -103,11 +103,14 @@ export default async function ResourcesPage(props: ResourcesPageProps) { name: siteResource.name, orgId: params.orgId, siteName: siteResource.siteName, + siteAddress: siteResource.siteAddress || null, + mode: siteResource.mode || "port" as any, protocol: siteResource.protocol, proxyPort: siteResource.proxyPort, siteId: siteResource.siteId, - destinationIp: siteResource.destinationIp, + destination: siteResource.destination, destinationPort: siteResource.destinationPort, + alias: siteResource.alias || null, siteNiceId: siteResource.siteNiceId }; } diff --git a/src/components/CreateInternalResourceDialog.tsx b/src/components/CreateInternalResourceDialog.tsx index a398f8f5..d90f155f 100644 --- a/src/components/CreateInternalResourceDialog.tsx +++ b/src/components/CreateInternalResourceDialog.tsx @@ -86,20 +86,24 @@ export default function CreateInternalResourceDialog({ .min(1, t("createInternalResourceDialogNameRequired")) .max(255, t("createInternalResourceDialogNameMaxLength")), siteId: z.number().int().positive(t("createInternalResourceDialogPleaseSelectSite")), - protocol: z.enum(["tcp", "udp"]), + mode: z.enum(["host", "cidr", "port"]), + protocol: z.enum(["tcp", "udp"]).nullish(), proxyPort: z .number() .int() .positive() .min(1, t("createInternalResourceDialogProxyPortMin")) - .max(65535, t("createInternalResourceDialogProxyPortMax")), - destinationIp: z.string(), + .max(65535, t("createInternalResourceDialogProxyPortMax")) + .nullish(), + destination: z.string().min(1), destinationPort: z .number() .int() .positive() .min(1, t("createInternalResourceDialogDestinationPortMin")) - .max(65535, t("createInternalResourceDialogDestinationPortMax")), + .max(65535, t("createInternalResourceDialogDestinationPortMax")) + .nullish(), + alias: z.string().nullish(), roles: z.array( z.object({ id: z.string(), @@ -112,8 +116,44 @@ export default function CreateInternalResourceDialog({ text: z.string() }) ).optional() - }); - + }) + .refine( + (data) => { + if (data.mode === "port") { + return data.protocol !== undefined && data.protocol !== null; + } + return true; + }, + { + message: t("createInternalResourceDialogProtocol") + " is required for port mode", + path: ["protocol"] + } + ) + .refine( + (data) => { + if (data.mode === "port") { + return data.proxyPort !== undefined && data.proxyPort !== null; + } + return true; + }, + { + message: t("createInternalResourceDialogSitePort") + " is required for port mode", + path: ["proxyPort"] + } + ) + .refine( + (data) => { + if (data.mode === "port") { + return data.destinationPort !== undefined && data.destinationPort !== null; + } + return true; + }, + { + message: t("targetPort") + " is required for port mode", + path: ["destinationPort"] + } + ); + type FormData = z.infer; const [allRoles, setAllRoles] = useState<{ id: string; text: string }[]>([]); @@ -130,24 +170,30 @@ export default function CreateInternalResourceDialog({ defaultValues: { name: "", siteId: availableSites[0]?.siteId || 0, + mode: "host", protocol: "tcp", proxyPort: undefined, - destinationIp: "", + destination: "", destinationPort: undefined, + alias: "", roles: [], users: [] } }); + const mode = form.watch("mode"); + useEffect(() => { if (open && availableSites.length > 0) { form.reset({ name: "", siteId: availableSites[0].siteId, + mode: "host", protocol: "tcp", proxyPort: undefined, - destinationIp: "", + destination: "", destinationPort: undefined, + alias: "", roles: [], users: [] }); @@ -194,11 +240,13 @@ export default function CreateInternalResourceDialog({ `/org/${orgId}/site/${data.siteId}/resource`, { name: data.name, - protocol: data.protocol, - proxyPort: data.proxyPort, - destinationIp: data.destinationIp, - destinationPort: data.destinationPort, - enabled: true + mode: data.mode, + protocol: data.mode === "port" ? data.protocol : undefined, + proxyPort: data.mode === "port" ? data.proxyPort : undefined, + destinationPort: data.mode === "port" ? data.destinationPort : undefined, + destination: data.destination, + enabled: true, + alias: data.alias && typeof data.alias === "string" && data.alias.trim() ? data.alias : undefined } ); @@ -294,126 +342,151 @@ export default function CreateInternalResourceDialog({ )} /> -
- ( - - {t("createInternalResourceDialogSite")} - - - - - - - - - - - {t("createInternalResourceDialogNoSitesFound")} - - {availableSites.map((site) => ( - { - field.onChange(site.siteId); - }} - > - - {site.name} - - ))} - - - - - - - - )} - /> - - ( - - - {t("createInternalResourceDialogProtocol")} - - - - - )} - /> -
- ( - - {t("createInternalResourceDialogSitePort")} - - - field.onChange( - e.target.value === "" ? undefined : parseInt(e.target.value) - ) - } - /> - - - {t("createInternalResourceDialogSitePortDescription")} - + + {t("createInternalResourceDialogSite")} + + + + + + + + + + + {t("createInternalResourceDialogNoSitesFound")} + + {availableSites.map((site) => ( + { + field.onChange(site.siteId); + }} + > + + {site.name} + + ))} + + + + + )} /> + + ( + + {t("createInternalResourceDialogMode")} + + + + )} + /> + + {mode === "port" && ( + <> +
+ ( + + + {t("createInternalResourceDialogProtocol")} + + + + + )} + /> + + ( + + {t("createInternalResourceDialogSitePort")} + + + field.onChange( + e.target.value === "" ? undefined : parseInt(e.target.value) + ) + } + /> + + + + )} + /> +
+ + )} @@ -423,28 +496,28 @@ export default function CreateInternalResourceDialog({ {t("createInternalResourceDialogTargetConfiguration")}
-
- ( - - - {t("targetAddr")} - - - - - - {t("createInternalResourceDialogDestinationIPDescription")} - - - - )} - /> + ( + + + {t("createInternalResourceDialogDestination")} + + + + + + {mode === "host" && t("createInternalResourceDialogDestinationHostDescription")} + {mode === "cidr" && t("createInternalResourceDialogDestinationCidrDescription")} + {mode === "port" && t("createInternalResourceDialogDestinationIPDescription")} + + + + )} + /> + {mode === "port" && ( )} /> -
+ )}
+ {/* Alias */} + {mode !== "cidr" && ( +
+ ( + + {t("createInternalResourceDialogAlias")} + + + + + {t("createInternalResourceDialogAliasDescription")} + + + + )} + /> +
+ )} + {/* Access Control Section */} -

{t("resourceUsersRoles")} diff --git a/src/components/EditInternalResourceDialog.tsx b/src/components/EditInternalResourceDialog.tsx index ff038920..8ebc0661 100644 --- a/src/components/EditInternalResourceDialog.tsx +++ b/src/components/EditInternalResourceDialog.tsx @@ -50,11 +50,13 @@ type InternalResourceData = { name: string; orgId: string; siteName: string; - protocol: string; + mode: "host" | "cidr" | "port"; + protocol: string | null; proxyPort: number | null; siteId: number; - destinationIp?: string; - destinationPort?: number; + destination: string; + destinationPort?: number | null; + alias?: string | null; }; type EditInternalResourceDialogProps = { @@ -78,10 +80,12 @@ export default function EditInternalResourceDialog({ const formSchema = z.object({ name: z.string().min(1, t("editInternalResourceDialogNameRequired")).max(255, t("editInternalResourceDialogNameMaxLength")), - protocol: z.enum(["tcp", "udp"]), - proxyPort: z.number().int().positive().min(1, t("editInternalResourceDialogProxyPortMin")).max(65535, t("editInternalResourceDialogProxyPortMax")), - destinationIp: z.string(), - destinationPort: z.number().int().positive().min(1, t("editInternalResourceDialogDestinationPortMin")).max(65535, t("editInternalResourceDialogDestinationPortMax")), + mode: z.enum(["host", "cidr", "port"]), + protocol: z.enum(["tcp", "udp"]).nullish(), + proxyPort: z.number().int().positive().min(1, t("editInternalResourceDialogProxyPortMin")).max(65535, t("editInternalResourceDialogProxyPortMax")).nullish(), + destination: z.string().min(1), + destinationPort: z.number().int().positive().min(1, t("editInternalResourceDialogDestinationPortMin")).max(65535, t("editInternalResourceDialogDestinationPortMax")).nullish(), + alias: z.string().nullish(), roles: z.array( z.object({ id: z.string(), @@ -94,7 +98,43 @@ export default function EditInternalResourceDialog({ text: z.string() }) ).optional() - }); + }) + .refine( + (data) => { + if (data.mode === "port") { + return data.protocol !== undefined && data.protocol !== null; + } + return true; + }, + { + message: t("editInternalResourceDialogProtocol") + " is required for port mode", + path: ["protocol"] + } + ) + .refine( + (data) => { + if (data.mode === "port") { + return data.proxyPort !== undefined && data.proxyPort !== null; + } + return true; + }, + { + message: t("editInternalResourceDialogSitePort") + " is required for port mode", + path: ["proxyPort"] + } + ) + .refine( + (data) => { + if (data.mode === "port") { + return data.destinationPort !== undefined && data.destinationPort !== null; + } + return true; + }, + { + message: t("targetPort") + " is required for port mode", + path: ["destinationPort"] + } + ); type FormData = z.infer; @@ -108,15 +148,19 @@ export default function EditInternalResourceDialog({ resolver: zodResolver(formSchema), defaultValues: { name: resource.name, - protocol: resource.protocol as "tcp" | "udp", - proxyPort: resource.proxyPort || undefined, - destinationIp: resource.destinationIp || "", - destinationPort: resource.destinationPort || undefined, + mode: resource.mode || "host", + protocol: (resource.protocol as "tcp" | "udp" | null | undefined) ?? undefined, + proxyPort: resource.proxyPort ?? undefined, + destination: resource.destination || "", + destinationPort: resource.destinationPort ?? undefined, + alias: resource.alias ?? null, roles: [], users: [] } }); + const mode = form.watch("mode"); + const fetchRolesAndUsers = async () => { setLoadingRolesUsers(true); try { @@ -180,10 +224,12 @@ export default function EditInternalResourceDialog({ if (open) { form.reset({ name: resource.name, - protocol: resource.protocol as "tcp" | "udp", - proxyPort: resource.proxyPort || undefined, - destinationIp: resource.destinationIp || "", - destinationPort: resource.destinationPort || undefined, + mode: resource.mode || "host", + protocol: (resource.protocol as "tcp" | "udp" | null | undefined) ?? undefined, + proxyPort: resource.proxyPort ?? undefined, + destination: resource.destination || "", + destinationPort: resource.destinationPort ?? undefined, + alias: resource.alias ?? null, roles: [], users: [] }); @@ -198,10 +244,12 @@ export default function EditInternalResourceDialog({ // Update the site resource await api.post(`/org/${orgId}/site/${resource.siteId}/resource/${resource.id}`, { name: data.name, - protocol: data.protocol, - proxyPort: data.proxyPort, - destinationIp: data.destinationIp, - destinationPort: data.destinationPort + mode: data.mode, + protocol: data.mode === "port" ? data.protocol : null, + proxyPort: data.mode === "port" ? data.proxyPort : null, + destinationPort: data.mode === "port" ? data.destinationPort : null, + destination: data.destination, + alias: data.alias && typeof data.alias === "string" && data.alias.trim() ? data.alias : null }); // Update roles and users @@ -264,50 +312,78 @@ export default function EditInternalResourceDialog({ )} /> -
- ( - - {t("editInternalResourceDialogProtocol")} - - - - )} - /> - - ( - - {t("editInternalResourceDialogSitePort")} + ( + + {t("editInternalResourceDialogMode")} + field.onChange(parseInt(e.target.value) || 0)} - /> + + + - - - )} - /> -
+ + {t("editInternalResourceDialogModePort")} + {t("editInternalResourceDialogModeHost")} + {t("editInternalResourceDialogModeCidr")} + + + + + )} + /> + + {mode === "port" && ( +
+ ( + + {t("editInternalResourceDialogProtocol")} + + + + )} + /> + + ( + + {t("editInternalResourceDialogSitePort")} + + field.onChange(e.target.value === "" ? undefined : parseInt(e.target.value) || 0)} + /> + + + + )} + /> +
+ )}

@@ -315,21 +391,26 @@ export default function EditInternalResourceDialog({

{t("editInternalResourceDialogTargetConfiguration")}

-
- ( - - {t("targetAddr")} - - - - - - )} - /> + ( + + {t("editInternalResourceDialogDestination")} + + + + + {mode === "host" && t("editInternalResourceDialogDestinationHostDescription")} + {mode === "cidr" && t("editInternalResourceDialogDestinationCidrDescription")} + {mode === "port" && t("editInternalResourceDialogDestinationIPDescription")} + + + + )} + /> + {mode === "port" && ( field.onChange(parseInt(e.target.value) || 0)} + value={field.value || ""} + onChange={(e) => field.onChange(e.target.value === "" ? undefined : parseInt(e.target.value) || 0)} /> )} /> -
+ )}
+ {/* Alias */} + {mode !== "cidr" && ( +
+ ( + + {t("editInternalResourceDialogAlias")} + + + + + {t("editInternalResourceDialogAliasDescription")} + + + + )} + /> +
+ )} + {/* Access Control Section */} -

{t("resourceUsersRoles")} diff --git a/src/components/ResourcesTable.tsx b/src/components/ResourcesTable.tsx index 0cdd0ba9..68aa4f49 100644 --- a/src/components/ResourcesTable.tsx +++ b/src/components/ResourcesTable.tsx @@ -90,12 +90,15 @@ export type InternalResourceRow = { name: string; orgId: string; siteName: string; - protocol: string; + siteAddress: string | null; + mode: "host" | "cidr" | "port"; + protocol: string | null; proxyPort: number | null; siteId: number; siteNiceId: string; - destinationIp: string; - destinationPort: number; + destination: string; + destinationPort: number | null; + alias: string | null; }; type Site = ListSitesResponse["sites"][0]; @@ -571,24 +574,16 @@ export default function ResourcesTable({ } }, { - accessorKey: "protocol", - header: () => ({t("protocol")}), + accessorKey: "mode", + header: () => ({t("editInternalResourceDialogMode")}), cell: ({ row }) => { const resourceRow = row.original; - return {resourceRow.protocol.toUpperCase()}; - } - }, - { - accessorKey: "proxyPort", - header: () => ({t("proxyPort")}), - cell: ({ row }) => { - const resourceRow = row.original; - return ( - - ); + const modeLabels: Record<"host" | "cidr" | "port", string> = { + host: t("editInternalResourceDialogModeHost"), + cidr: t("editInternalResourceDialogModeCidr"), + port: t("editInternalResourceDialogModePort") + }; + return {modeLabels[resourceRow.mode]}; } }, { @@ -596,8 +591,35 @@ export default function ResourcesTable({ header: () => ({t("resourcesTableDestination")}), cell: ({ row }) => { const resourceRow = row.original; - const destination = `${resourceRow.destinationIp}:${resourceRow.destinationPort}`; - return ; + let displayText: string; + let copyText: string; + + if (resourceRow.mode === "port" && resourceRow.protocol && resourceRow.proxyPort && resourceRow.destinationPort) { + const protocol = resourceRow.protocol.toUpperCase(); + // For port mode: site part uses alias or site address, destination part uses destination IP + // If site address has CIDR notation, extract just the IP address + let siteAddress = resourceRow.siteAddress; + if (siteAddress && siteAddress.includes("/")) { + siteAddress = siteAddress.split("/")[0]; + } + const siteDisplay = resourceRow.alias || siteAddress; + displayText = `${protocol} ${siteDisplay}:${resourceRow.proxyPort} -> ${resourceRow.destination}:${resourceRow.destinationPort}`; + copyText = `${siteDisplay}:${resourceRow.proxyPort}`; + } else if (resourceRow.mode === "host") { + // For host mode: use alias if available, otherwise use destination + const destinationDisplay = resourceRow.alias || resourceRow.destination; + displayText = destinationDisplay; + copyText = destinationDisplay; + } else if (resourceRow.mode === "cidr") { + displayText = resourceRow.destination; + copyText = resourceRow.destination; + } else { + const destinationDisplay = resourceRow.alias || resourceRow.destination; + displayText = destinationDisplay; + copyText = destinationDisplay; + } + + return ; } }, diff --git a/src/components/ui/dialog.tsx b/src/components/ui/dialog.tsx index 8870bdb5..fa81756a 100644 --- a/src/components/ui/dialog.tsx +++ b/src/components/ui/dialog.tsx @@ -38,7 +38,7 @@ const DialogContent = React.forwardRef<