'use client' import { useEffect, useState } from 'react' import { useRouter } from 'next/navigation' import Link from 'next/link' import { authFetch, clearToken, getApiBase, getToken } from '../lib/auth' import AdminShell from '../ui/AdminShell' type AdminUser = { id: number username: string role: string authProvider?: string | null lastLoginAt?: string | null isBlocked?: boolean stats?: UserStats } type UserStats = { total: number ready: number pending: number approved: number working: number partial: number declined: number in_progress: number last_request_at?: string | null } const formatLastLogin = (value?: string | null) => { if (!value) return 'Never' const date = new Date(value) if (Number.isNaN(date.valueOf())) return value return date.toLocaleString() } const formatLastRequest = (value?: string | null) => { if (!value) return '—' const date = new Date(value) if (Number.isNaN(date.valueOf())) return value return date.toLocaleString() } const emptyStats: UserStats = { total: 0, ready: 0, pending: 0, approved: 0, working: 0, partial: 0, declined: 0, in_progress: 0, last_request_at: null, } const normalizeStats = (stats: any): UserStats => ({ total: Number(stats?.total ?? 0), ready: Number(stats?.ready ?? 0), pending: Number(stats?.pending ?? 0), approved: Number(stats?.approved ?? 0), working: Number(stats?.working ?? 0), partial: Number(stats?.partial ?? 0), declined: Number(stats?.declined ?? 0), in_progress: Number(stats?.in_progress ?? 0), last_request_at: stats?.last_request_at ?? null, }) export default function UsersPage() { const router = useRouter() const [users, setUsers] = useState([]) const [error, setError] = useState(null) const [loading, setLoading] = useState(true) const [jellyseerrSyncStatus, setJellyseerrSyncStatus] = useState(null) const [jellyseerrSyncBusy, setJellyseerrSyncBusy] = useState(false) const [jellyseerrResyncBusy, setJellyseerrResyncBusy] = useState(false) const loadUsers = async () => { try { const baseUrl = getApiBase() const response = await authFetch(`${baseUrl}/admin/users/summary`) if (!response.ok) { if (response.status === 401) { clearToken() router.push('/login') return } if (response.status === 403) { router.push('/') return } throw new Error('Could not load users.') } const data = await response.json() if (Array.isArray(data?.users)) { setUsers( data.users.map((user: any) => ({ username: user.username ?? 'Unknown', role: user.role ?? 'user', authProvider: user.auth_provider ?? 'local', lastLoginAt: user.last_login_at ?? null, isBlocked: Boolean(user.is_blocked), id: Number(user.id ?? 0), stats: normalizeStats(user.stats ?? emptyStats), })) ) } else { setUsers([]) } setError(null) } catch (err) { console.error(err) setError('Could not load user list.') } finally { setLoading(false) } } const toggleUserBlock = async (username: string, blocked: boolean) => { try { const baseUrl = getApiBase() const response = await authFetch( `${baseUrl}/admin/users/${encodeURIComponent(username)}/${blocked ? 'block' : 'unblock'}`, { method: 'POST' } ) if (!response.ok) { throw new Error('Update failed') } await loadUsers() } catch (err) { console.error(err) setError('Could not update user access.') } } const updateUserRole = async (username: string, role: string) => { try { const baseUrl = getApiBase() const response = await authFetch( `${baseUrl}/admin/users/${encodeURIComponent(username)}/role`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ role }), } ) if (!response.ok) { throw new Error('Update failed') } await loadUsers() } catch (err) { console.error(err) setError('Could not update user role.') } } const syncJellyseerrUsers = async () => { setJellyseerrSyncStatus(null) setJellyseerrSyncBusy(true) try { const baseUrl = getApiBase() const response = await authFetch(`${baseUrl}/admin/jellyseerr/users/sync`, { method: 'POST', }) if (!response.ok) { const text = await response.text() throw new Error(text || 'Sync failed') } const data = await response.json() setJellyseerrSyncStatus( `Matched ${data?.matched ?? 0} users. Skipped ${data?.skipped ?? 0}.` ) await loadUsers() } catch (err) { console.error(err) setJellyseerrSyncStatus('Could not sync Jellyseerr users.') } finally { setJellyseerrSyncBusy(false) } } const resyncJellyseerrUsers = async () => { const confirmed = window.confirm( 'This will remove all non-admin users and re-import from Jellyseerr. Continue?' ) if (!confirmed) return setJellyseerrSyncStatus(null) setJellyseerrResyncBusy(true) try { const baseUrl = getApiBase() const response = await authFetch(`${baseUrl}/admin/jellyseerr/users/resync`, { method: 'POST', }) if (!response.ok) { const text = await response.text() throw new Error(text || 'Resync failed') } const data = await response.json() setJellyseerrSyncStatus( `Re-imported ${data?.imported ?? 0} users. Cleared ${data?.cleared ?? 0}.` ) await loadUsers() } catch (err) { console.error(err) setJellyseerrSyncStatus('Could not resync Jellyseerr users.') } finally { setJellyseerrResyncBusy(false) } } useEffect(() => { if (!getToken()) { router.push('/login') return } void loadUsers() }, [router]) if (loading) { return
Loading users...
} return ( } >
{error &&
{error}
} {jellyseerrSyncStatus &&
{jellyseerrSyncStatus}
} {users.length === 0 ? (
No users found yet.
) : (
{users.map((user) => (
{user.username} {user.role}
{user.isBlocked ? 'Blocked' : 'Active'}
Total {user.stats?.total ?? 0}
Ready {user.stats?.ready ?? 0}
Pending {user.stats?.pending ?? 0}
In progress {user.stats?.in_progress ?? 0}
Login: {user.authProvider || 'local'} Last login: {formatLastLogin(user.lastLoginAt)} Last request: {formatLastRequest(user.stats?.last_request_at)}
))}
)}
) }