diff --git a/CLAUDE.md b/CLAUDE.md index 5990c8bc..b88b90bb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -118,7 +118,7 @@ function MyComponent() { const { isAuthenticated, isHydrated, session, currentUser, googleProfile, - apiKeys, activeKey, addKey, removeKey, setKeys, + apiKeys, activeKey, addKey, removeKey, loginWithToken, logout, } = useAuth(); } diff --git a/app/(main)/keystore/page.tsx b/app/(main)/keystore/page.tsx index cafc6ef7..dcf330ee 100644 --- a/app/(main)/keystore/page.tsx +++ b/app/(main)/keystore/page.tsx @@ -13,8 +13,6 @@ import AddKeyModal from "@/app/components/keystore/AddKeyModal"; import { useAuth } from "@/app/lib/context/AuthContext"; import { useApp } from "@/app/lib/context/AppContext"; import { useToast } from "@/app/hooks/useToast"; -import { apiFetch } from "@/app/lib/apiClient"; -import { APIKey } from "@/app/lib/types/credentials"; export default function KaapiKeystore() { const { sidebarCollapsed } = useApp(); @@ -24,15 +22,11 @@ export default function KaapiKeystore() { const [isModalOpen, setIsModalOpen] = useState(false); const [newKeyLabel, setNewKeyLabel] = useState(""); const [newKeyValue, setNewKeyValue] = useState(""); - const [newKeyProvider, setNewKeyProvider] = useState("Kaapi"); - const [visibleKeys, setVisibleKeys] = useState>(new Set()); - const [copiedKeyId, setCopiedKeyId] = useState(null); const [isValidating, setIsValidating] = useState(false); const resetForm = () => { setNewKeyLabel(""); setNewKeyValue(""); - setNewKeyProvider("Kaapi"); }; const handleAddKey = async () => { @@ -41,63 +35,28 @@ export default function KaapiKeystore() { return; } - const trimmedKey = newKeyValue.trim(); setIsValidating(true); - try { - await apiFetch("/api/apikeys/verify", trimmedKey); + await addKey({ key: newKeyValue.trim(), label: newKeyLabel.trim() }); + resetForm(); + setIsModalOpen(false); + toast.success("API key added successfully"); } catch (err) { - console.error("API key validation failed:", err); - toast.error("Invalid API key. Please check the key and try again."); + toast.error( + err instanceof Error + ? err.message + : "Invalid API key. Please check the key and try again.", + ); + } finally { setIsValidating(false); - return; } - - const newKey: APIKey = { - id: Date.now().toString(), - label: newKeyLabel.trim(), - key: trimmedKey, - provider: newKeyProvider, - createdAt: new Date().toISOString(), - }; - - addKey(newKey); - resetForm(); - setIsModalOpen(false); - setIsValidating(false); - toast.success("API key added successfully"); }; - const handleDeleteKey = (id: string) => { - removeApiKey(id); - setVisibleKeys((prev) => { - const next = new Set(prev); - next.delete(id); - return next; - }); + const handleDeleteKey = async (id: string) => { + await removeApiKey(id); toast.success("API key removed"); }; - const toggleKeyVisibility = (id: string) => { - setVisibleKeys((prev) => { - const next = new Set(prev); - if (next.has(id)) next.delete(id); - else next.add(id); - return next; - }); - }; - - const copyToClipboard = async (text: string, id: string) => { - try { - await navigator.clipboard.writeText(text); - setCopiedKeyId(id); - toast.success("API key copied to clipboard"); - setTimeout(() => setCopiedKeyId(null), 1500); - } catch { - toast.error("Failed to copy API key"); - } - }; - const closeAddModal = () => { setIsModalOpen(false); resetForm(); @@ -118,10 +77,6 @@ export default function KaapiKeystore() {
setIsModalOpen(true)} /> @@ -134,11 +89,9 @@ export default function KaapiKeystore() { open={isModalOpen} newKeyLabel={newKeyLabel} newKeyValue={newKeyValue} - newKeyProvider={newKeyProvider} isValidating={isValidating} onLabelChange={setNewKeyLabel} onValueChange={setNewKeyValue} - onProviderChange={setNewKeyProvider} onAddKey={handleAddKey} onClose={closeAddModal} /> diff --git a/app/api/apikeys/route.ts b/app/api/apikeys/route.ts new file mode 100644 index 00000000..8b161257 --- /dev/null +++ b/app/api/apikeys/route.ts @@ -0,0 +1,72 @@ +import { NextRequest, NextResponse } from "next/server"; +import { apiClient } from "@/app/lib/apiClient"; +import { clearApiKeyCookies, setApiKeyCookies } from "@/app/lib/authCookie"; +import type { AddApiKeyRequest, ApiKeyMeta } from "@/app/lib/types/credentials"; + +function maskKey(key: string): string { + const tail = key.slice(-4); + return `${"•".repeat(8)}${tail}`; +} + +export async function POST(request: NextRequest) { + let body: AddApiKeyRequest; + try { + body = (await request.json()) as AddApiKeyRequest; + } catch { + return NextResponse.json( + { error: "Invalid request body" }, + { status: 400 }, + ); + } + + const key = body.key?.trim(); + const label = body.label?.trim(); + + if (!key || !label) { + return NextResponse.json( + { error: "Both a label and an API key are required" }, + { status: 400 }, + ); + } + + const verifyRequest = new Request(request.url, { + headers: { + "X-API-KEY": key, + Cookie: request.headers.get("Cookie") || "", + }, + }); + + let status: number; + try { + ({ status } = await apiClient(verifyRequest, "/api/v1/apikeys/verify")); + } catch { + return NextResponse.json( + { error: "Failed to reach backend for API key verification" }, + { status: 502 }, + ); + } + + if (status < 200 || status >= 300) { + return NextResponse.json( + { error: "Invalid API key. Please check the key and try again." }, + { status: 401 }, + ); + } + + const meta: ApiKeyMeta = { + id: crypto.randomUUID(), + label, + masked: maskKey(key), + createdAt: new Date().toISOString(), + }; + + const res = NextResponse.json({ data: meta }, { status: 200 }); + setApiKeyCookies(res, key, meta); + return res; +} + +export async function DELETE() { + const res = NextResponse.json({ success: true }, { status: 200 }); + clearApiKeyCookies(res); + return res; +} diff --git a/app/api/auth/logout/route.ts b/app/api/auth/logout/route.ts index 8461a4fe..ec14accc 100644 --- a/app/api/auth/logout/route.ts +++ b/app/api/auth/logout/route.ts @@ -1,6 +1,10 @@ import { NextRequest, NextResponse } from "next/server"; import { apiClient } from "@/app/lib/apiClient"; -import { clearFeaturesCookie, clearRoleCookie } from "@/app/lib/authCookie"; +import { + clearApiKeyCookies, + clearFeaturesCookie, + clearRoleCookie, +} from "@/app/lib/authCookie"; export async function POST(request: NextRequest) { const { status, data, headers } = await apiClient( @@ -18,6 +22,7 @@ export async function POST(request: NextRequest) { clearRoleCookie(res); clearFeaturesCookie(res); + clearApiKeyCookies(res); return res; } diff --git a/app/api/evaluations/stt/runs/route.ts b/app/api/evaluations/stt/runs/route.ts index 1c21d4b6..f045f5f7 100644 --- a/app/api/evaluations/stt/runs/route.ts +++ b/app/api/evaluations/stt/runs/route.ts @@ -1,5 +1,5 @@ import { NextResponse } from "next/server"; -import { apiClient } from "@/app/lib/apiClient"; +import { apiClient, getRequestApiKey } from "@/app/lib/apiClient"; export async function GET(request: Request) { try { @@ -18,7 +18,7 @@ export async function GET(request: Request) { export async function POST(request: Request) { try { - const apiKey = request.headers.get("X-API-KEY"); + const apiKey = getRequestApiKey(request); if (!apiKey) { return NextResponse.json( { diff --git a/app/components/keystore/AddKeyModal.tsx b/app/components/keystore/AddKeyModal.tsx index c6ae1b04..c994216f 100644 --- a/app/components/keystore/AddKeyModal.tsx +++ b/app/components/keystore/AddKeyModal.tsx @@ -1,19 +1,15 @@ "use client"; -import { Button, Field, Modal, Select } from "@/app/components/ui"; -import { InfoIcon } from "@/app/components/icons"; - -const PROVIDERS = [{ value: "Kaapi", label: "Kaapi" }]; +import { Button, Field, Modal } from "@/app/components/ui"; +import { APP_NAME } from "@/app/lib/constants"; interface AddKeyModalProps { open: boolean; newKeyLabel: string; newKeyValue: string; - newKeyProvider: string; isValidating?: boolean; onLabelChange: (value: string) => void; onValueChange: (value: string) => void; - onProviderChange: (value: string) => void; onAddKey: () => void; onClose: () => void; } @@ -22,11 +18,9 @@ export default function AddKeyModal({ open, newKeyLabel, newKeyValue, - newKeyProvider, isValidating, onLabelChange, onValueChange, - onProviderChange, onAddKey, onClose, }: AddKeyModalProps) { @@ -43,20 +37,16 @@ export default function AddKeyModal({

Add a new API key to use in your evaluation workflows. Keys are stored - locally in your browser. + securely server-side and cannot be viewed again after they are added.

-
- -