mirror of
https://github.com/swissmakers/fail2ban-ui.git
synced 2026-04-17 14:03:15 +02:00
Create new views settings and debug
This commit is contained in:
@@ -1,17 +1,20 @@
|
|||||||
package web
|
package web
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/swissmakers/fail2ban-ui/internal/config"
|
||||||
"github.com/swissmakers/fail2ban-ui/internal/fail2ban"
|
"github.com/swissmakers/fail2ban-ui/internal/fail2ban"
|
||||||
)
|
)
|
||||||
|
|
||||||
// SummaryResponse is what we return from /api/summary
|
// SummaryResponse is what we return from /api/summary
|
||||||
type SummaryResponse struct {
|
type SummaryResponse struct {
|
||||||
Jails []fail2ban.JailInfo `json:"jails"`
|
Jails []fail2ban.JailInfo `json:"jails"`
|
||||||
LastBans []fail2ban.BanEvent `json:"lastBans"`
|
LastBans []fail2ban.BanEvent `json:"lastBans"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// SummaryHandler returns a JSON summary of all jails, including
|
// SummaryHandler returns a JSON summary of all jails, including
|
||||||
@@ -78,53 +81,117 @@ func sortByTimeDesc(events []fail2ban.BanEvent) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// IndexHandler serves the main HTML page
|
// IndexHandler serves the HTML page
|
||||||
func IndexHandler(c *gin.Context) {
|
func IndexHandler(c *gin.Context) {
|
||||||
c.HTML(http.StatusOK, "index.html", gin.H{
|
c.HTML(http.StatusOK, "index.html", gin.H{
|
||||||
"timestamp": time.Now().Format(time.RFC1123),
|
"timestamp": time.Now().Format(time.RFC1123),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetJailConfigHandler returns the raw config for a given jail
|
// GetJailFilterConfigHandler returns the raw filter config for a given jail
|
||||||
func GetJailConfigHandler(c *gin.Context) {
|
func GetJailFilterConfigHandler(c *gin.Context) {
|
||||||
jail := c.Param("jail")
|
jail := c.Param("jail")
|
||||||
cfg, err := fail2ban.GetJailConfig(jail)
|
cfg, err := fail2ban.GetJailConfig(jail)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"jail": jail,
|
"jail": jail,
|
||||||
"config": cfg,
|
"config": cfg,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetJailConfigHandler overwrites the jail config with new content
|
// SetJailFilterConfigHandler overwrites the current filter config with new content
|
||||||
func SetJailConfigHandler(c *gin.Context) {
|
func SetJailFilterConfigHandler(c *gin.Context) {
|
||||||
jail := c.Param("jail")
|
jail := c.Param("jail")
|
||||||
|
|
||||||
var req struct {
|
var req struct {
|
||||||
Config string `json:"config"`
|
Config string `json:"config"`
|
||||||
}
|
}
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid JSON body"})
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid JSON body"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := fail2ban.SetJailConfig(jail, req.Config); err != nil {
|
if err := fail2ban.SetJailConfig(jail, req.Config); err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{"message": "jail config updated"})
|
c.JSON(http.StatusOK, gin.H{"message": "jail config updated"})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSettingsHandler returns the current fail2ban-ui settings
|
||||||
|
func GetSettingsHandler(c *gin.Context) {
|
||||||
|
s := config.GetSettings()
|
||||||
|
c.JSON(http.StatusOK, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateSettingsHandler updates the fail2ban-ui settings
|
||||||
|
func UpdateSettingsHandler(c *gin.Context) {
|
||||||
|
// var req config.Settings
|
||||||
|
// if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
// c.JSON(http.StatusBadRequest, gin.H{"error": "invalid JSON"})
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
|
||||||
|
// needsRestart, err := config.UpdateSettings(req)
|
||||||
|
// if err != nil {
|
||||||
|
// c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"message": "Settings updated",
|
||||||
|
// "needsRestart": needsRestart,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListFiltersHandler returns a JSON array of filter names
|
||||||
|
// found as *.conf in /etc/fail2ban/filter.d
|
||||||
|
func ListFiltersHandler(c *gin.Context) {
|
||||||
|
dir := "/etc/fail2ban/filter.d" // adjust if needed
|
||||||
|
|
||||||
|
files, err := ioutil.ReadDir(dir)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{
|
||||||
|
"error": "Failed to read filter directory: " + err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var filters []string
|
||||||
|
for _, f := range files {
|
||||||
|
if !f.IsDir() && strings.HasSuffix(f.Name(), ".conf") {
|
||||||
|
name := strings.TrimSuffix(f.Name(), ".conf")
|
||||||
|
filters = append(filters, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"filters": filters})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFilterHandler(c *gin.Context) {
|
||||||
|
var req struct {
|
||||||
|
FilterName string `json:"filterName"`
|
||||||
|
LogLines []string `json:"logLines"`
|
||||||
|
}
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid JSON"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// For now, just pretend nothing matches
|
||||||
|
c.JSON(http.StatusOK, gin.H{"matches": []string{}})
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReloadFail2banHandler reloads the Fail2ban service
|
// ReloadFail2banHandler reloads the Fail2ban service
|
||||||
func ReloadFail2banHandler(c *gin.Context) {
|
func ReloadFail2banHandler(c *gin.Context) {
|
||||||
err := fail2ban.ReloadFail2ban()
|
err := fail2ban.ReloadFail2ban()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.JSON(http.StatusOK, gin.H{"message": "Fail2ban reloaded successfully"})
|
c.JSON(http.StatusOK, gin.H{"message": "Fail2ban reloaded successfully"})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,12 +33,31 @@
|
|||||||
<body class="bg-light">
|
<body class="bg-light">
|
||||||
<!-- NavBar -->
|
<!-- NavBar -->
|
||||||
<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>
|
||||||
</div>
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse"
|
||||||
</nav>
|
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 -->
|
<!-- 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">
|
||||||
@@ -48,11 +67,62 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="container my-4">
|
<!-- Dashboard Section -->
|
||||||
|
<div id="dashboardSection" class="container my-4">
|
||||||
<h1 class="mb-4">Dashboard</h1>
|
<h1 class="mb-4">Dashboard</h1>
|
||||||
<div id="dashboard"></div>
|
<div id="dashboard"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Settings Section -->
|
||||||
|
<div id="settingsSection" style="display: none;" class="container my-4">
|
||||||
|
<h2>Settings</h2>
|
||||||
|
<form onsubmit="saveSettings(event)">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="languageSelect" class="form-label">Language</label>
|
||||||
|
<select id="languageSelect" class="form-select">
|
||||||
|
<option value="en">English</option>
|
||||||
|
<option value="de">Deutsch</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="alertEmail" class="form-label">Alert Email</label>
|
||||||
|
<input type="email" class="form-control" id="alertEmail"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Alert Countries</label>
|
||||||
|
<small class="text-muted">(Choose which country bans trigger an email. "all" for everything.)</small>
|
||||||
|
<input type="text" class="form-control" id="alertCountries"
|
||||||
|
placeholder="e.g. all or DE,CH"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="btn btn-primary">Save</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Filter Debug Section -->
|
||||||
|
<div id="filterSection" style="display: none;" class="container my-4">
|
||||||
|
<h2>Filter Debug</h2>
|
||||||
|
|
||||||
|
<!-- Dropdown of available 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"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="btn btn-secondary" onclick="testSelectedFilter()">Test Filter</button>
|
||||||
|
<hr/>
|
||||||
|
|
||||||
|
<div id="testResults"></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">
|
||||||
@@ -345,6 +415,180 @@ function reloadFail2ban() {
|
|||||||
showLoading(false);
|
showLoading(false);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function loadSettings() {
|
||||||
|
showLoading(true);
|
||||||
|
fetch('/api/settings')
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(data => {
|
||||||
|
// populate the form
|
||||||
|
document.getElementById('languageSelect').value = data.language || 'en';
|
||||||
|
document.getElementById('alertEmail').value = data.alertEmail || '';
|
||||||
|
if (data.alertCountries && data.alertCountries.length > 0) {
|
||||||
|
if (data.alertCountries[0] === 'all') {
|
||||||
|
document.getElementById('alertCountries').value = 'all';
|
||||||
|
} else {
|
||||||
|
document.getElementById('alertCountries').value = data.alertCountries.join(',');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
alert('Error loading settings: ' + err);
|
||||||
|
})
|
||||||
|
.finally(() => showLoading(false));
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveSettings(e) {
|
||||||
|
e.preventDefault(); // prevent form submission
|
||||||
|
|
||||||
|
showLoading(true);
|
||||||
|
const lang = document.getElementById('languageSelect').value;
|
||||||
|
const mail = document.getElementById('alertEmail').value;
|
||||||
|
const countries = document.getElementById('alertCountries').value;
|
||||||
|
|
||||||
|
let countryList = [];
|
||||||
|
if (!countries || countries.trim() === '') {
|
||||||
|
countryList.push('all');
|
||||||
|
} else {
|
||||||
|
countryList = countries.split(',').map(s => s.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = {
|
||||||
|
language: lang,
|
||||||
|
alertEmail: mail,
|
||||||
|
alertCountries: countryList
|
||||||
|
};
|
||||||
|
|
||||||
|
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);
|
||||||
|
} else {
|
||||||
|
alert(data.message || 'Settings saved');
|
||||||
|
if (data.needsRestart) {
|
||||||
|
// show the same "reload" banner used for filter changes
|
||||||
|
document.getElementById('reloadBanner').style.display = 'block';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
alert('Error: ' + err);
|
||||||
|
})
|
||||||
|
.finally(() => showLoading(false));
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 = '';
|
||||||
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user