modified: web/blueprints/auth.py
modified: web/blueprints/group_admin.py modified: web/blueprints/site_admin.py modified: web/config.py modified: web/panel_db.py modified: web/templates/admin/audit_log.html
This commit is contained in:
@@ -85,6 +85,10 @@ def admin_login():
|
|||||||
|
|
||||||
@auth.route("/logout", methods=["POST"])
|
@auth.route("/logout", methods=["POST"])
|
||||||
def logout():
|
def logout():
|
||||||
|
user_id = session.get("user_id")
|
||||||
|
username = session.get("username")
|
||||||
|
if user_id:
|
||||||
|
log_audit_event(user_id, username, "session.logout", ip_address=request.remote_addr)
|
||||||
session.clear()
|
session.clear()
|
||||||
return redirect(url_for("auth.login"))
|
return redirect(url_for("auth.login"))
|
||||||
|
|
||||||
|
|||||||
@@ -345,6 +345,12 @@ def database():
|
|||||||
)
|
)
|
||||||
test_conn.close()
|
test_conn.close()
|
||||||
db.set_group_db_creds(group_id, host, int(port), user, password, database_name)
|
db.set_group_db_creds(group_id, host, int(port), user, password, database_name)
|
||||||
|
db.log_audit_event(
|
||||||
|
session["user_id"], session["username"], "db.credentials_changed",
|
||||||
|
entity_type="group", entity_id=group_id,
|
||||||
|
details={"host": host, "port": port, "database": database_name},
|
||||||
|
group_id=group_id, ip_address=request.remote_addr,
|
||||||
|
)
|
||||||
flash("Database connection saved and tested ✓", "success")
|
flash("Database connection saved and tested ✓", "success")
|
||||||
return redirect(url_for("group_admin.database"))
|
return redirect(url_for("group_admin.database"))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -357,6 +363,12 @@ def database():
|
|||||||
@group_admin.route("/database/delete", methods=["POST"])
|
@group_admin.route("/database/delete", methods=["POST"])
|
||||||
@group_admin_required
|
@group_admin_required
|
||||||
def database_delete():
|
def database_delete():
|
||||||
db.delete_group_db_creds(session["group_id"])
|
group_id = session["group_id"]
|
||||||
|
db.delete_group_db_creds(group_id)
|
||||||
|
db.log_audit_event(
|
||||||
|
session["user_id"], session["username"], "db.credentials_deleted",
|
||||||
|
entity_type="group", entity_id=group_id,
|
||||||
|
group_id=group_id, ip_address=request.remote_addr,
|
||||||
|
)
|
||||||
flash("Database connection removed.", "success")
|
flash("Database connection removed.", "success")
|
||||||
return redirect(url_for("group_admin.database"))
|
return redirect(url_for("group_admin.database"))
|
||||||
|
|||||||
@@ -216,6 +216,11 @@ def group_delete(group_id):
|
|||||||
@site_admin.route("/groups/<int:group_id>/members")
|
@site_admin.route("/groups/<int:group_id>/members")
|
||||||
@admin_required
|
@admin_required
|
||||||
def group_members(group_id):
|
def group_members(group_id):
|
||||||
|
db.log_audit_event(
|
||||||
|
session["user_id"], session["username"], "admin.view_group_members",
|
||||||
|
entity_type="group", entity_id=group_id,
|
||||||
|
ip_address=request.remote_addr,
|
||||||
|
)
|
||||||
group = db.get_group_by_id(group_id)
|
group = db.get_group_by_id(group_id)
|
||||||
members = db.get_group_members(group_id)
|
members = db.get_group_members(group_id)
|
||||||
pending_invites = db.list_active_group_invites(group_id)
|
pending_invites = db.list_active_group_invites(group_id)
|
||||||
@@ -424,6 +429,10 @@ def group_invite_resend(group_id, invite_id):
|
|||||||
@site_admin.route("/users")
|
@site_admin.route("/users")
|
||||||
@admin_required
|
@admin_required
|
||||||
def users():
|
def users():
|
||||||
|
db.log_audit_event(
|
||||||
|
session["user_id"], session["username"], "admin.view_users",
|
||||||
|
ip_address=request.remote_addr,
|
||||||
|
)
|
||||||
return render_template(
|
return render_template(
|
||||||
"admin/users.html",
|
"admin/users.html",
|
||||||
users=db.list_all_users(),
|
users=db.list_all_users(),
|
||||||
@@ -597,6 +606,12 @@ def user_edit(user_id):
|
|||||||
db.update_user(user_id, username, email, is_site_admin)
|
db.update_user(user_id, username, email, is_site_admin)
|
||||||
if new_password:
|
if new_password:
|
||||||
db.change_password(user_id, new_password)
|
db.change_password(user_id, new_password)
|
||||||
|
db.log_audit_event(
|
||||||
|
session["user_id"], session["username"], "user.password_changed",
|
||||||
|
entity_type="user", entity_id=user_id,
|
||||||
|
details={"target": username},
|
||||||
|
ip_address=request.remote_addr,
|
||||||
|
)
|
||||||
flash("Password changed.", "info")
|
flash("Password changed.", "info")
|
||||||
db.log_audit_event(
|
db.log_audit_event(
|
||||||
session["user_id"], session["username"], "user.updated",
|
session["user_id"], session["username"], "user.updated",
|
||||||
@@ -715,4 +730,18 @@ def audit_log():
|
|||||||
actor_filter=actor_f or "",
|
actor_filter=actor_f or "",
|
||||||
all_groups=all_groups,
|
all_groups=all_groups,
|
||||||
actions=actions,
|
actions=actions,
|
||||||
|
retention_days=Config.AUDIT_LOG_RETENTION_DAYS,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@site_admin.route("/audit/purge", methods=["POST"])
|
||||||
|
@admin_required
|
||||||
|
def audit_purge():
|
||||||
|
deleted = db.purge_old_audit_events(Config.AUDIT_LOG_RETENTION_DAYS)
|
||||||
|
db.log_audit_event(
|
||||||
|
session["user_id"], session["username"], "audit.purged",
|
||||||
|
details={"deleted_count": deleted, "retention_days": Config.AUDIT_LOG_RETENTION_DAYS},
|
||||||
|
ip_address=request.remote_addr,
|
||||||
|
)
|
||||||
|
flash(f"Purged {deleted} audit log entries older than {Config.AUDIT_LOG_RETENTION_DAYS} days.", "success")
|
||||||
|
return redirect(url_for("site_admin.audit_log"))
|
||||||
|
|||||||
@@ -58,6 +58,12 @@ class Config:
|
|||||||
INVITE_MAX_ACTIVE_PER_GROUP = int(os.getenv("INVITE_MAX_ACTIVE_PER_GROUP") or "200")
|
INVITE_MAX_ACTIVE_PER_GROUP = int(os.getenv("INVITE_MAX_ACTIVE_PER_GROUP") or "200")
|
||||||
INVITE_RESEND_COOLDOWN_SECONDS = int(os.getenv("INVITE_RESEND_COOLDOWN_SECONDS") or "120")
|
INVITE_RESEND_COOLDOWN_SECONDS = int(os.getenv("INVITE_RESEND_COOLDOWN_SECONDS") or "120")
|
||||||
|
|
||||||
|
# ── Audit-Log-Aufbewahrung ────────────────────────────────
|
||||||
|
# Audit-Log-Einträge, die älter als dieser Wert (in Tagen) sind, werden automatisch gelöscht.
|
||||||
|
# IP-Adressen gelten als personenbezogene Daten (DSGVO Art. 4 Nr. 1); nach 90 Tagen sollten
|
||||||
|
# diese nicht mehr benötigt werden. Auf 0 setzen, um automatisches Löschen zu deaktivieren.
|
||||||
|
AUDIT_LOG_RETENTION_DAYS = int(os.getenv("AUDIT_LOG_RETENTION_DAYS") or "90")
|
||||||
|
|
||||||
DEFAULT_PERMISSIONS = {
|
DEFAULT_PERMISSIONS = {
|
||||||
"view_dashboard": True,
|
"view_dashboard": True,
|
||||||
"view_players": True,
|
"view_players": True,
|
||||||
|
|||||||
@@ -237,6 +237,9 @@ def init_databases():
|
|||||||
finally:
|
finally:
|
||||||
creds.close()
|
creds.close()
|
||||||
|
|
||||||
|
# Auto-Bereinigung: Audit-Log-Einträge älter als Retention-Tage löschen
|
||||||
|
purge_old_audit_events(Config.AUDIT_LOG_RETENTION_DAYS)
|
||||||
|
|
||||||
|
|
||||||
# ─────────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────────
|
||||||
# Nutzer
|
# Nutzer
|
||||||
@@ -812,3 +815,30 @@ def get_audit_log_distinct_actions() -> list[str]:
|
|||||||
rows = _panel_query("SELECT DISTINCT action FROM audit_log ORDER BY action")
|
rows = _panel_query("SELECT DISTINCT action FROM audit_log ORDER BY action")
|
||||||
return [r["action"] for r in rows] if rows else []
|
return [r["action"] for r in rows] if rows else []
|
||||||
|
|
||||||
|
|
||||||
|
def purge_old_audit_events(retention_days: int) -> int:
|
||||||
|
"""Deletes audit log entries older than *retention_days* days.
|
||||||
|
Returns the number of deleted rows. Skips if retention_days <= 0."""
|
||||||
|
import logging
|
||||||
|
_log = logging.getLogger(__name__)
|
||||||
|
if retention_days <= 0:
|
||||||
|
return 0
|
||||||
|
try:
|
||||||
|
conn = get_panel_db()
|
||||||
|
try:
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute(
|
||||||
|
"DELETE FROM audit_log WHERE created_at < UTC_TIMESTAMP() - INTERVAL %s DAY",
|
||||||
|
(retention_days,),
|
||||||
|
)
|
||||||
|
deleted = cur.rowcount
|
||||||
|
conn.commit()
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
if deleted:
|
||||||
|
_log.info("Purged %d audit log entries older than %d days", deleted, retention_days)
|
||||||
|
return deleted
|
||||||
|
except Exception as exc:
|
||||||
|
_log.warning("Failed to purge audit log: %s", exc)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,22 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="d-flex align-items-center justify-content-between mb-3">
|
<div class="d-flex align-items-center justify-content-between mb-3">
|
||||||
<h4 class="mb-0"><i class="bi bi-journal-text me-2"></i>Audit Log</h4>
|
<h4 class="mb-0"><i class="bi bi-journal-text me-2"></i>Audit Log</h4>
|
||||||
<span class="text-muted small">{{ total }} event{{ 's' if total != 1 }}</span>
|
<div class="d-flex align-items-center gap-3">
|
||||||
|
<span class="text-muted small">{{ total }} event{{ 's' if total != 1 }}</span>
|
||||||
|
{% if retention_days > 0 %}
|
||||||
|
<span class="badge bg-secondary" title="Entries older than {{ retention_days }} days are deleted automatically on startup">
|
||||||
|
<i class="bi bi-clock-history me-1"></i>Retention: {{ retention_days }}d
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
<form method="post" action="{{ url_for('site_admin.audit_purge') }}"
|
||||||
|
onsubmit="return confirm('Delete all audit entries older than {{ retention_days }} days?')">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ session.get('csrf_token', '') }}">
|
||||||
|
<button type="submit" class="btn btn-sm btn-outline-danger"
|
||||||
|
{% if retention_days <= 0 %}disabled title="Retention disabled (AUDIT_LOG_RETENTION_DAYS=0)"{% endif %}>
|
||||||
|
<i class="bi bi-trash3 me-1"></i>Purge now
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Filters -->
|
<!-- Filters -->
|
||||||
@@ -75,25 +90,33 @@
|
|||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{% set action_class = {
|
{% set action_class = {
|
||||||
'user.login': 'badge bg-success',
|
'user.login': 'badge bg-success',
|
||||||
'user.login_failed': 'badge bg-danger',
|
'user.login_failed': 'badge bg-danger',
|
||||||
'admin.login': 'badge bg-warning text-dark',
|
'user.password_changed': 'badge bg-warning text-dark',
|
||||||
'admin.login_failed': 'badge bg-danger',
|
'session.logout': 'badge bg-secondary',
|
||||||
'invite.created': 'badge bg-primary',
|
'admin.login': 'badge bg-warning text-dark',
|
||||||
'invite.accepted': 'badge bg-success',
|
'admin.login_failed': 'badge bg-danger',
|
||||||
'invite.revoked': 'badge bg-secondary',
|
'admin.view_users': 'badge bg-dark border border-secondary',
|
||||||
'invite.resent': 'badge bg-info text-dark',
|
'admin.view_group': 'badge bg-dark border border-secondary',
|
||||||
'member.added': 'badge bg-primary',
|
'admin.view_group_members': 'badge bg-dark border border-secondary',
|
||||||
'member.removed': 'badge bg-danger',
|
'invite.created': 'badge bg-primary',
|
||||||
'member.role_changed': 'badge bg-warning text-dark',
|
'invite.accepted': 'badge bg-success',
|
||||||
'member.updated': 'badge bg-warning text-dark',
|
'invite.revoked': 'badge bg-secondary',
|
||||||
'group.created': 'badge bg-success',
|
'invite.resent': 'badge bg-info text-dark',
|
||||||
'group.updated': 'badge bg-secondary',
|
'member.added': 'badge bg-primary',
|
||||||
'group.deleted': 'badge bg-danger',
|
'member.removed': 'badge bg-danger',
|
||||||
'user.updated': 'badge bg-secondary',
|
'member.role_changed': 'badge bg-warning text-dark',
|
||||||
'user.deleted': 'badge bg-danger',
|
'member.updated': 'badge bg-warning text-dark',
|
||||||
'mail.settings_saved': 'badge bg-info text-dark',
|
'group.created': 'badge bg-success',
|
||||||
'mail.settings_deleted':'badge bg-danger',
|
'group.updated': 'badge bg-secondary',
|
||||||
|
'group.deleted': 'badge bg-danger',
|
||||||
|
'db.credentials_changed': 'badge bg-warning text-dark',
|
||||||
|
'db.credentials_deleted': 'badge bg-danger',
|
||||||
|
'user.updated': 'badge bg-secondary',
|
||||||
|
'user.deleted': 'badge bg-danger',
|
||||||
|
'mail.settings_saved': 'badge bg-info text-dark',
|
||||||
|
'mail.settings_deleted': 'badge bg-danger',
|
||||||
|
'audit.purged': 'badge bg-danger',
|
||||||
} %}
|
} %}
|
||||||
<span class="{{ action_class.get(row.action, 'badge bg-secondary') }} font-monospace" style="font-size:.75em">
|
<span class="{{ action_class.get(row.action, 'badge bg-secondary') }} font-monospace" style="font-size:.75em">
|
||||||
{{ row.action }}
|
{{ row.action }}
|
||||||
|
|||||||
Reference in New Issue
Block a user