This commit is contained in:
2025-06-04 10:03:22 +02:00
commit 785a2b6134
14182 changed files with 1764617 additions and 0 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

335
blueprints/admin.py Normal file
View File

@@ -0,0 +1,335 @@
"""
Admin-Blueprint für das 3D-Druck-Management-System
Dieses Modul enthält alle Admin-spezifischen Routen und Funktionen,
einschließlich Benutzerverwaltung, Systemüberwachung und Drucker-Administration.
"""
from flask import Blueprint, render_template, request, jsonify, redirect, url_for, flash
from flask_login import login_required, current_user
from functools import wraps
from models import User, Printer, Job, get_db_session, Stats, SystemLog
from utils.logging_config import get_logger
from datetime import datetime
# Blueprint erstellen
admin_blueprint = Blueprint('admin', __name__, url_prefix='/admin')
# Logger initialisieren
admin_logger = get_logger("admin")
def admin_required(f):
"""Decorator für Admin-Berechtigung"""
@wraps(f)
@login_required
def decorated_function(*args, **kwargs):
admin_logger.info(f"Admin-Check für Funktion {f.__name__}: User authenticated: {current_user.is_authenticated}, User ID: {current_user.id if current_user.is_authenticated else 'None'}, Is Admin: {current_user.is_admin if current_user.is_authenticated else 'None'}")
if not current_user.is_admin:
admin_logger.warning(f"Admin-Zugriff verweigert für User {current_user.id if current_user.is_authenticated else 'Anonymous'} auf Funktion {f.__name__}")
return jsonify({"error": "Nur Administratoren haben Zugriff"}), 403
return f(*args, **kwargs)
return decorated_function
@admin_blueprint.route("/")
@login_required
@admin_required
def admin_dashboard():
"""Admin-Dashboard-Hauptseite"""
try:
db_session = get_db_session()
# Grundlegende Statistiken sammeln
total_users = db_session.query(User).count()
total_printers = db_session.query(Printer).count()
total_jobs = db_session.query(Job).count()
# Aktive Jobs zählen
active_jobs = db_session.query(Job).filter(
Job.status.in_(['pending', 'printing', 'paused'])
).count()
db_session.close()
stats = {
'total_users': total_users,
'total_printers': total_printers,
'total_jobs': total_jobs,
'active_jobs': active_jobs
}
return render_template('admin/dashboard.html', stats=stats)
except Exception as e:
admin_logger.error(f"Fehler beim Laden des Admin-Dashboards: {str(e)}")
flash("Fehler beim Laden der Dashboard-Daten", "error")
return render_template('admin/dashboard.html', stats={})
@admin_blueprint.route("/users")
@login_required
@admin_required
def users_overview():
"""Benutzerübersicht für Administratoren"""
return render_template('admin/users.html')
@admin_blueprint.route("/users/add", methods=["GET"])
@login_required
@admin_required
def add_user_page():
"""Seite zum Hinzufügen eines neuen Benutzers"""
return render_template('admin/add_user.html')
@admin_blueprint.route("/users/<int:user_id>/edit", methods=["GET"])
@login_required
@admin_required
def edit_user_page(user_id):
"""Seite zum Bearbeiten eines Benutzers"""
try:
db_session = get_db_session()
user = db_session.query(User).filter(User.id == user_id).first()
if not user:
db_session.close()
flash("Benutzer nicht gefunden", "error")
return redirect(url_for('admin.users_overview'))
db_session.close()
return render_template('admin/edit_user.html', user=user)
except Exception as e:
admin_logger.error(f"Fehler beim Laden der Benutzer-Bearbeitung: {str(e)}")
flash("Fehler beim Laden der Benutzerdaten", "error")
return redirect(url_for('admin.users_overview'))
@admin_blueprint.route("/printers")
@login_required
@admin_required
def printers_overview():
"""Druckerübersicht für Administratoren"""
return render_template('admin/printers.html')
@admin_blueprint.route("/printers/add", methods=["GET"])
@login_required
@admin_required
def add_printer_page():
"""Seite zum Hinzufügen eines neuen Druckers"""
return render_template('admin/add_printer.html')
@admin_blueprint.route("/printers/<int:printer_id>/edit", methods=["GET"])
@login_required
@admin_required
def edit_printer_page(printer_id):
"""Seite zum Bearbeiten eines Druckers"""
try:
db_session = get_db_session()
printer = db_session.query(Printer).filter(Printer.id == printer_id).first()
if not printer:
db_session.close()
flash("Drucker nicht gefunden", "error")
return redirect(url_for('admin.printers_overview'))
db_session.close()
return render_template('admin/edit_printer.html', printer=printer)
except Exception as e:
admin_logger.error(f"Fehler beim Laden der Drucker-Bearbeitung: {str(e)}")
flash("Fehler beim Laden der Druckerdaten", "error")
return redirect(url_for('admin.printers_overview'))
@admin_blueprint.route("/guest-requests")
@login_required
@admin_required
def guest_requests():
"""Gäste-Anfragen-Übersicht"""
return render_template('admin/guest_requests.html')
@admin_blueprint.route("/advanced-settings")
@login_required
@admin_required
def advanced_settings():
"""Erweiterte Systemeinstellungen"""
return render_template('admin/advanced_settings.html')
@admin_blueprint.route("/system-health")
@login_required
@admin_required
def system_health():
"""System-Gesundheitsstatus"""
return render_template('admin/system_health.html')
@admin_blueprint.route("/logs")
@login_required
@admin_required
def logs_overview():
"""System-Logs-Übersicht"""
return render_template('admin/logs.html')
@admin_blueprint.route("/maintenance")
@login_required
@admin_required
def maintenance():
"""Wartungsseite"""
return render_template('admin/maintenance.html')
# API-Endpunkte für Admin-Funktionen
@admin_blueprint.route("/api/users", methods=["POST"])
@login_required
@admin_required
def create_user_api():
"""API-Endpunkt zum Erstellen eines neuen Benutzers"""
try:
data = request.get_json()
# Validierung der erforderlichen Felder
required_fields = ['username', 'email', 'password', 'name']
for field in required_fields:
if field not in data or not data[field]:
return jsonify({"error": f"Feld '{field}' ist erforderlich"}), 400
db_session = get_db_session()
# Überprüfung auf bereits existierende Benutzer
existing_user = db_session.query(User).filter(
(User.username == data['username']) | (User.email == data['email'])
).first()
if existing_user:
db_session.close()
return jsonify({"error": "Benutzername oder E-Mail bereits vergeben"}), 400
# Neuen Benutzer erstellen
new_user = User(
username=data['username'],
email=data['email'],
name=data['name'],
role=data.get('role', 'user'),
department=data.get('department'),
position=data.get('position'),
phone=data.get('phone'),
bio=data.get('bio')
)
new_user.set_password(data['password'])
db_session.add(new_user)
db_session.commit()
admin_logger.info(f"Neuer Benutzer erstellt: {new_user.username} von Admin {current_user.username}")
db_session.close()
return jsonify({
"success": True,
"message": "Benutzer erfolgreich erstellt",
"user_id": new_user.id
})
except Exception as e:
admin_logger.error(f"Fehler beim Erstellen des Benutzers: {str(e)}")
return jsonify({"error": "Fehler beim Erstellen des Benutzers"}), 500
@admin_blueprint.route("/api/users/<int:user_id>", methods=["GET"])
@login_required
@admin_required
def get_user_api(user_id):
"""API-Endpunkt zum Abrufen von Benutzerdaten"""
try:
db_session = get_db_session()
user = db_session.query(User).filter(User.id == user_id).first()
if not user:
db_session.close()
return jsonify({"error": "Benutzer nicht gefunden"}), 404
user_data = {
"id": user.id,
"username": user.username,
"email": user.email,
"name": user.name,
"role": user.role,
"active": user.active,
"created_at": user.created_at.isoformat() if user.created_at else None,
"last_login": user.last_login.isoformat() if user.last_login else None,
"department": user.department,
"position": user.position,
"phone": user.phone,
"bio": user.bio
}
db_session.close()
return jsonify(user_data)
except Exception as e:
admin_logger.error(f"Fehler beim Abrufen der Benutzerdaten: {str(e)}")
return jsonify({"error": "Fehler beim Abrufen der Benutzerdaten"}), 500
@admin_blueprint.route("/api/users/<int:user_id>", methods=["PUT"])
@login_required
@admin_required
def update_user_api(user_id):
"""API-Endpunkt zum Aktualisieren von Benutzerdaten"""
try:
data = request.get_json()
db_session = get_db_session()
user = db_session.query(User).filter(User.id == user_id).first()
if not user:
db_session.close()
return jsonify({"error": "Benutzer nicht gefunden"}), 404
# Aktualisierbare Felder
updatable_fields = ['username', 'email', 'name', 'role', 'active', 'department', 'position', 'phone', 'bio']
for field in updatable_fields:
if field in data:
setattr(user, field, data[field])
# Passwort separat behandeln
if 'password' in data and data['password']:
user.set_password(data['password'])
user.updated_at = datetime.now()
db_session.commit()
admin_logger.info(f"Benutzer {user.username} aktualisiert von Admin {current_user.username}")
db_session.close()
return jsonify({
"success": True,
"message": "Benutzer erfolgreich aktualisiert"
})
except Exception as e:
admin_logger.error(f"Fehler beim Aktualisieren des Benutzers: {str(e)}")
return jsonify({"error": "Fehler beim Aktualisieren des Benutzers"}), 500
@admin_blueprint.route("/api/users/<int:user_id>", methods=["DELETE"])
@login_required
@admin_required
def delete_user_api(user_id):
"""API-Endpunkt zum Löschen eines Benutzers"""
try:
if user_id == current_user.id:
return jsonify({"error": "Sie können sich nicht selbst löschen"}), 400
db_session = get_db_session()
user = db_session.query(User).filter(User.id == user_id).first()
if not user:
db_session.close()
return jsonify({"error": "Benutzer nicht gefunden"}), 404
username = user.username
db_session.delete(user)
db_session.commit()
admin_logger.info(f"Benutzer {username} gelöscht von Admin {current_user.username}")
db_session.close()
return jsonify({
"success": True,
"message": "Benutzer erfolgreich gelöscht"
})
except Exception as e:
admin_logger.error(f"Fehler beim Löschen des Benutzers: {str(e)}")
return jsonify({"error": "Fehler beim Löschen des Benutzers"}), 500

489
blueprints/admin_api.py Normal file
View File

@@ -0,0 +1,489 @@
"""
Admin-API Blueprint für erweiterte Verwaltungsfunktionen
Dieses Blueprint stellt zusätzliche Admin-API-Endpunkte bereit für:
- System-Backups
- Datenbank-Optimierung
- Cache-Verwaltung
Autor: MYP Team
Datum: 2025-06-01
"""
import os
import shutil
import zipfile
import sqlite3
import glob
from datetime import datetime, timedelta
from flask import Blueprint, request, jsonify, current_app
from flask_login import login_required, current_user
from functools import wraps
from utils.logging_config import get_logger
# Blueprint erstellen
admin_api_blueprint = Blueprint('admin_api', __name__, url_prefix='/api/admin')
# Logger initialisieren
admin_logger = get_logger("admin_api")
def admin_required(f):
"""Decorator um sicherzustellen, dass nur Admins auf Endpunkte zugreifen können."""
@wraps(f)
@login_required
def decorated_function(*args, **kwargs):
if not current_user.is_authenticated or current_user.role != 'admin':
admin_logger.warning(f"Unauthorized admin access attempt by user {getattr(current_user, 'id', 'anonymous')}")
return jsonify({'error': 'Admin-Berechtigung erforderlich'}), 403
return f(*args, **kwargs)
return decorated_function
@admin_api_blueprint.route('/backup/create', methods=['POST'])
@admin_required
def create_backup():
"""
Erstellt ein manuelles System-Backup.
Erstellt eine Sicherung aller wichtigen Systemdaten einschließlich
Datenbank, Konfigurationsdateien und Benutzer-Uploads.
Returns:
JSON: Erfolgs-Status und Backup-Informationen
"""
try:
admin_logger.info(f"Backup-Erstellung angefordert von Admin {current_user.username}")
# Backup-Verzeichnis sicherstellen
backup_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'database', 'backups')
os.makedirs(backup_dir, exist_ok=True)
# Eindeutigen Backup-Namen erstellen
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
backup_name = f"system_backup_{timestamp}.zip"
backup_path = os.path.join(backup_dir, backup_name)
created_files = []
backup_size = 0
with zipfile.ZipFile(backup_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
# 1. Datenbank-Datei hinzufügen
try:
from config.settings import DATABASE_PATH
if os.path.exists(DATABASE_PATH):
zipf.write(DATABASE_PATH, 'database/main.db')
created_files.append('database/main.db')
admin_logger.debug("✅ Hauptdatenbank zur Sicherung hinzugefügt")
# WAL- und SHM-Dateien falls vorhanden
wal_path = DATABASE_PATH + '-wal'
shm_path = DATABASE_PATH + '-shm'
if os.path.exists(wal_path):
zipf.write(wal_path, 'database/main.db-wal')
created_files.append('database/main.db-wal')
if os.path.exists(shm_path):
zipf.write(shm_path, 'database/main.db-shm')
created_files.append('database/main.db-shm')
except Exception as db_error:
admin_logger.warning(f"Fehler beim Hinzufügen der Datenbank: {str(db_error)}")
# 2. Konfigurationsdateien
try:
config_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'config')
if os.path.exists(config_dir):
for root, dirs, files in os.walk(config_dir):
for file in files:
if file.endswith(('.py', '.json', '.yaml', '.yml', '.toml')):
file_path = os.path.join(root, file)
arc_path = os.path.relpath(file_path, os.path.dirname(os.path.dirname(__file__)))
zipf.write(file_path, arc_path)
created_files.append(arc_path)
admin_logger.debug("✅ Konfigurationsdateien zur Sicherung hinzugefügt")
except Exception as config_error:
admin_logger.warning(f"Fehler beim Hinzufügen der Konfiguration: {str(config_error)}")
# 3. Wichtige User-Uploads (limitiert auf die letzten 1000 Dateien)
try:
uploads_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'uploads')
if os.path.exists(uploads_dir):
file_count = 0
max_files = 1000 # Limit für Performance
for root, dirs, files in os.walk(uploads_dir):
for file in files[:max_files - file_count]:
if file_count >= max_files:
break
file_path = os.path.join(root, file)
file_size = os.path.getsize(file_path)
# Nur Dateien unter 50MB hinzufügen
if file_size < 50 * 1024 * 1024:
arc_path = os.path.relpath(file_path, os.path.dirname(os.path.dirname(__file__)))
zipf.write(file_path, arc_path)
created_files.append(arc_path)
file_count += 1
if file_count >= max_files:
break
admin_logger.debug(f"{file_count} Upload-Dateien zur Sicherung hinzugefügt")
except Exception as uploads_error:
admin_logger.warning(f"Fehler beim Hinzufügen der Uploads: {str(uploads_error)}")
# 4. System-Logs (nur die letzten 100 Log-Dateien)
try:
logs_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'logs')
if os.path.exists(logs_dir):
log_files = []
for root, dirs, files in os.walk(logs_dir):
for file in files:
if file.endswith(('.log', '.txt')):
file_path = os.path.join(root, file)
log_files.append((file_path, os.path.getmtime(file_path)))
# Sortiere nach Datum (neueste zuerst) und nimm nur die letzten 100
log_files.sort(key=lambda x: x[1], reverse=True)
for file_path, _ in log_files[:100]:
arc_path = os.path.relpath(file_path, os.path.dirname(os.path.dirname(__file__)))
zipf.write(file_path, arc_path)
created_files.append(arc_path)
admin_logger.debug(f"{len(log_files[:100])} Log-Dateien zur Sicherung hinzugefügt")
except Exception as logs_error:
admin_logger.warning(f"Fehler beim Hinzufügen der Logs: {str(logs_error)}")
# Backup-Größe bestimmen
if os.path.exists(backup_path):
backup_size = os.path.getsize(backup_path)
admin_logger.info(f"✅ System-Backup erfolgreich erstellt: {backup_name} ({backup_size / 1024 / 1024:.2f} MB)")
return jsonify({
'success': True,
'message': f'Backup erfolgreich erstellt: {backup_name}',
'backup_info': {
'filename': backup_name,
'size_bytes': backup_size,
'size_mb': round(backup_size / 1024 / 1024, 2),
'files_count': len(created_files),
'created_at': datetime.now().isoformat(),
'path': backup_path
}
})
except Exception as e:
admin_logger.error(f"❌ Fehler beim Erstellen des Backups: {str(e)}")
return jsonify({
'success': False,
'message': f'Fehler beim Erstellen des Backups: {str(e)}'
}), 500
@admin_api_blueprint.route('/database/optimize', methods=['POST'])
@admin_required
def optimize_database():
"""
Führt Datenbank-Optimierung durch.
Optimiert die SQLite-Datenbank durch VACUUM, ANALYZE und weitere
Wartungsoperationen für bessere Performance.
Returns:
JSON: Erfolgs-Status und Optimierungs-Statistiken
"""
try:
admin_logger.info(f"Datenbank-Optimierung angefordert von Admin {current_user.username}")
from config.settings import DATABASE_PATH
optimization_results = {
'vacuum_completed': False,
'analyze_completed': False,
'integrity_check': False,
'wal_checkpoint': False,
'size_before': 0,
'size_after': 0,
'space_saved': 0
}
# Datenbankgröße vor Optimierung
if os.path.exists(DATABASE_PATH):
optimization_results['size_before'] = os.path.getsize(DATABASE_PATH)
# Verbindung zur Datenbank herstellen
conn = sqlite3.connect(DATABASE_PATH, timeout=30.0)
cursor = conn.cursor()
try:
# 1. Integritätsprüfung
admin_logger.debug("🔍 Führe Integritätsprüfung durch...")
cursor.execute("PRAGMA integrity_check")
integrity_result = cursor.fetchone()
optimization_results['integrity_check'] = integrity_result[0] == 'ok'
if not optimization_results['integrity_check']:
admin_logger.warning(f"⚠️ Integritätsprüfung ergab: {integrity_result[0]}")
else:
admin_logger.debug("✅ Integritätsprüfung erfolgreich")
# 2. WAL-Checkpoint (falls WAL-Modus aktiv)
try:
admin_logger.debug("🔄 Führe WAL-Checkpoint durch...")
cursor.execute("PRAGMA wal_checkpoint(TRUNCATE)")
optimization_results['wal_checkpoint'] = True
admin_logger.debug("✅ WAL-Checkpoint erfolgreich")
except Exception as wal_error:
admin_logger.debug(f" WAL-Checkpoint nicht möglich: {str(wal_error)}")
# 3. ANALYZE - Statistiken aktualisieren
admin_logger.debug("📊 Aktualisiere Datenbank-Statistiken...")
cursor.execute("ANALYZE")
optimization_results['analyze_completed'] = True
admin_logger.debug("✅ ANALYZE erfolgreich")
# 4. VACUUM - Datenbank komprimieren und reorganisieren
admin_logger.debug("🗜️ Komprimiere und reorganisiere Datenbank...")
cursor.execute("VACUUM")
optimization_results['vacuum_completed'] = True
admin_logger.debug("✅ VACUUM erfolgreich")
# 5. Performance-Optimierungen
try:
# Cache-Größe optimieren
cursor.execute("PRAGMA cache_size = 10000") # 10MB Cache
# Journal-Modus auf WAL setzen für bessere Concurrent-Performance
cursor.execute("PRAGMA journal_mode = WAL")
# Synchronous auf NORMAL für Balance zwischen Performance und Sicherheit
cursor.execute("PRAGMA synchronous = NORMAL")
# Page-Größe optimieren (falls noch nicht gesetzt)
cursor.execute("PRAGMA page_size = 4096")
admin_logger.debug("✅ Performance-Optimierungen angewendet")
except Exception as perf_error:
admin_logger.warning(f"⚠️ Performance-Optimierungen teilweise fehlgeschlagen: {str(perf_error)}")
finally:
cursor.close()
conn.close()
# Datenbankgröße nach Optimierung
if os.path.exists(DATABASE_PATH):
optimization_results['size_after'] = os.path.getsize(DATABASE_PATH)
optimization_results['space_saved'] = optimization_results['size_before'] - optimization_results['size_after']
# Ergebnisse loggen
space_saved_mb = optimization_results['space_saved'] / 1024 / 1024
admin_logger.info(f"✅ Datenbank-Optimierung abgeschlossen - {space_saved_mb:.2f} MB Speicher gespart")
return jsonify({
'success': True,
'message': 'Datenbank erfolgreich optimiert',
'results': {
'vacuum_completed': optimization_results['vacuum_completed'],
'analyze_completed': optimization_results['analyze_completed'],
'integrity_check_passed': optimization_results['integrity_check'],
'wal_checkpoint_completed': optimization_results['wal_checkpoint'],
'size_before_mb': round(optimization_results['size_before'] / 1024 / 1024, 2),
'size_after_mb': round(optimization_results['size_after'] / 1024 / 1024, 2),
'space_saved_mb': round(space_saved_mb, 2),
'optimization_timestamp': datetime.now().isoformat()
}
})
except Exception as e:
admin_logger.error(f"❌ Fehler bei Datenbank-Optimierung: {str(e)}")
return jsonify({
'success': False,
'message': f'Fehler bei Datenbank-Optimierung: {str(e)}'
}), 500
@admin_api_blueprint.route('/cache/clear', methods=['POST'])
@admin_required
def clear_cache():
"""
Leert den System-Cache.
Entfernt alle temporären Dateien, Cache-Verzeichnisse und
Python-Bytecode um Speicher freizugeben und Performance zu verbessern.
Returns:
JSON: Erfolgs-Status und Lösch-Statistiken
"""
try:
admin_logger.info(f"Cache-Leerung angefordert von Admin {current_user.username}")
cleared_stats = {
'files_deleted': 0,
'dirs_deleted': 0,
'space_freed': 0,
'categories': {}
}
app_root = os.path.dirname(os.path.dirname(__file__))
# 1. Python-Bytecode-Cache leeren (__pycache__)
try:
pycache_count = 0
pycache_size = 0
for root, dirs, files in os.walk(app_root):
if '__pycache__' in root:
for file in files:
file_path = os.path.join(root, file)
try:
pycache_size += os.path.getsize(file_path)
os.remove(file_path)
pycache_count += 1
except Exception:
pass
# Versuche das __pycache__-Verzeichnis zu löschen
try:
os.rmdir(root)
cleared_stats['dirs_deleted'] += 1
except Exception:
pass
cleared_stats['categories']['python_bytecode'] = {
'files': pycache_count,
'size_mb': round(pycache_size / 1024 / 1024, 2)
}
cleared_stats['files_deleted'] += pycache_count
cleared_stats['space_freed'] += pycache_size
admin_logger.debug(f"✅ Python-Bytecode-Cache: {pycache_count} Dateien, {pycache_size / 1024 / 1024:.2f} MB")
except Exception as pycache_error:
admin_logger.warning(f"⚠️ Fehler beim Leeren des Python-Cache: {str(pycache_error)}")
# 2. Temporäre Dateien im uploads/temp Verzeichnis
try:
temp_count = 0
temp_size = 0
temp_dir = os.path.join(app_root, 'uploads', 'temp')
if os.path.exists(temp_dir):
for root, dirs, files in os.walk(temp_dir):
for file in files:
file_path = os.path.join(root, file)
try:
temp_size += os.path.getsize(file_path)
os.remove(file_path)
temp_count += 1
except Exception:
pass
cleared_stats['categories']['temp_uploads'] = {
'files': temp_count,
'size_mb': round(temp_size / 1024 / 1024, 2)
}
cleared_stats['files_deleted'] += temp_count
cleared_stats['space_freed'] += temp_size
admin_logger.debug(f"✅ Temporäre Upload-Dateien: {temp_count} Dateien, {temp_size / 1024 / 1024:.2f} MB")
except Exception as temp_error:
admin_logger.warning(f"⚠️ Fehler beim Leeren des Temp-Verzeichnisses: {str(temp_error)}")
# 3. System-Cache-Verzeichnisse (falls vorhanden)
try:
cache_count = 0
cache_size = 0
cache_dirs = [
os.path.join(app_root, 'static', 'cache'),
os.path.join(app_root, 'cache'),
os.path.join(app_root, '.cache')
]
for cache_dir in cache_dirs:
if os.path.exists(cache_dir):
for root, dirs, files in os.walk(cache_dir):
for file in files:
file_path = os.path.join(root, file)
try:
cache_size += os.path.getsize(file_path)
os.remove(file_path)
cache_count += 1
except Exception:
pass
cleared_stats['categories']['system_cache'] = {
'files': cache_count,
'size_mb': round(cache_size / 1024 / 1024, 2)
}
cleared_stats['files_deleted'] += cache_count
cleared_stats['space_freed'] += cache_size
admin_logger.debug(f"✅ System-Cache: {cache_count} Dateien, {cache_size / 1024 / 1024:.2f} MB")
except Exception as cache_error:
admin_logger.warning(f"⚠️ Fehler beim Leeren des System-Cache: {str(cache_error)}")
# 4. Alte Log-Dateien (älter als 30 Tage)
try:
logs_count = 0
logs_size = 0
logs_dir = os.path.join(app_root, 'logs')
cutoff_date = datetime.now().timestamp() - (30 * 24 * 60 * 60) # 30 Tage
if os.path.exists(logs_dir):
for root, dirs, files in os.walk(logs_dir):
for file in files:
if file.endswith(('.log', '.log.1', '.log.2', '.log.3')):
file_path = os.path.join(root, file)
try:
if os.path.getmtime(file_path) < cutoff_date:
logs_size += os.path.getsize(file_path)
os.remove(file_path)
logs_count += 1
except Exception:
pass
cleared_stats['categories']['old_logs'] = {
'files': logs_count,
'size_mb': round(logs_size / 1024 / 1024, 2)
}
cleared_stats['files_deleted'] += logs_count
cleared_stats['space_freed'] += logs_size
admin_logger.debug(f"✅ Alte Log-Dateien: {logs_count} Dateien, {logs_size / 1024 / 1024:.2f} MB")
except Exception as logs_error:
admin_logger.warning(f"⚠️ Fehler beim Leeren alter Log-Dateien: {str(logs_error)}")
# 5. Application-Level Cache leeren (falls Models-Cache existiert)
try:
from models import clear_model_cache
clear_model_cache()
admin_logger.debug("✅ Application-Level Cache geleert")
except (ImportError, AttributeError):
admin_logger.debug(" Kein Application-Level Cache verfügbar")
# Ergebnisse zusammenfassen
total_space_mb = cleared_stats['space_freed'] / 1024 / 1024
admin_logger.info(f"✅ Cache-Leerung abgeschlossen: {cleared_stats['files_deleted']} Dateien, {total_space_mb:.2f} MB freigegeben")
return jsonify({
'success': True,
'message': f'Cache erfolgreich geleert - {total_space_mb:.2f} MB freigegeben',
'statistics': {
'total_files_deleted': cleared_stats['files_deleted'],
'total_dirs_deleted': cleared_stats['dirs_deleted'],
'total_space_freed_mb': round(total_space_mb, 2),
'categories': cleared_stats['categories'],
'cleanup_timestamp': datetime.now().isoformat()
}
})
except Exception as e:
admin_logger.error(f"❌ Fehler beim Leeren des Cache: {str(e)}")
return jsonify({
'success': False,
'message': f'Fehler beim Leeren des Cache: {str(e)}'
}), 500

336
blueprints/auth.py Normal file
View File

@@ -0,0 +1,336 @@
"""
Authentifizierungs-Blueprint für das 3D-Druck-Management-System
Dieses Modul enthält alle Routen und Funktionen für die Benutzerauthentifizierung,
einschließlich Login, Logout, OAuth-Callbacks und Passwort-Reset.
"""
import logging
from datetime import datetime
from flask import Blueprint, render_template, request, jsonify, redirect, url_for, flash, session
from flask_login import login_user, logout_user, login_required, current_user
from werkzeug.security import check_password_hash
from models import User, get_db_session
from utils.logging_config import get_logger
# Blueprint erstellen
auth_blueprint = Blueprint('auth', __name__, url_prefix='/auth')
# Logger initialisieren
auth_logger = get_logger("auth")
@auth_blueprint.route("/login", methods=["GET", "POST"])
def login():
"""Benutzeranmeldung mit E-Mail/Benutzername und Passwort"""
if current_user.is_authenticated:
return redirect(url_for("index"))
error = None
if request.method == "POST":
# Debug-Logging für Request-Details
auth_logger.debug(f"Login-Request: Content-Type={request.content_type}, Headers={dict(request.headers)}")
# Erweiterte Content-Type-Erkennung für AJAX-Anfragen
content_type = request.content_type or ""
is_json_request = (
request.is_json or
"application/json" in content_type or
request.headers.get('X-Requested-With') == 'XMLHttpRequest' or
request.headers.get('Accept', '').startswith('application/json')
)
# Robuste Datenextraktion
username = None
password = None
remember_me = False
try:
if is_json_request:
# JSON-Request verarbeiten
try:
data = request.get_json(force=True) or {}
username = data.get("username") or data.get("email")
password = data.get("password")
remember_me = data.get("remember_me", False)
except Exception as json_error:
auth_logger.warning(f"JSON-Parsing fehlgeschlagen: {str(json_error)}")
# Fallback zu Form-Daten
username = request.form.get("email")
password = request.form.get("password")
remember_me = request.form.get("remember_me") == "on"
else:
# Form-Request verarbeiten
username = request.form.get("email")
password = request.form.get("password")
remember_me = request.form.get("remember_me") == "on"
# Zusätzlicher Fallback für verschiedene Feldnamen
if not username:
username = request.form.get("username") or request.values.get("email") or request.values.get("username")
if not password:
password = request.form.get("password") or request.values.get("password")
except Exception as extract_error:
auth_logger.error(f"Fehler beim Extrahieren der Login-Daten: {str(extract_error)}")
error = "Fehler beim Verarbeiten der Anmeldedaten."
if is_json_request:
return jsonify({"error": error, "success": False}), 400
if not username or not password:
error = "E-Mail-Adresse und Passwort müssen angegeben werden."
auth_logger.warning(f"Unvollständige Login-Daten: username={bool(username)}, password={bool(password)}")
if is_json_request:
return jsonify({"error": error, "success": False}), 400
else:
db_session = None
try:
db_session = get_db_session()
# Suche nach Benutzer mit übereinstimmendem Benutzernamen oder E-Mail
user = db_session.query(User).filter(
(User.username == username) | (User.email == username)
).first()
if user and user.check_password(password):
# Update last login timestamp
user.update_last_login()
db_session.commit()
login_user(user, remember=remember_me)
auth_logger.info(f"Benutzer {username} hat sich erfolgreich angemeldet")
next_page = request.args.get("next")
if is_json_request:
return jsonify({
"success": True,
"message": "Anmeldung erfolgreich",
"redirect_url": next_page or url_for("index")
})
else:
if next_page:
return redirect(next_page)
return redirect(url_for("index"))
else:
error = "Ungültige E-Mail-Adresse oder Passwort."
auth_logger.warning(f"Fehlgeschlagener Login-Versuch für Benutzer {username}")
if is_json_request:
return jsonify({"error": error, "success": False}), 401
except Exception as e:
# Fehlerbehandlung für Datenbankprobleme
error = "Anmeldefehler. Bitte versuchen Sie es später erneut."
auth_logger.error(f"Fehler bei der Anmeldung: {str(e)}")
if is_json_request:
return jsonify({"error": error, "success": False}), 500
finally:
# Sicherstellen, dass die Datenbankverbindung geschlossen wird
if db_session:
try:
db_session.close()
except Exception as close_error:
auth_logger.error(f"Fehler beim Schließen der DB-Session: {str(close_error)}")
return render_template("login.html", error=error)
@auth_blueprint.route("/logout", methods=["GET", "POST"])
@login_required
def logout():
"""Meldet den Benutzer ab"""
auth_logger.info(f"Benutzer {current_user.email} hat sich abgemeldet")
logout_user()
flash("Sie wurden erfolgreich abgemeldet.", "info")
return redirect(url_for("auth.login"))
@auth_blueprint.route("/reset-password-request", methods=["GET", "POST"])
def reset_password_request():
"""Passwort-Reset anfordern (Placeholder)"""
# TODO: Implement password reset functionality
flash("Passwort-Reset-Funktionalität ist noch nicht implementiert.", "info")
return redirect(url_for("auth.login"))
@auth_blueprint.route("/api/login", methods=["POST"])
def api_login():
"""API-Login-Endpunkt für Frontend"""
try:
data = request.get_json()
if not data:
return jsonify({"error": "Keine Daten erhalten"}), 400
username = data.get("username")
password = data.get("password")
remember_me = data.get("remember_me", False)
if not username or not password:
return jsonify({"error": "Benutzername und Passwort müssen angegeben werden"}), 400
db_session = get_db_session()
user = db_session.query(User).filter(
(User.username == username) | (User.email == username)
).first()
if user and user.check_password(password):
# Update last login timestamp
user.update_last_login()
db_session.commit()
login_user(user, remember=remember_me)
auth_logger.info(f"API-Login erfolgreich für Benutzer {username}")
user_data = {
"id": user.id,
"username": user.username,
"name": user.name,
"email": user.email,
"is_admin": user.is_admin
}
db_session.close()
return jsonify({
"success": True,
"user": user_data,
"redirect_url": url_for("index")
})
else:
auth_logger.warning(f"Fehlgeschlagener API-Login für Benutzer {username}")
db_session.close()
return jsonify({"error": "Ungültiger Benutzername oder Passwort"}), 401
except Exception as e:
auth_logger.error(f"Fehler beim API-Login: {str(e)}")
return jsonify({"error": "Anmeldefehler. Bitte versuchen Sie es später erneut"}), 500
@auth_blueprint.route("/api/callback", methods=["GET", "POST"])
def api_callback():
"""OAuth-Callback-Endpunkt für externe Authentifizierung"""
try:
# OAuth-Provider bestimmen
provider = request.args.get('provider', 'github')
if request.method == "GET":
# Authorization Code aus URL-Parameter extrahieren
code = request.args.get('code')
state = request.args.get('state')
error = request.args.get('error')
if error:
auth_logger.warning(f"OAuth-Fehler von {provider}: {error}")
return jsonify({
"error": f"OAuth-Authentifizierung fehlgeschlagen: {error}",
"redirect_url": url_for("auth.login")
}), 400
if not code:
auth_logger.warning(f"Kein Authorization Code von {provider} erhalten")
return jsonify({
"error": "Kein Authorization Code erhalten",
"redirect_url": url_for("auth.login")
}), 400
# State-Parameter validieren (CSRF-Schutz)
session_state = session.get('oauth_state')
if not state or state != session_state:
auth_logger.warning(f"Ungültiger State-Parameter von {provider}")
return jsonify({
"error": "Ungültiger State-Parameter",
"redirect_url": url_for("auth.login")
}), 400
# OAuth-Token austauschen
if provider == 'github':
user_data = handle_github_callback(code)
else:
auth_logger.error(f"Unbekannter OAuth-Provider: {provider}")
return jsonify({
"error": "Unbekannter OAuth-Provider",
"redirect_url": url_for("auth.login")
}), 400
if not user_data:
return jsonify({
"error": "Fehler beim Abrufen der Benutzerdaten",
"redirect_url": url_for("auth.login")
}), 400
# Benutzer in Datenbank suchen oder erstellen
db_session = get_db_session()
try:
user = db_session.query(User).filter(
User.email == user_data['email']
).first()
if not user:
# Neuen Benutzer erstellen
user = User(
username=user_data['username'],
email=user_data['email'],
name=user_data['name'],
role="user",
oauth_provider=provider,
oauth_id=str(user_data['id'])
)
# Zufälliges Passwort setzen (wird nicht verwendet)
import secrets
user.set_password(secrets.token_urlsafe(32))
db_session.add(user)
db_session.commit()
auth_logger.info(f"Neuer OAuth-Benutzer erstellt: {user.username} via {provider}")
else:
# Bestehenden Benutzer aktualisieren
user.oauth_provider = provider
user.oauth_id = str(user_data['id'])
user.name = user_data['name']
user.updated_at = datetime.now()
db_session.commit()
auth_logger.info(f"OAuth-Benutzer aktualisiert: {user.username} via {provider}")
# Update last login timestamp
user.update_last_login()
db_session.commit()
login_user(user, remember=True)
# Session-State löschen
session.pop('oauth_state', None)
response_data = {
"success": True,
"user": {
"id": user.id,
"username": user.username,
"name": user.name,
"email": user.email,
"is_admin": user.is_admin
},
"redirect_url": url_for("index")
}
db_session.close()
return jsonify(response_data)
except Exception as e:
db_session.rollback()
db_session.close()
auth_logger.error(f"Datenbankfehler bei OAuth-Callback: {str(e)}")
return jsonify({
"error": "Datenbankfehler bei der Benutzeranmeldung",
"redirect_url": url_for("auth.login")
}), 500
except Exception as e:
auth_logger.error(f"Fehler im OAuth-Callback: {str(e)}")
return jsonify({
"error": "OAuth-Callback-Fehler",
"redirect_url": url_for("auth.login")
}), 500
def handle_github_callback(code):
"""Verarbeite GitHub OAuth Callback"""
# TODO: Implementiere GitHub OAuth Handling
auth_logger.warning("GitHub OAuth Callback noch nicht implementiert")
return None
def get_github_user_data(access_token):
"""Lade Benutzerdaten von GitHub API"""
# TODO: Implementiere GitHub API Abfrage
auth_logger.warning("GitHub User Data Abfrage noch nicht implementiert")
return None

1318
blueprints/calendar.py Normal file

File diff suppressed because it is too large Load Diff

1035
blueprints/guest.py Normal file

File diff suppressed because it is too large Load Diff

612
blueprints/jobs.py Normal file
View File

@@ -0,0 +1,612 @@
"""
Jobs Blueprint - API-Endpunkte für Job-Verwaltung
Alle Job-bezogenen API-Endpunkte sind hier zentralisiert.
"""
from flask import Blueprint, request, jsonify, current_app
from flask_login import login_required, current_user
from datetime import datetime, timedelta
from functools import wraps
from sqlalchemy.orm import joinedload
from models import get_db_session, Job, Printer
from utils.logging_config import get_logger
from utils.conflict_manager import conflict_manager
# Blueprint initialisieren - URL-Präfix geändert um Konflikte zu vermeiden
jobs_blueprint = Blueprint('jobs', __name__, url_prefix='/api/jobs-bp')
# Logger für Jobs
jobs_logger = get_logger("jobs")
def job_owner_required(f):
"""Decorator um zu prüfen, ob der aktuelle Benutzer Besitzer eines Jobs ist oder Admin"""
@wraps(f)
def decorated_function(job_id, *args, **kwargs):
db_session = get_db_session()
job = db_session.query(Job).filter(Job.id == job_id).first()
if not job:
db_session.close()
return jsonify({"error": "Job nicht gefunden"}), 404
is_owner = job.user_id == int(current_user.id) or job.owner_id == int(current_user.id)
is_admin = current_user.is_admin
if not (is_owner or is_admin):
db_session.close()
return jsonify({"error": "Keine Berechtigung"}), 403
db_session.close()
return f(job_id, *args, **kwargs)
return decorated_function
def check_printer_status(ip_address: str, timeout: int = 7):
"""Mock-Implementierung für Drucker-Status-Check"""
# TODO: Implementiere echten Status-Check
if ip_address:
return "online", True
return "offline", False
@jobs_blueprint.route('', methods=['GET'])
@login_required
def get_jobs():
"""Gibt alle Jobs zurück. Admins sehen alle Jobs, normale Benutzer nur ihre eigenen."""
db_session = get_db_session()
try:
jobs_logger.info(f"📋 Jobs-Abfrage gestartet von Benutzer {current_user.id} (Admin: {current_user.is_admin})")
# Paginierung unterstützen
page = request.args.get('page', 1, type=int)
per_page = request.args.get('per_page', 50, type=int)
status_filter = request.args.get('status')
jobs_logger.debug(f"📋 Parameter: page={page}, per_page={per_page}, status_filter={status_filter}")
# Query aufbauen
query = db_session.query(Job).options(joinedload(Job.user), joinedload(Job.printer))
# Admin sieht alle Jobs, User nur eigene
if not current_user.is_admin:
query = query.filter(Job.user_id == int(current_user.id))
jobs_logger.debug(f"🔒 Benutzerfilter angewendet für User {current_user.id}")
# Status-Filter anwenden
if status_filter:
query = query.filter(Job.status == status_filter)
jobs_logger.debug(f"🏷️ Status-Filter angewendet: {status_filter}")
# Sortierung: neueste zuerst
query = query.order_by(Job.created_at.desc())
# Paginierung anwenden
offset = (page - 1) * per_page
jobs = query.offset(offset).limit(per_page).all()
# Gesamtanzahl für Paginierung
total_count = query.count()
# Convert jobs to dictionaries before closing the session
job_dicts = [job.to_dict() for job in jobs]
db_session.close()
jobs_logger.info(f"✅ Jobs erfolgreich abgerufen: {len(job_dicts)} von {total_count} (Seite {page})")
return jsonify({
"jobs": job_dicts,
"pagination": {
"page": page,
"per_page": per_page,
"total": total_count,
"pages": (total_count + per_page - 1) // per_page
}
})
except Exception as e:
jobs_logger.error(f"❌ Fehler beim Abrufen von Jobs: {str(e)}", exc_info=True)
try:
db_session.close()
except:
pass
return jsonify({"error": "Interner Serverfehler", "details": str(e)}), 500
@jobs_blueprint.route('/<int:job_id>', methods=['GET'])
@login_required
@job_owner_required
def get_job(job_id):
"""Gibt einen einzelnen Job zurück."""
db_session = get_db_session()
try:
jobs_logger.info(f"🔍 Job-Detail-Abfrage für Job {job_id} von Benutzer {current_user.id}")
# Eagerly load the user and printer relationships
job = db_session.query(Job).options(joinedload(Job.user), joinedload(Job.printer)).filter(Job.id == job_id).first()
if not job:
jobs_logger.warning(f"⚠️ Job {job_id} nicht gefunden")
db_session.close()
return jsonify({"error": "Job nicht gefunden"}), 404
# Convert to dict before closing session
job_dict = job.to_dict()
db_session.close()
jobs_logger.info(f"✅ Job-Details erfolgreich abgerufen für Job {job_id}")
return jsonify(job_dict)
except Exception as e:
jobs_logger.error(f"❌ Fehler beim Abrufen des Jobs {job_id}: {str(e)}", exc_info=True)
try:
db_session.close()
except:
pass
return jsonify({"error": "Interner Serverfehler", "details": str(e)}), 500
@jobs_blueprint.route('', methods=['POST'])
@login_required
def create_job():
"""
Erstellt einen neuen Job.
Body: {
"name": str (optional),
"description": str (optional),
"printer_id": int,
"start_iso": str,
"duration_minutes": int,
"file_path": str (optional)
}
"""
try:
jobs_logger.info(f"🚀 Neue Job-Erstellung gestartet von Benutzer {current_user.id}")
data = request.json
if not data:
jobs_logger.error("❌ Keine JSON-Daten empfangen")
return jsonify({"error": "Keine JSON-Daten empfangen"}), 400
jobs_logger.debug(f"📋 Empfangene Daten: {data}")
# Pflichtfelder prüfen
required_fields = ["printer_id", "start_iso", "duration_minutes"]
for field in required_fields:
if field not in data:
jobs_logger.error(f"❌ Pflichtfeld '{field}' fehlt in den Daten")
return jsonify({"error": f"Feld '{field}' fehlt"}), 400
# Daten extrahieren und validieren
try:
printer_id = int(data["printer_id"])
start_iso = data["start_iso"]
duration_minutes = int(data["duration_minutes"])
jobs_logger.debug(f"✅ Grunddaten validiert: printer_id={printer_id}, duration={duration_minutes}")
except (ValueError, TypeError) as e:
jobs_logger.error(f"❌ Fehler bei Datenvalidierung: {str(e)}")
return jsonify({"error": f"Ungültige Datenformate: {str(e)}"}), 400
# Optional: Jobtitel, Beschreibung und Dateipfad
name = data.get("name", f"Druckjob vom {datetime.now().strftime('%d.%m.%Y %H:%M')}")
description = data.get("description", "")
file_path = data.get("file_path")
# Start-Zeit parsen
try:
start_at = datetime.fromisoformat(start_iso.replace('Z', '+00:00'))
jobs_logger.debug(f"✅ Startzeit geparst: {start_at}")
except ValueError as e:
jobs_logger.error(f"❌ Ungültiges Startdatum '{start_iso}': {str(e)}")
return jsonify({"error": f"Ungültiges Startdatum: {str(e)}"}), 400
# Dauer validieren
if duration_minutes <= 0:
jobs_logger.error(f"❌ Ungültige Dauer: {duration_minutes} Minuten")
return jsonify({"error": "Dauer muss größer als 0 sein"}), 400
# End-Zeit berechnen
end_at = start_at + timedelta(minutes=duration_minutes)
db_session = get_db_session()
try:
# Prüfen, ob der Drucker existiert
printer = db_session.query(Printer).get(printer_id)
if not printer:
jobs_logger.error(f"❌ Drucker mit ID {printer_id} nicht gefunden")
db_session.close()
return jsonify({"error": "Drucker nicht gefunden"}), 404
jobs_logger.debug(f"✅ Drucker gefunden: {printer.name} (ID: {printer_id})")
# ERWEITERTE KONFLIKTPRÜFUNG
job_data = {
'printer_id': printer_id,
'start_time': start_at,
'end_time': end_at,
'priority': data.get('priority', 'normal'),
'duration_minutes': duration_minutes
}
# Konflikte erkennen
conflicts = conflict_manager.detect_conflicts(job_data, db_session)
if conflicts:
critical_conflicts = [c for c in conflicts if c.severity.value in ['kritisch', 'hoch']]
if critical_conflicts:
# Kritische Konflikte verhindern Job-Erstellung
conflict_descriptions = [c.description for c in critical_conflicts]
jobs_logger.warning(f"⚠️ Kritische Konflikte gefunden: {conflict_descriptions}")
db_session.close()
return jsonify({
"error": "Kritische Konflikte gefunden",
"conflicts": conflict_descriptions,
"suggestions": [s for c in critical_conflicts for s in c.suggested_solutions]
}), 409
# Mittlere/niedrige Konflikte protokollieren aber zulassen
jobs_logger.info(f"📋 {len(conflicts)} Konflikte erkannt, aber übergehbar")
# Prüfen, ob der Drucker online ist
printer_status, printer_active = check_printer_status(printer.plug_ip if printer.plug_ip else "")
jobs_logger.debug(f"🖨️ Drucker-Status: {printer_status}, aktiv: {printer_active}")
# Status basierend auf Drucker-Verfügbarkeit setzen
if printer_status == "online" and printer_active:
job_status = "scheduled"
else:
job_status = "waiting_for_printer"
jobs_logger.info(f"📋 Job-Status festgelegt: {job_status}")
# Neuen Job erstellen
new_job = Job(
name=name,
description=description,
printer_id=printer_id,
user_id=current_user.id,
owner_id=current_user.id,
start_at=start_at,
end_at=end_at,
status=job_status,
file_path=file_path,
duration_minutes=duration_minutes
)
db_session.add(new_job)
db_session.commit()
# Job-Objekt für die Antwort serialisieren
job_dict = new_job.to_dict()
db_session.close()
jobs_logger.info(f"✅ Neuer Job {new_job.id} erfolgreich erstellt für Drucker {printer_id}, Start: {start_at}, Dauer: {duration_minutes} Minuten")
return jsonify({"job": job_dict}), 201
except Exception as db_error:
jobs_logger.error(f"❌ Datenbankfehler beim Job-Erstellen: {str(db_error)}")
try:
db_session.rollback()
db_session.close()
except:
pass
return jsonify({"error": "Datenbankfehler beim Erstellen des Jobs", "details": str(db_error)}), 500
except Exception as e:
jobs_logger.error(f"❌ Kritischer Fehler beim Erstellen eines Jobs: {str(e)}", exc_info=True)
return jsonify({"error": "Interner Serverfehler", "details": str(e)}), 500
@jobs_blueprint.route('/<int:job_id>', methods=['PUT'])
@login_required
@job_owner_required
def update_job(job_id):
"""Aktualisiert einen existierenden Job."""
try:
data = request.json
db_session = get_db_session()
job = db_session.query(Job).get(job_id)
if not job:
db_session.close()
return jsonify({"error": "Job nicht gefunden"}), 404
# Prüfen, ob der Job bearbeitet werden kann
if job.status in ["finished", "aborted"]:
db_session.close()
return jsonify({"error": f"Job kann im Status '{job.status}' nicht bearbeitet werden"}), 400
# Felder aktualisieren, falls vorhanden
if "name" in data:
job.name = data["name"]
if "description" in data:
job.description = data["description"]
if "notes" in data:
job.notes = data["notes"]
if "start_iso" in data:
try:
new_start = datetime.fromisoformat(data["start_iso"].replace('Z', '+00:00'))
job.start_at = new_start
# End-Zeit neu berechnen falls Duration verfügbar
if job.duration_minutes:
job.end_at = new_start + timedelta(minutes=job.duration_minutes)
except ValueError:
db_session.close()
return jsonify({"error": "Ungültiges Startdatum"}), 400
if "duration_minutes" in data:
duration = int(data["duration_minutes"])
if duration <= 0:
db_session.close()
return jsonify({"error": "Dauer muss größer als 0 sein"}), 400
job.duration_minutes = duration
# End-Zeit neu berechnen
if job.start_at:
job.end_at = job.start_at + timedelta(minutes=duration)
# Aktualisierungszeitpunkt setzen
job.updated_at = datetime.now()
db_session.commit()
# Job-Objekt für die Antwort serialisieren
job_dict = job.to_dict()
db_session.close()
jobs_logger.info(f"Job {job_id} aktualisiert")
return jsonify({"job": job_dict})
except Exception as e:
jobs_logger.error(f"Fehler beim Aktualisieren von Job {job_id}: {str(e)}")
return jsonify({"error": "Interner Serverfehler", "details": str(e)}), 500
@jobs_blueprint.route('/<int:job_id>', methods=['DELETE'])
@login_required
@job_owner_required
def delete_job(job_id):
"""Löscht einen Job."""
try:
db_session = get_db_session()
job = db_session.query(Job).get(job_id)
if not job:
db_session.close()
return jsonify({"error": "Job nicht gefunden"}), 404
# Prüfen, ob der Job gelöscht werden kann
if job.status == "running":
db_session.close()
return jsonify({"error": "Laufende Jobs können nicht gelöscht werden"}), 400
job_name = job.name
db_session.delete(job)
db_session.commit()
db_session.close()
jobs_logger.info(f"Job '{job_name}' (ID: {job_id}) gelöscht von Benutzer {current_user.id}")
return jsonify({"success": True, "message": "Job erfolgreich gelöscht"})
except Exception as e:
jobs_logger.error(f"Fehler beim Löschen des Jobs {job_id}: {str(e)}")
return jsonify({"error": "Interner Serverfehler"}), 500
@jobs_blueprint.route('/active', methods=['GET'])
@login_required
def get_active_jobs():
"""Gibt alle aktiven Jobs zurück."""
try:
db_session = get_db_session()
query = db_session.query(Job).options(
joinedload(Job.user),
joinedload(Job.printer)
).filter(
Job.status.in_(["scheduled", "running"])
)
# Normale Benutzer sehen nur ihre eigenen aktiven Jobs
if not current_user.is_admin:
query = query.filter(Job.user_id == current_user.id)
active_jobs = query.all()
result = []
for job in active_jobs:
job_dict = job.to_dict()
# Aktuelle Restzeit berechnen
if job.status == "running" and job.end_at:
remaining_time = job.end_at - datetime.now()
if remaining_time.total_seconds() > 0:
job_dict["remaining_minutes"] = int(remaining_time.total_seconds() / 60)
else:
job_dict["remaining_minutes"] = 0
result.append(job_dict)
db_session.close()
return jsonify({"jobs": result})
except Exception as e:
jobs_logger.error(f"Fehler beim Abrufen aktiver Jobs: {str(e)}")
return jsonify({"error": "Interner Serverfehler", "details": str(e)}), 500
@jobs_blueprint.route('/current', methods=['GET'])
@login_required
def get_current_job():
"""Gibt den aktuell laufenden Job für den eingeloggten Benutzer zurück."""
db_session = get_db_session()
try:
current_job = db_session.query(Job).filter(
Job.user_id == current_user.id,
Job.status == "running"
).first()
if current_job:
job_dict = current_job.to_dict()
db_session.close()
return jsonify(job_dict)
else:
db_session.close()
return jsonify({"message": "Kein aktueller Job"}), 404
except Exception as e:
jobs_logger.error(f"Fehler beim Abrufen des aktuellen Jobs: {str(e)}")
db_session.close()
return jsonify({"error": "Interner Serverfehler"}), 500
@jobs_blueprint.route('/<int:job_id>/start', methods=['POST'])
@login_required
@job_owner_required
def start_job(job_id):
"""Startet einen Job manuell."""
try:
db_session = get_db_session()
job = db_session.query(Job).options(joinedload(Job.printer)).get(job_id)
if not job:
db_session.close()
return jsonify({"error": "Job nicht gefunden"}), 404
# Prüfen, ob der Job gestartet werden kann
if job.status not in ["scheduled", "waiting_for_printer"]:
db_session.close()
return jsonify({"error": f"Job kann im Status '{job.status}' nicht gestartet werden"}), 400
# Drucker-Status prüfen
if job.printer.plug_ip:
status, active = check_printer_status(job.printer.plug_ip)
if status != "online" or not active:
db_session.close()
return jsonify({"error": "Drucker ist nicht online"}), 400
# Job als laufend markieren
job.status = "running"
job.start_at = datetime.now()
job.end_at = job.start_at + timedelta(minutes=job.duration_minutes)
db_session.commit()
# Job-Objekt für die Antwort serialisieren
job_dict = job.to_dict()
db_session.close()
jobs_logger.info(f"Job {job_id} manuell gestartet")
return jsonify({"job": job_dict})
except Exception as e:
jobs_logger.error(f"Fehler beim Starten von Job {job_id}: {str(e)}")
return jsonify({"error": "Interner Serverfehler", "details": str(e)}), 500
@jobs_blueprint.route('/<int:job_id>/pause', methods=['POST'])
@login_required
@job_owner_required
def pause_job(job_id):
"""Pausiert einen laufenden Job."""
try:
db_session = get_db_session()
job = db_session.query(Job).get(job_id)
if not job:
db_session.close()
return jsonify({"error": "Job nicht gefunden"}), 404
# Prüfen, ob der Job pausiert werden kann
if job.status != "running":
db_session.close()
return jsonify({"error": f"Job kann im Status '{job.status}' nicht pausiert werden"}), 400
# Job pausieren
job.status = "paused"
db_session.commit()
# Job-Objekt für die Antwort serialisieren
job_dict = job.to_dict()
db_session.close()
jobs_logger.info(f"Job {job_id} pausiert")
return jsonify({"job": job_dict})
except Exception as e:
jobs_logger.error(f"Fehler beim Pausieren von Job {job_id}: {str(e)}")
return jsonify({"error": "Interner Serverfehler", "details": str(e)}), 500
@jobs_blueprint.route('/<int:job_id>/resume', methods=['POST'])
@login_required
@job_owner_required
def resume_job(job_id):
"""Setzt einen pausierten Job fort."""
try:
db_session = get_db_session()
job = db_session.query(Job).get(job_id)
if not job:
db_session.close()
return jsonify({"error": "Job nicht gefunden"}), 404
# Prüfen, ob der Job fortgesetzt werden kann
if job.status != "paused":
db_session.close()
return jsonify({"error": f"Job kann im Status '{job.status}' nicht fortgesetzt werden"}), 400
# Job fortsetzen
job.status = "running"
db_session.commit()
# Job-Objekt für die Antwort serialisieren
job_dict = job.to_dict()
db_session.close()
jobs_logger.info(f"Job {job_id} fortgesetzt")
return jsonify({"job": job_dict})
except Exception as e:
jobs_logger.error(f"Fehler beim Fortsetzen von Job {job_id}: {str(e)}")
return jsonify({"error": "Interner Serverfehler", "details": str(e)}), 500
@jobs_blueprint.route('/<int:job_id>/finish', methods=['POST'])
@login_required
def finish_job(job_id):
"""Beendet einen Job manuell."""
try:
db_session = get_db_session()
job = db_session.query(Job).options(joinedload(Job.printer)).get(job_id)
if not job:
db_session.close()
return jsonify({"error": "Job nicht gefunden"}), 404
# Berechtigung prüfen
is_owner = job.user_id == int(current_user.id) or job.owner_id == int(current_user.id)
is_admin = current_user.is_admin
if not (is_owner or is_admin):
db_session.close()
return jsonify({"error": "Keine Berechtigung"}), 403
# Prüfen, ob der Job beendet werden kann
if job.status not in ["scheduled", "running", "paused"]:
db_session.close()
return jsonify({"error": f"Job kann im Status '{job.status}' nicht beendet werden"}), 400
# Job als beendet markieren
job.status = "finished"
job.actual_end_time = datetime.now()
db_session.commit()
# Job-Objekt für die Antwort serialisieren
job_dict = job.to_dict()
db_session.close()
jobs_logger.info(f"Job {job_id} manuell beendet durch Benutzer {current_user.id}")
return jsonify({"job": job_dict})
except Exception as e:
jobs_logger.error(f"Fehler beim manuellen Beenden von Job {job_id}: {str(e)}")
return jsonify({"error": "Interner Serverfehler", "details": str(e)}), 500

966
blueprints/printers.py Normal file
View File

@@ -0,0 +1,966 @@
"""
Drucker-Blueprint für MYP Platform
Enthält alle Routen und Funktionen zur Druckerverwaltung, Statusüberwachung und Steuerung.
"""
import os
import json
import time
from datetime import datetime, timedelta
from flask import Blueprint, request, jsonify, current_app, abort, Response
from flask_login import login_required, current_user
from werkzeug.utils import secure_filename
from werkzeug.exceptions import NotFound, BadRequest
from sqlalchemy import func, desc, asc
from sqlalchemy.exc import SQLAlchemyError
from typing import Dict, List, Tuple, Any, Optional
from models import Printer, User, Job, get_db_session
from utils.logging_config import get_logger, measure_execution_time
from utils.permissions import require_permission, Permission, check_permission
from utils.printer_monitor import printer_monitor
from utils.drag_drop_system import drag_drop_manager
# Logger initialisieren
printers_logger = get_logger("printers")
# Blueprint erstellen
printers_blueprint = Blueprint("printers", __name__, url_prefix="/api/printers")
@printers_blueprint.route("/monitor/live-status", methods=["GET"])
@login_required
@measure_execution_time(logger=printers_logger, task_name="API-Live-Drucker-Status-Abfrage")
def get_live_printer_status():
"""
Liefert den aktuellen Live-Status aller Drucker.
Query-Parameter:
- use_cache: ob Cache verwendet werden soll (default: true)
Returns:
JSON mit Live-Status aller Drucker
"""
printers_logger.info(f"🔄 Live-Status-Abfrage von Benutzer {current_user.name} (ID: {current_user.id})")
# Parameter auslesen
use_cache_param = request.args.get("use_cache", "true").lower()
use_cache = use_cache_param == "true"
try:
# Live-Status über den PrinterMonitor abrufen
status_data = printer_monitor.get_live_printer_status(use_session_cache=use_cache)
# Zusammenfassung der Druckerstatus erstellen
summary = printer_monitor.get_printer_summary()
# Antwort mit Status und Zusammenfassung
response = {
"success": True,
"status": status_data,
"summary": summary,
"timestamp": datetime.now().isoformat(),
"cache_used": use_cache
}
printers_logger.info(f"✅ Live-Status-Abfrage erfolgreich: {len(status_data)} Drucker")
return jsonify(response)
except Exception as e:
printers_logger.error(f"❌ Fehler bei Live-Status-Abfrage: {str(e)}")
return jsonify({
"success": False,
"error": "Fehler bei Abfrage des Druckerstatus",
"message": str(e)
}), 500
@printers_blueprint.route("/control/<int:printer_id>/power", methods=["POST"])
@login_required
@require_permission(Permission.CONTROL_PRINTER) # Verwende die bereits vorhandene Berechtigung
@measure_execution_time(logger=printers_logger, task_name="API-Drucker-Stromversorgung-Steuerung")
def control_printer_power(printer_id):
"""
Steuert die Stromversorgung eines Druckers (ein-/ausschalten).
Args:
printer_id: ID des zu steuernden Druckers
JSON-Parameter:
- action: "on" oder "off"
Returns:
JSON mit Ergebnis der Steuerungsaktion
"""
printers_logger.info(f"🔌 Stromsteuerung für Drucker {printer_id} von Benutzer {current_user.name}")
# Parameter validieren
data = request.get_json()
if not data or "action" not in data:
return jsonify({
"success": False,
"error": "Parameter 'action' fehlt"
}), 400
action = data["action"]
if action not in ["on", "off"]:
return jsonify({
"success": False,
"error": "Ungültige Aktion. Erlaubt sind 'on' oder 'off'."
}), 400
try:
# Drucker aus Datenbank holen
db_session = get_db_session()
printer = db_session.query(Printer).filter(Printer.id == printer_id).first()
if not printer:
db_session.close()
return jsonify({
"success": False,
"error": f"Drucker mit ID {printer_id} nicht gefunden"
}), 404
# Prüfen, ob Drucker eine Steckdose konfiguriert hat
if not printer.plug_ip or not printer.plug_username or not printer.plug_password:
db_session.close()
return jsonify({
"success": False,
"error": f"Drucker {printer.name} hat keine Steckdose konfiguriert"
}), 400
# Steckdose steuern
from PyP100 import PyP110
try:
# TP-Link Tapo P110 Verbindung herstellen
p110 = PyP110.P110(printer.plug_ip, printer.plug_username, printer.plug_password)
p110.handshake() # Authentifizierung
p110.login() # Login
# Steckdose ein- oder ausschalten
if action == "on":
p110.turnOn()
success = True
message = "Steckdose erfolgreich eingeschaltet"
printer.status = "starting" # Status aktualisieren
else:
p110.turnOff()
success = True
message = "Steckdose erfolgreich ausgeschaltet"
printer.status = "offline" # Status aktualisieren
# Zeitpunkt der letzten Prüfung aktualisieren
printer.last_checked = datetime.now()
db_session.commit()
# Cache leeren, damit neue Status-Abfragen aktuell sind
printer_monitor.clear_all_caches()
printers_logger.info(f"{action.upper()}: Drucker {printer.name} erfolgreich {message}")
except Exception as e:
printers_logger.error(f"❌ Fehler bei Steckdosensteuerung für {printer.name}: {str(e)}")
db_session.close()
return jsonify({
"success": False,
"error": f"Fehler bei Steckdosensteuerung: {str(e)}"
}), 500
db_session.close()
return jsonify({
"success": True,
"message": message,
"printer_id": printer_id,
"printer_name": printer.name,
"action": action,
"timestamp": datetime.now().isoformat()
})
except Exception as e:
printers_logger.error(f"❌ Allgemeiner Fehler bei Stromsteuerung: {str(e)}")
return jsonify({
"success": False,
"error": f"Allgemeiner Fehler: {str(e)}"
}), 500
@printers_blueprint.route("/test/socket/<int:printer_id>", methods=["GET"])
@login_required
@require_permission(Permission.ADMIN)
@measure_execution_time(logger=printers_logger, task_name="API-Steckdosen-Test-Status")
def test_socket_status(printer_id):
"""
Prüft den aktuellen Status einer Steckdose für Testzwecke (nur für Ausbilder/Administratoren).
Args:
printer_id: ID des Druckers dessen Steckdose getestet werden soll
Returns:
JSON mit detailliertem Status der Steckdose und Warnungen
"""
printers_logger.info(f"🔍 Steckdosen-Test-Status für Drucker {printer_id} von Admin {current_user.name}")
try:
# Drucker aus Datenbank holen
db_session = get_db_session()
printer = db_session.query(Printer).filter(Printer.id == printer_id).first()
if not printer:
db_session.close()
return jsonify({
"success": False,
"error": f"Drucker mit ID {printer_id} nicht gefunden"
}), 404
# Prüfen, ob Drucker eine Steckdose konfiguriert hat
if not printer.plug_ip or not printer.plug_username or not printer.plug_password:
db_session.close()
return jsonify({
"success": False,
"error": f"Drucker {printer.name} hat keine Steckdose konfiguriert",
"warning": "Steckdose kann nicht getestet werden - Konfiguration fehlt"
}), 400
# Prüfen, ob der Drucker gerade aktive Jobs hat
active_jobs = db_session.query(Job).filter(
Job.printer_id == printer_id,
Job.status.in_(["running", "printing", "active"])
).all()
db_session.close()
# Steckdosen-Status prüfen
from PyP100 import PyP110
socket_status = None
socket_info = None
error_message = None
try:
# TP-Link Tapo P110 Verbindung herstellen
p110 = PyP110.P110(printer.plug_ip, printer.plug_username, printer.plug_password)
p110.handshake() # Authentifizierung
p110.login() # Login
# Geräteinformationen abrufen
device_info = p110.getDeviceInfo()
socket_status = "online" if device_info["result"]["device_on"] else "offline"
# Energieverbrauch abrufen (falls verfügbar)
try:
energy_info = p110.getEnergyUsage()
current_power = energy_info.get("result", {}).get("current_power", 0)
except:
current_power = None
socket_info = {
"device_on": device_info["result"]["device_on"],
"signal_level": device_info["result"].get("signal_level", 0),
"current_power": current_power,
"device_id": device_info["result"].get("device_id", "Unbekannt"),
"model": device_info["result"].get("model", "Unbekannt"),
"hw_ver": device_info["result"].get("hw_ver", "Unbekannt"),
"fw_ver": device_info["result"].get("fw_ver", "Unbekannt")
}
except Exception as e:
printers_logger.warning(f"⚠️ Fehler bei Steckdosen-Status-Abfrage für {printer.name}: {str(e)}")
socket_status = "error"
error_message = str(e)
# Warnungen und Empfehlungen zusammenstellen
warnings = []
recommendations = []
risk_level = "low"
if active_jobs:
warnings.append(f"ACHTUNG: Drucker hat {len(active_jobs)} aktive(n) Job(s)!")
risk_level = "high"
recommendations.append("Warten Sie bis alle Jobs abgeschlossen sind bevor Sie die Steckdose ausschalten")
if socket_status == "online" and socket_info and socket_info.get("device_on"):
if socket_info.get("current_power", 0) > 10: # Mehr als 10W Verbrauch
warnings.append(f"Drucker verbraucht aktuell {socket_info['current_power']}W - vermutlich aktiv")
risk_level = "medium" if risk_level == "low" else risk_level
recommendations.append("Prüfen Sie den Druckerstatus bevor Sie die Steckdose ausschalten")
else:
recommendations.append("Drucker scheint im Standby-Modus zu sein - Test sollte sicher möglich sein")
if socket_status == "error":
warnings.append("Steckdose nicht erreichbar - Netzwerk oder Konfigurationsproblem")
recommendations.append("Prüfen Sie die Netzwerkverbindung und Steckdosen-Konfiguration")
if not warnings and socket_status == "offline":
recommendations.append("Steckdose ist ausgeschaltet - Test kann sicher durchgeführt werden")
printers_logger.info(f"✅ Steckdosen-Test-Status erfolgreich abgerufen für {printer.name}")
return jsonify({
"success": True,
"printer": {
"id": printer.id,
"name": printer.name,
"model": printer.model,
"location": printer.location,
"status": printer.status
},
"socket": {
"status": socket_status,
"info": socket_info,
"error": error_message,
"ip_address": printer.plug_ip
},
"safety": {
"risk_level": risk_level,
"warnings": warnings,
"recommendations": recommendations,
"active_jobs_count": len(active_jobs),
"safe_to_test": len(warnings) == 0
},
"timestamp": datetime.now().isoformat()
})
except Exception as e:
printers_logger.error(f"❌ Allgemeiner Fehler bei Steckdosen-Test-Status: {str(e)}")
return jsonify({
"success": False,
"error": f"Allgemeiner Fehler: {str(e)}"
}), 500
@printers_blueprint.route("/test/socket/<int:printer_id>/control", methods=["POST"])
@login_required
@require_permission(Permission.ADMIN)
@measure_execution_time(logger=printers_logger, task_name="API-Steckdosen-Test-Steuerung")
def test_socket_control(printer_id):
"""
Steuert eine Steckdose für Testzwecke (nur für Ausbilder/Administratoren).
Diese Funktion zeigt Warnungen an, erlaubt aber trotzdem die Steuerung für Tests.
Args:
printer_id: ID des Druckers dessen Steckdose gesteuert werden soll
JSON-Parameter:
- action: "on" oder "off"
- force: boolean - überschreibt Sicherheitswarnungen (default: false)
- test_reason: string - Grund für den Test (optional)
Returns:
JSON mit Ergebnis der Steuerungsaktion und Warnungen
"""
printers_logger.info(f"🧪 Steckdosen-Test-Steuerung für Drucker {printer_id} von Admin {current_user.name}")
# Parameter validieren
data = request.get_json()
if not data or "action" not in data:
return jsonify({
"success": False,
"error": "Parameter 'action' fehlt"
}), 400
action = data["action"]
if action not in ["on", "off"]:
return jsonify({
"success": False,
"error": "Ungültige Aktion. Erlaubt sind 'on' oder 'off'."
}), 400
force = data.get("force", False)
test_reason = data.get("test_reason", "Routinetest")
try:
# Drucker aus Datenbank holen
db_session = get_db_session()
printer = db_session.query(Printer).filter(Printer.id == printer_id).first()
if not printer:
db_session.close()
return jsonify({
"success": False,
"error": f"Drucker mit ID {printer_id} nicht gefunden"
}), 404
# Prüfen, ob Drucker eine Steckdose konfiguriert hat
if not printer.plug_ip or not printer.plug_username or not printer.plug_password:
db_session.close()
return jsonify({
"success": False,
"error": f"Drucker {printer.name} hat keine Steckdose konfiguriert"
}), 400
# Aktive Jobs prüfen
active_jobs = db_session.query(Job).filter(
Job.printer_id == printer_id,
Job.status.in_(["running", "printing", "active"])
).all()
# Sicherheitsprüfungen
warnings = []
should_block = False
if active_jobs and action == "off":
warnings.append(f"WARNUNG: {len(active_jobs)} aktive Job(s) würden abgebrochen!")
if not force:
should_block = True
if should_block:
db_session.close()
return jsonify({
"success": False,
"error": "Aktion blockiert aufgrund von Sicherheitsbedenken",
"warnings": warnings,
"hint": "Verwenden Sie 'force': true um die Aktion trotzdem auszuführen",
"requires_force": True
}), 409 # Conflict
# Steckdose steuern
from PyP100 import PyP110
try:
# TP-Link Tapo P110 Verbindung herstellen
p110 = PyP110.P110(printer.plug_ip, printer.plug_username, printer.plug_password)
p110.handshake() # Authentifizierung
p110.login() # Login
# Aktuellen Status vor der Änderung abrufen
device_info_before = p110.getDeviceInfo()
status_before = device_info_before["result"]["device_on"]
# Steckdose ein- oder ausschalten
if action == "on":
p110.turnOn()
success = True
message = "Steckdose für Test erfolgreich eingeschaltet"
new_printer_status = "starting"
else:
p110.turnOff()
success = True
message = "Steckdose für Test erfolgreich ausgeschaltet"
new_printer_status = "offline"
# Kurz warten und neuen Status prüfen
time.sleep(2)
device_info_after = p110.getDeviceInfo()
status_after = device_info_after["result"]["device_on"]
# Drucker-Status aktualisieren
printer.status = new_printer_status
printer.last_checked = datetime.now()
db_session.commit()
# Cache leeren, damit neue Status-Abfragen aktuell sind
printer_monitor.clear_all_caches()
# Test-Eintrag für Audit-Log
printers_logger.info(f"🧪 TEST DURCHGEFÜHRT: {action.upper()} für {printer.name} | "
f"Admin: {current_user.name} | Grund: {test_reason} | "
f"Force: {force} | Status: {status_before}{status_after}")
except Exception as e:
printers_logger.error(f"❌ Fehler bei Test-Steckdosensteuerung für {printer.name}: {str(e)}")
db_session.close()
return jsonify({
"success": False,
"error": f"Fehler bei Steckdosensteuerung: {str(e)}"
}), 500
db_session.close()
return jsonify({
"success": True,
"message": message,
"test_info": {
"admin": current_user.name,
"reason": test_reason,
"forced": force,
"status_before": status_before,
"status_after": status_after
},
"printer": {
"id": printer_id,
"name": printer.name,
"status": new_printer_status
},
"action": action,
"warnings": warnings,
"timestamp": datetime.now().isoformat()
})
except Exception as e:
printers_logger.error(f"❌ Allgemeiner Fehler bei Test-Steckdosensteuerung: {str(e)}")
return jsonify({
"success": False,
"error": f"Allgemeiner Fehler: {str(e)}"
}), 500
@printers_blueprint.route("/test/all-sockets", methods=["GET"])
@login_required
@require_permission(Permission.ADMIN)
@measure_execution_time(logger=printers_logger, task_name="API-Alle-Steckdosen-Test-Status")
def test_all_sockets_status():
"""
Liefert den Test-Status aller konfigurierten Steckdosen (nur für Ausbilder/Administratoren).
Returns:
JSON mit Status aller Steckdosen und Gesamtübersicht
"""
printers_logger.info(f"🔍 Alle-Steckdosen-Test-Status von Admin {current_user.name}")
try:
# Alle Drucker mit Steckdosen-Konfiguration holen
db_session = get_db_session()
printers = db_session.query(Printer).filter(
Printer.plug_ip.isnot(None),
Printer.plug_username.isnot(None),
Printer.plug_password.isnot(None)
).all()
results = []
total_online = 0
total_offline = 0
total_error = 0
total_warnings = 0
from PyP100 import PyP110
for printer in printers:
# Aktive Jobs für diesen Drucker prüfen
active_jobs = db_session.query(Job).filter(
Job.printer_id == printer.id,
Job.status.in_(["running", "printing", "active"])
).count()
# Steckdosen-Status prüfen
socket_status = "unknown"
device_on = False
current_power = None
error_message = None
warnings = []
try:
p110 = PyP110.P110(printer.plug_ip, printer.plug_username, printer.plug_password)
p110.handshake()
p110.login()
device_info = p110.getDeviceInfo()
device_on = device_info["result"]["device_on"]
socket_status = "online" if device_on else "offline"
# Energieverbrauch abrufen
try:
energy_info = p110.getEnergyUsage()
current_power = energy_info.get("result", {}).get("current_power", 0)
except:
current_power = None
# Warnungen generieren
if active_jobs > 0:
warnings.append(f"{active_jobs} aktive Job(s)")
if device_on and current_power and current_power > 10:
warnings.append(f"Hoher Verbrauch: {current_power}W")
except Exception as e:
socket_status = "error"
error_message = str(e)
warnings.append(f"Verbindungsfehler: {str(e)[:50]}")
# Statistiken aktualisieren
if socket_status == "online":
total_online += 1
elif socket_status == "offline":
total_offline += 1
else:
total_error += 1
if warnings:
total_warnings += 1
results.append({
"printer": {
"id": printer.id,
"name": printer.name,
"model": printer.model,
"location": printer.location
},
"socket": {
"status": socket_status,
"device_on": device_on,
"current_power": current_power,
"ip_address": printer.plug_ip,
"error": error_message
},
"warnings": warnings,
"active_jobs": active_jobs,
"safe_to_test": len(warnings) == 0
})
db_session.close()
# Gesamtübersicht erstellen
summary = {
"total_sockets": len(results),
"online": total_online,
"offline": total_offline,
"error": total_error,
"with_warnings": total_warnings,
"safe_to_test": len(results) - total_warnings
}
printers_logger.info(f"✅ Alle-Steckdosen-Status erfolgreich abgerufen: {len(results)} Steckdosen")
return jsonify({
"success": True,
"sockets": results,
"summary": summary,
"timestamp": datetime.now().isoformat()
})
except Exception as e:
printers_logger.error(f"❌ Fehler bei Alle-Steckdosen-Test-Status: {str(e)}")
return jsonify({
"success": False,
"error": f"Allgemeiner Fehler: {str(e)}"
}), 500
# =============================================================================
# DRAG & DROP API - JOB-REIHENFOLGE-MANAGEMENT
# =============================================================================
@printers_blueprint.route("/<int:printer_id>/jobs/order", methods=["GET"])
@login_required
@measure_execution_time(logger=printers_logger, task_name="API-Job-Reihenfolge-Abfrage")
def get_job_order(printer_id):
"""
Holt die aktuelle Job-Reihenfolge für einen Drucker.
Args:
printer_id: ID des Druckers
Returns:
JSON mit Jobs in der korrekten Reihenfolge
"""
printers_logger.info(f"📋 Job-Reihenfolge-Abfrage für Drucker {printer_id} von Benutzer {current_user.name}")
try:
# Drucker existiert prüfen
db_session = get_db_session()
printer = db_session.query(Printer).filter(Printer.id == printer_id).first()
if not printer:
db_session.close()
return jsonify({
"success": False,
"error": f"Drucker mit ID {printer_id} nicht gefunden"
}), 404
db_session.close()
# Job-Reihenfolge und Details holen
ordered_jobs = drag_drop_manager.get_ordered_jobs_for_printer(printer_id)
job_order_ids = drag_drop_manager.get_job_order(printer_id)
# Job-Details für Response aufbereiten
jobs_data = []
for job in ordered_jobs:
jobs_data.append({
"id": job.id,
"name": job.name,
"description": job.description,
"user_name": job.user.name if job.user else "Unbekannt",
"user_id": job.user_id,
"duration_minutes": job.duration_minutes,
"created_at": job.created_at.isoformat() if job.created_at else None,
"start_at": job.start_at.isoformat() if job.start_at else None,
"status": job.status,
"file_path": job.file_path
})
printers_logger.info(f"✅ Job-Reihenfolge erfolgreich abgerufen: {len(jobs_data)} Jobs für Drucker {printer.name}")
return jsonify({
"success": True,
"printer": {
"id": printer.id,
"name": printer.name,
"model": printer.model,
"location": printer.location
},
"jobs": jobs_data,
"job_order": job_order_ids,
"total_jobs": len(jobs_data),
"total_duration_minutes": sum(job.duration_minutes for job in ordered_jobs),
"timestamp": datetime.now().isoformat()
})
except Exception as e:
printers_logger.error(f"❌ Fehler bei Job-Reihenfolge-Abfrage für Drucker {printer_id}: {str(e)}")
return jsonify({
"success": False,
"error": f"Fehler beim Laden der Job-Reihenfolge: {str(e)}"
}), 500
@printers_blueprint.route("/<int:printer_id>/jobs/order", methods=["POST"])
@login_required
@require_permission(Permission.APPROVE_JOBS) # Nur Benutzer mit Job-Genehmigungsrechten können Reihenfolge ändern
@measure_execution_time(logger=printers_logger, task_name="API-Job-Reihenfolge-Update")
def update_job_order(printer_id):
"""
Aktualisiert die Job-Reihenfolge für einen Drucker per Drag & Drop.
Args:
printer_id: ID des Druckers
JSON-Parameter:
- job_ids: Liste der Job-IDs in der gewünschten Reihenfolge
Returns:
JSON mit Bestätigung der Aktualisierung
"""
printers_logger.info(f"🔄 Job-Reihenfolge-Update für Drucker {printer_id} von Benutzer {current_user.name}")
# Parameter validieren
data = request.get_json()
if not data or "job_ids" not in data:
return jsonify({
"success": False,
"error": "Parameter 'job_ids' fehlt"
}), 400
job_ids = data["job_ids"]
if not isinstance(job_ids, list):
return jsonify({
"success": False,
"error": "Parameter 'job_ids' muss eine Liste sein"
}), 400
if not all(isinstance(job_id, int) for job_id in job_ids):
return jsonify({
"success": False,
"error": "Alle Job-IDs müssen Zahlen sein"
}), 400
try:
# Drucker existiert prüfen
db_session = get_db_session()
printer = db_session.query(Printer).filter(Printer.id == printer_id).first()
if not printer:
db_session.close()
return jsonify({
"success": False,
"error": f"Drucker mit ID {printer_id} nicht gefunden"
}), 404
# Validierung: Alle Jobs gehören zum Drucker und sind editierbar
valid_jobs = db_session.query(Job).filter(
Job.id.in_(job_ids),
Job.printer_id == printer_id,
Job.status.in_(['scheduled', 'paused'])
).all()
db_session.close()
if len(valid_jobs) != len(job_ids):
invalid_ids = set(job_ids) - {job.id for job in valid_jobs}
return jsonify({
"success": False,
"error": f"Ungültige oder nicht editierbare Job-IDs: {list(invalid_ids)}"
}), 400
# Berechtigung prüfen: Benutzer kann nur eigene Jobs oder als Admin alle verschieben
if not current_user.is_admin:
user_job_ids = {job.id for job in valid_jobs if job.user_id == current_user.id}
if user_job_ids != set(job_ids):
unauthorized_ids = set(job_ids) - user_job_ids
return jsonify({
"success": False,
"error": f"Keine Berechtigung für Jobs: {list(unauthorized_ids)}"
}), 403
# Job-Reihenfolge aktualisieren
success = drag_drop_manager.update_job_order(printer_id, job_ids)
if success:
# Neue Reihenfolge zur Bestätigung laden
updated_order = drag_drop_manager.get_job_order(printer_id)
printers_logger.info(f"✅ Job-Reihenfolge erfolgreich aktualisiert für Drucker {printer.name}")
printers_logger.info(f" Neue Reihenfolge: {job_ids}")
printers_logger.info(f" Benutzer: {current_user.name} (ID: {current_user.id})")
return jsonify({
"success": True,
"message": "Job-Reihenfolge erfolgreich aktualisiert",
"printer": {
"id": printer.id,
"name": printer.name
},
"old_order": job_ids, # Eingabe des Benutzers
"new_order": updated_order, # Bestätigung aus Datenbank
"total_jobs": len(job_ids),
"updated_by": {
"id": current_user.id,
"name": current_user.name
},
"timestamp": datetime.now().isoformat()
})
else:
return jsonify({
"success": False,
"error": "Fehler beim Speichern der Job-Reihenfolge"
}), 500
except Exception as e:
printers_logger.error(f"❌ Fehler bei Job-Reihenfolge-Update für Drucker {printer_id}: {str(e)}")
return jsonify({
"success": False,
"error": f"Unerwarteter Fehler: {str(e)}"
}), 500
@printers_blueprint.route("/<int:printer_id>/jobs/summary", methods=["GET"])
@login_required
@measure_execution_time(logger=printers_logger, task_name="API-Drucker-Job-Zusammenfassung")
def get_printer_job_summary(printer_id):
"""
Erstellt eine detaillierte Zusammenfassung der Jobs für einen Drucker.
Args:
printer_id: ID des Druckers
Returns:
JSON mit Zusammenfassung, Statistiken und Zeitschätzungen
"""
printers_logger.info(f"📊 Drucker-Job-Zusammenfassung für Drucker {printer_id} von Benutzer {current_user.name}")
try:
# Drucker existiert prüfen
db_session = get_db_session()
printer = db_session.query(Printer).filter(Printer.id == printer_id).first()
if not printer:
db_session.close()
return jsonify({
"success": False,
"error": f"Drucker mit ID {printer_id} nicht gefunden"
}), 404
db_session.close()
# Zusammenfassung über Drag-Drop-Manager erstellen
summary = drag_drop_manager.get_printer_summary(printer_id)
printers_logger.info(f"✅ Drucker-Job-Zusammenfassung erfolgreich erstellt für {printer.name}")
return jsonify({
"success": True,
"printer": {
"id": printer.id,
"name": printer.name,
"model": printer.model,
"location": printer.location,
"status": printer.status
},
"summary": summary,
"timestamp": datetime.now().isoformat()
})
except Exception as e:
printers_logger.error(f"❌ Fehler bei Drucker-Job-Zusammenfassung für Drucker {printer_id}: {str(e)}")
return jsonify({
"success": False,
"error": f"Fehler beim Erstellen der Zusammenfassung: {str(e)}"
}), 500
@printers_blueprint.route("/jobs/cleanup-orders", methods=["POST"])
@login_required
@require_permission(Permission.ADMIN)
@measure_execution_time(logger=printers_logger, task_name="API-Job-Reihenfolgen-Bereinigung")
def cleanup_job_orders():
"""
Bereinigt ungültige Job-Reihenfolgen (nur für Administratoren).
Entfernt Einträge für abgeschlossene oder gelöschte Jobs.
Returns:
JSON mit Bereinigungsergebnis
"""
printers_logger.info(f"🧹 Job-Reihenfolgen-Bereinigung von Admin {current_user.name}")
try:
# Bereinigung durchführen
drag_drop_manager.cleanup_invalid_orders()
printers_logger.info(f"✅ Job-Reihenfolgen-Bereinigung erfolgreich abgeschlossen")
return jsonify({
"success": True,
"message": "Job-Reihenfolgen erfolgreich bereinigt",
"admin": {
"id": current_user.id,
"name": current_user.name
},
"timestamp": datetime.now().isoformat()
})
except Exception as e:
printers_logger.error(f"❌ Fehler bei Job-Reihenfolgen-Bereinigung: {str(e)}")
return jsonify({
"success": False,
"error": f"Fehler bei der Bereinigung: {str(e)}"
}), 500
@printers_blueprint.route("/drag-drop/config", methods=["GET"])
@login_required
def get_drag_drop_config():
"""
Liefert die Konfiguration für das Drag & Drop System.
Returns:
JSON mit Drag & Drop Konfiguration und JavaScript/CSS
"""
printers_logger.info(f"⚙️ Drag-Drop-Konfiguration abgerufen von Benutzer {current_user.name}")
try:
from utils.drag_drop_system import get_drag_drop_javascript, get_drag_drop_css
# Benutzerberechtigungen prüfen
can_reorder_jobs = check_permission(current_user, Permission.APPROVE_JOBS)
can_upload_files = check_permission(current_user, Permission.CREATE_JOB)
config = {
"permissions": {
"can_reorder_jobs": can_reorder_jobs,
"can_upload_files": can_upload_files,
"is_admin": current_user.is_admin
},
"settings": {
"max_file_size": 50 * 1024 * 1024, # 50MB
"accepted_file_types": ["gcode", "stl", "3mf", "obj"],
"auto_upload": False,
"show_preview": True,
"enable_progress_tracking": True
},
"endpoints": {
"get_job_order": f"/api/printers/{{printer_id}}/jobs/order",
"update_job_order": f"/api/printers/{{printer_id}}/jobs/order",
"get_summary": f"/api/printers/{{printer_id}}/jobs/summary"
},
"javascript": get_drag_drop_javascript(),
"css": get_drag_drop_css()
}
return jsonify({
"success": True,
"config": config,
"user": {
"id": current_user.id,
"name": current_user.name,
"role": current_user.role
},
"timestamp": datetime.now().isoformat()
})
except Exception as e:
printers_logger.error(f"❌ Fehler bei Drag-Drop-Konfiguration: {str(e)}")
return jsonify({
"success": False,
"error": f"Fehler beim Laden der Konfiguration: {str(e)}"
}), 500
# =============================================================================
# ENDE DRAG & DROP API
# =============================================================================

359
blueprints/user.py Normal file
View File

@@ -0,0 +1,359 @@
"""
Benutzer-Blueprint für das 3D-Druck-Management-System
Dieses Modul enthält alle Benutzer-spezifischen Routen und Funktionen,
einschließlich Profilverwaltung, Einstellungen und Passwort-Änderung.
"""
import json
from datetime import datetime
from flask import Blueprint, render_template, request, jsonify, redirect, url_for, flash, make_response
from flask_login import login_required, current_user
from werkzeug.security import check_password_hash
from models import User, get_db_session
from utils.logging_config import get_logger
# Blueprint erstellen
user_blueprint = Blueprint('user', __name__, url_prefix='/user')
# Logger initialisieren
user_logger = get_logger("user")
@user_blueprint.route("/profile", methods=["GET"])
@login_required
def profile():
"""Benutzerprofil anzeigen"""
return render_template('user/profile.html', user=current_user)
@user_blueprint.route("/settings", methods=["GET"])
@login_required
def settings():
"""Benutzereinstellungen anzeigen"""
return render_template('user/settings.html', user=current_user)
@user_blueprint.route("/update-profile", methods=["POST"])
@login_required
def update_profile():
"""Benutzerprofil aktualisieren (Form-basiert)"""
try:
db_session = get_db_session()
user = db_session.query(User).filter(User.id == current_user.id).first()
if not user:
db_session.close()
flash("Benutzer nicht gefunden", "error")
return redirect(url_for('user.profile'))
# Aktualisierbare Felder aus dem Formular
user.name = request.form.get('name', user.name)
user.email = request.form.get('email', user.email)
user.department = request.form.get('department', user.department)
user.position = request.form.get('position', user.position)
user.phone = request.form.get('phone', user.phone)
user.bio = request.form.get('bio', user.bio)
user.updated_at = datetime.now()
db_session.commit()
user_logger.info(f"Profil aktualisiert für Benutzer {user.username}")
flash("Profil erfolgreich aktualisiert", "success")
db_session.close()
return redirect(url_for('user.profile'))
except Exception as e:
user_logger.error(f"Fehler beim Aktualisieren des Profils: {str(e)}")
flash("Fehler beim Aktualisieren des Profils", "error")
return redirect(url_for('user.profile'))
@user_blueprint.route("/api/update-settings", methods=["POST"])
@login_required
def api_update_settings():
"""API-Endpunkt für Einstellungen-Updates"""
try:
data = request.get_json()
db_session = get_db_session()
user = db_session.query(User).filter(User.id == current_user.id).first()
if not user:
db_session.close()
return jsonify({"error": "Benutzer nicht gefunden"}), 404
# Einstellungen JSON aktualisieren
current_settings = user.settings or {}
if isinstance(current_settings, str):
try:
current_settings = json.loads(current_settings)
except json.JSONDecodeError:
current_settings = {}
# Neue Einstellungen hinzufügen/aktualisieren
for key, value in data.items():
current_settings[key] = value
user.settings = json.dumps(current_settings)
user.updated_at = datetime.now()
db_session.commit()
user_logger.info(f"Einstellungen aktualisiert für Benutzer {user.username}")
db_session.close()
return jsonify({
"success": True,
"message": "Einstellungen erfolgreich aktualisiert"
})
except Exception as e:
user_logger.error(f"Fehler beim Aktualisieren der Einstellungen: {str(e)}")
return jsonify({"error": "Fehler beim Aktualisieren der Einstellungen"}), 500
@user_blueprint.route("/update-settings", methods=["POST"])
@login_required
def update_settings():
"""Benutzereinstellungen aktualisieren (Form-basiert)"""
try:
db_session = get_db_session()
user = db_session.query(User).filter(User.id == current_user.id).first()
if not user:
db_session.close()
flash("Benutzer nicht gefunden", "error")
return redirect(url_for('user.settings'))
# Einstellungen aus dem Formular sammeln
settings = {}
# Theme-Einstellungen
settings['theme'] = request.form.get('theme', 'light')
settings['language'] = request.form.get('language', 'de')
# Benachrichtigungseinstellungen
settings['email_notifications'] = request.form.get('email_notifications') == 'on'
settings['push_notifications'] = request.form.get('push_notifications') == 'on'
settings['job_completion_notifications'] = request.form.get('job_completion_notifications') == 'on'
settings['printer_error_notifications'] = request.form.get('printer_error_notifications') == 'on'
# Dashboard-Einstellungen
settings['default_dashboard_view'] = request.form.get('default_dashboard_view', 'overview')
settings['auto_refresh_interval'] = int(request.form.get('auto_refresh_interval', 30))
# Privacy-Einstellungen
settings['show_profile_publicly'] = request.form.get('show_profile_publicly') == 'on'
settings['allow_job_sharing'] = request.form.get('allow_job_sharing') == 'on'
user.settings = json.dumps(settings)
user.updated_at = datetime.now()
db_session.commit()
user_logger.info(f"Einstellungen aktualisiert für Benutzer {user.username}")
flash("Einstellungen erfolgreich aktualisiert", "success")
db_session.close()
return redirect(url_for('user.settings'))
except Exception as e:
user_logger.error(f"Fehler beim Aktualisieren der Einstellungen: {str(e)}")
flash("Fehler beim Aktualisieren der Einstellungen", "error")
return redirect(url_for('user.settings'))
@user_blueprint.route("/change-password", methods=["POST"])
@login_required
def change_password():
"""Passwort ändern"""
try:
# Daten aus Form oder JSON extrahieren
if request.is_json:
data = request.get_json()
current_password = data.get('current_password')
new_password = data.get('new_password')
confirm_password = data.get('confirm_password')
else:
current_password = request.form.get('current_password')
new_password = request.form.get('new_password')
confirm_password = request.form.get('confirm_password')
# Validierung
if not all([current_password, new_password, confirm_password]):
error_msg = "Alle Passwort-Felder sind erforderlich"
if request.is_json:
return jsonify({"error": error_msg}), 400
flash(error_msg, "error")
return redirect(url_for('user.settings'))
if new_password != confirm_password:
error_msg = "Neue Passwörter stimmen nicht überein"
if request.is_json:
return jsonify({"error": error_msg}), 400
flash(error_msg, "error")
return redirect(url_for('user.settings'))
if len(new_password) < 8:
error_msg = "Das neue Passwort muss mindestens 8 Zeichen lang sein"
if request.is_json:
return jsonify({"error": error_msg}), 400
flash(error_msg, "error")
return redirect(url_for('user.settings'))
db_session = get_db_session()
user = db_session.query(User).filter(User.id == current_user.id).first()
if not user:
db_session.close()
error_msg = "Benutzer nicht gefunden"
if request.is_json:
return jsonify({"error": error_msg}), 404
flash(error_msg, "error")
return redirect(url_for('user.settings'))
# Aktuelles Passwort überprüfen
if not user.check_password(current_password):
db_session.close()
error_msg = "Aktuelles Passwort ist falsch"
if request.is_json:
return jsonify({"error": error_msg}), 401
flash(error_msg, "error")
return redirect(url_for('user.settings'))
# Neues Passwort setzen
user.set_password(new_password)
user.updated_at = datetime.now()
db_session.commit()
user_logger.info(f"Passwort geändert für Benutzer {user.username}")
db_session.close()
success_msg = "Passwort erfolgreich geändert"
if request.is_json:
return jsonify({"success": True, "message": success_msg})
flash(success_msg, "success")
return redirect(url_for('user.settings'))
except Exception as e:
user_logger.error(f"Fehler beim Ändern des Passworts: {str(e)}")
error_msg = "Fehler beim Ändern des Passworts"
if request.is_json:
return jsonify({"error": error_msg}), 500
flash(error_msg, "error")
return redirect(url_for('user.settings'))
@user_blueprint.route("/export", methods=["GET"])
@login_required
def export_data():
"""Benutzerdaten exportieren (DSGVO-Compliance)"""
try:
db_session = get_db_session()
user = db_session.query(User).filter(User.id == current_user.id).first()
if not user:
db_session.close()
flash("Benutzer nicht gefunden", "error")
return redirect(url_for('user.settings'))
# Benutzerdaten sammeln
user_data = {
"personal_information": {
"id": user.id,
"username": user.username,
"email": user.email,
"name": user.name,
"department": user.department,
"position": user.position,
"phone": user.phone,
"bio": user.bio,
"role": user.role,
"created_at": user.created_at.isoformat() if user.created_at else None,
"last_login": user.last_login.isoformat() if user.last_login else None,
"updated_at": user.updated_at.isoformat() if user.updated_at else None,
"last_activity": user.last_activity.isoformat() if user.last_activity else None
},
"settings": json.loads(user.settings) if user.settings else {},
"jobs": [],
"export_date": datetime.now().isoformat(),
"export_note": "Dies ist ein Export Ihrer persönlichen Daten gemäß DSGVO Art. 20"
}
# Benutzer-Jobs sammeln (falls verfügbar)
try:
from models import Job
user_jobs = db_session.query(Job).filter(Job.user_id == user.id).all()
for job in user_jobs:
user_data["jobs"].append({
"id": job.id,
"filename": job.filename,
"status": job.status,
"created_at": job.created_at.isoformat() if job.created_at else None,
"estimated_duration": job.estimated_duration,
"material_used": job.material_used,
"notes": job.notes
})
except Exception as job_error:
user_logger.warning(f"Fehler beim Sammeln der Job-Daten: {str(job_error)}")
db_session.close()
# JSON-Response erstellen
response = make_response(jsonify(user_data))
response.headers['Content-Disposition'] = f'attachment; filename=user_data_{user.username}_{datetime.now().strftime("%Y%m%d_%H%M%S")}.json'
response.headers['Content-Type'] = 'application/json'
user_logger.info(f"Datenexport erstellt für Benutzer {user.username}")
return response
except Exception as e:
user_logger.error(f"Fehler beim Datenexport: {str(e)}")
flash("Fehler beim Erstellen des Datenexports", "error")
return redirect(url_for('user.settings'))
@user_blueprint.route("/profile", methods=["PUT"])
@login_required
def update_profile_api():
"""API-Endpunkt für Profil-Updates"""
try:
data = request.get_json()
db_session = get_db_session()
user = db_session.query(User).filter(User.id == current_user.id).first()
if not user:
db_session.close()
return jsonify({"error": "Benutzer nicht gefunden"}), 404
# Aktualisierbare Felder (ohne sensitive Daten)
updatable_fields = ['name', 'email', 'department', 'position', 'phone', 'bio']
for field in updatable_fields:
if field in data:
setattr(user, field, data[field])
user.updated_at = datetime.now()
db_session.commit()
user_logger.info(f"Profil über API aktualisiert für Benutzer {user.username}")
# Aktuelle Benutzerdaten zurückgeben
user_data = {
"id": user.id,
"username": user.username,
"email": user.email,
"name": user.name,
"department": user.department,
"position": user.position,
"phone": user.phone,
"bio": user.bio,
"role": user.role,
"updated_at": user.updated_at.isoformat()
}
db_session.close()
return jsonify({
"success": True,
"message": "Profil erfolgreich aktualisiert",
"user": user_data
})
except Exception as e:
user_logger.error(f"Fehler beim API-Profil-Update: {str(e)}")
return jsonify({"error": "Fehler beim Aktualisieren des Profils"}), 500

168
blueprints/users.py Normal file
View File

@@ -0,0 +1,168 @@
from flask import Blueprint, render_template, request, jsonify, redirect, url_for, abort
from flask_login import current_user, login_required
from sqlalchemy.exc import SQLAlchemyError
from functools import wraps
from models import User, UserPermission, get_cached_session
from utils.logging_config import get_logger
users_blueprint = Blueprint('users', __name__)
logger = get_logger("users")
def users_admin_required(f):
"""Decorator zur Prüfung der Admin-Berechtigung für Users Blueprint."""
@wraps(f)
@login_required
def users_decorated_function(*args, **kwargs):
if not current_user.is_admin:
abort(403, "Nur Administratoren haben Zugriff auf diese Seite")
return f(*args, **kwargs)
return users_decorated_function
@users_blueprint.route('/admin/users/<int:user_id>/permissions', methods=['GET'])
@users_admin_required
def admin_user_permissions(user_id):
"""Benutzerberechtigungen anzeigen und bearbeiten."""
with get_cached_session() as db_session:
user = db_session.query(User).filter_by(id=user_id).first()
if not user:
abort(404, "Benutzer nicht gefunden")
# Berechtigungen laden oder erstellen, falls nicht vorhanden
permission = db_session.query(UserPermission).filter_by(user_id=user_id).first()
if not permission:
permission = UserPermission(user_id=user_id)
db_session.add(permission)
db_session.commit()
return render_template('admin_user_permissions.html', user=user, permission=permission)
@users_blueprint.route('/api/users/<int:user_id>/permissions', methods=['GET'])
@login_required
def api_get_user_permissions(user_id):
"""Benutzerberechtigungen als JSON zurückgeben."""
# Nur Admins oder der Benutzer selbst darf seine Berechtigungen sehen
if not current_user.is_admin and current_user.id != user_id:
return jsonify({"error": "Keine Berechtigung"}), 403
try:
with get_cached_session() as db_session:
permission = db_session.query(UserPermission).filter_by(user_id=user_id).first()
if not permission:
# Falls keine Berechtigungen existieren, Standard-Werte zurückgeben
return jsonify({
"user_id": user_id,
"can_start_jobs": False,
"needs_approval": True,
"can_approve_jobs": False
})
return jsonify(permission.to_dict())
except Exception as e:
logger.error(f"Fehler beim Abrufen der Benutzerberechtigungen: {str(e)}")
return jsonify({"error": "Fehler beim Verarbeiten der Anfrage"}), 500
@users_blueprint.route('/api/users/<int:user_id>/permissions', methods=['PUT'])
@users_admin_required
def api_update_user_permissions(user_id):
"""Benutzerberechtigungen aktualisieren."""
try:
data = request.get_json()
if not data:
return jsonify({"error": "Keine Daten erhalten"}), 400
with get_cached_session() as db_session:
# Benutzer prüfen
user = db_session.query(User).filter_by(id=user_id).first()
if not user:
return jsonify({"error": "Benutzer nicht gefunden"}), 404
# Berechtigungen laden oder erstellen
permission = db_session.query(UserPermission).filter_by(user_id=user_id).first()
if not permission:
permission = UserPermission(user_id=user_id)
db_session.add(permission)
# Berechtigungen aktualisieren
if 'can_start_jobs' in data:
permission.can_start_jobs = bool(data['can_start_jobs'])
if 'needs_approval' in data:
permission.needs_approval = bool(data['needs_approval'])
if 'can_approve_jobs' in data:
permission.can_approve_jobs = bool(data['can_approve_jobs'])
db_session.commit()
logger.info(f"Berechtigungen für Benutzer {user_id} aktualisiert durch Admin {current_user.id}")
return jsonify({
"success": True,
"permissions": permission.to_dict()
})
except SQLAlchemyError as e:
logger.error(f"Datenbankfehler beim Aktualisieren der Benutzerberechtigungen: {str(e)}")
return jsonify({"error": "Datenbankfehler beim Verarbeiten der Anfrage"}), 500
except Exception as e:
logger.error(f"Fehler beim Aktualisieren der Benutzerberechtigungen: {str(e)}")
return jsonify({"error": "Fehler beim Verarbeiten der Anfrage"}), 500
@users_blueprint.route('/admin/users/<int:user_id>/permissions/update', methods=['POST'])
@users_admin_required
def admin_update_user_permissions(user_id):
"""Benutzerberechtigungen über Formular aktualisieren."""
try:
# Formularfelder auslesen
can_start_jobs = request.form.get('can_start_jobs') == 'on'
needs_approval = request.form.get('needs_approval') == 'on'
can_approve_jobs = request.form.get('can_approve_jobs') == 'on'
with get_cached_session() as db_session:
# Benutzer prüfen
user = db_session.query(User).filter_by(id=user_id).first()
if not user:
abort(404, "Benutzer nicht gefunden")
# Berechtigungen laden oder erstellen
permission = db_session.query(UserPermission).filter_by(user_id=user_id).first()
if not permission:
permission = UserPermission(user_id=user_id)
db_session.add(permission)
# Berechtigungen aktualisieren
permission.can_start_jobs = can_start_jobs
permission.needs_approval = needs_approval
permission.can_approve_jobs = can_approve_jobs
db_session.commit()
logger.info(f"Berechtigungen für Benutzer {user_id} aktualisiert durch Admin {current_user.id} (Formular)")
return redirect(url_for('users.admin_user_permissions', user_id=user_id))
except Exception as e:
logger.error(f"Fehler beim Aktualisieren der Benutzerberechtigungen: {str(e)}")
abort(500, "Fehler beim Verarbeiten der Anfrage")
# Erweiterung des bestehenden Benutzer-Bearbeitungsformulars
@users_blueprint.route('/admin/users/<int:user_id>/edit/permissions', methods=['GET'])
@users_admin_required
def admin_edit_user_permissions_section(user_id):
"""Rendert nur den Berechtigungsteil für das Benutzer-Edit-Formular."""
with get_cached_session() as db_session:
user = db_session.query(User).filter_by(id=user_id).first()
if not user:
abort(404, "Benutzer nicht gefunden")
# Berechtigungen laden oder erstellen, falls nicht vorhanden
permission = db_session.query(UserPermission).filter_by(user_id=user_id).first()
if not permission:
permission = UserPermission(user_id=user_id)
db_session.add(permission)
db_session.commit()
return render_template('_user_permissions_form.html', user=user, permission=permission)