"use client"; import { useEffect, useState } from "react"; import { ShieldCheck, UserPlus, KeyRound, RefreshCw, Import, Copy } from "lucide-react"; import { api, useApi, useSSE, toast } from "@/lib/api"; import { Card, Button, Badge, Input, Modal, EmptyState } from "@/components/ui"; import { timeAgo, fmtCountdown, fmtDateTime } from "@/lib/format"; type ApprovedPlayer = { id: number; username: string; status: "pending" | "registered" | "expired" | "revoked"; approved_at: number | null; window_expires_at: number | null; registered_at: number | null; last_seen_at: number | null; note: string | null; }; type AccessData = { approved: ApprovedPlayer[]; whitelist: string[] | null; registered: string[] | null; }; const STATUS_BADGE: Record = { pending: { color: "warn", label: "waiting to register" }, registered: { color: "good", label: "registered" }, expired: { color: "muted", label: "window expired" }, revoked: { color: "crit", label: "revoked" }, }; export default function AccessPage() { const { data, error, refresh } = useApi("/api/approved", 10000); const [newName, setNewName] = useState(""); const [busy, setBusy] = useState(null); const [tempPassword, setTempPassword] = useState<{ user: string; password: string } | null>(null); const [confirmRevoke, setConfirmRevoke] = useState(null); const [, tick] = useState(0); // live countdown for pending windows useEffect(() => { const t = setInterval(() => tick((x) => x + 1), 1000); return () => clearInterval(t); }, []); useSSE("/api/stream/events", { "mc-event": () => void refresh(), }); const run = async (label: string, body: Record, done?: (d: unknown) => void) => { setBusy(label); try { const d = await api("/api/approved", { body }); done?.(d); void refresh(); } catch (err) { toast((err as Error).message, "error"); } finally { setBusy(null); } }; const approve = () => { const name = newName.trim(); if (!name) return; void run("approve", { op: "approve", username: name }, () => { toast(`${name} approved — the registration window is open`, "success"); setNewName(""); }); }; const players = data?.approved ?? []; const outsideWhitelist = (data?.whitelist ?? []).filter( (w) => !players.some((p) => p.username.toLowerCase() === w.toLowerCase()) ); return (

Access & Auth

Only approved usernames can join. New players must /register within the window, then{" "} /login on every visit.

{/* approve box */}
setNewName(e.target.value)} onKeyDown={(e) => e.key === "Enter" && approve()} placeholder="e.g. Steve_123" className="w-full font-mono" maxLength={16} />

Approving whitelists the name and starts the registration countdown. Tell your friend to join{" "} right away and run /register <password> <password>. If the timer expires first, the name is automatically removed from the whitelist.

{error && (
{error}
)} {/* player table */} void run("import", { op: "import-whitelist" }, (d) => { const n = (d as { imported: number }).imported; toast(n > 0 ? `Imported ${n} whitelist entr${n === 1 ? "y" : "ies"}` : "Nothing new to import", "info"); }) } busy={busy === "import"} title="Pull in players whitelisted outside the panel" > Import whitelist } pad={false} > {players.length === 0 ? ( } title="Nobody approved yet" hint="Approve your friends' usernames above. Nobody else can join the server." /> ) : ( {players.map((p) => { const badge = STATUS_BADGE[p.status]; return ( ); })}
Player Status Last seen Registered Actions
{p.username} {badge.label} {p.status === "pending" && p.window_expires_at && ( {fmtCountdown(p.window_expires_at)} )} {p.note && ({p.note})} {timeAgo(p.last_seen_at)} {fmtDateTime(p.registered_at)}
{(p.status === "expired" || p.status === "revoked") && ( )} {p.status === "registered" && ( <> )} {p.status !== "revoked" && ( )}
)}
{/* whitelist drift warning */} {outsideWhitelist.length > 0 && (
{outsideWhitelist.map((w) => ( {w} ))} — use “Import whitelist” to manage them from the panel.
)} {/* temp password modal */} setTempPassword(null)} title={`Temporary password for ${tempPassword?.user}`} >

Share this privately. They log in with it and can change it in game via{" "} /account.

{tempPassword?.password}

This password is shown once and never stored by the panel.

{/* revoke confirm modal */} setConfirmRevoke(null)} title={`Revoke ${confirmRevoke}?`}>

This removes them from the whitelist, deletes their password account, and kicks them if online. You can re-approve them later.

); }