From daeea8e7eaed0c03927bfe1bd7615f018df3d421 Mon Sep 17 00:00:00 2001 From: Owen Date: Thu, 26 Feb 2026 21:37:47 -0800 Subject: [PATCH 1/6] Add alises to quieries Fixes #2556 --- server/routers/site/listSites.ts | 2 +- server/routers/siteResource/listAllSiteResourcesByOrg.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/server/routers/site/listSites.ts b/server/routers/site/listSites.ts index 14f3024d..54b207af 100644 --- a/server/routers/site/listSites.ts +++ b/server/routers/site/listSites.ts @@ -278,7 +278,7 @@ export async function listSites( // we need to add `as` so that drizzle filters the result as a subquery const countQuery = db.$count( - querySitesBase().where(and(...conditions)) + querySitesBase().where(and(...conditions)).as("filtered_sites") ); const siteListQuery = baseQuery diff --git a/server/routers/siteResource/listAllSiteResourcesByOrg.ts b/server/routers/siteResource/listAllSiteResourcesByOrg.ts index 5aec53c7..a86b4dea 100644 --- a/server/routers/siteResource/listAllSiteResourcesByOrg.ts +++ b/server/routers/siteResource/listAllSiteResourcesByOrg.ts @@ -172,7 +172,7 @@ export async function listAllSiteResourcesByOrg( const baseQuery = querySiteResourcesBase().where(and(...conditions)); const countQuery = db.$count( - querySiteResourcesBase().where(and(...conditions)) + querySiteResourcesBase().where(and(...conditions)).as("filtered_site_resources") ); const [siteResourcesList, totalCount] = await Promise.all([ From eed87af61d87387de9185f0b66ff7443b11c170f Mon Sep 17 00:00:00 2001 From: Owen Date: Thu, 26 Feb 2026 21:43:14 -0800 Subject: [PATCH 2/6] Use ecr base to build --- Dockerfile | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index eaf24971..9af37f89 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,5 @@ -FROM node:24-slim AS base +# FROM node:24-slim AS base +FROM public.ecr.aws/docker/library/node:24-slim AS base WORKDIR /app @@ -31,7 +32,8 @@ FROM base AS builder RUN npm ci --omit=dev -FROM node:24-slim AS runner +# FROM node:24-slim AS runner +FROM public.ecr.aws/docker/library/node:24-slim AS runner WORKDIR /app From 72bf6f3c414a12a16c184a19f50ad08ed78f239f Mon Sep 17 00:00:00 2001 From: Owen Date: Fri, 27 Feb 2026 17:53:44 -0800 Subject: [PATCH 3/6] Comma seperated --- messages/en-US.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/messages/en-US.json b/messages/en-US.json index 961930bd..f6a9f2f2 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -1670,10 +1670,10 @@ "sshSudoModeCommandsDescription": "User can run only the specified commands with sudo.", "sshSudo": "Allow sudo", "sshSudoCommands": "Sudo Commands", - "sshSudoCommandsDescription": "List of commands the user is allowed to run with sudo.", + "sshSudoCommandsDescription": "Comma separated list of commands the user is allowed to run with sudo.", "sshCreateHomeDir": "Create Home Directory", "sshUnixGroups": "Unix Groups", - "sshUnixGroupsDescription": "Unix groups to add the user to on the target host.", + "sshUnixGroupsDescription": "Comma separated Unix groups to add the user to on the target host.", "retryAttempts": "Retry Attempts", "expectedResponseCodes": "Expected Response Codes", "expectedResponseCodesDescription": "HTTP status code that indicates healthy status. If left blank, 200-300 is considered healthy.", From b0a34fa21bcbb437e98476285b049d42de104405 Mon Sep 17 00:00:00 2001 From: Laurence Date: Sat, 28 Feb 2026 11:27:19 +0000 Subject: [PATCH 4/6] fix(openapi): Add openapi call after catch fix: #2561 without making an explicit call to openapi a runtime error happens because it cannot infer the type, the call to openapi is the same across the codebase --- server/routers/siteResource/listSiteResources.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/server/routers/siteResource/listSiteResources.ts b/server/routers/siteResource/listSiteResources.ts index 5bdf6709..59126f1d 100644 --- a/server/routers/siteResource/listSiteResources.ts +++ b/server/routers/siteResource/listSiteResources.ts @@ -31,12 +31,23 @@ const listSiteResourcesQuerySchema = z.object({ sort_by: z .enum(["name"]) .optional() - .catch(undefined), + .catch(undefined) + .openapi({ + type: "string", + enum: ["name"], + description: "Field to sort by" + }), order: z .enum(["asc", "desc"]) .optional() .default("asc") .catch("asc") + .openapi({ + type: "string", + enum: ["asc", "desc"], + default: "asc", + description: "Sort order" + }) }); export type ListSiteResourcesResponse = { From fdeb89113710fb954884419e75f13b6b50a187a8 Mon Sep 17 00:00:00 2001 From: Owen Date: Sat, 28 Feb 2026 12:07:42 -0800 Subject: [PATCH 5/6] Fix pagination effecting drop downs --- .../resources/proxy/[niceId]/proxy/page.tsx | 107 +++++--- src/components/CreateShareLinkForm.tsx | 70 ++---- src/components/InternalResourceForm.tsx | 234 ++++++++++-------- src/lib/queries.ts | 41 +-- 4 files changed, 236 insertions(+), 216 deletions(-) diff --git a/src/app/[orgId]/settings/resources/proxy/[niceId]/proxy/page.tsx b/src/app/[orgId]/settings/resources/proxy/[niceId]/proxy/page.tsx index e7e64ae9..51f11a2c 100644 --- a/src/app/[orgId]/settings/resources/proxy/[niceId]/proxy/page.tsx +++ b/src/app/[orgId]/settings/resources/proxy/[niceId]/proxy/page.tsx @@ -89,7 +89,14 @@ import { } from "lucide-react"; import { useTranslations } from "next-intl"; import { useRouter } from "next/navigation"; -import { use, useActionState, useCallback, useEffect, useMemo, useState } from "react"; +import { + use, + useActionState, + useCallback, + useEffect, + useMemo, + useState +} from "react"; import { useForm } from "react-hook-form"; import { z } from "zod"; @@ -184,29 +191,35 @@ function ProxyResourceTargetsForm({ setDockerStates((prev) => new Map(prev.set(siteId, dockerState))); }; - const refreshContainersForSite = useCallback(async (siteId: number) => { - const dockerManager = new DockerManager(api, siteId); - const containers = await dockerManager.fetchContainers(); + const refreshContainersForSite = useCallback( + async (siteId: number) => { + const dockerManager = new DockerManager(api, siteId); + const containers = await dockerManager.fetchContainers(); - setDockerStates((prev) => { - const newMap = new Map(prev); - const existingState = newMap.get(siteId); - if (existingState) { - newMap.set(siteId, { ...existingState, containers }); - } - return newMap; - }); - }, [api]); + setDockerStates((prev) => { + const newMap = new Map(prev); + const existingState = newMap.get(siteId); + if (existingState) { + newMap.set(siteId, { ...existingState, containers }); + } + return newMap; + }); + }, + [api] + ); - const getDockerStateForSite = useCallback((siteId: number): DockerState => { - return ( - dockerStates.get(siteId) || { - isEnabled: false, - isAvailable: false, - containers: [] - } - ); - }, [dockerStates]); + const getDockerStateForSite = useCallback( + (siteId: number): DockerState => { + return ( + dockerStates.get(siteId) || { + isEnabled: false, + isAvailable: false, + containers: [] + } + ); + }, + [dockerStates] + ); const [isAdvancedMode, setIsAdvancedMode] = useState(() => { if (typeof window !== "undefined") { @@ -220,7 +233,9 @@ function ProxyResourceTargetsForm({ const removeTarget = useCallback((targetId: number) => { setTargets((prevTargets) => { - const targetToRemove = prevTargets.find((target) => target.targetId === targetId); + const targetToRemove = prevTargets.find( + (target) => target.targetId === targetId + ); if (targetToRemove && !targetToRemove.new) { setTargetsToRemove((prev) => [...prev, targetId]); } @@ -228,21 +243,24 @@ function ProxyResourceTargetsForm({ }); }, []); - const updateTarget = useCallback((targetId: number, data: Partial) => { - setTargets((prevTargets) => { - const site = sites.find((site) => site.siteId === data.siteId); - return prevTargets.map((target) => - target.targetId === targetId - ? { - ...target, - ...data, - updated: true, - siteType: site ? site.type : target.siteType - } - : target - ); - }); - }, [sites]); + const updateTarget = useCallback( + (targetId: number, data: Partial) => { + setTargets((prevTargets) => { + const site = sites.find((site) => site.siteId === data.siteId); + return prevTargets.map((target) => + target.targetId === targetId + ? { + ...target, + ...data, + updated: true, + siteType: site ? site.type : target.siteType + } + : target + ); + }); + }, + [sites] + ); const openHealthCheckDialog = useCallback((target: LocalTarget) => { setSelectedTargetForHealthCheck(target); @@ -250,7 +268,6 @@ function ProxyResourceTargetsForm({ }, []); const columns = useMemo((): ColumnDef[] => { - const priorityColumn: ColumnDef = { id: "priority", header: () => ( @@ -581,7 +598,17 @@ function ProxyResourceTargetsForm({ actionsColumn ]; } - }, [isAdvancedMode, isHttp, sites, updateTarget, getDockerStateForSite, refreshContainersForSite, openHealthCheckDialog, removeTarget, t]); + }, [ + isAdvancedMode, + isHttp, + sites, + updateTarget, + getDockerStateForSite, + refreshContainersForSite, + openHealthCheckDialog, + removeTarget, + t + ]); function addNewTarget() { const isHttp = resource.http; diff --git a/src/components/CreateShareLinkForm.tsx b/src/components/CreateShareLinkForm.tsx index 361bfe7d..2f6f9aff 100644 --- a/src/components/CreateShareLinkForm.tsx +++ b/src/components/CreateShareLinkForm.tsx @@ -20,7 +20,7 @@ import { import { toast } from "@app/hooks/useToast"; import { zodResolver } from "@hookform/resolvers/zod"; import { AxiosResponse } from "axios"; -import { useEffect, useState } from "react"; +import { useState, useMemo } from "react"; import { useForm } from "react-hook-form"; import { z } from "zod"; import CopyTextBox from "@app/components/CopyTextBox"; @@ -39,7 +39,8 @@ import { formatAxiosError } from "@app/lib/api"; import { cn } from "@app/lib/cn"; import { createApiClient } from "@app/lib/api"; import { useEnvContext } from "@app/hooks/useEnvContext"; -import { ListResourcesResponse } from "@server/routers/resource"; +import { useQuery } from "@tanstack/react-query"; +import { orgQueries } from "@app/lib/queries"; import { Popover, PopoverContent, @@ -94,14 +95,22 @@ export default function CreateShareLinkForm({ const [isOpen, setIsOpen] = useState(false); const t = useTranslations(); - const [resources, setResources] = useState< - { - resourceId: number; - name: string; - niceId: string; - resourceUrl: string; - }[] - >([]); + const { data: allResources = [] } = useQuery( + orgQueries.resources({ orgId: org?.org.orgId ?? "" }) + ); + + const resources = useMemo( + () => + allResources + .filter((r) => r.http) + .map((r) => ({ + resourceId: r.resourceId, + name: r.name, + niceId: r.niceId, + resourceUrl: `${r.ssl ? "https://" : "http://"}${toUnicode(r.fullDomain || "")}/` + })), + [allResources] + ); const formSchema = z.object({ resourceId: z.number({ message: t("shareErrorSelectResource") }), @@ -130,47 +139,6 @@ export default function CreateShareLinkForm({ } }); - useEffect(() => { - if (!open) { - return; - } - - async function fetchResources() { - const res = await api - .get< - AxiosResponse - >(`/org/${org?.org.orgId}/resources`) - .catch((e) => { - console.error(e); - toast({ - variant: "destructive", - title: t("shareErrorFetchResource"), - description: formatAxiosError( - e, - t("shareErrorFetchResourceDescription") - ) - }); - }); - - if (res?.status === 200) { - setResources( - res.data.data.resources - .filter((r) => { - return r.http; - }) - .map((r) => ({ - resourceId: r.resourceId, - name: r.name, - niceId: r.niceId, - resourceUrl: `${r.ssl ? "https://" : "http://"}${toUnicode(r.fullDomain || "")}/` - })) - ); - } - } - - fetchResources(); - }, [open]); - async function onSubmit(values: z.infer) { setLoading(true); diff --git a/src/components/InternalResourceForm.tsx b/src/components/InternalResourceForm.tsx index 3d18bf27..6df1aceb 100644 --- a/src/components/InternalResourceForm.tsx +++ b/src/components/InternalResourceForm.tsx @@ -1189,137 +1189,151 @@ export function InternalResourceForm({ {/* SSH Access tab */} {!disableEnterpriseFeatures && mode !== "cidr" && ( -
- -
- -
- {t.rich( - "internalResourceAuthDaemonDescription", - { - docsLink: (chunks) => ( - - {chunks} - - - ) - } - )} +
+ +
+ +
+ {t.rich( + "internalResourceAuthDaemonDescription", + { + docsLink: (chunks) => ( + + {chunks} + + + ) + } + )} +
-
-
- ( - - - {t( - "internalResourceAuthDaemonStrategyLabel" - )} - - - - value={field.value ?? undefined} - options={[ - { - id: "site", - title: t( - "internalResourceAuthDaemonSite" - ), - description: t( - "internalResourceAuthDaemonSiteDescription" - ), - disabled: sshSectionDisabled - }, - { - id: "remote", - title: t( - "internalResourceAuthDaemonRemote" - ), - description: t( - "internalResourceAuthDaemonRemoteDescription" - ), - disabled: sshSectionDisabled - } - ]} - onChange={(v) => { - if (sshSectionDisabled) return; - field.onChange(v); - if (v === "site") { - form.setValue( - "authDaemonPort", - null - ); - } - }} - cols={2} - /> - - - - )} - /> - {authDaemonMode === "remote" && ( +
( {t( - "internalResourceAuthDaemonPort" + "internalResourceAuthDaemonStrategyLabel" )} - { - if (sshSectionDisabled) return; - const v = - e.target.value; - if (v === "") { - field.onChange( + + value={ + field.value ?? undefined + } + options={[ + { + id: "site", + title: t( + "internalResourceAuthDaemonSite" + ), + description: t( + "internalResourceAuthDaemonSiteDescription" + ), + disabled: + sshSectionDisabled + }, + { + id: "remote", + title: t( + "internalResourceAuthDaemonRemote" + ), + description: t( + "internalResourceAuthDaemonRemoteDescription" + ), + disabled: + sshSectionDisabled + } + ]} + onChange={(v) => { + if (sshSectionDisabled) + return; + field.onChange(v); + if (v === "site") { + form.setValue( + "authDaemonPort", null ); - return; } - const num = parseInt( - v, - 10 - ); - field.onChange( - Number.isNaN(num) - ? null - : num - ); }} + cols={2} /> )} /> - )} + {authDaemonMode === "remote" && ( + ( + + + {t( + "internalResourceAuthDaemonPort" + )} + + + { + if ( + sshSectionDisabled + ) + return; + const v = + e.target.value; + if (v === "") { + field.onChange( + null + ); + return; + } + const num = + parseInt(v, 10); + field.onChange( + Number.isNaN( + num + ) + ? null + : num + ); + }} + /> + + + + )} + /> + )} +
-
)} diff --git a/src/lib/queries.ts b/src/lib/queries.ts index fe5350ff..d3e962d7 100644 --- a/src/lib/queries.ts +++ b/src/lib/queries.ts @@ -4,7 +4,8 @@ import type { ListClientsResponse } from "@server/routers/client"; import type { ListDomainsResponse } from "@server/routers/domain"; import type { GetResourceWhitelistResponse, - ListResourceNamesResponse + ListResourceNamesResponse, + ListResourcesResponse } from "@server/routers/resource"; import type { ListRolesResponse } from "@server/routers/role"; import type { ListSitesResponse } from "@server/routers/site"; @@ -90,23 +91,13 @@ export const productUpdatesQueries = { }) }; -export const clientFilterSchema = z.object({ - pageSize: z.int().prefault(1000).optional() -}); - export const orgQueries = { - clients: ({ - orgId, - filters - }: { - orgId: string; - filters?: z.infer; - }) => + clients: ({ orgId }: { orgId: string }) => queryOptions({ - queryKey: ["ORG", orgId, "CLIENTS", filters] as const, + queryKey: ["ORG", orgId, "CLIENTS"] as const, queryFn: async ({ signal, meta }) => { const sp = new URLSearchParams({ - pageSize: (filters?.pageSize ?? 1000).toString() + pageSize: "10000" }); const res = await meta!.api.get< @@ -143,9 +134,13 @@ export const orgQueries = { queryOptions({ queryKey: ["ORG", orgId, "SITES"] as const, queryFn: async ({ signal, meta }) => { + const sp = new URLSearchParams({ + pageSize: "10000" + }); + const res = await meta!.api.get< AxiosResponse - >(`/org/${orgId}/sites`, { signal }); + >(`/org/${orgId}/sites?${sp.toString()}`, { signal }); return res.data.data.sites; } }), @@ -182,6 +177,22 @@ export const orgQueries = { ); return res.data.data.idps; } + }), + + resources: ({ orgId }: { orgId: string }) => + queryOptions({ + queryKey: ["ORG", orgId, "RESOURCES"] as const, + queryFn: async ({ signal, meta }) => { + const sp = new URLSearchParams({ + pageSize: "10000" + }); + + const res = await meta!.api.get< + AxiosResponse + >(`/org/${orgId}/resources?${sp.toString()}`, { signal }); + + return res.data.data.resources; + } }) }; From 50c2aa01118de4c6f86e48bd442f03ed0e68002b Mon Sep 17 00:00:00 2001 From: Owen Date: Sat, 28 Feb 2026 12:14:27 -0800 Subject: [PATCH 6/6] Add default memory limits --- docker-compose.example.yml | 6 ++++++ install/config/docker-compose.yml | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/docker-compose.example.yml b/docker-compose.example.yml index 84a5140b..50cb1bcc 100644 --- a/docker-compose.example.yml +++ b/docker-compose.example.yml @@ -4,6 +4,12 @@ services: image: fosrl/pangolin:latest container_name: pangolin restart: unless-stopped + deploy: + resources: + limits: + memory: 1g + reservations: + memory: 256m volumes: - ./config:/app/config healthcheck: diff --git a/install/config/docker-compose.yml b/install/config/docker-compose.yml index e828ea6c..c0206e5b 100644 --- a/install/config/docker-compose.yml +++ b/install/config/docker-compose.yml @@ -4,6 +4,12 @@ services: image: docker.io/fosrl/pangolin:{{if .IsEnterprise}}ee-{{end}}{{.PangolinVersion}} container_name: pangolin restart: unless-stopped + deploy: + resources: + limits: + memory: 1g + reservations: + memory: 256m volumes: - ./config:/app/config healthcheck: