From 5c4b944d6a180f165c4baa88ee56737d5e7fe8f6 Mon Sep 17 00:00:00 2001 From: Martin Date: Thu, 28 May 2026 15:40:26 +0200 Subject: [PATCH] added login screen and admin console --- With Authentification/ADMIN_INTEGRATION.md | 479 +++++++++++++++ .../ADMIN_INTEGRATION_GUIDE.md | 215 +++++++ With Authentification/admin_e57c18.html | 568 ++++++++++++++++++ With Authentification/admin_routes.py | 179 ++++++ With Authentification/fix_article_data.py | 107 ++++ 5 files changed, 1548 insertions(+) create mode 100644 With Authentification/ADMIN_INTEGRATION.md create mode 100644 With Authentification/ADMIN_INTEGRATION_GUIDE.md create mode 100644 With Authentification/admin_e57c18.html create mode 100644 With Authentification/admin_routes.py create mode 100644 With Authentification/fix_article_data.py diff --git a/With Authentification/ADMIN_INTEGRATION.md b/With Authentification/ADMIN_INTEGRATION.md new file mode 100644 index 0000000..7ace322 --- /dev/null +++ b/With Authentification/ADMIN_INTEGRATION.md @@ -0,0 +1,479 @@ +# 📊 Admin Dashboard Integration – 4 Optionen + +## Übersicht + +Hier sind die Best Practices für die Integration eines Admin-Panels in die Flask-App: + +--- + +## Option 1: Embedded Admin-Route (Empfohlen für Beta) + +**Am schnellsten & sichersten für Beta-Phase** + +### Wie es funktioniert: +- Admin-HTML wird direkt als Template serviert +- Nur für Admin-Benutzer zugänglich (/admin) +- Integriertes Logging-Dashboard +- Keine separate App nötig + +### Implementierung: + +**In `app.py` (nach den Error Handlers hinzufügen):** + +```python +@app.route("/admin", methods=["GET"]) +@auth.login_required +def admin_dashboard(): + """Admin Dashboard - nur für Admins""" + user = auth.current_user() + if user not in ADMIN_USERS: + log_access(user, "/admin", "GET", 403) + return jsonify({"error": "Unauthorized"}), 403 + + log_access(user, "/admin", "GET") + return render_template("admin.html", username=user) + +@app.route("/api/admin/logs", methods=["GET"]) +@auth.login_required +def get_admin_logs(): + """Logs als JSON für Dashboard""" + user = auth.current_user() + if user not in ADMIN_USERS: + return jsonify({"error": "Unauthorized"}), 403 + + logs = [] + if os.path.exists(LOG_FILE): + with open(LOG_FILE, "r") as f: + logs = json.load(f) + + # Optional: Letzte 100 Logs + return jsonify({"logs": logs[-100:]}), 200 + +@app.route("/api/admin/stats", methods=["GET"]) +@auth.login_required +def get_admin_stats(): + """Statistiken für Dashboard""" + user = auth.current_user() + if user not in ADMIN_USERS: + return jsonify({"error": "Unauthorized"}), 403 + + logs = [] + if os.path.exists(LOG_FILE): + with open(LOG_FILE, "r") as f: + logs = json.load(f) + + # Statistiken berechnen + total_requests = len(logs) + unique_users = len(set(log["username"] for log in logs)) + failed_logins = len([l for l in logs if l["status"] == 401]) + + return jsonify({ + "total_requests": total_requests, + "unique_users": unique_users, + "failed_logins": failed_logins, + "last_update": datetime.now().isoformat() + }), 200 +``` + +**Neue Template-Datei: `templates/admin.html`** + +```html + + + + + + Naue Admin Panel + + + + +
+
+

🔐 Admin Panel

+ +
+ +
+ +
+ + +
+
Statistiken werden geladen...
+
+ + +
+
+

📋 Access Logs

+ +
+ + + + + + + + + + + + + + + + +
ZeitstempelBenutzerEndpointMethodeStatus
Logs werden geladen...
+
+ + + +
+ + + + + +``` + +### Zugriff: +- URL: `http://localhost:5000/admin` +- Nur für Admin-Benutzer (403 Unauthorized für andere) +- Automatische Log-Anzeige + +--- + +## Option 2: Separate Admin-App (für später) + +**Wenn Admin-Features wachsen:** + +Erstelle eine separate `admin_app.py`: + +```python +from flask import Flask, render_template +from flask_httpauth import HTTPBasicAuth + +admin = Flask(__name__, template_folder='admin_templates') +auth_admin = HTTPBasicAuth() + +ADMIN_USERS = {"admin": "admin_secure_pwd_2026"} + +@auth_admin.verify_password +def verify(username, password): + if username in ADMIN_USERS and ADMIN_USERS[username] == password: + return username + return None + +@admin.route("/", methods=["GET"]) +@auth_admin.login_required +def dashboard(): + return render_template("admin.html") + +if __name__ == "__main__": + admin.run(port=5001, debug=False) # Anderer Port +``` + +Dann starten mit: `python admin_app.py` + +--- + +## Option 3: Reverse Proxy Setup (Production) + +**Mit Nginx als Reverse Proxy:** + +```nginx +server { + listen 80; + server_name rollcalculator.naue.de; + + # Main App (Port 5000) + location / { + proxy_pass http://localhost:5000; + proxy_set_header Host $host; + proxy_set_header Authorization $http_authorization; + } + + # Admin (gleiche Auth, aber separates Template) + location /admin { + proxy_pass http://localhost:5000/admin; + proxy_set_header Host $host; + proxy_set_header Authorization $http_authorization; + } +} +``` + +--- + +## Option 4: Vue.js / React Admin UI (für erweiterte Features) + +Falls du später komplexere Admin-Features brauchst: + +```javascript +// static/js/admin-dashboard.js +class AdminDashboard { + constructor() { + this.stats = null; + this.logs = []; + this.init(); + } + + async init() { + await this.loadStats(); + await this.loadLogs(); + this.setupAutoRefresh(); + } + + async loadStats() { + const res = await fetch('/api/admin/stats'); + this.stats = await res.json(); + this.renderStats(); + } + + async loadLogs() { + const res = await fetch('/api/admin/logs'); + this.logs = (await res.json()).logs; + this.renderLogs(); + } + + renderStats() { + // Render logic... + } + + renderLogs() { + // Render logic... + } + + setupAutoRefresh() { + setInterval(() => this.loadStats(), 30000); + setInterval(() => this.loadLogs(), 30000); + } +} + +// Starten +new AdminDashboard(); +``` + +--- + +## 🎯 Empfehlungen + +| Phase | Option | Vorteile | Nachteile | +|-------|--------|----------|-----------| +| **Beta** | Option 1 (Embedded) | ✅ Schnell, ✅ Sicher, ✅ Integriert | ⚠️ Einfaches UI | +| **Production Beta** | Option 1 + Option 3 | ✅ Mit HTTPS, ✅ Skalierbar | ⚠️ Nginx-Setup | +| **Später erweitern** | Option 4 (Vue/React) | ✅ Modern, ✅ Interactive | ⚠️ Komplexer | +| **Separate Team** | Option 2 (Separate App) | ✅ Unabhängig, ✅ Unterschiedliche Tech Stack | ⚠️ 2 Apps | + +--- + +## ⚡ Quick Start: Option 1 implementieren + +1. **Kopiere den Code oben in `app.py`** (die neuen Routes) +2. **Erstelle `templates/admin.html`** mit dem HTML oben +3. **Starte Flask neu**: `python app.py` +4. **Öffne**: `http://localhost:5000/admin` +5. **Login**: admin / admin_secure_pwd_2026 + +--- + +## Nächste Schritte + +- [ ] admin.html als Template in templates/ speichern +- [ ] Routes in app.py hinzufügen +- [ ] Tests mit Admin-User durchführen +- [ ] Logs überprüfen +- [ ] Optional: Charts/Graphs hinzufügen (Chart.js) + diff --git a/With Authentification/ADMIN_INTEGRATION_GUIDE.md b/With Authentification/ADMIN_INTEGRATION_GUIDE.md new file mode 100644 index 0000000..56785b1 --- /dev/null +++ b/With Authentification/ADMIN_INTEGRATION_GUIDE.md @@ -0,0 +1,215 @@ +# 🔧 Admin Panel Integration – Schritt-für-Schritt + +## 1. Templates-Datei erstellen + +Kopiere `admin.html` in dein `templates/` Verzeichnis: + +```bash +cp admin.html templates/admin.html +``` + +## 2. Flask Routes hinzufügen + +Öffne `app.py` und füge die neuen Routes hinzu: + +**Nach den Error Handlers (ca. Zeile 80), vor `if __name__ == "__main__"`, hinzufügen:** + +```python +# ============================================================================ +# ADMIN ROUTES +# ============================================================================ + +@app.route("/admin", methods=["GET"]) +@auth.login_required +def admin_dashboard(): + """Admin Dashboard - nur für Admin-Benutzer""" + user = auth.current_user() + if user not in ADMIN_USERS: + log_access(user, "/admin", "GET", 403) + return jsonify({"error": "Unauthorized - Admin access required"}), 403 + + log_access(user, "/admin", "GET") + return render_template("admin.html") + + +@app.route("/api/admin/stats", methods=["GET"]) +@auth.login_required +def get_admin_stats(): + """Statistiken für Admin Dashboard""" + user = auth.current_user() + if user not in ADMIN_USERS: + log_access(user, "/api/admin/stats", "GET", 403) + return jsonify({"error": "Unauthorized"}), 403 + + log_access(user, "/api/admin/stats", "GET") + + logs = [] + if os.path.exists(LOG_FILE): + try: + with open(LOG_FILE, "r") as f: + logs = json.load(f) + except json.JSONDecodeError: + logs = [] + + total_requests = len(logs) + unique_users = len(set(log.get("username", "unknown") for log in logs)) + failed_logins = len([l for l in logs if l.get("status") == 401]) + + return jsonify({ + "total_requests": total_requests, + "unique_users": unique_users, + "failed_logins": failed_logins, + "timestamp": datetime.now().isoformat() + }), 200 + + +@app.route("/api/admin/logs", methods=["GET"]) +@auth.login_required +def get_admin_logs(): + """Access Logs für Admin Dashboard""" + user = auth.current_user() + if user not in ADMIN_USERS: + log_access(user, "/api/admin/logs", "GET", 403) + return jsonify({"error": "Unauthorized"}), 403 + + log_access(user, "/api/admin/logs", "GET") + + logs = [] + if os.path.exists(LOG_FILE): + try: + with open(LOG_FILE, "r") as f: + logs = json.load(f) + except json.JSONDecodeError: + logs = [] + + logs_sorted = sorted(logs, key=lambda x: x.get("timestamp", ""), reverse=True) + return jsonify({"logs": logs_sorted[-500:]}), 200 +``` + +## 3. Server testen + +```bash +python app.py +``` + +## 4. Zugriff + +- URL: **http://localhost:5000/admin** +- Login: **admin** / **admin_secure_pwd_2026** + +--- + +## Features im Admin Panel + +### 📊 Dashboard-Statistiken +- ✅ Gesamte API-Anfragen +- ✅ Eindeutige Benutzer +- ✅ Fehlgeschlagene Logins (401 Unauthorized) + +### 📋 Access Logs +- ✅ Echtzeit-Logs mit Timestamp +- ✅ Benutzer, Endpoint, HTTP-Method, Status +- ✅ Farbcodierung nach HTTP-Status (200, 401, 403) + +### 🔍 Filter & Suche +- ✅ Nach Benutzer filtern +- ✅ Nach Endpoint filtern +- ✅ Live-Filter während Eingabe + +### ⬇️ Export +- ✅ Download als JSON +- ✅ Optionale CSV-Export + +### 🔄 Auto-Refresh +- ✅ Automatische Aktualisierung alle 30 Sekunden +- ✅ Manueller Refresh-Button + +--- + +## Sicherheit + +### ✅ Implementiert: +- HTTP Basic Auth +- Admin-only Access (403 Unauthorized für Non-Admins) +- Audit-Logging aller Admin-Aktionen +- XSS-Protection (HTML-escaped) + +### ⚠️ Für Production: +- HTTPS erforderlich (Basic Auth unsicher über HTTP!) +- Admin-Passwort ändern +- Logs regelmäßig archivieren + +--- + +## Erweiterungen (Optional) + +### Charts hinzufügen (Chart.js) + +```html + + + + +``` + +### Weitere Admin-Features +- User-Management (neue Benutzer hinzufügen) +- Konfiguration Editor +- System-Monitor (CPU, RAM) +- Backup-Manager +- Audit-Trail Export + +--- + +## Troubleshooting + +### "403 Unauthorized" beim Öffnen von /admin +✓ Du musst als Admin angemeldet sein +✓ Login mit: `admin` / `admin_secure_pwd_2026` + +### "No JSON object could be decoded" (in access_log.json) +✓ Lösche `access_log.json` und starte neu +✓ Die Datei wird automatisch neu erstellt + +### Logs werden nicht angezeigt +✓ Prüfe, ob `access_log.json` im Projektverzeichnis existiert +✓ Öffne Browser Console (F12) auf Fehler + +--- + +## API-Referenz + +| Endpoint | Method | Auth | Beschreibung | +|----------|--------|------|-------------| +| `/admin` | GET | Admin | Admin-Dashboard | +| `/api/admin/stats` | GET | Admin | Statistiken (JSON) | +| `/api/admin/logs` | GET | Admin | Access-Logs (JSON) | +| `/api/admin/export` | GET | Admin | Export als JSON/CSV | +| `/api/admin/clear-logs` | POST | Admin | Alle Logs löschen | + +--- + +## Nächste Schritte + +- [ ] `admin.html` in `templates/` speichern +- [ ] Routes in `app.py` hinzufügen +- [ ] Server neustarten +- [ ] http://localhost:5000/admin testen +- [ ] Mit Admin-Credentials anmelden +- [ ] Logs prüfen + +--- + +**Fertig!** 🎉 Dein Admin-Panel ist jetzt integriert! diff --git a/With Authentification/admin_e57c18.html b/With Authentification/admin_e57c18.html new file mode 100644 index 0000000..8352350 --- /dev/null +++ b/With Authentification/admin_e57c18.html @@ -0,0 +1,568 @@ + + + + + + + Admin Panel – Roll Calculator + + + + + +
+
+

🔐 Admin Panel

+
Roll Calculator – Naue GmbH & Co. KG
+
+ +
+ + +
+ + +
+
+ ⏳ Statistiken werden geladen... +
+
+ + +
+ + +
+

📋 Access Logs

+
+ + +
+
+ + +
+ + +
+ + +
+ + + + + + + + + + + + + + + + +
ZeitstempelBenutzerEndpointMethodeStatus
Logs werden geladen...
+ +
+ + + + +
+ + + + + + diff --git a/With Authentification/admin_routes.py b/With Authentification/admin_routes.py new file mode 100644 index 0000000..e99528b --- /dev/null +++ b/With Authentification/admin_routes.py @@ -0,0 +1,179 @@ +""" +ADMIN ROUTES FÜR app.py + +Diese Routes sollten zu app.py hinzugefügt werden (nach den Error Handlers, vor if __name__) +""" + +# ============================================================================ +# ADMIN ROUTES (diese zeilen in app.py hinzufügen!) +# ============================================================================ + +@app.route("/admin", methods=["GET"]) +@auth.login_required +def admin_dashboard(): + """Admin Dashboard - nur für Admin-Benutzer""" + user = auth.current_user() + if user not in ADMIN_USERS: + log_access(user, "/admin", "GET", 403) + return jsonify({"error": "Unauthorized - Admin access required"}), 403 + + log_access(user, "/admin", "GET") + return render_template("admin.html") + + +@app.route("/api/admin/stats", methods=["GET"]) +@auth.login_required +def get_admin_stats(): + """Statistiken für Admin Dashboard""" + user = auth.current_user() + if user not in ADMIN_USERS: + log_access(user, "/api/admin/stats", "GET", 403) + return jsonify({"error": "Unauthorized"}), 403 + + log_access(user, "/api/admin/stats", "GET") + + logs = [] + if os.path.exists(LOG_FILE): + try: + with open(LOG_FILE, "r") as f: + logs = json.load(f) + except json.JSONDecodeError: + logs = [] + + # Berechne Statistiken + total_requests = len(logs) + unique_users = len(set(log.get("username", "unknown") for log in logs)) + failed_logins = len([l for l in logs if l.get("status") == 401]) + + return jsonify({ + "total_requests": total_requests, + "unique_users": unique_users, + "failed_logins": failed_logins, + "timestamp": datetime.now().isoformat() + }), 200 + + +@app.route("/api/admin/logs", methods=["GET"]) +@auth.login_required +def get_admin_logs(): + """Access Logs für Admin Dashboard""" + user = auth.current_user() + if user not in ADMIN_USERS: + log_access(user, "/api/admin/logs", "GET", 403) + return jsonify({"error": "Unauthorized"}), 403 + + log_access(user, "/api/admin/logs", "GET") + + logs = [] + if os.path.exists(LOG_FILE): + try: + with open(LOG_FILE, "r") as f: + logs = json.load(f) + except json.JSONDecodeError: + logs = [] + + # Sortiere nach Zeitstempel absteigend (neueste zuerst) + logs_sorted = sorted(logs, key=lambda x: x.get("timestamp", ""), reverse=True) + + # Optionales Limit (letzte 500 Logs) + return jsonify({"logs": logs_sorted[-500:]}), 200 + + +@app.route("/api/admin/export", methods=["GET"]) +@auth.login_required +def export_logs(): + """Exportiere Logs als CSV/JSON""" + user = auth.current_user() + if user not in ADMIN_USERS: + return jsonify({"error": "Unauthorized"}), 403 + + log_access(user, "/api/admin/export", "GET") + + logs = [] + if os.path.exists(LOG_FILE): + try: + with open(LOG_FILE, "r") as f: + logs = json.load(f) + except json.JSONDecodeError: + logs = [] + + # Optional: Als CSV exportieren + fmt = request.args.get("format", "json") + + if fmt == "csv": + import csv + from io import StringIO + + output = StringIO() + writer = csv.DictWriter(output, fieldnames=["timestamp", "username", "endpoint", "method", "status"]) + writer.writeheader() + writer.writerows(logs) + + response = app.make_response(output.getvalue()) + response.headers["Content-Disposition"] = f"attachment; filename=logs_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv" + response.headers["Content-Type"] = "text/csv" + return response + else: + response = app.make_response(json.dumps(logs, indent=2)) + response.headers["Content-Disposition"] = f"attachment; filename=logs_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json" + response.headers["Content-Type"] = "application/json" + return response + + +# ============================================================================ +# OPTIONAL: Admin-Utility-Routes +# ============================================================================ + +@app.route("/api/admin/clear-logs", methods=["POST"]) +@auth.login_required +def clear_logs(): + """Lösche alle Logs (nur für Admin, mit Bestätigung)""" + user = auth.current_user() + if user not in ADMIN_USERS: + return jsonify({"error": "Unauthorized"}), 403 + + # Safety: Verlange "confirm=true" Query Parameter + confirm = request.args.get("confirm") == "true" + if not confirm: + return jsonify({"error": "Confirmation required (add ?confirm=true)"}), 400 + + log_access(user, "/api/admin/clear-logs", "POST") + + # Backup erstellen vor dem Löschen + if os.path.exists(LOG_FILE): + backup_file = LOG_FILE + f".backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}" + import shutil + shutil.copy(LOG_FILE, backup_file) + + # Logs leeren + with open(LOG_FILE, "w") as f: + json.dump([], f) + + return jsonify({ + "status": "success", + "message": "Alle Logs wurden gelöscht (Backup erstellt)" + }), 200 + + +@app.route("/api/admin/system-info", methods=["GET"]) +@auth.login_required +def get_system_info(): + """System-Information für Admin""" + user = auth.current_user() + if user not in ADMIN_USERS: + return jsonify({"error": "Unauthorized"}), 403 + + log_access(user, "/api/admin/system-info", "GET") + + import psutil + + return jsonify({ + "environment": os.getenv("FLASK_ENV", "production"), + "debug_mode": app.debug, + "database_file": LOG_FILE, + "database_size_kb": os.path.getsize(LOG_FILE) / 1024 if os.path.exists(LOG_FILE) else 0, + "timestamp": datetime.now().isoformat(), + # Optional: CPU/RAM Info + # "cpu_percent": psutil.cpu_percent(), + # "memory_percent": psutil.virtual_memory().percent, + }), 200 diff --git a/With Authentification/fix_article_data.py b/With Authentification/fix_article_data.py new file mode 100644 index 0000000..a36c725 --- /dev/null +++ b/With Authentification/fix_article_data.py @@ -0,0 +1,107 @@ +#!/usr/bin/env python3 +""" +Automatischer Fix für article-data.json Fetch-URLs in Flask +Ersetzt relative URLs durch /static/ URLs +""" + +import re +import os +from pathlib import Path + +def fix_html_file(html_path): + """Ersetzt die alten Fetch-URLs durch die neuen Flask-URLs""" + + if not os.path.exists(html_path): + print(f"❌ Datei nicht gefunden: {html_path}") + return False + + print(f"📖 Öffne: {html_path}") + + with open(html_path, 'r', encoding='utf-8') as f: + content = f.read() + + original_content = content + changes = [] + + # Fix 1: fetch('article-data.json') → fetch('/static/article-data.json') + if "fetch('article-data.json')" in content or 'fetch("article-data.json")' in content: + content = content.replace("fetch('article-data.json')", "fetch('/static/article-data.json')") + content = content.replace('fetch("article-data.json")', 'fetch("/static/article-data.json")') + changes.append("✓ article-data.json URL aktualisiert") + + # Fix 2: fetch('config.json') → fetch('/static/config.json') + if "fetch('config.json')" in content or 'fetch("config.json")' in content: + content = content.replace("fetch('config.json')", "fetch('/static/config.json')") + content = content.replace('fetch("config.json")', 'fetch("/static/config.json")') + changes.append("✓ config.json URL aktualisiert") + + # Fix 3: register('service-worker.js') → register('/static/service-worker.js') + if "register('service-worker.js')" in content or 'register("service-worker.js")' in content: + content = content.replace("register('service-worker.js')", "register('/static/service-worker.js')") + content = content.replace('register("service-worker.js")', 'register("/static/service-worker.js")') + changes.append("✓ service-worker.js URL aktualisiert") + + if content != original_content: + # Backup erstellen + backup_path = html_path + ".backup" + with open(backup_path, 'w', encoding='utf-8') as f: + f.write(original_content) + print(f"💾 Backup erstellt: {backup_path}") + + # Neue Version speichern + with open(html_path, 'w', encoding='utf-8') as f: + f.write(content) + + print(f"✅ HTML aktualisiert: {html_path}") + for change in changes: + print(f" {change}") + return True + else: + print("⚠️ Keine Änderungen notwendig (URLs sind bereits korrekt)") + return False + +def check_static_files(): + """Prüft ob die benötigten Dateien im static/ Ordner sind""" + print("\n🔍 Prüfe static/ Verzeichnis...") + + files_to_check = ['article-data.json', 'config.json'] + found = [] + missing = [] + + for filename in files_to_check: + path = os.path.join('static', filename) + if os.path.exists(path): + size = os.path.getsize(path) + found.append(f" ✓ {filename} ({size} bytes)") + else: + missing.append(f" ❌ {filename} (NICHT GEFUNDEN)") + + for msg in found: + print(msg) + for msg in missing: + print(msg) + + return len(missing) == 0 + +if __name__ == "__main__": + print("=" * 60) + print("Fix für article-data.json Flask-URLs") + print("=" * 60) + + html_file = "templates/roll_calculator.html" + + success = fix_html_file(html_file) + files_ok = check_static_files() + + print("\n" + "=" * 60) + if success or files_ok: + print("✅ Fix abgeschlossen!") + print("\n📝 Nächste Schritte:") + print(" 1. Browser neuladen (Ctrl+Shift+R)") + print(" 2. Öffne Browser Console (F12)") + print(" 3. Du solltest sehen:") + print(" 'Article data loaded: XXX items'") + print(" 'Config updated from file'") + else: + print("⚠️ Keine Änderungen nötig") + print("=" * 60)