Files
Projektarbeit-MYP/backend/app.py

1716 lines
63 KiB
Python

"""
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
import pickle
import hashlib
from datetime import datetime, timedelta
from flask import Flask, render_template, request, jsonify, redirect, url_for, session, abort, send_from_directory
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
import uuid
# ===== MINIMALE SESSION-DATENKLASSE =====
class MinimalSessionInterface:
"""Minimale Session-Implementierung zur Reduzierung der Cookie-Größe"""
@staticmethod
def reduce_session_data():
"""Reduziert Session-Daten auf absolutes Minimum"""
from flask import session
# Nur kritische Daten behalten
essential_keys = ['_user_id', '_id', '_fresh', 'csrf_token']
# Alle nicht-essentiellen Keys entfernen
keys_to_remove = []
for key in session.keys():
if key not in essential_keys:
keys_to_remove.append(key)
for key in keys_to_remove:
session.pop(key, None)
@staticmethod
def get_minimal_session_data():
"""Gibt nur minimale Session-Daten zurück"""
from flask import session
return {
'user_id': session.get('_user_id'),
'session_id': session.get('_id'),
'is_fresh': session.get('_fresh', False)
}
# Globale Session-Interface-Instanz
minimal_session = MinimalSessionInterface()
# ===== SESSION-OPTIMIERUNG =====
class SessionManager:
"""Optimierter Session-Manager für große Session-Daten"""
def __init__(self, app=None):
self.app = app
self.session_storage_path = None
def init_app(self, app):
"""Initialisiert den Session-Manager für die Flask-App"""
self.app = app
self.session_storage_path = os.path.join(
app.instance_path, 'sessions'
)
os.makedirs(self.session_storage_path, exist_ok=True)
def store_large_session_data(self, key, data):
"""Speichert große Session-Daten im Dateisystem"""
if not self.session_storage_path:
return False
try:
session_id = session.get('session_id')
if not session_id:
session_id = hashlib.md5(
f"{request.remote_addr}_{datetime.now().isoformat()}".encode()
).hexdigest()
session['session_id'] = session_id
file_path = os.path.join(
self.session_storage_path,
f"{session_id}_{key}.pkl"
)
with open(file_path, 'wb') as f:
pickle.dump(data, f)
# Nur Referenz in Session speichern
session[f"{key}_ref"] = True
return True
except Exception as e:
logging.error(f"Fehler beim Speichern der Session-Daten: {e}")
return False
def load_large_session_data(self, key):
"""Lädt große Session-Daten aus dem Dateisystem"""
if not self.session_storage_path:
return None
try:
session_id = session.get('session_id')
if not session_id or not session.get(f"{key}_ref"):
return None
file_path = os.path.join(
self.session_storage_path,
f"{session_id}_{key}.pkl"
)
if not os.path.exists(file_path):
return None
with open(file_path, 'rb') as f:
return pickle.load(f)
except Exception as e:
logging.error(f"Fehler beim Laden der Session-Daten: {e}")
return None
def cleanup_expired_sessions(self):
"""Bereinigt abgelaufene Session-Dateien"""
if not self.session_storage_path:
return
try:
current_time = datetime.now()
for filename in os.listdir(self.session_storage_path):
file_path = os.path.join(self.session_storage_path, filename)
file_time = datetime.fromtimestamp(os.path.getmtime(file_path))
# Lösche Dateien älter als 24 Stunden
if current_time - file_time > timedelta(hours=24):
os.remove(file_path)
except Exception as e:
logging.error(f"Fehler bei Session-Cleanup: {e}")
# Globaler Session-Manager
session_manager = SessionManager()
# ===== PRODUCTION-KONFIGURATION =====
class ProductionConfig:
"""Production-Konfiguration für Mercedes-Benz TBA Marienfelde Air-Gapped Environment
Enthält alle Performance-Optimierungen, die vorher in OptimizedConfig waren,
plus Production-spezifische Sicherheits- und Compliance-Einstellungen.
"""
# Umgebung
ENV = 'production'
DEBUG = False
TESTING = False
# Performance-Optimierungen (ehemals OptimizedConfig)
OPTIMIZED_MODE = True
USE_MINIFIED_ASSETS = True
DISABLE_ANIMATIONS = True
LIMIT_GLASSMORPHISM = True
# Sicherheit (SECRET_KEY wird später gesetzt)
WTF_CSRF_ENABLED = True
WTF_CSRF_TIME_LIMIT = 3600 # 1 Stunde
# Session-Sicherheit
SESSION_COOKIE_SECURE = True # HTTPS erforderlich
SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_SAMESITE = 'Strict'
# PERMANENT_SESSION_LIFETIME wird später gesetzt
# Performance-Optimierungen
SEND_FILE_MAX_AGE_DEFAULT = 31536000 # 1 Jahr Cache für statische Dateien
TEMPLATES_AUTO_RELOAD = False
EXPLAIN_TEMPLATE_LOADING = False
# Upload-Beschränkungen
MAX_CONTENT_LENGTH = 100 * 1024 * 1024 # 100MB für Production
# JSON-Optimierungen
JSON_SORT_KEYS = False
JSONIFY_PRETTYPRINT_REGULAR = False
JSONIFY_MIMETYPE = 'application/json'
# Logging-Level
LOG_LEVEL = 'INFO'
# Air-Gapped Einstellungen
OFFLINE_MODE = True
DISABLE_EXTERNAL_APIS = True
USE_LOCAL_ASSETS_ONLY = True
# Datenbank-Performance
SQLALCHEMY_TRACK_MODIFICATIONS = False
SQLALCHEMY_POOL_RECYCLE = 3600
SQLALCHEMY_POOL_TIMEOUT = 20
SQLALCHEMY_ENGINE_OPTIONS = {
'pool_pre_ping': True,
'pool_recycle': 3600,
'echo': False
}
# Security Headers
SECURITY_HEADERS = {
'Strict-Transport-Security': 'max-age=31536000; includeSubDomains',
'X-Content-Type-Options': 'nosniff',
'X-Frame-Options': 'DENY',
'X-XSS-Protection': '1; mode=block',
'Referrer-Policy': 'strict-origin-when-cross-origin',
'Content-Security-Policy': "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self';"
}
# Mercedes-Benz Corporate Compliance
COMPANY_NAME = "Mercedes-Benz TBA Marienfelde"
ENVIRONMENT_NAME = "Production Air-Gapped"
COMPLIANCE_MODE = True
AUDIT_LOGGING = True
# Monitoring
ENABLE_METRICS = True
ENABLE_HEALTH_CHECKS = True
ENABLE_PERFORMANCE_MONITORING = True
# ===== DEVELOPMENT-KONFIGURATION =====
class DevelopmentConfig:
"""Development-Konfiguration für lokale Entwicklung
Konsolidiert alle Nicht-Production-Modi (development, default, fallback).
Optimiert für Entwicklerfreundlichkeit und Debugging.
"""
# Umgebung
ENV = 'development'
DEBUG = True
TESTING = False
# Performance (moderat optimiert für bessere Entwicklererfahrung)
OPTIMIZED_MODE = False
USE_MINIFIED_ASSETS = False
DISABLE_ANIMATIONS = False
LIMIT_GLASSMORPHISM = False
# Sicherheit (relaxed für Development)
WTF_CSRF_ENABLED = True
WTF_CSRF_TIME_LIMIT = 7200 # 2 Stunden für längere Dev-Sessions
# Session-Sicherheit (relaxed)
SESSION_COOKIE_SECURE = False # HTTP OK für Development
SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_SAMESITE = 'Lax'
# Performance (Developer-freundlich)
SEND_FILE_MAX_AGE_DEFAULT = 1 # Keine Cache für Development
TEMPLATES_AUTO_RELOAD = True
EXPLAIN_TEMPLATE_LOADING = True
# Upload-Beschränkungen (generous für Testing)
MAX_CONTENT_LENGTH = 50 * 1024 * 1024 # 50MB für Development
# JSON (Pretty für Debugging)
JSON_SORT_KEYS = True
JSONIFY_PRETTYPRINT_REGULAR = True
JSONIFY_MIMETYPE = 'application/json'
# Logging-Level
LOG_LEVEL = 'DEBUG'
# Entwicklungs-Einstellungen
OFFLINE_MODE = False
DISABLE_EXTERNAL_APIS = False
USE_LOCAL_ASSETS_ONLY = False
# Datenbank (Developer-freundlich)
SQLALCHEMY_TRACK_MODIFICATIONS = True # Für Debugging
SQLALCHEMY_POOL_RECYCLE = 1800 # 30 Minuten
SQLALCHEMY_POOL_TIMEOUT = 30
SQLALCHEMY_ENGINE_OPTIONS = {
'pool_pre_ping': True,
'pool_recycle': 1800,
'echo': True # SQL-Logging für Development
}
# Development-spezifische Einstellungen
COMPANY_NAME = "MYP Development Environment"
ENVIRONMENT_NAME = "Development/Testing"
COMPLIANCE_MODE = False
AUDIT_LOGGING = False
# Monitoring (minimal für Development)
ENABLE_METRICS = False
ENABLE_HEALTH_CHECKS = False
ENABLE_PERFORMANCE_MONITORING = 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 detect_production_environment():
"""Erkennt ob das System in der Production-Umgebung läuft"""
# Command-line Argument
if '--production' in sys.argv:
return True
# Umgebungsvariable
env = os.getenv('FLASK_ENV', '').lower()
if env in ['production', 'prod']:
return True
# Spezifische Production-Variablen
if os.getenv('USE_PRODUCTION_CONFIG', '').lower() in ['true', '1', 'yes']:
return True
# Mercedes-Benz spezifische Erkennung
if os.getenv('MERCEDES_ENVIRONMENT', '').lower() == 'production':
return True
# Air-Gapped Environment Detection
if os.getenv('AIR_GAPPED_MODE', '').lower() in ['true', '1', 'yes']:
return True
# Hostname-basierte Erkennung
try:
import socket
hostname = socket.gethostname().lower()
if any(keyword in hostname for keyword in ['prod', 'production', 'live', 'mercedes', 'tba']):
return True
except:
pass
# Automatische Production-Erkennung für Raspberry Pi oder Low-Memory-Systeme
if detect_raspberry_pi():
return True
try:
import psutil
memory_gb = psutil.virtual_memory().total / (1024**3)
if memory_gb < 2.0: # Unter 2GB RAM = wahrscheinlich Production-Umgebung
return True
except:
pass
return False
def get_environment_type():
"""Bestimmt den Umgebungstyp - nur noch production oder development"""
if detect_production_environment():
return 'production'
else:
return 'development'
# ===== GLOBALE KONFIGURATIONSVARIABLEN =====
# Diese werden später nach den Funktionsdefinitionen gesetzt
# Windows-spezifische Fixes
if os.name == 'nt':
try:
from utils.core_system 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.job_queue_system import queue_manager, start_queue_manager, stop_queue_manager
from utils.utilities_collection import SECRET_KEY, SESSION_LIFETIME
# Blueprints importieren
from blueprints.auth import auth_blueprint
from blueprints.admin_unified import admin_blueprint, admin_api_blueprint
from blueprints.guest import guest_blueprint
from blueprints.calendar import calendar_blueprint
from blueprints.user_management import users_blueprint # Konsolidierte User-Verwaltung
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
from blueprints.tapo_control import tapo_blueprint # Tapo-Steckdosen-Steuerung
from blueprints.api import api_blueprint # API-Endpunkte mit Session-Management
from blueprints.legal_pages import legal_bp # Rechtliche Seiten (Impressum, Datenschutz, etc.)
# Import der Sicherheits- und Hilfssysteme
from utils.security_suite import init_security
# Logging initialisieren
setup_logging()
log_startup_info()
# Logger für verschiedene Komponenten
app_logger = get_logger("app")
# ===== FLASK-APP INITIALISIERUNG =====
app = Flask(__name__)
# Konfiguration anwenden basierend auf Environment wird später gemacht
# (nach Definition der apply_*_config Funktionen)
# Session-Manager initialisieren
session_manager.init_app(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'
# CSRF-Schutz initialisieren
csrf = CSRFProtect(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()
def apply_production_config(app):
"""Wendet die Production-Konfiguration auf die Flask-App an"""
app_logger.info("[PRODUCTION] Aktiviere Production-Konfiguration für Mercedes-Benz TBA")
# Dynamische Werte setzen
from utils.utilities_collection import SECRET_KEY, SESSION_LIFETIME
ProductionConfig.SECRET_KEY = os.environ.get('SECRET_KEY') or SECRET_KEY
ProductionConfig.PERMANENT_SESSION_LIFETIME = SESSION_LIFETIME
# Flask-Basis-Konfiguration
app.config.update({
"ENV": ProductionConfig.ENV,
"DEBUG": ProductionConfig.DEBUG,
"TESTING": ProductionConfig.TESTING,
"SECRET_KEY": ProductionConfig.SECRET_KEY,
"WTF_CSRF_ENABLED": ProductionConfig.WTF_CSRF_ENABLED,
"WTF_CSRF_TIME_LIMIT": ProductionConfig.WTF_CSRF_TIME_LIMIT,
"SESSION_COOKIE_SECURE": ProductionConfig.SESSION_COOKIE_SECURE,
"SESSION_COOKIE_HTTPONLY": ProductionConfig.SESSION_COOKIE_HTTPONLY,
"SESSION_COOKIE_SAMESITE": ProductionConfig.SESSION_COOKIE_SAMESITE,
"PERMANENT_SESSION_LIFETIME": ProductionConfig.PERMANENT_SESSION_LIFETIME,
"SEND_FILE_MAX_AGE_DEFAULT": ProductionConfig.SEND_FILE_MAX_AGE_DEFAULT,
"TEMPLATES_AUTO_RELOAD": ProductionConfig.TEMPLATES_AUTO_RELOAD,
"EXPLAIN_TEMPLATE_LOADING": ProductionConfig.EXPLAIN_TEMPLATE_LOADING,
"MAX_CONTENT_LENGTH": ProductionConfig.MAX_CONTENT_LENGTH,
"JSON_SORT_KEYS": ProductionConfig.JSON_SORT_KEYS,
"JSONIFY_PRETTYPRINT_REGULAR": ProductionConfig.JSONIFY_PRETTYPRINT_REGULAR,
"JSONIFY_MIMETYPE": ProductionConfig.JSONIFY_MIMETYPE,
"SQLALCHEMY_TRACK_MODIFICATIONS": ProductionConfig.SQLALCHEMY_TRACK_MODIFICATIONS,
"SQLALCHEMY_ENGINE_OPTIONS": ProductionConfig.SQLALCHEMY_ENGINE_OPTIONS
})
# Jinja2-Umgebung für Production
app.jinja_env.globals.update({
'production_mode': True,
'development_mode': False,
'optimized_mode': ProductionConfig.OPTIMIZED_MODE,
'use_minified_assets': ProductionConfig.USE_MINIFIED_ASSETS,
'disable_animations': ProductionConfig.DISABLE_ANIMATIONS,
'limit_glassmorphism': ProductionConfig.LIMIT_GLASSMORPHISM,
'environment_name': ProductionConfig.ENVIRONMENT_NAME,
'company_name': ProductionConfig.COMPANY_NAME,
'compliance_mode': ProductionConfig.COMPLIANCE_MODE,
'offline_mode': ProductionConfig.OFFLINE_MODE,
'use_local_assets_only': ProductionConfig.USE_LOCAL_ASSETS_ONLY,
'base_template': 'base-production.html'
})
app_logger.info(f"[PRODUCTION] ✅ {ProductionConfig.COMPANY_NAME} Konfiguration aktiviert")
app_logger.info(f"[PRODUCTION] ✅ Environment: {ProductionConfig.ENVIRONMENT_NAME}")
app_logger.info(f"[PRODUCTION] ✅ Air-Gapped Mode: {ProductionConfig.OFFLINE_MODE}")
app_logger.info(f"[PRODUCTION] ✅ Compliance Mode: {ProductionConfig.COMPLIANCE_MODE}")
app_logger.info(f"[PRODUCTION] ✅ Performance Optimized: {ProductionConfig.OPTIMIZED_MODE}")
def apply_development_config(app):
"""Wendet die Development-Konfiguration auf die Flask-App an"""
app_logger.info("[DEVELOPMENT] Aktiviere Development-Konfiguration")
# Dynamische Werte setzen
from utils.utilities_collection import SECRET_KEY, SESSION_LIFETIME
DevelopmentConfig.SECRET_KEY = os.environ.get('SECRET_KEY') or SECRET_KEY
DevelopmentConfig.PERMANENT_SESSION_LIFETIME = SESSION_LIFETIME
# Flask-Basis-Konfiguration
app.config.update({
"ENV": DevelopmentConfig.ENV,
"DEBUG": DevelopmentConfig.DEBUG,
"TESTING": DevelopmentConfig.TESTING,
"SECRET_KEY": DevelopmentConfig.SECRET_KEY,
"WTF_CSRF_ENABLED": DevelopmentConfig.WTF_CSRF_ENABLED,
"WTF_CSRF_TIME_LIMIT": DevelopmentConfig.WTF_CSRF_TIME_LIMIT,
"SESSION_COOKIE_SECURE": DevelopmentConfig.SESSION_COOKIE_SECURE,
"SESSION_COOKIE_HTTPONLY": DevelopmentConfig.SESSION_COOKIE_HTTPONLY,
"SESSION_COOKIE_SAMESITE": DevelopmentConfig.SESSION_COOKIE_SAMESITE,
"PERMANENT_SESSION_LIFETIME": DevelopmentConfig.PERMANENT_SESSION_LIFETIME,
"SEND_FILE_MAX_AGE_DEFAULT": DevelopmentConfig.SEND_FILE_MAX_AGE_DEFAULT,
"TEMPLATES_AUTO_RELOAD": DevelopmentConfig.TEMPLATES_AUTO_RELOAD,
"EXPLAIN_TEMPLATE_LOADING": DevelopmentConfig.EXPLAIN_TEMPLATE_LOADING,
"MAX_CONTENT_LENGTH": DevelopmentConfig.MAX_CONTENT_LENGTH,
"JSON_SORT_KEYS": DevelopmentConfig.JSON_SORT_KEYS,
"JSONIFY_PRETTYPRINT_REGULAR": DevelopmentConfig.JSONIFY_PRETTYPRINT_REGULAR,
"JSONIFY_MIMETYPE": DevelopmentConfig.JSONIFY_MIMETYPE,
"SQLALCHEMY_TRACK_MODIFICATIONS": DevelopmentConfig.SQLALCHEMY_TRACK_MODIFICATIONS,
"SQLALCHEMY_ENGINE_OPTIONS": DevelopmentConfig.SQLALCHEMY_ENGINE_OPTIONS
})
# Jinja2-Umgebung für Development
app.jinja_env.globals.update({
'production_mode': False,
'development_mode': True,
'optimized_mode': DevelopmentConfig.OPTIMIZED_MODE,
'use_minified_assets': DevelopmentConfig.USE_MINIFIED_ASSETS,
'disable_animations': DevelopmentConfig.DISABLE_ANIMATIONS,
'limit_glassmorphism': DevelopmentConfig.LIMIT_GLASSMORPHISM,
'environment_name': DevelopmentConfig.ENVIRONMENT_NAME,
'company_name': DevelopmentConfig.COMPANY_NAME,
'compliance_mode': DevelopmentConfig.COMPLIANCE_MODE,
'offline_mode': DevelopmentConfig.OFFLINE_MODE,
'use_local_assets_only': DevelopmentConfig.USE_LOCAL_ASSETS_ONLY,
'base_template': 'base.html'
})
app_logger.info(f"[DEVELOPMENT] ✅ {DevelopmentConfig.COMPANY_NAME} Konfiguration aktiviert")
app_logger.info(f"[DEVELOPMENT] ✅ Environment: {DevelopmentConfig.ENVIRONMENT_NAME}")
app_logger.info(f"[DEVELOPMENT] ✅ Debug Mode: {DevelopmentConfig.DEBUG}")
app_logger.info(f"[DEVELOPMENT] ✅ SQL Echo: {DevelopmentConfig.SQLALCHEMY_ENGINE_OPTIONS.get('echo', False)}")
# ===== KONFIGURATION ANWENDEN =====
# Jetzt können wir die Funktionen aufrufen, da sie definiert sind
ENVIRONMENT_TYPE = get_environment_type()
USE_PRODUCTION_CONFIG = detect_production_environment()
OFFLINE_MODE = USE_PRODUCTION_CONFIG
app_logger.info(f"[CONFIG] Erkannte Umgebung: {ENVIRONMENT_TYPE}")
app_logger.info(f"[CONFIG] Production-Modus: {USE_PRODUCTION_CONFIG}")
if USE_PRODUCTION_CONFIG:
apply_production_config(app)
else:
# Development-Konfiguration (konsolidiert default/fallback)
app_logger.info("[CONFIG] Verwende Development-Konfiguration")
apply_development_config(app)
# Umgebungs-spezifische Einstellungen
if OFFLINE_MODE:
app_logger.info("[CONFIG] ✅ Air-Gapped/Offline-Modus aktiviert")
app.config['DISABLE_EXTERNAL_REQUESTS'] = True
# Session-Konfiguration
app.config["PERMANENT_SESSION_LIFETIME"] = SESSION_LIFETIME
app.config["WTF_CSRF_ENABLED"] = True
app.config["WTF_CSRF_TIME_LIMIT"] = 3600 # 1 Stunde
app.config["WTF_CSRF_SSL_STRICT"] = False # Für Development
app.config["WTF_CSRF_CHECK_DEFAULT"] = True
app.config["WTF_CSRF_METHODS"] = ['POST', 'PUT', 'PATCH', 'DELETE']
# CSRF-Schutz initialisieren
csrf = CSRFProtect(app)
# CSRF-Token in Session verfügbar machen
@app.before_request
def csrf_protect():
"""Stellt sicher, dass CSRF-Token verfügbar ist"""
if request.endpoint and request.endpoint.startswith('static'):
return
# Guest-API-Endpunkte von CSRF befreien
if request.path.startswith('/api/guest/'):
return # Kein CSRF für Guest-APIs
# Tapo-Hardware-Steuerung von CSRF befreien (Geräte verwenden kein CSRF)
if request.path.startswith('/tapo/'):
return # Kein CSRF für Tapo-Hardware-Steuerung
# Drucker-API-Endpunkte mit Tapo-Integration von CSRF befreien
tapo_api_paths = [
'/api/printers/control/', # Stromsteuerung über PyP100
'/api/printers/tapo/', # Alle Tapo-spezifischen APIs
'/api/printers/force-refresh', # Force-Refresh (verwendet Tapo-Status)
'/api/printers/status', # Status-API (verwendet Tapo-Status)
'/api/admin/printers/', # Admin-Printer-APIs (Toggle-Funktion)
]
for path in tapo_api_paths:
if request.path.startswith(path):
return # Kein CSRF für Tapo-Hardware-APIs
try:
from flask_wtf.csrf import generate_csrf
token = generate_csrf()
session['_csrf_token'] = token
except Exception as e:
app_logger.warning(f"CSRF-Token konnte nicht in Session gesetzt werden: {str(e)}")
# Template-Funktionen für CSRF-Token
@app.template_global()
def csrf_token():
"""CSRF-Token für Templates verfügbar machen."""
try:
from flask_wtf.csrf import generate_csrf
token = generate_csrf()
app_logger.debug(f"CSRF-Token generiert: {token[:10]}...")
return token
except Exception as e:
app_logger.error(f"CSRF-Token konnte nicht generiert werden: {str(e)}")
# Fallback: Einfaches Token basierend auf Session
import secrets
fallback_token = secrets.token_urlsafe(32)
app_logger.warning(f"Verwende Fallback-Token: {fallback_token[:10]}...")
return fallback_token
@app.errorhandler(CSRFError)
def csrf_error(error):
"""Behandelt CSRF-Fehler mit detaillierter Diagnose"""
# Guest-APIs sollten nie CSRF-Fehler haben
if request.path.startswith('/api/guest/'):
app_logger.warning(f"CSRF-Fehler bei Guest-API (sollte nicht passieren): {request.path}")
return jsonify({
"success": False,
"error": "Unerwarteter Sicherheitsfehler bei Guest-API"
}), 500
app_logger.error(f"CSRF-Fehler für {request.path}: {error.description}")
app_logger.error(f"Request Headers: {dict(request.headers)}")
app_logger.error(f"Request Form: {dict(request.form)}")
if request.path.startswith('/api/'):
# Für API-Anfragen: JSON-Response mit Hilfe
return jsonify({
"error": "CSRF-Token ungültig oder fehlt",
"description": str(error.description),
"help": "Fügen Sie ein gültiges CSRF-Token zu Ihrer Anfrage hinzu",
"csrf_token": csrf_token() # Neues Token für Retry
}), 400
else:
# Für normale Anfragen: Weiterleitung mit Flash-Message
from flask import flash, redirect
flash("Sicherheitsfehler: Anfrage wurde abgelehnt. Bitte versuchen Sie es erneut.", "error")
return redirect(request.url)
# 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."
# Session-Manager initialisieren
session_manager.init_app(app)
@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)
# Vereinheitlichte Admin-Blueprints registrieren
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) # Konsolidierte User-Verwaltung
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)
app.register_blueprint(tapo_blueprint) # Tapo-Steckdosen-Steuerung
app.register_blueprint(api_blueprint) # Einfache API-Endpunkte
app.register_blueprint(legal_bp) # Rechtliche Seiten (Impressum, Datenschutz, etc.)
# Energiemonitoring-Blueprints registrieren
from blueprints.energy_monitoring import energy_blueprint, energy_api_blueprint
app.register_blueprint(energy_blueprint) # Frontend-Routen für Energiemonitoring
app.register_blueprint(energy_api_blueprint) # API-Endpunkte für Energiedaten
# ===== HILFSSYSTEME INITIALISIEREN =====
init_security(app)
# ===== KONTEXT-PROZESSOREN =====
@app.context_processor
def inject_now():
"""Injiziert die aktuelle Zeit in alle Templates"""
return {'now': datetime.now}
@app.context_processor
def inject_current_route():
"""
Stellt current_route für alle Templates bereit.
Verhindert Template-Fehler wenn request.endpoint None ist (z.B. bei 404-Fehlern).
"""
current_route = getattr(request, 'endpoint', None) or ''
return {'current_route': current_route}
@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_PRODUCTION_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.after_request
def minimize_session_cookie(response):
"""Reduziert Session-Cookie automatisch nach jedem Request"""
if current_user.is_authenticated:
# Drastische Session-Cookie-Reduktion
minimal_session.reduce_session_data()
return response
@app.before_request
def check_session_activity():
"""Prüft Session-Aktivität und meldet inaktive Benutzer ab mit MINIMAL Cookie-Management"""
if current_user.is_authenticated:
from utils.utilities_collection import SESSION_LIFETIME
# DRASTISCHE Session-Reduktion - alle nicht-kritischen Daten entfernen
minimal_session.reduce_session_data()
# Session-Aktivität über externen Store (nicht in Cookie)
session_data = session_manager.load_large_session_data('activity') or {}
now = datetime.now()
# Aktivitätsprüfung über externen Store
last_activity = session_data.get('last_activity')
if last_activity:
try:
last_activity_time = datetime.fromisoformat(last_activity)
if (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 Exception as e:
app_logger.warning(f"Fehler beim Parsen der Session-Zeit: {e}")
# Aktivität NICHT in Session-Cookie speichern, sondern extern
session_data['last_activity'] = now.isoformat()
session_manager.store_large_session_data('activity', session_data)
# Session permanent ohne zusätzliche Daten
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("/csrf-test")
def csrf_test_page():
"""CSRF-Test-Seite für Diagnose und Debugging"""
return render_template("csrf_test.html")
@app.route("/api/csrf-test", methods=["POST"])
def csrf_test_api():
"""API-Endpunkt für CSRF-Tests"""
try:
# Test-Daten aus Request extrahieren
if request.is_json:
data = request.get_json()
test_data = data.get('test_data', 'Keine Daten')
else:
test_data = request.form.get('test_data', 'Keine Daten')
app_logger.info(f"CSRF-Test erfolgreich: {test_data}")
return jsonify({
"success": True,
"message": "CSRF-Test erfolgreich",
"data": test_data,
"timestamp": datetime.now().isoformat()
}), 200
except Exception as e:
app_logger.error(f"CSRF-Test Fehler: {str(e)}")
return jsonify({
"success": False,
"error": str(e)
}), 500
@app.route("/admin")
@login_required
def admin():
"""Admin-Dashboard"""
if not current_user.is_admin:
abort(403)
return redirect(url_for("admin.admin_dashboard"))
# ===== HAUPTSEITEN =====
@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("/jobs/new")
@login_required
def new_job_page():
"""Zeigt die Seite zum Erstellen neuer Druckaufträge an."""
return render_template("jobs.html")
@app.route("/stats")
@login_required
def stats_page():
"""Zeigt die Statistiken-Seite an"""
return render_template("stats.html", title="Statistiken")
# ===== API-ENDPUNKTE FÜR FRONTEND-KOMPATIBILITÄT =====
# Jobs-API wird über Blueprint gehandhabt - keine doppelten Routen hier
@app.route('/sw.js')
def service_worker():
"""Service Worker für PWA-Funktionalität"""
return send_from_directory('static', 'sw.js', mimetype='application/javascript')
@app.route("/api/jobs/<int:job_id>/start", methods=["POST"])
@login_required
def api_start_job(job_id):
"""API-Endpunkt für Job-Start - leitet an Jobs-Blueprint weiter"""
from blueprints.jobs import start_job
return start_job(job_id)
@app.route("/api/jobs/<int:job_id>/pause", methods=["POST"])
@login_required
def api_pause_job(job_id):
"""API-Endpunkt für Job-Pause - leitet an Jobs-Blueprint weiter"""
from blueprints.jobs import pause_job
return pause_job(job_id)
@app.route("/api/jobs/<int:job_id>/resume", methods=["POST"])
@login_required
def api_resume_job(job_id):
"""API-Endpunkt für Job-Resume - leitet an Jobs-Blueprint weiter"""
from blueprints.jobs import resume_job
return resume_job(job_id)
@app.route("/api/jobs/<int:job_id>/finish", methods=["POST"])
@login_required
def api_finish_job(job_id):
"""API-Endpunkt für Job-Finish - leitet an Jobs-Blueprint weiter"""
from blueprints.jobs import finish_job
return finish_job(job_id)
@app.route("/api/printers", methods=["GET"])
@login_required
def api_get_printers():
"""API-Endpunkt für Drucker-Liste mit konsistenter Response-Struktur
Query-Parameter:
- include_inactive: 'true' um auch inaktive Drucker anzuzeigen (default: 'false')
- show_all: 'true' um ALLE Drucker anzuzeigen, unabhängig vom Status (default: 'false')
"""
try:
from models import get_db_session, Printer
# Query-Parameter auslesen - Standardmäßig nur aktive TBA Marienfelde Drucker
include_inactive = request.args.get('include_inactive', 'false').lower() == 'true'
show_all = request.args.get('show_all', 'false').lower() == 'true'
db_session = get_db_session()
# Basis-Query - NUR aktive TBA Marienfelde Drucker (die korrekten 6)
query = db_session.query(Printer)
if show_all:
# Nur wenn explizit angefordert: ALLE Drucker zeigen
pass # Keine Filter
else:
# Standard: Nur aktive TBA Marienfelde Drucker mit korrekten Namen
correct_names = ['Drucker 1', 'Drucker 2', 'Drucker 3', 'Drucker 4', 'Drucker 5', 'Drucker 6']
query = query.filter(
Printer.location == 'TBA Marienfelde',
Printer.active == True,
Printer.name.in_(correct_names)
)
if not include_inactive:
# Zusätzlich: Keine offline/unreachable Drucker (außer wenn explizit gewünscht)
pass # Status-Filter wird später in der UI angewendet
printers = query.all()
printer_list = []
for printer in printers:
# Status-Bestimmung: Wenn nicht erreichbar, dann "offline"
status = printer.status or "offline"
# Zusätzliche Status-Informationen
is_reachable = status not in ["offline", "unreachable", "error"]
printer_dict = {
"id": printer.id,
"name": printer.name,
"model": printer.model or "Unbekanntes Modell",
"location": printer.location or "Unbekannter Standort",
"status": status,
"ip_address": printer.ip_address,
"plug_ip": printer.plug_ip,
"active": getattr(printer, 'active', True),
"is_reachable": is_reachable, # Zusätzliches Feld für UI
"is_selectable": True, # WICHTIG: Alle Drucker sind auswählbar!
"created_at": printer.created_at.isoformat() if printer.created_at else datetime.now().isoformat(),
"last_checked": printer.last_checked.isoformat() if printer.last_checked else None,
"display_status": f"{printer.name} - {status.title()}" # Für Dropdown-Anzeige
}
printer_list.append(printer_dict)
db_session.close()
app_logger.info(f"✅ API: {len(printer_list)} Drucker abgerufen (include_inactive={include_inactive})")
# Konsistente Response-Struktur wie erwartet
return jsonify({
"success": True,
"printers": printer_list,
"count": len(printer_list),
"message": "Drucker erfolgreich geladen",
"filters": {
"include_inactive": include_inactive,
"show_all": show_all
}
})
except Exception as e:
app_logger.error(f"❌ API-Fehler beim Abrufen der Drucker: {str(e)}")
return jsonify({
"success": False,
"error": "Fehler beim Laden der Drucker",
"details": str(e),
"printers": [],
"count": 0
}), 500
@app.route("/api/printers/status", methods=["GET"])
@login_required
def api_get_printer_status():
"""API-Endpunkt für Drucker-Status mit verbessertem Status-Management"""
try:
# Verwende den neuen TapoStatusManager
from utils.tapo_status_manager import tapo_status_manager
# Status für alle Drucker abrufen
status_list = tapo_status_manager.get_all_printer_status()
# Erweitere Status mit UI-freundlichen Informationen
for status in status_list:
# Status-Display-Informationen hinzufügen
plug_status = status.get("plug_status", "unknown")
if plug_status in tapo_status_manager.STATUS_DISPLAY:
status["status_display"] = tapo_status_manager.STATUS_DISPLAY[plug_status]
else:
status["status_display"] = {
"text": "Unbekannt",
"color": "gray",
"icon": "question"
}
app_logger.info(f"✅ API: Status für {len(status_list)} Drucker abgerufen")
# Erfolgreiche Response mit konsistenter Struktur
return jsonify({
"success": True,
"printers": status_list,
"count": len(status_list),
"timestamp": datetime.now().isoformat()
})
except Exception as e:
app_logger.error(f"❌ API-Fehler beim Abrufen des Drucker-Status: {str(e)}", exc_info=True)
# Fallback: Mindestens die Drucker-Grunddaten zurückgeben
try:
from models import get_db_session, Printer
db_session = get_db_session()
printers = db_session.query(Printer).all()
basic_status = []
for printer in printers:
basic_status.append({
"id": printer.id,
"name": printer.name,
"location": printer.location,
"model": printer.model,
"plug_status": "unreachable",
"plug_reachable": False,
"has_plug": bool(printer.plug_ip),
"error": "Status-Manager nicht verfügbar"
})
db_session.close()
return jsonify({
"success": False,
"error": "Eingeschränkte Status-Informationen",
"printers": basic_status,
"count": len(basic_status),
"timestamp": datetime.now().isoformat()
})
except:
return jsonify({
"success": False,
"error": "Fehler beim Laden des Drucker-Status",
"details": str(e),
"printers": [],
"count": 0
}), 500
@app.route("/api/health", methods=["GET"])
def api_health_check():
"""Einfacher Health-Check für Monitoring"""
try:
from models import get_db_session
# Datenbank-Verbindung testen
db_session = get_db_session()
db_session.execute("SELECT 1")
db_session.close()
return jsonify({
"status": "healthy",
"timestamp": datetime.now().isoformat(),
"version": "1.0.0",
"services": {
"database": "online",
"authentication": "online"
}
})
except Exception as e:
app_logger.error(f"❌ Health-Check fehlgeschlagen: {str(e)}")
return jsonify({
"status": "unhealthy",
"timestamp": datetime.now().isoformat(),
"error": str(e)
}), 503
@app.route("/api/version", methods=["GET"])
def api_version():
"""API-Version und System-Info"""
return jsonify({
"version": "1.0.0",
"name": "MYP - Manage Your Printer",
"description": "3D-Drucker-Verwaltung mit Smart-Steckdosen",
"build": datetime.now().strftime("%Y%m%d"),
"environment": get_environment_type()
})
@app.route("/api/stats", methods=['GET'])
@login_required
def api_stats():
"""
Allgemeine System-Statistiken API-Endpunkt.
Stellt grundlegende Statistiken über das System zur Verfügung.
"""
try:
from models import get_db_session, User, Printer, Job
db_session = get_db_session()
try:
# Grundlegende Counts
total_users = db_session.query(User).count()
total_printers = db_session.query(Printer).count()
total_jobs = db_session.query(Job).count()
# Aktive Jobs
active_jobs = db_session.query(Job).filter(
Job.status.in_(['pending', 'printing', 'paused'])
).count()
# Abgeschlossene Jobs heute
from datetime import date
today = date.today()
completed_today = db_session.query(Job).filter(
Job.status == 'completed',
Job.updated_at >= today
).count()
# Online-Drucker (aktive Drucker)
online_printers = db_session.query(Printer).filter(
Printer.active == True
).count()
finally:
db_session.close()
stats = {
'total_users': total_users,
'total_printers': total_printers,
'total_jobs': total_jobs,
'active_jobs': active_jobs,
'completed_today': completed_today,
'online_printers': online_printers,
'timestamp': datetime.now().isoformat()
}
app_logger.info(f"✅ API-Statistiken abgerufen von {current_user.username}")
return jsonify({
'success': True,
'stats': stats,
'message': 'Statistiken erfolgreich geladen'
})
except Exception as e:
app_logger.error(f"❌ Fehler beim Abrufen der API-Statistiken: {str(e)}")
return jsonify({
'success': False,
'error': 'Fehler beim Laden der Statistiken',
'details': str(e)
}), 500
# 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(400)
def bad_request_error(error):
"""400-Fehlerseite - Ungültige Anfrage"""
app_logger.warning(f"Bad Request (400): {request.url} - {str(error)}")
if request.is_json:
return jsonify({
"error": "Ungültige Anfrage",
"message": "Die Anfrage konnte nicht verarbeitet werden",
"status_code": 400
}), 400
return render_template('errors/400.html'), 400
@app.errorhandler(401)
def unauthorized_error(error):
"""401-Fehlerseite - Nicht autorisiert"""
app_logger.warning(f"Unauthorized (401): {request.url} - User: {getattr(current_user, 'username', 'Anonymous')}")
if request.is_json:
return jsonify({
"error": "Nicht autorisiert",
"message": "Anmeldung erforderlich",
"status_code": 401
}), 401
return redirect(url_for('auth.login'))
@app.errorhandler(403)
def forbidden_error(error):
"""403-Fehlerseite - Zugriff verweigert"""
app_logger.warning(f"Forbidden (403): {request.url} - User: {getattr(current_user, 'username', 'Anonymous')}")
if request.is_json:
return jsonify({
"error": "Zugriff verweigert",
"message": "Sie haben keine Berechtigung für diese Aktion",
"status_code": 403
}), 403
try:
return render_template('errors/403.html'), 403
except Exception as template_error:
# Fallback bei Template-Fehlern
app_logger.error(f"Template-Fehler in 403-Handler: {str(template_error)}")
return f"<h1>403 - Zugriff verweigert</h1><p>Sie haben keine Berechtigung für diese Aktion.</p>", 403
@app.errorhandler(404)
def not_found_error(error):
"""404-Fehlerseite - Seite nicht gefunden"""
app_logger.info(f"Not Found (404): {request.url}")
if request.is_json:
return jsonify({
"error": "Nicht gefunden",
"message": "Die angeforderte Ressource wurde nicht gefunden",
"status_code": 404
}), 404
try:
return render_template('errors/404.html'), 404
except Exception as template_error:
# Fallback bei Template-Fehlern
app_logger.error(f"Template-Fehler in 404-Handler: {str(template_error)}")
return f"<h1>404 - Nicht gefunden</h1><p>Die angeforderte Seite wurde nicht gefunden.</p>", 404
@app.errorhandler(405)
def method_not_allowed_error(error):
"""405-Fehlerseite - Methode nicht erlaubt"""
app_logger.warning(f"Method Not Allowed (405): {request.method} {request.url}")
if request.is_json:
return jsonify({
"error": "Methode nicht erlaubt",
"message": f"Die HTTP-Methode {request.method} ist für diese URL nicht erlaubt",
"status_code": 405
}), 405
return render_template('errors/405.html'), 405
@app.errorhandler(413)
def payload_too_large_error(error):
"""413-Fehlerseite - Datei zu groß"""
app_logger.warning(f"Payload Too Large (413): {request.url}")
if request.is_json:
return jsonify({
"error": "Datei zu groß",
"message": "Die hochgeladene Datei ist zu groß",
"status_code": 413
}), 413
return render_template('errors/413.html'), 413
@app.errorhandler(429)
def rate_limit_error(error):
"""429-Fehlerseite - Zu viele Anfragen"""
app_logger.warning(f"Rate Limit Exceeded (429): {request.url} - IP: {request.remote_addr}")
if request.is_json:
return jsonify({
"error": "Zu viele Anfragen",
"message": "Sie haben zu viele Anfragen gesendet. Bitte versuchen Sie es später erneut",
"status_code": 429
}), 429
return render_template('errors/429.html'), 429
@app.errorhandler(500)
def internal_error(error):
"""500-Fehlerseite - Interner Serverfehler"""
import traceback
error_id = datetime.now().strftime("%Y%m%d_%H%M%S")
# Detailliertes Logging für Debugging
app_logger.error(f"Internal Server Error (500) - ID: {error_id}")
app_logger.error(f"URL: {request.url}")
app_logger.error(f"Method: {request.method}")
app_logger.error(f"User: {getattr(current_user, 'username', 'Anonymous')}")
app_logger.error(f"Error: {str(error)}")
app_logger.error(f"Traceback: {traceback.format_exc()}")
if request.is_json:
return jsonify({
"error": "Interner Serverfehler",
"message": "Ein unerwarteter Fehler ist aufgetreten. Bitte versuchen Sie es später erneut",
"error_id": error_id,
"status_code": 500
}), 500
try:
return render_template('errors/500.html', error_id=error_id), 500
except Exception as template_error:
# Fallback bei Template-Fehlern
app_logger.error(f"Template-Fehler in 500-Handler: {str(template_error)}")
return f"<h1>500 - Interner Serverfehler</h1><p>Ein unerwarteter Fehler ist aufgetreten. Fehler-ID: {error_id}</p>", 500
@app.errorhandler(502)
def bad_gateway_error(error):
"""502-Fehlerseite - Bad Gateway"""
app_logger.error(f"Bad Gateway (502): {request.url}")
if request.is_json:
return jsonify({
"error": "Gateway-Fehler",
"message": "Der Server ist vorübergehend nicht verfügbar",
"status_code": 502
}), 502
return render_template('errors/502.html'), 502
@app.errorhandler(503)
def service_unavailable_error(error):
"""503-Fehlerseite - Service nicht verfügbar"""
app_logger.error(f"Service Unavailable (503): {request.url}")
if request.is_json:
return jsonify({
"error": "Service nicht verfügbar",
"message": "Der Service ist vorübergehend nicht verfügbar. Bitte versuchen Sie es später erneut",
"status_code": 503
}), 503
return render_template('errors/503.html'), 503
@app.errorhandler(505)
def http_version_not_supported_error(error):
"""505-Fehlerseite - HTTP-Version nicht unterstützt"""
app_logger.error(f"HTTP Version Not Supported (505): {request.url}")
if request.is_json:
return jsonify({
"error": "HTTP-Version nicht unterstützt",
"message": "Die verwendete HTTP-Version wird vom Server nicht unterstützt",
"status_code": 505
}), 505
return render_template('errors/505.html'), 505
# Allgemeiner Exception-Handler für unbehandelte Ausnahmen
@app.errorhandler(Exception)
def handle_exception(error):
"""Allgemeiner Handler für unbehandelte Ausnahmen"""
import traceback
error_id = datetime.now().strftime("%Y%m%d_%H%M%S")
# Detailliertes Logging
app_logger.error(f"Unhandled Exception - ID: {error_id}")
app_logger.error(f"URL: {request.url}")
app_logger.error(f"Method: {request.method}")
app_logger.error(f"User: {getattr(current_user, 'username', 'Anonymous')}")
app_logger.error(f"Exception Type: {type(error).__name__}")
app_logger.error(f"Exception: {str(error)}")
app_logger.error(f"Traceback: {traceback.format_exc()}")
# Für HTTP-Exceptions die ursprüngliche Behandlung verwenden
if hasattr(error, 'code'):
return error
# Für alle anderen Exceptions als 500 behandeln
if request.is_json:
return jsonify({
"error": "Unerwarteter Fehler",
"message": "Ein unerwarteter Fehler ist aufgetreten. Bitte versuchen Sie es später erneut",
"error_id": error_id,
"status_code": 500
}), 500
try:
return render_template('errors/500.html', error_id=error_id), 500
except Exception as template_error:
# Fallback bei Template-Fehlern
app_logger.error(f"Template-Fehler im Exception-Handler: {str(template_error)}")
return f"<h1>500 - Unerwarteter Fehler</h1><p>Ein unerwarteter Fehler ist aufgetreten. Fehler-ID: {error_id}</p>", 500
# ===== APP-FACTORY =====
def create_app(config_name=None):
"""
Flask-App-Factory für Tests und modulare Initialisierung
Args:
config_name: 'production', 'development' oder None (auto-detect)
Returns:
Flask: Konfigurierte Flask-App-Instanz
"""
# Bestimme Konfiguration
if config_name is None:
config_name = get_environment_type()
# Setze Environment-Variablen basierend auf config_name
if config_name == 'production':
os.environ['FLASK_ENV'] = 'production'
os.environ['USE_PRODUCTION_CONFIG'] = 'true'
else:
os.environ['FLASK_ENV'] = 'development'
os.environ['USE_PRODUCTION_CONFIG'] = 'false'
# Globale Variablen neu setzen
global ENVIRONMENT_TYPE, USE_PRODUCTION_CONFIG, OFFLINE_MODE
ENVIRONMENT_TYPE = config_name
USE_PRODUCTION_CONFIG = (config_name == 'production')
OFFLINE_MODE = USE_PRODUCTION_CONFIG
# App-Konfiguration anwenden
if USE_PRODUCTION_CONFIG:
apply_production_config(app)
app_logger.info(f"[FACTORY] ✅ Production-Konfiguration angewendet")
else:
apply_development_config(app)
app_logger.info(f"[FACTORY] ✅ Development-Konfiguration angewendet")
# Session-Manager initialisieren
session_manager.init_app(app)
# Sicherheitssuite initialisieren
try:
init_security(app)
app_logger.info("[FACTORY] ✅ Sicherheitssuite initialisiert")
except Exception as e:
app_logger.warning(f"[FACTORY] ⚠️ Sicherheitssuite-Fehler: {e}")
app_logger.info(f"[FACTORY] 🏭 Flask-App erstellt ({config_name})")
return app
# ===== HAUPTFUNKTION =====
def main():
"""Hauptfunktion zum Starten der Anwendung"""
try:
# Umgebungsinfo loggen
app_logger.info(f"[STARTUP] 🚀 Starte MYP {ENVIRONMENT_TYPE.upper()}-Umgebung")
app_logger.info(f"[STARTUP] 🏢 {getattr(ProductionConfig, 'COMPANY_NAME', 'Mercedes-Benz TBA Marienfelde')}")
app_logger.info(f"[STARTUP] 🔒 Air-Gapped: {OFFLINE_MODE or getattr(ProductionConfig, 'OFFLINE_MODE', False)}")
# Production-spezifische Initialisierung
if USE_PRODUCTION_CONFIG:
app_logger.info("[PRODUCTION] Initialisiere Production-Systeme...")
# Performance-Monitoring aktivieren
if getattr(ProductionConfig, 'ENABLE_PERFORMANCE_MONITORING', False):
try:
from utils.monitoring_analytics import performance_tracker
# Performance monitoring initialized via global instance
app_logger.info("[PRODUCTION] ✅ Performance-Monitoring aktiviert")
except ImportError:
app_logger.warning("[PRODUCTION] ⚠️ Performance-Monitoring nicht verfügbar")
# Health-Checks aktivieren
if getattr(ProductionConfig, 'ENABLE_HEALTH_CHECKS', False):
try:
from utils.monitoring_analytics import get_health_check
# Simple health check initialization
app_logger.info("[PRODUCTION] ✅ Health-Checks aktiviert")
except ImportError:
app_logger.warning("[PRODUCTION] ⚠️ Health-Checks nicht verfügbar")
# Audit-Logging aktivieren
if getattr(ProductionConfig, 'AUDIT_LOGGING', False):
try:
from utils.audit_logger import init_audit_logging
init_audit_logging(app)
app_logger.info("[PRODUCTION] ✅ Audit-Logging aktiviert")
except ImportError:
app_logger.warning("[PRODUCTION] ⚠️ Audit-Logging nicht verfügbar")
# Datenbank initialisieren
app_logger.info("[STARTUP] Initialisiere Datenbank...")
init_database()
app_logger.info("[STARTUP] ✅ Datenbank initialisiert")
# Initial-Admin erstellen falls nicht vorhanden
app_logger.info("[STARTUP] Prüfe Initial-Admin...")
create_initial_admin()
app_logger.info("[STARTUP] ✅ Admin-Benutzer geprüft")
# Statische Drucker für TBA Marienfelde erstellen/aktualisieren
app_logger.info("[STARTUP] Initialisiere statische Drucker...")
from models import create_initial_printers
success = create_initial_printers()
if success:
app_logger.info("[STARTUP] ✅ Statische Drucker konfiguriert")
else:
app_logger.warning("[STARTUP] ⚠️ Fehler bei Drucker-Initialisierung")
# Queue Manager starten
app_logger.info("[STARTUP] Starte Queue Manager...")
start_queue_manager()
app_logger.info("[STARTUP] ✅ Queue Manager gestartet")
# Job Scheduler starten
app_logger.info("[STARTUP] Starte Job Scheduler...")
scheduler = get_job_scheduler()
if scheduler:
scheduler.start()
app_logger.info("[STARTUP] ✅ Job Scheduler gestartet")
else:
app_logger.warning("[STARTUP] ⚠️ Job Scheduler nicht verfügbar")
# SSL-Kontext für Production
ssl_context = None
if USE_PRODUCTION_CONFIG:
app_logger.info("[PRODUCTION] Konfiguriere SSL...")
try:
from utils.ssl_suite import ssl_config
ssl_context = ssl_config.get_ssl_context()
app_logger.info("[PRODUCTION] ✅ SSL-Kontext konfiguriert")
except ImportError:
app_logger.warning("[PRODUCTION] ⚠️ SSL-Konfiguration nicht verfügbar")
# Server-Konfiguration
host = os.getenv('FLASK_HOST', '0.0.0.0')
port = int(os.getenv('FLASK_PORT', 5000))
# Production-spezifische Server-Einstellungen
server_options = {
'host': host,
'port': port,
'threaded': True
}
if USE_PRODUCTION_CONFIG:
# Production-Server-Optimierungen
server_options.update({
'threaded': True,
'processes': 1, # Für Air-Gapped Umgebung
'use_reloader': False,
'use_debugger': False
})
app_logger.info(f"[PRODUCTION] 🌐 Server startet auf https://{host}:{port}")
app_logger.info(f"[PRODUCTION] 🔧 Threaded: {server_options['threaded']}")
app_logger.info(f"[PRODUCTION] 🔒 SSL: {'Ja' if ssl_context else 'Nein'}")
else:
app_logger.info(f"[STARTUP] 🌐 Server startet auf http://{host}:{port}")
# Server starten
if ssl_context:
server_options['ssl_context'] = ssl_context
app.run(**server_options)
else:
app.run(**server_options)
except KeyboardInterrupt:
app_logger.info("[SHUTDOWN] 🛑 Shutdown durch Benutzer angefordert")
except Exception as e:
app_logger.error(f"[ERROR] ❌ Fehler beim Starten der Anwendung: {str(e)}")
if USE_PRODUCTION_CONFIG:
# Production-Fehlerbehandlung
import traceback
app_logger.error(f"[ERROR] Traceback: {traceback.format_exc()}")
raise
finally:
# Cleanup
app_logger.info("[SHUTDOWN] 🧹 Cleanup wird ausgeführt...")
try:
# Queue Manager stoppen
stop_queue_manager()
app_logger.info("[SHUTDOWN] ✅ Queue Manager gestoppt")
# Scheduler stoppen
if 'scheduler' in locals() and scheduler:
scheduler.shutdown()
app_logger.info("[SHUTDOWN] ✅ Job Scheduler gestoppt")
app_logger.info("[SHUTDOWN] ✅ Rate Limiter bereinigt")
# Caches leeren
clear_user_cache()
clear_printer_status_cache()
app_logger.info("[SHUTDOWN] ✅ Caches geleert")
if USE_PRODUCTION_CONFIG:
app_logger.info(f"[SHUTDOWN] 🏁 {ProductionConfig.COMPANY_NAME} System heruntergefahren")
else:
app_logger.info("[SHUTDOWN] 🏁 System heruntergefahren")
except Exception as cleanup_error:
app_logger.error(f"[SHUTDOWN] ❌ Cleanup-Fehler: {str(cleanup_error)}")
# Production-spezifische Funktionen
def get_production_info():
"""Gibt Production-Informationen zurück"""
if USE_PRODUCTION_CONFIG:
return {
'company': ProductionConfig.COMPANY_NAME,
'environment': ProductionConfig.ENVIRONMENT_NAME,
'offline_mode': ProductionConfig.OFFLINE_MODE,
'compliance_mode': ProductionConfig.COMPLIANCE_MODE,
'version': '1.0.0',
'build_date': datetime.now().strftime('%Y-%m-%d'),
'ssl_enabled': USE_PRODUCTION_CONFIG
}
return None
# Template-Funktion für Production-Info
@app.template_global()
def production_info():
"""Stellt Production-Informationen für Templates bereit"""
return get_production_info()
# Nach der Initialisierung der Blueprints und vor dem App-Start
try:
# Admin-Berechtigungen beim Start korrigieren
from utils.permissions import fix_all_admin_permissions
result = fix_all_admin_permissions()
if result['success']:
app_logger.info(f"Admin-Berechtigungen beim Start korrigiert: {result['created']} erstellt, {result['corrected']} aktualisiert")
else:
app_logger.warning(f"Fehler beim Korrigieren der Admin-Berechtigungen: {result.get('error', 'Unbekannter Fehler')}")
except Exception as e:
app_logger.error(f"Fehler beim Korrigieren der Admin-Berechtigungen beim Start: {str(e)}")
if __name__ == "__main__":
main()