mirror of
https://github.com/swissmakers/fail2ban-ui.git
synced 2026-04-11 13:47:05 +02:00
Create new views settings and debug
This commit is contained in:
@@ -1,17 +1,20 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/swissmakers/fail2ban-ui/internal/config"
|
||||
"github.com/swissmakers/fail2ban-ui/internal/fail2ban"
|
||||
)
|
||||
|
||||
// SummaryResponse is what we return from /api/summary
|
||||
type SummaryResponse struct {
|
||||
Jails []fail2ban.JailInfo `json:"jails"`
|
||||
LastBans []fail2ban.BanEvent `json:"lastBans"`
|
||||
Jails []fail2ban.JailInfo `json:"jails"`
|
||||
LastBans []fail2ban.BanEvent `json:"lastBans"`
|
||||
}
|
||||
|
||||
// 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) {
|
||||
c.HTML(http.StatusOK, "index.html", gin.H{
|
||||
"timestamp": time.Now().Format(time.RFC1123),
|
||||
})
|
||||
}
|
||||
|
||||
// GetJailConfigHandler returns the raw config for a given jail
|
||||
func GetJailConfigHandler(c *gin.Context) {
|
||||
jail := c.Param("jail")
|
||||
cfg, err := fail2ban.GetJailConfig(jail)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"jail": jail,
|
||||
"config": cfg,
|
||||
})
|
||||
// GetJailFilterConfigHandler returns the raw filter config for a given jail
|
||||
func GetJailFilterConfigHandler(c *gin.Context) {
|
||||
jail := c.Param("jail")
|
||||
cfg, err := fail2ban.GetJailConfig(jail)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"jail": jail,
|
||||
"config": cfg,
|
||||
})
|
||||
}
|
||||
|
||||
// SetJailConfigHandler overwrites the jail config with new content
|
||||
func SetJailConfigHandler(c *gin.Context) {
|
||||
jail := c.Param("jail")
|
||||
// SetJailFilterConfigHandler overwrites the current filter config with new content
|
||||
func SetJailFilterConfigHandler(c *gin.Context) {
|
||||
jail := c.Param("jail")
|
||||
|
||||
var req struct {
|
||||
Config string `json:"config"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid JSON body"})
|
||||
return
|
||||
}
|
||||
var req struct {
|
||||
Config string `json:"config"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid JSON body"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := fail2ban.SetJailConfig(jail, req.Config); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if err := fail2ban.SetJailConfig(jail, req.Config); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
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
|
||||
func ReloadFail2banHandler(c *gin.Context) {
|
||||
err := fail2ban.ReloadFail2ban()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Fail2ban reloaded successfully"})
|
||||
}
|
||||
err := fail2ban.ReloadFail2ban()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Fail2ban reloaded successfully"})
|
||||
}
|
||||
|
||||
@@ -33,12 +33,31 @@
|
||||
<body class="bg-light">
|
||||
<!-- NavBar -->
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="#">
|
||||
<strong>Fail2ban UI</strong>
|
||||
</a>
|
||||
</div>
|
||||
</nav>
|
||||
<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">
|
||||
@@ -48,11 +67,62 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="container my-4">
|
||||
<!-- Dashboard Section -->
|
||||
<div id="dashboardSection" class="container my-4">
|
||||
<h1 class="mb-4">Dashboard</h1>
|
||||
<div id="dashboard"></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 class="text-center mt-4 mb-4">
|
||||
<p class="mb-0">
|
||||
@@ -345,6 +415,180 @@ function reloadFail2ban() {
|
||||
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>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user