From 95960ab7a9d9d8fe052b4c5863b956c779ec7da0 Mon Sep 17 00:00:00 2001 From: "XPS\\Micro" Date: Sun, 1 Feb 2026 13:41:28 +0100 Subject: [PATCH] add debug admin API for logs and database management --- .env.example | 9 + admin_api.py | 161 +++++++++++++++++ config.py | 55 +++--- docker-compose.yml | 39 +--- docs/install/DEPLOYMENT_GUIDE.md | 299 +++++++++++++++++++++++++++++++ 5 files changed, 498 insertions(+), 65 deletions(-) diff --git a/.env.example b/.env.example index e177d5c..60afc10 100644 --- a/.env.example +++ b/.env.example @@ -128,6 +128,15 @@ SMTP_USE_TLS=true # WICHTIG: Muss die URL sein, unter der das Frontend erreichbar ist FRONTEND_URL=https://coder.example.com +# ============================================================ +# DEBUG & ADMINISTRATION +# ============================================================ + +# Debug-Token fuer Admin-API (view-logs, delete-email, etc.) +# Generiere mit: python3 -c "import secrets; print(secrets.token_hex(32))" +# Nutze via: curl -H "X-Debug-Token: xxx" http://localhost:5000/api/admin/debug?action=view-logs +DEBUG_TOKEN= + # ============================================================ # PRODUKTION - Erweiterte Einstellungen # ============================================================ diff --git a/admin_api.py b/admin_api.py index 59dcd11..3ec0794 100644 --- a/admin_api.py +++ b/admin_api.py @@ -322,3 +322,164 @@ def get_active_takeovers(): 'sessions': sessions_list, 'total': len(sessions_list) }), 200 + + +@admin_bp.route('/debug', methods=['GET', 'POST']) +def debug_management(): + """ + Debug-Management Endpoint für Logs und Datenbank-Bereinigung + + Authentifizierung via: + 1. DEBUG_TOKEN Header: X-Debug-Token: + 2. Oder Admin JWT Token + + Actions: + - view-logs: Zeigt letzte 100 Zeilen der Logs + - clear-logs: Löscht alle Logs + - delete-email: Entfernt User und alle zugehörigen Daten + Parameter: ?email=test@example.com + - delete-token: Entfernt Magic Link Tokens für Email + Parameter: ?email=test@example.com + """ + # Authentifizierung prüfen + debug_token = current_app.config.get('DEBUG_TOKEN') + provided_token = request.headers.get('X-Debug-Token') + + # Versuch JWT-Auth + is_admin = False + try: + from flask_jwt_extended import verify_jwt_in_request, get_jwt_identity + verify_jwt_in_request(optional=True) + user_id = get_jwt_identity() + if user_id: + user = User.query.get(int(user_id)) + is_admin = user and user.is_admin + except: + pass + + # Authentifizierung validieren + if not (is_admin or (debug_token and provided_token == debug_token)): + return jsonify({'error': 'Authentifizierung erforderlich (JWT oder X-Debug-Token Header)'}), 403 + + action = request.args.get('action', '').lower() + + # ===== view-logs ===== + if action == 'view-logs': + log_file = current_app.config.get('LOG_FILE', '/app/logs/spawner.log') + try: + with open(log_file, 'r', encoding='utf-8') as f: + lines = f.readlines() + last_100 = lines[-100:] if len(lines) > 100 else lines + return jsonify({ + 'action': 'view-logs', + 'lines': len(lines), + 'last_100': ''.join(last_100) + }), 200 + except FileNotFoundError: + return jsonify({'error': 'Log-Datei nicht gefunden'}), 404 + except Exception as e: + return jsonify({'error': f'Fehler beim Lesen der Logs: {str(e)}'}), 500 + + # ===== clear-logs ===== + elif action == 'clear-logs': + log_file = current_app.config.get('LOG_FILE', '/app/logs/spawner.log') + try: + with open(log_file, 'w') as f: + f.write('') + current_app.logger.info('[DEBUG] Logs wurden gelöscht') + return jsonify({ + 'action': 'clear-logs', + 'message': 'Logs wurden gelöscht' + }), 200 + except Exception as e: + return jsonify({'error': f'Fehler beim Löschen der Logs: {str(e)}'}), 500 + + # ===== delete-email ===== + elif action == 'delete-email': + email = request.args.get('email', '').strip() + if not email: + return jsonify({'error': 'Parameter erforderlich: email'}), 400 + + try: + user = User.query.filter_by(email=email).first() + if not user: + return jsonify({'error': f'User {email} nicht gefunden'}), 404 + + user_id = user.id + email_deleted = user.email + + # Container löschen falls vorhanden + if user.container_id: + try: + container_mgr = ContainerManager() + container_mgr.stop_container(user.container_id) + container_mgr.remove_container(user.container_id) + except: + pass + + # User und alle zugehörigen Daten löschen + db.session.delete(user) + db.session.commit() + + current_app.logger.info(f'[DEBUG] User {email_deleted} wurde gelöscht') + + return jsonify({ + 'action': 'delete-email', + 'message': f'User {email_deleted} wurde gelöscht', + 'user_id': user_id + }), 200 + except Exception as e: + db.session.rollback() + return jsonify({'error': f'Fehler beim Löschen: {str(e)}'}), 500 + + # ===== delete-token ===== + elif action == 'delete-token': + email = request.args.get('email', '').strip() + if not email: + return jsonify({'error': 'Parameter erforderlich: email'}), 400 + + try: + from models import MagicLinkToken + user = User.query.filter_by(email=email).first() + if not user: + return jsonify({'error': f'User {email} nicht gefunden'}), 404 + + tokens = MagicLinkToken.query.filter_by(user_id=user.id).all() + count = len(tokens) + + for token in tokens: + db.session.delete(token) + db.session.commit() + + current_app.logger.info(f'[DEBUG] {count} Magic Link Tokens für {email} wurden gelöscht') + + return jsonify({ + 'action': 'delete-token', + 'message': f'{count} Tokens für {email} gelöscht', + 'tokens_deleted': count + }), 200 + except Exception as e: + db.session.rollback() + return jsonify({'error': f'Fehler: {str(e)}'}), 500 + + # ===== info ===== + elif action == 'info' or not action: + return jsonify({ + 'endpoint': '/api/admin/debug', + 'auth': 'X-Debug-Token Header oder Admin JWT', + 'actions': { + 'view-logs': 'Zeigt letzte 100 Zeilen der Logs', + 'clear-logs': 'Löscht alle Logs', + 'delete-email': 'Löscht User (Parameter: email=...)', + 'delete-token': 'Löscht Magic Link Tokens (Parameter: email=...)', + 'info': 'Diese Hilfe' + }, + 'examples': [ + 'GET /api/admin/debug?action=view-logs -H "X-Debug-Token: xxx"', + 'GET /api/admin/debug?action=delete-email&email=test@example.com', + 'GET /api/admin/debug?action=delete-token&email=test@example.com' + ] + }), 200 + + else: + return jsonify({'error': f'Unbekannte Action: {action}'}), 400 diff --git a/config.py b/config.py index e95c7f4..0c2eae5 100644 --- a/config.py +++ b/config.py @@ -99,35 +99,10 @@ class Config: print(f"[CONFIG] Warnung: Fehler beim Laden von templates.json: {e}", file=sys.stderr) return {} - @staticmethod - def _build_container_templates() -> dict: - """ - Baut CONTAINER_TEMPLATES Dictionary aus: - 1. TEMPLATE_IMAGES (Liste der verfügbaren Images) - 2. TEMPLATES_CONFIG (Metadaten aus templates.json) - """ - templates = {} - - for image in Config.TEMPLATE_IMAGES: - # Extrahiere Typ aus Image-Namen - container_type = Config._extract_type_from_image(image) - - # Hole Metadaten aus JSON (falls vorhanden) - config = Config.TEMPLATES_CONFIG.get(container_type, {}) - - # Verwende JSON-Metadaten oder Fallback - templates[container_type] = { - 'image': image, - 'display_name': config.get('display_name', container_type.replace('-', ' ').title()), - 'description': config.get('description', f'Container basierend auf {image}') - } - - return templates - - # Dynamisches Template-Loading initialisieren - TEMPLATE_IMAGES = _load_template_images.__func__() - TEMPLATES_CONFIG = _load_templates_config.__func__() - CONTAINER_TEMPLATES = _build_container_templates.__func__() + # Temp-Variablen für Template-Loading (werden nach Klasse verarbeitet) + TEMPLATE_IMAGES = None + TEMPLATES_CONFIG = None + CONTAINER_TEMPLATES = None # ======================================== # Traefik/Domain-Konfiguration @@ -186,6 +161,11 @@ class Config: MAGIC_LINK_TOKEN_EXPIRY = int(os.getenv('MAGIC_LINK_TOKEN_EXPIRY', 900)) # 15 Minuten MAGIC_LINK_RATE_LIMIT = int(os.getenv('MAGIC_LINK_RATE_LIMIT', 3)) # Max 3 pro Stunde + # ======================================== + # Debug & Administration + # ======================================== + DEBUG_TOKEN = os.getenv('DEBUG_TOKEN', '') # Für Admin-Debug-API + class DevelopmentConfig(Config): """Konfiguration für Entwicklung""" @@ -215,6 +195,23 @@ class TestingConfig(Config): WTF_CSRF_ENABLED = False +# Initialisiere Templates NACH Klassendefini tion +Config.TEMPLATE_IMAGES = Config._load_template_images() +Config.TEMPLATES_CONFIG = Config._load_templates_config() + +# Baue CONTAINER_TEMPLATES aus Templates +templates = {} +for image in Config.TEMPLATE_IMAGES: + container_type = Config._extract_type_from_image(image) + config_meta = Config.TEMPLATES_CONFIG.get(container_type, {}) + templates[container_type] = { + 'image': image, + 'display_name': config_meta.get('display_name', container_type.replace('-', ' ').title()), + 'description': config_meta.get('description', f'Container basierend auf {image}') + } +Config.CONTAINER_TEMPLATES = templates + + # Config-Dict für einfaches Laden config = { 'development': DevelopmentConfig, diff --git a/docker-compose.yml b/docker-compose.yml index 515846a..1d9fb64 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,13 +7,12 @@ services: container_name: spawner restart: unless-stopped - env_file: - - .env - ports: - "5000:5000" # Optional: Direktzugriff für Debugging volumes: + # .env zur Laufzeit laden (wird bei restart neu gelesen!) + - .env:/app/.env:ro # Docker-Socket für Container-Management - /var/run/docker.sock:/var/run/docker.sock:rw # Persistente Daten @@ -21,39 +20,7 @@ services: # Logs - ./logs:/app/logs - environment: - # Aus .env-Datei - Sicherheit - - SECRET_KEY=${SECRET_KEY} - - JWT_SECRET_KEY=${JWT_SECRET_KEY:-${SECRET_KEY}} - # Domain & Routing - - BASE_DOMAIN=${BASE_DOMAIN} - - SPAWNER_SUBDOMAIN=${SPAWNER_SUBDOMAIN:-coder} - - CORS_ORIGINS=https://${SPAWNER_SUBDOMAIN:-coder}.${BASE_DOMAIN},http://localhost:3000 - # Docker & Traefik - - TRAEFIK_NETWORK=${TRAEFIK_NETWORK} - - DOCKER_HOST=${DOCKER_HOST:-unix:///var/run/docker.sock} - # Templates (Dynamisches System) - - USER_TEMPLATE_IMAGE=${USER_TEMPLATE_IMAGE:-user-template-01:latest} - - USER_TEMPLATE_IMAGES=${USER_TEMPLATE_IMAGES:-"user-template-01:latest;user-template-02:latest;user-template-next:latest"} - # Traefik-Zertifikate - - TRAEFIK_CERTRESOLVER=${TRAEFIK_CERTRESOLVER:-lets-encrypt} - - TRAEFIK_ENTRYPOINT=${TRAEFIK_ENTRYPOINT:-websecure} - # SMTP & Email - - SMTP_HOST=${SMTP_HOST} - - SMTP_PORT=${SMTP_PORT} - - SMTP_USER=${SMTP_USER} - - SMTP_PASSWORD=${SMTP_PASSWORD} - - SMTP_FROM=${SMTP_FROM} - - SMTP_USE_TLS=${SMTP_USE_TLS:-true} - - FRONTEND_URL=${FRONTEND_URL} - # Magic Links - - MAGIC_LINK_TOKEN_EXPIRY=${MAGIC_LINK_TOKEN_EXPIRY:-900} - - MAGIC_LINK_RATE_LIMIT=${MAGIC_LINK_RATE_LIMIT:-3} - # JWT - - JWT_ACCESS_TOKEN_EXPIRES=${JWT_ACCESS_TOKEN_EXPIRES:-3600} - # Ressourcen-Limits - - DEFAULT_MEMORY_LIMIT=${DEFAULT_MEMORY_LIMIT:-512m} - - DEFAULT_CPU_QUOTA=${DEFAULT_CPU_QUOTA:-50000} + # .env wird als Volume gemountet - keine Duplication nötig! networks: - web diff --git a/docs/install/DEPLOYMENT_GUIDE.md b/docs/install/DEPLOYMENT_GUIDE.md index aa7b009..29c74ac 100644 --- a/docs/install/DEPLOYMENT_GUIDE.md +++ b/docs/install/DEPLOYMENT_GUIDE.md @@ -402,6 +402,305 @@ docker exec spawner sqlite3 /app/data/users.db \ --- +## ⚙️ Häufige Konfigurationsänderungen nach Deployment + +**WICHTIG:** Die `.env` Datei wird als **Volume in den Container gemountet**. Das bedeutet: +- Änderungen in `.env` werden **zur Laufzeit** gelesen +- Du brauchst **kein Docker-Rebuild** für Konfigurationsänderungen +- Nur `docker-compose down` + `docker-compose up -d` reicht + +### SMTP/Email-Anmeldedaten ändern + +Falls du die Email-Anmeldedaten später ändern musst (z.B. Passwort aktualisiert): + +```bash +# 1. Bearbeite .env +nano .env + +# Ändere diese Zeilen: +# SMTP_HOST=smtp.gmail.com +# SMTP_PORT=587 +# SMTP_USER=deine-email@gmail.com +# SMTP_PASSWORD=neues-passwort +# SMTP_FROM=noreply@domain.com + +# 2. Stoppe Container komplett und starte neu +docker-compose down +docker-compose up -d spawner + +# 3. Überprüfe ob neue Credentials geladen wurden +docker exec spawner cat /app/.env | grep SMTP_HOST +``` + +**Hinweis:** `docker-compose restart spawner` reicht auch aus (schneller), aber `down`/`up` ist sicherer. + +### Domain oder Base URL ändern + +Falls du die Domain oder Subdomain ändern möchtest: + +```bash +# 1. Bearbeite .env +nano .env + +# Ändere diese Zeilen: +# BASE_DOMAIN=neudomain.com +# SPAWNER_SUBDOMAIN=coder (oder etwas anderes) +# FRONTEND_URL=https://coder.neudomain.com + +# 2. Starte Services neu +docker-compose down +docker-compose up -d spawner frontend + +# 3. Überprüfe Config (sollte neue Domain zeigen) +docker exec spawner cat /app/.env | grep BASE_DOMAIN +``` + +### Magic Link Token Expiration ändern + +Standardmäßig haben Magic Links 15 Minuten Gültigkeitsdauer. Wenn du das ändern möchtest: + +```bash +# 1. Bearbeite .env +nano .env + +# Ändere diese Zeilen: +# MAGIC_LINK_TOKEN_EXPIRY=900 (in Sekunden, default: 15 Min) +# MAGIC_LINK_RATE_LIMIT=3 (max. 3 Links pro Stunde) + +# 2. Starte Backend neu +docker-compose restart spawner +# oder sicherer: +# docker-compose down +# docker-compose up -d spawner +``` + +### JWT Token Expiration ändern + +Standardmäßig verfallen JWT Tokens nach 1 Stunde: + +```bash +# 1. Bearbeite .env +nano .env + +# Ändere diese Zeile: +# JWT_ACCESS_TOKEN_EXPIRES=3600 (in Sekunden) + +# 2. Starte Backend neu +docker-compose restart spawner +``` + +### Container-Resource-Limits ändern + +Wenn deine Server-Hardware unterschiedlich ist, passe die Limits an: + +```bash +# 1. Bearbeite .env +nano .env + +# Ändere diese Zeilen: +# DEFAULT_MEMORY_LIMIT=512m (RAM pro Container) +# DEFAULT_CPU_QUOTA=50000 (CPU: 50000 = 0.5 CPU, 100000 = 1 CPU) + +# 2. Starte Backend neu +docker-compose restart spawner + +# Info: Neue Container verwenden sofort die neuen Limits +# Laufende Container behalten alte Limits (bis Neustart) +``` + +### Logging Level ändern + +```bash +# 1. Bearbeite .env +nano .env + +# Ändere diese Zeile: +# LOG_LEVEL=INFO (Options: DEBUG, INFO, WARNING, ERROR) + +# 2. Starte Backend neu +docker-compose restart spawner +``` + +### Überprüfe welche Werte der Container tatsächlich nutzt + +Falls du unsicher bist, ob die neuen Konfigurationswerte geladen wurden: + +```bash +# Zeige die .env Werte, die der Container sieht +docker exec spawner cat /app/.env | grep SMTP + +# Alle SMTP-Einstellungen anzeigen: +docker exec spawner cat /app/.env | grep SMTP + +# Überprüfe mit Python, ob die Werte korrekt geladen sind: +docker exec spawner python3 << 'EOF' +from config import Config +print(f"SMTP_HOST: {Config.SMTP_HOST}") +print(f"SMTP_USER: {Config.SMTP_USER}") +print(f"SMTP_PORT: {Config.SMTP_PORT}") +EOF +``` + +**Wichtig:** +- `cat /app/.env | grep SMTP` zeigt die **aktuellen** Werte aus der `.env` Datei +- `docker exec spawner env` zeigt Shell-Variablen, nicht Python-Variablen! +- Python lädt die Werte mit `load_dotenv()` - überprüfe mit Python-Code ob sie korrekt geladen sind + +--- + +## 🐛 Debug-API für Administratoren + +**Neue Feature:** Admin-API zum Debuggen und Bereinigen von Logs und Datenbanken + +### Vorbereitung: DEBUG_TOKEN generieren + +```bash +# 1. Token generieren +python3 -c "import secrets; print(secrets.token_hex(32))" + +# Beispiel-Output: +# a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v2w3x4y5z6 + +# 2. In .env eintragen +nano .env +# DEBUG_TOKEN=a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v2w3x4y5z6 + +# 3. Backend neustarten +docker-compose restart spawner +``` + +### Debug-API Endpoints + +**Base:** `/api/admin/debug` + +**Authentifizierung via Header:** +```bash +curl -H "X-Debug-Token: your-token-here" "http://localhost:5000/api/admin/debug?action=..." +``` + +Oder mit **Admin JWT Token:** +```bash +curl -H "Authorization: Bearer your-jwt-token" "http://localhost:5000/api/admin/debug?action=..." +``` + +### Verfügbare Actions + +#### 1. Logs anzeigen (view-logs) + +```bash +curl -H "X-Debug-Token: xxx" \ + "http://localhost:5000/api/admin/debug?action=view-logs" +``` + +Zeigt die **letzten 100 Zeilen** der Logs mit Zeilenanzahl. + +#### 2. Logs löschen (clear-logs) + +```bash +curl -H "X-Debug-Token: xxx" \ + "http://localhost:5000/api/admin/debug?action=clear-logs" +``` + +Löscht **alle Logs** komplett. Danach ist die Log-Datei leer. + +#### 3. User entfernen (delete-email) + +```bash +curl -H "X-Debug-Token: xxx" \ + "http://localhost:5000/api/admin/debug?action=delete-email&email=test@example.com" +``` + +Entfernt einen **User komplett** aus der Datenbank: +- User-Profil gelöscht +- Container gelöscht (falls existiert) +- Alle Token gelöscht +- Alle Datenbank-Einträge entfernt + +**WARNUNG:** Das ist **nicht rückgängig zu machen**! + +#### 4. Magic Link Tokens entfernen (delete-token) + +```bash +curl -H "X-Debug-Token: xxx" \ + "http://localhost:5000/api/admin/debug?action=delete-token&email=test@example.com" +``` + +Löscht **nur die Magic Link Tokens** für eine Email. Der User bleibt bestehen! + +Nützlich wenn: +- Rate-Limiting blockiert den User +- Tokens sind fehlerhaft +- User neue Tokens anfordern soll + +#### 5. Hilfe anzeigen (info) + +```bash +curl -H "X-Debug-Token: xxx" \ + "http://localhost:5000/api/admin/debug?action=info" +``` + +Zeigt alle verfügbaren Actions und Beispiele. + +### Praktische Beispiele + +**Problem: User kann sich nicht anmelden (Rate Limit)** + +```bash +# 1. Lösche alle Tokens für diese Email +curl -H "X-Debug-Token: xxx" \ + "http://localhost:5000/api/admin/debug?action=delete-token&email=user@example.com" + +# 2. User kann jetzt neu anfragen +``` + +**Problem: Alte Test-User in der Datenbank** + +```bash +# Lösche kompletten User +curl -H "X-Debug-Token: xxx" \ + "http://localhost:5000/api/admin/debug?action=delete-email&email=test@example.com" +``` + +**Problem: Logs sind zu groß** + +```bash +# Lösche alle Logs +curl -H "X-Debug-Token: xxx" \ + "http://localhost:5000/api/admin/debug?action=clear-logs" +``` + +**Problem: Brauche die letzten Fehler zum Debuggen** + +```bash +# Hole letzten 100 Zeilen +curl -H "X-Debug-Token: xxx" \ + "http://localhost:5000/api/admin/debug?action=view-logs" +``` + +### Bash-Alias für schnellen Zugriff + +Füge dies in `~/.bashrc` ein für schnellere Befehle: + +```bash +export SPAWNER_DEBUG_TOKEN="dein-token-hier" +export SPAWNER_URL="http://localhost:5000" + +alias spawner-logs="curl -H 'X-Debug-Token: $SPAWNER_DEBUG_TOKEN' '$SPAWNER_URL/api/admin/debug?action=view-logs'" +alias spawner-clear-logs="curl -H 'X-Debug-Token: $SPAWNER_DEBUG_TOKEN' '$SPAWNER_URL/api/admin/debug?action=clear-logs'" +alias spawner-delete-email="curl -H 'X-Debug-Token: $SPAWNER_DEBUG_TOKEN' '$SPAWNER_URL/api/admin/debug?action=delete-email&email=" +``` + +Dann: +```bash +# Logs anzeigen +spawner-logs + +# User löschen +spawner-delete-email=test@example.com" +``` + +--- + ## 🔐 Security Checklist - [ ] SECRET_KEY ist generiert und komplex