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:
Aryan SinghandClaude Fable 5 committed 2026-07-11 11:50:27 -05:00
1 parent 4fe304c5af
commit deeeaf0042
13 files changed
+809 -3

No files matched your search

+21
View File
@@ -8,6 +8,7 @@ import {
whitelistAdd,
whitelistRemove,
whitelistList,
whitelistLive,
kickPlayer,
easyauthList,
easyauthRemove,
@@ -174,6 +175,26 @@ async function reconcile() {
return;
}
// Self-heal: make sure the server's LIVE whitelist actually contains every
// pending player. A `whitelist reload` can get lost (stale RCON connection,
// server restart mid-approval) — without this check the player sits at
// "You are not white-listed" while their window burns down.
try {
const live = new Set((await whitelistLive()).map((n) => n.toLowerCase()));
const missing = pending.filter((p) => !live.has(p.username.toLowerCase()));
if (missing.length > 0) {
for (const p of missing) await whitelistAdd(p.username); // rewrites file + reloads
// They couldn't have joined yet — give the windows back the lost time.
db()
.prepare("UPDATE approved_players SET window_expires_at = window_expires_at + 5000 WHERE status = 'pending'")
.run();
logActivity("whitelist_heal", null, missing.map((m) => m.username).join(", "));
return;
}
} catch {
// whitelist list unavailable — fall through and try again next tick
}
const now = Date.now();
for (const p of pending) {
if (registered.has(p.username.toLowerCase())) {