Wunsch: "jeder manager soll auch immer nur seine und die seiner scouts
zugeteilten creator und creator daten sehen. und nicht die der anderen."
DIE KETTE WAR GEBAUT -- SIE WURDE NUR NICHT BENUTZT
Manager -> seine Scouts -> deren Creator steht seit dem 01.09.2026 an
genau einer Stelle (betreuteIds). Die Frage war eine andere: Benutzt sie
auch JEDER Weg, der Creator-Daten herausgibt? Ueber zwanzig Stellen
prueften die ROLLE statt der ZUTEILUNG -- "ist Leitung? dann alles".
NICHT GELESEN, SONDERN GEMESSEN
server/pruef-manager-sicht.mjs baut zwei Managerinnen mit vollstaendig
getrennten Creators. Bei der fremden heisst ALLES "GEHEIM..." -- Aufgabe,
Termin, Bereichseintrag, Datei, Steckbrief, Content-Saeule. Danach wird
jede der 31 Leseschnittstellen abgefragt und die ganze Antwort danach
durchsucht. Ein Leck faellt damit auf, egal wo es sitzt und egal, ob ich
es beim Lesen uebersehen haette.
GEFUNDEN: DREIZEHN. Alle geschlossen:
Kalender fremde Fristen -- besonders unangenehm, weil es
nicht wie ein Leck aussieht: eine kleine orange
Marke mit einem Titel, in dem fremde Vorhaben stehen
Personenauswahl alle Namen im Zuweisungsfeld
Dateien alle Namen in der Freigabe-Auswahl
Uebersicht Gesamtuebersicht ueber ALLE Creator
Report Auswahl UND Auswertung ueber den ganzen Bestand
Start-Check alle Creator zur Auswahl
Steckbriefe Bild, Kanaele, "ueber mich" von allen
Profile alle Profile, samt interner Notiz
Schulung Schulungsstand aller Creator
Suche Creator-Profile aller -- die unauffaelligste Stelle:
Man sucht etwas anderes und bekommt fremde Namen
Content-Balance Themensaeulen fremder Kanaele (die Abfrage daneben
war korrekt eingeschraenkt, DIESE hatte eine eigene
Bedingung)
darfCreator eine einzige Zeile -- sie hing an Profil,
Start-Check und Uebersicht gleichzeitig. Es reichte,
eine Nummer in die Adresse zu schreiben.
Die Antwort steht jetzt an EINER Stelle: sichtbareCreatorIds und
sichtbarePersonenIds in workspace.js. Rueckgabe null heisst "alle" und
gilt allein DogFather -- bewusst kein leeres Feld: Eine leere Liste
bedeutet "niemand", und die Verwechslung der beiden macht aus einer
Sperre eine Freigabe.
ZWEI DINGE, DIE ICH MIR SELBST NACHTRAGEN MUSS
1. Beim Stopfen fehlte einmal ein Import. Der Weg warf einen Fehler,
antwortete 503 -- und weil in einer Fehlermeldung kein "GEHEIM" steht,
meldete die Pruefung "kein Leck". Sie war gruen, weil der Weg KAPUTT
war. Die Pruefung zaehlt jetzt beides: nichts durchsickern UND
antworten.
2. Ein Fehlalarm: Die Suche gibt den SUCHBEGRIFF in ihrer Antwort
zurueck. Wer nach "GEHEIM" sucht, findet das Wort zwangslaeufig --
auch bei null Treffern. Ich haette um ein Haar ein Leck "repariert",
das es nie gab. Das Echo wird jetzt entfernt, bevor gemessen wird.
FOLGEN, bewusst in Kauf genommen:
* Ein Manager ohne Zuteilung sieht keinen Creator. Die Uebersicht sagt
ihm das jetzt in einem Satz, statt leer zu bleiben.
* Er kann nur noch IN SEINEN Creator-Bereichen schreiben (darfCreator).
ZWEI PRUEFUNGEN UMGEDREHT statt geloescht -- eine geloeschte Pruefung
hinterlaesst keine Spur davon, dass hier einmal etwas anderes galt:
pruef-uebersicht ("Manager sieht dasselbe" -> "nur seine zugeteilten",
mit beiden Faellen) und pruef-scout-zuteilung ("sieht die ganze
Personenliste" -> "landet auf der Startseite").
GEPRUEFT: 43 neue Pruefungen, dazu 23 bestehende Laeufe gruen --
Startansicht 133, Rollen 97, Kalender 84, Serien 67, Steckbrief 65,
Handy 50, Sicht 48, Ampel 47, Content 45, Aufgabenbrett 44, Schulung 41,
Bereiche 37, Scout-Zuteilung 36, Uebersicht 35, Personenliste 33,
Freie Namen 32, Team 30, Protokoll-Loeschen 20, Personenformular 20,
Formulare 19, Betreuung 18, Code 17, Grosscheck 15.
Co-Authored-By: Claude Opus 5 <[email protected]>
217 lines
9.1 KiB
JavaScript
217 lines
9.1 KiB
JavaScript
/* =====================================================================
|
|
workspace-profil.js — Creator-Profile (Onboarding-Teil des Konzepts):
|
|
Stammdaten, Ziele, 90-Tage-Plan.
|
|
|
|
Sicherheitskern dieses Moduls ist das Feld `admin_notiz`. Das Konzept
|
|
fordert "private Admin-Notizen separat" -- diese Notiz darf ein Creator
|
|
unter keinen Umständen sehen, auch nicht als Nebeneffekt einer
|
|
Sammelabfrage. Deshalb wird sie nicht nur im Browser ausgeblendet,
|
|
sondern gar nicht erst mitgeschickt: Die Spaltenliste der Abfrage
|
|
haengt an der Rolle (siehe FELDER_OFFEN / FELDER_ADMIN).
|
|
===================================================================== */
|
|
|
|
import express from "express";
|
|
import {
|
|
sichtbareCreatorIds,
|
|
db, protokolliere, echteIp, sitzungLesen, darfCreator, betreuteIds, istLeitung,
|
|
istDogFather,
|
|
} from "./workspace.js";
|
|
|
|
export const profilRouter = express.Router();
|
|
|
|
/* Felder, die auch der Creator selbst sehen und ändern darf. */
|
|
const FELDER_OFFEN = [
|
|
"handles", "nische", "live_zeiten", "technik",
|
|
"ziel_live", "ziel_content", "ziel_community", "ziel_technik",
|
|
"plan_prio1", "plan_prio2", "plan_prio3",
|
|
];
|
|
/* Nur fürs Management. `admin_notiz` ist der eigentliche Grund für die
|
|
Trennung, die Termine stehen hier, weil sie zur Steuerung gehören. */
|
|
const FELDER_ADMIN = ["plan_start", "naechster_review", "admin_notiz"];
|
|
|
|
const DATUMSFELDER = new Set(["plan_start", "naechster_review"]);
|
|
const TEXT_MAX = 2000;
|
|
|
|
const jetzt = () => new Date().toISOString();
|
|
|
|
function angemeldet(req, res, next) {
|
|
const person = sitzungLesen(req);
|
|
if (!person) return res.status(401).json({ fehler: "nicht_angemeldet" });
|
|
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();
|
|
}
|
|
|
|
profilRouter.use("/workspace/api/profil", angemeldet);
|
|
|
|
/* Wer darf welches Profil?
|
|
|
|
"voll" = alle Felder, auch die management-internen (Plan-Start,
|
|
Review-Termin, interne Notiz). Nur das Management.
|
|
"betreuend"= ein Scout bei einem ihm zugeteilten Creator. Er arbeitet
|
|
mit dem Profil wie ein Manager, sieht aber die internen
|
|
Notizen NICHT -- eine Notiz UEBER die Betreuung gehoert
|
|
nicht in die Hand dessen, der betreut.
|
|
"eigen" = der Creator selbst.
|
|
null = 404. */
|
|
function darf(person, zielId) {
|
|
/* "voll" bekommt allein DogFather (03.09.2026). Vorher stand hier
|
|
istLeitung -- und damit kam jede Managerin an JEDES Creator-Profil,
|
|
auch an das einer fremden Managerin, samt der internen Notiz. Es
|
|
reichte, die Nummer in die Adresse zu schreiben.
|
|
|
|
Ein Manager behaelt volle Rechte an SEINEN Creators: darfCreator
|
|
prueft die Zuteilung, und "voll" bleibt es dort auch -- er soll die
|
|
Verwaltungsfelder seiner eigenen Leute pflegen koennen. */
|
|
if (istDogFather(person)) return "voll";
|
|
if (person.rolle === "creator") return person.id === zielId ? "eigen" : null;
|
|
if (person.rolle === "manager") return darfCreator(person, zielId) ? "voll" : null;
|
|
if (person.rolle === "scout") return darfCreator(person, zielId) ? "betreuend" : null;
|
|
return null;
|
|
}
|
|
|
|
/* ---------- Lesen ------------------------------------------------------- */
|
|
|
|
profilRouter.get("/workspace/api/profil/:id", (req, res) => {
|
|
try {
|
|
const id = Number(req.params.id);
|
|
if (!Number.isInteger(id)) return res.status(400).json({ fehler: "ungueltig" });
|
|
|
|
const recht = darf(req.person, id);
|
|
/* 404 statt 403: Wer kein Recht hat, soll nicht erfahren, ob es die
|
|
Person überhaupt gibt. */
|
|
if (!recht) return res.status(404).json({ fehler: "nicht_gefunden" });
|
|
|
|
const person = db().prepare(
|
|
"SELECT id, name, rolle, aktiv, erstellt, letzter_login FROM personen WHERE id = ?").get(id);
|
|
if (!person || person.rolle !== "creator") {
|
|
return res.status(404).json({ fehler: "nicht_gefunden" });
|
|
}
|
|
|
|
const felder = recht === "voll" ? [...FELDER_OFFEN, ...FELDER_ADMIN] : FELDER_OFFEN;
|
|
const reihe = db().prepare(
|
|
`SELECT ${felder.join(", ")}, geaendert FROM profile WHERE person_id = ?`).get(id);
|
|
|
|
res.json({
|
|
person,
|
|
/* Kein Profil angelegt = leeres Objekt, kein Fehler. */
|
|
profil: reihe || Object.fromEntries(felder.map((f) => [f, null])),
|
|
darf_alles: recht === "voll",
|
|
});
|
|
} catch (fehler) {
|
|
console.error("[workspace] Profil lesen:", fehler?.message);
|
|
res.status(503).json({ fehler: "nicht_verfuegbar" });
|
|
}
|
|
});
|
|
|
|
/* Liste aller Creator -- fürs Management, zur Auswahl. */
|
|
profilRouter.get("/workspace/api/profil", (req, res) => {
|
|
try {
|
|
if (req.person.rolle === "creator") {
|
|
return res.json({ creator: [{ id: req.person.id, name: req.person.name }], eigen: true });
|
|
}
|
|
/* Ein Scout bekommt genau die Creator, die er betreut -- und ohne
|
|
Zuteilung eine leere Liste statt einer Fehlermeldung. */
|
|
/* Zuteilung statt Rolle (03.09.2026): Eine Managerin sah hier die
|
|
Profile ALLER Creator. Jetzt bekommt sie genau ihre -- eigene und
|
|
die ihrer Scouts. */
|
|
let nur = "";
|
|
let werte = [];
|
|
const erlaubt = sichtbareCreatorIds(req.person);
|
|
if (erlaubt !== null) {
|
|
if (!erlaubt.length) {
|
|
return res.json({ creator: [], eigen: false, betreuend: req.person.rolle !== "creator" });
|
|
}
|
|
nur = ` AND p.id IN (${erlaubt.map(() => "?").join(",")})`;
|
|
werte = erlaubt;
|
|
} else if (false) {
|
|
return res.status(404).json({ fehler: "nicht_gefunden" });
|
|
}
|
|
res.json({
|
|
creator: db().prepare(`
|
|
SELECT p.id, p.name, p.aktiv, p.letzter_login,
|
|
(SELECT COUNT(*) FROM aufgaben a
|
|
WHERE a.creator_id = p.id AND a.status <> 'erledigt') AS offene_aufgaben,
|
|
(SELECT naechster_review FROM profile f WHERE f.person_id = p.id) AS naechster_review,
|
|
(SELECT geaendert FROM profile f WHERE f.person_id = p.id) AS profil_geaendert
|
|
FROM personen p WHERE p.rolle = 'creator'${nur}
|
|
ORDER BY p.aktiv DESC, p.name`).all(...werte),
|
|
eigen: false,
|
|
betreuend: req.person.rolle === "scout",
|
|
});
|
|
} catch (fehler) {
|
|
console.error("[workspace] Creator-Liste:", fehler?.message);
|
|
res.status(503).json({ fehler: "nicht_verfuegbar" });
|
|
}
|
|
});
|
|
|
|
/* ---------- Speichern ---------------------------------------------------- */
|
|
|
|
profilRouter.put("/workspace/api/profil/:id", gleicheHerkunft, (req, res) => {
|
|
try {
|
|
const id = Number(req.params.id);
|
|
if (!Number.isInteger(id)) return res.status(400).json({ fehler: "ungueltig" });
|
|
|
|
const recht = darf(req.person, id);
|
|
if (!recht) return res.status(404).json({ fehler: "nicht_gefunden" });
|
|
|
|
const person = db().prepare("SELECT id, rolle FROM personen WHERE id = ?").get(id);
|
|
if (!person || person.rolle !== "creator") return res.status(404).json({ fehler: "nicht_gefunden" });
|
|
|
|
/* Ein Creator kann die Verwaltungsfelder nicht setzen -- sie stehen
|
|
gar nicht erst in der Liste der zulässigen Felder. Was er trotzdem
|
|
mitschickt, wird stillschweigend ignoriert. */
|
|
const erlaubt = recht === "voll" ? [...FELDER_OFFEN, ...FELDER_ADMIN] : FELDER_OFFEN;
|
|
|
|
const werte = {};
|
|
for (const feld of erlaubt) {
|
|
if (req.body?.[feld] === undefined) continue;
|
|
let wert = req.body[feld];
|
|
if (wert === null) { werte[feld] = null; continue; }
|
|
wert = String(wert).trim();
|
|
if (!wert) { werte[feld] = null; continue; }
|
|
if (DATUMSFELDER.has(feld)) {
|
|
if (!/^\d{4}-\d{2}-\d{2}$/.test(wert) || Number.isNaN(Date.parse(wert))) {
|
|
return res.status(400).json({ fehler: `Kein gültiges Datum: ${feld}` });
|
|
}
|
|
} else if (wert.length > TEXT_MAX) {
|
|
return res.status(400).json({ fehler: "Ein Feld ist zu lang." });
|
|
}
|
|
werte[feld] = wert;
|
|
}
|
|
if (!Object.keys(werte).length) return res.status(400).json({ fehler: "nichts_zu_speichern" });
|
|
|
|
const spalten = Object.keys(werte);
|
|
/* UPSERT: Beim ersten Speichern entsteht die Zeile, danach wird sie
|
|
aktualisiert -- ohne vorher prüfen zu müssen, ob es sie gibt. */
|
|
db().prepare(`
|
|
INSERT INTO profile (person_id, ${spalten.join(", ")}, geaendert, geaendert_von)
|
|
VALUES (?, ${spalten.map(() => "?").join(", ")}, ?, ?)
|
|
ON CONFLICT(person_id) DO UPDATE SET
|
|
${spalten.map((s) => `${s} = excluded.${s}`).join(", ")},
|
|
geaendert = excluded.geaendert,
|
|
geaendert_von = excluded.geaendert_von
|
|
`).run(id, ...spalten.map((s) => werte[s]), jetzt(), req.person.id);
|
|
|
|
protokolliere("profil_gespeichert", {
|
|
personId: req.person.id, rolle: req.person.rolle, ip: echteIp(req),
|
|
/* Nur die Feldnamen, nie die Inhalte -- im Profil stehen persönliche
|
|
Angaben, die nicht zusätzlich im Protokoll landen sollen. */
|
|
detail: `Creator #${id}: ${spalten.join(",")}`.slice(0, 120),
|
|
});
|
|
|
|
res.json({ ok: true });
|
|
} catch (fehler) {
|
|
console.error("[workspace] Profil speichern:", fehler?.message);
|
|
res.status(503).json({ fehler: "nicht_verfuegbar" });
|
|
}
|
|
});
|