Aufgaben: - Bearbeiten-Dialog (Titel, Beschreibung, Prioritaet, Frist, Zuordnung). Als natives <dialog>: Fokusfang, Esc zum Schliessen und Abdunklung ohne eigenen Code. - Loeschen nur fuer Management, mit Rueckfrage und Protokolleintrag. Das Konzept will, dass Erledigtes stehen bleibt -- Loeschen ist der Ausnahmefall fuer Fehleintraege, nicht der normale Abschluss. Personen (/workspace/personen.html, nur Management): - Anlegen, Code erneuern, sperren/entsperren, Protokollansicht - Der Code wird genau einmal in der Antwort zurueckgegeben, nie gespeichert; beim Schliessen auch aus dem Dokument entfernt - Selbstschutz: niemand kann sich selbst sperren, und das letzte aktive Management laesst sich nicht sperren -- sonst kaeme niemand mehr hinein - Code fuer sich selbst tauschen nur mit ausdruecklicher Bestaetigung, weil es die eigene Sitzung sofort beendet Zwei Fehler, die beim Testen aufgefallen sind: 1. Rollenpruefung fehlte beim Ausliefern der Seiten. Ein Creator bekam personen.html mit HTTP 200 -- die Schnittstellen wiesen ihn zwar ab, das Geruest der Seite war aber sichtbar. GESCHUETZT ist jetzt eine Zuordnung Pfad -> erlaubte Rollen statt einer blossen Liste. 2. Das Protokoll nannte den falschen Verursacher. personAnlegen trug die NEU ANGELEGTE Person als person_id ein, der Eintrag las sich also so, als haette sie sich selbst angelegt. Akteur und Betroffener sind jetzt getrennt: Akteur in person_id, Betroffener im Text. Ueber die Kommandozeile angelegte Personen zeigen korrekt keinen Akteur.
203 lines
7.5 KiB
JavaScript
203 lines
7.5 KiB
JavaScript
/* ===================================================================
|
|
Personen und Zugangscodes verwalten. Nur für Management -- der Server
|
|
liefert allen anderen ohnehin 404 auf diese Schnittstellen.
|
|
=================================================================== */
|
|
(() => {
|
|
'use strict';
|
|
|
|
const $ = (id) => document.getElementById(id);
|
|
const ROLLENNAME = { admin: 'Management', creator: 'Creator', scout: 'Scout' };
|
|
let ich = null;
|
|
|
|
const el = (tag, klasse, text) => {
|
|
const k = document.createElement(tag);
|
|
if (klasse) k.className = klasse;
|
|
if (text !== undefined) k.textContent = text;
|
|
return k;
|
|
};
|
|
|
|
const melde = (t) => { $('fehler').textContent = t || ''; };
|
|
|
|
async function hole(pfad, optionen = {}) {
|
|
const a = await fetch(pfad, { credentials: 'same-origin', ...optionen });
|
|
if (a.status === 401) { location.assign('/workspace/'); throw new Error('abgemeldet'); }
|
|
if (a.status === 404 && pfad.includes('/verwaltung/')) {
|
|
location.assign('/workspace/start.html'); throw new Error('nicht erlaubt');
|
|
}
|
|
return a;
|
|
}
|
|
|
|
const datum = (iso) => (iso ? iso.slice(0, 16).replace('T', ' ') : '—');
|
|
|
|
/* ---------- Code anzeigen -------------------------------------------- */
|
|
|
|
function zeigeCode(p) {
|
|
$('code-name').textContent = `${p.name} · ${ROLLENNAME[p.rolle] || p.rolle}`;
|
|
$('code-wert').textContent = p.code;
|
|
$('code-kasten').hidden = false;
|
|
$('code-kasten').scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
}
|
|
|
|
$('code-schliessen').addEventListener('click', () => {
|
|
$('code-kasten').hidden = true;
|
|
/* Den Code auch aus dem Dokument entfernen, damit er nicht in einer
|
|
Bildschirmaufnahme oder im Verlauf hängen bleibt. */
|
|
$('code-wert').textContent = '';
|
|
});
|
|
|
|
$('code-kopieren').addEventListener('click', async () => {
|
|
try {
|
|
await navigator.clipboard.writeText($('code-wert').textContent);
|
|
$('code-kopieren').textContent = 'Kopiert ✓';
|
|
setTimeout(() => { $('code-kopieren').textContent = 'Kopieren'; }, 1800);
|
|
} catch {
|
|
melde('Kopieren ging nicht — bitte von Hand markieren.');
|
|
}
|
|
});
|
|
|
|
/* ---------- Liste ----------------------------------------------------- */
|
|
|
|
function zeile(p) {
|
|
const k = el('div', 'person');
|
|
k.dataset.aktiv = p.aktiv ? 'ja' : 'nein';
|
|
|
|
const links = el('div');
|
|
links.append(el('div', 'person__name', p.name + (p.id === ich.id ? ' (du)' : '')));
|
|
|
|
const z = el('div', 'person__zeile');
|
|
const rolle = el('span', 'marke-rolle', ROLLENNAME[p.rolle] || p.rolle);
|
|
rolle.dataset.rolle = p.rolle;
|
|
z.append(rolle);
|
|
if (!p.aktiv) z.append(el('span', null, 'gesperrt'));
|
|
z.append(el('span', null, `letzte Anmeldung: ${datum(p.letzter_login)}`));
|
|
z.append(el('span', null, `offene Aufgaben: ${p.offene_aufgaben}`));
|
|
if (p.sitzungen) z.append(el('span', null, `${p.sitzungen} offene Sitzung(en)`));
|
|
links.append(z);
|
|
|
|
const knoepfe = el('div', 'person__knoepfe');
|
|
knoepfe.append(tuKnopf('Neuer Code', async () => {
|
|
const eigen = p.id === ich.id;
|
|
const frage = eigen
|
|
? `Neuer Code für dich selbst?\n\nDeine aktuelle Sitzung wird sofort beendet und du musst dich mit dem neuen Code neu anmelden.`
|
|
: `Neuer Code für ${p.name}?\n\nDer alte Code gilt dann nicht mehr, offene Sitzungen werden beendet.`;
|
|
if (!confirm(frage)) return;
|
|
const a = await hole(`/workspace/api/verwaltung/personen/${p.id}/code`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(eigen ? { auch_mich: true } : {}),
|
|
});
|
|
if (!a.ok) { melde('Neuer Code hat nicht geklappt.'); return; }
|
|
zeigeCode(await a.json());
|
|
await laden();
|
|
}));
|
|
|
|
if (p.id !== ich.id) {
|
|
knoepfe.append(tuKnopf(p.aktiv ? 'Sperren' : 'Entsperren', async () => {
|
|
if (p.aktiv && !confirm(`${p.name} sperren?\n\nOffene Sitzungen werden sofort beendet.`)) return;
|
|
const a = await hole(`/workspace/api/verwaltung/personen/${p.id}`, {
|
|
method: 'PATCH',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ aktiv: !p.aktiv }),
|
|
});
|
|
if (!a.ok) { melde((await a.json().catch(() => ({}))).fehler || 'Ging nicht.'); return; }
|
|
await laden();
|
|
}));
|
|
}
|
|
|
|
k.append(links, knoepfe);
|
|
return k;
|
|
}
|
|
|
|
function tuKnopf(text, tu) {
|
|
const b = el('button', 'schritt', text);
|
|
b.type = 'button';
|
|
b.addEventListener('click', async () => {
|
|
b.disabled = true; melde('');
|
|
try { await tu(); } catch { /* umgeleitet */ } finally { b.disabled = false; }
|
|
});
|
|
return b;
|
|
}
|
|
|
|
async function laden() {
|
|
const ziel = $('liste');
|
|
try {
|
|
const a = await hole('/workspace/api/verwaltung/personen');
|
|
if (!a.ok) { melde('Personen konnten nicht geladen werden.'); return; }
|
|
const { personen } = await a.json();
|
|
ziel.textContent = '';
|
|
ziel.setAttribute('aria-busy', 'false');
|
|
for (const p of personen) ziel.append(zeile(p));
|
|
await protokollLaden();
|
|
} catch { /* umgeleitet */ }
|
|
}
|
|
|
|
async function protokollLaden() {
|
|
try {
|
|
const a = await hole('/workspace/api/verwaltung/protokoll?anzahl=25');
|
|
if (!a.ok) return;
|
|
const { eintraege } = await a.json();
|
|
const ziel = $('protokoll');
|
|
ziel.textContent = '';
|
|
for (const e of eintraege) {
|
|
const z = el('div', 'protokoll-zeile');
|
|
z.append(el('span', 'protokoll-zeit', datum(e.zeitpunkt)));
|
|
z.append(el('span', null, e.aktion));
|
|
z.append(el('span', null, [e.wer, e.detail].filter(Boolean).join(' · ')));
|
|
z.append(el('span', 'protokoll-ip', e.ip || ''));
|
|
ziel.append(z);
|
|
}
|
|
} catch { /* umgeleitet */ }
|
|
}
|
|
|
|
/* ---------- Anlegen ---------------------------------------------------- */
|
|
|
|
const umschalten = (auf) => {
|
|
$('neu').hidden = !auf; $('neu-oeffnen').hidden = auf;
|
|
if (auf) $('f-name').focus();
|
|
};
|
|
$('neu-oeffnen').addEventListener('click', () => umschalten(true));
|
|
$('neu-abbrechen').addEventListener('click', () => {
|
|
$('neu').reset(); $('neu-fehler').textContent = ''; umschalten(false);
|
|
});
|
|
|
|
$('neu').addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
$('neu-fehler').textContent = '';
|
|
const knopf = $('neu-speichern');
|
|
knopf.disabled = true;
|
|
try {
|
|
const a = await hole('/workspace/api/verwaltung/personen', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ name: $('f-name').value, rolle: $('f-rolle').value }),
|
|
});
|
|
if (!a.ok) {
|
|
$('neu-fehler').textContent = (await a.json().catch(() => ({}))).fehler || 'Ging nicht.';
|
|
return;
|
|
}
|
|
$('neu').reset(); umschalten(false);
|
|
zeigeCode(await a.json());
|
|
await laden();
|
|
} catch { /* umgeleitet */ } finally { knopf.disabled = false; }
|
|
});
|
|
|
|
/* ---------- Start ------------------------------------------------------- */
|
|
|
|
$('abmelden').addEventListener('click', async () => {
|
|
try { await fetch('/workspace/api/abmelden', { method: 'POST', credentials: 'same-origin' }); }
|
|
catch { /* egal */ }
|
|
location.assign('/workspace/');
|
|
});
|
|
|
|
(async () => {
|
|
try {
|
|
const a = await hole('/workspace/api/ich');
|
|
if (!a.ok) { location.assign('/workspace/'); return; }
|
|
ich = await a.json();
|
|
} catch { return; }
|
|
if (ich.rolle !== 'admin') { location.assign('/workspace/start.html'); return; }
|
|
$('wer').textContent = `${ich.name} · ${ich.rolle}`;
|
|
await laden();
|
|
})();
|
|
})();
|