From 5a40164962b6e48cec91b27b1586748efa1318d7 Mon Sep 17 00:00:00 2001 From: KingKDot Date: Sat, 28 Mar 2026 22:18:33 -0500 Subject: [PATCH] remove all clients --- Overlord-Server/public/assets/settings.js | 38 +++++++++++++++++++ Overlord-Server/public/settings.html | 31 +++++++++++++++ Overlord-Server/src/db.ts | 5 +++ .../src/server/routes/client-routes.ts | 28 ++++++++++++++ 4 files changed, 102 insertions(+) diff --git a/Overlord-Server/public/assets/settings.js b/Overlord-Server/public/assets/settings.js index 8974d3a..f1490f9 100644 --- a/Overlord-Server/public/assets/settings.js +++ b/Overlord-Server/public/assets/settings.js @@ -66,6 +66,10 @@ const exportSettingsBtn = document.getElementById("export-settings-btn"); const importSettingsFile = document.getElementById("import-settings-file"); const exportImportMessage = document.getElementById("export-import-message"); +const wipeOfflineSection = document.getElementById("wipe-offline-section"); +const wipeOfflineBtn = document.getElementById("wipe-offline-btn"); +const wipeOfflineMessage = document.getElementById("wipe-offline-message"); + let currentUser = null; let securityConfig = null; let tlsConfig = null; @@ -774,6 +778,35 @@ async function importSettings(event) { } } +async function wipeOfflineClients() { + if (!wipeOfflineMessage) return; + if (!confirm("Remove ALL offline clients from the dashboard?\n\nThey will reappear if they reconnect later.")) return; + + wipeOfflineBtn.disabled = true; + wipeOfflineMessage.className = "hidden text-sm rounded-lg px-3 py-2 border"; + + try { + const res = await fetch("/api/clients/offline", { + method: "DELETE", + credentials: "include", + }); + const data = await res.json().catch(() => ({})); + if (!res.ok) { + wipeOfflineMessage.textContent = data.error || "Failed to wipe offline clients."; + wipeOfflineMessage.className = "text-sm rounded-lg px-3 py-2 border text-rose-200 border-rose-700 bg-rose-900/30"; + } else { + const n = data.count ?? 0; + wipeOfflineMessage.textContent = n === 0 ? "No offline clients found." : `Removed ${n} offline client${n === 1 ? "" : "s"}.`; + wipeOfflineMessage.className = "text-sm rounded-lg px-3 py-2 border text-emerald-200 border-emerald-700 bg-emerald-900/30"; + } + } catch { + wipeOfflineMessage.textContent = "Request failed."; + wipeOfflineMessage.className = "text-sm rounded-lg px-3 py-2 border text-rose-200 border-rose-700 bg-rose-900/30"; + } finally { + wipeOfflineBtn.disabled = false; + } +} + async function init() { try { await loadCurrentUser(); @@ -783,6 +816,10 @@ async function init() { exportImportSection.classList.remove("hidden"); } + if (canManageClientBans(currentUser?.role) && wipeOfflineSection) { + wipeOfflineSection.classList.remove("hidden"); + } + await loadSecurityPolicy(); await loadTlsSettings(); await loadAppearanceSettings(); @@ -798,6 +835,7 @@ async function init() { if (importSettingsFile) importSettingsFile.addEventListener("change", importSettings); refreshBansBtn.addEventListener("click", loadBannedIps); bansTableBody.addEventListener("click", handleUnbanClick); + if (wipeOfflineBtn) wipeOfflineBtn.addEventListener("click", wipeOfflineClients); } catch (error) { console.error("settings init failed", error); showMessage("Failed to load settings.", "error"); diff --git a/Overlord-Server/public/settings.html b/Overlord-Server/public/settings.html index 4af3e57..baa302e 100644 --- a/Overlord-Server/public/settings.html +++ b/Overlord-Server/public/settings.html @@ -101,6 +101,34 @@ + +
@@ -459,6 +487,9 @@ + + + diff --git a/Overlord-Server/src/db.ts b/Overlord-Server/src/db.ts index abf8e86..ef39e5c 100644 --- a/Overlord-Server/src/db.ts +++ b/Overlord-Server/src/db.ts @@ -265,6 +265,11 @@ export function deleteClientRow(id: string) { db.run(`DELETE FROM clients WHERE id=?`, id); } +export function deleteOfflineClientRows(): number { + const result = db.run(`DELETE FROM clients WHERE online=0`); + return (result as any)?.changes || 0; +} + export function getClientOnlineState(id: string): boolean | null { const row = db.query<{ online: number }>(`SELECT online FROM clients WHERE id=?`).get(id); if (!row) return null; diff --git a/Overlord-Server/src/server/routes/client-routes.ts b/Overlord-Server/src/server/routes/client-routes.ts index 8a9720f..71cb14f 100644 --- a/Overlord-Server/src/server/routes/client-routes.ts +++ b/Overlord-Server/src/server/routes/client-routes.ts @@ -6,6 +6,7 @@ import { banIp, clientExists, deleteClientRow, + deleteOfflineClientRows, getClientOnlineState, getClientIp, isIpBanned, @@ -238,6 +239,33 @@ export async function handleClientRoutes( return Response.json({ ok: true, updated: success }, { headers: deps.CORS_HEADERS }); } + if (req.method === "DELETE" && url.pathname === "/api/clients/offline") { + const user = await authenticateRequest(req); + if (!user) return new Response("Unauthorized", { status: 401 }); + + try { + requirePermission(user, "clients:control"); + } catch (error) { + if (error instanceof Response) return error; + return new Response("Forbidden", { status: 403 }); + } + + const count = deleteOfflineClientRows(); + notifyDashboardViewers(); + + const ip = server.requestIP(req)?.address || "unknown"; + logAudit({ + timestamp: Date.now(), + username: user.username, + ip, + action: AuditAction.COMMAND, + details: `wipe_offline_clients: removed ${count}`, + success: true, + }); + + return Response.json({ ok: true, count }, { headers: deps.CORS_HEADERS }); + } + const clientDeleteMatch = url.pathname.match(/^\/api\/clients\/([^/]+)$/); if (req.method === "DELETE" && clientDeleteMatch) { const user = await authenticateRequest(req);