mirror of
https://github.com/swissmakers/fail2ban-ui.git
synced 2026-04-11 13:47:05 +02:00
Enhanced Filter Debug page with inline editing and improved SSH filter testing
This commit is contained in:
@@ -1982,6 +1982,31 @@ func ListFiltersHandler(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"filters": filters})
|
||||
}
|
||||
|
||||
func GetFilterContentHandler(c *gin.Context) {
|
||||
config.DebugLog("----------------------------")
|
||||
config.DebugLog("GetFilterContentHandler called (handlers.go)")
|
||||
filterName := c.Param("filter")
|
||||
conn, err := resolveConnector(c)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
content, filePath, err := conn.GetFilterConfig(c.Request.Context(), filterName)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get filter content: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Remove comments for display in Filter Debug page only
|
||||
content = fail2ban.RemoveComments(content)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"content": content,
|
||||
"filterPath": filePath,
|
||||
})
|
||||
}
|
||||
|
||||
func TestFilterHandler(c *gin.Context) {
|
||||
config.DebugLog("----------------------------")
|
||||
config.DebugLog("TestFilterHandler called (handlers.go)") // entry point
|
||||
@@ -1991,15 +2016,16 @@ func TestFilterHandler(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
var req struct {
|
||||
FilterName string `json:"filterName"`
|
||||
LogLines []string `json:"logLines"`
|
||||
FilterName string `json:"filterName"`
|
||||
LogLines []string `json:"logLines"`
|
||||
FilterContent string `json:"filterContent"` // Optional: if provided, use this instead of reading from file
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid JSON"})
|
||||
return
|
||||
}
|
||||
|
||||
output, filterPath, err := conn.TestFilter(c.Request.Context(), req.FilterName, req.LogLines)
|
||||
output, filterPath, err := conn.TestFilter(c.Request.Context(), req.FilterName, req.LogLines, req.FilterContent)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to test filter: " + err.Error()})
|
||||
return
|
||||
|
||||
@@ -62,6 +62,7 @@ func RegisterRoutes(r *gin.Engine, hub *Hub) {
|
||||
|
||||
// Filter debugger endpoints
|
||||
api.GET("/filters", ListFiltersHandler)
|
||||
api.GET("/filters/:filter/content", GetFilterContentHandler)
|
||||
api.POST("/filters/test", TestFilterHandler)
|
||||
api.POST("/filters", CreateFilterHandler)
|
||||
api.DELETE("/filters/:filter", DeleteFilterHandler)
|
||||
|
||||
@@ -43,9 +43,28 @@ function loadFilters() {
|
||||
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 {
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
@@ -54,9 +73,93 @@ function loadFilters() {
|
||||
.finally(() => showLoading(false));
|
||||
}
|
||||
|
||||
function loadFilterContent(filterName) {
|
||||
const filterContentTextarea = document.getElementById('filterContentTextarea');
|
||||
const editBtn = document.getElementById('editFilterContentBtn');
|
||||
if (!filterContentTextarea) return;
|
||||
|
||||
showLoading(true);
|
||||
fetch(withServerParam('/api/filters/' + encodeURIComponent(filterName) + '/content'), {
|
||||
headers: serverHeaders()
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.error) {
|
||||
showToast('Error loading filter content: ' + data.error, 'error');
|
||||
filterContentTextarea.value = '';
|
||||
filterContentTextarea.readOnly = true;
|
||||
if (editBtn) editBtn.classList.add('hidden');
|
||||
updateFilterContentHints(false);
|
||||
return;
|
||||
}
|
||||
filterContentTextarea.value = data.content || '';
|
||||
filterContentTextarea.readOnly = true; // Keep it readonly by default
|
||||
filterContentTextarea.classList.add('bg-gray-50');
|
||||
filterContentTextarea.classList.remove('bg-white');
|
||||
if (editBtn) editBtn.classList.remove('hidden');
|
||||
updateFilterContentHints(false);
|
||||
})
|
||||
.catch(err => {
|
||||
showToast('Error loading filter content: ' + err, 'error');
|
||||
filterContentTextarea.value = '';
|
||||
filterContentTextarea.readOnly = true;
|
||||
if (editBtn) editBtn.classList.add('hidden');
|
||||
updateFilterContentHints(false);
|
||||
})
|
||||
.finally(() => showLoading(false));
|
||||
}
|
||||
|
||||
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');
|
||||
if (editBtn) {
|
||||
editBtn.textContent = t('filter_debug.cancel_edit', 'Cancel');
|
||||
editBtn.classList.remove('bg-blue-600', 'hover:bg-blue-700');
|
||||
editBtn.classList.add('bg-gray-600', 'hover:bg-gray-700');
|
||||
}
|
||||
updateFilterContentHints(true);
|
||||
} else {
|
||||
// Make readonly
|
||||
filterContentTextarea.readOnly = true;
|
||||
filterContentTextarea.classList.add('bg-gray-50');
|
||||
filterContentTextarea.classList.remove('bg-white');
|
||||
if (editBtn) {
|
||||
editBtn.textContent = t('filter_debug.edit_filter', 'Edit');
|
||||
editBtn.classList.remove('bg-gray-600', 'hover:bg-gray-700');
|
||||
editBtn.classList.add('bg-blue-600', 'hover:bg-blue-700');
|
||||
}
|
||||
updateFilterContentHints(false);
|
||||
}
|
||||
}
|
||||
|
||||
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');
|
||||
} else {
|
||||
if (readonlyHint) readonlyHint.classList.remove('hidden');
|
||||
if (editableHint) editableHint.classList.add('hidden');
|
||||
}
|
||||
|
||||
if (typeof updateTranslations === 'function') {
|
||||
updateTranslations();
|
||||
}
|
||||
}
|
||||
|
||||
function testSelectedFilter() {
|
||||
const filterName = document.getElementById('filterSelect').value;
|
||||
const lines = document.getElementById('logLinesTextarea').value.split('\n').filter(line => line.trim() !== '');
|
||||
const filterContentTextarea = document.getElementById('filterContentTextarea');
|
||||
|
||||
if (!filterName) {
|
||||
showToast('Please select a filter.', 'info');
|
||||
@@ -74,13 +177,24 @@ function testSelectedFilter() {
|
||||
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' }),
|
||||
body: JSON.stringify({
|
||||
filterName: filterName,
|
||||
logLines: lines
|
||||
})
|
||||
body: JSON.stringify(requestBody)
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
@@ -122,6 +236,7 @@ function renderTestResults(output, filterPath) {
|
||||
|
||||
function showFilterSection() {
|
||||
const testResultsEl = document.getElementById('testResults');
|
||||
const filterContentTextarea = document.getElementById('filterContentTextarea');
|
||||
if (!currentServerId) {
|
||||
var notice = document.getElementById('filterNotice');
|
||||
if (notice) {
|
||||
@@ -130,6 +245,10 @@ function showFilterSection() {
|
||||
}
|
||||
document.getElementById('filterSelect').innerHTML = '';
|
||||
document.getElementById('logLinesTextarea').value = '';
|
||||
if (filterContentTextarea) {
|
||||
filterContentTextarea.value = '';
|
||||
filterContentTextarea.readOnly = true;
|
||||
}
|
||||
testResultsEl.innerHTML = '';
|
||||
testResultsEl.classList.add('hidden');
|
||||
document.getElementById('deleteFilterBtn').disabled = true;
|
||||
@@ -139,12 +258,37 @@ function showFilterSection() {
|
||||
testResultsEl.innerHTML = '';
|
||||
testResultsEl.classList.add('hidden');
|
||||
document.getElementById('logLinesTextarea').value = '';
|
||||
// Add change listener to enable/disable delete button
|
||||
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);
|
||||
// Add change listener to enable/disable delete button and load filter content
|
||||
const filterSelect = document.getElementById('filterSelect');
|
||||
const deleteBtn = document.getElementById('deleteFilterBtn');
|
||||
filterSelect.addEventListener('change', function() {
|
||||
deleteBtn.disabled = !filterSelect.value;
|
||||
});
|
||||
if (!filterSelect.hasAttribute('data-listener-added')) {
|
||||
filterSelect.setAttribute('data-listener-added', 'true');
|
||||
filterSelect.addEventListener('change', function() {
|
||||
deleteBtn.disabled = !filterSelect.value;
|
||||
if (filterSelect.value) {
|
||||
loadFilterContent(filterSelect.value);
|
||||
} else {
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function openCreateFilterModal() {
|
||||
@@ -234,6 +378,16 @@ function deleteFilter() {
|
||||
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);
|
||||
|
||||
@@ -163,6 +163,18 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Textarea for filter content (readonly by default, editable with Edit button) -->
|
||||
<div class="mb-4">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<label for="filterContentTextarea" class="block text-sm font-medium text-gray-700" data-i18n="filter_debug.filter_content">Filter Content</label>
|
||||
<button type="button" id="editFilterContentBtn" onclick="toggleFilterContentEdit()" class="text-sm px-3 py-1 bg-blue-600 text-white rounded hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 hidden" data-i18n="filter_debug.edit_filter">Edit</button>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 mb-2" data-i18n="filter_debug.filter_content_hint_readonly">Filter content is shown read-only. Click 'Edit' to modify for testing. Changes are temporary and not saved.</p>
|
||||
<p class="text-xs text-gray-500 mb-2 hidden" id="filterContentHintEditable" data-i18n="filter_debug.filter_content_hint">Edit the filter regex below for testing. Changes are temporary and not saved.</p>
|
||||
<textarea id="filterContentTextarea" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 h-40 font-mono text-sm bg-gray-50"
|
||||
placeholder="Filter content will appear here when a filter is selected..." readonly></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Textarea for log lines to test -->
|
||||
<div class="mb-4">
|
||||
<label for="logLinesTextarea" class="block text-sm font-medium text-gray-700 mb-2" data-i18n="filter_debug.log_lines">Log Lines</label>
|
||||
|
||||
Reference in New Issue
Block a user