from flask import Blueprint, request, jsonify from werkzeug.security import generate_password_hash from auth.token import verify_token from util.db_pool import get_connection from util.logger import logger admin_bp = Blueprint("admin", __name__) _VALID_ROLES = {"user", "admin"} # Module-level flag: DDL runs at most once per process lifetime. # Resets automatically on worker restart, which re-triggers the check. _tables_initialized = False def _require_admin(): user = verify_token() if not user: return None, (jsonify({"message": "Nicht autorisiert"}), 401) if user.get("role") != "admin": logger.warning("🚫 Adminbereich verweigert (kein Admin)") return None, (jsonify({"message": "Adminrechte erforderlich"}), 403) return user, None def _ensure_tables(cur): global _tables_initialized if _tables_initialized: return cur.execute( """ CREATE TABLE IF NOT EXISTS users ( id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(255) NOT NULL, role VARCHAR(20) NOT NULL ) """ ) cur.execute( """ CREATE TABLE IF NOT EXISTS websites ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(100) NOT NULL, url VARCHAR(255) NOT NULL, description VARCHAR(255) DEFAULT '' ) """ ) _tables_initialized = True @admin_bp.route("/api/admin/users", methods=["GET"]) def list_users(): _, err = _require_admin() if err: return err try: conn = get_connection() try: cur = conn.cursor(dictionary=True) _ensure_tables(cur) cur.execute("SELECT id, username, role FROM users ORDER BY username ASC") users = cur.fetchall() cur.close() finally: conn.close() return jsonify(users) except Exception as e: logger.error(f"[Admin list_users] {e}") return jsonify({"message": "Serverfehler"}), 500 @admin_bp.route("/api/admin/users", methods=["POST"]) def create_user(): admin, err = _require_admin() if err: return err data = request.get_json(silent=True) or {} username = data.get("username", "").strip() password = data.get("password", "") role = data.get("role", "user") if not username or not password: return jsonify({"message": "Username und Passwort erforderlich"}), 400 if role not in _VALID_ROLES: return jsonify({"message": f"Ungültige Rolle. Erlaubt: {', '.join(_VALID_ROLES)}"}), 400 try: conn = get_connection() try: cur = conn.cursor(dictionary=True) _ensure_tables(cur) cur.execute("SELECT id FROM users WHERE username=%s", (username,)) if cur.fetchone(): cur.close() return jsonify({"message": "Nutzer existiert bereits"}), 409 cur.execute( "INSERT INTO users (username, password, role) VALUES (%s, %s, %s)", (username, generate_password_hash(password), role) ) conn.commit() new_id = cur.lastrowid cur.close() finally: conn.close() logger.info(f"✅ User erstellt: {username} durch {admin['username']}") return jsonify({"id": new_id, "username": username, "role": role}), 201 except Exception as e: logger.error(f"[Admin create_user] {e}") return jsonify({"message": "Serverfehler"}), 500 @admin_bp.route("/api/admin/users/", methods=["PUT"]) def update_user(user_id): admin, err = _require_admin() if err: return err data = request.get_json(silent=True) or {} role = data.get("role") password = data.get("password") if role is None and password is None: return jsonify({"message": "Nichts zu aktualisieren"}), 400 if role is not None and role not in _VALID_ROLES: return jsonify({"message": f"Ungültige Rolle. Erlaubt: {', '.join(_VALID_ROLES)}"}), 400 try: conn = get_connection() try: cur = conn.cursor() _ensure_tables(cur) if role: cur.execute("UPDATE users SET role=%s WHERE id=%s", (role, user_id)) if password: cur.execute( "UPDATE users SET password=%s WHERE id=%s", (generate_password_hash(password), user_id) ) conn.commit() cur.close() finally: conn.close() logger.info(f"✏️ User aktualisiert (id={user_id}) durch {admin['username']}") return jsonify({"message": "Aktualisiert"}), 200 except Exception as e: logger.error(f"[Admin update_user] {e}") return jsonify({"message": "Serverfehler"}), 500 @admin_bp.route("/api/admin/users/", methods=["DELETE"]) def delete_user(user_id): admin, err = _require_admin() if err: return err try: conn = get_connection() try: cur = conn.cursor() _ensure_tables(cur) cur.execute("SELECT username FROM users WHERE id=%s", (user_id,)) row = cur.fetchone() if not row: cur.close() return jsonify({"message": "Nicht gefunden"}), 404 username = row[0] if username == admin["username"]: cur.close() return jsonify({"message": "Du kannst dich nicht selbst löschen"}), 400 cur.execute("DELETE FROM users WHERE id=%s", (user_id,)) conn.commit() cur.close() finally: conn.close() logger.info(f"🗑️ User gelöscht (id={user_id}) durch {admin['username']}") return jsonify({"message": "Gelöscht"}), 200 except Exception as e: logger.error(f"[Admin delete_user] {e}") return jsonify({"message": "Serverfehler"}), 500 # ---------- Websites (Admin CRUD) ---------- @admin_bp.route("/api/admin/websites", methods=["GET"]) def list_websites_admin(): _, err = _require_admin() if err: return err try: conn = get_connection() try: cur = conn.cursor(dictionary=True) _ensure_tables(cur) cur.execute("SELECT id, name, url, description FROM websites ORDER BY name ASC") rows = cur.fetchall() cur.close() finally: conn.close() return jsonify(rows) except Exception as e: logger.error(f"[Admin list_websites] {e}") return jsonify({"message": "Serverfehler"}), 500 @admin_bp.route("/api/admin/websites", methods=["POST"]) def create_website(): _, err = _require_admin() if err: return err data = request.get_json(silent=True) or {} name = data.get("name", "").strip() url = data.get("url", "").strip() description = data.get("description", "").strip() if not name or not url: return jsonify({"message": "Name und URL erforderlich"}), 400 try: conn = get_connection() try: cur = conn.cursor() _ensure_tables(cur) cur.execute( "INSERT INTO websites (name, url, description) VALUES (%s, %s, %s)", (name, url, description), ) conn.commit() new_id = cur.lastrowid cur.close() finally: conn.close() return jsonify({"id": new_id, "name": name, "url": url, "description": description}), 201 except Exception as e: logger.error(f"[Admin create_website] {e}") return jsonify({"message": "Serverfehler"}), 500 @admin_bp.route("/api/admin/websites/", methods=["PUT"]) def update_website(item_id): _, err = _require_admin() if err: return err data = request.get_json(silent=True) or {} name = data.get("name", "").strip() url = data.get("url", "").strip() if not name or not url: return jsonify({"message": "Name und URL erforderlich"}), 400 description = data.get("description", "").strip() try: conn = get_connection() try: cur = conn.cursor() _ensure_tables(cur) cur.execute( "UPDATE websites SET name=%s, url=%s, description=%s WHERE id=%s", (name, url, description, item_id), ) conn.commit() affected = cur.rowcount cur.close() finally: conn.close() if affected == 0: return jsonify({"message": "Nicht gefunden"}), 404 return jsonify({"message": "Aktualisiert"}), 200 except Exception as e: logger.error(f"[Admin update_website] {e}") return jsonify({"message": "Serverfehler"}), 500 @admin_bp.route("/api/admin/websites/", methods=["DELETE"]) def delete_website(item_id): _, err = _require_admin() if err: return err try: conn = get_connection() try: cur = conn.cursor() _ensure_tables(cur) cur.execute("DELETE FROM websites WHERE id=%s", (item_id,)) conn.commit() affected = cur.rowcount cur.close() finally: conn.close() if affected == 0: return jsonify({"message": "Nicht gefunden"}), 404 return jsonify({"message": "Gelöscht"}), 200 except Exception as e: logger.error(f"[Admin delete_website] {e}") return jsonify({"message": "Serverfehler"}), 500 # ---------- Public (logged-in) endpoints ---------- @admin_bp.route("/api/websites", methods=["GET"]) def list_websites_public(): user = verify_token() if not user: return jsonify({"message": "Nicht autorisiert"}), 401 try: conn = get_connection() try: cur = conn.cursor(dictionary=True) _ensure_tables(cur) cur.execute("SELECT id, name, url, description FROM websites ORDER BY name ASC") rows = cur.fetchall() cur.close() finally: conn.close() return jsonify(rows) except Exception as e: logger.error(f"[Public list_websites] {e}") return jsonify({"message": "Serverfehler"}), 500