import os import threading import time import json import secrets import subprocess from datetime import datetime, timedelta from typing import Dict, List, Optional, Tuple, Any, Union from functools import wraps from flask import Flask, request, jsonify, session, render_template, redirect, url_for, flash, Response from flask_login import LoginManager, UserMixin, login_user, logout_user, current_user, login_required import sqlalchemy.exc import sqlalchemy from PyP100 import PyP110 from flask_wtf.csrf import CSRFProtect from config.settings import ( SECRET_KEY, TAPO_USERNAME, TAPO_PASSWORD, PRINTERS, FLASK_HOST, FLASK_PORT, FLASK_DEBUG, SESSION_LIFETIME, SCHEDULER_INTERVAL, SCHEDULER_ENABLED, get_ssl_context, FLASK_FALLBACK_PORT, SSL_ENABLED, SSL_CERT_PATH, SSL_KEY_PATH ) from utils.logging_config import setup_logging, get_logger, log_startup_info from models import User, Printer, Job, Stats, get_db_session, init_database, create_initial_admin from utils.job_scheduler import scheduler from utils.template_helpers import register_template_helpers from blueprints.auth import auth_bp from blueprints.user import user_bp # Flask-App initialisieren app = Flask(__name__) app.secret_key = SECRET_KEY app.config["PERMANENT_SESSION_LIFETIME"] = SESSION_LIFETIME app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False app.config["WTF_CSRF_ENABLED"] = True # CSRF-Schutz initialisieren csrf = CSRFProtect(app) # Login-Manager initialisieren login_manager = LoginManager() login_manager.init_app(app) login_manager.login_view = "auth.login" login_manager.login_message = "Bitte melden Sie sich an, um auf diese Seite zuzugreifen." login_manager.login_message_category = "info" @login_manager.user_loader def load_user(user_id): db_session = get_db_session() user = db_session.query(User).filter(User.id == user_id).first() db_session.close() return user # Jinja2 Context Processors @app.context_processor def inject_now(): """Inject the current datetime into templates.""" return {'now': datetime.now()} # Custom Jinja2 filter für Datumsformatierung @app.template_filter('format_datetime') def format_datetime_filter(value, format='%d.%m.%Y %H:%M'): """Format a datetime object to a German-style date and time string""" if value is None: return "" if isinstance(value, str): try: value = datetime.fromisoformat(value) except ValueError: return value return value.strftime(format) # Blueprints registrieren try: from blueprints.kiosk_control import kiosk_bp app.register_blueprint(kiosk_bp) print("Kiosk-Kontrolle erfolgreich geladen") except ImportError: print("Kiosk-Kontrolle nicht verfügbar (nur im Kiosk-Modus)") # Auth-Blueprint registrieren app.register_blueprint(auth_bp) print("Auth-Blueprint erfolgreich geladen") # User-Blueprint registrieren app.register_blueprint(user_bp) print("User-Blueprint erfolgreich geladen") # Logging initialisieren setup_logging() log_startup_info() # Logger für verschiedene Komponenten app_logger = get_logger("app") auth_logger = get_logger("auth") jobs_logger = get_logger("jobs") printers_logger = get_logger("printers") # Custom decorator für Job-Besitzer-Check def job_owner_required(f): @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 # UI-Routen @app.route("/") def index(): if current_user.is_authenticated: return render_template("index.html") return redirect(url_for("auth.login")) @app.route("/dashboard") @login_required def dashboard(): return render_template("dashboard.html") @app.route("/profile") @login_required def profile_redirect(): """Leitet zur neuen Profilseite im User-Blueprint weiter.""" return redirect(url_for("user.profile")) @app.route("/profil") @login_required def profil_redirect(): """Leitet zur neuen Profilseite im User-Blueprint weiter (deutsche URL).""" return redirect(url_for("user.profile")) @app.route("/settings") @login_required def settings_redirect(): """Leitet zur neuen Einstellungsseite im User-Blueprint weiter.""" return redirect(url_for("user.settings")) @app.route("/einstellungen") @login_required def einstellungen_redirect(): """Leitet zur neuen Einstellungsseite im User-Blueprint weiter (deutsche URL).""" return redirect(url_for("user.settings")) @app.route("/admin") @login_required def admin(): """Leitet zur neuen Admin-Dashboard-Route weiter.""" if not current_user.is_admin: flash("Sie haben keine Berechtigung für den Admin-Bereich.", "error") return redirect(url_for("index")) return redirect(url_for("admin_page")) @app.route("/demo") @login_required def components_demo(): """Demo-Seite für UI-Komponenten""" return render_template("components_demo.html") @app.route("/printers") @login_required def printers_page(): """Zeigt die Übersichtsseite für Drucker an.""" return render_template("printers.html") @app.route("/jobs") @login_required def jobs_page(): """Zeigt die Übersichtsseite für Druckaufträge an.""" return render_template("jobs.html") @app.route("/stats") @login_required def stats_page(): """Zeigt die Statistik-Seite an.""" return render_template("stats.html") @app.route("/admin-dashboard") @login_required def admin_page(): """Zeigt die Administrationsseite an.""" if not current_user.is_admin: flash("Sie haben keine Berechtigung für den Admin-Bereich.", "error") return redirect(url_for("index")) # Aktives Tab aus der URL auslesen oder Default-Wert verwenden active_tab = request.args.get('tab', 'users') # Daten für das Admin-Panel direkt beim Laden vorbereiten stats = {} users = [] printers = [] scheduler_status = {"running": False, "message": "Nicht verfügbar"} system_info = {"cpu": 0, "memory": 0, "disk": 0} logs = [] db_session = get_db_session() try: # Statistiken laden from sqlalchemy.orm import joinedload # Benutzeranzahl stats["total_users"] = db_session.query(User).count() # Druckeranzahl stats["total_printers"] = db_session.query(Printer).count() # Aktive Jobs stats["active_jobs"] = db_session.query(Job).filter( Job.status.in_(["scheduled", "running"]) ).count() # Erfolgsrate total_jobs = db_session.query(Job).filter( Job.status.in_(["completed", "failed", "cancelled"]) ).count() successful_jobs = db_session.query(Job).filter( Job.status == "completed" ).count() if total_jobs > 0: stats["success_rate"] = int((successful_jobs / total_jobs) * 100) else: stats["success_rate"] = 0 # Benutzer laden if active_tab == 'users': users = db_session.query(User).all() users = [user.to_dict() for user in users] # Drucker laden if active_tab == 'printers': printers = db_session.query(Printer).all() printers = [printer.to_dict() for printer in printers] # Scheduler-Status laden if active_tab == 'scheduler': from utils.scheduler import scheduler_is_running scheduler_status = { "running": scheduler_is_running(), "message": "Der Scheduler läuft" if scheduler_is_running() else "Der Scheduler ist gestoppt" } # System-Informationen laden if active_tab == 'system': import psutil system_info = { "cpu": psutil.cpu_percent(), "memory": psutil.virtual_memory().percent, "disk": psutil.disk_usage('/').percent, "uptime": get_system_uptime_days() } # Logs laden if active_tab == 'logs': import os log_level = request.args.get('log_level', 'all') log_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'logs') # Logeinträge sammeln app_logs = [] for category in ['app', 'auth', 'jobs', 'printers', 'scheduler', 'errors']: log_file = os.path.join(log_dir, category, f'{category}.log') if os.path.exists(log_file): with open(log_file, 'r') as f: for line in f.readlines()[-100:]: # Nur die letzten 100 Zeilen pro Datei if log_level != 'all': if log_level.upper() not in line: continue app_logs.append({ 'timestamp': line.split(' - ')[0] if ' - ' in line else '', 'level': line.split(' - ')[1].split(' - ')[0] if ' - ' in line and len(line.split(' - ')) > 2 else 'INFO', 'category': category, 'message': ' - '.join(line.split(' - ')[2:]) if ' - ' in line and len(line.split(' - ')) > 2 else line }) # Nach Zeitstempel sortieren (neueste zuerst) logs = sorted(app_logs, key=lambda x: x['timestamp'] if x['timestamp'] else '', reverse=True)[:100] except Exception as e: app_logger.error(f"Fehler beim Laden der Admin-Daten: {str(e)}") finally: db_session.close() return render_template( "admin.html", active_tab=active_tab, stats=stats, users=users, printers=printers, scheduler_status=scheduler_status, system_info=system_info, logs=logs ) # Direkter Zugriff auf Logout-Route (für Fallback) @app.route("/logout", methods=["GET", "POST"]) def logout_redirect(): """Leitet zur Blueprint-Logout-Route weiter.""" return redirect(url_for("auth.logout")) # Job-Routen @app.route("/api/jobs", methods=["GET"]) @login_required def get_jobs(): db_session = get_db_session() try: # Import joinedload for eager loading from sqlalchemy.orm import joinedload # Admin sieht alle Jobs, User nur eigene if current_user.is_admin: # Eagerly load the user and printer relationships to avoid detached instance errors jobs = db_session.query(Job).options(joinedload(Job.user), joinedload(Job.printer)).all() else: jobs = db_session.query(Job).options(joinedload(Job.user), joinedload(Job.printer)).filter(Job.user_id == int(current_user.id)).all() # Convert jobs to dictionaries before closing the session job_dicts = [job.to_dict() for job in jobs] db_session.close() return jsonify({ "jobs": job_dicts }) except Exception as e: jobs_logger.error(f"Fehler beim Abrufen von Jobs: {str(e)}") db_session.close() return jsonify({"error": "Interner Serverfehler"}), 500 @app.route("/api/jobs/", methods=["GET"]) @login_required @job_owner_required def get_job(job_id): db_session = get_db_session() try: from sqlalchemy.orm import joinedload # 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: db_session.close() return jsonify({"error": "Job nicht gefunden"}), 404 # Convert to dict before closing session job_dict = job.to_dict() db_session.close() return jsonify(job_dict) except Exception as e: jobs_logger.error(f"Fehler beim Abrufen des Jobs {job_id}: {str(e)}") db_session.close() return jsonify({"error": "Interner Serverfehler"}), 500 @app.route('/api/jobs/active', methods=['GET']) @login_required def get_active_jobs(): """ Gibt alle aktiven Jobs zurück. """ try: db_session = get_db_session() from sqlalchemy.orm import joinedload active_jobs = db_session.query(Job).options( joinedload(Job.user), joinedload(Job.printer) ).filter( Job.status.in_(["scheduled", "running"]) ).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 @app.route('/api/jobs', methods=['POST']) @login_required def create_job(): """ Erstellt einen neuen Job mit dem Status "scheduled". Body: { "printer_id": int, "start_iso": str, # ISO-Datum-String "duration_minutes": int } """ try: data = request.json # Pflichtfelder prüfen required_fields = ["printer_id", "start_iso", "duration_minutes"] for field in required_fields: if field not in data: return jsonify({"error": f"Feld '{field}' fehlt"}), 400 # Daten extrahieren und validieren printer_id = int(data["printer_id"]) start_iso = data["start_iso"] duration_minutes = int(data["duration_minutes"]) # Optional: Jobtitel und Dateipfad name = data.get("name", f"Druckjob vom {datetime.now().strftime('%d.%m.%Y')}") file_path = data.get("file_path") # Start-Zeit parsen try: start_at = datetime.fromisoformat(start_iso) except ValueError: return jsonify({"error": "Ungültiges Startdatum"}), 400 # Dauer validieren if duration_minutes <= 0: 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() # Prüfen, ob der Drucker existiert printer = db_session.query(Printer).get(printer_id) if not printer: db_session.close() return jsonify({"error": "Drucker nicht gefunden"}), 404 # Neuen Job erstellen new_job = Job( name=name, printer_id=printer_id, user_id=current_user.id, owner_id=current_user.id, start_at=start_at, end_at=end_at, status="scheduled", 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} erstellt für Drucker {printer_id}, Start: {start_at}, Dauer: {duration_minutes} Minuten") return jsonify({"job": job_dict}), 201 except Exception as e: jobs_logger.error(f"Fehler beim Erstellen eines Jobs: {str(e)}") return jsonify({"error": "Interner Serverfehler", "details": str(e)}), 500 @app.route('/api/jobs//extend', methods=['POST']) @login_required @job_owner_required def extend_job(job_id): """ Verlängert die Endzeit eines Jobs. Body: { "extra_minutes": int } """ try: data = request.json # Prüfen, ob die erforderlichen Daten vorhanden sind if "extra_minutes" not in data: return jsonify({"error": "Feld 'extra_minutes' fehlt"}), 400 extra_minutes = int(data["extra_minutes"]) # Validieren if extra_minutes <= 0: return jsonify({"error": "Zusätzliche Minuten müssen größer als 0 sein"}), 400 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 verlängert werden kann if job.status not in ["scheduled", "running"]: db_session.close() return jsonify({"error": f"Job kann im Status '{job.status}' nicht verlängert werden"}), 400 # Endzeit aktualisieren job.end_at = job.end_at + timedelta(minutes=extra_minutes) job.duration_minutes += extra_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} um {extra_minutes} Minuten verlängert, neue Endzeit: {job.end_at}") return jsonify({"job": job_dict}) except Exception as e: jobs_logger.error(f"Fehler beim Verlängern von Job {job_id}: {str(e)}") return jsonify({"error": "Interner Serverfehler", "details": str(e)}), 500 @app.route('/api/jobs//finish', methods=['POST']) @login_required def finish_job(job_id): """ Beendet einen Job manuell und schaltet die Steckdose aus. Nur für Administratoren erlaubt. """ try: # Prüfen, ob der Benutzer Administrator ist if not current_user.is_admin: return jsonify({"error": "Nur Administratoren können Jobs manuell beenden"}), 403 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 beendet werden kann if job.status not in ["scheduled", "running"]: db_session.close() return jsonify({"error": f"Job kann im Status '{job.status}' nicht beendet werden"}), 400 # Steckdose ausschalten from utils.job_scheduler import toggle_plug if not toggle_plug(job.printer_id, False): # Trotzdem weitermachen, aber Warnung loggen jobs_logger.warning(f"Steckdose für Job {job_id} konnte nicht ausgeschaltet werden") # 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 Admin {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 @app.route("/api/printers", methods=["GET"]) @login_required def get_printers(): db_session = get_db_session() try: printers = db_session.query(Printer).all() # Optimierte Drucker-Liste mit schneller Status-Bestimmung printer_list = [] for printer in printers: # Bestimme Status basierend auf hardkodierten Druckern printer_config = PRINTERS.get(printer.name) if printer_config: status = "available" # Drucker verfügbar active = True else: status = "offline" active = False # Aktualisiere Status in der Datenbank printer.status = status printer.active = active printer_data = printer.to_dict() printer_data["status"] = status printer_data["active"] = active printer_list.append(printer_data) # Speichere Updates db_session.commit() db_session.close() return jsonify({ "printers": printer_list }) except Exception as e: printers_logger.error(f"Fehler beim Abrufen der Drucker: {str(e)}") db_session.rollback() db_session.close() return jsonify({"error": "Interner Serverfehler"}), 500 # API-Routen für Statistiken @app.route("/api/stats/users", methods=["GET"]) @login_required def get_stats_users(): """Gibt die Anzahl der Benutzer zurück.""" db_session = get_db_session() try: user_count = db_session.query(User).count() db_session.close() return jsonify({"value": user_count}) except Exception as e: db_session.close() return jsonify({"error": str(e)}), 500 @app.route("/api/stats/uptime", methods=["GET"]) @login_required def get_stats_uptime(): """Gibt die Systemlaufzeit zurück.""" import os with open('/proc/uptime', 'r') as f: uptime_seconds = float(f.readline().split()[0]) uptime_days = int(uptime_seconds / 86400) return jsonify({"value": f"{uptime_days} Tage"}) @app.route("/api/stats/active-jobs", methods=["GET"]) @login_required def get_stats_active_jobs(): """Gibt die Anzahl der aktiven Jobs zurück.""" db_session = get_db_session() try: active_jobs = db_session.query(Job).filter(Job.status.in_(["scheduled", "running"])).count() db_session.close() return jsonify({"value": active_jobs}) except Exception as e: db_session.close() return jsonify({"error": str(e)}), 500 @app.route("/api/stats/available-printers", methods=["GET"]) @login_required def get_stats_available_printers(): """Gibt die Anzahl der verfügbaren Drucker zurück.""" db_session = get_db_session() try: available_printers = db_session.query(Printer).filter(Printer.active == True).count() db_session.close() return jsonify({"value": available_printers}) except Exception as e: db_session.close() return jsonify({"error": str(e)}), 500 @app.route("/api/stats/success-rate", methods=["GET"]) @login_required def get_stats_success_rate(): """Gibt die Erfolgsrate der Druckaufträge zurück.""" db_session = get_db_session() try: total_jobs = db_session.query(Job).filter(Job.status == "finished").count() if total_jobs == 0: success_rate = 0 else: success_jobs = db_session.query(Job).filter( Job.status == "finished", Job.actual_end_time != None ).count() success_rate = int((success_jobs / total_jobs) * 100) db_session.close() return jsonify({"value": f"{success_rate}%"}) except Exception as e: db_session.close() return jsonify({"error": str(e)}), 500 @app.route("/api/stats/print-time", methods=["GET"]) @login_required def get_stats_print_time(): """Gibt die gesamte Druckzeit zurück.""" db_session = get_db_session() try: stats = db_session.query(Stats).first() if stats and stats.total_print_time: hours = stats.total_print_time // 3600 db_session.close() return jsonify({"value": f"{hours}h"}) db_session.close() return jsonify({"value": "0h"}) except Exception as e: db_session.close() return jsonify({"error": str(e)}), 500 @app.route("/api/activity", methods=["GET"]) @login_required def get_activity(): """Gibt die letzten Aktivitäten zurück.""" db_session = get_db_session() try: recent_jobs = db_session.query(Job).order_by(Job.created_at.desc()).limit(5).all() activities = [ { "type": "job", "id": job.id, "title": job.name, "status": job.status, "timestamp": job.created_at.isoformat() if job.created_at else None } for job in recent_jobs ] db_session.close() return jsonify(activities) except Exception as e: db_session.close() return jsonify({"error": str(e)}), 500 @app.route("/api/printers/status", methods=["GET"]) @login_required def get_printers_status(): """Gibt den Status aller Drucker zurück - optimiert für schnelle Antwort.""" db_session = get_db_session() try: printers = db_session.query(Printer).all() # Schnelle Status-Bestimmung basierend auf hardkodierten Druckern status_data = [] for printer in printers: # Bestimme Status basierend auf IP-Adresse aus der Konfiguration printer_config = PRINTERS.get(printer.name) if printer_config: # Drucker ist in der Konfiguration -> als online betrachten status = "online" active = True else: # Drucker nicht in Konfiguration -> offline status = "offline" active = False # Aktualisiere den Status in der Datenbank für Konsistenz printer.status = status printer.active = active status_data.append({ "id": printer.id, "name": printer.name, "status": status, "active": active, "ip_address": printer.ip_address, "location": printer.location }) # Speichere die aktualisierten Status db_session.commit() db_session.close() return jsonify(status_data) except Exception as e: db_session.rollback() db_session.close() printers_logger.error(f"Fehler beim Abrufen des Drucker-Status: {str(e)}") return jsonify({"error": str(e)}), 500 @app.route("/api/jobs/current", methods=["GET"]) @login_required def get_current_job(): """Gibt den aktuellen Job des Benutzers zurück.""" db_session = get_db_session() try: current_job = db_session.query(Job).filter( Job.user_id == current_user.id, Job.status.in_(["scheduled", "running"]) ).order_by(Job.start_at).first() if current_job: job_data = current_job.to_dict() else: job_data = None db_session.close() return jsonify(job_data) except Exception as e: db_session.close() return jsonify({"error": str(e)}), 500 # Admin API Endpoints @app.route("/api/users", methods=["GET"]) def get_users(): """Returns a list of all users (admin only)""" if not current_user.is_admin: return jsonify({"error": "Unauthorized"}), 403 db_session = get_db_session() try: users = db_session.query(User).all() users_list = [] for user in users: users_list.append({ "id": user.id, "email": user.email, "name": user.name, "role": user.role, "active": user.active, "created_at": user.created_at.isoformat() if user.created_at else None }) db_session.close() return jsonify({"users": users_list}) except Exception as e: db_session.close() app_logger.error(f"Error fetching users: {str(e)}") return jsonify({"error": "Failed to fetch users"}), 500 @app.route("/api/users", methods=["POST"]) @login_required def create_user(): """Create a new user (admin only)""" if not current_user.is_admin: flash("Sie haben keine Berechtigung, um neue Benutzer anzulegen.", "error") return redirect(url_for('admin_page', tab='users')) db_session = get_db_session() try: # Statt JSON-Daten die Formulardaten aus dem POST-Request holen email = request.form.get('email') name = request.form.get('name') password = request.form.get('password') role = request.form.get('role', 'user') if not email or not password: db_session.close() flash("E-Mail und Passwort sind Pflichtfelder.", "error") return redirect(url_for('admin_page', tab='users')) # Check if user with same email already exists existing_user = db_session.query(User).filter( User.email == email ).first() if existing_user: db_session.close() flash("Ein Benutzer mit dieser E-Mail existiert bereits.", "error") return redirect(url_for('admin_page', tab='users')) # Create new user new_user = User( email=email, name=name if name else "", username=email.split("@")[0], # Default username from email role=role, active=True, created_at=datetime.now() ) # Set password new_user.set_password(password) db_session.add(new_user) db_session.commit() user_id = new_user.id db_session.close() app_logger.info(f"New user created: {new_user.email} (ID: {user_id})") flash(f"Benutzer {email} wurde erfolgreich angelegt.", "success") return redirect(url_for('admin_page', tab='users')) except Exception as e: db_session.rollback() db_session.close() app_logger.error(f"Error creating user: {str(e)}") flash(f"Fehler beim Anlegen des Benutzers: {str(e)}", "error") return redirect(url_for('admin_page', tab='users')) @app.route("/api/users/", methods=["DELETE"]) @login_required def delete_user(user_id): """Delete a user (admin only)""" if not current_user.is_admin: flash("Sie haben keine Berechtigung, um Benutzer zu löschen.", "error") return redirect(url_for('admin_page', tab='users')) # Prevent admin from deleting themselves if user_id == current_user.id: flash("Sie können Ihren eigenen Account nicht löschen.", "error") return redirect(url_for('admin_page', tab='users')) db_session = get_db_session() try: 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_page', tab='users')) # Prevent deletion of admin users if user.role == "admin": db_session.close() flash("Administratoren können nicht gelöscht werden.", "error") return redirect(url_for('admin_page', tab='users')) email = user.email # Save for later logging db_session.delete(user) db_session.commit() db_session.close() app_logger.info(f"User deleted: {email} (ID: {user_id})") flash(f"Benutzer {email} wurde erfolgreich gelöscht.", "success") return redirect(url_for('admin_page', tab='users')) except Exception as e: db_session.rollback() db_session.close() app_logger.error(f"Error deleting user: {str(e)}") flash(f"Fehler beim Löschen des Benutzers: {str(e)}", "error") return redirect(url_for('admin_page', tab='users')) @app.route("/api/printers", methods=["POST"]) @login_required def create_printer(): """Create a new printer (admin only)""" if not current_user.is_admin: flash("Sie haben keine Berechtigung, um neue Drucker anzulegen.", "error") return redirect(url_for('admin_page', tab='printers')) db_session = get_db_session() try: # Statt JSON-Daten die Formulardaten aus dem POST-Request holen name = request.form.get('name') model = request.form.get('model') location = request.form.get('location') mac_address = request.form.get('mac_address') plug_ip = request.form.get('plug_ip') plug_username = request.form.get('plug_username') plug_password = request.form.get('plug_password') # Check required fields if not name or not mac_address: db_session.close() flash("Name und MAC-Adresse sind Pflichtfelder.", "error") return redirect(url_for('admin_page', tab='printers')) # Check if printer with same MAC already exists existing_printer = db_session.query(Printer).filter( Printer.mac_address == mac_address ).first() if existing_printer: db_session.close() flash("Ein Drucker mit dieser MAC-Adresse existiert bereits.", "error") return redirect(url_for('admin_page', tab='printers')) # Create new printer new_printer = Printer( name=name, model=model or "", location=location or "", mac_address=mac_address, plug_ip=plug_ip or "", plug_username=plug_username or "", plug_password=plug_password or "", active=True ) db_session.add(new_printer) db_session.commit() printer_id = new_printer.id db_session.close() app_logger.info(f"New printer created: {new_printer.name} (ID: {printer_id})") flash(f"Drucker {name} wurde erfolgreich angelegt.", "success") return redirect(url_for('admin_page', tab='printers')) except Exception as e: db_session.rollback() db_session.close() app_logger.error(f"Error creating printer: {str(e)}") flash(f"Fehler beim Anlegen des Druckers: {str(e)}", "error") return redirect(url_for('admin_page', tab='printers')) @app.route("/api/printers/", methods=["DELETE"]) @login_required def delete_printer(printer_id): """Delete a printer (admin only)""" if not current_user.is_admin: flash("Sie haben keine Berechtigung, um Drucker zu löschen.", "error") return redirect(url_for('admin_page', tab='printers')) db_session = get_db_session() try: 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_page', tab='printers')) printer_name = printer.name # Save for later logging db_session.delete(printer) db_session.commit() db_session.close() app_logger.info(f"Printer deleted: {printer_name} (ID: {printer_id})") flash(f"Drucker {printer_name} wurde erfolgreich gelöscht.", "success") return redirect(url_for('admin_page', tab='printers')) except Exception as e: db_session.rollback() db_session.close() app_logger.error(f"Error deleting printer: {str(e)}") flash(f"Fehler beim Löschen des Druckers: {str(e)}", "error") return redirect(url_for('admin_page', tab='printers')) @app.route("/api/stats", methods=["GET"]) @login_required def get_stats(): """Get overall system statistics""" if not current_user.is_admin: return jsonify({"error": "Unauthorized"}), 403 db_session = get_db_session() try: # Get basic stats stats = db_session.query(Stats).first() if not stats: # Create initial stats if none exist stats = Stats() db_session.add(stats) db_session.commit() # Count users, printers, active jobs user_count = db_session.query(User).count() printer_count = db_session.query(Printer).count() active_jobs = db_session.query(Job).filter(Job.status.in_(["scheduled", "running"])).count() result = { "total_users": user_count, "total_printers": printer_count, "active_jobs": active_jobs, "total_print_time_hours": round(stats.total_print_time / 3600, 1) if stats.total_print_time else 0, "total_jobs_completed": stats.total_jobs_completed or 0, "total_material_used": stats.total_material_used or 0, "last_updated": stats.last_updated.isoformat() if stats.last_updated else None } db_session.close() return jsonify(result) except Exception as e: db_session.close() app_logger.error(f"Error fetching stats: {str(e)}") return jsonify({"error": "Failed to fetch statistics"}), 500 @app.route("/api/scheduler/status", methods=["GET"]) @login_required def get_scheduler_status(): """Get the current status of the job scheduler""" if not current_user.is_admin: return jsonify({"error": "Unauthorized"}), 403 try: is_running = scheduler.is_running() tasks = [] # Add information about scheduler tasks for task_id, task in scheduler.get_tasks().items(): tasks.append({ "id": task_id, "interval": task.get("interval", 0), "last_run": task.get("last_run"), "enabled": task.get("enabled", False) }) return jsonify({ "running": is_running, "tasks": tasks, "uptime": scheduler.get_uptime() }) except Exception as e: app_logger.error(f"Error fetching scheduler status: {str(e)}") return jsonify({"error": "Failed to fetch scheduler status"}), 500 @app.route("/api/scheduler/start", methods=["POST"]) @login_required def start_scheduler(): """Start the job scheduler (admin only)""" if not current_user.is_admin: flash("Sie haben keine Berechtigung, um den Scheduler zu starten.", "error") return redirect(url_for('admin_page', tab='scheduler')) try: from utils.scheduler import start_scheduler as start_scheduler_func result = start_scheduler_func() if result: app_logger.info(f"Scheduler started by admin user: {current_user.email}") flash("Der Scheduler wurde erfolgreich gestartet.", "success") else: flash("Der Scheduler konnte nicht gestartet werden oder läuft bereits.", "warning") return redirect(url_for('admin_page', tab='scheduler')) except Exception as e: app_logger.error(f"Error starting scheduler: {str(e)}") flash(f"Fehler beim Starten des Schedulers: {str(e)}", "error") return redirect(url_for('admin_page', tab='scheduler')) @app.route("/api/scheduler/stop", methods=["POST"]) @login_required def stop_scheduler(): """Stop the job scheduler (admin only)""" if not current_user.is_admin: flash("Sie haben keine Berechtigung, um den Scheduler zu stoppen.", "error") return redirect(url_for('admin_page', tab='scheduler')) try: from utils.scheduler import stop_scheduler as stop_scheduler_func result = stop_scheduler_func() if result: app_logger.info(f"Scheduler stopped by admin user: {current_user.email}") flash("Der Scheduler wurde erfolgreich gestoppt.", "success") else: flash("Der Scheduler konnte nicht gestoppt werden oder läuft nicht.", "warning") return redirect(url_for('admin_page', tab='scheduler')) except Exception as e: app_logger.error(f"Error stopping scheduler: {str(e)}") flash(f"Fehler beim Stoppen des Schedulers: {str(e)}", "error") return redirect(url_for('admin_page', tab='scheduler')) @app.route("/api/logs", methods=["GET"]) @login_required def get_logs(): """Get system logs (admin only)""" if not current_user.is_admin: return jsonify({"error": "Unauthorized"}), 403 try: # Get log type from query params log_type = request.args.get("type", "app") limit = int(request.args.get("limit", 100)) log_mapping = { "app": "logs/app/app.log", "auth": "logs/auth/auth.log", "errors": "logs/errors/errors.log", "jobs": "logs/jobs/jobs.log", "printers": "logs/printers/printers.log", "scheduler": "logs/scheduler/scheduler.log" } if log_type not in log_mapping: return jsonify({"error": "Invalid log type"}), 400 log_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), log_mapping[log_type]) if not os.path.exists(log_path): return jsonify({"logs": [], "success": True}) logs = [] with open(log_path, "r") as f: for line in f.readlines()[-limit:]: try: # Parse log entry (format: [LEVEL] TIMESTAMP - MESSAGE) parts = line.strip().split(" - ", 1) if len(parts) == 2: header, message = parts level_timestamp = header.strip("[]").split("] [", 1) if len(level_timestamp) == 2: level, timestamp = level_timestamp logs.append({ "level": level.strip(), "timestamp": timestamp.strip(), "message": message.strip(), "source": log_type }) except Exception: # If parsing fails, add the raw line logs.append({ "level": "INFO", "timestamp": datetime.now().isoformat(), "message": line.strip(), "source": f"myp.{log_type}" }) # Sort logs by timestamp in descending order logs.sort(key=lambda x: x["timestamp"], reverse=True) return jsonify({"logs": logs, "success": True}) except Exception as e: app_logger.error(f"Error fetching logs: {str(e)}") return jsonify({"error": "Failed to fetch logs"}), 500 @app.route("/api/activity/recent", methods=["GET"]) @login_required def get_recent_activity(): """Get recent system activity""" try: # Create mock activity data (to be replaced with real data in future) activities = [ { "description": "Neuer Druckauftrag erstellt: 'Motor_Halterung_v2'", "timestamp": (datetime.now() - timedelta(minutes=15)).isoformat(), "user": "admin@example.com", "type": "job_created" }, { "description": "Drucker 'Prusa i3 MK3S' wurde neu konfiguriert", "timestamp": (datetime.now() - timedelta(hours=2)).isoformat(), "user": "admin@example.com", "type": "printer_updated" }, { "description": "Druckauftrag 'Getriebe_Prototyp' abgeschlossen", "timestamp": (datetime.now() - timedelta(hours=5)).isoformat(), "user": "user@example.com", "type": "job_completed" }, { "description": "Neuer Benutzer registriert: 'user@example.com'", "timestamp": (datetime.now() - timedelta(days=1)).isoformat(), "user": "admin@example.com", "type": "user_created" }, { "description": "Systemwartung durchgeführt", "timestamp": (datetime.now() - timedelta(days=2)).isoformat(), "user": "admin@example.com", "type": "system_maintenance" } ] # Get limit from query params limit = int(request.args.get("limit", 5)) activities = activities[:limit] return jsonify({"activities": activities}) except Exception as e: app_logger.error(f"Error fetching recent activity: {str(e)}") return jsonify({"error": "Failed to fetch recent activity"}), 500 # Service Worker Route @app.route('/sw.js') def service_worker(): """Serve the service worker script with proper headers""" response = app.send_static_file('js/sw.js') # Wichtig: Korrekte MIME-Type setzen response.headers['Content-Type'] = 'application/javascript' # Wichtig: Cache-Control Header setzen, um häufige Updates zu ermöglichen response.headers['Cache-Control'] = 'no-cache' # Service-Worker-Allowed Header setzen, um Scope-Probleme zu beheben response.headers['Service-Worker-Allowed'] = '/' return response # Fehlerbehandlung @app.errorhandler(404) def page_not_found(e): return render_template("404.html"), 404 @app.errorhandler(500) def internal_server_error(e): return render_template("500.html"), 500 # CLI-Befehle für Tailwind CSS @app.cli.group() def tailwind(): """Tailwind CSS Kommandos.""" pass @tailwind.command("build") def tailwind_build(): """Tailwind CSS für die Produktion kompilieren.""" print("Tailwind CSS wird kompiliert...") try: subprocess.run(["npx", "tailwindcss", "-i", "./static/css/input.css", "-o", "./static/css/tailwind-dark-consolidated.min.css", "--minify"], check=True) print("Tailwind CSS erfolgreich kompiliert.") except subprocess.CalledProcessError as e: print(f"Fehler beim Kompilieren von Tailwind CSS: {e}") raise @tailwind.command("watch") def tailwind_watch(): """Tailwind CSS im Watch-Modus starten.""" print("Tailwind CSS Watch-Modus wird gestartet...") try: subprocess.Popen(["npx", "tailwindcss", "-i", "./static/css/input.css", "-o", "./static/css/tailwind-dark-consolidated.min.css", "--watch"]) print("Tailwind CSS Watch-Modus gestartet. CSS wird bei Änderungen automatisch aktualisiert.") except subprocess.CalledProcessError as e: print(f"Fehler beim Starten des Tailwind CSS Watch-Modus: {e}") raise # Auto-Kompilierung beim Serverstart im Debug-Modus def compile_tailwind_if_debug(): """Kompiliert Tailwind CSS im Debug-Modus, falls notwendig.""" if FLASK_DEBUG: try: app_logger.info("Kompiliere Tailwind CSS...") # Prüfen, ob npx und Node.js verfügbar sind import platform import shutil import subprocess # Auf Windows nur fortfahren, wenn die CSS-Datei bereits existiert # oder npx verfügbar ist css_file_exists = os.path.exists("static/css/tailwind.min.css") # Prüfen, ob npx verfügbar ist npx_available = shutil.which("npx") is not None if platform.system() == "Windows" and not npx_available and not css_file_exists: app_logger.warning("npx nicht gefunden und keine CSS-Datei vorhanden. Tailwind CSS wird nicht kompiliert.") return # Tailwind CSS kompilieren if npx_available: subprocess.run([ "npx", "tailwindcss", "-i", "static/css/input.css", "-o", "static/css/tailwind.min.css", "--minify" ], check=True) app_logger.info("Tailwind CSS erfolgreich kompiliert.") elif css_file_exists: app_logger.info("Verwende existierende Tailwind CSS-Datei.") else: app_logger.warning("Tailwind konnte nicht kompiliert werden und keine CSS-Datei vorhanden.") except subprocess.CalledProcessError as e: app_logger.warning(f"Tailwind konnte nicht kompiliert werden. Möglicherweise ist npx/Node.js nicht installiert. Fehler: {e}") except Exception as e: app_logger.error(f"Fehler beim Kompilieren von Tailwind CSS: {str(e)}") # Tailwind CSS kompilieren, wenn im Debug-Modus if FLASK_DEBUG: compile_tailwind_if_debug() # Initialisierung der Datenbank beim Start def init_app(): """Initialisiert die App-Komponenten und startet den Scheduler.""" # Datenbank initialisieren try: init_database() create_initial_admin() except Exception as e: app_logger.error(f"Fehler bei der Datenbank-Initialisierung: {str(e)}") # Jinja2-Helfer registrieren register_template_helpers(app) # Tailwind im Debug-Modus kompilieren compile_tailwind_if_debug() # Scheduler starten, wenn aktiviert if SCHEDULER_ENABLED: try: # Scheduler-Task für Druckauftrags-Prüfung registrieren scheduler.register_task( "check_jobs", check_jobs, interval=SCHEDULER_INTERVAL ) # Scheduler starten scheduler.start() app_logger.info(f"Scheduler gestartet mit Intervall {SCHEDULER_INTERVAL} Sekunden.") except Exception as e: app_logger.error(f"Fehler beim Starten des Schedulers: {str(e)}") # SSL-Kontext protokollieren ssl_context = get_ssl_context() if ssl_context: app_logger.info(f"SSL aktiviert mit Zertifikat {ssl_context[0]}") else: app_logger.warning("SSL ist deaktiviert. Die Verbindung ist unverschlüsselt!") # Scheduler-Funktion zur Überprüfung der Druckaufträge def check_jobs(): """ Überprüft alle aktiven Druckaufträge und führt entsprechende Aktionen aus. Diese Funktion wird vom Scheduler regelmäßig aufgerufen. """ app_logger.info("Überprüfe Druckaufträge...") try: db_session = get_db_session() # Aktive Jobs abrufen active_jobs = db_session.query(Job).filter( Job.status.in_(["scheduled", "running"]) ).all() now = datetime.now() for job in active_jobs: # Prüfen, ob der Job gestartet werden soll if job.status == "scheduled" and job.start_at <= now: app_logger.info(f"Starte Job {job.id} für Drucker {job.printer_id}") job.status = "running" # Steckdose einschalten (implementieren Sie diese Funktion) from utils.job_scheduler import toggle_plug toggle_plug(job.printer_id, True) # Prüfen, ob der Job beendet werden soll elif job.status == "running" and job.end_at <= now: app_logger.info(f"Beende Job {job.id} für Drucker {job.printer_id}") job.status = "finished" job.actual_end_time = now # Steckdose ausschalten from utils.job_scheduler import toggle_plug toggle_plug(job.printer_id, False) db_session.commit() db_session.close() except Exception as e: app_logger.error(f"Fehler bei der Überprüfung von Druckaufträgen: {str(e)}") if 'db_session' in locals(): db_session.close() # App starten if __name__ == "__main__": import argparse import threading import ssl import socket import logging # Kommandozeilenargumente parsen parser = argparse.ArgumentParser(description='MYP Platform - 3D-Drucker Reservierungssystem') parser.add_argument('--port', type=int, help='Port für den Server (überschreibt die Konfiguration)') parser.add_argument('--no-ssl', action='store_true', help='Deaktiviert SSL/HTTPS') parser.add_argument('--dual-protocol', action='store_true', help='Startet sowohl HTTP als auch HTTPS Server') args = parser.parse_args() # Initialisierung init_app() # Port aus Kommandozeilenargument verwenden, falls angegeben port = args.port if args.port else FLASK_PORT # SSL-Kontext abrufen ssl_context = None if SSL_ENABLED and not args.no_ssl: try: if SSL_CERT_PATH and SSL_KEY_PATH: ssl_context = (SSL_CERT_PATH, SSL_KEY_PATH) logging.info(f"SSL aktiviert mit Zertifikat: {SSL_CERT_PATH}") else: ssl_context = 'adhoc' logging.info("SSL aktiviert mit selbstsigniertem Ad-hoc-Zertifikat") except Exception as e: logging.error(f"Fehler beim Laden des SSL-Kontexts: {e}") ssl_context = None # Dual-Protokoll-Modus: HTTP und HTTPS gleichzeitig if args.dual_protocol: # Funktion zum Starten des HTTP-Servers def start_http_server(): try: logging.info(f"Starte HTTP-Server auf Port 80...") # Kopie der App erstellen from werkzeug.serving import run_simple run_simple('0.0.0.0', 80, app, threaded=True) except socket.error as e: logging.error(f"Konnte HTTP-Server nicht starten: {e}") # Funktion zum Starten des HTTPS-Servers def start_https_server(): try: if ssl_context: logging.info(f"Starte HTTPS-Server auf Port {port}...") app.run(host='0.0.0.0', port=port, ssl_context=ssl_context, threaded=True) else: logging.warning("HTTPS deaktiviert aufgrund fehlender Zertifikate") app.run(host='0.0.0.0', port=port, threaded=True) except socket.error as e: logging.error(f"Konnte HTTPS-Server nicht starten: {e}") # Beide Server in separaten Threads starten http_thread = threading.Thread(target=start_http_server) https_thread = threading.Thread(target=start_https_server) http_thread.daemon = True https_thread.daemon = True http_thread.start() https_thread.start() # Warten, bis beide Threads beendet sind (was sie normalerweise nicht sein sollten) http_thread.join() https_thread.join() else: # Normaler Modus - entweder HTTP oder HTTPS if ssl_context: logging.info(f"Starte HTTPS-Server auf Port {port}...") app.run(host='0.0.0.0', port=port, ssl_context=ssl_context, threaded=True) else: logging.info(f"Starte HTTP-Server auf Port {port}...") app.run(host='0.0.0.0', port=port, threaded=True) # Content Security Policy anpassen @app.after_request def add_security_headers(response): """Fügt Sicherheitsheader zu allen Antworten hinzu""" # Content Security Policy definieren csp_directives = [ "default-src 'self'", "script-src 'self' 'unsafe-inline' 'unsafe-eval'", "script-src-elem 'self' 'unsafe-inline'", "style-src 'self' 'unsafe-inline'", "img-src 'self' data:", "font-src 'self'", "connect-src 'self'", "worker-src 'self'", # Erlaubt Service Worker "manifest-src 'self'" ] # Setze CSP Header response.headers['Content-Security-Policy'] = "; ".join(csp_directives) # Weitere Sicherheitsheader response.headers['X-Content-Type-Options'] = 'nosniff' response.headers['X-Frame-Options'] = 'SAMEORIGIN' response.headers['X-XSS-Protection'] = '1; mode=block' return response @app.route("/privacy") def privacy_page(): """Zeigt die Datenschutzseite an.""" return render_template("privacy.html") @app.route("/terms") def terms_page(): """Zeigt die Nutzungsbedingungen an.""" return render_template("terms.html") @app.route("/api/stats/export", methods=["GET"]) @login_required def export_stats(): """Exportiert Statistiken als JSON-Datei.""" if not current_user.is_admin: return jsonify({"error": "Nur Administratoren können Statistiken exportieren"}), 403 try: db_session = get_db_session() # Grundlegende Statistiken sammeln stats = db_session.query(Stats).first() if not stats: stats = Stats() # Benutzerzahlen user_count = db_session.query(User).count() active_user_count = db_session.query(User).filter(User.active == True).count() # Druckerzahlen printer_count = db_session.query(Printer).count() active_printer_count = db_session.query(Printer).filter(Printer.active == True).count() # Jobstatistiken total_jobs = db_session.query(Job).count() completed_jobs = db_session.query(Job).filter(Job.status == "finished").count() active_jobs = db_session.query(Job).filter(Job.status.in_(["scheduled", "running"])).count() failed_jobs = db_session.query(Job).filter(Job.status == "failed").count() # Berechne durchschnittliche Druckzeit jobs_with_duration = db_session.query(Job).filter( Job.start_at != None, Job.actual_end_time != None ).all() total_print_time = 0 avg_print_time = 0 if jobs_with_duration: for job in jobs_with_duration: duration = (job.actual_end_time - job.start_at).total_seconds() total_print_time += duration avg_print_time = total_print_time / len(jobs_with_duration) if len(jobs_with_duration) > 0 else 0 # Füge zusätzliche Statistiken für jede Druckerart hinzu printer_stats = [] printers = db_session.query(Printer).all() for printer in printers: printer_jobs = db_session.query(Job).filter(Job.printer_id == printer.id).count() printer_success_jobs = db_session.query(Job).filter( Job.printer_id == printer.id, Job.status == "finished" ).count() success_rate = (printer_success_jobs / printer_jobs * 100) if printer_jobs > 0 else 0 printer_stats.append({ "id": printer.id, "name": printer.name, "model": printer.model, "location": printer.location, "total_jobs": printer_jobs, "success_rate": round(success_rate, 2), "active": printer.active }) # Export-Daten zusammenstellen export_data = { "generated_at": datetime.now().isoformat(), "users": { "total": user_count, "active": active_user_count }, "printers": { "total": printer_count, "active": active_printer_count, "details": printer_stats }, "jobs": { "total": total_jobs, "completed": completed_jobs, "active": active_jobs, "failed": failed_jobs, "success_rate": round((completed_jobs / total_jobs * 100), 2) if total_jobs > 0 else 0 }, "print_time": { "total_seconds": total_print_time, "total_hours": round(total_print_time / 3600, 2), "average_seconds": avg_print_time, "average_minutes": round(avg_print_time / 60, 2) }, "system": { "version": "3.0.0", "uptime_days": get_system_uptime_days() } } db_session.close() # Als Datei zum Download anbieten from flask import make_response import json response = make_response(json.dumps(export_data, indent=4)) response.headers["Content-Disposition"] = "attachment; filename=stats_export.json" response.headers["Content-Type"] = "application/json" return response except Exception as e: app_logger.error(f"Fehler beim Exportieren der Statistiken: {str(e)}") return jsonify({"error": f"Fehler beim Exportieren: {str(e)}"}), 500 def get_system_uptime_days(): """Gibt die Systemlaufzeit in Tagen zurück.""" try: with open('/proc/uptime', 'r') as f: uptime_seconds = float(f.readline().split()[0]) return round(uptime_seconds / 86400, 2) # Umrechnung in Tage except Exception: return 0 @app.route("/api/printers/add", methods=["POST"]) @login_required def add_printer(): """Fügt einen neuen Drucker hinzu.""" if not current_user.is_admin: return jsonify({"error": "Nur Administratoren können Drucker hinzufügen"}), 403 try: data = request.json # Pflichtfelder prüfen required_fields = ["name", "mac_address", "plug_ip", "plug_username", "plug_password"] for field in required_fields: if field not in data or not data[field]: return jsonify({"error": f"Das Feld '{field}' ist ein Pflichtfeld"}), 400 # Druckerdaten extrahieren name = data["name"] model = data.get("model", "") location = data.get("location", "") mac_address = data["mac_address"] plug_ip = data["plug_ip"] plug_username = data["plug_username"] plug_password = data["plug_password"] db_session = get_db_session() # Prüfen, ob ein Drucker mit dieser MAC-Adresse bereits existiert existing_printer = db_session.query(Printer).filter(Printer.mac_address == mac_address).first() if existing_printer: db_session.close() return jsonify({"error": "Ein Drucker mit dieser MAC-Adresse existiert bereits"}), 400 # Neuen Drucker erstellen new_printer = Printer( name=name, model=model, location=location, mac_address=mac_address, plug_ip=plug_ip, plug_username=plug_username, plug_password=plug_password, status="offline", active=True, created_at=datetime.now() ) db_session.add(new_printer) db_session.commit() # Drucker-ID für die Antwort speichern printer_id = new_printer.id # Drucker-Objekt für die Antwort serialisieren printer_dict = new_printer.to_dict() db_session.close() printers_logger.info(f"Neuer Drucker {name} (ID: {printer_id}) wurde von {current_user.username} hinzugefügt") return jsonify({"success": True, "message": "Drucker erfolgreich hinzugefügt", "printer": printer_dict}), 201 except Exception as e: printers_logger.error(f"Fehler beim Hinzufügen eines Druckers: {str(e)}") return jsonify({"error": f"Fehler beim Hinzufügen des Druckers: {str(e)}"}), 500 @app.route("/my/jobs") @login_required def my_jobs(): """Zeigt die persönlichen Jobs des angemeldeten Benutzers an.""" # Weiterleitung zur Jobs-Seite mit Filter für den aktuellen Benutzer return redirect(url_for("jobs_page", user_filter=current_user.id)) @app.route("/api/user/export", methods=["GET"]) @login_required def api_user_export_redirect(): """Leitet den alten API-Pfad zum neuen Benutzer-Export weiter.""" return redirect(url_for("user.export_user_data")) @app.route("/api/user/profile", methods=["PUT"]) @login_required def api_user_profile_update_redirect(): """Leitet den alten API-Pfad zum neuen Benutzer-Profil-Update weiter.""" return redirect(url_for("user.update_profile_api")) @app.route("/user/update-settings", methods=["POST"]) @login_required def user_update_settings_redirect(): """Weiterleitung zur Blueprint-Route für Settings-Updates.""" return redirect(url_for("user.api_update_settings")) # SSL-Verwaltungsrouten @app.route("/api/ssl/info", methods=["GET"]) @login_required def get_ssl_info(): """Gibt Informationen über das aktuelle SSL-Zertifikat zurück.""" if not current_user.is_admin: return jsonify({"error": "Nur Administratoren können SSL-Informationen abrufen"}), 403 try: from utils.ssl_manager import ssl_manager cert_info = ssl_manager.get_certificate_info() if not cert_info: return jsonify({ "exists": False, "message": "Kein SSL-Zertifikat gefunden" }) return jsonify({ "exists": True, "certificate": cert_info, "paths": { "cert": ssl_manager.cert_path, "key": ssl_manager.key_path } }) except Exception as e: app_logger.error(f"Fehler beim Abrufen der SSL-Informationen: {e}") return jsonify({"error": f"Fehler beim Abrufen der SSL-Informationen: {str(e)}"}), 500 @app.route("/api/ssl/generate", methods=["POST"]) @login_required def generate_ssl_certificate(): """Generiert ein neues SSL-Zertifikat.""" if not current_user.is_admin: return jsonify({"error": "Nur Administratoren können SSL-Zertifikate generieren"}), 403 try: from utils.ssl_manager import ssl_manager # Parameter aus Request extrahieren data = request.json or {} key_size = data.get("key_size", 4096) validity_days = data.get("validity_days", 365) # Zertifikat generieren success = ssl_manager.generate_mercedes_certificate(key_size, validity_days) if success: cert_info = ssl_manager.get_certificate_info() app_logger.info(f"SSL-Zertifikat von {current_user.username} generiert") return jsonify({ "success": True, "message": "SSL-Zertifikat erfolgreich generiert", "certificate": cert_info }) else: return jsonify({ "success": False, "error": "Fehler beim Generieren des SSL-Zertifikats" }), 500 except Exception as e: app_logger.error(f"Fehler beim Generieren des SSL-Zertifikats: {e}") return jsonify({"error": f"Fehler beim Generieren: {str(e)}"}), 500 @app.route("/api/ssl/install", methods=["POST"]) @login_required def install_ssl_certificate(): """Installiert das SSL-Zertifikat im System.""" if not current_user.is_admin: return jsonify({"error": "Nur Administratoren können SSL-Zertifikate installieren"}), 403 try: from utils.ssl_manager import ssl_manager success = ssl_manager.install_system_certificate() if success: app_logger.info(f"SSL-Zertifikat von {current_user.username} im System installiert") return jsonify({ "success": True, "message": "SSL-Zertifikat erfolgreich im System installiert" }) else: return jsonify({ "success": False, "error": "Fehler bei der Installation des SSL-Zertifikats im System" }), 500 except Exception as e: app_logger.error(f"Fehler bei der SSL-Installation: {e}") return jsonify({"error": f"Fehler bei der Installation: {str(e)}"}), 500 @app.route("/api/ssl/copy-raspberry", methods=["POST"]) @login_required def copy_ssl_to_raspberry(): """Kopiert das SSL-Zertifikat auf den Raspberry Pi.""" if not current_user.is_admin: return jsonify({"error": "Nur Administratoren können SSL-Zertifikate kopieren"}), 403 try: from utils.ssl_manager import ssl_manager # Parameter aus Request extrahieren data = request.json or {} host = data.get("host", "raspberrypi") user = data.get("user", "user") dest = data.get("dest", "/home/user/Projektarbeit-MYP/backend/app/certs") success = ssl_manager.copy_to_raspberry(host, user, dest) if success: app_logger.info(f"SSL-Zertifikat von {current_user.username} auf Raspberry Pi kopiert") return jsonify({ "success": True, "message": f"SSL-Zertifikat erfolgreich auf {host} kopiert" }) else: return jsonify({ "success": False, "error": "Fehler beim Kopieren des SSL-Zertifikats auf den Raspberry Pi" }), 500 except Exception as e: app_logger.error(f"Fehler beim Kopieren auf Raspberry Pi: {e}") return jsonify({"error": f"Fehler beim Kopieren: {str(e)}"}), 500 @app.route("/api/ssl/validate", methods=["GET"]) @login_required def validate_ssl_certificate(): """Validiert das aktuelle SSL-Zertifikat.""" if not current_user.is_admin: return jsonify({"error": "Nur Administratoren können SSL-Zertifikate validieren"}), 403 try: from utils.ssl_manager import ssl_manager is_valid = ssl_manager.is_certificate_valid() cert_info = ssl_manager.get_certificate_info() return jsonify({ "valid": is_valid, "certificate": cert_info, "message": "Zertifikat ist gültig" if is_valid else "Zertifikat ist ungültig oder läuft bald ab" }) except Exception as e: app_logger.error(f"Fehler bei der SSL-Validierung: {e}") return jsonify({"error": f"Fehler bei der Validierung: {str(e)}"}), 500