mirror of
https://github.com/swissmakers/fail2ban-ui.git
synced 2026-04-17 05:53:15 +02:00
Move bad log parsing to utils.js and add section headers
This commit is contained in:
@@ -1,10 +1,17 @@
|
|||||||
// Header components: Clock and Backend Status Indicator
|
// Header components: Clock and Backend Status Indicator
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Global Variables
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
var clockInterval = null;
|
var clockInterval = null;
|
||||||
var statusUpdateCallback = null;
|
var statusUpdateCallback = null;
|
||||||
|
|
||||||
// Initialize clock
|
// =========================================================================
|
||||||
|
// Clock
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
function initClock() {
|
function initClock() {
|
||||||
function updateClock() {
|
function updateClock() {
|
||||||
var now = new Date();
|
var now = new Date();
|
||||||
@@ -18,30 +25,59 @@ function initClock() {
|
|||||||
clockElement.textContent = timeString;
|
clockElement.textContent = timeString;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update immediately
|
|
||||||
updateClock();
|
updateClock();
|
||||||
|
|
||||||
// Update every second
|
|
||||||
if (clockInterval) {
|
if (clockInterval) {
|
||||||
clearInterval(clockInterval);
|
clearInterval(clockInterval);
|
||||||
}
|
}
|
||||||
clockInterval = setInterval(updateClock, 1000);
|
clockInterval = setInterval(updateClock, 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update status indicator
|
// =========================================================================
|
||||||
|
// Status Indicator
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
function initStatusIndicator() {
|
||||||
|
updateStatusIndicator('connecting', 'Connecting...');
|
||||||
|
function registerStatusCallback() {
|
||||||
|
if (typeof wsManager !== 'undefined' && wsManager) {
|
||||||
|
wsManager.onStatusChange(function(state, text) {
|
||||||
|
updateStatusIndicator(state, text);
|
||||||
|
});
|
||||||
|
var currentState = wsManager.getConnectionState();
|
||||||
|
var currentText = 'Connecting...';
|
||||||
|
if (currentState === 'connected' && wsManager.isConnected) {
|
||||||
|
currentText = 'Connected';
|
||||||
|
} else if (currentState === 'connecting') {
|
||||||
|
currentText = 'Connecting...';
|
||||||
|
} else if (currentState === 'disconnected') {
|
||||||
|
currentText = 'Disconnected';
|
||||||
|
} else if (currentState === 'disconnecting') {
|
||||||
|
currentText = 'Disconnecting...';
|
||||||
|
}
|
||||||
|
updateStatusIndicator(currentState, currentText);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!registerStatusCallback()) {
|
||||||
|
var checkInterval = setInterval(function() {
|
||||||
|
if (registerStatusCallback()) {
|
||||||
|
clearInterval(checkInterval);
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
setTimeout(function() {
|
||||||
|
clearInterval(checkInterval);
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function updateStatusIndicator(state, text) {
|
function updateStatusIndicator(state, text) {
|
||||||
var statusDot = document.getElementById('statusDot');
|
var statusDot = document.getElementById('statusDot');
|
||||||
var statusText = document.getElementById('statusText');
|
var statusText = document.getElementById('statusText');
|
||||||
|
|
||||||
if (!statusDot || !statusText) {
|
if (!statusDot || !statusText) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove all color classes
|
|
||||||
statusDot.classList.remove('bg-green-500', 'bg-yellow-500', 'bg-red-500', 'bg-gray-400');
|
statusDot.classList.remove('bg-green-500', 'bg-yellow-500', 'bg-red-500', 'bg-gray-400');
|
||||||
|
|
||||||
// Set color and text based on state
|
|
||||||
switch (state) {
|
switch (state) {
|
||||||
case 'connected':
|
case 'connected':
|
||||||
statusDot.classList.add('bg-green-500');
|
statusDot.classList.add('bg-green-500');
|
||||||
@@ -63,82 +99,30 @@ function updateStatusIndicator(state, text) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize status indicator
|
// =========================================================================
|
||||||
function initStatusIndicator() {
|
// WebSocket Tooltip
|
||||||
// Set initial state
|
// =========================================================================
|
||||||
updateStatusIndicator('connecting', 'Connecting...');
|
|
||||||
|
|
||||||
// Register callback with WebSocket manager when available
|
|
||||||
function registerStatusCallback() {
|
|
||||||
if (typeof wsManager !== 'undefined' && wsManager) {
|
|
||||||
// Register callback for future status changes
|
|
||||||
wsManager.onStatusChange(function(state, text) {
|
|
||||||
updateStatusIndicator(state, text);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Immediately update status based on current connection state
|
|
||||||
// This handles the case where connection was established before callback registration
|
|
||||||
var currentState = wsManager.getConnectionState();
|
|
||||||
var currentText = 'Connecting...';
|
|
||||||
|
|
||||||
if (currentState === 'connected' && wsManager.isConnected) {
|
|
||||||
currentText = 'Connected';
|
|
||||||
} else if (currentState === 'connecting') {
|
|
||||||
currentText = 'Connecting...';
|
|
||||||
} else if (currentState === 'disconnected') {
|
|
||||||
currentText = 'Disconnected';
|
|
||||||
} else if (currentState === 'disconnecting') {
|
|
||||||
currentText = 'Disconnecting...';
|
|
||||||
}
|
|
||||||
|
|
||||||
updateStatusIndicator(currentState, currentText);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!registerStatusCallback()) {
|
|
||||||
// Wait for WebSocket manager to be available
|
|
||||||
var checkInterval = setInterval(function() {
|
|
||||||
if (registerStatusCallback()) {
|
|
||||||
clearInterval(checkInterval);
|
|
||||||
}
|
|
||||||
}, 100);
|
|
||||||
|
|
||||||
// Stop checking after 5 seconds to avoid infinite loop
|
|
||||||
setTimeout(function() {
|
|
||||||
clearInterval(checkInterval);
|
|
||||||
}, 5000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create and manage WebSocket tooltip
|
|
||||||
function createWebSocketTooltip() {
|
function createWebSocketTooltip() {
|
||||||
// Create tooltip element
|
|
||||||
const tooltip = document.createElement('div');
|
const tooltip = document.createElement('div');
|
||||||
tooltip.id = 'wsTooltip';
|
tooltip.id = 'wsTooltip';
|
||||||
tooltip.className = 'fixed z-50 px-3 py-2 bg-gray-900 text-white text-xs rounded shadow-lg pointer-events-none opacity-0 transition-opacity duration-200';
|
tooltip.className = 'fixed z-50 px-3 py-2 bg-gray-900 text-white text-xs rounded shadow-lg pointer-events-none opacity-0 transition-opacity duration-200';
|
||||||
tooltip.style.display = 'none';
|
tooltip.style.display = 'none';
|
||||||
tooltip.style.minWidth = '200px';
|
tooltip.style.minWidth = '200px';
|
||||||
document.body.appendChild(tooltip);
|
document.body.appendChild(tooltip);
|
||||||
|
|
||||||
const statusEl = document.getElementById('backendStatus');
|
const statusEl = document.getElementById('backendStatus');
|
||||||
if (!statusEl) {
|
if (!statusEl) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let tooltipUpdateInterval = null;
|
let tooltipUpdateInterval = null;
|
||||||
|
|
||||||
function updateTooltipContent() {
|
function updateTooltipContent() {
|
||||||
if (!wsManager || !wsManager.isConnected) {
|
if (!wsManager || !wsManager.isConnected) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const info = wsManager.getConnectionInfo();
|
const info = wsManager.getConnectionInfo();
|
||||||
if (!info) {
|
if (!info) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
tooltip.innerHTML = `
|
tooltip.innerHTML = `
|
||||||
<div class="font-semibold mb-2 text-green-400 border-b border-gray-700 pb-1">WebSocket Connection</div>
|
<div class="font-semibold mb-2 text-green-400 border-b border-gray-700 pb-1">WebSocket Connection</div>
|
||||||
<div class="space-y-1">
|
<div class="space-y-1">
|
||||||
@@ -165,21 +149,15 @@ function createWebSocketTooltip() {
|
|||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function showTooltip(e) {
|
function showTooltip(e) {
|
||||||
if (!wsManager || !wsManager.isConnected) {
|
if (!wsManager || !wsManager.isConnected) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
updateTooltipContent();
|
updateTooltipContent();
|
||||||
const rect = statusEl.getBoundingClientRect();
|
const rect = statusEl.getBoundingClientRect();
|
||||||
const tooltipRect = tooltip.getBoundingClientRect();
|
const tooltipRect = tooltip.getBoundingClientRect();
|
||||||
|
|
||||||
// Position tooltip below the status element, centered
|
|
||||||
let left = rect.left + (rect.width / 2) - (tooltipRect.width / 2);
|
let left = rect.left + (rect.width / 2) - (tooltipRect.width / 2);
|
||||||
let top = rect.bottom + 8;
|
let top = rect.bottom + 8;
|
||||||
|
|
||||||
// Adjust if tooltip would go off screen
|
|
||||||
if (left < 8) left = 8;
|
if (left < 8) left = 8;
|
||||||
if (left + tooltipRect.width > window.innerWidth - 8) {
|
if (left + tooltipRect.width > window.innerWidth - 8) {
|
||||||
left = window.innerWidth - tooltipRect.width - 8;
|
left = window.innerWidth - tooltipRect.width - 8;
|
||||||
@@ -187,37 +165,29 @@ function createWebSocketTooltip() {
|
|||||||
if (top + tooltipRect.height > window.innerHeight - 8) {
|
if (top + tooltipRect.height > window.innerHeight - 8) {
|
||||||
top = rect.top - tooltipRect.height - 8;
|
top = rect.top - tooltipRect.height - 8;
|
||||||
}
|
}
|
||||||
|
|
||||||
tooltip.style.left = left + 'px';
|
tooltip.style.left = left + 'px';
|
||||||
tooltip.style.top = top + 'px';
|
tooltip.style.top = top + 'px';
|
||||||
tooltip.style.display = 'block';
|
tooltip.style.display = 'block';
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
tooltip.style.opacity = '1';
|
tooltip.style.opacity = '1';
|
||||||
}, 10);
|
}, 10);
|
||||||
|
|
||||||
// Update tooltip content every second while visible
|
|
||||||
if (tooltipUpdateInterval) {
|
if (tooltipUpdateInterval) {
|
||||||
clearInterval(tooltipUpdateInterval);
|
clearInterval(tooltipUpdateInterval);
|
||||||
}
|
}
|
||||||
tooltipUpdateInterval = setInterval(updateTooltipContent, 1000);
|
tooltipUpdateInterval = setInterval(updateTooltipContent, 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
function hideTooltip() {
|
function hideTooltip() {
|
||||||
tooltip.style.opacity = '0';
|
tooltip.style.opacity = '0';
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
tooltip.style.display = 'none';
|
tooltip.style.display = 'none';
|
||||||
}, 200);
|
}, 200);
|
||||||
|
|
||||||
if (tooltipUpdateInterval) {
|
if (tooltipUpdateInterval) {
|
||||||
clearInterval(tooltipUpdateInterval);
|
clearInterval(tooltipUpdateInterval);
|
||||||
tooltipUpdateInterval = null;
|
tooltipUpdateInterval = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
statusEl.addEventListener('mouseenter', showTooltip);
|
statusEl.addEventListener('mouseenter', showTooltip);
|
||||||
statusEl.addEventListener('mouseleave', hideTooltip);
|
statusEl.addEventListener('mouseleave', hideTooltip);
|
||||||
|
|
||||||
// Also hide tooltip when status changes to disconnected
|
|
||||||
if (typeof wsManager !== 'undefined' && wsManager) {
|
if (typeof wsManager !== 'undefined' && wsManager) {
|
||||||
wsManager.onStatusChange(function(state, text) {
|
wsManager.onStatusChange(function(state, text) {
|
||||||
if (state !== 'connected') {
|
if (state !== 'connected') {
|
||||||
@@ -225,7 +195,6 @@ function createWebSocketTooltip() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Wait for WebSocket manager to be available
|
|
||||||
var checkInterval = setInterval(function() {
|
var checkInterval = setInterval(function() {
|
||||||
if (typeof wsManager !== 'undefined' && wsManager) {
|
if (typeof wsManager !== 'undefined' && wsManager) {
|
||||||
wsManager.onStatusChange(function(state, text) {
|
wsManager.onStatusChange(function(state, text) {
|
||||||
@@ -239,14 +208,16 @@ function createWebSocketTooltip() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize all header components
|
// =========================================================================
|
||||||
|
// Initialization
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
function initHeader() {
|
function initHeader() {
|
||||||
initClock();
|
initClock();
|
||||||
initStatusIndicator();
|
initStatusIndicator();
|
||||||
createWebSocketTooltip();
|
createWebSocketTooltip();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cleanup on page unload
|
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
window.addEventListener('beforeunload', function() {
|
window.addEventListener('beforeunload', function() {
|
||||||
if (clockInterval) {
|
if (clockInterval) {
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
// Ignore IPs tag management functions for Fail2ban UI
|
// "Ignore IPs" tag management for Fail2ban UI
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Tag Rendering, Adding and Removing Functions
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
function renderIgnoreIPsTags(ips) {
|
function renderIgnoreIPsTags(ips) {
|
||||||
const container = document.getElementById('ignoreIPsTags');
|
const container = document.getElementById('ignoreIPsTags');
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
@@ -16,33 +20,25 @@ function renderIgnoreIPsTags(ips) {
|
|||||||
|
|
||||||
function addIgnoreIPTag(ip) {
|
function addIgnoreIPTag(ip) {
|
||||||
if (!ip || !ip.trim()) return;
|
if (!ip || !ip.trim()) return;
|
||||||
|
|
||||||
const trimmedIP = ip.trim();
|
const trimmedIP = ip.trim();
|
||||||
|
|
||||||
// Validate IP before adding - isValidIP is in validation.js
|
|
||||||
if (typeof isValidIP === 'function' && !isValidIP(trimmedIP)) {
|
if (typeof isValidIP === 'function' && !isValidIP(trimmedIP)) {
|
||||||
if (typeof showToast === 'function') {
|
if (typeof showToast === 'function') {
|
||||||
showToast('Invalid IP address, CIDR, or hostname: ' + trimmedIP, 'error');
|
showToast('Invalid IP address, CIDR, or hostname: ' + trimmedIP, 'error');
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const container = document.getElementById('ignoreIPsTags');
|
const container = document.getElementById('ignoreIPsTags');
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
|
|
||||||
const existingTags = Array.from(container.querySelectorAll('.ignore-ip-tag')).map(tag => tag.dataset.ip);
|
const existingTags = Array.from(container.querySelectorAll('.ignore-ip-tag')).map(tag => tag.dataset.ip);
|
||||||
if (existingTags.includes(trimmedIP)) {
|
if (existingTags.includes(trimmedIP)) {
|
||||||
return; // Already exists
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const tag = document.createElement('span');
|
const tag = document.createElement('span');
|
||||||
tag.className = 'ignore-ip-tag inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-blue-100 text-blue-800';
|
tag.className = 'ignore-ip-tag inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-blue-100 text-blue-800';
|
||||||
tag.dataset.ip = trimmedIP;
|
tag.dataset.ip = trimmedIP;
|
||||||
const escapedIP = escapeHtml(trimmedIP);
|
const escapedIP = escapeHtml(trimmedIP);
|
||||||
tag.innerHTML = escapedIP + ' <button type="button" class="ml-1 text-blue-600 hover:text-blue-800 focus:outline-none" onclick="removeIgnoreIPTag(\'' + escapedIP.replace(/'/g, "\\'") + '\')">×</button>';
|
tag.innerHTML = escapedIP + ' <button type="button" class="ml-1 text-blue-600 hover:text-blue-800 focus:outline-none" onclick="removeIgnoreIPTag(\'' + escapedIP.replace(/'/g, "\\'") + '\')">×</button>';
|
||||||
container.appendChild(tag);
|
container.appendChild(tag);
|
||||||
|
|
||||||
// Clear input
|
|
||||||
const input = document.getElementById('ignoreIPInput');
|
const input = document.getElementById('ignoreIPInput');
|
||||||
if (input) input.value = '';
|
if (input) input.value = '';
|
||||||
}
|
}
|
||||||
@@ -57,43 +53,32 @@ function removeIgnoreIPTag(ip) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getIgnoreIPsArray() {
|
// =========================================================================
|
||||||
const container = document.getElementById('ignoreIPsTags');
|
// Input Handling
|
||||||
if (!container) return [];
|
// =========================================================================
|
||||||
const tags = container.querySelectorAll('.ignore-ip-tag');
|
|
||||||
return Array.from(tags).map(tag => tag.dataset.ip).filter(ip => ip && ip.trim());
|
|
||||||
}
|
|
||||||
|
|
||||||
function setupIgnoreIPsInput() {
|
function setupIgnoreIPsInput() {
|
||||||
const input = document.getElementById('ignoreIPInput');
|
const input = document.getElementById('ignoreIPInput');
|
||||||
if (!input) return;
|
if (!input) return;
|
||||||
|
|
||||||
// Allow spaces for space-separated lists, but validate on paste/enter/blur
|
|
||||||
let lastValue = '';
|
let lastValue = '';
|
||||||
input.addEventListener('input', function(e) {
|
input.addEventListener('input', function(e) {
|
||||||
// Allow spaces for space-separated lists
|
|
||||||
// Allow: 0-9, a-z, A-Z, :, ., /, -, _, space (for space-separated lists)
|
|
||||||
let value = this.value;
|
let value = this.value;
|
||||||
// Remove any characters that aren't valid for IPs/hostnames or spaces
|
|
||||||
const filtered = value.replace(/[^0-9a-zA-Z:.\/\-\_\s]/g, '');
|
const filtered = value.replace(/[^0-9a-zA-Z:.\/\-\_\s]/g, '');
|
||||||
if (value !== filtered) {
|
if (value !== filtered) {
|
||||||
this.value = filtered;
|
this.value = filtered;
|
||||||
}
|
}
|
||||||
lastValue = filtered;
|
lastValue = filtered;
|
||||||
});
|
});
|
||||||
|
|
||||||
input.addEventListener('keydown', function(e) {
|
input.addEventListener('keydown', function(e) {
|
||||||
if (e.key === 'Enter' || e.key === ',') {
|
if (e.key === 'Enter' || e.key === ',') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const value = input.value.trim();
|
const value = input.value.trim();
|
||||||
if (value) {
|
if (value) {
|
||||||
// Support space or comma separated IPs
|
|
||||||
const ips = value.split(/[,\s]+/).filter(ip => ip.trim());
|
const ips = value.split(/[,\s]+/).filter(ip => ip.trim());
|
||||||
ips.forEach(ip => addIgnoreIPTag(ip.trim()));
|
ips.forEach(ip => addIgnoreIPTag(ip.trim()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
input.addEventListener('blur', function(e) {
|
input.addEventListener('blur', function(e) {
|
||||||
const value = input.value.trim();
|
const value = input.value.trim();
|
||||||
if (value) {
|
if (value) {
|
||||||
@@ -101,21 +86,27 @@ function setupIgnoreIPsInput() {
|
|||||||
ips.forEach(ip => addIgnoreIPTag(ip.trim()));
|
ips.forEach(ip => addIgnoreIPTag(ip.trim()));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle paste events to automatically process space-separated lists
|
|
||||||
input.addEventListener('paste', function(e) {
|
input.addEventListener('paste', function(e) {
|
||||||
// Allow default paste behavior first
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const value = input.value.trim();
|
const value = input.value.trim();
|
||||||
if (value) {
|
if (value) {
|
||||||
// Check if pasted content contains spaces (likely a space-separated list)
|
|
||||||
if (value.includes(' ') || value.includes(',')) {
|
if (value.includes(' ') || value.includes(',')) {
|
||||||
const ips = value.split(/[,\s]+/).filter(ip => ip.trim());
|
const ips = value.split(/[,\s]+/).filter(ip => ip.trim());
|
||||||
ips.forEach(ip => addIgnoreIPTag(ip.trim()));
|
ips.forEach(ip => addIgnoreIPTag(ip.trim()));
|
||||||
input.value = ''; // Clear input after processing
|
input.value = '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, 0);
|
}, 0);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Data Access
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
function getIgnoreIPsArray() {
|
||||||
|
const container = document.getElementById('ignoreIPsTags');
|
||||||
|
if (!container) return [];
|
||||||
|
const tags = container.querySelectorAll('.ignore-ip-tag');
|
||||||
|
return Array.from(tags).map(tag => tag.dataset.ip).filter(ip => ip && ip.trim());
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,37 +1,36 @@
|
|||||||
// Initialization code for Fail2ban UI
|
// App bootstrap and initialization.
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Bootstrap
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
window.addEventListener('DOMContentLoaded', function() {
|
window.addEventListener('DOMContentLoaded', function() {
|
||||||
showLoading(true);
|
showLoading(true);
|
||||||
|
|
||||||
// Check authentication status first (if auth.js is loaded)
|
|
||||||
if (typeof checkAuthStatus === 'function') {
|
if (typeof checkAuthStatus === 'function') {
|
||||||
checkAuthStatus().then(function(authStatus) {
|
checkAuthStatus().then(function(authStatus) {
|
||||||
// Only proceed with initialization if authenticated or OIDC disabled
|
|
||||||
if (!authStatus.enabled || authStatus.authenticated) {
|
if (!authStatus.enabled || authStatus.authenticated) {
|
||||||
initializeApp();
|
initializeApp();
|
||||||
} else {
|
} else {
|
||||||
// Not authenticated, login page will be shown by checkAuthStatus
|
|
||||||
showLoading(false);
|
showLoading(false);
|
||||||
}
|
}
|
||||||
}).catch(function(err) {
|
}).catch(function(err) {
|
||||||
console.error('Auth check failed:', err);
|
console.error('Auth check failed:', err);
|
||||||
// Proceed with initialization anyway (fallback)
|
|
||||||
initializeApp();
|
initializeApp();
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Auth.js not loaded, proceed normally
|
|
||||||
initializeApp();
|
initializeApp();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// App Initialization
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
function initializeApp() {
|
function initializeApp() {
|
||||||
// Only display external IP if the element exists (not disabled via template variable)
|
|
||||||
if (document.getElementById('external-ip')) {
|
if (document.getElementById('external-ip')) {
|
||||||
displayExternalIP();
|
displayExternalIP();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize header components (clock and status indicator)
|
|
||||||
if (typeof initHeader === 'function') {
|
if (typeof initHeader === 'function') {
|
||||||
initHeader();
|
initHeader();
|
||||||
}
|
}
|
||||||
@@ -50,20 +49,16 @@ function initializeApp() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!registerBanEventHandler()) {
|
if (!registerBanEventHandler()) {
|
||||||
// Wait for WebSocket manager to be available
|
|
||||||
var wsCheckInterval = setInterval(function() {
|
var wsCheckInterval = setInterval(function() {
|
||||||
if (registerBanEventHandler()) {
|
if (registerBanEventHandler()) {
|
||||||
clearInterval(wsCheckInterval);
|
clearInterval(wsCheckInterval);
|
||||||
}
|
}
|
||||||
}, 100);
|
}, 100);
|
||||||
|
|
||||||
// Stop checking after 5 seconds
|
|
||||||
setTimeout(function() {
|
setTimeout(function() {
|
||||||
clearInterval(wsCheckInterval);
|
clearInterval(wsCheckInterval);
|
||||||
}, 5000);
|
}, 5000);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check LOTR mode on page load to apply immediately
|
|
||||||
fetch('/api/settings')
|
fetch('/api/settings')
|
||||||
.then(res => res.json())
|
.then(res => res.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
@@ -71,7 +66,6 @@ function initializeApp() {
|
|||||||
if (typeof checkAndApplyLOTRTheme === 'function') {
|
if (typeof checkAndApplyLOTRTheme === 'function') {
|
||||||
checkAndApplyLOTRTheme(alertCountries);
|
checkAndApplyLOTRTheme(alertCountries);
|
||||||
}
|
}
|
||||||
// Store in global for later use
|
|
||||||
if (typeof currentSettings === 'undefined') {
|
if (typeof currentSettings === 'undefined') {
|
||||||
window.currentSettings = {};
|
window.currentSettings = {};
|
||||||
}
|
}
|
||||||
@@ -81,7 +75,7 @@ function initializeApp() {
|
|||||||
console.warn('Could not check LOTR on load:', err);
|
console.warn('Could not check LOTR on load:', err);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Version and update check: only on page load; UPDATE_CHECK=false disables external GitHub request
|
// Check for updates and display version badge in the footer
|
||||||
var versionContainer = document.getElementById('version-badge-container');
|
var versionContainer = document.getElementById('version-badge-container');
|
||||||
if (versionContainer && versionContainer.getAttribute('data-update-check') === 'true') {
|
if (versionContainer && versionContainer.getAttribute('data-update-check') === 'true') {
|
||||||
fetch('/api/version')
|
fetch('/api/version')
|
||||||
@@ -98,9 +92,10 @@ function initializeApp() {
|
|||||||
versionContainer.innerHTML = '<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-100 text-green-800" title="' + latestLabel + '">' + latestLabel + '</span>';
|
versionContainer.innerHTML = '<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-100 text-green-800" title="' + latestLabel + '">' + latestLabel + '</span>';
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(function() { /* ignore; no badge on error */ });
|
.catch(function() { });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load servers and translations, then render the dashboard and initialize tooltips and search
|
||||||
Promise.all([
|
Promise.all([
|
||||||
loadServers(),
|
loadServers(),
|
||||||
getTranslationsSettingsOnPageload()
|
getTranslationsSettingsOnPageload()
|
||||||
@@ -119,12 +114,12 @@ function initializeApp() {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.finally(function() {
|
.finally(function() {
|
||||||
initializeTooltips(); // Initialize tooltips after fetching and rendering
|
initializeTooltips();
|
||||||
initializeSearch();
|
initializeSearch();
|
||||||
showLoading(false);
|
showLoading(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Setup Select2 for alert countries
|
// jQuery-dependent setup (Select2 for alert countries)
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
$('#alertCountries').select2({
|
$('#alertCountries').select2({
|
||||||
placeholder: 'Select countries..',
|
placeholder: 'Select countries..',
|
||||||
@@ -132,6 +127,7 @@ function initializeApp() {
|
|||||||
width: '100%'
|
width: '100%'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// When "ALL" is selected, deselect other countries and vice versa
|
||||||
$('#alertCountries').on('select2:select', function(e) {
|
$('#alertCountries').on('select2:select', function(e) {
|
||||||
var selectedValue = e.params.data.id;
|
var selectedValue = e.params.data.id;
|
||||||
var currentValues = $('#alertCountries').val() || [];
|
var currentValues = $('#alertCountries').val() || [];
|
||||||
@@ -147,7 +143,6 @@ function initializeApp() {
|
|||||||
$('#alertCountries').val(newValues).trigger('change');
|
$('#alertCountries').val(newValues).trigger('change');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Check LOTR mode after selection change
|
|
||||||
setTimeout(function() {
|
setTimeout(function() {
|
||||||
const selectedCountries = $('#alertCountries').val() || [];
|
const selectedCountries = $('#alertCountries').val() || [];
|
||||||
if (typeof checkAndApplyLOTRTheme === 'function') {
|
if (typeof checkAndApplyLOTRTheme === 'function') {
|
||||||
@@ -157,7 +152,6 @@ function initializeApp() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
$('#alertCountries').on('select2:unselect', function(e) {
|
$('#alertCountries').on('select2:unselect', function(e) {
|
||||||
// Check LOTR mode after deselection
|
|
||||||
setTimeout(function() {
|
setTimeout(function() {
|
||||||
const selectedCountries = $('#alertCountries').val() || [];
|
const selectedCountries = $('#alertCountries').val() || [];
|
||||||
if (typeof checkAndApplyLOTRTheme === 'function') {
|
if (typeof checkAndApplyLOTRTheme === 'function') {
|
||||||
@@ -175,17 +169,14 @@ function initializeApp() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Setup IgnoreIPs tag input
|
|
||||||
if (typeof setupIgnoreIPsInput === 'function') {
|
if (typeof setupIgnoreIPsInput === 'function') {
|
||||||
setupIgnoreIPsInput();
|
setupIgnoreIPsInput();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Setup form validation
|
|
||||||
if (typeof setupFormValidation === 'function') {
|
if (typeof setupFormValidation === 'function') {
|
||||||
setupFormValidation();
|
setupFormValidation();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Setup advanced integration fields
|
|
||||||
const advancedIntegrationSelect = document.getElementById('advancedIntegrationSelect');
|
const advancedIntegrationSelect = document.getElementById('advancedIntegrationSelect');
|
||||||
if (advancedIntegrationSelect && typeof updateAdvancedIntegrationFields === 'function') {
|
if (advancedIntegrationSelect && typeof updateAdvancedIntegrationFields === 'function') {
|
||||||
advancedIntegrationSelect.addEventListener('change', updateAdvancedIntegrationFields);
|
advancedIntegrationSelect.addEventListener('change', updateAdvancedIntegrationFields);
|
||||||
|
|||||||
@@ -8,31 +8,28 @@ function isLOTRMode(alertCountries) {
|
|||||||
return alertCountries.includes('LOTR');
|
return alertCountries.includes('LOTR');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Theme Application
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
function applyLOTRTheme(active) {
|
function applyLOTRTheme(active) {
|
||||||
const body = document.body;
|
const body = document.body;
|
||||||
const lotrCSS = document.getElementById('lotr-css');
|
const lotrCSS = document.getElementById('lotr-css');
|
||||||
|
|
||||||
if (active) {
|
if (active) {
|
||||||
// Enable CSS first
|
|
||||||
if (lotrCSS) {
|
if (lotrCSS) {
|
||||||
lotrCSS.disabled = false;
|
lotrCSS.disabled = false;
|
||||||
}
|
}
|
||||||
// Then add class to body
|
|
||||||
body.classList.add('lotr-mode');
|
body.classList.add('lotr-mode');
|
||||||
isLOTRModeActive = true;
|
isLOTRModeActive = true;
|
||||||
console.log('🎭 LOTR Mode Activated - Welcome to Middle-earth!');
|
console.log('🎭 LOTR Mode Activated - Welcome to Middle-earth!');
|
||||||
} else {
|
} else {
|
||||||
// Remove class first
|
|
||||||
body.classList.remove('lotr-mode');
|
body.classList.remove('lotr-mode');
|
||||||
// Then disable CSS
|
|
||||||
if (lotrCSS) {
|
if (lotrCSS) {
|
||||||
lotrCSS.disabled = true;
|
lotrCSS.disabled = true;
|
||||||
}
|
}
|
||||||
isLOTRModeActive = false;
|
isLOTRModeActive = false;
|
||||||
console.log('🎭 LOTR Mode Deactivated');
|
console.log('🎭 LOTR Mode Deactivated');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Force a reflow to ensure styles are applied
|
|
||||||
void body.offsetHeight;
|
void body.offsetHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,51 +43,36 @@ function checkAndApplyLOTRTheme(alertCountries) {
|
|||||||
|
|
||||||
function updateLOTRTerminology(active) {
|
function updateLOTRTerminology(active) {
|
||||||
if (active) {
|
if (active) {
|
||||||
// Update navigation title
|
|
||||||
const navTitle = document.querySelector('nav .text-xl');
|
const navTitle = document.querySelector('nav .text-xl');
|
||||||
if (navTitle) {
|
if (navTitle) {
|
||||||
navTitle.textContent = 'Middle-earth Security';
|
navTitle.textContent = 'Middle-earth Security';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update page title
|
|
||||||
const pageTitle = document.querySelector('title');
|
const pageTitle = document.querySelector('title');
|
||||||
if (pageTitle) {
|
if (pageTitle) {
|
||||||
pageTitle.textContent = 'Middle-earth Security Realm';
|
pageTitle.textContent = 'Middle-earth Security Realm';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update dashboard terminology
|
|
||||||
updateDashboardLOTRTerminology(true);
|
updateDashboardLOTRTerminology(true);
|
||||||
|
|
||||||
// Add decorative elements
|
|
||||||
addLOTRDecorations();
|
addLOTRDecorations();
|
||||||
} else {
|
} else {
|
||||||
// Restore original text
|
|
||||||
const navTitle = document.querySelector('nav .text-xl');
|
const navTitle = document.querySelector('nav .text-xl');
|
||||||
if (navTitle) {
|
if (navTitle) {
|
||||||
navTitle.textContent = 'Fail2ban UI';
|
navTitle.textContent = 'Fail2ban UI';
|
||||||
}
|
}
|
||||||
|
|
||||||
const pageTitle = document.querySelector('title');
|
const pageTitle = document.querySelector('title');
|
||||||
if (pageTitle && pageTitle.hasAttribute('data-i18n')) {
|
if (pageTitle && pageTitle.hasAttribute('data-i18n')) {
|
||||||
const i18nKey = pageTitle.getAttribute('data-i18n');
|
const i18nKey = pageTitle.getAttribute('data-i18n');
|
||||||
pageTitle.textContent = t(i18nKey, 'Fail2ban UI Dashboard');
|
pageTitle.textContent = t(i18nKey, 'Fail2ban UI Dashboard');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Restore dashboard terminology
|
|
||||||
updateDashboardLOTRTerminology(false);
|
updateDashboardLOTRTerminology(false);
|
||||||
|
|
||||||
// Remove decorative elements
|
|
||||||
removeLOTRDecorations();
|
removeLOTRDecorations();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateDashboardLOTRTerminology(active) {
|
function updateDashboardLOTRTerminology(active) {
|
||||||
// Update text elements that use data-i18n
|
|
||||||
const elements = document.querySelectorAll('[data-i18n]');
|
const elements = document.querySelectorAll('[data-i18n]');
|
||||||
elements.forEach(el => {
|
elements.forEach(el => {
|
||||||
const i18nKey = el.getAttribute('data-i18n');
|
const i18nKey = el.getAttribute('data-i18n');
|
||||||
if (active) {
|
if (active) {
|
||||||
// Check for LOTR-specific translations
|
|
||||||
if (i18nKey === 'dashboard.cards.total_banned') {
|
if (i18nKey === 'dashboard.cards.total_banned') {
|
||||||
el.textContent = t('lotr.threats_banished', 'Threats Banished');
|
el.textContent = t('lotr.threats_banished', 'Threats Banished');
|
||||||
} else if (i18nKey === 'dashboard.table.banned_ips') {
|
} else if (i18nKey === 'dashboard.table.banned_ips') {
|
||||||
@@ -101,14 +83,11 @@ function updateDashboardLOTRTerminology(active) {
|
|||||||
el.textContent = t('lotr.realms_protected', 'Manage Realms');
|
el.textContent = t('lotr.realms_protected', 'Manage Realms');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Restore original translations
|
|
||||||
if (i18nKey) {
|
if (i18nKey) {
|
||||||
el.textContent = t(i18nKey, el.textContent);
|
el.textContent = t(i18nKey, el.textContent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update "Unban" buttons
|
|
||||||
const unbanButtons = document.querySelectorAll('button, a');
|
const unbanButtons = document.querySelectorAll('button, a');
|
||||||
unbanButtons.forEach(btn => {
|
unbanButtons.forEach(btn => {
|
||||||
if (btn.textContent && btn.textContent.includes('Unban')) {
|
if (btn.textContent && btn.textContent.includes('Unban')) {
|
||||||
@@ -122,26 +101,20 @@ function updateDashboardLOTRTerminology(active) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function addLOTRDecorations() {
|
function addLOTRDecorations() {
|
||||||
// Add decorative divider to settings section if not already present
|
|
||||||
const settingsSection = document.getElementById('settingsSection');
|
const settingsSection = document.getElementById('settingsSection');
|
||||||
if (settingsSection && !settingsSection.querySelector('.lotr-divider')) {
|
if (settingsSection && !settingsSection.querySelector('.lotr-divider')) {
|
||||||
const divider = document.createElement('div');
|
const divider = document.createElement('div');
|
||||||
divider.className = 'lotr-divider';
|
divider.className = 'lotr-divider';
|
||||||
divider.style.marginTop = '20px';
|
divider.style.marginTop = '20px';
|
||||||
divider.style.marginBottom = '20px';
|
divider.style.marginBottom = '20px';
|
||||||
|
|
||||||
// Find the first child element (not text node) to insert before
|
|
||||||
const firstChild = Array.from(settingsSection.childNodes).find(
|
const firstChild = Array.from(settingsSection.childNodes).find(
|
||||||
node => node.nodeType === Node.ELEMENT_NODE
|
node => node.nodeType === Node.ELEMENT_NODE
|
||||||
);
|
);
|
||||||
|
|
||||||
if (firstChild && firstChild.parentNode === settingsSection) {
|
if (firstChild && firstChild.parentNode === settingsSection) {
|
||||||
settingsSection.insertBefore(divider, firstChild);
|
settingsSection.insertBefore(divider, firstChild);
|
||||||
} else if (settingsSection.firstChild) {
|
} else if (settingsSection.firstChild) {
|
||||||
// Fallback: append if insertBefore fails
|
|
||||||
settingsSection.insertBefore(divider, settingsSection.firstChild);
|
settingsSection.insertBefore(divider, settingsSection.firstChild);
|
||||||
} else {
|
} else {
|
||||||
// Last resort: append to end
|
|
||||||
settingsSection.appendChild(divider);
|
settingsSection.appendChild(divider);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -151,4 +124,3 @@ function removeLOTRDecorations() {
|
|||||||
const dividers = document.querySelectorAll('.lotr-divider');
|
const dividers = document.querySelectorAll('.lotr-divider');
|
||||||
dividers.forEach(div => div.remove());
|
dividers.forEach(div => div.remove());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
// Modal management functions for Fail2ban UI
|
// Modal management for Fail2ban UI
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Modal Lifecycle
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
function updateBodyScrollLock() {
|
function updateBodyScrollLock() {
|
||||||
if (openModalCount > 0) {
|
if (openModalCount > 0) {
|
||||||
document.body.classList.add('modal-open');
|
document.body.classList.add('modal-open');
|
||||||
@@ -9,7 +13,6 @@ function updateBodyScrollLock() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close modal
|
|
||||||
function closeModal(modalId) {
|
function closeModal(modalId) {
|
||||||
var modal = document.getElementById(modalId);
|
var modal = document.getElementById(modalId);
|
||||||
if (!modal || modal.classList.contains('hidden')) {
|
if (!modal || modal.classList.contains('hidden')) {
|
||||||
@@ -20,7 +23,6 @@ function closeModal(modalId) {
|
|||||||
updateBodyScrollLock();
|
updateBodyScrollLock();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Open modal
|
|
||||||
function openModal(modalId) {
|
function openModal(modalId) {
|
||||||
var modal = document.getElementById(modalId);
|
var modal = document.getElementById(modalId);
|
||||||
if (!modal || !modal.classList.contains('hidden')) {
|
if (!modal || !modal.classList.contains('hidden')) {
|
||||||
@@ -32,6 +34,11 @@ function openModal(modalId) {
|
|||||||
updateBodyScrollLock();
|
updateBodyScrollLock();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Whois and Logs Modal
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
// Whois modal
|
||||||
function openWhoisModal(eventIndex) {
|
function openWhoisModal(eventIndex) {
|
||||||
if (!latestBanEvents || !latestBanEvents[eventIndex]) {
|
if (!latestBanEvents || !latestBanEvents[eventIndex]) {
|
||||||
showToast("Event not found", 'error');
|
showToast("Event not found", 'error');
|
||||||
@@ -42,13 +49,13 @@ function openWhoisModal(eventIndex) {
|
|||||||
showToast("No whois data available for this event", 'info');
|
showToast("No whois data available for this event", 'info');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById('whoisModalIP').textContent = event.ip || 'N/A';
|
document.getElementById('whoisModalIP').textContent = event.ip || 'N/A';
|
||||||
var contentEl = document.getElementById('whoisModalContent');
|
var contentEl = document.getElementById('whoisModalContent');
|
||||||
contentEl.textContent = event.whois;
|
contentEl.textContent = event.whois;
|
||||||
openModal('whoisModal');
|
openModal('whoisModal');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Logs modal
|
||||||
function openLogsModal(eventIndex) {
|
function openLogsModal(eventIndex) {
|
||||||
if (!latestBanEvents || !latestBanEvents[eventIndex]) {
|
if (!latestBanEvents || !latestBanEvents[eventIndex]) {
|
||||||
showToast("Event not found", 'error');
|
showToast("Event not found", 'error');
|
||||||
@@ -59,27 +66,21 @@ function openLogsModal(eventIndex) {
|
|||||||
showToast("No logs data available for this event", 'info');
|
showToast("No logs data available for this event", 'info');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById('logsModalIP').textContent = event.ip || 'N/A';
|
document.getElementById('logsModalIP').textContent = event.ip || 'N/A';
|
||||||
document.getElementById('logsModalJail').textContent = event.jail || 'N/A';
|
document.getElementById('logsModalJail').textContent = event.jail || 'N/A';
|
||||||
|
|
||||||
var logs = event.logs;
|
var logs = event.logs;
|
||||||
var ip = event.ip || '';
|
var ip = event.ip || '';
|
||||||
var logLines = logs.split('\n');
|
var logLines = logs.split('\n');
|
||||||
|
|
||||||
// Determine which lines are suspicious (bad requests)
|
|
||||||
var suspiciousIndices = [];
|
var suspiciousIndices = [];
|
||||||
for (var i = 0; i < logLines.length; i++) {
|
for (var i = 0; i < logLines.length; i++) {
|
||||||
if (isSuspiciousLogLine(logLines[i], ip)) {
|
if (isSuspiciousLogLine(logLines[i], ip)) {
|
||||||
suspiciousIndices.push(i);
|
suspiciousIndices.push(i);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var contentEl = document.getElementById('logsModalContent');
|
var contentEl = document.getElementById('logsModalContent');
|
||||||
if (suspiciousIndices.length) {
|
if (suspiciousIndices.length) {
|
||||||
var highlightMap = {};
|
var highlightMap = {};
|
||||||
suspiciousIndices.forEach(function(idx) { highlightMap[idx] = true; });
|
suspiciousIndices.forEach(function(idx) { highlightMap[idx] = true; });
|
||||||
|
|
||||||
var html = '';
|
var html = '';
|
||||||
for (var j = 0; j < logLines.length; j++) {
|
for (var j = 0; j < logLines.length; j++) {
|
||||||
var safeLine = escapeHtml(logLines[j] || '');
|
var safeLine = escapeHtml(logLines[j] || '');
|
||||||
@@ -91,57 +92,14 @@ function openLogsModal(eventIndex) {
|
|||||||
}
|
}
|
||||||
contentEl.innerHTML = html;
|
contentEl.innerHTML = html;
|
||||||
} else {
|
} else {
|
||||||
// No suspicious lines detected; show raw logs without highlighting
|
|
||||||
contentEl.textContent = logs;
|
contentEl.textContent = logs;
|
||||||
}
|
}
|
||||||
|
|
||||||
openModal('logsModal');
|
openModal('logsModal');
|
||||||
}
|
}
|
||||||
|
|
||||||
function isSuspiciousLogLine(line, ip) {
|
// =========================================================================
|
||||||
if (!line) {
|
// Ban Insights Modal
|
||||||
return false;
|
// =========================================================================
|
||||||
}
|
|
||||||
|
|
||||||
var containsIP = ip && line.indexOf(ip) !== -1;
|
|
||||||
var lowered = line.toLowerCase();
|
|
||||||
|
|
||||||
// Detect HTTP status codes (>= 300 considered problematic)
|
|
||||||
var statusMatch = line.match(/"[^"]*"\s+(\d{3})\b/);
|
|
||||||
if (!statusMatch) {
|
|
||||||
statusMatch = line.match(/\s(\d{3})\s+(?:\d+|-)/);
|
|
||||||
}
|
|
||||||
var statusCode = statusMatch ? parseInt(statusMatch[1], 10) : NaN;
|
|
||||||
var hasBadStatus = !isNaN(statusCode) && statusCode >= 300;
|
|
||||||
|
|
||||||
// Detect common attack indicators in URLs/payloads
|
|
||||||
var indicators = [
|
|
||||||
'../',
|
|
||||||
'%2e%2e',
|
|
||||||
'%252e%252e',
|
|
||||||
'%24%7b',
|
|
||||||
'${',
|
|
||||||
'/etc/passwd',
|
|
||||||
'select%20',
|
|
||||||
'union%20',
|
|
||||||
'cmd=',
|
|
||||||
'wget',
|
|
||||||
'curl ',
|
|
||||||
'nslookup',
|
|
||||||
'/xmlrpc.php',
|
|
||||||
'/wp-admin',
|
|
||||||
'/cgi-bin',
|
|
||||||
'content-length: 0'
|
|
||||||
];
|
|
||||||
var hasIndicator = indicators.some(function(ind) {
|
|
||||||
return lowered.indexOf(ind) !== -1;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (containsIP) {
|
|
||||||
return hasBadStatus || hasIndicator;
|
|
||||||
}
|
|
||||||
return (hasBadStatus || hasIndicator) && !ip;
|
|
||||||
}
|
|
||||||
|
|
||||||
function openBanInsightsModal() {
|
function openBanInsightsModal() {
|
||||||
var countriesContainer = document.getElementById('countryStatsContainer');
|
var countriesContainer = document.getElementById('countryStatsContainer');
|
||||||
@@ -176,7 +134,6 @@ function openBanInsightsModal() {
|
|||||||
+ '</div>';
|
+ '</div>';
|
||||||
}).join('');
|
}).join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
var countries = (latestBanInsights && latestBanInsights.countries) || [];
|
var countries = (latestBanInsights && latestBanInsights.countries) || [];
|
||||||
if (!countries.length) {
|
if (!countries.length) {
|
||||||
countriesContainer.innerHTML = '<p class="text-sm text-gray-500" data-i18n="logs.modal.insights_countries_empty">No bans recorded for this period.</p>';
|
countriesContainer.innerHTML = '<p class="text-sm text-gray-500" data-i18n="logs.modal.insights_countries_empty">No bans recorded for this period.</p>';
|
||||||
@@ -201,7 +158,6 @@ function openBanInsightsModal() {
|
|||||||
}).join('');
|
}).join('');
|
||||||
countriesContainer.innerHTML = countryHTML;
|
countriesContainer.innerHTML = countryHTML;
|
||||||
}
|
}
|
||||||
|
|
||||||
var recurring = (latestBanInsights && latestBanInsights.recurring) || [];
|
var recurring = (latestBanInsights && latestBanInsights.recurring) || [];
|
||||||
if (!recurring.length) {
|
if (!recurring.length) {
|
||||||
recurringContainer.innerHTML = '<p class="text-sm text-gray-500" data-i18n="logs.modal.insights_recurring_empty">No recurring IPs detected.</p>';
|
recurringContainer.innerHTML = '<p class="text-sm text-gray-500" data-i18n="logs.modal.insights_recurring_empty">No recurring IPs detected.</p>';
|
||||||
@@ -226,10 +182,8 @@ function openBanInsightsModal() {
|
|||||||
}).join('');
|
}).join('');
|
||||||
recurringContainer.innerHTML = recurringHTML;
|
recurringContainer.innerHTML = recurringHTML;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof updateTranslations === 'function') {
|
if (typeof updateTranslations === 'function') {
|
||||||
updateTranslations();
|
updateTranslations();
|
||||||
}
|
}
|
||||||
openModal('banInsightsModal');
|
openModal('banInsightsModal');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
// Translation functions for Fail2ban UI
|
// Translation implementation for Fail2ban UI.
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
// Loads translation JSON file for given language (e.g., en, de, etc.)
|
// =========================================================================
|
||||||
|
// Translation Engine
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
function loadTranslations(lang) {
|
function loadTranslations(lang) {
|
||||||
$.getJSON('/locales/' + lang + '.json')
|
$.getJSON('/locales/' + lang + '.json')
|
||||||
.done(function(data) {
|
.done(function(data) {
|
||||||
@@ -13,7 +16,6 @@ function loadTranslations(lang) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Updates all elements with data-i18n attribute with corresponding translation.
|
|
||||||
function updateTranslations() {
|
function updateTranslations() {
|
||||||
$('[data-i18n]').each(function() {
|
$('[data-i18n]').each(function() {
|
||||||
var key = $(this).data('i18n');
|
var key = $(this).data('i18n');
|
||||||
@@ -21,7 +23,6 @@ function updateTranslations() {
|
|||||||
$(this).text(translations[key]);
|
$(this).text(translations[key]);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
// Updates placeholders.
|
|
||||||
$('[data-i18n-placeholder]').each(function() {
|
$('[data-i18n-placeholder]').each(function() {
|
||||||
var key = $(this).data('i18n-placeholder');
|
var key = $(this).data('i18n-placeholder');
|
||||||
if (translations[key]) {
|
if (translations[key]) {
|
||||||
@@ -43,4 +44,3 @@ function getTranslationsSettingsOnPageload() {
|
|||||||
loadTranslations('en');
|
loadTranslations('en');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
// Utility functions for Fail2ban UI
|
// Shared utilities for Fail2ban UI.
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Data Normalization
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
function normalizeInsights(data) {
|
function normalizeInsights(data) {
|
||||||
var normalized = data && typeof data === 'object' ? data : {};
|
var normalized = data && typeof data === 'object' ? data : {};
|
||||||
if (!normalized.totals || typeof normalized.totals !== 'object') {
|
if (!normalized.totals || typeof normalized.totals !== 'object') {
|
||||||
@@ -26,6 +30,10 @@ function t(key, fallback) {
|
|||||||
return fallback !== undefined ? fallback : key;
|
return fallback !== undefined ? fallback : key;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Focus Management
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
function captureFocusState(container) {
|
function captureFocusState(container) {
|
||||||
var active = document.activeElement;
|
var active = document.activeElement;
|
||||||
if (!active || !container || !container.contains(active)) {
|
if (!active || !container || !container.contains(active)) {
|
||||||
@@ -40,9 +48,7 @@ function captureFocusState(container) {
|
|||||||
state.selectionStart = active.selectionStart;
|
state.selectionStart = active.selectionStart;
|
||||||
state.selectionEnd = active.selectionEnd;
|
state.selectionEnd = active.selectionEnd;
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {}
|
||||||
// Ignore selection errors for elements that do not support it.
|
|
||||||
}
|
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,11 +71,13 @@ function restoreFocusState(state) {
|
|||||||
if (typeof state.selectionStart === 'number' && typeof state.selectionEnd === 'number' && typeof next.setSelectionRange === 'function') {
|
if (typeof state.selectionStart === 'number' && typeof state.selectionEnd === 'number' && typeof next.setSelectionRange === 'function') {
|
||||||
next.setSelectionRange(state.selectionStart, state.selectionEnd);
|
next.setSelectionRange(state.selectionStart, state.selectionEnd);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {}
|
||||||
// Element may not support setSelectionRange; ignore.
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// String Helpers
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
function highlightQueryMatch(value, query) {
|
function highlightQueryMatch(value, query) {
|
||||||
var text = value || '';
|
var text = value || '';
|
||||||
if (!query) {
|
if (!query) {
|
||||||
@@ -104,3 +112,46 @@ function slugifyId(value, prefix) {
|
|||||||
return (prefix || 'id') + '-' + base + '-' + hash;
|
return (prefix || 'id') + '-' + base + '-' + hash;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Log Analysis Helper
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
function isSuspiciousLogLine(line, ip) {
|
||||||
|
if (!line) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
var containsIP = ip && line.indexOf(ip) !== -1;
|
||||||
|
var lowered = line.toLowerCase();
|
||||||
|
// Detect HTTP status codes (>= 300 considered problematic)
|
||||||
|
var statusMatch = line.match(/"[^"]*"\s+(\d{3})\b/);
|
||||||
|
if (!statusMatch) {
|
||||||
|
statusMatch = line.match(/\s(\d{3})\s+(?:\d+|-)/);
|
||||||
|
}
|
||||||
|
var statusCode = statusMatch ? parseInt(statusMatch[1], 10) : NaN;
|
||||||
|
var hasBadStatus = !isNaN(statusCode) && statusCode >= 300;
|
||||||
|
// Detect common attack indicators in URLs/payloads
|
||||||
|
var indicators = [
|
||||||
|
'../',
|
||||||
|
'%2e%2e',
|
||||||
|
'%252e%252e',
|
||||||
|
'%24%7b',
|
||||||
|
'${',
|
||||||
|
'/etc/passwd',
|
||||||
|
'select%20',
|
||||||
|
'union%20',
|
||||||
|
'cmd=',
|
||||||
|
'wget',
|
||||||
|
'curl ',
|
||||||
|
'nslookup',
|
||||||
|
'/xmlrpc.php',
|
||||||
|
'/wp-admin',
|
||||||
|
'/cgi-bin',
|
||||||
|
'content-length: 0'
|
||||||
|
];
|
||||||
|
var hasIndicator = indicators.some(function(ind) {
|
||||||
|
return lowered.indexOf(ind) !== -1;
|
||||||
|
}); if (containsIP) {
|
||||||
|
return hasBadStatus || hasIndicator;
|
||||||
|
}
|
||||||
|
return (hasBadStatus || hasIndicator) && !ip;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user