mirror of
https://github.com/swissmakers/fail2ban-ui.git
synced 2026-04-11 13:47:05 +02:00
Implement basic settings save and load structures and enhance UI
This commit is contained in:
@@ -3,6 +3,7 @@ package config
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"sync"
|
||||
)
|
||||
@@ -11,6 +12,7 @@ import (
|
||||
// relevant Fail2ban jail/local config options.
|
||||
type AppSettings struct {
|
||||
Language string `json:"language"`
|
||||
Debug bool `json:"debug"`
|
||||
ReloadNeeded bool `json:"reloadNeeded"`
|
||||
AlertCountries []string `json:"alertCountries"`
|
||||
|
||||
@@ -56,6 +58,7 @@ func setDefaults() {
|
||||
|
||||
currentSettings = AppSettings{
|
||||
Language: "en",
|
||||
Debug: false,
|
||||
ReloadNeeded: false,
|
||||
AlertCountries: []string{"all"},
|
||||
|
||||
@@ -71,6 +74,8 @@ func setDefaults() {
|
||||
|
||||
// loadSettings reads the file (if exists) into currentSettings
|
||||
func loadSettings() error {
|
||||
fmt.Println("----------------------------")
|
||||
fmt.Println("loadSettings called (settings.go)") // entry point
|
||||
data, err := os.ReadFile(settingsFile)
|
||||
if os.IsNotExist(err) {
|
||||
return err // triggers setDefaults + save
|
||||
@@ -92,14 +97,23 @@ func loadSettings() error {
|
||||
|
||||
// saveSettings writes currentSettings to JSON
|
||||
func saveSettings() error {
|
||||
settingsLock.RLock()
|
||||
defer settingsLock.RUnlock()
|
||||
fmt.Println("----------------------------")
|
||||
fmt.Println("saveSettings called (settings.go)") // entry point
|
||||
|
||||
b, err := json.MarshalIndent(currentSettings, "", " ")
|
||||
if err != nil {
|
||||
fmt.Println("Error marshalling settings:", err) // Debug
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(settingsFile, b, 0644)
|
||||
fmt.Println("Settings marshaled, writing to file...") // Log marshaling success
|
||||
//return os.WriteFile(settingsFile, b, 0644)
|
||||
err = os.WriteFile(settingsFile, b, 0644)
|
||||
if err != nil {
|
||||
log.Println("Error writing to file:", err) // Debug
|
||||
} else {
|
||||
log.Println("Settings saved successfully!") // Debug
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetSettings returns a copy of the current settings
|
||||
@@ -132,6 +146,8 @@ func UpdateSettings(new AppSettings) (AppSettings, error) {
|
||||
settingsLock.Lock()
|
||||
defer settingsLock.Unlock()
|
||||
|
||||
fmt.Println("Locked settings for update") // Log lock acquisition
|
||||
|
||||
old := currentSettings
|
||||
|
||||
// If certain fields change, we mark reload needed
|
||||
@@ -154,11 +170,14 @@ func UpdateSettings(new AppSettings) (AppSettings, error) {
|
||||
}
|
||||
|
||||
currentSettings = new
|
||||
fmt.Println("New settings applied:", currentSettings) // Log settings applied
|
||||
|
||||
// persist to file
|
||||
if err := saveSettings(); err != nil {
|
||||
fmt.Println("Error saving settings:", err) // Log save error
|
||||
return currentSettings, err
|
||||
}
|
||||
fmt.Println("Settings saved to file successfully") // Log save success
|
||||
return currentSettings, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ package web
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
@@ -58,6 +57,8 @@ func SummaryHandler(c *gin.Context) {
|
||||
|
||||
// UnbanIPHandler unbans a given IP in a specific jail.
|
||||
func UnbanIPHandler(c *gin.Context) {
|
||||
fmt.Println("----------------------------")
|
||||
fmt.Println("UnbanIPHandler called (handlers.go)") // entry point
|
||||
jail := c.Param("jail")
|
||||
ip := c.Param("ip")
|
||||
|
||||
@@ -68,6 +69,7 @@ func UnbanIPHandler(c *gin.Context) {
|
||||
})
|
||||
return
|
||||
}
|
||||
fmt.Println(ip + " from jail " + jail + " unbanned successfully (handlers.go)")
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "IP unbanned successfully",
|
||||
})
|
||||
@@ -92,6 +94,8 @@ func IndexHandler(c *gin.Context) {
|
||||
|
||||
// GetJailFilterConfigHandler returns the raw filter config for a given jail
|
||||
func GetJailFilterConfigHandler(c *gin.Context) {
|
||||
fmt.Println("----------------------------")
|
||||
fmt.Println("GetJailFilterConfigHandler called (handlers.go)") // entry point
|
||||
jail := c.Param("jail")
|
||||
cfg, err := fail2ban.GetJailConfig(jail)
|
||||
if err != nil {
|
||||
@@ -106,6 +110,8 @@ func GetJailFilterConfigHandler(c *gin.Context) {
|
||||
|
||||
// SetJailFilterConfigHandler overwrites the current filter config with new content
|
||||
func SetJailFilterConfigHandler(c *gin.Context) {
|
||||
fmt.Println("----------------------------")
|
||||
fmt.Println("SetJailFilterConfigHandler called (handlers.go)") // entry point
|
||||
jail := c.Param("jail")
|
||||
|
||||
// Parse JSON body (containing the new filter content)
|
||||
@@ -140,23 +146,34 @@ func SetJailFilterConfigHandler(c *gin.Context) {
|
||||
|
||||
// GetSettingsHandler returns the entire AppSettings struct as JSON
|
||||
func GetSettingsHandler(c *gin.Context) {
|
||||
fmt.Println("----------------------------")
|
||||
fmt.Println("GetSettingsHandler called (handlers.go)") // entry point
|
||||
s := config.GetSettings()
|
||||
c.JSON(http.StatusOK, s)
|
||||
}
|
||||
|
||||
// UpdateSettingsHandler updates the AppSettings from a JSON body
|
||||
func UpdateSettingsHandler(c *gin.Context) {
|
||||
fmt.Println("----------------------------")
|
||||
fmt.Println("UpdateSettingsHandler called (handlers.go)") // entry point
|
||||
var req config.AppSettings
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid JSON"})
|
||||
fmt.Println("JSON binding error:", err) // Debug
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "invalid JSON",
|
||||
"details": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
fmt.Println("JSON binding successful, updating settings (handlers.go)")
|
||||
|
||||
newSettings, err := config.UpdateSettings(req)
|
||||
if err != nil {
|
||||
fmt.Println("Error updating settings:", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
fmt.Println("Settings updated successfully (handlers.go)")
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "Settings updated",
|
||||
@@ -167,9 +184,11 @@ func UpdateSettingsHandler(c *gin.Context) {
|
||||
// ListFiltersHandler returns a JSON array of filter names
|
||||
// found as *.conf in /etc/fail2ban/filter.d
|
||||
func ListFiltersHandler(c *gin.Context) {
|
||||
fmt.Println("----------------------------")
|
||||
fmt.Println("ListFiltersHandler called (handlers.go)") // entry point
|
||||
dir := "/etc/fail2ban/filter.d"
|
||||
|
||||
files, err := ioutil.ReadDir(dir)
|
||||
files, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to read filter directory: " + err.Error(),
|
||||
@@ -189,6 +208,8 @@ func ListFiltersHandler(c *gin.Context) {
|
||||
}
|
||||
|
||||
func TestFilterHandler(c *gin.Context) {
|
||||
fmt.Println("----------------------------")
|
||||
fmt.Println("TestFilterHandler called (handlers.go)") // entry point
|
||||
var req struct {
|
||||
FilterName string `json:"filterName"`
|
||||
LogLines []string `json:"logLines"`
|
||||
@@ -204,6 +225,8 @@ func TestFilterHandler(c *gin.Context) {
|
||||
|
||||
// ApplyFail2banSettings updates /etc/fail2ban/jail.local [DEFAULT] with our JSON
|
||||
func ApplyFail2banSettings(jailLocalPath string) error {
|
||||
fmt.Println("----------------------------")
|
||||
fmt.Println("ApplyFail2banSettings called (handlers.go)") // entry point
|
||||
s := config.GetSettings()
|
||||
|
||||
// open /etc/fail2ban/jail.local, parse or do a simplistic approach:
|
||||
@@ -228,6 +251,9 @@ func ApplyFail2banSettings(jailLocalPath string) error {
|
||||
|
||||
// ReloadFail2banHandler reloads the Fail2ban service
|
||||
func ReloadFail2banHandler(c *gin.Context) {
|
||||
fmt.Println("----------------------------")
|
||||
fmt.Println("ApplyFail2banSettings called (handlers.go)") // entry point
|
||||
|
||||
// First we write our new settings to /etc/fail2ban/jail.local
|
||||
// if err := fail2ban.ApplyFail2banSettings("/etc/fail2ban/jail.local"); err != nil {
|
||||
// c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
@@ -241,9 +267,9 @@ func ReloadFail2banHandler(c *gin.Context) {
|
||||
}
|
||||
|
||||
// We set reload done in config
|
||||
//if err := config.MarkReloadDone(); err != nil {
|
||||
// c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
// return
|
||||
//}
|
||||
if err := config.MarkReloadDone(); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Fail2ban reloaded successfully"})
|
||||
}
|
||||
|
||||
@@ -103,8 +103,12 @@
|
||||
|
||||
<!-- Settings Section -->
|
||||
<div id="settingsSection" style="display: none;" class="container my-4">
|
||||
<h2>Settings</h2>
|
||||
<form onsubmit="saveSettings(event)">
|
||||
<h2>Settings</h2>
|
||||
<form onsubmit="saveSettings(event)">
|
||||
<!-- General Settings Group -->
|
||||
<fieldset class="border p-3 rounded mb-4">
|
||||
<legend class="w-auto px-2">General Settings</legend>
|
||||
<!-- Language Selection -->
|
||||
<div class="mb-3">
|
||||
<label for="languageSelect" class="form-label">Language</label>
|
||||
<select id="languageSelect" class="form-select" disabled>
|
||||
@@ -112,13 +116,31 @@
|
||||
<option value="de">Deutsch</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="alertEmail" class="form-label">Alert Email</label>
|
||||
<input type="email" class="form-control" id="alertEmail"/>
|
||||
<!-- Debug Log Output -->
|
||||
<div class="mb-3 form-check">
|
||||
<input type="checkbox" class="form-check-input" id="debugMode">
|
||||
<label for="debugMode" class="form-check-label">Enable Debug Log</label>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<!-- Alert Settings Group -->
|
||||
<fieldset class="border p-3 rounded mb-4">
|
||||
<legend class="w-auto px-2">Alert Settings</legend>
|
||||
|
||||
<!-- Source Email -->
|
||||
<div class="mb-3">
|
||||
<label for="sourceEmail" class="form-label">Source Email - Emails are sent from this address.</label>
|
||||
<input type="email" class="form-control" id="sourceEmail"/>
|
||||
</div>
|
||||
<!-- Destination Email -->
|
||||
<div class="mb-3">
|
||||
<label for="destEmail" class="form-label">Destination Email - Where to sent the alert messages?</label>
|
||||
<input type="email" class="form-control" id="destEmail" placeholder="e.g., alerts@swissmakers.ch" />
|
||||
</div>
|
||||
<!-- Alert Countries -->
|
||||
<div class="mb-3">
|
||||
<label for="alertCountries" class="form-label">Select alert Countries</label>
|
||||
<p class="text-muted">Choose which country-IP bans should trigger an email. With CTRL, you can select multiple.</p>
|
||||
<p class="text-muted">Choose which country IP blocks should trigger an email. You can select multiple with CTRL.</p>
|
||||
<select id="alertCountries" class="form-select" multiple size="7">
|
||||
<option value="ALL">ALL (Every Country)</option>
|
||||
<option value="CH">Switzerland (CH)</option>
|
||||
@@ -130,9 +152,41 @@
|
||||
<!-- Maybe i will add more later.. -->
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Save</button>
|
||||
</form>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<!-- Fail2Ban Configuration Group -->
|
||||
<fieldset class="border p-3 rounded mb-4">
|
||||
<legend class="w-auto px-2">Fail2Ban Configuration</legend>
|
||||
|
||||
<!-- Bantime Increment -->
|
||||
<div class="mb-3 form-check">
|
||||
<input type="checkbox" class="form-check-input" id="bantimeIncrement" />
|
||||
<label for="bantimeIncrement" class="form-check-label">Enable Bantime Increment</label>
|
||||
</div>
|
||||
<!-- Bantime -->
|
||||
<div class="mb-3">
|
||||
<label for="banTime" class="form-label">Default Bantime</label>
|
||||
<input type="text" class="form-control" id="banTime" placeholder="e.g., 48h" />
|
||||
</div>
|
||||
<!-- Findtime -->
|
||||
<div class="mb-3">
|
||||
<label for="findTime" class="form-label">Default Findtime</label>
|
||||
<input type="text" class="form-control" id="findTime" placeholder="e.g., 30m" />
|
||||
</div>
|
||||
<!-- Max Retry -->
|
||||
<div class="mb-3">
|
||||
<label for="maxRetry" class="form-label">Default Max Retry</label>
|
||||
<input type="number" class="form-control" id="maxRetry" placeholder="Enter maximum retries" />
|
||||
</div>
|
||||
<!-- Ignore IPs -->
|
||||
<div class="mb-3">
|
||||
<label for="ignoreIP" class="form-label">Ignore IPs</label>
|
||||
<textarea class="form-control" id="ignoreIP" rows="2" placeholder="Enter IPs to ignore, separated by spaces"></textarea>
|
||||
</div>
|
||||
</fieldset>
|
||||
<button type="submit" class="btn btn-primary">Save</button>
|
||||
</form>
|
||||
</div>
|
||||
<!-- ******************************************************************* -->
|
||||
|
||||
<!-- Footer -->
|
||||
@@ -464,10 +518,13 @@ function loadSettings() {
|
||||
fetch('/api/settings')
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
// populate language, email, etc...
|
||||
// Get current general settings
|
||||
document.getElementById('languageSelect').value = data.language || 'en';
|
||||
document.getElementById('alertEmail').value = data.sender || '';
|
||||
document.getElementById('debugMode').checked = data.debug || false;
|
||||
|
||||
// Get current alert settings
|
||||
document.getElementById('sourceEmail').value = data.sender || '';
|
||||
document.getElementById('destEmail').value = data.destemail || '';
|
||||
// alertCountries multi
|
||||
const select = document.getElementById('alertCountries');
|
||||
// clear selection
|
||||
@@ -486,6 +543,13 @@ function loadSettings() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get current Fail2Ban Configuration
|
||||
document.getElementById('bantimeIncrement').checked = data.bantimeIncrement || false;
|
||||
document.getElementById('banTime').value = data.bantime || '';
|
||||
document.getElementById('findTime').value = data.findtime || '';
|
||||
document.getElementById('maxRetry').value = data.maxretry || '';
|
||||
document.getElementById('ignoreIP').value = data.ignoreip || '';
|
||||
})
|
||||
.catch(err => {
|
||||
alert('Error loading settings: ' + err);
|
||||
@@ -499,7 +563,9 @@ function saveSettings(e) {
|
||||
|
||||
showLoading(true);
|
||||
const lang = document.getElementById('languageSelect').value;
|
||||
const mail = document.getElementById('alertEmail').value;
|
||||
const debugMode = document.getElementById("debugMode").checked;
|
||||
const srcmail = document.getElementById('sourceEmail').value;
|
||||
const destmail = document.getElementById('destEmail').value;
|
||||
|
||||
const select = document.getElementById('alertCountries');
|
||||
let chosenCountries = [];
|
||||
@@ -513,10 +579,23 @@ function saveSettings(e) {
|
||||
chosenCountries = ["all"];
|
||||
}
|
||||
|
||||
const bantimeinc = document.getElementById('bantimeIncrement').checked;
|
||||
const bant = document.getElementById('banTime').value;
|
||||
const findt = document.getElementById('findTime').value;
|
||||
const maxre = parseInt(document.getElementById('maxRetry').value, 10) || 1; // Default to 1 (if parsing fails)
|
||||
const ignip = document.getElementById('ignoreIP').value;
|
||||
|
||||
const body = {
|
||||
language: lang,
|
||||
sender: mail,
|
||||
alertCountries: chosenCountries
|
||||
debug: debugMode,
|
||||
sender: srcmail,
|
||||
destemail: destmail,
|
||||
alertCountries: chosenCountries,
|
||||
bantimeIncrement: bantimeinc,
|
||||
bantime: bant,
|
||||
findtime: findt,
|
||||
maxretry: maxre,
|
||||
ignoreip: ignip
|
||||
};
|
||||
|
||||
fetch('/api/settings', {
|
||||
@@ -527,7 +606,7 @@ function saveSettings(e) {
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.error) {
|
||||
alert('Error saving settings: ' + data.error);
|
||||
alert('Error saving settings: ' + data.error + data.details);
|
||||
} else {
|
||||
//alert(data.message || 'Settings saved');
|
||||
console.log("Settings saved successfully. Reload needed? " + data.reloadNeeded);
|
||||
@@ -642,7 +721,6 @@ function reloadFail2ban() {
|
||||
if (data.error) {
|
||||
alert("Error: " + data.error);
|
||||
} else {
|
||||
alert(data.message || "Fail2ban reloaded");
|
||||
// Hide reload banner
|
||||
document.getElementById('reloadBanner').style.display = 'none';
|
||||
// Refresh data
|
||||
|
||||
Reference in New Issue
Block a user