mirror of
https://github.com/fosrl/pangolin.git
synced 2026-04-10 11:56:36 +00:00
basic ui working
This commit is contained in:
@@ -1819,6 +1819,9 @@
|
|||||||
"editInternalResourceDialogModeCidr": "CIDR",
|
"editInternalResourceDialogModeCidr": "CIDR",
|
||||||
"editInternalResourceDialogModeHttp": "HTTP",
|
"editInternalResourceDialogModeHttp": "HTTP",
|
||||||
"editInternalResourceDialogModeHttps": "HTTPS",
|
"editInternalResourceDialogModeHttps": "HTTPS",
|
||||||
|
"editInternalResourceDialogScheme": "Scheme",
|
||||||
|
"editInternalResourceDialogEnableSsl": "Enable SSL",
|
||||||
|
"editInternalResourceDialogEnableSslDescription": "Enable SSL/TLS encryption for secure HTTPS connections to the destination.",
|
||||||
"editInternalResourceDialogDestination": "Destination",
|
"editInternalResourceDialogDestination": "Destination",
|
||||||
"editInternalResourceDialogDestinationHostDescription": "The IP address or hostname of the resource on the site's network.",
|
"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.",
|
"editInternalResourceDialogDestinationIPDescription": "The IP or hostname address of the resource on the site's network.",
|
||||||
@@ -1864,11 +1867,17 @@
|
|||||||
"createInternalResourceDialogModeCidr": "CIDR",
|
"createInternalResourceDialogModeCidr": "CIDR",
|
||||||
"createInternalResourceDialogModeHttp": "HTTP",
|
"createInternalResourceDialogModeHttp": "HTTP",
|
||||||
"createInternalResourceDialogModeHttps": "HTTPS",
|
"createInternalResourceDialogModeHttps": "HTTPS",
|
||||||
|
"scheme": "Scheme",
|
||||||
|
"createInternalResourceDialogScheme": "Scheme",
|
||||||
|
"createInternalResourceDialogEnableSsl": "Enable SSL",
|
||||||
|
"createInternalResourceDialogEnableSslDescription": "Enable SSL/TLS encryption for secure HTTPS connections to the destination.",
|
||||||
"createInternalResourceDialogDestination": "Destination",
|
"createInternalResourceDialogDestination": "Destination",
|
||||||
"createInternalResourceDialogDestinationHostDescription": "The IP address or hostname of the resource on the site's network.",
|
"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.",
|
"createInternalResourceDialogDestinationCidrDescription": "The CIDR range of the resource on the site's network.",
|
||||||
"createInternalResourceDialogAlias": "Alias",
|
"createInternalResourceDialogAlias": "Alias",
|
||||||
"createInternalResourceDialogAliasDescription": "An optional internal DNS alias for this resource.",
|
"createInternalResourceDialogAliasDescription": "An optional internal DNS alias for this resource.",
|
||||||
|
"internalResourceDownstreamSchemeRequired": "Scheme is required for HTTP resources",
|
||||||
|
"internalResourceHttpPortRequired": "Destination port is required for HTTP resources",
|
||||||
"siteConfiguration": "Configuration",
|
"siteConfiguration": "Configuration",
|
||||||
"siteAcceptClientConnections": "Accept Client Connections",
|
"siteAcceptClientConnections": "Accept Client Connections",
|
||||||
"siteAcceptClientConnectionsDescription": "Allow user devices and clients to access resources on this site. This can be changed later.",
|
"siteAcceptClientConnectionsDescription": "Allow user devices and clients to access resources on this site. This can be changed later.",
|
||||||
|
|||||||
@@ -16,6 +16,20 @@ import { Config } from "./types";
|
|||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { getNextAvailableAliasAddress } from "../ip";
|
import { getNextAvailableAliasAddress } from "../ip";
|
||||||
|
|
||||||
|
function siteResourceModeForDb(mode: "host" | "cidr" | "http" | "https"): {
|
||||||
|
mode: "host" | "cidr" | "http";
|
||||||
|
ssl: boolean;
|
||||||
|
scheme: "http" | "https" | null;
|
||||||
|
} {
|
||||||
|
if (mode === "https") {
|
||||||
|
return { mode: "http", ssl: true, scheme: "https" };
|
||||||
|
}
|
||||||
|
if (mode === "http") {
|
||||||
|
return { mode: "http", ssl: false, scheme: "http" };
|
||||||
|
}
|
||||||
|
return { mode, ssl: false, scheme: null };
|
||||||
|
}
|
||||||
|
|
||||||
export type ClientResourcesResults = {
|
export type ClientResourcesResults = {
|
||||||
newSiteResource: SiteResource;
|
newSiteResource: SiteResource;
|
||||||
oldSiteResource?: SiteResource;
|
oldSiteResource?: SiteResource;
|
||||||
@@ -76,13 +90,16 @@ export async function updateClientResources(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (existingResource) {
|
if (existingResource) {
|
||||||
|
const mappedMode = siteResourceModeForDb(resourceData.mode);
|
||||||
// Update existing resource
|
// Update existing resource
|
||||||
const [updatedResource] = await trx
|
const [updatedResource] = await trx
|
||||||
.update(siteResources)
|
.update(siteResources)
|
||||||
.set({
|
.set({
|
||||||
name: resourceData.name || resourceNiceId,
|
name: resourceData.name || resourceNiceId,
|
||||||
siteId: site.siteId,
|
siteId: site.siteId,
|
||||||
mode: resourceData.mode,
|
mode: mappedMode.mode,
|
||||||
|
ssl: mappedMode.ssl,
|
||||||
|
scheme: mappedMode.scheme,
|
||||||
destination: resourceData.destination,
|
destination: resourceData.destination,
|
||||||
destinationPort: resourceData["destination-port"],
|
destinationPort: resourceData["destination-port"],
|
||||||
enabled: true, // hardcoded for now
|
enabled: true, // hardcoded for now
|
||||||
@@ -208,9 +225,9 @@ export async function updateClientResources(
|
|||||||
oldSiteResource: existingResource
|
oldSiteResource: existingResource
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
const mappedMode = siteResourceModeForDb(resourceData.mode);
|
||||||
let aliasAddress: string | null = null;
|
let aliasAddress: string | null = null;
|
||||||
if (resourceData.mode == "host") {
|
if (mappedMode.mode === "host" || mappedMode.mode === "http") {
|
||||||
// we can only have an alias on a host
|
|
||||||
aliasAddress = await getNextAvailableAliasAddress(orgId);
|
aliasAddress = await getNextAvailableAliasAddress(orgId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,7 +239,9 @@ export async function updateClientResources(
|
|||||||
siteId: site.siteId,
|
siteId: site.siteId,
|
||||||
niceId: resourceNiceId,
|
niceId: resourceNiceId,
|
||||||
name: resourceData.name || resourceNiceId,
|
name: resourceData.name || resourceNiceId,
|
||||||
mode: resourceData.mode,
|
mode: mappedMode.mode,
|
||||||
|
ssl: mappedMode.ssl,
|
||||||
|
scheme: mappedMode.scheme,
|
||||||
destination: resourceData.destination,
|
destination: resourceData.destination,
|
||||||
destinationPort: resourceData["destination-port"],
|
destinationPort: resourceData["destination-port"],
|
||||||
enabled: true, // hardcoded for now
|
enabled: true, // hardcoded for now
|
||||||
|
|||||||
@@ -652,7 +652,7 @@ export function generateSubnetProxyTargetV2(
|
|||||||
disableIcmp,
|
disableIcmp,
|
||||||
resourceId: siteResource.siteResourceId
|
resourceId: siteResource.siteResourceId
|
||||||
};
|
};
|
||||||
} else if (siteResource.mode == "http" || siteResource.mode == "https") {
|
} else if (siteResource.mode == "http") {
|
||||||
let destination = siteResource.destination;
|
let destination = siteResource.destination;
|
||||||
// check if this is a valid ip
|
// check if this is a valid ip
|
||||||
const ipSchema = z.union([z.ipv4(), z.ipv6()]);
|
const ipSchema = z.union([z.ipv4(), z.ipv6()]);
|
||||||
@@ -667,10 +667,11 @@ export function generateSubnetProxyTargetV2(
|
|||||||
!siteResource.scheme
|
!siteResource.scheme
|
||||||
) {
|
) {
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`Site resource ${siteResource.siteResourceId} is in HTTP/HTTPS mode but is missing alias or alias address or destinationPort, skipping alias target generation.`
|
`Site resource ${siteResource.siteResourceId} is in HTTP mode but is missing alias or alias address or destinationPort or scheme, skipping alias target generation.`
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const publicProtocol = siteResource.ssl ? "https" : "http";
|
||||||
// also push a match for the alias address
|
// also push a match for the alias address
|
||||||
target = {
|
target = {
|
||||||
sourcePrefixes: [],
|
sourcePrefixes: [],
|
||||||
@@ -679,14 +680,14 @@ export function generateSubnetProxyTargetV2(
|
|||||||
portRange,
|
portRange,
|
||||||
disableIcmp,
|
disableIcmp,
|
||||||
resourceId: siteResource.siteResourceId,
|
resourceId: siteResource.siteResourceId,
|
||||||
protocol: siteResource.mode, // will be either http or https,
|
protocol: publicProtocol,
|
||||||
httpTargets: [
|
httpTargets: [
|
||||||
{
|
{
|
||||||
destAddr: siteResource.destination,
|
destAddr: siteResource.destination,
|
||||||
destPort: siteResource.destinationPort,
|
destPort: siteResource.destinationPort,
|
||||||
scheme: siteResource.scheme
|
scheme: siteResource.scheme
|
||||||
}
|
}
|
||||||
],
|
]
|
||||||
// tlsCert: "",
|
// tlsCert: "",
|
||||||
// tlsKey: ""
|
// tlsKey: ""
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -267,7 +267,7 @@ export async function getTraefikConfig(
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Query siteResources in http/https mode that have aliases - needed for cert generation
|
// Query siteResources in HTTP mode with SSL enabled and aliases — cert generation / HTTPS edge
|
||||||
const siteResourcesWithAliases = await db
|
const siteResourcesWithAliases = await db
|
||||||
.select({
|
.select({
|
||||||
siteResourceId: siteResources.siteResourceId,
|
siteResourceId: siteResources.siteResourceId,
|
||||||
@@ -280,7 +280,8 @@ export async function getTraefikConfig(
|
|||||||
and(
|
and(
|
||||||
eq(siteResources.enabled, true),
|
eq(siteResources.enabled, true),
|
||||||
isNotNull(siteResources.alias),
|
isNotNull(siteResources.alias),
|
||||||
inArray(siteResources.mode, ["http", "https"]),
|
eq(siteResources.mode, "http"),
|
||||||
|
eq(siteResources.ssl, true),
|
||||||
or(
|
or(
|
||||||
eq(sites.exitNodeId, exitNodeId),
|
eq(sites.exitNodeId, exitNodeId),
|
||||||
and(
|
and(
|
||||||
@@ -900,7 +901,7 @@ export async function getTraefikConfig(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add Traefik routes for siteResource aliases in http/https mode so that
|
// Add Traefik routes for siteResource aliases (HTTP mode + SSL) so that
|
||||||
// Traefik generates TLS certificates for those domains even when no
|
// Traefik generates TLS certificates for those domains even when no
|
||||||
// matching resource exists yet.
|
// matching resource exists yet.
|
||||||
if (siteResourcesWithAliases.length > 0) {
|
if (siteResourcesWithAliases.length > 0) {
|
||||||
|
|||||||
@@ -111,6 +111,21 @@ const createSiteResourceSchema = z
|
|||||||
{
|
{
|
||||||
message: "Destination must be a valid CIDR notation for cidr mode"
|
message: "Destination must be a valid CIDR notation for cidr mode"
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
if (data.mode !== "http") return true;
|
||||||
|
return (
|
||||||
|
data.scheme !== undefined &&
|
||||||
|
data.destinationPort !== undefined &&
|
||||||
|
data.destinationPort >= 1 &&
|
||||||
|
data.destinationPort <= 65535
|
||||||
|
);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message:
|
||||||
|
"HTTP mode requires scheme (http or https) and a valid destination port"
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
export type CreateSiteResourceBody = z.infer<typeof createSiteResourceSchema>;
|
export type CreateSiteResourceBody = z.infer<typeof createSiteResourceSchema>;
|
||||||
|
|||||||
@@ -41,12 +41,12 @@ const listAllSiteResourcesByOrgQuerySchema = z.object({
|
|||||||
}),
|
}),
|
||||||
query: z.string().optional(),
|
query: z.string().optional(),
|
||||||
mode: z
|
mode: z
|
||||||
.enum(["host", "cidr", "http", "https"])
|
.enum(["host", "cidr", "http"])
|
||||||
.optional()
|
.optional()
|
||||||
.catch(undefined)
|
.catch(undefined)
|
||||||
.openapi({
|
.openapi({
|
||||||
type: "string",
|
type: "string",
|
||||||
enum: ["host", "cidr", "http", "https"],
|
enum: ["host", "cidr", "http"],
|
||||||
description: "Filter site resources by mode"
|
description: "Filter site resources by mode"
|
||||||
}),
|
}),
|
||||||
sort_by: z
|
sort_by: z
|
||||||
@@ -88,6 +88,7 @@ function querySiteResourcesBase() {
|
|||||||
niceId: siteResources.niceId,
|
niceId: siteResources.niceId,
|
||||||
name: siteResources.name,
|
name: siteResources.name,
|
||||||
mode: siteResources.mode,
|
mode: siteResources.mode,
|
||||||
|
ssl: siteResources.ssl,
|
||||||
scheme: siteResources.scheme,
|
scheme: siteResources.scheme,
|
||||||
proxyPort: siteResources.proxyPort,
|
proxyPort: siteResources.proxyPort,
|
||||||
destinationPort: siteResources.destinationPort,
|
destinationPort: siteResources.destinationPort,
|
||||||
@@ -193,7 +194,9 @@ export async function listAllSiteResourcesByOrg(
|
|||||||
const baseQuery = querySiteResourcesBase().where(and(...conditions));
|
const baseQuery = querySiteResourcesBase().where(and(...conditions));
|
||||||
|
|
||||||
const countQuery = db.$count(
|
const countQuery = db.$count(
|
||||||
querySiteResourcesBase().where(and(...conditions)).as("filtered_site_resources")
|
querySiteResourcesBase()
|
||||||
|
.where(and(...conditions))
|
||||||
|
.as("filtered_site_resources")
|
||||||
);
|
);
|
||||||
|
|
||||||
const [siteResourcesList, totalCount] = await Promise.all([
|
const [siteResourcesList, totalCount] = await Promise.all([
|
||||||
|
|||||||
@@ -125,6 +125,23 @@ const updateSiteResourceSchema = z
|
|||||||
{
|
{
|
||||||
message: "Destination must be a valid CIDR notation for cidr mode"
|
message: "Destination must be a valid CIDR notation for cidr mode"
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
if (data.mode !== "http") return true;
|
||||||
|
return (
|
||||||
|
data.scheme !== undefined &&
|
||||||
|
data.scheme !== null &&
|
||||||
|
data.destinationPort !== undefined &&
|
||||||
|
data.destinationPort !== null &&
|
||||||
|
data.destinationPort >= 1 &&
|
||||||
|
data.destinationPort <= 65535
|
||||||
|
);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message:
|
||||||
|
"HTTP mode requires scheme (http or https) and a valid destination port"
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
export type UpdateSiteResourceBody = z.infer<typeof updateSiteResourceSchema>;
|
export type UpdateSiteResourceBody = z.infer<typeof updateSiteResourceSchema>;
|
||||||
|
|||||||
@@ -56,13 +56,25 @@ export default async function ClientResourcesPage(
|
|||||||
|
|
||||||
const internalResourceRows: InternalResourceRow[] = siteResources.map(
|
const internalResourceRows: InternalResourceRow[] = siteResources.map(
|
||||||
(siteResource) => {
|
(siteResource) => {
|
||||||
|
const rawMode = siteResource.mode as string | undefined;
|
||||||
|
const normalizedMode =
|
||||||
|
rawMode === "https"
|
||||||
|
? ("http" as const)
|
||||||
|
: rawMode === "host" || rawMode === "cidr" || rawMode === "http"
|
||||||
|
? rawMode
|
||||||
|
: ("host" as const);
|
||||||
return {
|
return {
|
||||||
id: siteResource.siteResourceId,
|
id: siteResource.siteResourceId,
|
||||||
name: siteResource.name,
|
name: siteResource.name,
|
||||||
orgId: params.orgId,
|
orgId: params.orgId,
|
||||||
siteName: siteResource.siteName,
|
siteName: siteResource.siteName,
|
||||||
siteAddress: siteResource.siteAddress || null,
|
siteAddress: siteResource.siteAddress || null,
|
||||||
mode: siteResource.mode || ("port" as any),
|
mode: normalizedMode,
|
||||||
|
scheme:
|
||||||
|
siteResource.scheme ??
|
||||||
|
(rawMode === "https" ? ("https" as const) : null),
|
||||||
|
ssl:
|
||||||
|
siteResource.ssl === true || rawMode === "https",
|
||||||
// protocol: siteResource.protocol,
|
// protocol: siteResource.protocol,
|
||||||
// proxyPort: siteResource.proxyPort,
|
// proxyPort: siteResource.proxyPort,
|
||||||
siteId: siteResource.siteId,
|
siteId: siteResource.siteId,
|
||||||
|
|||||||
@@ -46,7 +46,9 @@ export type InternalResourceRow = {
|
|||||||
siteName: string;
|
siteName: string;
|
||||||
siteAddress: string | null;
|
siteAddress: string | null;
|
||||||
// mode: "host" | "cidr" | "port";
|
// mode: "host" | "cidr" | "port";
|
||||||
mode: "host" | "cidr" | "http" | "https";
|
mode: "host" | "cidr" | "http";
|
||||||
|
scheme: "http" | "https" | null;
|
||||||
|
ssl: boolean;
|
||||||
// protocol: string | null;
|
// protocol: string | null;
|
||||||
// proxyPort: number | null;
|
// proxyPort: number | null;
|
||||||
siteId: number;
|
siteId: number;
|
||||||
@@ -64,30 +66,27 @@ export type InternalResourceRow = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function resolveHttpHttpsDisplayPort(
|
function resolveHttpHttpsDisplayPort(
|
||||||
mode: "http" | "https",
|
mode: "http",
|
||||||
httpHttpsPort: number | null
|
httpHttpsPort: number | null
|
||||||
): number {
|
): number {
|
||||||
if (httpHttpsPort != null) {
|
if (httpHttpsPort != null) {
|
||||||
return httpHttpsPort;
|
return httpHttpsPort;
|
||||||
}
|
}
|
||||||
return mode === "https" ? 443 : 80;
|
return 80;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatDestinationDisplay(row: InternalResourceRow): string {
|
function formatDestinationDisplay(row: InternalResourceRow): string {
|
||||||
const { mode, destination, httpHttpsPort } = row;
|
const { mode, destination, httpHttpsPort, scheme } = row;
|
||||||
if (mode !== "http" && mode !== "https") {
|
if (mode !== "http") {
|
||||||
return destination;
|
return destination;
|
||||||
}
|
}
|
||||||
const port = resolveHttpHttpsDisplayPort(mode, httpHttpsPort);
|
const port = resolveHttpHttpsDisplayPort(mode, httpHttpsPort);
|
||||||
|
const downstreamScheme = scheme ?? "http";
|
||||||
const hostPart =
|
const hostPart =
|
||||||
destination.includes(":") && !destination.startsWith("[")
|
destination.includes(":") && !destination.startsWith("[")
|
||||||
? `[${destination}]`
|
? `[${destination}]`
|
||||||
: destination;
|
: destination;
|
||||||
return `${hostPart}:${port}`;
|
return `${downstreamScheme}://${hostPart}:${port}`;
|
||||||
}
|
|
||||||
|
|
||||||
function formatHttpHttpsAliasUrl(mode: "http" | "https", alias: string): string {
|
|
||||||
return `${mode}://${alias}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function isSafeUrlForLink(href: string): boolean {
|
function isSafeUrlForLink(href: string): boolean {
|
||||||
@@ -255,10 +254,6 @@ export default function ClientResourcesTable({
|
|||||||
{
|
{
|
||||||
value: "http",
|
value: "http",
|
||||||
label: t("editInternalResourceDialogModeHttp")
|
label: t("editInternalResourceDialogModeHttp")
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "https",
|
|
||||||
label: t("editInternalResourceDialogModeHttps")
|
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
selectedValue={searchParams.get("mode") ?? undefined}
|
selectedValue={searchParams.get("mode") ?? undefined}
|
||||||
@@ -272,14 +267,13 @@ export default function ClientResourcesTable({
|
|||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const resourceRow = row.original;
|
const resourceRow = row.original;
|
||||||
const modeLabels: Record<
|
const modeLabels: Record<
|
||||||
"host" | "cidr" | "port" | "http" | "https",
|
"host" | "cidr" | "port" | "http",
|
||||||
string
|
string
|
||||||
> = {
|
> = {
|
||||||
host: t("editInternalResourceDialogModeHost"),
|
host: t("editInternalResourceDialogModeHost"),
|
||||||
cidr: t("editInternalResourceDialogModeCidr"),
|
cidr: t("editInternalResourceDialogModeCidr"),
|
||||||
port: t("editInternalResourceDialogModePort"),
|
port: t("editInternalResourceDialogModePort"),
|
||||||
http: t("editInternalResourceDialogModeHttp"),
|
http: t("editInternalResourceDialogModeHttp")
|
||||||
https: t("editInternalResourceDialogModeHttps")
|
|
||||||
};
|
};
|
||||||
return <span>{modeLabels[resourceRow.mode]}</span>;
|
return <span>{modeLabels[resourceRow.mode]}</span>;
|
||||||
}
|
}
|
||||||
@@ -319,15 +313,8 @@ export default function ClientResourcesTable({
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (
|
if (resourceRow.mode === "http" && resourceRow.alias) {
|
||||||
(resourceRow.mode === "http" ||
|
const url = `${resourceRow.ssl ? "https" : "http"}://${resourceRow.alias}`;
|
||||||
resourceRow.mode === "https") &&
|
|
||||||
resourceRow.alias
|
|
||||||
) {
|
|
||||||
const url = formatHttpHttpsAliasUrl(
|
|
||||||
resourceRow.mode,
|
|
||||||
resourceRow.alias
|
|
||||||
);
|
|
||||||
return (
|
return (
|
||||||
<CopyToClipboard
|
<CopyToClipboard
|
||||||
text={url}
|
text={url}
|
||||||
|
|||||||
@@ -51,9 +51,7 @@ export default function CreateInternalResourceDialog({
|
|||||||
try {
|
try {
|
||||||
let data = { ...values };
|
let data = { ...values };
|
||||||
if (
|
if (
|
||||||
(data.mode === "host" ||
|
(data.mode === "host" || data.mode === "http") &&
|
||||||
data.mode === "http" ||
|
|
||||||
data.mode === "https") &&
|
|
||||||
isHostname(data.destination)
|
isHostname(data.destination)
|
||||||
) {
|
) {
|
||||||
const currentAlias = data.alias?.trim() || "";
|
const currentAlias = data.alias?.trim() || "";
|
||||||
@@ -74,21 +72,42 @@ export default function CreateInternalResourceDialog({
|
|||||||
mode: data.mode,
|
mode: data.mode,
|
||||||
destination: data.destination,
|
destination: data.destination,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
alias: data.alias && typeof data.alias === "string" && data.alias.trim() ? data.alias : undefined,
|
...(data.mode === "http" && {
|
||||||
|
scheme: data.scheme,
|
||||||
|
ssl: data.ssl ?? false,
|
||||||
|
destinationPort: data.httpHttpsPort ?? undefined
|
||||||
|
}),
|
||||||
|
alias:
|
||||||
|
data.alias &&
|
||||||
|
typeof data.alias === "string" &&
|
||||||
|
data.alias.trim()
|
||||||
|
? data.alias
|
||||||
|
: undefined,
|
||||||
tcpPortRangeString: data.tcpPortRangeString,
|
tcpPortRangeString: data.tcpPortRangeString,
|
||||||
udpPortRangeString: data.udpPortRangeString,
|
udpPortRangeString: data.udpPortRangeString,
|
||||||
disableIcmp: data.disableIcmp ?? false,
|
disableIcmp: data.disableIcmp ?? false,
|
||||||
...(data.authDaemonMode != null && { authDaemonMode: data.authDaemonMode }),
|
...(data.authDaemonMode != null && {
|
||||||
...(data.authDaemonMode === "remote" && data.authDaemonPort != null && { authDaemonPort: data.authDaemonPort }),
|
authDaemonMode: data.authDaemonMode
|
||||||
roleIds: data.roles ? data.roles.map((r) => parseInt(r.id)) : [],
|
}),
|
||||||
|
...(data.authDaemonMode === "remote" &&
|
||||||
|
data.authDaemonPort != null && {
|
||||||
|
authDaemonPort: data.authDaemonPort
|
||||||
|
}),
|
||||||
|
roleIds: data.roles
|
||||||
|
? data.roles.map((r) => parseInt(r.id))
|
||||||
|
: [],
|
||||||
userIds: data.users ? data.users.map((u) => u.id) : [],
|
userIds: data.users ? data.users.map((u) => u.id) : [],
|
||||||
clientIds: data.clients ? data.clients.map((c) => parseInt(c.id)) : []
|
clientIds: data.clients
|
||||||
|
? data.clients.map((c) => parseInt(c.id))
|
||||||
|
: []
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: t("createInternalResourceDialogSuccess"),
|
title: t("createInternalResourceDialogSuccess"),
|
||||||
description: t("createInternalResourceDialogInternalResourceCreatedSuccessfully"),
|
description: t(
|
||||||
|
"createInternalResourceDialogInternalResourceCreatedSuccessfully"
|
||||||
|
),
|
||||||
variant: "default"
|
variant: "default"
|
||||||
});
|
});
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
@@ -98,7 +117,9 @@ export default function CreateInternalResourceDialog({
|
|||||||
title: t("createInternalResourceDialogError"),
|
title: t("createInternalResourceDialogError"),
|
||||||
description: formatAxiosError(
|
description: formatAxiosError(
|
||||||
error,
|
error,
|
||||||
t("createInternalResourceDialogFailedToCreateInternalResource")
|
t(
|
||||||
|
"createInternalResourceDialogFailedToCreateInternalResource"
|
||||||
|
)
|
||||||
),
|
),
|
||||||
variant: "destructive"
|
variant: "destructive"
|
||||||
});
|
});
|
||||||
@@ -111,9 +132,13 @@ export default function CreateInternalResourceDialog({
|
|||||||
<Credenza open={open} onOpenChange={setOpen}>
|
<Credenza open={open} onOpenChange={setOpen}>
|
||||||
<CredenzaContent className="max-w-3xl">
|
<CredenzaContent className="max-w-3xl">
|
||||||
<CredenzaHeader>
|
<CredenzaHeader>
|
||||||
<CredenzaTitle>{t("createInternalResourceDialogCreateClientResource")}</CredenzaTitle>
|
<CredenzaTitle>
|
||||||
|
{t("createInternalResourceDialogCreateClientResource")}
|
||||||
|
</CredenzaTitle>
|
||||||
<CredenzaDescription>
|
<CredenzaDescription>
|
||||||
{t("createInternalResourceDialogCreateClientResourceDescription")}
|
{t(
|
||||||
|
"createInternalResourceDialogCreateClientResourceDescription"
|
||||||
|
)}
|
||||||
</CredenzaDescription>
|
</CredenzaDescription>
|
||||||
</CredenzaHeader>
|
</CredenzaHeader>
|
||||||
<CredenzaBody>
|
<CredenzaBody>
|
||||||
@@ -128,7 +153,11 @@ export default function CreateInternalResourceDialog({
|
|||||||
</CredenzaBody>
|
</CredenzaBody>
|
||||||
<CredenzaFooter>
|
<CredenzaFooter>
|
||||||
<CredenzaClose asChild>
|
<CredenzaClose asChild>
|
||||||
<Button variant="outline" onClick={() => setOpen(false)} disabled={isSubmitting}>
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => setOpen(false)}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
>
|
||||||
{t("createInternalResourceDialogCancel")}
|
{t("createInternalResourceDialogCancel")}
|
||||||
</Button>
|
</Button>
|
||||||
</CredenzaClose>
|
</CredenzaClose>
|
||||||
|
|||||||
@@ -55,9 +55,7 @@ export default function EditInternalResourceDialog({
|
|||||||
try {
|
try {
|
||||||
let data = { ...values };
|
let data = { ...values };
|
||||||
if (
|
if (
|
||||||
(data.mode === "host" ||
|
(data.mode === "host" || data.mode === "http") &&
|
||||||
data.mode === "http" ||
|
|
||||||
data.mode === "https") &&
|
|
||||||
isHostname(data.destination)
|
isHostname(data.destination)
|
||||||
) {
|
) {
|
||||||
const currentAlias = data.alias?.trim() || "";
|
const currentAlias = data.alias?.trim() || "";
|
||||||
@@ -76,6 +74,11 @@ export default function EditInternalResourceDialog({
|
|||||||
mode: data.mode,
|
mode: data.mode,
|
||||||
niceId: data.niceId,
|
niceId: data.niceId,
|
||||||
destination: data.destination,
|
destination: data.destination,
|
||||||
|
...(data.mode === "http" && {
|
||||||
|
scheme: data.scheme,
|
||||||
|
ssl: data.ssl ?? false,
|
||||||
|
destinationPort: data.httpHttpsPort ?? null
|
||||||
|
}),
|
||||||
alias:
|
alias:
|
||||||
data.alias &&
|
data.alias &&
|
||||||
typeof data.alias === "string" &&
|
typeof data.alias === "string" &&
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ import { SitesSelector, type Selectedsite } from "./site-selector";
|
|||||||
import { CaretSortIcon } from "@radix-ui/react-icons";
|
import { CaretSortIcon } from "@radix-ui/react-icons";
|
||||||
import { MachinesSelector } from "./machines-selector";
|
import { MachinesSelector } from "./machines-selector";
|
||||||
import DomainPicker from "@app/components/DomainPicker";
|
import DomainPicker from "@app/components/DomainPicker";
|
||||||
|
import { SwitchInput } from "@app/components/SwitchInput";
|
||||||
|
|
||||||
// --- Helpers (shared) ---
|
// --- Helpers (shared) ---
|
||||||
|
|
||||||
@@ -121,7 +122,7 @@ export const cleanForFQDN = (name: string): string =>
|
|||||||
|
|
||||||
type Site = ListSitesResponse["sites"][0];
|
type Site = ListSitesResponse["sites"][0];
|
||||||
|
|
||||||
export type InternalResourceMode = "host" | "cidr" | "http" | "https";
|
export type InternalResourceMode = "host" | "cidr" | "http";
|
||||||
|
|
||||||
export type InternalResourceData = {
|
export type InternalResourceData = {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -139,6 +140,8 @@ export type InternalResourceData = {
|
|||||||
authDaemonMode?: "site" | "remote" | null;
|
authDaemonMode?: "site" | "remote" | null;
|
||||||
authDaemonPort?: number | null;
|
authDaemonPort?: number | null;
|
||||||
httpHttpsPort?: number | null;
|
httpHttpsPort?: number | null;
|
||||||
|
scheme?: "http" | "https" | null;
|
||||||
|
ssl?: boolean;
|
||||||
httpConfigSubdomain?: string | null;
|
httpConfigSubdomain?: string | null;
|
||||||
httpConfigDomainId?: string | null;
|
httpConfigDomainId?: string | null;
|
||||||
httpConfigFullDomain?: string | null;
|
httpConfigFullDomain?: string | null;
|
||||||
@@ -159,6 +162,8 @@ export type InternalResourceFormValues = {
|
|||||||
authDaemonMode?: "site" | "remote" | null;
|
authDaemonMode?: "site" | "remote" | null;
|
||||||
authDaemonPort?: number | null;
|
authDaemonPort?: number | null;
|
||||||
httpHttpsPort?: number | null;
|
httpHttpsPort?: number | null;
|
||||||
|
scheme?: "http" | "https";
|
||||||
|
ssl?: boolean;
|
||||||
httpConfigSubdomain?: string | null;
|
httpConfigSubdomain?: string | null;
|
||||||
httpConfigDomainId?: string | null;
|
httpConfigDomainId?: string | null;
|
||||||
httpConfigFullDomain?: string | null;
|
httpConfigFullDomain?: string | null;
|
||||||
@@ -226,10 +231,18 @@ export function InternalResourceForm({
|
|||||||
variant === "create"
|
variant === "create"
|
||||||
? "createInternalResourceDialogModeHttp"
|
? "createInternalResourceDialogModeHttp"
|
||||||
: "editInternalResourceDialogModeHttp";
|
: "editInternalResourceDialogModeHttp";
|
||||||
const modeHttpsKey =
|
const schemeLabelKey =
|
||||||
variant === "create"
|
variant === "create"
|
||||||
? "createInternalResourceDialogModeHttps"
|
? "createInternalResourceDialogScheme"
|
||||||
: "editInternalResourceDialogModeHttps";
|
: "editInternalResourceDialogScheme";
|
||||||
|
const enableSslLabelKey =
|
||||||
|
variant === "create"
|
||||||
|
? "createInternalResourceDialogEnableSsl"
|
||||||
|
: "editInternalResourceDialogEnableSsl";
|
||||||
|
const enableSslDescriptionKey =
|
||||||
|
variant === "create"
|
||||||
|
? "createInternalResourceDialogEnableSslDescription"
|
||||||
|
: "editInternalResourceDialogEnableSslDescription";
|
||||||
const destinationLabelKey =
|
const destinationLabelKey =
|
||||||
variant === "create"
|
variant === "create"
|
||||||
? "createInternalResourceDialogDestination"
|
? "createInternalResourceDialogDestination"
|
||||||
@@ -255,48 +268,78 @@ export function InternalResourceForm({
|
|||||||
? "createInternalResourceDialogHttpConfigurationDescription"
|
? "createInternalResourceDialogHttpConfigurationDescription"
|
||||||
: "editInternalResourceDialogHttpConfigurationDescription";
|
: "editInternalResourceDialogHttpConfigurationDescription";
|
||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z
|
||||||
name: z.string().min(1, t(nameRequiredKey)).max(255, t(nameMaxKey)),
|
.object({
|
||||||
siteId: z
|
name: z.string().min(1, t(nameRequiredKey)).max(255, t(nameMaxKey)),
|
||||||
.number()
|
siteId: z
|
||||||
.int()
|
.number()
|
||||||
.positive(siteRequiredKey ? t(siteRequiredKey) : undefined),
|
.int()
|
||||||
mode: z.enum(["host", "cidr", "http", "https"]),
|
.positive(siteRequiredKey ? t(siteRequiredKey) : undefined),
|
||||||
destination: z
|
mode: z.enum(["host", "cidr", "http"]),
|
||||||
.string()
|
destination: z
|
||||||
.min(
|
.string()
|
||||||
1,
|
.min(
|
||||||
destinationRequiredKey
|
1,
|
||||||
? { message: t(destinationRequiredKey) }
|
destinationRequiredKey
|
||||||
: undefined
|
? { message: t(destinationRequiredKey) }
|
||||||
),
|
: undefined
|
||||||
alias: z.string().nullish(),
|
),
|
||||||
httpHttpsPort: z.number().int().min(1).max(65535).optional().nullable(),
|
alias: z.string().nullish(),
|
||||||
httpConfigSubdomain: z.string().nullish(),
|
httpHttpsPort: z
|
||||||
httpConfigDomainId: z.string().nullish(),
|
.number()
|
||||||
httpConfigFullDomain: z.string().nullish(),
|
.int()
|
||||||
niceId: z
|
.min(1)
|
||||||
.string()
|
.max(65535)
|
||||||
.min(1)
|
.optional()
|
||||||
.max(255)
|
.nullable(),
|
||||||
.regex(/^[a-zA-Z0-9-]+$/)
|
scheme: z.enum(["http", "https"]).optional(),
|
||||||
.optional(),
|
ssl: z.boolean().optional(),
|
||||||
tcpPortRangeString: createPortRangeStringSchema(t),
|
httpConfigSubdomain: z.string().nullish(),
|
||||||
udpPortRangeString: createPortRangeStringSchema(t),
|
httpConfigDomainId: z.string().nullish(),
|
||||||
disableIcmp: z.boolean().optional(),
|
httpConfigFullDomain: z.string().nullish(),
|
||||||
authDaemonMode: z.enum(["site", "remote"]).optional().nullable(),
|
niceId: z
|
||||||
authDaemonPort: z.number().int().positive().optional().nullable(),
|
.string()
|
||||||
roles: z.array(tagSchema).optional(),
|
.min(1)
|
||||||
users: z.array(tagSchema).optional(),
|
.max(255)
|
||||||
clients: z
|
.regex(/^[a-zA-Z0-9-]+$/)
|
||||||
.array(
|
.optional(),
|
||||||
z.object({
|
tcpPortRangeString: createPortRangeStringSchema(t),
|
||||||
clientId: z.number(),
|
udpPortRangeString: createPortRangeStringSchema(t),
|
||||||
name: z.string()
|
disableIcmp: z.boolean().optional(),
|
||||||
})
|
authDaemonMode: z.enum(["site", "remote"]).optional().nullable(),
|
||||||
)
|
authDaemonPort: z.number().int().positive().optional().nullable(),
|
||||||
.optional()
|
roles: z.array(tagSchema).optional(),
|
||||||
});
|
users: z.array(tagSchema).optional(),
|
||||||
|
clients: z
|
||||||
|
.array(
|
||||||
|
z.object({
|
||||||
|
clientId: z.number(),
|
||||||
|
name: z.string()
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.optional()
|
||||||
|
})
|
||||||
|
.superRefine((data, ctx) => {
|
||||||
|
if (data.mode !== "http") return;
|
||||||
|
if (!data.scheme) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
message: t("internalResourceDownstreamSchemeRequired"),
|
||||||
|
path: ["scheme"]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
data.httpHttpsPort == null ||
|
||||||
|
!Number.isFinite(data.httpHttpsPort) ||
|
||||||
|
data.httpHttpsPort < 1
|
||||||
|
) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
message: t("internalResourceHttpPortRequired"),
|
||||||
|
path: ["httpHttpsPort"]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
type FormData = z.infer<typeof formSchema>;
|
type FormData = z.infer<typeof formSchema>;
|
||||||
|
|
||||||
@@ -430,6 +473,8 @@ export function InternalResourceForm({
|
|||||||
authDaemonMode: resource.authDaemonMode ?? "site",
|
authDaemonMode: resource.authDaemonMode ?? "site",
|
||||||
authDaemonPort: resource.authDaemonPort ?? null,
|
authDaemonPort: resource.authDaemonPort ?? null,
|
||||||
httpHttpsPort: resource.httpHttpsPort ?? null,
|
httpHttpsPort: resource.httpHttpsPort ?? null,
|
||||||
|
scheme: resource.scheme ?? "http",
|
||||||
|
ssl: resource.ssl ?? false,
|
||||||
httpConfigSubdomain: resource.httpConfigSubdomain ?? null,
|
httpConfigSubdomain: resource.httpConfigSubdomain ?? null,
|
||||||
httpConfigDomainId: resource.httpConfigDomainId ?? null,
|
httpConfigDomainId: resource.httpConfigDomainId ?? null,
|
||||||
httpConfigFullDomain: resource.httpConfigFullDomain ?? null,
|
httpConfigFullDomain: resource.httpConfigFullDomain ?? null,
|
||||||
@@ -445,6 +490,8 @@ export function InternalResourceForm({
|
|||||||
destination: "",
|
destination: "",
|
||||||
alias: null,
|
alias: null,
|
||||||
httpHttpsPort: null,
|
httpHttpsPort: null,
|
||||||
|
scheme: "http",
|
||||||
|
ssl: false,
|
||||||
httpConfigSubdomain: null,
|
httpConfigSubdomain: null,
|
||||||
httpConfigDomainId: null,
|
httpConfigDomainId: null,
|
||||||
httpConfigFullDomain: null,
|
httpConfigFullDomain: null,
|
||||||
@@ -471,7 +518,7 @@ export function InternalResourceForm({
|
|||||||
const httpConfigSubdomain = form.watch("httpConfigSubdomain");
|
const httpConfigSubdomain = form.watch("httpConfigSubdomain");
|
||||||
const httpConfigDomainId = form.watch("httpConfigDomainId");
|
const httpConfigDomainId = form.watch("httpConfigDomainId");
|
||||||
const httpConfigFullDomain = form.watch("httpConfigFullDomain");
|
const httpConfigFullDomain = form.watch("httpConfigFullDomain");
|
||||||
const isHttpOrHttps = mode === "http" || mode === "https";
|
const isHttpMode = mode === "http";
|
||||||
const authDaemonMode = form.watch("authDaemonMode") ?? "site";
|
const authDaemonMode = form.watch("authDaemonMode") ?? "site";
|
||||||
const hasInitialized = useRef(false);
|
const hasInitialized = useRef(false);
|
||||||
const previousResourceId = useRef<number | null>(null);
|
const previousResourceId = useRef<number | null>(null);
|
||||||
@@ -496,6 +543,8 @@ export function InternalResourceForm({
|
|||||||
destination: "",
|
destination: "",
|
||||||
alias: null,
|
alias: null,
|
||||||
httpHttpsPort: null,
|
httpHttpsPort: null,
|
||||||
|
scheme: "http",
|
||||||
|
ssl: false,
|
||||||
httpConfigSubdomain: null,
|
httpConfigSubdomain: null,
|
||||||
httpConfigDomainId: null,
|
httpConfigDomainId: null,
|
||||||
httpConfigFullDomain: null,
|
httpConfigFullDomain: null,
|
||||||
@@ -527,6 +576,8 @@ export function InternalResourceForm({
|
|||||||
destination: resource.destination ?? "",
|
destination: resource.destination ?? "",
|
||||||
alias: resource.alias ?? null,
|
alias: resource.alias ?? null,
|
||||||
httpHttpsPort: resource.httpHttpsPort ?? null,
|
httpHttpsPort: resource.httpHttpsPort ?? null,
|
||||||
|
scheme: resource.scheme ?? "http",
|
||||||
|
ssl: resource.ssl ?? false,
|
||||||
httpConfigSubdomain: resource.httpConfigSubdomain ?? null,
|
httpConfigSubdomain: resource.httpConfigSubdomain ?? null,
|
||||||
httpConfigDomainId: resource.httpConfigDomainId ?? null,
|
httpConfigDomainId: resource.httpConfigDomainId ?? null,
|
||||||
httpConfigFullDomain: resource.httpConfigFullDomain ?? null,
|
httpConfigFullDomain: resource.httpConfigFullDomain ?? null,
|
||||||
@@ -681,6 +732,37 @@ export function InternalResourceForm({
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="mode"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>{t(modeLabelKey)}</FormLabel>
|
||||||
|
<Select
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
value={field.value}
|
||||||
|
>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="host">
|
||||||
|
{t(modeHostKey)}
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="cidr">
|
||||||
|
{t(modeCidrKey)}
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="http">
|
||||||
|
{t(modeHttpKey)}
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<HorizontalTabs
|
<HorizontalTabs
|
||||||
@@ -718,63 +800,56 @@ export function InternalResourceForm({
|
|||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"grid gap-4 items-start",
|
"grid gap-4 items-start",
|
||||||
mode === "cidr"
|
mode === "cidr" && "grid-cols-1",
|
||||||
? "grid-cols-4"
|
mode === "http" && "grid-cols-3",
|
||||||
: "grid-cols-12"
|
mode === "host" && "grid-cols-2"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
{mode === "http" && (
|
||||||
|
<div className="min-w-0">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="scheme"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
{t(schemeLabelKey)}
|
||||||
|
</FormLabel>
|
||||||
|
<Select
|
||||||
|
onValueChange={
|
||||||
|
field.onChange
|
||||||
|
}
|
||||||
|
value={
|
||||||
|
field.value ??
|
||||||
|
"http"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger className="w-full">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="http">
|
||||||
|
http
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="https">
|
||||||
|
https
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div
|
<div
|
||||||
className={
|
className={cn(
|
||||||
mode === "cidr"
|
mode === "cidr" && "col-span-1",
|
||||||
? "col-span-1"
|
(mode === "http" || mode === "host") &&
|
||||||
: "col-span-3"
|
"min-w-0"
|
||||||
}
|
)}
|
||||||
>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="mode"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>
|
|
||||||
{t(modeLabelKey)}
|
|
||||||
</FormLabel>
|
|
||||||
<Select
|
|
||||||
onValueChange={
|
|
||||||
field.onChange
|
|
||||||
}
|
|
||||||
value={field.value}
|
|
||||||
>
|
|
||||||
<FormControl>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
</FormControl>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="host">
|
|
||||||
{t(modeHostKey)}
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value="cidr">
|
|
||||||
{t(modeCidrKey)}
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value="http">
|
|
||||||
{t(modeHttpKey)}
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value="https">
|
|
||||||
{t(modeHttpsKey)}
|
|
||||||
</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className={
|
|
||||||
mode === "cidr"
|
|
||||||
? "col-span-3"
|
|
||||||
: "col-span-5"
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
@@ -785,7 +860,7 @@ export function InternalResourceForm({
|
|||||||
{t(destinationLabelKey)}
|
{t(destinationLabelKey)}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input {...field} />
|
<Input {...field} className="w-full" />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
@@ -793,7 +868,7 @@ export function InternalResourceForm({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{mode === "host" && (
|
{mode === "host" && (
|
||||||
<div className="col-span-4">
|
<div className="min-w-0">
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="alias"
|
name="alias"
|
||||||
@@ -805,6 +880,7 @@ export function InternalResourceForm({
|
|||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
{...field}
|
{...field}
|
||||||
|
className="w-full"
|
||||||
value={
|
value={
|
||||||
field.value ??
|
field.value ??
|
||||||
""
|
""
|
||||||
@@ -817,8 +893,8 @@ export function InternalResourceForm({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{(mode === "http" || mode === "https") && (
|
{mode === "http" && (
|
||||||
<div className="col-span-4">
|
<div className="min-w-0">
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="httpHttpsPort"
|
name="httpHttpsPort"
|
||||||
@@ -831,6 +907,7 @@ export function InternalResourceForm({
|
|||||||
</FormLabel>
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
|
className="w-full"
|
||||||
type="number"
|
type="number"
|
||||||
min={1}
|
min={1}
|
||||||
max={65535}
|
max={65535}
|
||||||
@@ -842,16 +919,16 @@ export function InternalResourceForm({
|
|||||||
const raw =
|
const raw =
|
||||||
e.target
|
e.target
|
||||||
.value;
|
.value;
|
||||||
if (
|
if (raw === "") {
|
||||||
raw === ""
|
|
||||||
) {
|
|
||||||
field.onChange(
|
field.onChange(
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const n =
|
const n =
|
||||||
Number(raw);
|
Number(
|
||||||
|
raw
|
||||||
|
);
|
||||||
field.onChange(
|
field.onChange(
|
||||||
Number.isFinite(
|
Number.isFinite(
|
||||||
n
|
n
|
||||||
@@ -871,7 +948,7 @@ export function InternalResourceForm({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isHttpOrHttps ? (
|
{isHttpMode ? (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="my-8">
|
<div className="my-8">
|
||||||
<label className="font-medium block">
|
<label className="font-medium block">
|
||||||
@@ -881,6 +958,29 @@ export function InternalResourceForm({
|
|||||||
{t(httpConfigurationDescriptionKey)}
|
{t(httpConfigurationDescriptionKey)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="ssl"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<SwitchInput
|
||||||
|
id="internal-resource-ssl"
|
||||||
|
label={t(
|
||||||
|
enableSslLabelKey
|
||||||
|
)}
|
||||||
|
description={t(
|
||||||
|
enableSslDescriptionKey
|
||||||
|
)}
|
||||||
|
checked={!!field.value}
|
||||||
|
onCheckedChange={
|
||||||
|
field.onChange
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<DomainPicker
|
<DomainPicker
|
||||||
key={
|
key={
|
||||||
variant === "edit" && siteResourceId
|
variant === "edit" && siteResourceId
|
||||||
@@ -913,6 +1013,7 @@ export function InternalResourceForm({
|
|||||||
"httpConfigFullDomain",
|
"httpConfigFullDomain",
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
|
form.setValue("alias", null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
form.setValue(
|
form.setValue(
|
||||||
@@ -927,6 +1028,7 @@ export function InternalResourceForm({
|
|||||||
"httpConfigFullDomain",
|
"httpConfigFullDomain",
|
||||||
res.fullDomain
|
res.fullDomain
|
||||||
);
|
);
|
||||||
|
form.setValue("alias", res.fullDomain);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user