// ============================================================================ // Users management page (PR-S.17c · admin-only) // ============================================================================ // // Admin-only:列所有 user,提供 role toggle(is_admin)跟 ban/unban 按钮。 // 进入流程: // 1. mount → GET /v1/auth/me 拿 is_admin // · 非 admin → 渲染 "Forbidden — 需要 admin 权限" 占位 + 5s 后跳回 /playground // · is_admin=true → 进真页 // 2. GET /v1/users 拉表 // 3. 每行 actions: // · Toggle admin: PATCH /v1/users/{id} { is_admin: !current } // · Ban / Unban: PATCH /v1/users/{id} { status: 'banned' | 'active' } // 4. Self-row 标 "(you)",所有 action 钮 disabled —— backend 也 enforce // self_modification_blocked 422,前端先挡免得 round-trip // // 设计跟 manage-app.jsx 视觉对齐(topbar + brand + tabs),不用单独 css。 const { useState, useEffect } = React; function UsersApp() { const [me, setMe] = useState(null); // null = loading; obj = loaded const [users, setUsers] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [busyUserId, setBusyUserId] = useState(null); // 防双击,行级 disable // Step 1: load /v1/auth/me first to gate useEffect(() => { (async () => { try { const m = await window.api.authMe(); setMe(m); } catch (err) { setError(`auth check failed: ${err.message || err}`); setLoading(false); } })(); }, []); // Step 2: 拿到 me 之后才拉 users(否则 admin 才 fetch,避免给 non-admin 一次 403) useEffect(() => { if (!me || !me.is_admin) { setLoading(false); return; } (async () => { try { const items = await window.api.listUsers(); setUsers(items); } catch (err) { setError(`load users failed: ${err.message || err}`); } finally { setLoading(false); } })(); }, [me]); async function patchUser(userId, patch) { setBusyUserId(userId); try { const updated = await window.api.updateUser(userId, patch); // 用 updated 替换该行,保证 UI 跟后端同步 setUsers((prev) => prev.map((u) => (u.user_id === userId ? { ...u, ...updated } : u)) ); } catch (err) { alert(`update failed: ${err.message || err}`); } finally { setBusyUserId(null); } } // -------- 非 admin / loading / error 占位 -------- if (loading) { return (
Loading…
); } if (error) { return (
{error}
); } if (!me || !me.is_admin) { // 非 admin 用 admin-only 链接进来 → 友好拒绝。不自动跳转 —— 之前用 // setTimeout 在 render 里调会 every-render schedule 一个新 timer // (React 反模式;也让用户看不清自己被识别成哪个 user)。改成显式 // 显示当前 user_id + 提供按钮:Go to Playground / Sign out。这样用户 // 能一眼看出"被认成哪个 user 所以非 admin",自决定下一步。 return (

Forbidden

This page is admin-only.

{me && (

you are signed in as {me.user_id} {me.is_admin ? "" : " (not admin)"}

)}
Go to Playground
); } // -------- admin: 正式 UI -------- return (

Users

{users.length} registered. Toggle admin role or ban/unban below. You can't modify your own row — ask another admin.

{users.map((u) => { const isSelf = u.user_id === me.user_id; const isBusy = busyUserId === u.user_id; const banned = u.status === "banned"; return ( ); })}
Email User ID Created Role Status Actions
{u.email} {isSelf && ( you )} {u.user_id} {u.created_at ? new Date(u.created_at * 1000).toLocaleString() : "—"}
); } function Topbar({ isAdmin }) { return (
baizhi / Users
Playground Manage Evals Traces Marketplace Apps {isAdmin && } API Docs
); } function Th({ children, align = "left" }) { return ( {children} ); } function Td({ children, align = "left", mono, small, muted }) { return ( {children} ); } function RoleBadge({ isAdmin }) { return ( {isAdmin ? "admin" : "user"} ); } function StatusBadge({ status }) { const banned = status === "banned"; return ( {status} ); } const tabLink = { background: "transparent", border: 0, padding: "6px 10px", borderRadius: 6, color: "var(--text-muted)", fontSize: 13, textDecoration: "none", }; const selfPillStyle = { marginLeft: 8, padding: "1px 6px", borderRadius: 3, background: "var(--accent-bg)", color: "var(--accent)", fontSize: 10, letterSpacing: 0.3, fontFamily: "var(--font-sans)", }; ReactDOM.createRoot(document.getElementById("root")).render();