feat(security): Implemented more security information

This commit is contained in:
2026-05-03 22:46:54 +03:00
parent 88ab6e808b
commit 9d9db5cf0b
20 changed files with 902 additions and 193 deletions

View File

@@ -9,6 +9,12 @@
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 = {
@@ -17,7 +23,8 @@
threat: document.getElementById("security-detail-threat"),
geo: document.getElementById("security-detail-geo"),
asn: document.getElementById("security-detail-asn"),
until: document.getElementById("security-detail-until")
until: document.getElementById("security-detail-until"),
why: document.getElementById("security-detail-why")
};
if (!eventsNode || !alertsNode || !bansNode) return;
@@ -25,17 +32,14 @@
events: parse(eventsNode),
alerts: parse(alertsNode),
bans: parse(bansNode),
selectedIP: ""
selectedIP: "",
selectedIPs: new Set()
};
setDefaultBanUntil();
function parse(node) {
try {
return JSON.parse(node.textContent || "[]");
} catch (_) {
return [];
}
try { return JSON.parse(node.textContent || "[]"); } catch (_) { return []; }
}
function showToast(message, type = "info", duration = 1800) {
@@ -107,33 +111,48 @@
});
}
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 banMap = new Map(state.bans.map((entry) => [entry.ip, entry]));
const ips = new Set();
state.events.forEach((event) => {
const ip = String(event.ip || "").trim();
if (ip) ips.add(ip);
});
state.bans.forEach((entry) => {
const ip = String(entry.ip || "").trim();
if (ip) ips.add(ip);
});
const rows = Array.from(ips).sort();
rows.forEach((ip) => {
const ban = banMap.get(ip) || null;
const status = ban ? "banned" : "observed";
const { rows } = rowData();
rows.forEach((rowData) => {
const row = document.createElement("tr");
row.className = "security-bans-body-row";
if (ip === state.selectedIP) row.classList.add("is-selected");
if (rowData.ip === state.selectedIP) row.classList.add("is-selected");
row.innerHTML = `
<td>${escapeHtml(ip || "-")}</td>
<td>${status}</td>
<td>${ban ? createdLabel(ban.until) : "-"}</td>
<td><input type="checkbox" class="security-row-select" data-ip="${escapeHtml(rowData.ip)}" ${state.selectedIPs.has(rowData.ip) ? "checked" : ""}></td>
<td>${escapeHtml(rowData.ip || "-")}</td>
<td>${rowData.status}</td>
<td>${createdLabel(rowData.until)}</td>
`;
row.addEventListener("click", () => setSelectedIP(ip));
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`;
}
@@ -143,18 +162,23 @@
detail.ip.textContent = "No IP selected";
detail.risk.textContent = "-";
detail.threat.textContent = "-";
detail.geo.textContent = "Placeholder (geoipfast later)";
detail.asn.textContent = "Placeholder";
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 = "Placeholder country/region lookup";
detail.asn.textContent = "Placeholder ASN/provider lookup";
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())) {
@@ -186,10 +210,7 @@
async function banIP() {
const ip = String(ipInput.value || "").trim();
if (!ip) {
showToast("Enter IP first", "warning");
return;
}
if (!ip) return showToast("Enter IP first", "warning");
const payload = await postAction("ban", { ip });
setSelectedIP(ip);
showToast(payload.message || "IP banned", "success");
@@ -197,15 +218,10 @@
async function banUntil() {
const ip = String(ipInput.value || "").trim();
if (!ip) {
showToast("Enter IP first", "warning");
return;
}
if (!ip) return showToast("Enter IP first", "warning");
const banUntil = toRFC3339FromLocalUTC(banUntilInput.value);
if (!banUntil) {
showToast("Set valid expiration date", "warning");
return;
}
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");
@@ -213,36 +229,43 @@
async function unbanIP() {
const ip = state.selectedIP || String(ipInput.value || "").trim();
if (!ip) {
showToast("Select or enter IP first", "warning");
return;
}
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") {
window.location.reload();
return;
}
if (command === "ban-ip") {
await banIP();
return;
}
if (command === "ban-until") {
await banUntil();
return;
}
if (command === "unban-ip") {
await unbanIP();
return;
}
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 {
@@ -253,6 +276,30 @@
});
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();
@@ -262,11 +309,6 @@
}
});
if (state.bans.length > 0) {
setSelectedIP(state.bans[0].ip);
} else {
const firstObserved = state.events.find((event) => String(event.ip || "").trim() !== "");
if (firstObserved) setSelectedIP(String(firstObserved.ip || "").trim());
}
if (state.bans.length > 0) setSelectedIP(state.bans[0].ip);
render();
})();