mirror of
https://github.com/swissmakers/fail2ban-ui.git
synced 2026-04-17 05:53:15 +02:00
Make current bans per jail colapsable
This commit is contained in:
@@ -1410,6 +1410,66 @@
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function recurringIPsLastWeekCount() {
|
||||||
|
if (!latestBanInsights || !Array.isArray(latestBanInsights.recurring)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
var cutoff = Date.now() - (7 * 24 * 60 * 60 * 1000);
|
||||||
|
var seen = {};
|
||||||
|
var count = 0;
|
||||||
|
latestBanInsights.recurring.forEach(function(stat) {
|
||||||
|
if (!stat || !stat.ip) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var lastSeenTime = stat.lastSeen ? new Date(stat.lastSeen).getTime() : NaN;
|
||||||
|
if (isNaN(lastSeenTime) || lastSeenTime >= cutoff) {
|
||||||
|
if (!seen[stat.ip]) {
|
||||||
|
seen[stat.ip] = true;
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
function captureFocusState(container) {
|
||||||
|
var active = document.activeElement;
|
||||||
|
if (!active || !container || !container.contains(active)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
var state = { id: active.id || null };
|
||||||
|
if (!state.id) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (typeof active.selectionStart === 'number' && typeof active.selectionEnd === 'number') {
|
||||||
|
state.selectionStart = active.selectionStart;
|
||||||
|
state.selectionEnd = active.selectionEnd;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// Ignore selection errors for elements that do not support it.
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
function restoreFocusState(state) {
|
||||||
|
if (!state || !state.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var next = document.getElementById(state.id);
|
||||||
|
if (!next) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
next.focus();
|
||||||
|
try {
|
||||||
|
if (typeof state.selectionStart === 'number' && typeof state.selectionEnd === 'number' && typeof next.setSelectionRange === 'function') {
|
||||||
|
next.setSelectionRange(state.selectionStart, state.selectionEnd);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// Element may not support setSelectionRange; ignore.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function getBanEventCountries() {
|
function getBanEventCountries() {
|
||||||
var countries = {};
|
var countries = {};
|
||||||
latestBanEvents.forEach(function(event) {
|
latestBanEvents.forEach(function(event) {
|
||||||
@@ -1492,6 +1552,7 @@
|
|||||||
function renderDashboard() {
|
function renderDashboard() {
|
||||||
var container = document.getElementById('dashboard');
|
var container = document.getElementById('dashboard');
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
|
var focusState = captureFocusState(container);
|
||||||
|
|
||||||
var enabledServers = serversCache.filter(function(s) { return s.enabled; });
|
var enabledServers = serversCache.filter(function(s) { return s.enabled; });
|
||||||
if (!serversCache.length) {
|
if (!serversCache.length) {
|
||||||
@@ -1531,7 +1592,7 @@
|
|||||||
} else {
|
} else {
|
||||||
var totalBanned = summary.jails ? summary.jails.reduce(function(sum, j) { return sum + (j.totalBanned || 0); }, 0) : 0;
|
var totalBanned = summary.jails ? summary.jails.reduce(function(sum, j) { return sum + (j.totalBanned || 0); }, 0) : 0;
|
||||||
var newLastHour = summary.jails ? summary.jails.reduce(function(sum, j) { return sum + (j.newInLastHour || 0); }, 0) : 0;
|
var newLastHour = summary.jails ? summary.jails.reduce(function(sum, j) { return sum + (j.newInLastHour || 0); }, 0) : 0;
|
||||||
var totalStored = totalStoredBans();
|
var recurringWeekCount = recurringIPsLastWeekCount();
|
||||||
|
|
||||||
html += ''
|
html += ''
|
||||||
+ '<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">'
|
+ '<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">'
|
||||||
@@ -1548,8 +1609,9 @@
|
|||||||
+ ' <p class="text-2xl font-semibold text-gray-800">' + newLastHour + '</p>'
|
+ ' <p class="text-2xl font-semibold text-gray-800">' + newLastHour + '</p>'
|
||||||
+ ' </div>'
|
+ ' </div>'
|
||||||
+ ' <div class="bg-white rounded-lg shadow p-4">'
|
+ ' <div class="bg-white rounded-lg shadow p-4">'
|
||||||
+ ' <p class="text-sm text-gray-500" data-i18n="dashboard.cards.total_logged">Stored Ban Events</p>'
|
+ ' <p class="text-sm text-gray-500" data-i18n="dashboard.cards.recurring_week">Recurring IPs (7 days)</p>'
|
||||||
+ ' <p class="text-2xl font-semibold text-gray-800">' + totalStored + '</p>'
|
+ ' <p class="text-2xl font-semibold text-gray-800">' + recurringWeekCount + '</p>'
|
||||||
|
+ ' <p class="text-xs text-gray-500 mt-1" data-i18n="dashboard.cards.recurring_hint">Keep an eye on repeated offenders across all servers.</p>'
|
||||||
+ ' </div>'
|
+ ' </div>'
|
||||||
+ '</div>';
|
+ '</div>';
|
||||||
|
|
||||||
@@ -1559,6 +1621,7 @@
|
|||||||
+ ' <div>'
|
+ ' <div>'
|
||||||
+ ' <h3 class="text-lg font-medium text-gray-900 mb-2" data-i18n="dashboard.overview">Overview active Jails and Blocks</h3>'
|
+ ' <h3 class="text-lg font-medium text-gray-900 mb-2" data-i18n="dashboard.overview">Overview active Jails and Blocks</h3>'
|
||||||
+ ' <p class="text-sm text-gray-500" data-i18n="dashboard.overview_hint">Use the search to filter banned IPs and click a jail to edit its configuration.</p>'
|
+ ' <p class="text-sm text-gray-500" data-i18n="dashboard.overview_hint">Use the search to filter banned IPs and click a jail to edit its configuration.</p>'
|
||||||
|
+ ' <p class="text-sm text-gray-500 mt-1" data-i18n="dashboard.overview_detail">Collapse or expand long lists to quickly focus on impacted services.</p>'
|
||||||
+ ' </div>'
|
+ ' </div>'
|
||||||
+ ' <div>'
|
+ ' <div>'
|
||||||
+ ' <label for="ipSearch" class="block text-sm font-medium text-gray-700 mb-2" data-i18n="dashboard.search_label">Search Banned IPs</label>'
|
+ ' <label for="ipSearch" class="block text-sm font-medium text-gray-700 mb-2" data-i18n="dashboard.search_label">Search Banned IPs</label>'
|
||||||
@@ -1607,6 +1670,7 @@
|
|||||||
html += renderLogOverview();
|
html += renderLogOverview();
|
||||||
|
|
||||||
container.innerHTML = html;
|
container.innerHTML = html;
|
||||||
|
restoreFocusState(focusState);
|
||||||
|
|
||||||
const extIpEl = document.getElementById('external-ip');
|
const extIpEl = document.getElementById('external-ip');
|
||||||
if (extIpEl) {
|
if (extIpEl) {
|
||||||
@@ -2188,21 +2252,72 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Render banned IPs with "Unban" button
|
// Render banned IPs with "Unban" button
|
||||||
|
function slugifyId(value, prefix) {
|
||||||
|
var input = (value || '').toString();
|
||||||
|
var base = input.toLowerCase().replace(/[^a-z0-9]+/g, '-');
|
||||||
|
var hash = 0;
|
||||||
|
for (var i = 0; i < input.length; i++) {
|
||||||
|
hash = ((hash << 5) - hash) + input.charCodeAt(i);
|
||||||
|
hash |= 0;
|
||||||
|
}
|
||||||
|
hash = Math.abs(hash);
|
||||||
|
base = base.replace(/^-+|-+$/g, '');
|
||||||
|
if (!base) {
|
||||||
|
base = 'item';
|
||||||
|
}
|
||||||
|
return (prefix || 'id') + '-' + base + '-' + hash;
|
||||||
|
}
|
||||||
|
|
||||||
function renderBannedIPs(jailName, ips) {
|
function renderBannedIPs(jailName, ips) {
|
||||||
if (!ips || ips.length === 0) {
|
if (!ips || ips.length === 0) {
|
||||||
return '<em class="text-gray-500" data-i18n="dashboard.no_banned_ips">No banned IPs</em>';
|
return '<em class="text-gray-500" data-i18n="dashboard.no_banned_ips">No banned IPs</em>';
|
||||||
}
|
}
|
||||||
|
var listId = slugifyId(jailName || 'jail', 'banned-list');
|
||||||
|
var hiddenId = listId + '-hidden';
|
||||||
|
var toggleId = listId + '-toggle';
|
||||||
|
var maxVisible = 5;
|
||||||
|
var visible = ips.slice(0, maxVisible);
|
||||||
|
var hidden = ips.slice(maxVisible);
|
||||||
var content = '<div class="space-y-2">';
|
var content = '<div class="space-y-2">';
|
||||||
ips.forEach(function(ip) {
|
|
||||||
content += ''
|
function bannedIpRow(ip) {
|
||||||
+ '<div class="flex items-center justify-between">'
|
var safeIp = escapeHtml(ip);
|
||||||
+ ' <span class="text-sm">' + ip + '</span>'
|
var encodedIp = encodeURIComponent(ip);
|
||||||
|
return ''
|
||||||
|
+ '<div class="flex items-center justify-between banned-ip-item" data-ip="' + safeIp + '">'
|
||||||
|
+ ' <span class="text-sm" data-ip-value="' + encodedIp + '">' + safeIp + '</span>'
|
||||||
+ ' <button class="bg-yellow-500 text-white px-3 py-1 rounded text-sm hover:bg-yellow-600 transition-colors"'
|
+ ' <button class="bg-yellow-500 text-white px-3 py-1 rounded text-sm hover:bg-yellow-600 transition-colors"'
|
||||||
+ ' onclick="unbanIP(\'' + jailName + '\', \'' + ip + '\')">'
|
+ ' onclick="unbanIP(\'' + jailName + '\', \'' + ip + '\')">'
|
||||||
+ ' <span data-i18n="dashboard.unban">Unban</span>'
|
+ ' <span data-i18n="dashboard.unban">Unban</span>'
|
||||||
+ ' </button>'
|
+ ' </button>'
|
||||||
+ '</div>';
|
+ '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
visible.forEach(function(ip) {
|
||||||
|
content += bannedIpRow(ip);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (hidden.length) {
|
||||||
|
content += '<div class="space-y-2 mt-2 hidden banned-ip-hidden" id="' + hiddenId + '" data-initially-hidden="true">';
|
||||||
|
hidden.forEach(function(ip) {
|
||||||
|
content += bannedIpRow(ip);
|
||||||
|
});
|
||||||
|
content += '</div>';
|
||||||
|
|
||||||
|
var moreLabel = t('dashboard.banned.show_more', 'Show more') + ' +' + hidden.length;
|
||||||
|
var lessLabel = t('dashboard.banned.show_less', 'Hide extra');
|
||||||
|
content += ''
|
||||||
|
+ '<button type="button" class="text-xs font-semibold text-blue-600 hover:text-blue-800 banned-ip-toggle"'
|
||||||
|
+ ' id="' + toggleId + '"'
|
||||||
|
+ ' data-target="' + hiddenId + '"'
|
||||||
|
+ ' data-more-label="' + escapeHtml(moreLabel) + '"'
|
||||||
|
+ ' data-less-label="' + escapeHtml(lessLabel) + '"'
|
||||||
|
+ ' data-expanded="false"'
|
||||||
|
+ ' onclick="toggleBannedList(\'' + hiddenId + '\', \'' + toggleId + '\')">'
|
||||||
|
+ escapeHtml(moreLabel)
|
||||||
|
+ '</button>';
|
||||||
|
}
|
||||||
|
|
||||||
content += '</div>';
|
content += '</div>';
|
||||||
return content;
|
return content;
|
||||||
}
|
}
|
||||||
@@ -2210,37 +2325,70 @@
|
|||||||
// Filter IPs on dashboard table
|
// Filter IPs on dashboard table
|
||||||
function filterIPs() {
|
function filterIPs() {
|
||||||
const input = document.getElementById("ipSearch");
|
const input = document.getElementById("ipSearch");
|
||||||
|
if (!input) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const query = input.value.trim();
|
const query = input.value.trim();
|
||||||
|
const rows = document.querySelectorAll("#jailsTable .jail-row");
|
||||||
|
|
||||||
// Process each row in the jails table
|
rows.forEach(row => {
|
||||||
document.querySelectorAll("#jailsTable .jail-row").forEach(row => {
|
const hiddenSections = row.querySelectorAll(".banned-ip-hidden");
|
||||||
const ipItems = row.querySelectorAll("div.flex");
|
const toggleButtons = row.querySelectorAll(".banned-ip-toggle");
|
||||||
|
|
||||||
|
if (query === "") {
|
||||||
|
hiddenSections.forEach(section => {
|
||||||
|
if (section.getAttribute("data-initially-hidden") === "true") {
|
||||||
|
section.classList.add("hidden");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
toggleButtons.forEach(button => {
|
||||||
|
const moreLabel = button.getAttribute("data-more-label");
|
||||||
|
if (moreLabel) {
|
||||||
|
button.textContent = moreLabel;
|
||||||
|
}
|
||||||
|
button.setAttribute("data-expanded", "false");
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
hiddenSections.forEach(section => section.classList.remove("hidden"));
|
||||||
|
toggleButtons.forEach(button => {
|
||||||
|
const lessLabel = button.getAttribute("data-less-label");
|
||||||
|
if (lessLabel) {
|
||||||
|
button.textContent = lessLabel;
|
||||||
|
}
|
||||||
|
button.setAttribute("data-expanded", "true");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const ipItems = row.querySelectorAll(".banned-ip-item");
|
||||||
let rowHasMatch = false;
|
let rowHasMatch = false;
|
||||||
|
|
||||||
ipItems.forEach(li => {
|
ipItems.forEach(item => {
|
||||||
const span = li.querySelector("span.text-sm");
|
const span = item.querySelector("span.text-sm");
|
||||||
if (!span) return;
|
if (!span) return;
|
||||||
|
|
||||||
// stash original if needed
|
const storedValue = span.getAttribute("data-ip-value");
|
||||||
let originalIP = span.getAttribute("data-original");
|
const originalIP = storedValue ? decodeURIComponent(storedValue) : span.textContent.trim();
|
||||||
if (!originalIP) {
|
|
||||||
originalIP = span.textContent.trim();
|
|
||||||
span.setAttribute("data-original", originalIP);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (query === "") {
|
if (query === "") {
|
||||||
// When the search query is empty, show all IP items and reset their inner HTML.
|
// When the search query is empty, show all IP items and reset their inner HTML.
|
||||||
li.style.display = "";
|
item.style.display = "";
|
||||||
span.innerHTML = originalIP;
|
span.textContent = originalIP;
|
||||||
rowHasMatch = true;
|
rowHasMatch = true;
|
||||||
} else if (originalIP.includes(query)) {
|
} else if (originalIP.indexOf(query) !== -1) {
|
||||||
// If the IP contains the query, show the item and highlight the matching text.
|
// If the IP contains the query, show the item and highlight the matching text.
|
||||||
li.style.display = "";
|
item.style.display = "";
|
||||||
const regex = new RegExp(query, "gi");
|
const escapedPattern = query.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||||
span.innerHTML = originalIP.replace(regex, m => `<mark>${m}</mark>`);
|
const regex = new RegExp(escapedPattern, "gi");
|
||||||
|
var highlighted = originalIP.replace(regex, function(match) {
|
||||||
|
return "%%MARK_START%%" + match + "%%MARK_END%%";
|
||||||
|
});
|
||||||
|
var safeHighlighted = escapeHtml(highlighted)
|
||||||
|
.replace(/%%MARK_START%%/g, "<mark>")
|
||||||
|
.replace(/%%MARK_END%%/g, "</mark>");
|
||||||
|
span.innerHTML = safeHighlighted;
|
||||||
rowHasMatch = true;
|
rowHasMatch = true;
|
||||||
} else {
|
} else {
|
||||||
li.style.display = "none";
|
item.style.display = "none";
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -2249,6 +2397,24 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toggleBannedList(hiddenId, buttonId) {
|
||||||
|
var hidden = document.getElementById(hiddenId);
|
||||||
|
var button = document.getElementById(buttonId);
|
||||||
|
if (!hidden || !button) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var isHidden = hidden.classList.contains("hidden");
|
||||||
|
if (isHidden) {
|
||||||
|
hidden.classList.remove("hidden");
|
||||||
|
button.textContent = button.getAttribute("data-less-label") || button.textContent;
|
||||||
|
button.setAttribute("data-expanded", "true");
|
||||||
|
} else {
|
||||||
|
hidden.classList.add("hidden");
|
||||||
|
button.textContent = button.getAttribute("data-more-label") || button.textContent;
|
||||||
|
button.setAttribute("data-expanded", "false");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//*******************************************************************
|
//*******************************************************************
|
||||||
//* Functions to manage IP-bans : *
|
//* Functions to manage IP-bans : *
|
||||||
//*******************************************************************
|
//*******************************************************************
|
||||||
|
|||||||
Reference in New Issue
Block a user