diff --git a/messages/en-US.json b/messages/en-US.json index 7642419c6..e5b073f6c 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -1817,6 +1817,8 @@ "editInternalResourceDialogModePort": "Port", "editInternalResourceDialogModeHost": "Host", "editInternalResourceDialogModeCidr": "CIDR", + "editInternalResourceDialogModeHttp": "HTTP", + "editInternalResourceDialogModeHttps": "HTTPS", "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.", @@ -1860,6 +1862,8 @@ "createInternalResourceDialogModePort": "Port", "createInternalResourceDialogModeHost": "Host", "createInternalResourceDialogModeCidr": "CIDR", + "createInternalResourceDialogModeHttp": "HTTP", + "createInternalResourceDialogModeHttps": "HTTPS", "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.", @@ -2661,6 +2665,10 @@ "editInternalResourceDialogDestinationLabel": "Destination", "editInternalResourceDialogDestinationDescription": "Specify the destination address for the internal resource. This can be a hostname, IP address, or CIDR range depending on the selected mode. Optionally set an internal DNS alias for easier identification.", "editInternalResourceDialogPortRestrictionsDescription": "Restrict access to specific TCP/UDP ports or allow/block all ports.", + "createInternalResourceDialogHttpConfiguration": "HTTP configuration", + "createInternalResourceDialogHttpConfigurationDescription": "Choose the domain clients will use to reach this resource over HTTP or HTTPS.", + "editInternalResourceDialogHttpConfiguration": "HTTP configuration", + "editInternalResourceDialogHttpConfigurationDescription": "Choose the domain clients will use to reach this resource over HTTP or HTTPS.", "editInternalResourceDialogTcp": "TCP", "editInternalResourceDialogUdp": "UDP", "editInternalResourceDialogIcmp": "ICMP", diff --git a/server/routers/siteResource/createSiteResource.ts b/server/routers/siteResource/createSiteResource.ts index ca7424bb6..e1b97bdca 100644 --- a/server/routers/siteResource/createSiteResource.ts +++ b/server/routers/siteResource/createSiteResource.ts @@ -36,7 +36,7 @@ const createSiteResourceParamsSchema = z.strictObject({ const createSiteResourceSchema = z .strictObject({ name: z.string().min(1).max(255), - mode: z.enum(["host", "cidr", "port", "http", "https"]), + mode: z.enum(["host", "cidr", "http", "https"]), siteId: z.int(), // protocol: z.enum(["tcp", "udp"]).optional(), // proxyPort: z.int().positive().optional(), @@ -286,8 +286,7 @@ export async function createSiteResource( const niceId = await getUniqueSiteResourceName(orgId); let aliasAddress: string | null = null; - if (mode == "host") { - // we can only have an alias on a host + if (mode === "host" || mode === "http" || mode === "https") { aliasAddress = await getNextAvailableAliasAddress(orgId); } @@ -299,7 +298,7 @@ export async function createSiteResource( niceId, orgId, name, - mode: mode as "host" | "cidr", + mode, destination, destinationPort, enabled, diff --git a/server/routers/siteResource/listAllSiteResourcesByOrg.ts b/server/routers/siteResource/listAllSiteResourcesByOrg.ts index 3320aa3b7..36bc6bee0 100644 --- a/server/routers/siteResource/listAllSiteResourcesByOrg.ts +++ b/server/routers/siteResource/listAllSiteResourcesByOrg.ts @@ -41,12 +41,12 @@ const listAllSiteResourcesByOrgQuerySchema = z.object({ }), query: z.string().optional(), mode: z - .enum(["host", "cidr"]) + .enum(["host", "cidr", "http", "https"]) .optional() .catch(undefined) .openapi({ type: "string", - enum: ["host", "cidr"], + enum: ["host", "cidr", "http", "https"], description: "Filter site resources by mode" }), sort_by: z diff --git a/src/components/ClientResourcesTable.tsx b/src/components/ClientResourcesTable.tsx index 5066f273d..b25409c44 100644 --- a/src/components/ClientResourcesTable.tsx +++ b/src/components/ClientResourcesTable.tsx @@ -46,7 +46,7 @@ export type InternalResourceRow = { siteName: string; siteAddress: string | null; // mode: "host" | "cidr" | "port"; - mode: "host" | "cidr"; + mode: "host" | "cidr" | "http" | "https"; // protocol: string | null; // proxyPort: number | null; siteId: number; @@ -215,6 +215,14 @@ export default function ClientResourcesTable({ { value: "cidr", label: t("editInternalResourceDialogModeCidr") + }, + { + value: "http", + label: t("editInternalResourceDialogModeHttp") + }, + { + value: "https", + label: t("editInternalResourceDialogModeHttps") } ]} selectedValue={searchParams.get("mode") ?? undefined} @@ -227,10 +235,15 @@ export default function ClientResourcesTable({ ), cell: ({ row }) => { const resourceRow = row.original; - const modeLabels: Record<"host" | "cidr" | "port", string> = { + const modeLabels: Record< + "host" | "cidr" | "port" | "http" | "https", + string + > = { host: t("editInternalResourceDialogModeHost"), cidr: t("editInternalResourceDialogModeCidr"), - port: t("editInternalResourceDialogModePort") + port: t("editInternalResourceDialogModePort"), + http: t("editInternalResourceDialogModeHttp"), + https: t("editInternalResourceDialogModeHttps") }; return {modeLabels[resourceRow.mode]}; } diff --git a/src/components/CreateInternalResourceDialog.tsx b/src/components/CreateInternalResourceDialog.tsx index d5ca61acc..d37acdc6e 100644 --- a/src/components/CreateInternalResourceDialog.tsx +++ b/src/components/CreateInternalResourceDialog.tsx @@ -50,7 +50,12 @@ export default function CreateInternalResourceDialog({ setIsSubmitting(true); try { let data = { ...values }; - if (data.mode === "host" && isHostname(data.destination)) { + if ( + (data.mode === "host" || + data.mode === "http" || + data.mode === "https") && + isHostname(data.destination) + ) { const currentAlias = data.alias?.trim() || ""; if (!currentAlias) { let aliasValue = data.destination; diff --git a/src/components/EditInternalResourceDialog.tsx b/src/components/EditInternalResourceDialog.tsx index 690ad405d..2a4cd35fc 100644 --- a/src/components/EditInternalResourceDialog.tsx +++ b/src/components/EditInternalResourceDialog.tsx @@ -54,7 +54,12 @@ export default function EditInternalResourceDialog({ async function handleSubmit(values: InternalResourceFormValues) { try { let data = { ...values }; - if (data.mode === "host" && isHostname(data.destination)) { + if ( + (data.mode === "host" || + data.mode === "http" || + data.mode === "https") && + isHostname(data.destination) + ) { const currentAlias = data.alias?.trim() || ""; if (!currentAlias) { let aliasValue = data.destination; diff --git a/src/components/InternalResourceForm.tsx b/src/components/InternalResourceForm.tsx index a4a793753..fb31d27b8 100644 --- a/src/components/InternalResourceForm.tsx +++ b/src/components/InternalResourceForm.tsx @@ -45,6 +45,7 @@ import { z } from "zod"; import { SitesSelector, type Selectedsite } from "./site-selector"; import { CaretSortIcon } from "@radix-ui/react-icons"; import { MachinesSelector } from "./machines-selector"; +import DomainPicker from "@app/components/DomainPicker"; // --- Helpers (shared) --- @@ -120,12 +121,14 @@ export const cleanForFQDN = (name: string): string => type Site = ListSitesResponse["sites"][0]; +export type InternalResourceMode = "host" | "cidr" | "http" | "https"; + export type InternalResourceData = { id: number; name: string; orgId: string; siteName: string; - mode: "host" | "cidr"; + mode: InternalResourceMode; siteId: number; niceId: string; destination: string; @@ -135,6 +138,10 @@ export type InternalResourceData = { disableIcmp?: boolean; authDaemonMode?: "site" | "remote" | null; authDaemonPort?: number | null; + httpHttpsPort?: number | null; + httpConfigSubdomain?: string | null; + httpConfigDomainId?: string | null; + httpConfigFullDomain?: string | null; }; const tagSchema = z.object({ id: z.string(), text: z.string() }); @@ -142,7 +149,7 @@ const tagSchema = z.object({ id: z.string(), text: z.string() }); export type InternalResourceFormValues = { name: string; siteId: number; - mode: "host" | "cidr"; + mode: InternalResourceMode; destination: string; alias?: string | null; niceId?: string; @@ -151,6 +158,10 @@ export type InternalResourceFormValues = { disableIcmp?: boolean; authDaemonMode?: "site" | "remote" | null; authDaemonPort?: number | null; + httpHttpsPort?: number | null; + httpConfigSubdomain?: string | null; + httpConfigDomainId?: string | null; + httpConfigFullDomain?: string | null; roles?: z.infer[]; users?: z.infer[]; clients?: z.infer[]; @@ -211,6 +222,14 @@ export function InternalResourceForm({ variant === "create" ? "createInternalResourceDialogModeCidr" : "editInternalResourceDialogModeCidr"; + const modeHttpKey = + variant === "create" + ? "createInternalResourceDialogModeHttp" + : "editInternalResourceDialogModeHttp"; + const modeHttpsKey = + variant === "create" + ? "createInternalResourceDialogModeHttps" + : "editInternalResourceDialogModeHttps"; const destinationLabelKey = variant === "create" ? "createInternalResourceDialogDestination" @@ -223,6 +242,18 @@ export function InternalResourceForm({ variant === "create" ? "createInternalResourceDialogAlias" : "editInternalResourceDialogAlias"; + const httpHttpsPortLabelKey = + variant === "create" + ? "createInternalResourceDialogModePort" + : "editInternalResourceDialogModePort"; + const httpConfigurationTitleKey = + variant === "create" + ? "createInternalResourceDialogHttpConfiguration" + : "editInternalResourceDialogHttpConfiguration"; + const httpConfigurationDescriptionKey = + variant === "create" + ? "createInternalResourceDialogHttpConfigurationDescription" + : "editInternalResourceDialogHttpConfigurationDescription"; const formSchema = z.object({ name: z.string().min(1, t(nameRequiredKey)).max(255, t(nameMaxKey)), @@ -230,7 +261,7 @@ export function InternalResourceForm({ .number() .int() .positive(siteRequiredKey ? t(siteRequiredKey) : undefined), - mode: z.enum(["host", "cidr"]), + mode: z.enum(["host", "cidr", "http", "https"]), destination: z .string() .min( @@ -240,6 +271,10 @@ export function InternalResourceForm({ : undefined ), alias: z.string().nullish(), + httpHttpsPort: z.number().int().min(1).max(65535).optional().nullable(), + httpConfigSubdomain: z.string().nullish(), + httpConfigDomainId: z.string().nullish(), + httpConfigFullDomain: z.string().nullish(), niceId: z .string() .min(1) @@ -394,6 +429,10 @@ export function InternalResourceForm({ disableIcmp: resource.disableIcmp ?? false, authDaemonMode: resource.authDaemonMode ?? "site", authDaemonPort: resource.authDaemonPort ?? null, + httpHttpsPort: resource.httpHttpsPort ?? null, + httpConfigSubdomain: resource.httpConfigSubdomain ?? null, + httpConfigDomainId: resource.httpConfigDomainId ?? null, + httpConfigFullDomain: resource.httpConfigFullDomain ?? null, niceId: resource.niceId, roles: [], users: [], @@ -405,6 +444,10 @@ export function InternalResourceForm({ mode: "host", destination: "", alias: null, + httpHttpsPort: null, + httpConfigSubdomain: null, + httpConfigDomainId: null, + httpConfigFullDomain: null, tcpPortRangeString: "*", udpPortRangeString: "*", disableIcmp: false, @@ -425,6 +468,10 @@ export function InternalResourceForm({ }); const mode = form.watch("mode"); + const httpConfigSubdomain = form.watch("httpConfigSubdomain"); + const httpConfigDomainId = form.watch("httpConfigDomainId"); + const httpConfigFullDomain = form.watch("httpConfigFullDomain"); + const isHttpOrHttps = mode === "http" || mode === "https"; const authDaemonMode = form.watch("authDaemonMode") ?? "site"; const hasInitialized = useRef(false); const previousResourceId = useRef(null); @@ -448,6 +495,10 @@ export function InternalResourceForm({ mode: "host", destination: "", alias: null, + httpHttpsPort: null, + httpConfigSubdomain: null, + httpConfigDomainId: null, + httpConfigFullDomain: null, tcpPortRangeString: "*", udpPortRangeString: "*", disableIcmp: false, @@ -475,6 +526,10 @@ export function InternalResourceForm({ mode: resource.mode ?? "host", destination: resource.destination ?? "", alias: resource.alias ?? null, + httpHttpsPort: resource.httpHttpsPort ?? null, + httpConfigSubdomain: resource.httpConfigSubdomain ?? null, + httpConfigDomainId: resource.httpConfigDomainId ?? null, + httpConfigFullDomain: resource.httpConfigFullDomain ?? null, tcpPortRangeString: resource.tcpPortRangeString ?? "*", udpPortRangeString: resource.udpPortRangeString ?? "*", disableIcmp: resource.disableIcmp ?? false, @@ -701,6 +756,12 @@ export function InternalResourceForm({ {t(modeCidrKey)} + + {t(modeHttpKey)} + + + {t(modeHttpsKey)} + @@ -731,7 +792,7 @@ export function InternalResourceForm({ )} /> - {mode !== "cidr" && ( + {mode === "host" && (
)} + {(mode === "http" || mode === "https") && ( +
+ ( + + + {t( + httpHttpsPortLabelKey + )} + + + { + const raw = + e.target + .value; + if ( + raw === "" + ) { + field.onChange( + null + ); + return; + } + const n = + Number(raw); + field.onChange( + Number.isFinite( + n + ) + ? n + : null + ); + }} + /> + + + + )} + /> +
+ )} -
-
- -
- {t( - "editInternalResourceDialogPortRestrictionsDescription" + {isHttpOrHttps ? ( +
+
+ +
+ {t(httpConfigurationDescriptionKey)} +
+
+ { + if (res === null) { + form.setValue( + "httpConfigSubdomain", + null + ); + form.setValue( + "httpConfigDomainId", + null + ); + form.setValue( + "httpConfigFullDomain", + null + ); + return; + } + form.setValue( + "httpConfigSubdomain", + res.subdomain ?? null + ); + form.setValue( + "httpConfigDomainId", + res.domainId + ); + form.setValue( + "httpConfigFullDomain", + res.fullDomain + ); + }} + /> +
+ ) : ( +
+
+ +
+ {t( + "editInternalResourceDialogPortRestrictionsDescription" + )} +
+
+
-
-
-
- - {t("editInternalResourceDialogTcp")} - -
-
- ( - -
- - {tcpPortMode === - "custom" ? ( - - - setTcpCustomPorts( - e.target - .value - ) - } - /> - - ) : ( - - )} -
- -
- )} - /> -
-
-
-
- - {t("editInternalResourceDialogUdp")} - -
-
- ( - -
- - {udpPortMode === - "custom" ? ( - - - setUdpCustomPorts( - e.target - .value - ) - } - /> - - ) : ( - - )} -
- -
- )} - /> -
-
-
-
- - {t("editInternalResourceDialogIcmp")} - -
-
- ( - -
- - + + {t("editInternalResourceDialogTcp")} + +
+
+ ( + +
+ + {tcpPortMode === + "custom" ? ( + + + setTcpCustomPorts( + e + .target + .value + ) + } + /> + + ) : ( + + )} +
+ +
+ )} + /> +
+
+
+
+ + {t("editInternalResourceDialogUdp")} + +
+
+ ( + +
+ + {udpPortMode === + "custom" ? ( + + + setUdpCustomPorts( + e + .target + .value + ) + } + /> + + ) : ( + + )} +
+ +
+ )} + /> +
+
+
+
+ + {t( + "editInternalResourceDialogIcmp" + )} + +
+
+ ( + +
+ + + field.onChange( + !checked + ) + } + /> + + + {field.value + ? t("blocked") + : t("allowed")} + +
+ +
+ )} + /> +
-
+ )}