- /errors/banned/ : bannissements groupés par AS (nom, pays, nb entrées) - Déblocage IP seule ou AS entier via POST /errors/unban - Filtre dynamique par IP/CIDR/nom AS, mise à jour DOM sans rechargement - Nav header + sudoers unbanip Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
161 lines
6 KiB
HTML
161 lines
6 KiB
HTML
{% extends "base.html" %}
|
|
{% block title %}Bannissements{% endblock %}
|
|
|
|
{% block content %}
|
|
<section class="card">
|
|
<div class="err-header">
|
|
<h2>Bannissements <span class="err-total-badge">{{ total }} entrée{{ 's' if total != 1 }}</span></h2>
|
|
<button type="button" class="btn btn-sm" onclick="location.reload()">↺ Actualiser</button>
|
|
</div>
|
|
|
|
{% if not groups %}
|
|
<p style="color:var(--muted);margin-top:.5rem">Aucune IP bannie dans global-blacklist.</p>
|
|
{% else %}
|
|
|
|
<div class="err-search-wrap" style="margin-top:.75rem">
|
|
<input type="search" id="ban-search" class="err-search" placeholder="Filtrer par IP, CIDR ou nom d'AS…" autocomplete="off">
|
|
<span class="err-search-count" id="ban-search-count"></span>
|
|
</div>
|
|
|
|
<table class="file-table banned-table">
|
|
<thead>
|
|
<tr>
|
|
<th>IP / CIDR</th>
|
|
<th class="col-asn">AS</th>
|
|
<th>Opérateur</th>
|
|
<th class="col-country">Pays</th>
|
|
<th class="col-actions"></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="ban-tbody">
|
|
{% for group in groups %}
|
|
<tr class="banned-as-row" data-asn="{{ group.asn }}" data-name="{{ group.name | lower }}">
|
|
<td colspan="4" class="banned-as-cell">
|
|
<span class="banned-as-label">
|
|
{% if group.asn %}<strong>AS{{ group.asn }}</strong>{% else %}<strong>AS inconnu</strong>{% endif %}
|
|
{% if group.name %} · {{ group.name }}{% endif %}
|
|
{% if group.country %} <span class="banned-country">[{{ group.country }}]</span>{% endif %}
|
|
</span>
|
|
<span class="err-total-badge" style="margin-left:.5rem">{{ group.entries|length }} entrée{{ 's' if group.entries|length != 1 }}</span>
|
|
</td>
|
|
<td class="col-actions">
|
|
<button type="button" class="btn-unban-as btn btn-sm"
|
|
data-asn="{{ group.asn }}"
|
|
data-entries="{{ group.entries | tojson | e }}"
|
|
title="Débloquer tout l'AS ({{ group.entries|length }} entrées)">
|
|
🔓 AS
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
{% for entry in group.entries %}
|
|
<tr class="banned-entry-row" data-asn="{{ group.asn }}" data-entry="{{ entry }}">
|
|
<td colspan="4"><code class="err-ip">{{ entry }}</code></td>
|
|
<td class="col-actions">
|
|
<button type="button" class="btn-unban btn btn-sm"
|
|
data-entry="{{ entry }}"
|
|
title="Débloquer {{ entry }}">
|
|
🔓
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
{% endif %}
|
|
</section>
|
|
|
|
<script>
|
|
(function () {
|
|
const UNBAN_URL = {{ url_for('errors_unban') | tojson }};
|
|
|
|
/* ── Recherche ── */
|
|
const searchInput = document.getElementById('ban-search');
|
|
const searchCount = document.getElementById('ban-search-count');
|
|
|
|
function applyFilter() {
|
|
const q = (searchInput?.value || '').trim().toLowerCase();
|
|
const asRows = [...document.querySelectorAll('.banned-as-row')];
|
|
let visible = 0;
|
|
asRows.forEach(asRow => {
|
|
const asn = (asRow.dataset.asn || '').toLowerCase();
|
|
const name = (asRow.dataset.name || '').toLowerCase();
|
|
const entryRows = [...document.querySelectorAll(`.banned-entry-row[data-asn="${asRow.dataset.asn}"]`)];
|
|
let anyMatch = false;
|
|
entryRows.forEach(er => {
|
|
const entry = (er.dataset.entry || '').toLowerCase();
|
|
const match = !q || entry.includes(q) || asn.includes(q) || name.includes(q);
|
|
er.style.display = match ? '' : 'none';
|
|
if (match) anyMatch = true;
|
|
});
|
|
asRow.style.display = (!q || anyMatch) ? '' : 'none';
|
|
if (!q || anyMatch) visible += entryRows.filter(r => r.style.display !== 'none').length;
|
|
});
|
|
if (searchCount) searchCount.textContent = q ? `${visible} / {{ total }}` : '';
|
|
}
|
|
|
|
searchInput?.addEventListener('input', applyFilter);
|
|
|
|
/* ── Débloquer une entrée ── */
|
|
document.addEventListener('click', async (e) => {
|
|
const btn = e.target.closest('.btn-unban:not(.btn-unban-as)');
|
|
if (!btn) return;
|
|
const entry = btn.dataset.entry;
|
|
if (!confirm(`Débloquer ${entry} ?`)) return;
|
|
await doUnban(btn, [entry]);
|
|
});
|
|
|
|
/* ── Débloquer tout un AS ── */
|
|
document.addEventListener('click', async (e) => {
|
|
const btn = e.target.closest('.btn-unban-as');
|
|
if (!btn) return;
|
|
let entries;
|
|
try { entries = JSON.parse(btn.dataset.entries); } catch { return; }
|
|
if (!confirm(`Débloquer les ${entries.length} entrée(s) de cet AS ?`)) return;
|
|
await doUnban(btn, entries, btn.dataset.asn);
|
|
});
|
|
|
|
async function doUnban(btn, entries, asn) {
|
|
btn.disabled = true; btn.textContent = '⏳';
|
|
const fd = new FormData();
|
|
entries.forEach(e => fd.append('entries', e));
|
|
try {
|
|
const r = await fetch(UNBAN_URL, { method: 'POST', body: fd });
|
|
const j = await r.json();
|
|
if (j.ok) {
|
|
if (asn !== undefined) {
|
|
document.querySelectorAll(`[data-asn="${asn}"]`).forEach(row => row.remove());
|
|
} else {
|
|
entries.forEach(entry => {
|
|
document.querySelector(`.banned-entry-row[data-entry="${CSS.escape(entry)}"]`)?.remove();
|
|
});
|
|
checkEmptyAs();
|
|
}
|
|
updateTotal();
|
|
} else {
|
|
btn.disabled = false;
|
|
btn.textContent = asn !== undefined ? '🔓 AS' : '🔓';
|
|
btn.title = j.error || 'Erreur';
|
|
alert('Erreur : ' + (j.error || 'voir console'));
|
|
}
|
|
} catch {
|
|
btn.disabled = false;
|
|
btn.textContent = asn !== undefined ? '🔓 AS' : '🔓';
|
|
}
|
|
}
|
|
|
|
function checkEmptyAs() {
|
|
document.querySelectorAll('.banned-as-row').forEach(asRow => {
|
|
const remaining = document.querySelectorAll(`.banned-entry-row[data-asn="${asRow.dataset.asn}"]`);
|
|
if (!remaining.length) asRow.remove();
|
|
});
|
|
}
|
|
|
|
function updateTotal() {
|
|
const remaining = document.querySelectorAll('.banned-entry-row').length;
|
|
document.querySelector('.err-total-badge').textContent =
|
|
`${remaining} entrée${remaining !== 1 ? 's' : ''}`;
|
|
}
|
|
})();
|
|
</script>
|
|
{% endblock %}
|