Files
fail2ban-ui/pkg/web/templates/index.html

1241 lines
52 KiB
HTML

<!--
Fail2ban UI - A Swiss made, management interface for Fail2ban.
Copyright (C) 2025 Swissmakers GmbH
Licensed under the GNU General Public License, Version 3 (GPL-3.0)
You may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.gnu.org/licenses/gpl-3.0.en.html
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no" />
<title data-i18n="page.title">Fail2ban UI Dashboard</title>
<!-- Bootstrap 5 (CDN) -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" rel="stylesheet" />
<!-- Select2 CSS -->
<link href="https://cdn.jsdelivr.net/npm/select2@4.0.13/dist/css/select2.min.css" rel="stylesheet" />
<style>
/* Loading overlay styling */
#loading-overlay {
display: none; /* hidden by default */
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.5);
z-index: 9999; /* on top */
align-items: center;
justify-content: center;
}
.spinner-border {
width: 4rem;
height: 4rem;
}
/* Restart banner */
#restartBanner {
display: none;
}
/* Improve table readability on small screens */
@media (max-width: 575px) {
.table th,
.table td {
font-size: 0.8rem;
padding: 5px;
}
.table thead {
font-size: 1rem;
}
}
</style>
</head>
<body class="bg-light">
<!-- ******************************************************************* -->
<!-- NAVIGATION : -->
<!-- ******************************************************************* -->
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
<div class="container-fluid">
<a class="navbar-brand" href="#">
<strong>Fail2ban UI</strong>
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent"
aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav ms-auto mb-2 mb-lg-0">
<li class="nav-item">
<a class="nav-link" href="#" onclick="showSection('dashboardSection')" data-i18n="nav.dashboard">Dashboard</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#" onclick="showSection('filterSection')" data-i18n="nav.filter_debug">Filter Debug</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#" onclick="showSection('settingsSection')" data-i18n="nav.settings">Settings</a>
</li>
</ul>
</div>
</div>
</nav>
<!-- ******************************************************************* -->
<!-- Restart Banner -->
<div id="restartBanner" class="bg-warning text-dark p-3 text-center">
<strong class="p-2" data-i18n="restart_banner.message">Fail2ban configuration changed! To apply the changes, please: </strong>
<button class="btn btn-dark" onclick="restartFail2ban()" data-i18n="restart_banner.button">Restart Service</button>
</div>
<!-- ******************************************************************* -->
<!-- APP Sections (Pages) : -->
<!-- ******************************************************************* -->
<!-- Dashboard Section -->
<div id="dashboardSection" class="container my-4">
<div class="d-flex align-items-center" style="position: relative;">
<h1 class="mb-4 flex-grow-1" data-i18n="dashboard.title">Dashboard</h1>
<button class="btn btn-outline-secondary" style="position: absolute; right: 0; top: 0;" onclick="openManageJailsModal()" data-i18n="dashboard.manage_jails">Manage Jails</button>
</div>
<div id="dashboard"></div>
</div>
<!-- Filter Debug Section -->
<div id="filterSection" style="display: none;" class="container my-4">
<h2 data-i18n="filter_debug.title">Filter Debug</h2>
<!-- Dropdown of available jail/filters -->
<div class="mb-3">
<label for="filterSelect" class="form-label" data-i18n="filter_debug.select_filter">Select a Filter</label>
<select id="filterSelect" class="form-select"></select>
</div>
<!-- Textarea for log lines to test -->
<div class="mb-3">
<label class="form-label" data-i18n="filter_debug.log_lines">Log Lines</label>
<textarea id="logLinesTextarea" class="form-control" rows="6" disabled
data-i18n-placeholder="filter_debug.log_lines_placeholder" placeholder="Enter log lines here..."></textarea>
</div>
<button class="btn btn-secondary" onclick="testSelectedFilter()" data-i18n="filter_debug.test_filter">Test Filter</button>
<hr/>
<div id="testResults"></div>
</div>
<!-- Settings Section -->
<div id="settingsSection" style="display: none;" class="container my-4">
<h2 data-i18n="settings.title">Settings</h2>
<form onsubmit="saveSettings(event)">
<!-- General Settings Group -->
<fieldset class="border p-3 rounded mb-4">
<legend class="w-auto px-2" data-i18n="settings.general">General Settings</legend>
<!-- Language Selection -->
<div class="mb-3">
<label for="languageSelect" class="form-label" data-i18n="settings.language">Language</label>
<select id="languageSelect" class="form-select">
<option value="en">English</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>
</div>
<!-- Fail2Ban UI Port (server) -->
<div class="mb-3">
<label for="uiPort" class="form-label" data-i18n="settings.server_port">Server Port</label>
<input type="number" class="form-control" id="uiPort"
data-i18n-placeholder="settings.server_port_placeholder" placeholder="e.g., 8080" required min="80" max="65535" required />
</div>
<!-- Debug Log Output -->
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="debugMode">
<label for="debugMode" class="form-check-label" data-i18n="settings.enable_debug">Enable Debug Log</label>
</div>
</fieldset>
<!-- Alert Settings Group -->
<fieldset class="border p-3 rounded mb-4">
<legend class="w-auto px-2" data-i18n="settings.alert">Alert Settings</legend>
<div class="mb-3">
<label for="destEmail" class="form-label" data-i18n="settings.destination_email">Destination Email (Alerts Receiver)</label>
<input type="email" class="form-control" id="destEmail"
data-i18n-placeholder="settings.destination_email_placeholder" placeholder="alerts@swissmakers.ch" />
</div>
<div class="mb-3">
<label for="alertCountries" class="form-label" data-i18n="settings.alert_countries">Alert Countries</label>
<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.
</p>
<select id="alertCountries" class="form-select" multiple>
<option value="ALL">ALL (Every Country)</option>
<option value="AF">Afghanistan (AF)</option>
<option value="AL">Albania (AL)</option>
<option value="DZ">Algeria (DZ)</option>
<option value="AD">Andorra (AD)</option>
<option value="AO">Angola (AO)</option>
<option value="AG">Antigua and Barbuda (AG)</option>
<option value="AR">Argentina (AR)</option>
<option value="AM">Armenia (AM)</option>
<option value="AU">Australia (AU)</option>
<option value="AT">Austria (AT)</option>
<option value="AZ">Azerbaijan (AZ)</option>
<option value="BS">Bahamas (BS)</option>
<option value="BH">Bahrain (BH)</option>
<option value="BD">Bangladesh (BD)</option>
<option value="BB">Barbados (BB)</option>
<option value="BY">Belarus (BY)</option>
<option value="BE">Belgium (BE)</option>
<option value="BZ">Belize (BZ)</option>
<option value="BJ">Benin (BJ)</option>
<option value="BT">Bhutan (BT)</option>
<option value="BO">Bolivia (BO)</option>
<option value="BA">Bosnia and Herzegovina (BA)</option>
<option value="BW">Botswana (BW)</option>
<option value="BR">Brazil (BR)</option>
<option value="BN">Brunei (BN)</option>
<option value="BG">Bulgaria (BG)</option>
<option value="BF">Burkina Faso (BF)</option>
<option value="BI">Burundi (BI)</option>
<option value="CV">Cabo Verde (CV)</option>
<option value="KH">Cambodia (KH)</option>
<option value="CM">Cameroon (CM)</option>
<option value="CA">Canada (CA)</option>
<option value="CF">Central African Republic (CF)</option>
<option value="TD">Chad (TD)</option>
<option value="CL">Chile (CL)</option>
<option value="CN">China (CN)</option>
<option value="CO">Colombia (CO)</option>
<option value="KM">Comoros (KM)</option>
<option value="CG">Congo - Brazzaville (CG)</option>
<option value="CD">Congo - Kinshasa (CD)</option>
<option value="CR">Costa Rica (CR)</option>
<option value="CI">Côte d'Ivoire (CI)</option>
<option value="HR">Croatia (HR)</option>
<option value="CU">Cuba (CU)</option>
<option value="CY">Cyprus (CY)</option>
<option value="CZ">Czechia (CZ)</option>
<option value="DK">Denmark (DK)</option>
<option value="DJ">Djibouti (DJ)</option>
<option value="DM">Dominica (DM)</option>
<option value="DO">Dominican Republic (DO)</option>
<option value="EC">Ecuador (EC)</option>
<option value="EG">Egypt (EG)</option>
<option value="SV">El Salvador (SV)</option>
<option value="GQ">Equatorial Guinea (GQ)</option>
<option value="ER">Eritrea (ER)</option>
<option value="EE">Estonia (EE)</option>
<option value="SZ">Eswatini (SZ)</option>
<option value="ET">Ethiopia (ET)</option>
<option value="FJ">Fiji (FJ)</option>
<option value="FI">Finland (FI)</option>
<option value="FR">France (FR)</option>
<option value="GA">Gabon (GA)</option>
<option value="GM">Gambia (GM)</option>
<option value="GE">Georgia (GE)</option>
<option value="DE">Germany (DE)</option>
<option value="GH">Ghana (GH)</option>
<option value="GR">Greece (GR)</option>
<option value="GD">Grenada (GD)</option>
<option value="GT">Guatemala (GT)</option>
<option value="GN">Guinea (GN)</option>
<option value="GW">Guinea-Bissau (GW)</option>
<option value="GY">Guyana (GY)</option>
<option value="HT">Haiti (HT)</option>
<option value="HN">Honduras (HN)</option>
<option value="HU">Hungary (HU)</option>
<option value="IS">Iceland (IS)</option>
<option value="IN">India (IN)</option>
<option value="ID">Indonesia (ID)</option>
<option value="IR">Iran (IR)</option>
<option value="IQ">Iraq (IQ)</option>
<option value="IE">Ireland (IE)</option>
<option value="IL">Israel (IL)</option>
<option value="IT">Italy (IT)</option>
<option value="JM">Jamaica (JM)</option>
<option value="JP">Japan (JP)</option>
<option value="JO">Jordan (JO)</option>
<option value="KZ">Kazakhstan (KZ)</option>
<option value="KE">Kenya (KE)</option>
<option value="KI">Kiribati (KI)</option>
<option value="KP">North Korea (KP)</option>
<option value="KR">South Korea (KR)</option>
<option value="KW">Kuwait (KW)</option>
<option value="KG">Kyrgyzstan (KG)</option>
<option value="LA">Laos (LA)</option>
<option value="LV">Latvia (LV)</option>
<option value="LB">Lebanon (LB)</option>
<option value="LS">Lesotho (LS)</option>
<option value="LR">Liberia (LR)</option>
<option value="LY">Libya (LY)</option>
<option value="LI">Liechtenstein (LI)</option>
<option value="LT">Lithuania (LT)</option>
<option value="LU">Luxembourg (LU)</option>
<option value="MG">Madagascar (MG)</option>
<option value="MW">Malawi (MW)</option>
<option value="MY">Malaysia (MY)</option>
<option value="MV">Maldives (MV)</option>
<option value="ML">Mali (ML)</option>
<option value="MT">Malta (MT)</option>
<option value="MH">Marshall Islands (MH)</option>
<option value="MR">Mauritania (MR)</option>
<option value="MU">Mauritius (MU)</option>
<option value="MX">Mexico (MX)</option>
<option value="FM">Micronesia (FM)</option>
<option value="LOTR">Middle-earth (LOTR)</option>
<option value="MD">Moldova (MD)</option>
<option value="MC">Monaco (MC)</option>
<option value="MN">Mongolia (MN)</option>
<option value="ME">Montenegro (ME)</option>
<option value="MA">Morocco (MA)</option>
<option value="MZ">Mozambique (MZ)</option>
<option value="MM">Myanmar (MM)</option>
<option value="NA">Namibia (NA)</option>
<option value="NR">Nauru (NR)</option>
<option value="NP">Nepal (NP)</option>
<option value="NL">Netherlands (NL)</option>
<option value="NZ">New Zealand (NZ)</option>
<option value="NI">Nicaragua (NI)</option>
<option value="NE">Niger (NE)</option>
<option value="NG">Nigeria (NG)</option>
<option value="NO">Norway (NO)</option>
<option value="OM">Oman (OM)</option>
<option value="PK">Pakistan (PK)</option>
<option value="PW">Palau (PW)</option>
<option value="PS">Palestine (PS)</option>
<option value="PA">Panama (PA)</option>
<option value="PG">Papua New Guinea (PG)</option>
<option value="PY">Paraguay (PY)</option>
<option value="PE">Peru (PE)</option>
<option value="PH">Philippines (PH)</option>
<option value="PL">Poland (PL)</option>
<option value="PT">Portugal (PT)</option>
<option value="QA">Qatar (QA)</option>
<option value="RO">Romania (RO)</option>
<option value="RU">Russia (RU)</option>
<option value="RW">Rwanda (RW)</option>
<option value="KN">Saint Kitts and Nevis (KN)</option>
<option value="LC">Saint Lucia (LC)</option>
<option value="VC">Saint Vincent and the Grenadines (VC)</option>
<option value="WS">Samoa (WS)</option>
<option value="SM">San Marino (SM)</option>
<option value="ST">Sao Tome and Principe (ST)</option>
<option value="SA">Saudi Arabia (SA)</option>
<option value="SN">Senegal (SN)</option>
<option value="RS">Serbia (RS)</option>
<option value="SC">Seychelles (SC)</option>
<option value="SL">Sierra Leone (SL)</option>
<option value="SG">Singapore (SG)</option>
<option value="SK">Slovakia (SK)</option>
<option value="SI">Slovenia (SI)</option>
<option value="SB">Solomon Islands (SB)</option>
<option value="SO">Somalia (SO)</option>
<option value="ZA">South Africa (ZA)</option>
<option value="SS">South Sudan (SS)</option>
<option value="ES">Spain (ES)</option>
<option value="LK">Sri Lanka (LK)</option>
<option value="SD">Sudan (SD)</option>
<option value="SR">Suriname (SR)</option>
<option value="SE">Sweden (SE)</option>
<option value="CH">Switzerland (CH)</option>
<option value="SY">Syria (SY)</option>
<option value="TW">Taiwan (TW)</option>
<option value="TJ">Tajikistan (TJ)</option>
<option value="TZ">Tanzania (TZ)</option>
<option value="TH">Thailand (TH)</option>
<option value="TL">Timor-Leste (TL)</option>
<option value="TG">Togo (TG)</option>
<option value="TO">Tonga (TO)</option>
<option value="TT">Trinidad and Tobago (TT)</option>
<option value="TN">Tunisia (TN)</option>
<option value="TR">Turkey (TR)</option>
<option value="TM">Turkmenistan (TM)</option>
<option value="TV">Tuvalu (TV)</option>
<option value="UG">Uganda (UG)</option>
<option value="UA">Ukraine (UA)</option>
<option value="AE">United Arab Emirates (AE)</option>
<option value="GB">United Kingdom (GB)</option>
<option value="US">United States (US)</option>
<option value="UY">Uruguay (UY)</option>
<option value="UZ">Uzbekistan (UZ)</option>
<option value="VU">Vanuatu (VU)</option>
<option value="VE">Venezuela (VE)</option>
<option value="VN">Vietnam (VN)</option>
<option value="YE">Yemen (YE)</option>
<option value="ZM">Zambia (ZM)</option>
<option value="ZW">Zimbabwe (ZW)</option>
</select>
</div>
</fieldset>
<!-- SMTP Configuration Group -->
<fieldset class="border p-3 rounded mb-4">
<legend class="w-auto px-2" data-i18n="settings.smtp">SMTP Configuration</legend>
<div class="mb-3">
<label for="smtpHost" class="form-label" data-i18n="settings.smtp_host">SMTP Host</label>
<input type="text" class="form-control" id="smtpHost"
data-i18n-placeholder="settings.smtp_host_placeholder" placeholder="e.g., smtp.gmail.com" required />
</div>
<label for="smtpPort" data-i18n="settings.smtp_port">SMTP Port</label>
<select id="smtpPort" class="form-select">
<option value="587" selected>587 (Recommended - STARTTLS)</option>
<option value="465" disabled>465 (Not Supported)</option>
</select>
<div class="mb-3">
<label for="smtpUsername" class="form-label" data-i18n="settings.smtp_username">SMTP Username</label>
<input type="text" class="form-control" id="smtpUsername"
data-i18n-placeholder="settings.smtp_username_placeholder" placeholder="e.g., user@example.com" required />
</div>
<div class="mb-3">
<label for="smtpPassword" class="form-label" data-i18n="settings.smtp_password">SMTP Password</label>
<input type="password" class="form-control" id="smtpPassword"
data-i18n-placeholder="settings.smtp_password_placeholder" placeholder="Enter SMTP Password" required />
</div>
<div class="mb-3">
<label for="smtpFrom" class="form-label" data-i18n="settings.smtp_sender">Sender Email</label>
<input type="email" class="form-control" id="smtpFrom"
data-i18n-placeholder="settings.smtp_sender_placeholder" placeholder="noreply@swissmakers.ch" required />
</div>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="smtpUseTLS">
<label for="smtpUseTLS" class="form-check-label" data-i18n="settings.smtp_tls">Use TLS (Recommended)</label>
</div>
<button type="button" class="btn btn-secondary mt-2" onclick="sendTestEmail()" data-i18n="settings.send_test_email">Send Test Email</button>
</fieldset>
<!-- Fail2Ban Configuration Group -->
<fieldset class="border p-3 rounded mb-4">
<legend class="w-auto px-2" data-i18n="settings.fail2ban">Fail2Ban Configuration</legend>
<!-- Bantime Increment -->
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="bantimeIncrement" />
<label for="bantimeIncrement" class="form-check-label" data-i18n="settings.enable_bantime_increment">Enable Bantime Increment</label>
</div>
<!-- Bantime -->
<div class="mb-3">
<label for="banTime" class="form-label" data-i18n="settings.default_bantime">Default Bantime</label>
<input type="text" class="form-control" id="banTime"
data-i18n-placeholder="settings.default_bantime_placeholder" placeholder="e.g., 48h" />
</div>
<!-- Findtime -->
<div class="mb-3">
<label for="findTime" class="form-label" data-i18n="settings.default_findtime">Default Findtime</label>
<input type="text" class="form-control" id="findTime"
data-i18n-placeholder="settings.default_findtime_placeholder" placeholder="e.g., 30m" />
</div>
<!-- Max Retry -->
<div class="mb-3">
<label for="maxRetry" class="form-label" data-i18n="settings.default_max_retry">Default Max Retry</label>
<input type="number" class="form-control" id="maxRetry"
data-i18n-placeholder="settings.default_max_retry_placeholder" placeholder="Enter maximum retries" />
</div>
<!-- Ignore IPs -->
<div class="mb-3">
<label for="ignoreIP" class="form-label" data-i18n="settings.ignore_ips">Ignore IPs</label>
<textarea class="form-control" id="ignoreIP" rows="2"
data-i18n-placeholder="settings.ignore_ips_placeholder" placeholder="IPs to ignore, separated by spaces"></textarea>
</div>
</fieldset>
<button type="submit" class="btn btn-primary" data-i18n="settings.save">Save</button>
</form>
</div>
<!-- ******************************************************************* -->
<!-- Footer -->
<footer class="text-center mt-4 mb-4">
<p class="mb-0">
&copy; <a href="https://swissmakers.ch" target="_blank">Swissmakers GmbH</a>
-
<a href="https://github.com/swissmakers/fail2ban-ui" target="_blank">GitHub</a>
</p>
</footer>
<!-- ******************************************************************* -->
<!-- APP Components (HTML) : -->
<!-- ******************************************************************* -->
<!-- Loading Overlay -->
<div id="loading-overlay" class="d-flex">
<div class="spinner-border text-light" role="status">
<span class="visually-hidden" data-i18n="loading">Loading...</span>
</div>
</div>
<!-- Jail Config Modal -->
<div class="modal fade" id="jailConfigModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
<span data-i18n="modal.filter_config">Filter Config:</span> <span id="modalJailName"></span>
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<textarea id="jailConfigTextarea" class="form-control" rows="15"></textarea>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" data-i18n="modal.cancel">Cancel</button>
<button type="button" class="btn btn-primary" onclick="saveJailConfig()" data-i18n="modal.save">Save</button>
</div>
</div>
</div>
</div>
<!-- Manage Jails Modal -->
<div class="modal fade" id="manageJailsModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" data-i18n="modal.manage_jails_title">Manage Jails</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<!-- Dynamically filled list of jails with toggle switches -->
<div id="jailsList"></div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" data-i18n="modal.cancel">Cancel</button>
<button type="button" class="btn btn-primary" onclick="saveManageJails()" data-i18n="modal.save">Save Changes</button>
</div>
</div>
</div>
</div>
<!-- ******************************************************************* -->
<!-- Bootstrap 5 JS (for modal, etc.) -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js"></script>
<!-- jQuery (used by Select2) -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<!-- Select2 JS -->
<script src="https://cdn.jsdelivr.net/npm/select2@4.0.13/dist/js/select2.min.js"></script>
<script>
// For information: We avoid ES6 backticks in our JS, to prevent confusion with the Go template parser.
"use strict";
//*******************************************************************
//* Init page and main-components : *
//*******************************************************************
var currentJailForConfig = null;
window.addEventListener('DOMContentLoaded', function() {
showLoading(true);
checkRestartNeeded();
fetchSummary().then(function() {
showLoading(false);
initializeTooltips(); // Initialize tooltips after fetching and rendering
getTranslationsSettingsOnPageload();
});
});
// Function to initialize Bootstrap tooltips
function initializeTooltips() {
const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
tooltipTriggerList.forEach(function (tooltipTriggerEl) {
new bootstrap.Tooltip(tooltipTriggerEl);
});
}
// Toggle the loading overlay (with !important)
function showLoading(show) {
var overlay = document.getElementById('loading-overlay');
if (show) {
overlay.style.setProperty('display', 'flex', 'important');
} else {
overlay.style.setProperty('display', 'none', 'important');
}
}
// Check if there is still a reload of the fail2ban service needed
function checkRestartNeeded() {
fetch('/api/settings')
.then(res => res.json())
.then(data => {
if (data.restartNeeded) {
document.getElementById('restartBanner').style.display = 'block';
} else {
document.getElementById('restartBanner').style.display = 'none';
}
})
.catch(err => console.error('Error checking restartNeeded:', err));
}
// Load dynamically the other pages when navigating in nav
function showSection(sectionId) {
// hide all sections
document.getElementById('dashboardSection').style.display = 'none';
document.getElementById('filterSection').style.display = 'none';
document.getElementById('settingsSection').style.display = 'none';
// show the requested section
document.getElementById(sectionId).style.display = 'block';
// If it's filterSection, load filters
if (sectionId === 'filterSection') {
showFilterSection();
}
// If it's settingsSection, load settings
if (sectionId === 'settingsSection') {
loadSettings();
}
// Close navbar on mobile when clicking a menu item
let navbar = document.getElementById('navbarSupportedContent');
if (navbar.classList.contains('show')) {
let navbarToggler = document.querySelector('.navbar-toggler');
navbarToggler.click();
}
}
//*******************************************************************
//* Fetch data and render dashboard : *
//*******************************************************************
// Fetch summary (jails, stats, last bans)
function fetchSummary() {
return fetch('/api/summary')
.then(function(res) { return res.json(); })
.then(function(data) {
if (data.error) {
document.getElementById('dashboard').innerHTML =
'<div class="alert alert-danger">' + data.error + '</div>';
return;
}
renderDashboard(data);
})
.catch(function(err) {
document.getElementById('dashboard').innerHTML =
'<div class="alert alert-danger">Error: ' + err + '</div>';
});
}
// Render the main dashboard
function renderDashboard(data) {
var html = "";
// Add a search bar
html += `
<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." data-i18n="dashboard.overview">Overview active Jails and Blocks</span></legend>
<div class="mb-3">
<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" data-i18n-placeholder="dashboard.search_placeholder" onkeyup="filterIPs()">
</div>
`;
// Jails table
if (!data.jails || data.jails.length === 0) {
html += '<p data-i18n="dashboard.no_jails">No jails found.</p>';
} else {
html += ''
+ '<div class="table-responsive">'
+ '<table class="table table-striped" id="jailsTable">'
+ ' <thead>'
+ ' <tr>'
+ ' <th data-i18n="dashboard.table.jail_name">Jail Name</th>'
+ ' <th data-i18n="dashboard.table.total_banned">Total Banned</th>'
+ ' <th data-i18n="dashboard.table.new_last_hour">New Last Hour</th>'
+ ' <th data-i18n="dashboard.table.banned_ips">Banned IPs (Unban)</th>'
+ ' </tr>'
+ ' </thead>'
+ ' <tbody>';
data.jails.forEach(function(jail) {
var bannedHTML = renderBannedIPs(jail.jailName, jail.bannedIPs);
html += ''
+ '<tr class="jail-row">'
+ ' <td>'
+ ' <a href="#" onclick="openJailConfigModal(\'' + jail.jailName + '\')">'
+ jail.jailName
+ ' </a>'
+ ' </td>'
+ ' <td>' + jail.totalBanned + '</td>'
+ ' <td>' + jail.newInLastHour + '</td>'
+ ' <td>' + bannedHTML + '</td>'
+ '</tr>';
});
html += '</tbody></table>';
html += '</div></fieldset>';
}
// Last 5 bans
html += '<fieldset class="border p-3 rounded mb-4">';
html += ' <legend class="w-auto px-2" data-i18n="dashboard.last_bans">Last 5 Ban Events</legend>';
if (!data.lastBans || data.lastBans.length === 0) {
html += '<p data-i18n="dashboard.no_recent_bans">No recent bans found.</p>';
} else {
html += ''
+ '<div class="table-responsive">'
+ '<table class="table table-bordered">'
+ ' <thead>'
+ ' <tr>'
+ ' <th data-i18n="dashboard.table.time">Time</th>'
+ ' <th data-i18n="dashboard.table.jail">Jail</th>'
+ ' <th data-i18n="dashboard.table.ip">IP</th>'
+ ' <th data-i18n="dashboard.table.log_line">Log Line</th>'
+ ' </tr>'
+ ' </thead>'
+ ' <tbody></div>';
data.lastBans.forEach(function(e) {
html += ''
+ '<tr>'
+ ' <td>' + e.Time + '</td>'
+ ' <td>' + e.Jail + '</td>'
+ ' <td>' + e.IP + '</td>'
+ ' <td>' + e.LogLine + '</td>'
+ '</tr>';
});
html += '</tbody></table></fieldset>';
}
document.getElementById('dashboard').innerHTML = html;
}
// Render banned IPs with "Unban" button
function renderBannedIPs(jailName, ips) {
if (!ips || ips.length === 0) {
return '<em data-i18n="dashboard.no_banned_ips">No banned IPs</em>';
}
var content = '<ul class="list-unstyled mb-0">';
ips.forEach(function(ip) {
content += ''
+ '<li class="d-flex align-items-center mb-1">'
+ ' <span class="me-auto">' + ip + '</span>'
+ ' <button class="btn btn-sm btn-warning"'
+ ' onclick="unbanIP(\'' + jailName + '\', \'' + ip + '\')">'
+ ' <span data-i18n="dashboard.unban">Unban</span>'
+ ' </button>'
+ '</li>';
});
content += '</ul>';
return content;
}
// Filter IPs on dashboard table
function filterIPs() {
const query = document.getElementById("ipSearch").value.toLowerCase();
const rows = document.querySelectorAll("#jailsTable .jail-row");
rows.forEach((row) => {
const ipSpans = row.querySelectorAll("ul li span");
let matchFound = false;
ipSpans.forEach((span) => {
const originalText = span.textContent;
const ipText = originalText.toLowerCase();
if (query && ipText.includes(query)) {
matchFound = true;
const highlightedText = originalText.replace(
new RegExp(query, "gi"),
(match) => `<mark>${match}</mark>`
);
span.innerHTML = highlightedText;
} else {
span.innerHTML = originalText;
}
});
row.style.display = matchFound || !query ? "" : "none";
});
}
//*******************************************************************
//* Functions to manage IP-bans : *
//*******************************************************************
function unbanIP(jail, ip) {
if (!confirm("Unban IP " + ip + " from jail " + jail + "?")) {
return;
}
showLoading(true);
fetch('/api/jails/' + jail + '/unban/' + ip, { method: 'POST' })
.then(function(res) { return res.json(); })
.then(function(data) {
if (data.error) {
alert("Error: " + data.error);
} else {
alert(data.message || "IP unbanned successfully");
}
return fetchSummary();
})
.catch(function(err) {
alert("Error: " + err);
})
.finally(function() {
showLoading(false);
});
}
//*******************************************************************
//* Filter-mod and config-mod actions : *
//*******************************************************************
function openJailConfigModal(jailName) {
currentJailForConfig = jailName;
var textArea = document.getElementById('jailConfigTextarea');
textArea.value = '';
document.getElementById('modalJailName').textContent = jailName;
showLoading(true);
fetch('/api/jails/' + jailName + '/config')
.then(function(res) { return res.json(); })
.then(function(data) {
if (data.error) {
alert("Error loading config: " + data.error);
} else {
textArea.value = data.config;
var modalEl = document.getElementById('jailConfigModal');
var myModal = new bootstrap.Modal(modalEl);
myModal.show();
}
})
.catch(function(err) {
alert("Error: " + err);
})
.finally(function() {
showLoading(false);
});
}
function saveJailConfig() {
if (!currentJailForConfig) return;
showLoading(true);
var newConfig = document.getElementById('jailConfigTextarea').value;
fetch('/api/jails/' + currentJailForConfig + '/config', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ config: newConfig }),
})
.then(function(res) { return res.json(); })
.then(function(data) {
if (data.error) {
alert("Error saving config: " + data.error);
} else {
console.log("Filter saved successfully. Restart needed? " + data.restartNeeded);
var modalEl = document.getElementById('jailConfigModal');
var modalObj = bootstrap.Modal.getInstance(modalEl);
modalObj.hide();
document.getElementById('restartBanner').style.display = 'block';
}
})
.catch(function(err) {
alert("Error: " + err);
})
.finally(function() {
showLoading(false);
});
}
// Function: openManageJailsModal
// Fetches the full-list of all jails (from /jails/manage) and builds a list with toggle switches.
function openManageJailsModal() {
showLoading(true);
fetch('/api/jails/manage')
.then(function(res) { return res.json(); })
.then(function(data) {
if (!data.jails || data.jails.length === 0) {
alert("No jails found.");
showLoading(false);
return;
}
var html = '<div class="list-group">';
data.jails.forEach(function(jail) {
var isEnabled = (jail.enabled === true);
html += '<div class="list-group-item d-flex justify-content-between align-items-center">';
html += '<span>' + jail.jailName + '</span>';
html += '<div class="form-check form-switch">';
html += '<input class="form-check-input" type="checkbox" id="toggle-' + jail.jailName + '" ' + (isEnabled ? 'checked' : '') + '>';
html += '</div>';
html += '</div>';
});
html += '</div>';
document.getElementById('jailsList').innerHTML = html;
var modalEl = document.getElementById('manageJailsModal');
var manageJailsModal = new bootstrap.Modal(modalEl);
manageJailsModal.show();
})
.catch(function(err) {
alert("Error fetching jails: " + err);
})
.finally(function() {
showLoading(false);
});
}
// Function: saveManageJails
// Collects the toggled states from the Manage Jails modal and sends updates to the API.
function saveManageJails() {
showLoading(true);
var updatedJails = {};
$('.list-group-item').each(function() {
var jailName = $(this).find('span').text();
var isEnabled = $(this).find('input[type="checkbox"]').is(':checked');
updatedJails[jailName] = isEnabled;
});
// Send updated states to the API endpoint /api/jails/manage.
fetch('/api/jails/manage', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(updatedJails),
})
.then(function(res) { return res.json(); })
.then(function(data) {
if (data.error) {
alert("Error saving jail settings: " + data.error);
} else {
// A restart of fail2ban is needed, to enable or disable jails - a reload is not enough
document.getElementById('restartBanner').style.display = 'block';
}
})
.catch(function(err) {
alert("Error: " + err);
})
.finally(function() {
showLoading(false);
var modalEl = document.getElementById('manageJailsModal');
var manageJailsModal = bootstrap.Modal.getInstance(modalEl);
if (manageJailsModal) {
manageJailsModal.hide();
}
});
}
//*******************************************************************
//* Load current settings when opening settings page : *
//*******************************************************************
function loadSettings() {
showLoading(true);
fetch('/api/settings')
.then(res => res.json())
.then(data => {
document.getElementById('languageSelect').value = data.language || 'en';
document.getElementById('uiPort').value = data.port || 8080,
document.getElementById('debugMode').checked = data.debug || false;
document.getElementById('destEmail').value = data.destemail || '';
const select = document.getElementById('alertCountries');
for (let i = 0; i < select.options.length; i++) {
select.options[i].selected = false;
}
if (!data.alertCountries || data.alertCountries.length === 0) {
select.options[0].selected = true;
} else {
for (let i = 0; i < select.options.length; i++) {
let val = select.options[i].value;
if (data.alertCountries.includes(val)) {
select.options[i].selected = true;
}
}
}
$('#alertCountries').trigger('change');
if (data.smtp) {
document.getElementById('smtpHost').value = data.smtp.host || '';
document.getElementById('smtpPort').value = data.smtp.port || 587;
document.getElementById('smtpUsername').value = data.smtp.username || '';
document.getElementById('smtpPassword').value = data.smtp.password || '';
document.getElementById('smtpFrom').value = data.smtp.from || '';
document.getElementById('smtpUseTLS').checked = data.smtp.useTLS || false;
}
document.getElementById('bantimeIncrement').checked = data.bantimeIncrement || false;
document.getElementById('banTime').value = data.bantime || '';
document.getElementById('findTime').value = data.findtime || '';
document.getElementById('maxRetry').value = data.maxretry || '';
document.getElementById('ignoreIP').value = data.ignoreip || '';
})
.catch(err => {
alert('Error loading settings: ' + err);
})
.finally(() => showLoading(false));
}
//*******************************************************************
//* Save settings when hitting the save button : *
//*******************************************************************
function saveSettings(event) {
event.preventDefault();
showLoading(true);
const smtpSettings = {
host: document.getElementById('smtpHost').value.trim(),
port: parseInt(document.getElementById('smtpPort').value, 10) || 587,
username: document.getElementById('smtpUsername').value.trim(),
password: document.getElementById('smtpPassword').value.trim(),
from: document.getElementById('smtpFrom').value.trim(),
useTLS: document.getElementById('smtpUseTLS').checked,
};
const selectedCountries = Array.from(document.getElementById('alertCountries').selectedOptions).map(opt => opt.value);
const settingsData = {
language: document.getElementById('languageSelect').value,
port: parseInt(document.getElementById('uiPort').value, 10) || 8080,
debug: document.getElementById('debugMode').checked,
destemail: document.getElementById('destEmail').value.trim(),
alertCountries: selectedCountries.length > 0 ? selectedCountries : ["ALL"],
bantimeIncrement: document.getElementById('bantimeIncrement').checked,
bantime: document.getElementById('banTime').value.trim(),
findtime: document.getElementById('findTime').value.trim(),
maxretry: parseInt(document.getElementById('maxRetry').value, 10) || 3,
ignoreip: document.getElementById('ignoreIP').value.trim(),
smtp: smtpSettings
};
fetch('/api/settings', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(settingsData),
})
.then(res => res.json())
.then(data => {
if (data.error) {
alert('Error saving settings: ' + data.error + data.details);
} else {
var selectedLang = $('#languageSelect').val();
loadTranslations(selectedLang);
console.log("Settings saved successfully. Restart needed? " + data.restartNeeded);
if (data.restartNeeded) {
document.getElementById('restartBanner').style.display = 'block';
}
}
})
.catch(err => alert('Error: ' + err))
.finally(() => showLoading(false));
}
//*******************************************************************
//* Load the list of filters from /api/filters : *
//*******************************************************************
function loadFilters() {
showLoading(true);
fetch('/api/filters')
.then(res => res.json())
.then(data => {
if (data.error) {
alert('Error loading filters: ' + data.error);
return;
}
const select = document.getElementById('filterSelect');
select.innerHTML = '';
if (!data.filters || data.filters.length === 0) {
const opt = document.createElement('option');
opt.value = '';
opt.textContent = 'No Filters Found';
select.appendChild(opt);
} else {
data.filters.forEach(f => {
const opt = document.createElement('option');
opt.value = f;
opt.textContent = f;
select.appendChild(opt);
});
}
})
.catch(err => {
alert('Error loading filters: ' + err);
})
.finally(() => showLoading(false));
}
function sendTestEmail() {
showLoading(true);
fetch('/api/settings/test-email', {
method: 'POST',
headers: { 'Content-Type': 'application/json' }
})
.then(res => res.json())
.then(data => {
if (data.error) {
alert('Error sending test email: ' + data.error);
} else {
alert('Test email sent successfully!');
}
})
.catch(error => alert('Error: ' + error))
.finally(() => showLoading(false));
}
// Called when clicking "Test Filter" button
function testSelectedFilter() {
const filterName = document.getElementById('filterSelect').value;
const lines = document.getElementById('logLinesTextarea').value.split('\n');
if (!filterName) {
alert('Please select a filter.');
return;
}
showLoading(true);
fetch('/api/filters/test', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
filterName: filterName,
logLines: lines
})
})
.then(res => res.json())
.then(data => {
if (data.error) {
alert('Error: ' + data.error);
return;
}
renderTestResults(data.matches);
})
.catch(err => {
alert('Error: ' + err);
})
.finally(() => showLoading(false));
}
function renderTestResults(matches) {
let html = '<h5 data-i18n="filter_debug.test_results_title">Test Results</h5>';
if (!matches || matches.length === 0) {
html += '<p data-i18n="filter_debug.no_matches">No matches found.</p>';
} else {
html += '<ul>';
matches.forEach(m => {
html += '<li>' + m + '</li>';
});
html += '</ul>';
}
document.getElementById('testResults').innerHTML = html;
}
// When showing the filter section
function showFilterSection() {
loadFilters();
document.getElementById('testResults').innerHTML = '';
document.getElementById('logLinesTextarea').value = '';
}
//*******************************************************************
//* Restart fail2ban action : *
//*******************************************************************
function restartFail2ban() {
if (!confirm("Keep in mind that while fail2ban is restarting, logs are not being parsed and no IP addresses are blocked. Restart fail2ban now? This will take some time.")) return;
showLoading(true);
fetch('/api/fail2ban/restart', { method: 'POST' })
.then(function(res) { return res.json(); })
.then(function(data) {
if (data.error) {
alert("Error: " + data.error);
} else {
document.getElementById('restartBanner').style.display = 'none';
return fetchSummary();
}
})
.catch(function(err) {
alert("Error: " + err);
})
.finally(function() {
showLoading(false);
});
}
//*******************************************************************
//* Is executed when doc-ready : *
//*******************************************************************
$(document).ready(function() {
$('#alertCountries').select2({
placeholder: 'Select countries..',
allowClear: true,
width: '100%'
});
$('#alertCountries').on('select2:select', function(e) {
var selectedValue = e.params.data.id;
var currentValues = $('#alertCountries').val() || [];
if (selectedValue === 'ALL') {
if (currentValues.length > 1) {
$('#alertCountries').val(['ALL']).trigger('change');
}
} else {
if (currentValues.indexOf('ALL') !== -1) {
var newValues = currentValues.filter(function(value) {
return value !== 'ALL';
});
$('#alertCountries').val(newValues).trigger('change');
}
}
});
});
//*******************************************************************
//* 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>
</html>