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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user