257 lines
9.7 KiB
JavaScript
257 lines
9.7 KiB
JavaScript
class AutoLogoutManager {
|
|
constructor() {
|
|
this.timer = null;
|
|
this.warningTimer = null;
|
|
this.timeout = 60; // Standard: 60 Minuten
|
|
this.warningTime = 5; // Warnung 5 Minuten vor Logout
|
|
this.isWarningShown = false;
|
|
|
|
// API Base URL Detection
|
|
this.apiBaseUrl = this.detectApiBaseUrl();
|
|
|
|
this.init();
|
|
}
|
|
|
|
/**
|
|
* Zentrale API-Response-Validierung mit umfassendem Error-Handling
|
|
* @param {Response} response - Fetch Response-Objekt
|
|
* @param {string} context - Kontext der API-Anfrage für bessere Fehlermeldungen
|
|
* @returns {Promise<Object>} - Validierte JSON-Daten
|
|
* @throws {Error} - Bei Validierungsfehlern
|
|
*/
|
|
async validateApiResponse(response, context = 'API-Anfrage') {
|
|
try {
|
|
// 1. HTTP Status Code prüfen
|
|
if (!response.ok) {
|
|
// Spezielle Behandlung für bekannte Fehler-Codes
|
|
switch (response.status) {
|
|
case 401:
|
|
throw new Error(`Authentifizierung fehlgeschlagen (${context})`);
|
|
case 403:
|
|
throw new Error(`Zugriff verweigert (${context})`);
|
|
case 404:
|
|
throw new Error(`Ressource nicht gefunden (${context})`);
|
|
case 429:
|
|
throw new Error(`Zu viele Anfragen (${context})`);
|
|
case 500:
|
|
throw new Error(`Serverfehler (${context})`);
|
|
case 503:
|
|
throw new Error(`Service nicht verfügbar (${context})`);
|
|
default:
|
|
throw new Error(`HTTP ${response.status}: ${response.statusText} (${context})`);
|
|
}
|
|
}
|
|
|
|
// 2. Content-Type prüfen (muss application/json enthalten)
|
|
const contentType = response.headers.get('content-type');
|
|
if (!contentType || !contentType.includes('application/json')) {
|
|
// Versuche Response-Text zu lesen für bessere Fehlermeldung
|
|
const responseText = await response.text();
|
|
|
|
// Prüfe auf HTML-Fehlerseiten (typisch für 404/500 Seiten)
|
|
if (responseText.includes('<!DOCTYPE html>') || responseText.includes('<html')) {
|
|
console.warn(`❌ HTML-Fehlerseite erhalten statt JSON (${context}):`, responseText.substring(0, 200));
|
|
throw new Error(`Server-Fehlerseite erhalten statt JSON-Response (${context})`);
|
|
}
|
|
|
|
console.warn(`❌ Ungültiger Content-Type (${context}):`, contentType);
|
|
console.warn(`❌ Response-Text (${context}):`, responseText.substring(0, 500));
|
|
throw new Error(`Ungültiger Content-Type: ${contentType || 'fehlt'} (${context})`);
|
|
}
|
|
|
|
// 3. JSON parsing mit detailliertem Error-Handling
|
|
let data;
|
|
try {
|
|
data = await response.json();
|
|
} catch (jsonError) {
|
|
// Versuche rohen Text zu lesen für Debugging
|
|
const rawText = await response.text();
|
|
console.error(`❌ JSON-Parsing-Fehler (${context}):`, jsonError);
|
|
console.error(`❌ Raw Response (${context}):`, rawText.substring(0, 1000));
|
|
throw new Error(`Ungültige JSON-Response: ${jsonError.message} (${context})`);
|
|
}
|
|
|
|
// 4. Prüfe auf null/undefined Response
|
|
if (data === null || data === undefined) {
|
|
throw new Error(`Leere Response erhalten (${context})`);
|
|
}
|
|
|
|
// 5. Validiere Response-Struktur (wenn success-Feld erwartet wird)
|
|
if (typeof data === 'object' && data.hasOwnProperty('success')) {
|
|
if (!data.success && data.error) {
|
|
console.warn(`❌ API-Fehler (${context}):`, data.error);
|
|
throw new Error(`API-Fehler: ${data.error} (${context})`);
|
|
}
|
|
}
|
|
|
|
// Erfolgreiche Validierung
|
|
console.log(`✅ API-Response validiert (${context}):`, data);
|
|
return data;
|
|
|
|
} catch (error) {
|
|
// Error-Logging mit Kontext
|
|
console.error(`❌ validateApiResponse fehlgeschlagen (${context}):`, error);
|
|
console.error(`❌ Response-Details (${context}):`, {
|
|
status: response.status,
|
|
statusText: response.statusText,
|
|
url: response.url,
|
|
headers: Object.fromEntries(response.headers.entries())
|
|
});
|
|
|
|
// Re-throw mit erweiterten Informationen
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
detectApiBaseUrl() {
|
|
const currentPort = window.location.port;
|
|
const currentProtocol = window.location.protocol;
|
|
const currentHost = window.location.hostname;
|
|
|
|
// Development-Umgebung (Port 5000)
|
|
if (currentPort === '5000') {
|
|
return `${currentProtocol}//${currentHost}:${currentPort}`;
|
|
}
|
|
|
|
// Production-Umgebung (Port 443 oder kein Port)
|
|
if (currentPort === '443' || currentPort === '') {
|
|
return `${currentProtocol}//${currentHost}`;
|
|
}
|
|
|
|
// Fallback für andere Ports
|
|
return window.location.origin;
|
|
}
|
|
|
|
async init() {
|
|
await this.loadSettings();
|
|
this.setupActivityListeners();
|
|
this.startTimer();
|
|
}
|
|
|
|
async loadSettings() {
|
|
try {
|
|
const response = await fetch(`${this.apiBaseUrl}/api/user/settings`);
|
|
const data = await this.validateApiResponse(response, 'Benutzereinstellungen laden');
|
|
|
|
if (data.success && data.settings.privacy?.auto_logout) {
|
|
const timeout = parseInt(data.settings.privacy.auto_logout);
|
|
if (timeout > 0 && timeout !== 'never') {
|
|
this.timeout = timeout;
|
|
} else {
|
|
this.timeout = 0; // Deaktiviert
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.warn('Auto-Logout-Einstellungen konnten nicht geladen werden:', error);
|
|
}
|
|
}
|
|
|
|
setupActivityListeners() {
|
|
const events = ['mousedown', 'mousemove', 'keypress', 'scroll', 'touchstart', 'click'];
|
|
|
|
events.forEach(event => {
|
|
document.addEventListener(event, () => this.resetTimer(), { passive: true });
|
|
});
|
|
}
|
|
|
|
startTimer() {
|
|
if (this.timeout <= 0) return;
|
|
|
|
this.clearTimers();
|
|
|
|
const timeoutMs = this.timeout * 60 * 1000;
|
|
const warningMs = this.warningTime * 60 * 1000;
|
|
|
|
this.warningTimer = setTimeout(() => this.showWarning(), timeoutMs - warningMs);
|
|
this.timer = setTimeout(() => this.performLogout(), timeoutMs);
|
|
}
|
|
|
|
resetTimer() {
|
|
if (this.isWarningShown) {
|
|
this.closeWarning();
|
|
}
|
|
this.startTimer();
|
|
}
|
|
|
|
clearTimers() {
|
|
if (this.timer) clearTimeout(this.timer);
|
|
if (this.warningTimer) clearTimeout(this.warningTimer);
|
|
}
|
|
|
|
showWarning() {
|
|
if (this.isWarningShown) return;
|
|
this.isWarningShown = true;
|
|
|
|
const modal = document.createElement('div');
|
|
modal.id = 'auto-logout-warning';
|
|
modal.className = 'fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50';
|
|
modal.innerHTML = `
|
|
<div class="bg-white dark:bg-slate-800 rounded-lg p-6 max-w-md mx-4 shadow-xl">
|
|
<h3 class="text-lg font-medium text-slate-900 dark:text-white mb-4">Automatische Abmeldung</h3>
|
|
<p class="text-sm text-slate-600 dark:text-slate-300 mb-4">
|
|
Sie werden in ${this.warningTime} Minuten aufgrund von Inaktivität abgemeldet.
|
|
</p>
|
|
<div class="flex space-x-3">
|
|
<button id="stay-logged-in" class="bg-blue-600 text-white px-4 py-2 rounded-lg">
|
|
Angemeldet bleiben
|
|
</button>
|
|
<button id="logout-now" class="bg-gray-300 text-slate-700 px-4 py-2 rounded-lg">
|
|
Jetzt abmelden
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
document.body.appendChild(modal);
|
|
|
|
document.getElementById('stay-logged-in').onclick = () => {
|
|
this.closeWarning();
|
|
this.sendKeepAlive();
|
|
this.resetTimer();
|
|
};
|
|
|
|
document.getElementById('logout-now').onclick = () => {
|
|
this.performLogout();
|
|
};
|
|
}
|
|
|
|
closeWarning() {
|
|
const modal = document.getElementById('auto-logout-warning');
|
|
if (modal) modal.remove();
|
|
this.isWarningShown = false;
|
|
}
|
|
|
|
async sendKeepAlive() {
|
|
try {
|
|
const response = await fetch(`${this.apiBaseUrl}/api/auth/keep-alive`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRFToken': this.getCSRFToken()
|
|
}
|
|
});
|
|
|
|
await this.validateApiResponse(response, 'Keep-Alive senden');
|
|
} catch (error) {
|
|
console.warn('Keep-Alive fehlgeschlagen:', error);
|
|
}
|
|
}
|
|
|
|
getCSRFToken() {
|
|
const metaTag = document.querySelector('meta[name="csrf-token"]');
|
|
return metaTag ? metaTag.getAttribute('content') : '';
|
|
}
|
|
|
|
async performLogout() {
|
|
this.closeWarning();
|
|
this.clearTimers();
|
|
window.location.href = '/auth/logout';
|
|
}
|
|
}
|
|
|
|
// Initialisierung
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
if (!window.location.pathname.includes('/login')) {
|
|
window.autoLogoutManager = new AutoLogoutManager();
|
|
}
|
|
}); |