mirror of
https://github.com/fosrl/pangolin.git
synced 2026-01-29 06:10:47 +00:00
add site resource modes and alias
This commit is contained in:
@@ -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.",
|
||||
|
||||
@@ -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", {
|
||||
|
||||
@@ -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", {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
})
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -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<typeof createSiteResourceSchema>;
|
||||
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}`
|
||||
);
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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}`
|
||||
);
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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<typeof formSchema>;
|
||||
|
||||
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({
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="siteId"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-col">
|
||||
<FormLabel>{t("createInternalResourceDialogSite")}</FormLabel>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<FormControl>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
className={cn(
|
||||
"w-full justify-between",
|
||||
!field.value && "text-muted-foreground"
|
||||
)}
|
||||
>
|
||||
{field.value
|
||||
? availableSites.find(
|
||||
(site) => site.siteId === field.value
|
||||
)?.name
|
||||
: t("createInternalResourceDialogSelectSite")}
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</FormControl>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-full p-0">
|
||||
<Command>
|
||||
<CommandInput placeholder={t("createInternalResourceDialogSearchSites")} />
|
||||
<CommandList>
|
||||
<CommandEmpty>{t("createInternalResourceDialogNoSitesFound")}</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{availableSites.map((site) => (
|
||||
<CommandItem
|
||||
key={site.siteId}
|
||||
value={site.name}
|
||||
onSelect={() => {
|
||||
field.onChange(site.siteId);
|
||||
}}
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
"mr-2 h-4 w-4",
|
||||
field.value === site.siteId
|
||||
? "opacity-100"
|
||||
: "opacity-0"
|
||||
)}
|
||||
/>
|
||||
{site.name}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="protocol"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t("createInternalResourceDialogProtocol")}
|
||||
</FormLabel>
|
||||
<Select
|
||||
onValueChange={
|
||||
field.onChange
|
||||
}
|
||||
value={field.value}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="tcp">
|
||||
{t("createInternalResourceDialogTcp")}
|
||||
</SelectItem>
|
||||
<SelectItem value="udp">
|
||||
{t("createInternalResourceDialogUdp")}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="proxyPort"
|
||||
name="siteId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("createInternalResourceDialogSitePort")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
value={field.value || ""}
|
||||
onChange={(e) =>
|
||||
field.onChange(
|
||||
e.target.value === "" ? undefined : parseInt(e.target.value)
|
||||
)
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t("createInternalResourceDialogSitePortDescription")}
|
||||
</FormDescription>
|
||||
<FormItem className="flex flex-col">
|
||||
<FormLabel>{t("createInternalResourceDialogSite")}</FormLabel>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<FormControl>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
className={cn(
|
||||
"w-full justify-between",
|
||||
!field.value && "text-muted-foreground"
|
||||
)}
|
||||
>
|
||||
{field.value
|
||||
? availableSites.find(
|
||||
(site) => site.siteId === field.value
|
||||
)?.name
|
||||
: t("createInternalResourceDialogSelectSite")}
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</FormControl>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-full p-0">
|
||||
<Command>
|
||||
<CommandInput placeholder={t("createInternalResourceDialogSearchSites")} />
|
||||
<CommandList>
|
||||
<CommandEmpty>{t("createInternalResourceDialogNoSitesFound")}</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{availableSites.map((site) => (
|
||||
<CommandItem
|
||||
key={site.siteId}
|
||||
value={site.name}
|
||||
onSelect={() => {
|
||||
field.onChange(site.siteId);
|
||||
}}
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
"mr-2 h-4 w-4",
|
||||
field.value === site.siteId
|
||||
? "opacity-100"
|
||||
: "opacity-0"
|
||||
)}
|
||||
/>
|
||||
{site.name}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="mode"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("createInternalResourceDialogMode")}</FormLabel>
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
value={field.value}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="port">{t("createInternalResourceDialogModePort")}</SelectItem>
|
||||
<SelectItem value="host">{t("createInternalResourceDialogModeHost")}</SelectItem>
|
||||
<SelectItem value="cidr">{t("createInternalResourceDialogModeCidr")}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{mode === "port" && (
|
||||
<>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="protocol"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t("createInternalResourceDialogProtocol")}
|
||||
</FormLabel>
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
value={field.value ?? undefined}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="tcp">
|
||||
{t("createInternalResourceDialogTcp")}
|
||||
</SelectItem>
|
||||
<SelectItem value="udp">
|
||||
{t("createInternalResourceDialogUdp")}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="proxyPort"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("createInternalResourceDialogSitePort")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
value={field.value || ""}
|
||||
onChange={(e) =>
|
||||
field.onChange(
|
||||
e.target.value === "" ? undefined : parseInt(e.target.value)
|
||||
)
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -423,28 +496,28 @@ export default function CreateInternalResourceDialog({
|
||||
{t("createInternalResourceDialogTargetConfiguration")}
|
||||
</h3>
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="destinationIp"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t("targetAddr")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t("createInternalResourceDialogDestinationIPDescription")}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="destination"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t("createInternalResourceDialogDestination")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{mode === "host" && t("createInternalResourceDialogDestinationHostDescription")}
|
||||
{mode === "cidr" && t("createInternalResourceDialogDestinationCidrDescription")}
|
||||
{mode === "port" && t("createInternalResourceDialogDestinationIPDescription")}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{mode === "port" && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="destinationPort"
|
||||
@@ -471,12 +544,33 @@ export default function CreateInternalResourceDialog({
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Alias */}
|
||||
{mode !== "cidr" && (
|
||||
<div>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="alias"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("createInternalResourceDialogAlias")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} value={field.value ?? ""} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t("createInternalResourceDialogAliasDescription")}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Access Control Section */}
|
||||
<Separator />
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-4">
|
||||
{t("resourceUsersRoles")}
|
||||
|
||||
@@ -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<typeof formSchema>;
|
||||
|
||||
@@ -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({
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="protocol"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("editInternalResourceDialogProtocol")}</FormLabel>
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
value={field.value}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="tcp">TCP</SelectItem>
|
||||
<SelectItem value="udp">UDP</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="proxyPort"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("editInternalResourceDialogSitePort")}</FormLabel>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="mode"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("editInternalResourceDialogMode")}</FormLabel>
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
value={field.value}
|
||||
>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
{...field}
|
||||
onChange={(e) => field.onChange(parseInt(e.target.value) || 0)}
|
||||
/>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<SelectContent>
|
||||
<SelectItem value="port">{t("editInternalResourceDialogModePort")}</SelectItem>
|
||||
<SelectItem value="host">{t("editInternalResourceDialogModeHost")}</SelectItem>
|
||||
<SelectItem value="cidr">{t("editInternalResourceDialogModeCidr")}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{mode === "port" && (
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="protocol"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("editInternalResourceDialogProtocol")}</FormLabel>
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
value={field.value ?? undefined}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="tcp">TCP</SelectItem>
|
||||
<SelectItem value="udp">UDP</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="proxyPort"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("editInternalResourceDialogSitePort")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
value={field.value || ""}
|
||||
onChange={(e) => field.onChange(e.target.value === "" ? undefined : parseInt(e.target.value) || 0)}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -315,21 +391,26 @@ export default function EditInternalResourceDialog({
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-4">{t("editInternalResourceDialogTargetConfiguration")}</h3>
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="destinationIp"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("targetAddr")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="destination"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("editInternalResourceDialogDestination")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{mode === "host" && t("editInternalResourceDialogDestinationHostDescription")}
|
||||
{mode === "cidr" && t("editInternalResourceDialogDestinationCidrDescription")}
|
||||
{mode === "port" && t("editInternalResourceDialogDestinationIPDescription")}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{mode === "port" && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="destinationPort"
|
||||
@@ -339,20 +420,41 @@ export default function EditInternalResourceDialog({
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
{...field}
|
||||
onChange={(e) => field.onChange(parseInt(e.target.value) || 0)}
|
||||
value={field.value || ""}
|
||||
onChange={(e) => field.onChange(e.target.value === "" ? undefined : parseInt(e.target.value) || 0)}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Alias */}
|
||||
{mode !== "cidr" && (
|
||||
<div>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="alias"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("editInternalResourceDialogAlias")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} value={field.value ?? ""} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t("editInternalResourceDialogAliasDescription")}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Access Control Section */}
|
||||
<Separator />
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-4">
|
||||
{t("resourceUsersRoles")}
|
||||
|
||||
@@ -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: () => (<span className="p-3">{t("protocol")}</span>),
|
||||
accessorKey: "mode",
|
||||
header: () => (<span className="p-3">{t("editInternalResourceDialogMode")}</span>),
|
||||
cell: ({ row }) => {
|
||||
const resourceRow = row.original;
|
||||
return <span>{resourceRow.protocol.toUpperCase()}</span>;
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: "proxyPort",
|
||||
header: () => (<span className="p-3">{t("proxyPort")}</span>),
|
||||
cell: ({ row }) => {
|
||||
const resourceRow = row.original;
|
||||
return (
|
||||
<CopyToClipboard
|
||||
text={resourceRow.proxyPort?.toString() || ""}
|
||||
isLink={false}
|
||||
/>
|
||||
);
|
||||
const modeLabels: Record<"host" | "cidr" | "port", string> = {
|
||||
host: t("editInternalResourceDialogModeHost"),
|
||||
cidr: t("editInternalResourceDialogModeCidr"),
|
||||
port: t("editInternalResourceDialogModePort")
|
||||
};
|
||||
return <span>{modeLabels[resourceRow.mode]}</span>;
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -596,8 +591,35 @@ export default function ResourcesTable({
|
||||
header: () => (<span className="p-3">{t("resourcesTableDestination")}</span>),
|
||||
cell: ({ row }) => {
|
||||
const resourceRow = row.original;
|
||||
const destination = `${resourceRow.destinationIp}:${resourceRow.destinationPort}`;
|
||||
return <CopyToClipboard text={destination} isLink={false} />;
|
||||
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 <CopyToClipboard text={copyText} isLink={false} displayText={displayText} />;
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ const DialogContent = React.forwardRef<
|
||||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-card p-6 shadow-sm duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:scale-95 data-[state=open]:scale-100 sm:rounded-lg",
|
||||
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-card p-6 duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:scale-95 data-[state=open]:scale-100 sm:rounded-lg",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
Reference in New Issue
Block a user