Filipe, 22.09.2026: "ersetze die zentrale durch, Die IrrenAnstalt.
genau so geschrieben bitte. und alles was treff heißt oder wo treff
steht soll durch Rudel ersetzt werden bitte."
Die Schreibweise "IrrenAnstalt" mit grossem A in der Mitte ist so
gewollt. Das steht als Hinweis daneben, damit sie beim naechsten Mal
niemand "korrigiert".
WAS UMBENANNT WURDE: alles, was jemand LIEST -- Kachelnamen,
Ueberschriften, Markenzeilen, Saetze, Meldungen, Aufgabenvorlagen.
116 Zeilen in 42 Dateien.
WAS BLEIBT: Adressen (treff-regeln.html), Bezeichner (TREFF_ROLLEN),
Datenbankwerte (bereich = 'treff'), Abfrageteile (b=treff). Eine
Adresse umzubenennen bricht jedes Lesezeichen, und ein Datenbankwert
umzuschreiben waere eine Umstellung ohne Gegenwert. Dieselbe
Entscheidung wie heute frueh bei Dogi-Media, wo material.html auch
material.html geblieben ist.
DREI GRAMMATIKFEHLER IM EIGENEN ENTWURF, alle vor dem Ausliefern
gefunden -- ein blindes Ersetzen reicht hier nicht:
1. "der Treff" ist maennlich, "das Rudel" saechlich. Ohne Tabelle
waere ueberall "Der Rudel" gestanden. (Und "Treffer" waere zu
"Rudeler" geworden, "Treffen" zu "Rudelen" -- deshalb greift die
Regel nur bei Wortgrenze und nie vor einem Kleinbuchstaben.)
2. BINDESTRICH-ZUSAMMENSETZUNGEN. In "der Treff-Chat" gehoert der
Artikel zu "Chat", nicht zu "Treff". Der erste Durchlauf machte
daraus "das Rudel-Chat", "das Rudel-Kacheln" und "ein Rudel-Raum".
Jetzt greift die Artikelregel nur, wenn das Wort allein steht.
3. GESCHUETZTE LEERZEICHEN. In den Markenzeilen steht
`Der Treff`, damit die zwei Woerter nicht umbrechen. Das
Muster hat daran vorbeigegriffen: "Der Rudel", auf fuenf
Seiten. Jetzt wird das Trennzeichen mitgefasst und unveraendert
wieder eingesetzt.
Gefunden wurden alle drei, weil jede einzelne der 116 Zeilen vor dem
Schreiben als ALT/NEU ausgegeben und gelesen wurde -- und danach
gezielt nach falschen Artikeln gesucht ("den Rudel", "der Rudel",
"einen Rudel"). Uebrig blieben sechs Treffer, und die sind alle
richtig: "einen Rudel-Raum", "der Rudel-Chat", "den Rudel-Regeln" --
Zusammensetzungen, bei denen der Artikel zum letzten Wort gehoert.
UND ZWEI PRUEFUNGEN, die die Umbenennung nicht mitbekommen haetten:
`/Treff/.test(...)` und `/Treff/i.test(...)`. Ein regulaerer Ausdruck
ist keine Zeichenkette -- sie haetten ab sofort nach einem Wort
gesucht, das die Seite nicht mehr sagt, und waeren still rot geworden,
ohne dass etwas kaputt ist.
Geprueft, alle gruen: pruef-treff, pruef-treffchat,
pruef-deutsche-texte, pruef-uebernahme, pruef-crew-adresse,
pruef-willkommen, pruef-wege-nach-draussen, pruef-vorlagen,
pruef-start-ansicht, pruef-struktur.
Co-Authored-By: Claude Opus 5 <[email protected]>
1312 lines
62 KiB
JavaScript
1312 lines
62 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";
|
||
/* WO SICH DIESE ROLLE ANMELDET -- aus derselben Datei, die auch die
|
||
Weiche steuert. Eine zweite Liste von Adressen waere die, die beim
|
||
naechsten Umzug stehen bleibt. */
|
||
import { anmeldeAdresseFuer } from "./crew-adresse.js";
|
||
import {
|
||
db, protokolliere, echteIp, sitzungLesen, personAnlegen, codeNeu, sitzungToken, personSperren, betreuungSetzen, scoutZuteilungSetzen, istLeitung, istDogFather, siehtModis, siehtAlles, ROLLEN_SORTIERUNG, ROLLEN_REIHE, istSpicy, verborgeneIds, TEAM_DOGI_ROLLEN, darfAnlegen, darfRollenWechseln, darfRolleAendern, rollenZumAendern, hausBedingung, istHand,
|
||
} 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();
|
||
}
|
||
|
||
/* DIE RECHTE HAND LIEST MIT (10.09.2026).
|
||
|
||
Filipe: "ich muss alle kategorien da sehen. und ich will dass die
|
||
rechte hand auch alle sieht."
|
||
|
||
LESEN, NICHT VERWALTEN -- und das ist keine Vorsicht von mir,
|
||
sondern sein eigenes Wort: "sieht". Codes, Sperren, Loeschen,
|
||
Rollen vergeben und das Protokoll bleiben bei DogFather; "Nur
|
||
DogFather hat alle endgueltigen Rechte" gilt unveraendert.
|
||
|
||
DIESELBE BAUWEISE WIE BEI SPICY MEDIA darueber: eine Methode, eine
|
||
Adresse, eine Rolle. Wer hier etwas anderes einbaut, hat zwei
|
||
Fassungen derselben Ausnahme -- und die zweite ist die, die
|
||
irgendwann mehr durchlaesst als gedacht.
|
||
|
||
WELCHE Menschen sie zu sehen bekommt, entscheidet sie nicht: Auf
|
||
ihrer Adresse grenzt `hausBedingung` die Liste ohnehin auf Team
|
||
Dogi ein. */
|
||
/* AUCH DIE LINKE HAND -- ABER NUR DIE LISTE (21.09.2026).
|
||
Sie muss sehen, wem sie eine Aufgabe geben kann. Anlegen darf sie
|
||
niemanden; das entscheidet nicht diese Zeile, sondern ANLEGBAR. */
|
||
if (istHand(person) && nurListe) {
|
||
req.person = person;
|
||
return next();
|
||
}
|
||
|
||
/* SPICY MEDIA DARF ROLLEN WECHSELN (11.09.2026).
|
||
|
||
Filipe: "ich will dass die rolle spicy und dogfather, auch die
|
||
rollen wechseln koennen wenn die personen schon drin sind. von
|
||
alle kategorien, creator, scouts, manager spicy."
|
||
|
||
DRITTE AUSNAHME, GLEICHE BAUWEISE wie die beiden darueber: eine
|
||
Methode, eine Adresse, eine Rolle. Wer hier etwas anderes baut,
|
||
hat drei Fassungen derselben Ausnahme -- und die dritte laesst
|
||
irgendwann mehr durch als gedacht.
|
||
|
||
Codes, Sperren, Loeschen, Zuteilung und Protokoll bleiben bei
|
||
DogFather. Filipe hat das Rollenwechseln genannt, nicht die
|
||
Verwaltung.
|
||
|
||
DER PFAD TRAEGT EINE NUMMER, laesst sich also nicht wie oben
|
||
vergleichen. Das Muster ist absichtlich streng: genau
|
||
/personen/<Ziffern>/rolle, nichts davor und nichts danach.
|
||
|
||
WELCHE Rolle sie vergeben darf, steht NICHT hier -- das entscheidet
|
||
ANLEGBAR in der Route. Diese Zeile oeffnet nur die Tuer. */
|
||
/* DIE RECHTE HAND LEGT PERSONEN AN UND SIEHT IHRE CODES (22.09.2026).
|
||
|
||
Filipe: "damit sie das auch machen kann wenn er live ist."
|
||
|
||
NUR DIE RECHTE, NICHT DIE LINKE. `istHand()` wuerde beide treffen;
|
||
fuer die linke Hand ist "legt niemanden an" eine ausdrueckliche
|
||
Entscheidung vom 21.09. Deshalb steht hier die Rolle und nicht der
|
||
Sammelbegriff -- der haette die Entscheidung stillschweigend
|
||
umgedreht.
|
||
|
||
ZWEI WEGE, NICHT DIE GANZE VERWALTUNG: anlegen und einen Code neu
|
||
erzeugen. Sperren, Loeschen, Betreuung und Protokoll bleiben bei
|
||
DogFather. Und WELCHE Rolle sie anlegen darf, entscheidet nicht
|
||
diese Zeile, sondern ANLEGBAR in der Route -- diese Tuer oeffnet
|
||
nur den Weg.
|
||
|
||
WARUM AUCH "Code neu erzeugen": Ein Code wird genau einmal
|
||
angezeigt. Geht er verloren, ist der Zugang ohne diesen Weg tot --
|
||
und dann muesste doch wieder DogFather ran, also genau das, was
|
||
nicht sein soll. */
|
||
const anlegeWeg = req.method === "POST"
|
||
&& req.baseUrl + req.path === "/workspace/api/verwaltung/personen";
|
||
const codeWeg = req.method === "POST"
|
||
&& /^\/workspace\/api\/verwaltung\/personen\/[0-9]+\/code$/
|
||
.test(req.baseUrl + req.path);
|
||
if (person.rolle === "hand" && (anlegeWeg || codeWeg)) {
|
||
req.person = person;
|
||
return next();
|
||
}
|
||
|
||
const rollenWeg = req.method === "PUT"
|
||
&& /^\/workspace\/api\/verwaltung\/personen\/[0-9]+\/rolle$/
|
||
.test(req.baseUrl + req.path);
|
||
if (darfRollenWechseln(person) && rollenWeg) {
|
||
req.person = person;
|
||
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);
|
||
|
||
/* DIE RECHTE HAND NUR AN DENEN, DIE SIE AUCH ANLEGEN DUERFTE
|
||
(22.09.2026).
|
||
|
||
Ohne diese Zeile koennte sie den Code einer LINKEN HAND neu
|
||
erzeugen -- und haette damit einen Zugang, der fast so viel darf
|
||
wie sie selbst, in der Hand. Das ist nicht gemeint und waere auch
|
||
nicht aufgefallen: Die Schranke darunter kennt nur "admin" und
|
||
"manager".
|
||
|
||
Gefragt wird dieselbe Liste wie beim Anlegen. Eine zweite waere
|
||
die, die beim naechsten Umbau auseinanderlaeuft. */
|
||
if (req.person?.rolle === "hand" && ziel
|
||
&& !darfAnlegen(req.person).includes(ziel.rolle)) {
|
||
return res.status(403).json({
|
||
fehler: "An dieser Rolle ändert die rechte Hand nichts.",
|
||
});
|
||
}
|
||
|
||
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
|
||
WHERE 1=1${hausBedingung(req.person, "p.rolle")}
|
||
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. */
|
||
/* SIE GEHEN AN ALLE, DIE DIE LISTE UEBERHAUPT BEKOMMEN -- also
|
||
an DogFather und an die rechte Hand, nicht an Spicy Media oder
|
||
einen Manager.
|
||
|
||
Sie sind nicht nur die Auswahl beim Anlegen, sondern seit
|
||
heute auch die UEBERSCHRIFTEN der Abschnitte: Der Browser
|
||
kennt nur die fuenf Rollen aus bereiche.js, und wer dort nicht
|
||
steht, wurde bis eben nicht gezeichnet. VanVan verschwand
|
||
genau in dem Moment aus der Liste, in dem sie die rechte Hand
|
||
wurde -- ohne Fehler, ohne Luecke, ohne Hinweis. */
|
||
/* DIE COMMUNITY STEHT NICHT IN DERSELBEN BEDINGUNG (11.09.2026).
|
||
|
||
Die beiden Eintraege unten haengen an `siehtModis` -- wer die
|
||
Modis sieht, darf sie auch anlegen. Fuer die Community gilt das
|
||
NICHT: Einen Zugang zum Treff vergibt allein DogFather (Plan
|
||
Teil C, Zeile "Zugang anlegen"). Ein Modi, der einen anlegen
|
||
koennte, koennte sich selbst Verstaerkung holen.
|
||
|
||
GEFRAGT WIRD `darfAnlegen` UND NICHT DIE ROLLE. Dieselbe
|
||
Auskunft, aus der auch die Route ihre Absage holt -- deshalb
|
||
kann hier kein Knopf erscheinen, den der Server ablehnt, und
|
||
keiner fehlen, den er annehmen wuerde. Auf der Team-Adresse
|
||
faellt er dadurch von selbst weg (darfAnlegen filtert dort auf
|
||
die Team-Rollen), und genau das ist richtig: Wer dort jemanden
|
||
anlegt, meint jemanden aus dem Team. */
|
||
/* DIE REIHENFOLGE KOMMT AUS ROLLEN_REIHE, nicht aus dieser Liste
|
||
(14.09.2026).
|
||
|
||
Filipe, mit dem Bildschirmfoto: "rechte hand soll immer ueber
|
||
modi stehen bitte." Hier stand Modi zuerst -- ROLLEN_REIHE
|
||
hatte die Rechte Hand dagegen laengst vor den Modis, und die
|
||
SQL-Sortierung ebenfalls. Eine Aussage, zwei Stellen, und die
|
||
zweite war die falsche: dasselbe Muster wie heute schon dreimal.
|
||
|
||
Sortiert wird deshalb ABGELEITET. Wer die Reihenfolge aendern
|
||
will, aendert ROLLEN_REIHE -- und alles andere zieht nach,
|
||
statt nachgezogen werden zu muessen.
|
||
|
||
Was dort NICHT steht (heute: "gast"), wandert ans Ende statt
|
||
stillschweigend nach vorn. */
|
||
zusatzrollen: [
|
||
...(darfAnlegen(req.person).includes("gast")
|
||
? [{
|
||
wert: "gast", name: "Community", symbol: "leute",
|
||
text: "Zugang zum Rudel. Sieht die sieben Bretter der Community "
|
||
+ "und sonst nichts \u2013 keine Aufgaben, keine Termine, "
|
||
+ "keine Namen aus dem Team.",
|
||
}]
|
||
: []),
|
||
...(siehtModis(req.person)
|
||
? [{
|
||
wert: "modi", name: "Modi",
|
||
/* DIE PFOTE (Filipe, 10.09.2026 mit Bildschirmfoto: "bei
|
||
rechte hand soll der husky sein und bei modis soll die
|
||
pfote sein").
|
||
|
||
Dritte Fassung, und die beiden davor sind lehrreich: Zuerst
|
||
stand hier das Schild -- falsch, es gehoert dem Scout und
|
||
stellte die Modis neben die Agentur. Dann der Husky, "das
|
||
gleiche wie bei dogfather". Beide Male ging es um dieselbe
|
||
Aussage: Die Modis gehoeren zu Dogi.
|
||
|
||
Solange es hier zwei Rollen gab, sagte der Husky das. Seit
|
||
es drei sind, sagt es die GRUPPE: Husky bei den beiden mit
|
||
dem Ueberblick, Pfote bei denen, die taeglich unterwegs
|
||
sind. Die Aussage ist dieselbe geblieben, das Zeichen dafuer
|
||
nicht. */
|
||
symbol: "pfote",
|
||
/* 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.",
|
||
}, {
|
||
/* DIE STELLVERTRETUNG (10.09.2026). Filipe: "3 rollen.
|
||
dogfather. rechte hand und modis."
|
||
|
||
Blueprint V3.0, Kapitel 3.1: "1 (austauschbar)" -- gedacht
|
||
ist EINE Person. Erzwungen wird das hier bewusst NICHT:
|
||
Beim Wechsel braucht es einen Moment, in dem die alte noch
|
||
da und die neue schon angelegt ist. Eine Sperre haette
|
||
genau diesen Moment unmoeglich gemacht.
|
||
|
||
DER HUSKY, wie bei DogFather: Sie vertritt ihn, und das
|
||
Zeichen sagt das -- ohne sie ueber jemanden zu stellen,
|
||
denn dasselbe Zeichen heisst "gleiche Sicht", nicht
|
||
"hoeher". Die Pfote, die hier zuerst stand, ist zu den
|
||
Modis gewandert. */
|
||
wert: "hand", name: "Rechte Hand", symbol: "husky",
|
||
text: "Vertritt dich im Alltag und koordiniert das Team. Sieht "
|
||
+ "dieselben Aufgaben, Ideen und Angebote wie du – aber keine "
|
||
+ "privaten Kalender und keine Einzelchats.",
|
||
}, {
|
||
/* DIE LINKE HAND (21.09.2026). Filipe: "Die linke Hand
|
||
erhaelt grundsaetzlich dieselben Rechte wie die rechte Hand
|
||
in allen Bereichen, die die Modis, Aufgaben,
|
||
Aufgabenzuweisungen, Uebersichten, Kommunikation und
|
||
Auswertungen betreffen."
|
||
|
||
DERSELBE HUSKY wie bei der rechten Hand -- sie tun
|
||
dasselbe, und ein eigenes Zeichen wuerde einen Unterschied
|
||
behaupten, den es im Alltag nicht gibt. Was sie
|
||
unterscheidet, steht im Text darunter, und zwar
|
||
vollstaendig: Wer nur "wie die rechte Hand" liest, legt
|
||
sie an und wundert sich spaeter. */
|
||
wert: "linke", name: "Linke Hand", symbol: "husky",
|
||
text: "Dieselben Team- und Aufgabenrechte wie die rechte Hand – "
|
||
+ "verteilt Aufgaben, sieht alle Auswertungen und den ganzen "
|
||
+ "Rudel. Kann aber keine Personen anlegen, keine Rollen "
|
||
+ "ändern und sieht weder Bewerbungen noch den vertraulichen "
|
||
+ "Meldeweg.",
|
||
}]
|
||
: []),
|
||
].sort((a, b) => {
|
||
const platz = (r) => {
|
||
const i = ROLLEN_REIHE.indexOf(r);
|
||
return i < 0 ? ROLLEN_REIHE.length : i;
|
||
};
|
||
return platz(a.wert) - platz(b.wert);
|
||
}),
|
||
});
|
||
} 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,
|
||
adresse: anmeldeAdresseFuer(neu.rolle),
|
||
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.
|
||
|
||
NACHTRAG 10.09.2026 — JETZT AUCH SCOUTS, UND TROTZDEM ZWEI TUEREN.
|
||
|
||
Filipe: "die spicy rolle soll auch manager und scouts hinzufuegen
|
||
koennen." Fuer Scouts gab es bis heute ueberhaupt keinen Weg ausser
|
||
ueber DogFather.
|
||
|
||
Der naheliegende Griff waere gewesen, hier ein Feld `rolle`
|
||
anzunehmen. Genau davon raet der Absatz oben ab, und der Rat gilt
|
||
weiter: Eine Tuer, die NICHTS anderes kann, als eine bestimmte Rolle
|
||
anzulegen, ist sicherer als eine, die vorher nachfragt. Eine
|
||
vergessene Abfrage waere ein zweiter Zugang mit allen Rechten; eine
|
||
vergessene Abfrage an einer Tuer, die nur "scout" kennt, ist ein
|
||
Aergernis, aber keine Luecke.
|
||
|
||
Also weiterhin zwei Tueren mit je einer festen Rolle — nur nicht mehr
|
||
zweimal derselbe Text. `teamTuer(rolle)` baut den Griff, die Rolle
|
||
wird beim EINHAENGEN festgelegt und kommt nie aus dem Aufruf.
|
||
|
||
Und die Frage "darf diese Person das ueberhaupt" beantwortet nicht
|
||
mehr diese Datei, sondern `darfAnlegen` in workspace.js — dieselbe
|
||
Auskunft, aus der auch die Oberflaeche ihre Knoepfe baut. Zwei Listen
|
||
fuer dieselbe Aussage waren der Grund, warum Spicy Media den
|
||
Manager-Knopf nie zu sehen bekam. */
|
||
function teamTuer(rolle) {
|
||
return (req, res) => {
|
||
try {
|
||
const person = sitzungLesen(req);
|
||
if (!person) return res.status(401).json({ fehler: "nicht_angemeldet" });
|
||
/* ZWEI SCHLOESSER, ABSICHTLICH. `siehtAlles` sagt, wer diese Tuer
|
||
ueberhaupt sieht; `darfAnlegen` sagt, ob sie fuer genau diese
|
||
Rolle offen ist. Faellt eines der beiden weg, haelt das andere. */
|
||
if (!siehtAlles(person)) return res.status(404).json({ fehler: "nicht_gefunden" });
|
||
if (!darfAnlegen(person).includes(rolle)) {
|
||
return res.status(403).json({ fehler: "Diese Rolle legst du nicht an." });
|
||
}
|
||
|
||
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, rolle, { ...person, ip: echteIp(req) });
|
||
res.status(201).json({ id: neu.id, name: neu.name, rolle: neu.rolle, code: neu.code,
|
||
adresse: anmeldeAdresseFuer(neu.rolle) });
|
||
} catch (fehler) {
|
||
console.error(`[workspace] ${rolle} anlegen:`, fehler?.message);
|
||
res.status(503).json({ fehler: "nicht_verfuegbar" });
|
||
}
|
||
};
|
||
}
|
||
personenRouter.post("/workspace/api/manager-anlegen", gleicheHerkunft, teamTuer("manager"));
|
||
personenRouter.post("/workspace/api/scout-anlegen", gleicheHerkunft, teamTuer("scout"));
|
||
|
||
/* 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. */
|
||
/* Die verborgenen Rollen vergibt nur DogFather. Aus der Menge
|
||
gelesen und nicht als `rolle === "modi"` hingeschrieben: Seit dem
|
||
10.09.2026 sind es zwei, und die zweite waere hier sonst still
|
||
durchgerutscht -- ein Manager haette eine rechte Hand anlegen
|
||
koennen, ohne dass irgendwo etwas rot wird. */
|
||
/* GEFRAGT WIRD `darfAnlegen`, NICHT `ROLLEN` (17.09.2026).
|
||
|
||
HIER STAND `!ROLLEN.includes(rolle)`, und `ROLLEN` ist
|
||
`ROLLEN_REIHE` -- die SORTIERREIHENFOLGE. Dort fehlt "gast" mit
|
||
Absicht (der Kommentar in workspace.js sagt es woertlich: "steht
|
||
mit Absicht NICHT in der Liste, sie faellt ans Ende").
|
||
|
||
Folge: Die Oberflaeche bot "Community" an -- sie holt ihre
|
||
Knoepfe aus `darfAnlegen` --, und der Server antwortete beim
|
||
Anlegen "Unbekannte Rolle." Filipe, am 17.09.: "und wieso kann
|
||
ich immer noch keine community rolle erstellen?"
|
||
|
||
Das ist die Falle, vor der in diesem Haus an zehn Stellen
|
||
gewarnt wird: EINE Liste, zwei Fragen. `ROLLEN_REIHE` beantwortet
|
||
"in welcher Reihenfolge", nicht "welche gibt es".
|
||
|
||
`darfAnlegen` beantwortet genau die Frage, die hier zaehlt --
|
||
und es ist dieselbe Auskunft, aus der die Oberflaeche ihre
|
||
Knoepfe baut. Damit koennen die beiden nicht mehr auseinander-
|
||
laufen. Die Team-Dogi-Regel steckt schon darin (ANLEGBAR gibt
|
||
"hand" und "modi" nur DogFather), ebenso die Regel, dass sich
|
||
kein zweiter DogFather anlegen laesst -- die das alte `ROLLEN`
|
||
uebrigens NICHT abgedeckt hat. */
|
||
if (!darfAnlegen(req.person).includes(rolle)) {
|
||
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.
|
||
|
||
SEIT DEM 10.09.2026 STEHT DIE ANTWORT NICHT MEHR HIER, sondern in
|
||
`darfAnlegen` (workspace.js) — derselben Auskunft, aus der auch
|
||
die beiden Team-Tueren und die Oberflaeche ihre Knoepfe bauen.
|
||
Vorher stand die Regel an vier Stellen in drei Dateien, und genau
|
||
daran ist sie zerbrochen: Zwei davon widersprachen einander, und
|
||
Spicy Media bekam den Manager-Knopf nie zu sehen.
|
||
|
||
Am Ergebnis aendert sich nichts — die Liste sagt fuer DogFather
|
||
"alles" und fuer einen Manager "nur Creator", also genau das, was
|
||
hier vorher ausgeschrieben stand. Es steht nur noch an einer
|
||
Stelle. */
|
||
if (!darfAnlegen(req.person).includes(rolle)) {
|
||
return res.status(403).json({
|
||
fehler: "Diese Rolle legst du nicht 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,
|
||
adresse: anmeldeAdresseFuer(neu.rolle) });
|
||
} 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, adresse: anmeldeAdresseFuer(neu.rolle) });
|
||
} 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" });
|
||
}
|
||
});
|
||
|
||
/* =====================================================================
|
||
DIE ROLLE EINES MENSCHEN AENDERN (10.09.2026)
|
||
|
||
Filipe wollte VanVan die Rolle "Rechte Hand" geben -- und konnte
|
||
nicht. Beim Nachsehen: Es gibt im ganzen Server KEINE einzige Stelle,
|
||
die `personen.rolle` aendert. Anlegen ja, sperren ja, loeschen ja;
|
||
die Rolle war ab dem ersten Tag in Stein.
|
||
|
||
Das ist kein Schoenheitsfehler. Ohne diese Funktion bleibt nur, den
|
||
Menschen zu loeschen und neu anzulegen -- und daran haengen seine
|
||
Aufgaben, seine Nachrichten, seine Eintraege, sein ganzer Verlauf.
|
||
Kapitel 4 des Pflichtenhefts verlangt ausdruecklich das Gegenteil:
|
||
"Rolle/Kategorie zuweisen".
|
||
|
||
---------------------------------------------------------------------
|
||
FUENF SICHERUNGEN, UND JEDE HAT IHREN GRUND
|
||
|
||
1. NUR DOGFATHER. Wie beim Anlegen. Wer Rollen vergeben kann, kann
|
||
sich selbst zum DogFather machen -- deshalb haengt die ganze
|
||
Verwaltung an `nurAdmin`, und diese Route erst recht.
|
||
|
||
2. NIE DIE EIGENE. Wer sich selbst herabstuft, sperrt sich aus dem
|
||
Haus aus, und niemand kann es rueckgaengig machen -- die Funktion
|
||
dafuer haengt ja an der Rolle, die er gerade abgegeben hat. Das
|
||
ist keine Warnung wert, das ist eine Tuer, die zubleibt.
|
||
|
||
3. NIE DEN LETZTEN DOGFATHER. Dieselbe Ueberlegung eine Ebene weiter,
|
||
und dieselbe wie beim Sperren darunter: Ohne einen aktiven
|
||
DogFather gibt es niemanden mehr, der Zugaenge vergibt.
|
||
|
||
4. ALLE SITZUNGEN DIESER PERSON ENDEN. Eine Sitzung gehoert seit dem
|
||
10.09.2026 zu einer ADRESSE, und welche das ist, entscheidet die
|
||
Rolle (sitzungPasstZurAdresse). Wer eben noch DogFather war und
|
||
jetzt rechte Hand ist, sitzt sonst mit einer Sitzung da, die auf
|
||
der Agenturadresse laeuft und dort nicht mehr hingehoert. Das
|
||
faellt beim naechsten Klick auf, nicht sofort -- und ein halb
|
||
gueltiger Zustand ist das Unangenehmste, was eine Rechteaenderung
|
||
hinterlassen kann.
|
||
|
||
5. DER CODE BLEIBT. Er haengt am Menschen, nicht an der Rolle. Ihn
|
||
hier mitzutauschen waere bequem und falsch: Dann muesste jede
|
||
Rollenaenderung von einem Gespraech begleitet sein ("dein Zugang
|
||
ist ein anderer"), und wer das vergisst, sperrt jemanden aus,
|
||
ohne es zu merken. Wer einen neuen Code will, hat den Knopf
|
||
daneben.
|
||
===================================================================== */
|
||
personenRouter.put("/workspace/api/verwaltung/personen/:id/rolle", gleicheHerkunft,
|
||
(req, res) => {
|
||
try {
|
||
const id = Number(req.params.id);
|
||
const rolle = String(req.body?.rolle ?? "");
|
||
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" });
|
||
|
||
/* ==== DARF ICH DIESEN MENSCHEN ANFASSEN? (20.09.2026) =========
|
||
|
||
Seit die rechte Hand Rollen aendern darf, genuegt "ist
|
||
angemeldet und kam durch die Schranke" nicht mehr. Die Regel
|
||
steht in darfRolleAendern() -- an EINER Stelle, damit die
|
||
Oberflaeche und der Server nicht auseinanderlaufen.
|
||
|
||
Sie beantwortet drei Fragen auf einmal: nicht die eigene Rolle,
|
||
nicht DogFather, und fuer die rechte Hand auch keine zweite
|
||
rechte Hand. */
|
||
/* DIE EIGENE ZEILE ZUERST -- und mit 400, nicht 403.
|
||
|
||
Sie ist keine Rechtefrage, sondern eine unsinnige Bitte: Es
|
||
gibt niemanden, der sie duerfte. Eine 403 haette hier
|
||
ausserdem den Fall "der letzte DogFather tritt zurueck"
|
||
verschluckt, den die Sicherung weiter unten mit einem
|
||
verstaendlichen Satz beantwortet. */
|
||
if (person.id === req.person.id) {
|
||
return res.status(400).json({
|
||
fehler: "Deine eigene Rolle kannst du nicht ändern. Das muss jemand anderes tun.",
|
||
});
|
||
}
|
||
if (!darfRolleAendern(req.person, person)) {
|
||
return res.status(403).json({ fehler: "Diese Rolle darfst du nicht ändern." });
|
||
}
|
||
|
||
/* Dieselbe Auskunft wie beim Anlegen: Wer eine Rolle nicht
|
||
vergeben darf, darf sie auch nicht zuweisen. Sonst waere das
|
||
Zuweisen der bequemere Weg an der Regel vorbei. */
|
||
/* EINE FRAGE, EINE ANTWORT (17.09.2026).
|
||
|
||
Hier standen ZWEI Pruefungen untereinander. Die erste las
|
||
`ROLLEN` -- die Sortierreihenfolge, in der "gast" mit Absicht
|
||
fehlt -- und lehnte deshalb ab, BEVOR die zweite, richtige
|
||
ueberhaupt drankam. Eine bestehende Person liess sich damit
|
||
nicht zu "Community" machen, aus demselben Grund wie beim
|
||
Anlegen.
|
||
|
||
Gefunden hat das nicht das Lesen, sondern pruef-rollen-anlegen:
|
||
Die Suche nach der alten Zeile reichte noch in diese Route
|
||
hinein, und dort stand sie ein zweites Mal.
|
||
|
||
DIE ZWEITE ANTWORT WAR AUSSERDEM GESPRAECHIGER als die erste
|
||
("Diese Rolle vergibst du nicht." mit 403 gegen "Unbekannte
|
||
Rolle." mit 400). Wer durchprobiert, haette daran ablesen
|
||
koennen, welche Rollen es gibt. Jetzt beide Faelle wortgleich
|
||
-- wie beim Anlegen. */
|
||
/* WELCHE ROLLE DARF DIESE PERSON VERGEBEN.
|
||
|
||
Hier stand `darfAnlegen`. Das war richtig, solange Vergeben und
|
||
Anlegen dasselbe waren -- seit dem 20.09. sind sie es nicht
|
||
mehr: Die rechte Hand darf NIEMANDEN anlegen und trotzdem
|
||
'modi' und 'gast' vergeben, ein Manager darf einen Creator
|
||
anlegen und gar keine Rolle vergeben. Gemessen:
|
||
|
||
admin anlegen = aendern (sieben Rollen)
|
||
spicy anlegen = aendern (vier)
|
||
hand anlegen = — aendern = modi, gast
|
||
manager anlegen = creator aendern = —
|
||
|
||
WORTGLEICH MIT DEM FALL DARUEBER, und das ist Absicht: Zwei
|
||
verschiedene Antworten -- "Unbekannte Rolle" gegen "die darfst
|
||
du nicht vergeben" -- verraten beim Durchprobieren, WELCHE
|
||
Rollen es gibt. Genau davor warnt der Kommentar von 17.09., und
|
||
eine 403 an dieser Stelle hatte die Luecke am 20.09. kurz
|
||
wieder aufgemacht. */
|
||
if (!rollenZumAendern(req.person).includes(rolle)) {
|
||
return res.status(400).json({ fehler: "Unbekannte Rolle." });
|
||
}
|
||
if (person.rolle === rolle) {
|
||
return res.status(400).json({ fehler: "Diese Rolle hat sie schon." });
|
||
}
|
||
|
||
/* SICHERUNG 6 -- NEU AM 11.09.2026, WEIL SICH DIE TUER GEOEFFNET HAT.
|
||
|
||
Seit Spicy Media Rollen wechseln darf, gibt es einen Fall, den
|
||
es vorher nicht geben konnte: jemand ohne DogFather-Rolle, der
|
||
an einer DogFather-Zeile steht.
|
||
|
||
DIE PRUEFUNGEN DARUEBER FANGEN DAS NICHT. Sie sehen auf die
|
||
ZIEL-Rolle ("darfst du 'manager' vergeben?") -- und 'manager'
|
||
darf Spicy Media vergeben. Dass die Person, die da herabgestuft
|
||
wird, DogFather ist, steht in der AUSGANGS-Rolle, und die kam
|
||
bis heute nirgends vor.
|
||
|
||
Sicherung 3 (nie den letzten DogFather) haette es heute
|
||
zufaellig abgefangen, weil es genau einen gibt. Eine Sperre,
|
||
die nur wegen einer Zahl im Bestand haelt, ist keine.
|
||
|
||
Zusaetzlich sieht Spicy Media DogFather gar nicht in der Liste
|
||
(`req.ohneDogFather`). Das ist eine Sicht, keine Schranke -- wer
|
||
die Nummer kennt, ruft den Weg direkt auf. */
|
||
if (person.rolle === "admin" && !istDogFather(req.person)) {
|
||
return res.status(403).json({ fehler: "An der DogFather-Rolle ändert nur DogFather." });
|
||
}
|
||
|
||
/* SICHERUNG 2 */
|
||
if (id === req.person.id) {
|
||
return res.status(400).json({ fehler: "Die eigene Rolle lässt sich nicht ändern.",
|
||
hinweis: "Sonst könntest du dich selbst aussperren, ohne es rückgängig machen zu können." });
|
||
}
|
||
|
||
/* SICHERUNG 3 -- gezaehlt werden die AKTIVEN. Ein gesperrter
|
||
DogFather kann niemanden hereinlassen; ihn mitzuzaehlen waere
|
||
eine Sicherung, die sich selbst belügt. */
|
||
if (person.rolle === "admin" && rolle !== "admin") {
|
||
const uebrig = db().prepare(
|
||
"SELECT COUNT(*) AS n FROM personen WHERE rolle = 'admin' AND aktiv = 1 AND id <> ?")
|
||
.get(id).n;
|
||
if (uebrig < 1) {
|
||
return res.status(400).json({ fehler: "Das ist der letzte DogFather-Zugang." });
|
||
}
|
||
}
|
||
|
||
const vorher = person.rolle;
|
||
db().prepare("UPDATE personen SET rolle = ? WHERE id = ?").run(rolle, id);
|
||
|
||
/* SICHERUNG 4 */
|
||
const raus = db().prepare("DELETE FROM sitzungen WHERE person_id = ?").run(id);
|
||
|
||
protokolliere("rolle_geaendert", {
|
||
personId: req.person.id, rolle: req.person.rolle, ip: echteIp(req),
|
||
detail: `#${id} ${person.name}: ${vorher} -> ${rolle}`.slice(0, 120),
|
||
});
|
||
res.json({ id, name: person.name, vorher, rolle,
|
||
abgemeldet: Number(raus.changes || 0) });
|
||
} catch (fehler) {
|
||
console.error("[workspace] Rolle aendern:", 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" });
|
||
}
|
||
});
|