144 lines
5.3 KiB
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 %} |