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