📚 Improved error handling documentation and codebase organization in the frontend application. 🖥️🔍
This commit is contained in:
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;
|
Reference in New Issue
Block a user