Mar 04 2026: added purge old logs button
This commit is contained in:
+59
-1
@@ -1,9 +1,11 @@
|
||||
import logging
|
||||
from flask import Blueprint, render_template, request
|
||||
from datetime import datetime, timedelta
|
||||
from flask import Blueprint, render_template, request, redirect, url_for, flash
|
||||
from flask_login import login_required
|
||||
from app.models.audit import AuditLog
|
||||
from app.models.user import User
|
||||
from app.utils.decorators import admin_required
|
||||
from app.utils.audit import log_action, ACTION_DELETE
|
||||
|
||||
bp = Blueprint('audit', __name__, url_prefix='/audit')
|
||||
|
||||
@@ -88,5 +90,61 @@ def view(log_id):
|
||||
return render_template('audit/view.html', entry=entry)
|
||||
|
||||
|
||||
# ── Purge old logs ────────────────────────────────────────────────────────────
|
||||
|
||||
PURGE_OPTIONS = {
|
||||
7: '7 days',
|
||||
30: '30 days',
|
||||
60: '60 days',
|
||||
90: '90 days',
|
||||
180: '180 days',
|
||||
365: '1 year',
|
||||
}
|
||||
|
||||
@bp.route('/purge', methods=['POST'])
|
||||
@login_required
|
||||
@admin_required
|
||||
def purge():
|
||||
"""Delete audit log entries older than the selected threshold.
|
||||
|
||||
Accepts a POST form field `older_than` (integer days).
|
||||
The purge itself is recorded as a new audit log entry so there is
|
||||
always a traceable record of who purged what and when.
|
||||
"""
|
||||
try:
|
||||
older_than = int(request.form.get('older_than', 0))
|
||||
except (ValueError, TypeError):
|
||||
older_than = 0
|
||||
|
||||
if older_than not in PURGE_OPTIONS:
|
||||
flash('Invalid purge threshold selected.', 'danger')
|
||||
return redirect(url_for('audit.index'))
|
||||
|
||||
cutoff = datetime.utcnow() - timedelta(days=older_than)
|
||||
deleted = AuditLog.query.filter(AuditLog.created_at < cutoff).delete()
|
||||
db.session.flush()
|
||||
|
||||
label = PURGE_OPTIONS[older_than]
|
||||
log_action(
|
||||
ACTION_DELETE, 'AuditLog', None,
|
||||
f'Purged {deleted} log entries older than {label}',
|
||||
f'cutoff={cutoff.strftime("%Y-%m-%d %H:%M:%S")} UTC; deleted={deleted}',
|
||||
)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
logger.info(
|
||||
'AUDIT PURGE | deleted=%s | older_than=%s days | by=%s',
|
||||
deleted, older_than, request.remote_addr,
|
||||
)
|
||||
|
||||
flash(
|
||||
f'{deleted} audit log entr{"ies" if deleted != 1 else "y"} '
|
||||
f'older than {label} have been permanently deleted.',
|
||||
'success' if deleted else 'info',
|
||||
)
|
||||
return redirect(url_for('audit.index'))
|
||||
|
||||
|
||||
# Avoid circular import — imported after function definitions
|
||||
from app import db # noqa: E402
|
||||
@@ -75,7 +75,13 @@
|
||||
<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 %}
|
||||
@@ -192,4 +198,57 @@
|
||||
</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 %}
|
||||
Reference in New Issue
Block a user