From 605dd2f3c9d7746a63fabd83b050d94654556af1 Mon Sep 17 00:00:00 2001 From: Owen Date: Mon, 1 Jun 2026 16:05:20 -0700 Subject: [PATCH] Add tcp and udp specific pages --- messages/en-US.json | 4 +- server/setup/scriptsPg/1.19.0.ts | 40 +++ server/setup/scriptsSqlite/1.19.0.ts | 64 ++++ .../resources/public/[niceId]/http/page.tsx | 209 +------------ .../resources/public/[niceId]/tcp/page.tsx | 291 ++++++++++++++++++ .../resources/public/[niceId]/udp/page.tsx | 43 +++ 6 files changed, 442 insertions(+), 209 deletions(-) create mode 100644 src/app/[orgId]/settings/resources/public/[niceId]/tcp/page.tsx create mode 100644 src/app/[orgId]/settings/resources/public/[niceId]/udp/page.tsx diff --git a/messages/en-US.json b/messages/en-US.json index bf038b031..937032c18 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -3430,5 +3430,7 @@ "memberPortalShowingResources": "Showing {start}-{end} of {total} resources", "memberPortalPrevious": "Previous", "memberPortalNext": "Next", - "httpSettings": "HTTP Settings" + "httpSettings": "HTTP Settings", + "tcpSettings": "TCP Settings", + "udpSettings": "UDP Settings" } diff --git a/server/setup/scriptsPg/1.19.0.ts b/server/setup/scriptsPg/1.19.0.ts index 4e5b67faa..68aa83afc 100644 --- a/server/setup/scriptsPg/1.19.0.ts +++ b/server/setup/scriptsPg/1.19.0.ts @@ -519,6 +519,46 @@ export default async function migration() { `); } + const existingRoleResources = await db.execute(sql` + SELECT "roleId" + FROM "roleResources" + WHERE "resourceId" = ${resource.resourceId} + `); + for (const roleRow of existingRoleResources.rows as { + roleId: number; + }[]) { + await db.execute(sql` + INSERT INTO "rolePolicies" ("roleId", "resourcePolicyId") + SELECT ${roleRow.roleId}, ${resourcePolicyId} + WHERE NOT EXISTS ( + SELECT 1 + FROM "rolePolicies" + WHERE "roleId" = ${roleRow.roleId} + AND "resourcePolicyId" = ${resourcePolicyId} + ) + `); + } + + const existingUserResources = await db.execute(sql` + SELECT "userId" + FROM "userResources" + WHERE "resourceId" = ${resource.resourceId} + `); + for (const userRow of existingUserResources.rows as { + userId: string; + }[]) { + await db.execute(sql` + INSERT INTO "userPolicies" ("userId", "resourcePolicyId") + SELECT ${userRow.userId}, ${resourcePolicyId} + WHERE NOT EXISTS ( + SELECT 1 + FROM "userPolicies" + WHERE "userId" = ${userRow.userId} + AND "resourcePolicyId" = ${resourcePolicyId} + ) + `); + } + await db.execute(sql` DELETE FROM "resourcePincode" WHERE "resourceId" = ${resource.resourceId} diff --git a/server/setup/scriptsSqlite/1.19.0.ts b/server/setup/scriptsSqlite/1.19.0.ts index 0a08b5abd..7f0e726d1 100644 --- a/server/setup/scriptsSqlite/1.19.0.ts +++ b/server/setup/scriptsSqlite/1.19.0.ts @@ -32,6 +32,8 @@ export function generateName(): string { return name.replace(/[^a-z0-9-]/g, ""); } +await migration(); + export default async function migration() { console.log(`Running setup script ${version}...`); @@ -456,6 +458,42 @@ export default async function migration() { ) VALUES (?, ?)` ); + const selectRoleResources = db.prepare( + `SELECT "roleId" + FROM 'roleResources' + WHERE "resourceId" = ?` + ); + const rolePolicyExists = db.prepare( + `SELECT 1 + FROM 'rolePolicies' + WHERE "roleId" = ? AND "resourcePolicyId" = ? + LIMIT 1` + ); + const insertRolePolicy = db.prepare( + `INSERT INTO 'rolePolicies' ( + "roleId", + "resourcePolicyId" + ) VALUES (?, ?)` + ); + + const selectUserResources = db.prepare( + `SELECT "userId" + FROM 'userResources' + WHERE "resourceId" = ?` + ); + const userPolicyExists = db.prepare( + `SELECT 1 + FROM 'userPolicies' + WHERE "userId" = ? AND "resourcePolicyId" = ? + LIMIT 1` + ); + const insertUserPolicy = db.prepare( + `INSERT INTO 'userPolicies' ( + "userId", + "resourcePolicyId" + ) VALUES (?, ?)` + ); + const deleteResourcePincodes = db.prepare( `DELETE FROM 'resourcePincode' WHERE "resourceId" = ?` ); @@ -586,6 +624,32 @@ export default async function migration() { ); } + const resourceRoles = selectRoleResources.all( + resource.resourceId + ) as { roleId: number }[]; + for (const role of resourceRoles) { + const exists = rolePolicyExists.get( + role.roleId, + policyId + ) as { 1: number } | undefined; + if (!exists) { + insertRolePolicy.run(role.roleId, policyId); + } + } + + const resourceUsers = selectUserResources.all( + resource.resourceId + ) as { userId: string }[]; + for (const user of resourceUsers) { + const exists = userPolicyExists.get( + user.userId, + policyId + ) as { 1: number } | undefined; + if (!exists) { + insertUserPolicy.run(user.userId, policyId); + } + } + deleteResourcePincodes.run(resource.resourceId); deleteResourcePasswords.run(resource.resourceId); deleteResourceHeaderAuth.run(resource.resourceId); diff --git a/src/app/[orgId]/settings/resources/public/[niceId]/http/page.tsx b/src/app/[orgId]/settings/resources/public/[niceId]/http/page.tsx index c65ddb9b2..ba6e24699 100644 --- a/src/app/[orgId]/settings/resources/public/[niceId]/http/page.tsx +++ b/src/app/[orgId]/settings/resources/public/[niceId]/http/page.tsx @@ -101,12 +101,6 @@ export default function ReverseProxyTargetsPage(props: { /> )} - {resource.mode == "tcp" && ( - - )} ); } @@ -405,205 +399,4 @@ function ProxyResourceHttpForm({ ); -} - -function ProxyResourceProtocolForm({ - resource, - updateResource -}: Pick) { - const t = useTranslations(); - - const api = createApiClient(useEnvContext()); - - const proxySettingsSchema = z.object({ - setHostHeader: z - .string() - .optional() - .refine( - (data) => { - if (data) { - return tlsNameSchema.safeParse(data).success; - } - return true; - }, - { - message: t("proxyErrorInvalidHeader") - } - ), - headers: z - .array(z.object({ name: z.string(), value: z.string() })) - .nullable(), - proxyProtocol: z.boolean().optional(), - proxyProtocolVersion: z.int().min(1).max(2).optional() - }); - - const proxySettingsForm = useForm({ - resolver: zodResolver(proxySettingsSchema), - defaultValues: { - setHostHeader: resource.setHostHeader || "", - headers: resource.headers, - proxyProtocol: resource.proxyProtocol || false, - proxyProtocolVersion: resource.proxyProtocolVersion || 1 - } - }); - - const router = useRouter(); - - const [, formAction, isSubmitting] = useActionState( - saveProtocolSettings, - null - ); - - async function saveProtocolSettings() { - const isValid = proxySettingsForm.trigger(); - if (!isValid) return; - - try { - // For TCP/UDP resources, save proxy protocol settings - const proxyData = proxySettingsForm.getValues(); - - const payload = { - proxyProtocol: proxyData.proxyProtocol || false, - proxyProtocolVersion: proxyData.proxyProtocolVersion || 1 - }; - - await api.post(`/resource/${resource.resourceId}`, payload); - - updateResource({ - ...resource, - proxyProtocol: proxyData.proxyProtocol || false, - proxyProtocolVersion: proxyData.proxyProtocolVersion || 1 - }); - - toast({ - title: t("settingsUpdated"), - description: t("settingsUpdatedDescription") - }); - - router.refresh(); - } catch (err) { - console.error(err); - toast({ - variant: "destructive", - title: t("settingsErrorUpdate"), - description: formatAxiosError( - err, - t("settingsErrorUpdateDescription") - ) - }); - } - } - - return ( - - - - {t("proxyProtocol")} - - - {t("proxyProtocolDescription")} - - - - -
- - ( - - - { - field.onChange(val); - }} - /> - - - )} - /> - - {proxySettingsForm.watch("proxyProtocol") && ( - <> - ( - - - {t("proxyProtocolVersion")} - - - - - - {t("versionDescription")} - - - )} - /> - - - - - {t("warning")}:{" "} - {t("proxyProtocolWarning")} - - - - )} - - -
-
- -
-
-
- ); -} +} \ No newline at end of file diff --git a/src/app/[orgId]/settings/resources/public/[niceId]/tcp/page.tsx b/src/app/[orgId]/settings/resources/public/[niceId]/tcp/page.tsx new file mode 100644 index 000000000..0e14ba9aa --- /dev/null +++ b/src/app/[orgId]/settings/resources/public/[niceId]/tcp/page.tsx @@ -0,0 +1,291 @@ +"use client"; + +import { Button } from "@/components/ui/button"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue +} from "@/components/ui/select"; +import { + SettingsContainer, + SettingsSection, + SettingsSectionBody, + SettingsSectionDescription, + SettingsSectionForm, + SettingsSectionHeader, + SettingsSectionTitle +} from "@app/components/Settings"; +import { SwitchInput } from "@app/components/SwitchInput"; +import { Alert, AlertDescription } from "@app/components/ui/alert"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage +} from "@app/components/ui/form"; +import type { ResourceContextType } from "@app/contexts/resourceContext"; +import { useEnvContext } from "@app/hooks/useEnvContext"; +import { useResourceContext } from "@app/hooks/useResourceContext"; +import { toast } from "@app/hooks/useToast"; +import { createApiClient } from "@app/lib/api"; +import { formatAxiosError } from "@app/lib/api/formatAxiosError"; +import { resourceQueries } from "@app/lib/queries"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { tlsNameSchema } from "@server/lib/schemas"; +import { useQuery } from "@tanstack/react-query"; +import { + ProxyResourceTargetsForm +} from "@app/app/[orgId]/settings/resources/public/ProxyResourceTargetsForm"; +import { + AlertTriangle, +} from "lucide-react"; +import { useTranslations } from "next-intl"; +import { useRouter } from "next/navigation"; +import { + use, + useActionState, +} from "react"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; + +export default function ReverseProxyTargetsPage(props: { + params: Promise<{ resourceId: number; orgId: string }>; +}) { + const params = use(props.params); + const { resource, updateResource } = useResourceContext(); + + const { data: remoteTargets = [], isLoading: isLoadingTargets } = useQuery( + resourceQueries.resourceTargets({ + resourceId: resource.resourceId + }) + ); + + if (isLoadingTargets) { + return null; + } + + return ( + + + + {resource.mode == "tcp" && ( + + )} + + ); +} + +function ProxyResourceProtocolForm({ + resource, + updateResource +}: Pick) { + const t = useTranslations(); + + const api = createApiClient(useEnvContext()); + + const proxySettingsSchema = z.object({ + setHostHeader: z + .string() + .optional() + .refine( + (data) => { + if (data) { + return tlsNameSchema.safeParse(data).success; + } + return true; + }, + { + message: t("proxyErrorInvalidHeader") + } + ), + headers: z + .array(z.object({ name: z.string(), value: z.string() })) + .nullable(), + proxyProtocol: z.boolean().optional(), + proxyProtocolVersion: z.int().min(1).max(2).optional() + }); + + const proxySettingsForm = useForm({ + resolver: zodResolver(proxySettingsSchema), + defaultValues: { + setHostHeader: resource.setHostHeader || "", + headers: resource.headers, + proxyProtocol: resource.proxyProtocol || false, + proxyProtocolVersion: resource.proxyProtocolVersion || 1 + } + }); + + const router = useRouter(); + + const [, formAction, isSubmitting] = useActionState( + saveProtocolSettings, + null + ); + + async function saveProtocolSettings() { + const isValid = proxySettingsForm.trigger(); + if (!isValid) return; + + try { + // For TCP/UDP resources, save proxy protocol settings + const proxyData = proxySettingsForm.getValues(); + + const payload = { + proxyProtocol: proxyData.proxyProtocol || false, + proxyProtocolVersion: proxyData.proxyProtocolVersion || 1 + }; + + await api.post(`/resource/${resource.resourceId}`, payload); + + updateResource({ + ...resource, + proxyProtocol: proxyData.proxyProtocol || false, + proxyProtocolVersion: proxyData.proxyProtocolVersion || 1 + }); + + toast({ + title: t("settingsUpdated"), + description: t("settingsUpdatedDescription") + }); + + router.refresh(); + } catch (err) { + console.error(err); + toast({ + variant: "destructive", + title: t("settingsErrorUpdate"), + description: formatAxiosError( + err, + t("settingsErrorUpdateDescription") + ) + }); + } + } + + return ( + + + + {t("proxyProtocol")} + + + {t("proxyProtocolDescription")} + + + + +
+ + ( + + + { + field.onChange(val); + }} + /> + + + )} + /> + + {proxySettingsForm.watch("proxyProtocol") && ( + <> + ( + + + {t("proxyProtocolVersion")} + + + + + + {t("versionDescription")} + + + )} + /> + + + + + {t("warning")}:{" "} + {t("proxyProtocolWarning")} + + + + )} + + +
+
+ +
+
+
+ ); +} diff --git a/src/app/[orgId]/settings/resources/public/[niceId]/udp/page.tsx b/src/app/[orgId]/settings/resources/public/[niceId]/udp/page.tsx new file mode 100644 index 000000000..42cb5daea --- /dev/null +++ b/src/app/[orgId]/settings/resources/public/[niceId]/udp/page.tsx @@ -0,0 +1,43 @@ +"use client"; + +import { + SettingsContainer, +} from "@app/components/Settings"; +import { useResourceContext } from "@app/hooks/useResourceContext"; +import { resourceQueries } from "@app/lib/queries"; +import { useQuery } from "@tanstack/react-query"; +import { + ProxyResourceTargetsForm +} from "@app/app/[orgId]/settings/resources/public/ProxyResourceTargetsForm"; +import { + use, +} from "react"; + +export default function ReverseProxyTargetsPage(props: { + params: Promise<{ resourceId: number; orgId: string }>; +}) { + const params = use(props.params); + const { resource, updateResource } = useResourceContext(); + + const { data: remoteTargets = [], isLoading: isLoadingTargets } = useQuery( + resourceQueries.resourceTargets({ + resourceId: resource.resourceId + }) + ); + + if (isLoadingTargets) { + return null; + } + + return ( + + + + ); +} \ No newline at end of file