Update web/templates/tools/login_attempts.html
This commit is contained in:
parent
7d93563f43
commit
254d624302
@ -6,6 +6,21 @@
|
||||
<div class="container">
|
||||
<h1 class="page-title">Security · Login Attempts for the last 7 Days</h1>
|
||||
|
||||
<!-- Quick stats -->
|
||||
<div class="card" style="padding:14px 16px; margin-bottom:14px;">
|
||||
<div style="display:flex; gap:12px; flex-wrap:wrap; align-items:center;">
|
||||
<div style="display:inline-flex; align-items:center; gap:8px; background:#eef2ff; color:#1f2937; border-radius:999px; padding:6px 12px;">
|
||||
<strong>Total:</strong> <span id="stat-total">—</span>
|
||||
</div>
|
||||
<div style="display:inline-flex; align-items:center; gap:8px; background:#ecfdf5; color:#065f46; border-radius:999px; padding:6px 12px;">
|
||||
<strong>Successes:</strong> <span id="stat-success">—</span>
|
||||
</div>
|
||||
<div style="display:inline-flex; align-items:center; gap:8px; background:#fff7ed; color:#7c2d12; border-radius:999px; padding:6px 12px;">
|
||||
<strong>Success rate:</strong> <span id="stat-rate">—</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" style="padding:20px; width:100%;">
|
||||
|
||||
<div class="table-wrap" style="overflow-x:auto;">
|
||||
@ -18,16 +33,23 @@
|
||||
<th style="padding:10px 14px; text-align:left;">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tbody id="attempts-body">
|
||||
{% for a in attempts %}
|
||||
<tr style="background:#fff; box-shadow:0 1px 3px rgba(0,0,0,0.08);">
|
||||
<tr
|
||||
style="background:#fff; box-shadow:0 1px 3px rgba(0,0,0,0.08);"
|
||||
data-ip="{{ a.ip_address|default_if_none:'' }}"
|
||||
data-success="{% if a.success %}1{% else %}0{% endif %}"
|
||||
>
|
||||
<td style="padding:10px 14px;">{{ a.timestamp|date:"Y-m-d H:i:s" }}</td>
|
||||
<td style="padding:10px 14px;">{{ a.username }}</td>
|
||||
<td style="padding:10px 14px;">
|
||||
{% if a.ip_address %}
|
||||
<a href="http://{{ a.ip_address }}" target="_blank"
|
||||
style="display:inline-block; padding:4px 10px; border-radius:999px;
|
||||
background:#e5e7eb; color:#111; text-decoration:none; font-size:0.85em;">
|
||||
<a
|
||||
href="https://ipinfo.io/{{ a.ip_address }}"
|
||||
target="_blank" rel="noopener noreferrer"
|
||||
title="Open IP details on ipinfo.io"
|
||||
style="display:inline-block; padding:4px 10px; border-radius:999px;
|
||||
background:#e5e7eb; color:#111; text-decoration:none; font-size:0.85em;">
|
||||
{{ a.ip_address }}
|
||||
</a>
|
||||
{% else %}
|
||||
@ -36,15 +58,10 @@
|
||||
</td>
|
||||
<td style="padding:10px 14px;">
|
||||
{% if a.success %}
|
||||
<span style="display:inline-block; padding:4px 12px; border-radius:999px;
|
||||
background:#22c55e; color:#fff; font-size:0.85em;">
|
||||
Success
|
||||
</span>
|
||||
<span class="pill pill-green">Success</span>
|
||||
{% else %}
|
||||
<span style="display:inline-block; padding:4px 12px; border-radius:999px;
|
||||
background:#ef4444; color:#fff; font-size:0.85em;">
|
||||
Failed
|
||||
</span>
|
||||
<span class="pill pill-red">Failed</span>
|
||||
<!-- red-dot gets injected here via JS when this IP has 2+ fails -->
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
@ -61,4 +78,67 @@
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Inline pill styles (kept here so you don't have to touch app.css) -->
|
||||
<style>
|
||||
.pill {
|
||||
display:inline-block; padding:4px 12px; border-radius:999px;
|
||||
color:#fff; font-size:0.85em; line-height:1;
|
||||
}
|
||||
.pill-green { background:#22c55e; } /* green-500 */
|
||||
.pill-red { background:#ef4444; } /* red-500 */
|
||||
.dot-red {
|
||||
display:inline-block; width:8px; height:8px; border-radius:50%;
|
||||
background:#dc2626; margin-left:8px; vertical-align:middle;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- Lightweight JS: compute success rate and flag IPs with 2+ fails -->
|
||||
<script>
|
||||
(function(){
|
||||
var tbody = document.getElementById('attempts-body');
|
||||
if(!tbody) return;
|
||||
|
||||
var rows = Array.from(tbody.querySelectorAll('tr[data-success]'));
|
||||
var total = 0, ok = 0;
|
||||
var failedCountByIp = Object.create(null);
|
||||
|
||||
// First pass: counts
|
||||
rows.forEach(function(row){
|
||||
var success = row.getAttribute('data-success') === '1';
|
||||
var ip = row.getAttribute('data-ip') || '';
|
||||
total += 1;
|
||||
if (success) ok += 1;
|
||||
else {
|
||||
if (!failedCountByIp[ip]) failedCountByIp[ip] = 0;
|
||||
failedCountByIp[ip] += 1;
|
||||
}
|
||||
});
|
||||
|
||||
// Update stats
|
||||
var rate = total ? Math.round((ok / total) * 100) : 0;
|
||||
var elTotal = document.getElementById('stat-total');
|
||||
var elSuccess = document.getElementById('stat-success');
|
||||
var elRate = document.getElementById('stat-rate');
|
||||
if (elTotal) elTotal.textContent = String(total);
|
||||
if (elSuccess) elSuccess.textContent = String(ok);
|
||||
if (elRate) elRate.textContent = total ? (rate + '%') : '—';
|
||||
|
||||
// Second pass: flag rows with 2+ fails from the same IP
|
||||
rows.forEach(function(row){
|
||||
var success = row.getAttribute('data-success') === '1';
|
||||
if (success) return;
|
||||
var ip = row.getAttribute('data-ip') || '';
|
||||
if (ip && failedCountByIp[ip] >= 2) {
|
||||
var statusCell = row.cells[3];
|
||||
if (statusCell) {
|
||||
var dot = document.createElement('span');
|
||||
dot.className = 'dot-red';
|
||||
dot.title = '2+ failed attempts from this IP in the last 7 days';
|
||||
statusCell.appendChild(dot);
|
||||
}
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
{% endblock %}
|
||||
Loading…
Reference in New Issue
Block a user