490 lines
19 KiB
JavaScript
490 lines
19 KiB
JavaScript
/**
|
|
* 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 {
|
|
// CSRF-Token aus dem Meta-Tag holen
|
|
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
|
|
|
|
const headers = {
|
|
'Content-Type': 'application/json',
|
|
'X-Requested-With': 'XMLHttpRequest'
|
|
};
|
|
|
|
// CSRF-Token hinzufügen wenn verfügbar - Flask-WTF erwartet X-CSRFToken oder den Token im Body
|
|
if (csrfToken) {
|
|
headers['X-CSRFToken'] = csrfToken;
|
|
}
|
|
|
|
const response = await fetch('/api/session/heartbeat', {
|
|
method: 'POST',
|
|
headers: headers,
|
|
body: JSON.stringify({
|
|
timestamp: new Date().toISOString(),
|
|
page: window.location.pathname,
|
|
csrf_token: csrfToken // Zusätzlich im Body senden
|
|
})
|
|
});
|
|
|
|
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');
|
|
} else if (response.status === 400) {
|
|
console.warn('⚠️ CSRF-Token Problem beim Heartbeat - versuche Seite neu zu laden');
|
|
// Bei CSRF-Problemen die Seite neu laden
|
|
setTimeout(() => location.reload(), 5000);
|
|
}
|
|
} 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;
|