Wunsch Filipe (Bildschirmfoto, 10.09.2026): "das modi symbol soll das
gleiche sein wie bei dogfather".
Vorher stand dort das Schild, und das war doppelt falsch: Es gehoert
schon dem Scout, und es stellte die Modis neben die Agentur statt zu
DogFather. Sie sind SEIN Team -- das Zeichen sagt das jetzt auch.
Kein neues Zeichen dafuer: `#r-husky` steht in personen.html laengst.
Ein eigenes waere eine Zeile mehr in einer Datei, die jeder bekommt --
und eine, die nur bei einer einzigen Rolle benutzt wird.
DIE PRUEFUNG VERGLEICHT GEGEN DIE ADMIN-KARTE, nicht gegen "#r-husky".
Waere dort morgen ein anderes Zeichen, muessten beide mitwandern --
der Wunsch war "dasselbe wie", nicht "der Husky".
AUF DEMSELBEN BILDSCHIRMFOTO stand "Sichtbar nur fuer die
DogFather-Rolle". In Kommentaren schreibt das Haus ASCII, in TEXT, den
jemand liest, nicht -- dort sieht es aus wie ein Fehler, weil es einer
ist. Berichtigt und mitgeprueft.
NEBENBEFUND, NICHT ANGEFASST: Im Creator-Katalog stehen zehn weitere
sichtbare Texte mit ASCII-Ersatz ("dafuer", "zaehlt", "spaet",
"groesser", "uebersteuert"). Sie stehen live auf den Bildschirmen der
Creator. Nachgemessen: In meinem eigenen Katalog aus Teil 2 sind es
0 von 120 sichtbaren Texten. Die zehn gehoeren berichtigt, aber nicht
nebenbei in einem Commit ueber ein Symbol.
Co-Authored-By: Claude Opus 5 <[email protected]>
841 lines
40 KiB
JavaScript
841 lines
40 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, sitzungToken, personSperren, betreuungSetzen, scoutZuteilungSetzen, istLeitung, istDogFather, siehtAlles, ROLLEN_SORTIERUNG, ROLLEN_REIHE, istSpicy, verborgeneIds,
|
||
} from "./workspace.js";
|
||
import { sicherungJetzt } from "./workspace-sicherung.js";
|
||
|
||
export const personenRouter = express.Router();
|
||
|
||
/* Reihenfolge und Umfang kommen aus workspace.js -- eine eigene
|
||
Liste hier waere die naechste Stelle, die beim Aendern vergessen wird. */
|
||
const ROLLEN = ROLLEN_REIHE;
|
||
const NAME_MAX = 60;
|
||
|
||
/* Nur Management. Alles andere bekommt 404 statt 403 -- wer nicht
|
||
berechtigt ist, muss nicht erfahren, dass es diesen Bereich gibt. */
|
||
/* NUR DogFather -- seit dem 02.09.2026 auch wirklich.
|
||
|
||
Hier stand istLeitung, und damit kam auch jede Managerin an die
|
||
Personenverwaltung: Codes erneuern, sperren, Zuteilungen ändern. Die
|
||
Rollenauswahl im selben Programm versprach dabei die ganze Zeit das
|
||
Gegenteil -- "Manager: dieselben Rechte wie DogFather, AUSSER der
|
||
Personenverwaltung".
|
||
|
||
Eine Beschreibung, die etwas anderes sagt als die Software, ist
|
||
schlimmer als beides einzeln: Man weiss danach nicht mehr, welcher
|
||
von beiden man glauben soll. Jetzt sagen beide dasselbe.
|
||
|
||
Wunsch vom 02.09.2026: "die manager sollen diese kategorien garnicht
|
||
sehen."
|
||
|
||
Weiterhin 404 statt 403: Wer nicht hierher gehört, 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" });
|
||
|
||
/* EINE EINZIGE AUSNAHME: die LISTE fuer Spicy Media (07.09.2026).
|
||
|
||
Filipe: "spicy soll genau das sehen koennen, aber nur nicht die
|
||
rolle dogfather." Bis hierher sah die Rolle auf der Personenseite
|
||
nur das Anlegen-Formular.
|
||
|
||
Die Ausnahme steht HIER und nicht als eigene Schicht davor. Der
|
||
erste Versuch war genau das -- eine Route vor `use(nurAdmin)`, die
|
||
mit `next("route")` weiterreicht. Das tut aber das Gegenteil von
|
||
dem, wonach es klingt: `next("route")` ueberspringt die restlichen
|
||
Handler DIESER Route und geht zur naechsten passenden Schicht --
|
||
also zu nurAdmin. Spicy Media bekam weiter 404, die Oberflaeche
|
||
verstand das als "nicht erlaubt" und schickte sie auf die
|
||
Startseite zurueck. Gemessen: Auf personen.html standen die
|
||
Kategorien der STARTSEITE.
|
||
|
||
Nur LESEN, nur diese eine Adresse, nur diese eine Rolle. Codes,
|
||
Sperren, Loeschen, Zuteilung und Protokoll bleiben bei DogFather --
|
||
Ueberblick ist nicht Verwaltung. Und DogFathers eigene Zeile faellt
|
||
unten aus dem Ergebnis (`req.ohneDogFather`). */
|
||
const nurListe = req.method === "GET"
|
||
&& req.baseUrl + req.path === "/workspace/api/verwaltung/personen";
|
||
if (istSpicy(person) && nurListe) {
|
||
req.person = person;
|
||
req.ohneDogFather = true;
|
||
return next();
|
||
}
|
||
|
||
if (!istDogFather(person)) 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);
|
||
|
||
/* ---------------------------------------------------------------------
|
||
Schranke: An DogFather und Managern aendert nur DogFather etwas.
|
||
|
||
Sie haengt an JEDEM Weg mit einer :id, nicht an einzelnen Routen. Der
|
||
erste Versuch sicherte nur Anlegen und Sperren ab -- und prompt blieb
|
||
"neuer Zugangscode" offen. Ein Manager konnte DogFather einen neuen
|
||
Code ausstellen, bekam ihn angezeigt und haette ihn damit aus seinem
|
||
eigenen Konto ausgesperrt. Genau das soll "nur DogFather hat alle
|
||
endgueltigen Rechte" verhindern.
|
||
|
||
Als Schranke statt als Einzelpruefung, damit der naechste Weg, der
|
||
hier dazukommt, automatisch mitgeschuetzt ist. */
|
||
function nurDogFatherBeiLeitung(req, res, next) {
|
||
if (istDogFather(req.person)) return next();
|
||
const id = Number(req.params.id);
|
||
if (!Number.isInteger(id)) return next();
|
||
const ziel = db().prepare("SELECT rolle FROM personen WHERE id = ?").get(id);
|
||
if (ziel && (ziel.rolle === "admin" || ziel.rolle === "manager")) {
|
||
return res.status(403).json({
|
||
fehler: "An DogFather und Managern ändert nur DogFather etwas.",
|
||
});
|
||
}
|
||
next();
|
||
}
|
||
|
||
/* ---------- Übersicht --------------------------------------------------- */
|
||
|
||
/* Sortierung: Die ROLLE zuerst, dann erst der Zustand.
|
||
|
||
Vorher stand "p.aktiv DESC" ganz vorn -- dadurch wanderte jede
|
||
gesperrte Person ans Ende der GESAMTEN Liste, quer durch alle Rollen.
|
||
Ein gesperrter Manager stand also unter den Creators, und die
|
||
Reihenfolge DogFather-Manager-Scout-Creator, die ueberall sonst gilt,
|
||
war ausgerechnet auf der Personenseite aufgehoben.
|
||
|
||
Jetzt: Rolle, dann Gesperrtes ans Ende SEINER Rolle, dann Name.
|
||
|
||
(Der Kommentar steht bewusst hier und nicht in der Abfrage: Er enthaelt
|
||
Rueckwaerts-Anfuehrungszeichen, und die beenden mitten in einer
|
||
Zeichenkette genau diese -- der Server startete danach nicht mehr.) */
|
||
personenRouter.get("/workspace/api/verwaltung/personen", (req, res) => {
|
||
try {
|
||
/* Fuer Spicy Media ohne DogFather -- gefiltert am ERGEBNIS und
|
||
nicht in der Abfrage: Die Abfrage ist lang und wird von mehreren
|
||
Feldern gelesen; eine zusaetzliche Bedingung mittendrin waere die
|
||
Stelle, an der beim naechsten Umbau jemand danebengreift. */
|
||
/* FUER SPICY MEDIA: DOGFATHER JA, DER ZWEITE ADMIN-ZUGANG NEIN.
|
||
|
||
Filipe: "die rolle spicy soll auch die rolle dogfather sehen,
|
||
aber nur dogfather sehen und nicht vanvan."
|
||
|
||
In der Datenbank tragen BEIDE die Rolle `admin` -- nachgemessen
|
||
am laufenden System: id 1 "Dogfather", id 4 "VanVan". Es gibt
|
||
kein Feld, das den einen vom anderen unterscheidet.
|
||
|
||
Der Unterschied, den es WIRKLICH gibt, ist das Alter: DogFather
|
||
ist der erste Zugang des Hauses. Deshalb zaehlt hier die
|
||
kleinste Nummer unter den Admins -- eine Eigenschaft, die
|
||
feststeht und nicht am Namen haengt.
|
||
|
||
DIE SCHWACHSTELLE STEHT HIER, damit sie niemand sucht: Wuerde
|
||
Zugang 1 je geloescht, rueckte der naechste Admin nach. Sollte
|
||
das eintreten, gehoert stattdessen ein ausdrueckliches Merkmal
|
||
in die Tabelle -- ein `haupt`-Feld. Solange es genau zwei
|
||
Admin-Zugaenge gibt und der erste DogFather ist, ist die
|
||
Nummer die ehrlichste Antwort ohne Datenbankumbau. */
|
||
/* SEIT DEM 09.09.2026 GILT DIE REGEL FUER ALLE, NICHT NUR FUER
|
||
SPICY MEDIA.
|
||
|
||
Filipe: "keiner soll vanvan sehen ausser ich, ueberall soll
|
||
keiner vanvan sehen ausser dogfather."
|
||
|
||
Hier stand eine eigene Fassung dieser Regel -- nur fuer Spicy
|
||
Media, nur an dieser Stelle. Sie ist ersetzt durch die zentrale
|
||
`verborgeneIds()` aus workspace.js, die ueberall dieselbe ist.
|
||
Zwei Fassungen derselben Regel waeren zwei Gelegenheiten, dass
|
||
eine nachgezogen wird und die andere nicht -- und bei einer
|
||
Sichtbarkeitsregel heisst das, dass jemand etwas sieht. */
|
||
const weg = new Set(verborgeneIds(req.person));
|
||
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 NOT IN ('erledigt', 'abgebrochen')) 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,
|
||
/* Manager -> Scouts (01.09.2026). Bei einem Scout steht
|
||
hier, wem er zugeteilt ist; bei einem Manager, wie viele
|
||
Scouts an ihm haengen. Beides aus derselben Abfrage --
|
||
eine zweite waere eine zweite Gelegenheit, dass die
|
||
Liste etwas anderes sagt als die Rechte. */
|
||
sz.manager_id,
|
||
(SELECT name FROM personen m WHERE m.id = sz.manager_id) AS manager_name,
|
||
(SELECT COUNT(*) FROM scout_zuteilung z WHERE z.manager_id = p.id) AS scouts_anzahl
|
||
FROM personen p
|
||
LEFT JOIN betreuung b ON b.creator_id = p.id
|
||
LEFT JOIN scout_zuteilung sz ON sz.scout_id = p.id
|
||
ORDER BY ${ROLLEN_SORTIERUNG.replace("rolle", "p.rolle")}, p.aktiv DESC, p.name`)
|
||
.all().filter((z) => !weg.has(z.id)),
|
||
/* Wer ueberhaupt als zustaendig eingetragen werden kann.
|
||
|
||
Bis zum 31.08.2026 waren das nur Scouts. In der Auswahl stand
|
||
"DogFather" -- aber als LEER-Wert, nicht als Person. Das sah aus
|
||
wie eine Zuordnung und war keine: Dieselben Creator zaehlten
|
||
gleichzeitig im Hinweis "Creator ohne zustaendige Person". Zwei
|
||
Stellen, zwei Aussagen, beide auf demselben Bildschirm.
|
||
|
||
Jetzt kann jede betreuende Rolle eingetragen werden -- DogFather
|
||
und Manager genauso wie Scouts. In der ueblichen Reihenfolge. */
|
||
/* Auch die Auswahl "wer betreut" -- ein verborgener Zugang darf
|
||
dort nicht als Vorschlag auftauchen. */
|
||
betreuer: db().prepare(
|
||
`SELECT id, name, rolle FROM personen
|
||
WHERE rolle IN ('admin', 'manager', 'scout') AND aktiv = 1
|
||
ORDER BY ${ROLLEN_SORTIERUNG}, name`).all().filter((z) => !weg.has(z.id)),
|
||
|
||
/* =============================================================
|
||
ROLLEN, DIE NICHT IM BROWSER STEHEN DUERFEN (09.09.2026)
|
||
|
||
Die Rollenauswahl im Formular kommt sonst aus einer Liste in
|
||
assets/js/personen.js. Diese Datei bekommt JEDER ausgeliefert,
|
||
der die Seite oeffnet -- auch ein Manager, ein Scout, ein
|
||
Creator. Steht dort `modi: 'Modi'`, genuegt ein Blick in den
|
||
Quelltext, und der ganze verborgene Zugang ist verraten.
|
||
|
||
Deshalb kommt dieser eine Eintrag vom Server und nur an die
|
||
DogFather-Rolle. Wer die Rolle nicht hat, bekommt eine leere
|
||
Liste -- nicht eine gefilterte Liste, sondern gar keine
|
||
Angabe. Ein Feld, das mal Inhalt hat und mal nicht, verraet
|
||
nichts; ein Feld mit einem "darfst du nicht" darin schon.
|
||
|
||
Text und Zeichen stehen deshalb ebenfalls hier und nicht im
|
||
Browser. `schutz` ist das vorhandene Schild-Zeichen -- es passt
|
||
zur Moderation und spart ein neues, das in der Zeichenliste
|
||
wieder fuer alle sichtbar waere. */
|
||
zusatzrollen: istDogFather(req.person)
|
||
? [{
|
||
wert: "modi", name: "Modi",
|
||
/* DERSELBE HUSKY WIE BEI DOGFATHER (Wunsch Filipe, 10.09.2026:
|
||
"das modi symbol soll das gleiche sein wie bei dogfather").
|
||
|
||
Vorher stand hier das Schild -- und das war doppelt falsch:
|
||
Es gehoert schon dem Scout, und es stellte die Modis neben
|
||
die Agentur statt zu DogFather. Sie sind SEIN Team; das
|
||
Zeichen sagt das jetzt auch.
|
||
|
||
UND ES IST KEIN NEUES ZEICHEN. `husky` gibt es in
|
||
personen.html laengst als `#r-husky`. Ein eigenes waere eine
|
||
Zeile mehr in einer Datei, die jeder bekommt -- und eine,
|
||
die nur bei einer einzigen Rolle benutzt wird. */
|
||
symbol: "husky",
|
||
/* ECHTE UMLAUTE. In Kommentaren schreibt das Haus ASCII, in
|
||
TEXT, den jemand liest, nicht -- auf dem Bildschirmfoto
|
||
stand "Sichtbar nur fuer die DogFather-Rolle", und das sieht
|
||
aus wie ein Fehler, weil es einer ist. */
|
||
text: "Moderation und Team-Aufgaben. Sichtbar nur für die "
|
||
+ "DogFather-Rolle und für die Modis untereinander.",
|
||
}]
|
||
: [],
|
||
});
|
||
} 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);
|
||
/* DogFather, Manager und Scouts. Ein Creator kann nicht fuer einen
|
||
anderen zustaendig sein -- das waere eine Rolle, die es nicht gibt.
|
||
|
||
ACHTUNG, SEIT DEM 01.09.2026 ANDERS: Hier stand frueher "ein
|
||
Eintrag auf DogFather oder Manager aendert an den Rechten nichts,
|
||
die Leitung sieht ohnehin jeden Creator". Das gilt nicht mehr --
|
||
nur noch DogFather sieht alles. Fuer einen MANAGER entscheidet
|
||
dieser Eintrag jetzt genauso ueber die Sichtbarkeit wie fuer einen
|
||
Scout: Wer ihm nicht zugeteilt ist (weder direkt noch ueber einen
|
||
seiner Scouts), taucht bei ihm nicht auf.
|
||
|
||
Nur bei DogFather sagt der Eintrag weiterhin bloss, WER sich
|
||
kuemmert. Die Regel dazu steht an genau einer Stelle:
|
||
betreuteIds() in workspace.js. */
|
||
if (!betreuer || !["admin", "manager", "scout"].includes(betreuer.rolle)) {
|
||
return res.status(400).json({
|
||
fehler: "Zuständig können nur aktive DogFather, Manager oder 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" });
|
||
}
|
||
});
|
||
|
||
/* ---------- Welcher Scout gehoert zu welchem Manager --------------------
|
||
|
||
"er soll nur die Scouts sehen, die ihm zugeteilt sind." (01.09.2026)
|
||
|
||
NUR DOGFATHER darf das setzen -- ausdruecklich so entschieden, und
|
||
der Grund ist nicht Misstrauen, sondern Bauart: Diese Zuteilung
|
||
ERWEITERT die Sicht eines Managers (auf die Leads seiner Scouts und
|
||
auf deren Creator). Duerfte er sie selbst setzen, koennte er sich
|
||
seine eigene Sichtbarkeit vergeben -- und eine Grenze, die der
|
||
Begrenzte selbst verschieben kann, ist keine.
|
||
|
||
Deshalb istDogFather und NICHT istLeitung. Der Unterschied ist hier
|
||
der ganze Punkt. */
|
||
|
||
personenRouter.put("/workspace/api/verwaltung/scout-zuteilung/:id", gleicheHerkunft, (req, res) => {
|
||
try {
|
||
if (!istDogFather(req.person)) {
|
||
/* 404 und nicht 403: Wer es nicht darf, soll nicht einmal
|
||
erfahren, dass es diesen Weg gibt. */
|
||
return res.status(404).json({ fehler: "nicht_gefunden" });
|
||
}
|
||
const id = Number(req.params.id);
|
||
if (!Number.isInteger(id)) return res.status(400).json({ fehler: "ungueltig" });
|
||
|
||
const scout = db().prepare("SELECT id, rolle FROM personen WHERE id = ?").get(id);
|
||
if (!scout || scout.rolle !== "scout") {
|
||
return res.status(404).json({ fehler: "nicht_gefunden" });
|
||
}
|
||
|
||
const w = req.body?.manager_id;
|
||
if (w === null || w === "" || w === undefined) {
|
||
scoutZuteilungSetzen(id, null, req.person.id);
|
||
protokolliere("scout_zuteilung", req.person, echteIp(req), `${id}: geloest`);
|
||
return res.json({ ok: true, manager_id: null });
|
||
}
|
||
|
||
const z = Number(w);
|
||
if (!Number.isInteger(z) || z < 1) return res.status(400).json({ fehler: "Ungültige Auswahl." });
|
||
const manager = db().prepare(
|
||
"SELECT id, name, rolle FROM personen WHERE id = ? AND aktiv = 1").get(z);
|
||
/* MANAGER ODER DOGFATHER -- wie bei den Creatorn (02.09.2026:
|
||
"ich will die Rollen, welcher Scout welchem Manager oder DogFather
|
||
gehoert, wie es bei den Creator ist").
|
||
|
||
Ein Scout unter einem Scout bleibt ausgeschlossen: Das waere eine
|
||
Ordnung, die es nicht gibt. Ein Creator kann ohnehin niemanden
|
||
fuehren.
|
||
|
||
WICHTIG ZUM VERSTAENDNIS -- die beiden Faelle bewirken
|
||
Verschiedenes, und das ist Absicht:
|
||
MANAGER Der Eintrag entscheidet ueber SICHTBARKEIT. Er sieht
|
||
danach die Leads dieses Scouts und dessen Creator.
|
||
DOGFATHER Der Eintrag sagt nur, WER zustaendig ist. An den
|
||
Rechten aendert er nichts -- DogFather sieht ohnehin
|
||
alles.
|
||
Genau so ist es bei "Betreut von" der Creator auch. Ein Eintrag,
|
||
der die Zustaendigkeit festhaelt, ist kein Eintrag ohne Wirkung:
|
||
Er beantwortet die Frage "wen frage ich?". */
|
||
if (!manager || !["manager", "admin"].includes(manager.rolle)) {
|
||
return res.status(400).json({
|
||
fehler: "Zuteilen kann man nur an einen aktiven Manager oder DogFather.",
|
||
});
|
||
}
|
||
|
||
scoutZuteilungSetzen(id, z, req.person.id);
|
||
protokolliere("scout_zuteilung", req.person, echteIp(req), `${id} -> ${z}`);
|
||
res.json({ ok: true, manager_id: z, manager_name: manager.name });
|
||
} catch (fehler) {
|
||
console.error("[workspace] Scout-Zuteilung:", fehler?.message);
|
||
res.status(503).json({ fehler: "nicht_verfuegbar" });
|
||
}
|
||
});
|
||
|
||
/* =======================================================================
|
||
EIN MANAGER LEGT EINEN CREATOR AN (07.09.2026)
|
||
|
||
Wunsch Filipe: "ich will auch dass manager ab jetzt creator und auch
|
||
wirklich nur creator hinzufuegen koennen auf die seite. die creator
|
||
sollen ... auch nur der person hinzugefuegt werden wo dan diesen
|
||
creator hinzufuegt oder den scouts von diesem manager."
|
||
|
||
-----------------------------------------------------------------------
|
||
WARUM DAS EIN EIGENER WEG IST UND NICHT DIE ALTE TUER
|
||
|
||
Alles unter `/workspace/api/verwaltung` haengt an EINER Schranke:
|
||
`nurAdmin`. Dahinter liegen acht Wege -- Rollen aendern, Codes neu
|
||
setzen, sperren, loeschen, das Protokoll lesen. Einen Manager dort
|
||
hineinzulassen und danach in jedem der acht Wege einzeln zu pruefen,
|
||
was er darf, ist genau die Bauweise, durch die am 31.08.2026 schon
|
||
einmal ein Loch entstanden ist: Zwei von drei Stellen waren
|
||
abgesichert, die dritte vergessen.
|
||
|
||
Deshalb bleibt die Tuer zu, und daneben steht eine neue mit genau
|
||
EINEM Zweck. Sie kann nichts anderes, als einen Creator anzulegen --
|
||
nicht weil eine Abfrage es verbietet, sondern weil es hier gar keinen
|
||
anderen Weg gibt. Das ist der Unterschied zwischen "darf nicht" und
|
||
"kann nicht".
|
||
|
||
DIE ROLLE STEHT NICHT IM AUFRUF. Sie wird hier gesetzt. Ein Feld
|
||
`rolle` im Koerper waere die naheliegende Loesung und die falsche:
|
||
Dann muesste eine Abfrage sie pruefen, und eine vergessene Abfrage
|
||
ist ein zweiter Zugang mit vollen Rechten.
|
||
|
||
DIE ZUTEILUNG PASSIERT SOFORT UND HIER. Ein Creator ohne Betreuung
|
||
ist fuer alle ausser DogFather unsichtbar -- der Manager haette ihn
|
||
angelegt und danach nicht mehr gesehen. Wer anlegt, betreut; ein Scout
|
||
des Managers kann gleich mitgegeben werden ("oder den scouts von
|
||
diesem manager").
|
||
======================================================================= */
|
||
function nurLeitung(req, res, next) {
|
||
const person = sitzungLesen(req);
|
||
if (!person) return res.status(401).json({ fehler: "nicht_angemeldet" });
|
||
if (!istLeitung(person)) return res.status(404).json({ fehler: "nicht_gefunden" });
|
||
req.person = person;
|
||
next();
|
||
}
|
||
|
||
personenRouter.post("/workspace/api/creator-anlegen", gleicheHerkunft, nurLeitung, (req, res) => {
|
||
try {
|
||
const name = String(req.body?.name ?? "").trim();
|
||
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." });
|
||
|
||
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." });
|
||
|
||
/* Wem der neue Creator gehoert. Vorgabe: dem, der ihn anlegt.
|
||
Ein Manager darf stattdessen einen SEINER Scouts angeben -- und
|
||
nur einen seiner eigenen. Wer eine fremde Nummer hineinschreibt,
|
||
bekommt sie nicht, sondern eine Absage: Stillschweigend auf die
|
||
Vorgabe zurueckzufallen waere schlimmer, weil der Manager dann
|
||
glaubt, er haette zugeteilt. */
|
||
let betreuerId = req.person.id;
|
||
const gewuenscht = req.body?.betreuer_id;
|
||
if (gewuenscht !== undefined && gewuenscht !== null && gewuenscht !== "") {
|
||
const z = Number(gewuenscht);
|
||
if (!Number.isInteger(z) || z < 1) return res.status(400).json({ fehler: "Ungültige Zuordnung." });
|
||
if (z !== req.person.id) {
|
||
const erlaubt = istDogFather(req.person)
|
||
? db().prepare("SELECT 1 FROM personen WHERE id = ? AND aktiv = 1 AND rolle IN ('admin','manager','scout')").get(z)
|
||
: db().prepare(`SELECT 1 FROM personen p
|
||
JOIN scout_zuteilung s ON s.scout_id = p.id
|
||
WHERE p.id = ? AND p.aktiv = 1 AND p.rolle = 'scout'
|
||
AND s.manager_id = ?`).get(z, req.person.id);
|
||
if (!erlaubt) {
|
||
return res.status(403).json({
|
||
fehler: "Diesen Scout betreust du nicht – zuteilen kannst du nur deine eigenen.",
|
||
});
|
||
}
|
||
}
|
||
betreuerId = z;
|
||
}
|
||
|
||
const neu = personAnlegen(name, "creator", { ...req.person, ip: echteIp(req) });
|
||
betreuungSetzen(neu.id, betreuerId, { ...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,
|
||
betreuer_id: betreuerId });
|
||
} catch (fehler) {
|
||
console.error("[workspace] Creator anlegen:", fehler?.message);
|
||
res.status(503).json({ fehler: "nicht_verfuegbar" });
|
||
}
|
||
});
|
||
|
||
/* ZWEITE TUER: EINEN MANAGER ANLEGEN -- nur Spicy Media und DogFather
|
||
(07.09.2026).
|
||
|
||
Filipe: "die agentur manager und creator und die dan auch einteilen."
|
||
|
||
Warum nicht einfach ein Feld `rolle` an der Tuer daneben? Weil damit
|
||
genau die Eigenschaft verloren ginge, die sie sicher macht: Sie KANN
|
||
nichts anderes, als einen Creator anzulegen. Sobald eine Abfrage
|
||
entscheidet, welche Rolle erlaubt ist, ist eine vergessene Abfrage ein
|
||
zweiter Zugang mit vollen Rechten.
|
||
|
||
Also zwei Tueren, jede mit genau einer Aufgabe. Die Rolle steht auch
|
||
hier nicht im Aufruf.
|
||
|
||
EINEN MANAGER LEGT MAN NICHT NEBENBEI AN: Er sieht danach jeden
|
||
Creator, den er betreut, und legt selbst welche an. Deshalb bleibt
|
||
diese Tuer Spicy Media und DogFather vorbehalten -- ein Manager kann
|
||
keinen zweiten Manager schaffen. */
|
||
personenRouter.post("/workspace/api/manager-anlegen", gleicheHerkunft, (req, res) => {
|
||
try {
|
||
const person = sitzungLesen(req);
|
||
if (!person) return res.status(401).json({ fehler: "nicht_angemeldet" });
|
||
if (!siehtAlles(person)) return res.status(404).json({ fehler: "nicht_gefunden" });
|
||
|
||
const name = String(req.body?.name ?? "").trim();
|
||
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." });
|
||
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, "manager", { ...person, ip: echteIp(req) });
|
||
res.status(201).json({ id: neu.id, name: neu.name, rolle: neu.rolle, code: neu.code });
|
||
} catch (fehler) {
|
||
console.error("[workspace] Manager anlegen:", fehler?.message);
|
||
res.status(503).json({ fehler: "nicht_verfuegbar" });
|
||
}
|
||
});
|
||
|
||
/* Welche Scouts kann ich einem neuen Creator gleich mitgeben?
|
||
Dieselbe Regel wie oben, nur lesend -- damit die Oberflaeche genau
|
||
die Liste anbietet, die der Server auch annimmt. Zwei getrennte
|
||
Listen waeren die naechste Stelle, an der beide auseinanderlaufen. */
|
||
personenRouter.get("/workspace/api/creator-anlegen/scouts", nurLeitung, (req, res) => {
|
||
try {
|
||
const zeilen = istDogFather(req.person)
|
||
? db().prepare("SELECT id, name FROM personen WHERE aktiv = 1 AND rolle = 'scout' ORDER BY name").all()
|
||
: db().prepare(`SELECT p.id, p.name FROM personen p
|
||
JOIN scout_zuteilung s ON s.scout_id = p.id
|
||
WHERE p.aktiv = 1 AND p.rolle = 'scout' AND s.manager_id = ?
|
||
ORDER BY p.name`).all(req.person.id);
|
||
res.json({ scouts: zeilen });
|
||
} catch {
|
||
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." });
|
||
/* 'MODI' IST FUER ALLE AUSSER DOGFATHER KEINE BEKANNTE ROLLE
|
||
(09.09.2026).
|
||
|
||
EHRLICHERWEISE: Diese Zeile schliesst heute keine Luecke. Der
|
||
ganze Zweig /workspace/api/verwaltung haengt an nurAdmin, und
|
||
nurAdmin laesst nur die DogFather-Rolle durch -- weiter kommt
|
||
ohnehin niemand. Sie steht hier fuer den Tag, an dem jemand
|
||
nurAdmin lockert, etwa damit Manager Leute verwalten duerfen.
|
||
Dann waere sonst genau hier der stille Weg zu einem Modi-Zugang.
|
||
|
||
WARUM DIESELBE ANTWORT WIE BEI EINER ERFUNDENEN ROLLE: Ein
|
||
"das darfst du nicht" waere schlimmer als das Recht selbst -- es
|
||
verriete, dass es die Rolle gibt. Deshalb wortgleich, mit
|
||
demselben Statuscode. */
|
||
const unbekannt = !ROLLEN.includes(rolle)
|
||
|| (rolle === "modi" && !istDogFather(req.person));
|
||
if (unbekannt) return res.status(400).json({ fehler: "Unbekannte Rolle." });
|
||
|
||
/* ERSTER VORBEHALT: Eine Leitung anlegen darf nur DogFather.
|
||
Duerfte ein Manager das, koennte er sich einen zweiten Zugang mit
|
||
vollen Rechten schaffen -- und waere damit nicht mehr begrenzbar.
|
||
"Nur DogFather hat alle endgueltigen Rechte" faengt hier an. */
|
||
if (!istDogFather(req.person) && (rolle === "admin" || rolle === "manager")) {
|
||
return res.status(403).json({
|
||
fehler: "DogFather und Manager legt nur DogFather selbst an.",
|
||
});
|
||
}
|
||
|
||
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, nurDogFatherBeiLeitung, (req, res) => {
|
||
try {
|
||
const id = Number(req.params.id);
|
||
if (!Number.isInteger(id)) return res.status(400).json({ fehler: "ungueltig" });
|
||
const eigener = id === req.person.id;
|
||
if (eigener) {
|
||
/* Den eigenen Code zu tauschen bleibt eine bewusste Entscheidung --
|
||
alle anderen Geräte werden abgemeldet, und der alte Code gilt
|
||
nicht mehr. Seit dem 02.09.2026 wirft es einen aber NICHT mehr
|
||
selbst hinaus: Diese eine Sitzung bleibt, damit man den neuen
|
||
Code in Ruhe abschreiben kann (siehe codeNeu). */
|
||
if (!req.body?.auch_mich) {
|
||
return res.status(400).json({ fehler: "eigener_code", hinweis:
|
||
"Der alte Code gilt danach nicht mehr, und alle anderen Geräte "
|
||
+ "werden abgemeldet. Hier bleibst du angemeldet." });
|
||
}
|
||
}
|
||
const neu = codeNeu(id, { ...req.person, ip: echteIp(req) },
|
||
eigener ? sitzungToken(req) : null);
|
||
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, nurDogFatherBeiLeitung, (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 den letzten aktiven DogFather sperren -- sonst kann
|
||
niemand mehr eine Leitung anlegen, auch kein Manager.
|
||
Bewusst istDogFather und NICHT istLeitung: Manager duerfen
|
||
gesperrt werden, es gibt ja noch DogFather. Die Massenumstellung
|
||
auf istLeitung hatte das hier faelschlich mitgezogen -- gezaehlt
|
||
werden aber nur DogFather-Zugaenge, also blockierte die Regel
|
||
plotzlich auch das Sperren eines Managers. */
|
||
if (!aktiv && istDogFather(person)) {
|
||
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" });
|
||
}
|
||
});
|
||
|
||
/* =====================================================================
|
||
Löschen.
|
||
|
||
Wunsch Filipe, 31.08.2026: "ich will auch die möglichkeit haben die
|
||
löschen zu können."
|
||
|
||
Das ist die einzige Handlung im ganzen Workspace, die sich nicht
|
||
rückgängig machen lässt. Deshalb vier Vorkehrungen:
|
||
|
||
1. NUR DOGFATHER. Nicht die Leitung, nicht ein Manager -- "nur
|
||
DogFather hat alle endgültigen Rechte" heisst genau hier etwas.
|
||
Ein Manager kann weiterhin sperren; das reicht für den Alltag und
|
||
ist umkehrbar.
|
||
|
||
2. VORSCHAU, was daran hängt. Man muss VOR dem Klick sehen, was
|
||
mitgeht und was nur seine Zuordnung verliert. Ohne diese Liste
|
||
löscht man ein Creator-Profil und merkt erst Wochen später, dass
|
||
damit auch der Start-Check und die Content-Säulen weg sind.
|
||
|
||
3. EINE SICHERUNG DIREKT DAVOR. Das ist der eigentliche Gewinn aus der
|
||
Sicherungsarbeit von heute: Vor jedem Löschen schreibt der Server
|
||
eine vollständige, sofort geprüfte Sicherung. Geht sie schief, wird
|
||
NICHT gelöscht. Damit ist "gelöscht" wiederherstellbar -- aus einer
|
||
unumkehrbaren Handlung wird eine, die man zurückholen kann.
|
||
|
||
4. Sich selbst und den letzten DogFather nie. Dieselbe Regel wie beim
|
||
Sperren, aus demselben Grund: Sonst sperrt man sich aus.
|
||
|
||
Was BLEIBT: Aufgaben, Termine, Bereichseinträge, Dateien und
|
||
Protokollzeilen. Sie verlieren nur den Verweis auf die Person
|
||
(ON DELETE SET NULL). Das ist Absicht -- eine Aufgabe verschwinden zu
|
||
lassen, weil jemand geht, wäre Geschichtsfälschung.
|
||
|
||
Was MITGEHT (ON DELETE CASCADE): Profil, Start-Check, Content-Säulen,
|
||
Sitzungen, Zuständigkeit. Dinge, die es ohne die Person nicht gibt.
|
||
===================================================================== */
|
||
|
||
/* Was hängt an dieser Person? Wird von der Vorschau UND vom Löschen
|
||
benutzt -- damit die Anzeige nicht etwas anderes behaupten kann, als
|
||
das Löschen dann tut. */
|
||
function anhang(id) {
|
||
const zahl = (sql, ...w) => db().prepare(sql).get(id, ...w)?.n ?? 0;
|
||
return {
|
||
mit: {
|
||
"Profil": zahl("SELECT COUNT(*) AS n FROM profile WHERE person_id = ?"),
|
||
"Start-Check": zahl("SELECT COUNT(*) AS n FROM startcheck WHERE creator_id = ?"),
|
||
"Content-Säulen": zahl("SELECT COUNT(*) AS n FROM content_saeulen WHERE creator_id = ?"),
|
||
"offene Sitzungen": zahl("SELECT COUNT(*) AS n FROM sitzungen WHERE person_id = ?"),
|
||
},
|
||
bleibt: {
|
||
"Aufgaben": zahl("SELECT COUNT(*) AS n FROM aufgaben WHERE creator_id = ? OR verantwortlich_id = ?", id),
|
||
"Einträge in den Bereichen": zahl("SELECT COUNT(*) AS n FROM eintraege WHERE creator_id = ?"),
|
||
"Termine": zahl("SELECT COUNT(*) AS n FROM termine WHERE creator_id = ? OR teilnehmer_id = ?", id),
|
||
"Dateien": zahl("SELECT COUNT(*) AS n FROM dateien WHERE creator_id = ?"),
|
||
"Leads in der Pipeline": zahl("SELECT COUNT(*) AS n FROM leads WHERE scout_id = ? OR creator_id = ?", id),
|
||
},
|
||
/* Der gefährlichste Einzelfall: Wer einen Scout löscht, nimmt seinen
|
||
Creators die zuständige Person weg -- und die stehen danach ohne
|
||
Betreuung da, ohne dass jemand etwas davon mitbekommt. */
|
||
verliertBetreuung: db().prepare(
|
||
"SELECT p.name FROM betreuung b JOIN personen p ON p.id = b.creator_id WHERE b.betreuer_id = ?")
|
||
.all(id).map((r) => r.name),
|
||
};
|
||
}
|
||
|
||
/* Alles, was gegen ein Löschen spricht -- als Fehlertext oder null.
|
||
Steht getrennt, damit Vorschau und Löschen GARANTIERT dieselbe Antwort
|
||
geben. Zwei Stellen mit derselben Regel laufen irgendwann auseinander. */
|
||
function darfGeloeschtWerden(person, akteur) {
|
||
if (!istDogFather(akteur)) return "Löschen darf nur DogFather.";
|
||
if (person.id === akteur.id) return "Dich selbst kannst du nicht löschen.";
|
||
if (istDogFather(person)) {
|
||
const { n } = db().prepare("SELECT COUNT(*) AS n FROM personen WHERE rolle = 'admin'").get();
|
||
if (n <= 1) return "Das ist der letzte DogFather-Zugang.";
|
||
}
|
||
return null;
|
||
}
|
||
|
||
personenRouter.get("/workspace/api/verwaltung/personen/:id/loeschbar", (req, res) => {
|
||
try {
|
||
const id = Number(req.params.id);
|
||
if (!Number.isInteger(id)) return res.status(400).json({ fehler: "ungueltig" });
|
||
/* Wer gar nicht löschen darf, sieht auch die Vorschau nicht -- sie
|
||
verrät, wie viel an einer Person hängt. */
|
||
if (!istDogFather(req.person)) return res.status(404).json({ fehler: "nicht_gefunden" });
|
||
|
||
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" });
|
||
|
||
res.json({
|
||
person: { id: person.id, name: person.name, rolle: person.rolle, aktiv: person.aktiv },
|
||
hindernis: darfGeloeschtWerden(person, req.person),
|
||
...anhang(id),
|
||
});
|
||
} catch (fehler) {
|
||
console.error("[workspace] Löschvorschau:", fehler?.message);
|
||
res.status(503).json({ fehler: "nicht_verfuegbar" });
|
||
}
|
||
});
|
||
|
||
personenRouter.delete("/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" });
|
||
if (!istDogFather(req.person)) return res.status(404).json({ fehler: "nicht_gefunden" });
|
||
|
||
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" });
|
||
|
||
const hindernis = darfGeloeschtWerden(person, req.person);
|
||
if (hindernis) return res.status(400).json({ fehler: hindernis });
|
||
|
||
/* Der Name muss wortwörtlich mitgeschickt werden. Nicht als Schikane:
|
||
Der Löschknopf sitzt neben dem Sperrknopf, und die beiden sind sehr
|
||
verschieden. Wer den Namen tippt, hat die Zeile gelesen, die er
|
||
trifft. */
|
||
if (String(req.body?.name ?? "").trim() !== person.name) {
|
||
return res.status(400).json({ fehler: "Zum Löschen bitte den Namen genau eingeben." });
|
||
}
|
||
|
||
const vorher = anhang(id);
|
||
|
||
/* Sicherung DIREKT davor -- scheitert sie, wird nicht gelöscht. Das
|
||
ist der Punkt, an dem aus "unumkehrbar" "wiederherstellbar" wird. */
|
||
let sicherung = null;
|
||
try {
|
||
/* Eigene Datei mit Uhrzeit -- drei Loeschungen hintereinander
|
||
wuerden sonst dreimal dieselbe Tagessicherung ueberschreiben,
|
||
und die erste geloeschte Person waere trotzdem weg. */
|
||
sicherung = sicherungJetzt("vor dem Löschen von " + person.name, true);
|
||
} catch (fehler) {
|
||
console.error("[workspace] Sicherung vor dem Löschen:", fehler?.message);
|
||
return res.status(503).json({
|
||
fehler: "Die Sicherung vor dem Löschen ist fehlgeschlagen – es wurde nichts gelöscht.",
|
||
});
|
||
}
|
||
|
||
/* Fremdschlüssel müssen an sein, sonst greifen CASCADE und SET NULL
|
||
nicht und es bleiben Reste zurück, die auf niemanden mehr zeigen. */
|
||
db().exec("PRAGMA foreign_keys = ON");
|
||
db().prepare("DELETE FROM personen WHERE id = ?").run(id);
|
||
|
||
protokolliere("person_geloescht", {
|
||
personId: req.person.id, rolle: req.person.rolle, ip: echteIp(req),
|
||
detail: `${person.name} (${person.rolle}) · Sicherung: ${sicherung.datei}`.slice(0, 160),
|
||
});
|
||
|
||
res.json({ ok: true, sicherung: sicherung.datei, war: vorher });
|
||
} catch (fehler) {
|
||
console.error("[workspace] Person löschen:", 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" });
|
||
}
|
||
});
|