alle new
This commit is contained in:
erik committed 2026-10-01 06:46:51 +02:00
1 parent da64472251
commit 5b20adee48
1605 files changed
+396517 -1674

No files matched your search

+196
View File
@@ -0,0 +1,196 @@
// -----------------------------
// Projektübersicht — Authentifizierung (Login-Overlay, Session, Logout)
// -----------------------------
// Wird von Test.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 Test.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' : '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.';
}
};
}