mirror of
https://github.com/swissmakers/fail2ban-ui.git
synced 2026-04-15 05:03:14 +02:00
Implementing WebSocked Support for immediately ban-messages
This commit is contained in:
@@ -18,6 +18,13 @@ function showLoading(show) {
|
||||
function showToast(message, type) {
|
||||
var container = document.getElementById('toast-container');
|
||||
if (!container || !message) return;
|
||||
|
||||
// Handle ban event objects
|
||||
if (typeof message === 'object' && message.type === 'ban_event') {
|
||||
showBanEventToast(message.data || message);
|
||||
return;
|
||||
}
|
||||
|
||||
var toast = document.createElement('div');
|
||||
var variant = type || 'info';
|
||||
toast.className = 'toast toast-' + variant;
|
||||
@@ -34,6 +41,60 @@ function showToast(message, type) {
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
// Show toast for ban event
|
||||
function showBanEventToast(event) {
|
||||
var container = document.getElementById('toast-container');
|
||||
if (!container || !event) return;
|
||||
|
||||
var toast = document.createElement('div');
|
||||
toast.className = 'toast toast-ban-event';
|
||||
|
||||
var ip = event.ip || 'Unknown IP';
|
||||
var jail = event.jail || 'Unknown Jail';
|
||||
var server = event.serverName || event.serverId || 'Unknown Server';
|
||||
var country = event.country || 'UNKNOWN';
|
||||
|
||||
toast.innerHTML = ''
|
||||
+ '<div class="flex items-start gap-3">'
|
||||
+ ' <div class="flex-shrink-0 mt-1">'
|
||||
+ ' <i class="fas fa-shield-alt text-red-500"></i>'
|
||||
+ ' </div>'
|
||||
+ ' <div class="flex-1 min-w-0">'
|
||||
+ ' <div class="font-semibold text-sm">New Block Detected</div>'
|
||||
+ ' <div class="text-sm mt-1">'
|
||||
+ ' <span class="font-mono font-semibold">' + escapeHtml(ip) + '</span>'
|
||||
+ ' <span class="text-gray-500"> banned in </span>'
|
||||
+ ' <span class="font-semibold">' + escapeHtml(jail) + '</span>'
|
||||
+ ' </div>'
|
||||
+ ' <div class="text-xs text-gray-400 mt-1">'
|
||||
+ ' ' + escapeHtml(server) + ' • ' + escapeHtml(country)
|
||||
+ ' </div>'
|
||||
+ ' </div>'
|
||||
+ '</div>';
|
||||
|
||||
// Add click handler to scroll to ban events table
|
||||
toast.addEventListener('click', function() {
|
||||
var logSection = document.getElementById('logOverviewSection');
|
||||
if (logSection) {
|
||||
logSection.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}
|
||||
});
|
||||
|
||||
toast.style.cursor = 'pointer';
|
||||
container.appendChild(toast);
|
||||
|
||||
requestAnimationFrame(function() {
|
||||
toast.classList.add('show');
|
||||
});
|
||||
|
||||
setTimeout(function() {
|
||||
toast.classList.remove('show');
|
||||
setTimeout(function() {
|
||||
toast.remove();
|
||||
}, 300);
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
// Escape HTML to prevent XSS
|
||||
function escapeHtml(value) {
|
||||
if (value === undefined || value === null) return '';
|
||||
|
||||
@@ -74,6 +74,10 @@ function fetchBanEventsData() {
|
||||
.then(function(res) { return res.json(); })
|
||||
.then(function(data) {
|
||||
latestBanEvents = data && data.events ? data.events : [];
|
||||
// Track the last event ID to prevent duplicates from WebSocket
|
||||
if (latestBanEvents.length > 0 && wsManager) {
|
||||
wsManager.lastBanEventId = latestBanEvents[0].id;
|
||||
}
|
||||
})
|
||||
.catch(function(err) {
|
||||
console.error('Error fetching ban events:', err);
|
||||
@@ -81,6 +85,72 @@ function fetchBanEventsData() {
|
||||
});
|
||||
}
|
||||
|
||||
// Add new ban event from WebSocket
|
||||
function addBanEventFromWebSocket(event) {
|
||||
// Check if event already exists (prevent duplicates)
|
||||
// Only check by ID if both events have IDs
|
||||
var exists = false;
|
||||
if (event.id) {
|
||||
exists = latestBanEvents.some(function(e) {
|
||||
return e.id === event.id;
|
||||
});
|
||||
} else {
|
||||
// If no ID, check by IP, jail, and occurredAt timestamp
|
||||
exists = latestBanEvents.some(function(e) {
|
||||
return e.ip === event.ip &&
|
||||
e.jail === event.jail &&
|
||||
e.occurredAt === event.occurredAt;
|
||||
});
|
||||
}
|
||||
|
||||
if (!exists) {
|
||||
console.log('Adding new ban event from WebSocket:', event);
|
||||
|
||||
// Prepend to the beginning of the array
|
||||
latestBanEvents.unshift(event);
|
||||
// Keep only the last 200 events
|
||||
if (latestBanEvents.length > 200) {
|
||||
latestBanEvents = latestBanEvents.slice(0, 200);
|
||||
}
|
||||
|
||||
// Show toast notification first
|
||||
if (typeof showBanEventToast === 'function') {
|
||||
showBanEventToast(event);
|
||||
}
|
||||
|
||||
// Refresh dashboard data (summary, stats, insights) and re-render
|
||||
refreshDashboardData();
|
||||
} else {
|
||||
console.log('Skipping duplicate ban event:', event);
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh dashboard data when new ban event arrives via WebSocket
|
||||
function refreshDashboardData() {
|
||||
// Refresh ban statistics and insights in the background
|
||||
// Also refresh summary if we have a server selected
|
||||
var enabledServers = serversCache.filter(function(s) { return s.enabled; });
|
||||
var summaryPromise;
|
||||
if (serversCache.length && enabledServers.length && currentServerId) {
|
||||
summaryPromise = fetchSummaryData();
|
||||
} else {
|
||||
summaryPromise = Promise.resolve();
|
||||
}
|
||||
|
||||
Promise.all([
|
||||
summaryPromise,
|
||||
fetchBanStatisticsData(),
|
||||
fetchBanInsightsData()
|
||||
]).then(function() {
|
||||
// Re-render the dashboard to show updated stats
|
||||
renderDashboard();
|
||||
}).catch(function(err) {
|
||||
console.error('Error refreshing dashboard data:', err);
|
||||
// Still re-render even if refresh fails
|
||||
renderDashboard();
|
||||
});
|
||||
}
|
||||
|
||||
function fetchBanInsightsData() {
|
||||
var sevenDaysAgo = new Date(Date.now() - (7 * 24 * 60 * 60 * 1000)).toISOString();
|
||||
var sinceQuery = '?since=' + encodeURIComponent(sevenDaysAgo);
|
||||
|
||||
102
pkg/web/static/js/header.js
Normal file
102
pkg/web/static/js/header.js
Normal file
@@ -0,0 +1,102 @@
|
||||
// Header components: Clock and Backend Status Indicator
|
||||
"use strict";
|
||||
|
||||
var clockInterval = null;
|
||||
var statusUpdateCallback = null;
|
||||
|
||||
// Initialize clock
|
||||
function initClock() {
|
||||
function updateClock() {
|
||||
var now = new Date();
|
||||
var hours = String(now.getHours()).padStart(2, '0');
|
||||
var minutes = String(now.getMinutes()).padStart(2, '0');
|
||||
var seconds = String(now.getSeconds()).padStart(2, '0');
|
||||
var timeString = hours + ':' + minutes + ':' + seconds;
|
||||
|
||||
var clockElement = document.getElementById('clockTime');
|
||||
if (clockElement) {
|
||||
clockElement.textContent = timeString;
|
||||
}
|
||||
}
|
||||
|
||||
// Update immediately
|
||||
updateClock();
|
||||
|
||||
// Update every second
|
||||
if (clockInterval) {
|
||||
clearInterval(clockInterval);
|
||||
}
|
||||
clockInterval = setInterval(updateClock, 1000);
|
||||
}
|
||||
|
||||
// Update status indicator
|
||||
function updateStatusIndicator(state, text) {
|
||||
var statusDot = document.getElementById('statusDot');
|
||||
var statusText = document.getElementById('statusText');
|
||||
|
||||
if (!statusDot || !statusText) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove all color classes
|
||||
statusDot.classList.remove('bg-green-500', 'bg-yellow-500', 'bg-red-500', 'bg-gray-400');
|
||||
|
||||
// Set color and text based on state
|
||||
switch (state) {
|
||||
case 'connected':
|
||||
statusDot.classList.add('bg-green-500');
|
||||
statusText.textContent = text || 'Connected';
|
||||
break;
|
||||
case 'connecting':
|
||||
case 'reconnecting':
|
||||
statusDot.classList.add('bg-yellow-500');
|
||||
statusText.textContent = text || 'Connecting...';
|
||||
break;
|
||||
case 'disconnected':
|
||||
case 'error':
|
||||
statusDot.classList.add('bg-red-500');
|
||||
statusText.textContent = text || 'Disconnected';
|
||||
break;
|
||||
default:
|
||||
statusDot.classList.add('bg-gray-400');
|
||||
statusText.textContent = text || 'Unknown';
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize status indicator
|
||||
function initStatusIndicator() {
|
||||
// Set initial state
|
||||
updateStatusIndicator('connecting', 'Connecting...');
|
||||
|
||||
// Register callback with WebSocket manager when available
|
||||
if (typeof wsManager !== 'undefined' && wsManager) {
|
||||
wsManager.onStatusChange(function(state, text) {
|
||||
updateStatusIndicator(state, text);
|
||||
});
|
||||
} else {
|
||||
// Wait for WebSocket manager to be available
|
||||
var checkInterval = setInterval(function() {
|
||||
if (typeof wsManager !== 'undefined' && wsManager) {
|
||||
wsManager.onStatusChange(function(state, text) {
|
||||
updateStatusIndicator(state, text);
|
||||
});
|
||||
clearInterval(checkInterval);
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize all header components
|
||||
function initHeader() {
|
||||
initClock();
|
||||
initStatusIndicator();
|
||||
}
|
||||
|
||||
// Cleanup on page unload
|
||||
if (typeof window !== 'undefined') {
|
||||
window.addEventListener('beforeunload', function() {
|
||||
if (clockInterval) {
|
||||
clearInterval(clockInterval);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -5,6 +5,38 @@ window.addEventListener('DOMContentLoaded', function() {
|
||||
showLoading(true);
|
||||
displayExternalIP();
|
||||
|
||||
// Initialize header components (clock and status indicator)
|
||||
if (typeof initHeader === 'function') {
|
||||
initHeader();
|
||||
}
|
||||
|
||||
// Initialize WebSocket connection and register ban event handler
|
||||
function registerBanEventHandler() {
|
||||
if (typeof wsManager !== 'undefined' && wsManager) {
|
||||
wsManager.onBanEvent(function(event) {
|
||||
if (typeof addBanEventFromWebSocket === 'function') {
|
||||
addBanEventFromWebSocket(event);
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!registerBanEventHandler()) {
|
||||
// Wait for WebSocket manager to be available
|
||||
var wsCheckInterval = setInterval(function() {
|
||||
if (registerBanEventHandler()) {
|
||||
clearInterval(wsCheckInterval);
|
||||
}
|
||||
}, 100);
|
||||
|
||||
// Stop checking after 5 seconds
|
||||
setTimeout(function() {
|
||||
clearInterval(wsCheckInterval);
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
// Check LOTR mode on page load to apply immediately
|
||||
fetch('/api/settings')
|
||||
.then(res => res.json())
|
||||
@@ -114,4 +146,3 @@ window.addEventListener('DOMContentLoaded', function() {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
205
pkg/web/static/js/websocket.js
Normal file
205
pkg/web/static/js/websocket.js
Normal file
@@ -0,0 +1,205 @@
|
||||
// WebSocket Manager for Fail2ban UI
|
||||
// Handles real-time communication with the backend
|
||||
|
||||
"use strict";
|
||||
|
||||
class WebSocketManager {
|
||||
constructor() {
|
||||
this.ws = null;
|
||||
this.reconnectAttempts = 0;
|
||||
this.maxReconnectAttempts = Infinity;
|
||||
this.reconnectDelay = 1000; // Start with 1 second
|
||||
this.maxReconnectDelay = 30000; // Max 30 seconds
|
||||
this.isConnecting = false;
|
||||
this.isConnected = false;
|
||||
this.lastBanEventId = null;
|
||||
this.statusCallbacks = [];
|
||||
this.banEventCallbacks = [];
|
||||
|
||||
// Get WebSocket URL
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const host = window.location.host;
|
||||
this.wsUrl = `${protocol}//${host}/api/ws`;
|
||||
|
||||
this.connect();
|
||||
}
|
||||
|
||||
connect() {
|
||||
if (this.isConnecting || (this.ws && this.ws.readyState === WebSocket.OPEN)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isConnecting = true;
|
||||
this.updateStatus('connecting', 'Connecting...');
|
||||
|
||||
try {
|
||||
this.ws = new WebSocket(this.wsUrl);
|
||||
|
||||
this.ws.onopen = () => {
|
||||
this.isConnecting = false;
|
||||
this.isConnected = true;
|
||||
this.reconnectAttempts = 0;
|
||||
this.reconnectDelay = 1000;
|
||||
this.updateStatus('connected', 'Connected');
|
||||
console.log('WebSocket connected');
|
||||
};
|
||||
|
||||
this.ws.onmessage = (event) => {
|
||||
try {
|
||||
const message = JSON.parse(event.data);
|
||||
this.handleMessage(message);
|
||||
} catch (err) {
|
||||
console.error('Error parsing WebSocket message:', err);
|
||||
}
|
||||
};
|
||||
|
||||
this.ws.onerror = (error) => {
|
||||
console.error('WebSocket error:', error);
|
||||
this.updateStatus('error', 'Connection error');
|
||||
};
|
||||
|
||||
this.ws.onclose = () => {
|
||||
this.isConnecting = false;
|
||||
this.isConnected = false;
|
||||
this.updateStatus('disconnected', 'Disconnected');
|
||||
console.log('WebSocket disconnected');
|
||||
|
||||
// Attempt to reconnect
|
||||
if (this.reconnectAttempts < this.maxReconnectAttempts) {
|
||||
this.scheduleReconnect();
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error creating WebSocket connection:', error);
|
||||
this.isConnecting = false;
|
||||
this.updateStatus('error', 'Connection failed');
|
||||
this.scheduleReconnect();
|
||||
}
|
||||
}
|
||||
|
||||
scheduleReconnect() {
|
||||
this.reconnectAttempts++;
|
||||
const delay = Math.min(this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1), this.maxReconnectDelay);
|
||||
|
||||
this.updateStatus('reconnecting', 'Reconnecting...');
|
||||
|
||||
setTimeout(() => {
|
||||
this.connect();
|
||||
}, delay);
|
||||
}
|
||||
|
||||
handleMessage(message) {
|
||||
switch (message.type) {
|
||||
case 'ban_event':
|
||||
this.handleBanEvent(message.data);
|
||||
break;
|
||||
case 'heartbeat':
|
||||
this.handleHeartbeat(message);
|
||||
break;
|
||||
default:
|
||||
console.log('Unknown message type:', message.type);
|
||||
}
|
||||
}
|
||||
|
||||
handleBanEvent(eventData) {
|
||||
// Check if we've already processed this event (prevent duplicates)
|
||||
// Only check if event has an ID and we have a lastBanEventId
|
||||
if (eventData.id && this.lastBanEventId !== null && eventData.id <= this.lastBanEventId) {
|
||||
console.log('Skipping duplicate ban event:', eventData.id);
|
||||
return;
|
||||
}
|
||||
|
||||
// Update lastBanEventId if event has an ID
|
||||
if (eventData.id) {
|
||||
if (this.lastBanEventId === null || eventData.id > this.lastBanEventId) {
|
||||
this.lastBanEventId = eventData.id;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Processing ban event:', eventData);
|
||||
|
||||
// Notify all registered callbacks
|
||||
this.banEventCallbacks.forEach(callback => {
|
||||
try {
|
||||
callback(eventData);
|
||||
} catch (err) {
|
||||
console.error('Error in ban event callback:', err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
handleHeartbeat(message) {
|
||||
// Update status to show backend is healthy
|
||||
if (this.isConnected) {
|
||||
this.updateStatus('connected', 'Connected');
|
||||
}
|
||||
}
|
||||
|
||||
updateStatus(state, text) {
|
||||
this.statusCallbacks.forEach(callback => {
|
||||
try {
|
||||
callback(state, text);
|
||||
} catch (err) {
|
||||
console.error('Error in status callback:', err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onStatusChange(callback) {
|
||||
this.statusCallbacks.push(callback);
|
||||
}
|
||||
|
||||
onBanEvent(callback) {
|
||||
this.banEventCallbacks.push(callback);
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
if (this.ws) {
|
||||
this.ws.close();
|
||||
this.ws = null;
|
||||
}
|
||||
this.isConnected = false;
|
||||
this.isConnecting = false;
|
||||
}
|
||||
|
||||
getConnectionState() {
|
||||
if (!this.ws) {
|
||||
return 'disconnected';
|
||||
}
|
||||
|
||||
switch (this.ws.readyState) {
|
||||
case WebSocket.CONNECTING:
|
||||
return 'connecting';
|
||||
case WebSocket.OPEN:
|
||||
return 'connected';
|
||||
case WebSocket.CLOSING:
|
||||
return 'disconnecting';
|
||||
case WebSocket.CLOSED:
|
||||
return 'disconnected';
|
||||
default:
|
||||
return 'unknown';
|
||||
}
|
||||
}
|
||||
|
||||
isHealthy() {
|
||||
return this.isConnected && this.ws && this.ws.readyState === WebSocket.OPEN;
|
||||
}
|
||||
}
|
||||
|
||||
// Create global instance - initialize immediately
|
||||
var wsManager = null;
|
||||
|
||||
// Initialize WebSocket manager
|
||||
if (typeof window !== 'undefined') {
|
||||
// Initialize immediately if DOM is already loaded, otherwise wait
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
if (!wsManager) {
|
||||
wsManager = new WebSocketManager();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
wsManager = new WebSocketManager();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user