diff --git a/cmd/server/main.go b/cmd/server/main.go index 9a68b05..7d8bc20 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -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. diff --git a/internal/locales/de.json b/internal/locales/de.json index 09a3e33..23dd884 100644 --- a/internal/locales/de.json +++ b/internal/locales/de.json @@ -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" } \ No newline at end of file diff --git a/internal/locales/de_ch.json b/internal/locales/de_ch.json index 2e40586..f980bf5 100644 --- a/internal/locales/de_ch.json +++ b/internal/locales/de_ch.json @@ -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" } \ No newline at end of file diff --git a/internal/locales/en.json b/internal/locales/en.json index cd04e50..6cad13e 100644 --- a/internal/locales/en.json +++ b/internal/locales/en.json @@ -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" } \ No newline at end of file diff --git a/internal/locales/es.json b/internal/locales/es.json index 591933e..4c7b7e0 100644 --- a/internal/locales/es.json +++ b/internal/locales/es.json @@ -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" } diff --git a/internal/locales/fr.json b/internal/locales/fr.json index 17afb11..505565e 100644 --- a/internal/locales/fr.json +++ b/internal/locales/fr.json @@ -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" +} diff --git a/internal/locales/it.json b/internal/locales/it.json index 2d16e75..feaa724 100644 --- a/internal/locales/it.json +++ b/internal/locales/it.json @@ -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" +} diff --git a/pkg/web/handlers.go b/pkg/web/handlers.go index a952bec..ef42dd8 100644 --- a/pkg/web/handlers.go +++ b/pkg/web/handlers.go @@ -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.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(` + + + + + + %s + + + +
+
+ + + +
+
+ +`, 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) diff --git a/pkg/web/static/lotr-theme.css b/pkg/web/static/lotr-theme.css new file mode 100644 index 0000000..9601893 --- /dev/null +++ b/pkg/web/static/lotr-theme.css @@ -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; + } +} + diff --git a/pkg/web/templates/index.html b/pkg/web/templates/index.html index 4c99b91..a2f237f 100644 --- a/pkg/web/templates/index.html +++ b/pkg/web/templates/index.html @@ -36,6 +36,12 @@ + + + + + + @@ -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');