5476 lines
210 KiB
Python
5476 lines
210 KiB
Python
import os
|
||
import sys
|
||
import logging
|
||
import atexit
|
||
from datetime import datetime, timedelta
|
||
from flask import Flask, render_template, request, jsonify, redirect, url_for, flash, send_file, abort, session, make_response, Response
|
||
from flask_login import LoginManager, login_user, logout_user, login_required, current_user
|
||
from flask_wtf import CSRFProtect
|
||
from flask_wtf.csrf import CSRFError
|
||
from werkzeug.utils import secure_filename
|
||
from werkzeug.security import generate_password_hash, check_password_hash
|
||
from sqlalchemy.orm import sessionmaker, joinedload
|
||
from sqlalchemy import func, text
|
||
from functools import wraps
|
||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||
from typing import List, Dict, Tuple
|
||
import time
|
||
import subprocess
|
||
import json
|
||
import signal
|
||
from contextlib import contextmanager
|
||
|
||
# Windows-spezifische Fixes früh importieren (sichere Version)
|
||
if os.name == 'nt':
|
||
try:
|
||
from utils.windows_fixes import get_windows_thread_manager
|
||
# apply_all_windows_fixes() wird automatisch beim Import ausgeführt
|
||
print("✅ Windows-Fixes (sichere Version) geladen")
|
||
except ImportError as e:
|
||
# Fallback falls windows_fixes nicht verfügbar
|
||
get_windows_thread_manager = None
|
||
print(f"⚠️ Windows-Fixes nicht verfügbar: {str(e)}")
|
||
else:
|
||
get_windows_thread_manager = None
|
||
|
||
# Lokale Imports
|
||
from models import init_database, create_initial_admin, User, Printer, Job, Stats, SystemLog, get_db_session, GuestRequest, UserPermission, Notification, JobOrder, Base, get_engine
|
||
from utils.logging_config import setup_logging, get_logger, measure_execution_time, log_startup_info, debug_request, debug_response
|
||
from utils.job_scheduler import JobScheduler, get_job_scheduler
|
||
from utils.queue_manager import start_queue_manager, stop_queue_manager, get_queue_manager
|
||
from config.settings import SECRET_KEY, UPLOAD_FOLDER, ALLOWED_EXTENSIONS, ENVIRONMENT, SESSION_LIFETIME, SCHEDULER_ENABLED, SCHEDULER_INTERVAL, TAPO_USERNAME, TAPO_PASSWORD
|
||
from utils.file_manager import file_manager, save_job_file, save_guest_file, save_avatar_file, save_asset_file, save_log_file, save_backup_file, save_temp_file, delete_file as delete_file_safe
|
||
|
||
# ===== OFFLINE-MODUS KONFIGURATION =====
|
||
# System läuft im Offline-Modus ohne Internetverbindung
|
||
OFFLINE_MODE = True # Produktionseinstellung für Offline-Betrieb
|
||
|
||
# ===== BEDINGTE IMPORTS FÜR OFFLINE-MODUS =====
|
||
if not OFFLINE_MODE:
|
||
# Nur laden wenn Online-Modus
|
||
import requests
|
||
else:
|
||
# Offline-Mock für requests
|
||
class OfflineRequestsMock:
|
||
"""Mock-Klasse für requests im Offline-Modus"""
|
||
|
||
@staticmethod
|
||
def get(*args, **kwargs):
|
||
raise ConnectionError("System läuft im Offline-Modus - keine Internet-Verbindung verfügbar")
|
||
|
||
@staticmethod
|
||
def post(*args, **kwargs):
|
||
raise ConnectionError("System läuft im Offline-Modus - keine Internet-Verbindung verfügbar")
|
||
|
||
requests = OfflineRequestsMock()
|
||
|
||
# Datenbank-Engine für Kompatibilität mit init_simple_db.py
|
||
from models import engine as db_engine
|
||
|
||
# Blueprints importieren
|
||
from blueprints.guest import guest_blueprint
|
||
from blueprints.calendar import calendar_blueprint
|
||
from blueprints.users import users_blueprint
|
||
from blueprints.printers import printers_blueprint
|
||
|
||
# Scheduler importieren falls verfügbar
|
||
try:
|
||
from utils.job_scheduler import scheduler
|
||
except ImportError:
|
||
scheduler = None
|
||
|
||
# SSL-Kontext importieren falls verfügbar
|
||
try:
|
||
from utils.ssl_config import get_ssl_context
|
||
except ImportError:
|
||
def get_ssl_context():
|
||
return None
|
||
|
||
# Template-Helfer importieren falls verfügbar
|
||
try:
|
||
from utils.template_helpers import register_template_helpers
|
||
except ImportError:
|
||
def register_template_helpers(app):
|
||
pass
|
||
|
||
# Datenbank-Monitor und Backup-Manager importieren falls verfügbar
|
||
try:
|
||
from utils.database_utils import DatabaseMonitor
|
||
database_monitor = DatabaseMonitor()
|
||
except ImportError:
|
||
database_monitor = None
|
||
|
||
try:
|
||
from utils.backup_manager import BackupManager
|
||
backup_manager = BackupManager()
|
||
except ImportError:
|
||
backup_manager = None
|
||
|
||
# Import neuer Systeme
|
||
from utils.rate_limiter import limit_requests, rate_limiter, cleanup_rate_limiter
|
||
from utils.security import init_security, require_secure_headers, security_check
|
||
from utils.permissions import init_permission_helpers, require_permission, Permission, check_permission
|
||
from utils.analytics import analytics_engine, track_event, get_dashboard_stats
|
||
|
||
# Import der neuen System-Module
|
||
from utils.form_validation import (
|
||
FormValidator, ValidationError, ValidationResult,
|
||
get_user_registration_validator, get_job_creation_validator,
|
||
get_printer_creation_validator, get_guest_request_validator,
|
||
validate_form, get_client_validation_js
|
||
)
|
||
from utils.report_generator import (
|
||
ReportFactory, ReportConfig, JobReportBuilder,
|
||
UserReportBuilder, PrinterReportBuilder, generate_comprehensive_report
|
||
)
|
||
from utils.realtime_dashboard import (
|
||
DashboardManager, EventType, DashboardEvent,
|
||
emit_job_event, emit_printer_event, emit_system_alert,
|
||
get_dashboard_client_js
|
||
)
|
||
from utils.drag_drop_system import (
|
||
drag_drop_manager, DragDropConfig, validate_file_upload,
|
||
get_drag_drop_javascript, get_drag_drop_css
|
||
)
|
||
from utils.advanced_tables import (
|
||
AdvancedTableQuery, TableDataProcessor, ColumnConfig,
|
||
create_table_config, get_advanced_tables_js, get_advanced_tables_css
|
||
)
|
||
from utils.maintenance_system import (
|
||
MaintenanceManager, MaintenanceType, MaintenanceStatus,
|
||
create_maintenance_task, schedule_maintenance,
|
||
get_maintenance_overview, update_maintenance_status
|
||
)
|
||
from utils.multi_location_system import (
|
||
LocationManager, LocationType, AccessLevel,
|
||
create_location, assign_user_to_location, get_user_locations,
|
||
calculate_distance, find_nearest_location
|
||
)
|
||
|
||
# Drucker-Monitor importieren
|
||
from utils.printer_monitor import printer_monitor
|
||
|
||
# Flask-App initialisieren
|
||
app = Flask(__name__)
|
||
app.secret_key = SECRET_KEY
|
||
app.config["PERMANENT_SESSION_LIFETIME"] = SESSION_LIFETIME
|
||
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
|
||
app.config["WTF_CSRF_ENABLED"] = True
|
||
|
||
# Globale db-Variable für Kompatibilität mit init_simple_db.py
|
||
db = db_engine
|
||
|
||
# System-Manager initialisieren
|
||
dashboard_manager = DashboardManager()
|
||
maintenance_manager = MaintenanceManager()
|
||
location_manager = LocationManager()
|
||
|
||
# SocketIO für Realtime Dashboard initialisieren
|
||
socketio = dashboard_manager.init_socketio(app, cors_allowed_origins="*")
|
||
|
||
# CSRF-Schutz initialisieren
|
||
csrf = CSRFProtect(app)
|
||
|
||
# Security-System initialisieren
|
||
app = init_security(app)
|
||
|
||
# Permission Template Helpers registrieren
|
||
init_permission_helpers(app)
|
||
|
||
# Template-Helper registrieren
|
||
register_template_helpers(app)
|
||
|
||
# CSRF-Error-Handler - Korrigierte Version für Flask-WTF 1.2.1+
|
||
@app.errorhandler(CSRFError)
|
||
def csrf_error(error):
|
||
"""Behandelt CSRF-Fehler und gibt detaillierte Informationen zurück."""
|
||
app_logger.error(f"CSRF-Fehler für {request.path}: {error}")
|
||
|
||
if request.path.startswith('/api/'):
|
||
# Für API-Anfragen: JSON-Response
|
||
return jsonify({
|
||
"error": "CSRF-Token fehlt oder ungültig",
|
||
"reason": str(error),
|
||
"help": "Fügen Sie ein gültiges CSRF-Token zu Ihrer Anfrage hinzu"
|
||
}), 400
|
||
else:
|
||
# Für normale Anfragen: Weiterleitung zur Fehlerseite
|
||
flash("Sicherheitsfehler: Anfrage wurde abgelehnt. Bitte versuchen Sie es erneut.", "error")
|
||
return redirect(request.url)
|
||
|
||
# Blueprints registrieren
|
||
app.register_blueprint(guest_blueprint)
|
||
app.register_blueprint(calendar_blueprint)
|
||
app.register_blueprint(users_blueprint)
|
||
app.register_blueprint(printers_blueprint)
|
||
|
||
# Login-Manager initialisieren
|
||
login_manager = LoginManager()
|
||
login_manager.init_app(app)
|
||
login_manager.login_view = "login"
|
||
login_manager.login_message = "Bitte melden Sie sich an, um auf diese Seite zuzugreifen."
|
||
login_manager.login_message_category = "info"
|
||
|
||
@login_manager.user_loader
|
||
def load_user(user_id):
|
||
"""
|
||
Robuster User-Loader mit verbessertem Error-Handling für Schema-Probleme.
|
||
"""
|
||
try:
|
||
# user_id von Flask-Login ist immer ein String - zu Integer konvertieren
|
||
try:
|
||
user_id_int = int(user_id)
|
||
except (ValueError, TypeError):
|
||
app_logger.error(f"Ungültige User-ID: {user_id}")
|
||
return None
|
||
|
||
db_session = get_db_session()
|
||
|
||
# Robuste Abfrage mit Error-Handling
|
||
try:
|
||
user = db_session.query(User).filter(User.id == user_id_int).first()
|
||
db_session.close()
|
||
return user
|
||
except Exception as db_error:
|
||
# Schema-Problem - versuche manuelle Abfrage
|
||
app_logger.warning(f"Schema-Problem beim User-Load für ID {user_id_int}: {str(db_error)}")
|
||
|
||
# Erweiterte manuelle Abfrage mit allen wichtigen Feldern
|
||
try:
|
||
result = db_session.execute(
|
||
text("""SELECT id, email, username, password_hash, name, role, active,
|
||
created_at, last_login, updated_at, settings, department,
|
||
position, phone, bio, last_activity
|
||
FROM users WHERE id = :user_id"""),
|
||
{"user_id": user_id_int}
|
||
).fetchone()
|
||
|
||
if result:
|
||
# Manuell User-Objekt erstellen mit robuster Tupel-Behandlung
|
||
user = User()
|
||
|
||
# Basis-Felder (immer vorhanden)
|
||
user.id = result[0] if len(result) > 0 else user_id_int
|
||
user.email = result[1] if len(result) > 1 and result[1] else f"user_{user_id_int}@system.local"
|
||
user.username = result[2] if len(result) > 2 and result[2] else user.email.split('@')[0]
|
||
user.password_hash = result[3] if len(result) > 3 and result[3] else ""
|
||
user.name = result[4] if len(result) > 4 and result[4] else f"User {user_id_int}"
|
||
user.role = result[5] if len(result) > 5 and result[5] else "user"
|
||
user.active = result[6] if len(result) > 6 and result[6] is not None else True
|
||
|
||
# Erweiterte Felder (optional)
|
||
user.created_at = result[7] if len(result) > 7 and result[7] else datetime.now()
|
||
user.last_login = result[8] if len(result) > 8 else None
|
||
user.updated_at = result[9] if len(result) > 9 and result[9] else datetime.now()
|
||
user.settings = result[10] if len(result) > 10 else None
|
||
user.department = result[11] if len(result) > 11 else None
|
||
user.position = result[12] if len(result) > 12 else None
|
||
user.phone = result[13] if len(result) > 13 else None
|
||
user.bio = result[14] if len(result) > 14 else None
|
||
user.last_activity = result[15] if len(result) > 15 else datetime.now()
|
||
|
||
app_logger.info(f"User {user_id_int} erfolgreich über manuelle Abfrage geladen")
|
||
db_session.close()
|
||
return user
|
||
|
||
except Exception as manual_error:
|
||
app_logger.error(f"Auch manuelle User-Abfrage fehlgeschlagen: {str(manual_error)}")
|
||
|
||
# Letzter Fallback: Minimale User-Daten erstellen
|
||
try:
|
||
# Prüfen ob User überhaupt existiert
|
||
exists_result = db_session.execute(
|
||
text("SELECT COUNT(*) FROM users WHERE id = :user_id"),
|
||
{"user_id": user_id_int}
|
||
).fetchone()
|
||
|
||
if exists_result and exists_result[0] > 0:
|
||
# User existiert, aber Schema ist korrupt - Notfall-User erstellen
|
||
user = User()
|
||
user.id = user_id_int
|
||
user.email = f"user_{user_id_int}@system.local"
|
||
user.username = f"user_{user_id_int}"
|
||
user.password_hash = ""
|
||
user.name = f"User {user_id_int}"
|
||
user.role = "user"
|
||
user.active = True
|
||
user.created_at = datetime.now()
|
||
user.last_login = None
|
||
user.updated_at = datetime.now()
|
||
|
||
app_logger.warning(f"Notfall-User-Objekt für ID {user_id_int} erstellt")
|
||
db_session.close()
|
||
return user
|
||
except Exception as fallback_error:
|
||
app_logger.error(f"Auch Fallback-User-Erstellung fehlgeschlagen: {str(fallback_error)}")
|
||
|
||
db_session.close()
|
||
return None
|
||
|
||
except Exception as e:
|
||
app_logger.error(f"Kritischer Fehler im User-Loader für ID {user_id}: {str(e)}")
|
||
return None
|
||
|
||
# Jinja2 Context Processors
|
||
@app.context_processor
|
||
def inject_now():
|
||
"""Inject the current datetime into templates."""
|
||
return {'now': datetime.now()}
|
||
|
||
# Custom Jinja2 filter für Datumsformatierung
|
||
@app.template_filter('format_datetime')
|
||
def format_datetime_filter(value, format='%d.%m.%Y %H:%M'):
|
||
"""Format a datetime object to a German-style date and time string"""
|
||
if value is None:
|
||
return ""
|
||
if isinstance(value, str):
|
||
try:
|
||
value = datetime.fromisoformat(value)
|
||
except ValueError:
|
||
return value
|
||
return value.strftime(format)
|
||
|
||
# Logging initialisieren
|
||
setup_logging()
|
||
log_startup_info()
|
||
|
||
# app_logger für verschiedene Komponenten
|
||
app_logger = get_logger("app")
|
||
auth_logger = get_logger("auth")
|
||
jobs_logger = get_logger("jobs")
|
||
printers_logger = get_logger("printers")
|
||
user_logger = get_logger("user")
|
||
kiosk_logger = get_logger("kiosk")
|
||
|
||
# HTTP-Request/Response-Middleware für automatisches Debug-Logging
|
||
@app.before_request
|
||
def log_request_info():
|
||
"""Loggt detaillierte Informationen über eingehende HTTP-Anfragen."""
|
||
# Nur für API-Endpunkte und wenn Debug-Level aktiviert ist
|
||
if request.path.startswith('/api/') or app_logger.level <= logging.DEBUG:
|
||
debug_request(app_logger, request)
|
||
|
||
@app.after_request
|
||
def log_response_info(response):
|
||
"""Loggt detaillierte Informationen über ausgehende HTTP-Antworten."""
|
||
# Nur für API-Endpunkte und wenn Debug-Level aktiviert ist
|
||
if request.path.startswith('/api/') or app_logger.level <= logging.DEBUG:
|
||
# Berechne Response-Zeit aus dem g-Objekt wenn verfügbar
|
||
duration_ms = None
|
||
if hasattr(request, '_start_time'):
|
||
duration_ms = (time.time() - request._start_time) * 1000
|
||
|
||
debug_response(app_logger, response, duration_ms)
|
||
|
||
return response
|
||
|
||
# Start-Zeit für Request-Timing setzen
|
||
@app.before_request
|
||
def start_timer():
|
||
"""Setzt einen Timer für die Request-Bearbeitung."""
|
||
request._start_time = time.time()
|
||
|
||
# Sicheres Passwort-Hash für Kiosk-Deaktivierung
|
||
KIOSK_PASSWORD_HASH = generate_password_hash("744563017196A")
|
||
|
||
print("Alle Blueprints wurden in app.py integriert")
|
||
|
||
# Custom decorator für Job-Besitzer-Check
|
||
def job_owner_required(f):
|
||
@wraps(f)
|
||
def decorated_function(job_id, *args, **kwargs):
|
||
db_session = get_db_session()
|
||
job = db_session.query(Job).filter(Job.id == job_id).first()
|
||
|
||
if not job:
|
||
db_session.close()
|
||
return jsonify({"error": "Job nicht gefunden"}), 404
|
||
|
||
is_owner = job.user_id == int(current_user.id) or job.owner_id == int(current_user.id)
|
||
is_admin = current_user.is_admin
|
||
|
||
if not (is_owner or is_admin):
|
||
db_session.close()
|
||
return jsonify({"error": "Keine Berechtigung"}), 403
|
||
|
||
db_session.close()
|
||
return f(job_id, *args, **kwargs)
|
||
return decorated_function
|
||
|
||
# Custom decorator für Admin-Check
|
||
def admin_required(f):
|
||
@wraps(f)
|
||
@login_required
|
||
def decorated_function(*args, **kwargs):
|
||
app_logger.info(f"Admin-Check für Funktion {f.__name__}: User authenticated: {current_user.is_authenticated}, User ID: {current_user.id if current_user.is_authenticated else 'None'}, Is Admin: {current_user.is_admin if current_user.is_authenticated else 'None'}")
|
||
if not current_user.is_admin:
|
||
app_logger.warning(f"Admin-Zugriff verweigert für User {current_user.id if current_user.is_authenticated else 'Anonymous'} auf Funktion {f.__name__}")
|
||
return jsonify({"error": "Nur Administratoren haben Zugriff"}), 403
|
||
return f(*args, **kwargs)
|
||
return decorated_function
|
||
|
||
# ===== AUTHENTIFIZIERUNGS-ROUTEN (ehemals auth.py) =====
|
||
|
||
@app.route("/auth/login", methods=["GET", "POST"])
|
||
def login():
|
||
if current_user.is_authenticated:
|
||
return redirect(url_for("index"))
|
||
|
||
error = None
|
||
if request.method == "POST":
|
||
# Debug-Logging für Request-Details
|
||
auth_logger.debug(f"Login-Request: Content-Type={request.content_type}, Headers={dict(request.headers)}")
|
||
|
||
# Erweiterte Content-Type-Erkennung für AJAX-Anfragen
|
||
content_type = request.content_type or ""
|
||
is_json_request = (
|
||
request.is_json or
|
||
"application/json" in content_type or
|
||
request.headers.get('X-Requested-With') == 'XMLHttpRequest' or
|
||
request.headers.get('Accept', '').startswith('application/json')
|
||
)
|
||
|
||
# Robuste Datenextraktion
|
||
username = None
|
||
password = None
|
||
remember_me = False
|
||
|
||
try:
|
||
if is_json_request:
|
||
# JSON-Request verarbeiten
|
||
try:
|
||
data = request.get_json(force=True) or {}
|
||
username = data.get("username") or data.get("email")
|
||
password = data.get("password")
|
||
remember_me = data.get("remember_me", False)
|
||
except Exception as json_error:
|
||
auth_logger.warning(f"JSON-Parsing fehlgeschlagen: {str(json_error)}")
|
||
# Fallback zu Form-Daten
|
||
username = request.form.get("email")
|
||
password = request.form.get("password")
|
||
remember_me = request.form.get("remember_me") == "on"
|
||
else:
|
||
# Form-Request verarbeiten
|
||
username = request.form.get("email")
|
||
password = request.form.get("password")
|
||
remember_me = request.form.get("remember_me") == "on"
|
||
|
||
# Zusätzlicher Fallback für verschiedene Feldnamen
|
||
if not username:
|
||
username = request.form.get("username") or request.values.get("email") or request.values.get("username")
|
||
if not password:
|
||
password = request.form.get("password") or request.values.get("password")
|
||
|
||
except Exception as extract_error:
|
||
auth_logger.error(f"Fehler beim Extrahieren der Login-Daten: {str(extract_error)}")
|
||
error = "Fehler beim Verarbeiten der Anmeldedaten."
|
||
if is_json_request:
|
||
return jsonify({"error": error, "success": False}), 400
|
||
|
||
if not username or not password:
|
||
error = "E-Mail-Adresse und Passwort müssen angegeben werden."
|
||
auth_logger.warning(f"Unvollständige Login-Daten: username={bool(username)}, password={bool(password)}")
|
||
if is_json_request:
|
||
return jsonify({"error": error, "success": False}), 400
|
||
else:
|
||
db_session = None
|
||
try:
|
||
db_session = get_db_session()
|
||
# Suche nach Benutzer mit übereinstimmendem Benutzernamen oder E-Mail
|
||
user = db_session.query(User).filter(
|
||
(User.username == username) | (User.email == username)
|
||
).first()
|
||
|
||
if user and user.check_password(password):
|
||
# Update last login timestamp
|
||
user.update_last_login()
|
||
db_session.commit()
|
||
|
||
login_user(user, remember=remember_me)
|
||
auth_logger.info(f"Benutzer {username} hat sich erfolgreich angemeldet")
|
||
|
||
next_page = request.args.get("next")
|
||
|
||
if is_json_request:
|
||
return jsonify({
|
||
"success": True,
|
||
"message": "Anmeldung erfolgreich",
|
||
"redirect_url": next_page or url_for("index")
|
||
})
|
||
else:
|
||
if next_page:
|
||
return redirect(next_page)
|
||
return redirect(url_for("index"))
|
||
else:
|
||
error = "Ungültige E-Mail-Adresse oder Passwort."
|
||
auth_logger.warning(f"Fehlgeschlagener Login-Versuch für Benutzer {username}")
|
||
|
||
if is_json_request:
|
||
return jsonify({"error": error, "success": False}), 401
|
||
except Exception as e:
|
||
# Fehlerbehandlung für Datenbankprobleme
|
||
error = "Anmeldefehler. Bitte versuchen Sie es später erneut."
|
||
auth_logger.error(f"Fehler bei der Anmeldung: {str(e)}")
|
||
if is_json_request:
|
||
return jsonify({"error": error, "success": False}), 500
|
||
finally:
|
||
# Sicherstellen, dass die Datenbankverbindung geschlossen wird
|
||
if db_session:
|
||
try:
|
||
db_session.close()
|
||
except Exception as close_error:
|
||
auth_logger.error(f"Fehler beim Schließen der DB-Session: {str(close_error)}")
|
||
|
||
return render_template("login.html", error=error)
|
||
|
||
@app.route("/auth/logout", methods=["GET", "POST"])
|
||
@login_required
|
||
def auth_logout():
|
||
"""Meldet den Benutzer ab."""
|
||
app_logger.info(f"Benutzer {current_user.email} hat sich abgemeldet")
|
||
logout_user()
|
||
flash("Sie wurden erfolgreich abgemeldet.", "info")
|
||
return redirect(url_for("login"))
|
||
|
||
@app.route("/auth/reset-password-request", methods=["GET", "POST"])
|
||
def reset_password_request():
|
||
"""Passwort-Reset anfordern (Placeholder)."""
|
||
# TODO: Implement password reset functionality
|
||
flash("Passwort-Reset-Funktionalität ist noch nicht implementiert.", "info")
|
||
return redirect(url_for("login"))
|
||
|
||
@app.route("/auth/api/login", methods=["POST"])
|
||
def api_login():
|
||
"""API-Login-Endpunkt für Frontend"""
|
||
try:
|
||
data = request.get_json()
|
||
if not data:
|
||
return jsonify({"error": "Keine Daten erhalten"}), 400
|
||
|
||
username = data.get("username")
|
||
password = data.get("password")
|
||
remember_me = data.get("remember_me", False)
|
||
|
||
if not username or not password:
|
||
return jsonify({"error": "Benutzername und Passwort müssen angegeben werden"}), 400
|
||
|
||
db_session = get_db_session()
|
||
user = db_session.query(User).filter(
|
||
(User.username == username) | (User.email == username)
|
||
).first()
|
||
|
||
if user and user.check_password(password):
|
||
# Update last login timestamp
|
||
user.update_last_login()
|
||
db_session.commit()
|
||
|
||
login_user(user, remember=remember_me)
|
||
auth_logger.info(f"API-Login erfolgreich für Benutzer {username}")
|
||
|
||
user_data = {
|
||
"id": user.id,
|
||
"username": user.username,
|
||
"name": user.name,
|
||
"email": user.email,
|
||
"is_admin": user.is_admin
|
||
}
|
||
|
||
db_session.close()
|
||
return jsonify({
|
||
"success": True,
|
||
"user": user_data,
|
||
"redirect_url": url_for("index")
|
||
})
|
||
else:
|
||
auth_logger.warning(f"Fehlgeschlagener API-Login für Benutzer {username}")
|
||
db_session.close()
|
||
return jsonify({"error": "Ungültiger Benutzername oder Passwort"}), 401
|
||
|
||
except Exception as e:
|
||
auth_logger.error(f"Fehler beim API-Login: {str(e)}")
|
||
return jsonify({"error": "Anmeldefehler. Bitte versuchen Sie es später erneut"}), 500
|
||
|
||
@app.route("/auth/api/callback", methods=["GET", "POST"])
|
||
def api_callback():
|
||
"""OAuth-Callback-Endpunkt für externe Authentifizierung"""
|
||
try:
|
||
# OAuth-Provider bestimmen
|
||
provider = request.args.get('provider', 'github')
|
||
|
||
if request.method == "GET":
|
||
# Authorization Code aus URL-Parameter extrahieren
|
||
code = request.args.get('code')
|
||
state = request.args.get('state')
|
||
error = request.args.get('error')
|
||
|
||
if error:
|
||
auth_logger.warning(f"OAuth-Fehler von {provider}: {error}")
|
||
return jsonify({
|
||
"error": f"OAuth-Authentifizierung fehlgeschlagen: {error}",
|
||
"redirect_url": url_for("login")
|
||
}), 400
|
||
|
||
if not code:
|
||
auth_logger.warning(f"Kein Authorization Code von {provider} erhalten")
|
||
return jsonify({
|
||
"error": "Kein Authorization Code erhalten",
|
||
"redirect_url": url_for("login")
|
||
}), 400
|
||
|
||
# State-Parameter validieren (CSRF-Schutz)
|
||
session_state = session.get('oauth_state')
|
||
if not state or state != session_state:
|
||
auth_logger.warning(f"Ungültiger State-Parameter von {provider}")
|
||
return jsonify({
|
||
"error": "Ungültiger State-Parameter",
|
||
"redirect_url": url_for("login")
|
||
}), 400
|
||
|
||
# OAuth-Token austauschen
|
||
if provider == 'github':
|
||
user_data = handle_github_callback(code)
|
||
else:
|
||
auth_logger.error(f"Unbekannter OAuth-Provider: {provider}")
|
||
return jsonify({
|
||
"error": "Unbekannter OAuth-Provider",
|
||
"redirect_url": url_for("login")
|
||
}), 400
|
||
|
||
if not user_data:
|
||
return jsonify({
|
||
"error": "Fehler beim Abrufen der Benutzerdaten",
|
||
"redirect_url": url_for("login")
|
||
}), 400
|
||
|
||
# Benutzer in Datenbank suchen oder erstellen
|
||
db_session = get_db_session()
|
||
try:
|
||
user = db_session.query(User).filter(
|
||
User.email == user_data['email']
|
||
).first()
|
||
|
||
if not user:
|
||
# Neuen Benutzer erstellen
|
||
user = User(
|
||
username=user_data['username'],
|
||
email=user_data['email'],
|
||
name=user_data['name'],
|
||
is_admin=False,
|
||
oauth_provider=provider,
|
||
oauth_id=str(user_data['id'])
|
||
)
|
||
# Zufälliges Passwort setzen (wird nicht verwendet)
|
||
import secrets
|
||
user.set_password(secrets.token_urlsafe(32))
|
||
db_session.add(user)
|
||
db_session.commit()
|
||
auth_logger.info(f"Neuer OAuth-Benutzer erstellt: {user.username} via {provider}")
|
||
else:
|
||
# Bestehenden Benutzer aktualisieren
|
||
user.oauth_provider = provider
|
||
user.oauth_id = str(user_data['id'])
|
||
user.name = user_data['name']
|
||
user.updated_at = datetime.now()
|
||
db_session.commit()
|
||
auth_logger.info(f"OAuth-Benutzer aktualisiert: {user.username} via {provider}")
|
||
|
||
# Update last login timestamp
|
||
user.update_last_login()
|
||
db_session.commit()
|
||
|
||
login_user(user, remember=True)
|
||
|
||
# Session-State löschen
|
||
session.pop('oauth_state', None)
|
||
|
||
response_data = {
|
||
"success": True,
|
||
"user": {
|
||
"id": user.id,
|
||
"username": user.username,
|
||
"name": user.name,
|
||
"email": user.email,
|
||
"is_admin": user.is_admin
|
||
},
|
||
"redirect_url": url_for("index")
|
||
}
|
||
|
||
db_session.close()
|
||
return jsonify(response_data)
|
||
|
||
except Exception as e:
|
||
db_session.rollback()
|
||
db_session.close()
|
||
auth_logger.error(f"Datenbankfehler bei OAuth-Callback: {str(e)}")
|
||
return jsonify({
|
||
"error": "Datenbankfehler bei der Benutzeranmeldung",
|
||
"redirect_url": url_for("login")
|
||
}), 500
|
||
|
||
elif request.method == "POST":
|
||
# POST-Anfragen für manuelle Token-Übermittlung
|
||
data = request.get_json()
|
||
if not data:
|
||
return jsonify({"error": "Keine Daten erhalten"}), 400
|
||
|
||
access_token = data.get('access_token')
|
||
provider = data.get('provider', 'github')
|
||
|
||
if not access_token:
|
||
return jsonify({"error": "Kein Access Token erhalten"}), 400
|
||
|
||
# Benutzerdaten mit Access Token abrufen
|
||
if provider == 'github':
|
||
user_data = get_github_user_data(access_token)
|
||
else:
|
||
return jsonify({"error": "Unbekannter OAuth-Provider"}), 400
|
||
|
||
if not user_data:
|
||
return jsonify({"error": "Fehler beim Abrufen der Benutzerdaten"}), 400
|
||
|
||
# Benutzer verarbeiten (gleiche Logik wie bei GET)
|
||
db_session = get_db_session()
|
||
try:
|
||
user = db_session.query(User).filter(
|
||
User.email == user_data['email']
|
||
).first()
|
||
|
||
if not user:
|
||
user = User(
|
||
username=user_data['username'],
|
||
email=user_data['email'],
|
||
name=user_data['name'],
|
||
is_admin=False,
|
||
oauth_provider=provider,
|
||
oauth_id=str(user_data['id'])
|
||
)
|
||
import secrets
|
||
user.set_password(secrets.token_urlsafe(32))
|
||
db_session.add(user)
|
||
db_session.commit()
|
||
auth_logger.info(f"Neuer OAuth-Benutzer erstellt: {user.username} via {provider}")
|
||
else:
|
||
user.oauth_provider = provider
|
||
user.oauth_id = str(user_data['id'])
|
||
user.name = user_data['name']
|
||
user.updated_at = datetime.now()
|
||
db_session.commit()
|
||
auth_logger.info(f"OAuth-Benutzer aktualisiert: {user.username} via {provider}")
|
||
|
||
# Update last login timestamp
|
||
user.update_last_login()
|
||
db_session.commit()
|
||
|
||
login_user(user, remember=True)
|
||
|
||
response_data = {
|
||
"success": True,
|
||
"user": {
|
||
"id": user.id,
|
||
"username": user.username,
|
||
"name": user.name,
|
||
"email": user.email,
|
||
"is_admin": user.is_admin
|
||
},
|
||
"redirect_url": url_for("index")
|
||
}
|
||
|
||
db_session.close()
|
||
return jsonify(response_data)
|
||
|
||
except Exception as e:
|
||
db_session.rollback()
|
||
db_session.close()
|
||
auth_logger.error(f"Datenbankfehler bei OAuth-Callback: {str(e)}")
|
||
return jsonify({
|
||
"error": "Datenbankfehler bei der Benutzeranmeldung",
|
||
"redirect_url": url_for("login")
|
||
}), 500
|
||
|
||
except Exception as e:
|
||
auth_logger.error(f"Fehler im OAuth-Callback: {str(e)}")
|
||
return jsonify({
|
||
"error": "OAuth-Callback-Fehler",
|
||
"redirect_url": url_for("login")
|
||
}), 500
|
||
|
||
def handle_github_callback(code):
|
||
"""GitHub OAuth-Callback verarbeiten"""
|
||
try:
|
||
import requests
|
||
|
||
# GitHub OAuth-Konfiguration (sollte aus Umgebungsvariablen kommen)
|
||
client_id = "7c5d8bef1a5519ec1fdc"
|
||
client_secret = "5f1e586204358fbd53cf5fb7d418b3f06ccab8fd"
|
||
|
||
if not client_id or not client_secret:
|
||
auth_logger.error("GitHub OAuth-Konfiguration fehlt")
|
||
return None
|
||
|
||
# Access Token anfordern
|
||
token_url = "https://github.com/login/oauth/access_token"
|
||
token_data = {
|
||
'client_id': client_id,
|
||
'client_secret': client_secret,
|
||
'code': code
|
||
}
|
||
|
||
token_response = requests.post(
|
||
token_url,
|
||
data=token_data,
|
||
headers={'Accept': 'application/json'},
|
||
timeout=10
|
||
)
|
||
|
||
if token_response.status_code != 200:
|
||
auth_logger.error(f"GitHub Token-Anfrage fehlgeschlagen: {token_response.status_code}")
|
||
return None
|
||
|
||
token_json = token_response.json()
|
||
access_token = token_json.get('access_token')
|
||
|
||
if not access_token:
|
||
auth_logger.error("Kein Access Token von GitHub erhalten")
|
||
return None
|
||
|
||
return get_github_user_data(access_token)
|
||
|
||
except Exception as e:
|
||
auth_logger.error(f"Fehler bei GitHub OAuth-Callback: {str(e)}")
|
||
return None
|
||
|
||
def get_github_user_data(access_token):
|
||
"""GitHub-Benutzerdaten mit Access Token abrufen"""
|
||
try:
|
||
import requests
|
||
|
||
# Benutzerdaten von GitHub API abrufen
|
||
user_url = "https://api.github.com/user"
|
||
headers = {
|
||
'Authorization': f'token {access_token}',
|
||
'Accept': 'application/vnd.github.v3+json'
|
||
}
|
||
|
||
user_response = requests.get(user_url, headers=headers, timeout=10)
|
||
|
||
if user_response.status_code != 200:
|
||
auth_logger.error(f"GitHub User-API-Anfrage fehlgeschlagen: {user_response.status_code}")
|
||
return None
|
||
|
||
user_data = user_response.json()
|
||
|
||
# E-Mail-Adresse separat abrufen (falls nicht öffentlich)
|
||
email = user_data.get('email')
|
||
if not email:
|
||
email_url = "https://api.github.com/user/emails"
|
||
email_response = requests.get(email_url, headers=headers, timeout=10)
|
||
|
||
if email_response.status_code == 200:
|
||
emails = email_response.json()
|
||
# Primäre E-Mail-Adresse finden
|
||
for email_obj in emails:
|
||
if email_obj.get('primary', False):
|
||
email = email_obj.get('email')
|
||
break
|
||
|
||
# Fallback: Erste E-Mail-Adresse verwenden
|
||
if not email and emails:
|
||
email = emails[0].get('email')
|
||
|
||
if not email:
|
||
auth_logger.error("Keine E-Mail-Adresse von GitHub erhalten")
|
||
return None
|
||
|
||
return {
|
||
'id': user_data.get('id'),
|
||
'username': user_data.get('login'),
|
||
'name': user_data.get('name') or user_data.get('login'),
|
||
'email': email
|
||
}
|
||
|
||
except Exception as e:
|
||
auth_logger.error(f"Fehler beim Abrufen der GitHub-Benutzerdaten: {str(e)}")
|
||
return None
|
||
|
||
# ===== BENUTZER-ROUTEN (ehemals user.py) =====
|
||
|
||
@app.route("/user/profile", methods=["GET"])
|
||
@login_required
|
||
def user_profile():
|
||
"""Profil-Seite anzeigen"""
|
||
user_logger.info(f"Benutzer {current_user.username} hat seine Profilseite aufgerufen")
|
||
return render_template("profile.html", user=current_user)
|
||
|
||
@app.route("/user/settings", methods=["GET"])
|
||
@login_required
|
||
def user_settings():
|
||
"""Einstellungen-Seite anzeigen"""
|
||
user_logger.info(f"Benutzer {current_user.username} hat seine Einstellungsseite aufgerufen")
|
||
return render_template("settings.html", user=current_user)
|
||
|
||
@app.route("/user/update-profile", methods=["POST"])
|
||
@login_required
|
||
def user_update_profile():
|
||
"""Benutzerprofilinformationen aktualisieren"""
|
||
try:
|
||
# Überprüfen, ob es sich um eine JSON-Anfrage handelt
|
||
is_json_request = request.is_json or request.headers.get('Content-Type') == 'application/json'
|
||
|
||
if is_json_request:
|
||
data = request.get_json()
|
||
name = data.get("name")
|
||
email = data.get("email")
|
||
department = data.get("department")
|
||
position = data.get("position")
|
||
phone = data.get("phone")
|
||
else:
|
||
name = request.form.get("name")
|
||
email = request.form.get("email")
|
||
department = request.form.get("department")
|
||
position = request.form.get("position")
|
||
phone = request.form.get("phone")
|
||
|
||
db_session = get_db_session()
|
||
user = db_session.query(User).filter(User.id == int(current_user.id)).first()
|
||
|
||
if user:
|
||
# Aktualisiere die Benutzerinformationen
|
||
if name:
|
||
user.name = name
|
||
if email:
|
||
user.email = email
|
||
if department:
|
||
user.department = department
|
||
if position:
|
||
user.position = position
|
||
if phone:
|
||
user.phone = phone
|
||
|
||
user.updated_at = datetime.now()
|
||
db_session.commit()
|
||
user_logger.info(f"Benutzer {current_user.username} hat sein Profil aktualisiert")
|
||
|
||
if is_json_request:
|
||
return jsonify({
|
||
"success": True,
|
||
"message": "Profil erfolgreich aktualisiert"
|
||
})
|
||
else:
|
||
flash("Profil erfolgreich aktualisiert", "success")
|
||
return redirect(url_for("user_profile"))
|
||
else:
|
||
error = "Benutzer nicht gefunden."
|
||
if is_json_request:
|
||
return jsonify({"error": error}), 404
|
||
else:
|
||
flash(error, "error")
|
||
return redirect(url_for("user_profile"))
|
||
|
||
except Exception as e:
|
||
error = f"Fehler beim Aktualisieren des Profils: {str(e)}"
|
||
user_logger.error(error)
|
||
if request.is_json:
|
||
return jsonify({"error": error}), 500
|
||
else:
|
||
flash(error, "error")
|
||
return redirect(url_for("user_profile"))
|
||
finally:
|
||
db_session.close()
|
||
|
||
@app.route("/user/api/update-settings", methods=["POST"])
|
||
@login_required
|
||
def user_api_update_settings():
|
||
"""API-Endpunkt für Einstellungen-Updates (JSON)"""
|
||
return user_update_profile()
|
||
|
||
@app.route("/user/update-settings", methods=["POST"])
|
||
@login_required
|
||
def user_update_settings():
|
||
"""Benutzereinstellungen aktualisieren"""
|
||
db_session = get_db_session()
|
||
try:
|
||
# Überprüfen, ob es sich um eine JSON-Anfrage handelt
|
||
is_json_request = request.is_json or request.headers.get('Content-Type') == 'application/json'
|
||
|
||
# Einstellungen aus der Anfrage extrahieren
|
||
if is_json_request:
|
||
data = request.get_json()
|
||
if not data:
|
||
return jsonify({"error": "Keine Daten empfangen"}), 400
|
||
|
||
theme = data.get("theme", "system")
|
||
reduced_motion = bool(data.get("reduced_motion", False))
|
||
contrast = data.get("contrast", "normal")
|
||
notifications = data.get("notifications", {})
|
||
privacy = data.get("privacy", {})
|
||
else:
|
||
theme = request.form.get("theme", "system")
|
||
reduced_motion = request.form.get("reduced_motion") == "on"
|
||
contrast = request.form.get("contrast", "normal")
|
||
notifications = {
|
||
"new_jobs": request.form.get("notify_new_jobs") == "on",
|
||
"job_updates": request.form.get("notify_job_updates") == "on",
|
||
"system": request.form.get("notify_system") == "on",
|
||
"email": request.form.get("notify_email") == "on"
|
||
}
|
||
privacy = {
|
||
"activity_logs": request.form.get("activity_logs") == "on",
|
||
"two_factor": request.form.get("two_factor") == "on",
|
||
"auto_logout": int(request.form.get("auto_logout", "60"))
|
||
}
|
||
|
||
# Validierung der Eingaben
|
||
valid_themes = ["light", "dark", "system"]
|
||
if theme not in valid_themes:
|
||
theme = "system"
|
||
|
||
valid_contrasts = ["normal", "high"]
|
||
if contrast not in valid_contrasts:
|
||
contrast = "normal"
|
||
|
||
# Benutzer aus der Datenbank laden
|
||
user = db_session.query(User).filter(User.id == int(current_user.id)).first()
|
||
|
||
if not user:
|
||
error = "Benutzer nicht gefunden."
|
||
if is_json_request:
|
||
return jsonify({"error": error}), 404
|
||
else:
|
||
flash(error, "error")
|
||
return redirect(url_for("user_settings"))
|
||
|
||
# Einstellungen-Dictionary erstellen
|
||
settings = {
|
||
"theme": theme,
|
||
"reduced_motion": reduced_motion,
|
||
"contrast": contrast,
|
||
"notifications": {
|
||
"new_jobs": bool(notifications.get("new_jobs", True)),
|
||
"job_updates": bool(notifications.get("job_updates", True)),
|
||
"system": bool(notifications.get("system", True)),
|
||
"email": bool(notifications.get("email", False))
|
||
},
|
||
"privacy": {
|
||
"activity_logs": bool(privacy.get("activity_logs", True)),
|
||
"two_factor": bool(privacy.get("two_factor", False)),
|
||
"auto_logout": max(5, min(480, int(privacy.get("auto_logout", 60)))) # 5-480 Minuten
|
||
},
|
||
"last_updated": datetime.now().isoformat()
|
||
}
|
||
|
||
# Prüfen, ob User-Tabelle eine settings-Spalte hat
|
||
if hasattr(user, 'settings'):
|
||
# Einstellungen in der Datenbank speichern
|
||
import json
|
||
user.settings = json.dumps(settings)
|
||
else:
|
||
# Fallback: In Session speichern (temporär)
|
||
session['user_settings'] = settings
|
||
|
||
user.updated_at = datetime.now()
|
||
db_session.commit()
|
||
|
||
user_logger.info(f"Benutzer {current_user.username} hat seine Einstellungen aktualisiert")
|
||
|
||
if is_json_request:
|
||
return jsonify({
|
||
"success": True,
|
||
"message": "Einstellungen erfolgreich aktualisiert",
|
||
"settings": settings
|
||
})
|
||
else:
|
||
flash("Einstellungen erfolgreich aktualisiert", "success")
|
||
return redirect(url_for("user_settings"))
|
||
|
||
except ValueError as e:
|
||
error = f"Ungültige Eingabedaten: {str(e)}"
|
||
user_logger.warning(f"Ungültige Einstellungsdaten von Benutzer {current_user.username}: {str(e)}")
|
||
if is_json_request:
|
||
return jsonify({"error": error}), 400
|
||
else:
|
||
flash(error, "error")
|
||
return redirect(url_for("user_settings"))
|
||
except Exception as e:
|
||
db_session.rollback()
|
||
error = f"Fehler beim Aktualisieren der Einstellungen: {str(e)}"
|
||
user_logger.error(f"Fehler beim Aktualisieren der Einstellungen für Benutzer {current_user.username}: {str(e)}")
|
||
if is_json_request:
|
||
return jsonify({"error": "Interner Serverfehler"}), 500
|
||
else:
|
||
flash("Fehler beim Speichern der Einstellungen", "error")
|
||
return redirect(url_for("user_settings"))
|
||
finally:
|
||
db_session.close()
|
||
|
||
@app.route("/api/user/settings", methods=["GET"])
|
||
@login_required
|
||
def get_user_settings():
|
||
"""Holt die aktuellen Benutzereinstellungen"""
|
||
try:
|
||
# Einstellungen aus Session oder Datenbank laden
|
||
user_settings = session.get('user_settings', {})
|
||
|
||
# Standard-Einstellungen falls keine vorhanden
|
||
default_settings = {
|
||
"theme": "system",
|
||
"reduced_motion": False,
|
||
"contrast": "normal",
|
||
"notifications": {
|
||
"new_jobs": True,
|
||
"job_updates": True,
|
||
"system": True,
|
||
"email": False
|
||
},
|
||
"privacy": {
|
||
"activity_logs": True,
|
||
"two_factor": False,
|
||
"auto_logout": 60
|
||
}
|
||
}
|
||
|
||
# Merge mit Standard-Einstellungen
|
||
settings = {**default_settings, **user_settings}
|
||
|
||
return jsonify({
|
||
"success": True,
|
||
"settings": settings
|
||
})
|
||
|
||
except Exception as e:
|
||
user_logger.error(f"Fehler beim Laden der Benutzereinstellungen: {str(e)}")
|
||
return jsonify({
|
||
"success": False,
|
||
"error": "Fehler beim Laden der Einstellungen"
|
||
}), 500
|
||
|
||
@app.route("/user/change-password", methods=["POST"])
|
||
@login_required
|
||
def user_change_password():
|
||
"""Benutzerpasswort ändern"""
|
||
try:
|
||
# Überprüfen, ob es sich um eine JSON-Anfrage handelt
|
||
is_json_request = request.is_json or request.headers.get('Content-Type') == 'application/json'
|
||
|
||
if is_json_request:
|
||
data = request.get_json()
|
||
current_password = data.get("current_password")
|
||
new_password = data.get("new_password")
|
||
confirm_password = data.get("confirm_password")
|
||
else:
|
||
current_password = request.form.get("current_password")
|
||
new_password = request.form.get("new_password")
|
||
confirm_password = request.form.get("confirm_password")
|
||
|
||
# Prüfen, ob alle Felder ausgefüllt sind
|
||
if not current_password or not new_password or not confirm_password:
|
||
error = "Alle Passwortfelder müssen ausgefüllt sein."
|
||
if is_json_request:
|
||
return jsonify({"error": error}), 400
|
||
else:
|
||
flash(error, "error")
|
||
return redirect(url_for("user_profile"))
|
||
|
||
# Prüfen, ob das neue Passwort und die Bestätigung übereinstimmen
|
||
if new_password != confirm_password:
|
||
error = "Das neue Passwort und die Bestätigung stimmen nicht überein."
|
||
if is_json_request:
|
||
return jsonify({"error": error}), 400
|
||
else:
|
||
flash(error, "error")
|
||
return redirect(url_for("user_profile"))
|
||
|
||
db_session = get_db_session()
|
||
user = db_session.query(User).filter(User.id == int(current_user.id)).first()
|
||
|
||
if user and user.check_password(current_password):
|
||
# Passwort aktualisieren
|
||
user.set_password(new_password)
|
||
user.updated_at = datetime.now()
|
||
db_session.commit()
|
||
|
||
user_logger.info(f"Benutzer {current_user.username} hat sein Passwort geändert")
|
||
|
||
if is_json_request:
|
||
return jsonify({
|
||
"success": True,
|
||
"message": "Passwort erfolgreich geändert"
|
||
})
|
||
else:
|
||
flash("Passwort erfolgreich geändert", "success")
|
||
return redirect(url_for("user_profile"))
|
||
else:
|
||
error = "Das aktuelle Passwort ist nicht korrekt."
|
||
if is_json_request:
|
||
return jsonify({"error": error}), 401
|
||
else:
|
||
flash(error, "error")
|
||
return redirect(url_for("user_profile"))
|
||
|
||
except Exception as e:
|
||
error = f"Fehler beim Ändern des Passworts: {str(e)}"
|
||
user_logger.error(error)
|
||
if request.is_json:
|
||
return jsonify({"error": error}), 500
|
||
else:
|
||
flash(error, "error")
|
||
return redirect(url_for("user_profile"))
|
||
finally:
|
||
db_session.close()
|
||
|
||
@app.route("/user/export", methods=["GET"])
|
||
@login_required
|
||
def user_export_data():
|
||
"""Exportiert alle Benutzerdaten als JSON für DSGVO-Konformität"""
|
||
try:
|
||
db_session = get_db_session()
|
||
user = db_session.query(User).filter(User.id == int(current_user.id)).first()
|
||
|
||
if not user:
|
||
db_session.close()
|
||
return jsonify({"error": "Benutzer nicht gefunden"}), 404
|
||
|
||
# Benutzerdaten abrufen
|
||
user_data = user.to_dict()
|
||
|
||
# Jobs des Benutzers abrufen
|
||
jobs = db_session.query(Job).filter(Job.user_id == user.id).all()
|
||
user_data["jobs"] = [job.to_dict() for job in jobs]
|
||
|
||
# Aktivitäten und Einstellungen hinzufügen
|
||
user_data["settings"] = session.get('user_settings', {})
|
||
|
||
# Persönliche Statistiken
|
||
user_data["statistics"] = {
|
||
"total_jobs": len(jobs),
|
||
"completed_jobs": len([j for j in jobs if j.status == "finished"]),
|
||
"failed_jobs": len([j for j in jobs if j.status == "failed"]),
|
||
"account_created": user.created_at.isoformat() if user.created_at else None,
|
||
"last_login": user.last_login.isoformat() if user.last_login else None
|
||
}
|
||
|
||
db_session.close()
|
||
|
||
# Daten als JSON-Datei zum Download anbieten
|
||
response = make_response(json.dumps(user_data, indent=4))
|
||
response.headers["Content-Disposition"] = f"attachment; filename=user_data_{user.username}.json"
|
||
response.headers["Content-Type"] = "application/json"
|
||
|
||
user_logger.info(f"Benutzer {current_user.username} hat seine Daten exportiert")
|
||
return response
|
||
|
||
except Exception as e:
|
||
error = f"Fehler beim Exportieren der Benutzerdaten: {str(e)}"
|
||
user_logger.error(error)
|
||
return jsonify({"error": error}), 500
|
||
|
||
@app.route("/user/profile", methods=["PUT"])
|
||
@login_required
|
||
def user_update_profile_api():
|
||
"""API-Endpunkt zum Aktualisieren des Benutzerprofils"""
|
||
try:
|
||
if not request.is_json:
|
||
return jsonify({"error": "Anfrage muss im JSON-Format sein"}), 400
|
||
|
||
data = request.get_json()
|
||
db_session = get_db_session()
|
||
user = db_session.query(User).filter(User.id == int(current_user.id)).first()
|
||
|
||
if not user:
|
||
db_session.close()
|
||
return jsonify({"error": "Benutzer nicht gefunden"}), 404
|
||
|
||
# Aktualisiere nur die bereitgestellten Felder
|
||
if "name" in data:
|
||
user.name = data["name"]
|
||
if "email" in data:
|
||
user.email = data["email"]
|
||
if "department" in data:
|
||
user.department = data["department"]
|
||
if "position" in data:
|
||
user.position = data["position"]
|
||
if "phone" in data:
|
||
user.phone = data["phone"]
|
||
if "bio" in data:
|
||
user.bio = data["bio"]
|
||
|
||
user.updated_at = datetime.now()
|
||
db_session.commit()
|
||
|
||
# Aktualisierte Benutzerdaten zurückgeben
|
||
user_data = user.to_dict()
|
||
db_session.close()
|
||
|
||
user_logger.info(f"Benutzer {current_user.username} hat sein Profil über die API aktualisiert")
|
||
return jsonify({
|
||
"success": True,
|
||
"message": "Profil erfolgreich aktualisiert",
|
||
"user": user_data
|
||
})
|
||
|
||
except Exception as e:
|
||
error = f"Fehler beim Aktualisieren des Profils: {str(e)}"
|
||
user_logger.error(error)
|
||
return jsonify({"error": error}), 500
|
||
|
||
# ===== KIOSK-KONTROLL-ROUTEN (ehemals kiosk_control.py) =====
|
||
|
||
@app.route('/api/kiosk/status', methods=['GET'])
|
||
def kiosk_get_status():
|
||
"""Kiosk-Status abrufen."""
|
||
try:
|
||
# Prüfen ob Kiosk-Modus aktiv ist
|
||
kiosk_active = os.path.exists('/tmp/kiosk_active')
|
||
|
||
return jsonify({
|
||
"active": kiosk_active,
|
||
"message": "Kiosk-Status erfolgreich abgerufen"
|
||
})
|
||
except Exception as e:
|
||
kiosk_logger.error(f"Fehler beim Abrufen des Kiosk-Status: {str(e)}")
|
||
return jsonify({"error": "Fehler beim Abrufen des Status"}), 500
|
||
|
||
@app.route('/api/kiosk/deactivate', methods=['POST'])
|
||
def kiosk_deactivate():
|
||
"""Kiosk-Modus mit Passwort deaktivieren."""
|
||
try:
|
||
data = request.get_json()
|
||
if not data or 'password' not in data:
|
||
return jsonify({"error": "Passwort erforderlich"}), 400
|
||
|
||
password = data['password']
|
||
|
||
# Passwort überprüfen
|
||
if not check_password_hash(KIOSK_PASSWORD_HASH, password):
|
||
kiosk_logger.warning(f"Fehlgeschlagener Kiosk-Deaktivierungsversuch von IP: {request.remote_addr}")
|
||
return jsonify({"error": "Ungültiges Passwort"}), 401
|
||
|
||
# Kiosk deaktivieren
|
||
try:
|
||
# Kiosk-Service stoppen
|
||
subprocess.run(['sudo', 'systemctl', 'stop', 'myp-kiosk'], check=True)
|
||
subprocess.run(['sudo', 'systemctl', 'disable', 'myp-kiosk'], check=True)
|
||
|
||
# Kiosk-Marker entfernen
|
||
if os.path.exists('/tmp/kiosk_active'):
|
||
os.remove('/tmp/kiosk_active')
|
||
|
||
# Normale Desktop-Umgebung wiederherstellen
|
||
subprocess.run(['sudo', 'systemctl', 'set-default', 'graphical.target'], check=True)
|
||
|
||
kiosk_logger.info(f"Kiosk-Modus erfolgreich deaktiviert von IP: {request.remote_addr}")
|
||
|
||
return jsonify({
|
||
"success": True,
|
||
"message": "Kiosk-Modus erfolgreich deaktiviert. System wird neu gestartet."
|
||
})
|
||
|
||
except subprocess.CalledProcessError as e:
|
||
kiosk_logger.error(f"Fehler beim Deaktivieren des Kiosk-Modus: {str(e)}")
|
||
return jsonify({"error": "Fehler beim Deaktivieren des Kiosk-Modus"}), 500
|
||
|
||
except Exception as e:
|
||
kiosk_logger.error(f"Unerwarteter Fehler bei Kiosk-Deaktivierung: {str(e)}")
|
||
return jsonify({"error": "Unerwarteter Fehler"}), 500
|
||
@app.route('/api/kiosk/activate', methods=['POST'])
|
||
@login_required
|
||
def kiosk_activate():
|
||
"""Kiosk-Modus aktivieren (nur für Admins)."""
|
||
try:
|
||
# Admin-Authentifizierung prüfen
|
||
if not current_user.is_admin:
|
||
kiosk_logger.warning(f"Nicht-Admin-Benutzer {current_user.username} versuchte Kiosk-Aktivierung")
|
||
return jsonify({"error": "Nur Administratoren können den Kiosk-Modus aktivieren"}), 403
|
||
|
||
# Kiosk aktivieren
|
||
try:
|
||
# Kiosk-Marker setzen
|
||
with open('/tmp/kiosk_active', 'w') as f:
|
||
f.write('1')
|
||
|
||
# Kiosk-Service aktivieren
|
||
subprocess.run(['sudo', 'systemctl', 'enable', 'myp-kiosk'], check=True)
|
||
subprocess.run(['sudo', 'systemctl', 'start', 'myp-kiosk'], check=True)
|
||
|
||
kiosk_logger.info(f"Kiosk-Modus erfolgreich aktiviert von Admin {current_user.username} (IP: {request.remote_addr})")
|
||
|
||
return jsonify({
|
||
"success": True,
|
||
"message": "Kiosk-Modus erfolgreich aktiviert"
|
||
})
|
||
|
||
except subprocess.CalledProcessError as e:
|
||
kiosk_logger.error(f"Fehler beim Aktivieren des Kiosk-Modus: {str(e)}")
|
||
return jsonify({"error": "Fehler beim Aktivieren des Kiosk-Modus"}), 500
|
||
|
||
except Exception as e:
|
||
kiosk_logger.error(f"Unerwarteter Fehler bei Kiosk-Aktivierung: {str(e)}")
|
||
return jsonify({"error": "Unerwarteter Fehler"}), 500
|
||
|
||
@app.route('/api/kiosk/restart', methods=['POST'])
|
||
def kiosk_restart_system():
|
||
"""System neu starten (nur nach Kiosk-Deaktivierung)."""
|
||
try:
|
||
data = request.get_json()
|
||
if not data or 'password' not in data:
|
||
return jsonify({"error": "Passwort erforderlich"}), 400
|
||
|
||
password = data['password']
|
||
|
||
# Passwort überprüfen
|
||
if not check_password_hash(KIOSK_PASSWORD_HASH, password):
|
||
kiosk_logger.warning(f"Fehlgeschlagener Neustart-Versuch von IP: {request.remote_addr}")
|
||
return jsonify({"error": "Ungültiges Passwort"}), 401
|
||
|
||
kiosk_logger.info(f"System-Neustart initiiert von IP: {request.remote_addr}")
|
||
|
||
# System nach kurzer Verzögerung neu starten
|
||
subprocess.Popen(['sudo', 'shutdown', '-r', '+1'])
|
||
|
||
return jsonify({
|
||
"success": True,
|
||
"message": "System wird in 1 Minute neu gestartet"
|
||
})
|
||
|
||
except Exception as e:
|
||
kiosk_logger.error(f"Fehler beim System-Neustart: {str(e)}")
|
||
return jsonify({"error": "Fehler beim Neustart"}), 500
|
||
|
||
# ===== HILFSFUNKTIONEN =====
|
||
|
||
@measure_execution_time(logger=printers_logger, task_name="Drucker-Status-Prüfung")
|
||
def check_printer_status(ip_address: str, timeout: int = 7) -> Tuple[str, bool]:
|
||
"""
|
||
Überprüft den Status eines Druckers anhand der Steckdosen-Logik:
|
||
- Steckdose erreichbar aber AUS = Drucker ONLINE (bereit zum Drucken)
|
||
- Steckdose erreichbar und AN = Drucker PRINTING (druckt gerade)
|
||
- Steckdose nicht erreichbar = Drucker OFFLINE (kritischer Fehler)
|
||
|
||
Args:
|
||
ip_address: IP-Adresse des Druckers oder der Steckdose
|
||
timeout: Timeout in Sekunden
|
||
|
||
Returns:
|
||
Tuple[str, bool]: (Status, Erreichbarkeit)
|
||
"""
|
||
status = "offline"
|
||
reachable = False
|
||
|
||
try:
|
||
# Überprüfen, ob die Steckdose erreichbar ist
|
||
import socket
|
||
|
||
# Erst Port 9999 versuchen (Tapo-Standard)
|
||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||
sock.settimeout(timeout)
|
||
result = sock.connect_ex((ip_address, 9999))
|
||
sock.close()
|
||
|
||
if result == 0:
|
||
reachable = True
|
||
try:
|
||
# TP-Link Tapo Steckdose mit PyP100 überprüfen
|
||
from PyP100 import PyP100
|
||
p100 = PyP100.P100(ip_address, TAPO_USERNAME, TAPO_PASSWORD)
|
||
p100.handshake() # Authentifizierung
|
||
p100.login() # Login
|
||
|
||
# Geräteinformationen abrufen
|
||
device_info = p100.getDeviceInfo()
|
||
|
||
# 🎯 KORREKTE LOGIK: Status auswerten
|
||
if device_info.get('device_on', False):
|
||
# Steckdose an = Drucker PRINTING (druckt gerade)
|
||
status = "printing"
|
||
printers_logger.info(f"🖨️ Drucker {ip_address}: PRINTING (Steckdose an - druckt gerade)")
|
||
else:
|
||
# Steckdose aus = Drucker ONLINE (bereit zum Drucken)
|
||
status = "online"
|
||
printers_logger.info(f"✅ Drucker {ip_address}: ONLINE (Steckdose aus - bereit zum Drucken)")
|
||
|
||
except Exception as e:
|
||
printers_logger.error(f"❌ Fehler bei Tapo-Status-Check für {ip_address}: {str(e)}")
|
||
reachable = False
|
||
status = "error"
|
||
else:
|
||
# Steckdose nicht erreichbar = kritischer Fehler
|
||
printers_logger.warning(f"❌ Drucker {ip_address}: OFFLINE (Steckdose nicht erreichbar)")
|
||
reachable = False
|
||
status = "offline"
|
||
|
||
except Exception as e:
|
||
printers_logger.error(f"❌ Unerwarteter Fehler bei Status-Check für {ip_address}: {str(e)}")
|
||
reachable = False
|
||
status = "error"
|
||
|
||
return status, reachable
|
||
|
||
@measure_execution_time(logger=printers_logger, task_name="Mehrere-Drucker-Status-Prüfung")
|
||
def check_multiple_printers_status(printers: List[Dict], timeout: int = 7) -> Dict[int, Tuple[str, bool]]:
|
||
"""
|
||
Überprüft den Status mehrerer Drucker parallel.
|
||
|
||
Args:
|
||
printers: Liste der zu prüfenden Drucker
|
||
timeout: Timeout für jeden einzelnen Drucker
|
||
|
||
Returns:
|
||
Dict[int, Tuple[str, bool]]: Dictionary mit Drucker-ID als Key und (Status, Aktiv) als Value
|
||
"""
|
||
results = {}
|
||
|
||
# Wenn keine Drucker vorhanden sind, gebe leeres Dict zurück
|
||
if not printers:
|
||
printers_logger.info("ℹ️ Keine Drucker zum Status-Check gefunden")
|
||
return results
|
||
|
||
printers_logger.info(f"🔍 Prüfe Status von {len(printers)} Druckern parallel...")
|
||
|
||
# Parallel-Ausführung mit ThreadPoolExecutor
|
||
# Sicherstellen, dass max_workers mindestens 1 ist
|
||
max_workers = min(max(len(printers), 1), 10)
|
||
|
||
with ThreadPoolExecutor(max_workers=max_workers) as executor:
|
||
# Futures für alle Drucker erstellen
|
||
future_to_printer = {
|
||
executor.submit(check_printer_status, printer.get('ip_address'), timeout): printer
|
||
for printer in printers
|
||
}
|
||
|
||
# Ergebnisse sammeln
|
||
for future in as_completed(future_to_printer, timeout=timeout + 2):
|
||
printer = future_to_printer[future]
|
||
try:
|
||
status, active = future.result()
|
||
results[printer['id']] = (status, active)
|
||
printers_logger.info(f"Drucker {printer['name']} ({printer.get('ip_address')}): {status}")
|
||
except Exception as e:
|
||
printers_logger.error(f"Fehler bei Status-Check für Drucker {printer['name']}: {str(e)}")
|
||
results[printer['id']] = ("offline", False)
|
||
|
||
printers_logger.info(f"✅ Status-Check abgeschlossen für {len(results)} Drucker")
|
||
|
||
return results
|
||
|
||
# ===== UI-ROUTEN =====
|
||
|
||
@app.route("/")
|
||
def index():
|
||
if current_user.is_authenticated:
|
||
return render_template("index.html")
|
||
return redirect(url_for("login"))
|
||
|
||
@app.route("/dashboard")
|
||
@login_required
|
||
def dashboard():
|
||
return render_template("dashboard.html")
|
||
|
||
@app.route("/profile")
|
||
@login_required
|
||
def profile_redirect():
|
||
"""Leitet zur neuen Profilseite im User-Blueprint weiter."""
|
||
return redirect(url_for("user_profile"))
|
||
|
||
@app.route("/profil")
|
||
@login_required
|
||
def profil_redirect():
|
||
"""Leitet zur neuen Profilseite im User-Blueprint weiter (deutsche URL)."""
|
||
return redirect(url_for("user_profile"))
|
||
|
||
@app.route("/settings")
|
||
@login_required
|
||
def settings_redirect():
|
||
"""Leitet zur neuen Einstellungsseite im User-Blueprint weiter."""
|
||
return redirect(url_for("user_settings"))
|
||
|
||
@app.route("/einstellungen")
|
||
@login_required
|
||
def einstellungen_redirect():
|
||
"""Leitet zur neuen Einstellungsseite im User-Blueprint weiter (deutsche URL)."""
|
||
return redirect(url_for("user_settings"))
|
||
|
||
@app.route("/admin")
|
||
@login_required
|
||
def admin():
|
||
if not current_user.is_admin:
|
||
flash("Nur Administratoren haben Zugriff auf diesen Bereich.", "error")
|
||
return redirect(url_for("index"))
|
||
|
||
return render_template("admin.html")
|
||
|
||
@app.route("/socket-test")
|
||
@login_required
|
||
@admin_required
|
||
def socket_test():
|
||
"""
|
||
Steckdosen-Test-Seite für Ausbilder und Administratoren.
|
||
"""
|
||
app_logger.info(f"Admin {current_user.name} hat die Steckdosen-Test-Seite aufgerufen")
|
||
return render_template("socket_test.html")
|
||
|
||
@app.route("/demo")
|
||
@login_required
|
||
def components_demo():
|
||
"""Demo-Seite für UI-Komponenten"""
|
||
return render_template("components_demo.html")
|
||
|
||
@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")
|
||
|
||
@app.route("/admin-dashboard")
|
||
@login_required
|
||
def admin_page():
|
||
"""Erweiterte Admin-Dashboard-Seite mit Live-Funktionen"""
|
||
if not current_user.is_admin:
|
||
return redirect(url_for("index"))
|
||
return render_template("admin_dashboard.html", title="Admin Dashboard")
|
||
|
||
# ===== NEUE SYSTEM UI-ROUTEN =====
|
||
|
||
@app.route("/dashboard/realtime")
|
||
@login_required
|
||
def realtime_dashboard():
|
||
"""Echtzeit-Dashboard mit WebSocket-Updates"""
|
||
return render_template("realtime_dashboard.html", title="Echtzeit-Dashboard")
|
||
|
||
@app.route("/reports")
|
||
@login_required
|
||
def reports_page():
|
||
"""Reports-Generierung-Seite"""
|
||
return render_template("reports.html", title="Reports")
|
||
|
||
@app.route("/maintenance")
|
||
@login_required
|
||
def maintenance_page():
|
||
"""Wartungs-Management-Seite"""
|
||
return render_template("maintenance.html", title="Wartung")
|
||
|
||
@app.route("/locations")
|
||
@login_required
|
||
@admin_required
|
||
def locations_page():
|
||
"""Multi-Standort-Management-Seite"""
|
||
return render_template("locations.html", title="Standorte")
|
||
|
||
@app.route("/validation-demo")
|
||
@login_required
|
||
def validation_demo():
|
||
"""Formular-Validierung Demo-Seite"""
|
||
return render_template("validation_demo.html", title="Formular-Validierung Demo")
|
||
|
||
@app.route("/tables-demo")
|
||
@login_required
|
||
def tables_demo():
|
||
"""Advanced Tables Demo-Seite"""
|
||
return render_template("tables_demo.html", title="Erweiterte Tabellen Demo")
|
||
|
||
@app.route("/dragdrop-demo")
|
||
@login_required
|
||
def dragdrop_demo():
|
||
"""Drag & Drop Demo-Seite"""
|
||
return render_template("dragdrop_demo.html", title="Drag & Drop Demo")
|
||
|
||
# ===== ERROR MONITORING SYSTEM =====
|
||
|
||
@app.route("/api/admin/system-health", methods=['GET'])
|
||
@login_required
|
||
@admin_required
|
||
def api_admin_system_health():
|
||
"""API-Endpunkt für System-Gesundheitscheck mit Dashboard-Integration."""
|
||
try:
|
||
# Basis-System-Gesundheitscheck durchführen
|
||
critical_errors = []
|
||
warnings = []
|
||
|
||
# Dashboard-Event für System-Check senden
|
||
emit_system_alert(
|
||
"System-Gesundheitscheck durchgeführt",
|
||
alert_type="info",
|
||
priority="normal"
|
||
)
|
||
|
||
return jsonify({
|
||
"success": True,
|
||
"health_status": "healthy",
|
||
"critical_errors": critical_errors,
|
||
"warnings": warnings,
|
||
"timestamp": datetime.now().isoformat()
|
||
})
|
||
|
||
except Exception as e:
|
||
app_logger.error(f"Fehler beim System-Gesundheitscheck: {str(e)}")
|
||
return jsonify({
|
||
"success": False,
|
||
"error": str(e)
|
||
}), 500
|
||
|
||
@app.route("/api/admin/system-health-dashboard", methods=['GET'])
|
||
@login_required
|
||
@admin_required
|
||
def api_admin_system_health_dashboard():
|
||
"""API-Endpunkt für System-Gesundheitscheck mit Dashboard-Integration."""
|
||
try:
|
||
# Basis-System-Gesundheitscheck durchführen
|
||
critical_errors = []
|
||
warnings = []
|
||
|
||
# Dashboard-Event für System-Check senden
|
||
emit_system_alert(
|
||
"System-Gesundheitscheck durchgeführt",
|
||
alert_type="info",
|
||
priority="normal"
|
||
)
|
||
|
||
return jsonify({
|
||
"success": True,
|
||
"health_status": "healthy",
|
||
"critical_errors": critical_errors,
|
||
"warnings": warnings,
|
||
"timestamp": datetime.now().isoformat()
|
||
})
|
||
|
||
except Exception as e:
|
||
app_logger.error(f"Fehler beim System-Gesundheitscheck: {str(e)}")
|
||
return jsonify({
|
||
"success": False,
|
||
"error": str(e)
|
||
}), 500
|
||
|
||
def admin_printer_settings_page(printer_id):
|
||
"""Zeigt die Drucker-Einstellungsseite an."""
|
||
if not current_user.is_admin:
|
||
flash("Sie haben keine Berechtigung für den Admin-Bereich.", "error")
|
||
return redirect(url_for("index"))
|
||
|
||
db_session = get_db_session()
|
||
try:
|
||
printer = db_session.get(Printer, printer_id)
|
||
if not printer:
|
||
flash("Drucker nicht gefunden.", "error")
|
||
return redirect(url_for("admin_page"))
|
||
|
||
printer_data = {
|
||
"id": printer.id,
|
||
"name": printer.name,
|
||
"model": printer.model or 'Unbekanntes Modell',
|
||
"location": printer.location or 'Unbekannter Standort',
|
||
"mac_address": printer.mac_address,
|
||
"plug_ip": printer.plug_ip,
|
||
"status": printer.status or "offline",
|
||
"active": printer.active if hasattr(printer, 'active') else True,
|
||
"created_at": printer.created_at.isoformat() if printer.created_at else datetime.now().isoformat()
|
||
}
|
||
|
||
db_session.close()
|
||
return render_template("admin_printer_settings.html", printer=printer_data)
|
||
|
||
except Exception as e:
|
||
db_session.close()
|
||
app_logger.error(f"Fehler beim Laden der Drucker-Einstellungen: {str(e)}")
|
||
flash("Fehler beim Laden der Drucker-Daten.", "error")
|
||
return redirect(url_for("admin_page"))
|
||
|
||
@app.route("/admin/guest-requests")
|
||
@login_required
|
||
@admin_required
|
||
def admin_guest_requests():
|
||
"""Admin-Seite für Gastanfragen Verwaltung"""
|
||
try:
|
||
app_logger.info(f"Admin-Gastanfragen Seite aufgerufen von User {current_user.id}")
|
||
return render_template("admin_guest_requests.html")
|
||
except Exception as e:
|
||
app_logger.error(f"Fehler beim Laden der Admin-Gastanfragen Seite: {str(e)}")
|
||
flash("Fehler beim Laden der Gastanfragen-Verwaltung.", "danger")
|
||
return redirect(url_for("admin"))
|
||
|
||
@app.route("/requests/overview")
|
||
@login_required
|
||
@admin_required
|
||
def admin_guest_requests_overview():
|
||
"""Admin-Oberfläche für die Verwaltung von Gastanfragen mit direkten Aktionen."""
|
||
try:
|
||
app_logger.info(f"Admin-Gastanträge Übersicht aufgerufen von User {current_user.id}")
|
||
return render_template("admin_guest_requests_overview.html")
|
||
except Exception as e:
|
||
app_logger.error(f"Fehler beim Laden der Admin-Gastanträge Übersicht: {str(e)}")
|
||
flash("Fehler beim Laden der Gastanträge-Übersicht.", "danger")
|
||
return redirect(url_for("admin"))
|
||
|
||
# ===== ADMIN API-ROUTEN FÜR BENUTZER UND DRUCKER =====
|
||
|
||
@app.route("/api/admin/users", methods=["POST"])
|
||
@login_required
|
||
def create_user_api():
|
||
"""Erstellt einen neuen Benutzer (nur für Admins)."""
|
||
if not current_user.is_admin:
|
||
return jsonify({"error": "Nur Administratoren können Benutzer erstellen"}), 403
|
||
|
||
try:
|
||
data = request.json
|
||
|
||
# Pflichtfelder prüfen
|
||
required_fields = ["username", "email", "password"]
|
||
for field in required_fields:
|
||
if field not in data or not data[field]:
|
||
return jsonify({"error": f"Feld '{field}' ist erforderlich"}), 400
|
||
|
||
db_session = get_db_session()
|
||
|
||
# Prüfen, ob bereits ein Benutzer mit diesem Benutzernamen oder E-Mail existiert
|
||
existing_user = db_session.query(User).filter(
|
||
(User.username == data["username"]) | (User.email == data["email"])
|
||
).first()
|
||
|
||
if existing_user:
|
||
db_session.close()
|
||
return jsonify({"error": "Ein Benutzer mit diesem Benutzernamen oder E-Mail existiert bereits"}), 400
|
||
|
||
# Neuen Benutzer erstellen
|
||
new_user = User(
|
||
username=data["username"],
|
||
email=data["email"],
|
||
first_name=data.get("first_name", ""),
|
||
last_name=data.get("last_name", ""),
|
||
is_admin=data.get("is_admin", False),
|
||
created_at=datetime.now()
|
||
)
|
||
|
||
# Passwort setzen
|
||
new_user.set_password(data["password"])
|
||
|
||
db_session.add(new_user)
|
||
db_session.commit()
|
||
|
||
user_data = {
|
||
"id": new_user.id,
|
||
"username": new_user.username,
|
||
"email": new_user.email,
|
||
"first_name": new_user.first_name,
|
||
"last_name": new_user.last_name,
|
||
"is_admin": new_user.is_admin,
|
||
"created_at": new_user.created_at.isoformat()
|
||
}
|
||
|
||
db_session.close()
|
||
|
||
user_logger.info(f"Neuer Benutzer '{new_user.username}' erstellt von Admin {current_user.id}")
|
||
return jsonify({"user": user_data}), 201
|
||
|
||
except Exception as e:
|
||
user_logger.error(f"Fehler beim Erstellen eines Benutzers: {str(e)}")
|
||
return jsonify({"error": "Interner Serverfehler"}), 500
|
||
|
||
@app.route("/api/admin/printers/<int:printer_id>/toggle", methods=["POST"])
|
||
@login_required
|
||
def toggle_printer_power(printer_id):
|
||
"""
|
||
Schaltet einen Drucker über die zugehörige Steckdose ein/aus.
|
||
"""
|
||
if not current_user.is_admin:
|
||
return jsonify({"error": "Administratorrechte erforderlich"}), 403
|
||
|
||
try:
|
||
# Robuste JSON-Datenverarbeitung
|
||
data = {}
|
||
try:
|
||
if request.is_json and request.get_json():
|
||
data = request.get_json()
|
||
elif request.form:
|
||
# Fallback für Form-Daten
|
||
data = request.form.to_dict()
|
||
except Exception as json_error:
|
||
printers_logger.warning(f"Fehler beim Parsen der JSON-Daten für Drucker {printer_id}: {str(json_error)}")
|
||
# Verwende Standard-Werte wenn JSON-Parsing fehlschlägt
|
||
data = {}
|
||
|
||
# Standard-Zustand ermitteln (Toggle-Verhalten)
|
||
db_session = get_db_session()
|
||
printer = db_session.get(Printer, printer_id) # Modernized from query().get()
|
||
|
||
if not printer:
|
||
db_session.close()
|
||
return jsonify({"error": "Drucker nicht gefunden"}), 404
|
||
|
||
# Aktuellen Status ermitteln für Toggle-Verhalten
|
||
current_status = getattr(printer, 'status', 'offline')
|
||
current_active = getattr(printer, 'active', False)
|
||
|
||
# Zielzustand bestimmen
|
||
if 'state' in data:
|
||
# Expliziter Zustand angegeben
|
||
state = bool(data.get("state", True))
|
||
else:
|
||
# Toggle-Verhalten: Umschalten basierend auf aktuellem Status
|
||
state = not (current_status == "available" and current_active)
|
||
|
||
db_session.close()
|
||
|
||
# Steckdose schalten
|
||
from utils.job_scheduler import toggle_plug
|
||
success = toggle_plug(printer_id, state)
|
||
|
||
if success:
|
||
action = "eingeschaltet" if state else "ausgeschaltet"
|
||
printers_logger.info(f"Drucker {printer.name} (ID: {printer_id}) erfolgreich {action} von Admin {current_user.name}")
|
||
|
||
return jsonify({
|
||
"success": True,
|
||
"message": f"Drucker erfolgreich {action}",
|
||
"printer_id": printer_id,
|
||
"printer_name": printer.name,
|
||
"state": state,
|
||
"action": action
|
||
})
|
||
else:
|
||
printers_logger.error(f"Fehler beim Schalten der Steckdose für Drucker {printer_id}")
|
||
return jsonify({
|
||
"success": False,
|
||
"error": "Fehler beim Schalten der Steckdose",
|
||
"printer_id": printer_id
|
||
}), 500
|
||
|
||
except Exception as e:
|
||
printers_logger.error(f"Fehler beim Schalten von Drucker {printer_id}: {str(e)}")
|
||
return jsonify({
|
||
"success": False,
|
||
"error": "Interner Serverfehler",
|
||
"details": str(e)
|
||
}), 500
|
||
|
||
@app.route("/api/admin/printers/<int:printer_id>/test-tapo", methods=["POST"])
|
||
@login_required
|
||
@admin_required
|
||
def test_printer_tapo_connection(printer_id):
|
||
"""
|
||
Testet die Tapo-Steckdosen-Verbindung für einen Drucker.
|
||
"""
|
||
try:
|
||
db_session = get_db_session()
|
||
printer = db_session.query(Printer).get(printer_id)
|
||
|
||
if not printer:
|
||
db_session.close()
|
||
return jsonify({"error": "Drucker nicht gefunden"}), 404
|
||
|
||
if not printer.plug_ip or not printer.plug_username or not printer.plug_password:
|
||
db_session.close()
|
||
return jsonify({
|
||
"error": "Unvollständige Tapo-Konfiguration",
|
||
"missing": [
|
||
key for key, value in {
|
||
"plug_ip": printer.plug_ip,
|
||
"plug_username": printer.plug_username,
|
||
"plug_password": printer.plug_password
|
||
}.items() if not value
|
||
]
|
||
}), 400
|
||
|
||
db_session.close()
|
||
|
||
# Tapo-Verbindung testen
|
||
from utils.job_scheduler import test_tapo_connection
|
||
test_result = test_tapo_connection(
|
||
printer.plug_ip,
|
||
printer.plug_username,
|
||
printer.plug_password
|
||
)
|
||
|
||
return jsonify({
|
||
"printer_id": printer_id,
|
||
"printer_name": printer.name,
|
||
"tapo_test": test_result
|
||
})
|
||
|
||
except Exception as e:
|
||
printers_logger.error(f"Fehler beim Testen der Tapo-Verbindung für Drucker {printer_id}: {str(e)}")
|
||
return jsonify({"error": "Interner Serverfehler beim Verbindungstest"}), 500
|
||
|
||
@app.route("/api/admin/printers/test-all-tapo", methods=["POST"])
|
||
@login_required
|
||
@admin_required
|
||
def test_all_printers_tapo_connection():
|
||
"""
|
||
Testet die Tapo-Steckdosen-Verbindung für alle Drucker.
|
||
Nützlich für Diagnose und Setup-Validierung.
|
||
"""
|
||
try:
|
||
db_session = get_db_session()
|
||
printers = db_session.query(Printer).filter(Printer.active == True).all()
|
||
db_session.close()
|
||
|
||
if not printers:
|
||
return jsonify({
|
||
"message": "Keine aktiven Drucker gefunden",
|
||
"results": []
|
||
})
|
||
|
||
# Alle Drucker testen
|
||
from utils.job_scheduler import test_tapo_connection
|
||
results = []
|
||
|
||
for printer in printers:
|
||
result = {
|
||
"printer_id": printer.id,
|
||
"printer_name": printer.name,
|
||
"plug_ip": printer.plug_ip,
|
||
"has_config": bool(printer.plug_ip and printer.plug_username and printer.plug_password)
|
||
}
|
||
|
||
if result["has_config"]:
|
||
# Tapo-Verbindung testen
|
||
test_result = test_tapo_connection(
|
||
printer.plug_ip,
|
||
printer.plug_username,
|
||
printer.plug_password
|
||
)
|
||
result["tapo_test"] = test_result
|
||
else:
|
||
result["tapo_test"] = {
|
||
"success": False,
|
||
"error": "Unvollständige Tapo-Konfiguration",
|
||
"device_info": None,
|
||
"status": "unconfigured"
|
||
}
|
||
result["missing_config"] = [
|
||
key for key, value in {
|
||
"plug_ip": printer.plug_ip,
|
||
"plug_username": printer.plug_username,
|
||
"plug_password": printer.plug_password
|
||
}.items() if not value
|
||
]
|
||
|
||
results.append(result)
|
||
|
||
# Zusammenfassung erstellen
|
||
total_printers = len(results)
|
||
successful_connections = sum(1 for r in results if r["tapo_test"]["success"])
|
||
configured_printers = sum(1 for r in results if r["has_config"])
|
||
|
||
return jsonify({
|
||
"summary": {
|
||
"total_printers": total_printers,
|
||
"configured_printers": configured_printers,
|
||
"successful_connections": successful_connections,
|
||
"success_rate": round(successful_connections / total_printers * 100, 1) if total_printers > 0 else 0
|
||
},
|
||
"results": results
|
||
})
|
||
|
||
except Exception as e:
|
||
printers_logger.error(f"Fehler beim Testen aller Tapo-Verbindungen: {str(e)}")
|
||
return jsonify({"error": "Interner Serverfehler beim Massentest"}), 500
|
||
|
||
# ===== ADMIN FORM ENDPOINTS =====
|
||
|
||
@app.route("/admin/users/create", methods=["POST"])
|
||
@login_required
|
||
def admin_create_user_form():
|
||
"""Erstellt einen neuen Benutzer über HTML-Form (nur für Admins)."""
|
||
if not current_user.is_admin:
|
||
flash("Sie haben keine Berechtigung für den Admin-Bereich.", "error")
|
||
return redirect(url_for("index"))
|
||
|
||
try:
|
||
# Form-Daten lesen
|
||
email = request.form.get("email", "").strip()
|
||
name = request.form.get("name", "").strip()
|
||
password = request.form.get("password", "").strip()
|
||
role = request.form.get("role", "user").strip()
|
||
|
||
# Pflichtfelder prüfen
|
||
if not email or not password:
|
||
flash("E-Mail und Passwort sind erforderlich.", "error")
|
||
return redirect(url_for("admin_add_user_page"))
|
||
|
||
# E-Mail validieren
|
||
import re
|
||
email_pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
|
||
if not re.match(email_pattern, email):
|
||
flash("Ungültige E-Mail-Adresse.", "error")
|
||
return redirect(url_for("admin_add_user_page"))
|
||
|
||
db_session = get_db_session()
|
||
|
||
# Prüfen, ob bereits ein Benutzer mit dieser E-Mail existiert
|
||
existing_user = db_session.query(User).filter(User.email == email).first()
|
||
if existing_user:
|
||
db_session.close()
|
||
flash("Ein Benutzer mit dieser E-Mail existiert bereits.", "error")
|
||
return redirect(url_for("admin_add_user_page"))
|
||
|
||
# E-Mail als Username verwenden (falls kein separates Username-Feld)
|
||
username = email.split('@')[0]
|
||
counter = 1
|
||
original_username = username
|
||
while db_session.query(User).filter(User.username == username).first():
|
||
username = f"{original_username}{counter}"
|
||
counter += 1
|
||
|
||
# Neuen Benutzer erstellen
|
||
new_user = User(
|
||
username=username,
|
||
email=email,
|
||
first_name=name.split(' ')[0] if name else "",
|
||
last_name=" ".join(name.split(' ')[1:]) if name and ' ' in name else "",
|
||
is_admin=(role == "admin"),
|
||
created_at=datetime.now()
|
||
)
|
||
|
||
# Passwort setzen
|
||
new_user.set_password(password)
|
||
|
||
db_session.add(new_user)
|
||
db_session.commit()
|
||
db_session.close()
|
||
|
||
user_logger.info(f"Neuer Benutzer '{new_user.username}' erstellt von Admin {current_user.id}")
|
||
flash(f"Benutzer '{new_user.email}' erfolgreich erstellt.", "success")
|
||
return redirect(url_for("admin_page", tab="users"))
|
||
|
||
except Exception as e:
|
||
user_logger.error(f"Fehler beim Erstellen eines Benutzers über Form: {str(e)}")
|
||
flash("Fehler beim Erstellen des Benutzers.", "error")
|
||
return redirect(url_for("admin_add_user_page"))
|
||
|
||
@app.route("/admin/printers/create", methods=["POST"])
|
||
@login_required
|
||
def admin_create_printer_form():
|
||
"""Erstellt einen neuen Drucker über HTML-Form (nur für Admins)."""
|
||
if not current_user.is_admin:
|
||
flash("Sie haben keine Berechtigung für den Admin-Bereich.", "error")
|
||
return redirect(url_for("index"))
|
||
|
||
try:
|
||
# Form-Daten lesen
|
||
name = request.form.get("name", "").strip()
|
||
ip_address = request.form.get("ip_address", "").strip()
|
||
model = request.form.get("model", "").strip()
|
||
location = request.form.get("location", "").strip()
|
||
description = request.form.get("description", "").strip()
|
||
status = request.form.get("status", "available").strip()
|
||
|
||
# Pflichtfelder prüfen
|
||
if not name or not ip_address:
|
||
flash("Name und IP-Adresse sind erforderlich.", "error")
|
||
return redirect(url_for("admin_add_printer_page"))
|
||
|
||
# IP-Adresse validieren
|
||
import re
|
||
ip_pattern = r'^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$'
|
||
if not re.match(ip_pattern, ip_address):
|
||
flash("Ungültige IP-Adresse.", "error")
|
||
return redirect(url_for("admin_add_printer_page"))
|
||
|
||
db_session = get_db_session()
|
||
|
||
# Prüfen, ob bereits ein Drucker mit diesem Namen existiert
|
||
existing_printer = db_session.query(Printer).filter(Printer.name == name).first()
|
||
if existing_printer:
|
||
db_session.close()
|
||
flash("Ein Drucker mit diesem Namen existiert bereits.", "error")
|
||
return redirect(url_for("admin_add_printer_page"))
|
||
|
||
# Neuen Drucker erstellen
|
||
new_printer = Printer(
|
||
name=name,
|
||
model=model,
|
||
location=location,
|
||
description=description,
|
||
mac_address="", # Wird später ausgefüllt
|
||
plug_ip=ip_address,
|
||
status=status,
|
||
created_at=datetime.now()
|
||
)
|
||
|
||
db_session.add(new_printer)
|
||
db_session.commit()
|
||
db_session.close()
|
||
|
||
printers_logger.info(f"Neuer Drucker '{new_printer.name}' erstellt von Admin {current_user.id}")
|
||
flash(f"Drucker '{new_printer.name}' erfolgreich erstellt.", "success")
|
||
return redirect(url_for("admin_page", tab="printers"))
|
||
|
||
except Exception as e:
|
||
printers_logger.error(f"Fehler beim Erstellen eines Druckers über Form: {str(e)}")
|
||
flash("Fehler beim Erstellen des Druckers.", "error")
|
||
return redirect(url_for("admin_add_printer_page"))
|
||
|
||
@app.route("/admin/users/<int:user_id>/edit", methods=["GET"])
|
||
@login_required
|
||
def admin_edit_user_page(user_id):
|
||
"""Zeigt die Benutzer-Bearbeitungsseite an."""
|
||
if not current_user.is_admin:
|
||
flash("Sie haben keine Berechtigung für den Admin-Bereich.", "error")
|
||
return redirect(url_for("index"))
|
||
|
||
db_session = get_db_session()
|
||
try:
|
||
user = db_session.get(User, user_id)
|
||
if not user:
|
||
flash("Benutzer nicht gefunden.", "error")
|
||
return redirect(url_for("admin_page", tab="users"))
|
||
|
||
user_data = {
|
||
"id": user.id,
|
||
"username": user.username,
|
||
"email": user.email,
|
||
"name": user.name or "",
|
||
"is_admin": user.is_admin,
|
||
"active": user.active,
|
||
"created_at": user.created_at.isoformat() if user.created_at else datetime.now().isoformat()
|
||
}
|
||
|
||
db_session.close()
|
||
return render_template("admin_edit_user.html", user=user_data)
|
||
|
||
except Exception as e:
|
||
db_session.close()
|
||
app_logger.error(f"Fehler beim Laden der Benutzer-Daten: {str(e)}")
|
||
flash("Fehler beim Laden der Benutzer-Daten.", "error")
|
||
return redirect(url_for("admin_page", tab="users"))
|
||
|
||
@app.route("/admin/users/<int:user_id>/update", methods=["POST"])
|
||
@login_required
|
||
def admin_update_user_form(user_id):
|
||
"""Aktualisiert einen Benutzer über HTML-Form (nur für Admins)."""
|
||
if not current_user.is_admin:
|
||
flash("Sie haben keine Berechtigung für den Admin-Bereich.", "error")
|
||
return redirect(url_for("index"))
|
||
|
||
try:
|
||
# Form-Daten lesen
|
||
email = request.form.get("email", "").strip()
|
||
name = request.form.get("name", "").strip()
|
||
password = request.form.get("password", "").strip()
|
||
role = request.form.get("role", "user").strip()
|
||
is_active = request.form.get("is_active", "true").strip() == "true"
|
||
|
||
# Pflichtfelder prüfen
|
||
if not email:
|
||
flash("E-Mail-Adresse ist erforderlich.", "error")
|
||
return redirect(url_for("admin_edit_user_page", user_id=user_id))
|
||
|
||
# E-Mail validieren
|
||
import re
|
||
email_pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
|
||
if not re.match(email_pattern, email):
|
||
flash("Ungültige E-Mail-Adresse.", "error")
|
||
return redirect(url_for("admin_edit_user_page", user_id=user_id))
|
||
|
||
db_session = get_db_session()
|
||
|
||
user = db_session.query(User).get(user_id)
|
||
if not user:
|
||
db_session.close()
|
||
flash("Benutzer nicht gefunden.", "error")
|
||
return redirect(url_for("admin_page", tab="users"))
|
||
|
||
# Prüfen, ob bereits ein anderer Benutzer mit dieser E-Mail existiert
|
||
existing_user = db_session.query(User).filter(
|
||
User.email == email,
|
||
User.id != user_id
|
||
).first()
|
||
if existing_user:
|
||
db_session.close()
|
||
flash("Ein Benutzer mit dieser E-Mail-Adresse existiert bereits.", "error")
|
||
return redirect(url_for("admin_edit_user_page", user_id=user_id))
|
||
|
||
# Benutzer aktualisieren
|
||
user.email = email
|
||
if name:
|
||
user.name = name
|
||
|
||
# Passwort nur ändern, wenn eines angegeben wurde
|
||
if password:
|
||
user.password_hash = generate_password_hash(password)
|
||
|
||
user.role = "admin" if role == "admin" else "user"
|
||
user.active = is_active
|
||
|
||
db_session.commit()
|
||
db_session.close()
|
||
|
||
auth_logger.info(f"Benutzer '{user.email}' (ID: {user_id}) aktualisiert von Admin {current_user.id}")
|
||
flash(f"Benutzer '{user.email}' erfolgreich aktualisiert.", "success")
|
||
return redirect(url_for("admin_page", tab="users"))
|
||
|
||
except Exception as e:
|
||
auth_logger.error(f"Fehler beim Aktualisieren eines Benutzers über Form: {str(e)}")
|
||
flash("Fehler beim Aktualisieren des Benutzers.", "error")
|
||
return redirect(url_for("admin_edit_user_page", user_id=user_id))
|
||
|
||
@app.route("/admin/printers/<int:printer_id>/update", methods=["POST"])
|
||
@login_required
|
||
def admin_update_printer_form(printer_id):
|
||
"""Aktualisiert einen Drucker über HTML-Form (nur für Admins)."""
|
||
if not current_user.is_admin:
|
||
flash("Sie haben keine Berechtigung für den Admin-Bereich.", "error")
|
||
return redirect(url_for("index"))
|
||
|
||
try:
|
||
# Form-Daten lesen
|
||
name = request.form.get("name", "").strip()
|
||
ip_address = request.form.get("ip_address", "").strip()
|
||
model = request.form.get("model", "").strip()
|
||
location = request.form.get("location", "").strip()
|
||
description = request.form.get("description", "").strip()
|
||
status = request.form.get("status", "available").strip()
|
||
|
||
# Pflichtfelder prüfen
|
||
if not name or not ip_address:
|
||
flash("Name und IP-Adresse sind erforderlich.", "error")
|
||
return redirect(url_for("admin_edit_printer_page", printer_id=printer_id))
|
||
|
||
# IP-Adresse validieren
|
||
import re
|
||
ip_pattern = r'^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$'
|
||
if not re.match(ip_pattern, ip_address):
|
||
flash("Ungültige IP-Adresse.", "error")
|
||
return redirect(url_for("admin_edit_printer_page", printer_id=printer_id))
|
||
|
||
db_session = get_db_session()
|
||
|
||
printer = db_session.query(Printer).get(printer_id)
|
||
if not printer:
|
||
db_session.close()
|
||
flash("Drucker nicht gefunden.", "error")
|
||
return redirect(url_for("admin_page", tab="printers"))
|
||
|
||
# Drucker aktualisieren
|
||
printer.name = name
|
||
printer.model = model
|
||
printer.location = location
|
||
printer.description = description
|
||
printer.plug_ip = ip_address
|
||
printer.status = status
|
||
|
||
db_session.commit()
|
||
db_session.close()
|
||
|
||
printers_logger.info(f"Drucker '{printer.name}' (ID: {printer_id}) aktualisiert von Admin {current_user.id}")
|
||
flash(f"Drucker '{printer.name}' erfolgreich aktualisiert.", "success")
|
||
return redirect(url_for("admin_page", tab="printers"))
|
||
|
||
except Exception as e:
|
||
printers_logger.error(f"Fehler beim Aktualisieren eines Druckers über Form: {str(e)}")
|
||
flash("Fehler beim Aktualisieren des Druckers.", "error")
|
||
return redirect(url_for("admin_edit_printer_page", printer_id=printer_id))
|
||
|
||
|
||
# ===== FILE-UPLOAD-ROUTEN =====
|
||
|
||
@app.route('/api/upload/job', methods=['POST'])
|
||
@login_required
|
||
def upload_job_file():
|
||
"""
|
||
Lädt eine Datei für einen Druckjob hoch
|
||
|
||
Form Data:
|
||
file: Die hochzuladende Datei
|
||
job_name: Name des Jobs (optional)
|
||
"""
|
||
try:
|
||
if 'file' not in request.files:
|
||
return jsonify({'error': 'Keine Datei ausgewählt'}), 400
|
||
|
||
file = request.files['file']
|
||
job_name = request.form.get('job_name', '')
|
||
|
||
if file.filename == '':
|
||
return jsonify({'error': 'Keine Datei ausgewählt'}), 400
|
||
|
||
# Metadaten für die Datei
|
||
metadata = {
|
||
'uploader_id': current_user.id,
|
||
'uploader_name': current_user.username,
|
||
'job_name': job_name
|
||
}
|
||
|
||
# Datei speichern
|
||
result = save_job_file(file, current_user.id, metadata)
|
||
|
||
if result:
|
||
relative_path, absolute_path, file_metadata = result
|
||
|
||
app_logger.info(f"Job-Datei hochgeladen: {file_metadata['original_filename']} von User {current_user.id}")
|
||
|
||
return jsonify({
|
||
'success': True,
|
||
'message': 'Datei erfolgreich hochgeladen',
|
||
'file_path': relative_path,
|
||
'filename': file_metadata['original_filename'],
|
||
'unique_filename': file_metadata['unique_filename'],
|
||
'file_size': file_metadata['file_size'],
|
||
'metadata': file_metadata
|
||
})
|
||
else:
|
||
return jsonify({'error': 'Fehler beim Speichern der Datei'}), 500
|
||
|
||
except Exception as e:
|
||
app_logger.error(f"Fehler beim Hochladen der Job-Datei: {str(e)}")
|
||
return jsonify({'error': f'Fehler beim Hochladen: {str(e)}'}), 500
|
||
|
||
@app.route('/api/upload/guest', methods=['POST'])
|
||
def upload_guest_file():
|
||
"""
|
||
Lädt eine Datei für einen Gastauftrag hoch
|
||
|
||
Form Data:
|
||
file: Die hochzuladende Datei
|
||
guest_name: Name des Gasts (optional)
|
||
guest_email: E-Mail des Gasts (optional)
|
||
"""
|
||
try:
|
||
if 'file' not in request.files:
|
||
return jsonify({'error': 'Keine Datei ausgewählt'}), 400
|
||
|
||
file = request.files['file']
|
||
guest_name = request.form.get('guest_name', '')
|
||
guest_email = request.form.get('guest_email', '')
|
||
|
||
if file.filename == '':
|
||
return jsonify({'error': 'Keine Datei ausgewählt'}), 400
|
||
|
||
# Metadaten für die Datei
|
||
metadata = {
|
||
'guest_name': guest_name,
|
||
'guest_email': guest_email
|
||
}
|
||
|
||
# Datei speichern
|
||
result = save_guest_file(file, metadata)
|
||
|
||
if result:
|
||
relative_path, absolute_path, file_metadata = result
|
||
|
||
app_logger.info(f"Gast-Datei hochgeladen: {file_metadata['original_filename']} für {guest_name or 'Unbekannt'}")
|
||
|
||
return jsonify({
|
||
'success': True,
|
||
'message': 'Datei erfolgreich hochgeladen',
|
||
'file_path': relative_path,
|
||
'filename': file_metadata['original_filename'],
|
||
'unique_filename': file_metadata['unique_filename'],
|
||
'file_size': file_metadata['file_size'],
|
||
'metadata': file_metadata
|
||
})
|
||
else:
|
||
return jsonify({'error': 'Fehler beim Speichern der Datei'}), 500
|
||
|
||
except Exception as e:
|
||
app_logger.error(f"Fehler beim Hochladen der Gast-Datei: {str(e)}")
|
||
return jsonify({'error': f'Fehler beim Hochladen: {str(e)}'}), 500
|
||
|
||
@app.route('/api/upload/avatar', methods=['POST'])
|
||
@login_required
|
||
def upload_avatar():
|
||
"""
|
||
Lädt ein Avatar-Bild für den aktuellen Benutzer hoch
|
||
|
||
Form Data:
|
||
file: Das Avatar-Bild
|
||
"""
|
||
try:
|
||
if 'file' not in request.files:
|
||
return jsonify({'error': 'Keine Datei ausgewählt'}), 400
|
||
|
||
file = request.files['file']
|
||
|
||
if file.filename == '':
|
||
return jsonify({'error': 'Keine Datei ausgewählt'}), 400
|
||
|
||
# Nur Bilder erlauben
|
||
allowed_extensions = {'png', 'jpg', 'jpeg', 'gif', 'webp'}
|
||
if not file.filename or '.' not in file.filename:
|
||
return jsonify({'error': 'Ungültiger Dateityp'}), 400
|
||
|
||
file_ext = file.filename.rsplit('.', 1)[1].lower()
|
||
if file_ext not in allowed_extensions:
|
||
return jsonify({'error': 'Nur Bilddateien sind erlaubt (PNG, JPG, JPEG, GIF, WebP)'}), 400
|
||
|
||
# Alte Avatar-Datei löschen falls vorhanden
|
||
db_session = get_db_session()
|
||
user = db_session.query(User).get(current_user.id)
|
||
if user and user.avatar_path:
|
||
delete_file_safe(user.avatar_path)
|
||
|
||
# Neue Avatar-Datei speichern
|
||
result = save_avatar_file(file, current_user.id)
|
||
|
||
if result:
|
||
relative_path, absolute_path, file_metadata = result
|
||
|
||
# Avatar-Pfad in der Datenbank aktualisieren
|
||
user.avatar_path = relative_path
|
||
db_session.commit()
|
||
db_session.close()
|
||
|
||
app_logger.info(f"Avatar hochgeladen für User {current_user.id}")
|
||
|
||
return jsonify({
|
||
'success': True,
|
||
'message': 'Avatar erfolgreich hochgeladen',
|
||
'file_path': relative_path,
|
||
'filename': file_metadata['original_filename'],
|
||
'unique_filename': file_metadata['unique_filename'],
|
||
'file_size': file_metadata['file_size']
|
||
})
|
||
else:
|
||
db_session.close()
|
||
return jsonify({'error': 'Fehler beim Speichern des Avatars'}), 500
|
||
|
||
except Exception as e:
|
||
app_logger.error(f"Fehler beim Hochladen des Avatars: {str(e)}")
|
||
return jsonify({'error': f'Fehler beim Hochladen: {str(e)}'}), 500
|
||
|
||
@app.route('/api/upload/asset', methods=['POST'])
|
||
@login_required
|
||
@admin_required
|
||
def upload_asset():
|
||
"""
|
||
Lädt ein statisches Asset hoch (nur für Administratoren)
|
||
|
||
Form Data:
|
||
file: Die Asset-Datei
|
||
asset_name: Name des Assets (optional)
|
||
"""
|
||
try:
|
||
if 'file' not in request.files:
|
||
return jsonify({'error': 'Keine Datei ausgewählt'}), 400
|
||
|
||
file = request.files['file']
|
||
asset_name = request.form.get('asset_name', '')
|
||
|
||
if file.filename == '':
|
||
return jsonify({'error': 'Keine Datei ausgewählt'}), 400
|
||
|
||
# Metadaten für die Datei
|
||
metadata = {
|
||
'uploader_id': current_user.id,
|
||
'uploader_name': current_user.username,
|
||
'asset_name': asset_name
|
||
}
|
||
|
||
# Datei speichern
|
||
result = save_asset_file(file, current_user.id, metadata)
|
||
|
||
if result:
|
||
relative_path, absolute_path, file_metadata = result
|
||
|
||
app_logger.info(f"Asset hochgeladen: {file_metadata['original_filename']} von Admin {current_user.id}")
|
||
|
||
return jsonify({
|
||
'success': True,
|
||
'message': 'Asset erfolgreich hochgeladen',
|
||
'file_path': relative_path,
|
||
'filename': file_metadata['original_filename'],
|
||
'unique_filename': file_metadata['unique_filename'],
|
||
'file_size': file_metadata['file_size'],
|
||
'metadata': file_metadata
|
||
})
|
||
else:
|
||
return jsonify({'error': 'Fehler beim Speichern des Assets'}), 500
|
||
|
||
except Exception as e:
|
||
app_logger.error(f"Fehler beim Hochladen des Assets: {str(e)}")
|
||
return jsonify({'error': f'Fehler beim Hochladen: {str(e)}'}), 500
|
||
|
||
@app.route('/api/upload/log', methods=['POST'])
|
||
@login_required
|
||
@admin_required
|
||
def upload_log():
|
||
"""
|
||
Lädt eine Log-Datei hoch (nur für Administratoren)
|
||
|
||
Form Data:
|
||
file: Die Log-Datei
|
||
log_type: Typ des Logs (optional)
|
||
"""
|
||
try:
|
||
if 'file' not in request.files:
|
||
return jsonify({'error': 'Keine Datei ausgewählt'}), 400
|
||
|
||
file = request.files['file']
|
||
log_type = request.form.get('log_type', 'allgemein')
|
||
|
||
if file.filename == '':
|
||
return jsonify({'error': 'Keine Datei ausgewählt'}), 400
|
||
|
||
# Metadaten für die Datei
|
||
metadata = {
|
||
'uploader_id': current_user.id,
|
||
'uploader_name': current_user.username,
|
||
'log_type': log_type
|
||
}
|
||
|
||
# Datei speichern
|
||
result = save_log_file(file, current_user.id, metadata)
|
||
|
||
if result:
|
||
relative_path, absolute_path, file_metadata = result
|
||
|
||
app_logger.info(f"Log-Datei hochgeladen: {file_metadata['original_filename']} von Admin {current_user.id}")
|
||
|
||
return jsonify({
|
||
'success': True,
|
||
'message': 'Log-Datei erfolgreich hochgeladen',
|
||
'file_path': relative_path,
|
||
'filename': file_metadata['original_filename'],
|
||
'unique_filename': file_metadata['unique_filename'],
|
||
'file_size': file_metadata['file_size'],
|
||
'metadata': file_metadata
|
||
})
|
||
else:
|
||
return jsonify({'error': 'Fehler beim Speichern der Log-Datei'}), 500
|
||
|
||
except Exception as e:
|
||
app_logger.error(f"Fehler beim Hochladen der Log-Datei: {str(e)}")
|
||
return jsonify({'error': f'Fehler beim Hochladen: {str(e)}'}), 500
|
||
|
||
@app.route('/api/upload/backup', methods=['POST'])
|
||
@login_required
|
||
@admin_required
|
||
def upload_backup():
|
||
"""
|
||
Lädt eine Backup-Datei hoch (nur für Administratoren)
|
||
|
||
Form Data:
|
||
file: Die Backup-Datei
|
||
backup_type: Typ des Backups (optional)
|
||
"""
|
||
try:
|
||
if 'file' not in request.files:
|
||
return jsonify({'error': 'Keine Datei ausgewählt'}), 400
|
||
|
||
file = request.files['file']
|
||
backup_type = request.form.get('backup_type', 'allgemein')
|
||
|
||
if file.filename == '':
|
||
return jsonify({'error': 'Keine Datei ausgewählt'}), 400
|
||
|
||
# Metadaten für die Datei
|
||
metadata = {
|
||
'uploader_id': current_user.id,
|
||
'uploader_name': current_user.username,
|
||
'backup_type': backup_type
|
||
}
|
||
|
||
# Datei speichern
|
||
result = save_backup_file(file, current_user.id, metadata)
|
||
|
||
if result:
|
||
relative_path, absolute_path, file_metadata = result
|
||
|
||
app_logger.info(f"Backup-Datei hochgeladen: {file_metadata['original_filename']} von Admin {current_user.id}")
|
||
|
||
return jsonify({
|
||
'success': True,
|
||
'message': 'Backup-Datei erfolgreich hochgeladen',
|
||
'file_path': relative_path,
|
||
'filename': file_metadata['original_filename'],
|
||
'unique_filename': file_metadata['unique_filename'],
|
||
'file_size': file_metadata['file_size'],
|
||
'metadata': file_metadata
|
||
})
|
||
else:
|
||
return jsonify({'error': 'Fehler beim Speichern der Backup-Datei'}), 500
|
||
|
||
except Exception as e:
|
||
app_logger.error(f"Fehler beim Hochladen der Backup-Datei: {str(e)}")
|
||
return jsonify({'error': f'Fehler beim Hochladen: {str(e)}'}), 500
|
||
|
||
@app.route('/api/upload/temp', methods=['POST'])
|
||
@login_required
|
||
def upload_temp_file():
|
||
"""
|
||
Lädt eine temporäre Datei hoch
|
||
|
||
Form Data:
|
||
file: Die temporäre Datei
|
||
purpose: Verwendungszweck (optional)
|
||
"""
|
||
try:
|
||
if 'file' not in request.files:
|
||
return jsonify({'error': 'Keine Datei ausgewählt'}), 400
|
||
|
||
file = request.files['file']
|
||
purpose = request.form.get('purpose', '')
|
||
|
||
if file.filename == '':
|
||
return jsonify({'error': 'Keine Datei ausgewählt'}), 400
|
||
|
||
# Metadaten für die Datei
|
||
metadata = {
|
||
'uploader_id': current_user.id,
|
||
'uploader_name': current_user.username,
|
||
'purpose': purpose
|
||
}
|
||
|
||
# Datei speichern
|
||
result = save_temp_file(file, current_user.id, metadata)
|
||
|
||
if result:
|
||
relative_path, absolute_path, file_metadata = result
|
||
|
||
app_logger.info(f"Temporäre Datei hochgeladen: {file_metadata['original_filename']} von User {current_user.id}")
|
||
|
||
return jsonify({
|
||
'success': True,
|
||
'message': 'Temporäre Datei erfolgreich hochgeladen',
|
||
'file_path': relative_path,
|
||
'filename': file_metadata['original_filename'],
|
||
'unique_filename': file_metadata['unique_filename'],
|
||
'file_size': file_metadata['file_size'],
|
||
'metadata': file_metadata
|
||
})
|
||
else:
|
||
return jsonify({'error': 'Fehler beim Speichern der temporären Datei'}), 500
|
||
|
||
except Exception as e:
|
||
app_logger.error(f"Fehler beim Hochladen der temporären Datei: {str(e)}")
|
||
return jsonify({'error': f'Fehler beim Hochladen: {str(e)}'}), 500
|
||
|
||
@app.route('/api/files/<path:file_path>', methods=['GET'])
|
||
@login_required
|
||
def serve_uploaded_file(file_path):
|
||
"""
|
||
Stellt hochgeladene Dateien bereit (mit Zugriffskontrolle)
|
||
"""
|
||
try:
|
||
# Datei-Info abrufen
|
||
file_info = file_manager.get_file_info(file_path)
|
||
|
||
if not file_info:
|
||
return jsonify({'error': 'Datei nicht gefunden'}), 404
|
||
|
||
# Zugriffskontrolle basierend auf Dateikategorie
|
||
if file_path.startswith('jobs/'):
|
||
# Job-Dateien: Nur Besitzer und Admins
|
||
if not current_user.is_admin:
|
||
# Prüfen ob Benutzer der Besitzer ist
|
||
if f"user_{current_user.id}" not in file_path:
|
||
return jsonify({'error': 'Zugriff verweigert'}), 403
|
||
|
||
elif file_path.startswith('guests/'):
|
||
# Gast-Dateien: Nur Admins
|
||
if not current_user.is_admin:
|
||
return jsonify({'error': 'Zugriff verweigert'}), 403
|
||
|
||
elif file_path.startswith('avatars/'):
|
||
# Avatar-Dateien: Öffentlich zugänglich für angemeldete Benutzer
|
||
pass
|
||
|
||
elif file_path.startswith('temp/'):
|
||
# Temporäre Dateien: Nur Besitzer und Admins
|
||
if not current_user.is_admin:
|
||
# Prüfen ob Benutzer der Besitzer ist
|
||
if f"user_{current_user.id}" not in file_path:
|
||
return jsonify({'error': 'Zugriff verweigert'}), 403
|
||
|
||
else:
|
||
# Andere Dateien (assets, logs, backups): Nur Admins
|
||
if not current_user.is_admin:
|
||
return jsonify({'error': 'Zugriff verweigert'}), 403
|
||
|
||
# Datei bereitstellen
|
||
return send_file(file_info['absolute_path'], as_attachment=False)
|
||
|
||
except Exception as e:
|
||
app_logger.error(f"Fehler beim Bereitstellen der Datei {file_path}: {str(e)}")
|
||
return jsonify({'error': 'Fehler beim Laden der Datei'}), 500
|
||
|
||
@app.route('/api/files/<path:file_path>', methods=['DELETE'])
|
||
@login_required
|
||
def delete_uploaded_file(file_path):
|
||
"""
|
||
Löscht eine hochgeladene Datei (mit Zugriffskontrolle)
|
||
"""
|
||
try:
|
||
# Datei-Info abrufen
|
||
file_info = file_manager.get_file_info(file_path)
|
||
|
||
if not file_info:
|
||
return jsonify({'error': 'Datei nicht gefunden'}), 404
|
||
|
||
# Zugriffskontrolle basierend auf Dateikategorie
|
||
if file_path.startswith('jobs/'):
|
||
# Job-Dateien: Nur Besitzer und Admins
|
||
if not current_user.is_admin:
|
||
# Prüfen ob Benutzer der Besitzer ist
|
||
if f"user_{current_user.id}" not in file_path:
|
||
return jsonify({'error': 'Zugriff verweigert'}), 403
|
||
|
||
elif file_path.startswith('guests/'):
|
||
# Gast-Dateien: Nur Admins
|
||
if not current_user.is_admin:
|
||
return jsonify({'error': 'Zugriff verweigert'}), 403
|
||
|
||
elif file_path.startswith('avatars/'):
|
||
# Avatar-Dateien: Nur Besitzer und Admins
|
||
if not current_user.is_admin:
|
||
# Prüfen ob Benutzer der Besitzer ist
|
||
if f"user_{current_user.id}" not in file_path:
|
||
return jsonify({'error': 'Zugriff verweigert'}), 403
|
||
|
||
elif file_path.startswith('temp/'):
|
||
# Temporäre Dateien: Nur Besitzer und Admins
|
||
if not current_user.is_admin:
|
||
# Prüfen ob Benutzer der Besitzer ist
|
||
if f"user_{current_user.id}" not in file_path:
|
||
return jsonify({'error': 'Zugriff verweigert'}), 403
|
||
|
||
else:
|
||
# Andere Dateien (assets, logs, backups): Nur Admins
|
||
if not current_user.is_admin:
|
||
return jsonify({'error': 'Zugriff verweigert'}), 403
|
||
|
||
# Datei löschen
|
||
if delete_file_safe(file_path):
|
||
app_logger.info(f"Datei gelöscht: {file_path} von User {current_user.id}")
|
||
return jsonify({'success': True, 'message': 'Datei erfolgreich gelöscht'})
|
||
else:
|
||
return jsonify({'error': 'Fehler beim Löschen der Datei'}), 500
|
||
|
||
except Exception as e:
|
||
app_logger.error(f"Fehler beim Löschen der Datei {file_path}: {str(e)}")
|
||
return jsonify({'error': f'Fehler beim Löschen der Datei: {str(e)}'}), 500
|
||
|
||
@app.route('/api/admin/files/stats', methods=['GET'])
|
||
@login_required
|
||
@admin_required
|
||
def get_file_stats():
|
||
"""
|
||
Gibt Statistiken zu allen Dateien zurück (nur für Administratoren)
|
||
"""
|
||
try:
|
||
stats = file_manager.get_category_stats()
|
||
|
||
# Gesamtstatistiken berechnen
|
||
total_files = sum(category.get('file_count', 0) for category in stats.values())
|
||
total_size = sum(category.get('total_size', 0) for category in stats.values())
|
||
|
||
return jsonify({
|
||
'success': True,
|
||
'categories': stats,
|
||
'totals': {
|
||
'file_count': total_files,
|
||
'total_size': total_size,
|
||
'total_size_mb': round(total_size / (1024 * 1024), 2)
|
||
}
|
||
})
|
||
|
||
except Exception as e:
|
||
app_logger.error(f"Fehler beim Abrufen der Datei-Statistiken: {str(e)}")
|
||
return jsonify({'error': f'Fehler beim Abrufen der Statistiken: {str(e)}'}), 500
|
||
|
||
@app.route('/api/admin/files/cleanup', methods=['POST'])
|
||
@login_required
|
||
@admin_required
|
||
def cleanup_temp_files():
|
||
"""
|
||
Räumt temporäre Dateien auf (nur für Administratoren)
|
||
"""
|
||
try:
|
||
data = request.get_json() or {}
|
||
max_age_hours = data.get('max_age_hours', 24)
|
||
|
||
# Temporäre Dateien aufräumen
|
||
deleted_count = file_manager.cleanup_temp_files(max_age_hours)
|
||
|
||
app_logger.info(f"Temporäre Dateien aufgeräumt: {deleted_count} Dateien gelöscht")
|
||
|
||
return jsonify({
|
||
'success': True,
|
||
'message': f'{deleted_count} temporäre Dateien erfolgreich gelöscht',
|
||
'deleted_count': deleted_count
|
||
})
|
||
|
||
except Exception as e:
|
||
app_logger.error(f"Fehler beim Aufräumen temporärer Dateien: {str(e)}")
|
||
return jsonify({'error': f'Fehler beim Aufräumen: {str(e)}'}), 500
|
||
|
||
|
||
# ===== WEITERE API-ROUTEN =====
|
||
|
||
@app.route("/api/jobs/current", methods=["GET"])
|
||
@login_required
|
||
def get_current_job():
|
||
"""Gibt den aktuellen Job des Benutzers zurück."""
|
||
db_session = get_db_session()
|
||
try:
|
||
current_job = db_session.query(Job).filter(
|
||
Job.user_id == int(current_user.id),
|
||
Job.status.in_(["scheduled", "running"])
|
||
).order_by(Job.start_at).first()
|
||
|
||
if current_job:
|
||
job_data = current_job.to_dict()
|
||
else:
|
||
job_data = None
|
||
|
||
db_session.close()
|
||
return jsonify(job_data)
|
||
except Exception as e:
|
||
db_session.close()
|
||
return jsonify({"error": str(e)}), 500
|
||
|
||
@app.route("/api/jobs/<int:job_id>", methods=["DELETE"])
|
||
@login_required
|
||
@job_owner_required
|
||
def delete_job(job_id):
|
||
"""Löscht einen Job."""
|
||
try:
|
||
db_session = get_db_session()
|
||
job = db_session.query(Job).get(job_id)
|
||
|
||
if not job:
|
||
db_session.close()
|
||
return jsonify({"error": "Job nicht gefunden"}), 404
|
||
|
||
# Prüfen, ob der Job gelöscht werden kann
|
||
if job.status == "running":
|
||
db_session.close()
|
||
return jsonify({"error": "Laufende Jobs können nicht gelöscht werden"}), 400
|
||
|
||
job_name = job.name
|
||
db_session.delete(job)
|
||
db_session.commit()
|
||
db_session.close()
|
||
|
||
jobs_logger.info(f"Job '{job_name}' (ID: {job_id}) gelöscht von Benutzer {current_user.id}")
|
||
return jsonify({"success": True, "message": "Job erfolgreich gelöscht"})
|
||
|
||
except Exception as e:
|
||
jobs_logger.error(f"Fehler beim Löschen des Jobs {job_id}: {str(e)}")
|
||
return jsonify({"error": "Interner Serverfehler"}), 500
|
||
|
||
# ===== DRUCKER-ROUTEN =====
|
||
|
||
@app.route("/api/printers", methods=["GET"])
|
||
@login_required
|
||
def get_printers():
|
||
"""Gibt alle Drucker zurück - OHNE Status-Check für schnelleres Laden."""
|
||
db_session = get_db_session()
|
||
|
||
try:
|
||
# Windows-kompatible Timeout-Implementierung
|
||
import threading
|
||
import time
|
||
|
||
printers = None
|
||
timeout_occurred = False
|
||
|
||
def fetch_printers():
|
||
nonlocal printers, timeout_occurred
|
||
try:
|
||
printers = db_session.query(Printer).all()
|
||
except Exception as e:
|
||
printers_logger.error(f"Datenbankfehler beim Laden der Drucker: {str(e)}")
|
||
timeout_occurred = True
|
||
|
||
# Starte Datenbankabfrage in separatem Thread
|
||
thread = threading.Thread(target=fetch_printers)
|
||
thread.daemon = True
|
||
thread.start()
|
||
thread.join(timeout=5) # 5 Sekunden Timeout
|
||
|
||
if thread.is_alive() or timeout_occurred or printers is None:
|
||
printers_logger.warning("Database timeout when fetching printers for basic loading")
|
||
return jsonify({
|
||
'error': 'Database timeout beim Laden der Drucker',
|
||
'timeout': True,
|
||
'printers': []
|
||
}), 408
|
||
|
||
# Drucker-Daten OHNE Status-Check zusammenstellen für schnelles Laden
|
||
printer_data = []
|
||
current_time = datetime.now()
|
||
|
||
for printer in printers:
|
||
printer_data.append({
|
||
"id": printer.id,
|
||
"name": printer.name,
|
||
"model": printer.model or 'Unbekanntes Modell',
|
||
"location": printer.location or 'Unbekannter Standort',
|
||
"mac_address": printer.mac_address,
|
||
"plug_ip": printer.plug_ip,
|
||
"status": printer.status or "offline", # Letzter bekannter Status
|
||
"active": printer.active if hasattr(printer, 'active') else True,
|
||
"ip_address": printer.plug_ip if printer.plug_ip else getattr(printer, 'ip_address', None),
|
||
"created_at": printer.created_at.isoformat() if printer.created_at else current_time.isoformat(),
|
||
"last_checked": printer.last_checked.isoformat() if hasattr(printer, 'last_checked') and printer.last_checked else None
|
||
})
|
||
|
||
db_session.close()
|
||
|
||
printers_logger.info(f"Schnelles Laden abgeschlossen: {len(printer_data)} Drucker geladen (ohne Status-Check)")
|
||
|
||
return jsonify({
|
||
"success": True,
|
||
"printers": printer_data,
|
||
"count": len(printer_data),
|
||
"message": "Drucker erfolgreich geladen"
|
||
})
|
||
|
||
except Exception as e:
|
||
db_session.rollback()
|
||
db_session.close()
|
||
printers_logger.error(f"Fehler beim Abrufen der Drucker: {str(e)}")
|
||
return jsonify({
|
||
"error": f"Fehler beim Laden der Drucker: {str(e)}",
|
||
"printers": []
|
||
}), 500
|
||
|
||
# ===== ERWEITERTE SESSION-MANAGEMENT UND AUTO-LOGOUT =====
|
||
|
||
@app.before_request
|
||
def check_session_activity():
|
||
"""
|
||
Überprüft Session-Aktivität und meldet Benutzer bei Inaktivität automatisch ab.
|
||
"""
|
||
# Skip für nicht-authentifizierte Benutzer und Login-Route
|
||
if not current_user.is_authenticated or request.endpoint in ['login', 'static', 'auth_logout']:
|
||
return
|
||
|
||
# Skip für AJAX/API calls die nicht als Session-Aktivität zählen sollen
|
||
if request.path.startswith('/api/') and request.path.endswith('/heartbeat'):
|
||
return
|
||
|
||
now = datetime.now()
|
||
|
||
# Session-Aktivität tracken
|
||
if 'last_activity' in session:
|
||
last_activity = datetime.fromisoformat(session['last_activity'])
|
||
inactive_duration = now - last_activity
|
||
|
||
# Definiere Inaktivitäts-Limits basierend auf Benutzerrolle
|
||
max_inactive_minutes = 30 # Standard: 30 Minuten
|
||
if hasattr(current_user, 'is_admin') and current_user.is_admin:
|
||
max_inactive_minutes = 60 # Admins: 60 Minuten
|
||
|
||
max_inactive_duration = timedelta(minutes=max_inactive_minutes)
|
||
|
||
# Benutzer abmelden wenn zu lange inaktiv
|
||
if inactive_duration > max_inactive_duration:
|
||
auth_logger.info(f"🕒 Automatische Abmeldung: Benutzer {current_user.email} war {inactive_duration.total_seconds()/60:.1f} Minuten inaktiv (Limit: {max_inactive_minutes}min)")
|
||
|
||
# Session-Daten vor Logout speichern für Benachrichtigung
|
||
session['auto_logout_reason'] = f"Automatische Abmeldung nach {max_inactive_minutes} Minuten Inaktivität"
|
||
session['auto_logout_time'] = now.isoformat()
|
||
|
||
logout_user()
|
||
session.clear()
|
||
|
||
# JSON-Response für AJAX-Requests
|
||
if request.headers.get('X-Requested-With') == 'XMLHttpRequest' or request.is_json:
|
||
return jsonify({
|
||
"error": "Session abgelaufen",
|
||
"reason": "auto_logout_inactivity",
|
||
"message": f"Sie wurden nach {max_inactive_minutes} Minuten Inaktivität automatisch abgemeldet",
|
||
"redirect_url": url_for("login")
|
||
}), 401
|
||
|
||
# HTML-Redirect für normale Requests
|
||
flash(f"Sie wurden nach {max_inactive_minutes} Minuten Inaktivität automatisch abgemeldet.", "warning")
|
||
return redirect(url_for("login"))
|
||
|
||
# Session-Aktivität aktualisieren (aber nicht bei jedem API-Call)
|
||
if not request.path.startswith('/api/stats/') and not request.path.startswith('/api/heartbeat'):
|
||
session['last_activity'] = now.isoformat()
|
||
session['user_agent'] = request.headers.get('User-Agent', '')[:200] # Begrenzt auf 200 Zeichen
|
||
session['ip_address'] = request.remote_addr
|
||
|
||
# Session-Sicherheit: Überprüfe IP-Adresse und User-Agent (Optional)
|
||
if 'session_ip' in session and session['session_ip'] != request.remote_addr:
|
||
auth_logger.warning(f"⚠️ IP-Adresse geändert für Benutzer {current_user.email}: {session['session_ip']} → {request.remote_addr}")
|
||
# Optional: Benutzer abmelden bei IP-Wechsel (kann bei VPN/Proxy problematisch sein)
|
||
# session['security_warning'] = "IP-Adresse hat sich geändert"
|
||
|
||
@app.before_request
|
||
def setup_session_security():
|
||
"""
|
||
Initialisiert Session-Sicherheit für neue Sessions.
|
||
"""
|
||
if current_user.is_authenticated and 'session_created' not in session:
|
||
session['session_created'] = datetime.now().isoformat()
|
||
session['session_ip'] = request.remote_addr
|
||
session['last_activity'] = datetime.now().isoformat()
|
||
session.permanent = True # Session als permanent markieren
|
||
|
||
auth_logger.info(f"🔐 Neue Session erstellt für Benutzer {current_user.email} von IP {request.remote_addr}")
|
||
|
||
# ===== SESSION-MANAGEMENT API-ENDPUNKTE =====
|
||
|
||
@app.route('/api/session/heartbeat', methods=['POST'])
|
||
@login_required
|
||
def session_heartbeat():
|
||
"""
|
||
Heartbeat-Endpunkt um Session am Leben zu halten.
|
||
Wird vom Frontend alle 5 Minuten aufgerufen.
|
||
"""
|
||
try:
|
||
now = datetime.now()
|
||
session['last_activity'] = now.isoformat()
|
||
|
||
# Berechne verbleibende Session-Zeit
|
||
last_activity = datetime.fromisoformat(session.get('last_activity', now.isoformat()))
|
||
max_inactive_minutes = 60 if hasattr(current_user, 'is_admin') and current_user.is_admin else 30
|
||
time_left = max_inactive_minutes * 60 - (now - last_activity).total_seconds()
|
||
|
||
return jsonify({
|
||
"success": True,
|
||
"session_active": True,
|
||
"time_left_seconds": max(0, int(time_left)),
|
||
"max_inactive_minutes": max_inactive_minutes,
|
||
"current_time": now.isoformat()
|
||
})
|
||
except Exception as e:
|
||
auth_logger.error(f"Fehler beim Session-Heartbeat: {str(e)}")
|
||
return jsonify({"error": "Heartbeat fehlgeschlagen"}), 500
|
||
|
||
@app.route('/api/session/status', methods=['GET'])
|
||
@login_required
|
||
def session_status():
|
||
"""
|
||
Gibt detaillierten Session-Status zurück.
|
||
"""
|
||
try:
|
||
now = datetime.now()
|
||
last_activity = datetime.fromisoformat(session.get('last_activity', now.isoformat()))
|
||
session_created = datetime.fromisoformat(session.get('session_created', now.isoformat()))
|
||
|
||
max_inactive_minutes = 60 if hasattr(current_user, 'is_admin') and current_user.is_admin else 30
|
||
inactive_duration = (now - last_activity).total_seconds()
|
||
time_left = max_inactive_minutes * 60 - inactive_duration
|
||
|
||
return jsonify({
|
||
"success": True,
|
||
"user": {
|
||
"id": current_user.id,
|
||
"email": current_user.email,
|
||
"name": current_user.name,
|
||
"is_admin": getattr(current_user, 'is_admin', False)
|
||
},
|
||
"session": {
|
||
"created": session_created.isoformat(),
|
||
"last_activity": last_activity.isoformat(),
|
||
"inactive_seconds": int(inactive_duration),
|
||
"time_left_seconds": max(0, int(time_left)),
|
||
"max_inactive_minutes": max_inactive_minutes,
|
||
"ip_address": session.get('session_ip', 'unbekannt'),
|
||
"user_agent": session.get('user_agent', 'unbekannt')[:50] + "..." if len(session.get('user_agent', '')) > 50 else session.get('user_agent', 'unbekannt')
|
||
},
|
||
"warnings": []
|
||
})
|
||
except Exception as e:
|
||
auth_logger.error(f"Fehler beim Abrufen des Session-Status: {str(e)}")
|
||
return jsonify({"error": "Session-Status nicht verfügbar"}), 500
|
||
|
||
@app.route('/api/session/extend', methods=['POST'])
|
||
@login_required
|
||
def extend_session():
|
||
"""Verlängert die aktuelle Session um die Standard-Lebensdauer"""
|
||
try:
|
||
# Session-Lebensdauer zurücksetzen
|
||
session.permanent = True
|
||
|
||
# Aktivität für Rate Limiting aktualisieren
|
||
current_user.update_last_activity()
|
||
|
||
# Optional: Session-Statistiken für Admin
|
||
user_agent = request.headers.get('User-Agent', 'Unknown')
|
||
ip_address = request.environ.get('HTTP_X_FORWARDED_FOR', request.remote_addr)
|
||
|
||
app_logger.info(f"Session verlängert für User {current_user.id} (IP: {ip_address})")
|
||
|
||
return jsonify({
|
||
'success': True,
|
||
'message': 'Session erfolgreich verlängert',
|
||
'expires_at': (datetime.now() + SESSION_LIFETIME).isoformat()
|
||
})
|
||
|
||
except Exception as e:
|
||
app_logger.error(f"Fehler beim Verlängern der Session: {str(e)}")
|
||
return jsonify({
|
||
'success': False,
|
||
'error': 'Fehler beim Verlängern der Session'
|
||
}), 500
|
||
|
||
# ===== GASTANTRÄGE API-ROUTEN =====
|
||
|
||
@app.route('/api/admin/guest-requests/test', methods=['GET'])
|
||
def test_admin_guest_requests():
|
||
"""Test-Endpunkt für Guest Requests Routing"""
|
||
app_logger.info("Test-Route /api/admin/guest-requests/test aufgerufen")
|
||
return jsonify({
|
||
'success': True,
|
||
'message': 'Test-Route funktioniert',
|
||
'user_authenticated': current_user.is_authenticated,
|
||
'user_is_admin': current_user.is_admin if current_user.is_authenticated else False
|
||
})
|
||
|
||
@app.route('/api/guest-status', methods=['POST'])
|
||
def get_guest_request_status():
|
||
"""
|
||
Öffentliche Route für Gäste um ihren Auftragsstatus mit OTP-Code zu prüfen.
|
||
Keine Authentifizierung erforderlich.
|
||
"""
|
||
try:
|
||
data = request.get_json()
|
||
if not data:
|
||
return jsonify({
|
||
'success': False,
|
||
'message': 'Keine Daten empfangen'
|
||
}), 400
|
||
|
||
otp_code = data.get('otp_code', '').strip()
|
||
email = data.get('email', '').strip() # Optional für zusätzliche Verifikation
|
||
|
||
if not otp_code:
|
||
return jsonify({
|
||
'success': False,
|
||
'message': 'OTP-Code ist erforderlich'
|
||
}), 400
|
||
|
||
db_session = get_db_session()
|
||
|
||
# Alle Gastaufträge finden, die den OTP-Code haben könnten
|
||
# Da OTP gehashed ist, müssen wir durch alle iterieren
|
||
guest_requests = db_session.query(GuestRequest).filter(
|
||
GuestRequest.otp_code.isnot(None)
|
||
).all()
|
||
|
||
found_request = None
|
||
for request_obj in guest_requests:
|
||
if request_obj.verify_otp(otp_code):
|
||
# Zusätzliche E-Mail-Verifikation falls angegeben
|
||
if email and request_obj.email.lower() != email.lower():
|
||
continue
|
||
found_request = request_obj
|
||
break
|
||
|
||
if not found_request:
|
||
db_session.close()
|
||
app_logger.warning(f"Ungültiger OTP-Code für Gast-Status-Abfrage: {otp_code[:4]}****")
|
||
return jsonify({
|
||
'success': False,
|
||
'message': 'Ungültiger Code oder E-Mail-Adresse'
|
||
}), 404
|
||
|
||
# Status-Informationen für den Gast zusammenstellen
|
||
status_info = {
|
||
'id': found_request.id,
|
||
'name': found_request.name,
|
||
'file_name': found_request.file_name,
|
||
'status': found_request.status,
|
||
'created_at': found_request.created_at.isoformat() if found_request.created_at else None,
|
||
'updated_at': found_request.updated_at.isoformat() if found_request.updated_at else None,
|
||
'duration_minutes': found_request.duration_minutes,
|
||
'copies': found_request.copies,
|
||
'reason': found_request.reason
|
||
}
|
||
|
||
# Status-spezifische Informationen hinzufügen
|
||
if found_request.status == 'approved':
|
||
status_info.update({
|
||
'approved_at': found_request.approved_at.isoformat() if found_request.approved_at else None,
|
||
'approval_notes': found_request.approval_notes,
|
||
'message': 'Ihr Auftrag wurde genehmigt! Sie können mit dem Drucken beginnen.'
|
||
})
|
||
|
||
elif found_request.status == 'rejected':
|
||
status_info.update({
|
||
'rejected_at': found_request.rejected_at.isoformat() if found_request.rejected_at else None,
|
||
'rejection_reason': found_request.rejection_reason,
|
||
'message': 'Ihr Auftrag wurde leider abgelehnt.'
|
||
})
|
||
|
||
elif found_request.status == 'pending':
|
||
# Berechne wie lange der Auftrag schon wartet
|
||
if found_request.created_at:
|
||
waiting_time = datetime.now() - found_request.created_at
|
||
hours_waiting = int(waiting_time.total_seconds() / 3600)
|
||
status_info.update({
|
||
'hours_waiting': hours_waiting,
|
||
'message': f'Ihr Auftrag wird bearbeitet. Wartezeit: {hours_waiting} Stunden.'
|
||
})
|
||
else:
|
||
status_info['message'] = 'Ihr Auftrag wird bearbeitet.'
|
||
|
||
db_session.commit() # OTP als verwendet markieren
|
||
db_session.close()
|
||
|
||
app_logger.info(f"Gast-Status-Abfrage erfolgreich für Request {found_request.id}")
|
||
|
||
return jsonify({
|
||
'success': True,
|
||
'request': status_info
|
||
})
|
||
|
||
except Exception as e:
|
||
app_logger.error(f"Fehler bei Gast-Status-Abfrage: {str(e)}")
|
||
return jsonify({
|
||
'success': False,
|
||
'message': 'Fehler beim Abrufen des Status'
|
||
}), 500
|
||
|
||
@app.route('/guest-status')
|
||
def guest_status_page():
|
||
"""
|
||
Öffentliche Seite für Gäste um ihren Auftragsstatus zu prüfen.
|
||
"""
|
||
return render_template('guest_status.html')
|
||
|
||
@app.route('/api/admin/guest-requests', methods=['GET'])
|
||
@admin_required
|
||
def get_admin_guest_requests():
|
||
"""Gibt alle Gastaufträge für Admin-Verwaltung zurück"""
|
||
try:
|
||
app_logger.info(f"API-Aufruf /api/admin/guest-requests von User {current_user.id if current_user.is_authenticated else 'Anonymous'}")
|
||
|
||
db_session = get_db_session()
|
||
|
||
# Parameter auslesen
|
||
status = request.args.get('status', 'all')
|
||
page = int(request.args.get('page', 0))
|
||
page_size = int(request.args.get('page_size', 50))
|
||
search = request.args.get('search', '')
|
||
sort = request.args.get('sort', 'newest')
|
||
urgent = request.args.get('urgent', 'all')
|
||
|
||
# Basis-Query
|
||
query = db_session.query(GuestRequest)
|
||
|
||
# Status-Filter
|
||
if status != 'all':
|
||
query = query.filter(GuestRequest.status == status)
|
||
|
||
# Suchfilter
|
||
if search:
|
||
search_term = f"%{search}%"
|
||
query = query.filter(
|
||
(GuestRequest.name.ilike(search_term)) |
|
||
(GuestRequest.email.ilike(search_term)) |
|
||
(GuestRequest.file_name.ilike(search_term)) |
|
||
(GuestRequest.reason.ilike(search_term))
|
||
)
|
||
|
||
# Dringlichkeitsfilter
|
||
if urgent == 'urgent':
|
||
urgent_cutoff = datetime.now() - timedelta(hours=24)
|
||
query = query.filter(
|
||
GuestRequest.status == 'pending',
|
||
GuestRequest.created_at < urgent_cutoff
|
||
)
|
||
elif urgent == 'normal':
|
||
urgent_cutoff = datetime.now() - timedelta(hours=24)
|
||
query = query.filter(
|
||
(GuestRequest.status != 'pending') |
|
||
(GuestRequest.created_at >= urgent_cutoff)
|
||
)
|
||
|
||
# Gesamtanzahl vor Pagination
|
||
total = query.count()
|
||
|
||
# Sortierung
|
||
if sort == 'oldest':
|
||
query = query.order_by(GuestRequest.created_at.asc())
|
||
elif sort == 'urgent':
|
||
# Urgent first, then by creation date desc
|
||
query = query.order_by(GuestRequest.created_at.asc()).order_by(GuestRequest.created_at.desc())
|
||
else: # newest
|
||
query = query.order_by(GuestRequest.created_at.desc())
|
||
|
||
# Pagination
|
||
offset = page * page_size
|
||
requests = query.offset(offset).limit(page_size).all()
|
||
|
||
# Statistiken berechnen
|
||
stats = {
|
||
'total': db_session.query(GuestRequest).count(),
|
||
'pending': db_session.query(GuestRequest).filter(GuestRequest.status == 'pending').count(),
|
||
'approved': db_session.query(GuestRequest).filter(GuestRequest.status == 'approved').count(),
|
||
'rejected': db_session.query(GuestRequest).filter(GuestRequest.status == 'rejected').count(),
|
||
}
|
||
|
||
# Requests zu Dictionary konvertieren
|
||
requests_data = []
|
||
for req in requests:
|
||
# Priorität berechnen
|
||
now = datetime.now()
|
||
hours_old = (now - req.created_at).total_seconds() / 3600 if req.created_at else 0
|
||
is_urgent = hours_old > 24 and req.status == 'pending'
|
||
|
||
request_data = {
|
||
'id': req.id,
|
||
'name': req.name,
|
||
'email': req.email,
|
||
'file_name': req.file_name,
|
||
'file_path': req.file_path,
|
||
'duration_minutes': req.duration_minutes,
|
||
'copies': req.copies,
|
||
'reason': req.reason,
|
||
'status': req.status,
|
||
'created_at': req.created_at.isoformat() if req.created_at else None,
|
||
'updated_at': req.updated_at.isoformat() if req.updated_at else None,
|
||
'approved_at': req.approved_at.isoformat() if req.approved_at else None,
|
||
'rejected_at': req.rejected_at.isoformat() if req.rejected_at else None,
|
||
'approval_notes': req.approval_notes,
|
||
'rejection_reason': req.rejection_reason,
|
||
'is_urgent': is_urgent,
|
||
'hours_old': round(hours_old, 1),
|
||
'author_ip': req.author_ip
|
||
}
|
||
requests_data.append(request_data)
|
||
|
||
db_session.close()
|
||
|
||
app_logger.info(f"Admin-Gastaufträge geladen: {len(requests_data)} von {total} (Status: {status})")
|
||
|
||
return jsonify({
|
||
'success': True,
|
||
'requests': requests_data,
|
||
'stats': stats,
|
||
'total': total,
|
||
'page': page,
|
||
'page_size': page_size,
|
||
'has_more': offset + page_size < total
|
||
})
|
||
|
||
except Exception as e:
|
||
app_logger.error(f"Fehler beim Laden der Admin-Gastaufträge: {str(e)}", exc_info=True)
|
||
return jsonify({
|
||
'success': False,
|
||
'message': f'Fehler beim Laden der Gastaufträge: {str(e)}'
|
||
}), 500
|
||
|
||
@app.route('/api/guest-requests/<int:request_id>/approve', methods=['POST'])
|
||
@admin_required
|
||
def approve_guest_request(request_id):
|
||
"""Genehmigt einen Gastauftrag"""
|
||
try:
|
||
db_session = get_db_session()
|
||
|
||
guest_request = db_session.query(GuestRequest).filter(GuestRequest.id == request_id).first()
|
||
|
||
if not guest_request:
|
||
db_session.close()
|
||
return jsonify({
|
||
'success': False,
|
||
'message': 'Gastauftrag nicht gefunden'
|
||
}), 404
|
||
|
||
if guest_request.status != 'pending':
|
||
db_session.close()
|
||
return jsonify({
|
||
'success': False,
|
||
'message': f'Gastauftrag kann im Status "{guest_request.status}" nicht genehmigt werden'
|
||
}), 400
|
||
|
||
# Daten aus Request Body
|
||
data = request.get_json() or {}
|
||
notes = data.get('notes', '')
|
||
printer_id = data.get('printer_id')
|
||
|
||
# Status aktualisieren
|
||
guest_request.status = 'approved'
|
||
guest_request.approved_at = datetime.now()
|
||
guest_request.approved_by = current_user.id
|
||
guest_request.approval_notes = notes
|
||
guest_request.updated_at = datetime.now()
|
||
|
||
# Falls Drucker zugewiesen werden soll
|
||
if printer_id:
|
||
printer = db_session.query(Printer).filter(Printer.id == printer_id).first()
|
||
if printer:
|
||
guest_request.assigned_printer_id = printer_id
|
||
|
||
# OTP-Code generieren falls noch nicht vorhanden (nutze die Methode aus models.py)
|
||
otp_code = None
|
||
if not guest_request.otp_code:
|
||
otp_code = guest_request.generate_otp()
|
||
guest_request.otp_expires_at = datetime.now() + timedelta(hours=48) # 48h gültig
|
||
|
||
db_session.commit()
|
||
|
||
# Benachrichtigung an den Gast senden (falls E-Mail verfügbar)
|
||
if guest_request.email and otp_code:
|
||
try:
|
||
# Hier würde normalerweise eine E-Mail gesendet werden
|
||
app_logger.info(f"Genehmigungs-E-Mail würde an {guest_request.email} gesendet (OTP für Status-Abfrage verfügbar)")
|
||
except Exception as e:
|
||
app_logger.warning(f"Fehler beim Senden der E-Mail-Benachrichtigung: {str(e)}")
|
||
|
||
db_session.close()
|
||
|
||
app_logger.info(f"Gastauftrag {request_id} von Admin {current_user.id} genehmigt")
|
||
|
||
response_data = {
|
||
'success': True,
|
||
'message': 'Gastauftrag erfolgreich genehmigt'
|
||
}
|
||
|
||
# OTP-Code nur zurückgeben wenn er neu generiert wurde (für Admin-Info)
|
||
if otp_code:
|
||
response_data['otp_code_generated'] = True
|
||
response_data['status_check_url'] = url_for('guest_status_page', _external=True)
|
||
|
||
return jsonify(response_data)
|
||
|
||
except Exception as e:
|
||
app_logger.error(f"Fehler beim Genehmigen des Gastauftrags {request_id}: {str(e)}")
|
||
return jsonify({
|
||
'success': False,
|
||
'message': f'Fehler beim Genehmigen: {str(e)}'
|
||
}), 500
|
||
|
||
@app.route('/api/guest-requests/<int:request_id>/reject', methods=['POST'])
|
||
@admin_required
|
||
def reject_guest_request(request_id):
|
||
"""Lehnt einen Gastauftrag ab"""
|
||
try:
|
||
db_session = get_db_session()
|
||
|
||
guest_request = db_session.query(GuestRequest).filter(GuestRequest.id == request_id).first()
|
||
|
||
if not guest_request:
|
||
db_session.close()
|
||
return jsonify({
|
||
'success': False,
|
||
'message': 'Gastauftrag nicht gefunden'
|
||
}), 404
|
||
|
||
if guest_request.status != 'pending':
|
||
db_session.close()
|
||
return jsonify({
|
||
'success': False,
|
||
'message': f'Gastauftrag kann im Status "{guest_request.status}" nicht abgelehnt werden'
|
||
}), 400
|
||
|
||
# Daten aus Request Body
|
||
data = request.get_json() or {}
|
||
reason = data.get('reason', '').strip()
|
||
|
||
if not reason:
|
||
db_session.close()
|
||
return jsonify({
|
||
'success': False,
|
||
'message': 'Ablehnungsgrund ist erforderlich'
|
||
}), 400
|
||
|
||
# Status aktualisieren
|
||
guest_request.status = 'rejected'
|
||
guest_request.rejected_at = datetime.now()
|
||
guest_request.rejected_by = current_user.id
|
||
guest_request.rejection_reason = reason
|
||
guest_request.updated_at = datetime.now()
|
||
|
||
db_session.commit()
|
||
|
||
# Benachrichtigung an den Gast senden (falls E-Mail verfügbar)
|
||
if guest_request.email:
|
||
try:
|
||
# Hier würde normalerweise eine E-Mail gesendet werden
|
||
app_logger.info(f"Ablehnungs-E-Mail würde an {guest_request.email} gesendet (Grund: {reason})")
|
||
except Exception as e:
|
||
app_logger.warning(f"Fehler beim Senden der Ablehnungs-E-Mail: {str(e)}")
|
||
|
||
db_session.close()
|
||
|
||
app_logger.info(f"Gastauftrag {request_id} von Admin {current_user.id} abgelehnt (Grund: {reason})")
|
||
|
||
return jsonify({
|
||
'success': True,
|
||
'message': 'Gastauftrag erfolgreich abgelehnt'
|
||
})
|
||
|
||
except Exception as e:
|
||
app_logger.error(f"Fehler beim Ablehnen des Gastauftrags {request_id}: {str(e)}")
|
||
return jsonify({
|
||
'success': False,
|
||
'message': f'Fehler beim Ablehnen: {str(e)}'
|
||
}), 500
|
||
|
||
@app.route('/api/guest-requests/<int:request_id>', methods=['DELETE'])
|
||
@admin_required
|
||
def delete_guest_request(request_id):
|
||
"""Löscht einen Gastauftrag"""
|
||
try:
|
||
db_session = get_db_session()
|
||
|
||
guest_request = db_session.query(GuestRequest).filter(GuestRequest.id == request_id).first()
|
||
|
||
if not guest_request:
|
||
db_session.close()
|
||
return jsonify({
|
||
'success': False,
|
||
'message': 'Gastauftrag nicht gefunden'
|
||
}), 404
|
||
|
||
# Datei löschen falls vorhanden
|
||
if guest_request.file_path and os.path.exists(guest_request.file_path):
|
||
try:
|
||
os.remove(guest_request.file_path)
|
||
app_logger.info(f"Datei {guest_request.file_path} für Gastauftrag {request_id} gelöscht")
|
||
except Exception as e:
|
||
app_logger.warning(f"Fehler beim Löschen der Datei: {str(e)}")
|
||
|
||
# Gastauftrag aus Datenbank löschen
|
||
request_name = guest_request.name
|
||
db_session.delete(guest_request)
|
||
db_session.commit()
|
||
db_session.close()
|
||
|
||
app_logger.info(f"Gastauftrag {request_id} ({request_name}) von Admin {current_user.id} gelöscht")
|
||
|
||
return jsonify({
|
||
'success': True,
|
||
'message': 'Gastauftrag erfolgreich gelöscht'
|
||
})
|
||
|
||
except Exception as e:
|
||
app_logger.error(f"Fehler beim Löschen des Gastauftrags {request_id}: {str(e)}")
|
||
return jsonify({
|
||
'success': False,
|
||
'message': f'Fehler beim Löschen: {str(e)}'
|
||
}), 500
|
||
|
||
@app.route('/api/guest-requests/<int:request_id>', methods=['GET'])
|
||
@admin_required
|
||
def get_guest_request_detail(request_id):
|
||
"""Gibt Details eines spezifischen Gastauftrags zurück"""
|
||
try:
|
||
db_session = get_db_session()
|
||
|
||
guest_request = db_session.query(GuestRequest).filter(GuestRequest.id == request_id).first()
|
||
|
||
if not guest_request:
|
||
db_session.close()
|
||
return jsonify({
|
||
'success': False,
|
||
'message': 'Gastauftrag nicht gefunden'
|
||
}), 404
|
||
|
||
# Detaildaten zusammenstellen
|
||
request_data = {
|
||
'id': guest_request.id,
|
||
'name': guest_request.name,
|
||
'email': guest_request.email,
|
||
'file_name': guest_request.file_name,
|
||
'file_path': guest_request.file_path,
|
||
'file_size': None,
|
||
'duration_minutes': guest_request.duration_minutes,
|
||
'copies': guest_request.copies,
|
||
'reason': guest_request.reason,
|
||
'status': guest_request.status,
|
||
'created_at': guest_request.created_at.isoformat() if guest_request.created_at else None,
|
||
'updated_at': guest_request.updated_at.isoformat() if guest_request.updated_at else None,
|
||
'approved_at': guest_request.approved_at.isoformat() if guest_request.approved_at else None,
|
||
'rejected_at': guest_request.rejected_at.isoformat() if guest_request.rejected_at else None,
|
||
'approval_notes': guest_request.approval_notes,
|
||
'rejection_reason': guest_request.rejection_reason,
|
||
'otp_code': guest_request.otp_code,
|
||
'otp_expires_at': guest_request.otp_expires_at.isoformat() if guest_request.otp_expires_at else None,
|
||
'author_ip': guest_request.author_ip
|
||
}
|
||
|
||
# Dateigröße ermitteln
|
||
if guest_request.file_path and os.path.exists(guest_request.file_path):
|
||
try:
|
||
file_size = os.path.getsize(guest_request.file_path)
|
||
request_data['file_size'] = file_size
|
||
request_data['file_size_mb'] = round(file_size / (1024 * 1024), 2)
|
||
except Exception as e:
|
||
app_logger.warning(f"Fehler beim Ermitteln der Dateigröße: {str(e)}")
|
||
|
||
# Bearbeiter-Informationen hinzufügen
|
||
if guest_request.approved_by:
|
||
approved_by_user = db_session.query(User).filter(User.id == guest_request.approved_by).first()
|
||
if approved_by_user:
|
||
request_data['approved_by_name'] = approved_by_user.name or approved_by_user.username
|
||
|
||
if guest_request.rejected_by:
|
||
rejected_by_user = db_session.query(User).filter(User.id == guest_request.rejected_by).first()
|
||
if rejected_by_user:
|
||
request_data['rejected_by_name'] = rejected_by_user.name or rejected_by_user.username
|
||
|
||
# Zugewiesener Drucker
|
||
if hasattr(guest_request, 'assigned_printer_id') and guest_request.assigned_printer_id:
|
||
assigned_printer = db_session.query(Printer).filter(Printer.id == guest_request.assigned_printer_id).first()
|
||
if assigned_printer:
|
||
request_data['assigned_printer'] = {
|
||
'id': assigned_printer.id,
|
||
'name': assigned_printer.name,
|
||
'location': assigned_printer.location,
|
||
'status': assigned_printer.status
|
||
}
|
||
|
||
db_session.close()
|
||
|
||
return jsonify({
|
||
'success': True,
|
||
'request': request_data
|
||
})
|
||
|
||
except Exception as e:
|
||
app_logger.error(f"Fehler beim Abrufen der Gastauftrag-Details {request_id}: {str(e)}")
|
||
return jsonify({
|
||
'success': False,
|
||
'message': f'Fehler beim Abrufen der Details: {str(e)}'
|
||
}), 500
|
||
|
||
@app.route('/api/admin/guest-requests/stats', methods=['GET'])
|
||
@admin_required
|
||
def get_guest_requests_stats():
|
||
"""Gibt detaillierte Statistiken zu Gastaufträgen zurück"""
|
||
try:
|
||
db_session = get_db_session()
|
||
|
||
# Basis-Statistiken
|
||
total = db_session.query(GuestRequest).count()
|
||
pending = db_session.query(GuestRequest).filter(GuestRequest.status == 'pending').count()
|
||
approved = db_session.query(GuestRequest).filter(GuestRequest.status == 'approved').count()
|
||
rejected = db_session.query(GuestRequest).filter(GuestRequest.status == 'rejected').count()
|
||
|
||
# Zeitbasierte Statistiken
|
||
today = datetime.now().date()
|
||
week_ago = datetime.now() - timedelta(days=7)
|
||
month_ago = datetime.now() - timedelta(days=30)
|
||
|
||
today_requests = db_session.query(GuestRequest).filter(
|
||
func.date(GuestRequest.created_at) == today
|
||
).count()
|
||
|
||
week_requests = db_session.query(GuestRequest).filter(
|
||
GuestRequest.created_at >= week_ago
|
||
).count()
|
||
|
||
month_requests = db_session.query(GuestRequest).filter(
|
||
GuestRequest.created_at >= month_ago
|
||
).count()
|
||
|
||
# Dringende Requests (älter als 24h und pending)
|
||
urgent_cutoff = datetime.now() - timedelta(hours=24)
|
||
urgent_requests = db_session.query(GuestRequest).filter(
|
||
GuestRequest.status == 'pending',
|
||
GuestRequest.created_at < urgent_cutoff
|
||
).count()
|
||
|
||
# Durchschnittliche Bearbeitungszeit
|
||
avg_processing_time = None
|
||
try:
|
||
processed_requests = db_session.query(GuestRequest).filter(
|
||
GuestRequest.status.in_(['approved', 'rejected']),
|
||
GuestRequest.updated_at.isnot(None)
|
||
).all()
|
||
|
||
if processed_requests:
|
||
total_time = sum([
|
||
(req.updated_at - req.created_at).total_seconds()
|
||
for req in processed_requests
|
||
if req.updated_at and req.created_at
|
||
])
|
||
avg_processing_time = round(total_time / len(processed_requests) / 3600, 2) # Stunden
|
||
except Exception as e:
|
||
app_logger.warning(f"Fehler beim Berechnen der durchschnittlichen Bearbeitungszeit: {str(e)}")
|
||
|
||
# Erfolgsrate
|
||
success_rate = 0
|
||
if approved + rejected > 0:
|
||
success_rate = round((approved / (approved + rejected)) * 100, 1)
|
||
|
||
stats = {
|
||
'total': total,
|
||
'pending': pending,
|
||
'approved': approved,
|
||
'rejected': rejected,
|
||
'urgent': urgent_requests,
|
||
'today': today_requests,
|
||
'week': week_requests,
|
||
'month': month_requests,
|
||
'success_rate': success_rate,
|
||
'avg_processing_time_hours': avg_processing_time,
|
||
'completion_rate': round(((approved + rejected) / total * 100), 1) if total > 0 else 0
|
||
}
|
||
|
||
db_session.close()
|
||
|
||
return jsonify({
|
||
'success': True,
|
||
'stats': stats,
|
||
'generated_at': datetime.now().isoformat()
|
||
})
|
||
|
||
except Exception as e:
|
||
app_logger.error(f"Fehler beim Abrufen der Gastauftrag-Statistiken: {str(e)}")
|
||
return jsonify({
|
||
'success': False,
|
||
'message': f'Fehler beim Abrufen der Statistiken: {str(e)}'
|
||
}), 500
|
||
|
||
@app.route('/api/admin/guest-requests/export', methods=['GET'])
|
||
@admin_required
|
||
def export_guest_requests():
|
||
"""Exportiert Gastaufträge als CSV"""
|
||
try:
|
||
db_session = get_db_session()
|
||
|
||
# Filter-Parameter
|
||
status = request.args.get('status', 'all')
|
||
start_date = request.args.get('start_date')
|
||
end_date = request.args.get('end_date')
|
||
|
||
# Query aufbauen
|
||
query = db_session.query(GuestRequest)
|
||
|
||
if status != 'all':
|
||
query = query.filter(GuestRequest.status == status)
|
||
|
||
if start_date:
|
||
try:
|
||
start_dt = datetime.fromisoformat(start_date)
|
||
query = query.filter(GuestRequest.created_at >= start_dt)
|
||
except ValueError:
|
||
pass
|
||
|
||
if end_date:
|
||
try:
|
||
end_dt = datetime.fromisoformat(end_date)
|
||
query = query.filter(GuestRequest.created_at <= end_dt)
|
||
except ValueError:
|
||
pass
|
||
|
||
requests = query.order_by(GuestRequest.created_at.desc()).all()
|
||
|
||
# CSV-Daten erstellen
|
||
import csv
|
||
import io
|
||
|
||
output = io.StringIO()
|
||
writer = csv.writer(output)
|
||
|
||
# Header
|
||
writer.writerow([
|
||
'ID', 'Name', 'E-Mail', 'Datei', 'Status', 'Erstellt am',
|
||
'Dauer (Min)', 'Kopien', 'Begründung', 'Genehmigt am',
|
||
'Abgelehnt am', 'Bearbeitungsnotizen', 'Ablehnungsgrund', 'OTP-Code'
|
||
])
|
||
|
||
# Daten
|
||
for req in requests:
|
||
writer.writerow([
|
||
req.id,
|
||
req.name or '',
|
||
req.email or '',
|
||
req.file_name or '',
|
||
req.status,
|
||
req.created_at.strftime('%Y-%m-%d %H:%M:%S') if req.created_at else '',
|
||
req.duration_minutes or '',
|
||
req.copies or '',
|
||
req.reason or '',
|
||
req.approved_at.strftime('%Y-%m-%d %H:%M:%S') if req.approved_at else '',
|
||
req.rejected_at.strftime('%Y-%m-%d %H:%M:%S') if req.rejected_at else '',
|
||
req.approval_notes or '',
|
||
req.rejection_reason or '',
|
||
req.otp_code or ''
|
||
])
|
||
|
||
db_session.close()
|
||
|
||
# Response erstellen
|
||
output.seek(0)
|
||
filename = f"gastantraege_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
|
||
|
||
response = make_response(output.getvalue())
|
||
response.headers['Content-Type'] = 'text/csv; charset=utf-8'
|
||
response.headers['Content-Disposition'] = f'attachment; filename="{filename}"'
|
||
|
||
app_logger.info(f"Gastaufträge-Export erstellt: {len(requests)} Datensätze")
|
||
|
||
return response
|
||
|
||
except Exception as e:
|
||
app_logger.error(f"Fehler beim Exportieren der Gastaufträge: {str(e)}")
|
||
return jsonify({
|
||
'success': False,
|
||
'message': f'Fehler beim Export: {str(e)}'
|
||
}), 500
|
||
|
||
|
||
# ===== AUTO-OPTIMIERUNG-API-ENDPUNKTE =====
|
||
|
||
|
||
@app.route('/api/optimization/auto-optimize', methods=['POST'])
|
||
@login_required
|
||
def auto_optimize_jobs():
|
||
"""
|
||
Automatische Optimierung der Druckaufträge durchführen
|
||
Implementiert intelligente Job-Verteilung basierend auf verschiedenen Algorithmen
|
||
"""
|
||
try:
|
||
data = request.get_json()
|
||
settings = data.get('settings', {})
|
||
enabled = data.get('enabled', False)
|
||
|
||
db_session = get_db_session()
|
||
|
||
# Aktuelle Jobs in der Warteschlange abrufen
|
||
pending_jobs = db_session.query(Job).filter(
|
||
Job.status.in_(['queued', 'pending'])
|
||
).all()
|
||
|
||
if not pending_jobs:
|
||
db_session.close()
|
||
return jsonify({
|
||
'success': True,
|
||
'message': 'Keine Jobs zur Optimierung verfügbar',
|
||
'optimized_jobs': 0
|
||
})
|
||
|
||
# Verfügbare Drucker abrufen
|
||
available_printers = db_session.query(Printer).filter(Printer.active == True).all()
|
||
|
||
if not available_printers:
|
||
db_session.close()
|
||
return jsonify({
|
||
'success': False,
|
||
'error': 'Keine verfügbaren Drucker für Optimierung'
|
||
})
|
||
|
||
# Optimierungs-Algorithmus anwenden
|
||
algorithm = settings.get('algorithm', 'round_robin')
|
||
optimized_count = 0
|
||
|
||
if algorithm == 'round_robin':
|
||
optimized_count = apply_round_robin_optimization(pending_jobs, available_printers, db_session)
|
||
elif algorithm == 'load_balance':
|
||
optimized_count = apply_load_balance_optimization(pending_jobs, available_printers, db_session)
|
||
elif algorithm == 'priority_based':
|
||
optimized_count = apply_priority_optimization(pending_jobs, available_printers, db_session)
|
||
|
||
db_session.commit()
|
||
jobs_logger.info(f"Auto-Optimierung durchgeführt: {optimized_count} Jobs optimiert mit Algorithmus {algorithm}")
|
||
|
||
# System-Log erstellen
|
||
log_entry = SystemLog(
|
||
level='INFO',
|
||
component='optimization',
|
||
message=f'Auto-Optimierung durchgeführt: {optimized_count} Jobs optimiert',
|
||
user_id=current_user.id if current_user.is_authenticated else None,
|
||
details=json.dumps({
|
||
'algorithm': algorithm,
|
||
'optimized_jobs': optimized_count,
|
||
'settings': settings
|
||
})
|
||
)
|
||
db_session.add(log_entry)
|
||
db_session.commit()
|
||
db_session.close()
|
||
|
||
return jsonify({
|
||
'success': True,
|
||
'optimized_jobs': optimized_count,
|
||
'algorithm': algorithm,
|
||
'message': f'Optimierung erfolgreich: {optimized_count} Jobs wurden optimiert'
|
||
})
|
||
|
||
except Exception as e:
|
||
app_logger.error(f"Fehler bei der Auto-Optimierung: {str(e)}")
|
||
return jsonify({
|
||
'success': False,
|
||
'error': f'Optimierung fehlgeschlagen: {str(e)}'
|
||
}), 500
|
||
|
||
@app.route('/api/optimization/settings', methods=['GET', 'POST'])
|
||
@login_required
|
||
def optimization_settings():
|
||
"""Optimierungs-Einstellungen abrufen und speichern"""
|
||
db_session = get_db_session()
|
||
|
||
if request.method == 'GET':
|
||
try:
|
||
# Standard-Einstellungen oder benutzerdefinierte laden
|
||
default_settings = {
|
||
'algorithm': 'round_robin',
|
||
'consider_distance': True,
|
||
'minimize_changeover': True,
|
||
'max_batch_size': 10,
|
||
'time_window': 24,
|
||
'auto_optimization_enabled': False
|
||
}
|
||
|
||
# Benutzereinstellungen aus der Session laden oder Standardwerte verwenden
|
||
user_settings = session.get('user_settings', {})
|
||
optimization_settings = user_settings.get('optimization', default_settings)
|
||
|
||
# Sicherstellen, dass alle erforderlichen Schlüssel vorhanden sind
|
||
for key, value in default_settings.items():
|
||
if key not in optimization_settings:
|
||
optimization_settings[key] = value
|
||
|
||
return jsonify({
|
||
'success': True,
|
||
'settings': optimization_settings
|
||
})
|
||
|
||
except Exception as e:
|
||
app_logger.error(f"Fehler beim Abrufen der Optimierungs-Einstellungen: {str(e)}")
|
||
return jsonify({
|
||
'success': False,
|
||
'error': 'Fehler beim Laden der Einstellungen'
|
||
}), 500
|
||
|
||
elif request.method == 'POST':
|
||
try:
|
||
settings = request.get_json()
|
||
|
||
# Validierung der Einstellungen
|
||
if not validate_optimization_settings(settings):
|
||
return jsonify({
|
||
'success': False,
|
||
'error': 'Ungültige Optimierungs-Einstellungen'
|
||
}), 400
|
||
|
||
# Einstellungen in der Session speichern
|
||
user_settings = session.get('user_settings', {})
|
||
if 'optimization' not in user_settings:
|
||
user_settings['optimization'] = {}
|
||
|
||
# Aktualisiere die Optimierungseinstellungen
|
||
user_settings['optimization'].update(settings)
|
||
session['user_settings'] = user_settings
|
||
|
||
# Einstellungen in der Datenbank speichern, wenn möglich
|
||
if hasattr(current_user, 'settings'):
|
||
import json
|
||
current_user.settings = json.dumps(user_settings)
|
||
current_user.updated_at = datetime.now()
|
||
db_session.commit()
|
||
|
||
app_logger.info(f"Optimierungs-Einstellungen für Benutzer {current_user.id} aktualisiert")
|
||
|
||
return jsonify({
|
||
'success': True,
|
||
'message': 'Optimierungs-Einstellungen erfolgreich gespeichert'
|
||
})
|
||
|
||
except Exception as e:
|
||
db_session.rollback()
|
||
app_logger.error(f"Fehler beim Speichern der Optimierungs-Einstellungen: {str(e)}")
|
||
return jsonify({
|
||
'success': False,
|
||
'error': f'Fehler beim Speichern der Einstellungen: {str(e)}'
|
||
}), 500
|
||
finally:
|
||
db_session.close()
|
||
|
||
# ===== OPTIMIERUNGS-ALGORITHMUS-FUNKTIONEN =====
|
||
|
||
def apply_round_robin_optimization(jobs, printers, db_session):
|
||
"""
|
||
Round-Robin-Optimierung: Gleichmäßige Verteilung der Jobs auf Drucker
|
||
Verteilt Jobs nacheinander auf verfügbare Drucker für optimale Balance
|
||
"""
|
||
optimized_count = 0
|
||
printer_index = 0
|
||
|
||
for job in jobs:
|
||
if printer_index >= len(printers):
|
||
printer_index = 0
|
||
|
||
# Job dem nächsten Drucker zuweisen
|
||
job.printer_id = printers[printer_index].id
|
||
job.assigned_at = datetime.now()
|
||
optimized_count += 1
|
||
printer_index += 1
|
||
|
||
return optimized_count
|
||
|
||
def apply_load_balance_optimization(jobs, printers, db_session):
|
||
"""
|
||
Load-Balancing-Optimierung: Jobs basierend auf aktueller Auslastung verteilen
|
||
Berücksichtigt die aktuelle Drucker-Auslastung für optimale Verteilung
|
||
"""
|
||
optimized_count = 0
|
||
|
||
# Aktuelle Drucker-Auslastung berechnen
|
||
printer_loads = {}
|
||
for printer in printers:
|
||
current_jobs = db_session.query(Job).filter(
|
||
Job.printer_id == printer.id,
|
||
Job.status.in_(['running', 'queued'])
|
||
).count()
|
||
printer_loads[printer.id] = current_jobs
|
||
|
||
for job in jobs:
|
||
# Drucker mit geringster Auslastung finden
|
||
min_load_printer_id = min(printer_loads, key=printer_loads.get)
|
||
|
||
job.printer_id = min_load_printer_id
|
||
job.assigned_at = datetime.now()
|
||
|
||
# Auslastung für nächste Iteration aktualisieren
|
||
printer_loads[min_load_printer_id] += 1
|
||
optimized_count += 1
|
||
|
||
return optimized_count
|
||
|
||
def apply_priority_optimization(jobs, printers, db_session):
|
||
"""
|
||
Prioritätsbasierte Optimierung: Jobs nach Priorität und verfügbaren Druckern verteilen
|
||
Hochpriorisierte Jobs erhalten bevorzugte Druckerzuweisung
|
||
"""
|
||
optimized_count = 0
|
||
|
||
# Jobs nach Priorität sortieren
|
||
priority_order = {'urgent': 1, 'high': 2, 'normal': 3, 'low': 4}
|
||
sorted_jobs = sorted(jobs, key=lambda j: priority_order.get(getattr(j, 'priority', 'normal'), 3))
|
||
|
||
# Hochpriorisierte Jobs den besten verfügbaren Druckern zuweisen
|
||
printer_assignments = {printer.id: 0 for printer in printers}
|
||
|
||
for job in sorted_jobs:
|
||
# Drucker mit geringster Anzahl zugewiesener Jobs finden
|
||
best_printer_id = min(printer_assignments, key=printer_assignments.get)
|
||
|
||
job.printer_id = best_printer_id
|
||
job.assigned_at = datetime.now()
|
||
|
||
printer_assignments[best_printer_id] += 1
|
||
optimized_count += 1
|
||
|
||
return optimized_count
|
||
|
||
def validate_optimization_settings(settings):
|
||
"""
|
||
Validiert die Optimierungs-Einstellungen auf Korrektheit und Sicherheit
|
||
Verhindert ungültige Parameter die das System beeinträchtigen könnten
|
||
"""
|
||
try:
|
||
# Algorithmus validieren
|
||
valid_algorithms = ['round_robin', 'load_balance', 'priority_based']
|
||
if settings.get('algorithm') not in valid_algorithms:
|
||
return False
|
||
|
||
# Numerische Werte validieren
|
||
max_batch_size = settings.get('max_batch_size', 10)
|
||
if not isinstance(max_batch_size, int) or max_batch_size < 1 or max_batch_size > 50:
|
||
return False
|
||
|
||
time_window = settings.get('time_window', 24)
|
||
if not isinstance(time_window, int) or time_window < 1 or time_window > 168:
|
||
return False
|
||
|
||
return True
|
||
|
||
except Exception:
|
||
return False
|
||
|
||
# ===== GASTANTRÄGE API-ROUTEN =====
|
||
|
||
# ===== NEUE SYSTEM API-ROUTEN =====
|
||
|
||
# ===== FORM VALIDATION API =====
|
||
@app.route('/api/validation/client-js', methods=['GET'])
|
||
def get_validation_js():
|
||
"""Liefert Client-seitige Validierungs-JavaScript"""
|
||
try:
|
||
js_content = get_client_validation_js()
|
||
response = make_response(js_content)
|
||
response.headers['Content-Type'] = 'application/javascript'
|
||
response.headers['Cache-Control'] = 'public, max-age=3600' # 1 Stunde Cache
|
||
return response
|
||
except Exception as e:
|
||
app_logger.error(f"Fehler beim Laden des Validierungs-JS: {str(e)}")
|
||
return "console.error('Validierungs-JavaScript konnte nicht geladen werden');", 500
|
||
|
||
@app.route('/api/validation/validate-form', methods=['POST'])
|
||
def validate_form_api():
|
||
"""API-Endpunkt für Formular-Validierung"""
|
||
try:
|
||
data = request.get_json() or {}
|
||
form_type = data.get('form_type')
|
||
form_data = data.get('data', {})
|
||
|
||
# Validator basierend auf Form-Typ auswählen
|
||
if form_type == 'user_registration':
|
||
validator = get_user_registration_validator()
|
||
elif form_type == 'job_creation':
|
||
validator = get_job_creation_validator()
|
||
elif form_type == 'printer_creation':
|
||
validator = get_printer_creation_validator()
|
||
elif form_type == 'guest_request':
|
||
validator = get_guest_request_validator()
|
||
else:
|
||
return jsonify({'success': False, 'error': 'Unbekannter Formular-Typ'}), 400
|
||
|
||
# Validierung durchführen
|
||
result = validator.validate(form_data)
|
||
|
||
return jsonify({
|
||
'success': result.is_valid,
|
||
'errors': result.errors,
|
||
'warnings': result.warnings,
|
||
'cleaned_data': result.cleaned_data if result.is_valid else {}
|
||
})
|
||
|
||
except Exception as e:
|
||
app_logger.error(f"Fehler bei Formular-Validierung: {str(e)}")
|
||
return jsonify({'success': False, 'error': str(e)}), 500
|
||
|
||
# ===== REPORT GENERATOR API =====
|
||
@app.route('/api/reports/generate', methods=['POST'])
|
||
@login_required
|
||
def generate_report():
|
||
"""Generiert Reports in verschiedenen Formaten"""
|
||
try:
|
||
data = request.get_json() or {}
|
||
report_type = data.get('type', 'comprehensive')
|
||
format_type = data.get('format', 'pdf')
|
||
filters = data.get('filters', {})
|
||
|
||
# Report-Konfiguration erstellen
|
||
config = ReportConfig(
|
||
title=f"MYP System Report - {report_type.title()}",
|
||
subtitle=f"Generiert am {datetime.now().strftime('%d.%m.%Y %H:%M')}",
|
||
author=current_user.name if current_user.is_authenticated else "System"
|
||
)
|
||
|
||
# Report-Daten basierend auf Typ sammeln
|
||
if report_type == 'jobs':
|
||
report_data = JobReportBuilder.build_jobs_report(
|
||
start_date=filters.get('start_date'),
|
||
end_date=filters.get('end_date'),
|
||
user_id=filters.get('user_id'),
|
||
printer_id=filters.get('printer_id')
|
||
)
|
||
elif report_type == 'users':
|
||
report_data = UserReportBuilder.build_users_report(
|
||
include_inactive=filters.get('include_inactive', False)
|
||
)
|
||
elif report_type == 'printers':
|
||
report_data = PrinterReportBuilder.build_printers_report(
|
||
include_inactive=filters.get('include_inactive', False)
|
||
)
|
||
else:
|
||
# Umfassender Report
|
||
report_bytes = generate_comprehensive_report(
|
||
format_type=format_type,
|
||
start_date=filters.get('start_date'),
|
||
end_date=filters.get('end_date'),
|
||
user_id=current_user.id if not current_user.is_admin else None
|
||
)
|
||
|
||
response = make_response(report_bytes)
|
||
response.headers['Content-Type'] = f'application/{format_type}'
|
||
response.headers['Content-Disposition'] = f'attachment; filename="myp_report.{format_type}"'
|
||
return response
|
||
|
||
# Generator erstellen und Report generieren
|
||
generator = ReportFactory.create_generator(format_type, config)
|
||
|
||
# Daten zum Generator hinzufügen
|
||
for section_name, section_data in report_data.items():
|
||
if isinstance(section_data, list):
|
||
generator.add_data_section(section_name, section_data)
|
||
|
||
# Report in BytesIO generieren
|
||
import io
|
||
output = io.BytesIO()
|
||
if generator.generate(output):
|
||
output.seek(0)
|
||
response = make_response(output.read())
|
||
response.headers['Content-Type'] = f'application/{format_type}'
|
||
response.headers['Content-Disposition'] = f'attachment; filename="myp_{report_type}_report.{format_type}"'
|
||
return response
|
||
else:
|
||
return jsonify({'error': 'Report-Generierung fehlgeschlagen'}), 500
|
||
|
||
except Exception as e:
|
||
app_logger.error(f"Fehler bei Report-Generierung: {str(e)}")
|
||
return jsonify({'error': str(e)}), 500
|
||
|
||
# ===== REALTIME DASHBOARD API =====
|
||
@app.route('/api/dashboard/config', methods=['GET'])
|
||
@login_required
|
||
def get_dashboard_config():
|
||
"""Holt Dashboard-Konfiguration für aktuellen Benutzer"""
|
||
try:
|
||
config = dashboard_manager.get_dashboard_config(current_user.id)
|
||
return jsonify(config)
|
||
except Exception as e:
|
||
app_logger.error(f"Fehler beim Laden der Dashboard-Konfiguration: {str(e)}")
|
||
return jsonify({'error': str(e)}), 500
|
||
|
||
@app.route('/api/dashboard/widgets/<widget_id>/data', methods=['GET'])
|
||
@login_required
|
||
def get_widget_data(widget_id):
|
||
"""Holt Daten für ein spezifisches Widget"""
|
||
try:
|
||
data = dashboard_manager._get_widget_data(widget_id)
|
||
return jsonify({
|
||
'widget_id': widget_id,
|
||
'data': data,
|
||
'timestamp': datetime.now().isoformat()
|
||
})
|
||
except Exception as e:
|
||
app_logger.error(f"Fehler beim Laden der Widget-Daten für {widget_id}: {str(e)}")
|
||
return jsonify({'error': str(e)}), 500
|
||
|
||
@app.route('/api/dashboard/emit-event', methods=['POST'])
|
||
@login_required
|
||
def emit_dashboard_event():
|
||
"""Sendet ein Dashboard-Ereignis"""
|
||
try:
|
||
data = request.get_json() or {}
|
||
event_type = EventType(data.get('event_type'))
|
||
event_data = data.get('data', {})
|
||
priority = data.get('priority', 'normal')
|
||
|
||
event = DashboardEvent(
|
||
event_type=event_type,
|
||
data=event_data,
|
||
timestamp=datetime.now(),
|
||
user_id=current_user.id,
|
||
priority=priority
|
||
)
|
||
|
||
dashboard_manager.emit_event(event)
|
||
return jsonify({'success': True})
|
||
|
||
except Exception as e:
|
||
app_logger.error(f"Fehler beim Senden des Dashboard-Ereignisses: {str(e)}")
|
||
return jsonify({'error': str(e)}), 500
|
||
|
||
@app.route('/api/dashboard/client-js', methods=['GET'])
|
||
def get_dashboard_js():
|
||
"""Liefert Client-seitige Dashboard-JavaScript"""
|
||
try:
|
||
js_content = get_dashboard_client_js()
|
||
response = make_response(js_content)
|
||
response.headers['Content-Type'] = 'application/javascript'
|
||
response.headers['Cache-Control'] = 'public, max-age=1800' # 30 Minuten Cache
|
||
return response
|
||
except Exception as e:
|
||
app_logger.error(f"Fehler beim Laden des Dashboard-JS: {str(e)}")
|
||
return "console.error('Dashboard-JavaScript konnte nicht geladen werden');", 500
|
||
|
||
# ===== DRAG & DROP API =====
|
||
@app.route('/api/dragdrop/update-job-order', methods=['POST'])
|
||
@login_required
|
||
def update_job_order():
|
||
"""Aktualisiert die Job-Reihenfolge per Drag & Drop"""
|
||
try:
|
||
data = request.get_json() or {}
|
||
printer_id = data.get('printer_id')
|
||
job_ids = data.get('job_ids', [])
|
||
|
||
if not printer_id or not isinstance(job_ids, list):
|
||
return jsonify({'error': 'Ungültige Parameter'}), 400
|
||
|
||
success = drag_drop_manager.update_job_order(printer_id, job_ids)
|
||
|
||
if success:
|
||
# Dashboard-Event senden
|
||
emit_system_alert(
|
||
f"Job-Reihenfolge für Drucker {printer_id} aktualisiert",
|
||
alert_type="info",
|
||
priority="normal"
|
||
)
|
||
|
||
return jsonify({
|
||
'success': True,
|
||
'message': 'Job-Reihenfolge erfolgreich aktualisiert'
|
||
})
|
||
else:
|
||
return jsonify({'error': 'Fehler beim Aktualisieren der Job-Reihenfolge'}), 500
|
||
|
||
except Exception as e:
|
||
app_logger.error(f"Fehler beim Aktualisieren der Job-Reihenfolge: {str(e)}")
|
||
return jsonify({'error': str(e)}), 500
|
||
|
||
@app.route('/api/dragdrop/get-job-order/<int:printer_id>', methods=['GET'])
|
||
@login_required
|
||
def get_job_order_api(printer_id):
|
||
"""Holt die aktuelle Job-Reihenfolge für einen Drucker"""
|
||
try:
|
||
job_ids = drag_drop_manager.get_job_order(printer_id)
|
||
ordered_jobs = drag_drop_manager.get_ordered_jobs_for_printer(printer_id)
|
||
|
||
job_data = []
|
||
for job in ordered_jobs:
|
||
job_data.append({
|
||
'id': job.id,
|
||
'name': job.name,
|
||
'duration_minutes': job.duration_minutes,
|
||
'user_name': job.user.name if job.user else 'Unbekannt',
|
||
'status': job.status,
|
||
'created_at': job.created_at.isoformat() if job.created_at else None
|
||
})
|
||
|
||
return jsonify({
|
||
'printer_id': printer_id,
|
||
'job_ids': job_ids,
|
||
'jobs': job_data,
|
||
'total_jobs': len(job_data)
|
||
})
|
||
|
||
except Exception as e:
|
||
app_logger.error(f"Fehler beim Abrufen der Job-Reihenfolge: {str(e)}")
|
||
return jsonify({'error': str(e)}), 500
|
||
|
||
@app.route('/api/dragdrop/upload-session', methods=['POST'])
|
||
@login_required
|
||
def create_upload_session():
|
||
"""Erstellt eine neue Upload-Session"""
|
||
try:
|
||
import uuid
|
||
session_id = str(uuid.uuid4())
|
||
drag_drop_manager.create_upload_session(session_id)
|
||
|
||
return jsonify({
|
||
'session_id': session_id,
|
||
'success': True
|
||
})
|
||
|
||
except Exception as e:
|
||
app_logger.error(f"Fehler beim Erstellen der Upload-Session: {str(e)}")
|
||
return jsonify({'error': str(e)}), 500
|
||
|
||
@app.route('/api/dragdrop/upload-progress/<session_id>', methods=['GET'])
|
||
@login_required
|
||
def get_upload_progress(session_id):
|
||
"""Holt Upload-Progress für eine Session"""
|
||
try:
|
||
progress = drag_drop_manager.get_session_progress(session_id)
|
||
return jsonify(progress)
|
||
except Exception as e:
|
||
app_logger.error(f"Fehler beim Abrufen des Upload-Progress: {str(e)}")
|
||
return jsonify({'error': str(e)}), 500
|
||
|
||
@app.route('/api/dragdrop/client-js', methods=['GET'])
|
||
def get_dragdrop_js():
|
||
"""Liefert Client-seitige Drag & Drop JavaScript"""
|
||
try:
|
||
js_content = get_drag_drop_javascript()
|
||
response = make_response(js_content)
|
||
response.headers['Content-Type'] = 'application/javascript'
|
||
response.headers['Cache-Control'] = 'public, max-age=3600'
|
||
return response
|
||
except Exception as e:
|
||
app_logger.error(f"Fehler beim Laden des Drag & Drop JS: {str(e)}")
|
||
return "console.error('Drag & Drop JavaScript konnte nicht geladen werden');", 500
|
||
|
||
@app.route('/api/dragdrop/client-css', methods=['GET'])
|
||
def get_dragdrop_css():
|
||
"""Liefert Client-seitige Drag & Drop CSS"""
|
||
try:
|
||
css_content = get_drag_drop_css()
|
||
response = make_response(css_content)
|
||
response.headers['Content-Type'] = 'text/css'
|
||
response.headers['Cache-Control'] = 'public, max-age=3600'
|
||
return response
|
||
except Exception as e:
|
||
app_logger.error(f"Fehler beim Laden des Drag & Drop CSS: {str(e)}")
|
||
return "/* Drag & Drop CSS konnte nicht geladen werden */", 500
|
||
|
||
# ===== ADVANCED TABLES API =====
|
||
@app.route('/api/tables/query', methods=['POST'])
|
||
@login_required
|
||
def query_advanced_table():
|
||
"""Führt erweiterte Tabellen-Abfragen durch"""
|
||
try:
|
||
data = request.get_json() or {}
|
||
table_type = data.get('table_type')
|
||
query_params = data.get('query', {})
|
||
|
||
# Tabellen-Konfiguration erstellen
|
||
if table_type == 'jobs':
|
||
config = create_table_config(
|
||
'jobs',
|
||
['id', 'name', 'user_name', 'printer_name', 'status', 'created_at'],
|
||
base_query='Job'
|
||
)
|
||
elif table_type == 'printers':
|
||
config = create_table_config(
|
||
'printers',
|
||
['id', 'name', 'model', 'location', 'status', 'ip_address'],
|
||
base_query='Printer'
|
||
)
|
||
elif table_type == 'users':
|
||
config = create_table_config(
|
||
'users',
|
||
['id', 'name', 'email', 'role', 'active', 'last_login'],
|
||
base_query='User'
|
||
)
|
||
else:
|
||
return jsonify({'error': 'Unbekannter Tabellen-Typ'}), 400
|
||
|
||
# Erweiterte Abfrage erstellen
|
||
query_builder = AdvancedTableQuery(config)
|
||
|
||
# Filter anwenden
|
||
if 'filters' in query_params:
|
||
for filter_data in query_params['filters']:
|
||
query_builder.add_filter(
|
||
filter_data['column'],
|
||
filter_data['operator'],
|
||
filter_data['value']
|
||
)
|
||
|
||
# Sortierung anwenden
|
||
if 'sort' in query_params:
|
||
query_builder.set_sorting(
|
||
query_params['sort']['column'],
|
||
query_params['sort']['direction']
|
||
)
|
||
|
||
# Paginierung anwenden
|
||
if 'pagination' in query_params:
|
||
query_builder.set_pagination(
|
||
query_params['pagination']['page'],
|
||
query_params['pagination']['per_page']
|
||
)
|
||
|
||
# Abfrage ausführen
|
||
result = query_builder.execute()
|
||
|
||
return jsonify(result)
|
||
|
||
except Exception as e:
|
||
app_logger.error(f"Fehler bei erweiterte Tabellen-Abfrage: {str(e)}")
|
||
return jsonify({'error': str(e)}), 500
|
||
|
||
@app.route('/api/tables/export', methods=['POST'])
|
||
@login_required
|
||
def export_table_data():
|
||
"""Exportiert Tabellen-Daten in verschiedenen Formaten"""
|
||
try:
|
||
data = request.get_json() or {}
|
||
table_type = data.get('table_type')
|
||
export_format = data.get('format', 'csv')
|
||
query_params = data.get('query', {})
|
||
|
||
# Vollständige Export-Logik implementierung
|
||
app_logger.info(f"📊 Starte Tabellen-Export: {table_type} als {export_format}")
|
||
|
||
# Tabellen-Konfiguration basierend auf Typ erstellen
|
||
if table_type == 'jobs':
|
||
config = create_table_config(
|
||
'jobs',
|
||
['id', 'filename', 'status', 'printer_name', 'user_name', 'created_at', 'completed_at'],
|
||
base_query='Job'
|
||
)
|
||
elif table_type == 'printers':
|
||
config = create_table_config(
|
||
'printers',
|
||
['id', 'name', 'ip_address', 'status', 'location', 'model'],
|
||
base_query='Printer'
|
||
)
|
||
elif table_type == 'users':
|
||
config = create_table_config(
|
||
'users',
|
||
['id', 'name', 'email', 'role', 'active', 'last_login'],
|
||
base_query='User'
|
||
)
|
||
else:
|
||
return jsonify({'error': 'Unbekannter Tabellen-Typ für Export'}), 400
|
||
|
||
# Erweiterte Abfrage für Export-Daten erstellen
|
||
query_builder = AdvancedTableQuery(config)
|
||
|
||
# Filter aus Query-Parametern anwenden
|
||
if 'filters' in query_params:
|
||
for filter_data in query_params['filters']:
|
||
query_builder.add_filter(
|
||
filter_data['column'],
|
||
filter_data['operator'],
|
||
filter_data['value']
|
||
)
|
||
|
||
# Sortierung anwenden
|
||
if 'sort' in query_params:
|
||
query_builder.set_sorting(
|
||
query_params['sort']['column'],
|
||
query_params['sort']['direction']
|
||
)
|
||
|
||
# Für Export: Alle Daten ohne Paginierung
|
||
query_builder.set_pagination(1, 10000) # Maximale Anzahl für Export
|
||
|
||
# Daten abrufen
|
||
result = query_builder.execute()
|
||
export_data = result.get('data', [])
|
||
|
||
if export_format == 'csv':
|
||
import csv
|
||
import io
|
||
|
||
# CSV-Export implementierung
|
||
output = io.StringIO()
|
||
writer = csv.writer(output, delimiter=';', quoting=csv.QUOTE_MINIMAL)
|
||
|
||
# Header-Zeile schreiben
|
||
if export_data:
|
||
headers = list(export_data[0].keys())
|
||
writer.writerow(headers)
|
||
|
||
# Daten-Zeilen schreiben
|
||
for row in export_data:
|
||
# Werte für CSV formatieren
|
||
formatted_row = []
|
||
for value in row.values():
|
||
if value is None:
|
||
formatted_row.append('')
|
||
elif isinstance(value, datetime):
|
||
formatted_row.append(value.strftime('%d.%m.%Y %H:%M:%S'))
|
||
else:
|
||
formatted_row.append(str(value))
|
||
writer.writerow(formatted_row)
|
||
|
||
# Response erstellen
|
||
csv_content = output.getvalue()
|
||
output.close()
|
||
|
||
response = make_response(csv_content)
|
||
response.headers['Content-Type'] = 'text/csv; charset=utf-8'
|
||
response.headers['Content-Disposition'] = f'attachment; filename="{table_type}_export_{datetime.now().strftime("%Y%m%d_%H%M%S")}.csv"'
|
||
|
||
app_logger.info(f"✅ CSV-Export erfolgreich: {len(export_data)} Datensätze")
|
||
return response
|
||
|
||
elif export_format == 'json':
|
||
# JSON-Export implementierung
|
||
json_content = json.dumps(export_data, indent=2, default=str, ensure_ascii=False)
|
||
|
||
response = make_response(json_content)
|
||
response.headers['Content-Type'] = 'application/json; charset=utf-8'
|
||
response.headers['Content-Disposition'] = f'attachment; filename="{table_type}_export_{datetime.now().strftime("%Y%m%d_%H%M%S")}.json"'
|
||
|
||
app_logger.info(f"✅ JSON-Export erfolgreich: {len(export_data)} Datensätze")
|
||
return response
|
||
|
||
elif export_format == 'excel':
|
||
# Excel-Export implementierung (falls openpyxl verfügbar)
|
||
try:
|
||
import openpyxl
|
||
from openpyxl.utils.dataframe import dataframe_to_rows
|
||
import pandas as pd
|
||
|
||
# DataFrame erstellen
|
||
df = pd.DataFrame(export_data)
|
||
|
||
# Excel-Datei in Memory erstellen
|
||
output = io.BytesIO()
|
||
with pd.ExcelWriter(output, engine='openpyxl') as writer:
|
||
df.to_excel(writer, sheet_name=table_type.capitalize(), index=False)
|
||
|
||
output.seek(0)
|
||
|
||
response = make_response(output.getvalue())
|
||
response.headers['Content-Type'] = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||
response.headers['Content-Disposition'] = f'attachment; filename="{table_type}_export_{datetime.now().strftime("%Y%m%d_%H%M%S")}.xlsx"'
|
||
|
||
app_logger.info(f"✅ Excel-Export erfolgreich: {len(export_data)} Datensätze")
|
||
return response
|
||
|
||
except ImportError:
|
||
app_logger.warning("⚠️ Excel-Export nicht verfügbar - openpyxl/pandas fehlt")
|
||
return jsonify({'error': 'Excel-Export nicht verfügbar - erforderliche Bibliotheken fehlen'}), 400
|
||
|
||
except Exception as e:
|
||
app_logger.error(f"Fehler beim Tabellen-Export: {str(e)}")
|
||
return jsonify({'error': str(e)}), 500
|
||
|
||
@app.route('/api/tables/client-js', methods=['GET'])
|
||
def get_tables_js():
|
||
"""Liefert Client-seitige Advanced Tables JavaScript"""
|
||
try:
|
||
js_content = get_advanced_tables_js()
|
||
response = make_response(js_content)
|
||
response.headers['Content-Type'] = 'application/javascript'
|
||
response.headers['Cache-Control'] = 'public, max-age=3600'
|
||
return response
|
||
except Exception as e:
|
||
app_logger.error(f"Fehler beim Laden des Tables-JS: {str(e)}")
|
||
return "console.error('Advanced Tables JavaScript konnte nicht geladen werden');", 500
|
||
|
||
@app.route('/api/tables/client-css', methods=['GET'])
|
||
def get_tables_css():
|
||
"""Liefert Client-seitige Advanced Tables CSS"""
|
||
try:
|
||
css_content = get_advanced_tables_css()
|
||
response = make_response(css_content)
|
||
response.headers['Content-Type'] = 'text/css'
|
||
response.headers['Cache-Control'] = 'public, max-age=3600'
|
||
return response
|
||
except Exception as e:
|
||
app_logger.error(f"Fehler beim Laden des Tables-CSS: {str(e)}")
|
||
return "/* Advanced Tables CSS konnte nicht geladen werden */", 500
|
||
|
||
# ===== MAINTENANCE SYSTEM API =====
|
||
@app.route('/api/maintenance/tasks', methods=['GET', 'POST'])
|
||
@login_required
|
||
def maintenance_tasks():
|
||
"""Wartungsaufgaben abrufen oder erstellen"""
|
||
if request.method == 'GET':
|
||
try:
|
||
filters = {
|
||
'printer_id': request.args.get('printer_id', type=int),
|
||
'status': request.args.get('status'),
|
||
'priority': request.args.get('priority'),
|
||
'due_date_from': request.args.get('due_date_from'),
|
||
'due_date_to': request.args.get('due_date_to')
|
||
}
|
||
|
||
tasks = maintenance_manager.get_tasks(filters)
|
||
return jsonify({
|
||
'tasks': [task.to_dict() for task in tasks],
|
||
'total': len(tasks)
|
||
})
|
||
|
||
except Exception as e:
|
||
app_logger.error(f"Fehler beim Abrufen der Wartungsaufgaben: {str(e)}")
|
||
return jsonify({'error': str(e)}), 500
|
||
|
||
elif request.method == 'POST':
|
||
try:
|
||
data = request.get_json() or {}
|
||
|
||
task = create_maintenance_task(
|
||
printer_id=data.get('printer_id'),
|
||
task_type=MaintenanceType(data.get('task_type')),
|
||
title=data.get('title'),
|
||
description=data.get('description'),
|
||
priority=data.get('priority', 'normal'),
|
||
assigned_to=data.get('assigned_to'),
|
||
due_date=data.get('due_date')
|
||
)
|
||
|
||
if task:
|
||
# Dashboard-Event senden
|
||
emit_system_alert(
|
||
f"Neue Wartungsaufgabe erstellt: {task.title}",
|
||
alert_type="info",
|
||
priority=task.priority
|
||
)
|
||
|
||
return jsonify({
|
||
'success': True,
|
||
'task': task.to_dict(),
|
||
'message': 'Wartungsaufgabe erfolgreich erstellt'
|
||
})
|
||
else:
|
||
return jsonify({'error': 'Fehler beim Erstellen der Wartungsaufgabe'}), 500
|
||
|
||
except Exception as e:
|
||
app_logger.error(f"Fehler beim Erstellen der Wartungsaufgabe: {str(e)}")
|
||
return jsonify({'error': str(e)}), 500
|
||
|
||
@app.route('/api/maintenance/tasks/<int:task_id>/status', methods=['PUT'])
|
||
@login_required
|
||
def update_maintenance_task_status(task_id):
|
||
"""Aktualisiert den Status einer Wartungsaufgabe"""
|
||
try:
|
||
data = request.get_json() or {}
|
||
new_status = MaintenanceStatus(data.get('status'))
|
||
notes = data.get('notes', '')
|
||
|
||
success = update_maintenance_status(
|
||
task_id=task_id,
|
||
new_status=new_status,
|
||
updated_by=current_user.id,
|
||
notes=notes
|
||
)
|
||
|
||
if success:
|
||
return jsonify({
|
||
'success': True,
|
||
'message': 'Wartungsaufgaben-Status erfolgreich aktualisiert'
|
||
})
|
||
else:
|
||
return jsonify({'error': 'Fehler beim Aktualisieren des Status'}), 500
|
||
|
||
except Exception as e:
|
||
app_logger.error(f"Fehler beim Aktualisieren des Wartungsaufgaben-Status: {str(e)}")
|
||
return jsonify({'error': str(e)}), 500
|
||
|
||
@app.route('/api/maintenance/overview', methods=['GET'])
|
||
@login_required
|
||
def get_maintenance_overview():
|
||
"""Holt Wartungs-Übersicht"""
|
||
try:
|
||
overview = get_maintenance_overview()
|
||
return jsonify(overview)
|
||
except Exception as e:
|
||
app_logger.error(f"Fehler beim Abrufen der Wartungs-Übersicht: {str(e)}")
|
||
return jsonify({'error': str(e)}), 500
|
||
|
||
@app.route('/api/maintenance/schedule', methods=['POST'])
|
||
@login_required
|
||
@admin_required
|
||
def schedule_maintenance_api():
|
||
"""Plant automatische Wartungen"""
|
||
try:
|
||
data = request.get_json() or {}
|
||
|
||
schedule = schedule_maintenance(
|
||
printer_id=data.get('printer_id'),
|
||
maintenance_type=MaintenanceType(data.get('maintenance_type')),
|
||
interval_days=data.get('interval_days'),
|
||
start_date=data.get('start_date')
|
||
)
|
||
|
||
if schedule:
|
||
return jsonify({
|
||
'success': True,
|
||
'schedule': schedule.to_dict(),
|
||
'message': 'Wartungsplan erfolgreich erstellt'
|
||
})
|
||
else:
|
||
return jsonify({'error': 'Fehler beim Erstellen des Wartungsplans'}), 500
|
||
|
||
except Exception as e:
|
||
app_logger.error(f"Fehler beim Planen der Wartung: {str(e)}")
|
||
return jsonify({'error': str(e)}), 500
|
||
|
||
# ===== MULTI-LOCATION SYSTEM API =====
|
||
@app.route('/api/locations', methods=['GET', 'POST'])
|
||
@login_required
|
||
def locations():
|
||
"""Standorte abrufen oder erstellen"""
|
||
if request.method == 'GET':
|
||
try:
|
||
filters = {
|
||
'location_type': request.args.get('type'),
|
||
'active_only': request.args.get('active_only', 'true').lower() == 'true'
|
||
}
|
||
|
||
locations = location_manager.get_locations(filters)
|
||
return jsonify({
|
||
'locations': [loc.to_dict() for loc in locations],
|
||
'total': len(locations)
|
||
})
|
||
|
||
except Exception as e:
|
||
app_logger.error(f"Fehler beim Abrufen der Standorte: {str(e)}")
|
||
return jsonify({'error': str(e)}), 500
|
||
|
||
elif request.method == 'POST':
|
||
try:
|
||
data = request.get_json() or {}
|
||
|
||
location = create_location(
|
||
name=data.get('name'),
|
||
location_type=LocationType(data.get('type')),
|
||
address=data.get('address'),
|
||
description=data.get('description'),
|
||
coordinates=data.get('coordinates'),
|
||
parent_location_id=data.get('parent_location_id')
|
||
)
|
||
|
||
if location:
|
||
return jsonify({
|
||
'success': True,
|
||
'location': location.to_dict(),
|
||
'message': 'Standort erfolgreich erstellt'
|
||
})
|
||
else:
|
||
return jsonify({'error': 'Fehler beim Erstellen des Standorts'}), 500
|
||
|
||
except Exception as e:
|
||
app_logger.error(f"Fehler beim Erstellen des Standorts: {str(e)}")
|
||
return jsonify({'error': str(e)}), 500
|
||
|
||
@app.route('/api/locations/<int:location_id>/users', methods=['GET', 'POST'])
|
||
@login_required
|
||
@admin_required
|
||
def location_users(location_id):
|
||
"""Benutzer-Zuweisungen für einen Standort verwalten"""
|
||
if request.method == 'GET':
|
||
try:
|
||
users = location_manager.get_location_users(location_id)
|
||
return jsonify({
|
||
'location_id': location_id,
|
||
'users': [user.to_dict() for user in users],
|
||
'total': len(users)
|
||
})
|
||
|
||
except Exception as e:
|
||
app_logger.error(f"Fehler beim Abrufen der Standort-Benutzer: {str(e)}")
|
||
return jsonify({'error': str(e)}), 500
|
||
|
||
elif request.method == 'POST':
|
||
try:
|
||
data = request.get_json() or {}
|
||
|
||
success = assign_user_to_location(
|
||
user_id=data.get('user_id'),
|
||
location_id=location_id,
|
||
access_level=AccessLevel(data.get('access_level', 'READ')),
|
||
valid_until=data.get('valid_until')
|
||
)
|
||
|
||
if success:
|
||
return jsonify({
|
||
'success': True,
|
||
'message': 'Benutzer erfolgreich zu Standort zugewiesen'
|
||
})
|
||
else:
|
||
return jsonify({'error': 'Fehler bei der Benutzer-Zuweisung'}), 500
|
||
|
||
except Exception as e:
|
||
app_logger.error(f"Fehler bei der Benutzer-Zuweisung: {str(e)}")
|
||
return jsonify({'error': str(e)}), 500
|
||
|
||
@app.route('/api/locations/user/<int:user_id>', methods=['GET'])
|
||
@login_required
|
||
def get_user_locations_api(user_id):
|
||
"""Holt alle Standorte eines Benutzers"""
|
||
try:
|
||
# Berechtigung prüfen
|
||
if current_user.id != user_id and not current_user.is_admin:
|
||
return jsonify({'error': 'Keine Berechtigung'}), 403
|
||
|
||
locations = get_user_locations(user_id)
|
||
return jsonify({
|
||
'user_id': user_id,
|
||
'locations': [loc.to_dict() for loc in locations],
|
||
'total': len(locations)
|
||
})
|
||
|
||
except Exception as e:
|
||
app_logger.error(f"Fehler beim Abrufen der Benutzer-Standorte: {str(e)}")
|
||
return jsonify({'error': str(e)}), 500
|
||
|
||
@app.route('/api/locations/distance', methods=['POST'])
|
||
@login_required
|
||
def calculate_distance_api():
|
||
"""Berechnet Entfernung zwischen zwei Standorten"""
|
||
try:
|
||
data = request.get_json() or {}
|
||
coord1 = data.get('coordinates1') # [lat, lon]
|
||
coord2 = data.get('coordinates2') # [lat, lon]
|
||
|
||
if not coord1 or not coord2:
|
||
return jsonify({'error': 'Koordinaten erforderlich'}), 400
|
||
|
||
distance = calculate_distance(coord1, coord2)
|
||
|
||
return jsonify({
|
||
'distance_km': distance,
|
||
'distance_m': distance * 1000
|
||
})
|
||
|
||
except Exception as e:
|
||
app_logger.error(f"Fehler bei Entfernungsberechnung: {str(e)}")
|
||
return jsonify({'error': str(e)}), 500
|
||
|
||
@app.route('/api/locations/nearest', methods=['POST'])
|
||
@login_required
|
||
def find_nearest_location_api():
|
||
"""Findet den nächstgelegenen Standort"""
|
||
try:
|
||
data = request.get_json() or {}
|
||
coordinates = data.get('coordinates') # [lat, lon]
|
||
location_type = data.get('location_type')
|
||
max_distance = data.get('max_distance', 50) # km
|
||
|
||
if not coordinates:
|
||
return jsonify({'error': 'Koordinaten erforderlich'}), 400
|
||
|
||
nearest = find_nearest_location(
|
||
coordinates=coordinates,
|
||
location_type=LocationType(location_type) if location_type else None,
|
||
max_distance_km=max_distance
|
||
)
|
||
|
||
if nearest:
|
||
location, distance = nearest
|
||
return jsonify({
|
||
'location': location.to_dict(),
|
||
'distance_km': distance
|
||
})
|
||
else:
|
||
return jsonify({
|
||
'location': None,
|
||
'message': 'Kein Standort in der Nähe gefunden'
|
||
})
|
||
|
||
except Exception as e:
|
||
app_logger.error(f"Fehler bei der Suche nach nächstem Standort: {str(e)}")
|
||
return jsonify({'error': str(e)}), 500
|
||
|
||
|
||
def setup_database_with_migrations():
|
||
"""
|
||
Datenbank initialisieren und alle erforderlichen Tabellen erstellen.
|
||
Führt Migrationen für neue Tabellen wie JobOrder durch.
|
||
"""
|
||
try:
|
||
app_logger.info("🔄 Starte Datenbank-Setup und Migrationen...")
|
||
|
||
# Standard-Datenbank-Initialisierung
|
||
init_database()
|
||
|
||
# Explizite Migration für JobOrder-Tabelle
|
||
engine = get_engine()
|
||
|
||
# Erstelle alle Tabellen (nur neue werden tatsächlich erstellt)
|
||
Base.metadata.create_all(engine)
|
||
|
||
# Prüfe ob JobOrder-Tabelle existiert
|
||
from sqlalchemy import inspect
|
||
inspector = inspect(engine)
|
||
existing_tables = inspector.get_table_names()
|
||
|
||
if 'job_orders' in existing_tables:
|
||
app_logger.info("✅ JobOrder-Tabelle bereits vorhanden")
|
||
else:
|
||
# Tabelle manuell erstellen
|
||
JobOrder.__table__.create(engine, checkfirst=True)
|
||
app_logger.info("✅ JobOrder-Tabelle erfolgreich erstellt")
|
||
|
||
# Initial-Admin erstellen falls nicht vorhanden
|
||
create_initial_admin()
|
||
|
||
app_logger.info("✅ Datenbank-Setup und Migrationen erfolgreich abgeschlossen")
|
||
|
||
except Exception as e:
|
||
app_logger.error(f"❌ Fehler bei Datenbank-Setup: {str(e)}")
|
||
raise e
|
||
|
||
# ===== PRIVACY UND TERMS ROUTEN =====
|
||
|
||
@app.route("/privacy")
|
||
def privacy_policy():
|
||
"""Datenschutzerklärung anzeigen"""
|
||
try:
|
||
return render_template("privacy_policy.html", title="Datenschutzerklärung")
|
||
except Exception as e:
|
||
app_logger.error(f"Fehler beim Laden der Datenschutzerklärung: {str(e)}")
|
||
flash("Fehler beim Laden der Datenschutzerklärung", "error")
|
||
return redirect(url_for("index"))
|
||
|
||
@app.route("/terms")
|
||
def terms_of_service():
|
||
"""Nutzungsbedingungen anzeigen"""
|
||
try:
|
||
return render_template("terms_of_service.html", title="Nutzungsbedingungen")
|
||
except Exception as e:
|
||
app_logger.error(f"Fehler beim Laden der Nutzungsbedingungen: {str(e)}")
|
||
flash("Fehler beim Laden der Nutzungsbedingungen", "error")
|
||
return redirect(url_for("index"))
|
||
|
||
@app.route("/legal")
|
||
def legal_notice():
|
||
"""Impressum anzeigen"""
|
||
try:
|
||
return render_template("legal_notice.html", title="Impressum")
|
||
except Exception as e:
|
||
app_logger.error(f"Fehler beim Laden des Impressums: {str(e)}")
|
||
flash("Fehler beim Laden des Impressums", "error")
|
||
return redirect(url_for("index"))
|
||
|
||
@app.route("/api/privacy/accept", methods=["POST"])
|
||
@login_required
|
||
def accept_privacy_policy():
|
||
"""API-Endpunkt für Akzeptierung der Datenschutzerklärung"""
|
||
db_session = get_db_session()
|
||
try:
|
||
data = request.get_json() or {}
|
||
version = data.get("version", "1.0")
|
||
|
||
# Benutzer aus der Datenbank laden
|
||
user = db_session.query(User).filter(User.id == int(current_user.id)).first()
|
||
|
||
if not user:
|
||
return jsonify({"error": "Benutzer nicht gefunden"}), 404
|
||
|
||
# Privacy-Akzeptierung in Benutzer-Einstellungen speichern
|
||
if hasattr(user, 'settings'):
|
||
import json
|
||
settings = json.loads(user.settings) if user.settings else {}
|
||
else:
|
||
settings = session.get('user_settings', {})
|
||
|
||
# Privacy-Akzeptierung hinzufügen
|
||
if 'privacy_acceptance' not in settings:
|
||
settings['privacy_acceptance'] = {}
|
||
|
||
settings['privacy_acceptance'] = {
|
||
'accepted': True,
|
||
'version': version,
|
||
'timestamp': datetime.now().isoformat(),
|
||
'ip_address': request.remote_addr
|
||
}
|
||
|
||
# Einstellungen speichern
|
||
if hasattr(user, 'settings'):
|
||
user.settings = json.dumps(settings)
|
||
user.updated_at = datetime.now()
|
||
db_session.commit()
|
||
else:
|
||
session['user_settings'] = settings
|
||
|
||
user_logger.info(f"Benutzer {current_user.username} hat Datenschutzerklärung v{version} akzeptiert")
|
||
|
||
return jsonify({
|
||
"success": True,
|
||
"message": "Datenschutzerklärung erfolgreich akzeptiert",
|
||
"version": version,
|
||
"timestamp": datetime.now().isoformat()
|
||
})
|
||
|
||
except Exception as e:
|
||
db_session.rollback()
|
||
app_logger.error(f"Fehler bei Privacy-Akzeptierung: {str(e)}")
|
||
return jsonify({"error": "Interner Serverfehler"}), 500
|
||
finally:
|
||
db_session.close()
|
||
|
||
@app.route("/api/terms/accept", methods=["POST"])
|
||
@login_required
|
||
def accept_terms_of_service():
|
||
"""API-Endpunkt für Akzeptierung der Nutzungsbedingungen"""
|
||
db_session = get_db_session()
|
||
try:
|
||
data = request.get_json() or {}
|
||
version = data.get("version", "1.0")
|
||
|
||
# Benutzer aus der Datenbank laden
|
||
user = db_session.query(User).filter(User.id == int(current_user.id)).first()
|
||
|
||
if not user:
|
||
return jsonify({"error": "Benutzer nicht gefunden"}), 404
|
||
|
||
# Terms-Akzeptierung in Benutzer-Einstellungen speichern
|
||
if hasattr(user, 'settings'):
|
||
import json
|
||
settings = json.loads(user.settings) if user.settings else {}
|
||
else:
|
||
settings = session.get('user_settings', {})
|
||
|
||
# Terms-Akzeptierung hinzufügen
|
||
if 'terms_acceptance' not in settings:
|
||
settings['terms_acceptance'] = {}
|
||
|
||
settings['terms_acceptance'] = {
|
||
'accepted': True,
|
||
'version': version,
|
||
'timestamp': datetime.now().isoformat(),
|
||
'ip_address': request.remote_addr
|
||
}
|
||
|
||
# Einstellungen speichern
|
||
if hasattr(user, 'settings'):
|
||
user.settings = json.dumps(settings)
|
||
user.updated_at = datetime.now()
|
||
db_session.commit()
|
||
else:
|
||
session['user_settings'] = settings
|
||
|
||
user_logger.info(f"Benutzer {current_user.username} hat Nutzungsbedingungen v{version} akzeptiert")
|
||
|
||
return jsonify({
|
||
"success": True,
|
||
"message": "Nutzungsbedingungen erfolgreich akzeptiert",
|
||
"version": version,
|
||
"timestamp": datetime.now().isoformat()
|
||
})
|
||
|
||
except Exception as e:
|
||
db_session.rollback()
|
||
app_logger.error(f"Fehler bei Terms-Akzeptierung: {str(e)}")
|
||
return jsonify({"error": "Interner Serverfehler"}), 500
|
||
finally:
|
||
db_session.close()
|
||
|
||
@app.route("/api/legal/status", methods=["GET"])
|
||
@login_required
|
||
def get_legal_status():
|
||
"""API-Endpunkt für Abfrage des rechtlichen Status (Privacy/Terms Akzeptierung)"""
|
||
try:
|
||
# Benutzer-Einstellungen laden
|
||
if hasattr(current_user, 'settings') and current_user.settings:
|
||
import json
|
||
settings = json.loads(current_user.settings)
|
||
else:
|
||
settings = session.get('user_settings', {})
|
||
|
||
privacy_acceptance = settings.get('privacy_acceptance', {})
|
||
terms_acceptance = settings.get('terms_acceptance', {})
|
||
|
||
return jsonify({
|
||
"success": True,
|
||
"legal_status": {
|
||
"privacy_policy": {
|
||
"accepted": privacy_acceptance.get('accepted', False),
|
||
"version": privacy_acceptance.get('version'),
|
||
"timestamp": privacy_acceptance.get('timestamp')
|
||
},
|
||
"terms_of_service": {
|
||
"accepted": terms_acceptance.get('accepted', False),
|
||
"version": terms_acceptance.get('version'),
|
||
"timestamp": terms_acceptance.get('timestamp')
|
||
},
|
||
"compliance_required": not (
|
||
privacy_acceptance.get('accepted', False) and
|
||
terms_acceptance.get('accepted', False)
|
||
)
|
||
}
|
||
})
|
||
|
||
except Exception as e:
|
||
app_logger.error(f"Fehler bei Legal-Status-Abfrage: {str(e)}")
|
||
return jsonify({"error": "Interner Serverfehler"}), 500
|
||
|
||
|
||
# ===== STARTUP UND MAIN =====
|
||
if __name__ == "__main__":
|
||
import sys
|
||
import signal
|
||
import os
|
||
|
||
# Debug-Modus prüfen
|
||
debug_mode = len(sys.argv) > 1 and sys.argv[1] == "--debug"
|
||
|
||
# Windows-spezifische Umgebungsvariablen setzen für bessere Flask-Kompatibilität
|
||
if os.name == 'nt' and debug_mode:
|
||
# Entferne problematische Werkzeug-Variablen
|
||
os.environ.pop('WERKZEUG_SERVER_FD', None)
|
||
os.environ.pop('WERKZEUG_RUN_MAIN', None)
|
||
|
||
# Setze saubere Umgebung
|
||
os.environ['FLASK_ENV'] = 'development'
|
||
os.environ['PYTHONIOENCODING'] = 'utf-8'
|
||
os.environ['PYTHONUTF8'] = '1'
|
||
|
||
# Windows-spezifisches Signal-Handling für ordnungsgemäßes Shutdown
|
||
def signal_handler(sig, frame):
|
||
"""Signal-Handler für ordnungsgemäßes Shutdown."""
|
||
app_logger.warning(f"🛑 Signal {sig} empfangen - fahre System herunter...")
|
||
try:
|
||
# Queue Manager stoppen
|
||
app_logger.info("🔄 Beende Queue Manager...")
|
||
stop_queue_manager()
|
||
|
||
# Scheduler stoppen falls aktiviert
|
||
if SCHEDULER_ENABLED and scheduler:
|
||
try:
|
||
scheduler.stop()
|
||
app_logger.info("Job-Scheduler gestoppt")
|
||
except Exception as e:
|
||
app_logger.error(f"Fehler beim Stoppen des Schedulers: {str(e)}")
|
||
|
||
# ===== DATENBANKVERBINDUNGEN ORDNUNGSGEMÄSS SCHLIESSEN =====
|
||
app_logger.info("💾 Führe Datenbank-Cleanup durch...")
|
||
try:
|
||
from models import get_db_session, create_optimized_engine
|
||
from sqlalchemy import text
|
||
|
||
# WAL-Checkpoint ausführen um .shm und .wal Dateien zu bereinigen
|
||
engine = create_optimized_engine()
|
||
|
||
with engine.connect() as conn:
|
||
# Vollständiger WAL-Checkpoint (TRUNCATE-Modus)
|
||
app_logger.info("📝 Führe WAL-Checkpoint durch...")
|
||
result = conn.execute(text("PRAGMA wal_checkpoint(TRUNCATE)")).fetchone()
|
||
|
||
if result:
|
||
app_logger.info(f"WAL-Checkpoint abgeschlossen: {result[1]} Seiten übertragen, {result[2]} Seiten zurückgesetzt")
|
||
|
||
# Alle pending Transaktionen committen
|
||
conn.commit()
|
||
|
||
# Journal-Mode zu DELETE wechseln (entfernt .wal/.shm Dateien)
|
||
app_logger.info("📁 Schalte Journal-Mode um...")
|
||
conn.execute(text("PRAGMA journal_mode=DELETE"))
|
||
|
||
# Optimize und Vacuum für sauberen Zustand
|
||
conn.execute(text("PRAGMA optimize"))
|
||
conn.execute(text("VACUUM"))
|
||
|
||
conn.commit()
|
||
|
||
# Engine-Connection-Pool schließen
|
||
engine.dispose()
|
||
|
||
app_logger.info("✅ Datenbank-Cleanup abgeschlossen - WAL-Dateien sollten verschwunden sein")
|
||
|
||
except Exception as db_error:
|
||
app_logger.error(f"❌ Fehler beim Datenbank-Cleanup: {str(db_error)}")
|
||
|
||
app_logger.info("✅ Shutdown abgeschlossen")
|
||
sys.exit(0)
|
||
except Exception as e:
|
||
app_logger.error(f"❌ Fehler beim Shutdown: {str(e)}")
|
||
sys.exit(1)
|
||
|
||
# Signal-Handler registrieren (Windows-kompatibel)
|
||
if os.name == 'nt': # Windows
|
||
signal.signal(signal.SIGINT, signal_handler)
|
||
signal.signal(signal.SIGTERM, signal_handler)
|
||
# Zusätzlich für Flask-Development-Server
|
||
signal.signal(signal.SIGBREAK, signal_handler)
|
||
else: # Unix/Linux
|
||
signal.signal(signal.SIGINT, signal_handler)
|
||
signal.signal(signal.SIGTERM, signal_handler)
|
||
signal.signal(signal.SIGHUP, signal_handler)
|
||
|
||
try:
|
||
# Datenbank initialisieren und Migrationen durchführen
|
||
setup_database_with_migrations()
|
||
|
||
# Template-Hilfsfunktionen registrieren
|
||
register_template_helpers(app)
|
||
|
||
# Drucker-Monitor Steckdosen-Initialisierung beim Start
|
||
try:
|
||
app_logger.info("🖨️ Starte automatische Steckdosen-Initialisierung...")
|
||
initialization_results = printer_monitor.initialize_all_outlets_on_startup()
|
||
|
||
if initialization_results:
|
||
success_count = sum(1 for success in initialization_results.values() if success)
|
||
total_count = len(initialization_results)
|
||
app_logger.info(f"✅ Steckdosen-Initialisierung: {success_count}/{total_count} Drucker erfolgreich")
|
||
|
||
if success_count < total_count:
|
||
app_logger.warning(f"⚠️ {total_count - success_count} Drucker konnten nicht initialisiert werden")
|
||
else:
|
||
app_logger.info("ℹ️ Keine Drucker zur Initialisierung gefunden")
|
||
|
||
except Exception as e:
|
||
app_logger.error(f"❌ Fehler bei automatischer Steckdosen-Initialisierung: {str(e)}")
|
||
|
||
# Queue-Manager für automatische Drucker-Überwachung starten
|
||
# Nur im Produktionsmodus starten (nicht im Debug-Modus)
|
||
if not debug_mode:
|
||
try:
|
||
queue_manager = start_queue_manager()
|
||
app_logger.info("✅ Printer Queue Manager erfolgreich gestartet")
|
||
|
||
# Verbesserte Shutdown-Handler registrieren
|
||
def cleanup_queue_manager():
|
||
try:
|
||
app_logger.info("🔄 Beende Queue Manager...")
|
||
stop_queue_manager()
|
||
except Exception as e:
|
||
app_logger.error(f"❌ Fehler beim Queue Manager Cleanup: {str(e)}")
|
||
|
||
atexit.register(cleanup_queue_manager)
|
||
|
||
# ===== DATENBANK-CLEANUP BEIM PROGRAMMENDE =====
|
||
def cleanup_database():
|
||
"""Führt Datenbank-Cleanup beim normalen Programmende aus."""
|
||
try:
|
||
app_logger.info("💾 Führe finales Datenbank-Cleanup durch...")
|
||
from models import create_optimized_engine
|
||
from sqlalchemy import text
|
||
|
||
engine = create_optimized_engine()
|
||
|
||
with engine.connect() as conn:
|
||
# WAL-Checkpoint für sauberes Beenden
|
||
result = conn.execute(text("PRAGMA wal_checkpoint(TRUNCATE)")).fetchone()
|
||
if result and result[1] > 0:
|
||
app_logger.info(f"Final WAL-Checkpoint: {result[1]} Seiten übertragen")
|
||
|
||
# Journal-Mode umschalten um .wal/.shm Dateien zu entfernen
|
||
conn.execute(text("PRAGMA journal_mode=DELETE"))
|
||
conn.commit()
|
||
|
||
# Connection-Pool ordnungsgemäß schließen
|
||
engine.dispose()
|
||
app_logger.info("✅ Finales Datenbank-Cleanup abgeschlossen")
|
||
|
||
except Exception as e:
|
||
app_logger.error(f"❌ Fehler beim finalen Datenbank-Cleanup: {str(e)}")
|
||
|
||
atexit.register(cleanup_database)
|
||
|
||
except Exception as e:
|
||
app_logger.error(f"❌ Fehler beim Starten des Queue-Managers: {str(e)}")
|
||
else:
|
||
app_logger.info("🔄 Debug-Modus: Queue Manager deaktiviert für Entwicklung")
|
||
|
||
# Scheduler starten (falls aktiviert)
|
||
if SCHEDULER_ENABLED:
|
||
try:
|
||
scheduler.start()
|
||
app_logger.info("Job-Scheduler gestartet")
|
||
except Exception as e:
|
||
app_logger.error(f"Fehler beim Starten des Schedulers: {str(e)}")
|
||
|
||
if debug_mode:
|
||
# Debug-Modus: HTTP auf Port 5000
|
||
app_logger.info("Starte Debug-Server auf 0.0.0.0:5000 (HTTP)")
|
||
|
||
# Windows-spezifische Flask-Konfiguration
|
||
run_kwargs = {
|
||
"host": "0.0.0.0",
|
||
"port": 5000,
|
||
"debug": True,
|
||
"threaded": True
|
||
}
|
||
|
||
if os.name == 'nt':
|
||
# Windows: Deaktiviere Auto-Reload um WERKZEUG_SERVER_FD Fehler zu vermeiden
|
||
run_kwargs["use_reloader"] = False
|
||
run_kwargs["passthrough_errors"] = False
|
||
app_logger.info("Windows-Debug-Modus: Auto-Reload deaktiviert")
|
||
|
||
app.run(**run_kwargs)
|
||
else:
|
||
# Produktions-Modus: HTTPS auf Port 443
|
||
ssl_context = get_ssl_context()
|
||
|
||
if ssl_context:
|
||
app_logger.info("Starte HTTPS-Server auf 0.0.0.0:443")
|
||
app.run(
|
||
host="0.0.0.0",
|
||
port=443,
|
||
debug=False,
|
||
ssl_context=ssl_context,
|
||
threaded=True
|
||
)
|
||
else:
|
||
app_logger.info("Starte HTTP-Server auf 0.0.0.0:80")
|
||
app.run(
|
||
host="0.0.0.0",
|
||
port=80,
|
||
debug=False,
|
||
threaded=True
|
||
)
|
||
except KeyboardInterrupt:
|
||
app_logger.info("🔄 Tastatur-Unterbrechung empfangen - beende Anwendung...")
|
||
signal_handler(signal.SIGINT, None)
|
||
except Exception as e:
|
||
app_logger.error(f"Fehler beim Starten der Anwendung: {str(e)}")
|
||
# Cleanup bei Fehler
|
||
try:
|
||
stop_queue_manager()
|
||
except:
|
||
pass
|
||
sys.exit(1)
|