diff --git a/pkg/web/static/js/dashboard.js b/pkg/web/static/js/dashboard.js
index a77a6c1..892cb7d 100644
--- a/pkg/web/static/js/dashboard.js
+++ b/pkg/web/static/js/dashboard.js
@@ -1,10 +1,13 @@
-// Dashboard rendering and data fetching functions for Fail2ban UI
"use strict";
+// Dashboard data fetching and rendering.
+
+// =========================================================================
+// Data Fetching
+// =========================================================================
function refreshData(options) {
options = options || {};
var enabledServers = serversCache.filter(function(s) { return s.enabled; });
-
var summaryPromise;
if (!serversCache.length || !enabledServers.length || !currentServerId) {
latestSummary = null;
@@ -13,11 +16,9 @@ function refreshData(options) {
} else {
summaryPromise = fetchSummaryData();
}
-
if (!options.silent) {
showLoading(true);
}
-
return Promise.all([
summaryPromise,
fetchBanStatisticsData(),
@@ -39,6 +40,18 @@ function refreshData(options) {
});
}
+function fetchBanStatisticsData() {
+ return fetch('/api/events/bans/stats')
+ .then(function(res) { return res.json(); })
+ .then(function(data) {
+ latestBanStats = data && data.counts ? data.counts : {};
+ })
+ .catch(function(err) {
+ console.error('Error fetching ban statistics:', err);
+ latestBanStats = latestBanStats || {};
+ });
+}
+
function fetchSummaryData() {
return fetch(withServerParam('/api/summary'))
.then(function(res) { return res.json(); })
@@ -60,39 +73,38 @@ function fetchSummaryData() {
});
}
-function fetchBanStatisticsData() {
- return fetch('/api/events/bans/stats')
+function fetchBanInsightsData() {
+ var sevenDaysAgo = new Date(Date.now() - (7 * 24 * 60 * 60 * 1000)).toISOString();
+ var sinceQuery = '?since=' + encodeURIComponent(sevenDaysAgo);
+ var globalPromise = fetch('/api/events/bans/insights' + sinceQuery)
.then(function(res) { return res.json(); })
.then(function(data) {
- latestBanStats = data && data.counts ? data.counts : {};
+ latestBanInsights = normalizeInsights(data);
})
.catch(function(err) {
- console.error('Error fetching ban statistics:', err);
- latestBanStats = latestBanStats || {};
+ console.error('Error fetching ban insights:', err);
+ if (!latestBanInsights) {
+ latestBanInsights = normalizeInsights(null);
+ }
});
-}
-
-// Builds query string for ban events API: limit, offset, search, country, serverId
-function buildBanEventsQuery(offset, append) {
- var params = [
- 'limit=' + BAN_EVENTS_PAGE_SIZE,
- 'offset=' + (append ? Math.min(latestBanEvents.length, BAN_EVENTS_MAX_LOADED) : 0)
- ];
- var search = (banEventsFilterText || '').trim();
- if (search) {
- params.push('search=' + encodeURIComponent(search));
- }
- var country = (banEventsFilterCountry || 'all').trim();
- if (country && country !== 'all') {
- params.push('country=' + encodeURIComponent(country));
- }
+ var serverPromise;
if (currentServerId) {
- params.push('serverId=' + encodeURIComponent(currentServerId));
+ serverPromise = fetch(withServerParam('/api/events/bans/insights' + sinceQuery))
+ .then(function(res) { return res.json(); })
+ .then(function(data) {
+ latestServerInsights = normalizeInsights(data);
+ })
+ .catch(function(err) {
+ console.error('Error fetching server-specific ban insights:', err);
+ latestServerInsights = null;
+ });
+ } else {
+ latestServerInsights = null;
+ serverPromise = Promise.resolve();
}
- return '/api/events/bans?' + params.join('&');
+ return Promise.all([globalPromise, serverPromise]);
}
-// options: { append: true } to load next page and append; otherwise fetches first page (reset).
function fetchBanEventsData(options) {
options = options || {};
var append = options.append === true;
@@ -128,381 +140,13 @@ function fetchBanEventsData(options) {
});
}
-// Add new ban or unban event from WebSocket (only when not searching; cap at BAN_EVENTS_MAX_LOADED)
-function addBanEventFromWebSocket(event) {
- var hasSearch = (banEventsFilterText || '').trim().length > 0;
- if (hasSearch) {
- // When user is searching, list is from API; don't prepend to avoid inconsistency
- if (typeof showBanEventToast === 'function') {
- showBanEventToast(event);
- }
- refreshDashboardData();
- return;
- }
- var exists = false;
- if (event.id) {
- exists = latestBanEvents.some(function(e) { return e.id === event.id; });
- } else {
- exists = latestBanEvents.some(function(e) {
- return e.ip === event.ip && e.jail === event.jail && e.eventType === event.eventType && e.occurredAt === event.occurredAt;
- });
- }
- if (!exists) {
- if (!event.eventType) {
- event.eventType = 'ban';
- }
- console.log('Adding new event from WebSocket:', event);
- latestBanEvents.unshift(event);
- if (latestBanEvents.length > BAN_EVENTS_MAX_LOADED) {
- latestBanEvents = latestBanEvents.slice(0, BAN_EVENTS_MAX_LOADED);
- }
- if (typeof showBanEventToast === 'function') {
- showBanEventToast(event);
- }
- refreshDashboardData();
- } else {
- console.log('Skipping duplicate 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);
- var globalPromise = fetch('/api/events/bans/insights' + sinceQuery)
- .then(function(res) { return res.json(); })
- .then(function(data) {
- latestBanInsights = normalizeInsights(data);
- })
- .catch(function(err) {
- console.error('Error fetching ban insights:', err);
- if (!latestBanInsights) {
- latestBanInsights = normalizeInsights(null);
- }
- });
-
- var serverPromise;
- if (currentServerId) {
- serverPromise = fetch(withServerParam('/api/events/bans/insights' + sinceQuery))
- .then(function(res) { return res.json(); })
- .then(function(data) {
- latestServerInsights = normalizeInsights(data);
- })
- .catch(function(err) {
- console.error('Error fetching server-specific ban insights:', err);
- latestServerInsights = null;
- });
- } else {
- latestServerInsights = null;
- serverPromise = Promise.resolve();
- }
-
- return Promise.all([globalPromise, serverPromise]);
-}
-
-function totalStoredBans() {
- if (latestBanInsights && latestBanInsights.totals && typeof latestBanInsights.totals.overall === 'number') {
- return latestBanInsights.totals.overall;
- }
- if (!latestBanStats) return 0;
- return Object.keys(latestBanStats).reduce(function(sum, key) {
- return sum + (latestBanStats[key] || 0);
- }, 0);
-}
-
-function totalBansToday() {
- if (latestBanInsights && latestBanInsights.totals && typeof latestBanInsights.totals.today === 'number') {
- return latestBanInsights.totals.today;
- }
- return 0;
-}
-
-function totalBansWeek() {
- if (latestBanInsights && latestBanInsights.totals && typeof latestBanInsights.totals.week === 'number') {
- return latestBanInsights.totals.week;
- }
- return 0;
-}
-
-function recurringIPsLastWeekCount() {
- var source = latestServerInsights || latestBanInsights;
- if (!source || !Array.isArray(source.recurring)) {
- return 0;
- }
- return source.recurring.length;
-}
-
-function getBanEventCountries() {
- var countries = {};
- latestBanEvents.forEach(function(event) {
- var country = (event.country || '').trim();
- var key = country.toLowerCase();
- if (!countries[key]) {
- countries[key] = country;
- }
- });
- var keys = Object.keys(countries);
- keys.sort();
- return keys.map(function(key) {
- return countries[key];
- });
-}
-
-// Debounced refetch of ban events from API (search/country) and re-render only the log overview (no full dashboard = no scroll jump)
-function scheduleBanEventsRefetch() {
- if (banEventsFilterDebounce) {
- clearTimeout(banEventsFilterDebounce);
- }
- banEventsFilterDebounce = setTimeout(function() {
- banEventsFilterDebounce = null;
- fetchBanEventsData().then(function() {
- renderLogOverviewSection();
- });
- }, 300);
-}
-
-function updateBanEventsSearch(value) {
- banEventsFilterText = value || '';
- scheduleBanEventsRefetch();
-}
-
-function updateBanEventsCountry(value) {
- banEventsFilterCountry = value || 'all';
- fetchBanEventsData().then(function() {
- renderLogOverviewSection();
- });
-}
-
-function loadMoreBanEvents() {
- if (latestBanEvents.length >= BAN_EVENTS_MAX_LOADED || !banEventsHasMore) {
- return;
- }
- fetchBanEventsData({ append: true }).then(function() {
- renderLogOverviewSection();
- });
-}
-
-function getRecurringIPMap() {
- var map = {};
- if (latestBanInsights && Array.isArray(latestBanInsights.recurring)) {
- latestBanInsights.recurring.forEach(function(stat) {
- if (stat && stat.ip) {
- map[stat.ip] = stat;
- }
- });
- }
- return map;
-}
-
-function renderBannedIPs(jailName, ips) {
- if (!ips || ips.length === 0) {
- return 'No banned IPs ';
- }
- 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 = '
No ban events recorded yet.
';
} else {
@@ -863,15 +531,12 @@ function renderLogOverviewContent() {
}
html += ' ';
}
-
html += '' + t('logs.overview.recent_count_label', 'Events shown') + ': ' + latestBanEvents.length + ' / ' + totalLabel + '
';
-
- html += ''
+ + ''
+ + '' + t('logs.overview.recent_count_label', 'Events shown') + ': ' + latestBanEvents.length + ' / ' + totalLabel + '
'
+ 'No output received.
';
} else {
@@ -234,6 +334,10 @@ function renderTestResults(output, filterPath) {
}
}
+// =========================================================================
+// Filter Section Init
+// =========================================================================
+
function showFilterSection() {
const testResultsEl = document.getElementById('testResults');
const filterContentTextarea = document.getElementById('filterContentTextarea');
@@ -267,7 +371,6 @@ function showFilterSection() {
}
if (editBtn) editBtn.classList.add('hidden');
updateFilterContentHints(false);
- // Add change listener to enable/disable delete button and load filter content
const filterSelect = document.getElementById('filterSelect');
const deleteBtn = document.getElementById('deleteFilterBtn');
if (!filterSelect.hasAttribute('data-listener-added')) {
@@ -290,111 +393,3 @@ function showFilterSection() {
});
}
}
-
-function openCreateFilterModal() {
- document.getElementById('newFilterName').value = '';
- document.getElementById('newFilterContent').value = '';
- openModal('createFilterModal');
-}
-
-function createFilter() {
- const filterName = document.getElementById('newFilterName').value.trim();
- const content = document.getElementById('newFilterContent').value.trim();
-
- if (!filterName) {
- showToast('Filter name is required', 'error');
- return;
- }
-
- showLoading(true);
- fetch(withServerParam('/api/filters'), {
- method: 'POST',
- headers: serverHeaders({ 'Content-Type': 'application/json' }),
- body: JSON.stringify({
- filterName: filterName,
- content: content
- })
- })
- .then(function(res) {
- if (!res.ok) {
- return res.json().then(function(data) {
- throw new Error(data.error || 'Server returned ' + res.status);
- });
- }
- return res.json();
- })
- .then(function(data) {
- if (data.error) {
- showToast('Error creating filter: ' + data.error, 'error');
- return;
- }
- closeModal('createFilterModal');
- showToast(data.message || 'Filter created successfully', 'success');
- // Reload filters
- loadFilters();
- })
- .catch(function(err) {
- console.error('Error creating filter:', err);
- showToast('Error creating filter: ' + (err.message || err), 'error');
- })
- .finally(function() {
- showLoading(false);
- });
-}
-
-function deleteFilter() {
- const filterName = document.getElementById('filterSelect').value;
- if (!filterName) {
- showToast('Please select a filter to delete', 'info');
- return;
- }
-
- if (!confirm('Are you sure you want to delete the filter "' + escapeHtml(filterName) + '"? This action cannot be undone.')) {
- return;
- }
-
- showLoading(true);
- fetch(withServerParam('/api/filters/' + encodeURIComponent(filterName)), {
- method: 'DELETE',
- headers: serverHeaders()
- })
- .then(function(res) {
- if (!res.ok) {
- return res.json().then(function(data) {
- throw new Error(data.error || 'Server returned ' + res.status);
- });
- }
- return res.json();
- })
- .then(function(data) {
- if (data.error) {
- showToast('Error deleting filter: ' + data.error, 'error');
- return;
- }
- showToast(data.message || 'Filter deleted successfully', 'success');
- // Reload filters
- loadFilters();
- // Clear test results
- document.getElementById('testResults').innerHTML = '';
- document.getElementById('testResults').classList.add('hidden');
- document.getElementById('logLinesTextarea').value = '';
- const filterContentTextarea = document.getElementById('filterContentTextarea');
- const editBtn = document.getElementById('editFilterContentBtn');
- if (filterContentTextarea) {
- filterContentTextarea.value = '';
- filterContentTextarea.readOnly = true;
- filterContentTextarea.classList.add('bg-gray-50');
- filterContentTextarea.classList.remove('bg-white');
- }
- if (editBtn) editBtn.classList.add('hidden');
- updateFilterContentHints(false);
- })
- .catch(function(err) {
- console.error('Error deleting filter:', err);
- showToast('Error deleting filter: ' + (err.message || err), 'error');
- })
- .finally(function() {
- showLoading(false);
- });
-}
-
diff --git a/pkg/web/static/js/jails.js b/pkg/web/static/js/jails.js
index 1c3ce70..dcf221d 100644
--- a/pkg/web/static/js/jails.js
+++ b/pkg/web/static/js/jails.js
@@ -1,133 +1,56 @@
// Jail management functions for Fail2ban UI
"use strict";
-function preventExtensionInterference(element) {
- if (!element) return;
- try {
- // Ensure control property exists to prevent "Cannot read properties of undefined" errors
- if (!element.control) {
- Object.defineProperty(element, 'control', {
- value: {
- type: element.type || 'textarea',
- name: element.name || 'filter-config-editor',
- form: null,
- autocomplete: 'off'
- },
- writable: false,
- enumerable: false,
- configurable: true
- });
- }
- // Prevent extensions from adding their own properties
- Object.seal(element.control);
- } catch (e) {
- // Silently ignore errors
+// =========================================================================
+// Jail creation
+// =========================================================================
+
+function createJail() {
+ const jailName = document.getElementById('newJailName').value.trim();
+ const content = document.getElementById('newJailContent').value.trim();
+
+ if (!jailName) {
+ showToast('Jail name is required', 'error');
+ return;
}
-}
-
-function openJailConfigModal(jailName) {
- currentJailForConfig = jailName;
- var filterTextArea = document.getElementById('filterConfigTextarea');
- var jailTextArea = document.getElementById('jailConfigTextarea');
- filterTextArea.value = '';
- jailTextArea.value = '';
-
- // Prevent browser extensions from interfering
- preventExtensionInterference(filterTextArea);
- preventExtensionInterference(jailTextArea);
-
- document.getElementById('modalJailName').textContent = jailName;
-
- // Hide test logpath section initially
- document.getElementById('testLogpathSection').classList.add('hidden');
- document.getElementById('logpathResults').classList.add('hidden');
-
showLoading(true);
- var url = '/api/jails/' + encodeURIComponent(jailName) + '/config';
- fetch(withServerParam(url), {
- headers: serverHeaders()
+ fetch(withServerParam('/api/jails'), {
+ method: 'POST',
+ headers: serverHeaders({ 'Content-Type': 'application/json' }),
+ body: JSON.stringify({
+ jailName: jailName,
+ content: content
+ })
})
- .then(function(res) { return res.json(); })
+ .then(function(res) {
+ if (!res.ok) {
+ return res.json().then(function(data) {
+ throw new Error(data.error || 'Server returned ' + res.status);
+ });
+ }
+ return res.json();
+ })
.then(function(data) {
if (data.error) {
- showToast("Error loading config: " + data.error, 'error');
+ showToast('Error creating jail: ' + data.error, 'error');
return;
}
- filterTextArea.value = data.filter || '';
- jailTextArea.value = data.jailConfig || '';
-
- // Display file paths if available
- var filterFilePathEl = document.getElementById('filterFilePath');
- var jailFilePathEl = document.getElementById('jailFilePath');
- if (filterFilePathEl && data.filterFilePath) {
- filterFilePathEl.textContent = data.filterFilePath;
- filterFilePathEl.style.display = 'block';
- } else if (filterFilePathEl) {
- filterFilePathEl.style.display = 'none';
- }
- if (jailFilePathEl && data.jailFilePath) {
- jailFilePathEl.textContent = data.jailFilePath;
- jailFilePathEl.style.display = 'block';
- } else if (jailFilePathEl) {
- jailFilePathEl.style.display = 'none';
- }
-
- // Check if logpath is set in jail config and show test button
- updateLogpathButtonVisibility();
-
- // Show hint for local servers
- var localServerHint = document.getElementById('localServerLogpathHint');
- if (localServerHint && currentServer && currentServer.type === 'local') {
- localServerHint.classList.remove('hidden');
- } else if (localServerHint) {
- localServerHint.classList.add('hidden');
- }
-
- // Add listener to update button visibility when jail config changes
- jailTextArea.addEventListener('input', updateLogpathButtonVisibility);
-
- // Prevent extension interference before opening modal
- preventExtensionInterference(filterTextArea);
- preventExtensionInterference(jailTextArea);
- openModal('jailConfigModal');
-
- // Setup syntax highlighting for both textareas after modal is visible
- setTimeout(function() {
- preventExtensionInterference(filterTextArea);
- preventExtensionInterference(jailTextArea);
- }, 200);
+ closeModal('createJailModal');
+ showToast(data.message || 'Jail created successfully', 'success');
+ openManageJailsModal();
})
.catch(function(err) {
- showToast("Error: " + err, 'error');
+ console.error('Error creating jail:', err);
+ showToast('Error creating jail: ' + (err.message || err), 'error');
})
.finally(function() {
showLoading(false);
});
}
-function updateLogpathButtonVisibility() {
- var jailTextArea = document.getElementById('jailConfigTextarea');
- var jailConfig = jailTextArea ? jailTextArea.value : '';
- var hasLogpath = /logpath\s*=/i.test(jailConfig);
- var testSection = document.getElementById('testLogpathSection');
- var localServerHint = document.getElementById('localServerLogpathHint');
-
- if (hasLogpath && testSection) {
- testSection.classList.remove('hidden');
- // Show hint for local servers
- if (localServerHint && currentServer && currentServer.type === 'local') {
- localServerHint.classList.remove('hidden');
- } else if (localServerHint) {
- localServerHint.classList.add('hidden');
- }
- } else if (testSection) {
- testSection.classList.add('hidden');
- document.getElementById('logpathResults').classList.add('hidden');
- if (localServerHint) {
- localServerHint.classList.add('hidden');
- }
- }
-}
+// =========================================================================
+// Jail configuration saving
+// =========================================================================
function saveJailConfig() {
if (!currentJailForConfig) return;
@@ -178,28 +101,195 @@ function saveJailConfig() {
});
}
-// Extract logpath from jail config text
-// Supports multiple logpaths in a single line (space-separated) or multiple lines
-// Fail2ban supports both formats:
-// logpath = /var/log/file1.log /var/log/file2.log
-// logpath = /var/log/file1.log
-// /var/log/file2.log
+function updateJailConfigFromFilter() {
+ const filterSelect = document.getElementById('newJailFilter');
+ const jailNameInput = document.getElementById('newJailName');
+ const contentTextarea = document.getElementById('newJailContent');
+
+ if (!filterSelect || !contentTextarea) return;
+ const selectedFilter = filterSelect.value;
+
+ if (!selectedFilter) {
+ return;
+ }
+ if (jailNameInput && !jailNameInput.value.trim()) {
+ jailNameInput.value = selectedFilter;
+ }
+
+ const jailName = (jailNameInput && jailNameInput.value.trim()) || selectedFilter;
+ const config = `[${jailName}]
+enabled = false
+filter = ${selectedFilter}
+logpath = /var/log/auth.log
+maxretry = 5
+bantime = 3600
+findtime = 600`;
+
+ contentTextarea.value = config;
+}
+
+// =========================================================================
+// Jail toggle enable/disable state of single jails
+// =========================================================================
+
+function saveManageJailsSingle(checkbox) {
+ const item = checkbox.closest('div.flex.items-center.justify-between');
+ if (!item) {
+ console.error('Could not find parent container for checkbox');
+ return;
+ }
+
+ const nameSpan = item.querySelector('span.text-sm.font-medium');
+ if (!nameSpan) {
+ console.error('Could not find jail name span');
+ return;
+ }
+
+ const jailName = nameSpan.textContent.trim();
+ if (!jailName) {
+ console.error('Jail name is empty');
+ return;
+ }
+
+ const isEnabled = checkbox.checked;
+ const updatedJails = {};
+ updatedJails[jailName] = isEnabled;
+
+ console.log('Saving jail state:', jailName, 'enabled:', isEnabled, 'payload:', updatedJails);
+
+ fetch(withServerParam('/api/jails/manage'), {
+ method: 'POST',
+ headers: serverHeaders({ 'Content-Type': 'application/json' }),
+ body: JSON.stringify(updatedJails),
+ })
+ .then(function(res) {
+ if (!res.ok) {
+ return res.json().then(function(data) {
+ throw new Error(data.error || 'Server returned ' + res.status);
+ });
+ }
+ return res.json();
+ })
+ .then(function(data) {
+ if (data.error) {
+ var errorMsg = data.error;
+ var toastType = 'error';
+ // If jails were auto-disabled, check if this jail was one of them
+ var wasAutoDisabled = data.autoDisabled && data.enabledJails && Array.isArray(data.enabledJails) && data.enabledJails.indexOf(jailName) !== -1;
+
+ if (wasAutoDisabled) {
+ checkbox.checked = false;
+ toastType = 'warning';
+ } else {
+ // Revert checkbox state if error occurs
+ checkbox.checked = !isEnabled;
+ }
+ showToast(errorMsg, toastType, wasAutoDisabled ? 15000 : undefined);
+
+ // Reload the jail list to reflect the actual state
+ return fetch(withServerParam('/api/jails/manage'), {
+ headers: serverHeaders()
+ }).then(function(res) { return res.json(); })
+ .then(function(data) {
+ if (data.jails && data.jails.length) {
+ const jail = data.jails.find(function(j) { return j.jailName === jailName; });
+ if (jail) {
+ checkbox.checked = jail.enabled;
+ }
+ }
+ loadServers().then(function() {
+ updateRestartBanner();
+ return refreshData({ silent: true });
+ });
+ });
+ }
+
+ if (data.warning) {
+ showToast(data.warning, 'warning');
+ }
+
+ console.log('Jail state saved successfully:', data);
+ showToast(data.message || ('Jail ' + jailName + ' ' + (isEnabled ? 'enabled' : 'disabled') + ' successfully'), 'success');
+ return fetch(withServerParam('/api/jails/manage'), {
+ headers: serverHeaders()
+ }).then(function(res) { return res.json(); })
+ .then(function(data) {
+ if (data.jails && data.jails.length) {
+ const jail = data.jails.find(function(j) { return j.jailName === jailName; });
+ if (jail) {
+ checkbox.checked = jail.enabled;
+ }
+ }
+ loadServers().then(function() {
+ updateRestartBanner();
+ return refreshData({ silent: true });
+ });
+ });
+ })
+ .catch(function(err) {
+ console.error('Error saving jail settings:', err);
+ showToast("Error saving jail settings: " + (err.message || err), 'error');
+ checkbox.checked = !isEnabled;
+ });
+}
+
+// =========================================================================
+// Jail deletion
+// =========================================================================
+
+function deleteJail(jailName) {
+ if (!confirm('Are you sure you want to delete the jail "' + escapeHtml(jailName) + '"? This action cannot be undone.')) {
+ return;
+ }
+ showLoading(true);
+ fetch(withServerParam('/api/jails/' + encodeURIComponent(jailName)), {
+ method: 'DELETE',
+ headers: serverHeaders()
+ })
+ .then(function(res) {
+ if (!res.ok) {
+ return res.json().then(function(data) {
+ throw new Error(data.error || 'Server returned ' + res.status);
+ });
+ }
+ return res.json();
+ })
+ .then(function(data) {
+ if (data.error) {
+ showToast('Error deleting jail: ' + data.error, 'error');
+ return;
+ }
+ showToast(data.message || 'Jail deleted successfully', 'success');
+ openManageJailsModal();
+ refreshData({ silent: true });
+ })
+ .catch(function(err) {
+ console.error('Error deleting jail:', err);
+ showToast('Error deleting jail: ' + (err.message || err), 'error');
+ })
+ .finally(function() {
+ showLoading(false);
+ });
+}
+
+// =========================================================================
+// Logpath Helpers
+// =========================================================================
+
+// Supported fail2ban logpath formats: space-separated / multi-line
function extractLogpathFromConfig(configText) {
if (!configText) return '';
-
var logpaths = [];
var lines = configText.split('\n');
var inLogpathLine = false;
var currentLogpath = '';
-
+
for (var i = 0; i < lines.length; i++) {
var line = lines[i].trim();
- // Skip comments
if (line.startsWith('#')) {
continue;
}
-
- // Check if this line starts with logpath =
+ // Check if the line starts with logpath =
var logpathMatch = line.match(/^logpath\s*=\s*(.+)$/i);
if (logpathMatch && logpathMatch[1]) {
// Trim whitespace and remove quotes if present
@@ -207,15 +297,11 @@ function extractLogpathFromConfig(configText) {
currentLogpath = currentLogpath.replace(/^["']|["']$/g, '');
inLogpathLine = true;
} else if (inLogpathLine) {
- // Continuation line (indented or starting with space)
- // Fail2ban allows continuation lines for logpath
+
if (line !== '' && !line.includes('=')) {
- // This is a continuation line, append to current logpath
currentLogpath += ' ' + line.trim();
} else {
- // End of logpath block, process current logpath
if (currentLogpath) {
- // Split by spaces to handle multiple logpaths in one line
var paths = currentLogpath.split(/\s+/).filter(function(p) { return p.length > 0; });
logpaths = logpaths.concat(paths);
currentLogpath = '';
@@ -223,7 +309,6 @@ function extractLogpathFromConfig(configText) {
inLogpathLine = false;
}
} else if (inLogpathLine && line === '') {
- // Empty line might end the logpath block
if (currentLogpath) {
var paths = currentLogpath.split(/\s+/).filter(function(p) { return p.length > 0; });
logpaths = logpaths.concat(paths);
@@ -232,35 +317,52 @@ function extractLogpathFromConfig(configText) {
inLogpathLine = false;
}
}
-
- // Process any remaining logpath
+
if (currentLogpath) {
var paths = currentLogpath.split(/\s+/).filter(function(p) { return p.length > 0; });
logpaths = logpaths.concat(paths);
}
-
- // Join multiple logpaths with newlines
return logpaths.join('\n');
}
+function updateLogpathButtonVisibility() {
+ var jailTextArea = document.getElementById('jailConfigTextarea');
+ var jailConfig = jailTextArea ? jailTextArea.value : '';
+ var hasLogpath = /logpath\s*=/i.test(jailConfig);
+ var testSection = document.getElementById('testLogpathSection');
+ var localServerHint = document.getElementById('localServerLogpathHint');
+
+ if (hasLogpath && testSection) {
+ testSection.classList.remove('hidden');
+ if (localServerHint && currentServer && currentServer.type === 'local') {
+ localServerHint.classList.remove('hidden');
+ } else if (localServerHint) {
+ localServerHint.classList.add('hidden');
+ }
+ } else if (testSection) {
+ testSection.classList.add('hidden');
+ document.getElementById('logpathResults').classList.add('hidden');
+ if (localServerHint) {
+ localServerHint.classList.add('hidden');
+ }
+ }
+}
+
function testLogpath() {
if (!currentJailForConfig) return;
-
- // Extract logpath from the textarea
+
var jailTextArea = document.getElementById('jailConfigTextarea');
var jailConfig = jailTextArea ? jailTextArea.value : '';
var logpath = extractLogpathFromConfig(jailConfig);
-
+
if (!logpath) {
showToast('No logpath found in jail configuration. Please add a logpath line (e.g., logpath = /var/log/example.log)', 'warning');
return;
}
-
var resultsDiv = document.getElementById('logpathResults');
resultsDiv.textContent = 'Testing logpath...';
resultsDiv.classList.remove('hidden');
resultsDiv.classList.remove('text-red-600', 'text-yellow-600');
-
showLoading(true);
var url = '/api/jails/' + encodeURIComponent(currentJailForConfig) + '/logpath/test';
fetch(withServerParam(url), {
@@ -274,49 +376,42 @@ function testLogpath() {
if (data.error) {
resultsDiv.textContent = 'Error: ' + data.error;
resultsDiv.classList.add('text-red-600');
- // Auto-scroll to results
setTimeout(function() {
resultsDiv.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}, 100);
return;
}
-
var originalLogpath = data.original_logpath || '';
var results = data.results || [];
var isLocalServer = data.is_local_server || false;
-
- // Build HTML output with visual indicators
var output = '';
-
+
if (results.length === 0) {
output = 'No logpath entries found.
';
resultsDiv.innerHTML = output;
resultsDiv.classList.add('text-yellow-600');
return;
}
-
- // Process each logpath result
+
results.forEach(function(result, idx) {
var logpath = result.logpath || '';
var resolvedPath = result.resolved_path || '';
var found = result.found || false;
var files = result.files || [];
var error = result.error || '';
-
+
if (idx > 0) {
output += '';
output += '
';
if (isLocalServer) {
@@ -348,11 +443,10 @@ function testLogpath() {
}
output += '
';
});
-
- // Set overall status color
+
var allFound = results.every(function(r) { return r.found; });
var anyFound = results.some(function(r) { return r.found; });
-
+
if (allFound) {
resultsDiv.classList.remove('text-red-600', 'text-yellow-600');
} else if (anyFound) {
@@ -362,10 +456,9 @@ function testLogpath() {
resultsDiv.classList.remove('text-yellow-600');
resultsDiv.classList.add('text-red-600');
}
-
+
resultsDiv.innerHTML = output;
-
- // Auto-scroll to results
+
setTimeout(function() {
resultsDiv.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}, 100);
@@ -374,356 +467,33 @@ function testLogpath() {
showLoading(false);
resultsDiv.textContent = 'Error: ' + err;
resultsDiv.classList.add('text-red-600');
- // Auto-scroll to results
setTimeout(function() {
resultsDiv.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}, 100);
});
}
-function openManageJailsModal() {
- if (!currentServerId) {
- showToast(t('servers.selector.none', 'Please add and select a Fail2ban server first.'), 'info');
- return;
- }
- showLoading(true);
- fetch(withServerParam('/api/jails/manage'), {
- headers: serverHeaders()
- })
- .then(res => res.json())
- .then(data => {
- if (!data.jails || !data.jails.length) {
- showToast("No jails found for this server.", 'info');
- return;
- }
+// =========================================================================
+// Extension interference workaround
+// =========================================================================
- const html = data.jails.map(jail => {
- const isEnabled = jail.enabled ? 'checked' : '';
- const escapedJailName = escapeHtml(jail.jailName);
- // Escape single quotes for JavaScript string
- const jsEscapedJailName = jail.jailName.replace(/'/g, "\\'");
- return ''
- + '
'
- + '
' + escapedJailName + ' '
- + '
'
- + '
'
- + escapeHtml(t('modal.filter_config_edit', 'Edit Filter / Jail'))
- + ' '
- + '
'
- + ' '
- + ' '
- + '
'
- + ' '
- + '
'
- + ' '
- + ' '
- + '
'
- + '
';
- }).join('');
-
- document.getElementById('jailsList').innerHTML = html;
-
- // Add auto-save on checkbox change with debouncing
- let saveTimeout;
- document.querySelectorAll('#jailsList input[type="checkbox"]').forEach(function(checkbox) {
- checkbox.addEventListener('change', function() {
- // Clear any pending save
- if (saveTimeout) {
- clearTimeout(saveTimeout);
- }
-
- // Debounce save by 300ms
- saveTimeout = setTimeout(function() {
- saveManageJailsSingle(checkbox);
- }, 300);
- });
+function preventExtensionInterference(element) {
+ if (!element) return;
+ try {
+ // Ensure control property exists to prevent "Cannot read properties of undefined" errors
+ if (!element.control) {
+ Object.defineProperty(element, 'control', {
+ value: {
+ type: element.type || 'textarea',
+ name: element.name || 'filter-config-editor',
+ form: null,
+ autocomplete: 'off'
+ },
+ writable: false,
+ enumerable: false,
+ configurable: true
});
-
- openModal('manageJailsModal');
- })
- .catch(err => showToast("Error fetching jails: " + err, 'error'))
- .finally(() => showLoading(false));
+ }
+ Object.seal(element.control);
+ } catch (e) { }
}
-
-function saveManageJailsSingle(checkbox) {
- // Find the parent container div
- const item = checkbox.closest('div.flex.items-center.justify-between');
- if (!item) {
- console.error('Could not find parent container for checkbox');
- return;
- }
-
- // Get jail name from the span - it's the first span with text-sm font-medium class
- const nameSpan = item.querySelector('span.text-sm.font-medium');
- if (!nameSpan) {
- console.error('Could not find jail name span');
- return;
- }
-
- const jailName = nameSpan.textContent.trim();
- if (!jailName) {
- console.error('Jail name is empty');
- return;
- }
-
- const isEnabled = checkbox.checked;
- const updatedJails = {};
- updatedJails[jailName] = isEnabled;
-
- console.log('Saving jail state:', jailName, 'enabled:', isEnabled, 'payload:', updatedJails);
-
- // Send updated state to the API endpoint /api/jails/manage.
- fetch(withServerParam('/api/jails/manage'), {
- method: 'POST',
- headers: serverHeaders({ 'Content-Type': 'application/json' }),
- body: JSON.stringify(updatedJails),
- })
- .then(function(res) {
- if (!res.ok) {
- return res.json().then(function(data) {
- throw new Error(data.error || 'Server returned ' + res.status);
- });
- }
- return res.json();
- })
- .then(function(data) {
- // Check if there was an error (including auto-disabled jails)
- if (data.error) {
- var errorMsg = data.error;
- var toastType = 'error';
-
- // If jails were auto-disabled, check if this jail was one of them
- var wasAutoDisabled = data.autoDisabled && data.enabledJails && Array.isArray(data.enabledJails) && data.enabledJails.indexOf(jailName) !== -1;
-
- if (wasAutoDisabled) {
- checkbox.checked = false;
- toastType = 'warning';
- } else {
- // Revert checkbox state on error
- checkbox.checked = !isEnabled;
- }
-
- showToast(errorMsg, toastType, wasAutoDisabled ? 15000 : undefined);
-
- // Still reload the jail list to reflect the actual state
- return fetch(withServerParam('/api/jails/manage'), {
- headers: serverHeaders()
- }).then(function(res) { return res.json(); })
- .then(function(data) {
- if (data.jails && data.jails.length) {
- // Update the checkbox state based on server response
- const jail = data.jails.find(function(j) { return j.jailName === jailName; });
- if (jail) {
- checkbox.checked = jail.enabled;
- }
- }
- loadServers().then(function() {
- updateRestartBanner();
- return refreshData({ silent: true });
- });
- });
- }
-
- // Check for warning (legacy support)
- if (data.warning) {
- showToast(data.warning, 'warning');
- }
-
- console.log('Jail state saved successfully:', data);
- // Show success toast
- showToast(data.message || ('Jail ' + jailName + ' ' + (isEnabled ? 'enabled' : 'disabled') + ' successfully'), 'success');
- // Reload the jail list to reflect the actual state
- return fetch(withServerParam('/api/jails/manage'), {
- headers: serverHeaders()
- }).then(function(res) { return res.json(); })
- .then(function(data) {
- if (data.jails && data.jails.length) {
- // Update the checkbox state based on server response
- const jail = data.jails.find(function(j) { return j.jailName === jailName; });
- if (jail) {
- checkbox.checked = jail.enabled;
- }
- }
- loadServers().then(function() {
- updateRestartBanner();
- return refreshData({ silent: true });
- });
- });
- })
- .catch(function(err) {
- console.error('Error saving jail settings:', err);
- showToast("Error saving jail settings: " + (err.message || err), 'error');
- // Revert checkbox state on error
- checkbox.checked = !isEnabled;
- });
-}
-
-function openCreateJailModal() {
- document.getElementById('newJailName').value = '';
- document.getElementById('newJailContent').value = '';
- const filterSelect = document.getElementById('newJailFilter');
- if (filterSelect) {
- filterSelect.value = '';
- }
-
- // Load filters into dropdown
- showLoading(true);
- fetch(withServerParam('/api/filters'), {
- headers: serverHeaders()
- })
- .then(res => res.json())
- .then(data => {
- if (filterSelect) {
- filterSelect.innerHTML = '
-- Select a filter -- ';
- if (data.filters && data.filters.length > 0) {
- data.filters.forEach(filter => {
- const opt = document.createElement('option');
- opt.value = filter;
- opt.textContent = filter;
- filterSelect.appendChild(opt);
- });
- }
- }
- openModal('createJailModal');
- })
- .catch(err => {
- console.error('Error loading filters:', err);
- openModal('createJailModal');
- })
- .finally(() => showLoading(false));
-}
-
-function updateJailConfigFromFilter() {
- const filterSelect = document.getElementById('newJailFilter');
- const jailNameInput = document.getElementById('newJailName');
- const contentTextarea = document.getElementById('newJailContent');
-
- if (!filterSelect || !contentTextarea) return;
-
- const selectedFilter = filterSelect.value;
-
- if (!selectedFilter) {
- return;
- }
-
- // Auto-fill jail name if empty
- if (jailNameInput && !jailNameInput.value.trim()) {
- jailNameInput.value = selectedFilter;
- }
-
- // Auto-populate jail config
- const jailName = (jailNameInput && jailNameInput.value.trim()) || selectedFilter;
- const config = `[${jailName}]
-enabled = false
-filter = ${selectedFilter}
-logpath = /var/log/auth.log
-maxretry = 5
-bantime = 3600
-findtime = 600`;
-
- contentTextarea.value = config;
-}
-
-function createJail() {
- const jailName = document.getElementById('newJailName').value.trim();
- const content = document.getElementById('newJailContent').value.trim();
-
- if (!jailName) {
- showToast('Jail name is required', 'error');
- return;
- }
-
- showLoading(true);
- fetch(withServerParam('/api/jails'), {
- method: 'POST',
- headers: serverHeaders({ 'Content-Type': 'application/json' }),
- body: JSON.stringify({
- jailName: jailName,
- content: content
- })
- })
- .then(function(res) {
- if (!res.ok) {
- return res.json().then(function(data) {
- throw new Error(data.error || 'Server returned ' + res.status);
- });
- }
- return res.json();
- })
- .then(function(data) {
- if (data.error) {
- showToast('Error creating jail: ' + data.error, 'error');
- return;
- }
- closeModal('createJailModal');
- showToast(data.message || 'Jail created successfully', 'success');
- // Reload the manage jails modal
- openManageJailsModal();
- })
- .catch(function(err) {
- console.error('Error creating jail:', err);
- showToast('Error creating jail: ' + (err.message || err), 'error');
- })
- .finally(function() {
- showLoading(false);
- });
-}
-
-function deleteJail(jailName) {
- if (!confirm('Are you sure you want to delete the jail "' + escapeHtml(jailName) + '"? This action cannot be undone.')) {
- return;
- }
-
- showLoading(true);
- fetch(withServerParam('/api/jails/' + encodeURIComponent(jailName)), {
- method: 'DELETE',
- headers: serverHeaders()
- })
- .then(function(res) {
- if (!res.ok) {
- return res.json().then(function(data) {
- throw new Error(data.error || 'Server returned ' + res.status);
- });
- }
- return res.json();
- })
- .then(function(data) {
- if (data.error) {
- showToast('Error deleting jail: ' + data.error, 'error');
- return;
- }
- showToast(data.message || 'Jail deleted successfully', 'success');
- // Reload the manage jails modal
- openManageJailsModal();
- // Refresh dashboard
- refreshData({ silent: true });
- })
- .catch(function(err) {
- console.error('Error deleting jail:', err);
- showToast('Error deleting jail: ' + (err.message || err), 'error');
- })
- .finally(function() {
- showLoading(false);
- });
-}
-
diff --git a/pkg/web/static/js/modals.js b/pkg/web/static/js/modals.js
index 0706681..52bf1e9 100644
--- a/pkg/web/static/js/modals.js
+++ b/pkg/web/static/js/modals.js
@@ -187,3 +187,232 @@ function openBanInsightsModal() {
}
openModal('banInsightsModal');
}
+
+// =========================================================================
+// Server Manager Modal
+// =========================================================================
+
+function openServerManager(serverId) {
+ showLoading(true);
+ loadServers()
+ .then(function() {
+ if (serverId) {
+ editServer(serverId);
+ } else {
+ resetServerForm();
+ }
+ renderServerManagerList();
+ openModal('serverManagerModal');
+ })
+ .finally(function() {
+ showLoading(false);
+ });
+}
+
+// =========================================================================
+// Manage Jails Modal
+// =========================================================================
+
+function openManageJailsModal() {
+ if (!currentServerId) {
+ showToast(t('servers.selector.none', 'Please add and select a Fail2ban server first.'), 'info');
+ return;
+ }
+ showLoading(true);
+ fetch(withServerParam('/api/jails/manage'), {
+ headers: serverHeaders()
+ })
+ .then(res => res.json())
+ .then(data => {
+ if (!data.jails || !data.jails.length) {
+ showToast("No jails found for this server.", 'info');
+ return;
+ }
+
+ const html = data.jails.map(jail => {
+ const isEnabled = jail.enabled ? 'checked' : '';
+ const escapedJailName = escapeHtml(jail.jailName);
+ const jsEscapedJailName = jail.jailName.replace(/'/g, "\\'");
+ return ''
+ + '
'
+ + '
' + escapedJailName + ' '
+ + '
'
+ + '
'
+ + escapeHtml(t('modal.filter_config_edit', 'Edit Filter / Jail'))
+ + ' '
+ + '
'
+ + ' '
+ + ' '
+ + '
'
+ + ' '
+ + '
'
+ + ' '
+ + ' '
+ + '
'
+ + '
';
+ }).join('');
+
+ document.getElementById('jailsList').innerHTML = html;
+
+ let saveTimeout;
+ document.querySelectorAll('#jailsList input[type="checkbox"]').forEach(function(checkbox) {
+ checkbox.addEventListener('change', function() {
+ if (saveTimeout) {
+ clearTimeout(saveTimeout);
+ }
+ saveTimeout = setTimeout(function() {
+ saveManageJailsSingle(checkbox);
+ }, 300);
+ });
+ });
+
+ openModal('manageJailsModal');
+ })
+ .catch(err => showToast("Error fetching jails: " + err, 'error'))
+ .finally(() => showLoading(false));
+}
+
+// =========================================================================
+// Create Jail Modal
+// =========================================================================
+
+function openCreateJailModal() {
+ document.getElementById('newJailName').value = '';
+ document.getElementById('newJailContent').value = '';
+ const filterSelect = document.getElementById('newJailFilter');
+ if (filterSelect) {
+ filterSelect.value = '';
+ }
+ showLoading(true);
+ fetch(withServerParam('/api/filters'), {
+ headers: serverHeaders()
+ })
+ .then(res => res.json())
+ .then(data => {
+ if (filterSelect) {
+ filterSelect.innerHTML = '
-- Select a filter -- ';
+ if (data.filters && data.filters.length > 0) {
+ data.filters.forEach(filter => {
+ const opt = document.createElement('option');
+ opt.value = filter;
+ opt.textContent = filter;
+ filterSelect.appendChild(opt);
+ });
+ }
+ }
+ openModal('createJailModal');
+ })
+ .catch(err => {
+ console.error('Error loading filters:', err);
+ openModal('createJailModal');
+ })
+ .finally(() => showLoading(false));
+}
+
+// =========================================================================
+// Create Filter Modal
+// =========================================================================
+
+function openCreateFilterModal() {
+ document.getElementById('newFilterName').value = '';
+ document.getElementById('newFilterContent').value = '';
+ openModal('createFilterModal');
+}
+
+// =========================================================================
+// Jail / Filter Config Editor Modal
+// =========================================================================
+
+function openJailConfigModal(jailName) {
+ currentJailForConfig = jailName;
+ var filterTextArea = document.getElementById('filterConfigTextarea');
+ var jailTextArea = document.getElementById('jailConfigTextarea');
+ filterTextArea.value = '';
+ jailTextArea.value = '';
+
+ // Prevent browser extensions from interfering
+ preventExtensionInterference(filterTextArea);
+ preventExtensionInterference(jailTextArea);
+
+ document.getElementById('modalJailName').textContent = jailName;
+ document.getElementById('testLogpathSection').classList.add('hidden');
+ document.getElementById('logpathResults').classList.add('hidden');
+
+ showLoading(true);
+ var url = '/api/jails/' + encodeURIComponent(jailName) + '/config';
+ fetch(withServerParam(url), {
+ headers: serverHeaders()
+ })
+ .then(function(res) { return res.json(); })
+ .then(function(data) {
+ if (data.error) {
+ showToast("Error loading config: " + data.error, 'error');
+ return;
+ }
+ filterTextArea.value = data.filter || '';
+ jailTextArea.value = data.jailConfig || '';
+
+ var filterFilePathEl = document.getElementById('filterFilePath');
+ var jailFilePathEl = document.getElementById('jailFilePath');
+ if (filterFilePathEl && data.filterFilePath) {
+ filterFilePathEl.textContent = data.filterFilePath;
+ filterFilePathEl.style.display = 'block';
+ } else if (filterFilePathEl) {
+ filterFilePathEl.style.display = 'none';
+ }
+ if (jailFilePathEl && data.jailFilePath) {
+ jailFilePathEl.textContent = data.jailFilePath;
+ jailFilePathEl.style.display = 'block';
+ } else if (jailFilePathEl) {
+ jailFilePathEl.style.display = 'none';
+ }
+
+ // Update logpath button visibility
+ updateLogpathButtonVisibility();
+
+ // Show hint for local servers
+ var localServerHint = document.getElementById('localServerLogpathHint');
+ if (localServerHint && currentServer && currentServer.type === 'local') {
+ localServerHint.classList.remove('hidden');
+ } else if (localServerHint) {
+ localServerHint.classList.add('hidden');
+ }
+
+ jailTextArea.addEventListener('input', updateLogpathButtonVisibility);
+
+ preventExtensionInterference(filterTextArea);
+ preventExtensionInterference(jailTextArea);
+ openModal('jailConfigModal');
+
+ setTimeout(function() {
+ preventExtensionInterference(filterTextArea);
+ preventExtensionInterference(jailTextArea);
+ }, 200);
+ })
+ .catch(function(err) {
+ showToast("Error: " + err, 'error');
+ })
+ .finally(function() {
+ showLoading(false);
+ });
+}
diff --git a/pkg/web/static/js/servers.js b/pkg/web/static/js/servers.js
index 4f755e2..528ae22 100644
--- a/pkg/web/static/js/servers.js
+++ b/pkg/web/static/js/servers.js
@@ -1,6 +1,10 @@
-// Server management functions for Fail2ban UI
+// Server management javascript functions for Fail2ban UI
"use strict";
+// =========================================================================
+// Server data loading
+// =========================================================================
+
function loadServers() {
return fetch('/api/servers')
.then(function(res) { return res.json(); })
@@ -35,6 +39,10 @@ function loadServers() {
});
}
+// =========================================================================
+// Views rendering
+// =========================================================================
+
function renderServerSelector() {
var container = document.getElementById('serverSelectorContainer');
if (!container) return;
@@ -104,38 +112,6 @@ function renderServerSubtitle() {
subtitle.textContent = parts.join(' • ');
}
-function setCurrentServer(serverId) {
- if (!serverId) {
- currentServerId = null;
- currentServer = null;
- } else {
- var next = serversCache.find(function(s) { return s.id === serverId && s.enabled; });
- currentServer = next || null;
- currentServerId = currentServer ? currentServer.id : null;
- }
- renderServerSelector();
- renderServerSubtitle();
- updateRestartBanner();
- refreshData();
-}
-
-function openServerManager(serverId) {
- showLoading(true);
- loadServers()
- .then(function() {
- if (serverId) {
- editServer(serverId);
- } else {
- resetServerForm();
- }
- renderServerManagerList();
- openModal('serverManagerModal');
- })
- .finally(function() {
- showLoading(false);
- });
-}
-
function renderServerManagerList() {
var list = document.getElementById('serverManagerList');
var emptyState = document.getElementById('serverManagerListEmpty');
@@ -189,7 +165,7 @@ function renderServerManagerList() {
+ '
Edit '
+ (server.isDefault ? '' : '
Set default ')
+ '
' + (server.enabled ? 'Disable' : 'Enable') + ' '
- + (server.enabled ? (server.type === 'local'
+ + (server.enabled ? (server.type === 'local'
? '
Reload Fail2ban '
: '
Restart Fail2ban ') : '')
+ '
Test connection '
@@ -202,7 +178,6 @@ function renderServerManagerList() {
list.innerHTML = html;
if (typeof updateTranslations === 'function') {
updateTranslations();
- // Set tooltip text for reload buttons after translations are updated
setTimeout(function() {
serversCache.forEach(function(server) {
if (server.enabled && server.type === 'local') {
@@ -217,6 +192,25 @@ function renderServerManagerList() {
}
}
+function setCurrentServer(serverId) {
+ if (!serverId) {
+ currentServerId = null;
+ currentServer = null;
+ } else {
+ var next = serversCache.find(function(s) { return s.id === serverId && s.enabled; });
+ currentServer = next || null;
+ currentServerId = currentServer ? currentServer.id : null;
+ }
+ renderServerSelector();
+ renderServerSubtitle();
+ updateRestartBanner();
+ refreshData();
+}
+
+// =========================================================================
+// Server manager form actions
+// =========================================================================
+
function resetServerForm() {
document.getElementById('serverId').value = '';
document.getElementById('serverName').value = '';
@@ -406,13 +400,11 @@ function populateSSHKeySelect(keys, selected) {
if (typeof updateTranslations === 'function') {
updateTranslations();
}
- // Sync readonly state of the path input
syncSSHKeyPathReadonly();
- // Attach change handler once
initSSHKeySelectHandler();
}
-// Toggle the SSH key path input between readonly (key selected) and editable (manual entry).
+// SSH key path input is readonly when a key is selected, editable for manual entry.
function syncSSHKeyPathReadonly() {
var select = document.getElementById('serverSSHKeySelect');
var input = document.getElementById('serverSSHKey');
@@ -426,7 +418,6 @@ function syncSSHKeyPathReadonly() {
}
}
-// Attach a change listener on the select dropdown (once).
var _sshKeySelectHandlerBound = false;
function initSSHKeySelectHandler() {
if (_sshKeySelectHandlerBound) return;
@@ -463,6 +454,10 @@ function loadSSHKeys() {
});
}
+// =========================================================================
+// Server Actions
+// =========================================================================
+
function setServerEnabled(serverId, enabled) {
var server = serversCache.find(function(s) { return s.id === serverId; });
if (!server) {
@@ -632,4 +627,3 @@ function restartFail2ban() {
if (!confirm("Keep in mind that while fail2ban is restarting, logs are not being parsed and no IP addresses are blocked. Restart fail2ban now? This will take some time.")) return;
restartFail2banServer(currentServerId);
}
-
diff --git a/pkg/web/static/js/settings.js b/pkg/web/static/js/settings.js
index 4ef3bad..e17a7f9 100644
--- a/pkg/web/static/js/settings.js
+++ b/pkg/web/static/js/settings.js
@@ -1,45 +1,9 @@
-// Settings page functions for Fail2ban UI
+// Settings page javascript logics for Fail2ban UI.
"use strict";
-// Handle GeoIP provider change
-function onGeoIPProviderChange(provider) {
- const dbPathContainer = document.getElementById('geoipDatabasePathContainer');
- if (dbPathContainer) {
- if (provider === 'maxmind') {
- dbPathContainer.style.display = 'block';
- } else {
- dbPathContainer.style.display = 'none';
- }
- }
-}
-
-// Update email fields state based on checkbox preferences
-function updateEmailFieldsState() {
- const emailAlertsForBans = document.getElementById('emailAlertsForBans').checked;
- const emailAlertsForUnbans = document.getElementById('emailAlertsForUnbans').checked;
- const emailEnabled = emailAlertsForBans || emailAlertsForUnbans;
-
- // Get all email-related fields
- const emailFields = [
- document.getElementById('destEmail'),
- document.getElementById('smtpHost'),
- document.getElementById('smtpPort'),
- document.getElementById('smtpUsername'),
- document.getElementById('smtpPassword'),
- document.getElementById('smtpFrom'),
- document.getElementById('smtpAuthMethod'),
- document.getElementById('smtpUseTLS'),
- document.getElementById('smtpInsecureSkipVerify'),
- document.getElementById('sendTestEmailBtn')
- ];
-
- // Enable/disable all email fields
- emailFields.forEach(field => {
- if (field) {
- field.disabled = !emailEnabled;
- }
- });
-}
+// =========================================================================
+// Load Settings
+// =========================================================================
function loadSettings() {
showLoading(true);
@@ -48,14 +12,12 @@ function loadSettings() {
.then(data => {
document.getElementById('languageSelect').value = data.language || 'en';
- // Handle PORT environment variable
const uiPortInput = document.getElementById('uiPort');
const portEnvHint = document.getElementById('portEnvHint');
const portEnvValue = document.getElementById('portEnvValue');
const portRestartHint = document.getElementById('portRestartHint');
if (data.portEnvSet) {
- // PORT env is set - make field readonly and show hint
uiPortInput.value = data.port || data.portFromEnv || 8080;
uiPortInput.readOnly = true;
uiPortInput.classList.add('bg-gray-100', 'cursor-not-allowed');
@@ -63,7 +25,6 @@ function loadSettings() {
portEnvHint.style.display = 'block';
portRestartHint.style.display = 'none';
} else {
- // PORT env not set - allow editing
uiPortInput.value = data.port || 8080;
uiPortInput.readOnly = false;
uiPortInput.classList.remove('bg-gray-100', 'cursor-not-allowed');
@@ -75,14 +36,12 @@ function loadSettings() {
const consoleOutputEl = document.getElementById('consoleOutput');
if (consoleOutputEl) {
consoleOutputEl.checked = data.consoleOutput || false;
- // Mark that console was enabled on load (settings were already saved)
if (typeof wasConsoleEnabledOnLoad !== 'undefined') {
wasConsoleEnabledOnLoad = consoleOutputEl.checked;
}
- toggleConsoleOutput(false); // false = not a user click, loading from saved settings
+ toggleConsoleOutput(false);
}
- // Set callback URL and add auto-update listener for port changes
const callbackURLInput = document.getElementById('callbackURL');
callbackURLInput.value = data.callbackUrl || '';
const callbackUrlEnvHint = document.getElementById('callbackUrlEnvHint');
@@ -90,7 +49,6 @@ function loadSettings() {
const callbackUrlDefaultHint = document.getElementById('callbackUrlDefaultHint');
if (data.callbackUrlEnvSet) {
- // CALLBACK_URL env is set - make field readonly and show hint
callbackURLInput.value = data.callbackUrlFromEnv || data.callbackUrl || '';
callbackURLInput.readOnly = true;
callbackURLInput.classList.add('bg-gray-100', 'cursor-not-allowed');
@@ -98,7 +56,6 @@ function loadSettings() {
callbackUrlEnvHint.style.display = 'block';
callbackUrlDefaultHint.style.display = 'none';
} else {
- // CALLBACK_URL env not set - allow editing
callbackURLInput.readOnly = false;
callbackURLInput.classList.remove('bg-gray-100', 'cursor-not-allowed');
callbackUrlEnvHint.style.display = 'none';
@@ -109,34 +66,27 @@ function loadSettings() {
const toggleLink = document.getElementById('toggleCallbackSecretLink');
if (callbackSecretInput) {
callbackSecretInput.value = data.callbackSecret || '';
- // Reset to password type when loading
if (callbackSecretInput.type === 'text') {
callbackSecretInput.type = 'password';
}
- // Update link text
if (toggleLink) {
toggleLink.textContent = 'show secret';
}
}
- // Auto-update callback URL when port changes (if using default localhost pattern)
+ // Syncs callback URL when port changes (only when using localhost)
function updateCallbackURLIfDefault() {
- if (data.callbackUrlEnvSet) return; // Skip auto-update when env is set
+ if (data.callbackUrlEnvSet) return;
const currentPort = parseInt(uiPortInput.value, 10) || 8080;
const currentCallbackURL = callbackURLInput.value.trim();
- // Check if callback URL matches default localhost pattern
const defaultPattern = /^http:\/\/127\.0\.0\.1:\d+$/;
if (currentCallbackURL === '' || defaultPattern.test(currentCallbackURL)) {
callbackURLInput.value = 'http://127.0.0.1:' + currentPort;
}
}
- // Add listener to port input to auto-update callback URL
uiPortInput.addEventListener('input', updateCallbackURLIfDefault);
-
document.getElementById('destEmail').value = data.destemail || '';
-
- // Load email alert preferences
document.getElementById('emailAlertsForBans').checked = data.emailAlertsForBans !== undefined ? data.emailAlertsForBans : true;
document.getElementById('emailAlertsForUnbans').checked = data.emailAlertsForUnbans !== undefined ? data.emailAlertsForUnbans : false;
updateEmailFieldsState();
@@ -156,10 +106,7 @@ function loadSettings() {
}
}
$('#alertCountries').trigger('change');
-
- // Check and apply LOTR theme
checkAndApplyLOTRTheme(data.alertCountries || []);
-
if (data.smtp) {
document.getElementById('smtpHost').value = data.smtp.host || '';
document.getElementById('smtpPort').value = data.smtp.port || 587;
@@ -174,7 +121,6 @@ function loadSettings() {
document.getElementById('bantimeIncrement').checked = data.bantimeIncrement || false;
document.getElementById('defaultJailEnable').checked = data.defaultJailEnable || false;
- // GeoIP settings
const geoipProvider = data.geoipProvider || 'builtin';
document.getElementById('geoipProvider').value = geoipProvider;
onGeoIPProviderChange(geoipProvider);
@@ -185,11 +131,9 @@ function loadSettings() {
document.getElementById('findTime').value = data.findtime || '';
document.getElementById('maxRetry').value = data.maxretry || '';
document.getElementById('defaultChain').value = data.chain || 'INPUT';
- // Load IgnoreIPs as array
const ignoreIPs = data.ignoreips || [];
renderIgnoreIPsTags(ignoreIPs);
- // Load banaction settings
document.getElementById('banaction').value = data.banaction || 'nftables-multiport';
document.getElementById('banactionAllports').value = data.banactionAllports || 'nftables-allports';
@@ -202,10 +146,13 @@ function loadSettings() {
.finally(() => showLoading(false));
}
+// =========================================================================
+// Save Settings
+// =========================================================================
+
function saveSettings(event) {
event.preventDefault();
- // Validate all fields before submitting
if (!validateAllSettings()) {
showToast('Please fix validation errors before saving', 'error');
return;
@@ -233,7 +180,6 @@ function saveSettings(event) {
const selectedCountries = Array.from(document.getElementById('alertCountries').selectedOptions).map(opt => opt.value);
- // Auto-update callback URL if using default localhost pattern and port changed
const callbackURLInput = document.getElementById('callbackURL');
let callbackUrl = callbackURLInput.value.trim();
const currentPort = parseInt(document.getElementById('uiPort').value, 10) || 8080;
@@ -283,8 +229,6 @@ function saveSettings(event) {
var selectedLang = $('#languageSelect').val();
loadTranslations(selectedLang);
console.log("Settings saved successfully. Restart needed? " + (data.restartNeeded || false));
-
- // Check and apply LOTR theme after saving
const selectedCountries = Array.from(document.getElementById('alertCountries').selectedOptions).map(opt => opt.value);
checkAndApplyLOTRTheme(selectedCountries.length > 0 ? selectedCountries : ["ALL"]);
@@ -302,6 +246,35 @@ function saveSettings(event) {
.finally(() => showLoading(false));
}
+// =========================================================================
+// Email Settings
+// =========================================================================
+
+function updateEmailFieldsState() {
+ const emailAlertsForBans = document.getElementById('emailAlertsForBans').checked;
+ const emailAlertsForUnbans = document.getElementById('emailAlertsForUnbans').checked;
+ const emailEnabled = emailAlertsForBans || emailAlertsForUnbans;
+
+ const emailFields = [
+ document.getElementById('destEmail'),
+ document.getElementById('smtpHost'),
+ document.getElementById('smtpPort'),
+ document.getElementById('smtpUsername'),
+ document.getElementById('smtpPassword'),
+ document.getElementById('smtpFrom'),
+ document.getElementById('smtpAuthMethod'),
+ document.getElementById('smtpUseTLS'),
+ document.getElementById('smtpInsecureSkipVerify'),
+ document.getElementById('sendTestEmailBtn')
+ ];
+
+ emailFields.forEach(field => {
+ if (field) {
+ field.disabled = !emailEnabled;
+ }
+ });
+}
+
function sendTestEmail() {
showLoading(true);
@@ -321,6 +294,10 @@ function sendTestEmail() {
.finally(() => showLoading(false));
}
+// =========================================================================
+// Advanced Actions
+// =========================================================================
+
function applyAdvancedActionsSettings(cfg) {
cfg = cfg || {};
const enabledEl = document.getElementById('advancedActionsEnabled');
@@ -405,6 +382,10 @@ function updateAdvancedIntegrationFields() {
document.getElementById('advancedOPNsenseFields').classList.toggle('hidden', selected !== 'opnsense');
}
+// =========================================================================
+// Permanent Block Log
+// =========================================================================
+
function loadPermanentBlockLog() {
fetch('/api/advanced-actions/blocks')
.then(res => res.json())
@@ -498,6 +479,10 @@ function refreshPermanentBlockLog() {
loadPermanentBlockLog();
}
+// =========================================================================
+// Advanced Test
+// =========================================================================
+
function openAdvancedTestModal() {
document.getElementById('advancedTestIP').value = '';
openModal('advancedTestModal');
@@ -520,9 +505,7 @@ function submitAdvancedTest(action) {
if (data.error) {
showToast('Advanced action failed: ' + data.error, 'error');
} else {
- // Check if this is an info message (e.g., IP already blocked)
- const toastType = data.info ? 'info' : 'success';
- showToast(data.message || 'Action completed', toastType);
+ showToast(data.message || 'Action completed', data.info ? 'info' : 'success');
loadPermanentBlockLog();
}
})
@@ -556,13 +539,15 @@ function advancedUnblockIP(ip, event) {
.catch(err => showToast('Failed to remove IP: ' + err, 'error'));
}
-// Initialize advanced integration select listener
+// =========================================================================
+// Misc
+// =========================================================================
+
const advancedIntegrationSelect = document.getElementById('advancedIntegrationSelect');
if (advancedIntegrationSelect) {
advancedIntegrationSelect.addEventListener('change', updateAdvancedIntegrationFields);
}
-// Toggle callback secret visibility
function toggleCallbackSecretVisibility() {
const input = document.getElementById('callbackSecret');
const link = document.getElementById('toggleCallbackSecretLink');
@@ -574,3 +559,13 @@ function toggleCallbackSecretVisibility() {
link.textContent = isPassword ? 'hide secret' : 'show secret';
}
+function onGeoIPProviderChange(provider) {
+ const dbPathContainer = document.getElementById('geoipDatabasePathContainer');
+ if (dbPathContainer) {
+ if (provider === 'maxmind') {
+ dbPathContainer.style.display = 'block';
+ } else {
+ dbPathContainer.style.display = 'none';
+ }
+ }
+}