import { useEffect, useMemo, useState } from 'react' import api from '@/services/api' import { useAuth } from '@/hooks/useAuth' import { useAuthConfig } from '@/hooks/useAuthConfig' import type { User } from '@/types' interface RoleOption { id: number name: string description?: string | null } interface ApiKeyPerms { can_reset_self: boolean can_reset_any: boolean } export default function UsersPage() { const { user } = useAuth() const { config: authCfg } = useAuthConfig() const oidcOnly = authCfg.oidcOnly const oidcEnabled = authCfg.oidcEnabled const isAdmin = user?.is_admin === true const [bindForm, setBindForm] = useState({ issuer: '', subject: '' }) const [users, setUsers] = useState([]) const [roles, setRoles] = useState([]) const [loading, setLoading] = useState(true) const [saving, setSaving] = useState(false) const [message, setMessage] = useState('') const [selectedId, setSelectedId] = useState(null) const [apikeyPerms, setApikeyPerms] = useState({ can_reset_self: false, can_reset_any: false }) const [generatedApiKey, setGeneratedApiKey] = useState(null) const [createForm, setCreateForm] = useState({ username: '', email: '', full_name: '', password: '', role_id: '', }) const [editForm, setEditForm] = useState({ email: '', full_name: '', password: '', role_id: '', is_active: true, }) const selectedUser = useMemo( () => users.find((u) => u.id === selectedId) ?? null, [users, selectedId], ) useEffect(() => { if (!isAdmin) { setLoading(false) return } fetchData() }, [isAdmin]) useEffect(() => { if (!selectedUser) return setGeneratedApiKey(null) setEditForm({ email: selectedUser.email, full_name: selectedUser.full_name || '', password: '', role_id: selectedUser.role_id ? String(selectedUser.role_id) : '', is_active: selectedUser.is_active, }) }, [selectedUser]) useEffect(() => { if (!createForm.role_id && roles.length > 0) { const guestRole = roles.find((r) => r.name === 'guest') ?? roles[0] if (guestRole) { setCreateForm((prev) => ({ ...prev, role_id: String(guestRole.id) })) } } }, [roles, createForm.role_id]) const fetchData = async () => { try { const [usersRes, rolesRes, apikeyRes] = await Promise.all([ api.get('/users'), api.get('/roles'), api.get('/auth/me/apikey-permissions').catch(() => ({ data: { can_reset_self: false, can_reset_any: false } })), ]) setApikeyPerms(apikeyRes.data) const assignableRoles = rolesRes.data .filter((role) => role.name !== 'admin') .sort((a, b) => a.name.localeCompare(b.name)) setUsers(usersRes.data) setRoles(assignableRoles) if (!selectedId && usersRes.data.length > 0) { setSelectedId(usersRes.data[0].id) } else if (selectedId && !usersRes.data.some((u) => u.id === selectedId)) { setSelectedId(usersRes.data[0]?.id ?? null) } } catch (err: any) { setMessage(err.response?.data?.detail || 'Failed to load user management data') } finally { setLoading(false) } } const handleCreateUser = async () => { if (!createForm.username.trim() || !createForm.email.trim()) return if (!oidcOnly && !createForm.password.trim()) return setSaving(true) setMessage('') try { const payload: Record = { username: createForm.username.trim(), email: createForm.email.trim(), full_name: createForm.full_name.trim() || null, role_id: createForm.role_id ? Number(createForm.role_id) : undefined, } if (!oidcOnly) { payload.password = createForm.password } const { data } = await api.post('/users', payload) const guestRole = roles.find((r) => r.name === 'guest') ?? roles[0] setCreateForm({ username: '', email: '', full_name: '', password: '', role_id: guestRole ? String(guestRole.id) : '', }) setMessage('User created successfully') await fetchData() setSelectedId(data.id) } catch (err: any) { setMessage(err.response?.data?.detail || 'Failed to create user') } finally { setSaving(false) } } const handleSaveUser = async () => { if (!selectedUser) return setSaving(true) setMessage('') try { const payload: Record = { email: editForm.email.trim(), full_name: editForm.full_name.trim() || null, is_active: editForm.is_active, } if (!selectedUser.is_admin) { payload.role_id = editForm.role_id ? Number(editForm.role_id) : undefined } if (editForm.password.trim()) { payload.password = editForm.password } await api.patch(`/users/${selectedUser.id}`, payload) setMessage('User updated successfully') await fetchData() setEditForm((prev) => ({ ...prev, password: '' })) } catch (err: any) { setMessage(err.response?.data?.detail || 'Failed to update user') } finally { setSaving(false) } } const canResetApiKey = (targetUser: User) => { if (apikeyPerms.can_reset_any) return true if (apikeyPerms.can_reset_self && targetUser.id === user?.id) return true return false } const handleResetApiKey = async () => { if (!selectedUser) return if (!confirm(`Reset API key for ${selectedUser.username}? The old key will be deactivated.`)) return setSaving(true) setMessage('') setGeneratedApiKey(null) try { const { data } = await api.post(`/users/${selectedUser.id}/reset-apikey`) setGeneratedApiKey(data.api_key) setMessage('API key reset successfully. Copy it now — it will not be shown again.') } catch (err: any) { setMessage(err.response?.data?.detail || 'Failed to reset API key') } finally { setSaving(false) } } const handleDeleteUser = async () => { if (!selectedUser) return if (!confirm(`Delete user ${selectedUser.username}? This cannot be undone.`)) return setSaving(true) setMessage('') try { await api.delete(`/users/${selectedUser.id}`) setMessage('User deleted successfully') await fetchData() } catch (err: any) { setMessage(err.response?.data?.detail || 'Failed to delete user') } finally { setSaving(false) } } const handleBindOidc = async () => { if (!selectedUser) return if (!bindForm.issuer.trim() || !bindForm.subject.trim()) return setSaving(true) setMessage('') try { await api.put(`/users/${selectedUser.id}/oidc-binding`, { issuer: bindForm.issuer.trim(), subject: bindForm.subject.trim(), }) setBindForm({ issuer: '', subject: '' }) setMessage('OIDC identity bound successfully') await fetchData() } catch (err: any) { setMessage(err.response?.data?.detail || 'Failed to bind OIDC identity') } finally { setSaving(false) } } const handleUnbindOidc = async () => { if (!selectedUser) return if (!confirm(`Remove the OIDC binding for ${selectedUser.username}?`)) return setSaving(true) setMessage('') try { await api.delete(`/users/${selectedUser.id}/oidc-binding`) setMessage('OIDC binding removed') await fetchData() } catch (err: any) { setMessage(err.response?.data?.detail || 'Failed to remove OIDC binding') } finally { setSaving(false) } } if (loading) return
Loading users...
if (!isAdmin) { return (

👥 User Management

Admin access required.

) } return (

👥 User Management

Create accounts, assign one non-admin role, and manage activation state.
{message && (
{message}
)}

Create User

{!oidcOnly && ( )} {oidcOnly && (

OIDC-only mode: users are created without a password and sign in via a bound OIDC identity.

)}

Users

{users.map((u) => ( ))}
{selectedUser ? ( <>

{selectedUser.username}

Created at {new Date(selectedUser.created_at).toLocaleString()}
{!oidcOnly && ( )}
Role
{selectedUser.is_admin ? (
{selectedUser.role_name || 'admin'} (admin accounts are managed outside this screen)
) : ( )}
{canResetApiKey(selectedUser) && (
API Key
{generatedApiKey && (
New API Key (copy now!):
{generatedApiKey}
)}
)} {oidcEnabled && (
OIDC Binding
{selectedUser.oidc_subject ? (
issuer: {selectedUser.oidc_issuer || '—'}
subject: {selectedUser.oidc_subject}
) : (
No OIDC identity bound.
)}
)}
) : (
No user selected.
)}
) }