Fix javascript files to include sections and cleanup the initial movment from one js to splited up ones

This commit is contained in:
2026-02-18 21:48:22 +01:00
parent 45f5907f7c
commit 2169b9862f
6 changed files with 1188 additions and 1223 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,58 @@
// Filter debug functions for Fail2ban UI
"use strict";
// =========================================================================
// Filter creation
// =========================================================================
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');
loadFilters();
})
.catch(function(err) {
console.error('Error creating filter:', err);
showToast('Error creating filter: ' + (err.message || err), 'error');
})
.finally(function() {
showLoading(false);
});
}
// =========================================================================
// Filter Loading
// =========================================================================
function loadFilters() {
showLoading(true);
fetch(withServerParam('/api/filters'), {
@@ -38,12 +90,10 @@ function loadFilters() {
opt.textContent = f;
select.appendChild(opt);
});
// Add change listener if not already added
if (!select.hasAttribute('data-listener-added')) {
select.setAttribute('data-listener-added', 'true');
select.addEventListener('change', function() {
if (deleteBtn) deleteBtn.disabled = !select.value;
// Load filter content when a filter is selected
if (select.value) {
loadFilterContent(select.value);
} else {
@@ -61,7 +111,6 @@ function loadFilters() {
});
}
if (deleteBtn) deleteBtn.disabled = !select.value;
// If a filter is already selected (e.g., first one by default), load its content
if (select.value) {
loadFilterContent(select.value);
}
@@ -93,7 +142,7 @@ function loadFilterContent(filterName) {
return;
}
filterContentTextarea.value = data.content || '';
filterContentTextarea.readOnly = true; // Keep it readonly by default
filterContentTextarea.readOnly = true;
filterContentTextarea.classList.add('bg-gray-50');
filterContentTextarea.classList.remove('bg-white');
if (editBtn) editBtn.classList.remove('hidden');
@@ -109,13 +158,15 @@ function loadFilterContent(filterName) {
.finally(() => showLoading(false));
}
// =========================================================================
// Filter Editing (on the filter section)
// =========================================================================
function toggleFilterContentEdit() {
const filterContentTextarea = document.getElementById('filterContentTextarea');
const editBtn = document.getElementById('editFilterContentBtn');
if (!filterContentTextarea) return;
if (filterContentTextarea.readOnly) {
// Make editable
filterContentTextarea.readOnly = false;
filterContentTextarea.classList.remove('bg-gray-50');
filterContentTextarea.classList.add('bg-white');
@@ -126,7 +177,6 @@ function toggleFilterContentEdit() {
}
updateFilterContentHints(true);
} else {
// Make readonly
filterContentTextarea.readOnly = true;
filterContentTextarea.classList.add('bg-gray-50');
filterContentTextarea.classList.remove('bg-white');
@@ -142,7 +192,7 @@ function toggleFilterContentEdit() {
function updateFilterContentHints(isEditable) {
const readonlyHint = document.querySelector('p[data-i18n="filter_debug.filter_content_hint_readonly"]');
const editableHint = document.getElementById('filterContentHintEditable');
if (isEditable) {
if (readonlyHint) readonlyHint.classList.add('hidden');
if (editableHint) editableHint.classList.remove('hidden');
@@ -150,12 +200,72 @@ function updateFilterContentHints(isEditable) {
if (readonlyHint) readonlyHint.classList.remove('hidden');
if (editableHint) editableHint.classList.add('hidden');
}
if (typeof updateTranslations === 'function') {
updateTranslations();
}
}
// =========================================================================
// Filter deletion
// =========================================================================
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');
loadFilters();
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);
});
}
// =========================================================================
// Filter Testing
// =========================================================================
function testSelectedFilter() {
const filterName = document.getElementById('filterSelect').value;
const lines = document.getElementById('logLinesTextarea').value.split('\n').filter(line => line.trim() !== '');
@@ -165,32 +275,24 @@ function testSelectedFilter() {
showToast('Please select a filter.', 'info');
return;
}
if (lines.length === 0) {
showToast('Please enter at least one log line to test.', 'info');
return;
}
// Hide results initially
const testResultsEl = document.getElementById('testResults');
testResultsEl.classList.add('hidden');
testResultsEl.innerHTML = '';
showLoading(true);
const requestBody = {
filterName: filterName,
logLines: lines
};
// Only include filter content if textarea is editable (not readonly)
// If readonly, test the original filter from server
if (filterContentTextarea && !filterContentTextarea.readOnly) {
const filterContent = filterContentTextarea.value.trim();
if (filterContent) {
requestBody.filterContent = filterContent;
}
}
fetch(withServerParam('/api/filters/test'), {
method: 'POST',
headers: serverHeaders({ 'Content-Type': 'application/json' }),
@@ -213,15 +315,13 @@ function testSelectedFilter() {
function renderTestResults(output, filterPath) {
const testResultsEl = document.getElementById('testResults');
let html = '<h5 class="text-lg font-medium text-white mb-4" data-i18n="filter_debug.test_results_title">Test Results</h5>';
// Show which filter file was used
if (filterPath) {
html += '<div class="mb-3 p-2 bg-gray-800 rounded text-sm">';
html += '<span class="text-gray-400">Used Filter (exact file):</span> ';
html += '<span class="text-yellow-300 font-mono">' + escapeHtml(filterPath) + '</span>';
html += '</div>';
}
if (!output || output.trim() === '') {
html += '<p class="text-gray-400" data-i18n="filter_debug.no_matches">No output received.</p>';
} 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);
});
}

View File

@@ -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 = '<div class="text-yellow-600">No logpath entries found.</div>';
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 += '<div class="my-4 border-t border-gray-300 pt-4"></div>';
}
output += '<div class="mb-3">';
output += '<div class="font-semibold text-gray-800 mb-1">Logpath ' + (idx + 1) + ':</div>';
output += '<div class="ml-4 text-sm text-gray-600 font-mono">' + escapeHtml(logpath) + '</div>';
if (resolvedPath && resolvedPath !== logpath) {
output += '<div class="ml-4 text-xs text-gray-500 mt-1">Resolved: <span class="font-mono">' + escapeHtml(resolvedPath) + '</span></div>';
}
output += '</div>';
// Test results
output += '<div class="ml-4 mb-2">';
output += '<div class="flex items-center gap-2">';
if (isLocalServer) {
@@ -348,11 +443,10 @@ function testLogpath() {
}
output += '</div>';
});
// 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 ''
+ '<div class="flex items-center justify-between gap-3 p-3 bg-gray-50">'
+ ' <span class="text-sm font-medium flex-1 text-gray-900">' + escapedJailName + '</span>'
+ ' <div class="flex items-center gap-3">'
+ ' <button'
+ ' type="button"'
+ ' onclick="openJailConfigModal(\'' + jsEscapedJailName + '\')"'
+ ' class="text-xs px-3 py-1.5 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors whitespace-nowrap"'
+ ' data-i18n="modal.filter_config_edit"'
+ ' title="' + escapeHtml(t('modal.filter_config_edit', 'Edit Filter / Jail')) + '"'
+ ' >'
+ escapeHtml(t('modal.filter_config_edit', 'Edit Filter / Jail'))
+ ' </button>'
+ ' <button'
+ ' type="button"'
+ ' onclick="deleteJail(\'' + jsEscapedJailName + '\')"'
+ ' class="text-xs px-3 py-1.5 bg-red-500 text-white rounded hover:bg-red-600 transition-colors whitespace-nowrap"'
+ ' title="' + escapeHtml(t('modal.delete_jail', 'Delete Jail')) + '"'
+ ' >'
+ ' <i class="fas fa-trash"></i>'
+ ' </button>'
+ ' <label class="inline-flex relative items-center cursor-pointer">'
+ ' <input'
+ ' type="checkbox"'
+ ' id="toggle-' + jail.jailName.replace(/[^a-zA-Z0-9]/g, '_') + '"'
+ ' class="sr-only peer"'
+ isEnabled
+ ' />'
+ ' <div'
+ ' class="w-11 h-6 bg-gray-200 rounded-full peer-focus:ring-4 peer-focus:ring-blue-300 peer-checked:bg-blue-600 transition-colors"'
+ ' ></div>'
+ ' <span'
+ ' class="absolute left-1 top-1/2 -translate-y-1/2 bg-white w-4 h-4 rounded-full transition-transform peer-checked:translate-x-5"'
+ ' ></span>'
+ ' </label>'
+ ' </div>'
+ '</div>';
}).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 = '<option value="">-- Select a filter --</option>';
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);
});
}

View File

@@ -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 ''
+ '<div class="flex items-center justify-between gap-3 p-3 bg-gray-50">'
+ ' <span class="text-sm font-medium flex-1 text-gray-900">' + escapedJailName + '</span>'
+ ' <div class="flex items-center gap-3">'
+ ' <button'
+ ' type="button"'
+ ' onclick="openJailConfigModal(\'' + jsEscapedJailName + '\')"'
+ ' class="text-xs px-3 py-1.5 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors whitespace-nowrap"'
+ ' data-i18n="modal.filter_config_edit"'
+ ' title="' + escapeHtml(t('modal.filter_config_edit', 'Edit Filter / Jail')) + '"'
+ ' >'
+ escapeHtml(t('modal.filter_config_edit', 'Edit Filter / Jail'))
+ ' </button>'
+ ' <button'
+ ' type="button"'
+ ' onclick="deleteJail(\'' + jsEscapedJailName + '\')"'
+ ' class="text-xs px-3 py-1.5 bg-red-500 text-white rounded hover:bg-red-600 transition-colors whitespace-nowrap"'
+ ' title="' + escapeHtml(t('modal.delete_jail', 'Delete Jail')) + '"'
+ ' >'
+ ' <i class="fas fa-trash"></i>'
+ ' </button>'
+ ' <label class="inline-flex relative items-center cursor-pointer">'
+ ' <input'
+ ' type="checkbox"'
+ ' id="toggle-' + jail.jailName.replace(/[^a-zA-Z0-9]/g, '_') + '"'
+ ' class="sr-only peer"'
+ isEnabled
+ ' />'
+ ' <div'
+ ' class="w-11 h-6 bg-gray-200 rounded-full peer-focus:ring-4 peer-focus:ring-blue-300 peer-checked:bg-blue-600 transition-colors"'
+ ' ></div>'
+ ' <span'
+ ' class="absolute left-1 top-1/2 -translate-y-1/2 bg-white w-4 h-4 rounded-full transition-transform peer-checked:translate-x-5"'
+ ' ></span>'
+ ' </label>'
+ ' </div>'
+ '</div>';
}).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 = '<option value="">-- Select a filter --</option>';
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);
});
}

View File

@@ -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() {
+ ' <button class="text-sm text-blue-600 hover:text-blue-800" onclick="editServer(\'' + escapeHtml(server.id) + '\')" data-i18n="servers.actions.edit">Edit</button>'
+ (server.isDefault ? '' : '<button class="text-sm text-blue-600 hover:text-blue-800" onclick="makeDefaultServer(\'' + escapeHtml(server.id) + '\')" data-i18n="servers.actions.set_default">Set default</button>')
+ ' <button class="text-sm text-blue-600 hover:text-blue-800" onclick="setServerEnabled(\'' + escapeHtml(server.id) + '\',' + (server.enabled ? 'false' : 'true') + ')" data-i18n="' + (server.enabled ? 'servers.actions.disable' : 'servers.actions.enable') + '">' + (server.enabled ? 'Disable' : 'Enable') + '</button>'
+ (server.enabled ? (server.type === 'local'
+ (server.enabled ? (server.type === 'local'
? '<button class="text-sm text-blue-600 hover:text-blue-800 relative group" onclick="restartFail2banServer(\'' + escapeHtml(server.id) + '\')" data-i18n="servers.actions.reload" title="" data-i18n-title="servers.actions.reload_tooltip">Reload Fail2ban</button>'
: '<button class="text-sm text-blue-600 hover:text-blue-800" onclick="restartFail2banServer(\'' + escapeHtml(server.id) + '\')" data-i18n="servers.actions.restart">Restart Fail2ban</button>') : '')
+ ' <button class="text-sm text-blue-600 hover:text-blue-800" onclick="testServerConnection(\'' + escapeHtml(server.id) + '\')" data-i18n="servers.actions.test">Test connection</button>'
@@ -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);
}

View File

@@ -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';
}
}
}