From 52f26396ac81a169cfb2cb93e816c8ff2fe9461f Mon Sep 17 00:00:00 2001 From: ChanningHe Date: Sun, 8 Feb 2026 12:19:51 +0900 Subject: [PATCH 1/9] feat(integration): add domain CRUD endpoints to integration API --- messages/en-US.json | 6 ++ server/middlewares/integration/index.ts | 1 + .../integration/verifyApiKeyDomainAccess.ts | 90 +++++++++++++++++++ server/routers/integration.ts | 53 ++++++++++- src/components/PermissionsSelectBox.tsx | 11 ++- 5 files changed, 159 insertions(+), 2 deletions(-) create mode 100644 server/middlewares/integration/verifyApiKeyDomainAccess.ts diff --git a/messages/en-US.json b/messages/en-US.json index 961930bdc..afb40fb6f 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -1102,6 +1102,12 @@ "actionGetUser": "Get User", "actionGetOrgUser": "Get Organization User", "actionListOrgDomains": "List Organization Domains", + "actionGetDomain": "Get Domain", + "actionCreateOrgDomain": "Create Domain", + "actionUpdateOrgDomain": "Update Domain", + "actionDeleteOrgDomain": "Delete Domain", + "actionGetDNSRecords": "Get DNS Records", + "actionRestartOrgDomain": "Restart Domain", "actionCreateSite": "Create Site", "actionDeleteSite": "Delete Site", "actionGetSite": "Get Site", diff --git a/server/middlewares/integration/index.ts b/server/middlewares/integration/index.ts index 565751913..df186c1c8 100644 --- a/server/middlewares/integration/index.ts +++ b/server/middlewares/integration/index.ts @@ -14,3 +14,4 @@ export * from "./verifyApiKeyApiKeyAccess"; export * from "./verifyApiKeyClientAccess"; export * from "./verifyApiKeySiteResourceAccess"; export * from "./verifyApiKeyIdpAccess"; +export * from "./verifyApiKeyDomainAccess"; diff --git a/server/middlewares/integration/verifyApiKeyDomainAccess.ts b/server/middlewares/integration/verifyApiKeyDomainAccess.ts new file mode 100644 index 000000000..db0f5d95d --- /dev/null +++ b/server/middlewares/integration/verifyApiKeyDomainAccess.ts @@ -0,0 +1,90 @@ +import { Request, Response, NextFunction } from "express"; +import { db, domains, orgDomains, apiKeyOrg } from "@server/db"; +import { and, eq } from "drizzle-orm"; +import createHttpError from "http-errors"; +import HttpCode from "@server/types/HttpCode"; + +export async function verifyApiKeyDomainAccess( + req: Request, + res: Response, + next: NextFunction +) { + try { + const apiKey = req.apiKey; + const domainId = + req.params.domainId || req.body.domainId || req.query.domainId; + const orgId = req.params.orgId; + + if (!apiKey) { + return next( + createHttpError(HttpCode.UNAUTHORIZED, "Key not authenticated") + ); + } + + if (!domainId) { + return next( + createHttpError(HttpCode.BAD_REQUEST, "Invalid domain ID") + ); + } + + if (apiKey.isRoot) { + // Root keys can access any domain in any org + return next(); + } + + // Verify domain exists and belongs to the organization + const [domain] = await db + .select() + .from(domains) + .innerJoin(orgDomains, eq(orgDomains.domainId, domains.domainId)) + .where( + and( + eq(orgDomains.domainId, domainId), + eq(orgDomains.orgId, orgId) + ) + ) + .limit(1); + + if (!domain) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + `Domain with ID ${domainId} not found in organization ${orgId}` + ) + ); + } + + // Verify the API key has access to this organization + if (!req.apiKeyOrg) { + const apiKeyOrgRes = await db + .select() + .from(apiKeyOrg) + .where( + and( + eq(apiKeyOrg.apiKeyId, apiKey.apiKeyId), + eq(apiKeyOrg.orgId, orgId) + ) + ) + .limit(1); + req.apiKeyOrg = apiKeyOrgRes[0]; + } + + if (!req.apiKeyOrg) { + return next( + createHttpError( + HttpCode.FORBIDDEN, + "Key does not have access to this organization" + ) + ); + } + + return next(); + } catch (error) { + return next( + createHttpError( + HttpCode.INTERNAL_SERVER_ERROR, + "Error verifying domain access" + ) + ); + } +} diff --git a/server/routers/integration.ts b/server/routers/integration.ts index 7272d740d..a36a61e84 100644 --- a/server/routers/integration.ts +++ b/server/routers/integration.ts @@ -27,7 +27,8 @@ import { verifyApiKeyClientAccess, verifyApiKeySiteResourceAccess, verifyApiKeySetResourceClients, - verifyLimits + verifyLimits, + verifyApiKeyDomainAccess } from "@server/middlewares"; import HttpCode from "@server/types/HttpCode"; import { Router } from "express"; @@ -347,6 +348,56 @@ authenticated.get( domain.listDomains ); +authenticated.get( + "/org/:orgId/domain/:domainId", + verifyApiKeyOrgAccess, + verifyApiKeyDomainAccess, + verifyApiKeyHasAction(ActionsEnum.getDomain), + domain.getDomain +); + +authenticated.put( + "/org/:orgId/domain", + verifyApiKeyOrgAccess, + verifyApiKeyHasAction(ActionsEnum.createOrgDomain), + logActionAudit(ActionsEnum.createOrgDomain), + domain.createOrgDomain +); + +authenticated.patch( + "/org/:orgId/domain/:domainId", + verifyApiKeyOrgAccess, + verifyApiKeyDomainAccess, + verifyApiKeyHasAction(ActionsEnum.updateOrgDomain), + domain.updateOrgDomain +); + +authenticated.delete( + "/org/:orgId/domain/:domainId", + verifyApiKeyOrgAccess, + verifyApiKeyDomainAccess, + verifyApiKeyHasAction(ActionsEnum.deleteOrgDomain), + logActionAudit(ActionsEnum.deleteOrgDomain), + domain.deleteAccountDomain +); + +authenticated.get( + "/org/:orgId/domain/:domainId/dns-records", + verifyApiKeyOrgAccess, + verifyApiKeyDomainAccess, + verifyApiKeyHasAction(ActionsEnum.getDNSRecords), + domain.getDNSRecords +); + +authenticated.post( + "/org/:orgId/domain/:domainId/restart", + verifyApiKeyOrgAccess, + verifyApiKeyDomainAccess, + verifyApiKeyHasAction(ActionsEnum.restartOrgDomain), + logActionAudit(ActionsEnum.restartOrgDomain), + domain.restartOrgDomain +); + authenticated.get( "/org/:orgId/invitations", verifyApiKeyOrgAccess, diff --git a/src/components/PermissionsSelectBox.tsx b/src/components/PermissionsSelectBox.tsx index b11c635a6..7536ecdcf 100644 --- a/src/components/PermissionsSelectBox.tsx +++ b/src/components/PermissionsSelectBox.tsx @@ -31,7 +31,6 @@ function getActionsCategories(root: boolean) { [t("actionListInvitations")]: "listInvitations", [t("actionRemoveUser")]: "removeUser", [t("actionListUsers")]: "listUsers", - [t("actionListOrgDomains")]: "listOrgDomains", [t("updateOrgUser")]: "updateOrgUser", [t("createOrgUser")]: "createOrgUser", [t("actionApplyBlueprint")]: "applyBlueprint", @@ -39,6 +38,16 @@ function getActionsCategories(root: boolean) { [t("actionGetBlueprint")]: "getBlueprint" }, + Domain: { + [t("actionListOrgDomains")]: "listOrgDomains", + [t("actionGetDomain")]: "getDomain", + [t("actionCreateOrgDomain")]: "createOrgDomain", + [t("actionUpdateOrgDomain")]: "updateOrgDomain", + [t("actionDeleteOrgDomain")]: "deleteOrgDomain", + [t("actionGetDNSRecords")]: "getDNSRecords", + [t("actionRestartOrgDomain")]: "restartOrgDomain" + }, + Site: { [t("actionCreateSite")]: "createSite", [t("actionDeleteSite")]: "deleteSite", From 81c1a1da9c82cf450856fdadd39a611deb201f77 Mon Sep 17 00:00:00 2001 From: Laurence Date: Thu, 26 Feb 2026 15:45:41 +0000 Subject: [PATCH 2/9] enhance(sidebar): make mobile org selector sticky Make org selector sticky on mobile sidebar Move OrgSelector outside the scrollable container so it stays fixed at the top while menu items scroll, matching the desktop sidebar behavior introduced in 9b2c0d0b. --- src/components/LayoutMobileMenu.tsx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/components/LayoutMobileMenu.tsx b/src/components/LayoutMobileMenu.tsx index b661d780a..c453be72f 100644 --- a/src/components/LayoutMobileMenu.tsx +++ b/src/components/LayoutMobileMenu.tsx @@ -69,14 +69,14 @@ export function LayoutMobileMenu({ {t("navbarDescription")} +
+ +
+
-
- -
-
{!isAdminPage && user.serverAdmin && ( From daeea8e7eaed0c03927bfe1bd7615f018df3d421 Mon Sep 17 00:00:00 2001 From: Owen Date: Thu, 26 Feb 2026 21:37:47 -0800 Subject: [PATCH 3/9] 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 14f3024d5..54b207af3 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 5aec53c79..a86b4deac 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 4/9] Use ecr base to build --- Dockerfile | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index eaf249713..9af37f89c 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 5/9] 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 961930bdc..f6a9f2f24 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 6/9] 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 5bdf67099..59126f1d0 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 7/9] 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 e7e64ae98..51f11a2c3 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 361bfe7d0..2f6f9aff2 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 3d18bf276..6df1aceb7 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 fe5350ff9..d3e962d74 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 8/9] 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 84a5140b4..50cb1bcc1 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 e828ea6c6..c0206e5bf 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: From c20babcb53ae502d2f2dd5d60c3d6bcd08954bc5 Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Sun, 1 Mar 2026 11:13:49 -0800 Subject: [PATCH 9/9] fix org selector spacing on mobile --- src/components/LayoutMobileMenu.tsx | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/components/LayoutMobileMenu.tsx b/src/components/LayoutMobileMenu.tsx index c453be72f..e1c883a2b 100644 --- a/src/components/LayoutMobileMenu.tsx +++ b/src/components/LayoutMobileMenu.tsx @@ -69,15 +69,16 @@ export function LayoutMobileMenu({ {t("navbarDescription")} -
- +
+
+ +
-
-
+
{!isAdminPage && user.serverAdmin && (