diff --git a/open-sse/services/tokenRefresh.ts b/open-sse/services/tokenRefresh.ts index 09a7861bc9..5001730875 100755 --- a/open-sse/services/tokenRefresh.ts +++ b/open-sse/services/tokenRefresh.ts @@ -871,7 +871,12 @@ export async function getAccessToken(provider, credentials, log, proxyConfig: un } const entry = { promise: null, waiters: 0 }; - entry.promise = _getAccessTokenWithStalenessCheck(provider, credentials, log, proxyConfig).finally(() => { + entry.promise = _getAccessTokenWithStalenessCheck( + provider, + credentials, + log, + proxyConfig + ).finally(() => { connectionRefreshMutex.delete(connectionId); }); connectionRefreshMutex.set(connectionId, entry); diff --git a/src/app/(dashboard)/dashboard/api-manager/ApiManagerPageClient.tsx b/src/app/(dashboard)/dashboard/api-manager/ApiManagerPageClient.tsx index e2c71e54d6..06e27cc639 100644 --- a/src/app/(dashboard)/dashboard/api-manager/ApiManagerPageClient.tsx +++ b/src/app/(dashboard)/dashboard/api-manager/ApiManagerPageClient.tsx @@ -107,7 +107,9 @@ export default function ApiManagerPageClient() { const [editingKey, setEditingKey] = useState(null); const [showPermissionsModal, setShowPermissionsModal] = useState(false); const [searchModel, setSearchModel] = useState(""); - const [error, setError] = useState(null); + const [pageError, setPageError] = useState(null); + const [nameError, setNameError] = useState(null); + const [createError, setCreateError] = useState(null); const [isSubmitting, setIsSubmitting] = useState(false); const [usageStats, setUsageStats] = useState>({}); const [sessionCounts, setSessionCounts] = useState>({}); @@ -226,19 +228,20 @@ export default function ApiManagerPageClient() { } }; - const clearError = useCallback(() => setError(null), []); + const clearPageError = useCallback(() => setPageError(null), []); const handleCreateKey = async () => { // Validate raw input first, then sanitize const validation = validateKeyName(newKeyName, t); if (!validation.valid) { - setError(validation.error || t("invalidKeyName")); + setNameError(validation.error || t("invalidKeyName")); return; } const sanitizedName = sanitizeInput(newKeyName); setIsSubmitting(true); - clearError(); + setNameError(null); + setCreateError(null); try { const res = await fetch("/api/keys", { @@ -254,11 +257,11 @@ export default function ApiManagerPageClient() { setNewKeyName(""); setShowAddModal(false); } else { - setError(data.error || t("failedCreateKey")); + setCreateError(data.error || t("failedCreateKey")); } } catch (error) { console.error("Error creating key:", error); - setError(t("failedCreateKeyRetry")); + setCreateError(t("failedCreateKeyRetry")); } finally { setIsSubmitting(false); } @@ -267,14 +270,14 @@ export default function ApiManagerPageClient() { const handleDeleteKey = async (id: string) => { // Validate ID format to prevent injection if (!id || typeof id !== "string" || !/^[a-zA-Z0-9_-]+$/.test(id)) { - setError(t("invalidKeyId")); + setPageError(t("invalidKeyId")); return; } if (!confirm(t("deleteConfirm"))) return; setIsSubmitting(true); - clearError(); + clearPageError(); try { const res = await fetch(`/api/keys/${encodeURIComponent(id)}`, { method: "DELETE" }); @@ -282,11 +285,11 @@ export default function ApiManagerPageClient() { setKeys((prev) => prev.filter((k) => k.id !== id)); } else { const data = await res.json(); - setError(data.error || t("failedDeleteKey")); + setPageError(data.error || t("failedDeleteKey")); } } catch (error) { console.error("Error deleting key:", error); - setError(t("failedDeleteKeyRetry")); + setPageError(t("failedDeleteKeyRetry")); } finally { setIsSubmitting(false); } @@ -329,23 +332,15 @@ 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")); return; } // Limit number of selected models to prevent abuse if (allowedModels.length > MAX_SELECTED_MODELS) { - setError(t("cannotSelectMoreThanModels", { max: MAX_SELECTED_MODELS })); return; } @@ -364,7 +359,7 @@ export default function ApiManagerPageClient() { : 0; setIsSubmitting(true); - clearError(); + clearPageError(); try { const res = await fetch(`/api/keys/${encodeURIComponent(editingKey.id)}`, { @@ -388,11 +383,11 @@ export default function ApiManagerPageClient() { setEditingKey(null); } else { const data = await res.json(); - setError(data.error || t("failedUpdatePermissions")); + setPageError(data.error || t("failedUpdatePermissions")); } } catch (error) { console.error("Error updating permissions:", error); - setError(t("failedUpdatePermissionsRetry")); + setPageError(t("failedUpdatePermissionsRetry")); } finally { setIsSubmitting(false); } @@ -441,12 +436,12 @@ export default function ApiManagerPageClient() { return (
{/* Error Banner */} - {error && ( + {pageError && (
error -

{error}

+

{pageError}

-
@@ -554,7 +557,14 @@ export default function ApiManagerPageClient() {

{t("noKeys")}

{t("noKeysDesc")}

- @@ -759,6 +769,8 @@ export default function ApiManagerPageClient() { onClose={() => { setShowAddModal(false); setNewKeyName(""); + setNameError(null); + setCreateError(null); }} >
@@ -768,25 +780,42 @@ export default function ApiManagerPageClient() { setNewKeyName(e.target.value)} + onChange={(e) => { + setNewKeyName(e.target.value); + setNameError(null); + }} placeholder={t("keyNamePlaceholder")} maxLength={MAX_KEY_NAME_LENGTH} + error={nameError} autoFocus />

{t("keyNameDesc")}

+ {createError && ( +
+ error +

{createError}

+
+ )}
-
@@ -905,6 +934,8 @@ const PermissionsModal = memo(function PermissionsModal({ const [scheduleTz, setScheduleTz] = useState( apiKey?.accessSchedule?.tz ?? Intl.DateTimeFormat().resolvedOptions().timeZone ); + const [nameError, setNameError] = useState(null); + const [saveError, setSaveError] = useState(null); const [selectedConnections, setSelectedConnections] = useState(initialConnections); const [allowAllConnections, setAllowAllConnections] = useState(initialConnections.length === 0); const [expandedProviders, setExpandedProviders] = useState>(() => { @@ -992,6 +1023,29 @@ const PermissionsModal = memo(function PermissionsModal({ ); const handleSave = useCallback(() => { + // Clear previous inline errors + setNameError(null); + setSaveError(null); + + // Validate name inline before calling onSave + const validation = validateKeyName(keyName, t); + if (!validation.valid) { + setNameError(validation.error || t("invalidKeyName")); + return; + } + + // Validate models selection + if (!allowAll && !Array.isArray(selectedModels)) { + setSaveError(t("invalidModelsSelection")); + return; + } + + // Limit number of selected models to prevent abuse + if (!allowAll && selectedModels.length > MAX_SELECTED_MODELS) { + setSaveError(t("cannotSelectMoreThanModels", { max: MAX_SELECTED_MODELS })); + return; + } + const schedule: AccessSchedule | null = scheduleEnabled ? { enabled: true, @@ -1027,6 +1081,7 @@ const PermissionsModal = memo(function PermissionsModal({ scheduleUntil, scheduleDays, scheduleTz, + t, ]); const selectedCount = selectedModels.length; @@ -1048,13 +1103,25 @@ const PermissionsModal = memo(function PermissionsModal({
setKeyName(e.target.value)} + onChange={(e) => { + setKeyName(e.target.value); + setNameError(null); + }} placeholder={t("keyNamePlaceholder")} maxLength={MAX_KEY_NAME_LENGTH} + error={nameError} />
+ {/* Inline save error */} + {saveError && ( +
+ error +

{saveError}

+
+ )} + {/* Access Mode Toggle */}