add loger, access log and bans/whitelist
This commit is contained in:
312
web/templates/admin_logs.html
Normal file
312
web/templates/admin_logs.html
Normal file
@@ -0,0 +1,312 @@
|
||||
{{define "admin_logs"}}
|
||||
{{template "base" .}}
|
||||
{{end}}
|
||||
|
||||
{{define "admin_logs_content"}}
|
||||
<div class="max-w-7xl mx-auto p-6">
|
||||
<div class="mb-6 flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-white">Admin Logs</h1>
|
||||
<p class="text-gray-400">Recent access, errors, failed logins, and IP bans</p>
|
||||
</div>
|
||||
<a href="/editor/admin" class="btn-secondary">Back to Admin</a>
|
||||
</div>
|
||||
|
||||
<!-- Filters -->
|
||||
<form method="GET" action="/editor/admin/logs" class="mb-6 bg-slate-800 border border-slate-700 rounded-lg p-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-5 gap-3 items-end">
|
||||
<div>
|
||||
<label class="block text-sm text-gray-300 mb-1">IP contains</label>
|
||||
<input type="text" name="ip" value="{{.FilterIP}}" class="form-input" placeholder="e.g. 192.168" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm text-gray-300 mb-1">Last hours</label>
|
||||
<input type="number" min="0" step="1" name="hours" value="{{.FilterHours}}" class="form-input" placeholder="e.g. 24" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm text-gray-300 mb-1">Limit</label>
|
||||
<input type="number" min="1" max="500" step="1" name="limit" value="{{.FilterLimit}}" class="form-input" placeholder="100" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm text-gray-300 mb-1">Bans filter</label>
|
||||
<select name="bans" class="form-input">
|
||||
<option value="all" {{if eq .FilterBans "all"}}selected{{end}}>All</option>
|
||||
<option value="active" {{if eq .FilterBans "active"}}selected{{end}}>Active only</option>
|
||||
<option value="perma" {{if or (eq .FilterBans "perma") (eq .FilterBans "permanent") (eq .FilterBans "perm")}}selected{{end}}>Permanent</option>
|
||||
<option value="whitelist" {{if or (eq .FilterBans "whitelist") (eq .FilterBans "whitelisted")}}selected{{end}}>Whitelisted</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex space-x-2">
|
||||
<button type="submit" class="btn-primary">Apply</button>
|
||||
<a href="/editor/admin/logs" class="btn-secondary">Reset</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Admin Actions: Manual Ban + Clear Logs -->
|
||||
<div class="mb-6 grid grid-cols-1 lg:grid-cols-3 gap-4">
|
||||
<div class="bg-slate-800 border border-slate-700 rounded-lg p-4 lg:col-span-2">
|
||||
<div class="text-white font-semibold mb-2">Manual Ban/Whitelist</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-6 gap-2">
|
||||
<input id="ban-ip" type="text" class="form-input" placeholder="IP e.g. 203.0.113.5" />
|
||||
<input id="ban-reason" type="text" class="form-input" placeholder="Reason (optional)" />
|
||||
<input id="ban-hours" type="number" min="1" step="1" class="form-input" placeholder="Hours (temp)" />
|
||||
<label class="flex items-center text-gray-300"><input id="ban-permanent" type="checkbox" class="mr-2">Permanent</label>
|
||||
<button id="ban-submit" class="btn-primary">Ban</button>
|
||||
<button id="whitelist-submit" class="btn-secondary">Whitelist</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-slate-800 border border-slate-700 rounded-lg p-4 flex items-center justify-between">
|
||||
<div class="flex-1 mr-3">
|
||||
<div class="text-white font-semibold">Clear Access Logs</div>
|
||||
<div class="text-sm text-gray-400">Delete entries older than N days (0 = delete all)</div>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<input id="clear-days" type="number" min="0" step="1" value="7" class="form-input w-24" title="Days threshold (0 deletes all)" />
|
||||
<button id="clear-access" class="btn-danger">Clear</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tabs -->
|
||||
<div class="mb-4 border-b border-slate-700">
|
||||
<nav class="flex flex-wrap gap-2" role="tablist">
|
||||
<button class="tab-btn active" data-tab="access" aria-selected="true"><i class="fas fa-list mr-2"></i>Access</button>
|
||||
<button class="tab-btn" data-tab="errors"><i class="fas fa-bug mr-2"></i>Errors</button>
|
||||
<button class="tab-btn" data-tab="failed"><i class="fas fa-user-lock mr-2"></i>Failed Logins</button>
|
||||
<button class="tab-btn" data-tab="bans"><i class="fas fa-shield-alt mr-2"></i>IP Bans</button>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div class="tab-panels space-y-6">
|
||||
<!-- Access Logs Panel -->
|
||||
<div class="tab-panel" data-tab-panel="access">
|
||||
<div class="bg-gray-800 rounded-lg p-4">
|
||||
<h2 class="text-xl font-semibold text-white mb-3"><i class="fas fa-list mr-2"></i>Access Logs</h2>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full text-sm">
|
||||
<thead>
|
||||
<tr class="text-gray-300 border-b border-gray-700">
|
||||
<th class="text-left py-2 pr-4">Time</th>
|
||||
<th class="text-left py-2 pr-4">IP</th>
|
||||
<th class="text-left py-2 pr-4">Method</th>
|
||||
<th class="text-left py-2 pr-4">Path</th>
|
||||
<th class="text-left py-2 pr-4">Status</th>
|
||||
<th class="text-left py-2 pr-4">Duration(ms)</th>
|
||||
<th class="text-left py-2 pr-4">User</th>
|
||||
<th class="text-left py-2 pr-4">UA</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .AccessLogs}}
|
||||
<tr class="border-b border-gray-700 text-gray-200">
|
||||
<td class="py-1 pr-4">{{formatTime .CreatedAt}}</td>
|
||||
<td class="py-1 pr-4">{{.IP}}</td>
|
||||
<td class="py-1 pr-4">{{.Method}}</td>
|
||||
<td class="py-1 pr-4">{{.Path}}</td>
|
||||
<td class="py-1 pr-4">{{.Status}}</td>
|
||||
<td class="py-1 pr-4">{{.Duration}}</td>
|
||||
<td class="py-1 pr-4">{{if .Username.Valid}}{{.Username.String}}{{else}}-{{end}}</td>
|
||||
<td class="py-1 pr-4 truncate max-w-[16rem]" title="{{.UserAgent}}">{{.UserAgent}}</td>
|
||||
</tr>
|
||||
{{else}}
|
||||
<tr><td class="py-2 text-gray-400" colspan="8">No access logs</td></tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Error Logs Panel -->
|
||||
<div class="tab-panel hidden" data-tab-panel="errors">
|
||||
<div class="bg-gray-800 rounded-lg p-4">
|
||||
<h2 class="text-xl font-semibold text-white mb-3"><i class="fas fa-bug mr-2"></i>Error Logs</h2>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full text-sm">
|
||||
<thead>
|
||||
<tr class="text-gray-300 border-b border-gray-700">
|
||||
<th class="text-left py-2 pr-4">Time</th>
|
||||
<th class="text-left py-2 pr-4">IP</th>
|
||||
<th class="text-left py-2 pr-4">Path</th>
|
||||
<th class="text-left py-2 pr-4">Message</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .ErrorLogs}}
|
||||
<tr class="border-b border-gray-700 text-gray-200">
|
||||
<td class="py-1 pr-4">{{formatTime .CreatedAt}}</td>
|
||||
<td class="py-1 pr-4">{{.IP.String}}</td>
|
||||
<td class="py-1 pr-4">{{.Path.String}}</td>
|
||||
<td class="py-1 pr-4 whitespace-pre-line">{{.Message}}</td>
|
||||
</tr>
|
||||
{{else}}
|
||||
<tr><td class="py-2 text-gray-400" colspan="4">No error logs</td></tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Failed Logins Panel -->
|
||||
<div class="tab-panel hidden" data-tab-panel="failed">
|
||||
<div class="bg-gray-800 rounded-lg p-4">
|
||||
<h2 class="text-xl font-semibold text-white mb-3"><i class="fas fa-user-lock mr-2"></i>Failed Logins</h2>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full text-sm">
|
||||
<thead>
|
||||
<tr class="text-gray-300 border-b border-gray-700">
|
||||
<th class="text-left py-2 pr-4">Time</th>
|
||||
<th class="text-left py-2 pr-4">IP</th>
|
||||
<th class="text-left py-2 pr-4">Username</th>
|
||||
<th class="text-left py-2 pr-4">Type</th>
|
||||
<th class="text-left py-2 pr-4">UserID</th>
|
||||
<th class="text-left py-2 pr-4">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .FailedLogins}}
|
||||
<tr class="border-b border-gray-700 text-gray-200">
|
||||
<td class="py-1 pr-4">{{formatTime .CreatedAt}}</td>
|
||||
<td class="py-1 pr-4">{{.IP}}</td>
|
||||
<td class="py-1 pr-4">{{.Username.String}}</td>
|
||||
<td class="py-1 pr-4">{{.Type}}</td>
|
||||
<td class="py-1 pr-4">{{if .UserID.Valid}}{{.UserID.Int64}}{{else}}-{{end}}</td>
|
||||
<td class="py-1 pr-4 space-x-2">
|
||||
<button class="btn-secondary btn-ban-24" data-ip="{{.IP}}" title="Temp ban 24h">Ban 24h</button>
|
||||
<button class="btn-danger btn-ban-perma" data-ip="{{.IP}}" title="Permanent ban">Perma</button>
|
||||
<button class="btn-secondary btn-whitelist" data-ip="{{.IP}}" title="Whitelist">Whitelist</button>
|
||||
</td>
|
||||
</tr>
|
||||
{{else}}
|
||||
<tr><td class="py-2 text-gray-400" colspan="6">No failed logins</td></tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- IP Bans Panel -->
|
||||
<div class="tab-panel hidden" data-tab-panel="bans">
|
||||
<div class="bg-gray-800 rounded-lg p-4">
|
||||
<h2 class="text-xl font-semibold text-white mb-3"><i class="fas fa-shield-alt mr-2"></i>IP Bans</h2>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full text-sm">
|
||||
<thead>
|
||||
<tr class="text-gray-300 border-b border-gray-700">
|
||||
<th class="text-left py-2 pr-4">IP</th>
|
||||
<th class="text-left py-2 pr-4">Reason</th>
|
||||
<th class="text-left py-2 pr-4">Until</th>
|
||||
<th class="text-left py-2 pr-4">Permanent</th>
|
||||
<th class="text-left py-2 pr-4">Whitelisted</th>
|
||||
<th class="text-left py-2 pr-4">Updated</th>
|
||||
<th class="text-left py-2 pr-4">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .IPBans}}
|
||||
<tr class="border-b border-gray-700 text-gray-200">
|
||||
<td class="py-1 pr-4">{{.IP}}</td>
|
||||
<td class="py-1 pr-4">{{if .Reason.Valid}}{{.Reason.String}}{{else}}-{{end}}</td>
|
||||
<td class="py-1 pr-4">{{if .Until.Valid}}{{formatTime .Until.Time}}{{else}}-{{end}}</td>
|
||||
<td class="py-1 pr-4">{{if eq .Permanent 1}}Yes{{else}}No{{end}}</td>
|
||||
<td class="py-1 pr-4">{{if eq .Whitelisted 1}}Yes{{else}}No{{end}}</td>
|
||||
<td class="py-1 pr-4">{{formatTime .UpdatedAt}}</td>
|
||||
<td class="py-1 pr-4 space-x-2">
|
||||
<button class="btn-danger btn-unban" data-ip="{{.IP}}">Unban</button>
|
||||
{{if eq .Whitelisted 1}}
|
||||
<button class="btn-secondary btn-whitelist-off" data-ip="{{.IP}}">Unwhitelist</button>
|
||||
{{else}}
|
||||
<button class="btn-secondary btn-whitelist-on" data-ip="{{.IP}}">Whitelist</button>
|
||||
{{end}}
|
||||
{{if ne .Permanent 1}}
|
||||
<button class="btn-secondary btn-permanent" data-ip="{{.IP}}">Make permanent</button>
|
||||
{{end}}
|
||||
</td>
|
||||
</tr>
|
||||
{{else}}
|
||||
<tr><td class="py-2 text-gray-400" colspan="7">No bans</td></tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{define "admin_logs_scripts"}}
|
||||
<script>
|
||||
function getCSRF() {
|
||||
const m = document.cookie.match(/(?:^|; )csrf_token=([^;]+)/);
|
||||
return m && m[1] ? decodeURIComponent(m[1]) : '';
|
||||
}
|
||||
|
||||
async function postForm(url, data) {
|
||||
const csrf = getCSRF();
|
||||
const form = new URLSearchParams();
|
||||
Object.entries(data || {}).forEach(([k, v]) => form.append(k, v));
|
||||
const res = await fetch(url, { method: 'POST', headers: Object.assign({'Content-Type': 'application/x-www-form-urlencoded'}, csrf ? {'X-CSRF-Token': csrf} : {}), body: form.toString() });
|
||||
if (!res.ok) {
|
||||
const j = await res.json().catch(() => ({}));
|
||||
throw new Error(j.error || res.statusText);
|
||||
}
|
||||
return res.json().catch(() => ({}));
|
||||
}
|
||||
|
||||
document.getElementById('ban-submit')?.addEventListener('click', async () => {
|
||||
const ip = document.getElementById('ban-ip').value.trim();
|
||||
const reason = document.getElementById('ban-reason').value.trim();
|
||||
const hours = document.getElementById('ban-hours').value.trim();
|
||||
const permanent = document.getElementById('ban-permanent').checked ? '1' : '0';
|
||||
if (!ip) { showNotification('IP is required', 'error'); return; }
|
||||
try { await postForm('/editor/admin/ip/ban', { ip, reason, hours, permanent }); location.reload(); } catch (e) { showNotification('Ban failed: ' + e.message, 'error'); }
|
||||
});
|
||||
|
||||
document.getElementById('whitelist-submit')?.addEventListener('click', async () => {
|
||||
const ip = document.getElementById('ban-ip').value.trim();
|
||||
if (!ip) { showNotification('IP is required', 'error'); return; }
|
||||
try { await postForm('/editor/admin/ip/whitelist', { ip, value: '1' }); location.reload(); } catch (e) { showNotification('Whitelist failed: ' + e.message, 'error'); }
|
||||
});
|
||||
|
||||
document.getElementById('clear-access')?.addEventListener('click', async () => {
|
||||
const days = (document.getElementById('clear-days')?.value || '7').trim();
|
||||
const d = parseInt(days, 10);
|
||||
if (isNaN(d) || d < 0) { showNotification('Days must be a non-negative number', 'error'); return; }
|
||||
const msg = d === 0 ? 'This will delete ALL access logs. Proceed?' : `Delete access logs older than ${d} days?`;
|
||||
if (!confirm(msg)) return;
|
||||
try { await postForm('/editor/admin/logs/clear_access', { days: String(d) }); location.reload(); } catch (e) { showNotification('Clear failed: ' + e.message, 'error'); }
|
||||
});
|
||||
|
||||
document.addEventListener('click', async (e) => {
|
||||
const t = e.target.closest('button'); if (!t) return;
|
||||
const ip = t.getAttribute('data-ip');
|
||||
try {
|
||||
if (t.classList.contains('btn-ban-24')) { await postForm('/editor/admin/ip/ban', { ip, hours: '24', permanent: '0' }); location.reload(); }
|
||||
else if (t.classList.contains('btn-ban-perma')) { await postForm('/editor/admin/ip/ban', { ip, permanent: '1' }); location.reload(); }
|
||||
else if (t.classList.contains('btn-whitelist')) { await postForm('/editor/admin/ip/whitelist', { ip, value: '1' }); location.reload(); }
|
||||
else if (t.classList.contains('btn-unban')) { await postForm('/editor/admin/ip/unban', { ip }); location.reload(); }
|
||||
else if (t.classList.contains('btn-whitelist-on')) { await postForm('/editor/admin/ip/whitelist', { ip, value: '1' }); location.reload(); }
|
||||
else if (t.classList.contains('btn-whitelist-off')) { await postForm('/editor/admin/ip/whitelist', { ip, value: '0' }); location.reload(); }
|
||||
else if (t.classList.contains('btn-permanent')) { await postForm('/editor/admin/ip/ban', { ip, permanent: '1' }); location.reload(); }
|
||||
} catch (err) {
|
||||
showNotification('Action failed: ' + err.message, 'error');
|
||||
}
|
||||
});
|
||||
|
||||
// Tabs behavior
|
||||
document.querySelectorAll('.tab-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
const key = btn.getAttribute('data-tab');
|
||||
document.querySelectorAll('.tab-panel').forEach(p => {
|
||||
if (p.getAttribute('data-tab-panel') === key) p.classList.remove('hidden'); else p.classList.add('hidden');
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{{end}}
|
||||
@@ -259,6 +259,7 @@
|
||||
<body class="bg-slate-900 text-gray-300 min-h-screen">
|
||||
<div class="flex h-screen">
|
||||
<!-- Sidebar -->
|
||||
{{if not .NoSidebar}}
|
||||
<div id="sidebar" class="w-80 bg-slate-800 border-r border-gray-700 flex flex-col">
|
||||
<!-- Header -->
|
||||
<div class="p-4 border-b border-gray-700">
|
||||
@@ -320,6 +321,7 @@
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="flex-1 flex flex-col overflow-hidden">
|
||||
@@ -357,6 +359,8 @@
|
||||
{{template "settings_content" .}}
|
||||
{{else if eq .Page "admin"}}
|
||||
{{template "admin_content" .}}
|
||||
{{else if eq .Page "admin_logs"}}
|
||||
{{template "admin_logs_content" .}}
|
||||
{{else if eq .Page "profile"}}
|
||||
{{template "profile_content" .}}
|
||||
{{else if eq .Page "error"}}
|
||||
@@ -726,6 +730,8 @@
|
||||
{{template "settings_scripts" .}}
|
||||
{{else if eq .Page "admin"}}
|
||||
{{template "admin_scripts" .}}
|
||||
{{else if eq .Page "admin_logs"}}
|
||||
{{template "admin_logs_scripts" .}}
|
||||
{{else if eq .Page "profile"}}
|
||||
{{template "profile_scripts" .}}
|
||||
{{else if eq .Page "error"}}
|
||||
|
||||
@@ -12,6 +12,22 @@
|
||||
|
||||
<!-- Settings Sections -->
|
||||
<div class="space-y-8">
|
||||
<!-- Quick Actions -->
|
||||
<div class="bg-gray-800 rounded-lg p-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 class="text-xl font-semibold text-white mb-1">
|
||||
<i class="fas fa-tools mr-2"></i>Admin Tools
|
||||
</h2>
|
||||
<p class="text-gray-400">Access logs and security controls</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<a href="/editor/admin/logs" target="_blank" class="btn-secondary inline-flex items-center">
|
||||
<i class="fas fa-list mr-2"></i>View Logs
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Image Storage Settings -->
|
||||
<div class="bg-gray-800 rounded-lg p-6">
|
||||
<h2 class="text-xl font-semibold text-white mb-4">
|
||||
@@ -170,6 +186,58 @@
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Security Settings -->
|
||||
<div class="bg-gray-800 rounded-lg p-6">
|
||||
<h2 class="text-xl font-semibold text-white mb-4">
|
||||
<i class="fas fa-shield-alt mr-2"></i>Security (IP Ban & Thresholds)
|
||||
</h2>
|
||||
<p class="text-gray-400 mb-6">Configure failed login thresholds, window, and automatic ban behavior</p>
|
||||
|
||||
<form id="security-settings-form" class="space-y-6">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label for="pwd_failures_threshold" class="block text-sm font-medium text-gray-300 mb-2">Password Failures Threshold</label>
|
||||
<input type="number" id="pwd_failures_threshold" name="pwd_failures_threshold" min="1"
|
||||
class="w-full bg-gray-700 border border-gray-600 rounded-lg px-4 py-2 text-white focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
placeholder="e.g., 5">
|
||||
</div>
|
||||
<div>
|
||||
<label for="mfa_failures_threshold" class="block text-sm font-medium text-gray-300 mb-2">MFA Failures Threshold</label>
|
||||
<input type="number" id="mfa_failures_threshold" name="mfa_failures_threshold" min="1"
|
||||
class="w-full bg-gray-700 border border-gray-600 rounded-lg px-4 py-2 text-white focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
placeholder="e.g., 10">
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label for="failures_window_minutes" class="block text-sm font-medium text-gray-300 mb-2">Failures Window (minutes)</label>
|
||||
<input type="number" id="failures_window_minutes" name="failures_window_minutes" min="1"
|
||||
class="w-full bg-gray-700 border border-gray-600 rounded-lg px-4 py-2 text-white focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
placeholder="e.g., 30">
|
||||
</div>
|
||||
<div>
|
||||
<label for="auto_ban_duration_hours" class="block text-sm font-medium text-gray-300 mb-2">Auto-ban Duration (hours)</label>
|
||||
<input type="number" id="auto_ban_duration_hours" name="auto_ban_duration_hours" min="1"
|
||||
class="w-full bg-gray-700 border border-gray-600 rounded-lg px-4 py-2 text-white focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
placeholder="e.g., 12">
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="flex items-center space-x-2">
|
||||
<input type="checkbox" id="auto_ban_permanent" name="auto_ban_permanent" class="h-4 w-4 text-blue-600 rounded border-gray-600 bg-gray-700">
|
||||
<span class="text-sm text-gray-300">Make auto-bans permanent</span>
|
||||
</label>
|
||||
<p class="text-xs text-gray-500 mt-1">If enabled, IPs exceeding thresholds are permanently banned instead of temporary bans.</p>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end">
|
||||
<button type="submit" class="btn-primary">
|
||||
<i class="fas fa-save mr-2"></i>Save Security Settings
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
@@ -213,6 +281,18 @@
|
||||
document.getElementById('show_files_in_folder').checked = !!data.show_files_in_folder;
|
||||
})
|
||||
.catch(error => console.error('Error loading file extensions settings:', error));
|
||||
|
||||
// Load security settings
|
||||
fetch('/editor/settings/security')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
document.getElementById('pwd_failures_threshold').value = data.pwd_failures_threshold ?? '';
|
||||
document.getElementById('mfa_failures_threshold').value = data.mfa_failures_threshold ?? '';
|
||||
document.getElementById('failures_window_minutes').value = data.failures_window_minutes ?? '';
|
||||
document.getElementById('auto_ban_duration_hours').value = data.auto_ban_duration_hours ?? '';
|
||||
document.getElementById('auto_ban_permanent').checked = !!data.auto_ban_permanent;
|
||||
})
|
||||
.catch(error => console.error('Error loading security settings:', error));
|
||||
}
|
||||
|
||||
// Toggle storage mode options
|
||||
@@ -232,6 +312,34 @@
|
||||
}
|
||||
});
|
||||
|
||||
// Security settings form
|
||||
document.getElementById('security-settings-form').addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const formData = new FormData(this);
|
||||
// Normalize checkbox to boolean string
|
||||
formData.set('auto_ban_permanent', document.getElementById('auto_ban_permanent').checked ? 'true' : 'false');
|
||||
|
||||
const csrf = (document.cookie.match(/(?:^|; )csrf_token=([^;]+)/)||[])[1] ? decodeURIComponent((document.cookie.match(/(?:^|; )csrf_token=([^;]+)/)||[])[1]) : '';
|
||||
|
||||
fetch('/editor/settings/security', {
|
||||
method: 'POST',
|
||||
headers: csrf ? { 'X-CSRF-Token': csrf } : {},
|
||||
body: formData
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showNotification('Security settings saved successfully', 'success');
|
||||
} else {
|
||||
throw new Error(data.error || 'Failed to save settings');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
showNotification('Error: ' + error.message, 'error');
|
||||
});
|
||||
});
|
||||
|
||||
// Image storage form
|
||||
document.getElementById('image-storage-form').addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
Reference in New Issue
Block a user