diff --git a/app.js b/app.js index 04e1b3d..026b56c 100644 --- a/app.js +++ b/app.js @@ -1228,17 +1228,42 @@ class DienstplanApp { // Initialize app when DOM is ready let app; document.addEventListener('DOMContentLoaded', async () => { + // 1. Auth-Status. Offline-bewusst: 401 = ungültige Session → Login-Overlay; + // Netzwerkfehler (Server weg) = NICHT aussperren, App läuft lokal weiter (NFR-8). + let me = null; + try { + const res = await fetch('/api/auth/me', { credentials: 'include', cache: 'no-store' }); + if (res.status === 401) { + if (window.AuthUI) window.AuthUI.showLogin(); + return; // App nicht starten + } + if (res.ok) me = await res.json(); + } catch (e) { + console.warn('auth/me offline — App läuft lokal weiter:', e.message); + } + + // 2. Nutzerwechsel-Isolation: anderer Nutzer als zuletzt gemerkt → lokale Daten + Key leeren. + if (me && me.email) { + const stored = localStorage.getItem('dienstplan_current_user'); + if (stored && stored !== me.email && window.AuthUI) window.AuthUI.clearLocalData(); + localStorage.setItem('dienstplan_current_user', me.email); + } + + // 3. App starten + if (window.AuthUI) window.AuthUI.hideLogin(); if (window.DataSync) { try { await window.DataSync.boot(); } catch (e) { console.error('Sync-Boot fehlgeschlagen, App laeuft lokal weiter:', e); } } app = new DienstplanApp(); window.app = app; - - // Bild-Import hier erzeugen (nicht in einem eigenen DOMContentLoaded-Listener): - // window.app wird erst nach dem await oben gesetzt, ein paralleler Listener - // liefe da noch ohne app und wuerde den Importer nie anlegen. if (window.ImageImporter && !window.imageImporter) { window.imageImporter = new window.ImageImporter(app); } + + // 4. Konto/Admin-UI + if (window.AuthUI) { + window.AuthUI.wireLogout(); + if (me && me.isAdmin) window.AuthUI.showAdminSection(); + } }); diff --git a/auth-ui.js b/auth-ui.js new file mode 100644 index 0000000..15d308e --- /dev/null +++ b/auth-ui.js @@ -0,0 +1,112 @@ +/** + * AuthUI — Login-Overlay, Admin-Nutzerverwaltung, Logout, Nutzer-Isolation. + * Hält app.js schlank; alle fetch mit credentials. + */ +const AuthUI = { + // Bei Nutzerwechsel/Logout zu leerende Schlüssel: Daten + pending + gerätelokaler OpenRouter-Key. + KEYS_TO_CLEAR: [ + 'dienstplan_employees', 'dienstplan_duties', 'dienstplan_vacation', + 'dienstplan_sync_pending', 'dienstplan_openrouter_key', 'dienstplan_openrouter_model', + ], + + clearLocalData() { this.KEYS_TO_CLEAR.forEach(k => localStorage.removeItem(k)); }, + + showLogin() { + const ov = document.getElementById('login-overlay'); + const cont = document.querySelector('.container'); + if (ov) ov.hidden = false; + if (cont) cont.style.display = 'none'; + this.wireLoginForm(); + }, + hideLogin() { + const ov = document.getElementById('login-overlay'); + const cont = document.querySelector('.container'); + if (ov) ov.hidden = true; + if (cont) cont.style.display = ''; + }, + + wireLoginForm() { + const form = document.getElementById('login-form'); + if (!form || form._wired) return; + form._wired = true; + form.addEventListener('submit', async (e) => { + e.preventDefault(); + const email = (document.getElementById('login-email').value || '').trim(); + try { + await fetch('/api/auth/request', { + method: 'POST', headers: { 'Content-Type': 'application/json' }, + credentials: 'include', body: JSON.stringify({ email }), + }); + } catch { /* neutral bleiben */ } + const msg = document.getElementById('login-message'); + if (msg) msg.hidden = false; + const btn = form.querySelector('button'); + if (btn) btn.disabled = true; + }); + }, + + showAdminSection() { + const sec = document.getElementById('admin-section'); + if (sec) sec.hidden = false; + this.loadUsers(); + const addForm = document.getElementById('admin-add-form'); + if (addForm && !addForm._wired) { + addForm._wired = true; + addForm.addEventListener('submit', async (e) => { + e.preventDefault(); + const input = document.getElementById('admin-add-email'); + const email = (input.value || '').trim(); + await fetch('/api/admin/users', { + method: 'POST', headers: { 'Content-Type': 'application/json' }, + credentials: 'include', body: JSON.stringify({ email }), + }); + input.value = ''; + this.loadUsers(); + }); + } + }, + + async loadUsers() { + const list = document.getElementById('admin-user-list'); + if (!list) return; + try { + const res = await fetch('/api/admin/users', { credentials: 'include' }); + if (!res.ok) return; + const { users } = await res.json(); + list.innerHTML = ''; + users.forEach((u) => { + const row = document.createElement('div'); + row.className = 'admin-user-row'; + row.style.cssText = 'display:flex;justify-content:space-between;align-items:center;padding:4px 0;border-bottom:1px solid #eee'; + const span = document.createElement('span'); + span.textContent = u.email + (u.isAdmin ? ' (Admin)' : ''); + row.appendChild(span); + const btn = document.createElement('button'); + btn.className = 'btn btn-secondary'; + btn.textContent = 'Entfernen'; + btn.addEventListener('click', async () => { + if (!confirm(`Nutzer ${u.email} entfernen?`)) return; + const r = await fetch('/api/admin/users/' + u.id, { method: 'DELETE', credentials: 'include' }); + if (!r.ok) { const j = await r.json().catch(() => ({})); alert(j.error || 'Fehler beim Entfernen'); } + this.loadUsers(); + }); + row.appendChild(btn); + list.appendChild(row); + }); + } catch { /* ignorieren */ } + }, + + wireLogout() { + const btn = document.getElementById('logout-btn'); + if (!btn || btn._wired) return; + btn._wired = true; + btn.addEventListener('click', async () => { + try { await fetch('/api/auth/logout', { method: 'POST', credentials: 'include' }); } catch { /* egal */ } + this.clearLocalData(); + localStorage.removeItem('dienstplan_current_user'); + location.reload(); + }); + }, +}; + +if (typeof window !== 'undefined') window.AuthUI = AuthUI; diff --git a/index.html b/index.html index 369f8bc..ac4a137 100644 --- a/index.html +++ b/index.html @@ -15,6 +15,17 @@ +

Dienstplan Pro

@@ -166,6 +177,19 @@

Einstellungen & Daten

+
+

Konto & Team

+ + +
+

Berechnungsregeln (NRW Psychiatrie 2011)

@@ -327,6 +351,7 @@ + diff --git a/sw.js b/sw.js index 15fa624..4d9b2c1 100644 --- a/sw.js +++ b/sw.js @@ -1,4 +1,4 @@ -const CACHE_NAME = 'dienstplan-pro-v7'; +const CACHE_NAME = 'dienstplan-pro-v8'; const ASSETS = [ './', './index.html', @@ -9,6 +9,7 @@ const ASSETS = [ './holidays.js', './storage.js', './sync.js', + './auth-ui.js', './image-import.js' ]; diff --git a/sync.js b/sync.js index db59f37..5cda745 100644 --- a/sync.js +++ b/sync.js @@ -52,7 +52,8 @@ const DataSync = { async boot() { let server = null; try { - const res = await fetch('/api/state', { cache: 'no-store' }); + const res = await fetch('/api/state', { cache: 'no-store', credentials: 'include' }); + if (res.status === 401) { if (window.AuthUI) window.AuthUI.showLogin(); this.online = false; return; } if (res.ok) server = await res.json(); } catch { /* offline */ } @@ -78,8 +79,10 @@ const DataSync = { const res = await fetch('/api/state', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, + credentials: 'include', body: JSON.stringify(this._local()), }); + if (res.status === 401) { if (window.AuthUI) window.AuthUI.showLogin(); this.online = false; return; } if (!res.ok) throw new Error('HTTP ' + res.status); if (this._dirty === gen) localStorage.removeItem(this.KEY_PENDING); // nur wenn kein neuerer push() lief this.online = true;