254 lines
12 KiB
HTML
254 lines
12 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}Audit Trail{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="row mb-3 align-items-center">
|
|
<div class="col">
|
|
<h2><i class="bi bi-shield-check"></i> Audit Trail</h2>
|
|
<p class="text-muted mb-0">Complete, immutable log of all system actions.</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ── Filter Bar ──────────────────────────────────────────────────────────── -->
|
|
<div class="card shadow-sm mb-4">
|
|
<div class="card-body py-3">
|
|
<form method="GET" action="{{ url_for('audit.index') }}" class="row g-2 align-items-end">
|
|
<div class="col-md-2">
|
|
<label class="form-label form-label-sm fw-semibold mb-1">User</label>
|
|
<select name="user_id" class="form-select form-select-sm">
|
|
<option value="">All Users</option>
|
|
{% for u in users %}
|
|
<option value="{{ u.id }}" {% if filter_user == u.id|string %}selected{% endif %}>
|
|
{{ u.username }}
|
|
</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
<div class="col-md-2">
|
|
<label class="form-label form-label-sm fw-semibold mb-1">Action</label>
|
|
<select name="action" class="form-select form-select-sm">
|
|
<option value="">All Actions</option>
|
|
{% for a in distinct_actions %}
|
|
<option value="{{ a }}" {% if filter_action == a %}selected{% endif %}>{{ a }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
<div class="col-md-2">
|
|
<label class="form-label form-label-sm fw-semibold mb-1">Entity Type</label>
|
|
<select name="entity_type" class="form-select form-select-sm">
|
|
<option value="">All Types</option>
|
|
{% for t in distinct_entity_types %}
|
|
<option value="{{ t }}" {% if filter_entity_type == t %}selected{% endif %}>{{ t }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
<div class="col-md-2">
|
|
<label class="form-label form-label-sm fw-semibold mb-1">From Date</label>
|
|
<input type="date" name="date_from" class="form-control form-control-sm"
|
|
value="{{ filter_date_from }}">
|
|
</div>
|
|
<div class="col-md-2">
|
|
<label class="form-label form-label-sm fw-semibold mb-1">To Date</label>
|
|
<input type="date" name="date_to" class="form-control form-control-sm"
|
|
value="{{ filter_date_to }}">
|
|
</div>
|
|
<div class="col-md-2 d-flex gap-2">
|
|
<button type="submit" class="btn btn-primary btn-sm flex-fill">
|
|
<i class="bi bi-funnel me-1"></i>Filter
|
|
</button>
|
|
<a href="{{ url_for('audit.index') }}" class="btn btn-outline-secondary btn-sm">
|
|
<i class="bi bi-x-circle"></i>
|
|
</a>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ── Results ─────────────────────────────────────────────────────────────── -->
|
|
<div class="card shadow-sm">
|
|
<div class="card-header bg-light d-flex justify-content-between align-items-center">
|
|
<span class="fw-semibold">
|
|
<i class="bi bi-list-ul me-1"></i>
|
|
{{ logs.total }} record{{ 's' if logs.total != 1 else '' }}
|
|
{% if filter_user or filter_action or filter_entity_type or filter_date_from or filter_date_to %}
|
|
<span class="badge bg-info ms-1">Filtered</span>
|
|
{% endif %}
|
|
</span>
|
|
<div class="d-flex align-items-center gap-2">
|
|
<small class="text-muted">Page {{ logs.page }} of {{ logs.pages }}</small>
|
|
<button type="button" class="btn btn-sm btn-outline-danger"
|
|
data-bs-toggle="modal" data-bs-target="#purgeModal">
|
|
<i class="bi bi-trash me-1"></i>Purge Old Logs
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="card-body p-0">
|
|
{% if logs.items %}
|
|
<div class="table-responsive">
|
|
<table class="table table-hover table-sm mb-0">
|
|
<thead class="table-light">
|
|
<tr>
|
|
<th style="width:160px">Timestamp</th>
|
|
<th>User</th>
|
|
<th>Role</th>
|
|
<th>Action</th>
|
|
<th>Entity</th>
|
|
<th>Label</th>
|
|
<th>IP Address</th>
|
|
<th style="width:60px"></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for entry in logs.items %}
|
|
<tr>
|
|
<td class="text-nowrap text-muted small">
|
|
{{ entry.created_at.strftime('%Y-%m-%d %H:%M:%S') }}
|
|
</td>
|
|
<td>
|
|
<strong>{{ entry.username }}</strong>
|
|
</td>
|
|
<td>
|
|
<span class="badge bg-{% if entry.user_role == 'admin' %}danger{% elif entry.user_role == 'supervisor' %}warning{% else %}info{% endif %} bg-opacity-75">
|
|
{{ entry.user_role | title }}
|
|
</span>
|
|
</td>
|
|
<td>
|
|
<span class="badge
|
|
{% if entry.action == 'CREATE' %}bg-success
|
|
{% elif entry.action == 'UPDATE' %}bg-primary
|
|
{% elif entry.action == 'DELETE' %}bg-danger
|
|
{% elif entry.action == 'LOGIN' %}bg-secondary
|
|
{% elif entry.action == 'LOGOUT' %}bg-secondary
|
|
{% elif entry.action == 'EXPORT' %}bg-warning text-dark
|
|
{% else %}bg-light text-dark{% endif %}">
|
|
{{ entry.action }}
|
|
</span>
|
|
</td>
|
|
<td class="text-muted small">{{ entry.entity_type }}</td>
|
|
<td class="small">
|
|
{{ entry.entity_label or '—' }}
|
|
{% if entry.entity_id %}
|
|
<span class="text-muted">#{{ entry.entity_id }}</span>
|
|
{% endif %}
|
|
</td>
|
|
<td class="text-muted small font-monospace">
|
|
{{ entry.ip_address or '—' }}
|
|
</td>
|
|
<td>
|
|
<a href="{{ url_for('audit.view', log_id=entry.id) }}"
|
|
class="btn btn-sm btn-outline-secondary py-0 px-2">
|
|
<i class="bi bi-eye"></i>
|
|
</a>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{% else %}
|
|
<div class="text-center py-5 text-muted">
|
|
<i class="bi bi-shield-check fs-2 d-block mb-2"></i>
|
|
No audit records match the current filters.
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<!-- Pagination -->
|
|
{% if logs.pages > 1 %}
|
|
<div class="card-footer bg-light d-flex justify-content-center">
|
|
<nav>
|
|
<ul class="pagination pagination-sm mb-0">
|
|
{% if logs.has_prev %}
|
|
<li class="page-item">
|
|
<a class="page-link" href="{{ url_for('audit.index', page=logs.prev_num,
|
|
user_id=filter_user, action=filter_action,
|
|
entity_type=filter_entity_type,
|
|
date_from=filter_date_from, date_to=filter_date_to) }}">
|
|
« Prev
|
|
</a>
|
|
</li>
|
|
{% endif %}
|
|
{% for p in logs.iter_pages(left_edge=1, right_edge=1, left_current=2, right_current=2) %}
|
|
{% if p %}
|
|
<li class="page-item {% if p == logs.page %}active{% endif %}">
|
|
<a class="page-link" href="{{ url_for('audit.index', page=p,
|
|
user_id=filter_user, action=filter_action,
|
|
entity_type=filter_entity_type,
|
|
date_from=filter_date_from, date_to=filter_date_to) }}">
|
|
{{ p }}
|
|
</a>
|
|
</li>
|
|
{% else %}
|
|
<li class="page-item disabled"><span class="page-link">…</span></li>
|
|
{% endif %}
|
|
{% endfor %}
|
|
{% if logs.has_next %}
|
|
<li class="page-item">
|
|
<a class="page-link" href="{{ url_for('audit.index', page=logs.next_num,
|
|
user_id=filter_user, action=filter_action,
|
|
entity_type=filter_entity_type,
|
|
date_from=filter_date_from, date_to=filter_date_to) }}">
|
|
Next »
|
|
</a>
|
|
</li>
|
|
{% endif %}
|
|
</ul>
|
|
</nav>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<!-- ── Purge Modal ──────────────────────────────────────────────────────────── -->
|
|
<div class="modal fade" id="purgeModal" tabindex="-1" aria-labelledby="purgeModalLabel" aria-hidden="true">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header bg-danger text-white">
|
|
<h5 class="modal-title" id="purgeModalLabel">
|
|
<i class="bi bi-trash me-2"></i>Purge Old Audit Logs
|
|
</h5>
|
|
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<form method="POST" action="{{ url_for('audit.purge') }}">
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
|
<div class="modal-body">
|
|
<div class="alert alert-warning d-flex align-items-center gap-2 mb-3">
|
|
<i class="bi bi-exclamation-triangle-fill fs-5 flex-shrink-0"></i>
|
|
<span>This action is <strong>permanent and irreversible.</strong>
|
|
Deleted log entries cannot be recovered.</span>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label fw-semibold">Delete logs older than</label>
|
|
<select name="older_than" class="form-select" id="purgeOlderThan" required>
|
|
<option value="">— Select a threshold —</option>
|
|
<option value="7">7 days</option>
|
|
<option value="30">30 days</option>
|
|
<option value="60">60 days</option>
|
|
<option value="90">90 days</option>
|
|
<option value="180">180 days</option>
|
|
<option value="365">1 year</option>
|
|
</select>
|
|
</div>
|
|
<p class="text-muted small mb-0">
|
|
All audit log entries created before the selected threshold will be
|
|
permanently deleted. A single audit entry recording this purge will
|
|
be retained.
|
|
</p>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
|
<button type="submit" class="btn btn-danger" id="purgeSubmitBtn" disabled>
|
|
<i class="bi bi-trash me-1"></i>Purge Logs
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
document.getElementById('purgeOlderThan').addEventListener('change', function () {
|
|
document.getElementById('purgeSubmitBtn').disabled = !this.value;
|
|
});
|
|
</script>
|
|
{% endblock %} |