'use client'; import { useCallback, useEffect, useState } from 'react'; import Link from 'next/link'; import { formatBytes, formatDate } from '@/lib/format'; type AdminUser = { id: string; email: string | null; role: string; banned_until: string | null; email_confirmed_at: string | null; created_at: string; last_sign_in_at: string | null; tunnel: { subdomain: string; is_active: boolean; bytes_used: number; quota_bytes: number; } | null; }; const PER_PAGE = 25; function isBanned(u: AdminUser): boolean { return !!u.banned_until && new Date(u.banned_until).getTime() > Date.now(); } export default function AdminUsersPage() { const [users, setUsers] = useState([]); const [total, setTotal] = useState(0); const [page, setPage] = useState(1); const [search, setSearch] = useState(''); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const load = useCallback(async () => { setLoading(true); setError(null); try { const params = new URLSearchParams({ page: String(page), perPage: String(PER_PAGE), }); if (search.trim()) params.set('search', search.trim()); const res = await fetch(`/api/admin/users?${params.toString()}`); if (!res.ok) { const b = (await res.json().catch(() => ({}))) as { error?: string }; throw new Error(b.error ?? `Request failed (${res.status})`); } const data = (await res.json()) as { users: AdminUser[]; total: number }; setUsers(data.users); setTotal(data.total); } catch (e) { setError((e as Error).message); } finally { setLoading(false); } }, [page, search]); useEffect(() => { const t = setTimeout(load, 250); return () => clearTimeout(t); }, [load]); const totalPages = Math.max(1, Math.ceil(total / PER_PAGE)); return (

Users

{ setPage(1); setSearch(e.target.value); }} style={{ maxWidth: 320 }} />
{error &&

{error}

} {loading ? (

Loading…

) : users.length === 0 ? (

No users found.

) : (
{users.map((u) => ( ))}
Email Role Status Tunnel Usage Created
{u.email ?? u.id} {u.role === 'admin' ? ( admin ) : ( user )} {isBanned(u) ? ( banned ) : u.email_confirmed_at ? ( confirmed ) : ( unconfirmed )} {u.tunnel ? u.tunnel.subdomain : '—'} {u.tunnel ? `${formatBytes(u.tunnel.bytes_used)} / ${formatBytes( u.tunnel.quota_bytes, )}` : '—'} {formatDate(u.created_at)}
)}
Page {page} of {totalPages} ({total} total)
); }