From 9d77fcc4571ae88404ff3dcbd94a783ac913b46d Mon Sep 17 00:00:00 2001 From: Owen Date: Fri, 22 May 2026 15:12:37 -0700 Subject: [PATCH] Make the first ssh page and conditional http page --- messages/en-US.json | 19 + server/db/pg/schema/schema.ts | 4 +- server/db/sqlite/schema/schema.ts | 3 +- server/routers/resource/updateResource.ts | 6 +- .../proxy/[niceId]/{proxy => http}/page.tsx | 0 .../resources/proxy/[niceId]/layout.tsx | 18 +- .../resources/proxy/[niceId]/ssh/page.tsx | 356 ++++++++++++++++++ 7 files changed, 398 insertions(+), 8 deletions(-) rename src/app/[orgId]/settings/resources/proxy/[niceId]/{proxy => http}/page.tsx (100%) create mode 100644 src/app/[orgId]/settings/resources/proxy/[niceId]/ssh/page.tsx diff --git a/messages/en-US.json b/messages/en-US.json index da3951046..5d52a136d 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -1970,6 +1970,25 @@ "timeIsInSeconds": "Time is in seconds", "requireDeviceApproval": "Require Device Approvals", "requireDeviceApprovalDescription": "Users with this role need new devices approved by an admin before they can connect and access resources.", + "sshSettings": "SSH Settings", + "sshServer": "SSH Server", + "sshServerDescription": "Set up the authentication method, daemon location, and server destination", + "sshServerMode": "Mode", + "sshServerModeStandard": "Standard SSH Server", + "sshAuthenticationMethod": "Authentication Method", + "sshAuthMethodManual": "Manual Authentication", + "sshAuthMethodManualDescription": "Requires existing host credentials. Bypasses automatic provisioning.", + "sshAuthMethodAutomated": "Automated Provisioning", + "sshAuthMethodAutomatedDescription": "Automatically creates users, groups, and sudo permissions on host.", + "sshAuthDaemonLocation": "Auth Daemon Location", + "sshDaemonLocationSiteDescription": "Executes locally on the machine hosting the site connector.", + "sshDaemonLocationRemote": "On Remote Host", + "sshDaemonLocationRemoteDescription": "Executes on a separate target machine on the same network.", + "sshDaemonDisclaimer": "Ensure your target host is properly configured to run the auth daemon before completing this setup, or provisioning will fail.", + "sshDaemonPort": "Daemon Port", + "sshServerDestination": "Server Destination", + "sshServerDestinationDescription": "Configure the destination and port of the SSH server", + "destination": "Destination", "sshAccess": "SSH Access", "roleAllowSsh": "Allow SSH", "roleAllowSshAllow": "Allow", diff --git a/server/db/pg/schema/schema.ts b/server/db/pg/schema/schema.ts index 1df6871a3..84bd6de07 100644 --- a/server/db/pg/schema/schema.ts +++ b/server/db/pg/schema/schema.ts @@ -147,7 +147,6 @@ export const resources = pgTable("resources", { headers: text("headers"), // comma-separated list of headers to add to the request proxyProtocol: boolean("proxyProtocol").notNull().default(false), proxyProtocolVersion: integer("proxyProtocolVersion").default(1), - maintenanceModeEnabled: boolean("maintenanceModeEnabled") .notNull() .default(false), @@ -166,7 +165,8 @@ export const resources = pgTable("resources", { .default("passthrough"), authDaemonMode: varchar("authDaemonMode", { length: 32 }) .$type<"site" | "remote" | "native">() - .default("site") + .default("site"), + authDaemonPort: integer("authDaemonPort").default(22123) }); export const labels = pgTable("labels", { diff --git a/server/db/sqlite/schema/schema.ts b/server/db/sqlite/schema/schema.ts index 04eba1c21..fecfa9e04 100644 --- a/server/db/sqlite/schema/schema.ts +++ b/server/db/sqlite/schema/schema.ts @@ -187,7 +187,8 @@ export const resources = sqliteTable("resources", { .default("passthrough"), authDaemonMode: text("authDaemonMode") .$type<"site" | "remote" | "native">() - .default("site") + .default("site"), + authDaemonPort: integer("authDaemonPort").default(22123) }); export const labels = sqliteTable("labels", { diff --git a/server/routers/resource/updateResource.ts b/server/routers/resource/updateResource.ts index 0cb8617aa..d799d1994 100644 --- a/server/routers/resource/updateResource.ts +++ b/server/routers/resource/updateResource.ts @@ -72,7 +72,11 @@ const updateHttpResourceBodySchema = z maintenanceMessage: z.string().max(2000).nullable().optional(), maintenanceEstimatedTime: z.string().max(100).nullable().optional(), postAuthPath: z.string().nullable().optional(), - browserAccessType: z.enum(["http", "ssh", "rdp", "vnc"]).optional() + browserAccessType: z.enum(["http", "ssh", "rdp", "vnc"]).optional(), + // SSH settings + pamMode: z.enum(["passthrough", "push"]).optional(), + authDaemonMode: z.enum(["site", "remote", "native"]).optional(), + authDaemonPort: z.int().min(1).max(65535).nullable().optional() }) .refine((data) => Object.keys(data).length > 0, { error: "At least one field must be provided for update" diff --git a/src/app/[orgId]/settings/resources/proxy/[niceId]/proxy/page.tsx b/src/app/[orgId]/settings/resources/proxy/[niceId]/http/page.tsx similarity index 100% rename from src/app/[orgId]/settings/resources/proxy/[niceId]/proxy/page.tsx rename to src/app/[orgId]/settings/resources/proxy/[niceId]/http/page.tsx diff --git a/src/app/[orgId]/settings/resources/proxy/[niceId]/layout.tsx b/src/app/[orgId]/settings/resources/proxy/[niceId]/layout.tsx index 2f6cd1492..f013afa27 100644 --- a/src/app/[orgId]/settings/resources/proxy/[niceId]/layout.tsx +++ b/src/app/[orgId]/settings/resources/proxy/[niceId]/layout.tsx @@ -84,13 +84,23 @@ export default async function ResourceLayout(props: ResourceLayoutProps) { { title: t("general"), href: `/{orgId}/settings/resources/proxy/{niceId}/general` - }, - { - title: t("proxy"), - href: `/{orgId}/settings/resources/proxy/{niceId}/proxy` } ]; + if (resource.browserAccessType === "http") { + navItems.push({ + title: t("httpSettings"), + href: `/{orgId}/settings/resources/proxy/{niceId}/http` + }); + } + + if (resource.browserAccessType === "ssh") { + navItems.push({ + title: t("sshSettings"), + href: `/{orgId}/settings/resources/proxy/{niceId}/ssh` + }); + } + if (resource.http) { navItems.push({ title: t("authentication"), diff --git a/src/app/[orgId]/settings/resources/proxy/[niceId]/ssh/page.tsx b/src/app/[orgId]/settings/resources/proxy/[niceId]/ssh/page.tsx new file mode 100644 index 000000000..e86248fac --- /dev/null +++ b/src/app/[orgId]/settings/resources/proxy/[niceId]/ssh/page.tsx @@ -0,0 +1,356 @@ +"use client"; + +import { + SettingsContainer, + SettingsSection, + SettingsSectionBody, + SettingsSectionDescription, + SettingsSectionHeader, + SettingsSectionTitle +} from "@app/components/Settings"; +import { StrategySelect, StrategyOption } from "@app/components/StrategySelect"; +import { Button } from "@app/components/ui/button"; +import { Input } from "@app/components/ui/input"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue +} from "@app/components/ui/select"; +import { ExternalLink } from "lucide-react"; +import { toast } from "@app/hooks/useToast"; +import { useResourceContext } from "@app/hooks/useResourceContext"; +import { useEnvContext } from "@app/hooks/useEnvContext"; +import { createApiClient } from "@app/lib/api"; +import { formatAxiosError } from "@app/lib/api/formatAxiosError"; +import { orgQueries } from "@app/lib/queries"; +import { useQuery } from "@tanstack/react-query"; +import { useTranslations } from "next-intl"; +import { useRouter } from "next/navigation"; +import { use, useActionState, useEffect, useState } from "react"; +import { GetResourceResponse } from "@server/routers/resource"; +import type { ResourceContextType } from "@app/contexts/resourceContext"; + +export default function SshSettingsPage(props: { + params: Promise<{ orgId: string }>; +}) { + const params = use(props.params); + const { resource, updateResource } = useResourceContext(); + + return ( + + + + ); +} + +function SshServerForm({ + orgId, + resource, + updateResource +}: { + orgId: string; + resource: GetResourceResponse; + updateResource: ResourceContextType["updateResource"]; +}) { + const t = useTranslations(); + const api = createApiClient(useEnvContext()); + const router = useRouter(); + + const [pamMode, setPamMode] = useState<"passthrough" | "push">( + (resource.pamMode as "passthrough" | "push") || "passthrough" + ); + const [authDaemonMode, setAuthDaemonMode] = useState<"site" | "remote">( + (resource.authDaemonMode as "site" | "remote") || "site" + ); + const [authDaemonPort, setAuthDaemonPort] = useState( + (resource as any).authDaemonPort + ? String((resource as any).authDaemonPort) + : "22123" + ); + + const [bgDestination, setBgDestination] = useState(""); + const [bgDestinationPort, setBgDestinationPort] = useState("22"); + const [bgSiteId, setBgSiteId] = useState(null); + const [bgTargetId, setBgTargetId] = useState(null); + + const { data: sites = [] } = useQuery(orgQueries.sites({ orgId })); + + const { data: bgTargetsResponse } = useQuery({ + queryKey: ["browserGatewayTargets", resource.resourceId, orgId], + queryFn: async () => { + const res = await api.get( + `/org/${orgId}/resource/${resource.resourceId}/browser-gateway-targets` + ); + return res.data.data as { + targets: Array<{ + browserGatewayTargetId: number; + resourceId: number; + siteId: number; + type: string; + destination: string; + destinationPort: number; + }>; + }; + } + }); + + useEffect(() => { + if (!bgTargetsResponse?.targets?.length) return; + const bgt = bgTargetsResponse.targets[0]; + setBgDestination(bgt.destination); + setBgDestinationPort(String(bgt.destinationPort)); + setBgSiteId(bgt.siteId); + setBgTargetId(bgt.browserGatewayTargetId); + }, [bgTargetsResponse]); + + useEffect(() => { + if (sites.length > 0 && bgSiteId === null) { + setBgSiteId(sites[0].siteId); + } + }, [sites, bgSiteId]); + + const [, formAction, isSubmitting] = useActionState(save, null); + + async function save() { + try { + await api.post(`/resource/${resource.resourceId}`, { + pamMode, + authDaemonMode, + authDaemonPort: authDaemonPort ? Number(authDaemonPort) : null + }); + + updateResource({ ...resource, pamMode, authDaemonMode }); + + if (bgDestination && bgDestinationPort) { + if (bgTargetId) { + await api.post( + `/org/${orgId}/browser-gateway-target/${bgTargetId}`, + { + type: "ssh", + destination: bgDestination, + destinationPort: Number(bgDestinationPort), + siteId: bgSiteId + } + ); + } else { + const res = await api.put( + `/org/${orgId}/resource/${resource.resourceId}/browser-gateway-target`, + { + siteId: bgSiteId ?? sites[0]?.siteId, + type: "ssh", + destination: bgDestination, + destinationPort: Number(bgDestinationPort) + } + ); + setBgTargetId(res.data.data.browserGatewayTargetId); + } + } + + 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") + ) + }); + } + } + + const authMethodOptions: StrategyOption<"passthrough" | "push">[] = [ + { + id: "passthrough", + title: t("sshAuthMethodManual"), + description: t("sshAuthMethodManualDescription") + }, + { + id: "push", + title: t("sshAuthMethodAutomated"), + description: t("sshAuthMethodAutomatedDescription") + } + ]; + + const daemonLocationOptions: StrategyOption<"site" | "remote">[] = [ + { + id: "site", + title: t("internalResourceAuthDaemonSite"), + description: t("sshDaemonLocationSiteDescription") + }, + { + id: "remote", + title: t("sshDaemonLocationRemote"), + description: t("sshDaemonLocationRemoteDescription") + } + ]; + + return ( + <> + + + + {t("sshServer")} + + + {t("sshServerDescription")} + + + +
+
+

+ {t("sshServerMode")} +

+ + {t("sshServerModeStandard")} + +
+ +
+

+ {t("sshAuthenticationMethod")} +

+ + value={pamMode} + options={authMethodOptions} + onChange={setPamMode} + cols={2} + /> +
+ +
+

+ {t("sshAuthDaemonLocation")} +

+ + value={authDaemonMode} + options={daemonLocationOptions} + onChange={setAuthDaemonMode} + cols={2} + /> +

+ {t("sshDaemonDisclaimer")}{" "} + + {t("learnMore")} + + +

+
+ +
+ + + setAuthDaemonPort(e.target.value) + } + /> +
+
+
+
+ + + + + {t("sshServerDestination")} + + + {t("sshServerDestinationDescription")} + + + +
+
+
+ + + setBgDestination(e.target.value) + } + /> +
+
+ + + setBgDestinationPort(e.target.value) + } + /> +
+
+ {sites.length > 0 && ( +
+ + +
+ )} +
+
+
+ +
+
+ + ); +}