import { useEffect, useState, useCallback } from 'react'; import PageHeader from '../../components/PageHeader'; import { listUsers, createUser, deleteUser, listTokens, createToken, revokeToken, type AppUser, type TokenInfo } from '../../services/admin.service'; import { ROLE_LABELS, roleAtLeast, type Role, type CurrentUser } from '../../services/auth.service'; import styles from './Admin.module.scss'; const fmt = (v: string | null) => (v ? new Date(v).toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' }) : '—'); export default function Admin({ user }: { user: CurrentUser }) { const isAdmin = user.role === 'admin'; const [tab, setTab] = useState<'users' | 'tokens'>('users'); useEffect(() => { if (!isAdmin) setTab('users'); }, [isAdmin]); return (
{isAdmin && }
{tab === 'users' ? : }
); } function UsersPanel({ me }: { me: CurrentUser }) { const [users, setUsers] = useState([]); const [error, setError] = useState(null); const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); const [role, setRole] = useState('viewer'); const [busy, setBusy] = useState(false); const load = useCallback(() => { listUsers().then(setUsers).catch(() => setError('Failed to load users')); }, []); useEffect(() => { load(); }, [load]); // Leadership can grant up to 'lead'; only admin can grant 'admin'. const grantable: Role[] = me.role === 'admin' ? ['viewer', 'pm', 'lead', 'admin'] : ['viewer', 'pm', 'lead']; const submit = async (e: React.FormEvent) => { e.preventDefault(); setBusy(true); setError(null); try { const res = await createUser(username.trim(), password, role); if (res.user) { setUsername(''); setPassword(''); setRole('viewer'); load(); } else setError(res.message || 'Could not create user'); } catch { setError('Could not create user'); } finally { setBusy(false); } }; const remove = async (u: string) => { if (!confirm(`Delete user "${u}"?`)) return; await deleteUser(u).catch(() => {}); load(); }; return (

Add user

setUsername(e.target.value)} autoComplete="off" /> setPassword(e.target.value)} autoComplete="new-password" placeholder="min 6 chars" /> {error &&
{error}
}

Users ({users.length})

{users.map(u => ( ))}
UsernameRoleLast login
{u.username} {ROLE_LABELS[u.role]} {fmt(u.lastLoginAt)} {u.username !== me.username && }
); } function TokensPanel() { const [tokens, setTokens] = useState([]); const [label, setLabel] = useState(''); const [minted, setMinted] = useState(null); const [busy, setBusy] = useState(false); const load = useCallback(() => { listTokens().then(setTokens).catch(() => {}); }, []); useEffect(() => { load(); }, [load]); const create = async (e: React.FormEvent) => { e.preventDefault(); setBusy(true); try { const res = await createToken(label.trim() || 'sync-extension'); setMinted(res.token); setLabel(''); load(); } finally { setBusy(false); } }; const revoke = async (id: number) => { if (confirm('Revoke this token?')) { await revokeToken(id).catch(() => {}); load(); } }; return (

Create API token

For the FORGE Snow Sync Chrome extension. The raw token is shown once.

setLabel(e.target.value)} placeholder="e.g. my laptop" /> {minted && (
Copy now — shown once:
navigator.clipboard?.writeText(minted)}>{minted}
)}

Tokens ({tokens.length})

{tokens.map(t => ( ))}
LabelIDLast usedExpires
{t.label} {t.tokenId} {fmt(t.lastUsedAt)} {fmt(t.expiresAt)} {t.revoked ? revoked : }
); }