From aa0544a4a5c73e3c18f0f7af3f511593ddb93f05 Mon Sep 17 00:00:00 2001 From: simon Date: Wed, 15 Apr 2026 12:42:37 +0200 Subject: [PATCH] modified: web/blueprints/group_admin.py modified: web/templates/admin/audit_log.html new file: web/templates/group_admin/player_delete_confirm.html modified: web/templates/panel/player_detail.html --- web/blueprints/group_admin.py | 174 +++++++++++++++++- web/templates/admin/audit_log.html | 2 + .../group_admin/player_delete_confirm.html | 66 +++++++ web/templates/panel/player_detail.html | 26 +++ 4 files changed, 266 insertions(+), 2 deletions(-) create mode 100644 web/templates/group_admin/player_delete_confirm.html diff --git a/web/blueprints/group_admin.py b/web/blueprints/group_admin.py index 5d2ca24..bc2154f 100644 --- a/web/blueprints/group_admin.py +++ b/web/blueprints/group_admin.py @@ -2,10 +2,15 @@ MCLogger – Gruppen-Admin-Bereich Gruppen-Admins können ihre Mitglieder und MC-DB-Verbindung verwalten. """ +import csv +import io import json +import zipfile from datetime import datetime, timedelta from functools import wraps -from flask import Blueprint, render_template, request, redirect, url_for, session, flash +import pymysql +import pymysql.cursors +from flask import Blueprint, Response, abort, render_template, request, redirect, url_for, session, flash from config import Config from mailer import send_mail, build_invite_email, force_https_url import panel_db as db @@ -337,7 +342,6 @@ def database(): error = "Password is required." else: try: - import pymysql test_conn = pymysql.connect( host=host, port=int(port), user=user, password=password, database=database_name, @@ -372,3 +376,169 @@ def database_delete(): ) flash("Database connection removed.", "success") return redirect(url_for("group_admin.database")) + + +# ────────────────────────────────────────────────────────────── +# GDPR: Spielerdaten – Export & Löschung +# ────────────────────────────────────────────────────────────── + +# Tables and the column name that holds the player UUID +_PLAYER_TABLES = [ + ("player_sessions", "player_uuid"), + ("player_chat", "player_uuid"), + ("player_commands", "player_uuid"), + ("player_deaths", "player_uuid"), + ("player_teleports", "player_uuid"), + ("player_stats", "player_uuid"), + ("block_events", "player_uuid"), + ("proxy_events", "player_uuid"), + ("inventory_events", "player_uuid"), + ("entity_events", "player_uuid"), +] + + +def _get_mc_db(group_id, autocommit: bool = True): + """Open a connection to the group's Minecraft database.""" + creds = db.get_group_db_creds(group_id) + if not creds: + abort(503) + return pymysql.connect( + host=creds["host"], + port=creds["port"], + user=creds["user"], + password=creds["password"], + database=creds["database"], + charset="utf8mb4", + cursorclass=pymysql.cursors.DictCursor, + autocommit=autocommit, + connect_timeout=10, + ) + + +@group_admin.route("/players//export") +@group_admin_required +def player_export(uuid): + """Export all MC data for a player as a ZIP archive (Art. 20 DSGVO).""" + group_id = session["group_id"] + if not db.has_db_configured(group_id): + flash("No database configured for this group.", "danger") + return redirect(url_for("panel.players")) + + try: + conn = _get_mc_db(group_id) + except Exception: + flash("Could not connect to the group database.", "danger") + return redirect(url_for("panel.players")) + + try: + with conn.cursor() as cur: + cur.execute("SELECT * FROM players WHERE uuid = %s", (uuid,)) + player = cur.fetchone() + if not player: + flash("Player not found.", "danger") + return redirect(url_for("panel.players")) + + buf = io.BytesIO() + with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as zf: + # players table (keyed by uuid directly) + csv_buf = io.StringIO() + writer = csv.DictWriter(csv_buf, fieldnames=player.keys()) + writer.writeheader() + writer.writerow(player) + zf.writestr("players.csv", csv_buf.getvalue()) + + for table, col in _PLAYER_TABLES: + try: + with conn.cursor() as cur: + cur.execute(f"SELECT * FROM `{table}` WHERE `{col}` = %s", (uuid,)) + rows = cur.fetchall() + except Exception: + rows = [] + csv_buf = io.StringIO() + if rows: + writer = csv.DictWriter(csv_buf, fieldnames=rows[0].keys()) + writer.writeheader() + writer.writerows(rows) + zf.writestr(f"{table}.csv", csv_buf.getvalue()) + finally: + conn.close() + + db.log_audit_event( + session["user_id"], session["username"], "player.data_exported", + entity_type="mc_player", entity_id=uuid, + details={"player_name": player["username"], "uuid": uuid}, + group_id=group_id, ip_address=request.remote_addr, + ) + + safe_name = "".join(c for c in player["username"] if c.isalnum() or c in "-_") + filename = f"player_{safe_name}_{uuid[:8]}.zip" + return Response( + buf.getvalue(), + mimetype="application/zip", + headers={"Content-Disposition": f"attachment; filename={filename}"}, + ) + + +@group_admin.route("/players//delete", methods=["GET", "POST"]) +@group_admin_required +def player_delete(uuid): + """Permanently delete all MC data for a player (Art. 17 DSGVO). Owner only.""" + if session.get("role") not in OWNER_ONLY_ROLES: + flash("Only the Group Owner can permanently delete player data.", "danger") + return redirect(url_for("panel.player_detail", uuid=uuid)) + + group_id = session["group_id"] + if not db.has_db_configured(group_id): + flash("No database configured for this group.", "danger") + return redirect(url_for("panel.players")) + + try: + conn = _get_mc_db(group_id, autocommit=False) + except Exception: + flash("Could not connect to the group database.", "danger") + return redirect(url_for("panel.players")) + + try: + with conn.cursor() as cur: + cur.execute("SELECT uuid, username FROM players WHERE uuid = %s", (uuid,)) + player = cur.fetchone() + finally: + conn.close() + + if not player: + flash("Player not found.", "danger") + return redirect(url_for("panel.players")) + + group = db.get_group_by_id(group_id) + + if request.method == "POST": + confirm_name = request.form.get("confirm_name", "").strip() + if confirm_name != player["username"]: + flash("Username confirmation did not match. No data was deleted.", "danger") + return redirect(url_for("group_admin.player_delete", uuid=uuid)) + + try: + conn = _get_mc_db(group_id, autocommit=False) + with conn.cursor() as cur: + for table, col in _PLAYER_TABLES: + cur.execute(f"DELETE FROM `{table}` WHERE `{col}` = %s", (uuid,)) + cur.execute("DELETE FROM `players` WHERE `uuid` = %s", (uuid,)) + conn.commit() + except Exception as e: + conn.rollback() + flash(f"Database error during deletion: {e}", "danger") + return redirect(url_for("group_admin.player_delete", uuid=uuid)) + finally: + conn.close() + + db.log_audit_event( + session["user_id"], session["username"], "player.data_deleted", + entity_type="mc_player", entity_id=uuid, + details={"player_name": player["username"], "uuid": uuid}, + group_id=group_id, ip_address=request.remote_addr, + ) + flash(f"All data for '{player['username']}' has been permanently deleted.", "success") + return redirect(url_for("panel.players")) + + return render_template("group_admin/player_delete_confirm.html", + player=player, group=group) diff --git a/web/templates/admin/audit_log.html b/web/templates/admin/audit_log.html index e01e62f..4a80775 100644 --- a/web/templates/admin/audit_log.html +++ b/web/templates/admin/audit_log.html @@ -131,6 +131,8 @@ 'panel.view_proxy': 'badge bg-dark border border-info', 'panel.view_server_events': 'badge bg-dark border border-info', 'panel.view_perms': 'badge bg-dark border border-info', + 'player.data_exported': 'badge bg-info text-dark', + 'player.data_deleted': 'badge bg-danger', } %} {{ row.action }} diff --git a/web/templates/group_admin/player_delete_confirm.html b/web/templates/group_admin/player_delete_confirm.html new file mode 100644 index 0000000..41da317 --- /dev/null +++ b/web/templates/group_admin/player_delete_confirm.html @@ -0,0 +1,66 @@ +{% extends "group_admin/base.html" %} +{% block title %}Delete Player Data{% endblock %} + +{% block content %} +
+
+
+
+ Permanently Delete Player Data +
+
+

+ You are about to permanently delete all logged data for: +

+ +
+ {{ player.username }} +
+
{{ player.username }}
+
{{ player.uuid }}
+
+
+ +

+ This action cannot be undone. +

+
    +
  • Sessions, chat, commands, deaths, teleports
  • +
  • Block events, proxy events, inventory events
  • +
  • Player stats, entity interactions
  • +
  • The player's base record (UUID, username, IP, playtime)
  • +
+ +
+ +
+ + +
+
+ + + Cancel + +
+
+
+
+ +
+ + This deletion is logged in the audit log as required by Art. 5(2) GDPR (accountability). + The export function (Art. 20 GDPR) is available on the player detail page. +
+
+
+{% endblock %} diff --git a/web/templates/panel/player_detail.html b/web/templates/panel/player_detail.html index a2fb5d5..0025f5c 100644 --- a/web/templates/panel/player_detail.html +++ b/web/templates/panel/player_detail.html @@ -140,4 +140,30 @@ Back to Overview + +{% if is_admin and not session.get('is_site_admin') %} +
+
+ GDPR Actions +
+
+
+ + Export Data (Art. 20 GDPR) + +
Download all logged data as ZIP (group admins & owners)
+
+ {% if session.get('role') == 'group_owner' %} +
+ + Delete All Data (Art. 17 GDPR) + +
Permanently erase all player data (owner only)
+
+ {% endif %} +
+
+{% endif %} {% endblock %}