From c86c43f78cf7eaf40d472b7111dd52552afa9c4a Mon Sep 17 00:00:00 2001 From: Andrew Munsell Date: Wed, 29 Apr 2026 16:11:24 -0700 Subject: [PATCH] feat(api-keys): add rename support in permissions modal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add an editable key name field at the top of the permissions modal, allowing users to rename API keys alongside existing permission settings. The backend already supported name updates via PATCH /api/keys/:id — this wires the UI to send the name field and refreshes the key list on success. Changes: - Add keyName state and text input to PermissionsModal - Update handleUpdatePermissions to validate and send name in PATCH body - Add integration test for rename via PATCH (valid, empty, too-long names) - Update E2E mock to handle PATCH requests --- CHANGELOG.md | 4 + .../api-manager/ApiManagerPageClient.tsx | 40 ++++- tests/e2e/api-keys-flow.spec.ts | 168 ++++++++++++++++++ tests/integration/api-keys.test.ts | 41 +++++ 4 files changed, 248 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d829a062b0..35f9169e00 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### ✨ New Features + +- **feat(api-keys):** add rename support in the permissions modal — editable key name field with validation + --- ## [3.7.4] — 2026-04-28 diff --git a/src/app/(dashboard)/dashboard/api-manager/ApiManagerPageClient.tsx b/src/app/(dashboard)/dashboard/api-manager/ApiManagerPageClient.tsx index a71b38a258..fff2b31ec3 100644 --- a/src/app/(dashboard)/dashboard/api-manager/ApiManagerPageClient.tsx +++ b/src/app/(dashboard)/dashboard/api-manager/ApiManagerPageClient.tsx @@ -6,7 +6,7 @@ import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard"; import { useTranslations } from "next-intl"; // Constants for validation -const MAX_KEY_NAME_LENGTH = 100; +const MAX_KEY_NAME_LENGTH = 200; const MAX_SELECTED_MODELS = 500; // Debounce hook for search optimization @@ -232,14 +232,13 @@ export default function ApiManagerPageClient() { const clearError = useCallback(() => setError(null), []); const handleCreateKey = async () => { - // Validate and sanitize input - const sanitizedName = sanitizeInput(newKeyName); - const validation = validateKeyName(sanitizedName, t); - + // Validate raw input first, then sanitize + const validation = validateKeyName(newKeyName, t); if (!validation.valid) { setError(validation.error || t("invalidKeyName")); return; } + const sanitizedName = sanitizeInput(newKeyName); setIsSubmitting(true); clearError(); @@ -322,6 +321,7 @@ export default function ApiManagerPageClient() { }; const handleUpdatePermissions = async ( + name: string, allowedModels: string[], noLog: boolean, allowedConnections: string[], @@ -332,6 +332,14 @@ export default function ApiManagerPageClient() { ) => { if (!editingKey || !editingKey.id) return; + // Validate raw input first, then sanitize + const nameValidation = validateKeyName(name, t); + if (!nameValidation.valid) { + setError(nameValidation.error || t("invalidKeyName")); + return; + } + const sanitizedName = sanitizeInput(name); + // Validate models array if (!Array.isArray(allowedModels)) { setError(t("invalidModelsSelection")); @@ -366,6 +374,7 @@ export default function ApiManagerPageClient() { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ + name: sanitizedName, allowedModels: validModels, allowedConnections: validConnections, noLog, @@ -764,6 +773,7 @@ export default function ApiManagerPageClient() { value={newKeyName} onChange={(e) => setNewKeyName(e.target.value)} placeholder={t("keyNamePlaceholder")} + maxLength={MAX_KEY_NAME_LENGTH} autoFocus />

{t("keyNameDesc")}

@@ -862,6 +872,7 @@ const PermissionsModal = memo(function PermissionsModal({ searchModel: string; onSearchChange: (v: string) => void; onSave: ( + name: string, models: string[], noLog: boolean, connections: string[], @@ -879,6 +890,7 @@ const PermissionsModal = memo(function PermissionsModal({ const initialConnections = Array.isArray(apiKey?.allowedConnections) ? apiKey.allowedConnections : []; + const [keyName, setKeyName] = useState(apiKey?.name ?? ""); const [selectedModels, setSelectedModels] = useState(initialModels); const [allowAll, setAllowAll] = useState(initialModels.length === 0); const [noLogEnabled, setNoLogEnabled] = useState(apiKey?.noLog === true); @@ -993,6 +1005,7 @@ const PermissionsModal = memo(function PermissionsModal({ } : null; onSave( + keyName, allowAll ? [] : selectedModels, noLogEnabled, allowAllConnections ? [] : selectedConnections, @@ -1003,6 +1016,7 @@ const PermissionsModal = memo(function PermissionsModal({ ); }, [ onSave, + keyName, allowAll, selectedModels, noLogEnabled, @@ -1028,6 +1042,22 @@ const PermissionsModal = memo(function PermissionsModal({ onClose={onClose} >
+ {/* Key Name */} +
+
+

{t("keyName")}

+

{t("keyNameDesc")}

+
+
+ setKeyName(e.target.value)} + placeholder={t("keyNamePlaceholder")} + maxLength={MAX_KEY_NAME_LENGTH} + /> +
+
+ {/* Access Mode Toggle */}