SPB Git

spb/akos

Public
8.2 KB · 56 lines tsx
1import { useState, type FormEvent } from "react";2import { api, ApiError, type Project } from "../api";3import { useFetch, fmtDate } from "../hooks";4import { Badge, Confirm, CopyBox, Empty, ErrorBox, KV, Loading, Td } from "../components/ui";5import { useMe } from "../App";67type Token = { id: string; name: string; prefix: string; scopes: string[]; project_id: string | null; project_slug: string | null; created_at: string; last_used_at: string | null; revoked_at: string | null };8type User = { id: string; email: string; role: string; disabled: boolean; created_at: string; last_login_at: string | null };9const SCOPES = ["read", "execute", "discover", "export", "mcp", "admin"];1011export default function Settings() {12  const { me } = useMe();13  const tokens = useFetch<{ tokens: Token[] }>("auth/tokens");14  const projects = useFetch<{ projects: Project[] }>("projects");15  const users = useFetch<{ users: User[] }>(me?.role === "admin" ? "auth/users" : null);16  const settings = useFetch<{ settings: { key: string; value: unknown }[]; env: Record<string, unknown> }>("settings");17  const [tf, setTf] = useState({ name: "", scopes: ["read", "execute", "discover", "mcp"], project_id: "" });18  const [minted, setMinted] = useState<string | null>(null);19  const [pw, setPw] = useState({ current_password: "", new_password: "" }); const [pwOk, setPwOk] = useState(false);20  const [uf, setUf] = useState({ email: "", password: "", role: "operator" });21  const [err, setErr] = useState<ApiError | null>(null);22  const publicUrl = (settings.data?.env?.public_url as string) ?? window.location.origin;23  const mint = async (e: FormEvent) => { e.preventDefault(); setErr(null); try { const r = await api<{ token: string }>("auth/tokens", { body: { name: tf.name, scopes: tf.scopes, project_id: tf.project_id || null } }); setMinted(r.token); setTf({ ...tf, name: "" }); tokens.reload(); } catch (ex) { setErr(ex as ApiError); } };24  const changePw = async (e: FormEvent) => { e.preventDefault(); setErr(null); setPwOk(false); try { await api("auth/password", { body: pw }); setPw({ current_password: "", new_password: "" }); setPwOk(true); } catch (ex) { setErr(ex as ApiError); } };25  const createUser = async (e: FormEvent) => { e.preventDefault(); setErr(null); try { await api("auth/users", { body: uf }); setUf({ email: "", password: "", role: "operator" }); users.reload(); } catch (ex) { setErr(ex as ApiError); } };26  return (27    <div>28      <h1>Settings</h1><ErrorBox error={err} />29      <div className="grid c2">30        <div className="card"><h3>Account</h3><KV items={[["Email", me?.email], ["Role", <Badge v={me?.role} />], ["Session", me?.kind]]} />31          <form onSubmit={changePw} style={{ marginTop: 10 }}><h3>Change password</h3><div className="field"><label htmlFor="pw-cur">Current password</label><input id="pw-cur" type="password" autoComplete="current-password" required value={pw.current_password} onChange={(e) => setPw({ ...pw, current_password: e.target.value })} /></div><div className="field"><label htmlFor="pw-new">New password (≥ 10 characters)</label><input id="pw-new" type="password" autoComplete="new-password" minLength={10} required value={pw.new_password} onChange={(e) => setPw({ ...pw, new_password: e.target.value })} /></div>{pwOk && <div className="alert ok">Password changed.</div>}<button className="btn">Update password</button></form></div>32        <div className="card"><h3>Connect Claude Code</h3><p className="hint">Mint a token with the <code>mcp</code> scope, then register the AKOS MCP endpoint at user scope (adds only the <code>akos</code> server; nothing else in your Claude settings is touched):</p>33          <CopyBox text={`claude mcp add --transport http akos ${publicUrl}/mcp --header "Authorization: Bearer ${minted ?? "<token>"}" -s user`} />34          <p className="hint">CLI: <code>pip install -e .</code> in the AKOS checkout then <code>akos login --url {publicUrl}</code>. SDK: <code>from akos.sdk import Akos</code>.</p>35          <h3>Runtime</h3>{settings.data && <KV items={[["Version", String(settings.data.env.version)], ["Public URL", String(settings.data.env.public_url)], ["Vault backend", String(settings.data.env.vault_backend)], ["Sandbox", String(settings.data.env.sandbox)], ["Rate limit / min", String(settings.data.env.rate_limit_per_minute)], ["Execute timeout", `${settings.data.env.execute_timeout_s} s`], ["Backup dir", String(settings.data.env.backup_dir)], ["Backup remote", String(settings.data.env.backup_remote || "—")]]} />}</div>36      </div>37      <div className="card"><h3>API tokens</h3>38        {minted && <div className="alert ok"><b>New token (shown once):</b><CopyBox text={minted} /></div>}39        <form onSubmit={mint} className="form-grid" style={{ marginBottom: 10 }}>40          <div className="field"><label htmlFor="t-name">Name</label><input id="t-name" required value={tf.name} onChange={(e) => setTf({ ...tf, name: e.target.value })} /></div>41          <div className="field"><label htmlFor="t-proj">Bind to project (tenant separation, optional)</label><select id="t-proj" value={tf.project_id} onChange={(e) => setTf({ ...tf, project_id: e.target.value })}><option value="">all projects</option>{projects.data?.projects.map((p) => <option key={p.id} value={p.slug}>{p.name}</option>)}</select></div>42          <div className="field span2"><label>Scopes</label><span className="row">{SCOPES.map((s) => <label key={s} style={{ margin: 0 }}><input type="checkbox" style={{ width: "auto", minHeight: 0 }} disabled={s === "admin" && me?.role !== "admin"} checked={tf.scopes.includes(s)} onChange={(e) => setTf({ ...tf, scopes: e.target.checked ? [...tf.scopes, s] : tf.scopes.filter((x) => x !== s) })} /> {s}</label>)}</span></div>43          <div><button className="btn primary">Create token</button></div>44        </form>45        {tokens.loading && <Loading />}<ErrorBox error={tokens.error} />46        {tokens.data && tokens.data.tokens.length === 0 && <Empty>No tokens yet.</Empty>}47        {tokens.data && tokens.data.tokens.length > 0 && <table className="tbl"><thead><tr><th>Name</th><th>Prefix</th><th>Scopes</th><th>Project</th><th>Created</th><th>Last used</th><th>Status</th><th></th></tr></thead><tbody>48          {tokens.data.tokens.map((t) => <tr key={t.id}><Td l="name"><b>{t.name}</b></Td><Td l="prefix"><code>{t.prefix}…</code></Td><Td l="scopes">{t.scopes.join(", ")}</Td><Td l="project">{t.project_slug ?? "all"}</Td><Td l="created">{fmtDate(t.created_at)}</Td><Td l="last used">{fmtDate(t.last_used_at)}</Td><Td l="status">{t.revoked_at ? <Badge v="revoked" /> : <Badge v="ok" label="active" />}</Td><Td l="">{!t.revoked_at && <Confirm label="Revoke" onConfirm={async () => { try { await api(`auth/tokens/${t.id}`, { method: "DELETE" }); tokens.reload(); } catch (ex) { setErr(ex as ApiError); } }} />}</Td></tr>)}49        </tbody></table>}</div>50      {me?.role === "admin" && <div className="card"><h3>Users (admin)</h3>{users.loading && <Loading />}<ErrorBox error={users.error} />51        {users.data && <table className="tbl"><thead><tr><th>Email</th><th>Role</th><th>Status</th><th>Created</th><th>Last login</th></tr></thead><tbody>{users.data.users.map((u) => <tr key={u.id}><Td l="email">{u.email}</Td><Td l="role"><Badge v={u.role} /></Td><Td l="status">{u.disabled ? <Badge v="revoked" label="disabled" /> : <Badge v="ok" label="active" />}</Td><Td l="created">{fmtDate(u.created_at)}</Td><Td l="last login">{fmtDate(u.last_login_at)}</Td></tr>)}</tbody></table>}52        <form onSubmit={createUser} className="form-grid" style={{ marginTop: 10 }}><div className="field"><label htmlFor="u-email">Email</label><input id="u-email" type="email" required value={uf.email} onChange={(e) => setUf({ ...uf, email: e.target.value })} /></div><div className="field"><label htmlFor="u-pw">Password (≥ 10)</label><input id="u-pw" type="password" autoComplete="new-password" minLength={10} required value={uf.password} onChange={(e) => setUf({ ...uf, password: e.target.value })} /></div><div className="field"><label htmlFor="u-role">Role</label><select id="u-role" value={uf.role} onChange={(e) => setUf({ ...uf, role: e.target.value })}><option>viewer</option><option>operator</option><option>admin</option></select></div><div><button className="btn primary">Create user</button></div></form></div>}53    </div>54  );55}56
Files · da58f809c01c422e65c786b05d6ffa3079c9feba
Menu

Theme