feat(frontend): Epic 4 — Login-Overlay, Bootstrap-Gate (offline≠401), Sync-Auth, Nutzerwechsel-Isolation, Admin-UI, Logout
This commit is contained in:
parent
66717014d0
commit
8d51b50f9a
5 changed files with 172 additions and 6 deletions
33
app.js
33
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();
|
||||
}
|
||||
});
|
||||
|
|
|
|||
112
auth-ui.js
Normal file
112
auth-ui.js
Normal file
|
|
@ -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;
|
||||
25
index.html
25
index.html
|
|
@ -15,6 +15,17 @@
|
|||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="login-overlay" hidden style="position:fixed;inset:0;z-index:10000;background:linear-gradient(135deg,#667eea,#764ba2);display:flex;align-items:center;justify-content:center">
|
||||
<div style="background:#fff;padding:2rem;border-radius:12px;box-shadow:0 10px 40px rgba(0,0,0,.2);max-width:360px;width:90%;text-align:center">
|
||||
<h1 style="font-size:1.4rem;margin:0 0 .5rem">Dienstplan-Pro</h1>
|
||||
<p style="color:#555;font-size:.9rem">Melde dich mit deiner freigeschalteten Arbeits-E-Mail an — wir schicken dir einen Login-Link.</p>
|
||||
<form id="login-form" style="margin-top:1rem;display:flex;flex-direction:column;gap:.75rem">
|
||||
<input type="email" id="login-email" placeholder="E-Mail" required style="padding:.75rem;border:1px solid #ccc;border-radius:8px;font-size:1rem">
|
||||
<button type="submit" class="btn btn-primary">Login-Link anfordern</button>
|
||||
</form>
|
||||
<p id="login-message" hidden style="color:#1e7e34;margin-top:1rem;font-size:.9rem">Prüfe dein Postfach und klicke den Link.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container">
|
||||
<header>
|
||||
<h1>Dienstplan Pro</h1>
|
||||
|
|
@ -166,6 +177,19 @@
|
|||
<div class="card">
|
||||
<h2>Einstellungen & Daten</h2>
|
||||
|
||||
<div class="settings-section" id="account-section">
|
||||
<h3>Konto & Team</h3>
|
||||
<button id="logout-btn" class="btn btn-secondary">Abmelden</button>
|
||||
<div id="admin-section" hidden style="margin-top:12px">
|
||||
<h4>Nutzer verwalten (Admin)</h4>
|
||||
<form id="admin-add-form" style="display:flex;gap:8px;margin:8px 0;flex-wrap:wrap">
|
||||
<input type="email" id="admin-add-email" placeholder="E-Mail freischalten" required style="flex:1;min-width:180px;padding:.5rem;border:1px solid #ccc;border-radius:6px">
|
||||
<button type="submit" class="btn btn-primary">Freischalten</button>
|
||||
</form>
|
||||
<div id="admin-user-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-section">
|
||||
<h3>Berechnungsregeln (NRW Psychiatrie 2011)</h3>
|
||||
<div class="info-box">
|
||||
|
|
@ -327,6 +351,7 @@
|
|||
<script src="calculator.js"></script>
|
||||
<script src="storage.js"></script>
|
||||
<script src="sync.js"></script>
|
||||
<script src="auth-ui.js"></script>
|
||||
<script src="app.js"></script>
|
||||
<script src="image-import.js"></script>
|
||||
</body>
|
||||
|
|
|
|||
3
sw.js
3
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'
|
||||
];
|
||||
|
||||
|
|
|
|||
5
sync.js
5
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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue