Fix loading wrong filter problem, implement creation and deletion of filters and jails, fix some css mismatches, update the handlers and routes

This commit is contained in:
2025-12-30 01:10:49 +01:00
parent b9d8f1b39a
commit 84a97eaa96
18 changed files with 1735 additions and 421 deletions

View File

@@ -1116,66 +1116,51 @@ func GetJailFilterConfigHandler(c *gin.Context) {
config.DebugLog("Connector resolved: %s", conn.Server().Name)
var filterCfg string
var filterFilePath string
var jailCfg string
var jailCfgLoaded bool
var jailFilePath string
var filterErr error
// First, try to load filter config using jail name
config.DebugLog("Loading filter config for jail: %s", jail)
filterCfg, filterErr = conn.GetFilterConfig(c.Request.Context(), jail)
if filterErr != nil {
config.DebugLog("Failed to load filter config with jail name, trying to find filter from jail config: %v", filterErr)
// Load jail config first to check for custom filter directive
var jailErr error
jailCfg, jailErr = conn.GetJailConfig(c.Request.Context(), jail)
if jailErr != nil {
config.DebugLog("Failed to load jail config: %v", jailErr)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to load filter config: " + filterErr.Error() + ". Also failed to load jail config: " + jailErr.Error()})
return
}
jailCfgLoaded = true
config.DebugLog("Jail config loaded, length: %d", len(jailCfg))
// Extract filter name from jail config
filterName := fail2ban.ExtractFilterFromJailConfig(jailCfg)
if filterName == "" {
config.DebugLog("No filter directive found in jail config")
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to load filter config: " + filterErr.Error() + ". No filter directive found in jail config."})
return
}
config.DebugLog("Found filter directive in jail config: %s, trying to load that filter", filterName)
// Try loading the filter specified in jail config
filterCfg, filterErr = conn.GetFilterConfig(c.Request.Context(), filterName)
if filterErr != nil {
config.DebugLog("Failed to load filter config for %s: %v", filterName, filterErr)
c.JSON(http.StatusInternalServerError, gin.H{
"error": fmt.Sprintf("Failed to load filter config. Tried '%s' (jail name) and '%s' (from jail config), both failed. Last error: %v", jail, filterName, filterErr),
})
return
}
config.DebugLog("Successfully loaded filter config for %s (from jail config directive)", filterName)
// Always load jail config first to determine which filter to load
config.DebugLog("Loading jail config for jail: %s", jail)
var jailErr error
jailCfg, jailFilePath, jailErr = conn.GetJailConfig(c.Request.Context(), jail)
if jailErr != nil {
config.DebugLog("Failed to load jail config: %v", jailErr)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to load jail config: " + jailErr.Error()})
return
}
config.DebugLog("Filter config loaded, length: %d", len(filterCfg))
config.DebugLog("Jail config loaded, length: %d, file: %s", len(jailCfg), jailFilePath)
// Load jail config if not already loaded
if !jailCfgLoaded {
config.DebugLog("Loading jail config for jail: %s", jail)
var jailErr error
jailCfg, jailErr = conn.GetJailConfig(c.Request.Context(), jail)
if jailErr != nil {
config.DebugLog("Failed to load jail config: %v", jailErr)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to load jail config: " + jailErr.Error()})
return
}
config.DebugLog("Jail config loaded, length: %d", len(jailCfg))
// Extract filter name from jail config, or use jail name as fallback
filterName := fail2ban.ExtractFilterFromJailConfig(jailCfg)
if filterName == "" {
// No filter directive found, use jail name as filter name (default behavior)
filterName = jail
config.DebugLog("No filter directive found in jail config, using jail name as filter name: %s", filterName)
} else {
config.DebugLog("Found filter directive in jail config: %s", filterName)
}
// Load filter config using the determined filter name
config.DebugLog("Loading filter config for filter: %s", filterName)
filterCfg, filterFilePath, filterErr = conn.GetFilterConfig(c.Request.Context(), filterName)
if filterErr != nil {
config.DebugLog("Failed to load filter config for %s: %v", filterName, filterErr)
// Don't fail completely - allow editing even if filter doesn't exist yet
config.DebugLog("Continuing without filter config (filter may not exist yet)")
filterCfg = ""
filterFilePath = ""
} else {
config.DebugLog("Filter config loaded, length: %d, file: %s", len(filterCfg), filterFilePath)
}
c.JSON(http.StatusOK, gin.H{
"jail": jail,
"filter": filterCfg,
"jailConfig": jailCfg,
"jail": jail,
"filter": filterCfg,
"filterFilePath": filterFilePath,
"jailConfig": jailCfg,
"jailFilePath": jailFilePath,
})
}
@@ -1219,15 +1204,47 @@ func SetJailFilterConfigHandler(c *gin.Context) {
config.DebugLog("Jail preview (first 100 chars): %s", req.Jail[:min(100, len(req.Jail))])
}
// Save filter config
// Save filter config - use original filter name, not the one from the new jail config
if req.Filter != "" {
config.DebugLog("Saving filter config for jail: %s", jail)
if err := conn.SetFilterConfig(c.Request.Context(), jail, req.Filter); err != nil {
// Load the original jail config to determine which filter was originally loaded
originalJailCfg, _, err := conn.GetJailConfig(c.Request.Context(), jail)
if err != nil {
config.DebugLog("Failed to load original jail config to determine filter name: %v", err)
// Fallback: extract from new jail config
originalJailCfg = req.Jail
}
// Extract the ORIGINAL filter name (the one that was loaded when the modal opened)
originalFilterName := fail2ban.ExtractFilterFromJailConfig(originalJailCfg)
if originalFilterName == "" {
// No filter directive found in original config, use jail name as filter name (default behavior)
originalFilterName = jail
config.DebugLog("No filter directive found in original jail config, using jail name as filter name: %s", originalFilterName)
} else {
config.DebugLog("Found original filter directive in jail config: %s", originalFilterName)
}
// Extract the NEW filter name from the updated jail config
newFilterName := fail2ban.ExtractFilterFromJailConfig(req.Jail)
if newFilterName == "" {
newFilterName = jail
}
// If the filter name changed, save to the ORIGINAL filter name (not the new one)
// This prevents overwriting a different filter with the old filter's content
if originalFilterName != newFilterName {
config.DebugLog("Filter name changed from %s to %s, saving filter to original name: %s", originalFilterName, newFilterName, originalFilterName)
} else {
config.DebugLog("Filter name unchanged: %s", originalFilterName)
}
config.DebugLog("Saving filter config for filter: %s", originalFilterName)
if err := conn.SetFilterConfig(c.Request.Context(), originalFilterName, req.Filter); err != nil {
config.DebugLog("Failed to save filter config: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save filter config: " + err.Error()})
return
}
config.DebugLog("Filter config saved successfully")
config.DebugLog("Filter config saved successfully to filter: %s", originalFilterName)
} else {
config.DebugLog("No filter config provided, skipping")
}
@@ -1307,7 +1324,7 @@ func TestLogpathHandler(c *gin.Context) {
config.DebugLog("Using logpath from request body: %s", originalLogpath)
} else {
// Fall back to reading from saved jail config
jailCfg, err := conn.GetJailConfig(c.Request.Context(), jail)
jailCfg, _, err := conn.GetJailConfig(c.Request.Context(), jail)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to load jail config: " + err.Error()})
return
@@ -1679,6 +1696,78 @@ func UpdateJailManagementHandler(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "Jail settings updated and fail2ban reloaded successfully"})
}
// CreateJailHandler creates a new jail.
func CreateJailHandler(c *gin.Context) {
config.DebugLog("----------------------------")
config.DebugLog("CreateJailHandler called (handlers.go)")
conn, err := resolveConnector(c)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
var req struct {
JailName string `json:"jailName" binding:"required"`
Content string `json:"content"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid JSON: " + err.Error()})
return
}
// Validate jail name
if err := fail2ban.ValidateJailName(req.JailName); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// If no content provided, create minimal jail config
if req.Content == "" {
req.Content = fmt.Sprintf("[%s]\nenabled = false\n", req.JailName)
}
// Create the jail
if err := conn.CreateJail(c.Request.Context(), req.JailName, req.Content); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create jail: " + err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": fmt.Sprintf("Jail '%s' created successfully", req.JailName)})
}
// DeleteJailHandler deletes a jail.
func DeleteJailHandler(c *gin.Context) {
config.DebugLog("----------------------------")
config.DebugLog("DeleteJailHandler called (handlers.go)")
conn, err := resolveConnector(c)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
jailName := c.Param("jail")
if jailName == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Jail name is required"})
return
}
// Validate jail name
if err := fail2ban.ValidateJailName(jailName); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Delete the jail
if err := conn.DeleteJail(c.Request.Context(), jailName); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete jail: " + err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": fmt.Sprintf("Jail '%s' deleted successfully", jailName)})
}
// GetSettingsHandler returns the entire AppSettings struct as JSON
func GetSettingsHandler(c *gin.Context) {
config.DebugLog("----------------------------")
@@ -1871,6 +1960,78 @@ func TestFilterHandler(c *gin.Context) {
})
}
// CreateFilterHandler creates a new filter.
func CreateFilterHandler(c *gin.Context) {
config.DebugLog("----------------------------")
config.DebugLog("CreateFilterHandler called (handlers.go)")
conn, err := resolveConnector(c)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
var req struct {
FilterName string `json:"filterName" binding:"required"`
Content string `json:"content"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid JSON: " + err.Error()})
return
}
// Validate filter name
if err := fail2ban.ValidateFilterName(req.FilterName); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// If no content provided, create empty filter
if req.Content == "" {
req.Content = fmt.Sprintf("# Filter: %s\n", req.FilterName)
}
// Create the filter
if err := conn.CreateFilter(c.Request.Context(), req.FilterName, req.Content); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create filter: " + err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": fmt.Sprintf("Filter '%s' created successfully", req.FilterName)})
}
// DeleteFilterHandler deletes a filter.
func DeleteFilterHandler(c *gin.Context) {
config.DebugLog("----------------------------")
config.DebugLog("DeleteFilterHandler called (handlers.go)")
conn, err := resolveConnector(c)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
filterName := c.Param("filter")
if filterName == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Filter name is required"})
return
}
// Validate filter name
if err := fail2ban.ValidateFilterName(filterName); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Delete the filter
if err := conn.DeleteFilter(c.Request.Context(), filterName); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete filter: " + err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": fmt.Sprintf("Filter '%s' deleted successfully", filterName)})
}
// ApplyFail2banSettings updates /etc/fail2ban/jail.local [DEFAULT] with our JSON
func ApplyFail2banSettings(jailLocalPath string) error {
config.DebugLog("----------------------------")

View File

@@ -41,6 +41,8 @@ func RegisterRoutes(r *gin.Engine, hub *Hub) {
// Routes for jail management
api.GET("/jails/manage", ManageJailsHandler)
api.POST("/jails/manage", UpdateJailManagementHandler)
api.POST("/jails", CreateJailHandler)
api.DELETE("/jails/:jail", DeleteJailHandler)
// Settings endpoints
api.GET("/settings", GetSettingsHandler)
@@ -60,9 +62,8 @@ func RegisterRoutes(r *gin.Engine, hub *Hub) {
// Filter debugger endpoints
api.GET("/filters", ListFiltersHandler)
api.POST("/filters/test", TestFilterHandler)
// TODO: create or generate new filters
// api.POST("/filters/generate", GenerateFilterHandler)
api.POST("/filters", CreateFilterHandler)
api.DELETE("/filters/:filter", DeleteFilterHandler)
// Restart endpoint
api.POST("/fail2ban/restart", RestartFail2banHandler)

View File

@@ -317,3 +317,106 @@ mark {
#advancedMikrotikFields, #advancedPfSenseFields {
padding: 10px;
}
/* Additional Tailwind color classes for buttons */
.bg-red-500 {
--tw-bg-opacity: 1;
background-color: rgb(239 68 68 / var(--tw-bg-opacity, 1));
}
.bg-red-600 {
--tw-bg-opacity: 1;
background-color: rgb(220 38 38 / var(--tw-bg-opacity, 1));
}
.hover\:bg-red-600:hover {
--tw-bg-opacity: 1;
background-color: rgb(220 38 38 / var(--tw-bg-opacity, 1));
}
.bg-purple-600 {
--tw-bg-opacity: 1;
background-color: rgb(147 51 234 / var(--tw-bg-opacity, 1));
}
.bg-purple-700 {
--tw-bg-opacity: 1;
background-color: rgb(126 34 206 / var(--tw-bg-opacity, 1));
}
.hover\:bg-purple-700:hover {
--tw-bg-opacity: 1;
background-color: rgb(126 34 206 / var(--tw-bg-opacity, 1));
}
/* Ensure Font Awesome icons are visible */
.fas, .far, .fab, .fal, .fad {
font-family: "Font Awesome 6 Free", "Font Awesome 6 Brands", "Font Awesome 6 Pro";
font-weight: 900;
display: inline-block;
font-style: normal;
font-variant: normal;
text-rendering: auto;
line-height: 1;
}
.fas::before {
font-weight: 900;
}
/* Button icon spacing */
button .fas, button .far, button .fab {
margin-right: 0.25rem;
}
button .fas:only-child, button .far:only-child, button .fab:only-child {
margin-right: 0;
}
/* Additional utility classes that might be missing */
/* Support for top-1/2 and -translate-y-1/2 (with escaped slash) */
.top-1\/2 {
top: 50%;
}
.-translate-y-1\/2 {
--tw-translate-y: -50%;
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
}
/* Alternative class name without escape (for compatibility) */
.top-1-2 {
top: 50%;
}
.-translate-y-1-2 {
--tw-translate-y: -50%;
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
}
.gap-2 {
gap: 0.5rem;
}
/* Ensure buttons with icons display properly */
button.inline-flex, .inline-flex {
display: inline-flex;
align-items: center;
}
button .fas, button .far, button .fab {
display: inline-block;
width: 1em;
text-align: center;
}
/* Ensure delete button is visible */
button.bg-red-500, button.bg-red-600 {
color: white;
border: none;
cursor: pointer;
}
button.bg-red-500:hover, button.bg-red-600:hover {
background-color: rgb(220 38 38);
}

View File

@@ -24,11 +24,13 @@ function loadFilters() {
}
}
select.innerHTML = '';
const deleteBtn = document.getElementById('deleteFilterBtn');
if (!data.filters || data.filters.length === 0) {
const opt = document.createElement('option');
opt.value = '';
opt.textContent = 'No Filters Found';
select.appendChild(opt);
if (deleteBtn) deleteBtn.disabled = true;
} else {
data.filters.forEach(f => {
const opt = document.createElement('option');
@@ -36,6 +38,14 @@ 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;
});
}
if (deleteBtn) deleteBtn.disabled = !select.value;
}
})
.catch(err => {
@@ -122,11 +132,115 @@ function showFilterSection() {
document.getElementById('logLinesTextarea').value = '';
testResultsEl.innerHTML = '';
testResultsEl.classList.add('hidden');
document.getElementById('deleteFilterBtn').disabled = true;
return;
}
loadFilters();
testResultsEl.innerHTML = '';
testResultsEl.classList.add('hidden');
document.getElementById('logLinesTextarea').value = '';
// Add change listener to enable/disable delete button
const filterSelect = document.getElementById('filterSelect');
const deleteBtn = document.getElementById('deleteFilterBtn');
filterSelect.addEventListener('change', function() {
deleteBtn.disabled = !filterSelect.value;
});
}
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 = '';
})
.catch(function(err) {
console.error('Error deleting filter:', err);
showToast('Error deleting filter: ' + (err.message || err), 'error');
})
.finally(function() {
showLoading(false);
});
}

View File

@@ -56,6 +56,22 @@ function openJailConfigModal(jailName) {
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();
@@ -267,9 +283,17 @@ function openManageJailsModal() {
+ ' 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')) + '"'
+ ' title="' + escapeHtml(t('modal.filter_config_edit', 'Edit Filter / Jail')) + '"'
+ ' >'
+ escapeHtml(t('modal.filter_config_edit', 'Edit Filter'))
+ 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'
@@ -428,3 +452,152 @@ function saveManageJailsSingle(checkbox) {
});
}
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

@@ -157,10 +157,16 @@
<div id="filterNotice" class="hidden mb-4 text-sm text-yellow-700 bg-yellow-100 border border-yellow-200 rounded px-4 py-3"></div>
<div class="bg-white rounded-lg shadow p-6 mb-6">
<!-- Dropdown of available jail/filters -->
<div class="mb-4">
<label for="filterSelect" class="block text-sm font-medium text-gray-700 mb-2" data-i18n="filter_debug.select_filter">Select a Filter</label>
<select id="filterSelect" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"></select>
<div class="mb-4 flex justify-between items-end">
<div class="flex-1 mr-4">
<label for="filterSelect" class="block text-sm font-medium text-gray-700 mb-2" data-i18n="filter_debug.select_filter">Select a Filter</label>
<div class="flex gap-2">
<select id="filterSelect" class="flex-1 border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"></select>
<button type="button" onclick="deleteFilter()" id="deleteFilterBtn" class="px-3 py-2 bg-red-500 text-white rounded hover:bg-red-600 transition-colors disabled:opacity-50 disabled:cursor-not-allowed" disabled title="Delete selected filter">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
</div>
<!-- Textarea for log lines to test -->
@@ -811,7 +817,10 @@
<div class="mt-4 space-y-4">
<!-- Filter Configuration -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-2" data-i18n="modal.filter_config_label">Filter Configuration</label>
<div class="flex items-center justify-between mb-2">
<label class="block text-sm font-medium text-gray-700" data-i18n="modal.filter_config_label">Filter Configuration</label>
<span id="filterFilePath" class="text-xs text-gray-500 font-mono" style="display: none;"></span>
</div>
<div class="relative" style="position: relative;">
<textarea id="filterConfigTextarea"
class="w-full border border-gray-700 rounded-md px-4 py-3 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 h-96 font-mono text-sm bg-gray-900 text-white resize-none overflow-auto"
@@ -841,7 +850,10 @@
<!-- Jail Configuration -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-2" data-i18n="modal.jail_config_label">Jail Configuration</label>
<div class="flex items-center justify-between mb-2">
<label class="block text-sm font-medium text-gray-700" data-i18n="modal.jail_config_label">Jail Configuration</label>
<span id="jailFilePath" class="text-xs text-gray-500 font-mono" style="display: none;"></span>
</div>
<div class="relative" style="position: relative;">
<textarea id="jailConfigTextarea"
class="w-full border border-gray-700 rounded-md px-4 py-3 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 font-mono text-sm bg-gray-900 text-white resize-none overflow-auto"
@@ -904,6 +916,16 @@
</button>
</div>
<div class="mt-4">
<div class="mb-4 flex justify-end gap-2">
<button type="button" onclick="openCreateFilterModal()" class="inline-flex items-center gap-2 px-4 py-2 bg-gray-600 text-white text-sm font-medium rounded-md hover:bg-gray-700 transition-colors">
<i class="fas fa-plus"></i>
<span data-i18n="modal.create_filter">Create New Filter</span>
</button>
<button type="button" onclick="openCreateJailModal()" class="inline-flex items-center gap-2 px-4 py-2 bg-gray-600 text-white text-sm font-medium rounded-md hover:bg-gray-700 transition-colors">
<i class="fas fa-plus"></i>
<span data-i18n="modal.create_jail">Create New Jail</span>
</button>
</div>
<!-- Dynamically filled list of jails with toggle switches -->
<div id="jailsList" class="divide-y divide-gray-200"></div>
</div>
@@ -917,6 +939,93 @@
</div>
</div>
<!-- Create Jail Modal -->
<div id="createJailModal" class="hidden fixed inset-0 z-50 overflow-y-auto">
<div class="relative flex min-h-full w-full items-center justify-center p-4 sm:p-6">
<div class="fixed inset-0 bg-gray-500 opacity-75" aria-hidden="true"></div>
<div class="relative z-10 w-full rounded-lg bg-white text-left shadow-xl transition-all" style="max-width: 600px;">
<div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<div class="sm:flex sm:items-start">
<div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left w-full">
<div class="flex items-center justify-between">
<h3 class="text-lg leading-6 font-medium text-gray-900" data-i18n="modal.create_jail_title">Create New Jail</h3>
<button type="button" onclick="closeModal('createJailModal')" class="text-gray-400 hover:text-gray-600 focus:outline-none focus:text-gray-600" aria-label="Close">
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div class="mt-4">
<div class="mb-4">
<label for="newJailName" class="block text-sm font-medium text-gray-700 mb-2" data-i18n="modal.jail_name">Jail Name</label>
<input type="text" id="newJailName" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="e.g., sshd" />
<p class="text-xs text-gray-500 mt-1" data-i18n="modal.jail_name_hint">Only alphanumeric characters, dashes, and underscores are allowed.</p>
</div>
<div class="mb-4">
<label for="newJailFilter" class="block text-sm font-medium text-gray-700 mb-2" data-i18n="modal.jail_filter">Filter (optional)</label>
<select id="newJailFilter" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" onchange="updateJailConfigFromFilter()">
<option value="">-- Select a filter --</option>
</select>
<p class="text-xs text-gray-500 mt-1" data-i18n="modal.jail_filter_hint">Selecting a filter will auto-populate the jail configuration.</p>
</div>
<div class="mb-4">
<label for="newJailContent" class="block text-sm font-medium text-gray-700 mb-2" data-i18n="modal.jail_config">Jail Configuration</label>
<textarea id="newJailContent" 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" placeholder="[jailname]&#10;enabled = false&#10;port = ssh&#10;filter = sshd&#10;logpath = /var/log/auth.log"></textarea>
<p class="text-xs text-gray-500 mt-1" data-i18n="modal.jail_config_hint">Jail configuration will be auto-populated when you select a filter.</p>
</div>
</div>
</div>
</div>
</div>
<div class="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
<button type="button" onclick="createJail()" class="mt-3 w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-green-600 text-base font-medium text-white hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm" data-i18n="modal.create">Create</button>
<button type="button" class="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm" onclick="closeModal('createJailModal')" data-i18n="modal.cancel">Cancel</button>
</div>
</div>
</div>
</div>
<!-- Create Filter Modal -->
<div id="createFilterModal" class="hidden fixed inset-0 z-50 overflow-y-auto">
<div class="relative flex min-h-full w-full items-center justify-center p-4 sm:p-6">
<div class="fixed inset-0 bg-gray-500 opacity-75" aria-hidden="true"></div>
<div class="relative z-10 w-full rounded-lg bg-white text-left shadow-xl transition-all" style="max-width: 600px;">
<div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<div class="sm:flex sm:items-start">
<div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left w-full">
<div class="flex items-center justify-between">
<h3 class="text-lg leading-6 font-medium text-gray-900" data-i18n="modal.create_filter_title">Create New Filter</h3>
<button type="button" onclick="closeModal('createFilterModal')" class="text-gray-400 hover:text-gray-600 focus:outline-none focus:text-gray-600" aria-label="Close">
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div class="mt-4">
<div class="mb-4">
<label for="newFilterName" class="block text-sm font-medium text-gray-700 mb-2" data-i18n="modal.filter_name">Filter Name</label>
<input type="text" id="newFilterName" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="e.g., myfilter" />
<p class="text-xs text-gray-500 mt-1" data-i18n="modal.filter_name_hint">Only alphanumeric characters, dashes, and underscores are allowed.</p>
</div>
<div class="mb-4">
<label for="newFilterContent" class="block text-sm font-medium text-gray-700 mb-2" data-i18n="modal.filter_config">Filter Configuration (optional)</label>
<textarea id="newFilterContent" 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" placeholder="# Filter: myfilter&#10;[Definition]&#10;failregex = ^.*Failed login.*$"></textarea>
<p class="text-xs text-gray-500 mt-1" data-i18n="modal.filter_config_hint">If left empty, an empty filter file will be created.</p>
</div>
</div>
</div>
</div>
</div>
<div class="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
<button type="button" onclick="createFilter()" class="mt-3 w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-green-600 text-base font-medium text-white hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm" data-i18n="modal.create">Create</button>
<button type="button" class="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm" onclick="closeModal('createFilterModal')" data-i18n="modal.cancel">Cancel</button>
</div>
</div>
</div>
</div>
<!-- Server Manager Modal -->
<div id="serverManagerModal" class="hidden fixed inset-0 z-50 overflow-y-auto">
<div class="relative flex min-h-full w-full items-center justify-center p-4 sm:p-6">