Files
fail2ban-ui/pkg/web/templates/index.html
2025-01-27 13:21:45 +01:00

791 lines
26 KiB
HTML

<!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>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"
/>
<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;
}
/* Reload banner */
#reloadBanner {
display: none;
}
</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')">Dashboard</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#" onclick="showSection('filterSection')">Filter Debug</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#" onclick="showSection('settingsSection')">Settings</a>
</li>
</ul>
</div>
</div>
</nav>
<!-- ******************************************************************* -->
<!-- Reload Banner -->
<div id="reloadBanner" class="bg-warning text-dark p-3 text-center">
<strong>Configuration changed! </strong>
<button class="btn btn-dark" onclick="reloadFail2ban()">
Reload Fail2ban
</button>
</div>
<!-- ******************************************************************* -->
<!-- APP Sections (Pages) : -->
<!-- ******************************************************************* -->
<!-- Dashboard Section -->
<div id="dashboardSection" class="container my-4">
<h1 class="mb-4">Dashboard</h1>
<div id="dashboard"></div>
</div>
<!-- Filter Debug Section -->
<div id="filterSection" style="display: none;" class="container my-4">
<h2>Filter Debug</h2>
<!-- Dropdown of available jail/filters -->
<div class="mb-3">
<label for="filterSelect" class="form-label">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">Log Lines</label>
<textarea id="logLinesTextarea" class="form-control" rows="6" disabled></textarea>
</div>
<button class="btn btn-secondary" onclick="testSelectedFilter()">Test Filter</button>
<hr/>
<div id="testResults"></div>
</div>
<!-- Settings Section -->
<div id="settingsSection" style="display: none;" class="container my-4">
<h2>Settings</h2>
<form onsubmit="saveSettings(event)">
<!-- General Settings Group -->
<fieldset class="border p-3 rounded mb-4">
<legend class="w-auto px-2">General Settings</legend>
<!-- Language Selection -->
<div class="mb-3">
<label for="languageSelect" class="form-label">Language</label>
<select id="languageSelect" class="form-select" disabled>
<option value="en">English</option>
<option value="de">Deutsch</option>
</select>
</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">Enable Debug Log</label>
</div>
</fieldset>
<!-- Alert Settings Group -->
<fieldset class="border p-3 rounded mb-4">
<legend class="w-auto px-2">Alert Settings</legend>
<!-- Source Email -->
<div class="mb-3">
<label for="sourceEmail" class="form-label">Source Email - Emails are sent from this address.</label>
<input type="email" class="form-control" id="sourceEmail"/>
</div>
<!-- Destination Email -->
<div class="mb-3">
<label for="destEmail" class="form-label">Destination Email - Where to sent the alert messages?</label>
<input type="email" class="form-control" id="destEmail" placeholder="e.g., alerts@swissmakers.ch" />
</div>
<!-- Alert Countries -->
<div class="mb-3">
<label for="alertCountries" class="form-label">Select alert Countries</label>
<p class="text-muted">Choose which country IP blocks should trigger an email. You can select multiple with CTRL.</p>
<select id="alertCountries" class="form-select" multiple size="7">
<option value="ALL">ALL (Every Country)</option>
<option value="CH">Switzerland (CH)</option>
<option value="DE">Germany (DE)</option>
<option value="IT">Italy (IT)</option>
<option value="FR">France (FR)</option>
<option value="UK">England (UK)</option>
<option value="US">United States (US)</option>
<!-- Maybe i will add more later.. -->
</select>
</div>
</fieldset>
<!-- Fail2Ban Configuration Group -->
<fieldset class="border p-3 rounded mb-4">
<legend class="w-auto px-2">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">Enable Bantime Increment</label>
</div>
<!-- Bantime -->
<div class="mb-3">
<label for="banTime" class="form-label">Default Bantime</label>
<input type="text" class="form-control" id="banTime" placeholder="e.g., 48h" />
</div>
<!-- Findtime -->
<div class="mb-3">
<label for="findTime" class="form-label">Default Findtime</label>
<input type="text" class="form-control" id="findTime" placeholder="e.g., 30m" />
</div>
<!-- Max Retry -->
<div class="mb-3">
<label for="maxRetry" class="form-label">Default Max Retry</label>
<input type="number" class="form-control" id="maxRetry" placeholder="Enter maximum retries" />
</div>
<!-- Ignore IPs -->
<div class="mb-3">
<label for="ignoreIP" class="form-label">Ignore IPs</label>
<textarea class="form-control" id="ignoreIP" rows="2" placeholder="Enter IPs to ignore, separated by spaces"></textarea>
</div>
</fieldset>
<button type="submit" class="btn btn-primary">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">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">
Filter Config: <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">Cancel</button>
<button type="button" class="btn btn-primary" onclick="saveJailConfig()">
Save
</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>
<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 : *
//*******************************************************************
// Init and run first function, when DOM is ready
var currentJailForConfig = null;
window.addEventListener('DOMContentLoaded', function() {
showLoading(true);
checkReloadNeeded();
fetchSummary().then(function() {
showLoading(false);
initializeTooltips(); // Initialize tooltips after fetching and rendering
});
});
// 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 checkReloadNeeded() {
fetch('/api/settings')
.then(res => res.json())
.then(data => {
if (data.reloadNeeded) {
document.getElementById('reloadBanner').style.display = 'block';
} else {
document.getElementById('reloadBanner').style.display = 'none';
}
})
.catch(err => console.error('Error checking reloadNeeded:', 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();
}
}
//*******************************************************************
//* Fetch data and render dashboard : *
//*******************************************************************
// Fetch summary (jails, stats, last 5 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 += `
<div class="mb-3">
<label for="ipSearch" class="form-label">Search Banned IPs</label>
<input type="text" id="ipSearch" class="form-control" placeholder="Enter IP address to search" onkeyup="filterIPs()">
</div>
`;
// Jails table
if (!data.jails || data.jails.length === 0) {
html += '<p>No jails found.</p>';
} else {
html += ''
+ '<h2><span data-bs-toggle="tooltip" data-bs-placement="top" title="The Overview displays the currently enabled jails that you have added to your jail.local configuration.">Overview</span></h2>'
+ '<table class="table table-striped" id="jailsTable">'
+ ' <thead>'
+ ' <tr>'
+ ' <th>Jail Name</th>'
+ ' <th>Total Banned</th>'
+ ' <th>New Last Hour</th>'
+ ' <th>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>';
}
// Last 5 bans
html += '<h2>Last 5 Ban Events</h2>';
if (!data.lastBans || data.lastBans.length === 0) {
html += '<p>No recent bans found.</p>';
} else {
html += ''
+ '<table class="table table-bordered">'
+ ' <thead>'
+ ' <tr>'
+ ' <th>Time</th>'
+ ' <th>Jail</th>'
+ ' <th>IP</th>'
+ ' <th>Log Line</th>'
+ ' </tr>'
+ ' </thead>'
+ ' <tbody>';
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>';
}
document.getElementById('dashboard').innerHTML = html;
}
// Render banned IPs with "Unban" button
function renderBannedIPs(jailName, ips) {
if (!ips || ips.length === 0) {
return '<em>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 + '\')">'
+ ' Unban'
+ ' </button>'
+ '</li>';
});
content += '</ul>';
return content;
}
// Filter IPs on dashboard table
function filterIPs() {
const query = document.getElementById("ipSearch").value.toLowerCase(); // Get the search query
const rows = document.querySelectorAll("#jailsTable .jail-row"); // Get all jail rows
rows.forEach((row) => {
const ipSpans = row.querySelectorAll("ul li span"); // Find all IP span elements in this row
let matchFound = false; // Reset match flag for the row
ipSpans.forEach((span) => {
const originalText = span.textContent; // The full original text
const ipText = originalText.toLowerCase();
if (query && ipText.includes(query)) {
matchFound = true; // Match found in this row
// Highlight the matching part
const highlightedText = originalText.replace(
new RegExp(query, "gi"), // Case-insensitive match
(match) => `<mark>${match}</mark>` // Wrap match in <mark>
);
span.innerHTML = highlightedText; // Update span's HTML with highlighting
} else {
// Remove highlighting if no match or search is cleared
span.innerHTML = originalText;
}
});
// Show the row if a match is found or the query is empty
row.style.display = matchFound || !query ? "" : "none";
});
}
//*******************************************************************
//* Functions to manage IP-bans : *
//*******************************************************************
// Unban IP
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 : *
//*******************************************************************
// Open jail/filter modal and load filter-config
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);
});
}
// Save filter config for the current opened jail
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 {
//alert(data.message || "Config saved");
console.log("Filter saved successfully. Reload needed? " + data.reloadNeeded);
// Hide modal
var modalEl = document.getElementById('jailConfigModal');
var modalObj = bootstrap.Modal.getInstance(modalEl);
modalObj.hide();
// Show the reload banner
document.getElementById('reloadBanner').style.display = 'block';
}
})
.catch(function(err) {
alert("Error: " + err);
})
.finally(function() {
showLoading(false);
});
}
// Load current settings when opening settings page
function loadSettings() {
showLoading(true);
fetch('/api/settings')
.then(res => res.json())
.then(data => {
// Get current general settings
document.getElementById('languageSelect').value = data.language || 'en';
document.getElementById('debugMode').checked = data.debug || false;
// Get current alert settings
document.getElementById('sourceEmail').value = data.sender || '';
document.getElementById('destEmail').value = data.destemail || '';
// alertCountries multi
const select = document.getElementById('alertCountries');
// clear selection
for (let i = 0; i < select.options.length; i++) {
select.options[i].selected = false;
}
if (!data.alertCountries || data.alertCountries.length === 0) {
// default to "ALL"
select.options[0].selected = true;
} else {
// Mark them selected
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;
}
}
}
// Get current Fail2Ban Configuration
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 hit the save button
function saveSettings(e) {
e.preventDefault(); // prevent form submission
showLoading(true);
const lang = document.getElementById('languageSelect').value;
const debugMode = document.getElementById("debugMode").checked;
const srcmail = document.getElementById('sourceEmail').value;
const destmail = document.getElementById('destEmail').value;
const select = document.getElementById('alertCountries');
let chosenCountries = [];
for (let i = 0; i < select.options.length; i++) {
if (select.options[i].selected) {
chosenCountries.push(select.options[i].value);
}
}
// If user selected "ALL", we override everything
if (chosenCountries.includes("ALL")) {
chosenCountries = ["all"];
}
const bantimeinc = document.getElementById('bantimeIncrement').checked;
const bant = document.getElementById('banTime').value;
const findt = document.getElementById('findTime').value;
const maxre = parseInt(document.getElementById('maxRetry').value, 10) || 1; // Default to 1 (if parsing fails)
const ignip = document.getElementById('ignoreIP').value;
const body = {
language: lang,
debug: debugMode,
sender: srcmail,
destemail: destmail,
alertCountries: chosenCountries,
bantimeIncrement: bantimeinc,
bantime: bant,
findtime: findt,
maxretry: maxre,
ignoreip: ignip
};
fetch('/api/settings', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(body)
})
.then(res => res.json())
.then(data => {
if (data.error) {
alert('Error saving settings: ' + data.error + data.details);
} else {
//alert(data.message || 'Settings saved');
console.log("Settings saved successfully. Reload needed? " + data.reloadNeeded);
if (data.reloadNeeded) {
document.getElementById('reloadBanner').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 = ''; // clear existing
if (!data.filters || data.filters.length === 0) {
// optional fallback
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));
}
// 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;
}
// data.matches, for example
renderTestResults(data.matches);
})
.catch(err => {
alert('Error: ' + err);
})
.finally(() => showLoading(false));
}
function renderTestResults(matches) {
let html = '<h5>Test Results</h5>';
if (!matches || matches.length === 0) {
html += '<p>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(); // fetch the filter list
document.getElementById('testResults').innerHTML = '';
document.getElementById('logLinesTextarea').value = '';
}
//*******************************************************************
//* Reload fail2ban action : *
//*******************************************************************
// Reload Fail2ban
function reloadFail2ban() {
if (!confirm("It can happen that some logs are not parsed during the reload of fail2ban. Reload Fail2ban now?")) return;
showLoading(true);
fetch('/api/fail2ban/reload', { method: 'POST' })
.then(function(res) { return res.json(); })
.then(function(data) {
if (data.error) {
alert("Error: " + data.error);
} else {
// Hide reload banner
document.getElementById('reloadBanner').style.display = 'none';
// Refresh data
return fetchSummary();
}
})
.catch(function(err) {
alert("Error: " + err);
})
.finally(function() {
showLoading(false);
});
}
</script>
</body>
</html>