(() => { const menuController = window.WarpBoxUI?.bindMenuBar?.() || { close() {} }; const eventsNode = document.getElementById("security-events-data"); const alertsNode = document.getElementById("security-alerts-data"); const bansNode = document.getElementById("security-bans-data"); const ipInput = document.getElementById("security-ip-input"); const banUntilInput = document.getElementById("security-ban-until"); const alertList = document.getElementById("security-alert-list"); const activityBody = document.getElementById("security-activity-body"); const bansBody = document.getElementById("security-bans-body"); const bansCount = document.getElementById("security-bans-count"); const filterInput = document.getElementById("security-ban-filter"); const sortSelect = document.getElementById("security-ban-sort"); const selectAll = document.getElementById("security-select-all"); const copyIPButton = document.getElementById("security-copy-ip"); const openActivityButton = document.getElementById("security-open-activity"); const openAlertsButton = document.getElementById("security-open-alerts"); const toast = document.getElementById("toast"); const detail = { ip: document.getElementById("security-detail-ip"), risk: document.getElementById("security-detail-risk"), threat: document.getElementById("security-detail-threat"), geo: document.getElementById("security-detail-geo"), asn: document.getElementById("security-detail-asn"), until: document.getElementById("security-detail-until"), why: document.getElementById("security-detail-why") }; if (!eventsNode || !alertsNode || !bansNode) return; const state = { events: parse(eventsNode), alerts: parse(alertsNode), bans: parse(bansNode), selectedIP: "", selectedIPs: new Set() }; setDefaultBanUntil(); function parse(node) { try { return JSON.parse(node.textContent || "[]"); } catch (_) { return []; } } function showToast(message, type = "info", duration = 1800) { window.WarpBoxUI?.toast?.(message, type, { target: toast, duration }); } function createdLabel(value) { const parsed = new Date(value); if (Number.isNaN(parsed.getTime())) return "-"; return parsed.toISOString().replace("T", " ").slice(0, 16) + " UTC"; } function setDefaultBanUntil() { const base = new Date(Date.now() + 30 * 60 * 1000); const yyyy = String(base.getUTCFullYear()); const mm = String(base.getUTCMonth() + 1).padStart(2, "0"); const dd = String(base.getUTCDate()).padStart(2, "0"); const hh = String(base.getUTCHours()).padStart(2, "0"); const mi = String(base.getUTCMinutes()).padStart(2, "0"); banUntilInput.value = `${yyyy}-${mm}-${dd}T${hh}:${mi}`; } function toRFC3339FromLocalUTC(datetimeLocalValue) { if (!datetimeLocalValue) return ""; const date = new Date(datetimeLocalValue + ":00Z"); if (Number.isNaN(date.getTime())) return ""; return date.toISOString(); } function setSelectedIP(ip) { state.selectedIP = ip || ""; if (state.selectedIP) ipInput.value = state.selectedIP; renderBans(); renderIPDetails(); } function render() { renderAlerts(); renderActivity(); renderBans(); renderIPDetails(); } function renderAlerts() { alertList.innerHTML = ""; state.alerts.slice(0, 12).forEach((alert) => { const entry = document.createElement("li"); entry.textContent = `${createdLabel(alert.created_at)} | ${alert.severity || "low"} | ${alert.title || "-"}`; alertList.appendChild(entry); }); } function renderActivity() { activityBody.innerHTML = ""; state.events .filter((event) => String(event.kind || "").startsWith("security") || String(event.kind || "").startsWith("auth.admin")) .slice(0, 60) .forEach((event) => { const row = document.createElement("tr"); row.innerHTML = ` ${createdLabel(event.created_at)} ${escapeHtml(event.kind || "-")} ${escapeHtml(event.severity || "-")} ${escapeHtml(event.ip || "-")} ${escapeHtml(event.path || "-")} ${escapeHtml(event.message || "-")} `; activityBody.appendChild(row); }); } function rowData() { const banMap = new Map(state.bans.map((entry) => [entry.ip, entry])); const filter = String(filterInput?.value || "").trim().toLowerCase(); let rows = state.bans.map((entry) => ({ ip: entry.ip, status: "banned", until: entry.until, ban: entry })); if (filter) rows = rows.filter((row) => row.ip.toLowerCase().includes(filter)); const sort = sortSelect?.value || "expiry_asc"; rows.sort((a, b) => { if (sort === "ip_asc") return a.ip.localeCompare(b.ip); if (sort === "ip_desc") return b.ip.localeCompare(a.ip); const av = new Date(a.until).getTime(); const bv = new Date(b.until).getTime(); return sort === "expiry_desc" ? bv - av : av - bv; }); return { rows, banMap }; } function renderBans() { bansBody.innerHTML = ""; const { rows } = rowData(); rows.forEach((rowData) => { const row = document.createElement("tr"); row.className = "security-bans-body-row"; if (rowData.ip === state.selectedIP) row.classList.add("is-selected"); row.innerHTML = ` ${escapeHtml(rowData.ip || "-")} ${rowData.status} ${createdLabel(rowData.until)} `; row.addEventListener("click", (event) => { if (event.target && event.target.classList.contains("security-row-select")) return; setSelectedIP(rowData.ip); }); bansBody.appendChild(row); }); bansBody.querySelectorAll(".security-row-select").forEach((checkbox) => { checkbox.addEventListener("change", () => { const ip = checkbox.getAttribute("data-ip"); if (!ip) return; if (checkbox.checked) state.selectedIPs.add(ip); else state.selectedIPs.delete(ip); }); }); bansCount.textContent = `${state.bans.length} active bans`; } function renderIPDetails() { const ip = state.selectedIP || String(ipInput.value || "").trim(); if (!ip) { detail.ip.textContent = "No IP selected"; detail.risk.textContent = "-"; detail.threat.textContent = "-"; detail.geo.textContent = "GeoIP not enabled yet"; detail.asn.textContent = "GeoIP not enabled yet"; detail.until.textContent = "-"; detail.why.textContent = "-"; return; } const ban = state.bans.find((entry) => entry.ip === ip); const matchingEvents = state.events.filter((event) => String(event.ip || "") === ip); const matchingAlerts = state.alerts.filter((alert) => String(alert?.meta?.ip || "") === ip); const lastEvent = matchingEvents[0] || null; detail.ip.textContent = ip; detail.risk.textContent = ban ? "high" : "medium"; detail.threat.textContent = ban ? "Temporary banned source" : "Observed source"; detail.geo.textContent = "GeoIP not enabled yet"; detail.asn.textContent = "GeoIP not enabled yet"; detail.until.textContent = ban ? createdLabel(ban.until) : "Not banned"; detail.why.textContent = `${matchingEvents.length} events, ${matchingAlerts.length} alerts${lastEvent ? `, latest=${lastEvent.kind}` : ""}`; if (ban && ban.until) { const parsed = new Date(ban.until); if (!Number.isNaN(parsed.getTime())) { const yyyy = String(parsed.getUTCFullYear()); const mm = String(parsed.getUTCMonth() + 1).padStart(2, "0"); const dd = String(parsed.getUTCDate()).padStart(2, "0"); const hh = String(parsed.getUTCHours()).padStart(2, "0"); const mi = String(parsed.getUTCMinutes()).padStart(2, "0"); banUntilInput.value = `${yyyy}-${mm}-${dd}T${hh}:${mi}`; } } } function escapeHtml(value) { return window.WarpBoxUI?.htmlEscape?.(value) || String(value ?? ""); } async function postAction(action, payload = {}) { const response = await fetch("/admin/security/actions", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ action, ...payload }) }); const result = await response.json().catch(() => ({})); if (!response.ok) throw new Error(result.error || "Request failed"); if (Array.isArray(result.bans)) state.bans = result.bans; return result; } async function banIP() { const ip = String(ipInput.value || "").trim(); if (!ip) return showToast("Enter IP first", "warning"); const payload = await postAction("ban", { ip }); setSelectedIP(ip); showToast(payload.message || "IP banned", "success"); } async function banUntil() { const ip = String(ipInput.value || "").trim(); if (!ip) return showToast("Enter IP first", "warning"); const banUntil = toRFC3339FromLocalUTC(banUntilInput.value); if (!banUntil) return showToast("Set valid expiration date", "warning"); if (!window.confirm("Apply custom ban expiration?")) return; const payload = await postAction("ban_until", { ip, ban_until: banUntil }); setSelectedIP(ip); showToast(payload.message || "IP ban expiration updated", "success"); } async function unbanIP() { const ip = state.selectedIP || String(ipInput.value || "").trim(); if (!ip) return showToast("Select or enter IP first", "warning"); if (!window.confirm(`Unban ${ip}?`)) return; const payload = await postAction("unban", { ip }); state.selectedIPs.delete(ip); setSelectedIP(""); showToast(payload.message || "IP unbanned", "success"); } async function bulkUnban() { const ips = Array.from(state.selectedIPs); if (ips.length === 0) return showToast("Select at least one banned IP", "warning"); if (!window.confirm(`Unban ${ips.length} selected IPs?`)) return; const payload = await postAction("bulk_unban", { ips }); state.selectedIPs.clear(); setSelectedIP(""); showToast(payload.message || "Bulk unban complete", "success"); } async function unbanAll() { if (!window.confirm("Unban all active bans?")) return; const payload = await postAction("unban_all"); state.selectedIPs.clear(); setSelectedIP(""); showToast(payload.message || "All bans cleared", "success"); } document.querySelectorAll("[data-command]").forEach((button) => { button.addEventListener("click", async () => { menuController.close(); try { const command = button.dataset.command; if (command === "refresh") return window.location.reload(); if (command === "ban-ip") return await banIP(); if (command === "ban-until") return await banUntil(); if (command === "unban-ip") return await unbanIP(); if (command === "bulk-unban") return await bulkUnban(); if (command === "unban-all") return await unbanAll(); } catch (error) { showToast(error.message, "error", 3200); } finally { renderBans(); renderIPDetails(); } }); }); ipInput.addEventListener("input", () => renderIPDetails()); filterInput?.addEventListener("input", () => renderBans()); sortSelect?.addEventListener("change", () => renderBans()); selectAll?.addEventListener("change", () => { if (selectAll.checked) state.bans.forEach((ban) => state.selectedIPs.add(ban.ip)); else state.selectedIPs.clear(); renderBans(); }); copyIPButton?.addEventListener("click", async () => { const ip = state.selectedIP || String(ipInput.value || "").trim(); if (!ip) return showToast("No IP selected", "warning"); await navigator.clipboard.writeText(ip); showToast("IP copied", "success"); }); openActivityButton?.addEventListener("click", () => { const ip = state.selectedIP || String(ipInput.value || "").trim(); if (!ip) return showToast("No IP selected", "warning"); window.location.href = `/admin/activity?q=${encodeURIComponent(ip)}`; }); openAlertsButton?.addEventListener("click", () => { const ip = state.selectedIP || String(ipInput.value || "").trim(); if (!ip) return showToast("No IP selected", "warning"); window.location.href = `/admin/alerts?q=${encodeURIComponent(ip)}`; }); document.addEventListener("keydown", (event) => { if (event.key === "Escape") menuController.close(); if (event.key === "F5") { event.preventDefault(); window.location.reload(); } }); if (state.bans.length > 0) setSelectedIP(state.bans[0].ip); render(); })();