mirror of
https://github.com/swissmakers/fail2ban-ui.git
synced 2026-04-19 06:53:14 +02:00
@@ -17,10 +17,9 @@
|
|||||||
package fail2ban
|
package fail2ban
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -30,9 +29,10 @@ type JailInfo struct {
|
|||||||
TotalBanned int `json:"totalBanned"`
|
TotalBanned int `json:"totalBanned"`
|
||||||
NewInLastHour int `json:"newInLastHour"`
|
NewInLastHour int `json:"newInLastHour"`
|
||||||
BannedIPs []string `json:"bannedIPs"`
|
BannedIPs []string `json:"bannedIPs"`
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetJails returns all configured jails using "fail2ban-client status".
|
// Get active jails using "fail2ban-client status".
|
||||||
func GetJails() ([]string, error) {
|
func GetJails() ([]string, error) {
|
||||||
cmd := exec.Command("fail2ban-client", "status")
|
cmd := exec.Command("fail2ban-client", "status")
|
||||||
out, err := cmd.CombinedOutput()
|
out, err := cmd.CombinedOutput()
|
||||||
@@ -140,33 +140,33 @@ func BuildJailInfos(logPath string) ([]JailInfo, error) {
|
|||||||
return results, nil
|
return results, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetJailConfig returns the config content for a given jail.
|
|
||||||
// Example: we assume each jail config is at /etc/fail2ban/filter.d/<jail>.conf
|
|
||||||
// Adapt this to your environment.
|
|
||||||
func GetJailConfig(jail string) (string, error) {
|
|
||||||
configPath := filepath.Join("/etc/fail2ban/filter.d", jail+".conf")
|
|
||||||
content, err := os.ReadFile(configPath)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("failed to read config for jail %s: %v", jail, err)
|
|
||||||
}
|
|
||||||
return string(content), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetJailConfig overwrites the config file for a given jail with new content.
|
|
||||||
func SetJailConfig(jail, newContent string) error {
|
|
||||||
configPath := filepath.Join("/etc/fail2ban/filter.d", jail+".conf")
|
|
||||||
if err := os.WriteFile(configPath, []byte(newContent), 0644); err != nil {
|
|
||||||
return fmt.Errorf("failed to write config for jail %s: %v", jail, err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReloadFail2ban runs "fail2ban-client reload"
|
// ReloadFail2ban runs "fail2ban-client reload"
|
||||||
func ReloadFail2ban() error {
|
func ReloadFail2ban() error {
|
||||||
cmd := exec.Command("fail2ban-client", "reload")
|
cmd := exec.Command("fail2ban-client", "reload")
|
||||||
out, err := cmd.CombinedOutput()
|
out, err := cmd.CombinedOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("fail2ban reload error: %v\nOutput: %s", err, out)
|
return fmt.Errorf("fail2ban reload error: %v\noutput: %s", err, out)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RestartFail2ban restarts the Fail2ban service.
|
||||||
|
func RestartFail2ban() error {
|
||||||
|
cmd := "systemctl restart fail2ban"
|
||||||
|
out, err := execCommand(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to restart fail2ban: %w - output: %s", err, out)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// execCommand is a helper function to execute shell commands.
|
||||||
|
func execCommand(command string) (string, error) {
|
||||||
|
parts := strings.Fields(command)
|
||||||
|
if len(parts) == 0 {
|
||||||
|
return "", errors.New("no command provided")
|
||||||
|
}
|
||||||
|
cmd := exec.Command(parts[0], parts[1:]...)
|
||||||
|
out, err := cmd.CombinedOutput()
|
||||||
|
return string(out), err
|
||||||
|
}
|
||||||
|
|||||||
44
internal/fail2ban/filter_management.go
Normal file
44
internal/fail2ban/filter_management.go
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
// Fail2ban UI - A Swiss made, management interface for Fail2ban.
|
||||||
|
//
|
||||||
|
// Copyright (C) 2025 Swissmakers GmbH (https://swissmakers.ch)
|
||||||
|
//
|
||||||
|
// Licensed under the GNU General Public License, Version 3 (GPL-3.0)
|
||||||
|
// You may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.en.html
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package fail2ban
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetFilterConfig returns the config content for a given jail filter.
|
||||||
|
// Example: we assume each jail config is at /etc/fail2ban/filter.d/<jailname>.conf
|
||||||
|
// Adapt this to your environment.
|
||||||
|
func GetFilterConfig(jail string) (string, error) {
|
||||||
|
configPath := filepath.Join("/etc/fail2ban/filter.d", jail+".conf")
|
||||||
|
content, err := os.ReadFile(configPath)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to read config for jail %s: %v", jail, err)
|
||||||
|
}
|
||||||
|
return string(content), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetFilterConfig overwrites the config file for a given jail with new content.
|
||||||
|
func SetFilterConfig(jail, newContent string) error {
|
||||||
|
configPath := filepath.Join("/etc/fail2ban/filter.d", jail+".conf")
|
||||||
|
if err := os.WriteFile(configPath, []byte(newContent), 0644); err != nil {
|
||||||
|
return fmt.Errorf("failed to write config for jail %s: %v", jail, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
150
internal/fail2ban/jail_management.go
Normal file
150
internal/fail2ban/jail_management.go
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
package fail2ban
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/swissmakers/fail2ban-ui/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetAllJails reads jails from both /etc/fail2ban/jail.local and /etc/fail2ban/jail.d directory.
|
||||||
|
func GetAllJails() ([]JailInfo, error) {
|
||||||
|
var jails []JailInfo
|
||||||
|
|
||||||
|
// Parse jails from jail.local
|
||||||
|
localPath := "/etc/fail2ban/jail.local"
|
||||||
|
localJails, err := parseJailConfigFile(localPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse %s: %w", localPath, err)
|
||||||
|
}
|
||||||
|
config.DebugLog("############################")
|
||||||
|
config.DebugLog(fmt.Sprintf("%+v", localJails))
|
||||||
|
config.DebugLog("############################")
|
||||||
|
|
||||||
|
jails = append(jails, localJails...)
|
||||||
|
|
||||||
|
// Parse jails from jail.d directory, if it exists
|
||||||
|
jailDPath := "/etc/fail2ban/jail.d"
|
||||||
|
files, err := os.ReadDir(jailDPath)
|
||||||
|
if err == nil {
|
||||||
|
for _, f := range files {
|
||||||
|
if !f.IsDir() && filepath.Ext(f.Name()) == ".conf" {
|
||||||
|
fullPath := filepath.Join(jailDPath, f.Name())
|
||||||
|
dJails, err := parseJailConfigFile(fullPath)
|
||||||
|
if err == nil {
|
||||||
|
jails = append(jails, dJails...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return jails, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseJailConfigFile parses a jail configuration file and returns a slice of JailInfo.
|
||||||
|
// It assumes each jail section is defined by [JailName] and that an "enabled" line may exist.
|
||||||
|
func parseJailConfigFile(path string) ([]JailInfo, error) {
|
||||||
|
var jails []JailInfo
|
||||||
|
file, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
scanner := bufio.NewScanner(file)
|
||||||
|
var currentJail string
|
||||||
|
|
||||||
|
// default value is true if "enabled" is missing; we set it for each section.
|
||||||
|
enabled := true
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := strings.TrimSpace(scanner.Text())
|
||||||
|
if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") {
|
||||||
|
// When a new section starts, save the previous jail if exists.
|
||||||
|
if currentJail != "" && currentJail != "DEFAULT" {
|
||||||
|
jails = append(jails, JailInfo{
|
||||||
|
JailName: currentJail,
|
||||||
|
Enabled: enabled,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// Start a new jail section.
|
||||||
|
currentJail = strings.Trim(line, "[]")
|
||||||
|
// Reset to default for the new section.
|
||||||
|
enabled = true
|
||||||
|
} else if strings.HasPrefix(strings.ToLower(line), "enabled") {
|
||||||
|
// Expect format: enabled = true/false
|
||||||
|
parts := strings.Split(line, "=")
|
||||||
|
if len(parts) == 2 {
|
||||||
|
value := strings.TrimSpace(parts[1])
|
||||||
|
enabled = strings.EqualFold(value, "true")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Add the final jail if one exists.
|
||||||
|
if currentJail != "" && currentJail != "DEFAULT" {
|
||||||
|
jails = append(jails, JailInfo{
|
||||||
|
JailName: currentJail,
|
||||||
|
Enabled: enabled,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return jails, scanner.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateJailEnabledStates updates the enabled state for each jail based on the provided updates map.
|
||||||
|
// It updates /etc/fail2ban/jail.local and attempts to update any jail.d files as well.
|
||||||
|
func UpdateJailEnabledStates(updates map[string]bool) error {
|
||||||
|
// Update jail.local file
|
||||||
|
localPath := "/etc/fail2ban/jail.local"
|
||||||
|
if err := updateJailConfigFile(localPath, updates); err != nil {
|
||||||
|
return fmt.Errorf("failed to update %s: %w", localPath, err)
|
||||||
|
}
|
||||||
|
// Update jail.d files (if any)
|
||||||
|
jailDPath := "/etc/fail2ban/jail.d"
|
||||||
|
files, err := os.ReadDir(jailDPath)
|
||||||
|
if err == nil {
|
||||||
|
for _, f := range files {
|
||||||
|
if !f.IsDir() && filepath.Ext(f.Name()) == ".conf" {
|
||||||
|
fullPath := filepath.Join(jailDPath, f.Name())
|
||||||
|
// Ignore error here, as jail.d files might not need to be updated.
|
||||||
|
_ = updateJailConfigFile(fullPath, updates)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateJailConfigFile updates a single jail configuration file with the new enabled states.
|
||||||
|
func updateJailConfigFile(path string, updates map[string]bool) error {
|
||||||
|
input, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
lines := strings.Split(string(input), "\n")
|
||||||
|
var outputLines []string
|
||||||
|
var currentJail string
|
||||||
|
for _, line := range lines {
|
||||||
|
trimmed := strings.TrimSpace(line)
|
||||||
|
if strings.HasPrefix(trimmed, "[") && strings.HasSuffix(trimmed, "]") {
|
||||||
|
currentJail = strings.Trim(trimmed, "[]")
|
||||||
|
outputLines = append(outputLines, line)
|
||||||
|
} else if strings.HasPrefix(trimmed, "enabled") {
|
||||||
|
if val, ok := updates[currentJail]; ok {
|
||||||
|
outputLines = append(outputLines, fmt.Sprintf("enabled = %t", val))
|
||||||
|
// Remove the update from map to mark it as processed.
|
||||||
|
delete(updates, currentJail)
|
||||||
|
} else {
|
||||||
|
outputLines = append(outputLines, line)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
outputLines = append(outputLines, line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// For any jails in updates that did not have an "enabled" line, append it.
|
||||||
|
for jail, val := range updates {
|
||||||
|
outputLines = append(outputLines, fmt.Sprintf("[%s]", jail))
|
||||||
|
outputLines = append(outputLines, fmt.Sprintf("enabled = %t", val))
|
||||||
|
}
|
||||||
|
newContent := strings.Join(outputLines, "\n")
|
||||||
|
return os.WriteFile(path, []byte(newContent), 0644)
|
||||||
|
}
|
||||||
@@ -64,6 +64,8 @@
|
|||||||
"modal.filter_config": "Filter-Konfiguration:",
|
"modal.filter_config": "Filter-Konfiguration:",
|
||||||
"modal.cancel": "Abbrechen",
|
"modal.cancel": "Abbrechen",
|
||||||
"modal.save": "Speichern",
|
"modal.save": "Speichern",
|
||||||
"loading": "Lade..."
|
"loading": "Lade...",
|
||||||
|
"dashboard.manage_jails": "Jails verwalten",
|
||||||
|
"modal.manage_jails_title": "Jails verwalten"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,6 +64,8 @@
|
|||||||
"modal.filter_config": "Filter-Konfiguration:",
|
"modal.filter_config": "Filter-Konfiguration:",
|
||||||
"modal.cancel": "Abbräche",
|
"modal.cancel": "Abbräche",
|
||||||
"modal.save": "Speicherä",
|
"modal.save": "Speicherä",
|
||||||
"loading": "Lade..."
|
"loading": "Lade...",
|
||||||
|
"dashboard.manage_jails": "Jails ala oder absteue",
|
||||||
|
"modal.manage_jails_title": "Jails ala oder absteue"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,6 +64,8 @@
|
|||||||
"modal.filter_config": "Filter Config:",
|
"modal.filter_config": "Filter Config:",
|
||||||
"modal.cancel": "Cancel",
|
"modal.cancel": "Cancel",
|
||||||
"modal.save": "Save",
|
"modal.save": "Save",
|
||||||
"loading": "Loading..."
|
"loading": "Loading...",
|
||||||
|
"dashboard.manage_jails": "Manage Jails",
|
||||||
|
"modal.manage_jails_title": "Manage Jails"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,5 +64,7 @@
|
|||||||
"modal.filter_config": "Configuración del filtro:",
|
"modal.filter_config": "Configuración del filtro:",
|
||||||
"modal.cancel": "Cancelar",
|
"modal.cancel": "Cancelar",
|
||||||
"modal.save": "Guardar",
|
"modal.save": "Guardar",
|
||||||
"loading": "Cargando..."
|
"loading": "Cargando...",
|
||||||
|
"dashboard.manage_jails": "Administrar jails",
|
||||||
|
"modal.manage_jails_title": "Administrar jails"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,5 +64,7 @@
|
|||||||
"modal.filter_config": "Configuration du filtre:",
|
"modal.filter_config": "Configuration du filtre:",
|
||||||
"modal.cancel": "Annuler",
|
"modal.cancel": "Annuler",
|
||||||
"modal.save": "Enregistrer",
|
"modal.save": "Enregistrer",
|
||||||
"loading": "Chargement..."
|
"loading": "Chargement...",
|
||||||
|
"dashboard.manage_jails": "Gérer les jails",
|
||||||
|
"modal.manage_jails_title": "Gérer les jails"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,5 +64,7 @@
|
|||||||
"modal.filter_config": "Configurazione del filtro:",
|
"modal.filter_config": "Configurazione del filtro:",
|
||||||
"modal.cancel": "Annulla",
|
"modal.cancel": "Annulla",
|
||||||
"modal.save": "Salva",
|
"modal.save": "Salva",
|
||||||
"loading": "Caricamento..."
|
"loading": "Caricamento...",
|
||||||
|
"dashboard.manage_jails": "Gestire i jails",
|
||||||
|
"modal.manage_jails_title": "Gestire i jails"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -233,7 +233,7 @@ func GetJailFilterConfigHandler(c *gin.Context) {
|
|||||||
config.DebugLog("----------------------------")
|
config.DebugLog("----------------------------")
|
||||||
config.DebugLog("GetJailFilterConfigHandler called (handlers.go)") // entry point
|
config.DebugLog("GetJailFilterConfigHandler called (handlers.go)") // entry point
|
||||||
jail := c.Param("jail")
|
jail := c.Param("jail")
|
||||||
cfg, err := fail2ban.GetJailConfig(jail)
|
cfg, err := fail2ban.GetFilterConfig(jail)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
return
|
return
|
||||||
@@ -260,7 +260,7 @@ func SetJailFilterConfigHandler(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Write the filter config file to /etc/fail2ban/filter.d/<jail>.conf
|
// Write the filter config file to /etc/fail2ban/filter.d/<jail>.conf
|
||||||
if err := fail2ban.SetJailConfig(jail, req.Config); err != nil {
|
if err := fail2ban.SetFilterConfig(jail, req.Config); err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -280,6 +280,49 @@ func SetJailFilterConfigHandler(c *gin.Context) {
|
|||||||
// })
|
// })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ManageJailsHandler returns a list of all jails (from jail.local and jail.d)
|
||||||
|
// including their enabled status.
|
||||||
|
func ManageJailsHandler(c *gin.Context) {
|
||||||
|
config.DebugLog("----------------------------")
|
||||||
|
config.DebugLog("ManageJailsHandler called (handlers.go)") // entry point
|
||||||
|
// Get all jails from jail.local and jail.d directories.
|
||||||
|
// This helper should parse both files and return []fail2ban.JailInfo.
|
||||||
|
jails, err := fail2ban.GetAllJails()
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to load jails: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{"jails": jails})
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateJailManagementHandler updates the enabled state for each jail.
|
||||||
|
// Expected JSON format: { "JailName1": true, "JailName2": false, ... }
|
||||||
|
// After updating, the Fail2ban service is restarted.
|
||||||
|
func UpdateJailManagementHandler(c *gin.Context) {
|
||||||
|
config.DebugLog("----------------------------")
|
||||||
|
config.DebugLog("UpdateJailManagementHandler called (handlers.go)") // entry point
|
||||||
|
var updates map[string]bool
|
||||||
|
if err := c.ShouldBindJSON(&updates); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid JSON: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Update jail configuration file(s) with the new enabled states.
|
||||||
|
if err := fail2ban.UpdateJailEnabledStates(updates); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update jail settings: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Restart the Fail2ban service.
|
||||||
|
//if err := fail2ban.RestartFail2ban(); err != nil {
|
||||||
|
// c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to reload fail2ban: " + err.Error()})
|
||||||
|
// return
|
||||||
|
//}
|
||||||
|
if err := config.MarkReloadNeeded(); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{"message": "Jail settings updated successfully"})
|
||||||
|
}
|
||||||
|
|
||||||
// GetSettingsHandler returns the entire AppSettings struct as JSON
|
// GetSettingsHandler returns the entire AppSettings struct as JSON
|
||||||
func GetSettingsHandler(c *gin.Context) {
|
func GetSettingsHandler(c *gin.Context) {
|
||||||
config.DebugLog("----------------------------")
|
config.DebugLog("----------------------------")
|
||||||
@@ -429,7 +472,7 @@ func sendEmail(to, subject, body string, settings config.AppSettings) error {
|
|||||||
smtpHost := settings.SMTP.Host
|
smtpHost := settings.SMTP.Host
|
||||||
smtpPort := settings.SMTP.Port
|
smtpPort := settings.SMTP.Port
|
||||||
auth := LoginAuth(settings.SMTP.Username, settings.SMTP.Password)
|
auth := LoginAuth(settings.SMTP.Username, settings.SMTP.Password)
|
||||||
smtpAddr := fmt.Sprintf("%s:%d", smtpHost, smtpPort)
|
smtpAddr := net.JoinHostPort(smtpHost, fmt.Sprintf("%d", smtpPort))
|
||||||
|
|
||||||
// **Choose Connection Type**
|
// **Choose Connection Type**
|
||||||
if smtpPort == 465 {
|
if smtpPort == 465 {
|
||||||
|
|||||||
@@ -34,10 +34,14 @@ func RegisterRoutes(r *gin.Engine) {
|
|||||||
api.GET("/summary", SummaryHandler)
|
api.GET("/summary", SummaryHandler)
|
||||||
api.POST("/jails/:jail/unban/:ip", UnbanIPHandler)
|
api.POST("/jails/:jail/unban/:ip", UnbanIPHandler)
|
||||||
|
|
||||||
// Config endpoints
|
// Routes for jail-filter management (TODO: rename API-call)
|
||||||
api.GET("/jails/:jail/config", GetJailFilterConfigHandler)
|
api.GET("/jails/:jail/config", GetJailFilterConfigHandler)
|
||||||
api.POST("/jails/:jail/config", SetJailFilterConfigHandler)
|
api.POST("/jails/:jail/config", SetJailFilterConfigHandler)
|
||||||
|
|
||||||
|
// Routes for jail management
|
||||||
|
api.GET("/jails/manage", ManageJailsHandler)
|
||||||
|
api.POST("/jails/manage", UpdateJailManagementHandler)
|
||||||
|
|
||||||
// Settings endpoints
|
// Settings endpoints
|
||||||
api.GET("/settings", GetSettingsHandler)
|
api.GET("/settings", GetSettingsHandler)
|
||||||
api.POST("/settings", UpdateSettingsHandler)
|
api.POST("/settings", UpdateSettingsHandler)
|
||||||
@@ -46,6 +50,7 @@ func RegisterRoutes(r *gin.Engine) {
|
|||||||
// Filter debugger endpoints
|
// Filter debugger endpoints
|
||||||
api.GET("/filters", ListFiltersHandler)
|
api.GET("/filters", ListFiltersHandler)
|
||||||
api.POST("/filters/test", TestFilterHandler)
|
api.POST("/filters/test", TestFilterHandler)
|
||||||
|
|
||||||
// TODO: create or generate new filters
|
// TODO: create or generate new filters
|
||||||
// api.POST("/filters/generate", GenerateFilterHandler)
|
// api.POST("/filters/generate", GenerateFilterHandler)
|
||||||
|
|
||||||
|
|||||||
@@ -107,7 +107,10 @@
|
|||||||
|
|
||||||
<!-- Dashboard Section -->
|
<!-- Dashboard Section -->
|
||||||
<div id="dashboardSection" class="container my-4">
|
<div id="dashboardSection" class="container my-4">
|
||||||
<h1 class="mb-4" data-i18n="dashboard.title">Dashboard</h1>
|
<div class="d-flex align-items-center" style="position: relative;">
|
||||||
|
<h1 class="mb-4 flex-grow-1" data-i18n="dashboard.title">Dashboard</h1>
|
||||||
|
<button class="btn btn-outline-secondary" style="position: absolute; right: 0; top: 0;" onclick="openManageJailsModal()" data-i18n="dashboard.manage_jails">Manage Jails</button>
|
||||||
|
</div>
|
||||||
<div id="dashboard"></div>
|
<div id="dashboard"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -149,6 +152,12 @@
|
|||||||
<option value="de_ch">Schwiizerdütsch</option>
|
<option value="de_ch">Schwiizerdütsch</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Fail2Ban UI Port (server) -->
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="uiPort" class="form-label" data-i18n="settings.server_port">Server Port</label>
|
||||||
|
<input type="number" class="form-control" id="uiPort"
|
||||||
|
data-i18n-placeholder="settings.server_port_placeholder" placeholder="e.g., 8080" required min="80" max="65535" required />
|
||||||
|
</div>
|
||||||
<!-- Debug Log Output -->
|
<!-- Debug Log Output -->
|
||||||
<div class="mb-3 form-check">
|
<div class="mb-3 form-check">
|
||||||
<input type="checkbox" class="form-check-input" id="debugMode">
|
<input type="checkbox" class="form-check-input" id="debugMode">
|
||||||
@@ -482,6 +491,26 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Manage Jails Modal -->
|
||||||
|
<div class="modal fade" id="manageJailsModal" tabindex="-1" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-dialog-scrollable">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" data-i18n="modal.manage_jails_title">Manage Jails</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<!-- Dynamically filled list of jails with toggle switches -->
|
||||||
|
<div id="jailsList"></div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" data-i18n="modal.cancel">Cancel</button>
|
||||||
|
<button type="button" class="btn btn-primary" onclick="saveManageJails()" data-i18n="modal.save">Save Changes</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<!-- ******************************************************************* -->
|
<!-- ******************************************************************* -->
|
||||||
|
|
||||||
<!-- Bootstrap 5 JS (for modal, etc.) -->
|
<!-- Bootstrap 5 JS (for modal, etc.) -->
|
||||||
@@ -814,6 +843,80 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Function: openManageJailsModal
|
||||||
|
// Fetches the full-list of all jails (from /jails/manage) and builds a list with toggle switches.
|
||||||
|
function openManageJailsModal() {
|
||||||
|
showLoading(true);
|
||||||
|
fetch('/api/jails/manage')
|
||||||
|
.then(function(res) { return res.json(); })
|
||||||
|
.then(function(data) {
|
||||||
|
if (!data.jails || data.jails.length === 0) {
|
||||||
|
alert("No jails found.");
|
||||||
|
showLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var html = '<div class="list-group">';
|
||||||
|
data.jails.forEach(function(jail) {
|
||||||
|
var isEnabled = (jail.enabled === true);
|
||||||
|
html += '<div class="list-group-item d-flex justify-content-between align-items-center">';
|
||||||
|
html += '<span>' + jail.jailName + '</span>';
|
||||||
|
html += '<div class="form-check form-switch">';
|
||||||
|
html += '<input class="form-check-input" type="checkbox" id="toggle-' + jail.jailName + '" ' + (isEnabled ? 'checked' : '') + '>';
|
||||||
|
html += '</div>';
|
||||||
|
html += '</div>';
|
||||||
|
});
|
||||||
|
html += '</div>';
|
||||||
|
document.getElementById('jailsList').innerHTML = html;
|
||||||
|
var modalEl = document.getElementById('manageJailsModal');
|
||||||
|
var manageJailsModal = new bootstrap.Modal(modalEl);
|
||||||
|
manageJailsModal.show();
|
||||||
|
})
|
||||||
|
.catch(function(err) {
|
||||||
|
alert("Error fetching jails: " + err);
|
||||||
|
})
|
||||||
|
.finally(function() {
|
||||||
|
showLoading(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function: saveManageJails
|
||||||
|
// Collects the toggled states from the Manage Jails modal and sends updates to the API.
|
||||||
|
function saveManageJails() {
|
||||||
|
showLoading(true);
|
||||||
|
var updatedJails = {};
|
||||||
|
$('.list-group-item').each(function() {
|
||||||
|
var jailName = $(this).find('span').text();
|
||||||
|
var isEnabled = $(this).find('input[type="checkbox"]').is(':checked');
|
||||||
|
updatedJails[jailName] = isEnabled;
|
||||||
|
});
|
||||||
|
// Send updated states to the API endpoint /api/jails/manage.
|
||||||
|
fetch('/api/jails/manage', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(updatedJails),
|
||||||
|
})
|
||||||
|
.then(function(res) { return res.json(); })
|
||||||
|
.then(function(data) {
|
||||||
|
if (data.error) {
|
||||||
|
alert("Error saving jail settings: " + data.error);
|
||||||
|
} else {
|
||||||
|
// A restart of fail2ban is needed, to enable or disable jails - a reload is not enough
|
||||||
|
document.getElementById('reloadBanner').style.display = 'block';
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(function(err) {
|
||||||
|
alert("Error: " + err);
|
||||||
|
})
|
||||||
|
.finally(function() {
|
||||||
|
showLoading(false);
|
||||||
|
var modalEl = document.getElementById('manageJailsModal');
|
||||||
|
var manageJailsModal = bootstrap.Modal.getInstance(modalEl);
|
||||||
|
if (manageJailsModal) {
|
||||||
|
manageJailsModal.hide();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
//*******************************************************************
|
//*******************************************************************
|
||||||
//* Load current settings when opening settings page : *
|
//* Load current settings when opening settings page : *
|
||||||
//*******************************************************************
|
//*******************************************************************
|
||||||
@@ -824,6 +927,7 @@
|
|||||||
.then(res => res.json())
|
.then(res => res.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
document.getElementById('languageSelect').value = data.language || 'en';
|
document.getElementById('languageSelect').value = data.language || 'en';
|
||||||
|
document.getElementById('uiPort').value = data.port || 8080,
|
||||||
document.getElementById('debugMode').checked = data.debug || false;
|
document.getElementById('debugMode').checked = data.debug || false;
|
||||||
|
|
||||||
document.getElementById('destEmail').value = data.destemail || '';
|
document.getElementById('destEmail').value = data.destemail || '';
|
||||||
@@ -886,6 +990,7 @@
|
|||||||
|
|
||||||
const settingsData = {
|
const settingsData = {
|
||||||
language: document.getElementById('languageSelect').value,
|
language: document.getElementById('languageSelect').value,
|
||||||
|
port: parseInt(document.getElementById('uiPort').value, 10) || 8080,
|
||||||
debug: document.getElementById('debugMode').checked,
|
debug: document.getElementById('debugMode').checked,
|
||||||
destemail: document.getElementById('destEmail').value.trim(),
|
destemail: document.getElementById('destEmail').value.trim(),
|
||||||
alertCountries: selectedCountries.length > 0 ? selectedCountries : ["ALL"],
|
alertCountries: selectedCountries.length > 0 ? selectedCountries : ["ALL"],
|
||||||
|
|||||||
Reference in New Issue
Block a user