From d1af7a153fa6dd72e8fb4fd93a994c614b7b246f Mon Sep 17 00:00:00 2001 From: Owen Date: Fri, 5 Jun 2026 16:57:53 -0700 Subject: [PATCH] Enforece some more things on the types --- server/lib/blueprints/proxyResources.ts | 7 + server/lib/blueprints/types.ts | 74 ++++++++++- .../resources/public/[niceId]/ssh/page.tsx | 121 +++++++++--------- 3 files changed, 137 insertions(+), 65 deletions(-) diff --git a/server/lib/blueprints/proxyResources.ts b/server/lib/blueprints/proxyResources.ts index 0fb3861e6..b17878974 100644 --- a/server/lib/blueprints/proxyResources.ts +++ b/server/lib/blueprints/proxyResources.ts @@ -579,6 +579,13 @@ export async function updateProxyResources( ? (resourceData["proxy-protocol-version"] ?? 1) : 1, + pamMode: + resourceData["auth-daemon"]?.pam || + "passthrough", + authDaemonMode: + resourceData["auth-daemon"]?.mode || "native", + authDaemonPort: + resourceData["auth-daemon"]?.port || 22123, resourcePolicyId: null, defaultResourcePolicyId: inlinePolicyId }) diff --git a/server/lib/blueprints/types.ts b/server/lib/blueprints/types.ts index fc73d83a0..a98843a99 100644 --- a/server/lib/blueprints/types.ts +++ b/server/lib/blueprints/types.ts @@ -268,8 +268,37 @@ export const PublicResourceSchema = z return true; } - // If protocol/mode is http, it must have a full-domain - if ((resource.mode ?? resource.protocol) === "http") { + const effectiveProtocol = resource.mode ?? resource.protocol; + if (effectiveProtocol !== "ssh") { + return true; + } + + const authDaemonMode = resource["auth-daemon"]?.mode; + if (authDaemonMode !== "native" && authDaemonMode !== "site") { + return true; + } + + return ( + resource.targets.filter((target) => target != null).length <= 1 + ); + }, + { + path: ["targets"], + error: "When protocol is 'ssh' and auth-daemon mode is 'native' or 'site', only one target/site is allowed" + } + ) + .refine( + (resource) => { + if (isTargetsOnlyResource(resource)) { + return true; + } + + // If protocol/mode is http, ssh, rdp, or vnc, it must have a full-domain + const effectiveProtocol = resource.mode ?? resource.protocol; + if ( + effectiveProtocol !== undefined && + ["http", "ssh", "rdp", "vnc"].includes(effectiveProtocol) + ) { return ( resource["full-domain"] !== undefined && resource["full-domain"].length > 0 @@ -279,7 +308,7 @@ export const PublicResourceSchema = z }, { path: ["full-domain"], - error: "When protocol is 'http', a 'full-domain' must be provided" + error: "When protocol is 'http', 'ssh', 'rdp', or 'vnc', a 'full-domain' must be provided" } ) .refine( @@ -506,7 +535,44 @@ export const PrivateResourceSchema = z { message: "Destination must be a valid CIDR notation for cidr mode" } - ); + ) + .refine( + (data) => { + if (data.mode !== "ssh") { + return true; + } + + const authDaemonMode = data["auth-daemon"]?.mode; + if (authDaemonMode !== "native" && authDaemonMode !== "site") { + return true; + } + + const uniqueSites = new Set(); + if (data.site) { + uniqueSites.add(data.site); + } + for (const site of data.sites) { + uniqueSites.add(site); + } + + return uniqueSites.size <= 1; + }, + { + path: ["sites"], + message: + "When mode is 'ssh' and auth-daemon mode is 'native' or 'site', only one site/target is allowed" + } + ) + .transform((data) => { + if ( + data.mode === "ssh" && + data.destination !== undefined && + data["destination-port"] === undefined + ) { + data["destination-port"] = 22; + } + return data; + }); export const ResourcePolicyRuleSchema = RuleSchema; diff --git a/src/app/[orgId]/settings/resources/public/[niceId]/ssh/page.tsx b/src/app/[orgId]/settings/resources/public/[niceId]/ssh/page.tsx index 0187a6c62..c6487ad68 100644 --- a/src/app/[orgId]/settings/resources/public/[niceId]/ssh/page.tsx +++ b/src/app/[orgId]/settings/resources/public/[niceId]/ssh/page.tsx @@ -205,7 +205,8 @@ function SshServerForm({ ? [] : targets.map((target) => ({ targetId: target.targetId, - siteId: target.siteId + siteId: target.siteId, + authToken: target.authToken })) ); @@ -253,7 +254,8 @@ function SshServerForm({ }); if (isNative) { - if (values.selectedNativeSite) { + const nativeSite = values.selectedNativeSite; + if (nativeSite) { if (nativeExistingTarget) { await api.post( `/target/${nativeExistingTarget.targetId}`, @@ -261,16 +263,20 @@ function SshServerForm({ mode: "ssh", ip: "localhost", port: 22, - siteId: selectedNativeSite.siteId, + siteId: nativeSite.siteId, authToken: nativeExistingTarget.authToken, hcEnabled: false } ); + setNativeExistingTarget({ + ...nativeExistingTarget, + siteId: nativeSite.siteId + }); } else { const res = await api.put( `/resource/${resource.resourceId}/target`, { - siteId: selectedNativeSite.siteId, + siteId: nativeSite.siteId, mode: "ssh", ip: "localhost", port: 22, @@ -279,7 +285,7 @@ function SshServerForm({ ); setNativeExistingTarget({ targetId: res.data.data.targetId, - siteId: selectedNativeSite.siteId, + siteId: nativeSite.siteId, authToken: res.data.data.authToken }); } @@ -304,71 +310,64 @@ function SshServerForm({ (t) => !selectedSiteIds.has(t.siteId) ); await Promise.all( - toDelete.map((t) => - api.delete( - `/org/${orgId}/browser-gateway-target/${t.browserGatewayTargetId}` ->>>>>>> 8ee520dbb58f6bd4009581c79322f77b17ff6757 - ) + toDelete.map((t) => api.delete(`/target/${t.targetId}`)) + ); + + const toUpdate = existingTargets.filter((t) => + selectedSiteIds.has(t.siteId) + ); + await Promise.all( + toUpdate.map((t) => + api.post(`/target/${t.targetId}`, { + mode: "ssh", + ip: values.destination, + port: Number(values.destinationPort), + siteId: t.siteId, + authToken: t.authToken, + hcEnabled: false + }) ) ); -<<<<<<< HEAD - const toUpdate = existingTargets.filter((t) => - selectedSiteIds.has(t.siteId) - ); - await Promise.all( - toUpdate.map((t) => - api.post( - `/target/${t.targetId}`, - { - mode: "ssh", - ip: bgDestination, - api.delete(`/target/${t.targetId}`) - } - ) + const toCreate = activeSites.filter( + (s) => !existingSiteIds.has(s.siteId) ); - -<<<<<<< HEAD - const toCreate = selectedSites.filter( - (s) => !existingSiteIds.has(s.siteId) - ); - `/target/${t.targetId}`, - toCreate.map((s) => - mode: "ssh", - ip: values.destination, - port: Number(values.destinationPort), - siteId: t.siteId, - authToken: t.authToken, - hcEnabled: false - port: Number(bgDestinationPort), - hcEnabled: false - ) - ); - + const created = await Promise.all( + toCreate.map((s) => + api.put(`/resource/${resource.resourceId}/target`, { + siteId: s.siteId, + mode: "ssh", + ip: values.destination, + port: Number(values.destinationPort), + hcEnabled: false + }) ) ); -<<<<<<< HEAD - const newTargets: ExistingTarget[] = created.map( - (res, i) => ({ - `/resource/${resource.resourceId}/target`, - siteId: toCreate[i].siteId, - authToken: res.data.data.authToken - mode: "ssh", - ip: values.destination, - port: Number(values.destinationPort), - hcEnabled: false const newTargets: ExistingTarget[] = created.map((res, i) => ({ - browserGatewayTargetId: - ) - ); + targetId: res.data.data.targetId, + siteId: toCreate[i].siteId, + authToken: res.data.data.authToken + })); + setExistingTargets([...toUpdate, ...newTargets]); + } + + toast({ + title: t("settingsUpdated"), + description: t("settingsUpdatedDescription") }); - const newTargets: ExistingTarget[] = created.map((res, i) => ({ - targetId: res.data.data.targetId, - siteId: toCreate[i].siteId, - authToken: res.data.data.authToken - })); - setExistingTargets([...toUpdate, ...newTargets]); + router.refresh(); + } catch (err) { + console.error(err); + toast({ + variant: "destructive", + title: t("settingsErrorUpdate"), + description: formatAxiosError( + err, + t("settingsErrorUpdateDescription") + ) + }); + } } const authMethodOptions: StrategyOption<"passthrough" | "push">[] = [