// -----------------------------
// 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 = systemRolleText(currentUser.role);
info.textContent = (currentUser.displayName || currentUser.username) + ' (' + roleLabel + ')';
}
const navAdminSection = document.getElementById('navAdminSection');
if(navAdminSection) navAdminSection.style.display = (currentUser && currentUser.role === 'admin') ? '' : 'none';
document.querySelectorAll('.nur-projektleitung').forEach(el => { el.hidden = !darfProjektlisteAendern(); });
}
// systemRolleText: Anzeigename einer Systemrolle (users.role). Auch von der
// Benutzerverwaltung (js/admin-users.js) genutzt.
function systemRolleText(role){
return role === 'admin' ? 'Administrator'
: role === 'projektleiter' ? 'Projektleiter'
: role === 'kiosk' ? 'Kiosk' : 'Mitglied';
}
// darfProjektlisteAendern: ob das angemeldete Konto Projekte anlegen und aus
// der Projektliste löschen darf (Projektleiter und Administratoren). Reines
// UI-Gate — dieselbe Regel setzt /save-week serverseitig durch
// (darfProjektlisteAendern() in server.js), dort mit 403. Knöpfe zum
// Anlegen tragen die Klasse .nur-projektleitung und werden von
// renderUserBar() ausgeblendet.
function darfProjektlisteAendern(){
return !!currentUser && (currentUser.role === 'admin' || currentUser.role === 'projektleiter');
}
// 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 die blockierende Login-Seite ein/aus.
function showLoginOverlay(){
renderLoginWoche();
document.getElementById('loginOverlay').style.display = 'flex';
const feld = document.getElementById('loginUsername');
if(feld && !feld.value) feld.focus();
}
// renderLoginWoche: füllt die rechte Seite der Login-Seite — laufende
// Kalenderwoche (getWeekNumber(), js/utils.js), Jahr der ISO-Woche, die
// Arbeitstage Mo-Fr mit dem heutigen Tag markiert und das Datum.
function renderLoginWoche(){
const heute = new Date();
const kw = getWeekNumber(heute);
// ISO-Jahr: das Jahr des Donnerstags dieser Woche (Ende Dezember/Anfang
// Januar weicht es vom Kalenderjahr ab, z. B. KW 01 / 2027 am 30.12.2026).
const donnerstag = new Date(heute); donnerstag.setDate(heute.getDate() + 3 - ((heute.getDay() + 6) % 7));
const zahl = document.getElementById('loginKwZahl');
if(!zahl) return;
zahl.textContent = String(kw).padStart(2, '0');
document.getElementById('loginKwJahr').textContent = donnerstag.getFullYear();
document.getElementById('loginDatum').textContent =
heute.toLocaleDateString('de-DE', { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' });
const montag = new Date(heute); montag.setDate(heute.getDate() - ((heute.getDay() + 6) % 7));
document.getElementById('loginTage').innerHTML = ['Mo', 'Di', 'Mi', 'Do', 'Fr'].map((n, i) => {
const d = new Date(montag); d.setDate(montag.getDate() + i);
const istHeute = d.toDateString() === heute.toDateString();
return '
' + n + '' + d.getDate() + '';
}).join('');
}
// verdrahteLoginHilfen: „Anzeigen" für das Passwort und der Hinweis auf
// eine aktive Feststelltaste. Einmalig.
let loginHilfenVerdrahtet = false;
function verdrahteLoginHilfen(){
if(loginHilfenVerdrahtet) return;
loginHilfenVerdrahtet = true;
const pw = document.getElementById('loginPassword');
const toggle = document.getElementById('loginPwToggle');
const caps = document.getElementById('loginCaps');
if(toggle) toggle.addEventListener('click', () => {
const sichtbar = pw.type === 'text';
pw.type = sichtbar ? 'password' : 'text';
toggle.textContent = sichtbar ? 'Anzeigen' : 'Verbergen';
toggle.setAttribute('aria-pressed', String(!sichtbar));
pw.focus();
});
const capsPruefen = e => { if(caps && e.getModifierState) caps.hidden = !e.getModifierState('CapsLock'); };
pw.addEventListener('keydown', capsPruefen);
pw.addEventListener('keyup', capsPruefen);
pw.addEventListener('blur', () => { if(caps) caps.hidden = true; });
['loginUsername', 'loginPassword'].forEach(id => {
document.getElementById(id).addEventListener('input', e => {
e.target.closest('.login-feld').classList.remove('hat-fehler');
document.getElementById('loginError').hidden = true;
});
});
}
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();
verdrahteLoginHilfen();
const form = document.getElementById('loginForm');
const errEl = document.getElementById('loginError');
const btn = document.getElementById('loginSubmit');
const zeigeFehler = (text, feldId) => {
errEl.textContent = text;
errEl.hidden = false;
if(feldId){
const feld = document.getElementById(feldId);
feld.closest('.login-feld').classList.add('hat-fehler');
feld.focus();
}
};
form.onsubmit = async function(ev){
ev.preventDefault();
errEl.hidden = true;
const username = document.getElementById('loginUsername').value.trim();
const password = document.getElementById('loginPassword').value;
if(!username) return zeigeFehler('Bitte den Benutzernamen eingeben, z. B. m.mustermann.', 'loginUsername');
if(!password) return zeigeFehler('Bitte das Passwort eingeben.', 'loginPassword');
btn.disabled = true; btn.classList.add('laedt');
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){
zeigeFehler(d.error || 'Anmeldung fehlgeschlagen.', 'loginPassword');
document.getElementById('loginPassword').select();
return;
}
currentUser = d.user;
document.getElementById('loginPassword').value = '';
afterAuthenticated(onAuthenticated);
} catch(err){
zeigeFehler('Der Server ist nicht erreichbar. Bitte später erneut versuchen.');
} finally {
btn.disabled = false; btn.classList.remove('laedt');
}
};
}