Bearbeitung hinter ein Passwort legen, Ansehen für das Team freigeben

Mit EDIT_PASSWORD sehen Besucher nur den Kalender: keine E-Mails, Notizen,
Abwesenheiten oder Nextcloud-Details, keine schreibenden Aktionen. Anmelden
in der Kopfzeile setzt ein signiertes Cookie für 30 Tage; nach fünf
Fehlversuchen wartet der Server eine Minute. Ohne EDIT_PASSWORD bleibt
alles offen wie bisher.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
ferdi2goandClaude Fable 5.1 committed 2026-09-16 12:36:23 +00:00
1 parent 9ed59bcf56
commit 33a8adef0e
8 files changed
+288 -13

No files matched your search

+62 -11
View File
@@ -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
? '<p class="status-ok">Bearbeitung ist durch das Passwort aus <code>EDIT_PASSWORD</code> geschützt. Das Team sieht nur den Kalender.</p>'
: '<p class="status-warn">Kein Bearbeitungspasswort gesetzt: Jeder, der die Seite öffnet, kann auch bearbeiten. Zum Schützen <code>EDIT_PASSWORD</code> als Umgebungsvariable setzen.</p>';
const nc = state.data.nextcloud;
const box = $('#nextcloud-status');
box.innerHTML = nc.configured
box.innerHTML = protectedHint + (nc.configured
? `<p class="status-ok">Konfiguriert: <code>${escapeHtml(nc.baseUrl)}</code>, angemeldet als <code>${escapeHtml(nc.user)}</code>.</p>`
+ `<p>Zielkalender: <strong>${s.targetCalendarHref ? escapeHtml(s.targetCalendarName || s.targetCalendarHref) : `neuer Kalender „${escapeHtml(s.calendarDisplayName)}" (${escapeHtml(nc.calendar)})`}</strong> – wählbar im Dialog „Nextcloud …" im Kalender.</p>`
+ `<p>Quellkalender für Abwesenheiten: <strong>${s.absenceCalendarHref ? escapeHtml(s.absenceCalendarName || s.absenceCalendarHref) : 'noch keiner'}</strong> – wählbar unter „Abwesenheiten".</p>`
: `<p class="status-warn">Nicht konfiguriert.</p><p class="hint">Umgebungsvariablen setzen: <code>NEXTCLOUD_URL</code>, <code>NEXTCLOUD_USER</code>, <code>NEXTCLOUD_APP_PASSWORD</code> (App-Passwort aus Nextcloud → Einstellungen → Sicherheit), optional <code>NEXTCLOUD_CALENDAR</code>. Bis dahin steht der ICS-Download zur Verfügung.</p>`;
: `<p class="status-warn">Nicht konfiguriert.</p><p class="hint">Umgebungsvariablen setzen: <code>NEXTCLOUD_URL</code>, <code>NEXTCLOUD_USER</code>, <code>NEXTCLOUD_APP_PASSWORD</code> (App-Passwort aus Nextcloud → Einstellungen → Sicherheit), optional <code>NEXTCLOUD_CALENDAR</code>. Bis dahin steht der ICS-Download zur Verfügung.</p>`);
}
function escapeHtml(s) { return String(s).replace(/[&<>"]/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c])); }
@@ -962,12 +1008,17 @@ $('#holiday-add').addEventListener('submit', async (e) => {
// ---------- Start ----------
function renderAll() {
applyAuthUI();
renderCalendar();
renderPersons();
renderPhotoField();
renderAbsences();
renderSettings();
if (!$('#view-abwesenheiten').hidden) prepareAbsenceImport();
if (isEditor()) {
renderPersons();
renderPhotoField();
renderAbsences();
renderSettings();
if (!$('#view-abwesenheiten').hidden) prepareAbsenceImport();
} else {
showView(location.hash.slice(1));
}
}
showView(location.hash.slice(1));
load().catch((err) => toast(`Laden fehlgeschlagen: ${err.message}`, true));