**Änderungen:** - ✅ app.py: Hinzugefügt, um CSRF-Fehler zu behandeln - ✅ models.py: Fehlerprotokollierung bei der Suche nach Gastanfragen per OTP - ✅ api.py: Fehlerprotokollierung beim Markieren von Benachrichtigungen als gelesen - ✅ calendar.py: Fallback-Daten zurückgeben, wenn keine Kalenderereignisse vorhanden sind - ✅ guest.py: Status-Check-Seite für Gäste aktualisiert - ✅ hardware_integration.py: Debugging-Informationen für erweiterte Geräteinformationen hinzugefügt - ✅ tapo_status_manager.py: Rückgabewert für Statusabfrage hinzugefügt **Ergebnis:** - Verbesserte Fehlerbehandlung und Protokollierung für eine robustere Anwendung - Bessere Nachverfolgbarkeit von Fehlern und Systemverhalten 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1577 lines
58 KiB
Python
1577 lines
58 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
|
|
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'
|
|
|
|
# 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")
|
|
|
|
# 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)}")
|
|
|
|
# Flask-App initialisieren
|
|
app = Flask(__name__)
|
|
app.secret_key = SECRET_KEY
|
|
|
|
# ===== KONFIGURATION ANWENDEN =====
|
|
ENVIRONMENT_TYPE = get_environment_type()
|
|
USE_PRODUCTION_CONFIG = detect_production_environment()
|
|
|
|
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
|
|
OFFLINE_MODE = getattr(ProductionConfig, 'OFFLINE_MODE', False) if USE_PRODUCTION_CONFIG else getattr(DevelopmentConfig, 'OFFLINE_MODE', False)
|
|
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
|
|
|
|
# CSRF-Schutz initialisieren
|
|
csrf = CSRFProtect(app)
|
|
|
|
# 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
|
|
return generate_csrf()
|
|
except Exception as e:
|
|
app_logger.warning(f"CSRF-Token konnte nicht generiert werden: {str(e)}")
|
|
return ""
|
|
|
|
@app.errorhandler(CSRFError)
|
|
def csrf_error(error):
|
|
"""Behandelt CSRF-Fehler"""
|
|
app_logger.warning(f"CSRF-Fehler: {error.description}")
|
|
return jsonify({"error": "CSRF-Token ungültig oder fehlt"}), 400
|
|
|
|
# Login-Manager initialisieren
|
|
login_manager = LoginManager()
|
|
login_manager.init_app(app)
|
|
login_manager.login_view = "auth.login"
|
|
login_manager.login_message = "Bitte melden Sie sich an, um auf diese Seite zuzugreifen."
|
|
|
|
# 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("/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 =====
|
|
|
|
@app.route("/api/jobs", methods=["GET"])
|
|
@login_required
|
|
def api_get_jobs():
|
|
"""API-Endpunkt für Jobs - leitet an Jobs-Blueprint weiter"""
|
|
from blueprints.jobs import get_jobs
|
|
return get_jobs()
|
|
|
|
@app.route("/api/jobs", methods=["POST"])
|
|
@login_required
|
|
def api_create_job():
|
|
"""API-Endpunkt für Job-Erstellung - leitet an Jobs-Blueprint weiter"""
|
|
from blueprints.jobs import create_job
|
|
return create_job()
|
|
|
|
@app.route("/api/jobs/<int:job_id>", methods=["GET"])
|
|
@login_required
|
|
def api_get_job(job_id):
|
|
"""API-Endpunkt für einzelnen Job - leitet an Jobs-Blueprint weiter"""
|
|
from blueprints.jobs import get_job
|
|
return get_job(job_id)
|
|
|
|
@app.route("/api/jobs/<int:job_id>", methods=["PUT"])
|
|
@login_required
|
|
def api_update_job(job_id):
|
|
"""API-Endpunkt für Job-Update - leitet an Jobs-Blueprint weiter"""
|
|
from blueprints.jobs import update_job
|
|
return update_job(job_id)
|
|
|
|
@app.route("/api/jobs/<int:job_id>", methods=["DELETE"])
|
|
@login_required
|
|
def api_delete_job(job_id):
|
|
"""API-Endpunkt für Job-Löschung - leitet an Jobs-Blueprint weiter"""
|
|
from blueprints.jobs import delete_job
|
|
return delete_job(job_id)
|
|
|
|
@app.route("/api/jobs/active", methods=["GET"])
|
|
@login_required
|
|
def api_get_active_jobs():
|
|
"""API-Endpunkt für aktive Jobs - leitet an Jobs-Blueprint weiter"""
|
|
from blueprints.jobs import get_active_jobs
|
|
return get_active_jobs()
|
|
|
|
@app.route("/api/jobs/current", methods=["GET"])
|
|
@login_required
|
|
def api_get_current_job():
|
|
"""API-Endpunkt für aktuellen Job - leitet an Jobs-Blueprint weiter"""
|
|
from blueprints.jobs import get_current_job
|
|
return get_current_job()
|
|
|
|
@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: 'true')
|
|
- show_all: 'true' um ALLE Drucker anzuzeigen, unabhängig vom Status
|
|
"""
|
|
try:
|
|
from models import get_db_session, Printer
|
|
|
|
# Query-Parameter auslesen
|
|
include_inactive = request.args.get('include_inactive', 'true').lower() == 'true'
|
|
show_all = request.args.get('show_all', 'true').lower() == 'true'
|
|
|
|
db_session = get_db_session()
|
|
|
|
# Basis-Query - standardmäßig ALLE Drucker zeigen für Dropdown-Auswahl
|
|
query = db_session.query(Printer)
|
|
|
|
# Optional: Nur aktive Drucker filtern (wenn explizit angefordert)
|
|
if not include_inactive and not show_all:
|
|
query = query.filter(Printer.active == True)
|
|
|
|
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
|
|
|
|
# ===== 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() |