Files
dogfather-universe/server/workspace-personen.js
T
DogFatherGit 1c3e642f12 Workspace: Rolle "Management" heisst jetzt "DogFather"
Auf Wunsch von Filipe. Betrifft ausschliesslich die Anzeige.

Der Rollenschluessel bleibt "admin". Er steckt in der CHECK-Regel der
Datenbank, in jeder bestehenden Sitzung und in jeder Rechteabfrage --
ihn umzubenennen haette alle drei gebrochen, und bestehende Anmeldungen
waeren ungueltig geworden. Umbenannt wird nur, was man LIEST.

Der Anzeigename steht jetzt an EINER Stelle (ROLLEN_NAME in
workspace.js) und kommt ueber /workspace/api/ich als rolle_name mit.
Stand er in elf Dateien, waere er beim naechsten Mal in zehn davon
geaendert.

Dabei aufgefallen: In der Kopfleiste stand auf JEDER Seite der rohe
Rollenschluessel -- "Chef · admin". Das war schon vorher unschoen, faellt
aber jetzt erst richtig auf. Alle elf Seiten zeigen jetzt "Chef ·
DogFather". Geprueft: kein rohes "admin" mehr in der Oberflaeche.

Geaendert: Rollenkachel und Untertitel auf der Anmeldeseite, Kopfleiste
aller Seiten, Rollentext auf dem Dashboard, Rollenmarken und Auswahl in
der Personenverwaltung, die Betreuungs-Auswahl ("nur DogFather"), der
Hinweis auf der Dateienseite, die Erklaerung zur internen Notiz im
Profil, die Hinweise fuer Scouts ohne Zuteilung -- und vier
Fehlermeldungen vom Server.

Schreibweise "DogFather" wie von Filipe geschrieben; das ist auch auf
der oeffentlichen Website die haeufigste Form (717 von 1216).

In den Code-Kommentaren der Fachmodule heisst die Rolle weiterhin "das
Management". Das bleibt bewusst so -- eine Massenaenderung an vierzig
Kommentaren waere reines Risiko ohne sichtbaren Nutzen. Ein Hinweis an
der ROLLEN_NAME-Zuordnung erklaert den Zusammenhang.
2026-08-28 11:55:05 +02:00

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 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" });
}
});