Files
Magent/frontend/app/users/page.tsx

247 lines
7.6 KiB
TypeScript

'use client'
import { useEffect, useState } from 'react'
import { useRouter } from 'next/navigation'
import { authFetch, clearToken, getApiBase, getToken } from '../lib/auth'
import AdminShell from '../ui/AdminShell'
type AdminUser = {
username: string
role: string
authProvider?: string | null
lastLoginAt?: string | null
isBlocked?: boolean
}
const formatLastLogin = (value?: string | null) => {
if (!value) return 'Never'
const date = new Date(value)
if (Number.isNaN(date.valueOf())) return value
return date.toLocaleString()
}
export default function UsersPage() {
const router = useRouter()
const [users, setUsers] = useState<AdminUser[]>([])
const [error, setError] = useState<string | null>(null)
const [loading, setLoading] = useState(true)
const [selected, setSelected] = useState<string[]>([])
const [bulkAction, setBulkAction] = useState('block')
const [bulkRole, setBulkRole] = useState('user')
const loadUsers = async () => {
try {
const baseUrl = getApiBase()
const response = await authFetch(`${baseUrl}/admin/users`)
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),
}))
)
} 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 toggleSelect = (username: string, isChecked: boolean) => {
setSelected((current) =>
isChecked ? [...new Set([...current, username])] : current.filter((name) => name !== username)
)
}
const toggleSelectAll = (isChecked: boolean) => {
setSelected(isChecked ? users.map((user) => user.username) : [])
}
const runBulkAction = async () => {
if (selected.length === 0) {
setError('Select at least one user to run a bulk action.')
return
}
try {
const baseUrl = getApiBase()
const response = await authFetch(`${baseUrl}/admin/users/bulk`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: bulkAction,
role: bulkRole,
usernames: selected,
}),
})
if (!response.ok) {
throw new Error('Bulk update failed')
}
setSelected([])
await loadUsers()
} catch (err) {
console.error(err)
setError('Could not run the bulk action.')
}
}
useEffect(() => {
if (!getToken()) {
router.push('/login')
return
}
void loadUsers()
}, [router])
if (loading) {
return <main className="card">Loading users...</main>
}
return (
<AdminShell
title="Users"
subtitle="Manage who can use Magent."
actions={
<button type="button" onClick={loadUsers}>
Reload list
</button>
}
>
<section className="admin-section">
{error && <div className="error-banner">{error}</div>}
{users.length > 0 && (
<div className="summary-card user-bulk-bar">
<label className="toggle">
<input
type="checkbox"
checked={selected.length === users.length}
onChange={(event) => toggleSelectAll(event.target.checked)}
/>
<span>Select all</span>
</label>
<div className="user-bulk-actions">
<select value={bulkAction} onChange={(event) => setBulkAction(event.target.value)}>
<option value="block">Block access</option>
<option value="unblock">Allow access</option>
<option value="role">Set role</option>
<option value="delete">Delete users</option>
</select>
{bulkAction === 'role' && (
<select value={bulkRole} onChange={(event) => setBulkRole(event.target.value)}>
<option value="user">User</option>
<option value="admin">Admin</option>
</select>
)}
<button type="button" onClick={runBulkAction}>
Apply to {selected.length} selected
</button>
</div>
</div>
)}
{users.length === 0 ? (
<div className="status-banner">No users found yet.</div>
) : (
<div className="admin-grid">
{users.map((user) => (
<div key={user.username} className="summary-card user-card">
<div>
<label className="toggle">
<input
type="checkbox"
checked={selected.includes(user.username)}
onChange={(event) => toggleSelect(user.username, event.target.checked)}
/>
<span>Select</span>
</label>
<strong>{user.username}</strong>
<span className="meta">Role: {user.role}</span>
<span className="meta">Login type: {user.authProvider || 'local'}</span>
<span className="meta">Last login: {formatLastLogin(user.lastLoginAt)}</span>
</div>
<div className="user-actions">
<label className="toggle">
<input
type="checkbox"
checked={user.role === 'admin'}
onChange={(event) =>
updateUserRole(user.username, event.target.checked ? 'admin' : 'user')
}
/>
<span>Make admin</span>
</label>
<button
type="button"
className="ghost-button"
onClick={() => toggleUserBlock(user.username, !user.isBlocked)}
>
{user.isBlocked ? 'Allow access' : 'Block access'}
</button>
</div>
</div>
))}
</div>
)}
</section>
</AdminShell>
)
}