"use client"; import { useMemo, useState, type FormEvent } from "react"; import { KeyRound, Plus, Trash2 } from "lucide-react"; import { api, type ApiKey, type UsageRow } from "@/lib/api"; import { useAsync } from "@/lib/hooks"; import { useAuth } from "@/lib/auth"; import { useToast } from "@/lib/toast"; import { fmtBytes, fmtCompact, fmtInt, fmtRelative, fmtTs } from "@/lib/format"; import { Banner, Card, ErrorState, Field, PageHeader, StatusBadge } from "@/components/ui"; import { DataTable } from "@/components/DataTable"; import { ConfirmDialog, Modal } from "@/components/ConfirmDialog"; import { CopyButton } from "@/components/CopyButton"; const SCOPES: { id: string; help: string }[] = [ { id: "read:market", help: "bars, sessions, coverage, catalog, instruments" }, { id: "read:fundamentals", help: "point-in-time fundamentals" }, { id: "query", help: "SQL playground (DuckDB, read-only)" }, { id: "export", help: "async Parquet/CSV exports" }, { id: "admin", help: "jobs, keys, storage, settings — owner only" }, ]; export default function KeysPage() { const { isOwner } = useAuth(); const toast = useToast(); const { data, error, loading, reload } = useAsync((s) => api.get<{ data: ApiKey[]; usage: UsageRow[] }>("/admin/keys", undefined, s), [], { enabled: isOwner }); const [create, setCreate] = useState(false); const [revoke, setRevoke] = useState(null); const [busy, setBusy] = useState(false); const [token, setToken] = useState<{ token: string; name: string } | null>(null); const usageByPrincipal = useMemo(() => { const m = new Map(); for (const u of data?.usage ?? []) { const e = m.get(u.principal) ?? { requests: 0, rows: 0, bytes: 0, last: u.day }; e.requests += Number(u.requests); e.rows += Number(u.rows_returned); e.bytes += Number(u.bytes_exported); if (u.day > e.last) e.last = u.day; m.set(u.principal, e); } return m; }, [data]); if (!isOwner) return API keys are managed by the owner.; async function doRevoke() { if (!revoke) return; setBusy(true); try { await api.del(`/admin/keys/${revoke.id}`); toast.success("Key revoked", revoke.name); setRevoke(null); reload(); } catch (e) { toast.error(e); } finally { setBusy(false); } } return ( <> setCreate(true)}> New key } /> {error && } k.id} mobile="cards" columns={[ { key: "name", header: "Name", render: (k) => (
{k.name}
{k.prefix}…
) }, { key: "scopes", header: "Scopes", render: (k) =>
{k.scopes.map((s) => {s})}
}, { key: "rate_limit_per_minute", header: "Rate/min", num: true, hideSm: true, render: (k) => fmtInt(k.rate_limit_per_minute) }, { key: "last_used_at", header: "Last used", render: (k) => {k.last_used_at ? fmtRelative(k.last_used_at) : "never"} }, { key: "usage", header: "Usage (14 d)", render: (k) => { const u = usageByPrincipal.get(`api_key:${k.id}`) ?? usageByPrincipal.get(`key:${k.prefix}`); return u ? {fmtInt(u.requests)} req · {fmtCompact(u.rows)} rows · {fmtBytes(u.bytes)} : —; }, }, { key: "expires_at", header: "Expires", hideSm: true, render: (k) => {k.expires_at ? fmtTs(k.expires_at) : "never"} }, { key: "status", header: "Status", render: (k) => }, { key: "actions", header: "", render: (k) => (!k.revoked_at ? () : null) }, ]} empty="No API keys yet." />
`${u.principal}-${u.day}`} mobile="scroll" maxHeight="20rem" columns={[ { key: "day", header: "Day", render: (u) => {u.day} }, { key: "principal", header: "Principal", mono: true }, { key: "requests", header: "Requests", num: true, render: (u) => fmtInt(u.requests) }, { key: "rows_returned", header: "Rows", num: true, render: (u) => fmtInt(u.rows_returned) }, { key: "bytes_exported", header: "Exported", num: true, render: (u) => fmtBytes(u.bytes_exported) }, ]} empty="No usage recorded." /> setCreate(false)}> { setCreate(false); setToken({ token: t, name }); reload(); }} /> setToken(null)}> {token && (
This token is shown once. Store it in a password manager; the server keeps only a hash.
{token.token}
{`export FINDATA_API_KEY="${token.token}"\ncurl -H "Authorization: Bearer $FINDATA_API_KEY" "${typeof window !== "undefined" ? window.location.origin : ""}/api/v1/health"`}
)}
setRevoke(null)} onConfirm={() => void doRevoke()}> Every client using this key will start receiving 401 immediately. This cannot be undone. ); } function CreateKeyForm({ onCreated }: { onCreated: (token: string, name: string) => void }) { const toast = useToast(); const [name, setName] = useState(""); const [scopes, setScopes] = useState(["read:market"]); const [rate, setRate] = useState(600); const [expires, setExpires] = useState(""); const [busy, setBusy] = useState(false); async function submit(e: FormEvent) { e.preventDefault(); setBusy(true); try { const out = await api.post<{ id: string; token: string }>("/admin/keys", { name: name.trim(), scopes, rate_limit_per_minute: rate, expires_at: expires ? new Date(expires).toISOString() : null, }); onCreated(out.token, name.trim()); } catch (err) { toast.error(err); } finally { setBusy(false); } } return (
setName(e.target.value)} maxLength={60} required placeholder="research-laptop" autoFocus />
Scopes
{SCOPES.map((s) => ( ))}
setRate(Number(e.target.value) || 600)} /> setExpires(e.target.value)} />
); }