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:
1 parent
9ed59bcf56
commit
33a8adef0e
8 files changed
+288
-13
No files matched your search
+62
-11
@@ -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) => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[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));
|
||||
Reference in new issue
Block a user