// ----------------------------- // Projektübersicht — Authentifizierung (Login-Overlay, Session, Logout) // ----------------------------- // Wird von App.html direkt nach // js/server.js geladen, da checkAuthAndInit() baseUrl() benötigt. Ersetzt den // früheren rein clientseitigen Passwortschutz (js/tabs-auth.js) durch eine // echte serverseitige Session (server.js: /login, /logout, /whoami). Ohne // gültige Session zeigt checkAuthAndInit() das blockierende Login-Overlay // (#loginOverlay/#loginForm, siehe App.html); nach // erfolgreichem Login läuft der eigentliche Seiteneinstieg (per Callback // übergeben) wie gewohnt weiter. // // Hier liegt außerdem der Dialog zum Ändern des eigenen Passworts // (#passwordModal, POST /change-password). Er hat zwei Betriebsarten: // freiwillig (aus dem Bereich „Einstellungen" heraus, mit Abbrechen) und // ERZWUNGEN — wenn das Konto must_change_password trägt, was der Server bei // jedem neu angelegten Konto und nach jedem Zurücksetzen setzt. Bis zu diesem // Umbau gab es zu dem Endpunkt gar keine Oberfläche: Die Benutzerverwaltung // zeigte „Passwortwechsel erforderlich" an, erfüllen konnte ihn niemand. // currentUser: der aktuell angemeldete Benutzer ({id, username, displayName, // email, role, personId, mustChangePassword}) oder null vor dem Login. let currentUser = null; // renderUserBar: zeigt Namen + Rolle des angemeldeten Benutzers im Menueband // an (Element #userBarInfo) und blendet den gesamten Admin-Bereich der // Seitenleiste (#navAdminSection) nur fuer Administratoren ein. Dieser // Bereich fuehrt zu allen Admin-Aufgaben — Benutzerverwaltung, firmenweite // Vorgaben, Datenbank und Sitzungen. Es ist ein reines UI-Gate; die // eigentliche Sperre sitzt serverseitig in requireRole('admin') auf den // zugehoerigen Routen, und showTab() leitet Nicht-Administratoren zusaetzlich // vom Bereich „Admin" weg (js/tabs-auth.js). function renderUserBar(){ const info = document.getElementById('userBarInfo'); if(info && currentUser){ const roleLabel = currentUser.role === 'admin' ? 'Administrator' : currentUser.role === 'kiosk' ? 'Kiosk' : 'Mitglied'; info.textContent = (currentUser.displayName || currentUser.username) + ' (' + roleLabel + ')'; } const navAdminSection = document.getElementById('navAdminSection'); if(navAdminSection) navAdminSection.style.display = (currentUser && currentUser.role === 'admin') ? '' : 'none'; } // logout: beendet die Server-Session und lädt die Seite neu (zeigt danach // wieder das Login-Overlay, da /whoami dann {user:null} liefert). async function logout(){ try { await fetch(baseUrl() + '/logout', { method:'POST' }); } catch(e){} window.location.reload(); } // showLoginOverlay / hideLoginOverlay: blenden das blockierende Login-Overlay ein/aus. function showLoginOverlay(){ document.getElementById('loginOverlay').style.display = 'flex'; } function hideLoginOverlay(){ document.getElementById('loginOverlay').style.display = 'none'; } // afterAuthenticated: gemeinsamer Nachlauf beider Anmeldewege (bestehende // Session laut /whoami bzw. frisch abgeschicktes Login-Formular). Startet die // Seite und erzwingt danach — erst danach, damit die Anwendung hinter dem // Dialog schon steht — den Passwortwechsel, falls das Konto ihn verlangt. // Parameter: onAuthenticated (Funktion). Keine Rückgabe. function afterAuthenticated(onAuthenticated){ hideLoginOverlay(); renderUserBar(); onAuthenticated(); if(currentUser && currentUser.mustChangePassword) openPasswordModal(true); } // ---------- Eigenes Passwort ändern (#passwordModal) ---------- // passwordChangeForced: true, solange der Dialog wegen must_change_password // offen ist. Dann gibt es kein Abbrechen — sonst bliebe die Kennzeichnung im // Konto stehen und das (oft per Zettel weitergegebene) Startpasswort gültig. let passwordChangeForced = false; // openPasswordModal: zeigt den Dialog. Parameter: forced (Boolean) — im // erzwungenen Fall ohne Abbrechen-Schaltfläche und mit Hinweistext. // Keine Rückgabe. function openPasswordModal(forced){ const modal = document.getElementById('passwordModal'); if(!modal) return; passwordChangeForced = !!forced; ['passwordCurrent','passwordNew','passwordRepeat'].forEach(id => { const el = document.getElementById(id); if(el) el.value = ''; }); const errEl = document.getElementById('passwordError'); if(errEl){ errEl.textContent = ''; errEl.hidden = true; } const forcedHint = document.getElementById('passwordForcedHint'); if(forcedHint) forcedHint.hidden = !passwordChangeForced; const cancelBtn = document.getElementById('passwordCancelBtn'); if(cancelBtn) cancelBtn.style.display = passwordChangeForced ? 'none' : ''; modal.style.display = 'flex'; const first = document.getElementById('passwordCurrent'); if(first) first.focus(); } // closePasswordModal: schließt den Dialog — im erzwungenen Fall verweigert er // sich. Keine Parameter, keine Rückgabe. function closePasswordModal(){ if(passwordChangeForced) return; const modal = document.getElementById('passwordModal'); if(modal) modal.style.display = 'none'; } // submitPasswordChange: schickt den Wechsel an POST /change-password. Der // Server verlangt immer das aktuelle Passwort (auch im erzwungenen Fall) und // mindestens 8 Zeichen; die Wiederholung prüft nur der Client, damit ein // Vertipper nicht erst nach dem Speichern auffällt. // Parameter: ev (Submit-Event). Rückgabe: keine. async function submitPasswordChange(ev){ if(ev) ev.preventDefault(); const errEl = document.getElementById('passwordError'); const show = msg => { if(errEl){ errEl.textContent = msg; errEl.hidden = false; } }; const currentPassword = (document.getElementById('passwordCurrent') || {}).value || ''; const newPassword = (document.getElementById('passwordNew') || {}).value || ''; const repeat = (document.getElementById('passwordRepeat') || {}).value || ''; if(errEl){ errEl.textContent = ''; errEl.hidden = true; } if(newPassword.length < 8) return show('Das neue Passwort muss mindestens 8 Zeichen haben.'); if(newPassword !== repeat) return show('Die Wiederholung stimmt nicht mit dem neuen Passwort überein.'); if(newPassword === currentPassword) return show('Das neue Passwort muss sich vom bisherigen unterscheiden.'); try { const r = await fetch(baseUrl() + '/change-password', { method: 'POST', headers: { 'Content-Type': 'application/json; charset=utf-8' }, body: JSON.stringify({ currentPassword, newPassword }) }); const d = await r.json(); if(!r.ok || !d.ok) return show(d && d.error ? d.error : 'Passwort konnte nicht geändert werden.'); } catch(err){ console.error('Passwortwechsel fehlgeschlagen:', err); return show('Verbindung zum Server fehlgeschlagen.'); } if(currentUser) currentUser.mustChangePassword = false; passwordChangeForced = false; closePasswordModal(); // Der Bereich „Einstellungen" zeigt die Kennzeichnung mit an. if(typeof renderSettings === 'function' && typeof currentTab !== 'undefined' && currentTab === 'settings') renderSettings(); alert('Das Passwort wurde geändert.'); } // checkAuthAndInit: prüft per GET /whoami, ob eine gültige Session besteht. // Falls ja, wird currentUser gesetzt, das Overlay ausgeblendet und der // übergebene Callback (der eigentliche Seiteneinstieg) ausgeführt. Falls // nein, wird das Login-Overlay gezeigt und das Login-Formular gebunden; nach // erfolgreichem Login läuft derselbe Callback an. Parameter: onAuthenticated // (Funktion ohne Argumente). async function checkAuthAndInit(onAuthenticated){ let data = null; try { const res = await fetch(baseUrl() + '/whoami'); data = await res.json(); } catch(err){ // Ohne diesen Zweig blieb bei nicht erreichbarem Server (oder einer // HTML-Antwort einer Zwischeninstanz) eine leere, tote Seite stehen: Das // Promise wurde abgelehnt, das Login-Fenster nie gezeigt. console.error('Anmeldestatus konnte nicht geprüft werden:', err); alert('Der Server unter ' + baseUrl() + ' ist nicht erreichbar.\n\n' + 'Bitte prüfen, ob er läuft, und die Seite anschließend neu laden.'); return; } if(data && data.user){ currentUser = data.user; afterAuthenticated(onAuthenticated); return; } showLoginOverlay(); const form = document.getElementById('loginForm'); const errEl = document.getElementById('loginError'); form.onsubmit = async function(ev){ ev.preventDefault(); errEl.textContent = ''; const username = document.getElementById('loginUsername').value; const password = document.getElementById('loginPassword').value; try { const r = await fetch(baseUrl() + '/login', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ username, password }) }); const d = await r.json(); if(!r.ok || !d.ok){ errEl.textContent = d.error || 'Anmeldung fehlgeschlagen.'; return; } currentUser = d.user; afterAuthenticated(onAuthenticated); } catch(err){ errEl.textContent = 'Verbindung zum Server fehlgeschlagen.'; } }; }