/* ===================================================================== workspace-personen.js — Personen und Zugangscodes über den Browser verwalten. Ausschließlich für die Rolle "admin". Bisher ging das nur per SSH über workspace-code.js. Das bleibt als Notweg bestehen (etwa wenn niemand mehr hineinkommt), für den Alltag ist es aber zu umständlich. Zum Anzeigen des Codes im Browser: Der Code wird genau einmal in der Antwort auf das Anlegen zurückgegeben und danach nirgends gespeichert -- in der Datenbank steht nur sein scrypt-Hash. Das ist dasselbe Verfahren, das etwa GitHub für Zugriffstoken verwendet. Er läuft über HTTPS und landet weder im Protokoll noch im Serverlog. ===================================================================== */ import express from "express"; import { db, protokolliere, echteIp, sitzungLesen, personAnlegen, codeNeu, personSperren, betreuungSetzen, } from "./workspace.js"; export const personenRouter = express.Router(); const ROLLEN = ["admin", "creator", "scout"]; const NAME_MAX = 60; /* Nur Management. Alles andere bekommt 404 statt 403 -- wer nicht berechtigt ist, muss nicht erfahren, dass es diesen Bereich gibt. */ function nurAdmin(req, res, next) { const person = sitzungLesen(req); if (!person) return res.status(401).json({ fehler: "nicht_angemeldet" }); if (person.rolle !== "admin") return res.status(404).json({ fehler: "nicht_gefunden" }); req.person = person; next(); } function gleicheHerkunft(req, res, next) { const herkunft = req.get("origin"); if (!herkunft) return next(); let erlaubt; try { erlaubt = new URL(herkunft).host === req.get("host"); } catch { erlaubt = false; } if (!erlaubt) return res.status(403).json({ fehler: "fremde_herkunft" }); next(); } personenRouter.use("/workspace/api/verwaltung", nurAdmin); /* ---------- Übersicht --------------------------------------------------- */ personenRouter.get("/workspace/api/verwaltung/personen", (req, res) => { try { res.json({ personen: db().prepare(` SELECT p.id, p.name, p.rolle, p.aktiv, p.erstellt, p.letzter_login, (SELECT COUNT(*) FROM sitzungen s WHERE s.person_id = p.id) AS sitzungen, (SELECT COUNT(*) FROM aufgaben a WHERE (a.creator_id = p.id OR a.verantwortlich_id = p.id) AND a.status <> 'erledigt') AS offene_aufgaben, b.betreuer_id, (SELECT name FROM personen x WHERE x.id = b.betreuer_id) AS betreuer_name, (SELECT COUNT(*) FROM betreuung y WHERE y.betreuer_id = p.id) AS betreut_anzahl FROM personen p LEFT JOIN betreuung b ON b.creator_id = p.id ORDER BY p.aktiv DESC, p.rolle, p.name`).all(), /* Wer ueberhaupt als zustaendig eingetragen werden kann. */ betreuer: db().prepare( "SELECT id, name FROM personen WHERE rolle = 'scout' AND aktiv = 1 ORDER BY name").all(), }); } catch (fehler) { console.error("[workspace] Personen lesen:", fehler?.message); res.status(503).json({ fehler: "nicht_verfuegbar" }); } }); /* ---------- Zuständigkeit ------------------------------------------------ Wer betreut welchen Creator. Nur das Management setzt das -- sonst koennte sich ein Scout selbst Creator zuteilen und haette damit die Rechtevergabe in der Hand, die ihn eigentlich begrenzen soll. */ personenRouter.put("/workspace/api/verwaltung/betreuung/:id", gleicheHerkunft, (req, res) => { try { const id = Number(req.params.id); if (!Number.isInteger(id)) return res.status(400).json({ fehler: "ungueltig" }); const creator = db().prepare("SELECT id, rolle FROM personen WHERE id = ?").get(id); if (!creator || creator.rolle !== "creator") { return res.status(404).json({ fehler: "nicht_gefunden" }); } const w = req.body?.betreuer_id; if (w === null || w === "" || w === undefined) { betreuungSetzen(id, null, { ...req.person, ip: echteIp(req) }); return res.json({ ok: true, betreuer_id: null }); } const z = Number(w); if (!Number.isInteger(z) || z < 1) return res.status(400).json({ fehler: "Ungültige Auswahl." }); const betreuer = db().prepare( "SELECT id, name, rolle FROM personen WHERE id = ? AND aktiv = 1").get(z); /* Nur Scouts. Das Management sieht ohnehin alle Creator und braucht keinen Eintrag -- einer waere irrefuehrend. */ if (!betreuer || betreuer.rolle !== "scout") { return res.status(400).json({ fehler: "Zuständig können nur aktive Scouts sein." }); } betreuungSetzen(id, z, { ...req.person, ip: echteIp(req) }); res.json({ ok: true, betreuer_id: z, betreuer_name: betreuer.name }); } catch (fehler) { console.error("[workspace] Betreuung setzen:", fehler?.message); res.status(503).json({ fehler: "nicht_verfuegbar" }); } }); /* ---------- Anlegen ----------------------------------------------------- */ personenRouter.post("/workspace/api/verwaltung/personen", gleicheHerkunft, (req, res) => { try { const name = String(req.body?.name ?? "").trim(); const rolle = String(req.body?.rolle ?? ""); if (name.length < 2) return res.status(400).json({ fehler: "Name fehlt." }); if (name.length > NAME_MAX) return res.status(400).json({ fehler: "Name ist zu lang." }); if (!ROLLEN.includes(rolle)) return res.status(400).json({ fehler: "Unbekannte Rolle." }); const vorhanden = db().prepare( "SELECT 1 FROM personen WHERE lower(name) = lower(?) AND aktiv = 1").get(name); if (vorhanden) return res.status(409).json({ fehler: "Diesen Namen gibt es schon." }); const neu = personAnlegen(name, rolle, { ...req.person, ip: echteIp(req) }); /* Der Code steht NUR hier in der Antwort. Danach ist er weg. */ res.status(201).json({ id: neu.id, name: neu.name, rolle: neu.rolle, code: neu.code }); } catch (fehler) { console.error("[workspace] Person anlegen:", fehler?.message); res.status(503).json({ fehler: "nicht_verfuegbar" }); } }); /* ---------- Neuer Code -------------------------------------------------- */ personenRouter.post("/workspace/api/verwaltung/personen/:id/code", gleicheHerkunft, (req, res) => { try { const id = Number(req.params.id); if (!Number.isInteger(id)) return res.status(400).json({ fehler: "ungueltig" }); if (id === req.person.id) { /* Sich selbst den Code zu tauschen wirft einen sofort hinaus (alle Sitzungen werden beendet). Erlaubt, aber nur bewusst. */ if (!req.body?.auch_mich) { return res.status(400).json({ fehler: "eigener_code", hinweis: "Das beendet deine eigene Sitzung sofort." }); } } const neu = codeNeu(id, { ...req.person, ip: echteIp(req) }); res.json({ id: neu.id, name: neu.name, rolle: neu.rolle, code: neu.code }); } catch (fehler) { if (/Keine Person/.test(fehler?.message || "")) { return res.status(404).json({ fehler: "nicht_gefunden" }); } console.error("[workspace] Code erneuern:", fehler?.message); res.status(503).json({ fehler: "nicht_verfuegbar" }); } }); /* ---------- Sperren / Entsperren ---------------------------------------- */ personenRouter.patch("/workspace/api/verwaltung/personen/:id", gleicheHerkunft, (req, res) => { try { const id = Number(req.params.id); if (!Number.isInteger(id)) return res.status(400).json({ fehler: "ungueltig" }); const person = db().prepare("SELECT id, name, rolle, aktiv FROM personen WHERE id = ?").get(id); if (!person) return res.status(404).json({ fehler: "nicht_gefunden" }); if (req.body?.aktiv !== undefined) { const aktiv = req.body.aktiv ? 1 : 0; /* Sich selbst zu sperren würde den letzten Zugang zusperren können. Deshalb gar nicht erst erlauben. */ if (id === req.person.id && !aktiv) { return res.status(400).json({ fehler: "Du kannst dich nicht selbst sperren." }); } /* Und niemals das letzte aktive Management sperren -- sonst kommt niemand mehr in die Verwaltung hinein. */ if (!aktiv && person.rolle === "admin") { const { n } = db().prepare( "SELECT COUNT(*) AS n FROM personen WHERE rolle = 'admin' AND aktiv = 1").get(); if (n <= 1) return res.status(400).json({ fehler: "Das ist der letzte aktive DogFather-Zugang." }); } personSperren(id, aktiv, { ...req.person, ip: echteIp(req) }); } if (req.body?.name !== undefined) { const name = String(req.body.name).trim(); if (name.length < 2 || name.length > NAME_MAX) { return res.status(400).json({ fehler: "Name ist ungültig." }); } db().prepare("UPDATE personen SET name = ? WHERE id = ?").run(name, id); protokolliere("person_umbenannt", { personId: req.person.id, rolle: req.person.rolle, ip: echteIp(req), detail: `${person.name} -> ${name}`, }); } res.json({ ok: true }); } catch (fehler) { console.error("[workspace] Person ändern:", fehler?.message); res.status(503).json({ fehler: "nicht_verfuegbar" }); } }); /* ---------- Protokoll ---------------------------------------------------- */ personenRouter.get("/workspace/api/verwaltung/protokoll", (req, res) => { try { const anzahl = Math.min(Math.max(Number(req.query.anzahl) || 40, 1), 200); res.json({ eintraege: db().prepare(` SELECT k.zeitpunkt, k.aktion, k.rolle, k.detail, k.ip, p.name AS wer FROM protokoll k LEFT JOIN personen p ON p.id = k.person_id ORDER BY k.id DESC LIMIT ?`).all(anzahl), }); } catch { res.status(503).json({ fehler: "nicht_verfuegbar" }); } });