# blueprints/__init__.py """ MCLogger – Authentifizierung Getrennte Login-Seiten für Site-Admins und normale Nutzer/Gruppen-Admins. """ import json from flask import Blueprint, render_template, request, redirect, url_for, session, flash from panel_db import check_login, get_user_groups auth = Blueprint("auth", __name__) @auth.route("/login", methods=["GET", "POST"]) def login(): if session.get("user_id"): return redirect(url_for("panel.dashboard")) error = None if request.method == "POST": user = check_login(request.form.get("username", ""), request.form.get("password", "")) if user and user["is_site_admin"]: flash("Bitte nutze den Site-Admin-Login.", "warning") return redirect(url_for("auth.admin_login")) if user: groups = get_user_groups(user["id"]) if not groups: error = "Du bist keiner Gruppe zugewiesen. Wende dich an einen Admin." else: _set_user_session(user, groups) return redirect(url_for("panel.dashboard")) else: error = "Falscher Benutzername oder Passwort." return render_template("auth/login.html", error=error) @auth.route("/admin/login", methods=["GET", "POST"]) def admin_login(): if session.get("is_site_admin"): return redirect(url_for("site_admin.dashboard")) error = None if request.method == "POST": user = check_login(request.form.get("username", ""), request.form.get("password", "")) if user and user["is_site_admin"]: session["user_id"] = user["id"] session["username"] = user["username"] session["is_site_admin"] = True session["group_id"] = None session["permissions"] = {} return redirect(url_for("site_admin.dashboard")) elif user: error = "Keine Site-Admin-Berechtigung." else: error = "Falscher Benutzername oder Passwort." return render_template("auth/admin_login.html", error=error) @auth.route("/logout") def logout(): session.clear() return redirect(url_for("auth.login")) @auth.route("/switch-group/") def switch_group(group_id): if not session.get("user_id") or session.get("is_site_admin"): return redirect(url_for("auth.login")) user_id = session["user_id"] groups = get_user_groups(user_id) target = next((g for g in groups if g["id"] == group_id), None) if not target: flash("Gruppe nicht gefunden oder kein Zugriff.", "danger") return redirect(url_for("panel.dashboard")) _apply_group(target) return redirect(url_for("panel.dashboard")) def _set_user_session(user, groups): session["user_id"] = user["id"] session["username"] = user["username"] session["is_site_admin"] = False _apply_group(groups[0]) # Erste Gruppe als Standard def _apply_group(group): raw = group.get("permissions") if isinstance(raw, str): perms = json.loads(raw) elif isinstance(raw, dict): perms = raw else: perms = {} session["group_id"] = group["id"] session["group_name"] = group["name"] session["role"] = group.get("role", "member") session["permissions"] = perms """ MCLogger – Gruppen-Admin-Bereich Gruppen-Admins können ihre Mitglieder und MC-DB-Verbindung verwalten. """ import json from functools import wraps from flask import Blueprint, render_template, request, redirect, url_for, session, flash import panel_db as db group_admin = Blueprint("group_admin", __name__, url_prefix="/group-admin") ALL_PERMISSIONS = [ ("view_dashboard", "Dashboard"), ("view_players", "Spieler"), ("view_sessions", "Sessions"), ("view_chat", "Chat"), ("view_commands", "Commands"), ("view_deaths", "Tode"), ("view_blocks", "Block-Events"), ("view_proxy", "Proxy-Events"), ("view_server_events", "Server-Events"), ("view_perms", "Berechtigungen"), ] def group_admin_required(f): @wraps(f) def decorated(*args, **kwargs): if not session.get("user_id"): return redirect(url_for("auth.login")) if session.get("is_site_admin"): return redirect(url_for("site_admin.dashboard")) if session.get("role") != "admin": flash("Du hast keine Gruppen-Admin-Berechtigung.", "danger") return redirect(url_for("panel.dashboard")) return f(*args, **kwargs) return decorated @group_admin.route("/") @group_admin_required def dashboard(): group_id = session["group_id"] group = db.get_group_by_id(group_id) members = db.get_group_members(group_id) has_db = db.has_db_configured(group_id) return render_template("group_admin/dashboard.html", group=group, members=members, has_db=has_db) # ────────────────────────────────────────────────────────────── # Mitglieder # ────────────────────────────────────────────────────────────── @group_admin.route("/members") @group_admin_required def members(): group_id = session["group_id"] group = db.get_group_by_id(group_id) members = db.get_group_members(group_id) all_users = db.list_all_users() member_ids = {m["id"] for m in members} non_members = [u for u in all_users if u["id"] not in member_ids and not u["is_site_admin"]] return render_template("group_admin/members.html", group=group, members=members, non_members=non_members, all_permissions=ALL_PERMISSIONS) @group_admin.route("/members/add", methods=["POST"]) @group_admin_required def member_add(): group_id = session["group_id"] user_id = request.form.get("user_id", type=int) role = request.form.get("role", "member") if user_id: db.add_group_member(user_id, group_id, role) flash("Mitglied hinzugefügt.", "success") return redirect(url_for("group_admin.members")) @group_admin.route("/members//edit", methods=["GET", "POST"]) @group_admin_required def member_edit(user_id): group_id = session["group_id"] group = db.get_group_by_id(group_id) member = db.get_group_member(user_id, group_id) user = db.get_user_by_id(user_id) if not member or not user: flash("Mitglied nicht gefunden.", "danger") return redirect(url_for("group_admin.members")) raw_perms = member.get("permissions") current_perms = json.loads(raw_perms) if isinstance(raw_perms, str) else (raw_perms or {}) if request.method == "POST": role = request.form.get("role", "member") new_perms = {key: (request.form.get(key) == "1") for key, _ in ALL_PERMISSIONS} db.update_member(user_id, group_id, role, new_perms) flash("Berechtigungen aktualisiert.", "success") return redirect(url_for("group_admin.members")) return render_template("group_admin/member_edit.html", group=group, user=user, member=member, current_perms=current_perms, all_permissions=ALL_PERMISSIONS) @group_admin.route("/members//remove", methods=["POST"]) @group_admin_required def member_remove(user_id): if user_id == session["user_id"]: flash("Du kannst dich nicht selbst entfernen.", "danger") else: db.remove_group_member(user_id, session["group_id"]) flash("Mitglied entfernt.", "success") return redirect(url_for("group_admin.members")) # ────────────────────────────────────────────────────────────── # Datenbank-Konfiguration # ────────────────────────────────────────────────────────────── @group_admin.route("/database", methods=["GET", "POST"]) @group_admin_required def database(): group_id = session["group_id"] group = db.get_group_by_id(group_id) has_db = db.has_db_configured(group_id) error = None if request.method == "POST": host = request.form.get("host", "").strip() port = request.form.get("port", "3306").strip() user = request.form.get("user", "").strip() password = request.form.get("password", "") database_name = request.form.get("database", "").strip() if not all([host, port, user, database_name]): error = "Host, Port, Benutzer und Datenbankname sind Pflichtfelder." else: try: # Verbindung testen import pymysql test = pymysql.connect( host=host, port=int(port), user=user, password=password, database=database_name, connect_timeout=5 ) test.close() db.set_group_db_creds(group_id, host, int(port), user, password, database_name) flash("Datenbankverbindung gespeichert und getestet ✓", "success") return redirect(url_for("group_admin.database")) except Exception as e: error = f"Verbindungstest fehlgeschlagen: {e}" return render_template("group_admin/database.html", group=group, has_db=has_db, error=error) @group_admin.route("/database/delete", methods=["POST"]) @group_admin_required def database_delete(): db.delete_group_db_creds(session["group_id"]) flash("Datenbankverbindung entfernt.", "success") return redirect(url_for("group_admin.database")) """ MCLogger – Panel (MC-Daten) Zeigt die Minecraft-Logdaten der Gruppe an. Die Datenbankverbindung kommt aus den verschlüsselten Gruppen-Credentials. """ from functools import wraps from datetime import datetime from flask import Blueprint, render_template, request, redirect, url_for, session, flash, jsonify, abort import pymysql import pymysql.cursors import panel_db as pdb panel = Blueprint("panel", __name__) # ───────────────────────────────────────────────────────────── # Hilfsfunktionen # ───────────────────────────────────────────────────────────── def login_required(f): @wraps(f) def decorated(*args, **kwargs): if not session.get("user_id"): return redirect(url_for("auth.login")) if session.get("is_site_admin") and not session.get("group_id"): return redirect(url_for("site_admin.dashboard")) if not session.get("group_id"): return redirect(url_for("auth.login")) return f(*args, **kwargs) return decorated def perm_required(perm): def decorator(f): @wraps(f) def wrapped(*args, **kwargs): if session.get("is_site_admin") or session.get("role") == "admin": return f(*args, **kwargs) perms = session.get("permissions", {}) if not perms.get(perm, False): flash("Du hast keine Berechtigung, diese Seite zu sehen.", "danger") return redirect(url_for("panel.dashboard")) return f(*args, **kwargs) return wrapped return decorator def get_mc_db(): """Liefert eine Datenbankverbindung zur MC-Datenbank der aktuellen Gruppe.""" group_id = session.get("group_id") if not group_id: abort(403) creds = pdb.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=True, connect_timeout=10, ) def query(sql, args=None, fetchone=False): conn = get_mc_db() try: with conn.cursor() as cur: cur.execute(sql, args or ()) return cur.fetchone() if fetchone else cur.fetchall() finally: conn.close() def query_paged(sql, count_sql, args=None, page=1, per_page=50): args = args or () total_row = query(count_sql, args, fetchone=True) total = list(total_row.values())[0] if total_row else 0 pages = max(1, (total + per_page - 1) // per_page) offset = (page - 1) * per_page rows = query(sql + f" LIMIT {per_page} OFFSET {offset}", args) return rows, total, pages # ───────────────────────────────────────────────────────────── # Fehler-Handler wenn DB nicht konfiguriert # ───────────────────────────────────────────────────────────── @panel.errorhandler(503) def no_db(e): return render_template("panel/no_db.html"), 503 # ───────────────────────────────────────────────────────────── # Dashboard # ───────────────────────────────────────────────────────────── @panel.route("/") @login_required @perm_required("view_dashboard") def dashboard(): group_id = session["group_id"] if not pdb.has_db_configured(group_id): return render_template("panel/no_db.html") try: stats = { "players_total": query("SELECT COUNT(*) AS c FROM players", fetchone=True)["c"], "sessions_today": query("SELECT COUNT(*) AS c FROM player_sessions WHERE login_time >= CURDATE()", fetchone=True)["c"], "chat_today": query("SELECT COUNT(*) AS c FROM player_chat WHERE timestamp >= CURDATE()", fetchone=True)["c"], "commands_today": query("SELECT COUNT(*) AS c FROM player_commands WHERE timestamp >= CURDATE()", fetchone=True)["c"], "blocks_today": query("SELECT COUNT(*) AS c FROM block_events WHERE timestamp >= CURDATE()", fetchone=True)["c"], "deaths_today": query("SELECT COUNT(*) AS c FROM player_deaths WHERE timestamp >= CURDATE()", fetchone=True)["c"], "proxy_events_today": query("SELECT COUNT(*) AS c FROM proxy_events WHERE timestamp >= CURDATE()", fetchone=True)["c"], } online = query(""" SELECT p.username, ps.server_name, ps.login_time, ps.country FROM player_sessions ps JOIN players p ON p.uuid = ps.player_uuid WHERE ps.logout_time IS NULL ORDER BY ps.login_time DESC """) top_players = query(""" SELECT username, total_playtime_sec FROM players ORDER BY total_playtime_sec DESC LIMIT 10 """) death_causes = query(""" SELECT cause, COUNT(*) AS cnt FROM player_deaths WHERE timestamp >= NOW() - INTERVAL 7 DAY GROUP BY cause ORDER BY cnt DESC LIMIT 8 """) server_events = query(""" SELECT timestamp, event_type, server_name, message FROM server_events WHERE timestamp >= NOW() - INTERVAL 24 HOUR ORDER BY timestamp DESC LIMIT 20 """) except Exception as e: flash(f"Datenbankfehler: {e}", "danger") return render_template("panel/no_db.html") return render_template("panel/dashboard.html", stats=stats, online=online, top_players=top_players, death_causes=death_causes, server_events=server_events) # ───────────────────────────────────────────────────────────── # Spieler # ───────────────────────────────────────────────────────────── @panel.route("/players") @login_required @perm_required("view_players") def players(): search = request.args.get("q", "") page = max(1, request.args.get("page", 1, type=int)) if search: base = "FROM players WHERE username LIKE %s" args = (f"%{search}%",) else: base = "FROM players WHERE 1" args = () rows, total, pages = query_paged( f"SELECT * {base} ORDER BY last_seen DESC", f"SELECT COUNT(*) AS c {base}", args, page) return render_template("panel/players.html", players=rows, total=total, pages=pages, page=page, search=search) @panel.route("/players/") @login_required @perm_required("view_players") def player_detail(uuid): player = query("SELECT * FROM players WHERE uuid = %s", (uuid,), fetchone=True) if not player: flash("Spieler nicht gefunden.", "danger") return redirect(url_for("panel.players")) perms = session.get("permissions", {}) is_admin = session.get("is_site_admin") or session.get("role") == "admin" return render_template("panel/player_detail.html", player=player, sessions = query("SELECT * FROM player_sessions WHERE player_uuid=%s ORDER BY login_time DESC LIMIT 20", (uuid,)), chat = query("SELECT * FROM player_chat WHERE player_uuid=%s ORDER BY timestamp DESC LIMIT 50", (uuid,)) if (is_admin or perms.get("view_chat")) else [], commands = query("SELECT * FROM player_commands WHERE player_uuid=%s ORDER BY timestamp DESC LIMIT 50", (uuid,)) if (is_admin or perms.get("view_commands")) else [], deaths = query("SELECT * FROM player_deaths WHERE player_uuid=%s ORDER BY timestamp DESC LIMIT 20", (uuid,)) if (is_admin or perms.get("view_deaths")) else [], teleports = query("SELECT * FROM player_teleports WHERE player_uuid=%s ORDER BY timestamp DESC LIMIT 20", (uuid,)), stats = query("SELECT * FROM player_stats WHERE player_uuid=%s ORDER BY timestamp DESC LIMIT 30", (uuid,)), proxy_events = query("SELECT * FROM proxy_events WHERE player_uuid=%s ORDER BY timestamp DESC LIMIT 30", (uuid,)) if (is_admin or perms.get("view_proxy")) else [], ) # ───────────────────────────────────────────────────────────── # Sessions # ───────────────────────────────────────────────────────────── @panel.route("/sessions") @login_required @perm_required("view_sessions") def sessions(): page = max(1, request.args.get("page", 1, type=int)) player = request.args.get("player", "") server = request.args.get("server", "") conditions, args = [], [] if player: conditions.append("player_name LIKE %s"); args.append(f"%{player}%") if server: conditions.append("server_name = %s"); args.append(server) where = ("WHERE " + " AND ".join(conditions)) if conditions else "" rows, total, pages = query_paged( f"SELECT * FROM player_sessions {where} ORDER BY login_time DESC", f"SELECT COUNT(*) AS c FROM player_sessions {where}", tuple(args), page) servers = [r["server_name"] for r in query("SELECT DISTINCT server_name FROM player_sessions ORDER BY server_name")] return render_template("panel/sessions.html", rows=rows, total=total, pages=pages, page=page, player=player, server=server, servers=servers) # ───────────────────────────────────────────────────────────── # Chat # ───────────────────────────────────────────────────────────── @panel.route("/chat") @login_required @perm_required("view_chat") def chat(): page = max(1, request.args.get("page", 1, type=int)) search = request.args.get("q", ""); server = request.args.get("server", "") date_from = request.args.get("from", ""); date_to = request.args.get("to", "") conditions, args = [], [] if search: conditions.append("message LIKE %s"); args.append(f"%{search}%") if server: conditions.append("server_name = %s"); args.append(server) if date_from: conditions.append("timestamp >= %s"); args.append(date_from) if date_to: conditions.append("timestamp <= %s"); args.append(date_to + " 23:59:59") where = ("WHERE " + " AND ".join(conditions)) if conditions else "" rows, total, pages = query_paged( f"SELECT * FROM player_chat {where} ORDER BY timestamp DESC", f"SELECT COUNT(*) AS c FROM player_chat {where}", tuple(args), page) servers = [r["server_name"] for r in query("SELECT DISTINCT server_name FROM player_chat ORDER BY server_name")] return render_template("panel/chat.html", rows=rows, total=total, pages=pages, page=page, search=search, server=server, servers=servers, date_from=date_from, date_to=date_to) # ───────────────────────────────────────────────────────────── # Commands # ───────────────────────────────────────────────────────────── @panel.route("/commands") @login_required @perm_required("view_commands") def commands(): page = max(1, request.args.get("page", 1, type=int)) player = request.args.get("player", ""); search = request.args.get("q", ""); server = request.args.get("server", "") conditions, args = [], [] if player: conditions.append("player_name LIKE %s"); args.append(f"%{player}%") if search: conditions.append("command LIKE %s"); args.append(f"%{search}%") if server: conditions.append("server_name = %s"); args.append(server) where = ("WHERE " + " AND ".join(conditions)) if conditions else "" rows, total, pages = query_paged( f"SELECT * FROM player_commands {where} ORDER BY timestamp DESC", f"SELECT COUNT(*) AS c FROM player_commands {where}", tuple(args), page) servers = [r["server_name"] for r in query("SELECT DISTINCT server_name FROM player_commands ORDER BY server_name")] return render_template("panel/commands.html", rows=rows, total=total, pages=pages, page=page, player=player, search=search, server=server, servers=servers) # ───────────────────────────────────────────────────────────── # Tode # ───────────────────────────────────────────────────────────── @panel.route("/deaths") @login_required @perm_required("view_deaths") def deaths(): page = max(1, request.args.get("page", 1, type=int)) player = request.args.get("player", ""); cause = request.args.get("cause", "") conditions, args = [], [] if player: conditions.append("player_name LIKE %s"); args.append(f"%{player}%") if cause: conditions.append("cause = %s"); args.append(cause) where = ("WHERE " + " AND ".join(conditions)) if conditions else "" rows, total, pages = query_paged( f"SELECT * FROM player_deaths {where} ORDER BY timestamp DESC", f"SELECT COUNT(*) AS c FROM player_deaths {where}", tuple(args), page) causes = [r["cause"] for r in query("SELECT DISTINCT cause FROM player_deaths ORDER BY cause")] return render_template("panel/deaths.html", rows=rows, total=total, pages=pages, page=page, player=player, cause=cause, causes=causes) # ───────────────────────────────────────────────────────────── # Block-Events # ───────────────────────────────────────────────────────────── @panel.route("/blocks") @login_required @perm_required("view_blocks") def blocks(): page = max(1, request.args.get("page", 1, type=int)) event_type = request.args.get("type", ""); player = request.args.get("player", "") world = request.args.get("world", ""); server = request.args.get("server", ""); block = request.args.get("block", "") conditions, args = [], [] if event_type: conditions.append("event_type = %s"); args.append(event_type) if player: conditions.append("player_name LIKE %s"); args.append(f"%{player}%") if world: conditions.append("world = %s"); args.append(world) if server: conditions.append("server_name = %s"); args.append(server) if block: conditions.append("block_type LIKE %s"); args.append(f"%{block}%") where = ("WHERE " + " AND ".join(conditions)) if conditions else "" rows, total, pages = query_paged( f"SELECT * FROM block_events {where} ORDER BY timestamp DESC", f"SELECT COUNT(*) AS c FROM block_events {where}", tuple(args), page) worlds = [r["world"] for r in query("SELECT DISTINCT world FROM block_events ORDER BY world")] servers = [r["server_name"] for r in query("SELECT DISTINCT server_name FROM block_events ORDER BY server_name")] return render_template("panel/blocks.html", rows=rows, total=total, pages=pages, page=page, event_type=event_type, player=player, world=world, server=server, block=block, worlds=worlds, servers=servers) # ───────────────────────────────────────────────────────────── # Proxy-Events # ───────────────────────────────────────────────────────────── @panel.route("/proxy") @login_required @perm_required("view_proxy") def proxy(): page = max(1, request.args.get("page", 1, type=int)) event_type = request.args.get("type", ""); player = request.args.get("player", "") conditions, args = [], [] if event_type: conditions.append("event_type = %s"); args.append(event_type) if player: conditions.append("player_name LIKE %s"); args.append(f"%{player}%") where = ("WHERE " + " AND ".join(conditions)) if conditions else "" rows, total, pages = query_paged( f"SELECT * FROM proxy_events {where} ORDER BY timestamp DESC", f"SELECT COUNT(*) AS c FROM proxy_events {where}", tuple(args), page) return render_template("panel/proxy.html", rows=rows, total=total, pages=pages, page=page, event_type=event_type, player=player) # ───────────────────────────────────────────────────────────── # Server-Events # ───────────────────────────────────────────────────────────── @panel.route("/server-events") @login_required @perm_required("view_server_events") def server_events(): page = max(1, request.args.get("page", 1, type=int)) server = request.args.get("server", ""); etype = request.args.get("type", "") conditions, args = [], [] if server: conditions.append("server_name = %s"); args.append(server) if etype: conditions.append("event_type = %s"); args.append(etype) where = ("WHERE " + " AND ".join(conditions)) if conditions else "" rows, total, pages = query_paged( f"SELECT * FROM server_events {where} ORDER BY timestamp DESC", f"SELECT COUNT(*) AS c FROM server_events {where}", tuple(args), page) servers = [r["server_name"] for r in query("SELECT DISTINCT server_name FROM server_events ORDER BY server_name")] etypes = [r["event_type"] for r in query("SELECT DISTINCT event_type FROM server_events ORDER BY event_type")] return render_template("panel/server_events.html", rows=rows, total=total, pages=pages, page=page, server=server, etype=etype, servers=servers, etypes=etypes) # ───────────────────────────────────────────────────────────── # Berechtigungen (plugin_events) # ───────────────────────────────────────────────────────────── @panel.route("/perms") @login_required @perm_required("view_perms") def perms(): page = max(1, request.args.get("page", 1, type=int)) player = request.args.get("player", ""); plugin_filter = request.args.get("plugin", ""); etype = request.args.get("type", "") conditions, args = [], [] if player: conditions.append("player_name LIKE %s"); args.append(f"%{player}%") if plugin_filter: conditions.append("plugin_name = %s"); args.append(plugin_filter) if etype: conditions.append("event_type LIKE %s"); args.append(f"%{etype}%") where = ("WHERE " + " AND ".join(conditions)) if conditions else "" rows, total, pages = query_paged( f"SELECT * FROM plugin_events {where} ORDER BY timestamp DESC", f"SELECT COUNT(*) AS c FROM plugin_events {where}", tuple(args), page) plugins = [r["plugin_name"] for r in query("SELECT DISTINCT plugin_name FROM plugin_events ORDER BY plugin_name")] return render_template("panel/perms.html", rows=rows, total=total, pages=pages, page=page, player=player, plugin_filter=plugin_filter, etype=etype, plugins=plugins) # ───────────────────────────────────────────────────────────── # API # ───────────────────────────────────────────────────────────── @panel.route("/api/online") @login_required def api_online(): rows = query(""" SELECT p.username, ps.server_name, ps.login_time, ps.country FROM player_sessions ps JOIN players p ON p.uuid = ps.player_uuid WHERE ps.logout_time IS NULL ORDER BY ps.login_time DESC """) return jsonify([dict(r) for r in rows]) @panel.route("/api/stats") @login_required def api_stats(): return jsonify({ "players_online": query("SELECT COUNT(*) AS c FROM player_sessions WHERE logout_time IS NULL", fetchone=True)["c"], }) """ MCLogger – Site-Admin-Bereich Verwaltet alle Gruppen und Nutzer global. """ from functools import wraps from flask import Blueprint, render_template, request, redirect, url_for, session, flash import panel_db as db site_admin = Blueprint("site_admin", __name__, url_prefix="/admin") def admin_required(f): @wraps(f) def decorated(*args, **kwargs): if not session.get("is_site_admin"): return redirect(url_for("auth.admin_login")) return f(*args, **kwargs) return decorated # ────────────────────────────────────────────────────────────── # Dashboard # ────────────────────────────────────────────────────────────── @site_admin.route("/") @admin_required def dashboard(): try: groups = db.list_all_groups() or [] users = db.list_all_users() or [] for g in groups: try: g["has_db"] = db.has_db_configured(g["id"]) except Exception: g["has_db"] = False except Exception: groups, users = [], [] stats = { "group_count": len(groups), "user_count": len(users), "db_configured": sum(1 for g in groups if g.get("has_db")), "admin_count": sum(1 for u in users if u.get("is_site_admin")), } return render_template("admin/dashboard.html", groups=groups, users=users, stats=stats) # ────────────────────────────────────────────────────────────── # Gruppen verwalten # ────────────────────────────────────────────────────────────── @site_admin.route("/groups") @admin_required def groups(): all_groups = db.list_all_groups() for g in all_groups: g["has_db"] = db.has_db_configured(g["id"]) return render_template("admin/groups.html", groups=all_groups) @site_admin.route("/groups/new", methods=["GET", "POST"]) @admin_required def group_new(): if request.method == "POST": name = request.form.get("name", "").strip() desc = request.form.get("description", "").strip() if not name: flash("Gruppenname darf nicht leer sein.", "danger") elif db.get_group_by_name(name): flash("Eine Gruppe mit diesem Namen existiert bereits.", "danger") else: db.create_group(name, desc) flash(f"Gruppe '{name}' erstellt.", "success") return redirect(url_for("site_admin.groups")) return render_template("admin/group_edit.html", group=None) @site_admin.route("/groups//edit", methods=["GET", "POST"]) @admin_required def group_edit(group_id): group = db.get_group_by_id(group_id) if not group: flash("Gruppe nicht gefunden.", "danger") return redirect(url_for("site_admin.groups")) if request.method == "POST": name = request.form.get("name", "").strip() desc = request.form.get("description", "").strip() if not name: flash("Gruppenname darf nicht leer sein.", "danger") else: db.update_group(group_id, name, desc) flash("Gruppe aktualisiert.", "success") return redirect(url_for("site_admin.groups")) return render_template("admin/group_edit.html", group=group) @site_admin.route("/groups//delete", methods=["POST"]) @admin_required def group_delete(group_id): db.delete_group(group_id) flash("Gruppe gelöscht.", "success") return redirect(url_for("site_admin.groups")) @site_admin.route("/groups//members") @admin_required def group_members(group_id): group = db.get_group_by_id(group_id) members = db.get_group_members(group_id) all_users = db.list_all_users() member_ids = {m["id"] for m in members} non_members = [u for u in all_users if u["id"] not in member_ids] return render_template("admin/group_members.html", group=group, members=members, non_members=non_members) @site_admin.route("/groups//members/add", methods=["POST"]) @admin_required def group_member_add(group_id): user_id = request.form.get("user_id", type=int) role = request.form.get("role", "member") if user_id: db.add_group_member(user_id, group_id, role) flash("Mitglied hinzugefügt.", "success") return redirect(url_for("site_admin.group_members", group_id=group_id)) @site_admin.route("/groups//members//remove", methods=["POST"]) @admin_required def group_member_remove(group_id, user_id): db.remove_group_member(user_id, group_id) flash("Mitglied entfernt.", "success") return redirect(url_for("site_admin.group_members", group_id=group_id)) @site_admin.route("/groups//members//toggle-role", methods=["POST"]) @admin_required def group_member_toggle_role(group_id, user_id): member = db.get_group_member(user_id, group_id) if member: import json as _json new_role = "member" if member["role"] == "admin" else "admin" perms = member["permissions"] if isinstance(member["permissions"], dict) else (_json.loads(member["permissions"]) if member["permissions"] else {}) db.update_member(user_id, group_id, new_role, perms) flash(f"Rolle auf '{new_role}' geändert.", "success") return redirect(url_for("site_admin.group_members", group_id=group_id)) # ────────────────────────────────────────────────────────────── # Nutzer verwalten # ────────────────────────────────────────────────────────────── @site_admin.route("/users") @admin_required def users(): return render_template("admin/users.html", users=db.list_all_users()) @site_admin.route("/users/new", methods=["GET", "POST"]) @admin_required def user_new(): if request.method == "POST": username = request.form.get("username", "").strip() email = request.form.get("email", "").strip() password = request.form.get("password", "") is_site_admin = request.form.get("is_site_admin") == "1" if not username or not email or not password: flash("Alle Felder sind Pflichtfelder.", "danger") elif db.get_user_by_username(username): flash("Benutzername bereits vergeben.", "danger") else: db.create_user(username, email, password, is_site_admin) flash(f"Nutzer '{username}' erstellt.", "success") return redirect(url_for("site_admin.users")) return render_template("admin/user_edit.html", user=None) @site_admin.route("/users//edit", methods=["GET", "POST"]) @admin_required def user_edit(user_id): user = db.get_user_by_id(user_id) if not user: flash("Nutzer nicht gefunden.", "danger") return redirect(url_for("site_admin.users")) if request.method == "POST": username = request.form.get("username", "").strip() email = request.form.get("email", "").strip() is_site_admin = request.form.get("is_site_admin") == "1" new_password = request.form.get("new_password", "") db.update_user(user_id, username, email, is_site_admin) if new_password: db.change_password(user_id, new_password) flash("Passwort geändert.", "info") flash("Nutzer aktualisiert.", "success") return redirect(url_for("site_admin.users")) return render_template("admin/user_edit.html", user=user) @site_admin.route("/users//delete", methods=["POST"]) @admin_required def user_delete(user_id): if user_id == session.get("user_id"): flash("Du kannst dich nicht selbst löschen.", "danger") else: db.delete_user(user_id) flash("Nutzer gelöscht.", "success") return redirect(url_for("site_admin.users")) # ────────────────────────────────────────────────────────────── # Als Gruppe anzeigen (Site-Admin liest Gruppen-DB) # ────────────────────────────────────────────────────────────── @site_admin.route("/view-group/") @admin_required def view_group(group_id): """Site-Admin wechselt temporär in eine Grup­pe, um deren MC-Daten zu sehen.""" group = db.get_group_by_id(group_id) if not group: flash("Gruppe nicht gefunden.", "danger") return redirect(url_for("site_admin.dashboard")) if not db.has_db_configured(group_id): flash("Für diese Gruppe ist noch keine Datenbank konfiguriert.", "warning") return redirect(url_for("site_admin.dashboard")) # Alle Berechtigungen als Admin all_perms = {k: True for k in ["view_dashboard","view_players","view_sessions", "view_chat","view_commands","view_deaths","view_blocks", "view_proxy","view_server_events","view_perms"]} session["group_id"] = group_id session["group_name"] = group["name"] session["role"] = "admin" session["permissions"] = all_perms session["admin_viewing"] = True return redirect(url_for("panel.dashboard")) @site_admin.route("/stop-view") @admin_required def stop_view(): """Kehrt zum Site-Admin-Dashboard zurück.""" session.pop("group_id", None) session.pop("group_name", None) session.pop("role", None) session.pop("permissions", None) session.pop("admin_viewing", None) return redirect(url_for("site_admin.dashboard")) {% block title %}Site Admin{% endblock %} — MCLogger
{% with messages = get_flashed_messages(with_categories=true) %} {% if messages %} {% for cat, msg in messages %} {% endfor %} {% endif %} {% endwith %} {% block content %}{% endblock %}
{% block scripts %}{% endblock %} {% extends "admin/base.html" %} {% block title %}Dashboard{% endblock %} {% block content %}

Site Admin Dashboard

{{ stats.group_count }}
Gruppen
{{ stats.user_count }}
Benutzer
{{ stats.db_configured }}
DBs konfiguriert
{{ stats.admin_count }}
Site Admins
Gruppen Neu
{% for g in groups %} {% else %} {% endfor %}
NameMitgliederDB
{{ g.name }} {{ g.member_count }} {% if g.has_db %} Konfiguriert {% else %} Keine {% endif %}
Keine Gruppen vorhanden
Benutzer Neu
{% for u in users %} {% else %} {% endfor %}
BenutzerGruppenAdmin
{{ u.username }} {{ u.group_count }} {% if u.is_site_admin %}{% endif %}
Keine Benutzer vorhanden
{% endblock %} {% extends "admin/base.html" %} {% block title %}{{ 'Gruppe bearbeiten' if group else 'Neue Gruppe' }}{% endblock %} {% block content %}

{{ 'Gruppe bearbeiten' if group else 'Neue Gruppe erstellen' }}

Abbrechen
{% endblock %} {% extends "admin/base.html" %} {% block title %}Mitglieder – {{ group.name }}{% endblock %} {% block content %}

Mitglieder: {{ group.name }}

Aktuelle Mitglieder ({{ members|length }})
{% for m in members %} {% else %} {% endfor %}
BenutzerRolleAktionen
{{ m.username }} {% if m.role == 'admin' %} Admin {% else %} Member {% endif %}
Keine Mitglieder
Benutzer hinzufügen
{% if non_members %}
{% else %}

Alle Benutzer sind bereits Mitglied.

{% endif %}
{% endblock %} {% extends "admin/base.html" %} {% block title %}Gruppen{% endblock %} {% block content %}
{% for g in groups %} {% else %} {% endfor %}
IDNameBeschreibungMitgliederDBErstelltAktionen
{{ g.id }} {{ g.name }} {{ g.description or '—' }} {{ g.member_count }} {% if g.has_db %} Konfiguriert {% else %} Keine DB {% endif %} {{ g.created_at | fmt_dt }}
Noch keine Gruppen vorhanden.
{% endblock %} {% extends "admin/base.html" %} {% block title %}{{ 'Benutzer bearbeiten' if user else 'Neuer Benutzer' }}{% endblock %} {% block content %}

{{ 'Benutzer bearbeiten: ' ~ user.username if user else 'Neuer Benutzer' }}

{% if not user %}
Mindestens 8 Zeichen empfohlen.
{% endif %}
Abbrechen
{% endblock %} {% extends "admin/base.html" %} {% block title %}Benutzer{% endblock %} {% block content %}
{% for u in users %} {% else %} {% endfor %}
BenutzerGruppenSite AdminErstelltAktionen
{{ u.username }} {% for g in u.groups %} {{ g.name }} {% if g.role == 'admin' %}{% endif %} {% else %}Keine{% endfor %} {% if u.is_site_admin %} Site Admin {% else %}—{% endif %} {{ u.created_at | fmt_dt }}
Keine Benutzer vorhanden.
{% endblock %} MCLogger – Site Admin Login MCLogger – Login {% block title %}Gruppen Admin{% endblock %} — {{ session.get('group_name','') }}
{% with messages = get_flashed_messages(with_categories=true) %} {% if messages %} {% for cat, msg in messages %} {% endfor %} {% endif %} {% endwith %} {% block content %}{% endblock %}
{% block scripts %}{% endblock %} {% extends "group_admin/base.html" %} {% block title %}Dashboard{% endblock %} {% block content %}

Gruppenadmin: {{ session.get('group_name') }}

{{ stats.member_count }}
Mitglieder
{{ 'Ja' if stats.db_configured else 'Nein' }}
DB konfiguriert
{{ stats.admin_count }}
Admins
Gruppeninfo
Name
{{ session.get('group_name') }}
Deine Rolle
Admin
Datenbank
{% if stats.db_configured %} Verbunden {% else %} Nicht konfiguriert {% endif %}
{% endblock %} {% extends "group_admin/base.html" %} {% block title %}Datenbank{% endblock %} {% block content %}

MC Datenbank konfigurieren

Verbindungsdaten
{% if test_result is defined %}
{% if test_result %} Verbindung erfolgreich! Daten wurden gespeichert. {% else %} Verbindung fehlgeschlagen: {{ test_error }} {% endif %}
{% endif %}
{% if creds %}
Leer lassen um das bestehende Passwort beizubehalten.
{% endif %}
{% if creds %} {% endif %}
Info

Gib hier die Verbindungsdaten zu deiner MCLogger MySQL-Datenbank ein. Das Panel liest nur Daten (SELECT) — schreibender Zugriff ist nicht nötig.

Die Zugangsdaten werden verschlüsselt gespeichert und sind nur für deine Gruppe sichtbar.


Benötigte Tabellen:

  • player_sessions
  • chat_messages
  • player_commands
  • block_events
  • player_deaths
  • proxy_events
  • server_events
  • permission_changes
{% endblock %} {% extends "group_admin/base.html" %} {% block title %}Berechtigungen – {{ member.username }}{% endblock %} {% block content %}

Berechtigungen: {{ member.username }}

Panel-Berechtigungen
Admins können Mitglieder und die DB-Verbindung verwalten.

Panel-Zugriff

{% for key, label in all_permissions %}
{% endfor %}
Abbrechen
{% endblock %} {% extends "group_admin/base.html" %} {% block title %}Mitglieder{% endblock %} {% block content %}

Mitglieder

Aktuelle Mitglieder ({{ members|length }})
{% for m in members %} {% else %} {% endfor %}
BenutzerRolleAktionen
{{ m.username }} {% if m.role == 'admin' %} Admin {% else %} Member {% endif %} {% if m.id != session.get('user_id') %}
{% else %} Du {% endif %}
Keine Mitglieder
Hinweis

Neue Mitglieder müssen vom Site Admin zur Gruppe hinzugefügt werden.

Als Gruppenadmin kannst du Berechtigungen bestehender Mitglieder verwalten und Mitglieder entfernen.

{% endblock %} {% extends "base.html" %} {% block title %}Block-Events{% endblock %} {% block page_title %}Block-Events{% endblock %} {% block content %}
Reset
{{ total }} block events
{% for r in rows %} {% else %} {% endfor %}
TimeTypePlayerBlockWorldPositionToolSilk
{{ r.timestamp | fmt_dt }} {% set colors = {'break':'danger','place':'success','ignite':'warning','burn':'orange','explode':'dark'} %} {{ r.event_type }} {{ r.player_name or '—' }} {{ r.block_type }} {{ r.world }} {{ r.x }}, {{ r.y }}, {{ r.z }} {{ r.tool or '—' }} {% if r.is_silk %}{% else %}—{% endif %}
No block events
{% include "_pagination.html" %} {% endblock %} {% extends "base.html" %} {% block title %}Chat Log{% endblock %} {% block page_title %}Chat Log{% endblock %} {% block content %}
Reset
{{ total }} messages
{% for r in rows %} {% else %} {% endfor %}
TimePlayerServerChannelMessage
{{ r.timestamp | fmt_dt }} {{ r.player_name or '—' }} {{ r.server_name or '—' }} {{ r.channel or 'global' }} {{ r.message }}
No messages found
{% include "_pagination.html" %} {% endblock %} {% extends "base.html" %} {% block title %}Commands{% endblock %} {% block page_title %}Commands{% endblock %} {% block content %}
Reset
{{ total }} commands
{% for r in rows %} {% else %} {% endfor %}
TimePlayerServerCommandPosition
{{ r.timestamp | fmt_dt }} {{ r.player_name or '—' }} {{ r.server_name or '—' }} {{ r.command }} {% if r.world %}{{ r.world }} ({{ r.x|round(0)|int }}, {{ r.y|round(0)|int }}, {{ r.z|round(0)|int }}){% else %}—{% endif %}
No commands
{% include "_pagination.html" %} {% endblock %} {% extends "base.html" %} {% block title %}Dashboard{% endblock %} {% block page_title %}Dashboard{% endblock %} {% block content %}
{% set cards = [ ('Total Players', stats.players_total, 'bi-people-fill', 'success'), ('Sessions Today', stats.sessions_today, 'bi-clock-history', 'info'), ('Chats Today', stats.chat_today, 'bi-chat-dots-fill', 'primary'), ('Commands Today', stats.commands_today, 'bi-terminal-fill', 'warning'), ('Blocks Today', stats.blocks_today, 'bi-bricks', 'secondary'), ('Deaths Today', stats.deaths_today, 'bi-heartbreak-fill', 'danger'), ('Entity Events', stats.entity_events_today, 'bi-bug-fill', 'light'), ('Proxy Events', stats.proxy_events_today, 'bi-diagram-3-fill', 'dark'), ] %} {% for label, value, icon, color in cards %}
{{ value | int }}
{{ label }}
{% endfor %}
Online Players
{% if online %} {% for s in online %} {% endfor %}
PlayerServerSince
{{ s.player_name }} {{ s.server_name }} {{ s.login_time | fmt_dt }}
{% else %}

No players online
{% endif %}
Last 24h Activity
{% for r in recent %} {% endfor %}
TimeTypePlayerServerDetail
{{ r.timestamp | fmt_dt }} {% set badge = {'chat':'primary','command':'warning','block':'secondary','death':'danger'} %} {{ r.source }} {{ r.player_name or '—' }} {{ r.server_name or '—' }} {{ r.detail }}
Block Events (last 7 days)
Death Causes (7d)
Top Playtime
{% for p in top_players %} {% endfor %}
{{ loop.index }}. {{ p.username }} {{ p.total_playtime_sec | fmt_duration }}
Server Events (last 24h)
{% for e in server_events %} {% endfor %}
TimeTypeServerMessage
{{ e.timestamp | fmt_dt }} {{ e.event_type }} {{ e.server_name }} {{ e.message }}
{% endblock %} {% block scripts %} {% endblock %} {% extends "base.html" %} {% block title %}Deaths{% endblock %} {% block page_title %}Deaths{% endblock %} {% block content %}
Reset
{{ total }} deaths
{% for r in rows %} {% else %} {% endfor %}
TimePlayerCauseKillerKiller TypeLevelWorldDeath Message
{{ r.timestamp | fmt_dt }} {{ r.player_name }} {{ r.cause or '—' }} {{ r.killer_name or '—' }} {{ r.killer_type or '—' }} {{ r.exp_level }} {{ r.world }} {{ r.death_message or '—' }}
No deaths
{% include "_pagination.html" %} {% endblock %} {% extends "base.html" %} {% block title %}No Database{% endblock %} {% block page_title %}Keine Datenbank{% endblock %} {% block content %}

Keine Datenbank konfiguriert

Für diese Gruppe ist noch keine MC-Datenbank eingerichtet. {% if session.get('role') == 'admin' %} Du kannst die Verbindung als Gruppen-Admin konfigurieren. {% else %} Bitte wende dich an deinen Gruppenadmin. {% endif %}

{% if session.get('role') == 'admin' %} Datenbank konfigurieren {% endif %}
{% endblock %} {% extends "base.html" %} {% block title %}Permissions{% endblock %} {% block page_title %}Permissions{% endblock %} {% block content %}
Reset
{{ total }} permission events
{% for r in rows %} {% set badge_colors = { 'luckperms_permission_set': 'success', 'luckperms_permission_unset': 'danger', 'luckperms_parent_add': 'primary', 'luckperms_parent_remove': 'warning', 'luckperms_meta_set': 'info', 'luckperms_meta_unset': 'secondary', 'luckperms_group_create': 'light', 'luckperms_group_delete': 'dark', } %} {% else %} {% endfor %}
TimePluginEvent TypeTarget PlayerActorTarget TypeTarget IDActionServer
{{ r.timestamp | fmt_dt }} {{ r.plugin_name or '—' }} {{ r.event_type }} {{ r.player_name or '—' }} {{ r.actor_name or '—' }} {{ r.target_type or '—' }} {{ r.target_id or '—' }} {{ r.action or '—' }} {{ r.server_name or '—' }}
No permission events found
{% include "_pagination.html" %} {% endblock %} {% extends "base.html" %} {% block title %}{{ player.username }}{% endblock %} {% block page_title %}{{ player.username }}{% endblock %} {% block content %}
{{ player.username }}
{{ player.username }}
{% if player.is_op %} OP {% endif %}
UUID{{ player.uuid }}
IP{{ player.ip_address or '—' }}
Locale{{ player.locale or '—' }}
Playtime{{ player.total_playtime_sec | fmt_duration }}
Since{{ player.first_seen | fmt_dt }}
Last Seen{{ player.last_seen | fmt_dt }}
{% for s in sessions %}{% else %}{% endfor %}
LoginLogoutDurationServerIP
{{ s.login_time | fmt_dt }} {{ s.logout_time | fmt_dt }} {{ s.duration_sec | fmt_duration }} {{ s.server_name or '—' }} {{ s.ip_address or '—' }}
No sessions
{% for c in chat %}{% else %}{% endfor %}
TimeServerMessage
{{ c.timestamp | fmt_dt }} {{ c.server_name or '—' }} {{ c.message }}
No chat messages
{% for c in commands %}{% else %}{% endfor %}
TimeServerCommandPosition
{{ c.timestamp | fmt_dt }} {{ c.server_name or '—' }} {{ c.command }} {{ c.world or '' }} {% if c.x %}({{ c.x|round(1) }}, {{ c.y|round(1) }}, {{ c.z|round(1) }}){% endif %}
No commands
{% for d in deaths %}{% else %}{% endfor %}
TimeCauseKillerLevelWorld
{{ d.timestamp | fmt_dt }} {{ d.cause or '—' }} {{ d.killer_name or '—' }} {{ d.exp_level }} {{ d.world }}
No deaths
{% for t in teleports %}{% else %}{% endfor %}
TimeFromToCause
{{ t.timestamp | fmt_dt }} {{ t.from_world }} ({{ t.from_x|round(0)|int }}, {{ t.from_y|round(0)|int }}, {{ t.from_z|round(0)|int }}) {{ t.to_world }} ({{ t.to_x|round(0)|int }}, {{ t.to_y|round(0)|int }}, {{ t.to_z|round(0)|int }}) {{ t.cause or '—' }}
No teleports
{% for e in proxy_events %}{% else %}{% endfor %}
TimeTypeFromTo
{{ e.timestamp | fmt_dt }} {{ e.event_type }} {{ e.from_server or '—' }} {{ e.to_server or '—' }}
No proxy events
Back to Overview {% endblock %} {% extends "base.html" %} {% block title %}Players{% endblock %} {% block page_title %}Players{% endblock %} {% block content %}
{% if search %}Reset{% endif %}
{{ total }} players found
{% for p in players %} {% else %} {% endfor %}
PlayerIPFirst SeenLast SeenPlaytimeOP
{{ p.username }} {{ p.ip_address or '—' }} {{ p.first_seen | fmt_dt }} {{ p.last_seen | fmt_dt }} {{ p.total_playtime_sec | fmt_duration }} {% if p.is_op %} OP {% else %}{% endif %}
No players found
{% include "_pagination.html" %} {% endblock %} {% extends "base.html" %} {% block title %}Proxy Events{% endblock %} {% block page_title %}Proxy Events{% endblock %} {% block content %}
Reset
{{ total }} proxy events
{% for r in rows %} {% set badge = {'login':'success','disconnect':'danger','server_switch':'primary','command':'warning','proxy_start':'info','proxy_stop':'dark'} %} {% else %} {% endfor %}
TimeTypePlayerProxyFromToIP
{{ r.timestamp | fmt_dt }} {{ r.event_type }} {{ r.player_name or '—' }} {{ r.proxy_name or '—' }} {{ r.from_server or '—' }} {{ r.to_server or '—' }} {{ r.ip_address or '—' }}
No proxy events
{% include "_pagination.html" %} {% endblock %} {% extends "base.html" %} {% block title %}Server Events{% endblock %} {% block page_title %}Server Events{% endblock %} {% block content %}
Reset
{{ total }} server events
{% for r in rows %} {% set badge = {'server_start':'success','server_stop':'danger','player_join':'info','player_quit':'secondary','player_kick':'warning'} %} {% else %} {% endfor %}
TimeTypeServerMessage
{{ r.timestamp | fmt_dt }} {{ r.event_type }} {{ r.server_name or '—' }} {{ r.message or '—' }}
No events
{% include "_pagination.html" %} {% endblock %} {% extends "base.html" %} {% block title %}Sessions{% endblock %} {% block page_title %}Sessions{% endblock %} {% block content %}
Reset
{{ total }} sessions
{% for r in rows %} {% else %} {% endfor %}
PlayerServerLoginLogoutDurationIPCountryClient
{{ r.player_name }} {{ r.server_name or '—' }} {{ r.login_time | fmt_dt }} {{ r.logout_time | fmt_dt }} {% if r.logout_time %}{{ r.duration_sec | fmt_duration }} {% else %}Online{% endif %} {{ r.ip_address or '—' }} {{ r.country or '—' }} {{ r.client_version or '—' }}
No sessions
{% include "_pagination.html" %} {% endblock %} {% if pages > 1 %} {% endif %} {% block title %}MCLogger{% endblock %} — Panel
{% block page_title %}{% endblock %}
{{ now.strftime('%d.%m.%Y %H:%M') }}
{% with messages = get_flashed_messages(with_categories=true) %} {% if messages %}
{% for cat, msg in messages %} {% endfor %}
{% endif %} {% endwith %}
{% block content %}{% endblock %}
{% block scripts %}{% endblock %} {% extends "base.html" %} {% block title %}Block-Events{% endblock %} {% block page_title %}Block-Events{% endblock %} {% block content %}
Reset
{{ total }} block events
{% for r in rows %} {% else %} {% endfor %}
TimeTypePlayerBlockWorldPositionToolSilk
{{ r.timestamp | fmt_dt }} {% set colors = {'break':'danger','place':'success','ignite':'warning','burn':'orange','explode':'dark'} %} {{ r.event_type }} {{ r.player_name or '—' }} {{ r.block_type }} {{ r.world }} {{ r.x }}, {{ r.y }}, {{ r.z }} {{ r.tool or '—' }} {% if r.is_silk %}{% else %}—{% endif %}
No block events
{% include "_pagination.html" %} {% endblock %} {% extends "base.html" %} {% block title %}Chat Log{% endblock %} {% block page_title %}Chat Log{% endblock %} {% block content %}
Reset
{{ total }} messages
{% for r in rows %} {% else %} {% endfor %}
TimePlayerServerChannelMessage
{{ r.timestamp | fmt_dt }} {{ r.player_name or '—' }} {{ r.server_name or '—' }} {{ r.channel or 'global' }} {{ r.message }}
No messages found
{% include "_pagination.html" %} {% endblock %} {% extends "base.html" %} {% block title %}Commands{% endblock %} {% block page_title %}Commands{% endblock %} {% block content %}
Reset
{{ total }} commands
{% for r in rows %} {% else %} {% endfor %}
TimePlayerServerCommandPosition
{{ r.timestamp | fmt_dt }} {{ r.player_name or '—' }} {{ r.server_name or '—' }} {{ r.command }} {% if r.world %}{{ r.world }} ({{ r.x|round(0)|int }}, {{ r.y|round(0)|int }}, {{ r.z|round(0)|int }}){% else %}—{% endif %}
No commands
{% include "_pagination.html" %} {% endblock %} {% extends "base.html" %} {% block title %}Dashboard{% endblock %} {% block page_title %}Dashboard{% endblock %} {% block content %}
{% set cards = [ ('Total Players', stats.players_total, 'bi-people-fill', 'success'), ('Sessions Today', stats.sessions_today, 'bi-clock-history', 'info'), ('Chats Today', stats.chat_today, 'bi-chat-dots-fill', 'primary'), ('Commands Today', stats.commands_today, 'bi-terminal-fill', 'warning'), ('Blocks Today', stats.blocks_today, 'bi-bricks', 'secondary'), ('Deaths Today', stats.deaths_today, 'bi-heartbreak-fill', 'danger'), ('Entity Events', stats.entity_events_today, 'bi-bug-fill', 'light'), ('Proxy Events', stats.proxy_events_today, 'bi-diagram-3-fill', 'dark'), ] %} {% for label, value, icon, color in cards %}
{{ value | int }}
{{ label }}
{% endfor %}
Online Players
{% if online %} {% for s in online %} {% endfor %}
PlayerServerCountrySince
{{ s.username }} {{ s.server_name }} {{ s.country or '—' }} {{ s.login_time | fmt_dt }}
{% else %}

No players online
{% endif %}
Last 24h Activity
{% for r in recent %} {% endfor %}
TimeTypePlayerServerDetail
{{ r.timestamp | fmt_dt }} {% set badge = {'chat':'primary','command':'warning','block':'secondary','death':'danger'} %} {{ r.source }} {{ r.player_name or '—' }} {{ r.server_name or '—' }} {{ r.detail }}
Block Events (last 7 days)
Death Causes (7d)
Top Playtime
{% for p in top_players %} {% endfor %}
{{ loop.index }}. {{ p.username }} {{ p.total_playtime_sec | fmt_duration }}
Server Events (last 24h)
{% for e in server_events %} {% endfor %}
TimeTypeServerMessage
{{ e.timestamp | fmt_dt }} {{ e.event_type }} {{ e.server_name }} {{ e.message }}
{% endblock %} {% block scripts %} {% endblock %} {% extends "base.html" %} {% block title %}Deaths{% endblock %} {% block page_title %}Deaths{% endblock %} {% block content %}
Reset
{{ total }} deaths
{% for r in rows %} {% else %} {% endfor %}
TimePlayerCauseKillerKiller TypeLevelWorldDeath Message
{{ r.timestamp | fmt_dt }} {{ r.player_name }} {{ r.cause or '—' }} {{ r.killer_name or '—' }} {{ r.killer_type or '—' }} {{ r.exp_level }} {{ r.world }} {{ r.death_message or '—' }}
No deaths
{% include "_pagination.html" %} {% endblock %} MCLogger – Login

MCLogger

Admin-Interface · SimolZimol

{% if error %}
{{ error }}
{% endif %}
{% extends "base.html" %} {% block title %}Permissions{% endblock %} {% block page_title %}Permissions{% endblock %} {% block content %}
Reset
{{ total }} permission events
{% for r in rows %} {% set badge_colors = { 'luckperms_permission_set': 'success', 'luckperms_permission_unset': 'danger', 'luckperms_parent_add': 'primary', 'luckperms_parent_remove': 'warning', 'luckperms_meta_set': 'info', 'luckperms_meta_unset': 'secondary', 'luckperms_group_create': 'light', 'luckperms_group_delete': 'dark', } %} {% else %} {% endfor %}
Time Plugin Event Type Target Player Actor Target Type Target ID Action Server
{{ r.timestamp | fmt_dt }} {{ r.plugin_name or '—' }} {{ r.event_type }} {{ r.player_name or '—' }} {{ r.actor_name or '—' }} {{ r.target_type or '—' }} {{ r.target_id or '—' }} {{ r.action or '—' }} {{ r.server_name or '—' }}
No permission events found
{% include "_pagination.html" %} {% endblock %} {% extends "base.html" %} {% block title %}{{ player.username }}{% endblock %} {% block page_title %}{{ player.username }}{% endblock %} {% block content %}
{{ player.username }}
{{ player.username }}
{% if player.is_op %} OP {% endif %}
UUID{{ player.uuid }}
IP{{ player.ip_address or '—' }}
Locale{{ player.locale or '—' }}
Playtime{{ player.total_playtime_sec | fmt_duration }}
Since{{ player.first_seen | fmt_dt }}
Last Seen{{ player.last_seen | fmt_dt }}
{% for s in sessions %} {% else %}{% endfor %}
LoginLogoutDurationServerIP
{{ s.login_time | fmt_dt }} {{ s.logout_time | fmt_dt }} {{ s.duration_sec | fmt_duration }} {{ s.server_name or '—' }} {{ s.ip_address or '—' }}
No sessions
{% for c in chat %} {% else %}{% endfor %}
TimeServerMessage
{{ c.timestamp | fmt_dt }} {{ c.server_name or '—' }} {{ c.message }}
No chat messages
{% for c in commands %} {% else %}{% endfor %}
TimeServerCommandPosition
{{ c.timestamp | fmt_dt }} {{ c.server_name or '—' }} {{ c.command }} {{ c.world or '' }} {% if c.x %}({{ c.x|round(1) }}, {{ c.y|round(1) }}, {{ c.z|round(1) }}){% endif %}
No commands
{% for d in deaths %} {% else %}{% endfor %}
TimeCauseKillerLevelWorld
{{ d.timestamp | fmt_dt }} {{ d.cause or '—' }} {{ d.killer_name or '—' }} {{ d.exp_level }} {{ d.world }}
No deaths
{% for t in teleports %} {% else %}{% endfor %}
TimeFromToCause
{{ t.timestamp | fmt_dt }} {{ t.from_world }} ({{ t.from_x|round(0)|int }}, {{ t.from_y|round(0)|int }}, {{ t.from_z|round(0)|int }}) {{ t.to_world }} ({{ t.to_x|round(0)|int }}, {{ t.to_y|round(0)|int }}, {{ t.to_z|round(0)|int }}) {{ t.cause or '—' }}
No teleports
{% for s in stats %} {% else %}{% endfor %}
TimeTypeOldNew
{{ s.timestamp | fmt_dt }} {{ s.event_type }} {{ s.old_value or '—' }} {{ s.new_value or '—' }}
No stats
{% for e in proxy_events %} {% else %}{% endfor %}
TimeTypeFromTo
{{ e.timestamp | fmt_dt }} {{ e.event_type }} {{ e.from_server or '—' }} {{ e.to_server or '—' }}
No proxy events
Back to Overview {% endblock %} {% extends "base.html" %} {% block title %}Players{% endblock %} {% block page_title %}Players{% endblock %} {% block content %}
{% if search %}Reset{% endif %}
{{ total }} players found
{% for p in players %} {% else %} {% endfor %}
PlayerIPFirst Seen Last SeenPlaytimeOP
{{ p.username }} {{ p.ip_address or '—' }} {{ p.first_seen | fmt_dt }} {{ p.last_seen | fmt_dt }} {{ p.total_playtime_sec | fmt_duration }} {% if p.is_op %} OP {% else %}{% endif %}
No players found
{% if pages > 1 %} {% endif %} {% endblock %} {% extends "base.html" %} {% block title %}Proxy Events{% endblock %} {% block page_title %}Proxy Events{% endblock %} {% block content %}
Reset
{{ total }} proxy events
{% for r in rows %} {% set badge = {'login':'success','disconnect':'danger','server_switch':'primary','command':'warning','proxy_start':'info','proxy_stop':'dark'} %} {% else %} {% endfor %}
TimeTypePlayerProxyFromToIP
{{ r.timestamp | fmt_dt }} {{ r.event_type }} {{ r.player_name or '—' }} {{ r.proxy_name or '—' }} {{ r.from_server or '—' }} {{ r.to_server or '—' }} {{ r.ip_address or '—' }}
No proxy events
{% include "_pagination.html" %} {% endblock %} {% extends "base.html" %} {% block title %}Server Events{% endblock %} {% block page_title %}Server Events{% endblock %} {% block content %}
Reset
{{ total }} server events
{% for r in rows %} {% set badge = {'server_start':'success','server_stop':'danger','player_join':'info','player_quit':'secondary','player_kick':'warning'} %} {% else %} {% endfor %}
TimeTypeServerMessage
{{ r.timestamp | fmt_dt }} {{ r.event_type }} {{ r.server_name or '—' }} {{ r.message or '—' }}
No events
{% include "_pagination.html" %} {% endblock %} {% extends "base.html" %} {% block title %}Sessions{% endblock %} {% block page_title %}Sessions{% endblock %} {% block content %}
Reset
{{ total }} sessions
{% for r in rows %} {% else %} {% endfor %}
PlayerServerLoginLogoutDurationIPCountryClient
{{ r.player_name }} {{ r.server_name or '—' }} {{ r.login_time | fmt_dt }} {{ r.logout_time | fmt_dt }} {% if r.logout_time %}{{ r.duration_sec | fmt_duration }} {% else %}Online{% endif %} {{ r.ip_address or '—' }} {{ r.country or '—' }} {{ r.client_version or '—' }}
No sessions
{% include "_pagination.html" %} {% endblock %} """ MCLogger – Flask Web-Panel Multi-Tenant mit Gruppen, Rollen & verschlüsselten DB-Zugangsdaten. Coolify-kompatibel: alle Einstellungen via ENV. """ from datetime import datetime from flask import Flask, session from config import Config from panel_db import init_databases, get_user_groups from blueprints.auth import auth from blueprints.site_admin import site_admin from blueprints.group_admin import group_admin from blueprints.panel import panel def create_app() -> Flask: app = Flask(__name__) app.secret_key = Config.SECRET_KEY # Blueprints registrieren app.register_blueprint(auth) app.register_blueprint(site_admin) app.register_blueprint(group_admin) app.register_blueprint(panel) # Panel-Datenbank-Tabellen anlegen try: init_databases() except Exception as e: app.logger.warning(f"DB-Initialisierung fehlgeschlagen (noch nicht konfiguriert?): {e}") # ── Template-Filter ─────────────────────────────────────── @app.template_filter("fmt_duration") def fmt_duration(seconds): if seconds is None: return "—" seconds = int(seconds) h = seconds // 3600 m = (seconds % 3600) // 60 s = seconds % 60 if h: return f"{h}h {m}m" elif m: return f"{m}m {s}s" return f"{s}s" @app.template_filter("fmt_dt") def fmt_dt(dt): if dt is None: return "—" if isinstance(dt, str): return dt return dt.strftime("%d.%m.%Y %H:%M:%S") @app.context_processor def inject_globals(): uid = session.get("user_id") try: groups = get_user_groups(uid) if uid else [] except Exception: groups = [] return { "now": datetime.now(), "app_version": "2.0.0", "author": "SimolZimol", "user_groups": groups, } return app app = create_app() if __name__ == "__main__": app.run(host=Config.HOST, port=Config.PORT, debug=Config.DEBUG) """ MCLogger – Konfiguration Alle Einstellungen über ENV-Variablen (Coolify-kompatibel). """ import os class Config: # ── Flask ────────────────────────────────────────────────── SECRET_KEY = os.getenv("SECRET_KEY", "change-me-use-a-long-random-string-min-32-chars") HOST = os.getenv("HOST") or "0.0.0.0" PORT = int(os.getenv("PORT") or "5000") DEBUG = (os.getenv("DEBUG") or "false").lower() == "true" # ── Panel-Datenbank (Nutzer, Gruppen, Mitgliedschaften) ──── PANEL_DB_HOST = os.getenv("PANEL_DB_HOST") or "localhost" PANEL_DB_PORT = int(os.getenv("PANEL_DB_PORT") or "3306") PANEL_DB_USER = os.getenv("PANEL_DB_USER") or "root" PANEL_DB_PASSWORD = os.getenv("PANEL_DB_PASSWORD") or "" PANEL_DB_NAME = os.getenv("PANEL_DB_NAME") or "mclogger_panel" # ── Credentials-Datenbank (verschlüsselte MC-DB-Zugangsdaten) ── CREDS_DB_HOST = os.getenv("CREDS_DB_HOST") or os.getenv("PANEL_DB_HOST") or "localhost" CREDS_DB_PORT = int(os.getenv("CREDS_DB_PORT") or os.getenv("PANEL_DB_PORT") or "3306") CREDS_DB_USER = os.getenv("CREDS_DB_USER") or os.getenv("PANEL_DB_USER") or "root" CREDS_DB_PASSWORD = os.getenv("CREDS_DB_PASSWORD") or os.getenv("PANEL_DB_PASSWORD") or "" CREDS_DB_NAME = os.getenv("CREDS_DB_NAME") or "mclogger_creds" # ── Sicherheit ──────────────────────────────────────────── PASSWORD_PEPPER = os.getenv("PASSWORD_PEPPER", "change-me-global-pepper-secret-never-change") # Generieren: python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())" FERNET_KEY = os.getenv("FERNET_KEY", "") # ── Standard-Berechtigungen neuer Gruppenmitglieder ─────── DEFAULT_PERMISSIONS = { "view_dashboard": True, "view_players": True, "view_sessions": True, "view_chat": True, "view_commands": True, "view_deaths": True, "view_blocks": True, "view_proxy": False, "view_server_events": False, "view_perms": False, } """ MCLogger – Kryptographie-Utilities - Passwort-Hashing: PBKDF2-HMAC-SHA256 mit Salt (pro Nutzer) + Pepper (global, via ENV) - DB-Credential-Verschlüsselung: Fernet (symmetrisch, Schlüssel via ENV) """ import hashlib import os from cryptography.fernet import Fernet from config import Config # ───────────────────────────────────────────────────────────── # Passwort-Hashing # ───────────────────────────────────────────────────────────── def generate_salt() -> str: """Generiert einen zufälligen 32-Byte Hex-Salt.""" return os.urandom(32).hex() def hash_password(password: str, salt: str) -> str: """ Hasht ein Passwort mit PBKDF2-HMAC-SHA256. Verwendet: salt (pro Nutzer) + pepper (global aus ENV) """ dk = hashlib.pbkdf2_hmac( "sha256", password.encode("utf-8"), (salt + Config.PASSWORD_PEPPER).encode("utf-8"), iterations=260_000, ) return dk.hex() def verify_password(password: str, salt: str, stored_hash: str) -> bool: """Prüft ob ein Passwort korrekt ist.""" return hash_password(password, salt) == stored_hash # ───────────────────────────────────────────────────────────── # Fernet-Verschlüsselung (für DB-Zugangsdaten) # ───────────────────────────────────────────────────────────── def _get_fernet() -> Fernet: key = Config.FERNET_KEY if not key: raise RuntimeError( "FERNET_KEY ist nicht gesetzt! " "Generieren: python -c \"from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())\"" ) if isinstance(key, str): key = key.encode() return Fernet(key) def encrypt_str(plaintext: str) -> str: """Verschlüsselt einen String mit Fernet.""" return _get_fernet().encrypt(plaintext.encode("utf-8")).decode("utf-8") def decrypt_str(ciphertext: str) -> str: """Entschlüsselt einen Fernet-verschlüsselten String.""" return _get_fernet().decrypt(ciphertext.encode("utf-8")).decode("utf-8") """ MCLogger – Panel-Datenbank-Operationen Verwaltet Nutzer, Gruppen, Mitgliedschaften (PANEL_DB) und verschlüsselte MC-DB-Zugangsdaten (CREDS_DB). """ import json import pymysql import pymysql.cursors from config import Config from crypto import generate_salt, hash_password, verify_password, encrypt_str, decrypt_str # ───────────────────────────────────────────────────────────── # Datenbankverbindungen # ───────────────────────────────────────────────────────────── def get_panel_db(): return pymysql.connect( host=Config.PANEL_DB_HOST, port=Config.PANEL_DB_PORT, user=Config.PANEL_DB_USER, password=Config.PANEL_DB_PASSWORD, database=Config.PANEL_DB_NAME, charset="utf8mb4", cursorclass=pymysql.cursors.DictCursor, autocommit=True, ) def get_creds_db(): return pymysql.connect( host=Config.CREDS_DB_HOST, port=Config.CREDS_DB_PORT, user=Config.CREDS_DB_USER, password=Config.CREDS_DB_PASSWORD, database=Config.CREDS_DB_NAME, charset="utf8mb4", cursorclass=pymysql.cursors.DictCursor, autocommit=True, ) def _panel_query(sql, args=None, fetchone=False, write=False): conn = get_panel_db() try: with conn.cursor() as cur: cur.execute(sql, args or ()) if write: return cur.lastrowid return cur.fetchone() if fetchone else cur.fetchall() finally: conn.close() def _creds_query(sql, args=None, fetchone=False, write=False): conn = get_creds_db() try: with conn.cursor() as cur: cur.execute(sql, args or ()) if write: return cur.lastrowid return cur.fetchone() if fetchone else cur.fetchall() finally: conn.close() # ───────────────────────────────────────────────────────────── # Initialisierung – Tabellen anlegen # ───────────────────────────────────────────────────────────── PANEL_SCHEMA = [ """CREATE TABLE IF NOT EXISTS users ( id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) UNIQUE NOT NULL, email VARCHAR(255) UNIQUE NOT NULL, password_hash VARCHAR(255) NOT NULL, salt VARCHAR(64) NOT NULL, is_site_admin TINYINT(1) DEFAULT 0, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, last_login TIMESTAMP NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4""", """CREATE TABLE IF NOT EXISTS user_groups ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(100) UNIQUE NOT NULL, description TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4""", """CREATE TABLE IF NOT EXISTS group_members ( id INT AUTO_INCREMENT PRIMARY KEY, user_id INT NOT NULL, group_id INT NOT NULL, role ENUM('admin','member') DEFAULT 'member', permissions JSON, joined_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uq_user_group (user_id, group_id), FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, FOREIGN KEY (group_id) REFERENCES user_groups(id) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4""", ] CREDS_SCHEMA = [ """CREATE TABLE IF NOT EXISTS group_databases ( id INT AUTO_INCREMENT PRIMARY KEY, group_id INT UNIQUE NOT NULL, enc_host TEXT NOT NULL, enc_port TEXT NOT NULL, enc_user TEXT NOT NULL, enc_password TEXT NOT NULL, enc_database TEXT NOT NULL, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4""", ] def init_databases(): """Erstellt alle benötigten Tabellen falls nicht vorhanden.""" panel = get_panel_db() try: with panel.cursor() as cur: for stmt in PANEL_SCHEMA: cur.execute(stmt) finally: panel.close() creds = get_creds_db() try: with creds.cursor() as cur: for stmt in CREDS_SCHEMA: cur.execute(stmt) finally: creds.close() # ───────────────────────────────────────────────────────────── # Nutzer # ───────────────────────────────────────────────────────────── def create_user(username: str, email: str, password: str, is_site_admin: bool = False) -> int: salt = generate_salt() pw_hash = hash_password(password, salt) return _panel_query( "INSERT INTO users (username, email, password_hash, salt, is_site_admin) VALUES (%s,%s,%s,%s,%s)", (username, email, pw_hash, salt, int(is_site_admin)), write=True ) def get_user_by_username(username: str): return _panel_query("SELECT * FROM users WHERE username=%s", (username,), fetchone=True) def get_user_by_id(user_id: int): return _panel_query("SELECT * FROM users WHERE id=%s", (user_id,), fetchone=True) def update_user(user_id: int, username: str, email: str, is_site_admin: bool): _panel_query( "UPDATE users SET username=%s, email=%s, is_site_admin=%s WHERE id=%s", (username, email, int(is_site_admin), user_id), write=True ) def change_password(user_id: int, new_password: str): salt = generate_salt() pw_hash = hash_password(new_password, salt) _panel_query( "UPDATE users SET password_hash=%s, salt=%s WHERE id=%s", (pw_hash, salt, user_id), write=True ) def delete_user(user_id: int): _panel_query("DELETE FROM users WHERE id=%s", (user_id,), write=True) def check_login(username: str, password: str): """Prüft Anmeldedaten. Gibt den Nutzer zurück oder None.""" user = get_user_by_username(username) if not user: return None if not verify_password(password, user["salt"], user["password_hash"]): return None _panel_query("UPDATE users SET last_login=NOW() WHERE id=%s", (user["id"],), write=True) return user def list_all_users(): return _panel_query( "SELECT u.*, COUNT(gm.group_id) AS group_count " "FROM users u LEFT JOIN group_members gm ON gm.user_id=u.id " "GROUP BY u.id ORDER BY u.username" ) # ───────────────────────────────────────────────────────────── # Gruppen # ───────────────────────────────────────────────────────────── def create_group(name: str, description: str = "") -> int: return _panel_query( "INSERT INTO user_groups (name, description) VALUES (%s,%s)", (name, description), write=True ) def get_group_by_id(group_id: int): return _panel_query("SELECT * FROM user_groups WHERE id=%s", (group_id,), fetchone=True) def get_group_by_name(name: str): return _panel_query("SELECT * FROM user_groups WHERE name=%s", (name,), fetchone=True) def update_group(group_id: int, name: str, description: str): _panel_query( "UPDATE user_groups SET name=%s, description=%s WHERE id=%s", (name, description, group_id), write=True ) def delete_group(group_id: int): _panel_query("DELETE FROM user_groups WHERE id=%s", (group_id,), write=True) def list_all_groups(): return _panel_query( "SELECT g.*, COUNT(gm.user_id) AS member_count " "FROM user_groups g LEFT JOIN group_members gm ON gm.group_id=g.id " "GROUP BY g.id ORDER BY g.name" ) # ───────────────────────────────────────────────────────────── # Gruppenmitgliedschaften # ───────────────────────────────────────────────────────────── def get_user_groups(user_id: int): return _panel_query( "SELECT g.*, gm.role, gm.permissions " "FROM user_groups g " "JOIN group_members gm ON gm.group_id=g.id " "WHERE gm.user_id=%s ORDER BY g.name", (user_id,) ) def get_group_member(user_id: int, group_id: int): return _panel_query( "SELECT * FROM group_members WHERE user_id=%s AND group_id=%s", (user_id, group_id), fetchone=True ) def get_group_members(group_id: int): return _panel_query( "SELECT u.id, u.username, u.email, u.last_login, gm.role, gm.permissions, gm.joined_at " "FROM group_members gm " "JOIN users u ON u.id=gm.user_id " "WHERE gm.group_id=%s ORDER BY gm.role DESC, u.username", (group_id,) ) def add_group_member(user_id: int, group_id: int, role: str = "member", permissions: dict = None): if permissions is None: permissions = Config.DEFAULT_PERMISSIONS _panel_query( "INSERT INTO group_members (user_id, group_id, role, permissions) VALUES (%s,%s,%s,%s) " "ON DUPLICATE KEY UPDATE role=VALUES(role), permissions=VALUES(permissions)", (user_id, group_id, role, json.dumps(permissions)), write=True ) def update_member(user_id: int, group_id: int, role: str, permissions: dict): _panel_query( "UPDATE group_members SET role=%s, permissions=%s WHERE user_id=%s AND group_id=%s", (role, json.dumps(permissions), user_id, group_id), write=True ) def remove_group_member(user_id: int, group_id: int): _panel_query( "DELETE FROM group_members WHERE user_id=%s AND group_id=%s", (user_id, group_id), write=True ) def get_permissions(user_id: int, group_id: int) -> dict: """Gibt die Berechtigungen des Nutzers in der Gruppe zurück.""" member = get_group_member(user_id, group_id) if not member: return {} raw = member.get("permissions") if isinstance(raw, str): return json.loads(raw) if isinstance(raw, dict): return raw return {} # ───────────────────────────────────────────────────────────── # MC-Datenbank-Zugangsdaten (verschlüsselt) # ───────────────────────────────────────────────────────────── def set_group_db_creds(group_id: int, host: str, port: int, user: str, password: str, database: str): """Verschlüsselt und speichert die MC-DB-Zugangsdaten einer Gruppe.""" _creds_query( "INSERT INTO group_databases (group_id, enc_host, enc_port, enc_user, enc_password, enc_database) " "VALUES (%s,%s,%s,%s,%s,%s) " "ON DUPLICATE KEY UPDATE enc_host=VALUES(enc_host), enc_port=VALUES(enc_port), " "enc_user=VALUES(enc_user), enc_password=VALUES(enc_password), enc_database=VALUES(enc_database)", (group_id, encrypt_str(host), encrypt_str(str(port)), encrypt_str(user), encrypt_str(password), encrypt_str(database)), write=True ) def get_group_db_creds(group_id: int) -> dict | None: """Gibt die entschlüsselten MC-DB-Zugangsdaten zurück oder None.""" row = _creds_query( "SELECT * FROM group_databases WHERE group_id=%s", (group_id,), fetchone=True ) if not row: return None return { "host": decrypt_str(row["enc_host"]), "port": int(decrypt_str(row["enc_port"])), "user": decrypt_str(row["enc_user"]), "password": decrypt_str(row["enc_password"]), "database": decrypt_str(row["enc_database"]), } def delete_group_db_creds(group_id: int): _creds_query("DELETE FROM group_databases WHERE group_id=%s", (group_id,), write=True) def has_db_configured(group_id: int) -> bool: row = _creds_query( "SELECT id FROM group_databases WHERE group_id=%s", (group_id,), fetchone=True ) return row is not None