Basic implementation of UI-language switching done

This commit is contained in:
2025-02-06 22:35:45 +01:00
parent 95befd30fd
commit 87745d4a97
7 changed files with 1406 additions and 953 deletions

69
internal/locales/de.json Normal file
View File

@@ -0,0 +1,69 @@
{
"page.title": "Fail2ban UI Dashboard",
"nav.dashboard": "Dashboard",
"nav.filter_debug": "Filter-Debug",
"nav.settings": "Einstellungen",
"reload_banner.message": "Konfiguration geändert!",
"reload_banner.button": "Fail2ban neu laden",
"dashboard.title": "Dashboard",
"dashboard.overview": "Aktive Jails und Blocks Übersicht",
"dashboard.search_label": "Suche gesperrte IPs",
"dashboard.search_placeholder": "Geben Sie eine IP-Adresse zum Suchen ein",
"dashboard.table.jail_name": "Jail-Name",
"dashboard.table.total_banned": "Insgesamt gesperrt",
"dashboard.table.new_last_hour": "Neu in letzter Stunde",
"dashboard.table.banned_ips": "Gesperrte IPs (Entsperren)",
"dashboard.no_jails": "Keine Jails gefunden.",
"dashboard.last_bans": "Letzte 5 Sperrvorgänge",
"dashboard.table.time": "Zeit",
"dashboard.table.jail": "Jail",
"dashboard.table.ip": "IP",
"dashboard.table.log_line": "Logzeile",
"dashboard.no_recent_bans": "Keine aktuellen Sperrvorgänge gefunden.",
"dashboard.no_banned_ips": "Keine gesperrten IPs",
"dashboard.unban": "Entsperren",
"filter_debug.title": "Filter-Debug",
"filter_debug.select_filter": "Wählen Sie einen Filter",
"filter_debug.log_lines": "Logzeilen",
"filter_debug.log_lines_placeholder": "Geben Sie die Logzeilen hier ein...",
"filter_debug.test_filter": "Filter testen",
"filter_debug.test_results_title": "Testergebnisse",
"filter_debug.no_matches": "Keine Übereinstimmungen gefunden.",
"settings.title": "Einstellungen",
"settings.general": "Allgemeine Einstellungen",
"settings.language": "Sprache",
"settings.enable_debug": "Debug-Protokoll aktivieren",
"settings.alert": "Alarm-Einstellungen",
"settings.destination_email": "Ziel-E-Mail (Alarmempfänger)",
"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.smtp": "SMTP-Konfiguration",
"settings.smtp_host": "SMTP-Host",
"settings.smtp_host_placeholder": "z.B. smtp.gmail.com",
"settings.smtp_port": "SMTP-Port",
"settings.smtp_username": "SMTP-Benutzername",
"settings.smtp_username_placeholder": "z.B. user@example.com",
"settings.smtp_password": "SMTP-Passwort",
"settings.smtp_password_placeholder": "Geben Sie das SMTP-Passwort ein",
"settings.smtp_sender": "Absender-E-Mail",
"settings.smtp_sender_placeholder": "noreply@swissmakers.ch",
"settings.smtp_tls": "TLS verwenden (empfohlen)",
"settings.send_test_email": "Test-E-Mail senden",
"settings.fail2ban": "Fail2Ban-Konfiguration",
"settings.enable_bantime_increment": "Bantime-Inkrement aktivieren",
"settings.default_bantime": "Standard-Bantime",
"settings.default_bantime_placeholder": "z.B. 48h",
"settings.default_findtime": "Standard-Findtime",
"settings.default_findtime_placeholder": "z.B. 30m",
"settings.default_max_retry": "Standard-Maximalversuche",
"settings.default_max_retry_placeholder": "Geben Sie die maximale Anzahl der Versuche ein",
"settings.ignore_ips": "IP-Adressen ignorieren",
"settings.ignore_ips_placeholder": "IP-Adressen, getrennt durch Leerzeichen",
"settings.save": "Speichern",
"modal.filter_config": "Filter-Konfiguration:",
"modal.cancel": "Abbrechen",
"modal.save": "Speichern",
"loading": "Lade..."
}

View File

@@ -0,0 +1,69 @@
{
"page.title": "Fail2ban UI Dashboard",
"nav.dashboard": "Dashboard",
"nav.filter_debug": "Filter Debug",
"nav.settings": "Istellige",
"reload_banner.message": "D'Konfiguration isch gänderet worde!",
"reload_banner.button": "Fail2ban neu lade",
"dashboard.title": "Dashboard",
"dashboard.overview": "Übersicht vo de aktive Jails und Blocks",
"dashboard.search_label": "Suech nach g'sperrte IPs",
"dashboard.search_placeholder": "Gib d'IP adrässe i, wo du suechsch",
"dashboard.table.jail_name": "Jail-Name",
"dashboard.table.total_banned": "Insgsamt g'sperrt",
"dashboard.table.new_last_hour": "Neu in dr letschte Stund",
"dashboard.table.banned_ips": "G'sperrti IPs (Entsperre)",
"dashboard.no_jails": "Kei Jails gfunde.",
"dashboard.last_bans": "Letschti 5 Sperrvorgäng",
"dashboard.table.time": "Zyt",
"dashboard.table.jail": "Jail",
"dashboard.table.ip": "IP",
"dashboard.table.log_line": "Log-Zile",
"dashboard.no_recent_bans": "Kei aktuelli Sperrvorgäng gfunde.",
"dashboard.no_banned_ips": "Kei g'sperrti IPs",
"dashboard.unban": "Entsperre",
"filter_debug.title": "Filter Debug",
"filter_debug.select_filter": "Wähl en Filter us",
"filter_debug.log_lines": "Log-Zile",
"filter_debug.log_lines_placeholder": "Gib ä Log-Zile da ii...",
"filter_debug.test_filter": "Filter teste",
"filter_debug.test_results_title": "Testergebnis",
"filter_debug.no_matches": "Kei Übereinstimmige gfunde.",
"settings.title": "Istellige",
"settings.general": "Allgemeini Istellige",
"settings.language": "Sprach",
"settings.enable_debug": "Debug-Modus aktivierä",
"settings.alert": "Alarm-Istellige",
"settings.destination_email": "Ziil-Email (Alarmempfänger)",
"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 passiert.",
"settings.smtp": "SMTP-Konfiguration",
"settings.smtp_host": "SMTP-Host",
"settings.smtp_host_placeholder": "z.B. smtp.gmail.com",
"settings.smtp_port": "SMTP-Port",
"settings.smtp_username": "SMTP-Benutzername",
"settings.smtp_username_placeholder": "z.B. user@example.com",
"settings.smtp_password": "SMTP-Passwort",
"settings.smtp_password_placeholder": "Gib s'SMTP-Passwort ii",
"settings.smtp_sender": "Absänder-Email",
"settings.smtp_sender_placeholder": "noreply@swissmakers.ch",
"settings.smtp_tls": "TLS bruuche (empfohlen)",
"settings.send_test_email": "Test-Email schicke",
"settings.fail2ban": "Fail2Ban-Konfiguration",
"settings.enable_bantime_increment": "Bantime-Inkrement aktivierä",
"settings.default_bantime": "Standard-Bantime",
"settings.default_bantime_placeholder": "z.B. 48h",
"settings.default_findtime": "Standard-Findtime",
"settings.default_findtime_placeholder": "z.B. 30m",
"settings.default_max_retry": "Standard-Maximalversüech",
"settings.default_max_retry_placeholder": "Gib d'maximal Versüech ii",
"settings.ignore_ips": "IPs ignorierä",
"settings.ignore_ips_placeholder": "IPs, getrennt dur e Leerzeichä",
"settings.save": "Speicherä",
"modal.filter_config": "Filter-Konfiguration:",
"modal.cancel": "Abbräche",
"modal.save": "Speicherä",
"loading": "Lade..."
}

69
internal/locales/en.json Normal file
View File

@@ -0,0 +1,69 @@
{
"page.title": "Fail2ban UI Dashboard",
"nav.dashboard": "Dashboard",
"nav.filter_debug": "Filter Debug",
"nav.settings": "Settings",
"reload_banner.message": "Configuration changed!",
"reload_banner.button": "Reload Fail2ban",
"dashboard.title": "Dashboard",
"dashboard.overview": "Overview active Jails and Blocks",
"dashboard.search_label": "Search Banned IPs",
"dashboard.search_placeholder": "Enter IP address to search",
"dashboard.table.jail_name": "Jail Name",
"dashboard.table.total_banned": "Total Banned",
"dashboard.table.new_last_hour": "New Last Hour",
"dashboard.table.banned_ips": "Banned IPs (Unban)",
"dashboard.no_jails": "No jails found.",
"dashboard.last_bans": "Last 5 Ban Events",
"dashboard.table.time": "Time",
"dashboard.table.jail": "Jail",
"dashboard.table.ip": "IP",
"dashboard.table.log_line": "Log Line",
"dashboard.no_recent_bans": "No recent bans found.",
"dashboard.no_banned_ips": "No banned IPs",
"dashboard.unban": "Unban",
"filter_debug.title": "Filter Debug",
"filter_debug.select_filter": "Select a Filter",
"filter_debug.log_lines": "Log Lines",
"filter_debug.log_lines_placeholder": "Enter log lines here...",
"filter_debug.test_filter": "Test Filter",
"filter_debug.test_results_title": "Test Results",
"filter_debug.no_matches": "No matches found.",
"settings.title": "Settings",
"settings.general": "General Settings",
"settings.language": "Language",
"settings.enable_debug": "Enable Debug Log",
"settings.alert": "Alert Settings",
"settings.destination_email": "Destination Email (Alerts Receiver)",
"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.smtp": "SMTP Configuration",
"settings.smtp_host": "SMTP Host",
"settings.smtp_host_placeholder": "e.g., smtp.gmail.com",
"settings.smtp_port": "SMTP Port",
"settings.smtp_username": "SMTP Username",
"settings.smtp_username_placeholder": "e.g., user@example.com",
"settings.smtp_password": "SMTP Password",
"settings.smtp_password_placeholder": "Enter SMTP Password",
"settings.smtp_sender": "Sender Email",
"settings.smtp_sender_placeholder": "noreply@swissmakers.ch",
"settings.smtp_tls": "Use TLS (Recommended)",
"settings.send_test_email": "Send Test Email",
"settings.fail2ban": "Fail2Ban Configuration",
"settings.enable_bantime_increment": "Enable Bantime Increment",
"settings.default_bantime": "Default Bantime",
"settings.default_bantime_placeholder": "e.g., 48h",
"settings.default_findtime": "Default Findtime",
"settings.default_findtime_placeholder": "e.g., 30m",
"settings.default_max_retry": "Default Max Retry",
"settings.default_max_retry_placeholder": "Enter maximum retries",
"settings.ignore_ips": "Ignore IPs",
"settings.ignore_ips_placeholder": "IPs to ignore, separated by spaces",
"settings.save": "Save",
"modal.filter_config": "Filter Config:",
"modal.cancel": "Cancel",
"modal.save": "Save",
"loading": "Loading..."
}

68
internal/locales/es.json Normal file
View File

@@ -0,0 +1,68 @@
{
"page.title": "Panel de control Fail2ban UI",
"nav.dashboard": "Panel de control",
"nav.filter_debug": "Depuración de filtros",
"nav.settings": "Configuración",
"reload_banner.message": "¡La configuración ha sido modificada!",
"reload_banner.button": "Recargar Fail2ban",
"dashboard.title": "Panel de control",
"dashboard.overview": "Resumen de Jails y Bloqueos activos",
"dashboard.search_label": "Buscar IP bloqueadas",
"dashboard.search_placeholder": "Introduce la dirección IP a buscar",
"dashboard.table.jail_name": "Nombre del Jail",
"dashboard.table.total_banned": "Total bloqueadas",
"dashboard.table.new_last_hour": "Nuevas en la última hora",
"dashboard.table.banned_ips": "IPs bloqueadas (Desbloquear)",
"dashboard.no_jails": "No se encontraron jails.",
"dashboard.last_bans": "Últimos 5 eventos de bloqueo",
"dashboard.table.time": "Hora",
"dashboard.table.jail": "Jail",
"dashboard.table.ip": "IP",
"dashboard.table.log_line": "Línea de log",
"dashboard.no_recent_bans": "No se encontraron bloqueos recientes.",
"dashboard.no_banned_ips": "No hay IP bloqueadas",
"dashboard.unban": "Desbloquear",
"filter_debug.title": "Depuración de filtros",
"filter_debug.select_filter": "Selecciona un filtro",
"filter_debug.log_lines": "Líneas de log",
"filter_debug.log_lines_placeholder": "Introduce las líneas de log aquí...",
"filter_debug.test_filter": "Probar filtro",
"filter_debug.test_results_title": "Resultados de la prueba",
"filter_debug.no_matches": "No se encontraron coincidencias.",
"settings.title": "Configuración",
"settings.general": "Configuración general",
"settings.language": "Idioma",
"settings.enable_debug": "Habilitar el modo de depuración",
"settings.alert": "Configuración de alertas",
"settings.destination_email": "Correo electrónico de destino (receptor de alertas)",
"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.smtp": "Configuración SMTP",
"settings.smtp_host": "Host SMTP",
"settings.smtp_host_placeholder": "p.ej., smtp.gmail.com",
"settings.smtp_port": "Puerto SMTP",
"settings.smtp_username": "Nombre de usuario SMTP",
"settings.smtp_username_placeholder": "p.ej., usuario@example.com",
"settings.smtp_password": "Contraseña SMTP",
"settings.smtp_password_placeholder": "Introduce la contraseña SMTP",
"settings.smtp_sender": "Correo electrónico del remitente",
"settings.smtp_sender_placeholder": "noreply@swissmakers.ch",
"settings.smtp_tls": "Usar TLS (recomendado)",
"settings.send_test_email": "Enviar correo de prueba",
"settings.fail2ban": "Configuración de Fail2Ban",
"settings.enable_bantime_increment": "Habilitar incremento de Bantime",
"settings.default_bantime": "Bantime por defecto",
"settings.default_bantime_placeholder": "p.ej., 48h",
"settings.default_findtime": "Findtime por defecto",
"settings.default_findtime_placeholder": "p.ej., 30m",
"settings.default_max_retry": "Número máximo de reintentos por defecto",
"settings.default_max_retry_placeholder": "Introduce el número máximo de reintentos",
"settings.ignore_ips": "Ignorar IPs",
"settings.ignore_ips_placeholder": "IPs a ignorar, separadas por espacios",
"settings.save": "Guardar",
"modal.filter_config": "Configuración del filtro:",
"modal.cancel": "Cancelar",
"modal.save": "Guardar",
"loading": "Cargando..."
}

68
internal/locales/fr.json Normal file
View File

@@ -0,0 +1,68 @@
{
"page.title": "Tableau de bord Fail2ban UI",
"nav.dashboard": "Tableau de bord",
"nav.filter_debug": "Débogage des filtres",
"nav.settings": "Paramètres",
"reload_banner.message": "Configuration modifiée!",
"reload_banner.button": "Recharger Fail2ban",
"dashboard.title": "Tableau de bord",
"dashboard.overview": "Vue d'ensemble des jails et blocages actifs",
"dashboard.search_label": "Rechercher des IP bloquées",
"dashboard.search_placeholder": "Entrez l'adresse IP à rechercher",
"dashboard.table.jail_name": "Nom du Jail",
"dashboard.table.total_banned": "Total bloqués",
"dashboard.table.new_last_hour": "Nouveaux dans la dernière heure",
"dashboard.table.banned_ips": "IPs bloquées (Débloquer)",
"dashboard.no_jails": "Aucun jail trouvé.",
"dashboard.last_bans": "5 derniers événements de blocage",
"dashboard.table.time": "Heure",
"dashboard.table.jail": "Jail",
"dashboard.table.ip": "IP",
"dashboard.table.log_line": "Ligne de log",
"dashboard.no_recent_bans": "Aucun blocage récent trouvé.",
"dashboard.no_banned_ips": "Aucune IP bloquée",
"dashboard.unban": "Débloquer",
"filter_debug.title": "Débogage des filtres",
"filter_debug.select_filter": "Sélectionnez un filtre",
"filter_debug.log_lines": "Lignes de log",
"filter_debug.log_lines_placeholder": "Entrez les lignes de log ici...",
"filter_debug.test_filter": "Tester le filtre",
"filter_debug.test_results_title": "Résultats du test",
"filter_debug.no_matches": "Aucune correspondance trouvée.",
"settings.title": "Paramètres",
"settings.general": "Paramètres généraux",
"settings.language": "Langue",
"settings.enable_debug": "Activer le mode débogage",
"settings.alert": "Paramètres d'alerte",
"settings.destination_email": "Email de destination (récepteur des alertes)",
"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.smtp": "Configuration SMTP",
"settings.smtp_host": "Hôte SMTP",
"settings.smtp_host_placeholder": "par exemple, smtp.gmail.com",
"settings.smtp_port": "Port SMTP",
"settings.smtp_username": "Nom d'utilisateur SMTP",
"settings.smtp_username_placeholder": "par exemple, utilisateur@example.com",
"settings.smtp_password": "Mot de passe SMTP",
"settings.smtp_password_placeholder": "Entrez le mot de passe SMTP",
"settings.smtp_sender": "Email de l'expéditeur",
"settings.smtp_sender_placeholder": "noreply@swissmakers.ch",
"settings.smtp_tls": "Utiliser TLS (recommandé)",
"settings.send_test_email": "Envoyer un email de test",
"settings.fail2ban": "Configuration Fail2Ban",
"settings.enable_bantime_increment": "Activer l'incrémentation du Bantime",
"settings.default_bantime": "Bantime par défaut",
"settings.default_bantime_placeholder": "par exemple, 48h",
"settings.default_findtime": "Findtime par défaut",
"settings.default_findtime_placeholder": "par exemple, 30m",
"settings.default_max_retry": "Nombre maximal de réessais par défaut",
"settings.default_max_retry_placeholder": "Entrez le nombre maximal de réessais",
"settings.ignore_ips": "Ignorer les IPs",
"settings.ignore_ips_placeholder": "IPs à ignorer, séparées par des espaces",
"settings.save": "Enregistrer",
"modal.filter_config": "Configuration du filtre:",
"modal.cancel": "Annuler",
"modal.save": "Enregistrer",
"loading": "Chargement..."
}

68
internal/locales/it.json Normal file
View File

@@ -0,0 +1,68 @@
{
"page.title": "Cruscotto Fail2ban UI",
"nav.dashboard": "Cruscotto",
"nav.filter_debug": "Debug Filtro",
"nav.settings": "Impostazioni",
"reload_banner.message": "Configurazione modificata!",
"reload_banner.button": "Ricarica Fail2ban",
"dashboard.title": "Cruscotto",
"dashboard.overview": "Panoramica dei jail e dei blocchi attivi",
"dashboard.search_label": "Cerca IP bloccate",
"dashboard.search_placeholder": "Inserisci l'indirizzo IP da cercare",
"dashboard.table.jail_name": "Nome del Jail",
"dashboard.table.total_banned": "Totale bloccate",
"dashboard.table.new_last_hour": "Nuove nell'ultima ora",
"dashboard.table.banned_ips": "IP bloccate (Sblocca)",
"dashboard.no_jails": "Nessun jail trovato.",
"dashboard.last_bans": "Ultimi 5 eventi di blocco",
"dashboard.table.time": "Ora",
"dashboard.table.jail": "Jail",
"dashboard.table.ip": "IP",
"dashboard.table.log_line": "Riga di log",
"dashboard.no_recent_bans": "Nessun blocco recente trovato.",
"dashboard.no_banned_ips": "Nessuna IP bloccata",
"dashboard.unban": "Sblocca",
"filter_debug.title": "Debug Filtro",
"filter_debug.select_filter": "Seleziona un filtro",
"filter_debug.log_lines": "Righe di log",
"filter_debug.log_lines_placeholder": "Inserisci qui le righe di log...",
"filter_debug.test_filter": "Testa filtro",
"filter_debug.test_results_title": "Risultati del test",
"filter_debug.no_matches": "Nessuna corrispondenza trovata.",
"settings.title": "Impostazioni",
"settings.general": "Impostazioni generali",
"settings.language": "Lingua",
"settings.enable_debug": "Abilita debug",
"settings.alert": "Impostazioni di allarme",
"settings.destination_email": "Email di destinazione (ricevente allarmi)",
"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.smtp": "Configurazione SMTP",
"settings.smtp_host": "Host SMTP",
"settings.smtp_host_placeholder": "es. smtp.gmail.com",
"settings.smtp_port": "Porta SMTP",
"settings.smtp_username": "Nome utente SMTP",
"settings.smtp_username_placeholder": "es. utente@example.com",
"settings.smtp_password": "Password SMTP",
"settings.smtp_password_placeholder": "Inserisci la password SMTP",
"settings.smtp_sender": "Email del mittente",
"settings.smtp_sender_placeholder": "noreply@swissmakers.ch",
"settings.smtp_tls": "Usa TLS (raccomandato)",
"settings.send_test_email": "Invia email di test",
"settings.fail2ban": "Configurazione Fail2Ban",
"settings.enable_bantime_increment": "Abilita incremento del Bantime",
"settings.default_bantime": "Bantime predefinito",
"settings.default_bantime_placeholder": "es. 48h",
"settings.default_findtime": "Findtime predefinito",
"settings.default_findtime_placeholder": "es. 30m",
"settings.default_max_retry": "Numero massimo di tentativi predefinito",
"settings.default_max_retry_placeholder": "Inserisci il numero massimo di tentativi",
"settings.ignore_ips": "Ignora IP",
"settings.ignore_ips_placeholder": "IP da ignorare, separate da spazi",
"settings.save": "Salva",
"modal.filter_config": "Configurazione del filtro:",
"modal.cancel": "Annulla",
"modal.save": "Salva",
"loading": "Caricamento..."
}

View File

@@ -21,7 +21,7 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no" />
<title>Fail2ban UI Dashboard</title> <title data-i18n="page.title">Fail2ban UI Dashboard</title>
<!-- Bootstrap 5 (CDN) --> <!-- Bootstrap 5 (CDN) -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" rel="stylesheet" /> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" rel="stylesheet" />
<!-- Select2 CSS --> <!-- Select2 CSS -->
@@ -31,15 +31,18 @@
#loading-overlay { #loading-overlay {
display: none; /* hidden by default */ display: none; /* hidden by default */
position: fixed; position: fixed;
top: 0; left: 0; top: 0;
width: 100%; height: 100%; left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.5); background: rgba(0,0,0,0.5);
z-index: 9999; /* on top */ z-index: 9999; /* on top */
align-items: center; align-items: center;
justify-content: center; justify-content: center;
} }
.spinner-border { .spinner-border {
width: 4rem; height: 4rem; width: 4rem;
height: 4rem;
} }
/* Reload banner */ /* Reload banner */
@@ -49,7 +52,8 @@
/* Improve table readability on small screens */ /* Improve table readability on small screens */
@media (max-width: 575px) { @media (max-width: 575px) {
.table th, .table td { .table th,
.table td {
font-size: 0.8rem; font-size: 0.8rem;
padding: 5px; padding: 5px;
} }
@@ -61,105 +65,108 @@
</head> </head>
<body class="bg-light"> <body class="bg-light">
<!-- ******************************************************************* --> <!-- ******************************************************************* -->
<!-- NAVIGATION : --> <!-- NAVIGATION : -->
<!-- ******************************************************************* --> <!-- ******************************************************************* -->
<nav class="navbar navbar-expand-lg navbar-dark bg-primary"> <nav class="navbar navbar-expand-lg navbar-dark bg-primary">
<div class="container-fluid"> <div class="container-fluid">
<a class="navbar-brand" href="#"> <a class="navbar-brand" href="#">
<strong>Fail2ban UI</strong> <strong>Fail2ban UI</strong>
</a> </a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent"
data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span> <span class="navbar-toggler-icon"></span>
</button> </button>
<div class="collapse navbar-collapse" id="navbarSupportedContent"> <div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav ms-auto mb-2 mb-lg-0"> <ul class="navbar-nav ms-auto mb-2 mb-lg-0">
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="#" onclick="showSection('dashboardSection')">Dashboard</a> <a class="nav-link" href="#" onclick="showSection('dashboardSection')" data-i18n="nav.dashboard">Dashboard</a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="#" onclick="showSection('filterSection')">Filter Debug</a> <a class="nav-link" href="#" onclick="showSection('filterSection')" data-i18n="nav.filter_debug">Filter Debug</a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="#" onclick="showSection('settingsSection')">Settings</a> <a class="nav-link" href="#" onclick="showSection('settingsSection')" data-i18n="nav.settings">Settings</a>
</li> </li>
</ul> </ul>
</div> </div>
</div> </div>
</nav> </nav>
<!-- ******************************************************************* --> <!-- ******************************************************************* -->
<!-- Reload Banner --> <!-- Reload Banner -->
<div id="reloadBanner" class="bg-warning text-dark p-3 text-center"> <div id="reloadBanner" class="bg-warning text-dark p-3 text-center">
<strong>Configuration changed! </strong> <strong data-i18n="reload_banner.message">Configuration changed!</strong>
<button class="btn btn-dark" onclick="reloadFail2ban()"> <button class="btn btn-dark" onclick="reloadFail2ban()" data-i18n="reload_banner.button">Reload Fail2ban</button>
Reload Fail2ban </div>
</button>
</div>
<!-- ******************************************************************* --> <!-- ******************************************************************* -->
<!-- APP Sections (Pages) : --> <!-- APP Sections (Pages) : -->
<!-- ******************************************************************* --> <!-- ******************************************************************* -->
<!-- Dashboard Section --> <!-- Dashboard Section -->
<div id="dashboardSection" class="container my-4"> <div id="dashboardSection" class="container my-4">
<h1 class="mb-4">Dashboard</h1> <h1 class="mb-4" data-i18n="dashboard.title">Dashboard</h1>
<div id="dashboard"></div> <div id="dashboard"></div>
</div> </div>
<!-- Filter Debug Section --> <!-- Filter Debug Section -->
<div id="filterSection" style="display: none;" class="container my-4"> <div id="filterSection" style="display: none;" class="container my-4">
<h2>Filter Debug</h2> <h2 data-i18n="filter_debug.title">Filter Debug</h2>
<!-- Dropdown of available jail/filters --> <!-- Dropdown of available jail/filters -->
<div class="mb-3"> <div class="mb-3">
<label for="filterSelect" class="form-label">Select a Filter</label> <label for="filterSelect" class="form-label" data-i18n="filter_debug.select_filter">Select a Filter</label>
<select id="filterSelect" class="form-select"></select> <select id="filterSelect" class="form-select"></select>
</div> </div>
<!-- Textarea for log lines to test --> <!-- Textarea for log lines to test -->
<div class="mb-3"> <div class="mb-3">
<label class="form-label">Log Lines</label> <label class="form-label" data-i18n="filter_debug.log_lines">Log Lines</label>
<textarea id="logLinesTextarea" class="form-control" rows="6" disabled></textarea> <textarea id="logLinesTextarea" class="form-control" rows="6" disabled
data-i18n-placeholder="filter_debug.log_lines_placeholder" placeholder="Enter log lines here..."></textarea>
</div> </div>
<button class="btn btn-secondary" onclick="testSelectedFilter()">Test Filter</button> <button class="btn btn-secondary" onclick="testSelectedFilter()" data-i18n="filter_debug.test_filter">Test Filter</button>
<hr/> <hr/>
<div id="testResults"></div> <div id="testResults"></div>
</div> </div>
<!-- Settings Section --> <!-- Settings Section -->
<div id="settingsSection" style="display: none;" class="container my-4"> <div id="settingsSection" style="display: none;" class="container my-4">
<h2>Settings</h2> <h2 data-i18n="settings.title">Settings</h2>
<form onsubmit="saveSettings(event)"> <form onsubmit="saveSettings(event)">
<!-- General Settings Group --> <!-- General Settings Group -->
<fieldset class="border p-3 rounded mb-4"> <fieldset class="border p-3 rounded mb-4">
<legend class="w-auto px-2">General Settings</legend> <legend class="w-auto px-2" data-i18n="settings.general">General Settings</legend>
<!-- Language Selection --> <!-- Language Selection -->
<div class="mb-3"> <div class="mb-3">
<label for="languageSelect" class="form-label">Language</label> <label for="languageSelect" class="form-label" data-i18n="settings.language">Language</label>
<select id="languageSelect" class="form-select" disabled> <select id="languageSelect" class="form-select">
<option value="en">English</option> <option value="en">English</option>
<option value="de">Deutsch</option> <option value="de">Deutsch</option>
<option value="es">Español</option>
<option value="fr">Français</option>
<option value="it">Italiano</option>
<option value="de_ch">Schwiizerdütsch</option>
</select> </select>
</div> </div>
<!-- Debug Log Output --> <!-- Debug Log Output -->
<div class="mb-3 form-check"> <div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="debugMode"> <input type="checkbox" class="form-check-input" id="debugMode">
<label for="debugMode" class="form-check-label">Enable Debug Log</label> <label for="debugMode" class="form-check-label" data-i18n="settings.enable_debug">Enable Debug Log</label>
</div> </div>
</fieldset> </fieldset>
<!-- Alert Settings Group --> <!-- Alert Settings Group -->
<fieldset class="border p-3 rounded mb-4"> <fieldset class="border p-3 rounded mb-4">
<legend class="w-auto px-2">Alert Settings</legend> <legend class="w-auto px-2" data-i18n="settings.alert">Alert Settings</legend>
<div class="mb-3"> <div class="mb-3">
<label for="destEmail" class="form-label">Destination Email (Alerts Receiver)</label> <label for="destEmail" class="form-label" data-i18n="settings.destination_email">Destination Email (Alerts Receiver)</label>
<input type="email" class="form-control" id="destEmail" placeholder="alerts@swissmakers.ch" /> <input type="email" class="form-control" id="destEmail"
data-i18n-placeholder="settings.destination_email_placeholder" placeholder="alerts@swissmakers.ch" />
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="alertCountries" class="form-label">Alert Countries</label> <label for="alertCountries" class="form-label" data-i18n="settings.alert_countries">Alert Countries</label>
<p class="text-muted"> <p class="text-muted" data-i18n="settings.alert_countries_description">
Choose the countries for which you want to receive email alerts when a block is triggered. Choose the countries for which you want to receive email alerts when a block is triggered.
</p> </p>
<select id="alertCountries" class="form-select" multiple> <select id="alertCountries" class="form-select" multiple>
@@ -365,165 +372,164 @@
<!-- SMTP Configuration Group --> <!-- SMTP Configuration Group -->
<fieldset class="border p-3 rounded mb-4"> <fieldset class="border p-3 rounded mb-4">
<legend class="w-auto px-2">SMTP Configuration</legend> <legend class="w-auto px-2" data-i18n="settings.smtp">SMTP Configuration</legend>
<div class="mb-3"> <div class="mb-3">
<label for="smtpHost" class="form-label">SMTP Host</label> <label for="smtpHost" class="form-label" data-i18n="settings.smtp_host">SMTP Host</label>
<input type="text" class="form-control" id="smtpHost" placeholder="e.g., smtp.gmail.com" required /> <input type="text" class="form-control" id="smtpHost"
data-i18n-placeholder="settings.smtp_host_placeholder" placeholder="e.g., smtp.gmail.com" required />
</div> </div>
<label for="smtpPort">SMTP Port</label> <label for="smtpPort" data-i18n="settings.smtp_port">SMTP Port</label>
<select id="smtpPort" class="form-select"> <select id="smtpPort" class="form-select">
<option value="587" selected>587 (Recommended - STARTTLS)</option> <option value="587" selected>587 (Recommended - STARTTLS)</option>
<option value="465" disabled>465 (Not Supported)</option> <option value="465" disabled>465 (Not Supported)</option>
</select> </select>
<div class="mb-3"> <div class="mb-3">
<label for="smtpUsername" class="form-label">SMTP Username</label> <label for="smtpUsername" class="form-label" data-i18n="settings.smtp_username">SMTP Username</label>
<input type="text" class="form-control" id="smtpUsername" placeholder="e.g., user@example.com" required /> <input type="text" class="form-control" id="smtpUsername"
data-i18n-placeholder="settings.smtp_username_placeholder" placeholder="e.g., user@example.com" required />
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="smtpPassword" class="form-label">SMTP Password</label> <label for="smtpPassword" class="form-label" data-i18n="settings.smtp_password">SMTP Password</label>
<input type="password" class="form-control" id="smtpPassword" placeholder="Enter SMTP Password" required /> <input type="password" class="form-control" id="smtpPassword"
data-i18n-placeholder="settings.smtp_password_placeholder" placeholder="Enter SMTP Password" required />
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="smtpFrom" class="form-label">Sender Email</label> <label for="smtpFrom" class="form-label" data-i18n="settings.smtp_sender">Sender Email</label>
<input type="email" class="form-control" id="smtpFrom" placeholder="noreply@swissmakers.ch" required /> <input type="email" class="form-control" id="smtpFrom"
data-i18n-placeholder="settings.smtp_sender_placeholder" placeholder="noreply@swissmakers.ch" required />
</div> </div>
<div class="mb-3 form-check"> <div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="smtpUseTLS"> <input type="checkbox" class="form-check-input" id="smtpUseTLS">
<label for="smtpUseTLS" class="form-check-label">Use TLS (Recommended)</label> <label for="smtpUseTLS" class="form-check-label" data-i18n="settings.smtp_tls">Use TLS (Recommended)</label>
</div> </div>
<button type="button" class="btn btn-secondary mt-2" onclick="sendTestEmail()">Send Test Email</button> <button type="button" class="btn btn-secondary mt-2" onclick="sendTestEmail()" data-i18n="settings.send_test_email">Send Test Email</button>
</fieldset> </fieldset>
<!-- Fail2Ban Configuration Group --> <!-- Fail2Ban Configuration Group -->
<fieldset class="border p-3 rounded mb-4"> <fieldset class="border p-3 rounded mb-4">
<legend class="w-auto px-2">Fail2Ban Configuration</legend> <legend class="w-auto px-2" data-i18n="settings.fail2ban">Fail2Ban Configuration</legend>
<!-- Bantime Increment --> <!-- Bantime Increment -->
<div class="mb-3 form-check"> <div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="bantimeIncrement" /> <input type="checkbox" class="form-check-input" id="bantimeIncrement" />
<label for="bantimeIncrement" class="form-check-label">Enable Bantime Increment</label> <label for="bantimeIncrement" class="form-check-label" data-i18n="settings.enable_bantime_increment">Enable Bantime Increment</label>
</div> </div>
<!-- Bantime --> <!-- Bantime -->
<div class="mb-3"> <div class="mb-3">
<label for="banTime" class="form-label">Default Bantime</label> <label for="banTime" class="form-label" data-i18n="settings.default_bantime">Default Bantime</label>
<input type="text" class="form-control" id="banTime" placeholder="e.g., 48h" /> <input type="text" class="form-control" id="banTime"
data-i18n-placeholder="settings.default_bantime_placeholder" placeholder="e.g., 48h" />
</div> </div>
<!-- Findtime --> <!-- Findtime -->
<div class="mb-3"> <div class="mb-3">
<label for="findTime" class="form-label">Default Findtime</label> <label for="findTime" class="form-label" data-i18n="settings.default_findtime">Default Findtime</label>
<input type="text" class="form-control" id="findTime" placeholder="e.g., 30m" /> <input type="text" class="form-control" id="findTime"
data-i18n-placeholder="settings.default_findtime_placeholder" placeholder="e.g., 30m" />
</div> </div>
<!-- Max Retry --> <!-- Max Retry -->
<div class="mb-3"> <div class="mb-3">
<label for="maxRetry" class="form-label">Default Max Retry</label> <label for="maxRetry" class="form-label" data-i18n="settings.default_max_retry">Default Max Retry</label>
<input type="number" class="form-control" id="maxRetry" placeholder="Enter maximum retries" /> <input type="number" class="form-control" id="maxRetry"
data-i18n-placeholder="settings.default_max_retry_placeholder" placeholder="Enter maximum retries" />
</div> </div>
<!-- Ignore IPs --> <!-- Ignore IPs -->
<div class="mb-3"> <div class="mb-3">
<label for="ignoreIP" class="form-label">Ignore IPs</label> <label for="ignoreIP" class="form-label" data-i18n="settings.ignore_ips">Ignore IPs</label>
<textarea class="form-control" id="ignoreIP" rows="2" placeholder="IPs to ignore, separated by spaces"></textarea> <textarea class="form-control" id="ignoreIP" rows="2"
data-i18n-placeholder="settings.ignore_ips_placeholder" placeholder="IPs to ignore, separated by spaces"></textarea>
</div> </div>
</fieldset> </fieldset>
<button type="submit" class="btn btn-primary">Save</button> <button type="submit" class="btn btn-primary" data-i18n="settings.save">Save</button>
</form> </form>
</div> </div>
<!-- ******************************************************************* --> <!-- ******************************************************************* -->
<!-- Footer --> <!-- Footer -->
<footer class="text-center mt-4 mb-4"> <footer class="text-center mt-4 mb-4">
<p class="mb-0"> <p class="mb-0">
&copy; <a href="https://swissmakers.ch" target="_blank">Swissmakers GmbH</a> &copy; <a href="https://swissmakers.ch" target="_blank">Swissmakers GmbH</a>
- -
<a href="https://github.com/swissmakers/fail2ban-ui" target="_blank"> <a href="https://github.com/swissmakers/fail2ban-ui" target="_blank">GitHub</a>
GitHub
</a>
</p> </p>
</footer> </footer>
<!-- ******************************************************************* --> <!-- ******************************************************************* -->
<!-- APP Components (HTML) : --> <!-- APP Components (HTML) : -->
<!-- ******************************************************************* --> <!-- ******************************************************************* -->
<!-- Loading Overlay --> <!-- Loading Overlay -->
<div id="loading-overlay" class="d-flex"> <div id="loading-overlay" class="d-flex">
<div class="spinner-border text-light" role="status"> <div class="spinner-border text-light" role="status">
<span class="visually-hidden">Loading...</span> <span class="visually-hidden" data-i18n="loading">Loading...</span>
</div>
</div> </div>
</div>
<!-- Jail Config Modal --> <!-- Jail Config Modal -->
<div class="modal fade" id="jailConfigModal" tabindex="-1" aria-hidden="true"> <div class="modal fade" id="jailConfigModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-scrollable"> <div class="modal-dialog modal-lg modal-dialog-scrollable">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title"> <h5 class="modal-title">
Filter Config: <span id="modalJailName"></span> <span data-i18n="modal.filter_config">Filter Config:</span> <span id="modalJailName"></span>
</h5> </h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
aria-label="Close"></button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<textarea id="jailConfigTextarea" class="form-control" rows="15"></textarea> <textarea id="jailConfigTextarea" class="form-control" rows="15"></textarea>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-secondary" <button type="button" class="btn btn-secondary" data-bs-dismiss="modal" data-i18n="modal.cancel">Cancel</button>
data-bs-dismiss="modal">Cancel</button> <button type="button" class="btn btn-primary" onclick="saveJailConfig()" data-i18n="modal.save">Save</button>
<button type="button" class="btn btn-primary" onclick="saveJailConfig()">
Save
</button>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- ******************************************************************* --> <!-- ******************************************************************* -->
<!-- Bootstrap 5 JS (for modal, etc.) --> <!-- Bootstrap 5 JS (for modal, etc.) -->
<script <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js"></script>
src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js"> <!-- jQuery (used by Select2) -->
</script> <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<!-- jQuery (used by Select2) --> <!-- Select2 JS -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/select2@4.0.13/dist/js/select2.min.js"></script>
<!-- Select2 JS -->
<script src="https://cdn.jsdelivr.net/npm/select2@4.0.13/dist/js/select2.min.js"></script>
<script> <script>
// For information: We avoid ES6 backticks in our JS, to prevent confusion with the Go template parser. // For information: We avoid ES6 backticks in our JS, to prevent confusion with the Go template parser.
"use strict"; "use strict";
//******************************************************************* //*******************************************************************
//* Init page and main-components : * //* Init page and main-components : *
//******************************************************************* //*******************************************************************
// Init and run first function, when DOM is ready var currentJailForConfig = null;
var currentJailForConfig = null; window.addEventListener('DOMContentLoaded', function() {
window.addEventListener('DOMContentLoaded', function() {
showLoading(true); showLoading(true);
checkReloadNeeded(); checkReloadNeeded();
fetchSummary().then(function() { fetchSummary().then(function() {
showLoading(false); showLoading(false);
initializeTooltips(); // Initialize tooltips after fetching and rendering initializeTooltips(); // Initialize tooltips after fetching and rendering
getTranslationsSettingsOnPageload();
});
}); });
});
// Function to initialize Bootstrap tooltips // Function to initialize Bootstrap tooltips
function initializeTooltips() { function initializeTooltips() {
const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]')); const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
tooltipTriggerList.forEach(function (tooltipTriggerEl) { tooltipTriggerList.forEach(function (tooltipTriggerEl) {
new bootstrap.Tooltip(tooltipTriggerEl); new bootstrap.Tooltip(tooltipTriggerEl);
}); });
} }
// Toggle the loading overlay (with !important) // Toggle the loading overlay (with !important)
function showLoading(show) { function showLoading(show) {
var overlay = document.getElementById('loading-overlay'); var overlay = document.getElementById('loading-overlay');
if (show) { if (show) {
overlay.style.setProperty('display', 'flex', 'important'); overlay.style.setProperty('display', 'flex', 'important');
} else { } else {
overlay.style.setProperty('display', 'none', 'important'); overlay.style.setProperty('display', 'none', 'important');
} }
} }
// Check if there is still a reload of the fail2ban service needed // Check if there is still a reload of the fail2ban service needed
function checkReloadNeeded() { function checkReloadNeeded() {
fetch('/api/settings') fetch('/api/settings')
.then(res => res.json()) .then(res => res.json())
.then(data => { .then(data => {
@@ -534,10 +540,10 @@ function checkReloadNeeded() {
} }
}) })
.catch(err => console.error('Error checking reloadNeeded:', err)); .catch(err => console.error('Error checking reloadNeeded:', err));
} }
// Load dynamically the other pages when navigating in nav // Load dynamically the other pages when navigating in nav
function showSection(sectionId) { function showSection(sectionId) {
// hide all sections // hide all sections
document.getElementById('dashboardSection').style.display = 'none'; document.getElementById('dashboardSection').style.display = 'none';
document.getElementById('filterSection').style.display = 'none'; document.getElementById('filterSection').style.display = 'none';
@@ -561,14 +567,14 @@ function showSection(sectionId) {
let navbarToggler = document.querySelector('.navbar-toggler'); let navbarToggler = document.querySelector('.navbar-toggler');
navbarToggler.click(); navbarToggler.click();
} }
} }
//******************************************************************* //*******************************************************************
//* Fetch data and render dashboard : * //* Fetch data and render dashboard : *
//******************************************************************* //*******************************************************************
// Fetch summary (jails, stats, last bans) // Fetch summary (jails, stats, last bans)
function fetchSummary() { function fetchSummary() {
return fetch('/api/summary') return fetch('/api/summary')
.then(function(res) { return res.json(); }) .then(function(res) { return res.json(); })
.then(function(data) { .then(function(data) {
@@ -583,35 +589,35 @@ function fetchSummary() {
document.getElementById('dashboard').innerHTML = document.getElementById('dashboard').innerHTML =
'<div class="alert alert-danger">Error: ' + err + '</div>'; '<div class="alert alert-danger">Error: ' + err + '</div>';
}); });
} }
// Render the main dashboard // Render the main dashboard
function renderDashboard(data) { function renderDashboard(data) {
var html = ""; var html = "";
// Add a search bar // Add a search bar
html += ` html += `
<fieldset class="border p-3 rounded mb-4"> <fieldset class="border p-3 rounded mb-4">
<legend class="w-auto px-2"><span data-bs-toggle="tooltip" data-bs-placement="top" data-bs-original-title="The Overview displays the currently enabled jails that you have added to your jail.local configuration.">Overview active Jails and Blocks</span></legend> <legend class="w-auto px-2"><span data-bs-toggle="tooltip" data-bs-placement="top" data-bs-original-title="The Overview displays the currently enabled jails that you have added to your jail.local configuration." data-i18n="dashboard.overview">Overview active Jails and Blocks</span></legend>
<div class="mb-3"> <div class="mb-3">
<label for="ipSearch" class="form-label">Search Banned IPs</label> <label for="ipSearch" class="form-label" data-i18n="dashboard.search_label">Search Banned IPs</label>
<input type="text" id="ipSearch" class="form-control" placeholder="Enter IP address to search" onkeyup="filterIPs()"> <input type="text" id="ipSearch" class="form-control" placeholder="Enter IP address to search" data-i18n-placeholder="dashboard.search_placeholder" onkeyup="filterIPs()">
</div> </div>
`; `;
// Jails table // Jails table
if (!data.jails || data.jails.length === 0) { if (!data.jails || data.jails.length === 0) {
html += '<p>No jails found.</p>'; html += '<p data-i18n="dashboard.no_jails">No jails found.</p>';
} else { } else {
html += '' html += ''
+ '<div class="table-responsive">' + '<div class="table-responsive">'
+ '<table class="table table-striped" id="jailsTable">' + '<table class="table table-striped" id="jailsTable">'
+ ' <thead>' + ' <thead>'
+ ' <tr>' + ' <tr>'
+ ' <th>Jail Name</th>' + ' <th data-i18n="dashboard.table.jail_name">Jail Name</th>'
+ ' <th>Total Banned</th>' + ' <th data-i18n="dashboard.table.total_banned">Total Banned</th>'
+ ' <th>New Last Hour</th>' + ' <th data-i18n="dashboard.table.new_last_hour">New Last Hour</th>'
+ ' <th>Banned IPs (Unban)</th>' + ' <th data-i18n="dashboard.table.banned_ips">Banned IPs (Unban)</th>'
+ ' </tr>' + ' </tr>'
+ ' </thead>' + ' </thead>'
+ ' <tbody>'; + ' <tbody>';
@@ -637,19 +643,19 @@ function renderDashboard(data) {
// Last 5 bans // Last 5 bans
html += '<fieldset class="border p-3 rounded mb-4">'; html += '<fieldset class="border p-3 rounded mb-4">';
html += ' <legend class="w-auto px-2">Last 5 Ban Events</legend>'; html += ' <legend class="w-auto px-2" data-i18n="dashboard.last_bans">Last 5 Ban Events</legend>';
if (!data.lastBans || data.lastBans.length === 0) { if (!data.lastBans || data.lastBans.length === 0) {
html += '<p>No recent bans found.</p>'; html += '<p data-i18n="dashboard.no_recent_bans">No recent bans found.</p>';
} else { } else {
html += '' html += ''
+ '<div class="table-responsive">' + '<div class="table-responsive">'
+ '<table class="table table-bordered">' + '<table class="table table-bordered">'
+ ' <thead>' + ' <thead>'
+ ' <tr>' + ' <tr>'
+ ' <th>Time</th>' + ' <th data-i18n="dashboard.table.time">Time</th>'
+ ' <th>Jail</th>' + ' <th data-i18n="dashboard.table.jail">Jail</th>'
+ ' <th>IP</th>' + ' <th data-i18n="dashboard.table.ip">IP</th>'
+ ' <th>Log Line</th>' + ' <th data-i18n="dashboard.table.log_line">Log Line</th>'
+ ' </tr>' + ' </tr>'
+ ' </thead>' + ' </thead>'
+ ' <tbody></div>'; + ' <tbody></div>';
@@ -668,12 +674,12 @@ function renderDashboard(data) {
} }
document.getElementById('dashboard').innerHTML = html; document.getElementById('dashboard').innerHTML = html;
} }
// Render banned IPs with "Unban" button // Render banned IPs with "Unban" button
function renderBannedIPs(jailName, ips) { function renderBannedIPs(jailName, ips) {
if (!ips || ips.length === 0) { if (!ips || ips.length === 0) {
return '<em>No banned IPs</em>'; return '<em data-i18n="dashboard.no_banned_ips">No banned IPs</em>';
} }
var content = '<ul class="list-unstyled mb-0">'; var content = '<ul class="list-unstyled mb-0">';
ips.forEach(function(ip) { ips.forEach(function(ip) {
@@ -682,53 +688,48 @@ function renderBannedIPs(jailName, ips) {
+ ' <span class="me-auto">' + ip + '</span>' + ' <span class="me-auto">' + ip + '</span>'
+ ' <button class="btn btn-sm btn-warning"' + ' <button class="btn btn-sm btn-warning"'
+ ' onclick="unbanIP(\'' + jailName + '\', \'' + ip + '\')">' + ' onclick="unbanIP(\'' + jailName + '\', \'' + ip + '\')">'
+ ' Unban' + ' <span data-i18n="dashboard.unban">Unban</span>'
+ ' </button>' + ' </button>'
+ '</li>'; + '</li>';
}); });
content += '</ul>'; content += '</ul>';
return content; return content;
} }
// Filter IPs on dashboard table // Filter IPs on dashboard table
function filterIPs() { function filterIPs() {
const query = document.getElementById("ipSearch").value.toLowerCase(); // Get the search query const query = document.getElementById("ipSearch").value.toLowerCase();
const rows = document.querySelectorAll("#jailsTable .jail-row"); // Get all jail rows const rows = document.querySelectorAll("#jailsTable .jail-row");
rows.forEach((row) => { rows.forEach((row) => {
const ipSpans = row.querySelectorAll("ul li span"); // Find all IP span elements in this row const ipSpans = row.querySelectorAll("ul li span");
let matchFound = false; // Reset match flag for the row let matchFound = false;
ipSpans.forEach((span) => { ipSpans.forEach((span) => {
const originalText = span.textContent; // The full original text const originalText = span.textContent;
const ipText = originalText.toLowerCase(); const ipText = originalText.toLowerCase();
if (query && ipText.includes(query)) { if (query && ipText.includes(query)) {
matchFound = true; // Match found in this row matchFound = true;
// Highlight the matching part
const highlightedText = originalText.replace( const highlightedText = originalText.replace(
new RegExp(query, "gi"), // Case-insensitive match new RegExp(query, "gi"),
(match) => `<mark>${match}</mark>` // Wrap match in <mark> (match) => `<mark>${match}</mark>`
); );
span.innerHTML = highlightedText; // Update span's HTML with highlighting span.innerHTML = highlightedText;
} else { } else {
// Remove highlighting if no match or search is cleared
span.innerHTML = originalText; span.innerHTML = originalText;
} }
}); });
// Show the row if a match is found or the query is empty
row.style.display = matchFound || !query ? "" : "none"; row.style.display = matchFound || !query ? "" : "none";
}); });
} }
//******************************************************************* //*******************************************************************
//* Functions to manage IP-bans : * //* Functions to manage IP-bans : *
//******************************************************************* //*******************************************************************
// Unban IP function unbanIP(jail, ip) {
function unbanIP(jail, ip) {
if (!confirm("Unban IP " + ip + " from jail " + jail + "?")) { if (!confirm("Unban IP " + ip + " from jail " + jail + "?")) {
return; return;
} }
@@ -749,14 +750,13 @@ function unbanIP(jail, ip) {
.finally(function() { .finally(function() {
showLoading(false); showLoading(false);
}); });
} }
//******************************************************************* //*******************************************************************
//* Filter-mod and config-mod actions : * //* Filter-mod and config-mod actions : *
//******************************************************************* //*******************************************************************
// Open jail/filter modal and load filter-config function openJailConfigModal(jailName) {
function openJailConfigModal(jailName) {
currentJailForConfig = jailName; currentJailForConfig = jailName;
var textArea = document.getElementById('jailConfigTextarea'); var textArea = document.getElementById('jailConfigTextarea');
textArea.value = ''; textArea.value = '';
@@ -782,10 +782,9 @@ function openJailConfigModal(jailName) {
.finally(function() { .finally(function() {
showLoading(false); showLoading(false);
}); });
} }
// Save filter config for the current opened jail function saveJailConfig() {
function saveJailConfig() {
if (!currentJailForConfig) return; if (!currentJailForConfig) return;
showLoading(true); showLoading(true);
@@ -800,13 +799,10 @@ function saveJailConfig() {
if (data.error) { if (data.error) {
alert("Error saving config: " + data.error); alert("Error saving config: " + data.error);
} else { } else {
//alert(data.message || "Config saved");
console.log("Filter saved successfully. Reload needed? " + data.reloadNeeded); console.log("Filter saved successfully. Reload needed? " + data.reloadNeeded);
// Hide modal
var modalEl = document.getElementById('jailConfigModal'); var modalEl = document.getElementById('jailConfigModal');
var modalObj = bootstrap.Modal.getInstance(modalEl); var modalObj = bootstrap.Modal.getInstance(modalEl);
modalObj.hide(); modalObj.hide();
// Show the reload banner
document.getElementById('reloadBanner').style.display = 'block'; document.getElementById('reloadBanner').style.display = 'block';
} }
}) })
@@ -816,28 +812,27 @@ function saveJailConfig() {
.finally(function() { .finally(function() {
showLoading(false); showLoading(false);
}); });
} }
// Load current settings when opening settings page //*******************************************************************
function loadSettings() { //* Load current settings when opening settings page : *
//*******************************************************************
function loadSettings() {
showLoading(true); showLoading(true);
fetch('/api/settings') fetch('/api/settings')
.then(res => res.json()) .then(res => res.json())
.then(data => { .then(data => {
// Get current general settings
document.getElementById('languageSelect').value = data.language || 'en'; document.getElementById('languageSelect').value = data.language || 'en';
document.getElementById('debugMode').checked = data.debug || false; document.getElementById('debugMode').checked = data.debug || false;
// Get Alert settings
document.getElementById('destEmail').value = data.destemail || ''; document.getElementById('destEmail').value = data.destemail || '';
// Get Alert countries selection
const select = document.getElementById('alertCountries'); const select = document.getElementById('alertCountries');
for (let i = 0; i < select.options.length; i++) { for (let i = 0; i < select.options.length; i++) {
select.options[i].selected = false; select.options[i].selected = false;
} }
if (!data.alertCountries || data.alertCountries.length === 0) { if (!data.alertCountries || data.alertCountries.length === 0) {
// default to "ALL"
select.options[0].selected = true; select.options[0].selected = true;
} else { } else {
for (let i = 0; i < select.options.length; i++) { for (let i = 0; i < select.options.length; i++) {
@@ -847,10 +842,8 @@ function loadSettings() {
} }
} }
} }
// Update Select2 UI
$('#alertCountries').trigger('change'); $('#alertCountries').trigger('change');
// Get SMTP settings
if (data.smtp) { if (data.smtp) {
document.getElementById('smtpHost').value = data.smtp.host || ''; document.getElementById('smtpHost').value = data.smtp.host || '';
document.getElementById('smtpPort').value = data.smtp.port || 587; document.getElementById('smtpPort').value = data.smtp.port || 587;
@@ -860,7 +853,6 @@ function loadSettings() {
document.getElementById('smtpUseTLS').checked = data.smtp.useTLS || false; document.getElementById('smtpUseTLS').checked = data.smtp.useTLS || false;
} }
// Get current Fail2Ban settings
document.getElementById('bantimeIncrement').checked = data.bantimeIncrement || false; document.getElementById('bantimeIncrement').checked = data.bantimeIncrement || false;
document.getElementById('banTime').value = data.bantime || ''; document.getElementById('banTime').value = data.bantime || '';
document.getElementById('findTime').value = data.findtime || ''; document.getElementById('findTime').value = data.findtime || '';
@@ -871,12 +863,14 @@ function loadSettings() {
alert('Error loading settings: ' + err); alert('Error loading settings: ' + err);
}) })
.finally(() => showLoading(false)); .finally(() => showLoading(false));
} }
// Save settings when hitting the save button //*******************************************************************
function saveSettings(event) { //* Save settings when hitting the save button : *
event.preventDefault(); // prevent form submission //*******************************************************************
function saveSettings(event) {
event.preventDefault();
showLoading(true); showLoading(true);
const smtpSettings = { const smtpSettings = {
@@ -895,15 +889,12 @@ function saveSettings(event) {
debug: document.getElementById('debugMode').checked, debug: document.getElementById('debugMode').checked,
destemail: document.getElementById('destEmail').value.trim(), destemail: document.getElementById('destEmail').value.trim(),
alertCountries: selectedCountries.length > 0 ? selectedCountries : ["ALL"], alertCountries: selectedCountries.length > 0 ? selectedCountries : ["ALL"],
bantimeIncrement: document.getElementById('bantimeIncrement').checked, bantimeIncrement: document.getElementById('bantimeIncrement').checked,
bantime: document.getElementById('banTime').value.trim(), bantime: document.getElementById('banTime').value.trim(),
findtime: document.getElementById('findTime').value.trim(), findtime: document.getElementById('findTime').value.trim(),
maxretry: parseInt(document.getElementById('maxRetry').value, 10) || 3, maxretry: parseInt(document.getElementById('maxRetry').value, 10) || 3,
ignoreip: document.getElementById('ignoreIP').value.trim(), ignoreip: document.getElementById('ignoreIP').value.trim(),
smtp: smtpSettings
smtp: smtpSettings // (Includes SMTP settings)
}; };
fetch('/api/settings', { fetch('/api/settings', {
@@ -916,6 +907,8 @@ function saveSettings(event) {
if (data.error) { if (data.error) {
alert('Error saving settings: ' + data.error + data.details); alert('Error saving settings: ' + data.error + data.details);
} else { } else {
var selectedLang = $('#languageSelect').val();
loadTranslations(selectedLang);
console.log("Settings saved successfully. Reload needed? " + data.reloadNeeded); console.log("Settings saved successfully. Reload needed? " + data.reloadNeeded);
if (data.reloadNeeded) { if (data.reloadNeeded) {
document.getElementById('reloadBanner').style.display = 'block'; document.getElementById('reloadBanner').style.display = 'block';
@@ -924,10 +917,13 @@ function saveSettings(event) {
}) })
.catch(err => alert('Error: ' + err)) .catch(err => alert('Error: ' + err))
.finally(() => showLoading(false)); .finally(() => showLoading(false));
} }
// Load the list of filters from /api/filters //*******************************************************************
function loadFilters() { //* Load the list of filters from /api/filters : *
//*******************************************************************
function loadFilters() {
showLoading(true); showLoading(true);
fetch('/api/filters') fetch('/api/filters')
.then(res => res.json()) .then(res => res.json())
@@ -937,7 +933,7 @@ function loadFilters() {
return; return;
} }
const select = document.getElementById('filterSelect'); const select = document.getElementById('filterSelect');
select.innerHTML = ''; // clear existing select.innerHTML = '';
if (!data.filters || data.filters.length === 0) { if (!data.filters || data.filters.length === 0) {
const opt = document.createElement('option'); const opt = document.createElement('option');
opt.value = ''; opt.value = '';
@@ -956,9 +952,9 @@ function loadFilters() {
alert('Error loading filters: ' + err); alert('Error loading filters: ' + err);
}) })
.finally(() => showLoading(false)); .finally(() => showLoading(false));
} }
function sendTestEmail() { function sendTestEmail() {
showLoading(true); showLoading(true);
fetch('/api/settings/test-email', { fetch('/api/settings/test-email', {
@@ -975,10 +971,10 @@ function sendTestEmail() {
}) })
.catch(error => alert('Error: ' + error)) .catch(error => alert('Error: ' + error))
.finally(() => showLoading(false)); .finally(() => showLoading(false));
} }
// Called when clicking "Test Filter" button // Called when clicking "Test Filter" button
function testSelectedFilter() { function testSelectedFilter() {
const filterName = document.getElementById('filterSelect').value; const filterName = document.getElementById('filterSelect').value;
const lines = document.getElementById('logLinesTextarea').value.split('\n'); const lines = document.getElementById('logLinesTextarea').value.split('\n');
@@ -1008,12 +1004,12 @@ function testSelectedFilter() {
alert('Error: ' + err); alert('Error: ' + err);
}) })
.finally(() => showLoading(false)); .finally(() => showLoading(false));
} }
function renderTestResults(matches) { function renderTestResults(matches) {
let html = '<h5>Test Results</h5>'; let html = '<h5 data-i18n="filter_debug.test_results_title">Test Results</h5>';
if (!matches || matches.length === 0) { if (!matches || matches.length === 0) {
html += '<p>No matches found.</p>'; html += '<p data-i18n="filter_debug.no_matches">No matches found.</p>';
} else { } else {
html += '<ul>'; html += '<ul>';
matches.forEach(m => { matches.forEach(m => {
@@ -1022,21 +1018,20 @@ function renderTestResults(matches) {
html += '</ul>'; html += '</ul>';
} }
document.getElementById('testResults').innerHTML = html; document.getElementById('testResults').innerHTML = html;
} }
// When showing the filter section // When showing the filter section
function showFilterSection() { function showFilterSection() {
loadFilters(); // fetch the filter list loadFilters();
document.getElementById('testResults').innerHTML = ''; document.getElementById('testResults').innerHTML = '';
document.getElementById('logLinesTextarea').value = ''; document.getElementById('logLinesTextarea').value = '';
} }
//******************************************************************* //*******************************************************************
//* Reload fail2ban action : * //* Reload fail2ban action : *
//******************************************************************* //*******************************************************************
// Reload Fail2ban function reloadFail2ban() {
function reloadFail2ban() {
if (!confirm("It can happen that some logs are not parsed during the reload of fail2ban. Reload Fail2ban now?")) return; if (!confirm("It can happen that some logs are not parsed during the reload of fail2ban. Reload Fail2ban now?")) return;
showLoading(true); showLoading(true);
fetch('/api/fail2ban/reload', { method: 'POST' }) fetch('/api/fail2ban/reload', { method: 'POST' })
@@ -1045,9 +1040,7 @@ function reloadFail2ban() {
if (data.error) { if (data.error) {
alert("Error: " + data.error); alert("Error: " + data.error);
} else { } else {
// Hide reload banner
document.getElementById('reloadBanner').style.display = 'none'; document.getElementById('reloadBanner').style.display = 'none';
// Refresh data
return fetchSummary(); return fetchSummary();
} }
}) })
@@ -1057,21 +1050,19 @@ function reloadFail2ban() {
.finally(function() { .finally(function() {
showLoading(false); showLoading(false);
}); });
} }
//******************************************************************* //*******************************************************************
//* Is executed when doc-ready : * //* Is executed when doc-ready : *
//******************************************************************* //*******************************************************************
$(document).ready(function() { $(document).ready(function() {
// Initialize the Select2 widget with a placeholder and full width.
$('#alertCountries').select2({ $('#alertCountries').select2({
placeholder: 'Select countries..', placeholder: 'Select countries..',
allowClear: true, allowClear: true,
width: '100%' width: '100%'
}); });
// Custom logic: If "ALL" is selected, remove other selections.
$('#alertCountries').on('select2:select', function(e) { $('#alertCountries').on('select2:select', function(e) {
var selectedValue = e.params.data.id; var selectedValue = e.params.data.id;
var currentValues = $('#alertCountries').val() || []; var currentValues = $('#alertCountries').val() || [];
@@ -1089,6 +1080,57 @@ function reloadFail2ban() {
} }
}); });
}); });
</script>
//*******************************************************************
//* Translation Related Functions : *
//*******************************************************************
var translations = {};
// Loads translation JSON file for given language (e.g., en, de, etc.)
function loadTranslations(lang) {
$.getJSON('/locales/' + lang + '.json')
.done(function(data) {
translations = data;
updateTranslations();
})
.fail(function() {
console.error('Failed to load translations for language:', lang);
});
}
// Updates all elements with data-i18n attribute with corresponding translation.
function updateTranslations() {
$('[data-i18n]').each(function() {
var key = $(this).data('i18n');
if (translations[key]) {
$(this).text(translations[key]);
}
});
// Updates placeholders.
$('[data-i18n-placeholder]').each(function() {
var key = $(this).data('i18n-placeholder');
if (translations[key]) {
$(this).attr('placeholder', translations[key]);
}
});
}
function getTranslationsSettingsOnPageload() {
// Fetch settings to get the current language preference
fetch('/api/settings')
.then(function(res) { return res.json(); })
.then(function(data) {
var lang = data.language || 'en'; // Use the language from settings or default to "en"
$('#languageSelect').val(lang); // Update the language dropdown accordingly
loadTranslations(lang); // Load the appropriate translation file
})
.catch(function(err) {
console.error('Error loading initial settings:', err);
// In case of an error, fallback to English
loadTranslations('en');
});
}
</script>
</body> </body>
</html> </html>