Bisher hatten Scouts mit der Creator-Betreuung nichts zu tun -- keine Profile, keine Betreuungsbereiche, keine Reports. Das aendert sich, mit zwei bewusst gesetzten Grenzen. GRENZE 1: nur zugeteilte Creator, keine Rollenregel. Neue Tabelle betreuung (creator_id PRIMARY KEY -> betreuer_id). Ein Creator hat genau EINE zustaendige Person, damit nie unklar ist, wer gefragt ist. Das Management sieht ohnehin alle und braucht keinen Eintrag. Wer nichts zugeteilt bekommt, sieht weiterhin nichts -- kein Recht entsteht automatisch aus der Rolle. Zugeteilt wird in "Personen & Zugaenge", direkt in der Personenzeile: Betreuung ist eine Eigenschaft der Person, kein eigener Vorgang. Nur das Management darf zuteilen -- koennte ein Scout sich selbst Creator geben, haette er die Rechtevergabe in der Hand, die ihn begrenzen soll. Zustaendig koennen nur aktive Scouts sein, kein Admin (der sieht alles) und kein anderer Creator. Bei der Uebergabe aus der Pipeline passiert die Zuteilung von selbst: Wer jemanden gefunden hat, betreut ihn weiter. Genau darum geht es bei "Creator-Onboarding starten". Umhaengen kann das Management jederzeit. GRENZE 2: betreuen, nicht verwalten. Profile, die fuenf Bereiche und Reports wie ein Manager. ABER: - keine Zugangscodes, kein Sperren von Personen (personen.html bleibt admin-only, unveraendert) - keine management-internen Felder. Der Scout bekommt admin_notiz, plan_start und naechster_review NICHT -- die Felder fehlen in der Antwort komplett, nicht nur in der Anzeige. Eine Notiz UEBER die Betreuung gehoert nicht in die Hand dessen, der betreut. Geprueft: Ein Scout, der admin_notiz mitschickt, aendert sie nicht. Die Regel steht an EINER Stelle (betreuteIds / betreutWo / darfCreator in workspace.js) und wird von sechs Modulen benutzt. Eine Rechteregel, die an sechs Stellen steht, ist eine Rechteregel, die irgendwann an fuenf Stellen stimmt. Genau das ist beim Bauen auch passiert: workspace-calls.js hatte eine wortgleiche Kopie der Kalender-Sichtbarkeit. Erweitert wurde nur der Kalender -- Scouts sahen die Termine ihrer Creator, dieselben Termine als Call aber nicht. Die Kopie ist jetzt weg, calls.js importiert die Regel aus workspace-kalender.js. Zwei Fehler, die die Aenderung selbst erzeugt haette, vorher gefunden: - Report-Entscheidung: ein Scout haette eine Aufgabe angelegt, deren "Creator" er selbst ist -- die waere in jeder Auswertung falsch mitgelaufen. Zeigt jetzt auf einen seiner Creator. - Bereichseintrag: derselbe Fehler. Ein Scout hat gar keinen eigenen Betreuungsbereich. Ein Eintrag ohne oder mit fremder Zuordnung landet beim ersten zugeteilten Creator, nie bei einem fremden. Geprueft: Mikas Bereich bleibt bei jedem Versuch unberuehrt.
222 lines
9.5 KiB
JavaScript
222 lines
9.5 KiB
JavaScript
/* =====================================================================
|
|
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 das letzte aktive Management." });
|
|
}
|
|
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" });
|
|
}
|
|
});
|