From 8f5517a4090f24d656bf203ac82faef50b8a7c40 Mon Sep 17 00:00:00 2001 From: Kerim Atilgan Date: Sun, 27 Sep 2026 12:21:35 +0200 Subject: [PATCH 1/2] Portal-Kopfleiste einbinden Laedt die zentrale Handy-Insel-Kopfleiste (topbar.js, data-app="timer") und blendet sie beim Drucken aus. In den Proxy-Beispielen ist dokumentiert, welche Quellen eine spaetere Content-Security-Policy erlauben muss. Co-Authored-By: Claude Opus 5.5 (1M context) --- deploy/Caddyfile.example | 7 +++++++ deploy/nginx.conf.example | 7 +++++++ frontend/index.html | 11 +++++++++++ 3 files changed, 25 insertions(+) diff --git a/deploy/Caddyfile.example b/deploy/Caddyfile.example index 766ca92..219c1eb 100644 --- a/deploy/Caddyfile.example +++ b/deploy/Caddyfile.example @@ -9,6 +9,13 @@ timer.firma.example { X-Frame-Options "SAMEORIGIN" X-Content-Type-Options "nosniff" Referrer-Policy "no-referrer" + # Die Seite laedt die Portal-Kopfleiste von portal.handy-insel.de. + # Wer hier eine Content-Security-Policy setzt, muss diese Quellen erlauben + # (sonst blockiert der Browser die Leiste), jeweils zusaetzlich zu den + # vorhandenen Quellen (die App nutzt Inline-Styles und onclick-Handler): + # script-src ... https://portal.handy-insel.de + # connect-src ... https://portal.handy-insel.de + # img-src ... https://portal.handy-insel.de https://login.handy-insel.de } reverse_proxy 127.0.0.1:3003 { diff --git a/deploy/nginx.conf.example b/deploy/nginx.conf.example index ab5da89..54178ea 100644 --- a/deploy/nginx.conf.example +++ b/deploy/nginx.conf.example @@ -22,6 +22,13 @@ server { add_header X-Frame-Options "SAMEORIGIN" always; add_header X-Content-Type-Options "nosniff" always; add_header Referrer-Policy "no-referrer" always; + # Die Seite laedt die Portal-Kopfleiste von portal.handy-insel.de. + # Wer hier eine Content-Security-Policy setzt, muss diese Quellen erlauben + # (sonst blockiert der Browser die Leiste), jeweils zusaetzlich zu den + # vorhandenen Quellen (die App nutzt Inline-Styles und onclick-Handler): + # script-src ... https://portal.handy-insel.de + # connect-src ... https://portal.handy-insel.de + # img-src ... https://portal.handy-insel.de https://login.handy-insel.de # CSV-Export darf auch bei vielen Einträgen dauern client_max_body_size 1m; diff --git a/frontend/index.html b/frontend/index.html index d48c3da..d6d96b2 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -5,6 +5,9 @@ Timer - Zeiterfassung + + -- 2.54.0 From 0ee9b708a1e1d29fa3506a2039c4ccc1f3c068b5 Mon Sep 17 00:00:00 2001 From: Kerim Atilgan Date: Sun, 25 Oct 2026 13:12:20 +0100 Subject: [PATCH 2/2] =?UTF-8?q?Kopfleiste:=20Benutzer-Men=C3=BC=20und=20ge?= =?UTF-8?q?meinsames=20Abmelden?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Benutzername und "Abmelden" im eigenen Header entfallen, nur noch Rückfall, wenn topbar.js nicht lädt - Sitzungsanzeige als slot="benutzer" im Benutzer-Menü der Leiste - hi-abmelden: POST /api/logout beendet die Timer-Sitzung, laufende Timer fragen vorher nach (Abbrechen stoppt das ganze Abmelden) - html data-theme="dark": die Leiste ist dunkel wie die App, auch bei hellem System - README/DEPLOY: Abmelden, Launch-URL /auth/login, CSP-Hinweis Co-Authored-By: Claude Opus 5.5 --- DEPLOY.md | 17 ++++++++++++++ README.md | 9 +++++-- backend/server.js | 9 +++++-- frontend/app.js | 57 +++++++++++++++++++++++++++++++++++++++------ frontend/index.html | 16 ++++++++++--- 5 files changed, 94 insertions(+), 14 deletions(-) diff --git a/DEPLOY.md b/DEPLOY.md index 2ac78c2..a406555 100644 --- a/DEPLOY.md +++ b/DEPLOY.md @@ -265,6 +265,23 @@ Wichtig in beiden Fällen: * `/health` ist der Endpunkt für Health-Checks (antwortet **ohne** Anmeldung) * Websockets werden **nicht** gebraucht +### Portal-Kopfleiste + +`frontend/index.html` lädt die zentrale Handy-Insel-Kopfleiste +(`https://portal.handy-insel.de/topbar.js`, `data-app="timer"`). Sie bringt +App-Umschalter und Benutzer-Menü mit; Benutzername und „Abmelden“ der App +stehen deshalb dort, die eigenen im Header erscheinen nur, wenn das Skript +nicht lädt. + +* **Abmelden** in der Leiste ruft vorher `POST /api/logout` auf (Ereignis + `hi-abmelden`) und meldet danach bei Portal und Authentik ab. +* **Launch-URL** der Kachel im Portal: `https://timer.handy-insel.de/auth/login` + (startet direkt die Anmeldung über Authentik). +* **Content-Security-Policy:** Die App setzt keine. Kommt im Proxy eine dazu, + muss sie `script-src`/`connect-src https://portal.handy-insel.de` und + `img-src https://portal.handy-insel.de https://login.handy-insel.de` + erlauben (Details in den Proxy-Beispielen). + ### Bondrucker Vollständige Einrichtung: **`deploy/bondrucker/README.md`**. Hier nur das diff --git a/README.md b/README.md index ee2e17e..20099ed 100644 --- a/README.md +++ b/README.md @@ -153,5 +153,10 @@ der Bondrucker hängt am USB-Anschluss. ## Abmelden -**🚪 Abmelden** meldet nur dieses Gerät ab. Laufende Timer bleiben davon -unberührt und laufen weiter. +Ganz oben steht die Handy-Insel-Leiste. Über den Avatar rechts öffnet sich das +Benutzer-Menü, dort steht **Abmelden**. Das meldet dieses Gerät beim Timer, im +Portal und bei Authentik ab. Laufende Timer bleiben davon unberührt und laufen +weiter. Laufen noch welche, fragt die App vorher nach. + +Ist das Portal einmal nicht erreichbar, fehlt die Leiste. Dann steht +**🚪 Abmelden** wie früher oben rechts in der App. diff --git a/backend/server.js b/backend/server.js index 4edb9d8..3ccb8ae 100644 --- a/backend/server.js +++ b/backend/server.js @@ -487,9 +487,14 @@ app.get('/auth/logout', (req, res) => { }); }); -// Passwort-Logout (Fallback, wird per fetch aufgerufen) +// Logout per fetch: beendet nur die lokale Sitzung. Aufgerufen von der +// Portal-Kopfleiste (Ereignis "hi-abmelden"), die danach selbst bei Portal +// und Authentik abmeldet. app.post('/api/logout', (req, res) => { - req.session.destroy(() => res.json({ success: true })); + req.session.destroy(() => { + res.clearCookie('connect.sid'); + res.json({ success: true }); + }); }); // --------------------------------------------------------------------------- diff --git a/frontend/app.js b/frontend/app.js index 3bf3954..0c451e6 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -161,28 +161,71 @@ async function enterApp() { startLoops(); } -// Zeigt den angemeldeten Authentik-Nutzer oben rechts im Header. +// Zeigt den angemeldeten Authentik-Nutzer: im Benutzer-Menue der +// Portal-Kopfleiste, ohne Leiste oben rechts im eigenen Header. function renderCurrentUser() { - const el = document.getElementById('currentUser'); - if (!el) return; const u = loggedInUser; - el.textContent = u ? (u.displayName || u.username || '') : ''; + const name = u ? (u.displayName || u.username || '') : ''; + const el = document.getElementById('currentUser'); + el.textContent = name; el.title = u && u.email ? u.email : 'Angemeldet'; + renderTopbarSession(name); } -async function logout() { +function confirmLogout() { const running = state.timers.filter(t => t.isRunning).length; - if (running > 0 && !confirm( + return running === 0 || confirm( `${running} Timer ${running === 1 ? 'läuft' : 'laufen'} noch.\n\n` + `Sie laufen serverseitig weiter und sind nach dem nächsten Anmelden wieder da. Trotzdem abmelden?` - )) return; + ); +} +// Eigener Abmelden-Knopf, nur sichtbar ohne Portal-Kopfleiste +async function logout() { + if (!confirmLogout()) return; stopLoops(); state.timers = []; // GET-Navigation: beendet lokale Sitzung und – bei Authentik-Login – auch die SSO-Sitzung window.location.href = '/auth/logout'; } +// --------------------------------------------------------------------------- +// Portal-Kopfleiste +// --------------------------------------------------------------------------- + +// topbar.js laeuft per defer erst nach diesem Skript, die Leiste steht also +// fruehestens ab DOMContentLoaded im DOM. Die Sitzungsanzeige wird einmal +// eingehaengt, sobald die Anmeldung steht, danach aendert sich nur ihr Text. +const topbarSession = document.createElement('div'); +topbarSession.slot = 'benutzer'; +topbarSession.className = 'topbar-session'; +topbarSession.append(Object.assign(document.createElement('span'), { textContent: 'Timer – angemeldet als' }), + document.createElement('strong')); +let topbarSessionName = null; // null = (noch) nicht angemeldet + +function renderTopbarSession(name) { + topbarSessionName = name || 'Angemeldet'; + topbarSession.lastChild.textContent = topbarSessionName; + if (!topbarSession.isConnected) document.querySelector('hi-topbar')?.append(topbarSession); +} + +document.addEventListener('DOMContentLoaded', () => { + if (topbarSessionName !== null) renderTopbarSession(topbarSessionName); +}); + +// "Abmelden" in der Leiste: erst die eigene Sitzung beenden, danach meldet +// die Leiste bei Portal und Authentik ab. Abbrechen bei laufenden Timern +// verhindert das ganze Abmelden. +document.addEventListener('hi-abmelden', (e) => { + if (!confirmLogout()) { + e.preventDefault(); + return; + } + stopLoops(); + state.timers = []; + e.detail.warten(fetch('/api/logout', { method: 'POST', credentials: 'same-origin' })); +}); + // --------------------------------------------------------------------------- // Timer-Synchronisation // --------------------------------------------------------------------------- diff --git a/frontend/index.html b/frontend/index.html index d6d96b2..224b86b 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1,5 +1,6 @@ - + + @@ -795,6 +796,15 @@ soll sie nicht erscheinen. */ hi-topbar { flex-shrink: 0; } @media print { hi-topbar { display: none !important; } } + + /* Benutzername und Abmelden sitzen im Benutzer-Menue der Leiste. Die + eigenen bleiben nur als Rueckfall, wenn topbar.js nicht laedt. */ + hi-topbar ~ .container #currentUser, + hi-topbar ~ .container #logoutBtn { display: none; } + + /* Sitzungsanzeige im Benutzer-Menue (slot="benutzer"), Schriftfarbe kommt von der Leiste */ + .topbar-session { padding: 10px 16px; font-size: 13px; line-height: 1.4; } + .topbar-session span { display: block; opacity: 0.7; } @@ -806,7 +816,7 @@ - + @@ -1019,6 +1029,6 @@
- + -- 2.54.0