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 && (
| Player |
Expires |
Last IP |
|
{banned.map(p => (
|
{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 (
{data ? `${data.length} total · ${data.filter(active).length} active` : ''}
{error &&
{error}
}
{data && data.length === 0 &&
}
{data && data.length > 0 && (
| CIDR |
Reason |
Issued |
Expires |
|
{data.map(b => (
| {b.cidr} |
{b.reason || '—'} |
{relativeTime(b.bannedAt)} |
{b.until ? {relativeTime(b.until)} : ∞}
|
|
))}
)}
);
}