From af5635e422cd439849e756aa4608e29d426396bc Mon Sep 17 00:00:00 2001 From: Jack <5443152+hijak@users.noreply.github.com> Date: Wed, 27 May 2026 10:20:40 +0100 Subject: [PATCH] feat(api): add endpoint restrictions for client API keys (#2777) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Merge PR #2777 — feat(api): add endpoint restrictions for client API keys --- .../api-manager/ApiManagerPageClient.tsx | 103 ++++++++- src/i18n/messages/en.json | 3 + src/lib/db/apiKeys.ts | 27 ++- src/shared/constants/endpointCategories.ts | 132 +++++++++++ src/shared/utils/apiKeyPolicy.ts | 22 ++ src/shared/validation/schemas.ts | 4 +- tests/unit/endpoint-categories.test.ts | 115 ++++++++++ .../unit/endpoint-restrictions-policy.test.ts | 210 ++++++++++++++++++ 8 files changed, 610 insertions(+), 6 deletions(-) create mode 100644 src/shared/constants/endpointCategories.ts create mode 100644 tests/unit/endpoint-categories.test.ts create mode 100644 tests/unit/endpoint-restrictions-policy.test.ts diff --git a/src/app/(dashboard)/dashboard/api-manager/ApiManagerPageClient.tsx b/src/app/(dashboard)/dashboard/api-manager/ApiManagerPageClient.tsx index f045ad2144..81ffb76516 100644 --- a/src/app/(dashboard)/dashboard/api-manager/ApiManagerPageClient.tsx +++ b/src/app/(dashboard)/dashboard/api-manager/ApiManagerPageClient.tsx @@ -5,6 +5,7 @@ import { Card, Button, Input, Modal, CardSkeleton } from "@/shared/components"; import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard"; import { useTranslations } from "next-intl"; import { getProviderDisplayName } from "@/lib/display/names"; +import { ENDPOINT_CATEGORIES } from "@/shared/constants/endpointCategories"; import ApiKeyFilterBar from "./components/ApiKeyFilterBar"; import { isKeyActive, @@ -88,6 +89,7 @@ interface ApiKey { accessSchedule?: AccessSchedule | null; rateLimits?: Array<{ limit: number; window: number }> | null; scopes?: string[]; + allowedEndpoints?: string[]; createdAt: string; } @@ -461,7 +463,8 @@ export default function ApiManagerPageClient() { maxSessions: number, accessSchedule: AccessSchedule | null, rateLimits: Array<{ limit: number; window: number }> | null, - scopes: string[] + scopes: string[], + allowedEndpoints: string[] ) => { if (!editingKey || !editingKey.id) return; @@ -521,6 +524,7 @@ export default function ApiManagerPageClient() { accessSchedule, rateLimits, scopes, + allowedEndpoints, }), }); @@ -1161,7 +1165,8 @@ const PermissionsModal = memo(function PermissionsModal({ maxSessions: number, accessSchedule: AccessSchedule | null, rateLimits: Array<{ limit: number; window: number }> | null, - scopes: string[] + scopes: string[], + allowedEndpoints: string[] ) => void; }) { const t = useTranslations("apiManager"); @@ -1218,6 +1223,10 @@ const PermissionsModal = memo(function PermissionsModal({ return new Set(); }); + const initialEndpoints = Array.isArray(apiKey?.allowedEndpoints) ? apiKey.allowedEndpoints : []; + const [selectedEndpoints, setSelectedEndpoints] = useState(initialEndpoints); + const [allowAllEndpoints, setAllowAllEndpoints] = useState(initialEndpoints.length === 0); + // Memoize callbacks to prevent child re-renders const handleToggleModel = useCallback( (modelId: string) => { @@ -1304,6 +1313,16 @@ const PermissionsModal = memo(function PermissionsModal({ [allowAllConnections] ); + const handleToggleEndpoint = useCallback( + (categoryId: string) => { + if (allowAllEndpoints) return; + setSelectedEndpoints((prev) => + prev.includes(categoryId) ? prev.filter((e) => e !== categoryId) : [...prev, categoryId] + ); + }, + [allowAllEndpoints] + ); + const handleSave = useCallback(() => { // Clear previous inline errors setNameError(null); @@ -1351,7 +1370,8 @@ const PermissionsModal = memo(function PermissionsModal({ maxSessions, schedule, rateLimits.length > 0 ? rateLimits : null, - manageEnabled ? ["manage"] : [] + manageEnabled ? ["manage"] : [], + allowAllEndpoints ? [] : selectedEndpoints ); }, [ onSave, @@ -1376,6 +1396,8 @@ const PermissionsModal = memo(function PermissionsModal({ scheduleDays, scheduleTz, rateLimits, + allowAllEndpoints, + selectedEndpoints, t, ]); @@ -2155,6 +2177,81 @@ const PermissionsModal = memo(function PermissionsModal({ )} + {/* Allowed Endpoints Section */} +
+
+
+

{t("endpointRestrictions")}

+

+ {allowAllEndpoints + ? t("allEndpointsAllowed") + : t("endpointsRestricted", { + count: selectedEndpoints.length, + })} +

+
+
+ + +
+
+ {!allowAllEndpoints && ( +
+ {ENDPOINT_CATEGORIES.map((cat) => { + const isSelected = selectedEndpoints.includes(cat.id); + return ( + + ); + })} +
+ )} +
+ {/* Actions */}