mirror of
https://github.com/swissmakers/fail2ban-ui.git
synced 2026-04-17 05:53:15 +02:00
switch to toast instead of alert messages, implement serverIDs and restart tracking for every remote-server
This commit is contained in:
@@ -77,6 +77,7 @@ const (
|
|||||||
jailDFile = "/etc/fail2ban/jail.d/ui-custom-action.conf"
|
jailDFile = "/etc/fail2ban/jail.d/ui-custom-action.conf"
|
||||||
actionFile = "/etc/fail2ban/action.d/ui-custom-action.conf"
|
actionFile = "/etc/fail2ban/action.d/ui-custom-action.conf"
|
||||||
actionCallbackPlaceholder = "__CALLBACK_URL__"
|
actionCallbackPlaceholder = "__CALLBACK_URL__"
|
||||||
|
actionServerIDPlaceholder = "__SERVER_ID__"
|
||||||
)
|
)
|
||||||
|
|
||||||
const fail2banActionTemplate = `[INCLUDES]
|
const fail2banActionTemplate = `[INCLUDES]
|
||||||
@@ -95,13 +96,14 @@ norestored = 1
|
|||||||
|
|
||||||
actionban = /usr/bin/curl -X POST __CALLBACK_URL__/api/ban \
|
actionban = /usr/bin/curl -X POST __CALLBACK_URL__/api/ban \
|
||||||
-H "Content-Type: application/json" \
|
-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 jail '<name>' \
|
||||||
--arg hostname '<fq-hostname>' \
|
--arg hostname '<fq-hostname>' \
|
||||||
--arg failures '<failures>' \
|
--arg failures '<failures>' \
|
||||||
--arg whois "$(whois <ip> || echo 'missing whois program')" \
|
--arg whois "$(whois <ip> || echo 'missing whois program')" \
|
||||||
--arg logs "$(tac <logpath> | grep <grepopts> -wF <ip>)" \
|
--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]
|
[Init]
|
||||||
|
|
||||||
@@ -143,6 +145,7 @@ type Fail2banServer struct {
|
|||||||
Tags []string `json:"tags,omitempty"`
|
Tags []string `json:"tags,omitempty"`
|
||||||
IsDefault bool `json:"isDefault"`
|
IsDefault bool `json:"isDefault"`
|
||||||
Enabled bool `json:"enabled"`
|
Enabled bool `json:"enabled"`
|
||||||
|
RestartNeeded bool `json:"restartNeeded"`
|
||||||
CreatedAt time.Time `json:"createdAt"`
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
UpdatedAt time.Time `json:"updatedAt"`
|
UpdatedAt time.Time `json:"updatedAt"`
|
||||||
|
|
||||||
@@ -323,6 +326,7 @@ func applyServerRecordsLocked(records []storage.ServerRecord) {
|
|||||||
Tags: tags,
|
Tags: tags,
|
||||||
IsDefault: rec.IsDefault,
|
IsDefault: rec.IsDefault,
|
||||||
Enabled: rec.Enabled,
|
Enabled: rec.Enabled,
|
||||||
|
RestartNeeded: rec.NeedsRestart,
|
||||||
CreatedAt: rec.CreatedAt,
|
CreatedAt: rec.CreatedAt,
|
||||||
UpdatedAt: rec.UpdatedAt,
|
UpdatedAt: rec.UpdatedAt,
|
||||||
enabledSet: true,
|
enabledSet: true,
|
||||||
@@ -399,6 +403,7 @@ func toServerRecordsLocked() ([]storage.ServerRecord, error) {
|
|||||||
TagsJSON: string(tagBytes),
|
TagsJSON: string(tagBytes),
|
||||||
IsDefault: srv.IsDefault,
|
IsDefault: srv.IsDefault,
|
||||||
Enabled: srv.Enabled,
|
Enabled: srv.Enabled,
|
||||||
|
NeedsRestart: srv.RestartNeeded,
|
||||||
CreatedAt: createdAt,
|
CreatedAt: createdAt,
|
||||||
UpdatedAt: updatedAt,
|
UpdatedAt: updatedAt,
|
||||||
})
|
})
|
||||||
@@ -563,6 +568,9 @@ func normalizeServersLocked() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
server.enabledSet = true
|
server.enabledSet = true
|
||||||
|
if !server.Enabled {
|
||||||
|
server.RestartNeeded = false
|
||||||
|
}
|
||||||
if server.IsDefault && !server.Enabled {
|
if server.IsDefault && !server.Enabled {
|
||||||
server.IsDefault = false
|
server.IsDefault = false
|
||||||
}
|
}
|
||||||
@@ -584,6 +592,8 @@ func normalizeServersLocked() {
|
|||||||
sort.SliceStable(currentSettings.Servers, func(i, j int) bool {
|
sort.SliceStable(currentSettings.Servers, func(i, j int) bool {
|
||||||
return currentSettings.Servers[i].CreatedAt.Before(currentSettings.Servers[j].CreatedAt)
|
return currentSettings.Servers[i].CreatedAt.Before(currentSettings.Servers[j].CreatedAt)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
updateGlobalRestartFlagLocked()
|
||||||
}
|
}
|
||||||
|
|
||||||
func generateServerID() string {
|
func generateServerID() string {
|
||||||
@@ -595,7 +605,7 @@ func generateServerID() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ensureFail2banActionFiles writes the local action files if Fail2ban is present.
|
// ensureFail2banActionFiles writes the local action files if Fail2ban is present.
|
||||||
func ensureFail2banActionFiles(callbackURL string) error {
|
func ensureFail2banActionFiles(callbackURL, serverID string) error {
|
||||||
DebugLog("----------------------------")
|
DebugLog("----------------------------")
|
||||||
DebugLog("ensureFail2banActionFiles called (settings.go)")
|
DebugLog("ensureFail2banActionFiles called (settings.go)")
|
||||||
|
|
||||||
@@ -609,7 +619,7 @@ func ensureFail2banActionFiles(callbackURL string) error {
|
|||||||
if err := ensureJailDConfig(); err != nil {
|
if err := ensureJailDConfig(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return writeFail2banAction(callbackURL)
|
return writeFail2banAction(callbackURL, serverID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// setupGeoCustomAction checks and replaces the default action in jail.local with our from fail2ban-UI
|
// 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.
|
// 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("Running initial writeFail2banAction()") // entry point
|
||||||
DebugLog("----------------------------")
|
DebugLog("----------------------------")
|
||||||
if err := os.MkdirAll(filepath.Dir(actionFile), 0o755); err != nil {
|
if err := os.MkdirAll(filepath.Dir(actionFile), 0o755); err != nil {
|
||||||
return fmt.Errorf("failed to ensure action.d directory: %w", err)
|
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)
|
err := os.WriteFile(actionFile, []byte(actionConfig), 0644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to write action file: %w", err)
|
return fmt.Errorf("failed to write action file: %w", err)
|
||||||
@@ -751,12 +761,16 @@ func cloneServer(src Fail2banServer) Fail2banServer {
|
|||||||
return dst
|
return dst
|
||||||
}
|
}
|
||||||
|
|
||||||
func BuildFail2banActionConfig(callbackURL string) string {
|
func BuildFail2banActionConfig(callbackURL, serverID string) string {
|
||||||
trimmed := strings.TrimRight(strings.TrimSpace(callbackURL), "/")
|
trimmed := strings.TrimRight(strings.TrimSpace(callbackURL), "/")
|
||||||
if trimmed == "" {
|
if trimmed == "" {
|
||||||
trimmed = "http://127.0.0.1:8080"
|
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 {
|
func getCallbackURLLocked() string {
|
||||||
@@ -786,7 +800,7 @@ func EnsureLocalFail2banAction(server Fail2banServer) error {
|
|||||||
settingsLock.RLock()
|
settingsLock.RLock()
|
||||||
callbackURL := getCallbackURLLocked()
|
callbackURL := getCallbackURLLocked()
|
||||||
settingsLock.RUnlock()
|
settingsLock.RUnlock()
|
||||||
return ensureFail2banActionFiles(callbackURL)
|
return ensureFail2banActionFiles(callbackURL, server.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func serverByIDLocked(id string) (Fail2banServer, bool) {
|
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.
|
// DeleteServer removes a server by ID.
|
||||||
func DeleteServer(id string) error {
|
func DeleteServer(id string) error {
|
||||||
settingsLock.Lock()
|
settingsLock.Lock()
|
||||||
@@ -998,21 +1041,43 @@ func GetSettings() AppSettings {
|
|||||||
return currentSettings
|
return currentSettings
|
||||||
}
|
}
|
||||||
|
|
||||||
// MarkRestartNeeded sets restartNeeded = true and saves JSON
|
// MarkRestartNeeded marks the specified server as requiring a restart.
|
||||||
func MarkRestartNeeded() error {
|
func MarkRestartNeeded(serverID string) error {
|
||||||
settingsLock.Lock()
|
settingsLock.Lock()
|
||||||
defer settingsLock.Unlock()
|
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()
|
return persistAppSettingsLocked()
|
||||||
}
|
}
|
||||||
|
|
||||||
// MarkRestartDone sets restartNeeded = false and saves JSON
|
// MarkRestartDone marks the specified server as no longer requiring a restart.
|
||||||
func MarkRestartDone() error {
|
func MarkRestartDone(serverID string) error {
|
||||||
settingsLock.Lock()
|
settingsLock.Lock()
|
||||||
defer settingsLock.Unlock()
|
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()
|
return persistAppSettingsLocked()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1026,16 +1091,15 @@ func UpdateSettings(new AppSettings) (AppSettings, error) {
|
|||||||
old := currentSettings
|
old := currentSettings
|
||||||
|
|
||||||
// If certain fields change, we mark reload needed
|
// If certain fields change, we mark reload needed
|
||||||
if old.BantimeIncrement != new.BantimeIncrement ||
|
restartTriggered := old.BantimeIncrement != new.BantimeIncrement ||
|
||||||
old.IgnoreIP != new.IgnoreIP ||
|
old.IgnoreIP != new.IgnoreIP ||
|
||||||
old.Bantime != new.Bantime ||
|
old.Bantime != new.Bantime ||
|
||||||
old.Findtime != new.Findtime ||
|
old.Findtime != new.Findtime ||
|
||||||
//old.Maxretry != new.Maxretry ||
|
old.Maxretry != new.Maxretry
|
||||||
old.Maxretry != new.Maxretry {
|
if restartTriggered {
|
||||||
new.RestartNeeded = true
|
new.RestartNeeded = true
|
||||||
} else {
|
} else {
|
||||||
// preserve previous RestartNeeded if it was already true
|
new.RestartNeeded = anyServerNeedsRestartLocked()
|
||||||
new.RestartNeeded = new.RestartNeeded || old.RestartNeeded
|
|
||||||
}
|
}
|
||||||
|
|
||||||
new.CallbackURL = strings.TrimSpace(new.CallbackURL)
|
new.CallbackURL = strings.TrimSpace(new.CallbackURL)
|
||||||
@@ -1047,6 +1111,10 @@ func UpdateSettings(new AppSettings) (AppSettings, error) {
|
|||||||
}
|
}
|
||||||
currentSettings = new
|
currentSettings = new
|
||||||
setDefaultsLocked()
|
setDefaultsLocked()
|
||||||
|
if currentSettings.RestartNeeded && restartTriggered {
|
||||||
|
markAllServersRestartLocked()
|
||||||
|
updateGlobalRestartFlagLocked()
|
||||||
|
}
|
||||||
DebugLog("New settings applied: %v", currentSettings) // Log settings applied
|
DebugLog("New settings applied: %v", currentSettings) // Log settings applied
|
||||||
|
|
||||||
if err := persistAllLocked(); err != nil {
|
if err := persistAllLocked(); err != nil {
|
||||||
|
|||||||
@@ -80,9 +80,18 @@ func ReloadFail2ban() error {
|
|||||||
return conn.Reload(context.Background())
|
return conn.Reload(context.Background())
|
||||||
}
|
}
|
||||||
|
|
||||||
// RestartFail2ban restarts the Fail2ban service using the default connector.
|
// RestartFail2ban restarts the Fail2ban service using the provided server or default connector.
|
||||||
func RestartFail2ban() error {
|
func RestartFail2ban(serverID string) error {
|
||||||
conn, err := GetManager().DefaultConnector()
|
manager := GetManager()
|
||||||
|
var (
|
||||||
|
conn Connector
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
if serverID != "" {
|
||||||
|
conn, err = manager.Connector(serverID)
|
||||||
|
} else {
|
||||||
|
conn, err = manager.DefaultConnector()
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ func (ac *AgentConnector) Server() config.Fail2banServer {
|
|||||||
func (ac *AgentConnector) ensureAction(ctx context.Context) error {
|
func (ac *AgentConnector) ensureAction(ctx context.Context) error {
|
||||||
payload := map[string]any{
|
payload := map[string]any{
|
||||||
"name": "ui-custom-action",
|
"name": "ui-custom-action",
|
||||||
"config": config.BuildFail2banActionConfig(config.GetCallbackURL()),
|
"config": config.BuildFail2banActionConfig(config.GetCallbackURL(), ac.server.ID),
|
||||||
"callbackUrl": config.GetCallbackURL(),
|
"callbackUrl": config.GetCallbackURL(),
|
||||||
"setDefault": true,
|
"setDefault": true,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -199,7 +199,7 @@ func (sc *SSHConnector) FetchBanEvents(ctx context.Context, limit int) ([]BanEve
|
|||||||
|
|
||||||
func (sc *SSHConnector) ensureAction(ctx context.Context) error {
|
func (sc *SSHConnector) ensureAction(ctx context.Context) error {
|
||||||
callbackURL := config.GetCallbackURL()
|
callbackURL := config.GetCallbackURL()
|
||||||
actionConfig := config.BuildFail2banActionConfig(callbackURL)
|
actionConfig := config.BuildFail2banActionConfig(callbackURL, sc.server.ID)
|
||||||
payload := base64.StdEncoding.EncodeToString([]byte(actionConfig))
|
payload := base64.StdEncoding.EncodeToString([]byte(actionConfig))
|
||||||
script := strings.ReplaceAll(sshEnsureActionScript, "__PAYLOAD__", payload)
|
script := strings.ReplaceAll(sshEnsureActionScript, "__PAYLOAD__", payload)
|
||||||
// Base64 encode the entire script to avoid shell escaping issues
|
// Base64 encode the entire script to avoid shell escaping issues
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -82,6 +83,7 @@ type ServerRecord struct {
|
|||||||
TagsJSON string
|
TagsJSON string
|
||||||
IsDefault bool
|
IsDefault bool
|
||||||
Enabled bool
|
Enabled bool
|
||||||
|
NeedsRestart bool
|
||||||
CreatedAt time.Time
|
CreatedAt time.Time
|
||||||
UpdatedAt time.Time
|
UpdatedAt time.Time
|
||||||
}
|
}
|
||||||
@@ -242,7 +244,7 @@ func ListServers(ctx context.Context) ([]ServerRecord, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
rows, err := db.QueryContext(ctx, `
|
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
|
FROM servers
|
||||||
ORDER BY created_at`)
|
ORDER BY created_at`)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -257,7 +259,7 @@ ORDER BY created_at`)
|
|||||||
var name, serverType sql.NullString
|
var name, serverType sql.NullString
|
||||||
var created, updated sql.NullString
|
var created, updated sql.NullString
|
||||||
var port sql.NullInt64
|
var port sql.NullInt64
|
||||||
var isDefault, enabled sql.NullInt64
|
var isDefault, enabled, needsRestart sql.NullInt64
|
||||||
|
|
||||||
if err := rows.Scan(
|
if err := rows.Scan(
|
||||||
&rec.ID,
|
&rec.ID,
|
||||||
@@ -275,6 +277,7 @@ ORDER BY created_at`)
|
|||||||
&tags,
|
&tags,
|
||||||
&isDefault,
|
&isDefault,
|
||||||
&enabled,
|
&enabled,
|
||||||
|
&needsRestart,
|
||||||
&created,
|
&created,
|
||||||
&updated,
|
&updated,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
@@ -295,6 +298,7 @@ ORDER BY created_at`)
|
|||||||
rec.TagsJSON = stringFromNull(tags)
|
rec.TagsJSON = stringFromNull(tags)
|
||||||
rec.IsDefault = intToBool(intFromNull(isDefault))
|
rec.IsDefault = intToBool(intFromNull(isDefault))
|
||||||
rec.Enabled = intToBool(intFromNull(enabled))
|
rec.Enabled = intToBool(intFromNull(enabled))
|
||||||
|
rec.NeedsRestart = intToBool(intFromNull(needsRestart))
|
||||||
|
|
||||||
if created.Valid {
|
if created.Valid {
|
||||||
if t, err := time.Parse(time.RFC3339Nano, created.String); err == nil {
|
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, `
|
stmt, err := tx.PrepareContext(ctx, `
|
||||||
INSERT INTO servers (
|
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 (
|
) VALUES (
|
||||||
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
|
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
|
||||||
)`)
|
)`)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -368,6 +372,7 @@ INSERT INTO servers (
|
|||||||
srv.TagsJSON,
|
srv.TagsJSON,
|
||||||
boolToInt(srv.IsDefault),
|
boolToInt(srv.IsDefault),
|
||||||
boolToInt(srv.Enabled),
|
boolToInt(srv.Enabled),
|
||||||
|
boolToInt(srv.NeedsRestart),
|
||||||
createdAt.Format(time.RFC3339Nano),
|
createdAt.Format(time.RFC3339Nano),
|
||||||
updatedAt.Format(time.RFC3339Nano),
|
updatedAt.Format(time.RFC3339Nano),
|
||||||
); err != nil {
|
); err != nil {
|
||||||
@@ -568,6 +573,7 @@ CREATE TABLE IF NOT EXISTS servers (
|
|||||||
tags TEXT,
|
tags TEXT,
|
||||||
is_default INTEGER,
|
is_default INTEGER,
|
||||||
enabled INTEGER,
|
enabled INTEGER,
|
||||||
|
needs_restart INTEGER DEFAULT 0,
|
||||||
created_at TEXT,
|
created_at TEXT,
|
||||||
updated_at TEXT
|
updated_at TEXT
|
||||||
);
|
);
|
||||||
@@ -591,10 +597,20 @@ 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);
|
CREATE INDEX IF NOT EXISTS idx_ban_events_occurred_at ON ban_events(occurred_at);
|
||||||
`
|
`
|
||||||
|
|
||||||
_, err := db.ExecContext(ctx, createTable)
|
if _, err := db.ExecContext(ctx, createTable); err != nil {
|
||||||
return err
|
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 {
|
func ensureDirectory(path string) error {
|
||||||
if path == ":memory:" {
|
if path == ":memory:" {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -516,7 +516,12 @@ 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.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 {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
return
|
return
|
||||||
@@ -532,6 +537,11 @@ func SetJailFilterConfigHandler(c *gin.Context) {
|
|||||||
config.DebugLog("----------------------------")
|
config.DebugLog("----------------------------")
|
||||||
config.DebugLog("SetJailFilterConfigHandler called (handlers.go)") // entry point
|
config.DebugLog("SetJailFilterConfigHandler called (handlers.go)") // entry point
|
||||||
jail := c.Param("jail")
|
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)
|
// Parse JSON body (containing the new filter content)
|
||||||
var req struct {
|
var req struct {
|
||||||
@@ -542,25 +552,17 @@ func SetJailFilterConfigHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write the filter config file to /etc/fail2ban/filter.d/<jail>.conf
|
if err := conn.SetFilterConfig(c.Request.Context(), 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
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mark reload needed in our UI settings
|
if err := conn.Reload(c.Request.Context()); err != nil {
|
||||||
// if err := config.MarkRestartNeeded(); err != nil {
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "filter saved but reload failed: " + err.Error()})
|
||||||
// c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
return
|
||||||
// return
|
}
|
||||||
// }
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{"message": "jail config updated"})
|
c.JSON(http.StatusOK, gin.H{"message": "Filter updated and fail2ban reloaded"})
|
||||||
|
|
||||||
// Return a simple JSON response without forcing a blocking alert
|
|
||||||
// c.JSON(http.StatusOK, gin.H{
|
|
||||||
// "message": "Filter updated, reload needed",
|
|
||||||
// "restartNeeded": true,
|
|
||||||
// })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ManageJailsHandler returns a list of all jails (from jail.local and jail.d)
|
// 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()})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update jail settings: " + err.Error()})
|
||||||
return
|
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()})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -739,17 +741,18 @@ func RestartFail2banHandler(c *gin.Context) {
|
|||||||
config.DebugLog("----------------------------")
|
config.DebugLog("----------------------------")
|
||||||
config.DebugLog("ApplyFail2banSettings called (handlers.go)") // entry point
|
config.DebugLog("ApplyFail2banSettings called (handlers.go)") // entry point
|
||||||
|
|
||||||
// First we write our new settings to /etc/fail2ban/jail.local
|
conn, err := resolveConnector(c)
|
||||||
// if err := fail2ban.ApplyFail2banSettings("/etc/fail2ban/jail.local"); err != nil {
|
if err != nil {
|
||||||
// c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
// return
|
return
|
||||||
// }
|
}
|
||||||
|
server := conn.Server()
|
||||||
|
|
||||||
// Attempt to restart the fail2ban service.
|
// Attempt to restart the fail2ban service.
|
||||||
restartErr := fail2ban.RestartFail2ban()
|
restartErr := fail2ban.RestartFail2ban(server.ID)
|
||||||
if restartErr != nil {
|
if restartErr != nil {
|
||||||
// Check if running inside a container.
|
// 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).
|
// 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 the error and continue, so we can mark the restart as done.
|
||||||
log.Printf("Warning: restart failed inside container (expected behavior): %v", restartErr)
|
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.
|
// 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()})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -136,6 +136,48 @@
|
|||||||
padding: 0.1em 0.2em;
|
padding: 0.1em 0.2em;
|
||||||
border-radius: 0.25em;
|
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>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
@@ -154,6 +196,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="toast-container"></div>
|
||||||
|
|
||||||
<!-- ******************************************************************* -->
|
<!-- ******************************************************************* -->
|
||||||
<!-- Navigation START -->
|
<!-- Navigation START -->
|
||||||
<!-- ******************************************************************* -->
|
<!-- ******************************************************************* -->
|
||||||
@@ -821,7 +865,7 @@
|
|||||||
getTranslationsSettingsOnPageload()
|
getTranslationsSettingsOnPageload()
|
||||||
])
|
])
|
||||||
.then(function() {
|
.then(function() {
|
||||||
checkRestartNeeded();
|
updateRestartBanner();
|
||||||
return refreshData({ silent: true });
|
return refreshData({ silent: true });
|
||||||
})
|
})
|
||||||
.catch(function(err) {
|
.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
|
// Fetch and display own external IP for webUI
|
||||||
function displayExternalIP() {
|
function displayExternalIP() {
|
||||||
fetch('https://api.ipify.org?format=json')
|
fetch('https://api.ipify.org?format=json')
|
||||||
@@ -905,18 +968,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if there is still a reload of the fail2ban service needed
|
function updateRestartBanner() {
|
||||||
function checkRestartNeeded() {
|
var banner = document.getElementById('restartBanner');
|
||||||
fetch('/api/settings')
|
if (!banner) return;
|
||||||
.then(res => res.json())
|
if (currentServer && currentServer.restartNeeded) {
|
||||||
.then(data => {
|
banner.style.display = 'block';
|
||||||
if (data.restartNeeded) {
|
|
||||||
document.getElementById('restartBanner').style.display = 'block';
|
|
||||||
} else {
|
} else {
|
||||||
document.getElementById('restartBanner').style.display = 'none';
|
banner.style.display = 'none';
|
||||||
}
|
}
|
||||||
})
|
|
||||||
.catch(err => console.error('Error checking restartNeeded:', err));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load dynamically the other pages when navigating in nav
|
// Load dynamically the other pages when navigating in nav
|
||||||
@@ -997,6 +1056,7 @@
|
|||||||
}
|
}
|
||||||
renderServerSelector();
|
renderServerSelector();
|
||||||
renderServerSubtitle();
|
renderServerSubtitle();
|
||||||
|
updateRestartBanner();
|
||||||
})
|
})
|
||||||
.catch(function(err) {
|
.catch(function(err) {
|
||||||
console.error('Error loading servers:', err);
|
console.error('Error loading servers:', err);
|
||||||
@@ -1005,6 +1065,7 @@
|
|||||||
currentServer = null;
|
currentServer = null;
|
||||||
renderServerSelector();
|
renderServerSelector();
|
||||||
renderServerSubtitle();
|
renderServerSubtitle();
|
||||||
|
updateRestartBanner();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1088,6 +1149,7 @@
|
|||||||
}
|
}
|
||||||
renderServerSelector();
|
renderServerSelector();
|
||||||
renderServerSubtitle();
|
renderServerSubtitle();
|
||||||
|
updateRestartBanner();
|
||||||
refreshData();
|
refreshData();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1643,10 +1705,10 @@
|
|||||||
.then(function(res) { return res.json(); })
|
.then(function(res) { return res.json(); })
|
||||||
.then(function(data) {
|
.then(function(data) {
|
||||||
if (data.error) {
|
if (data.error) {
|
||||||
alert('Error saving server: ' + data.error);
|
showToast('Error saving server: ' + (data.error || 'Unknown error'), 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
alert(t('servers.form.success', 'Server saved successfully.'));
|
showToast(t('servers.form.success', 'Server saved successfully.'), 'success');
|
||||||
var saved = data.server || {};
|
var saved = data.server || {};
|
||||||
currentServerId = saved.id || currentServerId;
|
currentServerId = saved.id || currentServerId;
|
||||||
return loadServers().then(function() {
|
return loadServers().then(function() {
|
||||||
@@ -1660,7 +1722,7 @@
|
|||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch(function(err) {
|
.catch(function(err) {
|
||||||
alert('Error saving server: ' + err);
|
showToast('Error saving server: ' + err, 'error');
|
||||||
})
|
})
|
||||||
.finally(function() {
|
.finally(function() {
|
||||||
showLoading(false);
|
showLoading(false);
|
||||||
@@ -1733,7 +1795,7 @@
|
|||||||
.then(function(res) { return res.json(); })
|
.then(function(res) { return res.json(); })
|
||||||
.then(function(data) {
|
.then(function(data) {
|
||||||
if (data.error) {
|
if (data.error) {
|
||||||
alert('Error saving server: ' + data.error);
|
showToast('Error saving server: ' + (data.error || 'Unknown error'), 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!enabled && currentServerId === serverId) {
|
if (!enabled && currentServerId === serverId) {
|
||||||
@@ -1748,7 +1810,7 @@
|
|||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch(function(err) {
|
.catch(function(err) {
|
||||||
alert('Error saving server: ' + err);
|
showToast('Error saving server: ' + err, 'error');
|
||||||
})
|
})
|
||||||
.finally(function() {
|
.finally(function() {
|
||||||
showLoading(false);
|
showLoading(false);
|
||||||
@@ -1764,13 +1826,13 @@
|
|||||||
.then(function(res) { return res.json(); })
|
.then(function(res) { return res.json(); })
|
||||||
.then(function(data) {
|
.then(function(data) {
|
||||||
if (data.error) {
|
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;
|
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) {
|
.catch(function(err) {
|
||||||
alert(t('servers.actions.test_failure', 'Connection failed') + ': ' + err);
|
showToast(t('servers.actions.test_failure', 'Connection failed') + ': ' + err, 'error');
|
||||||
})
|
})
|
||||||
.finally(function() {
|
.finally(function() {
|
||||||
showLoading(false);
|
showLoading(false);
|
||||||
@@ -1784,7 +1846,7 @@
|
|||||||
.then(function(res) { return res.json(); })
|
.then(function(res) { return res.json(); })
|
||||||
.then(function(data) {
|
.then(function(data) {
|
||||||
if (data.error) {
|
if (data.error) {
|
||||||
alert('Error deleting server: ' + data.error);
|
showToast('Error deleting server: ' + (data.error || 'Unknown error'), 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (currentServerId === serverId) {
|
if (currentServerId === serverId) {
|
||||||
@@ -1796,10 +1858,12 @@
|
|||||||
renderServerSelector();
|
renderServerSelector();
|
||||||
renderServerSubtitle();
|
renderServerSubtitle();
|
||||||
return refreshData({ silent: true });
|
return refreshData({ silent: true });
|
||||||
|
}).then(function() {
|
||||||
|
showToast(t('servers.actions.delete_success', 'Server removed'), 'success');
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch(function(err) {
|
.catch(function(err) {
|
||||||
alert('Error deleting server: ' + err);
|
showToast('Error deleting server: ' + err, 'error');
|
||||||
})
|
})
|
||||||
.finally(function() {
|
.finally(function() {
|
||||||
showLoading(false);
|
showLoading(false);
|
||||||
@@ -1812,7 +1876,7 @@
|
|||||||
.then(function(res) { return res.json(); })
|
.then(function(res) { return res.json(); })
|
||||||
.then(function(data) {
|
.then(function(data) {
|
||||||
if (data.error) {
|
if (data.error) {
|
||||||
alert('Error setting default server: ' + data.error);
|
showToast('Error setting default server: ' + (data.error || 'Unknown error'), 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
currentServerId = data.server ? data.server.id : serverId;
|
currentServerId = data.server ? data.server.id : serverId;
|
||||||
@@ -1821,10 +1885,12 @@
|
|||||||
renderServerSelector();
|
renderServerSelector();
|
||||||
renderServerSubtitle();
|
renderServerSubtitle();
|
||||||
return refreshData({ silent: true });
|
return refreshData({ silent: true });
|
||||||
|
}).then(function() {
|
||||||
|
showToast(t('servers.actions.set_default_success', 'Server set as default'), 'success');
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch(function(err) {
|
.catch(function(err) {
|
||||||
alert('Error setting default server: ' + err);
|
showToast('Error setting default server: ' + err, 'error');
|
||||||
})
|
})
|
||||||
.finally(function() {
|
.finally(function() {
|
||||||
showLoading(false);
|
showLoading(false);
|
||||||
@@ -1910,14 +1976,14 @@
|
|||||||
.then(function(res) { return res.json(); })
|
.then(function(res) { return res.json(); })
|
||||||
.then(function(data) {
|
.then(function(data) {
|
||||||
if (data.error) {
|
if (data.error) {
|
||||||
alert("Error: " + data.error);
|
showToast("Error unbanning IP: " + data.error, 'error');
|
||||||
} else {
|
} else {
|
||||||
alert(data.message || "IP unbanned successfully");
|
showToast(data.message || "IP unbanned successfully", 'success');
|
||||||
}
|
}
|
||||||
return refreshData({ silent: true });
|
return refreshData({ silent: true });
|
||||||
})
|
})
|
||||||
.catch(function(err) {
|
.catch(function(err) {
|
||||||
alert("Error: " + err);
|
showToast("Error: " + err, 'error');
|
||||||
})
|
})
|
||||||
.finally(function() {
|
.finally(function() {
|
||||||
showLoading(false);
|
showLoading(false);
|
||||||
@@ -1943,14 +2009,14 @@
|
|||||||
.then(function(res) { return res.json(); })
|
.then(function(res) { return res.json(); })
|
||||||
.then(function(data) {
|
.then(function(data) {
|
||||||
if (data.error) {
|
if (data.error) {
|
||||||
alert("Error loading config: " + data.error);
|
showToast("Error loading config: " + data.error, 'error');
|
||||||
} else {
|
return;
|
||||||
|
}
|
||||||
textArea.value = data.config;
|
textArea.value = data.config;
|
||||||
openModal('jailConfigModal');
|
openModal('jailConfigModal');
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.catch(function(err) {
|
.catch(function(err) {
|
||||||
alert("Error: " + err);
|
showToast("Error: " + err, 'error');
|
||||||
})
|
})
|
||||||
.finally(function() {
|
.finally(function() {
|
||||||
showLoading(false);
|
showLoading(false);
|
||||||
@@ -1971,15 +2037,15 @@
|
|||||||
.then(function(res) { return res.json(); })
|
.then(function(res) { return res.json(); })
|
||||||
.then(function(data) {
|
.then(function(data) {
|
||||||
if (data.error) {
|
if (data.error) {
|
||||||
alert("Error saving config: " + data.error);
|
showToast("Error saving config: " + data.error, 'error');
|
||||||
} else {
|
return;
|
||||||
console.log("Filter saved successfully. Restart needed? " + data.restartNeeded);
|
|
||||||
closeModal('jailConfigModal');
|
|
||||||
document.getElementById('restartBanner').style.display = 'block';
|
|
||||||
}
|
}
|
||||||
|
closeModal('jailConfigModal');
|
||||||
|
showToast(t('filter_debug.save_success', 'Filter saved and reloaded'), 'success');
|
||||||
|
return refreshData({ silent: true });
|
||||||
})
|
})
|
||||||
.catch(function(err) {
|
.catch(function(err) {
|
||||||
alert("Error: " + err);
|
showToast("Error: " + err, 'error');
|
||||||
})
|
})
|
||||||
.finally(function() {
|
.finally(function() {
|
||||||
showLoading(false);
|
showLoading(false);
|
||||||
@@ -1990,7 +2056,7 @@
|
|||||||
// Fetches the full-list of all jails (from /jails/manage) and builds a list with toggle switches.
|
// Fetches the full-list of all jails (from /jails/manage) and builds a list with toggle switches.
|
||||||
function openManageJailsModal() {
|
function openManageJailsModal() {
|
||||||
if (!currentServerId) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
showLoading(true);
|
showLoading(true);
|
||||||
@@ -2000,7 +2066,7 @@
|
|||||||
.then(res => res.json())
|
.then(res => res.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
if (!data.jails?.length) {
|
if (!data.jails?.length) {
|
||||||
alert("No jails found.");
|
showToast("No jails found for this server.", 'info');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2039,7 +2105,7 @@
|
|||||||
document.getElementById('jailsList').innerHTML = html;
|
document.getElementById('jailsList').innerHTML = html;
|
||||||
openModal('manageJailsModal');
|
openModal('manageJailsModal');
|
||||||
})
|
})
|
||||||
.catch(err => alert("Error fetching jails: " + err))
|
.catch(err => showToast("Error fetching jails: " + err, 'error'))
|
||||||
.finally(() => showLoading(false));
|
.finally(() => showLoading(false));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2063,14 +2129,17 @@
|
|||||||
.then(function(res) { return res.json(); })
|
.then(function(res) { return res.json(); })
|
||||||
.then(function(data) {
|
.then(function(data) {
|
||||||
if (data.error) {
|
if (data.error) {
|
||||||
alert("Error saving jail settings: " + data.error);
|
showToast("Error saving jail settings: " + data.error, 'error');
|
||||||
} else {
|
return;
|
||||||
// A restart of fail2ban is needed, to enable or disable jails - a reload is not enough
|
|
||||||
document.getElementById('restartBanner').style.display = 'block';
|
|
||||||
}
|
}
|
||||||
|
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) {
|
.catch(function(err) {
|
||||||
alert("Error: " + err);
|
showToast("Error: " + err, 'error');
|
||||||
})
|
})
|
||||||
.finally(function() {
|
.finally(function() {
|
||||||
showLoading(false);
|
showLoading(false);
|
||||||
@@ -2126,7 +2195,7 @@
|
|||||||
document.getElementById('ignoreIP').value = data.ignoreip || '';
|
document.getElementById('ignoreIP').value = data.ignoreip || '';
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
alert('Error loading settings: ' + err);
|
showToast('Error loading settings: ' + err, 'error');
|
||||||
})
|
})
|
||||||
.finally(() => showLoading(false));
|
.finally(() => showLoading(false));
|
||||||
}
|
}
|
||||||
@@ -2173,17 +2242,20 @@
|
|||||||
.then(res => res.json())
|
.then(res => res.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
if (data.error) {
|
if (data.error) {
|
||||||
alert('Error saving settings: ' + data.error + data.details);
|
showToast('Error saving settings: ' + (data.error + (data.details || '')), 'error');
|
||||||
} else {
|
} else {
|
||||||
var selectedLang = $('#languageSelect').val();
|
var selectedLang = $('#languageSelect').val();
|
||||||
loadTranslations(selectedLang);
|
loadTranslations(selectedLang);
|
||||||
console.log("Settings saved successfully. Restart needed? " + data.restartNeeded);
|
console.log("Settings saved successfully. Restart needed? " + data.restartNeeded);
|
||||||
|
showToast(t('settings.save_success', 'Settings saved'), 'success');
|
||||||
if (data.restartNeeded) {
|
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));
|
.finally(() => showLoading(false));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2199,7 +2271,7 @@
|
|||||||
.then(res => res.json())
|
.then(res => res.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
if (data.error) {
|
if (data.error) {
|
||||||
alert('Error loading filters: ' + data.error);
|
showToast('Error loading filters: ' + data.error, 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const select = document.getElementById('filterSelect');
|
const select = document.getElementById('filterSelect');
|
||||||
@@ -2229,7 +2301,7 @@
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
alert('Error loading filters: ' + err);
|
showToast('Error loading filters: ' + err, 'error');
|
||||||
})
|
})
|
||||||
.finally(() => showLoading(false));
|
.finally(() => showLoading(false));
|
||||||
}
|
}
|
||||||
@@ -2244,12 +2316,12 @@
|
|||||||
.then(res => res.json())
|
.then(res => res.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
if (data.error) {
|
if (data.error) {
|
||||||
alert('Error sending test email: ' + data.error);
|
showToast('Error sending test email: ' + data.error, 'error');
|
||||||
} else {
|
} 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));
|
.finally(() => showLoading(false));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2259,7 +2331,7 @@
|
|||||||
const lines = document.getElementById('logLinesTextarea').value.split('\n');
|
const lines = document.getElementById('logLinesTextarea').value.split('\n');
|
||||||
|
|
||||||
if (!filterName) {
|
if (!filterName) {
|
||||||
alert('Please select a filter.');
|
showToast('Please select a filter.', 'info');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2275,13 +2347,13 @@
|
|||||||
.then(res => res.json())
|
.then(res => res.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
if (data.error) {
|
if (data.error) {
|
||||||
alert('Error: ' + data.error);
|
showToast('Error testing filter: ' + data.error, 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
renderTestResults(data.matches);
|
renderTestResults(data.matches);
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
alert('Error: ' + err);
|
showToast('Error testing filter: ' + err, 'error');
|
||||||
})
|
})
|
||||||
.finally(() => showLoading(false));
|
.finally(() => showLoading(false));
|
||||||
}
|
}
|
||||||
@@ -2332,14 +2404,17 @@
|
|||||||
.then(function(res) { return res.json(); })
|
.then(function(res) { return res.json(); })
|
||||||
.then(function(data) {
|
.then(function(data) {
|
||||||
if (data.error) {
|
if (data.error) {
|
||||||
alert("Error: " + data.error);
|
showToast("Failed to restart Fail2ban: " + data.error, 'error');
|
||||||
} else {
|
return;
|
||||||
document.getElementById('restartBanner').style.display = 'none';
|
|
||||||
return refreshData({ silent: true });
|
|
||||||
}
|
}
|
||||||
|
return loadServers().then(function() {
|
||||||
|
updateRestartBanner();
|
||||||
|
showToast(t('restart_banner.success', 'Fail2ban restart triggered'), 'success');
|
||||||
|
return refreshData({ silent: true });
|
||||||
|
});
|
||||||
})
|
})
|
||||||
.catch(function(err) {
|
.catch(function(err) {
|
||||||
alert("Error: " + err);
|
showToast("Failed to restart Fail2ban: " + err, 'error');
|
||||||
})
|
})
|
||||||
.finally(function() {
|
.finally(function() {
|
||||||
showLoading(false);
|
showLoading(false);
|
||||||
|
|||||||
Reference in New Issue
Block a user