Implement basic settings save and load structures and enhance UI

This commit is contained in:
Michael Reber
2025-01-27 11:09:06 +01:00
parent 75a1ef9a24
commit c2e953a024
3 changed files with 149 additions and 26 deletions

View File

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

View File

@@ -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"})
}

View File

@@ -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