diff --git a/.env.example b/.env.example index cc6820d..0993bad 100644 --- a/.env.example +++ b/.env.example @@ -6,5 +6,8 @@ NEXTCLOUD_APP_PASSWORD= # optional App-Passwort aus Nextcloud → Einstellung NEXTCLOUD_CALENDAR=morning-meeting # Nur setzen, wenn die Nextcloud ein selbstsigniertes Zertifikat hat NEXTCLOUD_INSECURE_TLS=false -# Optionaler Zugriffsschutz der App (HTTP Basic Auth). Leer = kein Passwort. +# Bearbeitungspasswort: Betrachter sehen nur den Kalender, Auslosen und Verwalten erfordern die Anmeldung. +# Leer = jeder darf alles bearbeiten. +EDIT_PASSWORD="" +# Optionaler Zugriffsschutz der ganzen App (HTTP Basic Auth, auch fürs Ansehen). Leer = kein Passwort. APP_PASSWORD="" diff --git a/README.md b/README.md index 8a83389..0b207a9 100644 --- a/README.md +++ b/README.md @@ -63,11 +63,21 @@ Alle Variablen sind optional, siehe `.env.example`: | `NEXTCLOUD_APP_PASSWORD` | App-Passwort (Nextcloud → Einstellungen → Sicherheit → Geräte & Sitzungen) | | `NEXTCLOUD_CALENDAR` | ID des eigenen Kalenders, falls kein vorhandener Zielkalender gewählt wird (Vorgabe `morning-meeting`) | | `NEXTCLOUD_INSECURE_TLS` | `true`, wenn die Nextcloud ein selbstsigniertes Zertifikat nutzt | -| `APP_PASSWORD` | Wenn gesetzt, schützt HTTP Basic Auth die ganze App (Benutzername beliebig) | +| `EDIT_PASSWORD` | Bearbeitungspasswort. Wenn gesetzt, sehen alle nur den Kalender; Auslosen, Personen, Abwesenheiten, Einstellungen und Nextcloud erfordern die Anmeldung über „Anmelden" in der Kopfzeile | +| `APP_PASSWORD` | Wenn gesetzt, schützt HTTP Basic Auth die ganze App inklusive Ansehen (Benutzername beliebig) | Ohne Nextcloud-Variablen läuft die App vollständig, nur der Button „Go – schreiben" ist deaktiviert; der ICS-Download bleibt verfügbar. +## Teilen im Team + +Die Adresse der App kann an alle weitergegeben werden. Mit gesetztem `EDIT_PASSWORD` sehen +Besucher den Monatskalender mit Fotos und die Einsatzstatistik, aber keine E-Mail-Adressen, +Notizen oder Abwesenheiten, und sie können nichts ändern. Wer bearbeiten will, klickt oben rechts +auf „Anmelden". Die Anmeldung gilt 30 Tage im jeweiligen Browser; nach fünf falschen Versuchen +wartet der Server eine Minute. Ohne `EDIT_PASSWORD` darf jeder alles, wie bei einem Werkzeug +für eine einzelne Person. + ## Nextcloud 1. Umgebungsvariablen setzen und App neu starten. diff --git a/public/app.js b/public/app.js index 66bb51b..7089d7f 100644 --- a/public/app.js +++ b/public/app.js @@ -43,10 +43,51 @@ async function api(path, { method = 'GET', body, raw } = {}) { const res = await fetch(path, opts); if (res.status === 204) return null; const data = await res.json().catch(() => ({})); - if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`); + if (!res.ok) { + if (data.authRequired) { openLogin(); throw new Error('Zum Bearbeiten bitte anmelden.'); } + throw new Error(data.error || `HTTP ${res.status}`); + } return data; } +// ---------- Anmeldung zum Bearbeiten ---------- +const EDITOR_VIEWS = ['personen', 'abwesenheiten', 'einstellungen']; +function isEditor() { return !state.data || state.data.auth?.editor !== false; } +function applyAuthUI() { + const a = state.data?.auth || { protected: false, editor: true }; + document.body.classList.toggle('viewer', !a.editor); + const btn = $('#auth-button'); + btn.hidden = !a.protected; + btn.textContent = a.editor ? 'Abmelden' : 'Anmelden'; + for (const link of $$('.topbar nav a')) link.hidden = !a.editor && EDITOR_VIEWS.includes(link.dataset.view); + $('#open-plan').hidden = !a.editor; + $('#open-nextcloud').hidden = !a.editor; + if (!a.editor && EDITOR_VIEWS.includes(location.hash.slice(1))) location.hash = '#kalender'; +} +function openLogin() { + const dlg = $('#login-dialog'); + if (dlg.open) return; + $('#login-result').hidden = true; + $('#login-password').value = ''; + dlg.showModal(); +} +$('#auth-button').addEventListener('click', async () => { + if (isEditor()) { + try { await api('api/auth/logout', { method: 'POST' }); await load(); toast('Abgemeldet. Du siehst jetzt die Ansicht für das Team.'); } catch (err) { toast(err.message, true); } + } else openLogin(); +}); +$('#login-close').addEventListener('click', () => $('#login-dialog').close()); +$('#login-form').addEventListener('submit', async (e) => { + e.preventDefault(); + const out = $('#login-result'); + try { + await api('api/auth/login', { method: 'POST', body: { password: $('#login-password').value } }); + $('#login-dialog').close(); + await load(); + toast('Angemeldet. Bearbeiten ist freigeschaltet.'); + } catch (err) { out.textContent = err.message; out.hidden = false; } +}); + async function load() { state.data = await api('api/state'); renderAll(); @@ -100,6 +141,7 @@ function avatarEl(p, cls = '') { function showView(name) { const known = ['kalender', 'personen', 'abwesenheiten', 'einstellungen']; if (!known.includes(name)) name = 'kalender'; + if (!isEditor() && EDITOR_VIEWS.includes(name)) name = 'kalender'; if (name === 'abwesenheiten' && state.data) prepareAbsenceImport(); for (const v of $$('.view')) v.hidden = v.id !== `view-${name}`; for (const a of $$('.topbar nav a')) { @@ -128,9 +170,10 @@ function renderCalendar() { if (cur.getDay() === 0 || cur.getDay() === 6) continue; const date = iso(cur); const outside = cur.getMonth() !== d.getMonth(); - const cell = document.createElement(outside ? 'div' : 'button'); + const editable = !outside && isEditor(); + const cell = document.createElement(editable ? 'button' : 'div'); cell.className = 'day'; - if (!outside) { cell.type = 'button'; cell.dataset.date = date; } + if (editable) { cell.type = 'button'; cell.dataset.date = date; } if (outside) cell.classList.add('outside'); if (date === today) cell.classList.add('today'); if (date < today) cell.classList.add('past'); @@ -584,7 +627,7 @@ function imageFromClipboard(data) { } // Strg+V überall in der Personenansicht: Bild aus der Zwischenablage landet im Formular document.addEventListener('paste', (e) => { - if ($('#view-personen').hidden || !state.data) return; + if ($('#view-personen').hidden || !state.data || !isEditor()) return; const file = imageFromClipboard(e.clipboardData); if (!file) return; e.preventDefault(); @@ -920,13 +963,16 @@ function renderSettings() { list.appendChild(li); } + const protectedHint = state.data.auth?.protected + ? '
Bearbeitung ist durch das Passwort aus EDIT_PASSWORD geschützt. Das Team sieht nur den Kalender.
Kein Bearbeitungspasswort gesetzt: Jeder, der die Seite öffnet, kann auch bearbeiten. Zum Schützen EDIT_PASSWORD als Umgebungsvariable setzen.
Konfiguriert: ${escapeHtml(nc.baseUrl)}, angemeldet als ${escapeHtml(nc.user)}.
Zielkalender: ${s.targetCalendarHref ? escapeHtml(s.targetCalendarName || s.targetCalendarHref) : `neuer Kalender „${escapeHtml(s.calendarDisplayName)}" (${escapeHtml(nc.calendar)})`} – wählbar im Dialog „Nextcloud …" im Kalender.
` + `Quellkalender für Abwesenheiten: ${s.absenceCalendarHref ? escapeHtml(s.absenceCalendarName || s.absenceCalendarHref) : 'noch keiner'} – wählbar unter „Abwesenheiten".
` - : `Nicht konfiguriert.
Umgebungsvariablen setzen: NEXTCLOUD_URL, NEXTCLOUD_USER, NEXTCLOUD_APP_PASSWORD (App-Passwort aus Nextcloud → Einstellungen → Sicherheit), optional NEXTCLOUD_CALENDAR. Bis dahin steht der ICS-Download zur Verfügung.
Nicht konfiguriert.
Umgebungsvariablen setzen: NEXTCLOUD_URL, NEXTCLOUD_USER, NEXTCLOUD_APP_PASSWORD (App-Passwort aus Nextcloud → Einstellungen → Sicherheit), optional NEXTCLOUD_CALENDAR. Bis dahin steht der ICS-Download zur Verfügung.