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
This commit is contained in:
simon
2026-04-15 12:42:37 +02:00
parent 52674fee29
commit aa0544a4a5
4 changed files with 266 additions and 2 deletions

View File

@@ -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/<uuid>/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/<uuid>/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)