mirror of
https://github.com/swissmakers/fail2ban-ui.git
synced 2026-04-11 13:47:05 +02:00
Implement unban events and API and also add it to the Recent stored events, as well some cleanups
This commit is contained in:
@@ -77,6 +77,10 @@ type AppSettings struct {
|
||||
GeoIPProvider string `json:"geoipProvider"` // "maxmind" or "builtin"
|
||||
GeoIPDatabasePath string `json:"geoipDatabasePath"` // Path to MaxMind database (optional)
|
||||
MaxLogLines int `json:"maxLogLines"` // Maximum log lines to include (default: 50)
|
||||
|
||||
// Email alert preferences
|
||||
EmailAlertsForBans bool `json:"emailAlertsForBans"` // Enable email alerts for ban events (default: true)
|
||||
EmailAlertsForUnbans bool `json:"emailAlertsForUnbans"` // Enable email alerts for unban events (default: false)
|
||||
}
|
||||
|
||||
type AdvancedActionsConfig struct {
|
||||
@@ -132,9 +136,7 @@ func normalizeAdvancedActionsConfig(cfg AdvancedActionsConfig) AdvancedActionsCo
|
||||
// init paths to key-files
|
||||
const (
|
||||
settingsFile = "fail2ban-ui-settings.json" // this file is created, relatively to where the app was started
|
||||
defaultJailFile = "/etc/fail2ban/jail.conf"
|
||||
jailFile = "/etc/fail2ban/jail.local" // Path to jail.local (to override conf-values from jail.conf)
|
||||
jailDFile = "/etc/fail2ban/jail.d/ui-custom-action.conf"
|
||||
jailFile = "/etc/fail2ban/jail.local"
|
||||
actionFile = "/etc/fail2ban/action.d/ui-custom-action.conf"
|
||||
actionCallbackPlaceholder = "__CALLBACK_URL__"
|
||||
actionServerIDPlaceholder = "__SERVER_ID__"
|
||||
@@ -184,6 +186,18 @@ actionban = /usr/bin/curl -X POST __CALLBACK_URL__/api/ban \
|
||||
--arg logs "$(tac <logpath> | grep <grepopts> -wF <ip>)" \
|
||||
'{serverId: $serverId, ip: $ip, jail: $jail, hostname: $hostname, failures: $failures, logs: $logs}')"
|
||||
|
||||
# Option: actionunban
|
||||
# This executes a cURL request to notify our API when an IP is unbanned.
|
||||
|
||||
actionunban = /usr/bin/curl -X POST __CALLBACK_URL__/api/unban \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-Callback-Secret: __CALLBACK_SECRET__" \
|
||||
-d "$(jq -n --arg serverId '__SERVER_ID__' \
|
||||
--arg ip '<ip>' \
|
||||
--arg jail '<name>' \
|
||||
--arg hostname '<fq-hostname>' \
|
||||
'{serverId: $serverId, ip: $ip, jail: $jail, hostname: $hostname}')"
|
||||
|
||||
[Init]
|
||||
|
||||
# Default name of the chain
|
||||
@@ -396,6 +410,8 @@ func applyAppSettingsRecordLocked(rec storage.AppSettingsRecord) {
|
||||
currentSettings.GeoIPDatabasePath = rec.GeoIPDatabasePath
|
||||
currentSettings.MaxLogLines = rec.MaxLogLines
|
||||
currentSettings.CallbackSecret = rec.CallbackSecret
|
||||
currentSettings.EmailAlertsForBans = rec.EmailAlertsForBans
|
||||
currentSettings.EmailAlertsForUnbans = rec.EmailAlertsForUnbans
|
||||
}
|
||||
|
||||
func applyServerRecordsLocked(records []storage.ServerRecord) {
|
||||
@@ -447,18 +463,26 @@ func toAppSettingsRecordLocked() (storage.AppSettingsRecord, error) {
|
||||
}
|
||||
|
||||
return storage.AppSettingsRecord{
|
||||
// Basic app settings
|
||||
Language: currentSettings.Language,
|
||||
Port: currentSettings.Port,
|
||||
Debug: currentSettings.Debug,
|
||||
CallbackURL: currentSettings.CallbackURL,
|
||||
RestartNeeded: currentSettings.RestartNeeded,
|
||||
// Callback settings
|
||||
CallbackURL: currentSettings.CallbackURL,
|
||||
CallbackSecret: currentSettings.CallbackSecret,
|
||||
// Alert settings
|
||||
AlertCountriesJSON: string(countryBytes),
|
||||
EmailAlertsForBans: currentSettings.EmailAlertsForBans,
|
||||
EmailAlertsForUnbans: currentSettings.EmailAlertsForUnbans,
|
||||
// SMTP settings
|
||||
SMTPHost: currentSettings.SMTP.Host,
|
||||
SMTPPort: currentSettings.SMTP.Port,
|
||||
SMTPUsername: currentSettings.SMTP.Username,
|
||||
SMTPPassword: currentSettings.SMTP.Password,
|
||||
SMTPFrom: currentSettings.SMTP.From,
|
||||
SMTPUseTLS: currentSettings.SMTP.UseTLS,
|
||||
// Fail2Ban DEFAULT settings
|
||||
BantimeIncrement: currentSettings.BantimeIncrement,
|
||||
DefaultJailEnable: currentSettings.DefaultJailEnable,
|
||||
// Convert IgnoreIPs array to space-separated string for storage
|
||||
@@ -469,11 +493,11 @@ func toAppSettingsRecordLocked() (storage.AppSettingsRecord, error) {
|
||||
DestEmail: currentSettings.Destemail,
|
||||
Banaction: currentSettings.Banaction,
|
||||
BanactionAllports: currentSettings.BanactionAllports,
|
||||
// Advanced features
|
||||
AdvancedActionsJSON: string(advancedBytes),
|
||||
GeoIPProvider: currentSettings.GeoIPProvider,
|
||||
GeoIPDatabasePath: currentSettings.GeoIPDatabasePath,
|
||||
MaxLogLines: currentSettings.MaxLogLines,
|
||||
CallbackSecret: currentSettings.CallbackSecret,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -531,6 +555,18 @@ func setDefaultsLocked() {
|
||||
if currentSettings.Language == "" {
|
||||
currentSettings.Language = "en"
|
||||
}
|
||||
|
||||
// Set email alert defaults
|
||||
if !currentSettings.EmailAlertsForBans && !currentSettings.EmailAlertsForUnbans {
|
||||
// Check if it is uninitialized by checking if we have other initialized values
|
||||
// If we have a callback secret or port set, it means we've loaded from storage, so we don't override
|
||||
if currentSettings.CallbackSecret == "" && currentSettings.Port == 0 {
|
||||
// Uninitialized so we set defaults
|
||||
currentSettings.EmailAlertsForBans = true
|
||||
currentSettings.EmailAlertsForUnbans = false
|
||||
}
|
||||
}
|
||||
|
||||
// Check for PORT environment variable first - it always takes priority
|
||||
if portEnv := os.Getenv("PORT"); portEnv != "" {
|
||||
if port, err := strconv.Atoi(portEnv); err == nil && port > 0 && port <= 65535 {
|
||||
|
||||
@@ -110,6 +110,8 @@
|
||||
"settings.destination_email_placeholder": "alerts@swissmakers.ch",
|
||||
"settings.alert_countries": "Alarm-Länder",
|
||||
"settings.alert_countries_description": "Wählen Sie die Länder aus, für die E-Mail-Alarme ausgelöst werden sollen, wenn eine Sperrung erfolgt.",
|
||||
"settings.email_alerts_for_bans": "E-Mail-Benachrichtigungen für Sperrungen aktivieren",
|
||||
"settings.email_alerts_for_unbans": "E-Mail-Benachrichtigungen für Entsperrungen aktivieren",
|
||||
"settings.smtp": "SMTP-Konfiguration",
|
||||
"settings.smtp_host": "SMTP-Host",
|
||||
"settings.smtp_host_placeholder": "z.B. smtp.gmail.com",
|
||||
@@ -281,6 +283,15 @@
|
||||
"email.whois.no_data": "WHOIS-Daten wurden für dieses Ereignis nicht erfasst.",
|
||||
"email.logs.no_data": "Für diesen Block wurden keine Log-Einträge erfasst.",
|
||||
"email.footer.text": "Diese Nachricht wurde automatisch von Fail2Ban-UI generiert",
|
||||
"email.unban.title": "IP-Adresse entsperrt",
|
||||
"email.unban.intro": "IP-Adresse aus Fail2Ban-Jail entsperrt.",
|
||||
"email.unban.subject.unbanned": "Entsperrt",
|
||||
"email.unban.subject.from": "von",
|
||||
"email.unban.details.unbanned_ip": "Entsperrte IP",
|
||||
"email.unban.details.jail": "Jail",
|
||||
"email.unban.details.hostname": "Hostname",
|
||||
"email.unban.details.country": "Land",
|
||||
"email.unban.details.timestamp": "Zeitstempel",
|
||||
"lotr.email.title": "Ein dunkler Diener wurde verbannt",
|
||||
"lotr.email.intro": "Die Wächter von Mittelerde haben eine Bedrohung erkannt und aus dem Reich verbannt.",
|
||||
"lotr.email.you_shall_not_pass": "DU KANNST NICHT VORBEI",
|
||||
@@ -289,8 +300,15 @@
|
||||
"lotr.email.details.realm_protection": "Das Reich des Schutzes",
|
||||
"lotr.email.details.origins": "Herkunft aus den",
|
||||
"lotr.email.details.banished_at": "Verbannt zur",
|
||||
"lotr.email.unban.title": "Der festgehaltene wird wider freigelassen",
|
||||
"lotr.email.unban.intro": "Die Wächter von Mittelerde haben den Zugang wiederhergestellt.",
|
||||
"lotr.email.unban.details.restored_ip": "Wiederhergestellte IP",
|
||||
"lotr.banished": "Aus dem Reich verbannt",
|
||||
"lotr.realms_protected": "Geschützte Reiche",
|
||||
"lotr.threats_banished": "Verbannte Bedrohungen"
|
||||
"lotr.threats_banished": "Verbannte Bedrohungen",
|
||||
"toast.ban.title": "Neue Blockierung aufgetreten",
|
||||
"toast.ban.action": "gesperrt in",
|
||||
"toast.unban.title": "IP entsperrt",
|
||||
"toast.unban.action": "entsperrt von"
|
||||
}
|
||||
|
||||
@@ -110,6 +110,8 @@
|
||||
"settings.destination_email_placeholder": "alerts@swissmakers.ch",
|
||||
"settings.alert_countries": "Alarm-Länder",
|
||||
"settings.alert_countries_description": "Wähl d'Länder us, für weli du per Email ä Alarm becho wetsch, wenn e Sperrig erfolgt.",
|
||||
"settings.email_alerts_for_bans": "Email-Benachrichtigunge für Sperrige aktiviere",
|
||||
"settings.email_alerts_for_unbans": "Email-Benachrichtigunge für Entsperrige aktiviere",
|
||||
"settings.smtp": "SMTP-Konfiguration",
|
||||
"settings.smtp_host": "SMTP-Host",
|
||||
"settings.smtp_host_placeholder": "z.B. smtp.gmail.com",
|
||||
@@ -281,6 +283,15 @@
|
||||
"email.whois.no_data": "WHOIS-Date si für das Ereignis nid erfasst worde.",
|
||||
"email.logs.no_data": "Für de Block sind keni Log-Iiträg erfasst worde.",
|
||||
"email.footer.text": "Diä Nachricht isch automatisch vom Fail2Ban-UI generiert worde",
|
||||
"email.unban.title": "IP-Adrässä entsperrt",
|
||||
"email.unban.intro": "E IP-Adrässä isch usem Fail2Ban-Jail entsperrt worde.",
|
||||
"email.unban.subject.unbanned": "Entsperrt",
|
||||
"email.unban.subject.from": "vo",
|
||||
"email.unban.details.unbanned_ip": "Entsperrti IP",
|
||||
"email.unban.details.jail": "Jail",
|
||||
"email.unban.details.hostname": "Hostname",
|
||||
"email.unban.details.country": "Land",
|
||||
"email.unban.details.timestamp": "Ziitstämpfel",
|
||||
"lotr.email.title": "E dunkle Diener isch verbannt worde",
|
||||
"lotr.email.intro": "D Wächter vo Mittelerde hei e Bedrohig erkannt und us dim Riich verbannt.",
|
||||
"lotr.email.you_shall_not_pass": "DU DARFSCH NID VERBII",
|
||||
@@ -289,8 +300,15 @@
|
||||
"lotr.email.details.realm_protection": "S Riich vom Schutz",
|
||||
"lotr.email.details.origins": "Herkunft us de",
|
||||
"lotr.email.details.banished_at": "Verbannt zur",
|
||||
"lotr.email.unban.title": "Dr festghautnig wird wider freiglah",
|
||||
"lotr.email.unban.intro": "D Wächter vo Mittelerde hei dr Zugang wiederhergstellt.",
|
||||
"lotr.email.unban.details.restored_ip": "Widerhergstellti IP",
|
||||
"lotr.banished": "Us em Riich verbannt",
|
||||
"lotr.realms_protected": "Gschützti Riich",
|
||||
"lotr.threats_banished": "Verbannti Bedrohige"
|
||||
"lotr.threats_banished": "Verbannti Bedrohige",
|
||||
"toast.ban.title": "Neui Blockierig ufträte",
|
||||
"toast.ban.action": "gsperrt i",
|
||||
"toast.unban.title": "IP entsperrt",
|
||||
"toast.unban.action": "entsperrt vo"
|
||||
}
|
||||
|
||||
@@ -110,6 +110,8 @@
|
||||
"settings.destination_email_placeholder": "alerts@swissmakers.ch",
|
||||
"settings.alert_countries": "Alert Countries",
|
||||
"settings.alert_countries_description": "Choose the countries for which you want to receive email alerts when a block is triggered.",
|
||||
"settings.email_alerts_for_bans": "Enable email alerts for bans",
|
||||
"settings.email_alerts_for_unbans": "Enable email alerts for unbans",
|
||||
"settings.smtp": "SMTP Configuration",
|
||||
"settings.smtp_host": "SMTP Host",
|
||||
"settings.smtp_host_placeholder": "e.g., smtp.gmail.com",
|
||||
@@ -281,6 +283,15 @@
|
||||
"email.whois.no_data": "WHOIS data was not captured for this event.",
|
||||
"email.logs.no_data": "No log entries were captured for this block.",
|
||||
"email.footer.text": "This message was generated automatically by Fail2Ban-UI",
|
||||
"email.unban.title": "IP Address Unbanned",
|
||||
"email.unban.intro": "An IP address has been unbanned from a Fail2Ban jail.",
|
||||
"email.unban.subject.unbanned": "Unbanned",
|
||||
"email.unban.subject.from": "from",
|
||||
"email.unban.details.unbanned_ip": "Unbanned IP",
|
||||
"email.unban.details.jail": "Jail",
|
||||
"email.unban.details.hostname": "Hostname",
|
||||
"email.unban.details.country": "Country",
|
||||
"email.unban.details.timestamp": "Timestamp",
|
||||
"lotr.email.title": "A Dark Servant Has Been Banished",
|
||||
"lotr.email.intro": "The guardians of Middle-earth have detected a threat and banished it from the realm.",
|
||||
"lotr.email.you_shall_not_pass": "YOU SHALL NOT PASS",
|
||||
@@ -289,8 +300,15 @@
|
||||
"lotr.email.details.realm_protection": "The Realm of Protection",
|
||||
"lotr.email.details.origins": "Origins from the",
|
||||
"lotr.email.details.banished_at": "Banished at the",
|
||||
"lotr.email.unban.title": "The held prisoner has been released",
|
||||
"lotr.email.unban.intro": "The guardians of Middle-earth have restored access to the realm.",
|
||||
"lotr.email.unban.details.restored_ip": "Restored IP",
|
||||
"lotr.banished": "Banished from the realm",
|
||||
"lotr.realms_protected": "Realms Protected",
|
||||
"lotr.threats_banished": "Threats Banished"
|
||||
"lotr.threats_banished": "Threats Banished",
|
||||
"toast.ban.title": "New block occurred",
|
||||
"toast.ban.action": "banned in",
|
||||
"toast.unban.title": "IP unblocked",
|
||||
"toast.unban.action": "unblocked from"
|
||||
}
|
||||
|
||||
@@ -110,6 +110,8 @@
|
||||
"settings.destination_email_placeholder": "alerts@swissmakers.ch",
|
||||
"settings.alert_countries": "Países para alerta",
|
||||
"settings.alert_countries_description": "Elige los países para los que deseas recibir alertas por correo electrónico cuando se produzca un bloqueo.",
|
||||
"settings.email_alerts_for_bans": "Activar alertas por email para bloqueos",
|
||||
"settings.email_alerts_for_unbans": "Activar alertas por email para desbloqueos",
|
||||
"settings.smtp": "Configuración SMTP",
|
||||
"settings.smtp_host": "Host SMTP",
|
||||
"settings.smtp_host_placeholder": "p.ej., smtp.gmail.com",
|
||||
@@ -281,6 +283,15 @@
|
||||
"email.whois.no_data": "No se capturaron datos WHOIS para este evento.",
|
||||
"email.logs.no_data": "No se capturaron entradas de registro para este bloqueo.",
|
||||
"email.footer.text": "Este mensaje fue generado automáticamente por Fail2Ban-UI",
|
||||
"email.unban.title": "Dirección IP desbloqueada",
|
||||
"email.unban.intro": "Una dirección IP ha sido desbloqueada de una prisión Fail2Ban.",
|
||||
"email.unban.subject.unbanned": "Desbloqueado",
|
||||
"email.unban.subject.from": "de",
|
||||
"email.unban.details.unbanned_ip": "IP desbloqueada",
|
||||
"email.unban.details.jail": "Prisión",
|
||||
"email.unban.details.hostname": "Nombre de host",
|
||||
"email.unban.details.country": "País",
|
||||
"email.unban.details.timestamp": "Marca de tiempo",
|
||||
"lotr.email.title": "Un siervo oscuro ha sido desterrado",
|
||||
"lotr.email.intro": "Los guardianes de la Tierra Media han detectado una amenaza y la han desterrado del reino.",
|
||||
"lotr.email.you_shall_not_pass": "NO PASARÁS",
|
||||
@@ -289,7 +300,14 @@
|
||||
"lotr.email.details.realm_protection": "El reino de la protección",
|
||||
"lotr.email.details.origins": "Orígenes de las",
|
||||
"lotr.email.details.banished_at": "Desterrado a las",
|
||||
"lotr.email.unban.title": "El prisionero detenido ha sido liberado",
|
||||
"lotr.email.unban.intro": "Los guardianes de la Tierra Media han restaurado el acceso al reino.",
|
||||
"lotr.email.unban.details.restored_ip": "IP restaurada",
|
||||
"lotr.banished": "Desterrado del reino",
|
||||
"lotr.realms_protected": "Reinos protegidos",
|
||||
"lotr.threats_banished": "Amenazas desterradas"
|
||||
"lotr.threats_banished": "Amenazas desterradas",
|
||||
"toast.ban.title": "Nuevo bloqueo ocurrido",
|
||||
"toast.ban.action": "bloqueado en",
|
||||
"toast.unban.title": "IP desbloqueada",
|
||||
"toast.unban.action": "desbloqueada de"
|
||||
}
|
||||
|
||||
@@ -110,6 +110,8 @@
|
||||
"settings.destination_email_placeholder": "alerts@swissmakers.ch",
|
||||
"settings.alert_countries": "Pays d'alerte",
|
||||
"settings.alert_countries_description": "Choisissez les pays pour lesquels vous souhaitez recevoir des alertes par email lors d'un blocage.",
|
||||
"settings.email_alerts_for_bans": "Activer les alertes email pour les bannissements",
|
||||
"settings.email_alerts_for_unbans": "Activer les alertes email pour les débannissements",
|
||||
"settings.smtp": "Configuration SMTP",
|
||||
"settings.smtp_host": "Hôte SMTP",
|
||||
"settings.smtp_host_placeholder": "par exemple, smtp.gmail.com",
|
||||
@@ -281,6 +283,15 @@
|
||||
"email.whois.no_data": "Les données WHOIS n'ont pas été capturées pour cet événement.",
|
||||
"email.logs.no_data": "Aucune entrée de journal n'a été capturée pour ce blocage.",
|
||||
"email.footer.text": "Ce message a été généré automatiquement par Fail2Ban-UI",
|
||||
"email.unban.title": "Adresse IP débannie",
|
||||
"email.unban.intro": "Une adresse IP a été débannie d'une prison Fail2Ban.",
|
||||
"email.unban.subject.unbanned": "Débanni",
|
||||
"email.unban.subject.from": "de",
|
||||
"email.unban.details.unbanned_ip": "IP débannie",
|
||||
"email.unban.details.jail": "Prison",
|
||||
"email.unban.details.hostname": "Nom d'hôte",
|
||||
"email.unban.details.country": "Pays",
|
||||
"email.unban.details.timestamp": "Horodatage",
|
||||
"lotr.email.title": "Un serviteur des ténèbres a été banni",
|
||||
"lotr.email.intro": "Les gardiens de la Terre du Milieu ont détecté une menace et l'ont bannie du royaume.",
|
||||
"lotr.email.you_shall_not_pass": "TU NE PASSERAS PAS",
|
||||
@@ -289,7 +300,14 @@
|
||||
"lotr.email.details.realm_protection": "Le royaume de la protection",
|
||||
"lotr.email.details.origins": "Origines des",
|
||||
"lotr.email.details.banished_at": "Banni à",
|
||||
"lotr.email.unban.title": "Le détenu a été libéré",
|
||||
"lotr.email.unban.intro": "Les gardiens de la Terre du Milieu ont restauré l'accès au royaume.",
|
||||
"lotr.email.unban.details.restored_ip": "IP restaurée",
|
||||
"lotr.banished": "Banni du royaume",
|
||||
"lotr.realms_protected": "Royaumes protégés",
|
||||
"lotr.threats_banished": "Menaces bannies"
|
||||
"lotr.threats_banished": "Menaces bannies",
|
||||
"toast.ban.title": "Nouveau blocage survenu",
|
||||
"toast.ban.action": "banni dans",
|
||||
"toast.unban.title": "IP débloquée",
|
||||
"toast.unban.action": "débloquée de"
|
||||
}
|
||||
|
||||
@@ -110,6 +110,8 @@
|
||||
"settings.destination_email_placeholder": "alerts@swissmakers.ch",
|
||||
"settings.alert_countries": "Paesi per allarme",
|
||||
"settings.alert_countries_description": "Seleziona i paesi per i quali desideri ricevere allarmi via email quando si verifica un blocco.",
|
||||
"settings.email_alerts_for_bans": "Abilita allarmi email per i ban",
|
||||
"settings.email_alerts_for_unbans": "Abilita allarmi email per gli unban",
|
||||
"settings.smtp": "Configurazione SMTP",
|
||||
"settings.smtp_host": "Host SMTP",
|
||||
"settings.smtp_host_placeholder": "es. smtp.gmail.com",
|
||||
@@ -281,6 +283,15 @@
|
||||
"email.whois.no_data": "I dati WHOIS non sono stati acquisiti per questo evento.",
|
||||
"email.logs.no_data": "Nessuna voce di log è stata acquisita per questo blocco.",
|
||||
"email.footer.text": "Questo messaggio è stato generato automaticamente da Fail2Ban-UI",
|
||||
"email.unban.title": "Indirizzo IP sbannato",
|
||||
"email.unban.intro": "Un indirizzo IP è stato sbannato da una prigione Fail2Ban.",
|
||||
"email.unban.subject.unbanned": "Sbannato",
|
||||
"email.unban.subject.from": "da",
|
||||
"email.unban.details.unbanned_ip": "IP sbannato",
|
||||
"email.unban.details.jail": "Prigione",
|
||||
"email.unban.details.hostname": "Nome host",
|
||||
"email.unban.details.country": "Paese",
|
||||
"email.unban.details.timestamp": "Timestamp",
|
||||
"lotr.email.title": "Un servitore oscuro è stato bandito",
|
||||
"lotr.email.intro": "I guardiani della Terra di Mezzo hanno rilevato una minaccia e l'hanno bandita dal regno.",
|
||||
"lotr.email.you_shall_not_pass": "NON PASSERAI",
|
||||
@@ -289,7 +300,14 @@
|
||||
"lotr.email.details.realm_protection": "Il regno della protezione",
|
||||
"lotr.email.details.origins": "Origini dalle",
|
||||
"lotr.email.details.banished_at": "Bandito alle",
|
||||
"lotr.email.unban.title": "Il detenuto è stato rilasciato",
|
||||
"lotr.email.unban.intro": "I guardiani della Terra di Mezzo hanno ripristinato l'accesso al regno.",
|
||||
"lotr.email.unban.details.restored_ip": "IP ripristinato",
|
||||
"lotr.banished": "Bandito dal regno",
|
||||
"lotr.realms_protected": "Regni protetti",
|
||||
"lotr.threats_banished": "Minacce bandite"
|
||||
"lotr.threats_banished": "Minacce bandite",
|
||||
"toast.ban.title": "Nuovo blocco verificato",
|
||||
"toast.ban.action": "bannato in",
|
||||
"toast.unban.title": "IP sbloccato",
|
||||
"toast.unban.action": "sbloccato da"
|
||||
}
|
||||
|
||||
@@ -48,18 +48,26 @@ func intFromNull(ni sql.NullInt64) int {
|
||||
}
|
||||
|
||||
type AppSettingsRecord struct {
|
||||
// Basic app settings
|
||||
Language string
|
||||
Port int
|
||||
Debug bool
|
||||
CallbackURL string
|
||||
RestartNeeded bool
|
||||
// Callback settings
|
||||
CallbackURL string
|
||||
CallbackSecret string
|
||||
// Alert settings
|
||||
AlertCountriesJSON string
|
||||
EmailAlertsForBans bool
|
||||
EmailAlertsForUnbans bool
|
||||
// SMTP settings
|
||||
SMTPHost string
|
||||
SMTPPort int
|
||||
SMTPUsername string
|
||||
SMTPPassword string
|
||||
SMTPFrom string
|
||||
SMTPUseTLS bool
|
||||
// Fail2Ban DEFAULT settings
|
||||
BantimeIncrement bool
|
||||
DefaultJailEnable bool
|
||||
IgnoreIP string // Stored as space-separated string, converted to array in AppSettings
|
||||
@@ -69,11 +77,11 @@ type AppSettingsRecord struct {
|
||||
DestEmail string
|
||||
Banaction string
|
||||
BanactionAllports string
|
||||
// Advanced features
|
||||
AdvancedActionsJSON string
|
||||
GeoIPProvider string
|
||||
GeoIPDatabasePath string
|
||||
MaxLogLines int
|
||||
CallbackSecret string
|
||||
}
|
||||
|
||||
type ServerRecord struct {
|
||||
@@ -97,7 +105,7 @@ type ServerRecord struct {
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
// BanEventRecord represents a single ban event stored in the internal database.
|
||||
// BanEventRecord represents a single ban or unban event stored in the internal database.
|
||||
type BanEventRecord struct {
|
||||
ID int64 `json:"id"`
|
||||
ServerID string `json:"serverId"`
|
||||
@@ -109,6 +117,7 @@ type BanEventRecord struct {
|
||||
Failures string `json:"failures"`
|
||||
Whois string `json:"whois"`
|
||||
Logs string `json:"logs"`
|
||||
EventType string `json:"eventType"`
|
||||
OccurredAt time.Time `json:"occurredAt"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
}
|
||||
@@ -175,17 +184,17 @@ func GetAppSettings(ctx context.Context) (AppSettingsRecord, bool, error) {
|
||||
}
|
||||
|
||||
row := db.QueryRowContext(ctx, `
|
||||
SELECT language, port, debug, callback_url, restart_needed, alert_countries, smtp_host, smtp_port, smtp_username, smtp_password, smtp_from, smtp_use_tls, bantime_increment, default_jail_enable, ignore_ip, bantime, findtime, maxretry, destemail, banaction, banaction_allports, advanced_actions, geoip_provider, geoip_database_path, max_log_lines, callback_secret
|
||||
SELECT language, port, debug, restart_needed, callback_url, callback_secret, alert_countries, email_alerts_for_bans, email_alerts_for_unbans, smtp_host, smtp_port, smtp_username, smtp_password, smtp_from, smtp_use_tls, bantime_increment, default_jail_enable, ignore_ip, bantime, findtime, maxretry, destemail, banaction, banaction_allports, advanced_actions, geoip_provider, geoip_database_path, max_log_lines
|
||||
FROM app_settings
|
||||
WHERE id = 1`)
|
||||
|
||||
var (
|
||||
lang, callback, alerts, smtpHost, smtpUser, smtpPass, smtpFrom, ignoreIP, bantime, findtime, destemail, banaction, banactionAllports, advancedActions, geoipProvider, geoipDatabasePath, callbackSecret sql.NullString
|
||||
lang, callback, callbackSecret, alerts, smtpHost, smtpUser, smtpPass, smtpFrom, ignoreIP, bantime, findtime, destemail, banaction, banactionAllports, advancedActions, geoipProvider, geoipDatabasePath sql.NullString
|
||||
port, smtpPort, maxretry, maxLogLines sql.NullInt64
|
||||
debug, restartNeeded, smtpTLS, bantimeInc, defaultJailEn sql.NullInt64
|
||||
debug, restartNeeded, smtpTLS, bantimeInc, defaultJailEn, emailAlertsForBans, emailAlertsForUnbans sql.NullInt64
|
||||
)
|
||||
|
||||
err := row.Scan(&lang, &port, &debug, &callback, &restartNeeded, &alerts, &smtpHost, &smtpPort, &smtpUser, &smtpPass, &smtpFrom, &smtpTLS, &bantimeInc, &defaultJailEn, &ignoreIP, &bantime, &findtime, &maxretry, &destemail, &banaction, &banactionAllports, &advancedActions, &geoipProvider, &geoipDatabasePath, &maxLogLines, &callbackSecret)
|
||||
err := row.Scan(&lang, &port, &debug, &restartNeeded, &callback, &callbackSecret, &alerts, &emailAlertsForBans, &emailAlertsForUnbans, &smtpHost, &smtpPort, &smtpUser, &smtpPass, &smtpFrom, &smtpTLS, &bantimeInc, &defaultJailEn, &ignoreIP, &bantime, &findtime, &maxretry, &destemail, &banaction, &banactionAllports, &advancedActions, &geoipProvider, &geoipDatabasePath, &maxLogLines)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return AppSettingsRecord{}, false, nil
|
||||
}
|
||||
@@ -194,18 +203,26 @@ WHERE id = 1`)
|
||||
}
|
||||
|
||||
rec := AppSettingsRecord{
|
||||
// Basic app settings
|
||||
Language: stringFromNull(lang),
|
||||
Port: intFromNull(port),
|
||||
Debug: intToBool(intFromNull(debug)),
|
||||
CallbackURL: stringFromNull(callback),
|
||||
RestartNeeded: intToBool(intFromNull(restartNeeded)),
|
||||
// Callback settings
|
||||
CallbackURL: stringFromNull(callback),
|
||||
CallbackSecret: stringFromNull(callbackSecret),
|
||||
// Alert settings
|
||||
AlertCountriesJSON: stringFromNull(alerts),
|
||||
EmailAlertsForBans: intToBool(intFromNull(emailAlertsForBans)),
|
||||
EmailAlertsForUnbans: intToBool(intFromNull(emailAlertsForUnbans)),
|
||||
// SMTP settings
|
||||
SMTPHost: stringFromNull(smtpHost),
|
||||
SMTPPort: intFromNull(smtpPort),
|
||||
SMTPUsername: stringFromNull(smtpUser),
|
||||
SMTPPassword: stringFromNull(smtpPass),
|
||||
SMTPFrom: stringFromNull(smtpFrom),
|
||||
SMTPUseTLS: intToBool(intFromNull(smtpTLS)),
|
||||
// Fail2Ban DEFAULT settings
|
||||
BantimeIncrement: intToBool(intFromNull(bantimeInc)),
|
||||
DefaultJailEnable: intToBool(intFromNull(defaultJailEn)),
|
||||
IgnoreIP: stringFromNull(ignoreIP),
|
||||
@@ -215,11 +232,11 @@ WHERE id = 1`)
|
||||
DestEmail: stringFromNull(destemail),
|
||||
Banaction: stringFromNull(banaction),
|
||||
BanactionAllports: stringFromNull(banactionAllports),
|
||||
// Advanced features
|
||||
AdvancedActionsJSON: stringFromNull(advancedActions),
|
||||
GeoIPProvider: stringFromNull(geoipProvider),
|
||||
GeoIPDatabasePath: stringFromNull(geoipDatabasePath),
|
||||
MaxLogLines: intFromNull(maxLogLines),
|
||||
CallbackSecret: stringFromNull(callbackSecret),
|
||||
}
|
||||
|
||||
return rec, true, nil
|
||||
@@ -231,16 +248,19 @@ func SaveAppSettings(ctx context.Context, rec AppSettingsRecord) error {
|
||||
}
|
||||
_, err := db.ExecContext(ctx, `
|
||||
INSERT INTO app_settings (
|
||||
id, language, port, debug, callback_url, restart_needed, alert_countries, smtp_host, smtp_port, smtp_username, smtp_password, smtp_from, smtp_use_tls, bantime_increment, default_jail_enable, ignore_ip, bantime, findtime, maxretry, destemail, banaction, banaction_allports, advanced_actions, geoip_provider, geoip_database_path, max_log_lines, callback_secret
|
||||
id, language, port, debug, restart_needed, callback_url, callback_secret, alert_countries, email_alerts_for_bans, email_alerts_for_unbans, smtp_host, smtp_port, smtp_username, smtp_password, smtp_from, smtp_use_tls, bantime_increment, default_jail_enable, ignore_ip, bantime, findtime, maxretry, destemail, banaction, banaction_allports, advanced_actions, geoip_provider, geoip_database_path, max_log_lines
|
||||
) VALUES (
|
||||
1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
|
||||
1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
|
||||
) ON CONFLICT(id) DO UPDATE SET
|
||||
language = excluded.language,
|
||||
port = excluded.port,
|
||||
debug = excluded.debug,
|
||||
callback_url = excluded.callback_url,
|
||||
restart_needed = excluded.restart_needed,
|
||||
callback_url = excluded.callback_url,
|
||||
callback_secret = excluded.callback_secret,
|
||||
alert_countries = excluded.alert_countries,
|
||||
email_alerts_for_bans = excluded.email_alerts_for_bans,
|
||||
email_alerts_for_unbans = excluded.email_alerts_for_unbans,
|
||||
smtp_host = excluded.smtp_host,
|
||||
smtp_port = excluded.smtp_port,
|
||||
smtp_username = excluded.smtp_username,
|
||||
@@ -259,14 +279,16 @@ INSERT INTO app_settings (
|
||||
advanced_actions = excluded.advanced_actions,
|
||||
geoip_provider = excluded.geoip_provider,
|
||||
geoip_database_path = excluded.geoip_database_path,
|
||||
max_log_lines = excluded.max_log_lines,
|
||||
callback_secret = excluded.callback_secret
|
||||
max_log_lines = excluded.max_log_lines
|
||||
`, rec.Language,
|
||||
rec.Port,
|
||||
boolToInt(rec.Debug),
|
||||
rec.CallbackURL,
|
||||
boolToInt(rec.RestartNeeded),
|
||||
rec.CallbackURL,
|
||||
rec.CallbackSecret,
|
||||
rec.AlertCountriesJSON,
|
||||
boolToInt(rec.EmailAlertsForBans),
|
||||
boolToInt(rec.EmailAlertsForUnbans),
|
||||
rec.SMTPHost,
|
||||
rec.SMTPPort,
|
||||
rec.SMTPUsername,
|
||||
@@ -286,7 +308,6 @@ INSERT INTO app_settings (
|
||||
rec.GeoIPProvider,
|
||||
rec.GeoIPDatabasePath,
|
||||
rec.MaxLogLines,
|
||||
rec.CallbackSecret,
|
||||
)
|
||||
return err
|
||||
}
|
||||
@@ -462,10 +483,16 @@ func RecordBanEvent(ctx context.Context, record BanEventRecord) error {
|
||||
record.OccurredAt = now
|
||||
}
|
||||
|
||||
// Default to 'ban' if event type is not set
|
||||
eventType := record.EventType
|
||||
if eventType == "" {
|
||||
eventType = "ban"
|
||||
}
|
||||
|
||||
const query = `
|
||||
INSERT INTO ban_events (
|
||||
server_id, server_name, jail, ip, country, hostname, failures, whois, logs, occurred_at, created_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
||||
server_id, server_name, jail, ip, country, hostname, failures, whois, logs, event_type, occurred_at, created_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
||||
|
||||
_, err := db.ExecContext(
|
||||
ctx,
|
||||
@@ -479,6 +506,7 @@ INSERT INTO ban_events (
|
||||
record.Failures,
|
||||
record.Whois,
|
||||
record.Logs,
|
||||
eventType,
|
||||
record.OccurredAt.UTC(),
|
||||
record.CreatedAt.UTC(),
|
||||
)
|
||||
@@ -500,7 +528,7 @@ func ListBanEvents(ctx context.Context, serverID string, limit int, since time.T
|
||||
}
|
||||
|
||||
baseQuery := `
|
||||
SELECT id, server_id, server_name, jail, ip, country, hostname, failures, whois, logs, occurred_at, created_at
|
||||
SELECT id, server_id, server_name, jail, ip, country, hostname, failures, whois, logs, event_type, occurred_at, created_at
|
||||
FROM ban_events
|
||||
WHERE 1=1`
|
||||
|
||||
@@ -526,6 +554,7 @@ WHERE 1=1`
|
||||
var results []BanEventRecord
|
||||
for rows.Next() {
|
||||
var rec BanEventRecord
|
||||
var eventType sql.NullString
|
||||
if err := rows.Scan(
|
||||
&rec.ID,
|
||||
&rec.ServerID,
|
||||
@@ -537,11 +566,18 @@ WHERE 1=1`
|
||||
&rec.Failures,
|
||||
&rec.Whois,
|
||||
&rec.Logs,
|
||||
&eventType,
|
||||
&rec.OccurredAt,
|
||||
&rec.CreatedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Default to 'ban' if event_type is NULL (for backward compatibility)
|
||||
if eventType.Valid {
|
||||
rec.EventType = eventType.String
|
||||
} else {
|
||||
rec.EventType = "ban"
|
||||
}
|
||||
results = append(results, rec)
|
||||
}
|
||||
|
||||
@@ -786,18 +822,26 @@ func ensureSchema(ctx context.Context) error {
|
||||
const createTable = `
|
||||
CREATE TABLE IF NOT EXISTS app_settings (
|
||||
id INTEGER PRIMARY KEY CHECK (id = 1),
|
||||
-- Basic app settings
|
||||
language TEXT,
|
||||
port INTEGER,
|
||||
debug INTEGER,
|
||||
callback_url TEXT,
|
||||
restart_needed INTEGER,
|
||||
-- Callback settings
|
||||
callback_url TEXT,
|
||||
callback_secret TEXT,
|
||||
-- Alert settings
|
||||
alert_countries TEXT,
|
||||
email_alerts_for_bans INTEGER DEFAULT 1,
|
||||
email_alerts_for_unbans INTEGER DEFAULT 0,
|
||||
-- SMTP settings
|
||||
smtp_host TEXT,
|
||||
smtp_port INTEGER,
|
||||
smtp_username TEXT,
|
||||
smtp_password TEXT,
|
||||
smtp_from TEXT,
|
||||
smtp_use_tls INTEGER,
|
||||
-- Fail2Ban DEFAULT settings
|
||||
bantime_increment INTEGER,
|
||||
default_jail_enable INTEGER,
|
||||
ignore_ip TEXT,
|
||||
@@ -807,11 +851,11 @@ CREATE TABLE IF NOT EXISTS app_settings (
|
||||
destemail TEXT,
|
||||
banaction TEXT,
|
||||
banaction_allports TEXT,
|
||||
-- Advanced features
|
||||
advanced_actions TEXT,
|
||||
geoip_provider TEXT,
|
||||
geoip_database_path TEXT,
|
||||
max_log_lines INTEGER,
|
||||
callback_secret TEXT
|
||||
max_log_lines INTEGER
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS servers (
|
||||
@@ -846,6 +890,7 @@ CREATE TABLE IF NOT EXISTS ban_events (
|
||||
failures TEXT,
|
||||
whois TEXT,
|
||||
logs TEXT,
|
||||
event_type TEXT NOT NULL DEFAULT 'ban',
|
||||
occurred_at DATETIME NOT NULL,
|
||||
created_at DATETIME NOT NULL
|
||||
);
|
||||
@@ -873,69 +918,16 @@ CREATE INDEX IF NOT EXISTS idx_perm_blocks_status ON permanent_blocks(status);
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// Backfill banaction columns for existing databases that predate them.
|
||||
if _, err := db.ExecContext(ctx, `ALTER TABLE app_settings ADD COLUMN banaction TEXT`); err != nil {
|
||||
if !strings.Contains(strings.ToLower(err.Error()), "duplicate column name") {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if _, err := db.ExecContext(ctx, `ALTER TABLE app_settings ADD COLUMN banaction_allports TEXT`); err != nil {
|
||||
if !strings.Contains(strings.ToLower(err.Error()), "duplicate column name") {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := db.ExecContext(ctx, `ALTER TABLE app_settings ADD COLUMN advanced_actions TEXT`); err != nil {
|
||||
if !strings.Contains(strings.ToLower(err.Error()), "duplicate column name") {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Add geoip_provider column
|
||||
if _, err := db.ExecContext(ctx, `ALTER TABLE app_settings ADD COLUMN geoip_provider TEXT`); err != nil {
|
||||
if !strings.Contains(strings.ToLower(err.Error()), "duplicate column name") {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Add geoip_database_path column
|
||||
if _, err := db.ExecContext(ctx, `ALTER TABLE app_settings ADD COLUMN geoip_database_path TEXT`); err != nil {
|
||||
if !strings.Contains(strings.ToLower(err.Error()), "duplicate column name") {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Add max_log_lines column
|
||||
if _, err := db.ExecContext(ctx, `ALTER TABLE app_settings ADD COLUMN max_log_lines INTEGER`); err != nil {
|
||||
if !strings.Contains(strings.ToLower(err.Error()), "duplicate column name") {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Add callback_secret column
|
||||
if _, err := db.ExecContext(ctx, `ALTER TABLE app_settings ADD COLUMN callback_secret TEXT`); err != nil {
|
||||
if !strings.Contains(strings.ToLower(err.Error()), "duplicate column name") {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Set default values for new columns if they are NULL
|
||||
if _, err := db.ExecContext(ctx, `UPDATE app_settings SET geoip_provider = 'maxmind' WHERE geoip_provider IS NULL`); err != nil {
|
||||
log.Printf("Warning: Failed to set default value for geoip_provider: %v", err)
|
||||
}
|
||||
if _, err := db.ExecContext(ctx, `UPDATE app_settings SET geoip_database_path = '/usr/share/GeoIP/GeoLite2-Country.mmdb' WHERE geoip_database_path IS NULL`); err != nil {
|
||||
log.Printf("Warning: Failed to set default value for geoip_database_path: %v", err)
|
||||
}
|
||||
if _, err := db.ExecContext(ctx, `UPDATE app_settings SET max_log_lines = 50 WHERE max_log_lines IS NULL OR max_log_lines = 0`); err != nil {
|
||||
log.Printf("Warning: Failed to set default value for max_log_lines: %v", err)
|
||||
}
|
||||
// NOTE: Database migrations for feature releases
|
||||
// For this version, we start with a fresh schema. Future feature releases
|
||||
// that require database schema changes should add migration logic here.
|
||||
// Example migration pattern:
|
||||
// if _, err := db.ExecContext(ctx, `ALTER TABLE table_name ADD COLUMN new_column TYPE DEFAULT value`); err != nil {
|
||||
// if err != nil && !strings.Contains(strings.ToLower(err.Error()), "duplicate column name") {
|
||||
// return err
|
||||
// }
|
||||
// }
|
||||
_ = strings.Contains // Keep strings import for migration example above
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -261,6 +261,81 @@ func BanNotificationHandler(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Ban notification processed successfully"})
|
||||
}
|
||||
|
||||
// UnbanNotificationHandler processes incoming unban notifications from Fail2Ban.
|
||||
func UnbanNotificationHandler(c *gin.Context) {
|
||||
// Validate callback secret
|
||||
settings := config.GetSettings()
|
||||
providedSecret := c.GetHeader("X-Callback-Secret")
|
||||
expectedSecret := settings.CallbackSecret
|
||||
|
||||
// Use constant-time comparison to prevent timing attacks
|
||||
if expectedSecret == "" {
|
||||
log.Printf("⚠️ Callback secret not configured, rejecting request from %s", c.ClientIP())
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Callback secret not configured"})
|
||||
return
|
||||
}
|
||||
|
||||
if providedSecret == "" {
|
||||
log.Printf("⚠️ Missing X-Callback-Secret header in request from %s", c.ClientIP())
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Missing X-Callback-Secret header"})
|
||||
return
|
||||
}
|
||||
|
||||
// Constant-time comparison
|
||||
if subtle.ConstantTimeCompare([]byte(providedSecret), []byte(expectedSecret)) != 1 {
|
||||
log.Printf("⚠️ Invalid callback secret in request from %s", c.ClientIP())
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid callback secret"})
|
||||
return
|
||||
}
|
||||
|
||||
var request struct {
|
||||
ServerID string `json:"serverId"`
|
||||
IP string `json:"ip" binding:"required"`
|
||||
Jail string `json:"jail" binding:"required"`
|
||||
Hostname string `json:"hostname"`
|
||||
}
|
||||
|
||||
body, _ := io.ReadAll(c.Request.Body)
|
||||
config.DebugLog("📩 Incoming Unban Notification: %s\n", string(body))
|
||||
|
||||
// Rebind body so Gin can parse it again
|
||||
c.Request.Body = io.NopCloser(bytes.NewBuffer(body))
|
||||
|
||||
// Parse JSON request body
|
||||
if err := c.ShouldBindJSON(&request); err != nil {
|
||||
var verr validator.ValidationErrors
|
||||
if errors.As(err, &verr) {
|
||||
for _, fe := range verr {
|
||||
log.Printf("❌ Validation error: Field '%s' violated rule '%s'", fe.Field(), fe.ActualTag())
|
||||
}
|
||||
} else {
|
||||
log.Printf("❌ JSON parsing error: %v", err)
|
||||
}
|
||||
log.Printf("Raw JSON: %s", string(body))
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("✅ Parsed Unban Request - IP: %s, Jail: %s, Hostname: %s",
|
||||
request.IP, request.Jail, request.Hostname)
|
||||
|
||||
server, err := resolveServerForNotification(request.ServerID, request.Hostname)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Handle the Fail2Ban notification
|
||||
if err := HandleUnbanNotification(c.Request.Context(), server, request.IP, request.Jail, request.Hostname, "", ""); err != nil {
|
||||
log.Printf("❌ Failed to process unban notification: %v\n", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to process unban notification: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Respond with success
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Unban notification processed successfully"})
|
||||
}
|
||||
|
||||
// ListBanEventsHandler returns stored ban events from the internal database.
|
||||
func ListBanEventsHandler(c *gin.Context) {
|
||||
serverID := c.Query("serverId")
|
||||
@@ -658,6 +733,7 @@ func HandleBanNotification(ctx context.Context, server config.Fail2banServer, ip
|
||||
Failures: failures,
|
||||
Whois: whoisData,
|
||||
Logs: filteredLogs,
|
||||
EventType: "ban",
|
||||
OccurredAt: time.Now().UTC(),
|
||||
}
|
||||
if err := storage.RecordBanEvent(ctx, event); err != nil {
|
||||
@@ -682,6 +758,12 @@ func HandleBanNotification(ctx context.Context, server config.Fail2banServer, ip
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check if email alerts for bans are enabled
|
||||
if !settings.EmailAlertsForBans {
|
||||
log.Printf("❌ Email alerts for bans are disabled. No alert sent for IP %s", ip)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Send email notification
|
||||
if err := sendBanAlert(ip, jail, hostname, failures, whoisData, filteredLogs, country, settings); err != nil {
|
||||
log.Printf("❌ Failed to send alert email: %v", err)
|
||||
@@ -692,6 +774,93 @@ func HandleBanNotification(ctx context.Context, server config.Fail2banServer, ip
|
||||
return nil
|
||||
}
|
||||
|
||||
// HandleUnbanNotification processes Fail2Ban unban notifications, stores the event, and sends alerts.
|
||||
func HandleUnbanNotification(ctx context.Context, server config.Fail2banServer, ip, jail, hostname, whois, country string) error {
|
||||
// Load settings to get alert countries and GeoIP provider
|
||||
settings := config.GetSettings()
|
||||
|
||||
// Perform whois lookup if not provided
|
||||
var whoisData string
|
||||
var err error
|
||||
if whois == "" || whois == "missing whois program" {
|
||||
log.Printf("Performing whois lookup for IP %s", ip)
|
||||
whoisData, err = lookupWhois(ip)
|
||||
if err != nil {
|
||||
log.Printf("⚠️ Whois lookup failed for IP %s: %v", ip, err)
|
||||
whoisData = ""
|
||||
}
|
||||
} else {
|
||||
log.Printf("Using provided whois data for IP %s", ip)
|
||||
whoisData = whois
|
||||
}
|
||||
|
||||
// Lookup the country for the given IP if not provided
|
||||
if country == "" {
|
||||
country, err = lookupCountry(ip, settings.GeoIPProvider, settings.GeoIPDatabasePath)
|
||||
if err != nil {
|
||||
log.Printf("⚠️ GeoIP lookup failed for IP %s: %v", ip, err)
|
||||
// Try to extract country from whois as fallback
|
||||
if whoisData != "" {
|
||||
country = extractCountryFromWhois(whoisData)
|
||||
if country != "" {
|
||||
log.Printf("Extracted country %s from whois data for IP %s", country, ip)
|
||||
}
|
||||
}
|
||||
if country == "" {
|
||||
country = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
event := storage.BanEventRecord{
|
||||
ServerID: server.ID,
|
||||
ServerName: server.Name,
|
||||
Jail: jail,
|
||||
IP: ip,
|
||||
Country: country,
|
||||
Hostname: hostname,
|
||||
Failures: "",
|
||||
Whois: whoisData,
|
||||
Logs: "",
|
||||
EventType: "unban",
|
||||
OccurredAt: time.Now().UTC(),
|
||||
}
|
||||
if err := storage.RecordBanEvent(ctx, event); err != nil {
|
||||
log.Printf("⚠️ Failed to record unban event: %v", err)
|
||||
}
|
||||
|
||||
// Broadcast unban event to WebSocket clients
|
||||
if wsHub != nil {
|
||||
wsHub.BroadcastUnbanEvent(event)
|
||||
}
|
||||
|
||||
// Check if email alerts for unbans are enabled
|
||||
if !settings.EmailAlertsForUnbans {
|
||||
log.Printf("❌ Email alerts for unbans are disabled. No alert sent for IP %s", ip)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check if country is in alert list
|
||||
displayCountry := country
|
||||
if displayCountry == "" {
|
||||
displayCountry = "UNKNOWN"
|
||||
}
|
||||
|
||||
if !shouldAlertForCountry(country, settings.AlertCountries) {
|
||||
log.Printf("❌ IP %s belongs to %s, which is NOT in alert countries (%v). No alert sent.", ip, displayCountry, settings.AlertCountries)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Send email notification
|
||||
if err := sendUnbanAlert(ip, jail, hostname, whoisData, country, settings); err != nil {
|
||||
log.Printf("❌ Failed to send unban alert email: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
log.Printf("✅ Email alert sent for unbanned IP %s (%s)", ip, displayCountry)
|
||||
return nil
|
||||
}
|
||||
|
||||
// lookupCountry finds the country ISO code for a given IP using the configured provider.
|
||||
func lookupCountry(ip, provider, dbPath string) (string, error) {
|
||||
switch provider {
|
||||
@@ -2320,7 +2489,11 @@ func sendBanAlert(ip, jail, hostname, failures, whois, logs, country string, set
|
||||
// Get translations
|
||||
var subject string
|
||||
if isLOTRMode {
|
||||
subject = fmt.Sprintf("[Middle-earth] The Dark Lord's Servant Has Been Banished: %s from %s", ip, hostname)
|
||||
subject = fmt.Sprintf("[Middle-earth] %s: %s %s %s",
|
||||
getEmailTranslation(lang, "lotr.email.title"),
|
||||
ip,
|
||||
getEmailTranslation(lang, "email.ban.subject.from"),
|
||||
hostname)
|
||||
} else {
|
||||
subject = fmt.Sprintf("[Fail2Ban] %s: %s %s %s %s", jail,
|
||||
getEmailTranslation(lang, "email.ban.subject.banned"),
|
||||
@@ -2337,19 +2510,10 @@ func sendBanAlert(ip, jail, hostname, failures, whois, logs, country string, set
|
||||
var title, intro, whoisTitle, logsTitle, footerText string
|
||||
if isLOTRMode {
|
||||
title = getEmailTranslation(lang, "lotr.email.title")
|
||||
if title == "lotr.email.title" {
|
||||
title = "A Dark Servant Has Been Banished"
|
||||
}
|
||||
intro = getEmailTranslation(lang, "lotr.email.intro")
|
||||
if intro == "lotr.email.intro" {
|
||||
intro = "The guardians of Middle-earth have detected a threat and banished it from the realm."
|
||||
}
|
||||
whoisTitle = getEmailTranslation(lang, "email.ban.whois_title")
|
||||
logsTitle = getEmailTranslation(lang, "email.ban.logs_title")
|
||||
footerText = getEmailTranslation(lang, "lotr.email.footer")
|
||||
if footerText == "lotr.email.footer" {
|
||||
footerText = "May the servers be protected. One ban to rule them all."
|
||||
}
|
||||
} else {
|
||||
title = getEmailTranslation(lang, "email.ban.title")
|
||||
intro = getEmailTranslation(lang, "email.ban.intro")
|
||||
@@ -2364,34 +2528,15 @@ func sendBanAlert(ip, jail, hostname, failures, whois, logs, country string, set
|
||||
if isLOTRMode {
|
||||
// Transform labels to LOTR terminology
|
||||
bannedIPLabel := getEmailTranslation(lang, "lotr.email.details.dark_servant_location")
|
||||
if bannedIPLabel == "lotr.email.details.dark_servant_location" {
|
||||
bannedIPLabel = "The Dark Servant's Location"
|
||||
}
|
||||
jailLabel := getEmailTranslation(lang, "lotr.email.details.realm_protection")
|
||||
if jailLabel == "lotr.email.details.realm_protection" {
|
||||
jailLabel = "The Realm of Protection"
|
||||
}
|
||||
countryLabelKey := getEmailTranslation(lang, "lotr.email.details.origins")
|
||||
var countryLabel string
|
||||
if countryLabelKey == "lotr.email.details.origins" {
|
||||
// Use default English format
|
||||
if country != "" {
|
||||
countryLabel = fmt.Sprintf("Origins from the %s Lands", country)
|
||||
} else {
|
||||
countryLabel = "Origins from Unknown Lands"
|
||||
}
|
||||
} else {
|
||||
// Use translated label and append country
|
||||
if country != "" {
|
||||
countryLabel = fmt.Sprintf("%s %s", countryLabelKey, country)
|
||||
} else {
|
||||
countryLabel = fmt.Sprintf("%s Unknown", countryLabelKey)
|
||||
}
|
||||
}
|
||||
timestampLabel := getEmailTranslation(lang, "lotr.email.details.banished_at")
|
||||
if timestampLabel == "lotr.email.details.banished_at" {
|
||||
timestampLabel = "Banished at the"
|
||||
}
|
||||
|
||||
details = []emailDetail{
|
||||
{Label: bannedIPLabel, Value: ip},
|
||||
@@ -2428,6 +2573,87 @@ func sendBanAlert(ip, jail, hostname, failures, whois, logs, country string, set
|
||||
return sendEmail(settings.Destemail, subject, body, settings)
|
||||
}
|
||||
|
||||
// *******************************************************************
|
||||
// * sendUnbanAlert Function : *
|
||||
// *******************************************************************
|
||||
func sendUnbanAlert(ip, jail, hostname, whois, country string, settings config.AppSettings) error {
|
||||
lang := settings.Language
|
||||
if lang == "" {
|
||||
lang = "en"
|
||||
}
|
||||
|
||||
isLOTRMode := isLOTRModeActive(settings.AlertCountries)
|
||||
|
||||
// Get translations
|
||||
var subject string
|
||||
if isLOTRMode {
|
||||
subject = fmt.Sprintf("[Middle-earth] %s: %s %s %s",
|
||||
getEmailTranslation(lang, "lotr.email.unban.title"),
|
||||
ip,
|
||||
getEmailTranslation(lang, "email.unban.subject.from"),
|
||||
hostname)
|
||||
} else {
|
||||
subject = fmt.Sprintf("[Fail2Ban] %s: %s %s %s %s", jail,
|
||||
getEmailTranslation(lang, "email.unban.subject.unbanned"),
|
||||
ip,
|
||||
getEmailTranslation(lang, "email.unban.subject.from"),
|
||||
hostname)
|
||||
}
|
||||
|
||||
// Determine email style and LOTR mode
|
||||
emailStyle := getEmailStyle()
|
||||
isModern := emailStyle == "modern"
|
||||
|
||||
// Get translations - use LOTR translations if in LOTR mode
|
||||
var title, intro, whoisTitle, footerText string
|
||||
if isLOTRMode {
|
||||
title = getEmailTranslation(lang, "lotr.email.unban.title")
|
||||
intro = getEmailTranslation(lang, "lotr.email.unban.intro")
|
||||
whoisTitle = getEmailTranslation(lang, "email.ban.whois_title")
|
||||
footerText = getEmailTranslation(lang, "lotr.email.footer")
|
||||
} else {
|
||||
title = getEmailTranslation(lang, "email.unban.title")
|
||||
intro = getEmailTranslation(lang, "email.unban.intro")
|
||||
whoisTitle = getEmailTranslation(lang, "email.ban.whois_title")
|
||||
footerText = getEmailTranslation(lang, "email.footer.text")
|
||||
}
|
||||
supportEmail := "support@swissmakers.ch"
|
||||
|
||||
// Format details - use shared keys for common fields, LOTR-specific only for restored_ip
|
||||
var details []emailDetail
|
||||
if isLOTRMode {
|
||||
details = []emailDetail{
|
||||
{Label: getEmailTranslation(lang, "lotr.email.unban.details.restored_ip"), Value: ip},
|
||||
{Label: getEmailTranslation(lang, "email.unban.details.jail"), Value: jail},
|
||||
{Label: getEmailTranslation(lang, "email.unban.details.hostname"), Value: hostname},
|
||||
{Label: getEmailTranslation(lang, "email.unban.details.country"), Value: country},
|
||||
{Label: getEmailTranslation(lang, "email.unban.details.timestamp"), Value: time.Now().UTC().Format(time.RFC3339)},
|
||||
}
|
||||
} else {
|
||||
details = []emailDetail{
|
||||
{Label: getEmailTranslation(lang, "email.unban.details.unbanned_ip"), Value: ip},
|
||||
{Label: getEmailTranslation(lang, "email.unban.details.jail"), Value: jail},
|
||||
{Label: getEmailTranslation(lang, "email.unban.details.hostname"), Value: hostname},
|
||||
{Label: getEmailTranslation(lang, "email.unban.details.country"), Value: country},
|
||||
{Label: getEmailTranslation(lang, "email.unban.details.timestamp"), Value: time.Now().UTC().Format(time.RFC3339)},
|
||||
}
|
||||
}
|
||||
|
||||
whoisHTML := formatWhoisForEmail(whois, lang, isModern)
|
||||
|
||||
var body string
|
||||
if isLOTRMode {
|
||||
// Use LOTR-themed email template
|
||||
body = buildLOTREmailBody(title, intro, details, whoisHTML, "", whoisTitle, "", footerText)
|
||||
} else if isModern {
|
||||
body = buildModernEmailBody(title, intro, details, whoisHTML, "", whoisTitle, "", footerText)
|
||||
} else {
|
||||
body = buildClassicEmailBody(title, intro, details, whoisHTML, "", whoisTitle, "", footerText, supportEmail)
|
||||
}
|
||||
|
||||
return sendEmail(settings.Destemail, subject, body, settings)
|
||||
}
|
||||
|
||||
// *******************************************************************
|
||||
// * TestEmailHandler to send test-mail : *
|
||||
// *******************************************************************
|
||||
|
||||
@@ -69,6 +69,7 @@ func RegisterRoutes(r *gin.Engine, hub *Hub) {
|
||||
|
||||
// Handle Fail2Ban notifications
|
||||
api.POST("/ban", BanNotificationHandler)
|
||||
api.POST("/unban", UnbanNotificationHandler)
|
||||
|
||||
// Internal database overview
|
||||
api.GET("/events/bans", ListBanEventsHandler)
|
||||
|
||||
@@ -26,6 +26,10 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
#serverManagerList {
|
||||
min-height: 480px;
|
||||
}
|
||||
|
||||
/* Custom scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
@@ -168,6 +172,18 @@ mark {
|
||||
box-shadow: 0 15px 20px -3px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.toast-unban-event {
|
||||
background-color: #14532d;
|
||||
pointer-events: auto;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.toast-unban-event:hover {
|
||||
background-color: #166534;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 15px 20px -3px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
/* Backend Status Indicator */
|
||||
#backendStatus {
|
||||
display: flex;
|
||||
|
||||
@@ -46,24 +46,29 @@ function showBanEventToast(event) {
|
||||
var container = document.getElementById('toast-container');
|
||||
if (!container || !event) return;
|
||||
|
||||
var isUnban = event.eventType === 'unban';
|
||||
var toast = document.createElement('div');
|
||||
toast.className = 'toast toast-ban-event';
|
||||
toast.className = isUnban ? 'toast toast-unban-event' : 'toast toast-ban-event';
|
||||
|
||||
var ip = event.ip || 'Unknown IP';
|
||||
var jail = event.jail || 'Unknown Jail';
|
||||
var server = event.serverName || event.serverId || 'Unknown Server';
|
||||
var country = event.country || 'UNKNOWN';
|
||||
|
||||
var title = isUnban ? t('toast.unban.title', 'IP unblocked') : t('toast.ban.title', 'New block occurred');
|
||||
var action = isUnban ? t('toast.unban.action', 'unblocked from') : t('toast.ban.action', 'banned in');
|
||||
var icon = isUnban ? 'fas fa-check-circle text-green-400' : 'fas fa-shield-alt text-red-500';
|
||||
|
||||
toast.innerHTML = ''
|
||||
+ '<div class="flex items-start gap-3">'
|
||||
+ ' <div class="flex-shrink-0 mt-1">'
|
||||
+ ' <i class="fas fa-shield-alt text-red-500"></i>'
|
||||
+ ' <i class="' + icon + '"></i>'
|
||||
+ ' </div>'
|
||||
+ ' <div class="flex-1 min-w-0">'
|
||||
+ ' <div class="font-semibold text-sm">New block occurred</div>'
|
||||
+ ' <div class="font-semibold text-sm">' + title + '</div>'
|
||||
+ ' <div class="text-sm mt-1">'
|
||||
+ ' <span class="font-mono font-semibold">' + escapeHtml(ip) + '</span>'
|
||||
+ ' <span> banned in </span>'
|
||||
+ ' <span> ' + action + ' </span>'
|
||||
+ ' <span class="font-semibold">' + escapeHtml(jail) + '</span>'
|
||||
+ ' </div>'
|
||||
+ ' <div class="text-xs text-gray-400 mt-1">'
|
||||
|
||||
@@ -85,7 +85,7 @@ function fetchBanEventsData() {
|
||||
});
|
||||
}
|
||||
|
||||
// Add new ban event from WebSocket
|
||||
// Add new ban or unban event from WebSocket
|
||||
function addBanEventFromWebSocket(event) {
|
||||
// Check if event already exists (prevent duplicates)
|
||||
// Only check by ID if both events have IDs
|
||||
@@ -95,16 +95,21 @@ function addBanEventFromWebSocket(event) {
|
||||
return e.id === event.id;
|
||||
});
|
||||
} else {
|
||||
// If no ID, check by IP, jail, and occurredAt timestamp
|
||||
// If no ID, check by IP, jail, eventType, and occurredAt timestamp
|
||||
exists = latestBanEvents.some(function(e) {
|
||||
return e.ip === event.ip &&
|
||||
e.jail === event.jail &&
|
||||
e.eventType === event.eventType &&
|
||||
e.occurredAt === event.occurredAt;
|
||||
});
|
||||
}
|
||||
|
||||
if (!exists) {
|
||||
console.log('Adding new ban event from WebSocket:', event);
|
||||
// Ensure eventType is set (default to 'ban' for backward compatibility)
|
||||
if (!event.eventType) {
|
||||
event.eventType = 'ban';
|
||||
}
|
||||
console.log('Adding new event from WebSocket:', event);
|
||||
|
||||
// Prepend to the beginning of the array
|
||||
latestBanEvents.unshift(event);
|
||||
@@ -121,7 +126,7 @@ function addBanEventFromWebSocket(event) {
|
||||
// Refresh dashboard data (summary, stats, insights) and re-render
|
||||
refreshDashboardData();
|
||||
} else {
|
||||
console.log('Skipping duplicate ban event:', event);
|
||||
console.log('Skipping duplicate event:', event);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -453,9 +458,8 @@ function unbanIP(jail, ip) {
|
||||
.then(function(data) {
|
||||
if (data.error) {
|
||||
showToast("Error unbanning IP: " + data.error, 'error');
|
||||
} else {
|
||||
showToast(data.message || "IP unbanned successfully", 'success');
|
||||
}
|
||||
// Don't show success toast here - the WebSocket unban event will show a proper toast
|
||||
return refreshData({ silent: true });
|
||||
})
|
||||
.catch(function(err) {
|
||||
@@ -761,12 +765,19 @@ function renderLogOverviewContent() {
|
||||
if (event.ip && recurringMap[event.ip]) {
|
||||
ipCell += ' <span class="ml-2 inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-yellow-100 text-yellow-800">' + t('logs.badge.recurring', 'Recurring') + '</span>';
|
||||
}
|
||||
var eventType = event.eventType || 'ban';
|
||||
var eventTypeBadge = '';
|
||||
if (eventType === 'unban') {
|
||||
eventTypeBadge = ' <span class="ml-2 inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-100 text-green-800">Unban</span>';
|
||||
} else {
|
||||
eventTypeBadge = ' <span class="ml-2 inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-red-100 text-red-800">Ban</span>';
|
||||
}
|
||||
html += ''
|
||||
+ ' <tr class="hover:bg-gray-50">'
|
||||
+ ' <td class="px-2 py-2 whitespace-nowrap">' + escapeHtml(formatDateTime(event.occurredAt || event.createdAt)) + '</td>'
|
||||
+ ' <td class="px-2 py-2 whitespace-nowrap">' + serverCell + '</td>'
|
||||
+ ' <td class="hidden sm:table-cell px-2 py-2 whitespace-nowrap">' + jailCell + '</td>'
|
||||
+ ' <td class="px-2 py-2 whitespace-nowrap">' + ipCell + '</td>'
|
||||
+ ' <td class="px-2 py-2 whitespace-nowrap">' + ipCell + eventTypeBadge + '</td>'
|
||||
+ ' <td class="hidden md:table-cell px-2 py-2 whitespace-nowrap">' + escapeHtml(event.country || '—') + '</td>'
|
||||
+ ' <td class="px-2 py-2 whitespace-nowrap">'
|
||||
+ ' <div class="flex gap-2">'
|
||||
|
||||
@@ -260,7 +260,7 @@ function openManageJailsModal() {
|
||||
const jsEscapedJailName = jail.jailName.replace(/'/g, "\\'");
|
||||
return ''
|
||||
+ '<div class="flex items-center justify-between gap-3 p-3 bg-gray-50">'
|
||||
+ ' <span class="text-sm font-medium flex-1">' + escapedJailName + '</span>'
|
||||
+ ' <span class="text-sm font-medium flex-1 text-gray-900">' + escapedJailName + '</span>'
|
||||
+ ' <div class="flex items-center gap-3">'
|
||||
+ ' <button'
|
||||
+ ' type="button"'
|
||||
@@ -282,7 +282,7 @@ function openManageJailsModal() {
|
||||
+ ' class="w-11 h-6 bg-gray-200 rounded-full peer-focus:ring-4 peer-focus:ring-blue-300 peer-checked:bg-blue-600 transition-colors"'
|
||||
+ ' ></div>'
|
||||
+ ' <span'
|
||||
+ ' class="absolute left-1 top-1 bg-white w-4 h-4 rounded-full transition-transform peer-checked:translate-x-5"'
|
||||
+ ' class="absolute left-1 top-1/2 -translate-y-1/2 bg-white w-4 h-4 rounded-full transition-transform peer-checked:translate-x-5"'
|
||||
+ ' ></span>'
|
||||
+ ' </label>'
|
||||
+ ' </div>'
|
||||
|
||||
@@ -13,6 +13,32 @@ function onGeoIPProviderChange(provider) {
|
||||
}
|
||||
}
|
||||
|
||||
// Update email fields state based on checkbox preferences
|
||||
function updateEmailFieldsState() {
|
||||
const emailAlertsForBans = document.getElementById('emailAlertsForBans').checked;
|
||||
const emailAlertsForUnbans = document.getElementById('emailAlertsForUnbans').checked;
|
||||
const emailEnabled = emailAlertsForBans || emailAlertsForUnbans;
|
||||
|
||||
// Get all email-related fields
|
||||
const emailFields = [
|
||||
document.getElementById('destEmail'),
|
||||
document.getElementById('smtpHost'),
|
||||
document.getElementById('smtpPort'),
|
||||
document.getElementById('smtpUsername'),
|
||||
document.getElementById('smtpPassword'),
|
||||
document.getElementById('smtpFrom'),
|
||||
document.getElementById('smtpUseTLS'),
|
||||
document.getElementById('sendTestEmailBtn')
|
||||
];
|
||||
|
||||
// Enable/disable all email fields
|
||||
emailFields.forEach(field => {
|
||||
if (field) {
|
||||
field.disabled = !emailEnabled;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function loadSettings() {
|
||||
showLoading(true);
|
||||
fetch('/api/settings')
|
||||
@@ -78,6 +104,11 @@ function loadSettings() {
|
||||
|
||||
document.getElementById('destEmail').value = data.destemail || '';
|
||||
|
||||
// Load email alert preferences
|
||||
document.getElementById('emailAlertsForBans').checked = data.emailAlertsForBans !== undefined ? data.emailAlertsForBans : true;
|
||||
document.getElementById('emailAlertsForUnbans').checked = data.emailAlertsForUnbans !== undefined ? data.emailAlertsForUnbans : false;
|
||||
updateEmailFieldsState();
|
||||
|
||||
const select = document.getElementById('alertCountries');
|
||||
for (let i = 0; i < select.options.length; i++) {
|
||||
select.options[i].selected = false;
|
||||
@@ -174,6 +205,8 @@ function saveSettings(event) {
|
||||
callbackUrl: callbackUrl,
|
||||
callbackSecret: document.getElementById('callbackSecret').value.trim(),
|
||||
alertCountries: selectedCountries.length > 0 ? selectedCountries : ["ALL"],
|
||||
emailAlertsForBans: document.getElementById('emailAlertsForBans').checked,
|
||||
emailAlertsForUnbans: document.getElementById('emailAlertsForUnbans').checked,
|
||||
bantimeIncrement: document.getElementById('bantimeIncrement').checked,
|
||||
defaultJailEnable: document.getElementById('defaultJailEnable').checked,
|
||||
bantime: document.getElementById('banTime').value.trim(),
|
||||
|
||||
@@ -93,6 +93,9 @@ class WebSocketManager {
|
||||
case 'ban_event':
|
||||
this.handleBanEvent(message.data);
|
||||
break;
|
||||
case 'unban_event':
|
||||
this.handleBanEvent(message.data); // Use same handler for unban events
|
||||
break;
|
||||
case 'heartbeat':
|
||||
this.handleHeartbeat(message);
|
||||
break;
|
||||
|
||||
@@ -419,10 +419,42 @@ body.lotr-mode .bg-gray-50 {
|
||||
}
|
||||
|
||||
body.lotr-mode .text-blue-600 {
|
||||
color: var(--lotr-gold);
|
||||
color: var(--lotr-gold) !important;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Text colors in LOTR mode for better visibility */
|
||||
body.lotr-mode .bg-gray-50 .text-gray-900,
|
||||
body.lotr-mode .bg-gray-50 .text-sm,
|
||||
body.lotr-mode .bg-gray-50 .text-sm.font-medium {
|
||||
color: var(--lotr-text-dark) !important;
|
||||
}
|
||||
|
||||
/* Open insights button styling */
|
||||
body.lotr-mode .border-blue-200 {
|
||||
border-color: var(--lotr-gold) !important;
|
||||
}
|
||||
|
||||
body.lotr-mode .hover\:bg-blue-50:hover {
|
||||
background-color: rgba(212, 175, 55, 0.1) !important;
|
||||
}
|
||||
|
||||
/* Logpath results styling */
|
||||
body.lotr-mode #logpathResults,
|
||||
body.lotr-mode pre#logpathResults {
|
||||
background: var(--lotr-warm-parchment) !important;
|
||||
color: var(--lotr-text-dark) !important;
|
||||
border: 2px solid var(--lotr-stone-gray) !important;
|
||||
}
|
||||
|
||||
body.lotr-mode #logpathResults.text-red-600 {
|
||||
color: var(--lotr-fire-red) !important;
|
||||
}
|
||||
|
||||
body.lotr-mode #logpathResults.text-yellow-600 {
|
||||
color: var(--lotr-dark-gold) !important;
|
||||
}
|
||||
|
||||
/* Tables */
|
||||
body.lotr-mode table {
|
||||
border-collapse: separate;
|
||||
@@ -570,12 +602,71 @@ body.lotr-mode .select2-container--default .select2-selection--multiple .select2
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
body.lotr-mode .select2-container--default .select2-selection--multiple > .select2-selection--inline > input.select2-search__field {
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
body.lotr-mode .select2-container--default .select2-selection--multiple .select2-selection__choice__remove {
|
||||
color: var(--lotr-text-dark) !important;
|
||||
font-weight: 700;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
/* Select2 Dropdown Results Styling */
|
||||
body.lotr-mode .select2-results__options {
|
||||
background: var(--lotr-warm-parchment) !important;
|
||||
border: 2px solid var(--lotr-stone-gray) !important;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4) !important;
|
||||
}
|
||||
|
||||
body.lotr-mode .select2-results__option {
|
||||
background: transparent !important;
|
||||
color: var(--lotr-text-dark) !important;
|
||||
padding: 8px 12px;
|
||||
border-bottom: 1px solid var(--lotr-stone-gray);
|
||||
}
|
||||
|
||||
body.lotr-mode .select2-results__option:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
body.lotr-mode .select2-results__option--highlighted,
|
||||
body.lotr-mode .select2-results__option[aria-selected="true"] {
|
||||
background: linear-gradient(135deg, var(--lotr-gold) 0%, var(--lotr-dark-gold) 100%) !important;
|
||||
color: var(--lotr-text-dark) !important;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
body.lotr-mode .select2-results__option--highlighted[aria-selected="true"] {
|
||||
background: linear-gradient(135deg, var(--lotr-bright-gold) 0%, var(--lotr-gold) 100%) !important;
|
||||
}
|
||||
|
||||
/* Ignore IP Tags Styling */
|
||||
body.lotr-mode .ignore-ip-tag {
|
||||
background: linear-gradient(135deg, var(--lotr-warm-parchment) 0%, var(--lotr-dark-parchment) 100%) !important;
|
||||
color: var(--lotr-text-dark) !important;
|
||||
border: 2px solid var(--lotr-stone-gray) !important;
|
||||
font-weight: 600;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
body.lotr-mode .ignore-ip-tag button {
|
||||
color: var(--lotr-text-dark) !important;
|
||||
font-weight: 700;
|
||||
font-size: 1.1em;
|
||||
line-height: 1;
|
||||
padding: 0 2px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
body.lotr-mode .ignore-ip-tag button:hover {
|
||||
color: var(--lotr-fire-red) !important;
|
||||
transform: scale(1.2);
|
||||
text-shadow: 0 0 4px rgba(193, 18, 31, 0.5);
|
||||
}
|
||||
|
||||
/* Scrollbar Styling */
|
||||
body.lotr-mode ::-webkit-scrollbar {
|
||||
width: 14px;
|
||||
@@ -744,6 +835,52 @@ body.lotr-mode input[type="radio"] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Toggle Switch Styling for LOTR Mode */
|
||||
body.lotr-mode label.inline-flex.relative.items-center.cursor-pointer {
|
||||
position: relative;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Toggle switch track - default state */
|
||||
body.lotr-mode label.inline-flex.relative.items-center.cursor-pointer > div.w-11 {
|
||||
background: var(--lotr-stone-gray) !important;
|
||||
border: 2px solid var(--lotr-brown) !important;
|
||||
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.3) !important;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Toggle switch track - checked state (using peer-checked) */
|
||||
body.lotr-mode label.inline-flex.relative.items-center.cursor-pointer input.peer:checked ~ div {
|
||||
background: linear-gradient(135deg, var(--lotr-bright-gold) 0%, var(--lotr-gold) 100%) !important;
|
||||
border-color: var(--lotr-gold) !important;
|
||||
box-shadow:
|
||||
0 0 10px rgba(212, 175, 55, 0.5),
|
||||
inset 0 2px 4px rgba(255, 255, 255, 0.3) !important;
|
||||
}
|
||||
|
||||
/* Toggle switch focus ring */
|
||||
body.lotr-mode label.inline-flex.relative.items-center.cursor-pointer input.peer:focus ~ div {
|
||||
box-shadow:
|
||||
0 0 0 4px rgba(212, 175, 55, 0.3),
|
||||
inset 0 2px 4px rgba(0, 0, 0, 0.3) !important;
|
||||
}
|
||||
|
||||
/* Toggle switch thumb (the circle) - properly centered */
|
||||
body.lotr-mode label.inline-flex.relative.items-center.cursor-pointer > span.absolute {
|
||||
background: var(--lotr-parchment) !important;
|
||||
border: 2px solid var(--lotr-brown) !important;
|
||||
box-shadow:
|
||||
0 2px 4px rgba(0, 0, 0, 0.3),
|
||||
inset 0 1px 2px rgba(255, 255, 255, 0.5) !important;
|
||||
top: 50% !important;
|
||||
transform: translateY(-50%) !important;
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
|
||||
body.lotr-mode label.inline-flex.relative.items-center.cursor-pointer input.peer:checked ~ span {
|
||||
transform: translateX(1.25rem) translateY(-50%) !important;
|
||||
}
|
||||
|
||||
/* Labels */
|
||||
body.lotr-mode label {
|
||||
color: var(--lotr-text-dark);
|
||||
|
||||
@@ -341,9 +341,24 @@
|
||||
<div class="bg-white rounded-lg shadow p-6">
|
||||
<h3 class="text-lg font-medium text-gray-900 mb-4" data-i18n="settings.alert">Alert Settings</h3>
|
||||
|
||||
<!-- Email Alert Preferences -->
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2" data-i18n="settings.email_alerts">Email Alert Preferences</label>
|
||||
<div class="space-y-2">
|
||||
<label class="flex items-center">
|
||||
<input type="checkbox" id="emailAlertsForBans" class="rounded border-gray-300 text-blue-600 focus:ring-blue-500" onchange="updateEmailFieldsState()">
|
||||
<span class="ml-2 text-sm text-gray-700" data-i18n="settings.email_alerts_for_bans">Enable email alerts for bans</span>
|
||||
</label>
|
||||
<label class="flex items-center">
|
||||
<input type="checkbox" id="emailAlertsForUnbans" class="rounded border-gray-300 text-blue-600 focus:ring-blue-500" onchange="updateEmailFieldsState()">
|
||||
<span class="ml-2 text-sm text-gray-700" data-i18n="settings.email_alerts_for_unbans">Enable email alerts for unbans</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4" id="emailFieldsContainer">
|
||||
<label for="destEmail" class="block text-sm font-medium text-gray-700 mb-2" data-i18n="settings.destination_email">Destination Email (Alerts Receiver)</label>
|
||||
<input type="email" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" id="destEmail"
|
||||
<input type="email" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:bg-gray-100 disabled:cursor-not-allowed" id="destEmail"
|
||||
data-i18n-placeholder="settings.destination_email_placeholder" placeholder="alerts@swissmakers.ch" />
|
||||
<p class="text-xs text-red-600 mt-1 hidden" id="destEmailError"></p>
|
||||
</div>
|
||||
@@ -579,40 +594,40 @@
|
||||
</div>
|
||||
|
||||
<!-- SMTP Configuration Group -->
|
||||
<div class="bg-white rounded-lg shadow p-6">
|
||||
<div class="bg-white rounded-lg shadow p-6" id="smtpFieldsContainer">
|
||||
<h3 class="text-lg font-medium text-gray-900 mb-4" data-i18n="settings.smtp">SMTP Configuration</h3>
|
||||
<div class="mb-4">
|
||||
<label for="smtpHost" class="block text-sm font-medium text-gray-700 mb-2" data-i18n="settings.smtp_host">SMTP Host</label>
|
||||
<input type="text" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" id="smtpHost"
|
||||
<input type="text" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:bg-gray-100 disabled:cursor-not-allowed" id="smtpHost"
|
||||
data-i18n-placeholder="settings.smtp_host_placeholder" placeholder="e.g., smtp.gmail.com" required />
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label for="smtpPort" class="block text-sm font-medium text-gray-700 mb-2" data-i18n="settings.smtp_port">SMTP Port</label>
|
||||
<select id="smtpPort" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
<select id="smtpPort" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:bg-gray-100 disabled:cursor-not-allowed">
|
||||
<option value="587" selected>587 (Recommended - STARTTLS)</option>
|
||||
<option value="465" disabled>465 (Not Supported)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label for="smtpUsername" class="block text-sm font-medium text-gray-700 mb-2" data-i18n="settings.smtp_username">SMTP Username</label>
|
||||
<input type="text" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" id="smtpUsername"
|
||||
<input type="text" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:bg-gray-100 disabled:cursor-not-allowed" id="smtpUsername"
|
||||
data-i18n-placeholder="settings.smtp_username_placeholder" placeholder="e.g., user@example.com" required />
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label for="smtpPassword" class="block text-sm font-medium text-gray-700 mb-2" data-i18n="settings.smtp_password">SMTP Password</label>
|
||||
<input type="password" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" id="smtpPassword"
|
||||
<input type="password" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:bg-gray-100 disabled:cursor-not-allowed" id="smtpPassword"
|
||||
data-i18n-placeholder="settings.smtp_password_placeholder" placeholder="Enter SMTP Password" required />
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label for="smtpFrom" class="block text-sm font-medium text-gray-700 mb-2" data-i18n="settings.smtp_sender">Sender Email</label>
|
||||
<input type="email" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" id="smtpFrom"
|
||||
<input type="email" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:bg-gray-100 disabled:cursor-not-allowed" id="smtpFrom"
|
||||
data-i18n-placeholder="settings.smtp_sender_placeholder" placeholder="noreply@swissmakers.ch" required />
|
||||
</div>
|
||||
<div class="flex items-center mb-4">
|
||||
<input type="checkbox" id="smtpUseTLS" class="h-4 w-7 text-blue-600 transition duration-150 ease-in-out">
|
||||
<input type="checkbox" id="smtpUseTLS" class="h-4 w-7 text-blue-600 transition duration-150 ease-in-out disabled:opacity-50 disabled:cursor-not-allowed">
|
||||
<label for="smtpUseTLS" class="ml-2 block text-sm text-gray-700" data-i18n="settings.smtp_tls">Use TLS (Recommended)</label>
|
||||
</div>
|
||||
<button type="button" class="bg-gray-600 text-white px-4 py-2 rounded hover:bg-gray-700 transition-colors" onclick="sendTestEmail()" data-i18n="settings.send_test_email">Send Test Email</button>
|
||||
<button type="button" class="bg-gray-600 text-white px-4 py-2 rounded hover:bg-gray-700 transition-colors disabled:bg-gray-400 disabled:cursor-not-allowed" onclick="sendTestEmail()" id="sendTestEmailBtn" data-i18n="settings.send_test_email">Send Test Email</button>
|
||||
</div>
|
||||
|
||||
<!-- Fail2Ban Configuration Group -->
|
||||
|
||||
@@ -169,6 +169,25 @@ func (h *Hub) BroadcastBanEvent(event storage.BanEventRecord) {
|
||||
}
|
||||
}
|
||||
|
||||
// BroadcastUnbanEvent broadcasts an unban event to all connected clients
|
||||
func (h *Hub) BroadcastUnbanEvent(event storage.BanEventRecord) {
|
||||
message := map[string]interface{}{
|
||||
"type": "unban_event",
|
||||
"data": event,
|
||||
}
|
||||
data, err := json.Marshal(message)
|
||||
if err != nil {
|
||||
log.Printf("Error marshaling unban event: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
select {
|
||||
case h.broadcast <- data:
|
||||
default:
|
||||
log.Printf("Broadcast channel full, dropping unban event")
|
||||
}
|
||||
}
|
||||
|
||||
// readPump pumps messages from the WebSocket connection to the hub
|
||||
func (c *Client) readPump() {
|
||||
defer func() {
|
||||
|
||||
Reference in New Issue
Block a user