From 935dc3f909ed6cfd5d21135ed1d6f49495e34ec2 Mon Sep 17 00:00:00 2001 From: simon Date: Mon, 13 Apr 2026 09:55:50 +0200 Subject: [PATCH] modified: web/app.py modified: web/blueprints/auth.py modified: web/blueprints/group_admin.py modified: web/blueprints/panel.py modified: web/blueprints/site_admin.py modified: web/config.py new file: web/templates/404.html modified: web/templates/admin/base.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/database.html modified: web/templates/group_admin/member_edit.html modified: web/templates/group_admin/members.html modified: web/templates/login.html modified: web/templates/panel/dashboard.html --- web/app.py | 73 ++++++++++++++++++++- web/blueprints/auth.py | 4 +- web/blueprints/group_admin.py | 2 +- web/blueprints/panel.py | 9 +-- web/blueprints/site_admin.py | 4 +- web/config.py | 40 +++++++++++- web/templates/404.html | 76 ++++++++++++++++++++++ web/templates/admin/base.html | 9 ++- web/templates/admin/group_edit.html | 1 + web/templates/admin/group_members.html | 3 + web/templates/admin/groups.html | 10 ++- web/templates/admin/user_edit.html | 1 + web/templates/admin/users.html | 1 + web/templates/auth/admin_login.html | 1 + web/templates/auth/login.html | 1 + web/templates/base.html | 29 ++++++--- web/templates/group_admin/base.html | 9 ++- web/templates/group_admin/database.html | 23 ++++--- web/templates/group_admin/member_edit.html | 3 +- web/templates/group_admin/members.html | 1 + web/templates/login.html | 1 + web/templates/panel/dashboard.html | 9 +-- 22 files changed, 260 insertions(+), 50 deletions(-) create mode 100644 web/templates/404.html diff --git a/web/app.py b/web/app.py index d7a5f9a..ea9b153 100644 --- a/web/app.py +++ b/web/app.py @@ -3,8 +3,9 @@ MCLogger – Flask Web-Panel Multi-Tenant mit Gruppen, Rollen & verschlüsselten DB-Zugangsdaten. Coolify-kompatibel: alle Einstellungen via ENV. """ +import secrets from datetime import datetime -from flask import Flask, session +from flask import Flask, abort, render_template, request, session, url_for from config import Config from panel_db import init_databases, get_user_groups @@ -17,6 +18,13 @@ from blueprints.panel import panel def create_app() -> Flask: app = Flask(__name__) app.secret_key = Config.SECRET_KEY + app.config.update( + SESSION_COOKIE_HTTPONLY=Config.SESSION_COOKIE_HTTPONLY, + SESSION_COOKIE_SAMESITE=Config.SESSION_COOKIE_SAMESITE, + SESSION_COOKIE_SECURE=Config.SESSION_COOKIE_SECURE, + ) + + Config.validate_security() # Blueprints registrieren app.register_blueprint(auth) @@ -32,6 +40,68 @@ def create_app() -> Flask: # ── Template-Filter ─────────────────────────────────────── + def _get_or_create_csrf_token() -> str: + token = session.get("_csrf_token") + if not token: + token = secrets.token_urlsafe(32) + session["_csrf_token"] = token + return token + + @app.before_request + def enforce_csrf(): + if request.method not in {"POST", "PUT", "PATCH", "DELETE"}: + return + + session_token = session.get("_csrf_token") + request_token = request.form.get("_csrf_token") or request.headers.get("X-CSRF-Token") + if not session_token or not request_token or session_token != request_token: + abort(400) + + @app.after_request + def set_security_headers(resp): + resp.headers.setdefault("X-Content-Type-Options", "nosniff") + resp.headers.setdefault("X-Frame-Options", "DENY") + resp.headers.setdefault("Referrer-Policy", "strict-origin-when-cross-origin") + resp.headers.setdefault("Content-Security-Policy", "default-src 'self'; script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; img-src 'self' data:; font-src 'self' https://cdn.jsdelivr.net; connect-src 'self'; frame-ancestors 'none';") + return resp + + @app.errorhandler(400) + def bad_request(_): + return "Bad request", 400 + + @app.errorhandler(404) + def not_found(_): + uid = session.get("user_id") + is_site_admin = bool(session.get("is_site_admin")) + role = session.get("role") + + links = [] + if not uid: + links = [ + {"label": "Login", "href": url_for("auth.login"), "btn": "btn-success"}, + {"label": "Site Admin Login", "href": url_for("auth.admin_login"), "btn": "btn-outline-danger"}, + ] + elif is_site_admin and not session.get("group_id"): + links = [ + {"label": "Site Admin Dashboard", "href": url_for("site_admin.dashboard"), "btn": "btn-danger"}, + ] + else: + links.append({"label": "Panel Dashboard", "href": url_for("panel.dashboard"), "btn": "btn-success"}) + if is_site_admin: + links.append({"label": "Site Admin", "href": url_for("site_admin.dashboard"), "btn": "btn-outline-danger"}) + if role == "admin" and not is_site_admin: + links.append({"label": "Group Admin", "href": url_for("group_admin.dashboard"), "btn": "btn-outline-warning"}) + + return render_template( + "404.html", + requested_path=request.path, + request_method=request.method, + links=links, + is_logged_in=bool(uid), + is_site_admin=is_site_admin, + role=role, + ), 404 + @app.template_filter("fmt_duration") def fmt_duration(seconds): if seconds is None: @@ -64,6 +134,7 @@ def create_app() -> Flask: "app_version": "2.0.0", "author": "SimolZimol", "user_groups": groups, + "csrf_token": _get_or_create_csrf_token, } return app diff --git a/web/blueprints/auth.py b/web/blueprints/auth.py index 1830d10..8f0d387 100644 --- a/web/blueprints/auth.py +++ b/web/blueprints/auth.py @@ -52,13 +52,13 @@ def admin_login(): return render_template("auth/admin_login.html", error=error) -@auth.route("/logout") +@auth.route("/logout", methods=["POST"]) def logout(): session.clear() return redirect(url_for("auth.login")) -@auth.route("/switch-group/") +@auth.route("/switch-group/", methods=["POST"]) def switch_group(group_id): if not session.get("user_id") or session.get("is_site_admin"): return redirect(url_for("auth.login")) diff --git a/web/blueprints/group_admin.py b/web/blueprints/group_admin.py index 449c0eb..2f76bfe 100644 --- a/web/blueprints/group_admin.py +++ b/web/blueprints/group_admin.py @@ -99,7 +99,7 @@ def member_edit(user_id): if request.method == "POST": role = request.form.get("role", "member") - new_perms = {key: (request.form.get(key) == "1") for key, _ in ALL_PERMISSIONS} + new_perms = {key: bool(request.form.get(f"perm_{key}")) for key, _ in ALL_PERMISSIONS} db.update_member(user_id, group_id, role, new_perms) flash("Permissions updated.", "success") return redirect(url_for("group_admin.members")) diff --git a/web/blueprints/panel.py b/web/blueprints/panel.py index 8951252..a66a5f8 100644 --- a/web/blueprints/panel.py +++ b/web/blueprints/panel.py @@ -118,7 +118,7 @@ def dashboard(): "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 + SELECT p.uuid AS player_uuid, p.username AS player_name, ps.server_name, ps.login_time FROM player_sessions ps JOIN players p ON p.uuid = ps.player_uuid WHERE ps.logout_time IS NULL @@ -148,8 +148,9 @@ def dashboard(): recent = query(""" SELECT * FROM v_recent_activity LIMIT 50 """) - except Exception as e: - flash(f"Database error: {e}", "danger") + except Exception: + panel.logger.exception("Database error while rendering dashboard") + flash("Database query failed. Please contact an administrator.", "danger") return render_template("panel/no_db.html") return render_template("panel/dashboard.html", @@ -405,7 +406,7 @@ def perms(): @login_required def api_online(): rows = query(""" - SELECT p.username, ps.server_name, ps.login_time + SELECT p.uuid AS player_uuid, p.username AS player_name, ps.server_name, ps.login_time 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 diff --git a/web/blueprints/site_admin.py b/web/blueprints/site_admin.py index bb437d7..e48c4b7 100644 --- a/web/blueprints/site_admin.py +++ b/web/blueprints/site_admin.py @@ -216,7 +216,7 @@ def user_delete(user_id): # Als Gruppe anzeigen (Site-Admin liest Gruppen-DB) # ────────────────────────────────────────────────────────────── -@site_admin.route("/view-group/") +@site_admin.route("/view-group/", methods=["POST"]) @admin_required def view_group(group_id): """Site Admin temporarily switches into a group to view its MC data.""" @@ -239,7 +239,7 @@ def view_group(group_id): return redirect(url_for("panel.dashboard")) -@site_admin.route("/stop-view") +@site_admin.route("/stop-view", methods=["POST"]) @admin_required def stop_view(): """Kehrt zum Site-Admin-Dashboard zurück.""" diff --git a/web/config.py b/web/config.py index a39a10b..57c5851 100644 --- a/web/config.py +++ b/web/config.py @@ -5,12 +5,29 @@ Alle Einstellungen über ENV-Variablen (Coolify-kompatibel). import os +def _as_bool(value: str | None, default: bool = False) -> bool: + if value is None: + return default + return value.strip().lower() in {"1", "true", "yes", "on"} + + class Config: + DEFAULT_SECRET_KEY = "change-me-use-a-long-random-string-min-32-chars" + DEFAULT_PASSWORD_PEPPER = "change-me-global-pepper-secret-never-change" + # ── Flask ────────────────────────────────────────────────── - SECRET_KEY = os.getenv("SECRET_KEY", "change-me-use-a-long-random-string-min-32-chars") + SECRET_KEY = os.getenv("SECRET_KEY", DEFAULT_SECRET_KEY) HOST = os.getenv("HOST") or "0.0.0.0" PORT = int(os.getenv("PORT") or "5000") - DEBUG = (os.getenv("DEBUG") or "false").lower() == "true" + DEBUG = _as_bool(os.getenv("DEBUG"), default=False) + + # Session-Cookie-Hardening + SESSION_COOKIE_HTTPONLY = True + SESSION_COOKIE_SAMESITE = os.getenv("SESSION_COOKIE_SAMESITE") or "Lax" + SESSION_COOKIE_SECURE = _as_bool(os.getenv("SESSION_COOKIE_SECURE"), default=not DEBUG) + + # Erzwingt sichere Secrets in Nicht-Debug-Umgebungen + ENFORCE_SECURE_CONFIG = _as_bool(os.getenv("ENFORCE_SECURE_CONFIG"), default=True) # ── Panel-Datenbank (Nutzer, Gruppen, Mitgliedschaften) ──── PANEL_DB_HOST = os.getenv("PANEL_DB_HOST") or "localhost" @@ -27,7 +44,7 @@ class Config: 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") + PASSWORD_PEPPER = os.getenv("PASSWORD_PEPPER", DEFAULT_PASSWORD_PEPPER) # Generieren: python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())" FERNET_KEY = os.getenv("FERNET_KEY", "") @@ -44,3 +61,20 @@ class Config: "view_server_events": False, "view_perms": False, } + + @classmethod + def validate_security(cls): + """Fail-fast, damit unsichere Defaults nicht in Produktion laufen.""" + if not cls.ENFORCE_SECURE_CONFIG or cls.DEBUG: + return + + issues = [] + if cls.SECRET_KEY == cls.DEFAULT_SECRET_KEY: + issues.append("SECRET_KEY must be set to a strong random value.") + if cls.PASSWORD_PEPPER == cls.DEFAULT_PASSWORD_PEPPER: + issues.append("PASSWORD_PEPPER must be set to a strong secret value.") + if not cls.FERNET_KEY: + issues.append("FERNET_KEY must be configured.") + + if issues: + raise RuntimeError("Invalid security configuration: " + " ".join(issues)) diff --git a/web/templates/404.html b/web/templates/404.html new file mode 100644 index 0000000..f876e7d --- /dev/null +++ b/web/templates/404.html @@ -0,0 +1,76 @@ + + + + + + 404 - Not Found + + + + + +
+
+
+ +
+

404 - Seite nicht gefunden

+

Die angeforderte Route existiert nicht oder wurde verschoben.

+
+
+ +
+
Anfrage
+
{{ request_method }} {{ requested_path }}
+
+ +
+ {% if not is_logged_in %} +

Du bist aktuell nicht eingeloggt. Starte am besten ueber die Login-Seite.

+ {% elif is_site_admin and not session.get('group_id') %} +

Du bist als Site Admin eingeloggt. Von dort kannst du Gruppen und Benutzer verwalten.

+ {% elif role == 'admin' %} +

Du bist Gruppen-Admin. Nutze Panel oder Group-Admin, um wieder in gueltige Bereiche zu kommen.

+ {% else %} +

Nutze das Dashboard, um wieder in bekannte Bereiche zu navigieren.

+ {% endif %} +
+ +
+ {% for link in links %} + {{ link.label }} + {% endfor %} + Zurueck +
+
+
+ + diff --git a/web/templates/admin/base.html b/web/templates/admin/base.html index ad64c5c..1f27289 100644 --- a/web/templates/admin/base.html +++ b/web/templates/admin/base.html @@ -18,9 +18,12 @@ Dashboard Groups Users - - Logout - +
+ + +
diff --git a/web/templates/admin/group_edit.html b/web/templates/admin/group_edit.html index 6a5b959..bbf9b63 100644 --- a/web/templates/admin/group_edit.html +++ b/web/templates/admin/group_edit.html @@ -13,6 +13,7 @@
+
+
+ @@ -57,6 +59,7 @@
{% if non_members %} +
+ + @@ -43,6 +46,7 @@
+ diff --git a/web/templates/admin/user_edit.html b/web/templates/admin/user_edit.html index f093996..4a28938 100644 --- a/web/templates/admin/user_edit.html +++ b/web/templates/admin/user_edit.html @@ -13,6 +13,7 @@
+
+ diff --git a/web/templates/auth/admin_login.html b/web/templates/auth/admin_login.html index c7ef0a8..314098e 100644 --- a/web/templates/auth/admin_login.html +++ b/web/templates/auth/admin_login.html @@ -30,6 +30,7 @@
+
diff --git a/web/templates/auth/login.html b/web/templates/auth/login.html index 4c07a6d..d9ae180 100644 --- a/web/templates/auth/login.html +++ b/web/templates/auth/login.html @@ -30,6 +30,7 @@
+
diff --git a/web/templates/base.html b/web/templates/base.html index b5f703e..5cf0614 100644 --- a/web/templates/base.html +++ b/web/templates/base.html @@ -103,10 +103,13 @@ {% endif %} @@ -119,9 +122,12 @@ {% endif %} {% if session.get('is_site_admin') %} {% if session.get('admin_viewing') %} - - Back to Admin - +
+ + +
{% else %} Site Admin @@ -135,9 +141,12 @@ Online
{{ session.get('username', '') }} - - Logout - +
+ + +
diff --git a/web/templates/group_admin/base.html b/web/templates/group_admin/base.html index 1fe0559..a832284 100644 --- a/web/templates/group_admin/base.html +++ b/web/templates/group_admin/base.html @@ -21,9 +21,12 @@ Panel - - - +
+ + +
diff --git a/web/templates/group_admin/database.html b/web/templates/group_admin/database.html index fd549cb..43f604c 100644 --- a/web/templates/group_admin/database.html +++ b/web/templates/group_admin/database.html @@ -15,6 +15,7 @@ {% endif %}
+
@@ -48,18 +49,22 @@
-
+
- {% if creds %} - - {% endif %}
+ + {% if creds %} +
+ + +
+ {% endif %}
@@ -79,13 +84,13 @@

Required tables:

  • player_sessions
  • -
  • chat_messages
  • +
  • player_chat
  • player_commands
  • block_events
  • player_deaths
  • proxy_events
  • server_events
  • -
  • permission_changes
  • +
  • plugin_events
diff --git a/web/templates/group_admin/member_edit.html b/web/templates/group_admin/member_edit.html index c4b60ae..e124a94 100644 --- a/web/templates/group_admin/member_edit.html +++ b/web/templates/group_admin/member_edit.html @@ -16,6 +16,7 @@
+
+ {{ 'checked' if current_perms.get(key, True) }}>
diff --git a/web/templates/group_admin/members.html b/web/templates/group_admin/members.html index 41388cd..e399ca1 100644 --- a/web/templates/group_admin/members.html +++ b/web/templates/group_admin/members.html @@ -29,6 +29,7 @@ + diff --git a/web/templates/login.html b/web/templates/login.html index 782e13a..0edd476 100644 --- a/web/templates/login.html +++ b/web/templates/login.html @@ -24,6 +24,7 @@ {% endif %} +
diff --git a/web/templates/panel/dashboard.html b/web/templates/panel/dashboard.html index 689cf52..1e92718 100644 --- a/web/templates/panel/dashboard.html +++ b/web/templates/panel/dashboard.html @@ -38,7 +38,7 @@
Online Players -
@@ -183,12 +183,5 @@ new Chart(deathCtx, { }, options: { plugins: { legend: { position: 'bottom', labels: { font: { size:10 } } } } } }); -function refreshOnline() { - fetch('/api/online').then(r => r.json()).then(data => { - document.getElementById('online-count').textContent = data.length; - }); -} -setInterval(refreshOnline, 30000); -refreshOnline(); {% endblock %}