From 06991584864cd80fd7d51a9b229bf7a9771ed8d1 Mon Sep 17 00:00:00 2001 From: Nirodan Date: Thu, 22 Jan 2026 12:18:27 +0100 Subject: [PATCH] Add admin dashboard and tool icons --- Dockerfile | 1 + backend/admin.py | 135 +++++++++++++++++ backend/app.py | 2 + frontend/src/App.jsx | 11 +- frontend/src/components/AdminDashboard.jsx | 163 +++++++++++++++++++++ frontend/src/components/NavBar.jsx | 4 + frontend/src/components/ToolOverview.jsx | 4 +- frontend/src/css/admin.css | 131 +++++++++++++++++ 8 files changed, 448 insertions(+), 3 deletions(-) create mode 100644 backend/admin.py create mode 100644 frontend/src/components/AdminDashboard.jsx create mode 100644 frontend/src/css/admin.css diff --git a/Dockerfile b/Dockerfile index 90816e7..b06467d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,6 +14,7 @@ COPY backend/app.py ./backend/app.py COPY backend/util ./backend/util COPY backend/auth ./backend/auth COPY backend/tools ./backend/tools +COPY backend/admin.py ./backend/admin.py COPY backend/templates ./backend/templates # Store DB config in a docker-friendly location (/config), override via DB_CONFIG_PATH env if needed COPY backend/config /config diff --git a/backend/admin.py b/backend/admin.py new file mode 100644 index 0000000..212d58b --- /dev/null +++ b/backend/admin.py @@ -0,0 +1,135 @@ +from flask import Blueprint, request, jsonify +from mysql.connector import connect +from werkzeug.security import generate_password_hash +from auth.token import verify_token +from util.db_config import load_config +from util.logger import logger + +admin_bp = Blueprint("admin", __name__) + + +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 + + +@admin_bp.route("/api/admin/users", methods=["GET"]) +def list_users(): + _, err = _require_admin() + if err: + return err + try: + cfg = load_config() + conn = connect(**cfg) + cur = conn.cursor(dictionary=True) + cur.execute("SELECT id, username, role FROM users ORDER BY username ASC") + users = cur.fetchall() + cur.close() + 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() 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 + try: + cfg = load_config() + conn = connect(**cfg) + cur = conn.cursor(dictionary=True) + cur.execute("SELECT id FROM users WHERE username=%s", (username,)) + if cur.fetchone(): + cur.close() + conn.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() + 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() or {} + role = data.get("role") + password = data.get("password") + if role is None and password is None: + return jsonify({"message": "Nichts zu aktualisieren"}), 400 + try: + cfg = load_config() + conn = connect(**cfg) + cur = conn.cursor() + 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() + 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: + cfg = load_config() + conn = connect(**cfg) + cur = conn.cursor() + # Schutz: Admin darf sich nicht selbst löschen + cur.execute("SELECT username FROM users WHERE id=%s", (user_id,)) + row = cur.fetchone() + if not row: + cur.close() + conn.close() + return jsonify({"message": "Nicht gefunden"}), 404 + username = row[0] + if username == admin["username"]: + cur.close() + conn.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() + 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 diff --git a/backend/app.py b/backend/app.py index f1e90cc..b20a579 100644 --- a/backend/app.py +++ b/backend/app.py @@ -10,6 +10,7 @@ from util.db_config import is_configured, load_config, test_connection from util.setup_routes import setup_blueprint from auth import auth_bp from tools import md5_blueprint +from admin import admin_bp app = Flask(__name__, template_folder="templates") @@ -18,6 +19,7 @@ app = Flask(__name__, template_folder="templates") app.register_blueprint(setup_blueprint) app.register_blueprint(auth_bp) app.register_blueprint(md5_blueprint) +app.register_blueprint(admin_bp) # 🌐 React-Frontend ausliefern @app.route('/', defaults={'path': ''}) diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 71db7f5..23d8137 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -5,6 +5,7 @@ import LoginForm from './components/LoginForm'; import Md5Tool from './components/Md5Tool'; import NavBar from './components/NavBar'; import ToolOverview from './components/ToolOverview'; +import AdminDashboard from './components/AdminDashboard'; import './css/base.css'; @@ -12,6 +13,7 @@ import './css/buttons.css'; import './css/dark.css'; import './css/light.css'; import './css/menu.css'; +import './css/admin.css'; function App() { @@ -26,7 +28,14 @@ function App() { } /> {/*} />*/} : } /> - {/* : } />*/} + + : + } + /> ); diff --git a/frontend/src/components/AdminDashboard.jsx b/frontend/src/components/AdminDashboard.jsx new file mode 100644 index 0000000..992a8af --- /dev/null +++ b/frontend/src/components/AdminDashboard.jsx @@ -0,0 +1,163 @@ +import { useEffect, useState } from 'react'; +import axios from '../services/api'; + +function AdminDashboard() { + const [users, setUsers] = useState([]); + const [loading, setLoading] = useState(true); + const [creating, setCreating] = useState(false); + const [form, setForm] = useState({ username: '', password: '', role: 'user' }); + const [error, setError] = useState(null); + + const fetchUsers = async () => { + try { + setLoading(true); + const res = await axios.get('/api/admin/users'); + setUsers(res.data); + setError(null); + } catch (e) { + setError('Konnte Nutzerliste nicht laden'); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchUsers(); + }, []); + + const createUser = async () => { + if (!form.username || !form.password) { + setError('Username und Passwort erforderlich'); + return; + } + try { + setCreating(true); + await axios.post('/api/admin/users', form); + setForm({ username: '', password: '', role: 'user' }); + await fetchUsers(); + } catch (e) { + setError(e.response?.data?.message || 'Erstellen fehlgeschlagen'); + } finally { + setCreating(false); + } + }; + + const updateRole = async (id, role) => { + try { + await axios.put(`/api/admin/users/${id}`, { role }); + await fetchUsers(); + } catch (e) { + setError('Rolle konnte nicht aktualisiert werden'); + } + }; + + const resetPassword = async (id) => { + const pw = prompt('Neues Passwort setzen:'); + if (!pw) return; + try { + await axios.put(`/api/admin/users/${id}`, { password: pw }); + alert('Passwort aktualisiert.'); + } catch (e) { + setError('Passwort konnte nicht gesetzt werden'); + } + }; + + const deleteUser = async (id) => { + if (!window.confirm('Diesen Nutzer löschen?')) return; + try { + await axios.delete(`/api/admin/users/${id}`); + await fetchUsers(); + } catch (e) { + setError(e.response?.data?.message || 'Löschen fehlgeschlagen'); + } + }; + + return ( +
+
+
+

Adminbereich

+

Benutzerverwaltung

+

Nutzer anlegen, Rollen setzen, Passwörter zurücksetzen.

+
+
+ +
+
+

Neuen Nutzer anlegen

+
+ + + +
+ + {error &&

{error}

} +
+ +
+
+

Nutzer

+ +
+ {loading ? ( +

Lade Nutzer...

+ ) : ( +
+
+ 👤 Nutzer + Rolle + Aktionen +
+ {users.map((u) => ( +
+ {u.username} + + + + + + + +
+ ))} +
+ )} +
+
+
+ ); +} + +export default AdminDashboard; diff --git a/frontend/src/components/NavBar.jsx b/frontend/src/components/NavBar.jsx index 3d3af0e..7f3cc32 100644 --- a/frontend/src/components/NavBar.jsx +++ b/frontend/src/components/NavBar.jsx @@ -5,11 +5,15 @@ import LogoutButton from './LogoutButton'; function NavBar() { const isLoggedIn = localStorage.getItem('token') !== null; + const role = localStorage.getItem('role'); return (