Add dedicated Bans page (bans.html) with active IP bans table, unban and refresh; nav item for admins; settings card kept
This commit is contained in:
@@ -60,6 +60,7 @@ function buildSidebar(active) {
|
||||
{ page: 'users', label: 'Пользователи', icon: 'user-cog' },
|
||||
{ page: 'worker', label: 'Воркер ИИ', icon: 'bot' },
|
||||
{ page: 'audit', label: 'Аудит', icon: 'scroll-text' },
|
||||
{ page: 'bans', label: 'Блокировки', icon: 'shield-off' },
|
||||
{ page: 'settings', label: 'Настройки', icon: 'settings' },
|
||||
];
|
||||
const nav = [...base, ...(isAdmin() ? adminNav : [])];
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Блокировки IP — Админ-панель</title>
|
||||
<link rel="stylesheet" href="admin.css">
|
||||
</head>
|
||||
<body data-page="bans">
|
||||
<div class="layout">
|
||||
<div class="sidebar" id="sidebar"></div>
|
||||
<div class="main">
|
||||
<div class="page-head">
|
||||
<h2>Блокировки IP</h2>
|
||||
<p class="page-sub">IP автоматически блокируется на 24 часа: за заполнение скрытого антиспам-поля, за 10 неудачных попыток входа или 10 неверных паролей на защищённой ссылке</p>
|
||||
</div>
|
||||
<div class="trash-head">
|
||||
<span class="trash-muted" id="bansInfo"></span>
|
||||
<button class="btn-primary ghost" id="refreshBtn" type="button"><i data-lucide="refresh-cw"></i> Обновить</button>
|
||||
</div>
|
||||
<table class="audit-table" id="bansTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>IP</th>
|
||||
<th>Причина</th>
|
||||
<th>Заблокирован</th>
|
||||
<th>Разблокируется</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="bansBody"><tr><td colspan="5">Загрузка…</td></tr></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="toast" id="toast"></div>
|
||||
<script src="vendor/lucide.min.js"></script>
|
||||
<script src="admin.js"></script>
|
||||
<script src="js/bans.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,57 @@
|
||||
const REASONS = {
|
||||
'honeypot': 'Антиспам-поле заполнено',
|
||||
'login-bruteforce': 'Подбор пароля (10 неудачных входов)',
|
||||
'share-password-bruteforce': 'Подбор пароля ссылки',
|
||||
'manual': 'Вручную',
|
||||
};
|
||||
|
||||
function reasonLabel(r) {
|
||||
return REASONS[r] || r || '—';
|
||||
}
|
||||
|
||||
async function loadBans() {
|
||||
const tbody = document.getElementById('bansBody');
|
||||
const info = document.getElementById('bansInfo');
|
||||
const res = await fetch(`${API}/api/bans`, { headers: hdr() });
|
||||
if (!res.ok) {
|
||||
tbody.innerHTML = `<tr><td colspan="5">Ошибка загрузки (${res.status})</td></tr>`;
|
||||
info.textContent = '';
|
||||
return;
|
||||
}
|
||||
const bans = await res.json();
|
||||
info.textContent = `Активных блокировок: ${bans.length}`;
|
||||
if (!bans.length) {
|
||||
tbody.innerHTML = '<tr><td colspan="5" style="color:var(--muted)">Активных блокировок нет</td></tr>';
|
||||
return;
|
||||
}
|
||||
tbody.innerHTML = bans.map(b => `
|
||||
<tr>
|
||||
<td><b>${esc(b.ip)}</b></td>
|
||||
<td>${esc(reasonLabel(b.reason))}</td>
|
||||
<td>${new Date(b.created_at).toLocaleString('ru')}</td>
|
||||
<td>${new Date(b.banned_until).toLocaleString('ru')}</td>
|
||||
<td><button class="danger-btn" data-unban="${esc(b.ip)}" type="button">Разблокировать</button></td>
|
||||
</tr>`).join('');
|
||||
renderIcons();
|
||||
}
|
||||
|
||||
document.getElementById('bansBody').addEventListener('click', async e => {
|
||||
const ip = e.target.closest('[data-unban]')?.dataset.unban;
|
||||
if (!ip) return;
|
||||
const btn = e.target.closest('[data-unban]');
|
||||
btn.disabled = true;
|
||||
const res = await fetch(`${API}/api/bans/${encodeURIComponent(ip)}`, { method: 'DELETE', headers: hdr() });
|
||||
if (res.ok) showToast('IP разблокирован: ' + ip);
|
||||
else showToast('Ошибка разблокировки');
|
||||
btn.disabled = false;
|
||||
loadBans();
|
||||
});
|
||||
|
||||
document.getElementById('refreshBtn').addEventListener('click', loadBans);
|
||||
|
||||
(async () => {
|
||||
if (await requireAdminPage()) {
|
||||
buildSidebar(document.body.dataset.page);
|
||||
loadBans();
|
||||
}
|
||||
})();
|
||||
Reference in New Issue
Block a user