📚 Improved backend structure & logs for better tracking and management

This commit is contained in:
2025-06-11 09:39:30 +02:00
parent 6fe5882e7d
commit 66e2162f7a
17 changed files with 721 additions and 5 deletions

603
backend/app_unified.py Normal file
View File

@ -0,0 +1,603 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
MYP Druckerverwaltung - UNIFIED VERSION
======================================
Einheitliche Flask App für Entwicklung UND Produktion.
Diese App ersetzt sowohl app.py als auch app_production.py.
Verwendung:
- Development: python app_unified.py
- Production: sudo python app_unified.py --production
- SSL-Force: python app_unified.py --ssl
Version: 6.0.0 Unified
"""
import os
import sys
import ssl
import logging
import platform
import argparse
from datetime import datetime, timedelta
# Füge App-Verzeichnis zum Python-Pfad hinzu
sys.path.insert(0, '/opt/myp')
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
# Import der Haupt-App-Logik
from app import app, app_logger, init_database, create_initial_admin, main as app_main
from app import start_queue_manager, stop_queue_manager, get_job_scheduler, cleanup_rate_limiter
# Flask-Imports für Request-Handling
from flask import request, redirect
# =========================== UMGEBUNGS-ERKENNUNG ===========================
def detect_environment():
"""Erkennt automatisch die Laufzeitumgebung"""
# Kommandozeilen-Argumente prüfen
if '--production' in sys.argv or '--prod' in sys.argv:
return 'production'
if '--development' in sys.argv or '--dev' in sys.argv:
return 'development'
# Umgebungsvariablen prüfen
env_mode = os.getenv('MYP_MODE', '').lower()
if env_mode in ['production', 'prod']:
return 'production'
elif env_mode in ['development', 'dev']:
return 'development'
# Automatische Erkennung basierend auf System
if detect_raspberry_pi():
return 'production'
if platform.system() == 'Windows':
return 'development'
# Standard: Development für unbekannte Systeme
return 'development'
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:
machine = platform.machine().lower()
if 'arm' in machine or 'aarch64' in machine:
return True
except:
pass
return os.getenv('FORCE_RASPBERRY_PI', '').lower() in ['true', '1', 'yes']
def should_use_ssl():
"""Bestimmt ob SSL verwendet werden soll"""
if '--ssl' in sys.argv or '--https' in sys.argv:
return True
if '--no-ssl' in sys.argv or '--http' in sys.argv:
return False
env_ssl = os.getenv('MYP_SSL', '').lower()
if env_ssl in ['true', '1', 'yes', 'force']:
return True
elif env_ssl in ['false', '0', 'no', 'disable']:
return False
# Automatisch: SSL für Production, HTTP für Development
return detect_environment() == 'production'
# =========================== KONFIGURATIONSKLASSEN ===========================
class DevelopmentConfig:
"""Konfiguration für Entwicklungsumgebung"""
# Debug-Einstellungen
DEBUG = True
TESTING = False
# HTTP-Konfiguration
FORCE_HTTPS = False
SSL_REQUIRED = False
HTTP_PORT = 5000
# Performance (weniger optimiert für bessere Debug-Möglichkeiten)
OPTIMIZED_MODE = False
USE_MINIFIED_ASSETS = False
DISABLE_ANIMATIONS = False
# Session-Konfiguration (weniger restriktiv für Development)
SESSION_COOKIE_SECURE = False
SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_SAMESITE = 'Lax'
# Reload-Features für Development
TEMPLATES_AUTO_RELOAD = True
EXPLAIN_TEMPLATE_LOADING = False
class ProductionConfig:
"""Konfiguration für Produktionsumgebung"""
# Produktions-Einstellungen
DEBUG = False
TESTING = False
# HTTPS-Only Konfiguration
FORCE_HTTPS = True
SSL_REQUIRED = True
HTTPS_PORT = 443
# Performance-Optimierungen
OPTIMIZED_MODE = True
USE_MINIFIED_ASSETS = True
DISABLE_ANIMATIONS = True
# Sicherheits-Einstellungen
SESSION_COOKIE_SECURE = True
SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_SAMESITE = 'Strict'
WTF_CSRF_ENABLED = True
# Template-Optimierungen
TEMPLATES_AUTO_RELOAD = False
EXPLAIN_TEMPLATE_LOADING = False
# SSL-Konfiguration
SSL_CERT_PATH = None # Wird automatisch erkannt
SSL_KEY_PATH = None # Wird automatisch erkannt
# =========================== SSL-SETUP ===========================
def get_ssl_paths():
"""Ermittelt die SSL-Zertifikat-Pfade plattformspezifisch"""
if platform.system() == 'Windows':
ssl_dir = os.path.join(os.path.dirname(__file__), 'ssl')
else:
# Probiere verschiedene Standard-Pfade
possible_dirs = [
'/opt/myp/ssl',
'/etc/ssl/myp',
os.path.join(os.path.dirname(__file__), 'ssl'),
'./ssl'
]
ssl_dir = None
for dir_path in possible_dirs:
if os.path.exists(dir_path):
ssl_dir = dir_path
break
if not ssl_dir:
ssl_dir = possible_dirs[0] # Erstelle in /opt/myp/ssl
cert_file = os.path.join(ssl_dir, 'cert.pem')
key_file = os.path.join(ssl_dir, 'key.pem')
return ssl_dir, cert_file, key_file
def setup_ssl_certificates():
"""Erstellt SSL-Zertifikate falls sie nicht existieren"""
ssl_dir, cert_file, key_file = get_ssl_paths()
app_logger.info(f"🔐 Prüfe SSL-Zertifikate in: {ssl_dir}")
# Erstelle SSL-Verzeichnis
os.makedirs(ssl_dir, exist_ok=True)
# Prüfe ob Zertifikate existieren
if os.path.exists(cert_file) and os.path.exists(key_file):
try:
# Teste Zertifikat-Gültigkeit
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
context.load_cert_chain(cert_file, key_file)
app_logger.info("✅ Bestehende SSL-Zertifikate sind gültig")
return cert_file, key_file
except Exception as e:
app_logger.warning(f"⚠️ Bestehende SSL-Zertifikate ungültig: {e}")
# Erstelle neue Zertifikate
app_logger.info("🔧 Erstelle neue SSL-Zertifikate...")
try:
# Versuche existierende SSL-Utilities zu verwenden
if os.path.exists('./ssl/ssl_fix.py'):
try:
import subprocess
result = subprocess.run([
sys.executable, './ssl/ssl_fix.py'
], capture_output=True, text=True, timeout=60)
if result.returncode == 0:
app_logger.info("✅ SSL-Zertifikate mit ssl_fix.py erstellt")
return cert_file, key_file
except Exception as e:
app_logger.warning(f"⚠️ ssl_fix.py fehlgeschlagen: {e}")
# Fallback: Einfache SSL-Erstellung
create_simple_ssl_certificates(ssl_dir, cert_file, key_file)
return cert_file, key_file
except Exception as e:
app_logger.error(f"❌ SSL-Zertifikat-Erstellung fehlgeschlagen: {e}")
raise Exception(f"SSL-Setup fehlgeschlagen: {e}")
def create_simple_ssl_certificates(ssl_dir, cert_file, key_file):
"""Erstellt einfache selbstsignierte SSL-Zertifikate"""
try:
# Versuche mit Python Cryptography Library
from cryptography import x509
from cryptography.x509.oid import NameOID
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import rsa
import ipaddress
app_logger.info("🐍 Erstelle SSL-Zertifikate mit Python Cryptography...")
# Private Key generieren
private_key = rsa.generate_private_key(
public_exponent=65537,
key_size=2048,
)
# Subject und Issuer
subject = issuer = x509.Name([
x509.NameAttribute(NameOID.COUNTRY_NAME, "DE"),
x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, "Baden-Wuerttemberg"),
x509.NameAttribute(NameOID.LOCALITY_NAME, "Stuttgart"),
x509.NameAttribute(NameOID.ORGANIZATION_NAME, "Mercedes-Benz AG"),
x509.NameAttribute(NameOID.ORGANIZATIONAL_UNIT_NAME, "MYP Druckerverwaltung"),
x509.NameAttribute(NameOID.COMMON_NAME, platform.node()),
])
# Subject Alternative Names
san_list = [
x509.DNSName("localhost"),
x509.DNSName("127.0.0.1"),
x509.IPAddress(ipaddress.IPv4Address("127.0.0.1")),
x509.DNSName(platform.node()),
]
# Zertifikat erstellen
cert = x509.CertificateBuilder().subject_name(
subject
).issuer_name(
issuer
).public_key(
private_key.public_key()
).serial_number(
x509.random_serial_number()
).not_valid_before(
datetime.now()
).not_valid_after(
datetime.now() + timedelta(days=365)
).add_extension(
x509.SubjectAlternativeName(san_list),
critical=False,
).sign(private_key, hashes.SHA256())
# Private Key schreiben
with open(key_file, 'wb') as f:
f.write(private_key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.PKCS8,
encryption_algorithm=serialization.NoEncryption()
))
# Zertifikat schreiben
with open(cert_file, 'wb') as f:
f.write(cert.public_bytes(serialization.Encoding.PEM))
# Berechtigungen setzen (Unix)
try:
os.chmod(cert_file, 0o644)
os.chmod(key_file, 0o600)
except:
pass # Windows hat andere Berechtigungen
app_logger.info("✅ SSL-Zertifikate mit Python Cryptography erstellt")
except ImportError:
# Fallback: OpenSSL verwenden
app_logger.info("🔧 Erstelle SSL-Zertifikate mit OpenSSL...")
import subprocess
# Private Key erstellen
subprocess.run([
'openssl', 'genrsa', '-out', key_file, '2048'
], check=True, capture_output=True)
# Selbstsigniertes Zertifikat erstellen
subprocess.run([
'openssl', 'req', '-new', '-x509',
'-key', key_file,
'-out', cert_file,
'-days', '365',
'-subj', f'/C=DE/ST=Baden-Wuerttemberg/L=Stuttgart/O=Mercedes-Benz AG/CN={platform.node()}'
], check=True, capture_output=True)
app_logger.info("✅ SSL-Zertifikate mit OpenSSL erstellt")
def get_ssl_context():
"""Erstellt SSL-Kontext mit Zertifikaten"""
if not should_use_ssl():
return None
try:
cert_file, key_file = setup_ssl_certificates()
# SSL-Kontext erstellen
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
context.load_cert_chain(cert_file, key_file)
# Sichere SSL-Einstellungen
context.set_ciphers('ECDHE+AESGCM:ECDHE+CHACHA20:DHE+AESGCM:DHE+CHACHA20:!aNULL:!MD5:!DSS')
context.options |= ssl.OP_NO_SSLv2
context.options |= ssl.OP_NO_SSLv3
context.options |= ssl.OP_NO_TLSv1
context.options |= ssl.OP_NO_TLSv1_1
app_logger.info("✅ SSL-Kontext erfolgreich konfiguriert")
return context
except Exception as e:
app_logger.error(f"❌ SSL-Kontext-Erstellung fehlgeschlagen: {e}")
app_logger.warning("⚠️ Fallback zu HTTP ohne SSL")
return None
# =========================== APP-KONFIGURATION ===========================
def configure_app_for_environment(environment):
"""Konfiguriert die App für die erkannte Umgebung"""
if environment == 'production':
config_class = ProductionConfig
app_logger.info("🚀 Produktions-Modus aktiviert")
else:
config_class = DevelopmentConfig
app_logger.info("🔧 Entwicklungs-Modus aktiviert")
# Konfiguration anwenden
for attr in dir(config_class):
if not attr.startswith('_'):
app.config[attr] = getattr(config_class, attr)
# Jinja-Globals setzen
app.jinja_env.globals.update({
'environment': environment,
'optimized_mode': config_class.OPTIMIZED_MODE,
'use_minified_assets': config_class.USE_MINIFIED_ASSETS if hasattr(config_class, 'USE_MINIFIED_ASSETS') else False,
'disable_animations': config_class.DISABLE_ANIMATIONS if hasattr(config_class, 'DISABLE_ANIMATIONS') else False,
})
return config_class
# =========================== MIDDLEWARE ===========================
@app.before_request
def force_https_if_required():
"""Erzwingt HTTPS wenn in der Konfiguration aktiviert"""
if (app.config.get('FORCE_HTTPS', False) and
not request.is_secure and
not request.headers.get('X-Forwarded-Proto') == 'https'):
# Redirect zu HTTPS
url = request.url.replace('http://', 'https://', 1)
if ':5000' in url:
url = url.replace(':5000', ':443')
elif ':80' in url:
url = url.replace(':80', ':443')
return redirect(url, code=301)
@app.after_request
def add_environment_headers(response):
"""Fügt umgebungsspezifische Headers hinzu"""
if app.config.get('FORCE_HTTPS', False):
# Produktions-Sicherheits-Headers
response.headers['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains'
response.headers['X-Content-Type-Options'] = 'nosniff'
response.headers['X-Frame-Options'] = 'SAMEORIGIN'
response.headers['X-XSS-Protection'] = '1; mode=block'
response.headers['Referrer-Policy'] = 'strict-origin-when-cross-origin'
# Cache-Headers für statische Dateien
if request.endpoint == 'static' or '/static/' in request.path:
if app.config.get('OPTIMIZED_MODE', False):
response.headers['Cache-Control'] = 'public, max-age=31536000'
else:
response.headers['Cache-Control'] = 'public, max-age=3600'
return response
# =========================== LOGGING-SETUP ===========================
def setup_environment_logging(environment):
"""Konfiguriert Logging für die Umgebung"""
if environment == 'production':
# Produktions-Logging: Weniger verbose
logging.getLogger('werkzeug').setLevel(logging.WARNING)
logging.getLogger('urllib3').setLevel(logging.WARNING)
app_logger.setLevel(logging.INFO)
# Entferne Debug-Handler
for handler in app_logger.handlers[:]:
if handler.level == logging.DEBUG:
app_logger.removeHandler(handler)
else:
# Development-Logging: Vollständig
app_logger.setLevel(logging.DEBUG)
app_logger.info(f"✅ Logging für {environment} konfiguriert")
# =========================== ARGUMENT-PARSER ===========================
def parse_arguments():
"""Parst Kommandozeilen-Argumente für vereinheitlichte Steuerung"""
parser = argparse.ArgumentParser(description='MYP Druckerverwaltung - Unified Server')
parser.add_argument('--production', '--prod', action='store_true',
help='Starte im Produktions-Modus')
parser.add_argument('--ssl', '--https', action='store_true',
help='Erzwinge SSL/HTTPS')
parser.add_argument('--port', type=int, default=None,
help='Port-Nummer')
return parser.parse_args()
def show_usage_info():
"""Zeigt Nutzungsinformationen an"""
environment = "Production" if '--production' in sys.argv else "Development"
ssl_enabled = '--ssl' in sys.argv or '--production' in sys.argv
app_logger.info("🎯 MYP Unified App - Eine einzige funktionale App!")
app_logger.info(f"📋 Modus: {environment}")
app_logger.info(f"🔐 SSL: {'Aktiviert' if ssl_enabled else 'Deaktiviert'}")
app_logger.info(f"💻 Plattform: {platform.system()}")
app_logger.info("=" * 60)
# =========================== HAUPTFUNKTION ===========================
def main():
"""Hauptfunktion für den unified Server"""
try:
# Argumente parsen
args = parse_arguments()
# Umgebung ermitteln
environment = detect_environment()
# Logging für Umgebung konfigurieren
setup_environment_logging(environment)
app_logger.info("🚀 MYP Unified Server startet...")
app_logger.info(f"📅 Start-Zeit: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
app_logger.info(f"🖥️ Hostname: {platform.node()}")
app_logger.info(f"🐍 Python: {sys.version}")
app_logger.info(f"🌍 Umgebung: {environment}")
app_logger.info(f"💻 Plattform: {platform.system()} {platform.release()}")
# App für Umgebung konfigurieren
config_class = configure_app_for_environment(environment)
# Root-Berechtigung prüfen (nur für Production + Port 443)
if (environment == 'production' and
config_class.HTTPS_PORT == 443 and
hasattr(os, 'geteuid') and
os.geteuid() != 0):
app_logger.error("❌ Root-Berechtigung erforderlich für Port 443")
app_logger.error("💡 Führe aus mit: sudo python app_unified.py --production")
sys.exit(1)
elif platform.system() == 'Windows' and environment == 'production':
app_logger.info("🪟 Windows: Root-Check übersprungen")
# SSL-Kontext erstellen falls erforderlich
ssl_context = get_ssl_context()
# Datenbank initialisieren
init_database()
create_initial_admin()
# Background-Services starten
start_queue_manager()
scheduler = get_job_scheduler()
if scheduler:
scheduler.start()
app_logger.info("✅ Job-Scheduler gestartet")
# Server-Konfiguration
if args.port:
port = args.port
elif ssl_context and environment == 'production':
port = 443
elif environment == 'production':
port = 5443 # Alternative HTTPS-Port falls keine Root-Rechte
else:
port = 5000 # Development HTTP-Port
# Debug-Modus
debug_mode = (environment == 'development' and not ssl_context)
# Server-Informationen anzeigen
protocol = 'https' if ssl_context else 'http'
app_logger.info(f"🌐 Server läuft auf: {protocol}://{platform.node()}:{port}")
if platform.system() == 'Windows':
app_logger.info(f"🏠 Lokaler Zugriff: {protocol}://localhost:{port}")
if ssl_context:
app_logger.info("🔐 SSL/HTTPS aktiviert")
else:
app_logger.info("🔓 HTTP-Modus (unverschlüsselt)")
# Flask-Server starten
app.run(
host=platform.node(),
port=port,
ssl_context=ssl_context,
debug=debug_mode,
threaded=True,
use_reloader=False # Deaktiviert für Produktionsstabilität
)
except PermissionError:
app_logger.error("❌ Berechtigung verweigert")
if platform.system() != 'Windows':
app_logger.error("💡 Führe als Root aus: sudo python app_unified.py --production")
else:
app_logger.error("💡 Führe als Administrator aus")
sys.exit(1)
except OSError as e:
if "Address already in use" in str(e):
app_logger.error("❌ Port bereits belegt")
app_logger.error("💡 Andere Services stoppen oder anderen Port verwenden")
else:
app_logger.error(f"❌ Netzwerk-Fehler: {e}")
sys.exit(1)
except KeyboardInterrupt:
app_logger.info("🛑 Server durch Benutzer gestoppt")
sys.exit(0)
except Exception as e:
app_logger.error(f"❌ Kritischer Fehler beim Server-Start: {e}")
import traceback
app_logger.error(f"Traceback: {traceback.format_exc()}")
sys.exit(1)
finally:
# Cleanup
try:
stop_queue_manager()
if 'scheduler' in locals() and scheduler:
scheduler.shutdown()
cleanup_rate_limiter()
app_logger.info("✅ Cleanup abgeschlossen")
except:
pass
if __name__ == "__main__":
args = parse_arguments()
show_usage_info()
# Verwende die existierende App-Main-Funktion
app_main()