Illustrations/web/templates/tools/login_attempts.html

144 lines
5.3 KiB
HTML

{% extends "base.html" %}
{% block title %}Login Attempts (7 days){% endblock %}
{% block body_class %}themed-bg{% endblock %}
{% block content %}
<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;">
<table class="table" style="width:100%; border-collapse:separate; border-spacing:0 6px;">
<thead>
<tr>
<th style="padding:10px 14px; text-align:left;">Date / Time</th>
<th style="padding:10px 14px; text-align:left;">Username</th>
<th style="padding:10px 14px; text-align:left;">IP</th>
<th style="padding:10px 14px; text-align:left;">Status</th>
</tr>
</thead>
<tbody id="attempts-body">
{% for a in attempts %}
<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="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 %}
{% endif %}
</td>
<td style="padding:10px 14px;">
{% if a.success %}
<span class="pill pill-green">Success</span>
{% else %}
<span class="pill pill-red">Failed</span>
<!-- red-dot gets injected here via JS when this IP has 2+ fails -->
{% endif %}
</td>
</tr>
{% empty %}
<tr>
<td colspan="4" class="muted" style="padding:12px 14px;">
No login attempts in the last 7 days.
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</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 %}