import { useState } from 'react'; import { api } from '../lib/api'; import type { IpBan, Player } from '../lib/types'; import { useApi } from '../lib/useApi'; import { PageHeader } from '../components/PageHeader'; import { useToast } from '../components/Toast'; import { Empty } from '../components/Empty'; import { absoluteTime, relativeTime } from '../lib/format'; import { RefreshCw, Trash } from '../components/Icons'; export function Bans({ embedded }: { embedded?: boolean } = {}) { const [tab, setTab] = useState<'players' | 'ip'>('players'); return (
{!embedded && ( )}
{(['players', 'ip'] as const).map(t => ( ))}
{tab === 'players' && } {tab === 'ip' && }
); } function PlayerBans() { // Pull a generous page of players and filter client-side for currently-banned. // For a small private server this is fine; if we ever scale we can add a // dedicated /players/banned endpoint. const { data, loading, error, refresh } = useApi('/players?take=200'); const now = Date.now(); const banned = (data ?? []).filter(p => p.bannedUntil && new Date(p.bannedUntil).getTime() > now); const toast = useToast(); const unban = async (id: number) => { try { await api(`/players/${id}/unban`, { method: 'POST', body: { Reason: 'admin-unban' } }); toast.push('Unbanned', 'success'); refresh(); } catch (e) { toast.push((e as Error).message, 'error'); } }; return (
{banned.length} active player ban{banned.length === 1 ? '' : 's'}
{error &&
{error}
} {!error && banned.length === 0 && } {banned.length > 0 && (
{banned.map(p => ( ))}
Player Expires Last IP
{p.displayName || p.username}
@{p.username} · #{p.id}
{relativeTime(p.bannedUntil)} ({absoluteTime(p.bannedUntil)}) {p.lastIp ?? '—'}
)}
); } function IpBans() { const { data, loading, error, refresh } = useApi('/ipbans'); const toast = useToast(); const [cidr, setCidr] = useState(''); const [reason, setReason] = useState(''); const [days, setDays] = useState(''); const add = async (e: React.FormEvent) => { e.preventDefault(); if (!cidr.trim()) return; try { await api('/ipbans', { method: 'POST', body: { Cidr: cidr.trim(), Reason: reason, DurationDays: days || null } }); setCidr(''); setReason(''); setDays(''); toast.push('IP ban added', 'success'); refresh(); } catch (err) { toast.push((err as Error).message, 'error'); } }; const remove = async (id: number) => { if (!confirm('Remove this IP ban?')) return; try { await api(`/ipbans/${id}`, { method: 'DELETE' }); toast.push('IP ban removed', 'success'); refresh(); } catch (err) { toast.push((err as Error).message, 'error'); } }; const active = (b: IpBan) => !b.until || new Date(b.until).getTime() > Date.now(); return (
setCidr(e.target.value)} placeholder="1.2.3.4 or 10.0.0.0/24" required className="input" /> setReason(e.target.value)} placeholder="Reason (logged)" className="input" /> setDays(e.target.value === '' ? '' : parseInt(e.target.value))} placeholder="days (∞)" className="input" />
{data ? `${data.length} total · ${data.filter(active).length} active` : ''}
{error &&
{error}
} {data && data.length === 0 && } {data && data.length > 0 && (
{data.map(b => ( ))}
CIDR Reason Issued Expires
{b.cidr} {b.reason || '—'} {relativeTime(b.bannedAt)} {b.until ? {relativeTime(b.until)} : ∞}
)}
); }