diff --git a/public/admin.js b/public/admin.js index 42efe5f..56bbad2 100644 --- a/public/admin.js +++ b/public/admin.js @@ -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 : [])]; diff --git a/public/bans.html b/public/bans.html new file mode 100644 index 0000000..d91f0a6 --- /dev/null +++ b/public/bans.html @@ -0,0 +1,41 @@ + + + + + +Блокировки IP — Админ-панель + + + +
+ +
+
+

Блокировки IP

+

IP автоматически блокируется на 24 часа: за заполнение скрытого антиспам-поля, за 10 неудачных попыток входа или 10 неверных паролей на защищённой ссылке

+
+
+ + +
+ + + + + + + + + + + +
IPПричинаЗаблокированРазблокируется
Загрузка…
+
+
+ +
+ + + + + diff --git a/public/js/bans.js b/public/js/bans.js new file mode 100644 index 0000000..95e661a --- /dev/null +++ b/public/js/bans.js @@ -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 = `Ошибка загрузки (${res.status})`; + info.textContent = ''; + return; + } + const bans = await res.json(); + info.textContent = `Активных блокировок: ${bans.length}`; + if (!bans.length) { + tbody.innerHTML = 'Активных блокировок нет'; + return; + } + tbody.innerHTML = bans.map(b => ` + + ${esc(b.ip)} + ${esc(reasonLabel(b.reason))} + ${new Date(b.created_at).toLocaleString('ru')} + ${new Date(b.banned_until).toLocaleString('ru')} + + `).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(); + } +})();