📚 Improved error handling documentation and codebase organization in the frontend application. 🖥️🔍
This commit is contained in:
parent
d1a6281577
commit
0d966712a7
@ -1,3 +1,98 @@
|
|||||||
|
## ✅ 30.05.2025 21:15 - SQLite WAL-Dateien beim Programmende bereinigen
|
||||||
|
|
||||||
|
### Problem
|
||||||
|
|
||||||
|
Nach dem Beenden des Programms blieben zwei SQLite-Datenbankdateien zurück:
|
||||||
|
- `myp.db-shm` (Shared Memory)
|
||||||
|
- `myp.db-wal` (Write-Ahead Log)
|
||||||
|
|
||||||
|
Diese Dateien sind Teil des SQLite WAL-Modus (Write-Ahead Logging) und sollten normalerweise beim ordnungsgemäßen Herunterfahren automatisch aufgeräumt werden.
|
||||||
|
|
||||||
|
### Root-Cause-Analyse
|
||||||
|
|
||||||
|
**SQLite WAL-Mode Problem:**
|
||||||
|
- Die Datenbank ist im WAL-Mode konfiguriert (`PRAGMA journal_mode=WAL`)
|
||||||
|
- WAL-Mode erstellt `.wal` und `.shm` Dateien für bessere Performance
|
||||||
|
- Diese Dateien bleiben bestehen wenn keine ordnungsgemäße WAL-Checkpoint und Journal-Mode-Umschaltung beim Shutdown erfolgt
|
||||||
|
- Signal-Handler waren vorhanden, aber keine atexit-Handler für normales Programmende
|
||||||
|
|
||||||
|
### Implementierte Lösung
|
||||||
|
|
||||||
|
**1. Erweiterte Signal-Handler mit Datenbank-Cleanup:**
|
||||||
|
```python
|
||||||
|
def signal_handler(sig, frame):
|
||||||
|
# Queue Manager und Scheduler stoppen
|
||||||
|
|
||||||
|
# ===== DATENBANKVERBINDUNGEN ORDNUNGSGEMÄSS SCHLIESSEN =====
|
||||||
|
app_logger.info("💾 Führe Datenbank-Cleanup durch...")
|
||||||
|
try:
|
||||||
|
engine = create_optimized_engine()
|
||||||
|
|
||||||
|
with engine.connect() as conn:
|
||||||
|
# Vollständiger WAL-Checkpoint (TRUNCATE-Modus)
|
||||||
|
result = conn.execute(text("PRAGMA wal_checkpoint(TRUNCATE)")).fetchone()
|
||||||
|
|
||||||
|
# Journal-Mode zu DELETE wechseln (entfernt .wal/.shm Dateien)
|
||||||
|
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()
|
||||||
|
|
||||||
|
except Exception as db_error:
|
||||||
|
app_logger.error(f"❌ Fehler beim Datenbank-Cleanup: {str(db_error)}")
|
||||||
|
```
|
||||||
|
|
||||||
|
**2. Zusätzlicher atexit-Handler für normales Programmende:**
|
||||||
|
```python
|
||||||
|
def cleanup_database():
|
||||||
|
"""Führt Datenbank-Cleanup beim normalen Programmende aus."""
|
||||||
|
try:
|
||||||
|
engine = create_optimized_engine()
|
||||||
|
|
||||||
|
with engine.connect() as conn:
|
||||||
|
# WAL-Checkpoint für sauberes Beenden
|
||||||
|
result = conn.execute(text("PRAGMA wal_checkpoint(TRUNCATE)")).fetchone()
|
||||||
|
|
||||||
|
# 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()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
app_logger.error(f"❌ Fehler beim finalen Datenbank-Cleanup: {str(e)}")
|
||||||
|
|
||||||
|
atexit.register(cleanup_database)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Funktionalität nach der Behebung
|
||||||
|
|
||||||
|
- ✅ **WAL-Checkpoint**: Vollständiger `PRAGMA wal_checkpoint(TRUNCATE)` überträgt alle WAL-Daten zurück in die Hauptdatenbank
|
||||||
|
- ✅ **Journal-Mode-Umschaltung**: `PRAGMA journal_mode=DELETE` entfernt die `.wal` und `.shm` Dateien
|
||||||
|
- ✅ **Engine-Cleanup**: `engine.dispose()` schließt alle Connection-Pools ordnungsgemäß
|
||||||
|
- ✅ **Signal-Handler**: Funktioniert bei Ctrl+C, SIGTERM, SIGBREAK (Windows)
|
||||||
|
- ✅ **atexit-Handler**: Funktioniert bei normalem Programmende
|
||||||
|
- ✅ **Fehlerbehandlung**: Detailliertes Logging für Debugging
|
||||||
|
- ✅ **Cross-Platform**: Windows und Unix/Linux kompatibel
|
||||||
|
|
||||||
|
### Ergebnis
|
||||||
|
|
||||||
|
✅ **Die `.shm` und `.wal` Dateien verschwinden jetzt ordnungsgemäß beim Beenden des Programms**
|
||||||
|
✅ **Robuste Datenbank-Cleanup-Mechanismen für alle Shutdown-Szenarien**
|
||||||
|
✅ **Bessere Performance durch behaltenen WAL-Mode während der Laufzeit**
|
||||||
|
✅ **Sauberer Dateisystem-Zustand nach Programmende**
|
||||||
|
|
||||||
|
**Status:** Problem vollständig behoben - SQLite WAL-Dateien werden automatisch aufgeräumt
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## ✅ 30.05.2025 19:10 - Schnellaufträge-Funktionalität komplett repariert
|
## ✅ 30.05.2025 19:10 - Schnellaufträge-Funktionalität komplett repariert
|
||||||
|
|
||||||
### Problem
|
### Problem
|
||||||
@ -1200,4 +1295,110 @@ Die Standard-Browser-Scrollbalken waren zu aufdringlich und störten das elegant
|
|||||||
|
|
||||||
**Status:** Design-Enhancement erfolgreich implementiert
|
**Status:** Design-Enhancement erfolgreich implementiert
|
||||||
|
|
||||||
---
|
## ✅ 30.05.2025 20:10 - Ultra-dezente Scrollbalken über base.html implementiert
|
||||||
|
|
||||||
|
### Problem
|
||||||
|
|
||||||
|
Die Scrollbalken in jobs.html waren immer noch zu auffällig. Benutzer wünschte ultra-dezente Scrollbalken die fast unsichtbar sind und nur bei Hover erscheinen.
|
||||||
|
|
||||||
|
### Lösung - Global über base.html Template
|
||||||
|
|
||||||
|
**Ultra-dezente Scrollbalken CSS direkt in `templates/base.html`:**
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* ===== ULTRA-DEZENTE SCROLLBALKEN ===== */
|
||||||
|
|
||||||
|
/* Webkit-Browser (Chrome, Safari, Edge) */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: transparent;
|
||||||
|
border-radius: 3px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Nur bei Hover über scrollbaren Container sichtbar */
|
||||||
|
*:hover::-webkit-scrollbar-thumb {
|
||||||
|
background: rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
*:hover::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark Mode - noch dezenter */
|
||||||
|
.dark *:hover::-webkit-scrollbar-thumb {
|
||||||
|
background: rgba(255, 255, 255, 0.03);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark *:hover::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Firefox - ultra-thin */
|
||||||
|
* {
|
||||||
|
scrollbar-width: none; /* Komplett versteckt in Firefox */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Nur bei Hover sichtbar in Firefox */
|
||||||
|
*:hover {
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: rgba(0, 0, 0, 0.05) transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark *:hover {
|
||||||
|
scrollbar-color: rgba(255, 255, 255, 0.03) transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Spezielle Container die scrollbar brauchen */
|
||||||
|
.modal-content::-webkit-scrollbar,
|
||||||
|
.dropdown-menu::-webkit-scrollbar {
|
||||||
|
width: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content::-webkit-scrollbar-thumb,
|
||||||
|
.dropdown-menu::-webkit-scrollbar-thumb {
|
||||||
|
background: rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .modal-content::-webkit-scrollbar-thumb,
|
||||||
|
.dark .dropdown-menu::-webkit-scrollbar-thumb {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Features der neuen Implementation
|
||||||
|
|
||||||
|
1. **Ultra-dezent**: Nur 6px breit, komplett transparent bis Hover
|
||||||
|
2. **Hover-only Sichtbarkeit**: Scrollbalken erscheinen nur bei Hover über Container
|
||||||
|
3. **Noch schwächere Opacity**: rgba(0,0,0,0.05) vs vorher 0.1
|
||||||
|
4. **Dark Mode ultra-dezent**: rgba(255,255,255,0.03) - kaum sichtbar
|
||||||
|
5. **Firefox hidden by default**: `scrollbar-width: none` macht sie komplett unsichtbar
|
||||||
|
6. **Global Implementation**: Über base.html auf allen Seiten verfügbar
|
||||||
|
7. **Modals extra-schmal**: Nur 4px für beste UX
|
||||||
|
8. **Entfernt aus jobs.html**: Keine Dopplung mehr
|
||||||
|
|
||||||
|
### Vorteile
|
||||||
|
|
||||||
|
- ✅ **Fast unsichtbar**: Nur bei Hover schwach sichtbar
|
||||||
|
- ✅ **Nicht störend**: Design bleibt komplett clean
|
||||||
|
- ✅ **Global verfügbar**: Alle Seiten haben konsistente Scrollbalken
|
||||||
|
- ✅ **Performance**: Keine Dopplung von CSS-Regeln
|
||||||
|
- ✅ **Dark Mode optimiert**: Noch dezenter in dunklem Theme
|
||||||
|
- ✅ **Firefox clean**: Scrollbalken komplett versteckt bis Hover
|
||||||
|
|
||||||
|
### Ergebnis
|
||||||
|
|
||||||
|
✅ **Ultra-dezente Scrollbalken die praktisch unsichtbar sind**
|
||||||
|
✅ **Global über base.html implementiert - kein Duplicate CSS**
|
||||||
|
✅ **Erscheinen nur bei tatsächlichem Hover über scrollbare Container**
|
||||||
|
✅ **Design bleibt völlig clean und ungestört**
|
||||||
|
|
||||||
|
**Status:** Ultra-dezente Scrollbalken final implementiert
|
@ -343,11 +343,11 @@ def login():
|
|||||||
password = request.form.get("password")
|
password = request.form.get("password")
|
||||||
remember_me = request.form.get("remember_me") == "on"
|
remember_me = request.form.get("remember_me") == "on"
|
||||||
|
|
||||||
# Zusätzlicher Fallback für verschiedene Feldnamen
|
# Zusätzlicher Fallback für verschiedene Feldnamen
|
||||||
if not username:
|
if not username:
|
||||||
username = request.form.get("username") or request.values.get("email") or request.values.get("username")
|
username = request.form.get("username") or request.values.get("email") or request.values.get("username")
|
||||||
if not password:
|
if not password:
|
||||||
password = request.form.get("password") or request.values.get("password")
|
password = request.form.get("password") or request.values.get("password")
|
||||||
|
|
||||||
except Exception as extract_error:
|
except Exception as extract_error:
|
||||||
auth_logger.error(f"Fehler beim Extrahieren der Login-Daten: {str(extract_error)}")
|
auth_logger.error(f"Fehler beim Extrahieren der Login-Daten: {str(extract_error)}")
|
||||||
@ -2299,16 +2299,16 @@ def finish_job(job_id):
|
|||||||
def cancel_job(job_id):
|
def cancel_job(job_id):
|
||||||
"""Bricht einen Job ab."""
|
"""Bricht einen Job ab."""
|
||||||
try:
|
try:
|
||||||
db_session = get_db_session()
|
db_session = get_db_session()
|
||||||
job = db_session.query(Job).get(job_id)
|
job = db_session.query(Job).get(job_id)
|
||||||
|
|
||||||
if not job:
|
if not job:
|
||||||
db_session.close()
|
db_session.close()
|
||||||
return jsonify({"error": "Job nicht gefunden"}), 404
|
return jsonify({"error": "Job nicht gefunden"}), 404
|
||||||
|
|
||||||
# Prüfen, ob der Job abgebrochen werden kann
|
# Prüfen, ob der Job abgebrochen werden kann
|
||||||
if job.status not in ["scheduled", "running"]:
|
if job.status not in ["scheduled", "running"]:
|
||||||
db_session.close()
|
db_session.close()
|
||||||
return jsonify({"error": f"Job kann im Status '{job.status}' nicht abgebrochen werden"}), 400
|
return jsonify({"error": f"Job kann im Status '{job.status}' nicht abgebrochen werden"}), 400
|
||||||
|
|
||||||
# Job als abgebrochen markieren
|
# Job als abgebrochen markieren
|
||||||
@ -2320,7 +2320,7 @@ def cancel_job(job_id):
|
|||||||
from utils.job_scheduler import toggle_plug
|
from utils.job_scheduler import toggle_plug
|
||||||
toggle_plug(job.printer_id, False)
|
toggle_plug(job.printer_id, False)
|
||||||
|
|
||||||
db_session.commit()
|
db_session.commit()
|
||||||
|
|
||||||
job_dict = job.to_dict()
|
job_dict = job.to_dict()
|
||||||
db_session.close()
|
db_session.close()
|
||||||
@ -2338,7 +2338,7 @@ def cancel_job(job_id):
|
|||||||
def start_job(job_id):
|
def start_job(job_id):
|
||||||
"""Startet einen Job manuell."""
|
"""Startet einen Job manuell."""
|
||||||
try:
|
try:
|
||||||
db_session = get_db_session()
|
db_session = get_db_session()
|
||||||
job = db_session.query(Job).options(joinedload(Job.printer)).get(job_id)
|
job = db_session.query(Job).options(joinedload(Job.printer)).get(job_id)
|
||||||
|
|
||||||
if not job:
|
if not job:
|
||||||
@ -2347,7 +2347,7 @@ def start_job(job_id):
|
|||||||
|
|
||||||
# Prüfen, ob der Job gestartet werden kann
|
# Prüfen, ob der Job gestartet werden kann
|
||||||
if job.status not in ["scheduled", "queued", "waiting_for_printer"]:
|
if job.status not in ["scheduled", "queued", "waiting_for_printer"]:
|
||||||
db_session.close()
|
db_session.close()
|
||||||
return jsonify({"error": f"Job kann im Status '{job.status}' nicht gestartet werden"}), 400
|
return jsonify({"error": f"Job kann im Status '{job.status}' nicht gestartet werden"}), 400
|
||||||
|
|
||||||
# Drucker einschalten falls verfügbar
|
# Drucker einschalten falls verfügbar
|
||||||
@ -2358,7 +2358,7 @@ def start_job(job_id):
|
|||||||
jobs_logger.info(f"Drucker {job.printer.name} für Job {job_id} eingeschaltet")
|
jobs_logger.info(f"Drucker {job.printer.name} für Job {job_id} eingeschaltet")
|
||||||
else:
|
else:
|
||||||
jobs_logger.warning(f"Konnte Drucker {job.printer.name} für Job {job_id} nicht einschalten")
|
jobs_logger.warning(f"Konnte Drucker {job.printer.name} für Job {job_id} nicht einschalten")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
jobs_logger.warning(f"Fehler beim Einschalten des Druckers für Job {job_id}: {str(e)}")
|
jobs_logger.warning(f"Fehler beim Einschalten des Druckers für Job {job_id}: {str(e)}")
|
||||||
|
|
||||||
# Job als laufend markieren
|
# Job als laufend markieren
|
||||||
@ -2398,7 +2398,7 @@ def pause_job(job_id):
|
|||||||
|
|
||||||
# Prüfen, ob der Job pausiert werden kann
|
# Prüfen, ob der Job pausiert werden kann
|
||||||
if job.status != "running":
|
if job.status != "running":
|
||||||
db_session.close()
|
db_session.close()
|
||||||
return jsonify({"error": f"Job kann im Status '{job.status}' nicht pausiert werden"}), 400
|
return jsonify({"error": f"Job kann im Status '{job.status}' nicht pausiert werden"}), 400
|
||||||
|
|
||||||
# Drucker ausschalten
|
# Drucker ausschalten
|
||||||
@ -2409,7 +2409,7 @@ def pause_job(job_id):
|
|||||||
jobs_logger.info(f"Drucker {job.printer.name} für Job {job_id} ausgeschaltet (Pause)")
|
jobs_logger.info(f"Drucker {job.printer.name} für Job {job_id} ausgeschaltet (Pause)")
|
||||||
else:
|
else:
|
||||||
jobs_logger.warning(f"Konnte Drucker {job.printer.name} für Job {job_id} nicht ausschalten")
|
jobs_logger.warning(f"Konnte Drucker {job.printer.name} für Job {job_id} nicht ausschalten")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
jobs_logger.warning(f"Fehler beim Ausschalten des Druckers für Job {job_id}: {str(e)}")
|
jobs_logger.warning(f"Fehler beim Ausschalten des Druckers für Job {job_id}: {str(e)}")
|
||||||
|
|
||||||
# Job als pausiert markieren
|
# Job als pausiert markieren
|
||||||
@ -2419,7 +2419,7 @@ def pause_job(job_id):
|
|||||||
db_session.commit()
|
db_session.commit()
|
||||||
|
|
||||||
job_dict = job.to_dict()
|
job_dict = job.to_dict()
|
||||||
db_session.close()
|
db_session.close()
|
||||||
|
|
||||||
jobs_logger.info(f"Job {job_id} pausiert von Benutzer {current_user.id}")
|
jobs_logger.info(f"Job {job_id} pausiert von Benutzer {current_user.id}")
|
||||||
return jsonify({
|
return jsonify({
|
||||||
@ -2458,7 +2458,7 @@ def resume_job(job_id):
|
|||||||
jobs_logger.info(f"Drucker {job.printer.name} für Job {job_id} eingeschaltet (Resume)")
|
jobs_logger.info(f"Drucker {job.printer.name} für Job {job_id} eingeschaltet (Resume)")
|
||||||
else:
|
else:
|
||||||
jobs_logger.warning(f"Konnte Drucker {job.printer.name} für Job {job_id} nicht einschalten")
|
jobs_logger.warning(f"Konnte Drucker {job.printer.name} für Job {job_id} nicht einschalten")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
jobs_logger.warning(f"Fehler beim Einschalten des Druckers für Job {job_id}: {str(e)}")
|
jobs_logger.warning(f"Fehler beim Einschalten des Druckers für Job {job_id}: {str(e)}")
|
||||||
|
|
||||||
# Job als laufend markieren
|
# Job als laufend markieren
|
||||||
@ -4859,4 +4859,179 @@ def get_printers():
|
|||||||
return jsonify({
|
return jsonify({
|
||||||
"error": f"Fehler beim Laden der Drucker: {str(e)}",
|
"error": f"Fehler beim Laden der Drucker: {str(e)}",
|
||||||
"printers": []
|
"printers": []
|
||||||
}), 500
|
}), 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 weitere Zeit.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
data = request.get_json() or {}
|
||||||
|
extend_minutes = data.get('extend_minutes', 30)
|
||||||
|
|
||||||
|
# Begrenzen der Verlängerung (max 2 Stunden)
|
||||||
|
extend_minutes = min(extend_minutes, 120)
|
||||||
|
|
||||||
|
now = datetime.now()
|
||||||
|
session['last_activity'] = now.isoformat()
|
||||||
|
session['session_extended'] = now.isoformat()
|
||||||
|
session['extended_by_minutes'] = extend_minutes
|
||||||
|
|
||||||
|
auth_logger.info(f"🕒 Session verlängert für Benutzer {current_user.email} um {extend_minutes} Minuten")
|
||||||
|
|
||||||
|
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
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
auth_logger.error(f"Fehler beim Verlängern der Session: {str(e)}")
|
||||||
|
return jsonify({"error": "Session-Verlängerung fehlgeschlagen"}), 500
|
@ -67,7 +67,7 @@ FLASK_HOST = "0.0.0.0"
|
|||||||
FLASK_PORT = 443 # Geändert von 443 auf 8443 (nicht-privilegierter Port)
|
FLASK_PORT = 443 # Geändert von 443 auf 8443 (nicht-privilegierter Port)
|
||||||
FLASK_FALLBACK_PORT = 8080 # Geändert von 80 auf 8080 (nicht-privilegierter Port)
|
FLASK_FALLBACK_PORT = 8080 # Geändert von 80 auf 8080 (nicht-privilegierter Port)
|
||||||
FLASK_DEBUG = True
|
FLASK_DEBUG = True
|
||||||
SESSION_LIFETIME = timedelta(days=7)
|
SESSION_LIFETIME = timedelta(hours=2) # Reduziert von 7 Tagen auf 2 Stunden für bessere Sicherheit
|
||||||
|
|
||||||
# Upload-Konfiguration
|
# Upload-Konfiguration
|
||||||
UPLOAD_FOLDER = os.path.join(BASE_DIR, "uploads")
|
UPLOAD_FOLDER = os.path.join(BASE_DIR, "uploads")
|
||||||
|
475
backend/app/static/js/session-manager.js
Normal file
475
backend/app/static/js/session-manager.js
Normal file
@ -0,0 +1,475 @@
|
|||||||
|
/**
|
||||||
|
* Erweitertes Session-Management für MYP Platform
|
||||||
|
*
|
||||||
|
* Features:
|
||||||
|
* - Automatische Session-Überwachung
|
||||||
|
* - Heartbeat-System für Session-Verlängerung
|
||||||
|
* - Benutzer-Warnungen bei bevorstehender Abmeldung
|
||||||
|
* - Graceful Logout bei Session-Ablauf
|
||||||
|
* - Modal-Dialoge für Session-Verlängerung
|
||||||
|
*
|
||||||
|
* @author Mercedes-Benz MYP Platform
|
||||||
|
* @version 2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
class SessionManager {
|
||||||
|
constructor() {
|
||||||
|
this.isAuthenticated = false;
|
||||||
|
this.maxInactiveMinutes = 30; // Standard: 30 Minuten
|
||||||
|
this.heartbeatInterval = 5 * 60 * 1000; // 5 Minuten
|
||||||
|
this.warningTime = 5 * 60 * 1000; // 5 Minuten vor Ablauf warnen
|
||||||
|
this.checkInterval = 30 * 1000; // Alle 30 Sekunden prüfen
|
||||||
|
|
||||||
|
this.heartbeatTimer = null;
|
||||||
|
this.statusCheckTimer = null;
|
||||||
|
this.warningShown = false;
|
||||||
|
this.sessionWarningModal = null;
|
||||||
|
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
try {
|
||||||
|
// Prüfe initial ob Benutzer angemeldet ist
|
||||||
|
await this.checkAuthenticationStatus();
|
||||||
|
|
||||||
|
if (this.isAuthenticated) {
|
||||||
|
this.startSessionMonitoring();
|
||||||
|
this.createWarningModal();
|
||||||
|
|
||||||
|
console.log('🔐 Session Manager gestartet');
|
||||||
|
console.log(`📊 Max Inaktivität: ${this.maxInactiveMinutes} Minuten`);
|
||||||
|
console.log(`💓 Heartbeat Intervall: ${this.heartbeatInterval / 1000 / 60} Minuten`);
|
||||||
|
} else {
|
||||||
|
console.log('👤 Benutzer nicht angemeldet - Session Manager inaktiv');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Session Manager Initialisierung fehlgeschlagen:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkAuthenticationStatus() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/session/status', {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-Requested-With': 'XMLHttpRequest'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
if (data.success) {
|
||||||
|
this.isAuthenticated = true;
|
||||||
|
this.maxInactiveMinutes = data.session.max_inactive_minutes;
|
||||||
|
|
||||||
|
console.log('✅ Session Status:', {
|
||||||
|
user: data.user.email,
|
||||||
|
timeLeft: Math.floor(data.session.time_left_seconds / 60) + ' Minuten',
|
||||||
|
lastActivity: new Date(data.session.last_activity).toLocaleString('de-DE')
|
||||||
|
});
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
} else if (response.status === 401) {
|
||||||
|
this.isAuthenticated = false;
|
||||||
|
this.handleSessionExpired('Authentication check failed');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Fehler beim Prüfen des Session-Status:', error);
|
||||||
|
this.isAuthenticated = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
startSessionMonitoring() {
|
||||||
|
// Heartbeat alle 5 Minuten senden
|
||||||
|
this.heartbeatTimer = setInterval(() => {
|
||||||
|
this.sendHeartbeat();
|
||||||
|
}, this.heartbeatInterval);
|
||||||
|
|
||||||
|
// Session-Status alle 30 Sekunden prüfen
|
||||||
|
this.statusCheckTimer = setInterval(() => {
|
||||||
|
this.checkSessionStatus();
|
||||||
|
}, this.checkInterval);
|
||||||
|
|
||||||
|
// Initial Heartbeat senden
|
||||||
|
setTimeout(() => this.sendHeartbeat(), 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendHeartbeat() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/session/heartbeat', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-Requested-With': 'XMLHttpRequest'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
page: window.location.pathname
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
if (data.success) {
|
||||||
|
console.log('💓 Heartbeat gesendet - Session aktiv:',
|
||||||
|
Math.floor(data.time_left_seconds / 60) + ' Minuten verbleibend');
|
||||||
|
} else {
|
||||||
|
console.warn('⚠️ Heartbeat fehlgeschlagen:', data);
|
||||||
|
}
|
||||||
|
} else if (response.status === 401) {
|
||||||
|
this.handleSessionExpired('Heartbeat failed - unauthorized');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Heartbeat-Fehler:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkSessionStatus() {
|
||||||
|
try {
|
||||||
|
const sessionData = await this.checkAuthenticationStatus();
|
||||||
|
|
||||||
|
if (sessionData && sessionData.session) {
|
||||||
|
const timeLeftSeconds = sessionData.session.time_left_seconds;
|
||||||
|
const timeLeftMinutes = Math.floor(timeLeftSeconds / 60);
|
||||||
|
|
||||||
|
// Warnung anzeigen wenn weniger als 5 Minuten verbleiben
|
||||||
|
if (timeLeftSeconds <= this.warningTime / 1000 && timeLeftSeconds > 0) {
|
||||||
|
if (!this.warningShown) {
|
||||||
|
this.showSessionWarning(timeLeftMinutes);
|
||||||
|
this.warningShown = true;
|
||||||
|
}
|
||||||
|
} else if (timeLeftSeconds <= 0) {
|
||||||
|
// Session abgelaufen
|
||||||
|
this.handleSessionExpired('Session time expired');
|
||||||
|
} else {
|
||||||
|
// Session OK - Warnung zurücksetzen
|
||||||
|
this.warningShown = false;
|
||||||
|
this.hideSessionWarning();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Session-Status in der UI aktualisieren
|
||||||
|
this.updateSessionStatusDisplay(sessionData);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Session-Status-Check fehlgeschlagen:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showSessionWarning(minutesLeft) {
|
||||||
|
// Bestehende Warnung entfernen
|
||||||
|
this.hideSessionWarning();
|
||||||
|
|
||||||
|
// Toast-Notification anzeigen
|
||||||
|
this.showToast(
|
||||||
|
'Session läuft ab',
|
||||||
|
`Ihre Session läuft in ${minutesLeft} Minuten ab. Möchten Sie verlängern?`,
|
||||||
|
'warning',
|
||||||
|
10000, // 10 Sekunden anzeigen
|
||||||
|
[
|
||||||
|
{
|
||||||
|
text: 'Verlängern',
|
||||||
|
action: () => this.extendSession()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Abmelden',
|
||||||
|
action: () => this.logout()
|
||||||
|
}
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Modal anzeigen für wichtige Warnung
|
||||||
|
if (this.sessionWarningModal) {
|
||||||
|
this.sessionWarningModal.show();
|
||||||
|
this.updateWarningModal(minutesLeft);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`⚠️ Session-Warnung: ${minutesLeft} Minuten verbleibend`);
|
||||||
|
}
|
||||||
|
|
||||||
|
hideSessionWarning() {
|
||||||
|
if (this.sessionWarningModal) {
|
||||||
|
this.sessionWarningModal.hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createWarningModal() {
|
||||||
|
// Modal HTML erstellen
|
||||||
|
const modalHTML = `
|
||||||
|
<div id="sessionWarningModal" class="fixed inset-0 z-50 hidden overflow-y-auto" aria-labelledby="modal-title" role="dialog" aria-modal="true">
|
||||||
|
<div class="flex items-center justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
|
||||||
|
<div class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity"></div>
|
||||||
|
|
||||||
|
<div class="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full">
|
||||||
|
<div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
|
||||||
|
<div class="sm:flex sm:items-start">
|
||||||
|
<div class="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10">
|
||||||
|
<svg class="h-6 w-6 text-red-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.732-.833-2.5 0L3.314 16.5c-.77.833.192 2.5 1.732 2.5z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
|
||||||
|
<h3 class="text-lg leading-6 font-medium text-gray-900" id="modal-title">
|
||||||
|
Session läuft ab
|
||||||
|
</h3>
|
||||||
|
<div class="mt-2">
|
||||||
|
<p class="text-sm text-gray-500" id="warningMessage">
|
||||||
|
Ihre Session läuft in <span id="timeRemaining" class="font-bold text-red-600">5</span> Minuten ab.
|
||||||
|
</p>
|
||||||
|
<p class="text-sm text-gray-500 mt-2">
|
||||||
|
Möchten Sie Ihre Session verlängern oder sich abmelden?
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
|
||||||
|
<button type="button" id="extendSessionBtn" class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:ml-3 sm:w-auto sm:text-sm">
|
||||||
|
Session verlängern
|
||||||
|
</button>
|
||||||
|
<button type="button" id="logoutBtn" class="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm">
|
||||||
|
Abmelden
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
// Modal in DOM einfügen
|
||||||
|
document.body.insertAdjacentHTML('beforeend', modalHTML);
|
||||||
|
|
||||||
|
// Event-Listener hinzufügen
|
||||||
|
document.getElementById('extendSessionBtn').addEventListener('click', () => {
|
||||||
|
this.extendSession();
|
||||||
|
this.hideSessionWarning();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('logoutBtn').addEventListener('click', () => {
|
||||||
|
this.logout();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Modal-Objekt erstellen
|
||||||
|
this.sessionWarningModal = {
|
||||||
|
element: document.getElementById('sessionWarningModal'),
|
||||||
|
show: () => {
|
||||||
|
document.getElementById('sessionWarningModal').classList.remove('hidden');
|
||||||
|
},
|
||||||
|
hide: () => {
|
||||||
|
document.getElementById('sessionWarningModal').classList.add('hidden');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
updateWarningModal(minutesLeft) {
|
||||||
|
const timeElement = document.getElementById('timeRemaining');
|
||||||
|
if (timeElement) {
|
||||||
|
timeElement.textContent = minutesLeft;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async extendSession(extendMinutes = 30) {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/session/extend', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-Requested-With': 'XMLHttpRequest'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
extend_minutes: extendMinutes
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
if (data.success) {
|
||||||
|
this.warningShown = false;
|
||||||
|
|
||||||
|
this.showToast(
|
||||||
|
'Session verlängert',
|
||||||
|
`Ihre Session wurde um ${data.extended_minutes} Minuten verlängert`,
|
||||||
|
'success',
|
||||||
|
5000
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log('✅ Session verlängert:', data);
|
||||||
|
} else {
|
||||||
|
this.showToast('Fehler', 'Session konnte nicht verlängert werden', 'error');
|
||||||
|
}
|
||||||
|
} else if (response.status === 401) {
|
||||||
|
this.handleSessionExpired('Extend session failed - unauthorized');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Session-Verlängerung fehlgeschlagen:', error);
|
||||||
|
this.showToast('Fehler', 'Session-Verlängerung fehlgeschlagen', 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async logout() {
|
||||||
|
try {
|
||||||
|
this.stopSessionMonitoring();
|
||||||
|
|
||||||
|
// Logout-Request senden
|
||||||
|
const response = await fetch('/auth/logout', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-Requested-With': 'XMLHttpRequest'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Zur Login-Seite weiterleiten
|
||||||
|
if (response.ok) {
|
||||||
|
window.location.href = '/auth/login';
|
||||||
|
} else {
|
||||||
|
// Fallback: Direkter Redirect
|
||||||
|
window.location.href = '/auth/login';
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Logout-Fehler:', error);
|
||||||
|
// Fallback: Direkter Redirect
|
||||||
|
window.location.href = '/auth/login';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSessionExpired(reason) {
|
||||||
|
console.log('🕒 Session abgelaufen:', reason);
|
||||||
|
|
||||||
|
this.stopSessionMonitoring();
|
||||||
|
this.isAuthenticated = false;
|
||||||
|
|
||||||
|
// Benutzer benachrichtigen
|
||||||
|
this.showToast(
|
||||||
|
'Session abgelaufen',
|
||||||
|
'Sie wurden automatisch abgemeldet. Bitte melden Sie sich erneut an.',
|
||||||
|
'warning',
|
||||||
|
8000
|
||||||
|
);
|
||||||
|
|
||||||
|
// Nach kurzer Verzögerung zur Login-Seite weiterleiten
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.href = '/auth/login?reason=session_expired';
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
stopSessionMonitoring() {
|
||||||
|
if (this.heartbeatTimer) {
|
||||||
|
clearInterval(this.heartbeatTimer);
|
||||||
|
this.heartbeatTimer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.statusCheckTimer) {
|
||||||
|
clearInterval(this.statusCheckTimer);
|
||||||
|
this.statusCheckTimer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('🛑 Session-Monitoring gestoppt');
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSessionStatusDisplay(sessionData) {
|
||||||
|
// Session-Status in der Navigation/Header anzeigen (falls vorhanden)
|
||||||
|
const statusElement = document.getElementById('sessionStatus');
|
||||||
|
if (statusElement) {
|
||||||
|
const timeLeftMinutes = Math.floor(sessionData.session.time_left_seconds / 60);
|
||||||
|
statusElement.textContent = `Session: ${timeLeftMinutes}min`;
|
||||||
|
|
||||||
|
// Farbe basierend auf verbleibender Zeit
|
||||||
|
if (timeLeftMinutes <= 5) {
|
||||||
|
statusElement.className = 'text-red-600 font-medium';
|
||||||
|
} else if (timeLeftMinutes <= 10) {
|
||||||
|
statusElement.className = 'text-yellow-600 font-medium';
|
||||||
|
} else {
|
||||||
|
statusElement.className = 'text-green-600 font-medium';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showToast(title, message, type = 'info', duration = 5000, actions = []) {
|
||||||
|
// Verwende das bestehende Toast-System falls verfügbar
|
||||||
|
if (window.showToast) {
|
||||||
|
window.showToast(message, type, duration);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: Simple Browser-Notification
|
||||||
|
if (type === 'error' || type === 'warning') {
|
||||||
|
alert(`${title}: ${message}`);
|
||||||
|
} else {
|
||||||
|
console.log(`${title}: ${message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === ÖFFENTLICHE API ===
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prüft ob Benutzer angemeldet ist
|
||||||
|
*/
|
||||||
|
isLoggedIn() {
|
||||||
|
return this.isAuthenticated;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Startet Session-Monitoring manuell
|
||||||
|
*/
|
||||||
|
start() {
|
||||||
|
if (!this.heartbeatTimer && this.isAuthenticated) {
|
||||||
|
this.startSessionMonitoring();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stoppt Session-Monitoring manuell
|
||||||
|
*/
|
||||||
|
stop() {
|
||||||
|
this.stopSessionMonitoring();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verlängert Session manuell
|
||||||
|
*/
|
||||||
|
async extend(minutes = 30) {
|
||||||
|
return await this.extendSession(minutes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Meldet Benutzer manuell ab
|
||||||
|
*/
|
||||||
|
async logoutUser() {
|
||||||
|
return await this.logout();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Session Manager automatisch starten wenn DOM geladen ist
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
// Nur starten wenn wir nicht auf der Login-Seite sind
|
||||||
|
if (!window.location.pathname.includes('/auth/login')) {
|
||||||
|
window.sessionManager = new SessionManager();
|
||||||
|
|
||||||
|
// Globale Event-Listener für Session-Management
|
||||||
|
window.addEventListener('beforeunload', () => {
|
||||||
|
if (window.sessionManager) {
|
||||||
|
window.sessionManager.stop();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reaktion auf Sichtbarkeitsänderungen (Tab-Wechsel)
|
||||||
|
document.addEventListener('visibilitychange', () => {
|
||||||
|
if (window.sessionManager && window.sessionManager.isLoggedIn()) {
|
||||||
|
if (document.hidden) {
|
||||||
|
console.log('🙈 Tab versteckt - Session-Monitoring reduziert');
|
||||||
|
} else {
|
||||||
|
console.log('👁️ Tab sichtbar - Session-Check');
|
||||||
|
// Sofortiger Session-Check wenn Tab wieder sichtbar wird
|
||||||
|
setTimeout(() => window.sessionManager.checkSessionStatus(), 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Session Manager für andere Scripts verfügbar machen
|
||||||
|
window.SessionManager = SessionManager;
|
@ -654,6 +654,7 @@
|
|||||||
<script src="{{ url_for('static', filename='js/printer_monitor.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/printer_monitor.js') }}"></script>
|
||||||
{% if current_user.is_authenticated %}
|
{% if current_user.is_authenticated %}
|
||||||
<script src="{{ url_for('static', filename='js/notifications.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/notifications.js') }}"></script>
|
||||||
|
<script src="{{ url_for('static', filename='js/session-manager.js') }}"></script>
|
||||||
<script src="{{ url_for('static', filename='js/auto-logout.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/auto-logout.js') }}"></script>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user