mirror of
https://github.com/fosrl/pangolin.git
synced 2026-01-28 22:00:51 +00:00
add site resource modes and alias
This commit is contained in:
@@ -1545,6 +1545,17 @@
|
|||||||
"editInternalResourceDialogInvalidIPAddressFormat": "Invalid IP address format",
|
"editInternalResourceDialogInvalidIPAddressFormat": "Invalid IP address format",
|
||||||
"editInternalResourceDialogDestinationPortMin": "Destination port must be at least 1",
|
"editInternalResourceDialogDestinationPortMin": "Destination port must be at least 1",
|
||||||
"editInternalResourceDialogDestinationPortMax": "Destination port must be less than 65536",
|
"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",
|
"createInternalResourceDialogNoSitesAvailable": "No Sites Available",
|
||||||
"createInternalResourceDialogNoSitesAvailableDescription": "You need to have at least one Newt site with a subnet configured to create internal resources.",
|
"createInternalResourceDialogNoSitesAvailableDescription": "You need to have at least one Newt site with a subnet configured to create internal resources.",
|
||||||
"createInternalResourceDialogClose": "Close",
|
"createInternalResourceDialogClose": "Close",
|
||||||
@@ -1578,6 +1589,16 @@
|
|||||||
"createInternalResourceDialogInvalidIPAddressFormat": "Invalid IP address format",
|
"createInternalResourceDialogInvalidIPAddressFormat": "Invalid IP address format",
|
||||||
"createInternalResourceDialogDestinationPortMin": "Destination port must be at least 1",
|
"createInternalResourceDialogDestinationPortMin": "Destination port must be at least 1",
|
||||||
"createInternalResourceDialogDestinationPortMax": "Destination port must be less than 65536",
|
"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",
|
"siteConfiguration": "Configuration",
|
||||||
"siteAcceptClientConnections": "Accept Client Connections",
|
"siteAcceptClientConnections": "Accept Client Connections",
|
||||||
"siteAcceptClientConnectionsDescription": "Allow other devices to connect through this Newt instance as a gateway using clients.",
|
"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" }),
|
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
||||||
niceId: varchar("niceId").notNull(),
|
niceId: varchar("niceId").notNull(),
|
||||||
name: varchar("name").notNull(),
|
name: varchar("name").notNull(),
|
||||||
protocol: varchar("protocol").notNull(),
|
mode: varchar("mode").notNull(), // "host" | "cidr" | "port"
|
||||||
proxyPort: integer("proxyPort").notNull(),
|
protocol: varchar("protocol"), // only for port mode
|
||||||
destinationPort: integer("destinationPort").notNull(),
|
proxyPort: integer("proxyPort"), // only for port mode
|
||||||
destinationIp: varchar("destinationIp").notNull(),
|
destinationPort: integer("destinationPort"), // only for port mode
|
||||||
enabled: boolean("enabled").notNull().default(true)
|
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", {
|
export const roleSiteResources = pgTable("roleSiteResources", {
|
||||||
|
|||||||
@@ -225,11 +225,13 @@ export const siteResources = sqliteTable("siteResources", {
|
|||||||
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
||||||
niceId: text("niceId").notNull(),
|
niceId: text("niceId").notNull(),
|
||||||
name: text("name").notNull(),
|
name: text("name").notNull(),
|
||||||
protocol: text("protocol").notNull(),
|
mode: text("mode").notNull(), // "host" | "cidr" | "port"
|
||||||
proxyPort: integer("proxyPort").notNull(),
|
protocol: text("protocol"), // only for port mode
|
||||||
destinationPort: integer("destinationPort").notNull(),
|
proxyPort: integer("proxyPort"), // only for port mode
|
||||||
destinationIp: text("destinationIp").notNull(),
|
destinationPort: integer("destinationPort"), // only for port mode
|
||||||
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true)
|
destination: text("destination").notNull(), // ip, cidr, hostname
|
||||||
|
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true),
|
||||||
|
alias: text("alias")
|
||||||
});
|
});
|
||||||
|
|
||||||
export const roleSiteResources = sqliteTable("roleSiteResources", {
|
export const roleSiteResources = sqliteTable("roleSiteResources", {
|
||||||
|
|||||||
@@ -122,14 +122,14 @@ export async function applyBlueprint({
|
|||||||
)
|
)
|
||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
if (site) {
|
if (site && result.resource.mode === "port" && result.resource.protocol && result.resource.proxyPort && result.resource.destinationPort) {
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`Updating client resource ${result.resource.siteResourceId} on site ${site.sites.siteId}`
|
`Updating client resource ${result.resource.siteResourceId} on site ${site.sites.siteId}`
|
||||||
);
|
);
|
||||||
|
|
||||||
await addClientTargets(
|
await addClientTargets(
|
||||||
site.newt.newtId,
|
site.newt.newtId,
|
||||||
result.resource.destinationIp,
|
result.resource.destination,
|
||||||
result.resource.destinationPort,
|
result.resource.destinationPort,
|
||||||
result.resource.protocol,
|
result.resource.protocol,
|
||||||
result.resource.proxyPort
|
result.resource.proxyPort
|
||||||
|
|||||||
@@ -75,8 +75,9 @@ export async function updateClientResources(
|
|||||||
.set({
|
.set({
|
||||||
name: resourceData.name || resourceNiceId,
|
name: resourceData.name || resourceNiceId,
|
||||||
siteId: site.siteId,
|
siteId: site.siteId,
|
||||||
|
mode: "port",
|
||||||
proxyPort: resourceData["proxy-port"]!,
|
proxyPort: resourceData["proxy-port"]!,
|
||||||
destinationIp: resourceData.hostname,
|
destination: resourceData.hostname,
|
||||||
destinationPort: resourceData["internal-port"],
|
destinationPort: resourceData["internal-port"],
|
||||||
protocol: resourceData.protocol
|
protocol: resourceData.protocol
|
||||||
})
|
})
|
||||||
@@ -98,8 +99,9 @@ export async function updateClientResources(
|
|||||||
siteId: site.siteId,
|
siteId: site.siteId,
|
||||||
niceId: resourceNiceId,
|
niceId: resourceNiceId,
|
||||||
name: resourceData.name || resourceNiceId,
|
name: resourceData.name || resourceNiceId,
|
||||||
|
mode: "port",
|
||||||
proxyPort: resourceData["proxy-port"]!,
|
proxyPort: resourceData["proxy-port"]!,
|
||||||
destinationIp: resourceData.hostname,
|
destination: resourceData.hostname,
|
||||||
destinationPort: resourceData["internal-port"],
|
destinationPort: resourceData["internal-port"],
|
||||||
protocol: resourceData.protocol
|
protocol: resourceData.protocol
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -53,8 +53,6 @@ export async function verifyOrgAccess(
|
|||||||
session: req.session
|
session: req.session
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.debug("Org check policy result", { policyCheck });
|
|
||||||
|
|
||||||
if (!policyCheck.allowed || policyCheck.error) {
|
if (!policyCheck.allowed || policyCheck.error) {
|
||||||
return next(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
|
|||||||
@@ -216,13 +216,18 @@ export const handleGetConfigMessage: MessageHandler = async (context) => {
|
|||||||
|
|
||||||
const { tcpTargets, udpTargets } = allSiteResources.reduce(
|
const { tcpTargets, udpTargets } = allSiteResources.reduce(
|
||||||
(acc, resource) => {
|
(acc, resource) => {
|
||||||
|
// Only process port mode resources
|
||||||
|
if (resource.mode !== "port") {
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
|
||||||
// Filter out invalid targets
|
// Filter out invalid targets
|
||||||
if (!resource.proxyPort || !resource.destinationIp || !resource.destinationPort) {
|
if (!resource.proxyPort || !resource.destination || !resource.destinationPort || !resource.protocol) {
|
||||||
return acc;
|
return acc;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Format target into string
|
// 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
|
// Add to the appropriate protocol array
|
||||||
if (resource.protocol === "tcp") {
|
if (resource.protocol === "tcp") {
|
||||||
|
|||||||
@@ -22,13 +22,30 @@ const createSiteResourceParamsSchema = z
|
|||||||
const createSiteResourceSchema = z
|
const createSiteResourceSchema = z
|
||||||
.object({
|
.object({
|
||||||
name: z.string().min(1).max(255),
|
name: z.string().min(1).max(255),
|
||||||
protocol: z.enum(["tcp", "udp"]),
|
mode: z.enum(["host", "cidr", "port"]),
|
||||||
proxyPort: z.number().int().positive(),
|
protocol: z.enum(["tcp", "udp"]).optional(),
|
||||||
destinationPort: z.number().int().positive(),
|
proxyPort: z.number().int().positive().optional(),
|
||||||
destinationIp: z.string(),
|
destinationPort: z.number().int().positive().optional(),
|
||||||
enabled: z.boolean().default(true)
|
destination: z.string().min(1),
|
||||||
})
|
enabled: z.boolean().default(true),
|
||||||
.strict();
|
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 CreateSiteResourceBody = z.infer<typeof createSiteResourceSchema>;
|
||||||
export type CreateSiteResourceResponse = SiteResource;
|
export type CreateSiteResourceResponse = SiteResource;
|
||||||
@@ -82,11 +99,13 @@ export async function createSiteResource(
|
|||||||
const { siteId, orgId } = parsedParams.data;
|
const { siteId, orgId } = parsedParams.data;
|
||||||
const {
|
const {
|
||||||
name,
|
name,
|
||||||
|
mode,
|
||||||
protocol,
|
protocol,
|
||||||
proxyPort,
|
proxyPort,
|
||||||
destinationPort,
|
destinationPort,
|
||||||
destinationIp,
|
destination,
|
||||||
enabled
|
enabled,
|
||||||
|
alias
|
||||||
} = parsedBody.data;
|
} = parsedBody.data;
|
||||||
|
|
||||||
// Verify the site exists and belongs to the org
|
// 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"));
|
return next(createHttpError(HttpCode.NOT_FOUND, "Site not found"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if resource with same protocol and proxy port already exists
|
// check if resource with same protocol and proxy port already exists (only for port mode)
|
||||||
const [existingResource] = await db
|
if (mode === "port" && protocol && proxyPort) {
|
||||||
.select()
|
const [existingResource] = await db
|
||||||
.from(siteResources)
|
.select()
|
||||||
.where(
|
.from(siteResources)
|
||||||
and(
|
.where(
|
||||||
eq(siteResources.siteId, siteId),
|
and(
|
||||||
eq(siteResources.orgId, orgId),
|
eq(siteResources.siteId, siteId),
|
||||||
eq(siteResources.protocol, protocol),
|
eq(siteResources.orgId, orgId),
|
||||||
eq(siteResources.proxyPort, proxyPort)
|
eq(siteResources.protocol, protocol),
|
||||||
|
eq(siteResources.proxyPort, proxyPort)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
.limit(1);
|
||||||
.limit(1);
|
if (existingResource && existingResource.siteResourceId) {
|
||||||
if (existingResource && existingResource.siteResourceId) {
|
return next(
|
||||||
return next(
|
createHttpError(
|
||||||
createHttpError(
|
HttpCode.CONFLICT,
|
||||||
HttpCode.CONFLICT,
|
"A resource with the same protocol and proxy port already exists"
|
||||||
"A resource with the same protocol and proxy port already exists"
|
)
|
||||||
)
|
);
|
||||||
);
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const niceId = await getUniqueSiteResourceName(orgId);
|
const niceId = await getUniqueSiteResourceName(orgId);
|
||||||
@@ -132,11 +153,13 @@ export async function createSiteResource(
|
|||||||
niceId,
|
niceId,
|
||||||
orgId,
|
orgId,
|
||||||
name,
|
name,
|
||||||
protocol,
|
mode,
|
||||||
proxyPort,
|
protocol: mode === "port" ? protocol : null,
|
||||||
destinationPort,
|
proxyPort: mode === "port" ? proxyPort : null,
|
||||||
destinationIp,
|
destinationPort: mode === "port" ? destinationPort : null,
|
||||||
enabled
|
destination,
|
||||||
|
enabled,
|
||||||
|
alias: alias || null
|
||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
@@ -157,24 +180,29 @@ export async function createSiteResource(
|
|||||||
siteResourceId: newSiteResource.siteResourceId
|
siteResourceId: newSiteResource.siteResourceId
|
||||||
});
|
});
|
||||||
|
|
||||||
const [newt] = await db
|
// Only add targets for port mode
|
||||||
.select()
|
if (mode === "port" && protocol && proxyPort && destinationPort) {
|
||||||
.from(newts)
|
const [newt] = await db
|
||||||
.where(eq(newts.siteId, site.siteId))
|
.select()
|
||||||
.limit(1);
|
.from(newts)
|
||||||
|
.where(eq(newts.siteId, site.siteId))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
if (!newt) {
|
if (!newt) {
|
||||||
return next(createHttpError(HttpCode.NOT_FOUND, "Newt not found"));
|
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(
|
logger.info(
|
||||||
`Created site resource ${newSiteResource.siteResourceId} for site ${siteId}`
|
`Created site resource ${newSiteResource.siteResourceId} for site ${siteId}`
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -91,24 +91,27 @@ export async function deleteSiteResource(
|
|||||||
eq(siteResources.orgId, orgId)
|
eq(siteResources.orgId, orgId)
|
||||||
));
|
));
|
||||||
|
|
||||||
const [newt] = await db
|
// Only remove targets for port mode
|
||||||
.select()
|
if (existingSiteResource.mode === "port" && existingSiteResource.protocol && existingSiteResource.proxyPort && existingSiteResource.destinationPort) {
|
||||||
.from(newts)
|
const [newt] = await db
|
||||||
.where(eq(newts.siteId, site.siteId))
|
.select()
|
||||||
.limit(1);
|
.from(newts)
|
||||||
|
.where(eq(newts.siteId, site.siteId))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
if (!newt) {
|
if (!newt) {
|
||||||
return next(createHttpError(HttpCode.NOT_FOUND, "Newt not found"));
|
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}`);
|
logger.info(`Deleted site resource ${siteResourceId} for site ${siteId}`);
|
||||||
|
|
||||||
return response(res, {
|
return response(res, {
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ const listAllSiteResourcesByOrgQuerySchema = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export type ListAllSiteResourcesByOrgResponse = {
|
export type ListAllSiteResourcesByOrgResponse = {
|
||||||
siteResources: (SiteResource & { siteName: string, siteNiceId: string })[];
|
siteResources: (SiteResource & { siteName: string, siteNiceId: string, siteAddress: string | null })[];
|
||||||
};
|
};
|
||||||
|
|
||||||
registry.registerPath({
|
registry.registerPath({
|
||||||
@@ -82,14 +82,18 @@ export async function listAllSiteResourcesByOrg(
|
|||||||
siteResourceId: siteResources.siteResourceId,
|
siteResourceId: siteResources.siteResourceId,
|
||||||
siteId: siteResources.siteId,
|
siteId: siteResources.siteId,
|
||||||
orgId: siteResources.orgId,
|
orgId: siteResources.orgId,
|
||||||
|
niceId: siteResources.niceId,
|
||||||
name: siteResources.name,
|
name: siteResources.name,
|
||||||
|
mode: siteResources.mode,
|
||||||
protocol: siteResources.protocol,
|
protocol: siteResources.protocol,
|
||||||
proxyPort: siteResources.proxyPort,
|
proxyPort: siteResources.proxyPort,
|
||||||
destinationPort: siteResources.destinationPort,
|
destinationPort: siteResources.destinationPort,
|
||||||
destinationIp: siteResources.destinationIp,
|
destination: siteResources.destination,
|
||||||
enabled: siteResources.enabled,
|
enabled: siteResources.enabled,
|
||||||
|
alias: siteResources.alias,
|
||||||
siteName: sites.name,
|
siteName: sites.name,
|
||||||
siteNiceId: sites.niceId
|
siteNiceId: sites.niceId,
|
||||||
|
siteAddress: sites.address
|
||||||
})
|
})
|
||||||
.from(siteResources)
|
.from(siteResources)
|
||||||
.innerJoin(sites, eq(siteResources.siteId, sites.siteId))
|
.innerJoin(sites, eq(siteResources.siteId, sites.siteId))
|
||||||
|
|||||||
@@ -25,11 +25,13 @@ const updateSiteResourceParamsSchema = z
|
|||||||
const updateSiteResourceSchema = z
|
const updateSiteResourceSchema = z
|
||||||
.object({
|
.object({
|
||||||
name: z.string().min(1).max(255).optional(),
|
name: z.string().min(1).max(255).optional(),
|
||||||
protocol: z.enum(["tcp", "udp"]).optional(),
|
mode: z.enum(["host", "cidr", "port"]).optional(),
|
||||||
proxyPort: z.number().int().positive().optional(),
|
protocol: z.enum(["tcp", "udp"]).nullish(),
|
||||||
destinationPort: z.number().int().positive().optional(),
|
proxyPort: z.number().int().positive().nullish(),
|
||||||
destinationIp: z.string().optional(),
|
destinationPort: z.number().int().positive().nullish(),
|
||||||
enabled: z.boolean().optional()
|
destination: z.string().min(1).optional(),
|
||||||
|
enabled: z.boolean().optional(),
|
||||||
|
alias: z.string().nullish()
|
||||||
})
|
})
|
||||||
.strict();
|
.strict();
|
||||||
|
|
||||||
@@ -114,39 +116,77 @@ export async function updateSiteResource(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const protocol = updateData.protocol || existingSiteResource.protocol;
|
// Determine the final mode and validate port mode requirements
|
||||||
const proxyPort =
|
const finalMode = updateData.mode || existingSiteResource.mode;
|
||||||
updateData.proxyPort || existingSiteResource.proxyPort;
|
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
|
if (finalMode === "port") {
|
||||||
const [existingResource] = await db
|
if (!finalProtocol || !finalProxyPort || !finalDestinationPort) {
|
||||||
.select()
|
return next(
|
||||||
.from(siteResources)
|
createHttpError(
|
||||||
.where(
|
HttpCode.BAD_REQUEST,
|
||||||
and(
|
"Protocol, proxy port, and destination port are required for port mode"
|
||||||
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
|
||||||
|
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);
|
||||||
.limit(1);
|
if (
|
||||||
if (
|
existingResource &&
|
||||||
existingResource &&
|
existingResource.siteResourceId !== siteResourceId
|
||||||
existingResource.siteResourceId !== siteResourceId
|
) {
|
||||||
) {
|
return next(
|
||||||
return next(
|
createHttpError(
|
||||||
createHttpError(
|
HttpCode.CONFLICT,
|
||||||
HttpCode.CONFLICT,
|
"A resource with the same protocol and proxy port already exists"
|
||||||
"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
|
// Update the site resource
|
||||||
const [updatedSiteResource] = await db
|
const [updatedSiteResource] = await db
|
||||||
.update(siteResources)
|
.update(siteResources)
|
||||||
.set(updateData)
|
.set(updateValues)
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
eq(siteResources.siteResourceId, siteResourceId),
|
eq(siteResources.siteResourceId, siteResourceId),
|
||||||
@@ -156,24 +196,27 @@ export async function updateSiteResource(
|
|||||||
)
|
)
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
const [newt] = await db
|
// Only add targets for port mode
|
||||||
.select()
|
if (updatedSiteResource.mode === "port" && updatedSiteResource.protocol && updatedSiteResource.proxyPort && updatedSiteResource.destinationPort) {
|
||||||
.from(newts)
|
const [newt] = await db
|
||||||
.where(eq(newts.siteId, site.siteId))
|
.select()
|
||||||
.limit(1);
|
.from(newts)
|
||||||
|
.where(eq(newts.siteId, site.siteId))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
if (!newt) {
|
if (!newt) {
|
||||||
return next(createHttpError(HttpCode.NOT_FOUND, "Newt not found"));
|
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(
|
logger.info(
|
||||||
`Updated site resource ${siteResourceId} for site ${siteId}`
|
`Updated site resource ${siteResourceId} for site ${siteId}`
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -103,11 +103,14 @@ export default async function ResourcesPage(props: ResourcesPageProps) {
|
|||||||
name: siteResource.name,
|
name: siteResource.name,
|
||||||
orgId: params.orgId,
|
orgId: params.orgId,
|
||||||
siteName: siteResource.siteName,
|
siteName: siteResource.siteName,
|
||||||
|
siteAddress: siteResource.siteAddress || null,
|
||||||
|
mode: siteResource.mode || "port" as any,
|
||||||
protocol: siteResource.protocol,
|
protocol: siteResource.protocol,
|
||||||
proxyPort: siteResource.proxyPort,
|
proxyPort: siteResource.proxyPort,
|
||||||
siteId: siteResource.siteId,
|
siteId: siteResource.siteId,
|
||||||
destinationIp: siteResource.destinationIp,
|
destination: siteResource.destination,
|
||||||
destinationPort: siteResource.destinationPort,
|
destinationPort: siteResource.destinationPort,
|
||||||
|
alias: siteResource.alias || null,
|
||||||
siteNiceId: siteResource.siteNiceId
|
siteNiceId: siteResource.siteNiceId
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -86,20 +86,24 @@ export default function CreateInternalResourceDialog({
|
|||||||
.min(1, t("createInternalResourceDialogNameRequired"))
|
.min(1, t("createInternalResourceDialogNameRequired"))
|
||||||
.max(255, t("createInternalResourceDialogNameMaxLength")),
|
.max(255, t("createInternalResourceDialogNameMaxLength")),
|
||||||
siteId: z.number().int().positive(t("createInternalResourceDialogPleaseSelectSite")),
|
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
|
proxyPort: z
|
||||||
.number()
|
.number()
|
||||||
.int()
|
.int()
|
||||||
.positive()
|
.positive()
|
||||||
.min(1, t("createInternalResourceDialogProxyPortMin"))
|
.min(1, t("createInternalResourceDialogProxyPortMin"))
|
||||||
.max(65535, t("createInternalResourceDialogProxyPortMax")),
|
.max(65535, t("createInternalResourceDialogProxyPortMax"))
|
||||||
destinationIp: z.string(),
|
.nullish(),
|
||||||
|
destination: z.string().min(1),
|
||||||
destinationPort: z
|
destinationPort: z
|
||||||
.number()
|
.number()
|
||||||
.int()
|
.int()
|
||||||
.positive()
|
.positive()
|
||||||
.min(1, t("createInternalResourceDialogDestinationPortMin"))
|
.min(1, t("createInternalResourceDialogDestinationPortMin"))
|
||||||
.max(65535, t("createInternalResourceDialogDestinationPortMax")),
|
.max(65535, t("createInternalResourceDialogDestinationPortMax"))
|
||||||
|
.nullish(),
|
||||||
|
alias: z.string().nullish(),
|
||||||
roles: z.array(
|
roles: z.array(
|
||||||
z.object({
|
z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
@@ -112,8 +116,44 @@ export default function CreateInternalResourceDialog({
|
|||||||
text: z.string()
|
text: z.string()
|
||||||
})
|
})
|
||||||
).optional()
|
).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>;
|
type FormData = z.infer<typeof formSchema>;
|
||||||
|
|
||||||
const [allRoles, setAllRoles] = useState<{ id: string; text: string }[]>([]);
|
const [allRoles, setAllRoles] = useState<{ id: string; text: string }[]>([]);
|
||||||
@@ -130,24 +170,30 @@ export default function CreateInternalResourceDialog({
|
|||||||
defaultValues: {
|
defaultValues: {
|
||||||
name: "",
|
name: "",
|
||||||
siteId: availableSites[0]?.siteId || 0,
|
siteId: availableSites[0]?.siteId || 0,
|
||||||
|
mode: "host",
|
||||||
protocol: "tcp",
|
protocol: "tcp",
|
||||||
proxyPort: undefined,
|
proxyPort: undefined,
|
||||||
destinationIp: "",
|
destination: "",
|
||||||
destinationPort: undefined,
|
destinationPort: undefined,
|
||||||
|
alias: "",
|
||||||
roles: [],
|
roles: [],
|
||||||
users: []
|
users: []
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const mode = form.watch("mode");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (open && availableSites.length > 0) {
|
if (open && availableSites.length > 0) {
|
||||||
form.reset({
|
form.reset({
|
||||||
name: "",
|
name: "",
|
||||||
siteId: availableSites[0].siteId,
|
siteId: availableSites[0].siteId,
|
||||||
|
mode: "host",
|
||||||
protocol: "tcp",
|
protocol: "tcp",
|
||||||
proxyPort: undefined,
|
proxyPort: undefined,
|
||||||
destinationIp: "",
|
destination: "",
|
||||||
destinationPort: undefined,
|
destinationPort: undefined,
|
||||||
|
alias: "",
|
||||||
roles: [],
|
roles: [],
|
||||||
users: []
|
users: []
|
||||||
});
|
});
|
||||||
@@ -194,11 +240,13 @@ export default function CreateInternalResourceDialog({
|
|||||||
`/org/${orgId}/site/${data.siteId}/resource`,
|
`/org/${orgId}/site/${data.siteId}/resource`,
|
||||||
{
|
{
|
||||||
name: data.name,
|
name: data.name,
|
||||||
protocol: data.protocol,
|
mode: data.mode,
|
||||||
proxyPort: data.proxyPort,
|
protocol: data.mode === "port" ? data.protocol : undefined,
|
||||||
destinationIp: data.destinationIp,
|
proxyPort: data.mode === "port" ? data.proxyPort : undefined,
|
||||||
destinationPort: data.destinationPort,
|
destinationPort: data.mode === "port" ? data.destinationPort : undefined,
|
||||||
enabled: true
|
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
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="proxyPort"
|
name="siteId"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem className="flex flex-col">
|
||||||
<FormLabel>{t("createInternalResourceDialogSitePort")}</FormLabel>
|
<FormLabel>{t("createInternalResourceDialogSite")}</FormLabel>
|
||||||
<FormControl>
|
<Popover>
|
||||||
<Input
|
<PopoverTrigger asChild>
|
||||||
type="number"
|
<FormControl>
|
||||||
value={field.value || ""}
|
<Button
|
||||||
onChange={(e) =>
|
variant="outline"
|
||||||
field.onChange(
|
role="combobox"
|
||||||
e.target.value === "" ? undefined : parseInt(e.target.value)
|
className={cn(
|
||||||
)
|
"w-full justify-between",
|
||||||
}
|
!field.value && "text-muted-foreground"
|
||||||
/>
|
)}
|
||||||
</FormControl>
|
>
|
||||||
<FormDescription>
|
{field.value
|
||||||
{t("createInternalResourceDialogSitePortDescription")}
|
? availableSites.find(
|
||||||
</FormDescription>
|
(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 />
|
<FormMessage />
|
||||||
</FormItem>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -423,28 +496,28 @@ export default function CreateInternalResourceDialog({
|
|||||||
{t("createInternalResourceDialogTargetConfiguration")}
|
{t("createInternalResourceDialogTargetConfiguration")}
|
||||||
</h3>
|
</h3>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<FormField
|
||||||
<FormField
|
control={form.control}
|
||||||
control={form.control}
|
name="destination"
|
||||||
name="destinationIp"
|
render={({ field }) => (
|
||||||
render={({ field }) => (
|
<FormItem>
|
||||||
<FormItem>
|
<FormLabel>
|
||||||
<FormLabel>
|
{t("createInternalResourceDialogDestination")}
|
||||||
{t("targetAddr")}
|
</FormLabel>
|
||||||
</FormLabel>
|
<FormControl>
|
||||||
<FormControl>
|
<Input {...field} />
|
||||||
<Input
|
</FormControl>
|
||||||
{...field}
|
<FormDescription>
|
||||||
/>
|
{mode === "host" && t("createInternalResourceDialogDestinationHostDescription")}
|
||||||
</FormControl>
|
{mode === "cidr" && t("createInternalResourceDialogDestinationCidrDescription")}
|
||||||
<FormDescription>
|
{mode === "port" && t("createInternalResourceDialogDestinationIPDescription")}
|
||||||
{t("createInternalResourceDialogDestinationIPDescription")}
|
</FormDescription>
|
||||||
</FormDescription>
|
<FormMessage />
|
||||||
<FormMessage />
|
</FormItem>
|
||||||
</FormItem>
|
)}
|
||||||
)}
|
/>
|
||||||
/>
|
|
||||||
|
|
||||||
|
{mode === "port" && (
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="destinationPort"
|
name="destinationPort"
|
||||||
@@ -471,12 +544,33 @@ export default function CreateInternalResourceDialog({
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
)}
|
||||||
</div>
|
</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 */}
|
{/* Access Control Section */}
|
||||||
<Separator />
|
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-semibold mb-4">
|
<h3 className="text-lg font-semibold mb-4">
|
||||||
{t("resourceUsersRoles")}
|
{t("resourceUsersRoles")}
|
||||||
|
|||||||
@@ -50,11 +50,13 @@ type InternalResourceData = {
|
|||||||
name: string;
|
name: string;
|
||||||
orgId: string;
|
orgId: string;
|
||||||
siteName: string;
|
siteName: string;
|
||||||
protocol: string;
|
mode: "host" | "cidr" | "port";
|
||||||
|
protocol: string | null;
|
||||||
proxyPort: number | null;
|
proxyPort: number | null;
|
||||||
siteId: number;
|
siteId: number;
|
||||||
destinationIp?: string;
|
destination: string;
|
||||||
destinationPort?: number;
|
destinationPort?: number | null;
|
||||||
|
alias?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
type EditInternalResourceDialogProps = {
|
type EditInternalResourceDialogProps = {
|
||||||
@@ -78,10 +80,12 @@ export default function EditInternalResourceDialog({
|
|||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
name: z.string().min(1, t("editInternalResourceDialogNameRequired")).max(255, t("editInternalResourceDialogNameMaxLength")),
|
name: z.string().min(1, t("editInternalResourceDialogNameRequired")).max(255, t("editInternalResourceDialogNameMaxLength")),
|
||||||
protocol: z.enum(["tcp", "udp"]),
|
mode: z.enum(["host", "cidr", "port"]),
|
||||||
proxyPort: z.number().int().positive().min(1, t("editInternalResourceDialogProxyPortMin")).max(65535, t("editInternalResourceDialogProxyPortMax")),
|
protocol: z.enum(["tcp", "udp"]).nullish(),
|
||||||
destinationIp: z.string(),
|
proxyPort: z.number().int().positive().min(1, t("editInternalResourceDialogProxyPortMin")).max(65535, t("editInternalResourceDialogProxyPortMax")).nullish(),
|
||||||
destinationPort: z.number().int().positive().min(1, t("editInternalResourceDialogDestinationPortMin")).max(65535, t("editInternalResourceDialogDestinationPortMax")),
|
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(
|
roles: z.array(
|
||||||
z.object({
|
z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
@@ -94,7 +98,43 @@ export default function EditInternalResourceDialog({
|
|||||||
text: z.string()
|
text: z.string()
|
||||||
})
|
})
|
||||||
).optional()
|
).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>;
|
type FormData = z.infer<typeof formSchema>;
|
||||||
|
|
||||||
@@ -108,15 +148,19 @@ export default function EditInternalResourceDialog({
|
|||||||
resolver: zodResolver(formSchema),
|
resolver: zodResolver(formSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
name: resource.name,
|
name: resource.name,
|
||||||
protocol: resource.protocol as "tcp" | "udp",
|
mode: resource.mode || "host",
|
||||||
proxyPort: resource.proxyPort || undefined,
|
protocol: (resource.protocol as "tcp" | "udp" | null | undefined) ?? undefined,
|
||||||
destinationIp: resource.destinationIp || "",
|
proxyPort: resource.proxyPort ?? undefined,
|
||||||
destinationPort: resource.destinationPort || undefined,
|
destination: resource.destination || "",
|
||||||
|
destinationPort: resource.destinationPort ?? undefined,
|
||||||
|
alias: resource.alias ?? null,
|
||||||
roles: [],
|
roles: [],
|
||||||
users: []
|
users: []
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const mode = form.watch("mode");
|
||||||
|
|
||||||
const fetchRolesAndUsers = async () => {
|
const fetchRolesAndUsers = async () => {
|
||||||
setLoadingRolesUsers(true);
|
setLoadingRolesUsers(true);
|
||||||
try {
|
try {
|
||||||
@@ -180,10 +224,12 @@ export default function EditInternalResourceDialog({
|
|||||||
if (open) {
|
if (open) {
|
||||||
form.reset({
|
form.reset({
|
||||||
name: resource.name,
|
name: resource.name,
|
||||||
protocol: resource.protocol as "tcp" | "udp",
|
mode: resource.mode || "host",
|
||||||
proxyPort: resource.proxyPort || undefined,
|
protocol: (resource.protocol as "tcp" | "udp" | null | undefined) ?? undefined,
|
||||||
destinationIp: resource.destinationIp || "",
|
proxyPort: resource.proxyPort ?? undefined,
|
||||||
destinationPort: resource.destinationPort || undefined,
|
destination: resource.destination || "",
|
||||||
|
destinationPort: resource.destinationPort ?? undefined,
|
||||||
|
alias: resource.alias ?? null,
|
||||||
roles: [],
|
roles: [],
|
||||||
users: []
|
users: []
|
||||||
});
|
});
|
||||||
@@ -198,10 +244,12 @@ export default function EditInternalResourceDialog({
|
|||||||
// Update the site resource
|
// Update the site resource
|
||||||
await api.post(`/org/${orgId}/site/${resource.siteId}/resource/${resource.id}`, {
|
await api.post(`/org/${orgId}/site/${resource.siteId}/resource/${resource.id}`, {
|
||||||
name: data.name,
|
name: data.name,
|
||||||
protocol: data.protocol,
|
mode: data.mode,
|
||||||
proxyPort: data.proxyPort,
|
protocol: data.mode === "port" ? data.protocol : null,
|
||||||
destinationIp: data.destinationIp,
|
proxyPort: data.mode === "port" ? data.proxyPort : null,
|
||||||
destinationPort: data.destinationPort
|
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
|
// Update roles and users
|
||||||
@@ -264,50 +312,78 @@ export default function EditInternalResourceDialog({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<FormField
|
||||||
<FormField
|
control={form.control}
|
||||||
control={form.control}
|
name="mode"
|
||||||
name="protocol"
|
render={({ field }) => (
|
||||||
render={({ field }) => (
|
<FormItem>
|
||||||
<FormItem>
|
<FormLabel>{t("editInternalResourceDialogMode")}</FormLabel>
|
||||||
<FormLabel>{t("editInternalResourceDialogProtocol")}</FormLabel>
|
<Select
|
||||||
<Select
|
onValueChange={field.onChange}
|
||||||
onValueChange={field.onChange}
|
value={field.value}
|
||||||
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>
|
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<SelectTrigger>
|
||||||
type="number"
|
<SelectValue />
|
||||||
{...field}
|
</SelectTrigger>
|
||||||
onChange={(e) => field.onChange(parseInt(e.target.value) || 0)}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<SelectContent>
|
||||||
</FormItem>
|
<SelectItem value="port">{t("editInternalResourceDialogModePort")}</SelectItem>
|
||||||
)}
|
<SelectItem value="host">{t("editInternalResourceDialogModeHost")}</SelectItem>
|
||||||
/>
|
<SelectItem value="cidr">{t("editInternalResourceDialogModeCidr")}</SelectItem>
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -315,21 +391,26 @@ export default function EditInternalResourceDialog({
|
|||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-semibold mb-4">{t("editInternalResourceDialogTargetConfiguration")}</h3>
|
<h3 className="text-lg font-semibold mb-4">{t("editInternalResourceDialogTargetConfiguration")}</h3>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<FormField
|
||||||
<FormField
|
control={form.control}
|
||||||
control={form.control}
|
name="destination"
|
||||||
name="destinationIp"
|
render={({ field }) => (
|
||||||
render={({ field }) => (
|
<FormItem>
|
||||||
<FormItem>
|
<FormLabel>{t("editInternalResourceDialogDestination")}</FormLabel>
|
||||||
<FormLabel>{t("targetAddr")}</FormLabel>
|
<FormControl>
|
||||||
<FormControl>
|
<Input {...field} />
|
||||||
<Input {...field} />
|
</FormControl>
|
||||||
</FormControl>
|
<FormDescription>
|
||||||
<FormMessage />
|
{mode === "host" && t("editInternalResourceDialogDestinationHostDescription")}
|
||||||
</FormItem>
|
{mode === "cidr" && t("editInternalResourceDialogDestinationCidrDescription")}
|
||||||
)}
|
{mode === "port" && t("editInternalResourceDialogDestinationIPDescription")}
|
||||||
/>
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{mode === "port" && (
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="destinationPort"
|
name="destinationPort"
|
||||||
@@ -339,20 +420,41 @@ export default function EditInternalResourceDialog({
|
|||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
{...field}
|
value={field.value || ""}
|
||||||
onChange={(e) => field.onChange(parseInt(e.target.value) || 0)}
|
onChange={(e) => field.onChange(e.target.value === "" ? undefined : parseInt(e.target.value) || 0)}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
)}
|
||||||
</div>
|
</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 */}
|
{/* Access Control Section */}
|
||||||
<Separator />
|
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-semibold mb-4">
|
<h3 className="text-lg font-semibold mb-4">
|
||||||
{t("resourceUsersRoles")}
|
{t("resourceUsersRoles")}
|
||||||
|
|||||||
@@ -90,12 +90,15 @@ export type InternalResourceRow = {
|
|||||||
name: string;
|
name: string;
|
||||||
orgId: string;
|
orgId: string;
|
||||||
siteName: string;
|
siteName: string;
|
||||||
protocol: string;
|
siteAddress: string | null;
|
||||||
|
mode: "host" | "cidr" | "port";
|
||||||
|
protocol: string | null;
|
||||||
proxyPort: number | null;
|
proxyPort: number | null;
|
||||||
siteId: number;
|
siteId: number;
|
||||||
siteNiceId: string;
|
siteNiceId: string;
|
||||||
destinationIp: string;
|
destination: string;
|
||||||
destinationPort: number;
|
destinationPort: number | null;
|
||||||
|
alias: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
type Site = ListSitesResponse["sites"][0];
|
type Site = ListSitesResponse["sites"][0];
|
||||||
@@ -571,24 +574,16 @@ export default function ResourcesTable({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "protocol",
|
accessorKey: "mode",
|
||||||
header: () => (<span className="p-3">{t("protocol")}</span>),
|
header: () => (<span className="p-3">{t("editInternalResourceDialogMode")}</span>),
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const resourceRow = row.original;
|
const resourceRow = row.original;
|
||||||
return <span>{resourceRow.protocol.toUpperCase()}</span>;
|
const modeLabels: Record<"host" | "cidr" | "port", string> = {
|
||||||
}
|
host: t("editInternalResourceDialogModeHost"),
|
||||||
},
|
cidr: t("editInternalResourceDialogModeCidr"),
|
||||||
{
|
port: t("editInternalResourceDialogModePort")
|
||||||
accessorKey: "proxyPort",
|
};
|
||||||
header: () => (<span className="p-3">{t("proxyPort")}</span>),
|
return <span>{modeLabels[resourceRow.mode]}</span>;
|
||||||
cell: ({ row }) => {
|
|
||||||
const resourceRow = row.original;
|
|
||||||
return (
|
|
||||||
<CopyToClipboard
|
|
||||||
text={resourceRow.proxyPort?.toString() || ""}
|
|
||||||
isLink={false}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -596,8 +591,35 @@ export default function ResourcesTable({
|
|||||||
header: () => (<span className="p-3">{t("resourcesTableDestination")}</span>),
|
header: () => (<span className="p-3">{t("resourcesTableDestination")}</span>),
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const resourceRow = row.original;
|
const resourceRow = row.original;
|
||||||
const destination = `${resourceRow.destinationIp}:${resourceRow.destinationPort}`;
|
let displayText: string;
|
||||||
return <CopyToClipboard text={destination} isLink={false} />;
|
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
|
<DialogPrimitive.Content
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
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
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
Reference in New Issue
Block a user