|
|
|
@@ -4492,6 +4492,9 @@ function updateStatsFromCpuMonitoringEvent(monitoringData) {
|
|
|
|
|
if (monitoringData.process_id !== undefined) {
|
|
|
|
|
updateStatsCell('process-id', monitoringData.process_id.toString());
|
|
|
|
|
}
|
|
|
|
|
if (monitoringData.active_connections !== undefined) {
|
|
|
|
|
updateStatsCell('websocket-connections', monitoringData.active_connections.toString());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (monitoringData.memory_usage_mb !== undefined) {
|
|
|
|
|
updateStatsCell('memory-usage', monitoringData.memory_usage_mb.toFixed(1) + ' MB');
|
|
|
|
@@ -5568,6 +5571,12 @@ function handleSqlQueryResponse(response) {
|
|
|
|
|
console.log('=== HANDLING SQL QUERY RESPONSE ===');
|
|
|
|
|
console.log('Response:', response);
|
|
|
|
|
|
|
|
|
|
// Route IP bans queries to the IP bans handler
|
|
|
|
|
if (response.query && response.query.includes('ip_bans')) {
|
|
|
|
|
handleIpBansResponse(response);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Always display SQL query results when received
|
|
|
|
|
displaySqlQueryResults(response);
|
|
|
|
|
|
|
|
|
@@ -6196,10 +6205,11 @@ function initializeRelayEvents() {
|
|
|
|
|
// ================================
|
|
|
|
|
|
|
|
|
|
let ipBansFilter = 'all'; // 'all', 'banned', 'expired'
|
|
|
|
|
let ipBansCachedResults = null; // Cache last fetched results for client-side filtering
|
|
|
|
|
|
|
|
|
|
// Load IP bans from database
|
|
|
|
|
async function loadIpBans() {
|
|
|
|
|
const query = "SELECT ip, failure_count, ban_count, banned_until, first_failure, has_authed_successfully, last_success_at, total_connections, total_failures, total_successes, first_seen FROM ip_bans ORDER BY banned_until DESC, total_failures DESC";
|
|
|
|
|
const query = "SELECT ip, failure_count, ban_count, banned_until, first_failure, has_authed_successfully, last_success_at, total_connections, total_failures, total_successes, first_seen, idle_failure_count, idle_ban_count, idle_banned_until FROM ip_bans ORDER BY MAX(banned_until, idle_banned_until) DESC, total_failures DESC";
|
|
|
|
|
await executeSqlQueryRaw(query, 'ip_bans_load');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -6220,7 +6230,22 @@ async function executeSqlQueryRaw(query, queryId) {
|
|
|
|
|
|
|
|
|
|
// Handle IP bans SQL response
|
|
|
|
|
function handleIpBansResponse(responseData) {
|
|
|
|
|
if (!responseData.results || responseData.results.length === 0) {
|
|
|
|
|
// Convert rows+columns format to array of objects
|
|
|
|
|
let results = [];
|
|
|
|
|
if (responseData._cachedResults) {
|
|
|
|
|
results = responseData._cachedResults;
|
|
|
|
|
} else if (responseData.rows && responseData.columns) {
|
|
|
|
|
const cols = responseData.columns;
|
|
|
|
|
results = responseData.rows.map(row => {
|
|
|
|
|
const obj = {};
|
|
|
|
|
cols.forEach((col, i) => { obj[col] = row[i]; });
|
|
|
|
|
return obj;
|
|
|
|
|
});
|
|
|
|
|
} else if (responseData.results) {
|
|
|
|
|
results = responseData.results;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (results.length === 0) {
|
|
|
|
|
document.getElementById('ip-bans-tbody').innerHTML = '<tr><td colspan="7" style="text-align: center;">No IP bans found</td></tr>';
|
|
|
|
|
document.getElementById('ip-bans-total').textContent = '0';
|
|
|
|
|
document.getElementById('ip-bans-active').textContent = '0';
|
|
|
|
@@ -6229,24 +6254,26 @@ function handleIpBansResponse(responseData) {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const now = Math.floor(Date.now() / 1000);
|
|
|
|
|
let totalIPs = responseData.results.length;
|
|
|
|
|
let totalIPs = results.length;
|
|
|
|
|
let currentlyBanned = 0;
|
|
|
|
|
let totalBans = 0;
|
|
|
|
|
|
|
|
|
|
// Helper: is this row currently banned (auth or idle)
|
|
|
|
|
const isRowBanned = row => (row.banned_until > now) || (row.idle_banned_until > now);
|
|
|
|
|
const isRowExpired = row => !isRowBanned(row) && ((row.ban_count > 0) || (row.idle_ban_count > 0));
|
|
|
|
|
|
|
|
|
|
// Filter results based on current filter
|
|
|
|
|
let filteredResults = responseData.results;
|
|
|
|
|
let filteredResults = results;
|
|
|
|
|
if (ipBansFilter === 'banned') {
|
|
|
|
|
filteredResults = responseData.results.filter(row => row.banned_until > now);
|
|
|
|
|
filteredResults = results.filter(isRowBanned);
|
|
|
|
|
} else if (ipBansFilter === 'expired') {
|
|
|
|
|
filteredResults = responseData.results.filter(row => row.banned_until <= now && row.banned_until > 0);
|
|
|
|
|
filteredResults = results.filter(isRowExpired);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Calculate stats
|
|
|
|
|
responseData.results.forEach(row => {
|
|
|
|
|
totalBans += parseInt(row.ban_count || 0);
|
|
|
|
|
if (row.banned_until > now) {
|
|
|
|
|
currentlyBanned++;
|
|
|
|
|
}
|
|
|
|
|
// Calculate stats (include both auth and idle bans)
|
|
|
|
|
results.forEach(row => {
|
|
|
|
|
totalBans += parseInt(row.ban_count || 0) + parseInt(row.idle_ban_count || 0);
|
|
|
|
|
if (isRowBanned(row)) currentlyBanned++;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Update stats
|
|
|
|
@@ -6254,25 +6281,44 @@ function handleIpBansResponse(responseData) {
|
|
|
|
|
document.getElementById('ip-bans-active').textContent = currentlyBanned;
|
|
|
|
|
document.getElementById('ip-bans-issued').textContent = totalBans;
|
|
|
|
|
|
|
|
|
|
// Cache results for client-side filtering
|
|
|
|
|
ipBansCachedResults = results;
|
|
|
|
|
|
|
|
|
|
// Get whitelist for display
|
|
|
|
|
const whitelistStr = (currentConfig && currentConfig.idle_ban_whitelist) || '';
|
|
|
|
|
const whitelistedIPs = new Set(whitelistStr.split(',').map(s => s.trim()).filter(s => s.length > 0));
|
|
|
|
|
|
|
|
|
|
// Render table
|
|
|
|
|
const tbody = document.getElementById('ip-bans-tbody');
|
|
|
|
|
tbody.innerHTML = filteredResults.map(row => {
|
|
|
|
|
const isBanned = row.banned_until > now;
|
|
|
|
|
const status = isBanned ? '🔴 Banned' : (row.ban_count > 0 ? '🟡 Expired' : '🟢 Clean');
|
|
|
|
|
const bannedUntil = row.banned_until > 0 ? new Date(row.banned_until * 1000).toLocaleString() : '-';
|
|
|
|
|
const isBanned = isRowBanned(row);
|
|
|
|
|
const isIdleBanned = row.idle_banned_until > now;
|
|
|
|
|
const isAuthBanned = row.banned_until > now;
|
|
|
|
|
const isWhitelisted = whitelistedIPs.has(row.ip);
|
|
|
|
|
let statusLabel;
|
|
|
|
|
if (isWhitelisted) statusLabel = '⭐ Whitelisted';
|
|
|
|
|
else if (isAuthBanned && isIdleBanned) statusLabel = '🔴 Banned (auth+idle)';
|
|
|
|
|
else if (isAuthBanned) statusLabel = '🔴 Banned (auth)';
|
|
|
|
|
else if (isIdleBanned) statusLabel = '🔴 Banned (idle)';
|
|
|
|
|
else if ((row.ban_count > 0) || (row.idle_ban_count > 0)) statusLabel = '🟡 Expired';
|
|
|
|
|
else statusLabel = '🟢 Clean';
|
|
|
|
|
|
|
|
|
|
// Show the later of the two ban expiry times
|
|
|
|
|
const effectiveBannedUntil = Math.max(row.banned_until || 0, row.idle_banned_until || 0);
|
|
|
|
|
const bannedUntil = effectiveBannedUntil > 0 ? new Date(effectiveBannedUntil * 1000).toLocaleString() : '-';
|
|
|
|
|
const failures = row.total_failures || 0;
|
|
|
|
|
const authedSuccessfully = row.has_authed_successfully ? '✅ Yes' : '❌ No';
|
|
|
|
|
const connectionAttempts = row.total_connections || 0;
|
|
|
|
|
|
|
|
|
|
return `<tr>
|
|
|
|
|
<td>${escapeHtml(row.ip)}</td>
|
|
|
|
|
<td>${status}</td>
|
|
|
|
|
<td>${statusLabel}</td>
|
|
|
|
|
<td>${bannedUntil}</td>
|
|
|
|
|
<td>${failures}</td>
|
|
|
|
|
<td>${authedSuccessfully}</td>
|
|
|
|
|
<td>${connectionAttempts}</td>
|
|
|
|
|
<td>
|
|
|
|
|
${isBanned ? `<button type="button" onclick="unbanIp('${escapeHtml(row.ip)}')">Unban</button>` : ''}
|
|
|
|
|
${isBanned && !isWhitelisted ? `<button type="button" onclick="unbanIp('${escapeHtml(row.ip)}')">Unban</button>` : ''}
|
|
|
|
|
<button type="button" onclick="deleteIpBan('${escapeHtml(row.ip)}')">Delete</button>
|
|
|
|
|
</td>
|
|
|
|
|
</tr>`;
|
|
|
|
@@ -6337,7 +6383,7 @@ async function deleteIpBan(ip) {
|
|
|
|
|
setTimeout(() => loadIpBans(), 500);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Set filter and reload
|
|
|
|
|
// Set filter and re-render from cache (no re-fetch needed)
|
|
|
|
|
function setIpBansFilter(filter) {
|
|
|
|
|
ipBansFilter = filter;
|
|
|
|
|
|
|
|
|
@@ -6346,8 +6392,12 @@ function setIpBansFilter(filter) {
|
|
|
|
|
document.getElementById('ip-ban-filter-banned').classList.toggle('active', filter === 'banned');
|
|
|
|
|
document.getElementById('ip-ban-filter-expired').classList.toggle('active', filter === 'expired');
|
|
|
|
|
|
|
|
|
|
// Reload with new filter
|
|
|
|
|
loadIpBans();
|
|
|
|
|
// Re-render from cache if available, otherwise fetch
|
|
|
|
|
if (ipBansCachedResults !== null) {
|
|
|
|
|
handleIpBansResponse({ rows: null, columns: null, _cachedResults: ipBansCachedResults });
|
|
|
|
|
} else {
|
|
|
|
|
loadIpBans();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Escape HTML to prevent XSS
|
|
|
|
@@ -6358,6 +6408,63 @@ function escapeHtml(text) {
|
|
|
|
|
return div.innerHTML;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Add IP to whitelist
|
|
|
|
|
async function addToWhitelist() {
|
|
|
|
|
const input = document.getElementById('whitelist-ip-input');
|
|
|
|
|
const status = document.getElementById('whitelist-status');
|
|
|
|
|
const ip = input.value.trim();
|
|
|
|
|
if (!ip) { status.innerHTML = '<span style="color:red">Enter an IP address</span>'; return; }
|
|
|
|
|
|
|
|
|
|
// Get current whitelist from config
|
|
|
|
|
const current = (currentConfig && currentConfig.idle_ban_whitelist) || '';
|
|
|
|
|
const ips = current.split(',').map(s => s.trim()).filter(s => s.length > 0);
|
|
|
|
|
if (ips.includes(ip)) { status.innerHTML = '<span style="color:orange">Already whitelisted</span>'; return; }
|
|
|
|
|
ips.push(ip);
|
|
|
|
|
const newValue = ips.join(', ');
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await sendAdminCommand(['config_set', 'idle_ban_whitelist', newValue]);
|
|
|
|
|
status.innerHTML = `<span style="color:green">✅ ${ip} added to whitelist</span>`;
|
|
|
|
|
input.value = '';
|
|
|
|
|
updateWhitelistDisplay(newValue);
|
|
|
|
|
// Update local config cache
|
|
|
|
|
if (currentConfig) currentConfig.idle_ban_whitelist = newValue;
|
|
|
|
|
} catch (e) {
|
|
|
|
|
status.innerHTML = `<span style="color:red">Failed: ${e.message}</span>`;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Update whitelist display
|
|
|
|
|
function updateWhitelistDisplay(whitelistStr) {
|
|
|
|
|
const div = document.getElementById('whitelist-current');
|
|
|
|
|
if (!div) return;
|
|
|
|
|
const ips = (whitelistStr || '').split(',').map(s => s.trim()).filter(s => s.length > 0);
|
|
|
|
|
if (ips.length === 0) {
|
|
|
|
|
div.innerHTML = '<em>No IPs whitelisted</em>';
|
|
|
|
|
} else {
|
|
|
|
|
div.innerHTML = 'Current whitelist: ' + ips.map(ip =>
|
|
|
|
|
`<span style="background:var(--accent-color,#ff0000);color:white;padding:1px 6px;border-radius:3px;margin:2px;display:inline-block">
|
|
|
|
|
${escapeHtml(ip)}
|
|
|
|
|
<button onclick="removeFromWhitelist('${escapeHtml(ip)}')" style="background:none;border:none;color:white;cursor:pointer;padding:0 0 0 4px;font-size:11px">✕</button>
|
|
|
|
|
</span>`
|
|
|
|
|
).join('');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Remove IP from whitelist
|
|
|
|
|
async function removeFromWhitelist(ip) {
|
|
|
|
|
const current = (currentConfig && currentConfig.idle_ban_whitelist) || '';
|
|
|
|
|
const ips = current.split(',').map(s => s.trim()).filter(s => s.length > 0 && s !== ip);
|
|
|
|
|
const newValue = ips.join(', ');
|
|
|
|
|
try {
|
|
|
|
|
await sendAdminCommand(['config_set', 'idle_ban_whitelist', newValue]);
|
|
|
|
|
updateWhitelistDisplay(newValue);
|
|
|
|
|
if (currentConfig) currentConfig.idle_ban_whitelist = newValue;
|
|
|
|
|
} catch (e) {
|
|
|
|
|
log('Failed to remove from whitelist: ' + e.message, 'ERROR');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Initialize IP Bans event listeners
|
|
|
|
|
function initIpBansEventListeners() {
|
|
|
|
|
const addBanBtn = document.getElementById('add-ban-btn');
|
|
|
|
@@ -6365,13 +6472,14 @@ function initIpBansEventListeners() {
|
|
|
|
|
const filterAllBtn = document.getElementById('ip-ban-filter-all');
|
|
|
|
|
const filterBannedBtn = document.getElementById('ip-ban-filter-banned');
|
|
|
|
|
const filterExpiredBtn = document.getElementById('ip-ban-filter-expired');
|
|
|
|
|
const addWhitelistBtn = document.getElementById('add-whitelist-btn');
|
|
|
|
|
|
|
|
|
|
if (addBanBtn) {
|
|
|
|
|
addBanBtn.addEventListener('click', banIp);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (refreshBtn) {
|
|
|
|
|
refreshBtn.addEventListener('click', loadIpBans);
|
|
|
|
|
refreshBtn.addEventListener('click', () => { ipBansCachedResults = null; loadIpBans(); });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (filterAllBtn) {
|
|
|
|
@@ -6386,24 +6494,15 @@ function initIpBansEventListeners() {
|
|
|
|
|
filterExpiredBtn.addEventListener('click', () => setIpBansFilter('expired'));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (addWhitelistBtn) {
|
|
|
|
|
addWhitelistBtn.addEventListener('click', addToWhitelist);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Show current whitelist on page load
|
|
|
|
|
updateWhitelistDisplay(currentConfig && currentConfig.idle_ban_whitelist);
|
|
|
|
|
|
|
|
|
|
console.log('IP Bans event listeners initialized');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Handle SQL query responses for IP bans
|
|
|
|
|
const originalHandleSqlQueryResponse = handleSqlQueryResponse;
|
|
|
|
|
handleSqlQueryResponse = function(response) {
|
|
|
|
|
console.log('=== HANDLING SQL QUERY RESPONSE ===');
|
|
|
|
|
console.log('Response:', response);
|
|
|
|
|
|
|
|
|
|
// Check if this is an IP bans query
|
|
|
|
|
if (response.query_id && (response.query_id.startsWith('ip_ban') || response.query_id === 'ip_bans_load')) {
|
|
|
|
|
handleIpBansResponse(response);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Call original handler for other queries
|
|
|
|
|
return originalHandleSqlQueryResponse(response);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Initialize when DOM is ready
|
|
|
|
|
document.addEventListener('DOMContentLoaded', initIpBansEventListeners);
|