'use client' import { useEffect, useState } from 'react' import { useParams, useRouter } from 'next/navigation' import { authFetch, clearToken, getApiBase, getToken } from '../../lib/auth' import AdminShell from '../../ui/AdminShell' type UserStats = { total: number ready: number pending: number approved: number working: number partial: number declined: number in_progress: number last_request_at?: string | null } type AdminUser = { id?: number username: string role: string auth_provider?: string | null last_login_at?: string | null is_blocked?: boolean auto_search_enabled?: boolean jellyseerr_user_id?: number | null profile_id?: number | null expires_at?: string | null is_expired?: boolean } type UserProfileOption = { id: number name: string is_active?: boolean } const formatDateTime = (value?: string | null) => { if (!value) return 'Never' const date = new Date(value) if (Number.isNaN(date.valueOf())) return value return date.toLocaleString() } const toLocalDateTimeInput = (value?: string | null) => { if (!value) return '' const date = new Date(value) if (Number.isNaN(date.valueOf())) return '' const offsetMs = date.getTimezoneOffset() * 60_000 const local = new Date(date.getTime() - offsetMs) return local.toISOString().slice(0, 16) } const fromLocalDateTimeInput = (value: string) => { if (!value.trim()) return null const date = new Date(value) if (Number.isNaN(date.valueOf())) return null return date.toISOString() } 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 UserDetailPage() { const params = useParams() const router = useRouter() const idParam = Array.isArray(params?.id) ? params.id[0] : params?.id const [user, setUser] = useState(null) const [stats, setStats] = useState(null) const [error, setError] = useState(null) const [loading, setLoading] = useState(true) const [profiles, setProfiles] = useState([]) const [profileSelection, setProfileSelection] = useState('') const [expiryInput, setExpiryInput] = useState('') const [savingProfile, setSavingProfile] = useState(false) const [savingExpiry, setSavingExpiry] = useState(false) const [actionStatus, setActionStatus] = useState(null) const loadProfiles = async () => { try { const baseUrl = getApiBase() const response = await authFetch(`${baseUrl}/admin/profiles`) if (!response.ok) { return } const data = await response.json() if (!Array.isArray(data?.profiles)) { setProfiles([]) return } setProfiles( data.profiles.map((profile: any) => ({ id: Number(profile.id ?? 0), name: String(profile.name ?? 'Unnamed profile'), is_active: Boolean(profile.is_active ?? true), })) ) } catch (err) { console.error(err) } } const loadUser = async () => { if (!idParam) return try { const baseUrl = getApiBase() const response = await authFetch( `${baseUrl}/admin/users/id/${encodeURIComponent(idParam)}` ) if (!response.ok) { if (response.status === 401) { clearToken() router.push('/login') return } if (response.status === 403) { router.push('/') return } if (response.status === 404) { setError('User not found.') return } throw new Error('Could not load user.') } const data = await response.json() const nextUser = data?.user ?? null setUser(nextUser) setStats(normalizeStats(data?.stats)) setProfileSelection( nextUser?.profile_id == null || Number.isNaN(Number(nextUser?.profile_id)) ? '' : String(nextUser.profile_id) ) setExpiryInput(toLocalDateTimeInput(nextUser?.expires_at)) setError(null) } catch (err) { console.error(err) setError('Could not load user.') } finally { setLoading(false) } } const toggleUserBlock = async (blocked: boolean) => { if (!user) return try { setActionStatus(null) const baseUrl = getApiBase() const response = await authFetch( `${baseUrl}/admin/users/${encodeURIComponent(user.username)}/${blocked ? 'block' : 'unblock'}`, { method: 'POST' } ) if (!response.ok) { throw new Error('Update failed') } await loadUser() setActionStatus(blocked ? 'User blocked.' : 'User unblocked.') } catch (err) { console.error(err) setError('Could not update user access.') } } const updateUserRole = async (role: string) => { if (!user) return try { setActionStatus(null) const baseUrl = getApiBase() const response = await authFetch( `${baseUrl}/admin/users/${encodeURIComponent(user.username)}/role`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ role }), } ) if (!response.ok) { throw new Error('Update failed') } await loadUser() setActionStatus(`Role updated to ${role}.`) } catch (err) { console.error(err) setError('Could not update user role.') } } const updateAutoSearchEnabled = async (enabled: boolean) => { if (!user) return try { setActionStatus(null) const baseUrl = getApiBase() const response = await authFetch( `${baseUrl}/admin/users/${encodeURIComponent(user.username)}/auto-search`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ enabled }), } ) if (!response.ok) { throw new Error('Update failed') } await loadUser() setActionStatus(`Auto search/download ${enabled ? 'enabled' : 'disabled'}.`) } catch (err) { console.error(err) setError('Could not update auto search access.') } } const applyProfileToUser = async (profileOverride?: string | null) => { if (!user) return const profileValue = profileOverride ?? profileSelection setSavingProfile(true) setError(null) setActionStatus(null) try { const baseUrl = getApiBase() const response = await authFetch( `${baseUrl}/admin/users/${encodeURIComponent(user.username)}/profile`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ profile_id: profileValue || null }), } ) if (!response.ok) { const text = await response.text() throw new Error(text || 'Profile update failed') } await loadUser() setActionStatus(profileValue ? 'Profile applied to user.' : 'Profile assignment cleared.') } catch (err) { console.error(err) setError('Could not update user profile.') } finally { setSavingProfile(false) } } const saveUserExpiry = async () => { if (!user) return const expiresAt = fromLocalDateTimeInput(expiryInput) if (expiryInput.trim() && !expiresAt) { setError('Invalid expiry date/time.') return } setSavingExpiry(true) setError(null) setActionStatus(null) try { const baseUrl = getApiBase() const response = await authFetch( `${baseUrl}/admin/users/${encodeURIComponent(user.username)}/expiry`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ expires_at: expiresAt }), } ) if (!response.ok) { const text = await response.text() throw new Error(text || 'Expiry update failed') } await loadUser() setActionStatus(expiresAt ? 'User expiry updated.' : 'User expiry cleared.') } catch (err) { console.error(err) setError('Could not update user expiry.') } finally { setSavingExpiry(false) } } const clearUserExpiry = async () => { if (!user) return setSavingExpiry(true) setError(null) setActionStatus(null) try { const baseUrl = getApiBase() const response = await authFetch( `${baseUrl}/admin/users/${encodeURIComponent(user.username)}/expiry`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ clear: true }), } ) if (!response.ok) { const text = await response.text() throw new Error(text || 'Expiry clear failed') } setExpiryInput('') await loadUser() setActionStatus('User expiry cleared.') } catch (err) { console.error(err) setError('Could not clear user expiry.') } finally { setSavingExpiry(false) } } useEffect(() => { if (!getToken()) { router.push('/login') return } void loadUser() void loadProfiles() }, [router, idParam]) if (loading) { return
Loading user...
} return ( router.push('/users')}> Back to users } >
{error &&
{error}
} {actionStatus &&
{actionStatus}
} {!user ? (
No user data found.
) : (
{user.username} {user.is_blocked ? 'Blocked' : 'Active'} {user.is_expired ? 'Expired' : user.expires_at ? 'Expiry set' : 'No expiry'}

User identity, access state, and request history for this account.

Jellyseerr ID {user.jellyseerr_user_id ?? user.id ?? 'Unknown'}
Role {user.role}
Login type {user.auth_provider || 'local'}
Assigned profile {user.profile_id ?? 'None'}
Last login {formatDateTime(user.last_login_at)}
Account expiry {user.expires_at ? formatDateTime(user.expires_at) : 'Never'}

Request statistics

Snapshot of request states and recent activity for this user.

Total {stats?.total ?? 0}
Ready {stats?.ready ?? 0}
Pending {stats?.pending ?? 0}
Approved {stats?.approved ?? 0}
Working {stats?.working ?? 0}
Partial {stats?.partial ?? 0}
Declined {stats?.declined ?? 0}
In progress {stats?.in_progress ?? 0}
Last request {formatDateTime(stats?.last_request_at)}

Access controls

Role, login access, and auto-download behavior.

{user.role === 'admin' && (
Admins always have auto search/download access.
)}

Profile defaults

Assign or clear an invite profile for this user.

Account expiry

Set a specific expiry date/time for this user account.

)}
) }