Workspace: Aufgaben bearbeiten, Personenverwaltung im Browser
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.
This commit is contained in:
@@ -0,0 +1,202 @@
|
||||
/* ===================================================================
|
||||
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();
|
||||
})();
|
||||
})();
|
||||
Reference in New Issue
Block a user