Compare commits

...

5 Commits

Author SHA1 Message Date
Owen 64475f5b1d Send gateway resources downstream like cidr resources to newt 2026-09-23 15:24:33 -04:00
Owen 45c00f3159 Dont show the gateway resources in the table 2026-09-23 15:24:17 -04:00
Owen 47f1156afe Create gateway resources in blueprints 2026-09-23 14:54:00 -04:00
Owen d143e4762a Can create gateway resource 2026-09-23 14:48:21 -04:00
Owen b7cde83e8b Adding gateway resources 2026-09-23 14:48:21 -04:00
16 changed files with 332 additions and 83 deletions
+6
View File
@@ -307,6 +307,8 @@
"privateResourceTypeCidrDescription": "Expose a CIDR range on the site network to connected clients",
"privateResourceTypeHttpDescription": "Access an HTTP or HTTPS service through a domain",
"privateResourceTypeSshDescription": "Access an SSH server from connected clients",
"privateResourceTypeGatewayDescription": "Send all internet traffic to exit through the site network.",
"resourceTypeGatewayDescription": "Send all internet traffic to exit through the site network.",
"resourceDomainDescription": "The resource will be served at this fully qualified domain name.",
"resourceHTTPSSettings": "HTTPS Settings",
"resourceHTTPSSettingsDescription": "Configure how the resource will be accessed over HTTPS",
@@ -2927,6 +2929,7 @@
"editInternalResourceDialogModePort": "Port",
"editInternalResourceDialogModeHost": "Host",
"editInternalResourceDialogModeCidr": "CIDR",
"editInternalResourceDialogModeGateway": "Exit Node",
"editInternalResourceDialogModeHttp": "HTTP",
"editInternalResourceDialogModeHttps": "HTTPS",
"editInternalResourceDialogModeInference": "AI Gateway",
@@ -2938,6 +2941,7 @@
"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.",
"editInternalResourceDialogDestinationGatewayDescription": "The sites to uses as exit nodes for this resource",
"editInternalResourceDialogAlias": "Alias",
"editInternalResourceDialogAliasDescription": "An optional internal DNS alias for this resource.",
"createInternalResourceDialogNoSitesAvailable": "No Sites Available",
@@ -2952,6 +2956,7 @@
"privateResourceNetworkAccessDescription": "Control TCP/UDP port access and whether ICMP ping is allowed for this resource.",
"hostSettings": "Host",
"cidrSettings": "CIDR",
"gatewaySettings": "Exit Node",
"createInternalResourceDialogResourceProperties": "Resource Properties",
"createInternalResourceDialogName": "Name",
"createInternalResourceDialogSite": "Site",
@@ -2990,6 +2995,7 @@
"createInternalResourceDialogModeHttps": "HTTPS",
"createInternalResourceDialogModeSsh": "SSH",
"createInternalResourceDialogModeInference": "AI Gateway",
"createInternalResourceDialogModeGateway": "Exit Node",
"scheme": "Scheme",
"createInternalResourceDialogScheme": "Scheme",
"createInternalResourceDialogEnableSsl": "Enable TLS",
+2 -2
View File
@@ -492,8 +492,8 @@ export const siteResources = pgTable(
name: varchar("name").notNull(),
ssl: boolean("ssl").notNull().default(false),
mode: varchar("mode")
.$type<"host" | "cidr" | "http" | "ssh" | "inference">()
.notNull(), // "host" | "cidr" | "http"
.$type<"host" | "cidr" | "http" | "ssh" | "inference" | "gateway">()
.notNull(),
scheme: varchar("scheme").$type<"http" | "https">(), // only for when we are doing https or http mode
proxyPort: integer("proxyPort"), // only for port mode
destinationPort: integer("destinationPort"), // only for port mode
+2 -2
View File
@@ -513,8 +513,8 @@ export const siteResources = sqliteTable("siteResources", {
name: text("name").notNull(),
ssl: integer("ssl", { mode: "boolean" }).notNull().default(false),
mode: text("mode")
.$type<"host" | "cidr" | "http" | "ssh" | "inference">()
.notNull(), // "host" | "cidr" | "http"
.$type<"host" | "cidr" | "http" | "ssh" | "inference" | "gateway">()
.notNull(),
scheme: text("scheme").$type<"http" | "https">(), // only for when we are doing https or http mode
proxyPort: integer("proxyPort"), // only for port mode
destinationPort: integer("destinationPort"), // only for port mode
+36 -16
View File
@@ -259,6 +259,11 @@ export async function updatePrivateResources(
}
const isInference = resourceData.mode === "inference";
const isGateway = resourceData.mode === "gateway";
// gateway resources always route the whole subnet with everything open
const effectiveDestination = isGateway
? "0.0.0.0/0"
: resourceData.destination;
// Update existing resource
const [updatedResource] = await trx
@@ -268,23 +273,28 @@ export async function updatePrivateResources(
mode: resourceData.mode,
ssl: resourceSsl,
scheme: resourceData.scheme,
destination: resourceData.destination,
destination: effectiveDestination,
destinationPort: resourceData["destination-port"],
enabled: resourceEnabled,
alias: resourceData.alias || null,
disableIcmp:
resourceData["disable-icmp"] ||
(resourceData.mode == "http" || isInference
? true
: false), // default to true for http/inference resources, otherwise false
disableIcmp: isGateway
? false // gateway always allows icmp
: resourceData["disable-icmp"] ||
(resourceData.mode == "http" || isInference
? true
: false), // default to true for http/inference resources, otherwise false
tcpPortRangeString:
resourceData.mode == "http" || isInference
? "443,80"
: resourceData["tcp-ports"],
: isGateway
? "*"
: resourceData["tcp-ports"],
udpPortRangeString:
resourceData.mode == "http" || isInference
? ""
: resourceData["udp-ports"],
: isGateway
? "*"
: resourceData["udp-ports"],
fullDomain: resourceData["full-domain"] || null,
subdomain: domainInfo ? domainInfo.subdomain : null,
domainId: domainInfo ? domainInfo.domainId : null,
@@ -529,6 +539,11 @@ export async function updatePrivateResources(
}
const isInference = resourceData.mode === "inference";
const isGateway = resourceData.mode === "gateway";
// gateway resources always route the whole subnet with everything open
const effectiveDestination = isGateway
? "0.0.0.0/0"
: resourceData.destination;
let domainInfo:
| { subdomain: string | null; domainId: string }
@@ -590,24 +605,29 @@ export async function updatePrivateResources(
mode: resourceData.mode,
ssl: resourceSsl,
scheme: resourceData.scheme,
destination: resourceData.destination,
destination: effectiveDestination,
destinationPort: resourceData["destination-port"],
enabled: resourceEnabled,
alias: resourceData.alias || null,
aliasAddress: aliasAddress,
disableIcmp:
resourceData["disable-icmp"] ||
(resourceData.mode == "http" || isInference
? true
: false), // default to true for http/inference resources, otherwise false
disableIcmp: isGateway
? false // gateway always allows icmp
: resourceData["disable-icmp"] ||
(resourceData.mode == "http" || isInference
? true
: false), // default to true for http/inference resources, otherwise false
tcpPortRangeString:
resourceData.mode == "http" || isInference
? "443,80"
: resourceData["tcp-ports"],
: isGateway
? "*"
: resourceData["tcp-ports"],
udpPortRangeString:
resourceData.mode == "http" || isInference
? ""
: resourceData["udp-ports"],
: isGateway
? "*"
: resourceData["udp-ports"],
fullDomain: resourceData["full-domain"] || null,
subdomain: domainInfo ? domainInfo.subdomain : null,
domainId: domainInfo ? domainInfo.domainId : null,
+5 -3
View File
@@ -612,7 +612,7 @@ export function isTargetsOnlyResource(resource: any): boolean {
export const PrivateResourceSchema = z
.object({
name: z.string().min(1).max(255),
mode: z.enum(["host", "cidr", "http", "ssh", "inference"]),
mode: z.enum(["host", "cidr", "http", "ssh", "inference", "gateway"]),
site: z.string().optional(), // DEPRECATED IN FAVOR OF sites
sites: z.array(z.string()).optional().default([]),
// protocol: z.enum(["tcp", "udp"]).optional(),
@@ -652,13 +652,15 @@ export const PrivateResourceSchema = z
})
.refine(
(data) => {
// destination is optional only for ssh+native or inference; required for everything else
// destination is optional only for ssh+native, inference, or gateway
// (gateway always routes the whole subnet, so destination is ignored); required for everything else
const isNativeSSH =
data.mode === "ssh" &&
(data["auth-daemon"] === undefined ||
data["auth-daemon"].mode === "native");
if (
data.mode !== "inference" &&
data.mode !== "gateway" &&
!isNativeSSH &&
!data.destination
) {
@@ -669,7 +671,7 @@ export const PrivateResourceSchema = z
{
path: ["destination"],
message:
"destination is required unless mode is 'ssh' with auth-daemon mode 'native', or mode is 'inference'"
"destination is required unless mode is 'ssh' with auth-daemon mode 'native', 'inference', or 'gateway'"
}
)
.refine(
@@ -17,7 +17,6 @@ import {
performDeleteSiteResources,
runSiteResourceDeleteSideEffects
} from "@server/lib/deleteSiteResource";
import logger from "@server/logger";
export const MAX_SITE_ASSOCIATED_RESOURCES_FOR_BULK_DELETE = 250;
+1 -1
View File
@@ -808,7 +808,7 @@ export async function generateSubnetProxyTargetV2(
resourceId: siteResource.siteResourceId
});
}
} else if (siteResource.mode == "cidr") {
} else if (siteResource.mode == "cidr" || siteResource.mode == "gateway") {
targets.push({
sourcePrefixes: [],
destPrefix: siteResource.destination!,
@@ -53,7 +53,7 @@ const createSiteResourceSchema = z
name: z.string().min(1).max(255),
niceId: z.string().optional(),
// protocol: z.enum(["tcp", "udp"]).optional(),
mode: z.enum(["host", "cidr", "http", "ssh", "inference"]),
mode: z.enum(["host", "cidr", "http", "ssh", "inference", "gateway"]),
ssl: z.boolean().optional(), // only used for http mode
scheme: z.enum(["http", "https"]).optional(),
siteIds: z.array(z.int()).optional(),
@@ -165,10 +165,11 @@ const createSiteResourceSchema = z
)
.refine(
(data) => {
// destination is only optional for ssh mode with native authDaemonMode or inference
// destination is only optional for ssh mode with native authDaemonMode, inference, or gateway
if (
(data.mode === "ssh" && data.authDaemonMode === "native") ||
data.mode == "inference"
data.mode == "inference" ||
data.mode == "gateway"
) {
return true;
}
@@ -179,7 +180,7 @@ const createSiteResourceSchema = z
},
{
message:
"Destination is required unless mode is ssh with authDaemonMode native or inference"
"Destination is required unless mode is ssh with authDaemonMode native, inference, or gateway"
}
)
.refine(
@@ -447,14 +448,18 @@ export async function createSiteResource(
);
}
// gateway resources always route the whole subnet with everything open
const effectiveDestination =
mode === "gateway" ? "0.0.0.0/0" : destination;
// Only check if destination is an IP address
const isIp = z
.union([z.ipv4(), z.ipv6()])
.safeParse(destination).success;
.safeParse(effectiveDestination).success;
if (
isIp &&
(isIpInCidr(destination!, org.subnet) ||
isIpInCidr(destination!, org.utilitySubnet))
(isIpInCidr(effectiveDestination!, org.subnet) ||
isIpInCidr(effectiveDestination!, org.utilitySubnet))
) {
return next(
createHttpError(
@@ -584,6 +589,32 @@ export async function createSiteResource(
tcpPortRangeStringAdjusted = destinationPort
? destinationPort.toString()
: "22";
} else if (mode === "gateway") {
tcpPortRangeStringAdjusted = "*";
}
let udpPortRangeStringAdjusted = udpPortRangeString;
if (mode === "gateway") {
udpPortRangeStringAdjusted = "*";
} else if (
mode === "http" ||
mode === "ssh" ||
mode === "inference"
) {
udpPortRangeStringAdjusted = "";
}
// default to true for http/ssh/inference, false otherwise;
// gateway always allows icmp
let disableIcmpAdjusted = disableIcmp ?? false;
if (mode === "gateway") {
disableIcmpAdjusted = false;
} else if (
mode === "http" ||
mode === "ssh" ||
mode === "inference"
) {
disableIcmpAdjusted = true;
}
// Create the site resource
@@ -594,21 +625,14 @@ export async function createSiteResource(
mode,
ssl,
networkId: network ? network.networkId : null,
destination: destination, // the ssh can be null
destination: effectiveDestination, // the ssh can be null
scheme,
destinationPort,
alias: alias ? alias.trim() : null,
aliasAddress,
tcpPortRangeString: tcpPortRangeStringAdjusted,
udpPortRangeString:
mode == "http" || mode == "ssh" || mode == "inference"
? ""
: udpPortRangeString,
disableIcmp:
disableIcmp ||
(mode == "http" || mode == "ssh" || mode == "inference"
? true
: false), // default to true for http resources, otherwise false
udpPortRangeString: udpPortRangeStringAdjusted,
disableIcmp: disableIcmpAdjusted,
domainId,
subdomain: finalSubdomain,
fullDomain,
@@ -51,7 +51,9 @@ const updateSiteResourceSchema = z
)
.optional(),
// mode: z.enum(["host", "cidr", "port"]).optional(),
mode: z.enum(["host", "cidr", "http", "ssh", "inference"]).optional(),
mode: z
.enum(["host", "cidr", "http", "ssh", "inference", "gateway"])
.optional(),
ssl: z.boolean().optional(),
scheme: z.enum(["http", "https"]).nullish(),
destinationPort: z.int().positive().nullish(),
@@ -158,10 +160,11 @@ const updateSiteResourceSchema = z
if (data.mode === undefined && data.destination === undefined) {
return true;
}
// destination is only optional for ssh mode with native authDaemonMode or inference
// destination is only optional for ssh mode with native authDaemonMode, inference, or gateway
if (
(data.mode === "ssh" && data.authDaemonMode === "native") ||
data.mode == "inference"
data.mode == "inference" ||
data.mode == "gateway"
) {
return true;
}
@@ -172,7 +175,7 @@ const updateSiteResourceSchema = z
},
{
message:
"Destination is required unless mode is ssh with authDaemonMode native or inference"
"Destination is required unless mode is ssh with authDaemonMode native, inference, or gateway"
}
)
.refine(
@@ -409,14 +412,18 @@ export async function updateSiteResource(
}
}
// gateway resources always route the whole subnet with everything open
const effectiveDestination =
mode === "gateway" ? "0.0.0.0/0" : destination;
// Only check if destination is an IP address
const isIp = z
.union([z.ipv4(), z.ipv6()])
.safeParse(destination).success;
.safeParse(effectiveDestination).success;
if (
isIp &&
(isIpInCidr(destination!, org.subnet) ||
isIpInCidr(destination!, org.utilitySubnet))
(isIpInCidr(effectiveDestination!, org.subnet) ||
isIpInCidr(effectiveDestination!, org.utilitySubnet))
) {
return next(
createHttpError(
@@ -542,6 +549,34 @@ export async function updateSiteResource(
tcpPortRangeStringAdjusted = destinationPort
? destinationPort.toString()
: "22";
} else if (mode === "gateway") {
tcpPortRangeStringAdjusted = "*";
}
// undefined means "leave unchanged" (partial update); only
// adjusted when the mode is explicitly being changed
let udpPortRangeStringAdjusted = udpPortRangeString;
if (mode === "gateway") {
udpPortRangeStringAdjusted = "*";
} else if (
mode === "http" ||
mode === "ssh" ||
mode === "inference"
) {
udpPortRangeStringAdjusted = "";
}
let disableIcmpAdjusted = disableIcmp;
if (mode === "gateway") {
disableIcmpAdjusted = false;
} else if (
mode === "http" ||
mode === "ssh" ||
mode === "inference"
) {
disableIcmpAdjusted = true;
} else if (mode !== undefined) {
disableIcmpAdjusted = disableIcmp ?? false;
}
[updatedSiteResource] = await trx
@@ -552,7 +587,8 @@ export async function updateSiteResource(
mode: mode,
scheme,
ssl,
destination: destination,
destination:
mode === "gateway" ? effectiveDestination : destination,
destinationPort: destinationPort,
enabled: enabled,
alias:
@@ -562,19 +598,8 @@ export async function updateSiteResource(
: null
: undefined,
tcpPortRangeString: tcpPortRangeStringAdjusted,
udpPortRangeString:
mode == "http" || mode == "ssh" || mode == "inference"
? ""
: udpPortRangeString,
disableIcmp:
mode !== undefined
? disableIcmp ||
(mode == "http" ||
mode == "ssh" ||
mode == "inference"
? true
: false)
: disableIcmp,
udpPortRangeString: udpPortRangeStringAdjusted,
disableIcmp: disableIcmpAdjusted,
domainId,
subdomain: finalSubdomain,
fullDomain,
@@ -0,0 +1,106 @@
"use client";
import {
SettingsContainer,
SettingsFormCell,
SettingsFormGrid,
SettingsSection,
SettingsSectionBody,
SettingsSectionDescription,
SettingsSectionFooter,
SettingsSectionForm,
SettingsSectionHeader,
SettingsSectionTitle
} from "@app/components/Settings";
import { Button } from "@app/components/ui/button";
import { Form } from "@app/components/ui/form";
import { createGatewayFormSchema } from "@app/lib/privateResourceForm";
import { zodResolver } from "@hookform/resolvers/zod";
import { useTranslations } from "next-intl";
import { useActionState, useMemo, useState } from "react";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { PrivateResourceSitesField } from "@app/components/PrivateResourceSitesField";
import { useSaveSiteResource } from "@app/hooks/useSaveSiteResource";
import { buildSelectedSitesForResource } from "@app/lib/privateResourceUtils";
export default function PrivateResourceGatewayPage() {
const t = useTranslations();
const { save, siteResource } = useSaveSiteResource();
const [selectedSites, setSelectedSites] = useState(() =>
buildSelectedSitesForResource(siteResource)
);
const formSchema = useMemo(() => createGatewayFormSchema(t), [t]);
type FormValues = z.infer<typeof formSchema>;
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
siteIds: siteResource.siteIds,
mode: "gateway"
}
});
const [, formAction, saveLoading] = useActionState(async () => {
const isValid = await form.trigger();
if (!isValid) return;
const data = form.getValues();
await save({
siteIds: data.siteIds,
mode: "gateway"
});
}, null);
return (
<SettingsContainer>
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
{t("gatewaySettings")}
</SettingsSectionTitle>
<SettingsSectionDescription>
{t(
"editInternalResourceDialogDestinationGatewayDescription"
)}
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
<SettingsSectionForm variant="half">
<Form {...form}>
<form
action={formAction}
id="private-resource-gateway-form"
>
<SettingsFormGrid>
<SettingsFormCell span="half">
<PrivateResourceSitesField
control={form.control}
orgId={siteResource.orgId}
selectedSites={selectedSites}
onSelectedSitesChange={
setSelectedSites
}
/>
</SettingsFormCell>
</SettingsFormGrid>
</form>
</Form>
</SettingsSectionForm>
</SettingsSectionBody>
<SettingsSectionFooter>
<Button
type="submit"
form="private-resource-gateway-form"
loading={saveLoading}
>
{t("saveSettings")}
</Button>
</SettingsSectionFooter>
</SettingsSection>
</SettingsContainer>
);
}
@@ -53,7 +53,8 @@ export default async function PrivateResourceLayout(
| "cidrSettings"
| "httpSettings"
| "sshSettings"
| "inferenceSettings";
| "inferenceSettings"
| "gatewaySettings";
const navItems = [
{
@@ -164,6 +164,11 @@ export default function CreatePrivateResourcePage() {
value: "inference" as const,
title: t("createInternalResourceDialogModeInference"),
description: t("resourceTypeInferenceDescription")
},
{
value: "gateway" as const,
title: t("createInternalResourceDialogModeGateway"),
description: t("resourceTypeGatewayDescription")
}
];
@@ -560,6 +565,38 @@ export default function CreatePrivateResourcePage() {
</SettingsSection>
)}
{/* Gateway destination */}
{mode === "gateway" && (
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
{t("gatewaySettings")}
</SettingsSectionTitle>
<SettingsSectionDescription>
{t(
"editInternalResourceDialogDestinationGatewayDescription"
)}
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
<SettingsSectionForm variant="half">
<SettingsFormGrid>
<SettingsFormCell span="half">
<PrivateResourceSitesField
control={form.control}
orgId={orgId}
selectedSites={selectedSites}
onSelectedSitesChange={
setSelectedSites
}
/>
</SettingsFormCell>
</SettingsFormGrid>
</SettingsSectionForm>
</SettingsSectionBody>
</SettingsSection>
)}
{/* HTTP configuration */}
{mode === "http" && (
<SettingsSection>
+23 -15
View File
@@ -93,7 +93,8 @@ export function PrivateResourceInfoSections({
cidr: t("editInternalResourceDialogModeCidr"),
http: t("editInternalResourceDialogModeHttp"),
ssh: t("editInternalResourceDialogModeSsh"),
inference: t("editInternalResourceDialogModeInference")
inference: t("editInternalResourceDialogModeInference"),
gateway: t("editInternalResourceDialogModeGateway")
};
const destination = formatSiteResourceDestinationDisplay({
@@ -107,15 +108,19 @@ export function PrivateResourceInfoSections({
tcpPortRangeString: siteResource.tcpPortRangeString ?? "*",
udpPortRangeString: siteResource.udpPortRangeString ?? "*"
});
const showAccess = siteResource.mode !== "gateway";
const showAlias =
siteResource.mode !== "cidr" &&
siteResource.mode !== "http" &&
siteResource.mode !== "inference";
siteResource.mode !== "inference" &&
siteResource.mode !== "gateway";
const showDestination =
!(
siteResource.mode === "ssh" &&
siteResource.authDaemonMode === "native"
) && siteResource.mode !== "inference";
) &&
siteResource.mode !== "inference" &&
siteResource.mode !== "gateway";
const showCertificate = !!(
(siteResource.mode === "http" || siteResource.mode === "inference") &&
siteResource.ssl &&
@@ -128,7 +133,8 @@ export function PrivateResourceInfoSections({
siteResource.mode !== "inference";
const numSections =
2 +
1 +
(showAccess ? 1 : 0) +
(showDestination ? 1 : 0) +
(showAlias ? 1 : 0) +
(showCertificate ? 1 : 0) +
@@ -143,17 +149,19 @@ export function PrivateResourceInfoSections({
</InfoSectionContent>
</InfoSection>
<InfoSection>
<InfoSectionTitle>{t("access")}</InfoSectionTitle>
<InfoSectionContent>
<AccessMethodContent
accessDisplay={access.accessDisplay}
accessCopyValue={access.accessCopyValue}
accessUrl={access.accessUrl}
className={accessClassName}
/>
</InfoSectionContent>
</InfoSection>
{showAccess ? (
<InfoSection>
<InfoSectionTitle>{t("access")}</InfoSectionTitle>
<InfoSectionContent>
<AccessMethodContent
accessDisplay={access.accessDisplay}
accessCopyValue={access.accessCopyValue}
accessUrl={access.accessUrl}
className={accessClassName}
/>
</InfoSectionContent>
</InfoSection>
) : null}
{showDestination ? (
<InfoSection>
+7 -2
View File
@@ -376,7 +376,8 @@ export default function PrivateResourcesTable({
cidr: t("editInternalResourceDialogModeCidr"),
http: t("editInternalResourceDialogModeHttp"),
ssh: t("editInternalResourceDialogModeSsh"),
inference: t("editInternalResourceDialogModeInference")
inference: t("editInternalResourceDialogModeInference"),
gateway: t("editInternalResourceDialogModeGateway")
};
return <span>{modeLabels[resourceRow.mode]}</span>;
}
@@ -392,7 +393,11 @@ export default function PrivateResourcesTable({
cell: ({ row }) => {
const resourceRow = row.original;
const display = formatDestinationDisplay(resourceRow);
if (resourceRow.destination) {
if (
resourceRow.destination &&
resourceRow.mode !== "gateway"
) {
// don't show the gateway resource destination which is 0.0.0.0/0 to not confuse people
return (
<CopyToClipboard
text={display}
+2 -1
View File
@@ -71,7 +71,8 @@ function PrivateResourceMeta({ row }: { row: SiteResourceRow }) {
cidr: t("editInternalResourceDialogModeCidr"),
http: t("editInternalResourceDialogModeHttp"),
ssh: t("editInternalResourceDialogModeSsh"),
inference: t("editInternalResourceDialogModeInference")
inference: t("editInternalResourceDialogModeInference"),
gateway: t("editInternalResourceDialogModeGateway")
};
const dest = formatSiteResourceDestinationDisplay({
mode: row.mode,
+16 -1
View File
@@ -421,7 +421,14 @@ export function createCreateFormSchema(t: TranslateFn) {
.min(1, t("createInternalResourceDialogNameRequired"))
.max(255, t("createInternalResourceDialogNameMaxLength")),
siteIds: z.array(z.number().int().positive()).optional(),
mode: z.enum(["host", "cidr", "http", "ssh", "inference"]),
mode: z.enum([
"host",
"cidr",
"http",
"ssh",
"inference",
"gateway"
]),
destination: z.string().nullish(),
alias: z.string().nullish(),
destinationPort: z
@@ -468,6 +475,7 @@ export function createCreateFormSchema(t: TranslateFn) {
if (
data.mode !== "ssh" &&
data.mode !== "inference" &&
data.mode !== "gateway" &&
(!trimmedDestination || trimmedDestination.length < 1)
) {
ctx.addIssue({
@@ -652,6 +660,13 @@ export function createCidrFormSchema(t: TranslateFn) {
.superRefine((data, ctx) => destinationRefine(data, ctx, t));
}
export function createGatewayFormSchema(t: TranslateFn) {
return z.object({
siteIds: z.array(z.number().int().positive()).min(1),
mode: z.literal("gateway")
});
}
export function createHttpFormSchema(t: TranslateFn) {
return z
.object({