Anhaenge an Aufgaben -- ohne eine zweite Dateiablage
Stufe 9 aus dem Plan. Eine Aufgabe "Vertrag pruefen" ohne den Vertrag daneben ist eine Aufforderung zum Suchen. KEIN ZWEITER HOCHLADEWEG Dateien leben in der Dateiablage -- mit ihrer Sichtbarkeit, ihren Freigaben, ihren Fassungen und ihrem Protokoll. Ein eigener Weg an der Aufgabe waere eine zweite Ablage mit einer zweiten Rechtelogik gewesen, und die zweite ist immer die, die etwas durchlaesst. Deshalb nur eine Spalte: Die Datei WEISS, zu welcher Aufgabe sie gehoert. Hochgeladen wird ueber denselben Weg wie immer, mit einem Kopf mehr. Alles andere bleibt, wo es schon richtig ist -- die Anhaenge tragen deshalb auch ihre Fassungsnummer und die Warnung "nicht mehr aktuell" mit, ohne dass dafuer eine Zeile geschrieben werden musste. DER RIEGEL Waere `x-aufgabe` ungeprueft, waere der Kopf ein Weg, die Existenz fremder Aufgaben zu erfahren -- man muesste nur Zahlen durchprobieren. Geprueft wird deshalb gegen dieselbe Schranke wie fuer die Datei selbst: `darfCreator`, die eine Stelle im Haus, an der diese Frage beantwortet wird. Eine eigene Herleitung hier waere eine zweite Meinung darueber gewesen. (Erster Anlauf: genau so eine Herleitung, mit einer Funktion, die gar nicht importiert war.) Gemessen: Ein Scout darf an die Aufgabe SEINER Creatorin, an die eines fremden Creators nicht -- mit wortgleicher Absage wie bei einer erfundenen Nummer. Der Unterschied waere sonst die Auskunft. Und es bleibt auch keine lose Datei liegen: Die Pruefung findet vor dem ersten Byte statt. ON DELETE SET NULL, NICHT CASCADE Der wichtigste Teil der Spalte: Wer eine Aufgabe loescht, will die Aufgabe loeschen, nicht den Vertrag, der daran hing. Die Datei verliert nur ihren Bezug und steht danach wieder in der Ablage. Gemessen. EIN EIGENER FEHLER, GEFUNDEN BEIM MESSEN Der Verweis am Anhang zeigte auf /workspace/api/dateien/:id -- den es gar nicht gibt, der Weg heisst /inhalt. Die Pruefung ruft ihn jetzt wirklich auf und erwartet 200, statt nur die Adresse zu vergleichen. PRUEFUNGEN: pruef-anhaenge neu mit 37, davon 8 im Browser. Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
@@ -208,7 +208,7 @@ export function freigabenSetzen(dateiId, ids, person) {
|
||||
const SPALTEN = `
|
||||
d.id, d.name_original, d.groesse, d.typ, d.status, d.notiz,
|
||||
d.creator_id, d.hochgeladen_von, d.erstellt, d.geaendert,
|
||||
d.creator_extern, d.ersetzt_id,
|
||||
d.creator_extern, d.ersetzt_id, d.aufgabe_id,
|
||||
${externSql("pc.name", "d.creator_extern")} AS creator_name,
|
||||
ph.name AS hochgeladen_name`;
|
||||
|
||||
@@ -293,6 +293,50 @@ dateienRouter.delete("/workspace/api/dateien/:id(\\d+)/fassung", (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
/** Darf dieser Mensch diese Aufgabe sehen?
|
||||
*
|
||||
* BEWUSST EINFACH GEHALTEN und nicht aus workspace-aufgaben.js
|
||||
* importiert: Dort haengt die Sichtbarkeit an einer laengeren Abfrage
|
||||
* mit Betreuung und Sicht. Hier wird nur gefragt, ob die Datei an eine
|
||||
* Aufgabe darf, und dafuer gilt dieselbe Schranke wie fuer die Datei
|
||||
* selbst -- der Creator-Bezug. Wer den Creator sehen darf, darf die
|
||||
* Aufgabe daran haengen.
|
||||
*
|
||||
* Waere hier mehr erlaubt als bei der Datei, entstuende genau das
|
||||
* Leck, das die Spalte verhindern soll. */
|
||||
function aufgabeSichtbar(aufgabe, person) {
|
||||
if (!aufgabe) return false;
|
||||
/* Eine Aufgabe ohne Creator-Bezug gehoert dem Haus -- sie ist fuer
|
||||
alle sichtbar, die ueberhaupt Dateien sehen duerfen. */
|
||||
if (!aufgabe.creator_id) return true;
|
||||
/* `darfCreator` ist genau diese Frage, und sie steht an einer Stelle
|
||||
im Haus. Eine eigene Herleitung hier waere eine zweite Meinung
|
||||
darueber -- und die zweite ist immer die, die mehr durchlaesst.
|
||||
(Erster Anlauf: eine handgebaute Abfrage plus sichtbareCreatorIds,
|
||||
das gar nicht importiert war.) */
|
||||
return darfCreator(person, aufgabe.creator_id);
|
||||
}
|
||||
|
||||
/* ---------- Anhaenge einer Aufgabe --------------------------------------
|
||||
Eigener Weg statt eines Filters auf der grossen Liste: Die Aufgabe
|
||||
fragt nach IHREN Dateien, und die Antwort ist kurz genug, um sie beim
|
||||
Oeffnen des Dialogs zu holen. */
|
||||
dateienRouter.get("/workspace/api/aufgaben/:id(\\d+)/dateien", (req, res) => {
|
||||
try {
|
||||
const id = Number(req.params.id);
|
||||
const { wo, werte } = sichtbar(req.sicht || req.person);
|
||||
const liste = db().prepare(`
|
||||
SELECT ${SPALTEN} ${VERBUND}
|
||||
WHERE ${wo} AND d.aufgabe_id = ?
|
||||
ORDER BY d.erstellt DESC`).all(...werte, id);
|
||||
fassungenSetzen(liste);
|
||||
res.json({ dateien: liste });
|
||||
} catch (fehler) {
|
||||
console.error("[dateien] Anhaenge:", fehler?.message);
|
||||
res.status(503).json({ fehler: "nicht_verfuegbar" });
|
||||
}
|
||||
});
|
||||
|
||||
/* ---------- Liste ------------------------------------------------------- */
|
||||
|
||||
dateienRouter.get("/workspace/api/dateien", (req, res) => {
|
||||
@@ -411,6 +455,33 @@ dateienRouter.post("/workspace/api/dateien",
|
||||
const dateiname = `${Date.now().toString(36)}-${randomBytes(8).toString("hex")}${endung}`;
|
||||
writeFileSync(join(ORDNER, dateiname), req.body, { flag: "wx" });
|
||||
|
||||
/* ---- GEHOERT DIE DATEI ZU EINER AUFGABE? (15.09.2026) ---------
|
||||
|
||||
Die Nummer kommt im Kopf, wie der Dateiname auch. Geprueft wird
|
||||
sie gegen die Aufgaben, die dieser Mensch ueberhaupt sehen
|
||||
darf -- sonst waere der Kopf ein Weg, eine Datei an eine fremde
|
||||
Aufgabe zu haengen und damit ihre Existenz zu verraten.
|
||||
|
||||
EINE UNBEKANNTE NUMMER IST EIN FEHLER, kein stilles Ignorieren:
|
||||
Wer eine Datei an eine Aufgabe haengen wollte und sie liegt
|
||||
danach lose in der Ablage, sucht sie spaeter an der Aufgabe. */
|
||||
let aufgabeId = null;
|
||||
const rohAufgabe = req.get("x-aufgabe");
|
||||
if (rohAufgabe) {
|
||||
const n = Number(rohAufgabe);
|
||||
const gefunden = Number.isInteger(n) && n > 0
|
||||
? db().prepare(`SELECT a.id, a.creator_id FROM aufgaben a WHERE a.id = ?`).get(n)
|
||||
: null;
|
||||
if (!gefunden || !aufgabeSichtbar(gefunden, req.sicht || req.person)) {
|
||||
return res.status(404).json({ fehler: "aufgabe_nicht_gefunden" });
|
||||
}
|
||||
aufgabeId = gefunden.id;
|
||||
/* Der Creator-Bezug folgt der Aufgabe, wenn keiner mitkam --
|
||||
sonst laege die Datei fuer die Sichtbarkeit woanders als die
|
||||
Aufgabe, an der sie haengt. */
|
||||
if (creatorId === null && gefunden.creator_id) creatorId = gefunden.creator_id;
|
||||
}
|
||||
|
||||
/* ---- IST DAS EINE NEUE FASSUNG? (15.09.2026) ------------------
|
||||
|
||||
AUTOMATISCH VERKNUEPFT, NICHT GEFRAGT -- und das ist eine
|
||||
@@ -436,11 +507,11 @@ dateienRouter.post("/workspace/api/dateien",
|
||||
const { lastInsertRowid } = db().prepare(`
|
||||
INSERT INTO dateien
|
||||
(name_original, name_datei, groesse, typ, status, notiz,
|
||||
creator_id, hochgeladen_von, erstellt, ersetzt_id)
|
||||
VALUES (?,?,?,?,'entwurf',?,?,?,?,?)`).run(
|
||||
creator_id, hochgeladen_von, erstellt, ersetzt_id, aufgabe_id)
|
||||
VALUES (?,?,?,?,'entwurf',?,?,?,?,?,?)`).run(
|
||||
name, dateiname, req.body.length,
|
||||
(req.get("content-type") || "application/octet-stream").slice(0, 100),
|
||||
notiz, creatorId, req.person.id, jetzt(), ersetztId);
|
||||
notiz, creatorId, req.person.id, jetzt(), ersetztId, aufgabeId);
|
||||
|
||||
/* Ausdrueckliche Freigaben. Der Kopf traegt eine Liste von Ids --
|
||||
geprueft wird jede einzelne gegen die erlaubte Auswahl. */
|
||||
@@ -456,7 +527,9 @@ dateienRouter.post("/workspace/api/dateien",
|
||||
detail: `#${lastInsertRowid} ${name} (${Math.round(req.body.length / 1024)} KB)`
|
||||
+ (anzahl ? `, freigegeben fuer ${anzahl}` : "").slice(0, 40),
|
||||
});
|
||||
res.status(201).json({ id: Number(lastInsertRowid), ersetzt_id: ersetztId });
|
||||
res.status(201).json({
|
||||
id: Number(lastInsertRowid), ersetzt_id: ersetztId, aufgabe_id: aufgabeId,
|
||||
});
|
||||
} catch (fehler) {
|
||||
if (fehler?.type === "entity.too.large") {
|
||||
return res.status(413).json({ fehler: "Datei ist zu groß (max. 25 MB)." });
|
||||
|
||||
Reference in New Issue
Block a user