switch to toast instead of alert messages, implement serverIDs and restart tracking for every remote-server

This commit is contained in:
2025-11-14 10:22:44 +01:00
parent bff920e5b3
commit a24e0779d2
7 changed files with 359 additions and 188 deletions

View File

@@ -77,6 +77,7 @@ const (
jailDFile = "/etc/fail2ban/jail.d/ui-custom-action.conf"
actionFile = "/etc/fail2ban/action.d/ui-custom-action.conf"
actionCallbackPlaceholder = "__CALLBACK_URL__"
actionServerIDPlaceholder = "__SERVER_ID__"
)
const fail2banActionTemplate = `[INCLUDES]
@@ -95,13 +96,14 @@ norestored = 1
actionban = /usr/bin/curl -X POST __CALLBACK_URL__/api/ban \
-H "Content-Type: application/json" \
-d "$(jq -n --arg ip '<ip>' \
-d "$(jq -n --arg serverId '__SERVER_ID__' \
--arg ip '<ip>' \
--arg jail '<name>' \
--arg hostname '<fq-hostname>' \
--arg failures '<failures>' \
--arg whois "$(whois <ip> || echo 'missing whois program')" \
--arg logs "$(tac <logpath> | grep <grepopts> -wF <ip>)" \
'{ip: $ip, jail: $jail, hostname: $hostname, failures: $failures, whois: $whois, logs: $logs}')"
'{serverId: $serverId, ip: $ip, jail: $jail, hostname: $hostname, failures: $failures, whois: $whois, logs: $logs}')"
[Init]
@@ -128,23 +130,24 @@ var (
// Fail2banServer represents a Fail2ban instance the UI can manage.
type Fail2banServer struct {
ID string `json:"id"`
Name string `json:"name"`
Type string `json:"type"` // local, ssh, agent
Host string `json:"host,omitempty"`
Port int `json:"port,omitempty"`
SocketPath string `json:"socketPath,omitempty"`
LogPath string `json:"logPath,omitempty"`
SSHUser string `json:"sshUser,omitempty"`
SSHKeyPath string `json:"sshKeyPath,omitempty"`
AgentURL string `json:"agentUrl,omitempty"`
AgentSecret string `json:"agentSecret,omitempty"`
Hostname string `json:"hostname,omitempty"`
Tags []string `json:"tags,omitempty"`
IsDefault bool `json:"isDefault"`
Enabled bool `json:"enabled"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
ID string `json:"id"`
Name string `json:"name"`
Type string `json:"type"` // local, ssh, agent
Host string `json:"host,omitempty"`
Port int `json:"port,omitempty"`
SocketPath string `json:"socketPath,omitempty"`
LogPath string `json:"logPath,omitempty"`
SSHUser string `json:"sshUser,omitempty"`
SSHKeyPath string `json:"sshKeyPath,omitempty"`
AgentURL string `json:"agentUrl,omitempty"`
AgentSecret string `json:"agentSecret,omitempty"`
Hostname string `json:"hostname,omitempty"`
Tags []string `json:"tags,omitempty"`
IsDefault bool `json:"isDefault"`
Enabled bool `json:"enabled"`
RestartNeeded bool `json:"restartNeeded"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
enabledSet bool
}
@@ -308,24 +311,25 @@ func applyServerRecordsLocked(records []storage.ServerRecord) {
_ = json.Unmarshal([]byte(rec.TagsJSON), &tags)
}
server := Fail2banServer{
ID: rec.ID,
Name: rec.Name,
Type: rec.Type,
Host: rec.Host,
Port: rec.Port,
SocketPath: rec.SocketPath,
LogPath: rec.LogPath,
SSHUser: rec.SSHUser,
SSHKeyPath: rec.SSHKeyPath,
AgentURL: rec.AgentURL,
AgentSecret: rec.AgentSecret,
Hostname: rec.Hostname,
Tags: tags,
IsDefault: rec.IsDefault,
Enabled: rec.Enabled,
CreatedAt: rec.CreatedAt,
UpdatedAt: rec.UpdatedAt,
enabledSet: true,
ID: rec.ID,
Name: rec.Name,
Type: rec.Type,
Host: rec.Host,
Port: rec.Port,
SocketPath: rec.SocketPath,
LogPath: rec.LogPath,
SSHUser: rec.SSHUser,
SSHKeyPath: rec.SSHKeyPath,
AgentURL: rec.AgentURL,
AgentSecret: rec.AgentSecret,
Hostname: rec.Hostname,
Tags: tags,
IsDefault: rec.IsDefault,
Enabled: rec.Enabled,
RestartNeeded: rec.NeedsRestart,
CreatedAt: rec.CreatedAt,
UpdatedAt: rec.UpdatedAt,
enabledSet: true,
}
servers = append(servers, server)
}
@@ -384,23 +388,24 @@ func toServerRecordsLocked() ([]storage.ServerRecord, error) {
updatedAt = createdAt
}
records = append(records, storage.ServerRecord{
ID: srv.ID,
Name: srv.Name,
Type: srv.Type,
Host: srv.Host,
Port: srv.Port,
SocketPath: srv.SocketPath,
LogPath: srv.LogPath,
SSHUser: srv.SSHUser,
SSHKeyPath: srv.SSHKeyPath,
AgentURL: srv.AgentURL,
AgentSecret: srv.AgentSecret,
Hostname: srv.Hostname,
TagsJSON: string(tagBytes),
IsDefault: srv.IsDefault,
Enabled: srv.Enabled,
CreatedAt: createdAt,
UpdatedAt: updatedAt,
ID: srv.ID,
Name: srv.Name,
Type: srv.Type,
Host: srv.Host,
Port: srv.Port,
SocketPath: srv.SocketPath,
LogPath: srv.LogPath,
SSHUser: srv.SSHUser,
SSHKeyPath: srv.SSHKeyPath,
AgentURL: srv.AgentURL,
AgentSecret: srv.AgentSecret,
Hostname: srv.Hostname,
TagsJSON: string(tagBytes),
IsDefault: srv.IsDefault,
Enabled: srv.Enabled,
NeedsRestart: srv.RestartNeeded,
CreatedAt: createdAt,
UpdatedAt: updatedAt,
})
}
return records, nil
@@ -563,6 +568,9 @@ func normalizeServersLocked() {
}
}
server.enabledSet = true
if !server.Enabled {
server.RestartNeeded = false
}
if server.IsDefault && !server.Enabled {
server.IsDefault = false
}
@@ -584,6 +592,8 @@ func normalizeServersLocked() {
sort.SliceStable(currentSettings.Servers, func(i, j int) bool {
return currentSettings.Servers[i].CreatedAt.Before(currentSettings.Servers[j].CreatedAt)
})
updateGlobalRestartFlagLocked()
}
func generateServerID() string {
@@ -595,7 +605,7 @@ func generateServerID() string {
}
// ensureFail2banActionFiles writes the local action files if Fail2ban is present.
func ensureFail2banActionFiles(callbackURL string) error {
func ensureFail2banActionFiles(callbackURL, serverID string) error {
DebugLog("----------------------------")
DebugLog("ensureFail2banActionFiles called (settings.go)")
@@ -609,7 +619,7 @@ func ensureFail2banActionFiles(callbackURL string) error {
if err := ensureJailDConfig(); err != nil {
return err
}
return writeFail2banAction(callbackURL)
return writeFail2banAction(callbackURL, serverID)
}
// setupGeoCustomAction checks and replaces the default action in jail.local with our from fail2ban-UI
@@ -725,14 +735,14 @@ action_mwlg = %(action_)s
}
// writeFail2banAction creates or updates the action file with the AlertCountries.
func writeFail2banAction(callbackURL string) error {
func writeFail2banAction(callbackURL, serverID string) error {
DebugLog("Running initial writeFail2banAction()") // entry point
DebugLog("----------------------------")
if err := os.MkdirAll(filepath.Dir(actionFile), 0o755); err != nil {
return fmt.Errorf("failed to ensure action.d directory: %w", err)
}
actionConfig := BuildFail2banActionConfig(callbackURL)
actionConfig := BuildFail2banActionConfig(callbackURL, serverID)
err := os.WriteFile(actionFile, []byte(actionConfig), 0644)
if err != nil {
return fmt.Errorf("failed to write action file: %w", err)
@@ -751,12 +761,16 @@ func cloneServer(src Fail2banServer) Fail2banServer {
return dst
}
func BuildFail2banActionConfig(callbackURL string) string {
func BuildFail2banActionConfig(callbackURL, serverID string) string {
trimmed := strings.TrimRight(strings.TrimSpace(callbackURL), "/")
if trimmed == "" {
trimmed = "http://127.0.0.1:8080"
}
return strings.ReplaceAll(fail2banActionTemplate, actionCallbackPlaceholder, trimmed)
if serverID == "" {
serverID = "local"
}
config := strings.ReplaceAll(fail2banActionTemplate, actionCallbackPlaceholder, trimmed)
return strings.ReplaceAll(config, actionServerIDPlaceholder, serverID)
}
func getCallbackURLLocked() string {
@@ -786,7 +800,7 @@ func EnsureLocalFail2banAction(server Fail2banServer) error {
settingsLock.RLock()
callbackURL := getCallbackURLLocked()
settingsLock.RUnlock()
return ensureFail2banActionFiles(callbackURL)
return ensureFail2banActionFiles(callbackURL, server.ID)
}
func serverByIDLocked(id string) (Fail2banServer, bool) {
@@ -934,6 +948,35 @@ func clearDefaultLocked() {
}
}
func setServerRestartFlagLocked(serverID string, value bool) bool {
for idx := range currentSettings.Servers {
if currentSettings.Servers[idx].ID == serverID {
currentSettings.Servers[idx].RestartNeeded = value
return true
}
}
return false
}
func anyServerNeedsRestartLocked() bool {
for _, srv := range currentSettings.Servers {
if srv.RestartNeeded {
return true
}
}
return false
}
func updateGlobalRestartFlagLocked() {
currentSettings.RestartNeeded = anyServerNeedsRestartLocked()
}
func markAllServersRestartLocked() {
for idx := range currentSettings.Servers {
currentSettings.Servers[idx].RestartNeeded = true
}
}
// DeleteServer removes a server by ID.
func DeleteServer(id string) error {
settingsLock.Lock()
@@ -998,21 +1041,43 @@ func GetSettings() AppSettings {
return currentSettings
}
// MarkRestartNeeded sets restartNeeded = true and saves JSON
func MarkRestartNeeded() error {
// MarkRestartNeeded marks the specified server as requiring a restart.
func MarkRestartNeeded(serverID string) error {
settingsLock.Lock()
defer settingsLock.Unlock()
currentSettings.RestartNeeded = true
if serverID == "" {
return fmt.Errorf("server id must be provided")
}
if !setServerRestartFlagLocked(serverID, true) {
return fmt.Errorf("server %s not found", serverID)
}
updateGlobalRestartFlagLocked()
if err := persistServersLocked(); err != nil {
return err
}
return persistAppSettingsLocked()
}
// MarkRestartDone sets restartNeeded = false and saves JSON
func MarkRestartDone() error {
// MarkRestartDone marks the specified server as no longer requiring a restart.
func MarkRestartDone(serverID string) error {
settingsLock.Lock()
defer settingsLock.Unlock()
currentSettings.RestartNeeded = false
if serverID == "" {
return fmt.Errorf("server id must be provided")
}
if !setServerRestartFlagLocked(serverID, false) {
return fmt.Errorf("server %s not found", serverID)
}
updateGlobalRestartFlagLocked()
if err := persistServersLocked(); err != nil {
return err
}
return persistAppSettingsLocked()
}
@@ -1026,16 +1091,15 @@ func UpdateSettings(new AppSettings) (AppSettings, error) {
old := currentSettings
// If certain fields change, we mark reload needed
if old.BantimeIncrement != new.BantimeIncrement ||
restartTriggered := old.BantimeIncrement != new.BantimeIncrement ||
old.IgnoreIP != new.IgnoreIP ||
old.Bantime != new.Bantime ||
old.Findtime != new.Findtime ||
//old.Maxretry != new.Maxretry ||
old.Maxretry != new.Maxretry {
old.Maxretry != new.Maxretry
if restartTriggered {
new.RestartNeeded = true
} else {
// preserve previous RestartNeeded if it was already true
new.RestartNeeded = new.RestartNeeded || old.RestartNeeded
new.RestartNeeded = anyServerNeedsRestartLocked()
}
new.CallbackURL = strings.TrimSpace(new.CallbackURL)
@@ -1047,6 +1111,10 @@ func UpdateSettings(new AppSettings) (AppSettings, error) {
}
currentSettings = new
setDefaultsLocked()
if currentSettings.RestartNeeded && restartTriggered {
markAllServersRestartLocked()
updateGlobalRestartFlagLocked()
}
DebugLog("New settings applied: %v", currentSettings) // Log settings applied
if err := persistAllLocked(); err != nil {

View File

@@ -80,9 +80,18 @@ func ReloadFail2ban() error {
return conn.Reload(context.Background())
}
// RestartFail2ban restarts the Fail2ban service using the default connector.
func RestartFail2ban() error {
conn, err := GetManager().DefaultConnector()
// RestartFail2ban restarts the Fail2ban service using the provided server or default connector.
func RestartFail2ban(serverID string) error {
manager := GetManager()
var (
conn Connector
err error
)
if serverID != "" {
conn, err = manager.Connector(serverID)
} else {
conn, err = manager.DefaultConnector()
}
if err != nil {
return err
}

View File

@@ -63,7 +63,7 @@ func (ac *AgentConnector) Server() config.Fail2banServer {
func (ac *AgentConnector) ensureAction(ctx context.Context) error {
payload := map[string]any{
"name": "ui-custom-action",
"config": config.BuildFail2banActionConfig(config.GetCallbackURL()),
"config": config.BuildFail2banActionConfig(config.GetCallbackURL(), ac.server.ID),
"callbackUrl": config.GetCallbackURL(),
"setDefault": true,
}

View File

@@ -199,7 +199,7 @@ func (sc *SSHConnector) FetchBanEvents(ctx context.Context, limit int) ([]BanEve
func (sc *SSHConnector) ensureAction(ctx context.Context) error {
callbackURL := config.GetCallbackURL()
actionConfig := config.BuildFail2banActionConfig(callbackURL)
actionConfig := config.BuildFail2banActionConfig(callbackURL, sc.server.ID)
payload := base64.StdEncoding.EncodeToString([]byte(actionConfig))
script := strings.ReplaceAll(sshEnsureActionScript, "__PAYLOAD__", payload)
// Base64 encode the entire script to avoid shell escaping issues

View File

@@ -7,6 +7,7 @@ import (
"fmt"
"os"
"path/filepath"
"strings"
"sync"
"time"
@@ -67,23 +68,24 @@ type AppSettingsRecord struct {
}
type ServerRecord struct {
ID string
Name string
Type string
Host string
Port int
SocketPath string
LogPath string
SSHUser string
SSHKeyPath string
AgentURL string
AgentSecret string
Hostname string
TagsJSON string
IsDefault bool
Enabled bool
CreatedAt time.Time
UpdatedAt time.Time
ID string
Name string
Type string
Host string
Port int
SocketPath string
LogPath string
SSHUser string
SSHKeyPath string
AgentURL string
AgentSecret string
Hostname string
TagsJSON string
IsDefault bool
Enabled bool
NeedsRestart bool
CreatedAt time.Time
UpdatedAt time.Time
}
// BanEventRecord represents a single ban event stored in the internal database.
@@ -242,7 +244,7 @@ func ListServers(ctx context.Context) ([]ServerRecord, error) {
}
rows, err := db.QueryContext(ctx, `
SELECT id, name, type, host, port, socket_path, log_path, ssh_user, ssh_key_path, agent_url, agent_secret, hostname, tags, is_default, enabled, created_at, updated_at
SELECT id, name, type, host, port, socket_path, log_path, ssh_user, ssh_key_path, agent_url, agent_secret, hostname, tags, is_default, enabled, needs_restart, created_at, updated_at
FROM servers
ORDER BY created_at`)
if err != nil {
@@ -257,7 +259,7 @@ ORDER BY created_at`)
var name, serverType sql.NullString
var created, updated sql.NullString
var port sql.NullInt64
var isDefault, enabled sql.NullInt64
var isDefault, enabled, needsRestart sql.NullInt64
if err := rows.Scan(
&rec.ID,
@@ -275,6 +277,7 @@ ORDER BY created_at`)
&tags,
&isDefault,
&enabled,
&needsRestart,
&created,
&updated,
); err != nil {
@@ -295,6 +298,7 @@ ORDER BY created_at`)
rec.TagsJSON = stringFromNull(tags)
rec.IsDefault = intToBool(intFromNull(isDefault))
rec.Enabled = intToBool(intFromNull(enabled))
rec.NeedsRestart = intToBool(intFromNull(needsRestart))
if created.Valid {
if t, err := time.Parse(time.RFC3339Nano, created.String); err == nil {
@@ -334,9 +338,9 @@ func ReplaceServers(ctx context.Context, servers []ServerRecord) error {
stmt, err := tx.PrepareContext(ctx, `
INSERT INTO servers (
id, name, type, host, port, socket_path, log_path, ssh_user, ssh_key_path, agent_url, agent_secret, hostname, tags, is_default, enabled, created_at, updated_at
id, name, type, host, port, socket_path, log_path, ssh_user, ssh_key_path, agent_url, agent_secret, hostname, tags, is_default, enabled, needs_restart, created_at, updated_at
) VALUES (
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
)`)
if err != nil {
return err
@@ -368,6 +372,7 @@ INSERT INTO servers (
srv.TagsJSON,
boolToInt(srv.IsDefault),
boolToInt(srv.Enabled),
boolToInt(srv.NeedsRestart),
createdAt.Format(time.RFC3339Nano),
updatedAt.Format(time.RFC3339Nano),
); err != nil {
@@ -568,6 +573,7 @@ CREATE TABLE IF NOT EXISTS servers (
tags TEXT,
is_default INTEGER,
enabled INTEGER,
needs_restart INTEGER DEFAULT 0,
created_at TEXT,
updated_at TEXT
);
@@ -591,8 +597,18 @@ CREATE INDEX IF NOT EXISTS idx_ban_events_server_id ON ban_events(server_id);
CREATE INDEX IF NOT EXISTS idx_ban_events_occurred_at ON ban_events(occurred_at);
`
_, err := db.ExecContext(ctx, createTable)
return err
if _, err := db.ExecContext(ctx, createTable); err != nil {
return err
}
// Backfill needs_restart column for existing databases that predate it.
if _, err := db.ExecContext(ctx, `ALTER TABLE servers ADD COLUMN needs_restart INTEGER DEFAULT 0`); err != nil {
if !strings.Contains(strings.ToLower(err.Error()), "duplicate column name") {
return err
}
}
return nil
}
func ensureDirectory(path string) error {

View File

@@ -516,7 +516,12 @@ func GetJailFilterConfigHandler(c *gin.Context) {
config.DebugLog("----------------------------")
config.DebugLog("GetJailFilterConfigHandler called (handlers.go)") // entry point
jail := c.Param("jail")
cfg, err := fail2ban.GetFilterConfig(jail)
conn, err := resolveConnector(c)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
cfg, err := conn.GetFilterConfig(c.Request.Context(), jail)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
@@ -532,6 +537,11 @@ func SetJailFilterConfigHandler(c *gin.Context) {
config.DebugLog("----------------------------")
config.DebugLog("SetJailFilterConfigHandler called (handlers.go)") // entry point
jail := c.Param("jail")
conn, err := resolveConnector(c)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Parse JSON body (containing the new filter content)
var req struct {
@@ -542,25 +552,17 @@ func SetJailFilterConfigHandler(c *gin.Context) {
return
}
// Write the filter config file to /etc/fail2ban/filter.d/<jail>.conf
if err := fail2ban.SetFilterConfig(jail, req.Config); err != nil {
if err := conn.SetFilterConfig(c.Request.Context(), jail, req.Config); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// Mark reload needed in our UI settings
// if err := config.MarkRestartNeeded(); err != nil {
// c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
// return
// }
if err := conn.Reload(c.Request.Context()); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "filter saved but reload failed: " + err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "jail config updated"})
// Return a simple JSON response without forcing a blocking alert
// c.JSON(http.StatusOK, gin.H{
// "message": "Filter updated, reload needed",
// "restartNeeded": true,
// })
c.JSON(http.StatusOK, gin.H{"message": "Filter updated and fail2ban reloaded"})
}
// ManageJailsHandler returns a list of all jails (from jail.local and jail.d)
@@ -602,7 +604,7 @@ func UpdateJailManagementHandler(c *gin.Context) {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update jail settings: " + err.Error()})
return
}
if err := config.MarkRestartNeeded(); err != nil {
if err := config.MarkRestartNeeded(conn.Server().ID); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
@@ -739,17 +741,18 @@ func RestartFail2banHandler(c *gin.Context) {
config.DebugLog("----------------------------")
config.DebugLog("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()})
// return
// }
conn, err := resolveConnector(c)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
server := conn.Server()
// Attempt to restart the fail2ban service.
restartErr := fail2ban.RestartFail2ban()
restartErr := fail2ban.RestartFail2ban(server.ID)
if restartErr != nil {
// Check if running inside a container.
if _, container := os.LookupEnv("CONTAINER"); container {
if _, container := os.LookupEnv("CONTAINER"); container && server.Type == "local" {
// In a container, the restart command may fail (since fail2ban runs on the host).
// Log the error and continue, so we can mark the restart as done.
log.Printf("Warning: restart failed inside container (expected behavior): %v", restartErr)
@@ -761,7 +764,7 @@ func RestartFail2banHandler(c *gin.Context) {
}
// Only call MarkRestartDone if we either successfully restarted the service or we are in a container.
if err := config.MarkRestartDone(); err != nil {
if err := config.MarkRestartDone(server.ID); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}

View File

@@ -136,6 +136,48 @@
padding: 0.1em 0.2em;
border-radius: 0.25em;
}
/* Toast notifications */
#toast-container {
position: fixed;
top: 1.5rem;
right: 1.5rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
z-index: 10000;
pointer-events: none;
}
.toast {
min-width: 240px;
max-width: 360px;
padding: 0.75rem 1rem;
border-radius: 0.5rem;
color: #fff;
font-weight: 500;
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
opacity: 0;
transform: translateY(-6px);
transition: opacity 0.25s ease, transform 0.25s ease;
}
.toast.show {
opacity: 1;
transform: translateY(0);
}
.toast-success {
background-color: #047857;
}
.toast-error {
background-color: #b91c1c;
}
.toast-info {
background-color: #1d4ed8;
}
</style>
</head>
@@ -154,6 +196,8 @@
</div>
</div>
<div id="toast-container"></div>
<!-- ******************************************************************* -->
<!-- Navigation START -->
<!-- ******************************************************************* -->
@@ -821,7 +865,7 @@
getTranslationsSettingsOnPageload()
])
.then(function() {
checkRestartNeeded();
updateRestartBanner();
return refreshData({ silent: true });
})
.catch(function(err) {
@@ -854,6 +898,25 @@
}
}
function showToast(message, type) {
var container = document.getElementById('toast-container');
if (!container || !message) return;
var toast = document.createElement('div');
var variant = type || 'info';
toast.className = 'toast toast-' + variant;
toast.textContent = message;
container.appendChild(toast);
requestAnimationFrame(function() {
toast.classList.add('show');
});
setTimeout(function() {
toast.classList.remove('show');
setTimeout(function() {
toast.remove();
}, 300);
}, 5000);
}
// Fetch and display own external IP for webUI
function displayExternalIP() {
fetch('https://api.ipify.org?format=json')
@@ -905,18 +968,14 @@
}
}
// Check if there is still a reload of the fail2ban service needed
function checkRestartNeeded() {
fetch('/api/settings')
.then(res => res.json())
.then(data => {
if (data.restartNeeded) {
document.getElementById('restartBanner').style.display = 'block';
} else {
document.getElementById('restartBanner').style.display = 'none';
}
})
.catch(err => console.error('Error checking restartNeeded:', err));
function updateRestartBanner() {
var banner = document.getElementById('restartBanner');
if (!banner) return;
if (currentServer && currentServer.restartNeeded) {
banner.style.display = 'block';
} else {
banner.style.display = 'none';
}
}
// Load dynamically the other pages when navigating in nav
@@ -997,6 +1056,7 @@
}
renderServerSelector();
renderServerSubtitle();
updateRestartBanner();
})
.catch(function(err) {
console.error('Error loading servers:', err);
@@ -1005,6 +1065,7 @@
currentServer = null;
renderServerSelector();
renderServerSubtitle();
updateRestartBanner();
});
}
@@ -1088,6 +1149,7 @@
}
renderServerSelector();
renderServerSubtitle();
updateRestartBanner();
refreshData();
}
@@ -1643,10 +1705,10 @@
.then(function(res) { return res.json(); })
.then(function(data) {
if (data.error) {
alert('Error saving server: ' + data.error);
showToast('Error saving server: ' + (data.error || 'Unknown error'), 'error');
return;
}
alert(t('servers.form.success', 'Server saved successfully.'));
showToast(t('servers.form.success', 'Server saved successfully.'), 'success');
var saved = data.server || {};
currentServerId = saved.id || currentServerId;
return loadServers().then(function() {
@@ -1660,7 +1722,7 @@
});
})
.catch(function(err) {
alert('Error saving server: ' + err);
showToast('Error saving server: ' + err, 'error');
})
.finally(function() {
showLoading(false);
@@ -1733,7 +1795,7 @@
.then(function(res) { return res.json(); })
.then(function(data) {
if (data.error) {
alert('Error saving server: ' + data.error);
showToast('Error saving server: ' + (data.error || 'Unknown error'), 'error');
return;
}
if (!enabled && currentServerId === serverId) {
@@ -1748,7 +1810,7 @@
});
})
.catch(function(err) {
alert('Error saving server: ' + err);
showToast('Error saving server: ' + err, 'error');
})
.finally(function() {
showLoading(false);
@@ -1764,13 +1826,13 @@
.then(function(res) { return res.json(); })
.then(function(data) {
if (data.error) {
alert(t(data.messageKey || 'servers.actions.test_failure', data.error));
showToast(t(data.messageKey || 'servers.actions.test_failure', data.error), 'error');
return;
}
alert(t(data.messageKey || 'servers.actions.test_success', data.message || 'Connection successful'));
showToast(t(data.messageKey || 'servers.actions.test_success', data.message || 'Connection successful'), 'success');
})
.catch(function(err) {
alert(t('servers.actions.test_failure', 'Connection failed') + ': ' + err);
showToast(t('servers.actions.test_failure', 'Connection failed') + ': ' + err, 'error');
})
.finally(function() {
showLoading(false);
@@ -1784,7 +1846,7 @@
.then(function(res) { return res.json(); })
.then(function(data) {
if (data.error) {
alert('Error deleting server: ' + data.error);
showToast('Error deleting server: ' + (data.error || 'Unknown error'), 'error');
return;
}
if (currentServerId === serverId) {
@@ -1796,10 +1858,12 @@
renderServerSelector();
renderServerSubtitle();
return refreshData({ silent: true });
}).then(function() {
showToast(t('servers.actions.delete_success', 'Server removed'), 'success');
});
})
.catch(function(err) {
alert('Error deleting server: ' + err);
showToast('Error deleting server: ' + err, 'error');
})
.finally(function() {
showLoading(false);
@@ -1812,7 +1876,7 @@
.then(function(res) { return res.json(); })
.then(function(data) {
if (data.error) {
alert('Error setting default server: ' + data.error);
showToast('Error setting default server: ' + (data.error || 'Unknown error'), 'error');
return;
}
currentServerId = data.server ? data.server.id : serverId;
@@ -1821,10 +1885,12 @@
renderServerSelector();
renderServerSubtitle();
return refreshData({ silent: true });
}).then(function() {
showToast(t('servers.actions.set_default_success', 'Server set as default'), 'success');
});
})
.catch(function(err) {
alert('Error setting default server: ' + err);
showToast('Error setting default server: ' + err, 'error');
})
.finally(function() {
showLoading(false);
@@ -1910,14 +1976,14 @@
.then(function(res) { return res.json(); })
.then(function(data) {
if (data.error) {
alert("Error: " + data.error);
showToast("Error unbanning IP: " + data.error, 'error');
} else {
alert(data.message || "IP unbanned successfully");
showToast(data.message || "IP unbanned successfully", 'success');
}
return refreshData({ silent: true });
})
.catch(function(err) {
alert("Error: " + err);
showToast("Error: " + err, 'error');
})
.finally(function() {
showLoading(false);
@@ -1943,14 +2009,14 @@
.then(function(res) { return res.json(); })
.then(function(data) {
if (data.error) {
alert("Error loading config: " + data.error);
} else {
textArea.value = data.config;
openModal('jailConfigModal');
showToast("Error loading config: " + data.error, 'error');
return;
}
textArea.value = data.config;
openModal('jailConfigModal');
})
.catch(function(err) {
alert("Error: " + err);
showToast("Error: " + err, 'error');
})
.finally(function() {
showLoading(false);
@@ -1971,15 +2037,15 @@
.then(function(res) { return res.json(); })
.then(function(data) {
if (data.error) {
alert("Error saving config: " + data.error);
} else {
console.log("Filter saved successfully. Restart needed? " + data.restartNeeded);
closeModal('jailConfigModal');
document.getElementById('restartBanner').style.display = 'block';
showToast("Error saving config: " + data.error, 'error');
return;
}
closeModal('jailConfigModal');
showToast(t('filter_debug.save_success', 'Filter saved and reloaded'), 'success');
return refreshData({ silent: true });
})
.catch(function(err) {
alert("Error: " + err);
showToast("Error: " + err, 'error');
})
.finally(function() {
showLoading(false);
@@ -1990,7 +2056,7 @@
// Fetches the full-list of all jails (from /jails/manage) and builds a list with toggle switches.
function openManageJailsModal() {
if (!currentServerId) {
alert(t('servers.selector.none', 'Please add and select a Fail2ban server first.'));
showToast(t('servers.selector.none', 'Please add and select a Fail2ban server first.'), 'info');
return;
}
showLoading(true);
@@ -2000,7 +2066,7 @@
.then(res => res.json())
.then(data => {
if (!data.jails?.length) {
alert("No jails found.");
showToast("No jails found for this server.", 'info');
return;
}
@@ -2039,7 +2105,7 @@
document.getElementById('jailsList').innerHTML = html;
openModal('manageJailsModal');
})
.catch(err => alert("Error fetching jails: " + err))
.catch(err => showToast("Error fetching jails: " + err, 'error'))
.finally(() => showLoading(false));
}
@@ -2063,14 +2129,17 @@
.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('restartBanner').style.display = 'block';
showToast("Error saving jail settings: " + data.error, 'error');
return;
}
showToast(t('jails.manage.save_success', 'Jail settings saved. Please restart Fail2ban.'), 'info');
return loadServers().then(function() {
updateRestartBanner();
return refreshData({ silent: true });
});
})
.catch(function(err) {
alert("Error: " + err);
showToast("Error: " + err, 'error');
})
.finally(function() {
showLoading(false);
@@ -2126,7 +2195,7 @@
document.getElementById('ignoreIP').value = data.ignoreip || '';
})
.catch(err => {
alert('Error loading settings: ' + err);
showToast('Error loading settings: ' + err, 'error');
})
.finally(() => showLoading(false));
}
@@ -2173,17 +2242,20 @@
.then(res => res.json())
.then(data => {
if (data.error) {
alert('Error saving settings: ' + data.error + data.details);
showToast('Error saving settings: ' + (data.error + (data.details || '')), 'error');
} else {
var selectedLang = $('#languageSelect').val();
loadTranslations(selectedLang);
console.log("Settings saved successfully. Restart needed? " + data.restartNeeded);
showToast(t('settings.save_success', 'Settings saved'), 'success');
if (data.restartNeeded) {
document.getElementById('restartBanner').style.display = 'block';
loadServers().then(function() {
updateRestartBanner();
});
}
}
})
.catch(err => alert('Error: ' + err))
.catch(err => showToast('Error saving settings: ' + err, 'error'))
.finally(() => showLoading(false));
}
@@ -2199,7 +2271,7 @@
.then(res => res.json())
.then(data => {
if (data.error) {
alert('Error loading filters: ' + data.error);
showToast('Error loading filters: ' + data.error, 'error');
return;
}
const select = document.getElementById('filterSelect');
@@ -2229,7 +2301,7 @@
}
})
.catch(err => {
alert('Error loading filters: ' + err);
showToast('Error loading filters: ' + err, 'error');
})
.finally(() => showLoading(false));
}
@@ -2244,12 +2316,12 @@
.then(res => res.json())
.then(data => {
if (data.error) {
alert('Error sending test email: ' + data.error);
showToast('Error sending test email: ' + data.error, 'error');
} else {
alert('Test email sent successfully!');
showToast('Test email sent successfully!', 'success');
}
})
.catch(error => alert('Error: ' + error))
.catch(error => showToast('Error sending test email: ' + error, 'error'))
.finally(() => showLoading(false));
}
@@ -2259,7 +2331,7 @@
const lines = document.getElementById('logLinesTextarea').value.split('\n');
if (!filterName) {
alert('Please select a filter.');
showToast('Please select a filter.', 'info');
return;
}
@@ -2275,13 +2347,13 @@
.then(res => res.json())
.then(data => {
if (data.error) {
alert('Error: ' + data.error);
showToast('Error testing filter: ' + data.error, 'error');
return;
}
renderTestResults(data.matches);
})
.catch(err => {
alert('Error: ' + err);
showToast('Error testing filter: ' + err, 'error');
})
.finally(() => showLoading(false));
}
@@ -2332,14 +2404,17 @@
.then(function(res) { return res.json(); })
.then(function(data) {
if (data.error) {
alert("Error: " + data.error);
} else {
document.getElementById('restartBanner').style.display = 'none';
return refreshData({ silent: true });
showToast("Failed to restart Fail2ban: " + data.error, 'error');
return;
}
return loadServers().then(function() {
updateRestartBanner();
showToast(t('restart_banner.success', 'Fail2ban restart triggered'), 'success');
return refreshData({ silent: true });
});
})
.catch(function(err) {
alert("Error: " + err);
showToast("Failed to restart Fail2ban: " + err, 'error');
})
.finally(function() {
showLoading(false);