FIN INIT
This commit is contained in:
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.
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
File diff suppressed because it is too large
Load Diff
+1035
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
@@ -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
|
||||
# =============================================================================
|
||||
@@ -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
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user