Raw implement lotr-idea

This commit is contained in:
2025-12-01 23:25:54 +01:00
parent 35937c47ed
commit 66465d0080
10 changed files with 957 additions and 45 deletions

View File

@@ -21,6 +21,7 @@ import (
"log"
"os"
"strconv"
"strings"
"time"
"github.com/gin-gonic/gin"
@@ -75,8 +76,15 @@ func main() {
// Register all application routes, including the static files and templates.
web.RegisterRoutes(router)
printWelcomeBanner(serverPort)
log.Println("--- Fail2Ban-UI started in", gin.Mode(), "mode ---")
// Check if LOTR mode is active
isLOTRMode := isLOTRModeActive(settings.AlertCountries)
printWelcomeBanner(serverPort, isLOTRMode)
if isLOTRMode {
log.Println("--- Middle-earth Security Realm activated ---")
log.Println("🎭 LOTR Mode: The guardians of Middle-earth stand ready!")
} else {
log.Println("--- Fail2Ban-UI started in", gin.Mode(), "mode ---")
}
log.Println("Server listening on port", serverPort, ".")
// Start the server on port 8080.
@@ -85,10 +93,45 @@ func main() {
}
}
// isLOTRModeActive checks if LOTR mode is enabled in alert countries
func isLOTRModeActive(alertCountries []string) bool {
if len(alertCountries) == 0 {
return false
}
for _, country := range alertCountries {
if strings.EqualFold(country, "LOTR") {
return true
}
}
return false
}
// printWelcomeBanner prints a cool Tux banner with startup info.
func printWelcomeBanner(appPort string) {
func printWelcomeBanner(appPort string, isLOTRMode bool) {
greeting := getGreeting()
const tuxBanner = `
if isLOTRMode {
const lotrBanner = `
.--.
|o_o | %s
|:_/ |
// \ \
(| | )
/'\_ _/'\
\___)=(___/
Middle-earth Security Realm - LOTR Mode Activated
══════════════════════════════════════════════════
⚔️ The guardians of Middle-earth stand ready! ⚔️
Developers: https://swissmakers.ch
Mode: %s
Listening on: http://0.0.0.0:%s
══════════════════════════════════════════════════
`
fmt.Printf(lotrBanner, greeting, gin.Mode(), appPort)
} else {
const tuxBanner = `
.--.
|o_o | %s
|:_/ |
@@ -105,7 +148,8 @@ Listening on: http://0.0.0.0:%s
----------------------------------------------
`
fmt.Printf(tuxBanner, greeting, gin.Mode(), appPort)
fmt.Printf(tuxBanner, greeting, gin.Mode(), appPort)
}
}
// getGreeting returns a friendly greeting based on the time of day.

View File

@@ -255,6 +255,17 @@
"email.test.sample_logs": "2025-01-01T12:00:00Z Beispiel-Log-Eintrag von Fail2ban-UI.",
"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.footer.text": "Diese Nachricht wurde automatisch von Fail2Ban-UI generiert",
"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",
"lotr.email.footer": "Mögen die Server geschützt sein. Ein Bann, um sie alle zu beherrschen.",
"lotr.email.details.dark_servant_location": "Die Position des dunklen Dieners",
"lotr.email.details.realm_protection": "Das Reich des Schutzes",
"lotr.email.details.origins": "Herkunft aus den",
"lotr.email.details.banished_at": "Verbannt zur",
"lotr.banished": "Aus dem Reich verbannt",
"lotr.realms_protected": "Geschützte Reiche",
"lotr.threats_banished": "Verbannte Bedrohungen"
}

View File

@@ -255,6 +255,17 @@
"email.test.sample_logs": "2025-01-01T12:00:00Z Bispil-Log-Itrag vom Fail2ban-UI.",
"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.footer.text": "Diä Nachricht isch automatisch vom Fail2Ban-UI generiert worde",
"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",
"lotr.email.footer": "Möge d Server gschützt si. E Bann, um si alli z beherrsche.",
"lotr.email.details.dark_servant_location": "Dr Ort vom garstige Ork",
"lotr.email.details.realm_protection": "S Riich vom Schutz",
"lotr.email.details.origins": "Herkunft us de",
"lotr.email.details.banished_at": "Verbannt zur",
"lotr.banished": "Us em Riich verbannt",
"lotr.realms_protected": "Gschützti Riich",
"lotr.threats_banished": "Verbannti Bedrohige"
}

View File

@@ -255,6 +255,17 @@
"email.test.sample_logs": "2025-01-01T12:00:00Z Sample log entry of Fail2ban-UI.",
"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.footer.text": "This message was generated automatically by Fail2Ban-UI",
"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",
"lotr.email.footer": "May the servers be protected. One ban to rule them all.",
"lotr.email.details.dark_servant_location": "The Dark Servant's Location",
"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.banished": "Banished from the realm",
"lotr.realms_protected": "Realms Protected",
"lotr.threats_banished": "Threats Banished"
}

View File

@@ -255,5 +255,16 @@
"email.test.sample_logs": "2025-01-01T12:00:00Z Entrada de registro de ejemplo de Fail2ban-UI.",
"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.footer.text": "Este mensaje fue generado automáticamente por Fail2Ban-UI",
"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",
"lotr.email.footer": "Que los servidores estén protegidos. Un ban para gobernarlos a todos.",
"lotr.email.details.dark_servant_location": "La ubicación del siervo oscuro",
"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.banished": "Desterrado del reino",
"lotr.realms_protected": "Reinos protegidos",
"lotr.threats_banished": "Amenazas desterradas"
}

View File

@@ -253,7 +253,18 @@
"email.test.details.triggered_at": "Déclenché à",
"email.test.whois_no_data": "Aucune recherche WHOIS n'est exécutée pour les emails de test.",
"email.test.sample_logs": "2025-01-01T12:00:00Z Entrée de journal d'exemple de Fail2ban-UI.",
"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.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",
"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",
"lotr.email.footer": "Que les serveurs soient protégés. Un bannissement pour les gouverner tous.",
"lotr.email.details.dark_servant_location": "L'emplacement du serviteur des ténèbres",
"lotr.email.details.realm_protection": "Le royaume de la protection",
"lotr.email.details.origins": "Origines des",
"lotr.email.details.banished_at": "Banni à",
"lotr.banished": "Banni du royaume",
"lotr.realms_protected": "Royaumes protégés",
"lotr.threats_banished": "Menaces bannies"
}

View File

@@ -253,7 +253,18 @@
"email.test.details.triggered_at": "Attivato alle",
"email.test.whois_no_data": "Nessuna ricerca WHOIS viene eseguita per le email di test.",
"email.test.sample_logs": "2025-01-01T12:00:00Z Voce di log di esempio di Fail2ban-UI.",
"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.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",
"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",
"lotr.email.footer": "Possano i server essere protetti. Un ban per governarli tutti.",
"lotr.email.details.dark_servant_location": "La posizione del servitore oscuro",
"lotr.email.details.realm_protection": "Il regno della protezione",
"lotr.email.details.origins": "Origini dalle",
"lotr.email.details.banished_at": "Bandito alle",
"lotr.banished": "Bandito dal regno",
"lotr.realms_protected": "Regni protetti",
"lotr.threats_banished": "Minacce bandite"
}

View File

@@ -857,24 +857,24 @@ func GetSettingsHandler(c *gin.Context) {
config.DebugLog("----------------------------")
config.DebugLog("GetSettingsHandler called (handlers.go)") // entry point
s := config.GetSettings()
// Check if PORT environment variable is set
envPort, envPortSet := config.GetPortFromEnv()
// Create response with PORT env info
response := make(map[string]interface{})
responseBytes, _ := json.Marshal(s)
json.Unmarshal(responseBytes, &response)
// Add PORT environment variable information
response["portFromEnv"] = envPort
response["portEnvSet"] = envPortSet
// If PORT env is set, override the port value in response
if envPortSet {
response["port"] = envPort
}
c.JSON(http.StatusOK, response)
}
@@ -1131,6 +1131,19 @@ func getEmailStyle() string {
return "modern"
}
// isLOTRModeActive checks if LOTR mode is enabled in alert countries
func isLOTRModeActive(alertCountries []string) bool {
if len(alertCountries) == 0 {
return false
}
for _, country := range alertCountries {
if strings.EqualFold(country, "LOTR") {
return true
}
}
return false
}
// *******************************************************************
// * Unified Email Sending Function : *
// *******************************************************************
@@ -1308,6 +1321,101 @@ func buildClassicEmailBody(title, intro string, details []emailDetail, whoisHTML
</html>`, html.EscapeString(title), html.EscapeString(title), html.EscapeString(intro), detailRows, html.EscapeString(whoisTitle), whoisHTML, html.EscapeString(logsTitle), logsHTML, html.EscapeString(footerText), html.EscapeString(supportEmail), html.EscapeString(supportEmail), year)
}
// buildLOTREmailBody creates the dramatic LOTR-themed email template with "You Shall Not Pass" styling
func buildLOTREmailBody(title, intro string, details []emailDetail, whoisHTML, logsHTML, whoisTitle, logsTitle, footerText string) string {
detailRows := renderEmailDetails(details)
year := strconv.Itoa(time.Now().Year())
return fmt.Sprintf(`<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>%s</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { margin:0; padding:0; background: linear-gradient(135deg, #0d2818 0%%, #1a4d2e 50%%, #2d0a4f 100%%); font-family: Georgia, "Times New Roman", serif; color:#f4e8d0; line-height:1.6; -webkit-font-smoothing:antialiased; }
.email-wrapper { width:100%%; padding:20px 10px; background: linear-gradient(135deg, #0d2818 0%%, #1a4d2e 50%%, #2d0a4f 100%%); }
.email-container { max-width:640px; margin:0 auto; background:#f4e8d0; border:4px solid #d4af37; border-radius:12px; box-shadow:0 8px 32px rgba(0,0,0,0.6), inset 0 0 40px rgba(212,175,55,0.1); overflow:hidden; position:relative; }
.email-container::before { content:''; position:absolute; top:0; left:0; right:0; bottom:0; background: repeating-linear-gradient(0deg, transparent, transparent 2px, rgba(139,115,85,0.03) 2px, rgba(139,115,85,0.03) 4px); pointer-events:none; }
.email-header { background: linear-gradient(180deg, #c1121f 0%%, #ff6b35 30%%, #d4af37 70%%, #1a4d2e 100%%); color:#ffffff; padding:40px 28px; text-align:center; position:relative; overflow:hidden; }
.email-header::before { content:''; position:absolute; top:0; left:0; right:0; bottom:0; background: radial-gradient(circle at center, rgba(255,255,255,0.1) 0%%, transparent 70%%); animation: fireFlicker 3s ease-in-out infinite; }
@keyframes fireFlicker { 0%%,100%% { opacity:0.6; } 50%% { opacity:1; } }
.email-header-brand { margin:0 0 12px; font-size:12px; letter-spacing:0.4em; text-transform:uppercase; opacity:0.9; font-weight:600; font-family:'Cinzel', serif; position:relative; z-index:1; }
.email-header-title { margin:20px 0; font-size:42px; font-weight:700; line-height:1.1; text-shadow: 0 0 20px rgba(255,255,255,0.8), 0 0 40px rgba(255,107,53,0.6), 0 0 60px rgba(193,18,31,0.4); font-family:'Cinzel', serif; letter-spacing:0.1em; position:relative; z-index:1; animation: textGlow 2s ease-in-out infinite; }
@keyframes textGlow { 0%%,100%% { text-shadow: 0 0 20px rgba(255,255,255,0.8), 0 0 40px rgba(255,107,53,0.6), 0 0 60px rgba(193,18,31,0.4); } 50%% { text-shadow: 0 0 30px rgba(255,255,255,1), 0 0 60px rgba(255,107,53,0.8), 0 0 90px rgba(193,18,31,0.6); } }
.ring-divider { text-align:center; margin:30px 0; position:relative; }
.ring-divider::before { content:'⚔'; position:absolute; left:20%%; top:50%%; transform:translateY(-50%%); font-size:24px; color:#d4af37; background:#f4e8d0; padding:0 15px; }
.ring-divider::after { content:'⚔'; position:absolute; right:20%%; top:50%%; transform:translateY(-50%%); font-size:24px; color:#d4af37; background:#f4e8d0; padding:0 15px; }
.ring-divider-line { height:3px; background:linear-gradient(90deg, transparent 0%%, #d4af37 20%%, #d4af37 80%%, transparent 100%%); margin:0 25%%; }
.email-body { padding:36px 28px; background:#f4e8d0; color:#3d2817; }
.email-intro { font-size:18px; line-height:1.8; margin:0 0 28px; color:#3d2817; font-style:italic; text-align:center; }
.email-details-wrapper { background:#e8d5b7; border:3px solid #8b7355; border-radius:8px; padding:24px; margin:0 0 32px; box-shadow:inset 0 2px 4px rgba(0,0,0,0.1); }
.email-details-wrapper p { margin:12px 0; font-size:15px; line-height:1.7; color:#3d2817; }
.email-details-wrapper p:first-child { margin-top:0; }
.email-details-wrapper p:last-child { margin-bottom:0; }
.email-detail-label { font-weight:700; color:#1a4d2e; margin-right:8px; font-family:'Cinzel', serif; }
.email-section { margin:36px 0 0; }
.email-section-title { font-size:16px; text-transform:uppercase; letter-spacing:0.2em; color:#1a4d2e; margin:0 0 16px; font-weight:700; font-family:'Cinzel', serif; border-bottom:2px solid #d4af37; padding-bottom:8px; }
.email-terminal { background:#1a1a1a; color:#d4af37; padding:20px; font-family:"Courier New", Courier, monospace; border-radius:8px; font-size:13px; line-height:1.7; white-space:pre-wrap; word-break:break-word; overflow-x:auto; margin:0; border:2px solid #8b7355; box-shadow:inset 0 0 20px rgba(212,175,55,0.1); }
.email-log-stack { background:#0f0f0f; border-radius:8px; padding:16px; border:2px solid #8b7355; }
.email-log-line { font-family:"Courier New", Courier, monospace; font-size:12px; line-height:1.6; color:#d4af37; padding:8px 12px; border-radius:6px; margin:0 0 6px; background:rgba(212,175,55,0.1); border-left:3px solid #d4af37; }
.email-log-line:last-child { margin-bottom:0; }
.email-log-line-alert { background:rgba(193,18,31,0.3); color:#ff6b35; border-left-color:#c1121f; }
.email-muted { color:#8b7355; font-size:14px; line-height:1.6; font-style:italic; }
.email-footer { border-top:3px solid #d4af37; padding:24px 28px; font-size:13px; color:#3d2817; text-align:center; background:#e8d5b7; font-family:'Cinzel', serif; }
.email-footer-text { margin:0 0 8px; font-weight:600; }
.email-footer-copyright { margin:0; font-size:11px; color:#8b7355; }
@media only screen and (max-width:600px) {
.email-wrapper { padding:12px 8px; }
.email-header { padding:30px 20px; }
.email-header-title { font-size:32px; }
.email-body { padding:28px 20px; }
.email-intro { font-size:16px; }
.email-details-wrapper { padding:20px; }
.email-footer { padding:20px 16px; }
}
@media only screen and (max-width:480px) {
.email-header-title { font-size:28px; }
.email-body { padding:24px 16px; }
.email-details-wrapper { padding:16px; }
}
</style>
</head>
<body>
<div class="email-wrapper">
<div class="email-container">
<div class="email-header">
<p class="email-header-brand">Middle-earth Security</p>
<h1 class="email-header-title">YOU SHALL NOT PASS</h1>
<div class="ring-divider">
<div class="ring-divider-line"></div>
</div>
</div>
<div class="email-body">
<p class="email-intro">%s</p>
<div class="email-details-wrapper">
%s
</div>
<div class="email-section">
<p class="email-section-title">%s</p>
%s
</div>
<div class="email-section">
<p class="email-section-title">%s</p>
%s
</div>
</div>
<div class="email-footer">
<p class="email-footer-text">%s</p>
<p class="email-footer-copyright">© %s Swissmakers GmbH. All rights reserved.</p>
</div>
</div>
</div>
</body>
</html>`, html.EscapeString(title), html.EscapeString(intro), detailRows, html.EscapeString(whoisTitle), whoisHTML, html.EscapeString(logsTitle), logsHTML, html.EscapeString(footerText), year)
}
// buildModernEmailBody creates the modern responsive email template (new design)
func buildModernEmailBody(title, intro string, details []emailDetail, whoisHTML, logsHTML, whoisTitle, logsTitle, footerText string) string {
detailRows := renderEmailDetails(details)
@@ -1497,38 +1605,112 @@ func sendBanAlert(ip, jail, hostname, failures, whois, logs, country string, set
lang = "en"
}
// Get translations
subject := fmt.Sprintf("[Fail2Ban] %s: %s %s %s %s", jail,
getEmailTranslation(lang, "email.ban.subject.banned"),
ip,
getEmailTranslation(lang, "email.ban.subject.from"),
hostname)
// Check if LOTR mode is active for subject line
isLOTRMode := isLOTRModeActive(settings.AlertCountries)
details := []emailDetail{
{Label: getEmailTranslation(lang, "email.ban.details.banned_ip"), Value: ip},
{Label: getEmailTranslation(lang, "email.ban.details.jail"), Value: jail},
{Label: getEmailTranslation(lang, "email.ban.details.hostname"), Value: hostname},
{Label: getEmailTranslation(lang, "email.ban.details.failed_attempts"), Value: failures},
{Label: getEmailTranslation(lang, "email.ban.details.country"), Value: country},
{Label: getEmailTranslation(lang, "email.ban.details.timestamp"), Value: time.Now().UTC().Format(time.RFC3339)},
// 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)
} else {
subject = fmt.Sprintf("[Fail2Ban] %s: %s %s %s %s", jail,
getEmailTranslation(lang, "email.ban.subject.banned"),
ip,
getEmailTranslation(lang, "email.ban.subject.from"),
hostname)
}
title := getEmailTranslation(lang, "email.ban.title")
intro := getEmailTranslation(lang, "email.ban.intro")
whoisTitle := getEmailTranslation(lang, "email.ban.whois_title")
logsTitle := getEmailTranslation(lang, "email.ban.logs_title")
footerText := getEmailTranslation(lang, "email.footer.text")
supportEmail := "support@swissmakers.ch"
// Determine email style
// Determine email style and LOTR mode
emailStyle := getEmailStyle()
isModern := emailStyle == "modern"
// Get translations - use LOTR translations if in LOTR mode
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")
whoisTitle = getEmailTranslation(lang, "email.ban.whois_title")
logsTitle = getEmailTranslation(lang, "email.ban.logs_title")
footerText = getEmailTranslation(lang, "email.footer.text")
}
supportEmail := "support@swissmakers.ch"
// Format details with LOTR terminology if in LOTR mode
var details []emailDetail
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},
{Label: jailLabel, Value: jail},
{Label: getEmailTranslation(lang, "email.ban.details.hostname"), Value: hostname},
{Label: getEmailTranslation(lang, "email.ban.details.failed_attempts"), Value: failures},
{Label: countryLabel, Value: ""},
{Label: timestampLabel, Value: time.Now().UTC().Format(time.RFC3339)},
}
} else {
details = []emailDetail{
{Label: getEmailTranslation(lang, "email.ban.details.banned_ip"), Value: ip},
{Label: getEmailTranslation(lang, "email.ban.details.jail"), Value: jail},
{Label: getEmailTranslation(lang, "email.ban.details.hostname"), Value: hostname},
{Label: getEmailTranslation(lang, "email.ban.details.failed_attempts"), Value: failures},
{Label: getEmailTranslation(lang, "email.ban.details.country"), Value: country},
{Label: getEmailTranslation(lang, "email.ban.details.timestamp"), Value: time.Now().UTC().Format(time.RFC3339)},
}
}
whoisHTML := formatWhoisForEmail(whois, lang, isModern)
logsHTML := formatLogsForEmail(ip, logs, lang, isModern)
var body string
if isModern {
if isLOTRMode {
// Use LOTR-themed email template
body = buildLOTREmailBody(title, intro, details, whoisHTML, logsHTML, whoisTitle, logsTitle, footerText)
} else if isModern {
body = buildModernEmailBody(title, intro, details, whoisHTML, logsHTML, whoisTitle, logsTitle, footerText)
} else {
body = buildClassicEmailBody(title, intro, details, whoisHTML, logsHTML, whoisTitle, logsTitle, footerText, supportEmail)

View File

@@ -0,0 +1,439 @@
/* ============================================
LOTR Easter Egg Theme - Middle-earth Styling
============================================ */
/* Only apply when body has lotr-mode class */
body.lotr-mode {
/* Color Variables */
--lotr-forest-green: #1a4d2e;
--lotr-dark-green: #0d2818;
--lotr-gold: #d4af37;
--lotr-dark-gold: #b8941f;
--lotr-brown: #3d2817;
--lotr-stone-gray: #8b7355;
--lotr-parchment: #f4e8d0;
--lotr-dark-parchment: #e8d5b7;
--lotr-purple: #4a148c;
--lotr-dark-purple: #2d0a4f;
--lotr-fire-orange: #ff6b35;
--lotr-fire-red: #c1121f;
}
/* Base Theme Overrides */
body.lotr-mode {
background: linear-gradient(135deg, var(--lotr-dark-green) 0%, var(--lotr-forest-green) 50%, var(--lotr-brown) 100%);
background-attachment: fixed;
color: var(--lotr-parchment);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}
/* Typography */
body.lotr-mode h1,
body.lotr-mode h2,
body.lotr-mode h3,
body.lotr-mode .text-2xl,
body.lotr-mode .text-xl {
font-family: 'Cinzel', serif;
color: var(--lotr-gold);
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);
letter-spacing: 0.05em;
}
body.lotr-mode h1 {
font-weight: 700;
}
body.lotr-mode h2,
body.lotr-mode h3 {
font-weight: 600;
}
/* Cards - Parchment Style */
body.lotr-mode .bg-white {
background: var(--lotr-parchment) !important;
border: 3px solid var(--lotr-gold);
border-radius: 8px;
box-shadow:
0 4px 6px rgba(0, 0, 0, 0.3),
inset 0 1px 0 rgba(255, 255, 255, 0.2),
inset 0 -1px 0 rgba(0, 0, 0, 0.1);
position: relative;
}
body.lotr-mode .bg-white::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background:
repeating-linear-gradient(
0deg,
transparent,
transparent 2px,
rgba(139, 115, 85, 0.03) 2px,
rgba(139, 115, 85, 0.03) 4px
);
pointer-events: none;
border-radius: 8px;
}
body.lotr-mode .bg-white .text-gray-800,
body.lotr-mode .bg-white .text-gray-900 {
color: var(--lotr-brown) !important;
}
body.lotr-mode .bg-white .text-gray-700 {
color: var(--lotr-brown) !important;
}
/* Buttons - Medieval Shield Style */
body.lotr-mode button,
body.lotr-mode .bg-blue-500,
body.lotr-mode .bg-blue-600 {
background: linear-gradient(135deg, var(--lotr-gold) 0%, var(--lotr-dark-gold) 100%) !important;
border: 2px solid var(--lotr-brown) !important;
color: var(--lotr-brown) !important;
font-weight: 600;
text-shadow: 1px 1px 2px rgba(255, 255, 255, 0.3);
box-shadow:
0 2px 4px rgba(0, 0, 0, 0.3),
inset 0 1px 0 rgba(255, 255, 255, 0.2);
transition: all 0.2s ease;
}
body.lotr-mode button:hover,
body.lotr-mode .bg-blue-500:hover,
body.lotr-mode .bg-blue-600:hover {
background: linear-gradient(135deg, var(--lotr-dark-gold) 0%, var(--lotr-gold) 100%) !important;
box-shadow:
0 4px 8px rgba(0, 0, 0, 0.4),
inset 0 1px 0 rgba(255, 255, 255, 0.3);
transform: translateY(-1px);
}
body.lotr-mode button:active {
transform: translateY(0);
}
/* Input Fields - Scroll Style */
body.lotr-mode input,
body.lotr-mode select,
body.lotr-mode textarea {
background: var(--lotr-dark-parchment) !important;
border: 2px solid var(--lotr-stone-gray) !important;
color: var(--lotr-brown) !important;
border-radius: 4px;
}
body.lotr-mode input:focus,
body.lotr-mode select:focus,
body.lotr-mode textarea:focus {
border-color: var(--lotr-gold) !important;
box-shadow: 0 0 0 3px rgba(212, 175, 55, 0.2) !important;
outline: none;
}
/* Navigation */
body.lotr-mode nav {
background: linear-gradient(135deg, var(--lotr-brown) 0%, var(--lotr-dark-green) 100%) !important;
border-bottom: 3px solid var(--lotr-gold);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);
}
body.lotr-mode nav .text-gray-700,
body.lotr-mode nav .text-gray-800 {
color: var(--lotr-gold) !important;
}
/* Loading Spinner - One Ring Animation */
body.lotr-mode #loading-overlay .animate-spin {
border-color: var(--lotr-gold);
border-top-color: transparent;
width: 60px;
height: 60px;
position: relative;
}
body.lotr-mode #loading-overlay .animate-spin::before {
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 40px;
height: 40px;
border: 3px solid var(--lotr-gold);
border-radius: 50%;
box-shadow:
0 0 20px var(--lotr-gold),
inset 0 0 20px var(--lotr-gold);
animation: ringGlow 2s ease-in-out infinite;
}
@keyframes ringGlow {
0%, 100% {
opacity: 0.6;
box-shadow:
0 0 20px var(--lotr-gold),
inset 0 0 20px var(--lotr-gold);
}
50% {
opacity: 1;
box-shadow:
0 0 40px var(--lotr-gold),
0 0 60px var(--lotr-gold),
inset 0 0 30px var(--lotr-gold);
}
}
/* Toast Notifications - Scroll Style */
body.lotr-mode .toast {
background: var(--lotr-parchment) !important;
border: 2px solid var(--lotr-gold);
color: var(--lotr-brown) !important;
box-shadow:
0 4px 12px rgba(0, 0, 0, 0.4),
inset 0 0 20px rgba(212, 175, 55, 0.1);
position: relative;
padding-left: 50px;
}
body.lotr-mode .toast::before {
content: '⚔';
position: absolute;
left: 15px;
font-size: 20px;
}
body.lotr-mode .toast-success {
border-color: var(--lotr-forest-green);
}
body.lotr-mode .toast-error {
border-color: var(--lotr-fire-red);
}
body.lotr-mode .toast-info {
border-color: var(--lotr-gold);
}
/* Dashboard Cards - Medieval Banners */
body.lotr-mode .bg-blue-50,
body.lotr-mode .bg-gray-50 {
background: var(--lotr-dark-parchment) !important;
}
body.lotr-mode .text-blue-600 {
color: var(--lotr-gold) !important;
}
/* Tables */
body.lotr-mode table {
border-collapse: separate;
border-spacing: 0;
}
body.lotr-mode table th {
background: linear-gradient(135deg, var(--lotr-brown) 0%, var(--lotr-stone-gray) 100%);
color: var(--lotr-gold);
border: 2px solid var(--lotr-gold);
font-family: 'Cinzel', serif;
font-weight: 600;
}
body.lotr-mode table td {
background: var(--lotr-parchment);
border: 1px solid var(--lotr-stone-gray);
color: var(--lotr-brown);
}
body.lotr-mode table tr:hover td {
background: var(--lotr-dark-parchment);
}
/* Modal - Parchment Scroll */
body.lotr-mode .modal-content {
background: var(--lotr-parchment) !important;
border: 4px solid var(--lotr-gold);
border-radius: 12px;
box-shadow:
0 8px 32px rgba(0, 0, 0, 0.5),
inset 0 0 40px rgba(212, 175, 55, 0.1);
}
body.lotr-mode .modal-content h2,
body.lotr-mode .modal-content h3 {
border-bottom: 2px solid var(--lotr-gold);
padding-bottom: 10px;
margin-bottom: 20px;
}
/* Badges and Labels */
body.lotr-mode .bg-green-100,
body.lotr-mode .bg-green-500 {
background: var(--lotr-forest-green) !important;
color: var(--lotr-gold) !important;
}
body.lotr-mode .bg-red-100,
body.lotr-mode .bg-red-500 {
background: var(--lotr-fire-red) !important;
color: var(--lotr-parchment) !important;
}
body.lotr-mode .bg-yellow-400 {
background: var(--lotr-gold) !important;
color: var(--lotr-brown) !important;
}
/* Restart Banner */
body.lotr-mode #restartBanner {
background: linear-gradient(135deg, var(--lotr-gold) 0%, var(--lotr-dark-gold) 100%) !important;
border-top: 3px solid var(--lotr-brown);
border-bottom: 3px solid var(--lotr-brown);
color: var(--lotr-brown) !important;
font-weight: 600;
}
/* Select2 Styling */
body.lotr-mode .select2-container--default .select2-selection {
background: var(--lotr-dark-parchment) !important;
border: 2px solid var(--lotr-stone-gray) !important;
color: var(--lotr-brown) !important;
}
body.lotr-mode .select2-container--default .select2-selection--multiple .select2-selection__choice {
background: var(--lotr-gold) !important;
border: 1px solid var(--lotr-brown) !important;
color: var(--lotr-brown) !important;
}
/* Scrollbar Styling */
body.lotr-mode ::-webkit-scrollbar {
width: 12px;
height: 12px;
}
body.lotr-mode ::-webkit-scrollbar-track {
background: var(--lotr-dark-green);
border: 1px solid var(--lotr-stone-gray);
}
body.lotr-mode ::-webkit-scrollbar-thumb {
background: linear-gradient(135deg, var(--lotr-gold) 0%, var(--lotr-dark-gold) 100%);
border: 2px solid var(--lotr-brown);
border-radius: 6px;
}
body.lotr-mode ::-webkit-scrollbar-thumb:hover {
background: linear-gradient(135deg, var(--lotr-dark-gold) 0%, var(--lotr-gold) 100%);
}
/* Decorative Elements */
body.lotr-mode .lotr-divider {
height: 2px;
background: linear-gradient(90deg,
transparent 0%,
var(--lotr-gold) 20%,
var(--lotr-gold) 80%,
transparent 100%);
margin: 20px 0;
position: relative;
}
body.lotr-mode .lotr-divider::before,
body.lotr-mode .lotr-divider::after {
content: '⚔';
position: absolute;
top: 50%;
transform: translateY(-50%);
color: var(--lotr-gold);
font-size: 20px;
background: var(--lotr-parchment);
padding: 0 10px;
}
body.lotr-mode .lotr-divider::before {
left: 20%;
}
body.lotr-mode .lotr-divider::after {
right: 20%;
}
/* Glow Effects */
body.lotr-mode .lotr-glow {
text-shadow:
0 0 10px var(--lotr-gold),
0 0 20px var(--lotr-gold),
0 0 30px var(--lotr-gold);
animation: gentleGlow 3s ease-in-out infinite;
}
@keyframes gentleGlow {
0%, 100% {
text-shadow:
0 0 10px var(--lotr-gold),
0 0 20px var(--lotr-gold),
0 0 30px var(--lotr-gold);
}
50% {
text-shadow:
0 0 15px var(--lotr-gold),
0 0 30px var(--lotr-gold),
0 0 45px var(--lotr-gold);
}
}
/* Fire Effect (for email headers) */
body.lotr-mode .lotr-fire {
background: linear-gradient(180deg,
var(--lotr-fire-red) 0%,
var(--lotr-fire-orange) 50%,
var(--lotr-gold) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
font-weight: 700;
text-shadow: 0 0 20px var(--lotr-fire-orange);
animation: fireFlicker 2s ease-in-out infinite;
}
@keyframes fireFlicker {
0%, 100% {
filter: brightness(1);
}
25% {
filter: brightness(1.2);
}
50% {
filter: brightness(0.9);
}
75% {
filter: brightness(1.1);
}
}
/* Smooth Theme Transition */
body.lotr-mode,
body.lotr-mode * {
transition: background-color 0.5s ease,
color 0.5s ease,
border-color 0.5s ease;
}
/* Mobile Responsive */
@media (max-width: 768px) {
body.lotr-mode .bg-white {
border-width: 2px;
}
body.lotr-mode h1,
body.lotr-mode h2,
body.lotr-mode h3 {
font-size: 1.5em;
}
}

View File

@@ -36,6 +36,12 @@
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/select2@4.0.13/dist/css/select2.min.css" />
<!-- Fail2ban UI CSS -->
<link rel="stylesheet" href="/static/fail2ban-ui.css">
<!-- LOTR Theme CSS (loaded conditionally) -->
<link rel="stylesheet" href="/static/lotr-theme.css" id="lotr-theme-css" disabled>
<!-- Google Fonts for LOTR theme -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Cinzel:wght@400;600;700&family=MedievalSharp&display=swap" rel="stylesheet">
</head>
<body class="bg-gray-50 overflow-y-scroll">
@@ -1798,6 +1804,10 @@
if (typeof updateTranslations === 'function') {
updateTranslations();
}
// Update LOTR terminology if active
if (isLOTRModeActive) {
updateDashboardLOTRTerminology(true);
}
}
function renderLogOverviewSection() {
@@ -2554,7 +2564,10 @@
//*******************************************************************
function unbanIP(jail, ip) {
if (!confirm("Unban IP " + ip + " from jail " + jail + "?")) {
const confirmMsg = isLOTRModeActive
? `Restore ${ip} to the realm from ${jail}?`
: `Unban IP ${ip} from jail ${jail}?`;
if (!confirm(confirmMsg)) {
return;
}
showLoading(true);
@@ -2992,6 +3005,153 @@
});
}
//*******************************************************************
//* LOTR Mode Detection and Management : *
//*******************************************************************
// Global variable to track LOTR mode state
let isLOTRModeActive = false;
// Check if LOTR mode should be active based on alertCountries
function isLOTRMode(alertCountries) {
if (!alertCountries || !Array.isArray(alertCountries)) {
return false;
}
return alertCountries.includes('LOTR');
}
// Apply or remove LOTR theme
function applyLOTRTheme(active) {
const body = document.body;
const lotrCSS = document.getElementById('lotr-theme-css');
if (active) {
body.classList.add('lotr-mode');
if (lotrCSS) {
lotrCSS.disabled = false;
}
isLOTRModeActive = true;
console.log('🎭 LOTR Mode Activated - Welcome to Middle-earth!');
} else {
body.classList.remove('lotr-mode');
if (lotrCSS) {
lotrCSS.disabled = true;
}
isLOTRModeActive = false;
console.log('🎭 LOTR Mode Deactivated');
}
}
// Check and apply LOTR theme based on current settings
function checkAndApplyLOTRTheme(alertCountries) {
const shouldBeActive = isLOTRMode(alertCountries);
if (shouldBeActive !== isLOTRModeActive) {
applyLOTRTheme(shouldBeActive);
updateLOTRTerminology(shouldBeActive);
}
}
// Update UI terminology when LOTR mode is active
function updateLOTRTerminology(active) {
if (active) {
// Update navigation title
const navTitle = document.querySelector('nav .text-xl');
if (navTitle) {
navTitle.textContent = 'Middle-earth Security';
}
// Update page title
const pageTitle = document.querySelector('title');
if (pageTitle) {
pageTitle.textContent = 'Middle-earth Security Realm';
}
// Update dashboard terminology
updateDashboardLOTRTerminology(true);
// Add decorative elements
addLOTRDecorations();
} else {
// Restore original text
const navTitle = document.querySelector('nav .text-xl');
if (navTitle) {
navTitle.textContent = 'Fail2ban UI';
}
const pageTitle = document.querySelector('title');
if (pageTitle && pageTitle.hasAttribute('data-i18n')) {
const i18nKey = pageTitle.getAttribute('data-i18n');
pageTitle.textContent = t(i18nKey, 'Fail2ban UI Dashboard');
}
// Restore dashboard terminology
updateDashboardLOTRTerminology(false);
// Remove decorative elements
removeLOTRDecorations();
}
}
// Update dashboard terminology for LOTR mode
function updateDashboardLOTRTerminology(active) {
// Update text elements that use data-i18n
const elements = document.querySelectorAll('[data-i18n]');
elements.forEach(el => {
const i18nKey = el.getAttribute('data-i18n');
if (active) {
// Check for LOTR-specific translations
if (i18nKey === 'dashboard.cards.total_banned') {
el.textContent = t('lotr.threats_banished', 'Threats Banished');
} else if (i18nKey === 'dashboard.table.banned_ips') {
el.textContent = t('lotr.threats_banished', 'Threats Banished');
} else if (i18nKey === 'dashboard.search_label') {
el.textContent = t('lotr.threats_banished', 'Search Banished Threats');
} else if (i18nKey === 'dashboard.manage_servers') {
el.textContent = t('lotr.realms_protected', 'Manage Realms');
}
} else {
// Restore original translations
if (i18nKey) {
el.textContent = t(i18nKey, el.textContent);
}
}
});
// Update "Unban" buttons
const unbanButtons = document.querySelectorAll('button, a');
unbanButtons.forEach(btn => {
if (btn.textContent && btn.textContent.includes('Unban')) {
if (active) {
btn.textContent = btn.textContent.replace(/Unban/gi, t('lotr.banished', 'Restore to Realm'));
} else {
btn.textContent = btn.textContent.replace(/Restore to Realm/gi, t('dashboard.unban', 'Unban'));
}
}
});
}
// Add LOTR decorative elements to the UI
function addLOTRDecorations() {
// Add decorative divider to settings section if not already present
const settingsSection = document.getElementById('settingsSection');
if (settingsSection && !settingsSection.querySelector('.lotr-divider')) {
const divider = document.createElement('div');
divider.className = 'lotr-divider';
divider.style.marginTop = '20px';
divider.style.marginBottom = '20px';
const firstChild = settingsSection.querySelector('.bg-white');
if (firstChild) {
settingsSection.insertBefore(divider, firstChild);
}
}
}
// Remove LOTR decorative elements
function removeLOTRDecorations() {
const dividers = document.querySelectorAll('.lotr-divider');
dividers.forEach(div => div.remove());
}
//*******************************************************************
//* Load current settings when opening settings page : *
//*******************************************************************
@@ -3063,6 +3223,9 @@
}
}
$('#alertCountries').trigger('change');
// Check and apply LOTR theme
checkAndApplyLOTRTheme(data.alertCountries || []);
if (data.smtp) {
document.getElementById('smtpHost').value = data.smtp.host || '';
@@ -3145,6 +3308,11 @@
var selectedLang = $('#languageSelect').val();
loadTranslations(selectedLang);
console.log("Settings saved successfully. Restart needed? " + data.restartNeeded);
// Check and apply LOTR theme after saving
const selectedCountries = Array.from(document.getElementById('alertCountries').selectedOptions).map(opt => opt.value);
checkAndApplyLOTRTheme(selectedCountries.length > 0 ? selectedCountries : ["ALL"]);
showToast(t('settings.save_success', 'Settings saved'), 'success');
if (data.restartNeeded) {
loadServers().then(function() {
@@ -3557,6 +3725,19 @@
$('#alertCountries').val(newValues).trigger('change');
}
}
// Check LOTR mode after selection change
setTimeout(function() {
const selectedCountries = $('#alertCountries').val() || [];
checkAndApplyLOTRTheme(selectedCountries);
}, 100);
});
$('#alertCountries').on('select2:unselect', function(e) {
// Check LOTR mode after deselection
setTimeout(function() {
const selectedCountries = $('#alertCountries').val() || [];
checkAndApplyLOTRTheme(selectedCountries);
}, 100);
});
var sshKeySelect = document.getElementById('serverSSHKeySelect');