import { useCallback, useEffect, useState } from 'react' import { useParams, Link } from 'react-router-dom' import { Loading, ErrorState } from '../../../components/PageState.jsx' import { useAsync } from '../../../lib/useAsync.js' import { dateTime } from '../../../lib/format.js' import { api } from '../../../api/client.js' import Slot from '../../../modules/Slot.jsx' // Admin view of one user: who they are, their security posture (trusted devices // and MFA), and then whatever the installed module contributes about them — // today core's own UO footprint, via the `admin.users.detail` extension slot // (MODULE_API.md §3.7). Reached from the Users table's "View" action; Edit stays // a separate modal. const ROLE_BADGE = { admin: 'badge-admin', editor: 'badge-editor', moderator: 'badge-moderator', player: 'badge-player', } function SectionTitle({ children }) { return (
{children}
) } // Admin security controls for one user: their trusted devices (view + revoke) and // an MFA reset for a locked-out user. Every action is audit-logged server-side. function SecurityAdmin({ userId }) { const [devices, setDevices] = useState(null) const [error, setError] = useState('') const [busy, setBusy] = useState(false) const [msg, setMsg] = useState('') const load = useCallback(async () => { try { setDevices(await api.admin.userTrustedDevices(userId)) } catch { setError('Could not load trusted devices.') } }, [userId]) useEffect(() => { load() }, [load]) async function revoke(deviceId) { setBusy(true); setMsg(''); setError('') try { await api.admin.revokeUserTrustedDevice(userId, deviceId) await load() } catch { setError('Could not revoke that device.') } finally { setBusy(false) } } async function revokeAll() { if (!window.confirm('Revoke ALL of this user’s trusted devices?')) return setBusy(true); setMsg(''); setError('') try { await api.admin.revokeAllUserTrustedDevices(userId) setMsg('All trusted devices revoked.') await load() } catch { setError('Could not revoke devices.') } finally { setBusy(false) } } async function resetMfa() { if (!window.confirm('Reset this user’s two-factor? This turns TOTP off, revokes their trusted devices, and clears their recovery codes so they can sign in with their password.')) return setBusy(true); setMsg(''); setError('') try { await api.admin.resetUserMfa(userId) setMsg('Two-factor has been reset for this user.') await load() } catch { setError('Could not reset two-factor.') } finally { setBusy(false) } } const fmt = (d) => { const t = d ? new Date(d) : null return t && !Number.isNaN(t.getTime()) ? t.toLocaleDateString() : '—' } return (
Security & two-factor {devices == null ? (

Loading…

) : devices.length === 0 ? (

No trusted devices.

) : ( )}
{devices && devices.length > 0 && ( )}
{msg &&

{msg}

} {error &&

{error}

}
) } export default function UserDetail() { const { id } = useParams() const { loading, error, data: user } = useAsync(() => api.admin.getUser(id), [id]) if (loading) return if (error) return return (
← Back to users {/* Header */}
{user.username} {user.role} {user.status || 'active'}
{user.email && {user.email}} Last login: {user.last_login_at ? dateTime(user.last_login_at) : 'never'} {user.created_at && Joined: {dateTime(user.created_at)}}
{/* Whatever the installed module has to say about this user, or nothing at all. Core's own UO sections fill it today (UserShardSections.jsx, registered in main.jsx) — MODULE_API.md §3.7. */}
) }