Security page, admin quick controls, and whitelist reload self-healing
- RCON client retries once on a fresh connection after stale-socket timeouts (panel idle overnight held dead sockets, so 'whitelist reload' silently failed and approvals needed a server restart) - Auth watcher verifies pending players against the LIVE whitelist every 5s and re-writes/reloads if the server missed it, refunding lost window time - Log watcher records blocked join attempts (name + IP) with rate- limited notifications — scanner bots are now visible in the panel - New Security page: blocked-attempt log with one-click IP ban, banned players/IPs management, kick-everyone lockdown - Player detail modal: ban, op/deop, gamemode switching - Overview quick controls: time/weather/difficulty, gamerule toggles (keepInventory, mobGriefing, PvP...), broadcast box - Settings toggles for blocked-attempt notifications (panel + Discord) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
4fe304c5af
commit
deeeaf0042
13 files changed
+809
-3
No files matched your search
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Users, UserX, Wifi } from "lucide-react";
|
||||
import { Users, UserX, Wifi, Crown, Gavel } from "lucide-react";
|
||||
import { api, useApi, useSSE, toast } from "@/lib/api";
|
||||
import { Card, Badge, Button, EmptyState, Modal } from "@/components/ui";
|
||||
import { timeAgo, fmtDateTime } from "@/lib/format";
|
||||
@@ -31,6 +31,33 @@ export default function PlayersPage() {
|
||||
const { data: joins, refresh: refreshJoins } = useApi<ActivityRow[]>("/api/activity?limit=30", 20000);
|
||||
const [detail, setDetail] = useState<PlayerRow | null>(null);
|
||||
const [kicking, setKicking] = useState<string | null>(null);
|
||||
const [adminBusy, setAdminBusy] = useState<string | null>(null);
|
||||
|
||||
const adminAction = async (label: string, body: Record<string, unknown>, message: string) => {
|
||||
setAdminBusy(label);
|
||||
try {
|
||||
await api("/api/admin", { body });
|
||||
toast(message, "success");
|
||||
} catch (err) {
|
||||
toast((err as Error).message, "error");
|
||||
} finally {
|
||||
setAdminBusy(null);
|
||||
}
|
||||
};
|
||||
|
||||
const banPlayer = async (name: string) => {
|
||||
setAdminBusy("ban");
|
||||
try {
|
||||
await api("/api/security", { body: { op: "ban-player", name, reason: "Banned by the admin" } });
|
||||
toast(`${name} banned`, "warn");
|
||||
setDetail(null);
|
||||
void refresh();
|
||||
} catch (err) {
|
||||
toast((err as Error).message, "error");
|
||||
} finally {
|
||||
setAdminBusy(null);
|
||||
}
|
||||
};
|
||||
|
||||
useSSE("/api/stream/events", {
|
||||
"mc-event": () => {
|
||||
@@ -193,6 +220,43 @@ export default function PlayersPage() {
|
||||
<div>UUID: <code>{detail.uuid}</code></div>
|
||||
<div>Last seen: {detail.isOnline ? "online now" : fmtDateTime(detail.lastSeenAt)}</div>
|
||||
</div>
|
||||
|
||||
{/* admin actions */}
|
||||
<div className="mt-4 flex flex-wrap items-center gap-1.5 border-t border-border pt-3">
|
||||
{detail.isOnline && (
|
||||
<>
|
||||
{(["survival", "creative", "spectator"] as const).map((mode) => (
|
||||
<Button
|
||||
key={mode}
|
||||
size="sm"
|
||||
busy={adminBusy === `gm-${mode}`}
|
||||
onClick={() => void adminAction(`gm-${mode}`, { op: "gamemode", name: detail.name, mode }, `${detail.name} → ${mode}`)}
|
||||
>
|
||||
{mode}
|
||||
</Button>
|
||||
))}
|
||||
<span className="mx-1 h-4 w-px bg-border" />
|
||||
</>
|
||||
)}
|
||||
<Button
|
||||
size="sm"
|
||||
busy={adminBusy === "op"}
|
||||
onClick={() => void adminAction("op", { op: "op", name: detail.name, grant: true }, `${detail.name} is now an operator`)}
|
||||
>
|
||||
<Crown size={12} /> Op
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
busy={adminBusy === "deop"}
|
||||
onClick={() => void adminAction("deop", { op: "op", name: detail.name, grant: false }, `${detail.name} de-opped`)}
|
||||
>
|
||||
De-op
|
||||
</Button>
|
||||
<span className="mx-1 h-4 w-px bg-border" />
|
||||
<Button size="sm" variant="danger" busy={adminBusy === "ban"} onClick={() => void banPlayer(detail.name)}>
|
||||
<Gavel size={12} /> Ban
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
|
||||
Reference in new issue
Block a user