diff --git a/backend/app.py b/backend/app.py
index 964bca5a..90a89a71 100644
--- a/backend/app.py
+++ b/backend/app.py
@@ -4828,34 +4828,32 @@ def session_status():
@app.route('/api/session/extend', methods=['POST'])
@login_required
def extend_session():
- """
- Verlängert die aktuelle Session um weitere Zeit.
- """
+ """Verlängert die aktuelle Session um die Standard-Lebensdauer"""
try:
- data = request.get_json() or {}
- extend_minutes = data.get('extend_minutes', 30)
+ # Session-Lebensdauer zurücksetzen
+ session.permanent = True
- # Begrenzen der Verlängerung (max 2 Stunden)
- extend_minutes = min(extend_minutes, 120)
+ # Aktivität für Rate Limiting aktualisieren
+ current_user.update_last_activity()
- now = datetime.now()
- session['last_activity'] = now.isoformat()
- session['session_extended'] = now.isoformat()
- session['extended_by_minutes'] = extend_minutes
+ # 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)
- auth_logger.info(f"🕒 Session verlängert für Benutzer {current_user.email} um {extend_minutes} Minuten")
+ app_logger.info(f"Session verlängert für User {current_user.id} (IP: {ip_address})")
return jsonify({
- "success": True,
- "message": f"Session um {extend_minutes} Minuten verlängert",
- "extended_until": (now + timedelta(minutes=extend_minutes)).isoformat(),
- "extended_minutes": extend_minutes
+ 'success': True,
+ 'message': 'Session erfolgreich verlängert',
+ 'expires_at': (datetime.now() + SESSION_LIFETIME).isoformat()
})
+
except Exception as e:
- auth_logger.error(f"Fehler beim Verlängern der Session: {str(e)}")
- return jsonify({"error": "Session-Verlängerung fehlgeschlagen"}), 500
-
-
+ 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 =====
@@ -5668,4 +5666,274 @@ if __name__ == "__main__":
stop_queue_manager()
except:
pass
- sys.exit(1)
\ No newline at end of file
+ sys.exit(1)
+
+# ===== 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 =====
\ No newline at end of file
diff --git a/backend/database/myp.db b/backend/database/myp.db
index ad323259..2a753946 100644
Binary files a/backend/database/myp.db and b/backend/database/myp.db differ
diff --git a/backend/database/myp.db-shm b/backend/database/myp.db-shm
new file mode 100644
index 00000000..ba938876
Binary files /dev/null and b/backend/database/myp.db-shm differ
diff --git a/backend/database/myp.db-wal b/backend/database/myp.db-wal
new file mode 100644
index 00000000..9e87a894
Binary files /dev/null and b/backend/database/myp.db-wal differ
diff --git a/backend/docs/AUTO_OPTIMIERUNG_MODAL_VERBESSERUNGEN.md b/backend/docs/AUTO_OPTIMIERUNG_MODAL_VERBESSERUNGEN.md
new file mode 100644
index 00000000..fd908412
--- /dev/null
+++ b/backend/docs/AUTO_OPTIMIERUNG_MODAL_VERBESSERUNGEN.md
@@ -0,0 +1,290 @@
+# Auto-Optimierung mit belohnendem Modal - Fehlerbehebung und Verbesserungen
+
+## 📋 Übersicht
+
+Dieses Dokument beschreibt die implementierten Verbesserungen für die Auto-Optimierung-Funktion der MYP-Plattform, einschließlich der Fehlerbehebung und der Hinzufügung eines belohnenden animierten Modals.
+
+## 🐛 Behobene Fehler
+
+### Problem 1: 404 Fehler bei Auto-Optimierung
+**Symptom:** `POST http://127.0.0.1:5000/api/optimization/auto-optimize 404 (NOT FOUND)`
+
+**Ursache:** Der API-Endpunkt `/api/optimization/auto-optimize` war nicht in der aktuellen `app.py` implementiert.
+
+**Lösung:**
+- Hinzufügung des fehlenden Endpunkts zur `app.py`
+- Implementierung der unterstützenden Optimierungs-Algorithmus-Funktionen
+- Vollständige Cascade-Analyse durchgeführt
+
+### Problem 2: JSON-Parsing-Fehler
+**Symptom:** `SyntaxError: Unexpected token '<', "
+
+
+
+ ${this.generateConfetti()}
+
+
+
+
+
+
+
+
+
+
+
${celebration}
+
✨
+
⭐
+
+
+
+
+ ${message}
+
+
+
+
+
+
+
+ ${optimizedCount}
+
+
+ Jobs optimiert
+
+
+
+
+ ${data.algorithm?.replace('_', ' ') || 'Standard'}
+
+
+ Algorithmus
+
+
+
+
+
+
+
+
+ Effizienz-Boost erreicht!
+
+
+
+
+
+
+
+
+
+ `;
+
+ // Modal zum DOM hinzufügen
+ document.body.appendChild(modal);
+
+ // Sound-Effekt (optional)
+ this.playSuccessSound();
+
+ // Auto-Close nach 10 Sekunden
+ setTimeout(() => {
+ if (modal && modal.parentNode) {
+ modal.style.opacity = '0';
+ modal.style.transform = 'scale(0.95)';
+ setTimeout(() => modal.remove(), 300);
+ }
+ }, 10000);
+ }
+
+ /**
+ * Konfetti-Animation generieren
+ */
+ generateConfetti() {
+ const colors = ['#FFD700', '#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7'];
+ let confetti = '';
+
+ for (let i = 0; i < 30; i++) {
+ const color = colors[Math.floor(Math.random() * colors.length)];
+ const delay = Math.random() * 3;
+ const duration = 3 + Math.random() * 2;
+ const left = Math.random() * 100;
+
+ confetti += `
+
+ `;
+ }
+
+ return confetti;
+ }
+
+ /**
+ * Ladeanimation für Optimierung anzeigen
+ */
+ showOptimizationLoading() {
+ const loader = document.createElement('div');
+ loader.id = 'optimization-loader';
+ loader.className = 'fixed inset-0 bg-black/50 backdrop-blur-sm z-40 flex items-center justify-center';
+ loader.innerHTML = `
+
+
+
+ Optimierung läuft...
+
+
+ Jobs werden intelligent verteilt
+
+
+ `;
+ document.body.appendChild(loader);
+ }
+
+ /**
+ * Ladeanimation ausblenden
+ */
+ hideOptimizationLoading() {
+ const loader = document.getElementById('optimization-loader');
+ if (loader) {
+ loader.style.opacity = '0';
+ setTimeout(() => loader.remove(), 200);
+ }
+ }
+
+ /**
+ * Erfolgs-Sound abspielen (optional)
+ */
+ playSuccessSound() {
+ try {
+ // Erstelle einen kurzen, angenehmen Ton
+ const audioContext = new (window.AudioContext || window.webkitAudioContext)();
+ const oscillator = audioContext.createOscillator();
+ const gainNode = audioContext.createGain();
+
+ oscillator.connect(gainNode);
+ gainNode.connect(audioContext.destination);
+
+ oscillator.frequency.setValueAtTime(523.25, audioContext.currentTime); // C5
+ oscillator.frequency.setValueAtTime(659.25, audioContext.currentTime + 0.1); // E5
+ oscillator.frequency.setValueAtTime(783.99, audioContext.currentTime + 0.2); // G5
+
+ gainNode.gain.setValueAtTime(0.1, audioContext.currentTime);
+ gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.5);
+
+ oscillator.start(audioContext.currentTime);
+ oscillator.stop(audioContext.currentTime + 0.5);
+ } catch (e) {
+ // Sound nicht verfügbar, ignorieren
+ console.log('Audio-API nicht verfügbar');
+ }
+ }
+
/**
* Batch-Operationen durchführen
*/
diff --git a/backend/templates/base.html b/backend/templates/base.html
index 7e076878..6a2b107a 100644
--- a/backend/templates/base.html
+++ b/backend/templates/base.html
@@ -29,6 +29,7 @@
+
diff --git a/backend/utils/form_validation.py b/backend/utils/form_validation.py
new file mode 100644
index 00000000..c0157c32
--- /dev/null
+++ b/backend/utils/form_validation.py
@@ -0,0 +1,663 @@
+"""
+Erweiterte Formular-Validierung für das MYP-System
+==================================================
+
+Dieses Modul stellt umfassende Client- und serverseitige Validierung
+mit benutzerfreundlichem UI-Feedback bereit.
+
+Funktionen:
+- Multi-Level-Validierung (Client/Server)
+- Echtzeitvalidierung mit JavaScript
+- Barrierefreie Fehlermeldungen
+- Custom Validators für spezielle Anforderungen
+- Automatische Sanitization von Eingaben
+"""
+
+import re
+import html
+import json
+import logging
+from typing import Dict, List, Any, Optional, Callable, Union
+from datetime import datetime, timedelta
+from flask import request, jsonify, session
+from functools import wraps
+from werkzeug.datastructures import FileStorage
+
+from utils.logging_config import get_logger
+from config.settings import ALLOWED_EXTENSIONS, MAX_FILE_SIZE
+
+logger = get_logger("validation")
+
+class ValidationError(Exception):
+ """Custom Exception für Validierungsfehler"""
+ def __init__(self, message: str, field: str = None, code: str = None):
+ self.message = message
+ self.field = field
+ self.code = code
+ super().__init__(self.message)
+
+class ValidationResult:
+ """Ergebnis einer Validierung"""
+ def __init__(self):
+ self.is_valid = True
+ self.errors: Dict[str, List[str]] = {}
+ self.warnings: Dict[str, List[str]] = {}
+ self.cleaned_data: Dict[str, Any] = {}
+
+ def add_error(self, field: str, message: str):
+ """Fügt einen Validierungsfehler hinzu"""
+ if field not in self.errors:
+ self.errors[field] = []
+ self.errors[field].append(message)
+ self.is_valid = False
+
+ def add_warning(self, field: str, message: str):
+ """Fügt eine Warnung hinzu"""
+ if field not in self.warnings:
+ self.warnings[field] = []
+ self.warnings[field].append(message)
+
+ def to_dict(self) -> Dict[str, Any]:
+ """Konvertiert das Ergebnis zu einem Dictionary"""
+ return {
+ "is_valid": self.is_valid,
+ "errors": self.errors,
+ "warnings": self.warnings,
+ "cleaned_data": self.cleaned_data
+ }
+
+class BaseValidator:
+ """Basis-Klasse für alle Validatoren"""
+
+ def __init__(self, required: bool = False, allow_empty: bool = True):
+ self.required = required
+ self.allow_empty = allow_empty
+
+ def validate(self, value: Any, field_name: str = None) -> ValidationResult:
+ """Führt die Validierung durch"""
+ result = ValidationResult()
+
+ # Prüfung auf erforderliche Felder
+ if self.required and (value is None or value == ""):
+ result.add_error(field_name or "field", "Dieses Feld ist erforderlich.")
+ return result
+
+ # Wenn Wert leer und erlaubt, keine weitere Validierung
+ if not value and self.allow_empty:
+ result.cleaned_data[field_name or "field"] = value
+ return result
+
+ return self._validate_value(value, field_name, result)
+
+ def _validate_value(self, value: Any, field_name: str, result: ValidationResult) -> ValidationResult:
+ """Überschreibbar für spezifische Validierungslogik"""
+ result.cleaned_data[field_name or "field"] = value
+ return result
+
+class StringValidator(BaseValidator):
+ """Validator für String-Werte"""
+
+ def __init__(self, min_length: int = None, max_length: int = None,
+ pattern: str = None, trim: bool = True, **kwargs):
+ super().__init__(**kwargs)
+ self.min_length = min_length
+ self.max_length = max_length
+ self.pattern = re.compile(pattern) if pattern else None
+ self.trim = trim
+
+ def _validate_value(self, value: Any, field_name: str, result: ValidationResult) -> ValidationResult:
+ # String konvertieren und trimmen
+ str_value = str(value)
+ if self.trim:
+ str_value = str_value.strip()
+
+ # Längenprüfung
+ if self.min_length is not None and len(str_value) < self.min_length:
+ result.add_error(field_name, f"Mindestlänge: {self.min_length} Zeichen")
+
+ if self.max_length is not None and len(str_value) > self.max_length:
+ result.add_error(field_name, f"Maximallänge: {self.max_length} Zeichen")
+
+ # Pattern-Prüfung
+ if self.pattern and not self.pattern.match(str_value):
+ result.add_error(field_name, "Format ist ungültig")
+
+ # HTML-Sanitization
+ cleaned_value = html.escape(str_value)
+ result.cleaned_data[field_name] = cleaned_value
+
+ return result
+
+class EmailValidator(StringValidator):
+ """Validator für E-Mail-Adressen"""
+
+ EMAIL_PATTERN = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
+
+ def __init__(self, **kwargs):
+ super().__init__(pattern=self.EMAIL_PATTERN, **kwargs)
+
+ def _validate_value(self, value: Any, field_name: str, result: ValidationResult) -> ValidationResult:
+ result = super()._validate_value(value, field_name, result)
+
+ if result.is_valid:
+ # Normalisierung der E-Mail
+ email = str(value).lower().strip()
+ result.cleaned_data[field_name] = email
+
+ return result
+
+class IntegerValidator(BaseValidator):
+ """Validator für Integer-Werte"""
+
+ def __init__(self, min_value: int = None, max_value: int = None, **kwargs):
+ super().__init__(**kwargs)
+ self.min_value = min_value
+ self.max_value = max_value
+
+ def _validate_value(self, value: Any, field_name: str, result: ValidationResult) -> ValidationResult:
+ try:
+ int_value = int(value)
+ except (ValueError, TypeError):
+ result.add_error(field_name, "Muss eine ganze Zahl sein")
+ return result
+
+ if self.min_value is not None and int_value < self.min_value:
+ result.add_error(field_name, f"Mindestwert: {self.min_value}")
+
+ if self.max_value is not None and int_value > self.max_value:
+ result.add_error(field_name, f"Maximalwert: {self.max_value}")
+
+ result.cleaned_data[field_name] = int_value
+ return result
+
+class FloatValidator(BaseValidator):
+ """Validator für Float-Werte"""
+
+ def __init__(self, min_value: float = None, max_value: float = None,
+ decimal_places: int = None, **kwargs):
+ super().__init__(**kwargs)
+ self.min_value = min_value
+ self.max_value = max_value
+ self.decimal_places = decimal_places
+
+ def _validate_value(self, value: Any, field_name: str, result: ValidationResult) -> ValidationResult:
+ try:
+ float_value = float(value)
+ except (ValueError, TypeError):
+ result.add_error(field_name, "Muss eine Dezimalzahl sein")
+ return result
+
+ if self.min_value is not None and float_value < self.min_value:
+ result.add_error(field_name, f"Mindestwert: {self.min_value}")
+
+ if self.max_value is not None and float_value > self.max_value:
+ result.add_error(field_name, f"Maximalwert: {self.max_value}")
+
+ # Rundung auf bestimmte Dezimalstellen
+ if self.decimal_places is not None:
+ float_value = round(float_value, self.decimal_places)
+
+ result.cleaned_data[field_name] = float_value
+ return result
+
+class DateTimeValidator(BaseValidator):
+ """Validator für DateTime-Werte"""
+
+ def __init__(self, format_string: str = "%Y-%m-%d %H:%M",
+ min_date: datetime = None, max_date: datetime = None, **kwargs):
+ super().__init__(**kwargs)
+ self.format_string = format_string
+ self.min_date = min_date
+ self.max_date = max_date
+
+ def _validate_value(self, value: Any, field_name: str, result: ValidationResult) -> ValidationResult:
+ if isinstance(value, datetime):
+ dt_value = value
+ else:
+ try:
+ dt_value = datetime.strptime(str(value), self.format_string)
+ except ValueError:
+ result.add_error(field_name, f"Ungültiges Datumsformat. Erwartet: {self.format_string}")
+ return result
+
+ if self.min_date and dt_value < self.min_date:
+ result.add_error(field_name, f"Datum muss nach {self.min_date.strftime('%d.%m.%Y')} liegen")
+
+ if self.max_date and dt_value > self.max_date:
+ result.add_error(field_name, f"Datum muss vor {self.max_date.strftime('%d.%m.%Y')} liegen")
+
+ result.cleaned_data[field_name] = dt_value
+ return result
+
+class FileValidator(BaseValidator):
+ """Validator für Datei-Uploads"""
+
+ def __init__(self, allowed_extensions: List[str] = None,
+ max_size_mb: int = None, min_size_kb: int = None, **kwargs):
+ super().__init__(**kwargs)
+ self.allowed_extensions = allowed_extensions or ALLOWED_EXTENSIONS
+ self.max_size_mb = max_size_mb or (MAX_FILE_SIZE / (1024 * 1024))
+ self.min_size_kb = min_size_kb
+
+ def _validate_value(self, value: Any, field_name: str, result: ValidationResult) -> ValidationResult:
+ if not isinstance(value, FileStorage):
+ result.add_error(field_name, "Muss eine gültige Datei sein")
+ return result
+
+ # Dateiname prüfen
+ if not value.filename:
+ result.add_error(field_name, "Dateiname ist erforderlich")
+ return result
+
+ # Dateierweiterung prüfen
+ extension = value.filename.rsplit('.', 1)[-1].lower() if '.' in value.filename else ''
+ if extension not in self.allowed_extensions:
+ result.add_error(field_name,
+ f"Nur folgende Dateiformate sind erlaubt: {', '.join(self.allowed_extensions)}")
+
+ # Dateigröße prüfen
+ value.seek(0, 2) # Zum Ende der Datei
+ file_size = value.tell()
+ value.seek(0) # Zurück zum Anfang
+
+ if self.max_size_mb and file_size > (self.max_size_mb * 1024 * 1024):
+ result.add_error(field_name, f"Datei zu groß. Maximum: {self.max_size_mb} MB")
+
+ if self.min_size_kb and file_size < (self.min_size_kb * 1024):
+ result.add_error(field_name, f"Datei zu klein. Minimum: {self.min_size_kb} KB")
+
+ result.cleaned_data[field_name] = value
+ return result
+
+class FormValidator:
+ """Haupt-Formular-Validator"""
+
+ def __init__(self):
+ self.fields: Dict[str, BaseValidator] = {}
+ self.custom_validators: List[Callable] = []
+ self.rate_limit_key = None
+ self.csrf_check = True
+
+ def add_field(self, name: str, validator: BaseValidator):
+ """Fügt ein Feld mit Validator hinzu"""
+ self.fields[name] = validator
+ return self
+
+ def add_custom_validator(self, validator_func: Callable):
+ """Fügt einen benutzerdefinierten Validator hinzu"""
+ self.custom_validators.append(validator_func)
+ return self
+
+ def set_rate_limit(self, key: str):
+ """Setzt einen Rate-Limiting-Schlüssel"""
+ self.rate_limit_key = key
+ return self
+
+ def disable_csrf(self):
+ """Deaktiviert CSRF-Prüfung für dieses Formular"""
+ self.csrf_check = False
+ return self
+
+ def validate(self, data: Dict[str, Any]) -> ValidationResult:
+ """Validiert die gesamten Formulardaten"""
+ result = ValidationResult()
+
+ # Einzelfeldvalidierung
+ for field_name, validator in self.fields.items():
+ field_value = data.get(field_name)
+ field_result = validator.validate(field_value, field_name)
+
+ if not field_result.is_valid:
+ result.errors.update(field_result.errors)
+ result.is_valid = False
+
+ result.warnings.update(field_result.warnings)
+ result.cleaned_data.update(field_result.cleaned_data)
+
+ # Benutzerdefinierte Validierung
+ if result.is_valid:
+ for custom_validator in self.custom_validators:
+ try:
+ custom_result = custom_validator(result.cleaned_data)
+ if isinstance(custom_result, ValidationResult):
+ if not custom_result.is_valid:
+ result.errors.update(custom_result.errors)
+ result.is_valid = False
+ result.warnings.update(custom_result.warnings)
+ except Exception as e:
+ logger.error(f"Fehler bei benutzerdefinierter Validierung: {str(e)}")
+ result.add_error("form", "Unerwarteter Validierungsfehler")
+
+ return result
+
+# Vordefinierte Formular-Validatoren
+def get_user_registration_validator() -> FormValidator:
+ """Validator für Benutzerregistrierung"""
+ return FormValidator() \
+ .add_field("username", StringValidator(min_length=3, max_length=50, required=True)) \
+ .add_field("email", EmailValidator(required=True)) \
+ .add_field("password", StringValidator(min_length=8, required=True)) \
+ .add_field("password_confirm", StringValidator(min_length=8, required=True)) \
+ .add_field("name", StringValidator(min_length=2, max_length=100, required=True)) \
+ .add_custom_validator(lambda data: _validate_password_match(data))
+
+def get_job_creation_validator() -> FormValidator:
+ """Validator für Job-Erstellung"""
+ return FormValidator() \
+ .add_field("name", StringValidator(min_length=1, max_length=200, required=True)) \
+ .add_field("description", StringValidator(max_length=500)) \
+ .add_field("printer_id", IntegerValidator(min_value=1, required=True)) \
+ .add_field("duration_minutes", IntegerValidator(min_value=1, max_value=1440, required=True)) \
+ .add_field("start_at", DateTimeValidator(min_date=datetime.now())) \
+ .add_field("file", FileValidator(required=True))
+
+def get_printer_creation_validator() -> FormValidator:
+ """Validator für Drucker-Erstellung"""
+ return FormValidator() \
+ .add_field("name", StringValidator(min_length=1, max_length=100, required=True)) \
+ .add_field("model", StringValidator(max_length=100)) \
+ .add_field("location", StringValidator(max_length=100)) \
+ .add_field("ip_address", StringValidator(pattern=r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$')) \
+ .add_field("mac_address", StringValidator(pattern=r'^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$', required=True)) \
+ .add_field("plug_ip", StringValidator(pattern=r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$', required=True)) \
+ .add_field("plug_username", StringValidator(min_length=1, required=True)) \
+ .add_field("plug_password", StringValidator(min_length=1, required=True))
+
+def get_guest_request_validator() -> FormValidator:
+ """Validator für Gastanfragen"""
+ return FormValidator() \
+ .add_field("name", StringValidator(min_length=2, max_length=100, required=True)) \
+ .add_field("email", EmailValidator()) \
+ .add_field("reason", StringValidator(min_length=10, max_length=500, required=True)) \
+ .add_field("duration_minutes", IntegerValidator(min_value=5, max_value=480, required=True)) \
+ .add_field("copies", IntegerValidator(min_value=1, max_value=10)) \
+ .add_field("file", FileValidator(required=True)) \
+ .set_rate_limit("guest_request")
+
+def _validate_password_match(data: Dict[str, Any]) -> ValidationResult:
+ """Validiert, ob Passwörter übereinstimmen"""
+ result = ValidationResult()
+
+ password = data.get("password")
+ password_confirm = data.get("password_confirm")
+
+ if password != password_confirm:
+ result.add_error("password_confirm", "Passwörter stimmen nicht überein")
+
+ return result
+
+# Decorator für automatische Formularvalidierung
+def validate_form(validator_func: Callable[[], FormValidator]):
+ """Decorator für automatische Formularvalidierung"""
+ def decorator(f):
+ @wraps(f)
+ def decorated_function(*args, **kwargs):
+ try:
+ # Validator erstellen
+ validator = validator_func()
+
+ # Daten aus Request extrahieren
+ if request.is_json:
+ data = request.get_json() or {}
+ else:
+ data = dict(request.form)
+ # Dateien hinzufügen
+ for key, file in request.files.items():
+ data[key] = file
+
+ # Validierung durchführen
+ validation_result = validator.validate(data)
+
+ # Bei Fehlern JSON-Response zurückgeben
+ if not validation_result.is_valid:
+ logger.warning(f"Validierungsfehler für {request.endpoint}: {validation_result.errors}")
+ return jsonify({
+ "success": False,
+ "errors": validation_result.errors,
+ "warnings": validation_result.warnings
+ }), 400
+
+ # Gereinigte Daten an die Request anhängen
+ request.validated_data = validation_result.cleaned_data
+ request.validation_warnings = validation_result.warnings
+
+ return f(*args, **kwargs)
+
+ except Exception as e:
+ logger.error(f"Fehler bei Formularvalidierung: {str(e)}")
+ return jsonify({
+ "success": False,
+ "errors": {"form": ["Unerwarteter Validierungsfehler"]}
+ }), 500
+
+ return decorated_function
+ return decorator
+
+# JavaScript für Client-seitige Validierung
+def get_client_validation_js() -> str:
+ """Generiert JavaScript für Client-seitige Validierung"""
+ return """
+ class FormValidator {
+ constructor(formId, validationRules = {}) {
+ this.form = document.getElementById(formId);
+ this.rules = validationRules;
+ this.errors = {};
+ this.setupEventListeners();
+ }
+
+ setupEventListeners() {
+ if (!this.form) return;
+
+ // Echtzeit-Validierung bei Eingabe
+ this.form.addEventListener('input', (e) => {
+ this.validateField(e.target);
+ });
+
+ // Formular-Submission
+ this.form.addEventListener('submit', (e) => {
+ if (!this.validateForm()) {
+ e.preventDefault();
+ }
+ });
+ }
+
+ validateField(field) {
+ const fieldName = field.name;
+ const value = field.value;
+ const rule = this.rules[fieldName];
+
+ if (!rule) return true;
+
+ this.clearFieldError(field);
+
+ // Required-Prüfung
+ if (rule.required && (!value || value.trim() === '')) {
+ this.addFieldError(field, 'Dieses Feld ist erforderlich.');
+ return false;
+ }
+
+ // Längenprüfung
+ if (rule.minLength && value.length < rule.minLength) {
+ this.addFieldError(field, `Mindestlänge: ${rule.minLength} Zeichen`);
+ return false;
+ }
+
+ if (rule.maxLength && value.length > rule.maxLength) {
+ this.addFieldError(field, `Maximallänge: ${rule.maxLength} Zeichen`);
+ return false;
+ }
+
+ // Pattern-Prüfung
+ if (rule.pattern && !new RegExp(rule.pattern).test(value)) {
+ this.addFieldError(field, rule.patternMessage || 'Format ist ungültig');
+ return false;
+ }
+
+ // Email-Prüfung
+ if (rule.type === 'email' && value) {
+ const emailPattern = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
+ if (!emailPattern.test(value)) {
+ this.addFieldError(field, 'Bitte geben Sie eine gültige E-Mail-Adresse ein');
+ return false;
+ }
+ }
+
+ // Custom Validierung
+ if (rule.customValidator) {
+ const customResult = rule.customValidator(value, field);
+ if (customResult !== true) {
+ this.addFieldError(field, customResult);
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ validateForm() {
+ let isValid = true;
+ this.errors = {};
+
+ // Alle Felder validieren
+ const fields = this.form.querySelectorAll('input, textarea, select');
+ fields.forEach(field => {
+ if (!this.validateField(field)) {
+ isValid = false;
+ }
+ });
+
+ // Custom Form-Validierung
+ if (this.rules._formValidator) {
+ const formData = new FormData(this.form);
+ const customResult = this.rules._formValidator(formData, this.form);
+ if (customResult !== true) {
+ this.addFormError(customResult);
+ isValid = false;
+ }
+ }
+
+ return isValid;
+ }
+
+ addFieldError(field, message) {
+ const fieldName = field.name;
+
+ // Error-Container finden oder erstellen
+ let errorContainer = field.parentNode.querySelector('.field-error');
+ if (!errorContainer) {
+ errorContainer = document.createElement('div');
+ errorContainer.className = 'field-error text-red-600 text-sm mt-1';
+ errorContainer.setAttribute('role', 'alert');
+ errorContainer.setAttribute('aria-live', 'polite');
+ field.parentNode.appendChild(errorContainer);
+ }
+
+ errorContainer.textContent = message;
+ field.classList.add('border-red-500');
+ field.setAttribute('aria-invalid', 'true');
+
+ // Für Screen Reader
+ if (!field.getAttribute('aria-describedby')) {
+ const errorId = `error-${fieldName}-${Date.now()}`;
+ errorContainer.id = errorId;
+ field.setAttribute('aria-describedby', errorId);
+ }
+
+ this.errors[fieldName] = message;
+ }
+
+ clearFieldError(field) {
+ const errorContainer = field.parentNode.querySelector('.field-error');
+ if (errorContainer) {
+ errorContainer.remove();
+ }
+
+ field.classList.remove('border-red-500');
+ field.removeAttribute('aria-invalid');
+ field.removeAttribute('aria-describedby');
+
+ delete this.errors[field.name];
+ }
+
+ addFormError(message) {
+ let formErrorContainer = this.form.querySelector('.form-error');
+ if (!formErrorContainer) {
+ formErrorContainer = document.createElement('div');
+ formErrorContainer.className = 'form-error bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4';
+ formErrorContainer.setAttribute('role', 'alert');
+ this.form.insertBefore(formErrorContainer, this.form.firstChild);
+ }
+
+ formErrorContainer.textContent = message;
+ }
+
+ clearFormErrors() {
+ const formErrorContainer = this.form.querySelector('.form-error');
+ if (formErrorContainer) {
+ formErrorContainer.remove();
+ }
+ }
+
+ showServerErrors(errors) {
+ // Server-Fehler anzeigen
+ for (const [fieldName, messages] of Object.entries(errors)) {
+ const field = this.form.querySelector(`[name="${fieldName}"]`);
+ if (field && messages.length > 0) {
+ this.addFieldError(field, messages[0]);
+ }
+ }
+ }
+ }
+
+ // Utility-Funktionen
+ window.FormValidationUtils = {
+ // Passwort-Stärke prüfen
+ validatePasswordStrength: (password) => {
+ if (password.length < 8) return 'Passwort muss mindestens 8 Zeichen lang sein';
+ if (!/[A-Z]/.test(password)) return 'Passwort muss mindestens einen Großbuchstaben enthalten';
+ if (!/[a-z]/.test(password)) return 'Passwort muss mindestens einen Kleinbuchstaben enthalten';
+ if (!/[0-9]/.test(password)) return 'Passwort muss mindestens eine Zahl enthalten';
+ return true;
+ },
+
+ // Passwort-Bestätigung prüfen
+ validatePasswordConfirm: (password, confirm) => {
+ return password === confirm ? true : 'Passwörter stimmen nicht überein';
+ },
+
+ // Datei-Validierung
+ validateFile: (file, allowedTypes = [], maxSizeMB = 10) => {
+ if (!file) return 'Bitte wählen Sie eine Datei aus';
+
+ const fileType = file.name.split('.').pop().toLowerCase();
+ if (allowedTypes.length > 0 && !allowedTypes.includes(fileType)) {
+ return `Nur folgende Dateiformate sind erlaubt: ${allowedTypes.join(', ')}`;
+ }
+
+ if (file.size > maxSizeMB * 1024 * 1024) {
+ return `Datei ist zu groß. Maximum: ${maxSizeMB} MB`;
+ }
+
+ return true;
+ }
+ };
+ """
+
+def render_validation_errors(errors: Dict[str, List[str]]) -> str:
+ """Rendert Validierungsfehler als HTML"""
+ if not errors:
+ return ""
+
+ html_parts = ['']
+
+ for field, messages in errors.items():
+ for message in messages:
+ html_parts.append(
+ f'
'
+ f'{field}: {html.escape(message)}'
+ f'
'
+ )
+
+ html_parts.append('
')
+
+ return '\n'.join(html_parts)
\ No newline at end of file