From e3b34bfc47a5de3914e09958c3e94185e8193279 Mon Sep 17 00:00:00 2001 From: Nirodan Date: Thu, 22 Jan 2026 12:26:21 +0100 Subject: [PATCH] Extend admin for websites/scripts and surface links --- backend/admin.py | 248 +++++++++++++++++++++ frontend/src/components/AdminDashboard.jsx | 234 +++++++++++++++++-- frontend/src/components/ToolOverview.jsx | 43 ++++ frontend/src/css/admin.css | 4 + frontend/src/css/base.css | 48 ++++ 5 files changed, 555 insertions(+), 22 deletions(-) diff --git a/backend/admin.py b/backend/admin.py index 212d58b..82ae57d 100644 --- a/backend/admin.py +++ b/backend/admin.py @@ -18,6 +18,40 @@ def _require_admin(): return user, None +def _ensure_tables(cur): + 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 '' + ) + """ + ) + cur.execute( + """ + CREATE TABLE IF NOT EXISTS scripts ( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(100) NOT NULL, + description VARCHAR(255) DEFAULT '', + content TEXT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + """ + ) + + @admin_bp.route("/api/admin/users", methods=["GET"]) def list_users(): _, err = _require_admin() @@ -27,6 +61,7 @@ def list_users(): cfg = load_config() conn = connect(**cfg) 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() @@ -52,6 +87,7 @@ def create_user(): cfg = load_config() conn = connect(**cfg) cur = conn.cursor(dictionary=True) + _ensure_tables(cur) cur.execute("SELECT id FROM users WHERE username=%s", (username,)) if cur.fetchone(): cur.close() @@ -86,6 +122,7 @@ def update_user(user_id): cfg = load_config() conn = connect(**cfg) cur = conn.cursor() + _ensure_tables(cur) if role: cur.execute("UPDATE users SET role=%s WHERE id=%s", (role, user_id)) if password: @@ -112,6 +149,7 @@ def delete_user(user_id): cfg = load_config() conn = connect(**cfg) cur = conn.cursor() + _ensure_tables(cur) # Schutz: Admin darf sich nicht selbst löschen cur.execute("SELECT username FROM users WHERE id=%s", (user_id,)) row = cur.fetchone() @@ -133,3 +171,213 @@ def delete_user(user_id): 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: + cfg = load_config() + conn = connect(**cfg) + 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() + 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() 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: + cfg = load_config() + conn = connect(**cfg) + 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() + 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() or {} + try: + cfg = load_config() + conn = connect(**cfg) + cur = conn.cursor() + _ensure_tables(cur) + cur.execute( + "UPDATE websites SET name=%s, url=%s, description=%s WHERE id=%s", + (data.get("name"), data.get("url"), data.get("description", ""), item_id), + ) + conn.commit() + cur.close() + conn.close() + 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: + cfg = load_config() + conn = connect(**cfg) + cur = conn.cursor() + _ensure_tables(cur) + cur.execute("DELETE FROM websites WHERE id=%s", (item_id,)) + conn.commit() + cur.close() + conn.close() + return jsonify({"message": "Gelöscht"}), 200 + except Exception as e: + logger.error(f"[Admin delete_website] {e}") + return jsonify({"message": "Serverfehler"}), 500 + + +# ---------- Scripts (Admin CRUD) ---------- + +@admin_bp.route("/api/admin/scripts", methods=["GET"]) +def list_scripts_admin(): + _, err = _require_admin() + if err: + return err + try: + cfg = load_config() + conn = connect(**cfg) + cur = conn.cursor(dictionary=True) + _ensure_tables(cur) + cur.execute("SELECT id, name, description, created_at FROM scripts ORDER BY created_at DESC") + rows = cur.fetchall() + cur.close() + conn.close() + return jsonify(rows) + except Exception as e: + logger.error(f"[Admin list_scripts] {e}") + return jsonify({"message": "Serverfehler"}), 500 + + +@admin_bp.route("/api/admin/scripts", methods=["POST"]) +def create_script(): + _, err = _require_admin() + if err: + return err + data = request.get_json() or {} + name = data.get("name", "").strip() + description = data.get("description", "").strip() + content = data.get("content", "") + if not name or not content: + return jsonify({"message": "Name und Inhalt erforderlich"}), 400 + try: + cfg = load_config() + conn = connect(**cfg) + cur = conn.cursor() + _ensure_tables(cur) + cur.execute( + "INSERT INTO scripts (name, description, content) VALUES (%s, %s, %s)", + (name, description, content), + ) + conn.commit() + new_id = cur.lastrowid + cur.close() + conn.close() + return jsonify({"id": new_id, "name": name, "description": description}), 201 + except Exception as e: + logger.error(f"[Admin create_script] {e}") + return jsonify({"message": "Serverfehler"}), 500 + + +@admin_bp.route("/api/admin/scripts/", methods=["DELETE"]) +def delete_script(item_id): + _, err = _require_admin() + if err: + return err + try: + cfg = load_config() + conn = connect(**cfg) + cur = conn.cursor() + _ensure_tables(cur) + cur.execute("DELETE FROM scripts WHERE id=%s", (item_id,)) + conn.commit() + cur.close() + conn.close() + return jsonify({"message": "Gelöscht"}), 200 + except Exception as e: + logger.error(f"[Admin delete_script] {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: + cfg = load_config() + conn = connect(**cfg) + 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() + conn.close() + return jsonify(rows) + except Exception as e: + logger.error(f"[Public list_websites] {e}") + return jsonify({"message": "Serverfehler"}), 500 + + +@admin_bp.route("/api/scripts", methods=["GET"]) +def list_scripts_public(): + user = verify_token() + if not user: + return jsonify({"message": "Nicht autorisiert"}), 401 + try: + cfg = load_config() + conn = connect(**cfg) + cur = conn.cursor(dictionary=True) + _ensure_tables(cur) + cur.execute("SELECT id, name, description, created_at FROM scripts ORDER BY created_at DESC") + rows = cur.fetchall() + cur.close() + conn.close() + return jsonify(rows) + except Exception as e: + logger.error(f"[Public list_scripts] {e}") + return jsonify({"message": "Serverfehler"}), 500 diff --git a/frontend/src/components/AdminDashboard.jsx b/frontend/src/components/AdminDashboard.jsx index 992a8af..d9ccceb 100644 --- a/frontend/src/components/AdminDashboard.jsx +++ b/frontend/src/components/AdminDashboard.jsx @@ -3,42 +3,110 @@ 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 [websites, setWebsites] = useState([]); + const [scripts, setScripts] = useState([]); + const [loadingUsers, setLoadingUsers] = useState(true); + const [loadingSites, setLoadingSites] = useState(true); + const [loadingScripts, setLoadingScripts] = useState(true); + const [creatingUser, setCreatingUser] = useState(false); + const [creatingSite, setCreatingSite] = useState(false); + const [creatingScript, setCreatingScript] = useState(false); + const [formUser, setFormUser] = useState({ username: '', password: '', role: 'user' }); + const [formSite, setFormSite] = useState({ name: '', url: '', description: '' }); + const [formScript, setFormScript] = useState({ name: '', description: '', content: '' }); const [error, setError] = useState(null); const fetchUsers = async () => { try { - setLoading(true); + setLoadingUsers(true); const res = await axios.get('/api/admin/users'); setUsers(res.data); setError(null); } catch (e) { setError('Konnte Nutzerliste nicht laden'); } finally { - setLoading(false); + setLoadingUsers(false); + } + }; + + const fetchWebsites = async () => { + try { + setLoadingSites(true); + const res = await axios.get('/api/admin/websites'); + setWebsites(res.data); + } catch (e) { + setError('Webseiten konnten nicht geladen werden'); + } finally { + setLoadingSites(false); + } + }; + + const fetchScripts = async () => { + try { + setLoadingScripts(true); + const res = await axios.get('/api/admin/scripts'); + setScripts(res.data); + } catch (e) { + setError('Scripts konnten nicht geladen werden'); + } finally { + setLoadingScripts(false); } }; useEffect(() => { fetchUsers(); + fetchWebsites(); + fetchScripts(); }, []); const createUser = async () => { - if (!form.username || !form.password) { + if (!formUser.username || !formUser.password) { setError('Username und Passwort erforderlich'); return; } try { - setCreating(true); - await axios.post('/api/admin/users', form); - setForm({ username: '', password: '', role: 'user' }); + setCreatingUser(true); + await axios.post('/api/admin/users', formUser); + setFormUser({ username: '', password: '', role: 'user' }); await fetchUsers(); } catch (e) { setError(e.response?.data?.message || 'Erstellen fehlgeschlagen'); } finally { - setCreating(false); + setCreatingUser(false); + } + }; + + const createWebsite = async () => { + if (!formSite.name || !formSite.url) { + setError('Name und URL erforderlich'); + return; + } + try { + setCreatingSite(true); + await axios.post('/api/admin/websites', formSite); + setFormSite({ name: '', url: '', description: '' }); + await fetchWebsites(); + } catch (e) { + setError(e.response?.data?.message || 'Webseite konnte nicht angelegt werden'); + } finally { + setCreatingSite(false); + } + }; + + const createScript = async () => { + if (!formScript.name || !formScript.content) { + setError('Name und Inhalt erforderlich'); + return; + } + try { + setCreatingScript(true); + await axios.post('/api/admin/scripts', formScript); + setFormScript({ name: '', description: '', content: '' }); + await fetchScripts(); + } catch (e) { + setError(e.response?.data?.message || 'Script konnte nicht angelegt werden'); + } finally { + setCreatingScript(false); } }; @@ -72,13 +140,33 @@ function AdminDashboard() { } }; + const deleteWebsite = async (id) => { + if (!window.confirm('Webseite löschen?')) return; + try { + await axios.delete(`/api/admin/websites/${id}`); + await fetchWebsites(); + } catch (e) { + setError('Konnte Webseite nicht löschen'); + } + }; + + const deleteScript = async (id) => { + if (!window.confirm('Script löschen?')) return; + try { + await axios.delete(`/api/admin/scripts/${id}`); + await fetchScripts(); + } catch (e) { + setError('Konnte Script nicht löschen'); + } + }; + return (

Adminbereich

-

Benutzerverwaltung

-

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

+

Verwaltung

+

Nutzer, externe Links und Python-Skripte zentral verwalten.

@@ -89,8 +177,8 @@ function AdminDashboard() { @@ -98,34 +186,33 @@ function AdminDashboard() { Passwort setForm({ ...form, password: e.target.value })} + value={formUser.password} + onChange={(e) => setFormUser({ ...formUser, password: e.target.value })} placeholder="Sicheres Passwort" />
- - {error &&

{error}

}

Nutzer

- +
- {loading ? ( + {loadingUsers ? (

Lade Nutzer...

) : (
@@ -156,6 +243,109 @@ function AdminDashboard() { )}
+ +
+
+

Externe Webseiten

+
+ + + +
+ + {loadingSites ?

Lade Webseiten...

: ( +
+
+ 🌐 Name + URL + Aktionen +
+ {websites.map((w) => ( +
+ {w.name} + {w.url} + + + +
+ ))} +
+ )} +
+ +
+

Python-Skripte

+
+ + +