add debug admin API for logs and database management

This commit is contained in:
XPS\Micro 2026-02-01 13:41:28 +01:00
parent b41fd980ce
commit 95960ab7a9
5 changed files with 498 additions and 65 deletions

View File

@ -128,6 +128,15 @@ SMTP_USE_TLS=true
# WICHTIG: Muss die URL sein, unter der das Frontend erreichbar ist # WICHTIG: Muss die URL sein, unter der das Frontend erreichbar ist
FRONTEND_URL=https://coder.example.com 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 # PRODUKTION - Erweiterte Einstellungen
# ============================================================ # ============================================================

View File

@ -322,3 +322,164 @@ def get_active_takeovers():
'sessions': sessions_list, 'sessions': sessions_list,
'total': len(sessions_list) 'total': len(sessions_list)
}), 200 }), 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: <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

View File

@ -99,35 +99,10 @@ class Config:
print(f"[CONFIG] Warnung: Fehler beim Laden von templates.json: {e}", file=sys.stderr) print(f"[CONFIG] Warnung: Fehler beim Laden von templates.json: {e}", file=sys.stderr)
return {} return {}
@staticmethod # Temp-Variablen für Template-Loading (werden nach Klasse verarbeitet)
def _build_container_templates() -> dict: TEMPLATE_IMAGES = None
""" TEMPLATES_CONFIG = None
Baut CONTAINER_TEMPLATES Dictionary aus: CONTAINER_TEMPLATES = None
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__()
# ======================================== # ========================================
# Traefik/Domain-Konfiguration # 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_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 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): class DevelopmentConfig(Config):
"""Konfiguration für Entwicklung""" """Konfiguration für Entwicklung"""
@ -215,6 +195,23 @@ class TestingConfig(Config):
WTF_CSRF_ENABLED = False 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-Dict für einfaches Laden
config = { config = {
'development': DevelopmentConfig, 'development': DevelopmentConfig,

View File

@ -7,13 +7,12 @@ services:
container_name: spawner container_name: spawner
restart: unless-stopped restart: unless-stopped
env_file:
- .env
ports: ports:
- "5000:5000" # Optional: Direktzugriff für Debugging - "5000:5000" # Optional: Direktzugriff für Debugging
volumes: volumes:
# .env zur Laufzeit laden (wird bei restart neu gelesen!)
- .env:/app/.env:ro
# Docker-Socket für Container-Management # Docker-Socket für Container-Management
- /var/run/docker.sock:/var/run/docker.sock:rw - /var/run/docker.sock:/var/run/docker.sock:rw
# Persistente Daten # Persistente Daten
@ -21,39 +20,7 @@ services:
# Logs # Logs
- ./logs:/app/logs - ./logs:/app/logs
environment: # .env wird als Volume gemountet - keine Duplication nötig!
# 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}
networks: networks:
- web - web

View File

@ -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 ## 🔐 Security Checklist
- [ ] SECRET_KEY ist generiert und komplex - [ ] SECRET_KEY ist generiert und komplex