""" Hauptanwendung für das 3D-Druck-Management-System Diese Datei initialisiert die Flask-Anwendung und registriert alle Blueprints. Die eigentlichen Routen sind in den jeweiligen Blueprint-Modulen definiert. """ import os import sys import logging import atexit import signal from datetime import datetime from flask import Flask, render_template, request, jsonify, redirect, url_for, session, abort from flask_login import LoginManager, current_user, logout_user, login_required from flask_wtf import CSRFProtect from flask_wtf.csrf import CSRFError from sqlalchemy import event from contextlib import contextmanager import threading # ===== OPTIMIERTE KONFIGURATION FÜR RASPBERRY PI ===== class OptimizedConfig: """Konfiguration für performance-optimierte Bereitstellung auf Raspberry Pi""" # Performance-Optimierungs-Flags OPTIMIZED_MODE = True USE_MINIFIED_ASSETS = True DISABLE_ANIMATIONS = True LIMIT_GLASSMORPHISM = True # Flask-Performance-Einstellungen DEBUG = False TESTING = False SEND_FILE_MAX_AGE_DEFAULT = 31536000 # 1 Jahr Cache für statische Dateien # Template-Einstellungen TEMPLATES_AUTO_RELOAD = False EXPLAIN_TEMPLATE_LOADING = False # Session-Konfiguration SESSION_COOKIE_SECURE = True SESSION_COOKIE_HTTPONLY = True SESSION_COOKIE_SAMESITE = 'Lax' # Performance-Optimierungen MAX_CONTENT_LENGTH = 16 * 1024 * 1024 # 16MB max Upload JSON_SORT_KEYS = False JSONIFY_PRETTYPRINT_REGULAR = False def detect_raspberry_pi(): """Erkennt ob das System auf einem Raspberry Pi läuft""" try: with open('/proc/cpuinfo', 'r') as f: cpuinfo = f.read() if 'Raspberry Pi' in cpuinfo or 'BCM' in cpuinfo: return True except: pass try: import platform machine = platform.machine().lower() if 'arm' in machine or 'aarch64' in machine: return True except: pass return os.getenv('FORCE_OPTIMIZED_MODE', '').lower() in ['true', '1', 'yes'] def should_use_optimized_config(): """Bestimmt ob die optimierte Konfiguration verwendet werden soll""" if '--optimized' in sys.argv: return True if detect_raspberry_pi(): return True if os.getenv('USE_OPTIMIZED_CONFIG', '').lower() in ['true', '1', 'yes']: return True try: import psutil memory_gb = psutil.virtual_memory().total / (1024**3) if memory_gb < 2.0: return True except: pass return False # Windows-spezifische Fixes if os.name == 'nt': try: from utils.windows_fixes import get_windows_thread_manager print("[OK] Windows-Fixes (sichere Version) geladen") except ImportError as e: get_windows_thread_manager = None print(f"[WARN] Windows-Fixes nicht verfügbar: {str(e)}") else: get_windows_thread_manager = None # Lokale Imports from models import init_database, create_initial_admin, User, get_db_session from utils.logging_config import setup_logging, get_logger, log_startup_info from utils.job_scheduler import JobScheduler, get_job_scheduler from utils.queue_manager import start_queue_manager, stop_queue_manager from utils.settings import SECRET_KEY, SESSION_LIFETIME # ===== OFFLINE-MODUS KONFIGURATION ===== OFFLINE_MODE = True # Produktionseinstellung für Offline-Betrieb # Blueprints importieren from blueprints.auth import auth_blueprint from blueprints.user import user_blueprint from blueprints.admin import admin_blueprint from blueprints.admin_api import admin_api_blueprint from blueprints.guest import guest_blueprint from blueprints.calendar import calendar_blueprint from blueprints.users import users_blueprint from blueprints.printers import printers_blueprint from blueprints.jobs import jobs_blueprint from blueprints.kiosk import kiosk_blueprint from blueprints.uploads import uploads_blueprint from blueprints.sessions import sessions_blueprint # Import der Sicherheits- und Hilfssysteme from utils.rate_limiter import cleanup_rate_limiter from utils.security import init_security from utils.permissions import init_permission_helpers # Logging initialisieren setup_logging() log_startup_info() # Logger für verschiedene Komponenten app_logger = get_logger("app") # Thread-sichere Caches _user_cache = {} _user_cache_lock = threading.RLock() _printer_status_cache = {} _printer_status_cache_lock = threading.RLock() # Cache-Konfiguration USER_CACHE_TTL = 300 # 5 Minuten PRINTER_STATUS_CACHE_TTL = 30 # 30 Sekunden def clear_user_cache(user_id=None): """Löscht User-Cache""" with _user_cache_lock: if user_id: _user_cache.pop(user_id, None) else: _user_cache.clear() def clear_printer_status_cache(): """Löscht Drucker-Status-Cache""" with _printer_status_cache_lock: _printer_status_cache.clear() # ===== AGGRESSIVE SHUTDOWN HANDLER ===== def aggressive_shutdown_handler(sig, frame): """Aggressiver Signal-Handler für sofortiges Herunterfahren bei Strg+C""" print("\n[ALERT] STRG+C ERKANNT - SOFORTIGES SHUTDOWN!") try: # Caches leeren clear_user_cache() clear_printer_status_cache() # Queue Manager stoppen try: stop_queue_manager() print("[OK] Queue Manager gestoppt") except Exception as e: print(f"[WARN] Queue Manager Stop fehlgeschlagen: {e}") # Datenbank-Cleanup try: from models import _engine, _scoped_session if _scoped_session: _scoped_session.remove() if _engine: _engine.dispose() print("[OK] Datenbank geschlossen") except Exception as e: print(f"[WARN] Datenbank-Cleanup fehlgeschlagen: {e}") except Exception as e: print(f"[ERROR] Fehler beim Cleanup: {e}") print("[STOP] SOFORTIGES PROGRAMM-ENDE") os._exit(0) def register_aggressive_shutdown(): """Registriert den aggressiven Shutdown-Handler""" signal.signal(signal.SIGINT, aggressive_shutdown_handler) signal.signal(signal.SIGTERM, aggressive_shutdown_handler) if os.name == 'nt': try: signal.signal(signal.SIGBREAK, aggressive_shutdown_handler) except AttributeError: pass else: try: signal.signal(signal.SIGHUP, aggressive_shutdown_handler) except AttributeError: pass atexit.register(lambda: print("[RESTART] Atexit-Handler ausgeführt")) print("[ALERT] AGGRESSIVER STRG+C SHUTDOWN-HANDLER AKTIVIERT") # Shutdown-Handler registrieren register_aggressive_shutdown() # Flask-App initialisieren app = Flask(__name__) app.secret_key = SECRET_KEY # ===== KONFIGURATION ANWENDEN ===== USE_OPTIMIZED_CONFIG = should_use_optimized_config() if USE_OPTIMIZED_CONFIG: app_logger.info("[START] Aktiviere optimierte Konfiguration") app.config.update({ "DEBUG": OptimizedConfig.DEBUG, "TESTING": OptimizedConfig.TESTING, "SEND_FILE_MAX_AGE_DEFAULT": OptimizedConfig.SEND_FILE_MAX_AGE_DEFAULT, "TEMPLATES_AUTO_RELOAD": OptimizedConfig.TEMPLATES_AUTO_RELOAD, "EXPLAIN_TEMPLATE_LOADING": OptimizedConfig.EXPLAIN_TEMPLATE_LOADING, "SESSION_COOKIE_SECURE": OptimizedConfig.SESSION_COOKIE_SECURE, "SESSION_COOKIE_HTTPONLY": OptimizedConfig.SESSION_COOKIE_HTTPONLY, "SESSION_COOKIE_SAMESITE": OptimizedConfig.SESSION_COOKIE_SAMESITE, "MAX_CONTENT_LENGTH": OptimizedConfig.MAX_CONTENT_LENGTH, "JSON_SORT_KEYS": OptimizedConfig.JSON_SORT_KEYS, "JSONIFY_PRETTYPRINT_REGULAR": OptimizedConfig.JSONIFY_PRETTYPRINT_REGULAR }) app.jinja_env.globals.update({ 'optimized_mode': True, 'use_minified_assets': OptimizedConfig.USE_MINIFIED_ASSETS, 'disable_animations': OptimizedConfig.DISABLE_ANIMATIONS, 'limit_glassmorphism': OptimizedConfig.LIMIT_GLASSMORPHISM, 'base_template': 'base-optimized.html' }) @app.after_request def add_optimized_cache_headers(response): """Fügt optimierte Cache-Header hinzu""" if request.endpoint == 'static' or '/static/' in request.path: response.headers['Cache-Control'] = 'public, max-age=31536000' response.headers['Vary'] = 'Accept-Encoding' return response else: app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False app.jinja_env.globals.update({ 'optimized_mode': False, 'use_minified_assets': False, 'disable_animations': False, 'limit_glassmorphism': False, 'base_template': 'base.html' }) # Session-Konfiguration app.config["PERMANENT_SESSION_LIFETIME"] = SESSION_LIFETIME app.config["WTF_CSRF_ENABLED"] = True # CSRF-Schutz initialisieren csrf = CSRFProtect(app) @app.errorhandler(CSRFError) def csrf_error(error): """Behandelt CSRF-Fehler""" app_logger.warning(f"CSRF-Fehler: {error.description}") return jsonify({"error": "CSRF-Token ungültig oder fehlt"}), 400 # 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.user_loader def load_user(user_id): """Lädt einen Benutzer für Flask-Login""" try: with get_db_session() as db_session: user = db_session.query(User).filter_by(id=int(user_id)).first() if user: db_session.expunge(user) return user except Exception as e: app_logger.error(f"Fehler beim Laden des Benutzers {user_id}: {str(e)}") return None # ===== BLUEPRINTS REGISTRIEREN ===== app.register_blueprint(auth_blueprint) app.register_blueprint(user_blueprint) app.register_blueprint(admin_blueprint) app.register_blueprint(admin_api_blueprint) app.register_blueprint(guest_blueprint) app.register_blueprint(calendar_blueprint) app.register_blueprint(users_blueprint) app.register_blueprint(printers_blueprint) app.register_blueprint(jobs_blueprint) app.register_blueprint(kiosk_blueprint) app.register_blueprint(uploads_blueprint) app.register_blueprint(sessions_blueprint) # ===== HILFSSYSTEME INITIALISIEREN ===== init_security(app) init_permission_helpers(app) # ===== KONTEXT-PROZESSOREN ===== @app.context_processor def inject_now(): """Injiziert die aktuelle Zeit in alle Templates""" return {'now': datetime.now} @app.template_filter('format_datetime') def format_datetime_filter(value, format='%d.%m.%Y %H:%M'): """Template-Filter für Datums-Formatierung""" if value is None: return "" if isinstance(value, str): try: value = datetime.fromisoformat(value) except: return value return value.strftime(format) @app.template_global() def is_optimized_mode(): """Prüft ob der optimierte Modus aktiv ist""" return USE_OPTIMIZED_CONFIG # ===== REQUEST HOOKS ===== @app.before_request def log_request_info(): """Loggt Request-Informationen""" if request.endpoint != 'static': app_logger.debug(f"Request: {request.method} {request.path}") @app.after_request def log_response_info(response): """Loggt Response-Informationen""" if request.endpoint != 'static': app_logger.debug(f"Response: {response.status_code}") return response @app.before_request def check_session_activity(): """Prüft Session-Aktivität und meldet inaktive Benutzer ab""" if current_user.is_authenticated: last_activity = session.get('last_activity') if last_activity: try: last_activity_time = datetime.fromisoformat(last_activity) if (datetime.now() - last_activity_time).total_seconds() > SESSION_LIFETIME.total_seconds(): app_logger.info(f"Session abgelaufen für Benutzer {current_user.id}") logout_user() return redirect(url_for('auth.login')) except: pass # Aktivität aktualisieren session['last_activity'] = datetime.now().isoformat() session.permanent = True # ===== HAUPTROUTEN ===== @app.route("/") def index(): """Startseite - leitet zur Login-Seite oder zum Dashboard""" if current_user.is_authenticated: return redirect(url_for("dashboard")) return redirect(url_for("auth.login")) @app.route("/dashboard") @login_required def dashboard(): """Haupt-Dashboard""" return render_template("dashboard.html") @app.route("/admin") @login_required def admin(): """Admin-Dashboard""" if not current_user.is_admin: abort(403) return redirect(url_for("admin.dashboard")) # Statische Seiten @app.route("/privacy") def privacy(): """Datenschutzerklärung""" return render_template("privacy.html") @app.route("/terms") def terms(): """Nutzungsbedingungen""" return render_template("terms.html") @app.route("/imprint") def imprint(): """Impressum""" return render_template("imprint.html") @app.route("/legal") def legal(): """Rechtliche Hinweise - Weiterleitung zum Impressum""" return redirect(url_for("imprint")) # ===== FEHLERBEHANDLUNG ===== @app.errorhandler(404) def not_found_error(error): """404-Fehlerseite""" return render_template('errors/404.html'), 404 @app.errorhandler(403) def forbidden_error(error): """403-Fehlerseite""" return render_template('errors/403.html'), 403 @app.errorhandler(500) def internal_error(error): """500-Fehlerseite""" app_logger.error(f"Interner Serverfehler: {str(error)}") return render_template('errors/500.html'), 500 # ===== HAUPTFUNKTION ===== def main(): """Hauptfunktion zum Starten der Anwendung""" try: # Datenbank initialisieren init_database() # Initial-Admin erstellen falls nicht vorhanden create_initial_admin() # Queue Manager starten start_queue_manager() # Job Scheduler starten scheduler = get_job_scheduler() if scheduler: scheduler.start() # SSL-Kontext ssl_context = None try: from utils.ssl_config import get_ssl_context ssl_context = get_ssl_context() except ImportError: app_logger.warning("SSL-Konfiguration nicht verfügbar") # Server starten host = os.getenv('FLASK_HOST', '0.0.0.0') port = int(os.getenv('FLASK_PORT', 5000)) app_logger.info(f"[START] Server startet auf {host}:{port}") if ssl_context: app.run(host=host, port=port, ssl_context=ssl_context, threaded=True) else: app.run(host=host, port=port, threaded=True) except Exception as e: app_logger.error(f"Fehler beim Starten der Anwendung: {str(e)}") raise finally: # Cleanup try: stop_queue_manager() if scheduler: scheduler.shutdown() cleanup_rate_limiter() except: pass if __name__ == "__main__": main()