remove all clients
This commit is contained in:
@@ -66,6 +66,10 @@ const exportSettingsBtn = document.getElementById("export-settings-btn");
|
|||||||
const importSettingsFile = document.getElementById("import-settings-file");
|
const importSettingsFile = document.getElementById("import-settings-file");
|
||||||
const exportImportMessage = document.getElementById("export-import-message");
|
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 currentUser = null;
|
||||||
let securityConfig = null;
|
let securityConfig = null;
|
||||||
let tlsConfig = 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() {
|
async function init() {
|
||||||
try {
|
try {
|
||||||
await loadCurrentUser();
|
await loadCurrentUser();
|
||||||
@@ -783,6 +816,10 @@ async function init() {
|
|||||||
exportImportSection.classList.remove("hidden");
|
exportImportSection.classList.remove("hidden");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (canManageClientBans(currentUser?.role) && wipeOfflineSection) {
|
||||||
|
wipeOfflineSection.classList.remove("hidden");
|
||||||
|
}
|
||||||
|
|
||||||
await loadSecurityPolicy();
|
await loadSecurityPolicy();
|
||||||
await loadTlsSettings();
|
await loadTlsSettings();
|
||||||
await loadAppearanceSettings();
|
await loadAppearanceSettings();
|
||||||
@@ -798,6 +835,7 @@ async function init() {
|
|||||||
if (importSettingsFile) importSettingsFile.addEventListener("change", importSettings);
|
if (importSettingsFile) importSettingsFile.addEventListener("change", importSettings);
|
||||||
refreshBansBtn.addEventListener("click", loadBannedIps);
|
refreshBansBtn.addEventListener("click", loadBannedIps);
|
||||||
bansTableBody.addEventListener("click", handleUnbanClick);
|
bansTableBody.addEventListener("click", handleUnbanClick);
|
||||||
|
if (wipeOfflineBtn) wipeOfflineBtn.addEventListener("click", wipeOfflineClients);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("settings init failed", error);
|
console.error("settings init failed", error);
|
||||||
showMessage("Failed to load settings.", "error");
|
showMessage("Failed to load settings.", "error");
|
||||||
|
|||||||
@@ -101,6 +101,34 @@
|
|||||||
<p id="export-import-message" class="hidden text-sm rounded-lg px-3 py-2 border"></p>
|
<p id="export-import-message" class="hidden text-sm rounded-lg px-3 py-2 border"></p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section id="wipe-offline-section" class="hidden bg-slate-900/60 border border-slate-800 rounded-xl p-5 space-y-4">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="flex items-center justify-center w-9 h-9 rounded-lg bg-red-500/10 border border-red-500/30">
|
||||||
|
<i class="fa-solid fa-circle-minus text-red-400"></i>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2 class="text-lg font-semibold">Wipe Offline Clients</h2>
|
||||||
|
<p class="text-sm text-slate-400">Permanently remove all offline clients from the dashboard in one click.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="rounded-lg border border-slate-700 bg-slate-900 px-4 py-4 flex flex-col sm:flex-row sm:items-center gap-4">
|
||||||
|
<div class="flex-1">
|
||||||
|
<p class="text-sm text-slate-300">
|
||||||
|
Deletes every client currently marked as offline. If any of them reconnect later they will reappear automatically.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
id="wipe-offline-btn"
|
||||||
|
type="button"
|
||||||
|
class="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-red-800/70 hover:bg-red-700 transition-colors text-red-100 border border-red-700 text-sm font-medium whitespace-nowrap"
|
||||||
|
>
|
||||||
|
<i class="fa-solid fa-circle-minus"></i>
|
||||||
|
Wipe Offline
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p id="wipe-offline-message" class="hidden text-sm rounded-lg px-3 py-2 border"></p>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section class="grid grid-cols-1 xl:grid-cols-2 gap-6">
|
<section class="grid grid-cols-1 xl:grid-cols-2 gap-6">
|
||||||
<div class="bg-slate-900/60 border border-slate-800 rounded-xl p-5 space-y-4">
|
<div class="bg-slate-900/60 border border-slate-800 rounded-xl p-5 space-y-4">
|
||||||
<div>
|
<div>
|
||||||
@@ -459,6 +487,9 @@
|
|||||||
|
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
|
||||||
|
</main>
|
||||||
|
|
||||||
<script type="module" src="/assets/nav.js"></script>
|
<script type="module" src="/assets/nav.js"></script>
|
||||||
<script type="module" src="/assets/settings.js"></script>
|
<script type="module" src="/assets/settings.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -265,6 +265,11 @@ export function deleteClientRow(id: string) {
|
|||||||
db.run(`DELETE FROM clients WHERE id=?`, id);
|
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 {
|
export function getClientOnlineState(id: string): boolean | null {
|
||||||
const row = db.query<{ online: number }>(`SELECT online FROM clients WHERE id=?`).get(id);
|
const row = db.query<{ online: number }>(`SELECT online FROM clients WHERE id=?`).get(id);
|
||||||
if (!row) return null;
|
if (!row) return null;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
banIp,
|
banIp,
|
||||||
clientExists,
|
clientExists,
|
||||||
deleteClientRow,
|
deleteClientRow,
|
||||||
|
deleteOfflineClientRows,
|
||||||
getClientOnlineState,
|
getClientOnlineState,
|
||||||
getClientIp,
|
getClientIp,
|
||||||
isIpBanned,
|
isIpBanned,
|
||||||
@@ -238,6 +239,33 @@ export async function handleClientRoutes(
|
|||||||
return Response.json({ ok: true, updated: success }, { headers: deps.CORS_HEADERS });
|
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\/([^/]+)$/);
|
const clientDeleteMatch = url.pathname.match(/^\/api\/clients\/([^/]+)$/);
|
||||||
if (req.method === "DELETE" && clientDeleteMatch) {
|
if (req.method === "DELETE" && clientDeleteMatch) {
|
||||||
const user = await authenticateRequest(req);
|
const user = await authenticateRequest(req);
|
||||||
|
|||||||
Reference in New Issue
Block a user