1"use client";23import { useEffect, useState } from "react";4import { api, ApiError } from "@/lib/api";5import { fmtAgo, fmtDate, fmtNum } from "@/lib/format";6import type { ApiKey } from "@/lib/types";7import { Code, Modal, PageHeader, Pill, useToast } from "@/components/ui";89export default function KeysPage() {10 const toast = useToast();11 const [keys, setKeys] = useState<ApiKey[]>([]);12 const [name, setName] = useState("");13 const [admin, setAdmin] = useState(false);14 const [created, setCreated] = useState<string | null>(null);15 const [busy, setBusy] = useState(false);1617 const load = () => api.get<{ keys: ApiKey[] }>("/api/keys").then((r) => setKeys(r.keys)).catch(() => {});18 useEffect(() => { load(); }, []);1920 const create = async (e: React.FormEvent) => {21 e.preventDefault();22 setBusy(true);23 try {24 const r = await api.post<{ key: string }>("/api/keys", { name: name.trim() || "key", scopes: admin ? ["inference", "admin"] : ["inference"] });25 setCreated(r.key);26 setName("");27 setAdmin(false);28 load();29 } catch (err) {30 toast.push(err instanceof ApiError ? err.message : "Failed", "bad");31 } finally {32 setBusy(false);33 }34 };35 const revoke = async (k: ApiKey) => {36 if (!confirm(`Revoke "${k.name}"? Clients using it will stop working.`)) return;37 await api.del(`/api/keys/${k.id}`);38 load();39 };40 const rename = async (k: ApiKey) => {41 const n = prompt("New name", k.name);42 if (!n) return;43 await api.patch(`/api/keys/${k.id}`, { name: n });44 load();45 };46 const origin = typeof location !== "undefined" ? location.origin : "https://www.llm-api.io";4748 return (49 <div>50 {toast.view}51 <PageHeader title="API keys" sub="Keys are shown once at creation and stored hashed. Use them as Bearer tokens with any OpenAI SDK." />52 <form onSubmit={create} className="card p-4 mb-5 flex flex-col sm:flex-row gap-2 sm:items-center">53 <input className="input sm:max-w-xs" placeholder="Key name (e.g. laptop, cursor, n8n)" value={name} onChange={(e) => setName(e.target.value)} />54 <label className="text-sm flex items-center gap-2 text-ink-2"><input type="checkbox" checked={admin} onChange={(e) => setAdmin(e.target.checked)} /> admin scope (management API)</label>55 <button className="btn btn-primary sm:ml-auto" disabled={busy}>Create key</button>56 </form>57 <div className="card overflow-x-auto">58 <table className="tbl">59 <thead><tr><th>Name</th><th>Prefix</th><th>Scopes</th><th>Created</th><th>Last used</th><th className="text-right">Requests</th><th>Status</th><th></th></tr></thead>60 <tbody>61 {keys.map((k) => (62 <tr key={k.id} className={k.revoked_at ? "opacity-50" : ""}>63 <td className="font-medium">{k.name}</td>64 <td className="mono text-xs">{k.prefix}…</td>65 <td className="flex gap-1">{k.scopes.map((s) => <Pill key={s} tone={s === "admin" ? "violet" : "neutral"}>{s}</Pill>)}</td>66 <td className="text-ink-2">{fmtDate(k.created_at)}</td>67 <td className="text-ink-2">{fmtAgo(k.last_used_at)}</td>68 <td className="text-right num">{fmtNum(k.request_count)}</td>69 <td>{k.revoked_at ? <Pill tone="bad">revoked</Pill> : <Pill tone="good">active</Pill>}</td>70 <td className="text-right whitespace-nowrap">{!k.revoked_at && <><button className="btn btn-sm btn-ghost" onClick={() => rename(k)}>Rename</button> <button className="btn btn-sm btn-danger" onClick={() => revoke(k)}>Revoke</button></>}</td>71 </tr>72 ))}73 {!keys.length && <tr><td colSpan={8} className="text-center text-ink-3 py-8">No keys yet.</td></tr>}74 </tbody>75 </table>76 </div>77 <Modal open={!!created} onClose={() => setCreated(null)} title="Your new API key" width={640}>78 <p className="text-sm mb-3">Copy it now — it will not be shown again.</p>79 <Code>{created || ""}</Code>80 <p className="text-sm mt-4 mb-2 text-ink-2">Quick start:</p>81 <Code>{`from openai import OpenAI82client = OpenAI(base_url="${origin}/v1", api_key="${created}")83r = client.chat.completions.create(model="default", messages=[{"role": "user", "content": "Hello"}])84print(r.choices[0].message.content)`}</Code>85 <div className="flex justify-end mt-4"><button className="btn btn-primary" onClick={() => setCreated(null)}>Done</button></div>86 </Modal>87 </div>88 );89}90