'use client' import { useState, useEffect, useCallback } from 'react' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from '@/components/ui/card' import { UserPlus, Copy, Check, Trash2, Clock, CheckCircle2, Loader2, KeyRound, Ban, Infinity, Hash, } from 'lucide-react' // ── Shared helper ───────────────────────────────────────────────────────────── function CopyButton({ text }: { text: string }) { const [copied, setCopied] = useState(false) return ( ) } // ── Invites ─────────────────────────────────────────────────────────────────── interface Invite { id: string token: string email: string created_at: string expires_at: string used_at: string | null } function inviteStatus(invite: Invite): 'used' | 'expired' | 'pending' { if (invite.used_at) return 'used' if (new Date(invite.expires_at) < new Date()) return 'expired' return 'pending' } function InvitesSection() { const [invites, setInvites] = useState([]) const [email, setEmail] = useState('') const [isLoading, setIsLoading] = useState(true) const [isCreating, setIsCreating] = useState(false) const [error, setError] = useState(null) const [newToken, setNewToken] = useState(null) const [successEmail, setSuccessEmail] = useState(null) const fetchInvites = useCallback(async () => { try { const res = await fetch('/api/invites') const data = await res.json() if (res.ok) setInvites(data.invites) } finally { setIsLoading(false) } }, []) useEffect(() => { fetchInvites() }, [fetchInvites]) const handleCreate = async (e: React.SyntheticEvent) => { e.preventDefault() setError(null); setSuccessEmail(null); setNewToken(null) setIsCreating(true) try { const res = await fetch('/api/invites', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email }), }) const data = await res.json() if (!res.ok) { setError(data.error); return } setSuccessEmail(email) setNewToken(data.invite.token) setEmail('') await fetchInvites() } catch { setError('Network error. Please try again.') } finally { setIsCreating(false) } } const handleDelete = async (id: string) => { await fetch('/api/invites', { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id }), }) await fetchInvites() } const inviteUrl = (token: string) => `${window.location.origin}/auth/invite/${token}` const statusBadge = (invite: Invite) => { const s = inviteStatus(invite) if (s === 'used') return Used if (s === 'expired') return Expired return Pending } return (
Invite user Send an invite link to a new user. Links expire after 7 days.
setEmail(e.target.value)} className="bg-secondary/50" />
{error &&

{error}

} {newToken && successEmail && (

Invite created for {successEmail}

{inviteUrl(newToken)}
)}
Invitations All invite links, newest first {isLoading ? (
) : invites.length === 0 ? (

No invitations yet

) : (
{invites.map((invite) => { const status = inviteStatus(invite) return (

{invite.email}

{statusBadge(invite)} {new Date(invite.created_at).toLocaleDateString()}
{status === 'pending' && }
) })}
)}
) } // ── Enrollment tokens ───────────────────────────────────────────────────────── interface EnrollmentToken { id: string token: string label: string | null created_at: string expires_at: string | null used_count: number max_uses: number | null revoked_at: string | null } function tokenStatus(t: EnrollmentToken): 'active' | 'revoked' | 'expired' | 'exhausted' { if (t.revoked_at) return 'revoked' if (t.expires_at && new Date(t.expires_at) < new Date()) return 'expired' if (t.max_uses !== null && t.used_count >= t.max_uses) return 'exhausted' return 'active' } function EnrollmentSection() { const [tokens, setTokens] = useState([]) const [isLoading, setIsLoading] = useState(true) const [isCreating, setIsCreating] = useState(false) const [error, setError] = useState(null) const [newToken, setNewToken] = useState(null) // form state const [label, setLabel] = useState('') const [expiresInDays, setExpiresInDays] = useState('') const [maxUses, setMaxUses] = useState('') const fetchTokens = useCallback(async () => { try { const res = await fetch('/api/enrollment-tokens') const data = await res.json() if (res.ok) setTokens(data.tokens) } finally { setIsLoading(false) } }, []) useEffect(() => { fetchTokens() }, [fetchTokens]) const handleCreate = async (e: React.SyntheticEvent) => { e.preventDefault() setError(null); setNewToken(null) setIsCreating(true) try { const res = await fetch('/api/enrollment-tokens', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ label: label || undefined, expiresInDays: expiresInDays ? parseInt(expiresInDays) : undefined, maxUses: maxUses ? parseInt(maxUses) : undefined, }), }) const data = await res.json() if (!res.ok) { setError(data.error); return } setNewToken(data.token) setLabel(''); setExpiresInDays(''); setMaxUses('') await fetchTokens() } catch { setError('Network error. Please try again.') } finally { setIsCreating(false) } } const handleRevoke = async (id: string) => { await fetch('/api/enrollment-tokens', { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id }), }) await fetchTokens() } const statusBadge = (t: EnrollmentToken) => { const s = tokenStatus(t) const styles: Record = { active: 'text-green-500', revoked: 'text-destructive', expired: 'text-muted-foreground', exhausted: 'text-yellow-500', } const labels: Record = { active: 'Active', revoked: 'Revoked', expired: 'Expired', exhausted: 'Exhausted', } return {labels[s]} } const agentCommand = (token: string) => `python agent.py --server ${window.location.origin} --enroll ${token}` return (
Create enrollment token Tokens let agents self-register. Share the token or use it in a silent installer for mass deployment.
setLabel(e.target.value)} className="bg-secondary/50" />
setExpiresInDays(e.target.value)} className="bg-secondary/50" />
setMaxUses(e.target.value)} className="bg-secondary/50" />
{error &&

{error}

}
{newToken && (

Token created

Token

{newToken.token}

Agent command

{agentCommand(newToken.token)}

Silent Windows installer

{`RemoteLink-Setup.exe /S /SERVER=${window.location.origin} /ENROLL=${newToken.token}`}
)}
Enrollment tokens Manage tokens used to register new agents {isLoading ? (
) : tokens.length === 0 ? (

No enrollment tokens yet

) : (
{tokens.map((t) => { const status = tokenStatus(t) const isActive = status === 'active' return (

{t.label || Unlabelled}

{statusBadge(t)}
{t.token} {t.used_count} use{t.used_count !== 1 ? 's' : ''} {t.max_uses !== null ? ` / ${t.max_uses}` : ''} {t.expires_at ? ( {new Date(t.expires_at) > new Date() ? `Expires ${new Date(t.expires_at).toLocaleDateString()}` : `Expired ${new Date(t.expires_at).toLocaleDateString()}`} ) : ( No expiry )}
{isActive && } {isActive && ( )}
) })}
)}
) } // ── Page ────────────────────────────────────────────────────────────────────── type Tab = 'invites' | 'enrollment' export default function AdminPage() { const [tab, setTab] = useState('invites') return (

Admin

Manage users and agent deployment

{/* Tab bar */}
{(['invites', 'enrollment'] as Tab[]).map((t) => ( ))}
{tab === 'invites' ? : }
) }