From c9c684f97a307cf3c901f9d4f3a7cb53bac2d196 Mon Sep 17 00:00:00 2001 From: SimolZimol <70102430+SimolZimol@users.noreply.github.com> Date: Wed, 1 Apr 2026 02:55:32 +0200 Subject: [PATCH] modified: web/blueprints/auth.py modified: web/blueprints/group_admin.py modified: web/blueprints/panel.py modified: web/blueprints/site_admin.py modified: web/templates/admin/base.html modified: web/templates/admin/dashboard.html modified: web/templates/admin/group_edit.html modified: web/templates/admin/group_members.html modified: web/templates/admin/groups.html modified: web/templates/admin/user_edit.html modified: web/templates/admin/users.html modified: web/templates/auth/admin_login.html modified: web/templates/auth/login.html modified: web/templates/base.html modified: web/templates/group_admin/base.html modified: web/templates/group_admin/dashboard.html modified: web/templates/group_admin/database.html modified: web/templates/group_admin/member_edit.html modified: web/templates/group_admin/members.html modified: web/templates/panel/no_db.html --- web/blueprints/auth.py | 12 +- web/blueprints/group_admin.py | 30 +- web/blueprints/panel.py | 6 +- web/blueprints/site_admin.py | 42 +- web/templates/admin/base.html | 6 +- web/templates/admin/dashboard.html | 34 +- web/templates/admin/group_edit.html | 12 +- web/templates/admin/group_members.html | 26 +- web/templates/admin/groups.html | 24 +- web/templates/admin/user_edit.html | 16 +- web/templates/admin/users.html | 18 +- web/templates/auth/admin_login.html | 10 +- web/templates/auth/login.html | 8 +- web/templates/base.html | 8 +- web/templates/group_admin/base.html | 8 +- web/templates/group_admin/dashboard.html | 24 +- web/templates/group_admin/database.html | 34 +- web/templates/group_admin/member_edit.html | 14 +- web/templates/group_admin/members.html | 24 +- web/templates/panel/no_db.html | 12 +- web_all_content.txt | 4449 ++++++++++++++++++++ 21 files changed, 4633 insertions(+), 184 deletions(-) create mode 100644 web_all_content.txt diff --git a/web/blueprints/auth.py b/web/blueprints/auth.py index 215e745..1830d10 100644 --- a/web/blueprints/auth.py +++ b/web/blueprints/auth.py @@ -17,17 +17,17 @@ def login(): 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") + flash("Please use the 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." + error = "You are not assigned to any group. Please contact an admin." else: _set_user_session(user, groups) return redirect(url_for("panel.dashboard")) else: - error = "Falscher Benutzername oder Passwort." + error = "Incorrect username or password." return render_template("auth/login.html", error=error) @@ -46,9 +46,9 @@ def admin_login(): session["permissions"] = {} return redirect(url_for("site_admin.dashboard")) elif user: - error = "Keine Site-Admin-Berechtigung." + error = "No Site Admin privileges." else: - error = "Falscher Benutzername oder Passwort." + error = "Incorrect username or password." return render_template("auth/admin_login.html", error=error) @@ -66,7 +66,7 @@ def switch_group(group_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") + flash("Group not found or no access.", "danger") return redirect(url_for("panel.dashboard")) _apply_group(target) return redirect(url_for("panel.dashboard")) diff --git a/web/blueprints/group_admin.py b/web/blueprints/group_admin.py index 247b44c..35e9779 100644 --- a/web/blueprints/group_admin.py +++ b/web/blueprints/group_admin.py @@ -11,15 +11,15 @@ group_admin = Blueprint("group_admin", __name__, url_prefix="/group-admin") ALL_PERMISSIONS = [ ("view_dashboard", "Dashboard"), - ("view_players", "Spieler"), + ("view_players", "Players"), ("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"), + ("view_deaths", "Deaths"), + ("view_blocks", "Block Events"), + ("view_proxy", "Proxy Events"), + ("view_server_events", "Server Events"), + ("view_perms", "Permissions"), ] @@ -31,7 +31,7 @@ def group_admin_required(f): 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") + flash("You do not have group admin permission.", "danger") return redirect(url_for("panel.dashboard")) return f(*args, **kwargs) return decorated @@ -74,7 +74,7 @@ def member_add(): role = request.form.get("role", "member") if user_id: db.add_group_member(user_id, group_id, role) - flash("Mitglied hinzugefügt.", "success") + flash("Member added.", "success") return redirect(url_for("group_admin.members")) @@ -86,7 +86,7 @@ def member_edit(user_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") + flash("Member not found.", "danger") return redirect(url_for("group_admin.members")) raw_perms = member.get("permissions") @@ -96,7 +96,7 @@ def member_edit(user_id): 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") + flash("Permissions updated.", "success") return redirect(url_for("group_admin.members")) return render_template("group_admin/member_edit.html", @@ -108,10 +108,10 @@ def member_edit(user_id): @group_admin_required def member_remove(user_id): if user_id == session["user_id"]: - flash("Du kannst dich nicht selbst entfernen.", "danger") + flash("You cannot remove yourself.", "danger") else: db.remove_group_member(user_id, session["group_id"]) - flash("Mitglied entfernt.", "success") + flash("Member removed.", "success") return redirect(url_for("group_admin.members")) @@ -135,7 +135,7 @@ def database(): database_name = request.form.get("database", "").strip() if not all([host, port, user, database_name]): - error = "Host, Port, Benutzer und Datenbankname sind Pflichtfelder." + error = "Host, Port, User and Database name are required." else: try: # Verbindung testen @@ -147,7 +147,7 @@ def database(): ) test.close() db.set_group_db_creds(group_id, host, int(port), user, password, database_name) - flash("Datenbankverbindung gespeichert und getestet ✓", "success") + flash("Database connection saved and tested ✓", "success") return redirect(url_for("group_admin.database")) except Exception as e: error = f"Verbindungstest fehlgeschlagen: {e}" @@ -160,5 +160,5 @@ def database(): @group_admin_required def database_delete(): db.delete_group_db_creds(session["group_id"]) - flash("Datenbankverbindung entfernt.", "success") + flash("Database connection removed.", "success") return redirect(url_for("group_admin.database")) diff --git a/web/blueprints/panel.py b/web/blueprints/panel.py index 88fa399..657c634 100644 --- a/web/blueprints/panel.py +++ b/web/blueprints/panel.py @@ -38,7 +38,7 @@ def perm_required(perm): return f(*args, **kwargs) perms = session.get("permissions", {}) if not perms.get(perm, False): - flash("Du hast keine Berechtigung, diese Seite zu sehen.", "danger") + flash("You do not have permission to view this page.", "danger") return redirect(url_for("panel.dashboard")) return f(*args, **kwargs) return wrapped @@ -139,7 +139,7 @@ def dashboard(): ORDER BY timestamp DESC LIMIT 20 """) except Exception as e: - flash(f"Datenbankfehler: {e}", "danger") + flash(f"Database error: {e}", "danger") return render_template("panel/no_db.html") return render_template("panel/dashboard.html", @@ -176,7 +176,7 @@ def players(): def player_detail(uuid): player = query("SELECT * FROM players WHERE uuid = %s", (uuid,), fetchone=True) if not player: - flash("Spieler nicht gefunden.", "danger") + flash("Player not found.", "danger") return redirect(url_for("panel.players")) perms = session.get("permissions", {}) is_admin = session.get("is_site_admin") or session.get("role") == "admin" diff --git a/web/blueprints/site_admin.py b/web/blueprints/site_admin.py index a5aa349..f48eb39 100644 --- a/web/blueprints/site_admin.py +++ b/web/blueprints/site_admin.py @@ -64,12 +64,12 @@ def group_new(): name = request.form.get("name", "").strip() desc = request.form.get("description", "").strip() if not name: - flash("Gruppenname darf nicht leer sein.", "danger") + flash("Group name must not be empty.", "danger") elif db.get_group_by_name(name): - flash("Eine Gruppe mit diesem Namen existiert bereits.", "danger") + flash("A group with that name already exists.", "danger") else: db.create_group(name, desc) - flash(f"Gruppe '{name}' erstellt.", "success") + flash(f"Group '{name}' created.", "success") return redirect(url_for("site_admin.groups")) return render_template("admin/group_edit.html", group=None) @@ -79,16 +79,16 @@ def group_new(): def group_edit(group_id): group = db.get_group_by_id(group_id) if not group: - flash("Gruppe nicht gefunden.", "danger") + flash("Group not found.", "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") + flash("Group name must not be empty.", "danger") else: db.update_group(group_id, name, desc) - flash("Gruppe aktualisiert.", "success") + flash("Group updated.", "success") return redirect(url_for("site_admin.groups")) return render_template("admin/group_edit.html", group=group) @@ -97,7 +97,7 @@ def group_edit(group_id): @admin_required def group_delete(group_id): db.delete_group(group_id) - flash("Gruppe gelöscht.", "success") + flash("Group deleted.", "success") return redirect(url_for("site_admin.groups")) @@ -120,7 +120,7 @@ def group_member_add(group_id): role = request.form.get("role", "member") if user_id: db.add_group_member(user_id, group_id, role) - flash("Mitglied hinzugefügt.", "success") + flash("Member added.", "success") return redirect(url_for("site_admin.group_members", group_id=group_id)) @@ -128,7 +128,7 @@ def group_member_add(group_id): @admin_required def group_member_remove(group_id, user_id): db.remove_group_member(user_id, group_id) - flash("Mitglied entfernt.", "success") + flash("Member removed.", "success") return redirect(url_for("site_admin.group_members", group_id=group_id)) @@ -141,7 +141,7 @@ def group_member_toggle_role(group_id, user_id): 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") + flash(f"Role changed to '{new_role}'.", "success") return redirect(url_for("site_admin.group_members", group_id=group_id)) @@ -164,12 +164,12 @@ def user_new(): 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") + flash("All fields are required.", "danger") elif db.get_user_by_username(username): - flash("Benutzername bereits vergeben.", "danger") + flash("Username already taken.", "danger") else: db.create_user(username, email, password, is_site_admin) - flash(f"Nutzer '{username}' erstellt.", "success") + flash(f"User '{username}' created.", "success") return redirect(url_for("site_admin.users")) return render_template("admin/user_edit.html", user=None) @@ -179,7 +179,7 @@ def user_new(): def user_edit(user_id): user = db.get_user_by_id(user_id) if not user: - flash("Nutzer nicht gefunden.", "danger") + flash("User not found.", "danger") return redirect(url_for("site_admin.users")) if request.method == "POST": username = request.form.get("username", "").strip() @@ -189,8 +189,8 @@ def user_edit(user_id): 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") + flash("Password changed.", "info") + flash("User updated.", "success") return redirect(url_for("site_admin.users")) return render_template("admin/user_edit.html", user=user) @@ -199,10 +199,10 @@ def user_edit(user_id): @admin_required def user_delete(user_id): if user_id == session.get("user_id"): - flash("Du kannst dich nicht selbst löschen.", "danger") + flash("You cannot delete yourself.", "danger") else: db.delete_user(user_id) - flash("Nutzer gelöscht.", "success") + flash("User deleted.", "success") return redirect(url_for("site_admin.users")) @@ -213,13 +213,13 @@ def user_delete(user_id): @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.""" + """Site Admin temporarily switches into a group to view its MC data.""" group = db.get_group_by_id(group_id) if not group: - flash("Gruppe nicht gefunden.", "danger") + flash("Group not found.", "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") + flash("No database configured for this group.", "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", diff --git a/web/templates/admin/base.html b/web/templates/admin/base.html index 513b168..ad64c5c 100644 --- a/web/templates/admin/base.html +++ b/web/templates/admin/base.html @@ -1,5 +1,5 @@ - + @@ -16,8 +16,8 @@
Dashboard - Gruppen - Benutzer + Groups + Users Logout diff --git a/web/templates/admin/dashboard.html b/web/templates/admin/dashboard.html index e265c46..f977f6c 100644 --- a/web/templates/admin/dashboard.html +++ b/web/templates/admin/dashboard.html @@ -8,7 +8,7 @@
{{ stats.group_count }}
-
Gruppen
+
Groups
@@ -16,7 +16,7 @@
{{ stats.user_count }}
-
Benutzer
+
Users
@@ -24,7 +24,7 @@
{{ stats.db_configured }}
-
DBs konfiguriert
+
DBs configured
@@ -42,14 +42,14 @@
- Gruppen + Groups - Neu + New
- + {% for g in groups %} @@ -57,28 +57,28 @@ {% else %} - + {% endfor %}
NameMitgliederDB
NameMembersDB
{{ g.member_count }} {% if g.has_db %} - Konfiguriert + Configured {% else %} - Keine + None {% endif %} - +
Keine Gruppen vorhanden
No groups yet
@@ -86,14 +86,14 @@
- Benutzer + Users - Neu + New
- + {% for u in users %} @@ -101,19 +101,19 @@ {% else %} - + {% endfor %}
BenutzerGruppenAdmin
UserGroupsAdmin
{{ u.group_count }} {% if u.is_site_admin %}{% endif %} - +
Keine Benutzer vorhanden
No users yet
diff --git a/web/templates/admin/group_edit.html b/web/templates/admin/group_edit.html index 416062e..6a5b959 100644 --- a/web/templates/admin/group_edit.html +++ b/web/templates/admin/group_edit.html @@ -1,11 +1,11 @@ {% extends "admin/base.html" %} -{% block title %}{{ 'Gruppe bearbeiten' if group else 'Neue Gruppe' }}{% endblock %} +{% block title %}{{ 'Edit Group' if group else 'New Group' }}{% endblock %} {% block content %}
-

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

+

{{ 'Edit Group' if group else 'Create New Group' }}

@@ -14,19 +14,19 @@
- +
- +
- Abbrechen + Cancel
diff --git a/web/templates/admin/group_members.html b/web/templates/admin/group_members.html index 2963720..3391e65 100644 --- a/web/templates/admin/group_members.html +++ b/web/templates/admin/group_members.html @@ -1,21 +1,21 @@ {% extends "admin/base.html" %} -{% block title %}Mitglieder – {{ group.name }}{% endblock %} +{% block title %}Members – {{ group.name }}{% endblock %} {% block content %}
-

Mitglieder: {{ group.name }}

+

Members: {{ group.name }}

-
Aktuelle Mitglieder ({{ members|length }})
+
Current Members ({{ members|length }})
- + {% for m in members %} @@ -29,20 +29,20 @@ {% else %} - + {% endfor %}
BenutzerRolleAktionen
UserRoleActions
-
-
Keine Mitglieder
No members
@@ -53,12 +53,12 @@
-
Benutzer hinzufügen
+
Add User
{% if non_members %}
- +
- +
{% else %} -

Alle Benutzer sind bereits Mitglied.

+

All users are already members.

{% endif %}
diff --git a/web/templates/admin/groups.html b/web/templates/admin/groups.html index fe7b062..2c47349 100644 --- a/web/templates/admin/groups.html +++ b/web/templates/admin/groups.html @@ -1,10 +1,10 @@ {% extends "admin/base.html" %} -{% block title %}Gruppen{% endblock %} +{% block title %}Groups{% endblock %} {% block content %}
-

Gruppen

+

Groups

- Neue Gruppe + New Group
@@ -13,7 +13,7 @@ - + @@ -25,32 +25,32 @@ {% else %} - + {% endfor %}
IDNameBeschreibungMitgliederDBErstelltAktionenIDNameDescriptionMembersDBCreatedActions
{{ g.member_count }} {% if g.has_db %} - Konfiguriert + Configured {% else %} - Keine DB + No DB {% endif %} {{ g.created_at | fmt_dt }} - + - + - +
-
Noch keine Gruppen vorhanden.
No groups yet.
diff --git a/web/templates/admin/user_edit.html b/web/templates/admin/user_edit.html index 2deafc2..0bdee3a 100644 --- a/web/templates/admin/user_edit.html +++ b/web/templates/admin/user_edit.html @@ -1,11 +1,11 @@ {% extends "admin/base.html" %} -{% block title %}{{ 'Benutzer bearbeiten' if user else 'Neuer Benutzer' }}{% endblock %} +{% block title %}{{ 'Edit User' if user else 'New User' }}{% endblock %} {% block content %}
-

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

+

{{ 'Edit User: ' ~ user.username if user else 'New User' }}

@@ -14,16 +14,16 @@
- +
- + {% if not user %} -
Mindestens 8 Zeichen empfohlen.
+
Minimum 8 characters recommended.
{% endif %}
@@ -32,15 +32,15 @@ value="1" {{ 'checked' if user and user.is_site_admin }}>
- Abbrechen + Cancel
diff --git a/web/templates/admin/users.html b/web/templates/admin/users.html index 7b0554d..3f5fc89 100644 --- a/web/templates/admin/users.html +++ b/web/templates/admin/users.html @@ -1,10 +1,10 @@ {% extends "admin/base.html" %} -{% block title %}Benutzer{% endblock %} +{% block title %}Users{% endblock %} {% block content %} @@ -12,7 +12,7 @@
- + {% for u in users %} @@ -23,7 +23,7 @@ {{ g.name }} {% if g.role == 'admin' %}{% endif %} - {% else %}Keine{% endfor %} + {% else %}None{% endfor %} {% else %} - + {% endfor %}
BenutzerGruppenSite AdminErstelltAktionen
UserGroupsSite AdminCreatedActions
{% if u.is_site_admin %} @@ -32,19 +32,19 @@ {{ u.created_at | fmt_dt }} - +
-
Keine Benutzer vorhanden.
No users yet.
diff --git a/web/templates/auth/admin_login.html b/web/templates/auth/admin_login.html index 5879da4..c7ef0a8 100644 --- a/web/templates/auth/admin_login.html +++ b/web/templates/auth/admin_login.html @@ -1,5 +1,5 @@ - + @@ -16,7 +16,7 @@

Site Admin

-

Administrativer Zugang

+

Administrative Access

{% with messages = get_flashed_messages(with_categories=true) %} @@ -31,7 +31,7 @@
- +
- + diff --git a/web/templates/auth/login.html b/web/templates/auth/login.html index 7231159..4c07a6d 100644 --- a/web/templates/auth/login.html +++ b/web/templates/auth/login.html @@ -1,5 +1,5 @@ - + @@ -31,7 +31,7 @@
- +
- +
diff --git a/web/templates/base.html b/web/templates/base.html index 10a9061..ce03c8e 100644 --- a/web/templates/base.html +++ b/web/templates/base.html @@ -1,5 +1,5 @@ - + @@ -101,7 +101,7 @@ {% if not session.get('is_site_admin') and user_groups and user_groups|length > 1 %}
- Gruppe wechseln: + Switch group: {% for g in user_groups %} @@ -114,13 +114,13 @@ {% if session.get('role') == 'admin' and not session.get('is_site_admin') %} - Gruppe verwalten + Manage Group {% endif %} {% if session.get('is_site_admin') %} {% if session.get('admin_viewing') %} - Zurück zum Admin + Back to Admin {% else %} diff --git a/web/templates/group_admin/base.html b/web/templates/group_admin/base.html index 7d4c712..1fe0559 100644 --- a/web/templates/group_admin/base.html +++ b/web/templates/group_admin/base.html @@ -1,9 +1,9 @@ - + - {% block title %}Gruppen Admin{% endblock %} — {{ session.get('group_name','') }} + {% block title %}Group Admin{% endblock %} — {{ session.get('group_name','') }} @@ -16,8 +16,8 @@
Dashboard - Mitglieder - Datenbank + Members + Database Panel diff --git a/web/templates/group_admin/dashboard.html b/web/templates/group_admin/dashboard.html index 075d260..6b9a4b6 100644 --- a/web/templates/group_admin/dashboard.html +++ b/web/templates/group_admin/dashboard.html @@ -1,14 +1,14 @@ {% extends "group_admin/base.html" %} {% block title %}Dashboard{% endblock %} {% block content %} -

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

+

Group Admin: {{ session.get('group_name') }}

{{ stats.member_count }}
-
Mitglieder
+
Members
@@ -16,9 +16,9 @@
- {{ 'Ja' if stats.db_configured else 'Nein' }} + {{ 'Yes' if stats.db_configured else 'No' }}
-
DB konfiguriert
+
DB configured
@@ -40,13 +40,13 @@
@@ -54,19 +54,19 @@
-
Gruppeninfo
+
Group Info
Name
{{ session.get('group_name') }}
-
Deine Rolle
+
Your Role
Admin
-
Datenbank
+
Database
{% if stats.db_configured %} - Verbunden + Connected {% else %} - Nicht konfiguriert + Not configured {% endif %}
diff --git a/web/templates/group_admin/database.html b/web/templates/group_admin/database.html index 437ac2f..b968c38 100644 --- a/web/templates/group_admin/database.html +++ b/web/templates/group_admin/database.html @@ -1,19 +1,19 @@ {% extends "group_admin/base.html" %} -{% block title %}Datenbank{% endblock %} +{% block title %}Database{% endblock %} {% block content %} -

MC Datenbank konfigurieren

+

Configure MC Database

-
Verbindungsdaten
+
Connection Details
{% if test_result is defined %}
{% if test_result %} - Verbindung erfolgreich! Daten wurden gespeichert. + Connection successful! Settings saved. {% else %} - Verbindung fehlgeschlagen: {{ test_error }} + Connection failed: {{ test_error }} {% endif %}
{% endif %} @@ -32,34 +32,34 @@ value="{{ creds.port if creds else request.form.get('port', '3306') }}">
- +
- +
- + + placeholder="{{ '(unchanged)' if creds else '' }}"> {% if creds %} -
Leer lassen um das bestehende Passwort beizubehalten.
+
Leave blank to keep the existing password.
{% endif %}
{% if creds %} {% endif %}
@@ -73,14 +73,14 @@
Info

- Gib hier die Verbindungsdaten zu deiner MCLogger MySQL-Datenbank ein. - Das Panel liest nur Daten (SELECT) — schreibender Zugriff ist nicht nötig. + Enter the connection details for your MCLogger MySQL database. + The panel only reads data (SELECT) — write access is not required.

- Die Zugangsdaten werden verschlüsselt gespeichert und sind nur für deine Gruppe sichtbar. + Credentials are stored encrypted and are only visible to your group.


-

Benötigte Tabellen:

+

Required tables:

  • player_sessions
  • chat_messages
  • diff --git a/web/templates/group_admin/member_edit.html b/web/templates/group_admin/member_edit.html index a50b30c..c4b60ae 100644 --- a/web/templates/group_admin/member_edit.html +++ b/web/templates/group_admin/member_edit.html @@ -1,11 +1,11 @@ {% extends "group_admin/base.html" %} -{% block title %}Berechtigungen – {{ member.username }}{% endblock %} +{% block title %}Permissions – {{ member.username }}{% endblock %} {% block content %}
    -

    Berechtigungen: {{ member.username }}

    +

    Permissions: {{ member.username }}

    @@ -17,16 +17,16 @@
    - + -
    Admins können Mitglieder und die DB-Verbindung verwalten.
    +
    Admins can manage members and the DB connection.

    -

    Panel-Zugriff

    +

    Panel Access

    {% for key, label in all_permissions %}
    @@ -42,9 +42,9 @@
    - Abbrechen + Cancel
    diff --git a/web/templates/group_admin/members.html b/web/templates/group_admin/members.html index 3e40e67..41388cd 100644 --- a/web/templates/group_admin/members.html +++ b/web/templates/group_admin/members.html @@ -1,16 +1,16 @@ {% extends "group_admin/base.html" %} -{% block title %}Mitglieder{% endblock %} +{% block title %}Members{% endblock %} {% block content %} -

    Mitglieder

    +

    Members

    -
    Aktuelle Mitglieder ({{ members|length }})
    +
    Current Members ({{ members|length }})
    - + {% for m in members %} @@ -24,22 +24,22 @@ {% else %} - + {% endfor %}
    BenutzerRolleAktionen
    UserRoleActions
    {% if m.id != session.get('user_id') %} - +
    -
    {% else %} - Du + You {% endif %}
    Keine Mitglieder
    No members
    @@ -50,13 +50,13 @@
    -
    Hinweis
    +
    Note

    - Neue Mitglieder müssen vom Site Admin zur Gruppe hinzugefügt werden. + New members must be added by the Site Admin.

    - Als Gruppenadmin kannst du Berechtigungen bestehender Mitglieder verwalten und Mitglieder entfernen. + As group admin you can manage permissions of existing members and remove members.

    diff --git a/web/templates/panel/no_db.html b/web/templates/panel/no_db.html index 594b00a..91237a3 100644 --- a/web/templates/panel/no_db.html +++ b/web/templates/panel/no_db.html @@ -1,22 +1,22 @@ {% extends "base.html" %} {% block title %}No Database{% endblock %} -{% block page_title %}Keine Datenbank{% endblock %} +{% block page_title %}No Database{% endblock %} {% block content %}
    -

    Keine Datenbank konfiguriert

    +

    No database configured

    - Für diese Gruppe ist noch keine MC-Datenbank eingerichtet. + No MC database has been set up for this group. {% if session.get('role') == 'admin' %} - Du kannst die Verbindung als Gruppen-Admin konfigurieren. + You can configure the connection as group admin. {% else %} - Bitte wende dich an deinen Gruppenadmin. + Please contact your group admin. {% endif %}

    {% if session.get('role') == 'admin' %} - Datenbank konfigurieren + Configure Database {% endif %}
    diff --git a/web_all_content.txt b/web_all_content.txt new file mode 100644 index 0000000..0dd0a8c --- /dev/null +++ b/web_all_content.txt @@ -0,0 +1,4449 @@ +# 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 %} +
    +

    Gruppen

    + + Neue Gruppe + +
    + +
    +
    + + + + + + + + {% 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 %} +
    +

    Benutzer

    + + Neuer Benutzer + +
    + +
    +
    + + + + + + {% 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 %} + +
    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 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 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
    +
    +
    +
    + + +{% 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 +