Files
2026-09-29 10:51:17 +02:00

13 KiB

phase, plan, quick_id, subsystem, tags, status, requires, provides, key-files, decisions, completed, commits, plan_head_before, plan_head_after, actuals
phase plan quick_id subsystem tags status requires provides key-files decisions completed commits plan_head_before plan_head_after actuals
quick-260929-dzu 01 260929-dzu api, web, prisma
custom-modules
personal
rls
settings
complete
260929-9wc
Spalte CustomModule.ownerUserId (NULL = gemeinsam, gesetzt = persoenlich), Migration 20260929130000
Zeilenschutz mit Benutzerdimension nach Muster SearchProvider
API /custom-modules mit persoenlichen und gemeinsamen Eintraegen (Antwortfeld personal)
Einstellungen > Eigene Module (/settings/custom-modules) fuer jeden Benutzer
gemeinsame Oberflaeche CustomModuleManager (Formular, Loeschdialog, Liste) fuer Verwaltung und Einstellungen
created modified
apps/api/prisma/migrations/20260929130000_custom_module_owner/migration.sql
apps/web/src/components/custom-modules/custom-module-manager.tsx
apps/web/src/app/(portal)/settings/custom-modules/page.tsx
apps/web/src/app/(portal)/settings/custom-modules/custom-modules-settings.test.tsx
apps/api/prisma/schema.prisma
apps/api/src/custom-modules/ (Dienst, Controller, DTO, Specs)
docs/mandantentrennung-zugriffsklassifikation.md
apps/web/src/components/custom-modules/custom-module-form-modal.tsx (verschoben aus admin/custom-modules/components)
apps/web/src/components/custom-modules/delete-custom-module-dialog.tsx (verschoben)
apps/web/src/app/(portal)/admin/custom-modules/page.tsx (+ Test)
apps/web/src/components/settings/settings-sidebar.tsx
apps/web/src/lib/custom-modules-api.ts
apps/web/src/messages/de.json, en.json
CHANGELOG.md, docs/anleitung-anwender.md, docs/anleitung-administration.md
RLS: Muster SearchProvider (nullable Besitzerspalte, vier Regeln je Befehl), nicht die einfache Muster DashboardImage (Pflicht-userId)
Gemeinsame Eintraege werden ohne Benutzerkontext geschrieben (forTenant ohne userId), persoenliche mit Benutzer
Rollenpruefung fuer gemeinsame Eintraege im Dienst statt per @Roles, weil sie vom Eintrag abhaengt
Filter fuer Verwaltung/Einstellungen im Web ueber personal, kein scope-Parameter in der API
Texte von Formular und Loeschdialog in eigenen Namensraum customModules.form, Umzug aus admin.customModules
2026-09-29 3 76a923450f 8f41bd26bd
tokens tasks commits
42000 3 3

Phase quick-260929-dzu Plan 01: Eigene Module fuer jeden Benutzer Summary

Jeder angemeldete Benutzer legt unter Einstellungen > Eigene Module persoenliche Seitenleisten-Eintraege an, die nur er sieht; Administratoren pflegen weiter gemeinsame Eintraege unter Verwaltung > Eigene Module (Senden von shared: true). Niemand kann etwas in die Seitenleiste eines anderen Benutzers legen.

Was gebaut wurde

Aufgabe 1, Commit c703d87 (Modell + API, Tests zuerst angepasst)

  • Schema: ownerUserId String? mit Relation zu User (onDelete: Cascade), Index [tenantId, ownerUserId]; Gegenfeld customModules am User.
  • Migration 20260929130000_custom_module_owner (von Hand, mit Kopfkommentar): Spalte, Index, Fremdschluessel, alte Regel ersetzt durch vier Regeln.
  • Dienst/Controller: GET /custom-modules liefert gemeinsame plus eigene Zeilen mit personal: boolean (ownerUserId wird nicht ausgeliefert); GET :id 404 bei fremdem persoenlichem Eintrag (auch fuer Administratoren); POST fuer jeden Angemeldeten, shared: true nur fuer ADMIN/SUPER_ADMIN (sonst 403), Standard persoenlich; PATCH/DELETE: persoenlich nur Besitzer (fremd: 404), gemeinsam nur Administrator (sonst 403). shared/ownerUserId sind per PATCH nicht aenderbar (OmitType im DTO plus whitelist).
  • Routen: list steht weiter vor getOne; kein @Roles mehr an den Schreibrouten, die Rollenpruefung sitzt im Dienst.
  • Specs: Dienst (22 Faelle: A sieht/aendert/loescht B nicht, Nicht-Admin nicht shared, Admin persoenlich vs. gemeinsam, RLS-Bindung mit/ohne Benutzer), Controller, DTO-Pipe-Faelle.
  • Zugriffsklassifikation nachgemessen (siehe unten).

Aufgabe 2, Commit ee97b4e (Web)

  • Neue Seite /settings/custom-modules und Nav-Eintrag „Eigene Module“ unter „Allgemein“.
  • Gemeinsame Komponenten unter components/custom-modules/: CustomModuleFormModal und DeleteCustomModuleDialog (verschoben, Parameter shared) plus neu CustomModuleManager (Liste, Anlegen/Bearbeiten/Loeschen, bumpSidebarRefresh), aufgerufen mit scope="shared" (Verwaltung) oder scope="personal" (Einstellungen). Filter ueber personal im Web.
  • Verwaltung sendet beim Anlegen shared: true, zeigt nur gemeinsame Eintraege, Einleitung nennt „alle Benutzer“; Einstellungen senden kein shared, Einleitung: „Diese Einträge sehen nur Sie.“
  • Texte de/en (Namensraeume customModules.form, customModules.manage, settings.customModules), Umlaut-Waechter gruen.
  • Tests: neuer Settings-Test (7), Admin-Test angepasst (shared: true, Filter; 14).

Aufgabe 3, Commit 8f41bd2 (Doku) + Neubau

  • CHANGELOG-Punkt „Eigene Module“ umgeschrieben (Einstellungen fuer jeden, Verwaltung zusaetzlich fuer alle), docs/anleitung-anwender.md (Abschnitt „Allgemein > Eigene Module“) und docs/anleitung-administration.md (Unterabschnitt bei 5.).
  • docker compose up -d --build web api: web :3000/login 200, api /health ok, GET /custom-modules anonym 401, /settings/custom-modules ohne Anmeldung 307 (Umleitung auf Login).

RLS-Muster und Begruendung

Gefolgt bin ich dem Muster SearchProvider aus 20260911120000_rls_user_dimension_personal_tables: nullable Besitzerspalte, vier nach Befehl getrennte Regeln.

  • SELECT: Mandant UND (kein Benutzer gesetzt ODER ownerUserId IS NULL ODER ownerUserId = current_user_id()).
  • INSERT/UPDATE/DELETE: Mandant UND (kein Benutzer gesetzt ODER ownerUserId = current_user_id()).

Warum nicht das einfachere Muster DashboardImage/Favorite (Pflicht-userId, eine Regel): eigene Module haben gemeinsame Zeilen (NULL), die jeder lesen, aber nur ein Administrator schreiben darf. Eine einzelne Regel, die die gemeinsame Zeile zum Lesen freigibt, wuerde sie auch zum Aendern/Loeschen freigeben (Praezedenz 260910-jab (3)), deshalb getrennte Befehle. Folge: ein Benutzerkontext kann gemeinsame Zeilen nicht schreiben; der Dienst bindet Schreibzugriffe auf gemeinsame Eintraege deshalb bewusst OHNE Benutzer (forTenant(prisma, tenantId)), nachdem er die Administrator-Rolle geprueft hat. Persoenliche Zugriffe binden mit Benutzer. Wie bei allen RLS-Regeln wirkt der Schutz erst mit dem Datenbankrollen-Schalter (heute AUS); bis dahin tragen die Anwendungspruefungen (row.tenantId, ownerUserId) den Schutz.

Curl-Pruefung (lokal, echte API :3001)

Benutzer: admin (SUPER_ADMIN), testuser (USER), curltmp (USER, nur fuer die Pruefung angelegt und danach geloescht).

Fall Ergebnis
USER legt Eintrag ohne shared an 200, personal: true
USER shared: true 403 „Gemeinsame Einträge dürfen nur Administratoren anlegen“
Admin shared: true 200, personal: false
Admin ohne shared 200, personal: true
Liste USER gemeinsam + eigener
Liste zweiter USER nur gemeinsam
Liste Admin nur gemeinsam (persoenliche Eintraege anderer nicht)
zweiter USER: GET / PATCH / DELETE auf fremden persoenlichen Eintrag 404 / 404 / 404
Admin: GET / DELETE auf persoenlichen Eintrag eines Benutzers 404 / 404
USER GET gemeinsam 200
USER PATCH / DELETE gemeinsam 403 / 403
Admin PATCH gemeinsam (mit eingeschmuggeltem shared:false) 200, bleibt gemeinsam
USER PATCH eigenen mit shared:true, ownerUserId:null 200, bleibt persoenlich
http-Adresse 400
anonym 401
Benutzer loeschen -> seine persoenlichen Eintraege Cascade, 0 Zeilen

Alle Testeintraege sind geloescht, CustomModule ist leer.

Tore (gemessen)

Tor Ergebnis
API-Tests vollstaendig 88 Dateien, 1511 Tests gruen
Web-Tests vollstaendig 103 Dateien, 1003 Tests gruen
pnpm turbo run type-check lint --force 9/9 erfolgreich
Biome-Warnungen Web / API 55 (Grundlinie 55) / 82 (Grundlinie 82)
rls-coverage / rls-access-inventory gruen
prisma migrate deploy lokal (Container-IP 172.19.0.2) Migration angewendet, migrate diff danach leer
Zugriffsklassifikation Gate-Schleife 61/223/6 (vorher 61/224/6); custom-modules 0/6/0

Testbenutzer fuer die Browser-Pruefung des Orchestrators

Es gab lokal schon die Nicht-Admin-Konten nutzer1 und nutzer2, deren Passwoerter aber nicht bekannt sind. Deshalb habe ich per Admin-API angelegt: Login testuser, Passwort Test1234!test (Rolle USER, mustChangePassword auf false gesetzt, damit die Anmeldung nicht auf die Passwort-Seite umleitet). Der Administrator ist wie gehabt admin / admin123.

Vorschlag fuer die Browser-Pruefung (dunkel): als testuser unter Einstellungen > Allgemein > Eigene Module einen Eintrag anlegen (Seitenleiste zieht ohne Neuladen nach), als admin unter Verwaltung > Eigene Module einen gemeinsamen Eintrag anlegen (testuser sieht ihn in der Seitenleiste, kann ihn unter Einstellungen aber nicht bearbeiten), als admin pruefen, dass der persoenliche Eintrag von testuser weder in Seitenleiste noch Verwaltung erscheint. Danach die Testeintraege loeschen.

Deviations from Plan

Auto-fixed Issues

1. [Rule 3 - Blocking] Festplatte voll (0 Byte frei) mitten in der Arbeit

  • Found during: Aufgabe 2 (Biome meldete „No space left on device“)
  • Issue: die Docker-Build-Cache-Ablagen der Neubauten fuellten die Platte.
  • Fix: docker builder prune -f (nur Build-Cache, 17,97 GB, keine Images, Container oder Volumes); danach type-check/lint/Tests frisch und vollstaendig wiederholt, alle gruen.
  • Commit: kein Code betroffen.

2. [Rule 1 - Bug] Detektor-Vorgaben fuer forTenant

  • Found during: Aufgabe 1 (rls-access-inventory schlug zweimal fehl)
  • Issue: eine Ternary-Bindung (shared ? forTenant(..) : forTenant(..)) und ein client.customModule.create in einer Hilfsfunktion werden vom Detektor nicht als Zuweisungsform/Modellaufruf erkannt.
  • Fix: je Zweig const tenantPrisma = forTenant(...) mit direktem Modellaufruf; Ausnahmeliste unveraendert leer.
  • Files modified: apps/api/src/custom-modules/custom-modules.service.ts
  • Commit: c703d87

3. Plan-Feinheit: Kein API-Parameter scope; die Verwaltung filtert im Web ueber personal (Plan liess beides zu, „das Einfachste“). Nebenwirkung: die Verwaltungsseite laedt auch die eigenen persoenlichen Eintraege des Administrators und blendet sie aus.

4. Plan-Feinheit: Formular-/Dialog-Texte aus admin.customModules in den neuen Namensraum customModules.form umgezogen (beide Aufrufer teilen sie); Admin-Test entsprechend angepasst. Die Anleitung des Anwenders hatte den Punkt „Eigene Module“ vorher nicht, er ist jetzt neu beschrieben (der Plan sprach von „aktualisieren“).

Hinweise

  • Zwischen c703d87 und ee97b4e liegt ein fremder Commit bc4c011 (fix(web) Widgets nicht mehr zur Mitte versetzen), nicht von diesem Plan; er beruehrt CHANGELOG.md und docs/anleitung-anwender.md an anderen Stellen. Die 3 Commits dieses Plans sind c703d87, ee97b4e, 8f41bd2 (git rev-list ab dem Vorgaenger von c703d87 zaehlt 4 inklusive des fremden). Der Ledger nach Protokoll 0c wurde nicht vor dem ersten Commit angelegt, plan_head_before ist deshalb der Vorgaenger von c703d87.
  • Die Verwaltungs-Nav zeigt weiterhin „Eigene Module“; sie fuehrt jetzt auf die gemeinsamen Eintraege, die Einleitung nennt das.
  • Nichts gepusht.

Known Stubs

Keine.

Threat Flags

Keine neue Angriffsflaeche ausserhalb des bestehenden Modells: ownerUserId kommt nie aus dem Body (Whitelist, im Test belegt), shared ist per PATCH nicht setzbar, fremde persoenliche Eintraege sind ununterscheidbar 404.

Self-Check: PASSED

  • Dateien vorhanden: Migration 20260929130000_custom_module_owner, custom-module-manager.tsx, settings/custom-modules/page.tsx, Settings-Test.
  • Commits vorhanden: c703d87, ee97b4e, 8f41bd2 (git log); nichts gepusht (git branch -r --contains HEAD leer).

Browser-Pruefung (Orchestrator, 29.09.)

  • testuser: Einstellungen > Eigene Module vorhanden; „Meine Seite“ angelegt -> sofort in eigener Seitenleiste (Infrastruktur).
  • admin: sieht „Meine Seite“ weder in Seitenleiste noch Verwaltung; Direktlink zeigt „Dieses Modul gibt es nicht mehr.“; Verwaltung heisst „Gemeinsamen Eintrag anlegen“.
  • admin legt „Firmenseite“ (Sicherheit) an -> testuser sieht sie in der Seitenleiste, nicht in seinen Einstellungen; DELETE als testuser -> 403.
  • Nebenbei: Widgets nicht mehr zentriert (bc4c011) — alle linken Kanten am Raster (272 px bei Rasterbeginn 260 + 12 Rand).
  • Testeintraege geloescht, CustomModule leer.