Files
Projektarbeit-MYP/backend/app.py

2089 lines
78 KiB
Python
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# -*- coding: utf-8 -*-
"""
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, flash
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
import threading
# ===== 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:
from flask import session, request
session_id = session.get('session_id')
if not session_id:
timestamp = datetime.now().isoformat()
remote_addr = request.remote_addr
session_string = remote_addr + "_" + timestamp
session_id = hashlib.md5(session_string.encode()).hexdigest()
session['session_id'] = session_id
file_path = os.path.join(
self.session_storage_path,
session_id + "_" + key + ".pkl"
)
with open(file_path, 'wb') as f:
pickle.dump(data, f)
# Nur Referenz in Session speichern
session[key + "_ref"] = True
return True
except Exception as e:
logging.error("Fehler beim Speichern der Session-Daten: " + str(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(key + "_ref"):
return None
file_path = os.path.join(
self.session_storage_path,
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("Fehler beim Laden der Session-Daten: " + str(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("Fehler bei Session-Cleanup: " + str(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("[WARN] Windows-Fixes nicht verfügbar: ")
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("[WARN] Queue Manager Stop fehlgeschlagen: ")
# 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("[WARN] Datenbank-Cleanup fehlgeschlagen: ")
except Exception as e:
print("[ERROR] Fehler beim Cleanup: ")
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("[PRODUCTION] ✅ Konfiguration aktiviert")
app_logger.info("[PRODUCTION] ✅ Environment: ")
app_logger.info("[PRODUCTION] ✅ Air-Gapped Mode: ")
app_logger.info("[PRODUCTION] ✅ Compliance Mode: ")
app_logger.info("[PRODUCTION] ✅ Performance Optimized: ")
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("[DEVELOPMENT] ✅ Konfiguration aktiviert")
app_logger.info("[DEVELOPMENT] ✅ Environment: ")
app_logger.info("[DEVELOPMENT] ✅ Debug Mode: ")
app_logger.info("[DEVELOPMENT] ✅ SQL Echo: ")
# ===== 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("[CONFIG] Erkannte Umgebung: ")
app_logger.info("[CONFIG] Production-Modus: ")
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("CSRF-Token konnte nicht in Session gesetzt werden: ")
# 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("CSRF-Token generiert: ...")
return token
except Exception as e:
app_logger.error("CSRF-Token konnte nicht generiert werden: ")
# Fallback: Einfaches Token basierend auf Session
import secrets
fallback_token = secrets.token_urlsafe(32)
app_logger.warning("Verwende Fallback-Token: ...")
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("CSRF-Fehler bei Guest-API (sollte nicht passieren): ")
return jsonify({
"success": False,
"error": "Unerwarteter Sicherheitsfehler bei Guest-API"
}), 500
app_logger.error("CSRF-Fehler für : ")
app_logger.error("Request Headers: ")
app_logger.error("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.
KRITISCHER FIX: Alle User-Attribute vollständig laden VOR expunge(),
um Lazy-Loading-Probleme mit theme_preference und anderen Attributen zu vermeiden.
"""
try:
with get_db_session() as db_session:
user = db_session.query(User).filter_by(id=int(user_id)).first()
if user:
# ALLE User-Attribute explizit laden VOR expunge()
# Dies verhindert lazy-loading Fehler nach Session-Trennung
_ = user.theme_preference
_ = user.language_preference
_ = user.email_notifications
_ = user.browser_notifications
_ = user.dashboard_layout
_ = user.compact_mode
_ = user.show_completed_jobs
_ = user.auto_refresh_interval
_ = user.auto_logout_timeout
_ = user.department
_ = user.position
_ = user.phone
_ = user.bio
_ = user.settings
# Jetzt sicher expunge (alle Daten sind geladen)
db_session.expunge(user)
return user
except Exception as e:
app_logger.error("Fehler beim Laden des Benutzers : ")
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
# Drucker-Steuerungs-Blueprint registrieren (Backend-only Hardware-Kontrolle)
from blueprints.drucker_steuerung import drucker_blueprint
app.register_blueprint(drucker_blueprint) # Backend-only Drucker-Steuerung ohne JavaScript
# ===== 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.context_processor
def inject_moment():
"""
Injiziert eine moment-ähnliche Funktion für Template-Kompatibilität.
Behebt UndefinedError: 'moment' is not defined in Templates.
Ersetzt moment.js durch native Python datetime-Funktionalität.
"""
def moment():
"""Mock moment() Funktion die ein datetime-ähnliches Objekt zurückgibt"""
class MomentLike:
def __init__(self):
self.dt = datetime.now()
def format(self, format_str):
"""Konvertiert moment.js Format-Strings zu Python strftime"""
# Moment.js -> Python strftime Mapping
format_mapping = {
'DD.MM.YYYY': '%d.%m.%Y',
'DD/MM/YYYY': '%d/%m/%Y',
'YYYY-MM-DD': '%Y-%m-%d',
'HH:mm': '%H:%M',
'HH:mm:ss': '%H:%M:%S',
'DD.MM.YYYY HH:mm': '%d.%m.%Y %H:%M'
}
# Direkte Ersetzung wenn bekanntes Format
if format_str in format_mapping:
return self.dt.strftime(format_mapping[format_str])
# Fallback: Standard deutsche Formatierung
return self.dt.strftime('%d.%m.%Y')
return MomentLike()
return {'moment': moment}
@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("Request: ")
@app.after_request
def log_response_info(response):
"""Loggt Response-Informationen"""
if request.endpoint != 'static':
app_logger.debug("Response: ")
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)
# SESSION_LIFETIME ist bereits in Sekunden (Integer), nicht timedelta
session_age_seconds = (now - last_activity_time).total_seconds()
if session_age_seconds > SESSION_LIFETIME:
app_logger.info("Session abgelaufen für Benutzer ")
logout_user()
return redirect(url_for('auth.login'))
except Exception as e:
app_logger.warning("Fehler beim Parsen der Session-Zeit: ")
# 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("CSRF-Test erfolgreich: ")
return jsonify({
"success": True,
"message": "CSRF-Test erfolgreich",
"data": test_data,
"timestamp": datetime.now().isoformat()
}), 200
except Exception as e:
app_logger.error("CSRF-Test Fehler: ")
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 mit Server-Side Rendering."""
try:
from utils.hardware_integration import get_tapo_controller
from models import get_db_session, Printer
# Drucker-Daten server-side laden
db_session = get_db_session()
all_printers = db_session.query(Printer).filter(Printer.active == True).all()
# Live-Status direkt über TapoController abrufen
tapo_controller = get_tapo_controller()
# Drucker-Daten mit Status anreichern
printers_with_status = []
for printer in all_printers:
printer_info = {
'id': printer.id,
'name': printer.name,
'model': printer.model or 'Unbekannt',
'location': printer.location or 'Unbekannt',
'ip_address': printer.ip_address,
'plug_ip': printer.plug_ip,
'active': printer.active,
'status': 'offline'
}
# Status direkt über TapoController prüfen und in DB persistieren
if printer.plug_ip:
try:
reachable, plug_status = tapo_controller.check_outlet_status(
printer.plug_ip, printer_id=printer.id
)
# Drucker-Status basierend auf Steckdosen-Status aktualisieren
if not reachable:
# Nicht erreichbar = offline
printer.status = 'offline'
status_text = 'Offline'
status_color = 'red'
elif plug_status == 'on':
# Steckdose an = belegt
printer.status = 'busy'
status_text = 'Belegt'
status_color = 'green'
elif plug_status == 'off':
# Steckdose aus = verfügbar
printer.status = 'idle'
status_text = 'Verfügbar'
status_color = 'gray'
else:
# Unbekannter Status = offline
printer.status = 'offline'
status_text = 'Unbekannt'
status_color = 'red'
# Zeitstempel aktualisieren und in DB speichern
printer.last_checked = datetime.now()
printer.updated_at = datetime.now()
# Status-Änderung protokollieren (nur bei tatsächlicher Änderung)
from models import PlugStatusLog
current_db_status = printer.status
log_status = 'connected' if reachable else 'disconnected'
if plug_status == 'on':
log_status = 'on'
elif plug_status == 'off':
log_status = 'off'
# Nur loggen wenn sich der Status geändert hat (vereinfachte Prüfung)
try:
PlugStatusLog.log_status_change(
printer_id=printer.id,
status=log_status,
source='system',
ip_address=printer.plug_ip,
notes="Automatische Status-Prüfung beim Laden der Drucker-Seite"
)
app_logger.debug("📊 Auto-Status protokolliert: Drucker -> ")
except Exception as log_error:
app_logger.error("❌ Fehler beim Auto-Protokollieren: ")
printer_info.update({
'plug_status': plug_status,
'plug_reachable': reachable,
'can_control': reachable,
'status': printer.status,
'last_checked': datetime.now().isoformat()
})
# Status-Display für UI
printer_info['status_display'] = {
'text': status_text,
'color': status_color
}
except Exception as e:
printer_info.update({
'plug_status': 'unknown',
'plug_reachable': False,
'can_control': False,
'error': str(e),
'status_display': {'text': 'Fehler', 'color': 'red'}
})
else:
printer_info.update({
'plug_status': 'no_plug',
'plug_reachable': False,
'can_control': False,
'status_display': {'text': 'Kein Smart-Plug', 'color': 'gray'}
})
printers_with_status.append(printer_info)
# Alle Status-Updates in die Datenbank committen
try:
db_session.commit()
app_logger.debug("✅ Status-Updates für Drucker erfolgreich gespeichert")
except Exception as commit_error:
app_logger.error("❌ Fehler beim Speichern der Status-Updates: ")
db_session.rollback()
# Einzigartige Werte für Filter
models = list(set([p['model'] for p in printers_with_status if p['model'] != 'Unbekannt']))
locations = list(set([p['location'] for p in printers_with_status if p['location'] != 'Unbekannt']))
db_session.close()
return render_template("printers.html",
printers=printers_with_status,
models=models,
locations=locations,
no_javascript=True)
except Exception as e:
app_logger.error("Fehler beim Laden der Drucker-Seite: ")
return render_template("printers.html",
printers=[],
models=[],
locations=[],
error=str(e),
no_javascript=True)
@app.route("/printers/control", methods=["POST"])
@login_required
def printer_control():
"""Server-Side Drucker-Steuerung ohne JavaScript."""
try:
from utils.hardware_integration import get_tapo_controller
from models import get_db_session, Printer
printer_id = request.form.get('printer_id')
action = request.form.get('action') # 'on' oder 'off'
if not printer_id or not action:
flash('Ungültige Parameter für Drucker-Steuerung', 'error')
return redirect(url_for('printers_page'))
if action not in ['on', 'off']:
flash('Ungültige Aktion. Nur "on" oder "off" erlaubt.', 'error')
return redirect(url_for('printers_page'))
# Drucker aus Datenbank laden
db_session = get_db_session()
printer = db_session.query(Printer).filter(Printer.id == int(printer_id)).first()
if not printer:
flash('Drucker nicht gefunden', 'error')
db_session.close()
return redirect(url_for('printers_page'))
if not printer.plug_ip:
flash('Keine Steckdose für diesen Drucker konfiguriert', 'error')
db_session.close()
return redirect(url_for('printers_page'))
# Erst Erreichbarkeit der Steckdose prüfen
tapo_controller = get_tapo_controller()
# Prüfe ob Steckdose erreichbar ist
reachable, current_status = tapo_controller.check_outlet_status(printer.plug_ip, printer_id=int(printer_id))
if not reachable:
# Steckdose nicht erreichbar = Drucker offline
printer.status = 'offline'
printer.last_checked = datetime.now()
printer.updated_at = datetime.now()
# Status-Änderung protokollieren
from models import PlugStatusLog
try:
PlugStatusLog.log_status_change(
printer_id=int(printer_id),
status='disconnected',
source='system',
user_id=current_user.id,
ip_address=printer.plug_ip,
error_message="Steckdose nicht erreichbar",
notes="Erreichbarkeitsprüfung durch fehlgeschlagen"
)
app_logger.debug("📊 Offline-Status protokolliert: Drucker -> disconnected")
except Exception as log_error:
app_logger.error("❌ Fehler beim Protokollieren des Offline-Status: ")
db_session.commit()
flash(f'Steckdose nicht erreichbar - Drucker als offline markiert', 'error')
app_logger.warning("⚠️ Steckdose für Drucker nicht erreichbar")
db_session.close()
return redirect(url_for('printers_page'))
# Steckdose erreichbar - Steuerung ausführen
state = action == 'on'
success = tapo_controller.toggle_plug(printer.plug_ip, state)
if success:
# Drucker-Status basierend auf Steckdosen-Aktion aktualisieren
if action == 'on':
# Steckdose an = Drucker belegt (busy)
printer.status = 'busy'
status_text = "belegt"
plug_status = 'on'
else:
# Steckdose aus = Drucker verfügbar (idle)
printer.status = 'idle'
status_text = "verfügbar"
plug_status = 'off'
# Zeitstempel der letzten Überprüfung aktualisieren
printer.last_checked = datetime.now()
printer.updated_at = datetime.now()
# Status-Änderung in PlugStatusLog protokollieren mit Energiedaten
from models import PlugStatusLog
try:
# Energiedaten abrufen falls verfügbar
energy_data = {}
try:
reachable, current_status = tapo_controller.check_outlet_status(printer.plug_ip, printer_id=int(printer_id))
if reachable:
# Versuche Energiedaten zu holen (falls P110)
extra_info = tapo_controller._get_extra_device_info(printer.plug_ip)
if extra_info:
energy_data = {
'power_consumption': extra_info.get('power_consumption'),
'voltage': extra_info.get('voltage'),
'current': extra_info.get('current'),
'firmware_version': extra_info.get('firmware_version')
}
except Exception as energy_error:
app_logger.debug("⚡ Energiedaten für nicht verfügbar: ")
action_text = "eingeschaltet" if action == 'on' else "ausgeschaltet"
PlugStatusLog.log_status_change(
printer_id=int(printer_id),
status=plug_status,
source='manual',
user_id=current_user.id,
ip_address=printer.plug_ip,
power_consumption=energy_data.get('power_consumption'),
voltage=energy_data.get('voltage'),
current=energy_data.get('current'),
firmware_version=energy_data.get('firmware_version'),
notes="Manuell durch "
)
app_logger.debug("📊 Status-Änderung mit Energiedaten protokolliert: Drucker -> ")
except Exception as log_error:
app_logger.error("❌ Fehler beim Protokollieren der Status-Änderung: ")
# Änderungen in Datenbank speichern
db_session.commit()
action_text = "eingeschaltet" if action == 'on' else "ausgeschaltet"
flash(f'Drucker erfolgreich - Status: ', 'success')
app_logger.info("✅ Drucker erfolgreich durch - Status: ")
else:
action_text = "einschalten" if action == 'on' else "ausschalten"
flash(f'Fehler beim der Steckdose', 'error')
app_logger.error("❌ Fehler beim von Drucker ")
db_session.close()
return redirect(url_for('printers_page'))
except Exception as e:
app_logger.error("Unerwarteter Fehler bei Drucker-Steuerung: ")
flash(f'Systemfehler: ', 'error')
return redirect(url_for('printers_page'))
@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ür Dropdown-Anzeige
}
printer_list.append(printer_dict)
db_session.close()
app_logger.info("✅ API: Drucker abgerufen (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("❌ API-Fehler beim Abrufen der Drucker: ")
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 konsolidierten Hardware Integration Monitor
from utils.hardware_integration import printer_monitor
# Status für alle Drucker abrufen
status_data = printer_monitor.get_live_printer_status()
status_list = list(status_data.values())
# 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 printer_monitor.STATUS_DISPLAY:
status["status_display"] = printer_monitor.STATUS_DISPLAY[plug_status]
else:
status["status_display"] = {
"text": "Unbekannt",
"color": "gray",
"icon": "question"
}
app_logger.info("✅ API: Status für 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("❌ API-Fehler beim Abrufen des Drucker-Status: ", 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("❌ Health-Check fehlgeschlagen: ")
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("✅ API-Statistiken abgerufen von ")
return jsonify({
'success': True,
'stats': stats,
'message': 'Statistiken erfolgreich geladen'
})
except Exception as e:
app_logger.error("❌ Fehler beim Abrufen der API-Statistiken: ")
return jsonify({
'success': False,
'error': 'Fehler beim Laden der Statistiken',
'details': str(e)
}), 500
# Statische Seiten - Weiterleitungen zu Legal Blueprint
@app.route("/privacy")
def privacy():
"""Datenschutzerklärung - Weiterleitung zum Legal Blueprint"""
return redirect(url_for("legal.privacy"))
@app.route("/terms")
def terms():
"""Nutzungsbedingungen - Weiterleitung zum Legal Blueprint"""
return redirect(url_for("legal.terms"))
@app.route("/imprint")
def imprint():
"""Impressum - Weiterleitung zum Legal Blueprint"""
return redirect(url_for("legal.imprint"))
@app.route("/legal")
def legal():
"""Rechtliche Hinweise - Weiterleitung zum Legal Blueprint"""
return redirect(url_for("legal.legal"))
# ===== FEHLERBEHANDLUNG =====
@app.errorhandler(400)
def bad_request_error(error):
"""400-Fehlerseite - Ungültige Anfrage"""
app_logger.warning("Bad Request (400): - ")
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("Unauthorized (401): - User: ")
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("Forbidden (403): - User: ")
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("Template-Fehler in 403-Handler: ")
return "<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("Not Found (404): ")
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("Template-Fehler in 404-Handler: ")
return "<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("Method Not Allowed (405): ")
if request.is_json:
return jsonify({
"error": "Methode nicht erlaubt",
"message": "Die HTTP-Methode 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("Payload Too Large (413): ")
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("Rate Limit Exceeded (429): - IP: ")
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("Internal Server Error (500) - ID: ")
app_logger.error("URL: ")
app_logger.error("Method: ")
app_logger.error("User: ")
app_logger.error("Error: ")
app_logger.error("Traceback: ")
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("Template-Fehler in 500-Handler: ")
return "<h1>500 - Interner Serverfehler</h1><p>Ein unerwarteter Fehler ist aufgetreten. Fehler-ID: </p>", 500
@app.errorhandler(502)
def bad_gateway_error(error):
"""502-Fehlerseite - Bad Gateway"""
app_logger.error("Bad Gateway (502): ")
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("Service Unavailable (503): ")
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("HTTP Version Not Supported (505): ")
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("Unhandled Exception - ID: ")
app_logger.error("URL: ")
app_logger.error("Method: ")
app_logger.error("User: ")
app_logger.error("Exception Type: ")
app_logger.error("Exception: ")
app_logger.error("Traceback: ")
# 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("Template-Fehler im Exception-Handler: ")
return "<h1>500 - Unerwarteter Fehler</h1><p>Ein unerwarteter Fehler ist aufgetreten. Fehler-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("[FACTORY] ✅ Production-Konfiguration angewendet")
else:
apply_development_config(app)
app_logger.info("[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("[FACTORY] ⚠️ Sicherheitssuite-Fehler: ")
app_logger.info("[FACTORY] 🏭 Flask-App erstellt ()")
return app
# ===== HAUPTFUNKTION =====
def main():
"""Hauptfunktion zum Starten der Anwendung"""
try:
# Umgebungsinfo loggen
app_logger.info("[STARTUP] 🚀 Starte MYP -Umgebung")
app_logger.info("[STARTUP] 🏢 ")
app_logger.info("[STARTUP] 🔒 Air-Gapped: ")
# 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")
# Steckdosen beim Systemstart initialisieren
app_logger.info("[STARTUP] Initialisiere Steckdosen (alle auf 'aus' = frei)...")
try:
initialization_results = scheduler.initialize_all_outlets_on_startup()
if initialization_results:
success_count = sum(1 for result in initialization_results.values() if result.get('success', False))
total_count = len(initialization_results)
if success_count == total_count:
app_logger.info(f"[STARTUP] ✅ Alle {total_count} Steckdosen erfolgreich initialisiert")
elif success_count > 0:
app_logger.info(f"[STARTUP] ⚡ {success_count}/{total_count} Steckdosen erfolgreich initialisiert")
else:
app_logger.warning(f"[STARTUP] ⚠️ Keine der {total_count} Steckdosen konnte initialisiert werden")
else:
app_logger.info("[STARTUP] Keine Steckdosen zur Initialisierung gefunden")
except Exception as e:
app_logger.warning(f"[STARTUP] ⚠️ Fehler bei Steckdosen-Initialisierung: {str(e)}")
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("[PRODUCTION] 🌐 Server startet auf https://:")
app_logger.info("[PRODUCTION] 🔧 Threaded: ")
app_logger.info("[PRODUCTION] 🔒 SSL: ")
else:
app_logger.info("[STARTUP] 🌐 Server startet auf http://:")
# 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("[ERROR] ❌ Fehler beim Starten der Anwendung: ")
if USE_PRODUCTION_CONFIG:
# Production-Fehlerbehandlung
import traceback
app_logger.error("[ERROR] Traceback: ")
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("[SHUTDOWN] 🏁 System heruntergefahren")
else:
app_logger.info("[SHUTDOWN] 🏁 System heruntergefahren")
except Exception as cleanup_error:
app_logger.error("[SHUTDOWN] ❌ Cleanup-Fehler: ")
# 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("Admin-Berechtigungen beim Start korrigiert: erstellt, aktualisiert")
else:
app_logger.warning("Fehler beim Korrigieren der Admin-Berechtigungen: ")
except Exception as e:
app_logger.error("Fehler beim Korrigieren der Admin-Berechtigungen beim Start: ")
if __name__ == "__main__":
main()