diff --git a/src/app/(dashboard)/dashboard/endpoint/ApiEndpointsTab.tsx b/src/app/(dashboard)/dashboard/endpoint/ApiEndpointsTab.tsx index 2489e7ccfd..2a9c4b1c08 100644 --- a/src/app/(dashboard)/dashboard/endpoint/ApiEndpointsTab.tsx +++ b/src/app/(dashboard)/dashboard/endpoint/ApiEndpointsTab.tsx @@ -17,6 +17,7 @@ interface Endpoint { security: boolean; parameters: any[]; requestBody: boolean; + exampleBody?: any; responses: string[]; loopbackOnly?: boolean; alwaysProtected?: boolean; @@ -100,6 +101,13 @@ export default function ApiEndpointsTab() { const [tryBody, setTryBody] = useState(""); const [tryResult, setTryResult] = useState(null); const [trying, setTrying] = useState(false); + const [availableApiKeys, setAvailableApiKeys] = useState>([]); + const [selectedApiKeyId, setSelectedApiKeyId] = useState(""); + const [revealedApiKeys, setRevealedApiKeys] = useState>({}); + const [apiKeyLoadError, setApiKeyLoadError] = useState(null); + const [manualApiKey, setManualApiKey] = useState(""); + const [useManualKey, setUseManualKey] = useState(false); + const selectedApiKey = availableApiKeys.find((apiKey) => apiKey.id === selectedApiKeyId) || null; const loadCatalog = async () => { try { @@ -134,6 +142,38 @@ export default function ApiEndpointsTab() { }; }, []); + // Load API keys for Try It functionality. The list endpoint returns masked + // keys; the selected key is revealed only when sending a Try It request. + useEffect(() => { + let cancelled = false; + fetch("/api/keys?limit=100", { credentials: "same-origin" }) + .then(async (res) => { + if (!res.ok) throw new Error(`Failed to load API keys (${res.status})`); + return res.json(); + }) + .then((data) => { + if (cancelled) return; + const rows = Array.isArray(data?.keys) ? data.keys : Array.isArray(data) ? data : []; + const keys = rows + .filter((k) => k?.id && k.isActive !== false && k.isBanned !== true) + .map((k) => ({ id: String(k.id), key: String(k.key || k.id) })); + setAvailableApiKeys(keys); + setApiKeyLoadError(null); + if (keys.length > 0) { + setSelectedApiKeyId((current) => current || keys[0].id); + } + }) + .catch((error) => { + if (!cancelled) { + setAvailableApiKeys([]); + setApiKeyLoadError(error instanceof Error ? error.message : "Failed to load API keys"); + } + }); + return () => { + cancelled = true; + }; + }, []); + // Filter endpoints const filteredEndpoints = useMemo(() => { if (!catalog) return []; @@ -176,6 +216,13 @@ export default function ApiEndpointsTab() { }, [catalog]); // Try It handler + // Generate example body from OpenAPI schema + const generateExampleBody = (ep: Endpoint): string => { + if (ep.method === "GET") return ""; + if (ep.exampleBody) return JSON.stringify(ep.exampleBody, null, 2); + return "{\n \n}"; + }; + const handleTryIt = async (ep: Endpoint) => { const key = `${ep.method}:${ep.path}`; if (tryingEndpoint === key) { @@ -185,22 +232,67 @@ export default function ApiEndpointsTab() { } setTryingEndpoint(key); setTryResult(null); - setTryBody(ep.method === "GET" ? "" : "{\n \n}"); + setTryBody(generateExampleBody(ep)); + }; + + const revealSelectedApiKey = async () => { + if (!selectedApiKey) return ""; + if (revealedApiKeys[selectedApiKey.id]) return revealedApiKeys[selectedApiKey.id]; + + const res = await fetch(`/api/keys/${encodeURIComponent(selectedApiKey.id)}/reveal`, { + credentials: "same-origin", + }); + if (!res.ok) { + throw new Error( + res.status === 403 + ? "API key reveal is disabled (ALLOW_API_KEY_REVEAL). Change it in the feature flag page or paste an API key manually." + : `Failed to reveal API key (${res.status})` + ); + } + const data = await res.json(); + if (!data?.key || typeof data.key !== "string") { + throw new Error("API key reveal returned an invalid response"); + } + setRevealedApiKeys((current) => ({ ...current, [selectedApiKey.id]: data.key })); + return data.key; }; const executeTryIt = async (ep: Endpoint) => { setTrying(true); try { + const headers: Record = {}; + + // Add Authorization header if endpoint requires auth + if (ep.security) { + let apiKeyForRequest = ""; + if (useManualKey) { + apiKeyForRequest = manualApiKey; + } else if (selectedApiKey) { + apiKeyForRequest = await revealSelectedApiKey(); + } + + if (apiKeyForRequest) { + headers["Authorization"] = `Bearer ${apiKeyForRequest}`; + } else { + throw new Error("API key is required for this endpoint."); + } + } + const res = await fetch("/api/openapi/try", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ method: ep.method, - path: ep.path.replace("/api/", "/"), + path: ep.path, + headers, body: tryBody ? JSON.parse(tryBody) : undefined, }), }); if (res.ok) setTryResult(await res.json()); + else { + const err = await res.json().catch(() => ({})); + throw new Error(err.error || `Request failed (${res.status})`); + } } catch (err: any) { setTryResult({ status: 0, @@ -504,7 +596,7 @@ export default function ApiEndpointsTab() {

curl -X {ep.method} {baseUrl} - {ep.path.replace("/api/", "/")} + {ep.path} {ep.security ? ' -H "Authorization: Bearer YOUR_KEY"' : ""} {ep.requestBody ? " -H \"Content-Type: application/json\" -d '{...}'" @@ -515,6 +607,53 @@ export default function ApiEndpointsTab() { {/* Try It panel */} {isTrying && (
+ {ep.security && ( +
+
+ + +
+ + {useManualKey ? ( + setManualApiKey(e.target.value)} + placeholder="Paste your API key here" + className="w-full px-3 py-2 text-xs font-mono rounded-lg border border-black/10 + dark:border-white/10 bg-white dark:bg-black/30 focus:outline-none + focus:ring-1 focus:ring-primary" + /> + ) : availableApiKeys.length > 0 ? ( + + ) : ( +

+ {apiKeyLoadError || + "No active API keys found. Toggle manual entry to paste one."} +

+ )} +
+ )} {ep.method !== "GET" && (