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));
|
||||
@@ -18,6 +18,7 @@
|
||||
<a href="#abwesenheiten" data-view="abwesenheiten">Abwesenheiten</a>
|
||||
<a href="#einstellungen" data-view="einstellungen">Einstellungen</a>
|
||||
</nav>
|
||||
<button type="button" id="auth-button" class="topbar-action" hidden>Anmelden</button>
|
||||
</header>
|
||||
|
||||
<main id="main">
|
||||
@@ -255,6 +256,21 @@
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<!-- Dialog: Anmelden zum Bearbeiten -->
|
||||
<dialog id="login-dialog">
|
||||
<form method="dialog" class="dialog-form" id="login-form">
|
||||
<h2>Zum Bearbeiten anmelden</h2>
|
||||
<p class="hint">Ansehen kann jeder. Auslosen, Personen, Abwesenheiten und Nextcloud erfordern das Bearbeitungspasswort.</p>
|
||||
<label>Passwort <input type="password" name="password" id="login-password" autocomplete="current-password" required></label>
|
||||
<p id="login-result" class="result error" hidden></p>
|
||||
<div class="form-actions">
|
||||
<span class="spacer"></span>
|
||||
<button type="button" class="ghost" id="login-close">Abbrechen</button>
|
||||
<button type="submit" class="primary">Anmelden</button>
|
||||
</div>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<div id="toast" class="toast" role="status" aria-live="polite" hidden></div>
|
||||
<script type="module" src="app.js"></script>
|
||||
</body>
|
||||
|
||||
@@ -59,12 +59,19 @@ svg.icon { width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-w
|
||||
.topbar nav a:hover { color: var(--white); }
|
||||
.topbar nav a[aria-current="page"] { color: var(--white); border-bottom-color: var(--white); }
|
||||
.topbar nav a:focus-visible { outline: 2px solid var(--white); outline-offset: -4px; }
|
||||
.topbar-action { margin-left: auto; background: transparent; color: var(--white); border-color: rgba(255, 255, 255, .4); min-height: 34px; padding: 0 12px; font-size: 13px; }
|
||||
.topbar-action:hover { background: rgba(255, 255, 255, .1); }
|
||||
.topbar-action:focus-visible { outline: 2px solid var(--white); outline-offset: 2px; }
|
||||
/* Betrachtermodus: Kalender ansehen, nichts bearbeiten */
|
||||
.viewer .day { cursor: default; }
|
||||
.viewer .day:hover:not(.outside) { background: var(--white); }
|
||||
@media (max-width: 560px) {
|
||||
.topbar { padding: 0 16px; gap: 0 16px; }
|
||||
.topbar h1 { padding: 12px 0; }
|
||||
.topbar nav { flex-wrap: nowrap; overflow-x: auto; width: 100%; margin: 0 -16px; padding: 0 8px; scrollbar-width: none; }
|
||||
.topbar nav::-webkit-scrollbar { display: none; }
|
||||
.topbar nav a { min-height: 44px; padding: 0 10px; font-size: 13px; white-space: nowrap; }
|
||||
.topbar-action { margin-left: auto; margin-block: 8px; }
|
||||
}
|
||||
button:focus-visible, input:focus-visible, select:focus-visible, .button:focus-visible, .day:focus-visible, .avatar[tabindex]:focus-visible {
|
||||
outline: 2px solid var(--tradeo-blue); outline-offset: 2px;
|
||||
|
||||
Reference in new issue
Block a user