Mar 04 2026: added purge old logs button
This commit is contained in:
+59
-1
@@ -1,9 +1,11 @@
|
|||||||
import logging
|
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 flask_login import login_required
|
||||||
from app.models.audit import AuditLog
|
from app.models.audit import AuditLog
|
||||||
from app.models.user import User
|
from app.models.user import User
|
||||||
from app.utils.decorators import admin_required
|
from app.utils.decorators import admin_required
|
||||||
|
from app.utils.audit import log_action, ACTION_DELETE
|
||||||
|
|
||||||
bp = Blueprint('audit', __name__, url_prefix='/audit')
|
bp = Blueprint('audit', __name__, url_prefix='/audit')
|
||||||
|
|
||||||
@@ -88,5 +90,61 @@ def view(log_id):
|
|||||||
return render_template('audit/view.html', entry=entry)
|
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
|
# Avoid circular import — imported after function definitions
|
||||||
from app import db # noqa: E402
|
from app import db # noqa: E402
|
||||||
@@ -75,7 +75,13 @@
|
|||||||
<span class="badge bg-info ms-1">Filtered</span>
|
<span class="badge bg-info ms-1">Filtered</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</span>
|
</span>
|
||||||
<small class="text-muted">Page {{ logs.page }} of {{ logs.pages }}</small>
|
<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>
|
||||||
<div class="card-body p-0">
|
<div class="card-body p-0">
|
||||||
{% if logs.items %}
|
{% if logs.items %}
|
||||||
@@ -192,4 +198,57 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</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 %}
|
{% endblock %}
|
||||||
Reference in New Issue
Block a user