docs: alle Anleitungen gegen den Code geprueft und nachgearbeitet
Anwender-, Administrations-, Betriebs- und Entwicklungsanleitung gegen Code und Oberflaechentexte abgeglichen; falsche und veraltete Stellen korrigiert, fehlende Funktionen ergaenzt. Willkommensmail: Hinweis nennt jetzt sechs statt vier Felder. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+184
-40
@@ -138,6 +138,21 @@ Das startet:
|
||||
`docker compose up` ohne `--build`/`--force-recreate` baut bestehende Images **nicht** neu — nach
|
||||
Änderungen an Dockerfiles oder Dependencies muss `--build` explizit mitgegeben werden.
|
||||
|
||||
Die API läuft im Dev-Stack im Watch-Modus (`nest start --watch`). Dasselbe Skript gibt es für den
|
||||
Betrieb außerhalb von Compose als `pnpm --filter @tessera/api start:dev`; `pnpm --filter
|
||||
@tessera/api start` startet dagegen den fertig gebauten Stand (`node dist/main.js`). Im
|
||||
produktiven Image startet die API nicht direkt, sondern über `apps/api/scripts/migrate-and-start.sh`
|
||||
(siehe [Datenbank und Migrationen](#datenbank-und-migrationen)).
|
||||
|
||||
Neben den beiden Dateien für die Entwicklung liegen zwei weitere Compose-Dateien im Repository, die
|
||||
Sie lokal nicht brauchen:
|
||||
|
||||
- `docker-compose.prod.yml` — der produktive Stack. Er zieht die fertigen Abbilder `web`, `api`
|
||||
und `db` aus der Registry; der Kanal (`beta` oder `live`) kommt aus `IMAGE_TAG` in der `.env`
|
||||
des jeweiligen Servers (Vorgabe `beta`).
|
||||
- `docker-compose.ci.yml` — beschreibt den Gitea-Runner (`act_runner`), der die CI-Aufträge in
|
||||
Containern ausführt. Details dazu stehen in `docs/ci-cd-setup.md`.
|
||||
|
||||
`web`, `api`, `db` und `mailhog` tragen `restart: unless-stopped`: Nach einem Neustart des
|
||||
Rechners kommen sie von selbst wieder hoch, sofern sie vorher liefen. Mit `docker compose stop`
|
||||
angehaltene Container bleiben aus.
|
||||
@@ -224,7 +239,7 @@ vorgesehen. Sprache, Symbol und Bilder des Installers stehen in
|
||||
sich nur über den CI-Bau auf einem Windows-Rechner prüfen, lokal validiert
|
||||
`cargo check` lediglich die Schlüssel.
|
||||
|
||||
**Im CI wird die Desktop-App nur gebaut, wenn sich etwas an ihr geändert hat.** Der Job `desktop` vergleicht einen Stempel aus Versionsnummer und letztem Commit an `apps/desktop/`, `desktop-version.sh`, `desktop-collect.sh`, `desktop-stamp.sh` und `ci.yml` mit dem Zwischenspeicher des Runners und übernimmt bei Treffer die zuletzt gebauten Pakete (Details: `docs/ci-cd-setup.md`, Abschnitt 4). Eine Änderung außerhalb dieser Pfade – etwa nur in `pnpm-lock.yaml` – löst keinen Desktop-Bau aus; soll trotzdem neu gebaut werden, genügt eine Änderung unter `apps/desktop/`. Stempel lokal ansehen:
|
||||
**Im CI wird die Desktop-App nur gebaut, wenn sich etwas an ihr geändert hat.** Der Job `desktop` vergleicht einen Stempel aus Versionsnummer und letztem Commit an `apps/desktop/`, `desktop-version.sh`, `desktop-collect.sh`, `desktop-stamp.sh`, `appimage-strip-wayland.sh` und `ci.yml` mit dem Zwischenspeicher des Runners und übernimmt bei Treffer die zuletzt gebauten Pakete (Details: `docs/ci-cd-setup.md`, Abschnitt 4). Eine Änderung außerhalb dieser Pfade – etwa nur in `pnpm-lock.yaml` – löst keinen Desktop-Bau aus; soll trotzdem neu gebaut werden, genügt eine Änderung unter `apps/desktop/`. Stempel lokal ansehen:
|
||||
|
||||
```bash
|
||||
DESKTOP_TAG=v1.2.0 GITHUB_REF=refs/heads/main sh .gitea/scripts/desktop-stamp.sh stamp
|
||||
@@ -236,20 +251,39 @@ DESKTOP_TAG=v1.2.0 GITHUB_REF=refs/heads/main sh .gitea/scripts/desktop-stamp.sh
|
||||
|
||||
```
|
||||
(auth)/ — /login, /reset-password — öffentliche Routen
|
||||
(portal)/ — alles hinter Login: /admin, /marketplace, /modules, /settings, /change-password
|
||||
(portal)/ — alles hinter Login: /admin, /changelog, /marketplace, /modules, /settings,
|
||||
/change-password
|
||||
```
|
||||
|
||||
Die Route-Groups `(auth)` und `(portal)` teilen sich kein gemeinsames Layout im URL-Pfad, tragen
|
||||
aber unterschiedliche Layout-Bäume. Innerhalb von `(portal)` liegt `modules/[category]/[moduleSlug]`
|
||||
als generische Route für beliebige Module sowie vier fest verdrahtete Modulverzeichnisse
|
||||
(`cert-manager`, `dkv-fleet`, `domaincheck`, `tender-radar`) mit eigenen `layout.tsx`-Dateien —
|
||||
Details dazu im Abschnitt [Das Modulsystem](#das-modulsystem).
|
||||
als generische Route für beliebige Module sowie zehn fest verdrahtete Modulverzeichnisse
|
||||
(`cert-manager`, `dkv-fleet`, `domaincheck`, `domains`, `handelsware-datev`, `kantine-datev`,
|
||||
`nextcloud-files`, `nextcloud-status`, `proxmox`, `tender-radar`) mit eigenen `layout.tsx`-Dateien.
|
||||
Dazu kommt `modules/custom/[id]` für die „Eigenen Module“ (ohne Gate-Layout, siehe
|
||||
[Eigene Module](#eigene-module)) — Details im Abschnitt [Das Modulsystem](#das-modulsystem).
|
||||
|
||||
**Backend** (`apps/api/src`, NestJS): ein Modul pro fachlicher Domäne
|
||||
(`auth`, `user`, `tenant`, `groups`, `module-registry`, `domaincheck`, `dkv`, `cert-manager`,
|
||||
`tenders`, `calendar`, `dashboard`, `favorites`, `settings`, `ldap`, `mail`, `crypto`, `health`,
|
||||
`prisma`). Jedes Domänen-Modul folgt dem NestJS-Muster `*.module.ts` / `*.controller.ts` /
|
||||
`*.service.ts`.
|
||||
Weitere Bereiche hinter dem Login:
|
||||
|
||||
- `/changelog` — die Seite „Was ist neu“ (siehe [Konventionen und Fallstricke](#konventionen-und-fallstricke)).
|
||||
- `/admin` — der Bereich „Administration“ (Benutzer, Gruppen, Module, Verzeichnisanbindung).
|
||||
Unterbereiche: `users`, `groups`, `modules`
|
||||
(mit `grants` für die Freigaben und `categories` für die Modulkategorien), `custom-modules`,
|
||||
`ldap`, `smtp`, `welcome-mail` und `tenants`.
|
||||
- `/settings` — persönliche Einstellungen mit `general` (Konto, Desktop), `dashboard` und
|
||||
`custom-modules`.
|
||||
|
||||
**Backend** (`apps/api/src`, NestJS): ein Modul pro fachlicher Domäne. Die Plattform-Bausteine sind
|
||||
`auth`, `user`, `tenant`, `groups`, `module-registry`, `module-categories`, `custom-modules`,
|
||||
`dashboard`, `favorites`, `settings`, `reminders`, `bug-reports`, `desktop`, `ldap`, `mail`,
|
||||
`inbox` (gemeinsame IMAP-/Exchange-Postfachanbindung, die mehrere Fachmodule nutzen), `crypto`,
|
||||
`health` und `prisma`. Die Fachmodule sind `domaincheck`, `domains`, `dkv`, `cert-manager`,
|
||||
`tenders`, `calendar`, `proxmox`, `nextcloud-files`, `nextcloud-status`, `handelsware-datev` und
|
||||
`kantine-datev`; `accounting` enthält Hilfsfunktionen (CSV- und Dateinamen-Dekodierung), die
|
||||
Handelsware und Kantinenabrechnung gemeinsam nutzen. `common` ist ein reiner Hilfsordner, kein
|
||||
Fachmodul. Jedes Domänen-Modul folgt dem NestJS-Muster `*.module.ts` / `*.controller.ts` /
|
||||
`*.service.ts`. Der `desktop`-Controller liefert die Desktop-Pakete aus (`GET /desktop/latest`,
|
||||
`GET /desktop/update`, `GET /desktop/download/:platform`).
|
||||
|
||||
**Weg einer Anfrage** (Beispiel: eine Modulseite lädt Daten):
|
||||
|
||||
@@ -271,8 +305,10 @@ Details dazu im Abschnitt [Das Modulsystem](#das-modulsystem).
|
||||
## Das Modulsystem
|
||||
|
||||
Module sind das zentrale Organisationsprinzip von Tessera: fachliche Werkzeuge (Domaincheck,
|
||||
Zertifikatsmanager, DKV-Rechnung, Ausschreibungs-Radar), die im Marktplatz erscheinen, pro Mandant
|
||||
aktiviert und dann einzelnen Gruppen oder Benutzern freigegeben werden.
|
||||
Domains, Zertifikatsmanager, DKV-Rechnung, Ausschreibungs-Radar, Proxmox, Dateien, Nextcloud-Status,
|
||||
Handelsware und Kantinenabrechnung), die im Marktplatz erscheinen, pro
|
||||
Mandant aktiviert und dann einzelnen Gruppen oder Benutzern freigegeben werden. Daneben gibt es die
|
||||
[Eigenen Module](#eigene-module), die ohne Aktivierung und Freigabe auskommen.
|
||||
|
||||
### Registrierung
|
||||
|
||||
@@ -369,17 +405,26 @@ Zugriff auf ein Modul besteht aus zwei unabhängigen Stufen:
|
||||
SUPER_ADMIN). Ohne Aktivierung ist das Modul für niemanden im Mandanten erreichbar, auch nicht
|
||||
über einen Grant.
|
||||
2. **Grant pro Gruppe oder Benutzer** (`ModuleGrant`) — erst wenn das Modul aktiv ist, entscheidet
|
||||
ein Grant, wer es tatsächlich sieht. `ModuleGrant` trägt bewusst kein Rechtestufen-Feld, nur
|
||||
An/Aus (D-04 im Code-Kommentar des Schemas), und ist Gruppe **oder** Benutzer, nie beides.
|
||||
ein Grant, wer es tatsächlich sieht. `ModuleGrant` trägt
|
||||
eine Freigabestufe, das Feld `level` (Enum `ModuleGrantLevel`, Vorgabe `USE`): `USE` heißt
|
||||
Benutzen, `MANAGE` heißt Benutzen und zusätzlich die Einstellungen des Moduls ändern. Freigaben
|
||||
erteilen bleibt Administratoren vorbehalten. Ein Grant gilt für eine Gruppe **oder** einen
|
||||
Benutzer, nie beides (D-04).
|
||||
|
||||
Beide Stufen werden ausschließlich von einer einzigen Funktion aufgelöst:
|
||||
`ModuleAccessService.getAccessibleModuleIds(tenantId, userId, role)`
|
||||
(`apps/api/src/module-registry/module-access.service.ts`). ADMIN und SUPER_ADMIN umgehen die
|
||||
Grant-Prüfung und bekommen automatisch alle mandantenweit aktiven Module. Für die Rolle USER ist es
|
||||
die Vereinigungsmenge aus Direkt-Grants und Grants über Gruppenmitgliedschaft, geschnitten mit den
|
||||
aktiven Modulen des Mandanten. Diese eine Funktion versorgt drei Stellen — den `ModuleGuard` im
|
||||
Beide Stufen werden ausschließlich an einer Stelle aufgelöst:
|
||||
`ModuleAccessService.getModuleAccessLevels(tenantId, userId, role)`
|
||||
(`apps/api/src/module-registry/module-access.service.ts`). Die Funktion liefert je zugänglichem Modul die
|
||||
Stufe. ADMIN und SUPER_ADMIN umgehen die Grant-Prüfung und bekommen automatisch alle
|
||||
mandantenweit aktiven Module mit der Stufe `MANAGE`. Für die Rolle USER ist es die
|
||||
Vereinigungsmenge aus Direkt-Grants und Grants über Gruppenmitgliedschaft, geschnitten mit den
|
||||
aktiven Modulen des Mandanten; besteht der Zugriff über mehrere Wege, gewinnt die höhere Stufe.
|
||||
`getAccessibleModuleIds(tenantId, userId, role)` ist nur die Schlüsselmenge davon — es liefert
|
||||
also die Modul-IDs ohne Stufe. Diese eine Auflösung versorgt drei Stellen — den `ModuleGuard` im
|
||||
Backend, `GET /modules/active` (Sidebar) und `GET /modules/catalog` (Marktplatz) — damit keine
|
||||
dieser Stellen unabhängig voneinander driften kann.
|
||||
dieser Stellen unabhängig voneinander driften kann. Der Dekorator `@UseModule(slug)` verlangt
|
||||
Zugriff auf das Modul; `@ModuleManage(slug)` (ebenfalls in `module.guard.ts`) verlangt zusätzlich
|
||||
die Stufe `MANAGE` und ersetzt `@Roles(ADMIN, SUPER_ADMIN)` bei Routen, die nur dieses eine Modul
|
||||
konfigurieren. Beide nie zusammen mit `@Roles` am selben Handler einsetzen.
|
||||
|
||||
### Vom Backend-Endpunkt zur Seite im Portal
|
||||
|
||||
@@ -400,9 +445,13 @@ Im Frontend gibt es zwei Wege, wie eine Modulseite unter `/modules/...` erreichb
|
||||
|
||||
- **Generische Route** `apps/web/src/app/(portal)/modules/[category]/[moduleSlug]/page.tsx` — für
|
||||
jedes Modul über `[category]`/`[moduleSlug]` erreichbar.
|
||||
- **Vier fest verdrahtete Modulverzeichnisse**: `modules/cert-manager`, `modules/dkv-fleet`,
|
||||
`modules/domaincheck`, `modules/tender-radar` — mit eigenen Unterrouten (z. B.
|
||||
`dkv-fleet/vehicles`, `tender-radar/my-sources`, `*/settings`).
|
||||
- **Zehn fest verdrahtete Modulverzeichnisse**: `modules/cert-manager`, `modules/dkv-fleet`,
|
||||
`modules/domaincheck`, `modules/domains`, `modules/handelsware-datev`, `modules/kantine-datev`,
|
||||
`modules/nextcloud-files`, `modules/nextcloud-status`, `modules/proxmox` und
|
||||
`modules/tender-radar` — mit eigenen Unterrouten (z. B. `dkv-fleet/vehicles`,
|
||||
`tender-radar/my-sources`, `*/settings`). Jedes davon hat zugleich einen Eintrag in
|
||||
`MODULE_REGISTRY`. Daneben gibt es `modules/custom` — die bewusste Ausnahme ohne Gate-Layout
|
||||
für die [Eigenen Module](#eigene-module).
|
||||
|
||||
Beide Wege rendern denselben Baustein: die Server-Komponente `ModuleAccessGate`
|
||||
(`apps/web/src/components/modules/module-access-gate.tsx`). Sie ruft `checkModuleAccess(moduleSlug)`
|
||||
@@ -412,11 +461,12 @@ auf, was `GET /modules/active` mit dem Session-Cookie anfragt — dieselbe
|
||||
einen Fehler) zeigt eine gemeinsame 403-Ansicht.
|
||||
|
||||
**Bekannter Fallstrick (behoben, aber lehrreich):** Ursprünglich saß dieser Zugriffs-Check nur in
|
||||
der generischen `[category]/[moduleSlug]`-Route. Die vier fest verdrahteten Modulverzeichnisse
|
||||
hatten **keinen eigenen** `ModuleAccessGate` und liefen an der Prüfung vorbei — ein direkter Aufruf
|
||||
der generischen `[category]/[moduleSlug]`-Route. Die damals vier fest verdrahteten Modulverzeichnisse
|
||||
(`cert-manager`, `dkv-fleet`, `domaincheck`, `tender-radar`) hatten **keinen eigenen** `ModuleAccessGate` und liefen an der Prüfung vorbei — ein direkter Aufruf
|
||||
von z. B. `/modules/dkv-fleet` umging die Freigabeprüfung vollständig, obwohl die generische Route
|
||||
korrekt geschützt war. Der Fix (Commit `74a30fb`/`5504931`) gibt jedem der vier Modulverzeichnisse
|
||||
ein eigenes `layout.tsx`, das denselben `ModuleAccessGate` einbindet:
|
||||
korrekt geschützt war. Der Fix (Commit `74a30fb`/`5504931`) gab jedem dieser Verzeichnisse
|
||||
ein eigenes `layout.tsx`, das denselben `ModuleAccessGate` einbindet; heute tragen es alle zehn
|
||||
fest verdrahteten Modulverzeichnisse:
|
||||
|
||||
```ts
|
||||
// apps/web/src/app/(portal)/modules/dkv-fleet/layout.tsx
|
||||
@@ -429,7 +479,13 @@ export default function DkvFleetLayout({ children }: { children: ReactNode }) {
|
||||
braucht **immer** ein eigenes `layout.tsx` mit `ModuleAccessGate`, genau wie sein Backend-Controller
|
||||
`@UseModule('<slug>')` braucht. Beide Prüfungen sind unabhängig voneinander — die eine ersetzt nicht
|
||||
die andere; das Frontend-Gate ist Komfort/UX (keine leere Seite ohne Erklärung), das Backend-Gate ist
|
||||
die tatsächliche Zugriffskontrolle.
|
||||
die tatsächliche Zugriffskontrolle. Den Wächter dafür stellt
|
||||
`apps/web/src/app/(portal)/modules/module-layouts.test.tsx`: Er prüft für die dort aufgeführten Layouts, dass sie das
|
||||
`ModuleAccessGate` mit genau dem Slug ihres Ordnernamens einbinden (`proxmox` fehlt in dieser Liste
|
||||
bisher), und dass **jedes**
|
||||
Modulverzeichnis (außer den dynamischen `[...]`-Ordnern und dem bewusst ausgenommenen `custom`)
|
||||
ein `layout.tsx` besitzt. Wer ein neues Modulverzeichnis ohne Layout anlegt, bekommt einen roten
|
||||
Test; wer ein Layout anlegt, trägt es dort zusätzlich in die Slug-Tabelle ein.
|
||||
|
||||
Zusätzlich läuft im Frontend eine dritte, unabhängige Absicherung: `ModuleShell`
|
||||
(`apps/web/src/app/(portal)/modules/[category]/[moduleSlug]/module-shell.tsx`) lädt die
|
||||
@@ -439,7 +495,7 @@ ein beliebiger Slug aus der URL löst sonst keinen Import aus.
|
||||
|
||||
### So entsteht ein neues Modul — Walkthrough am Beispiel Domaincheck
|
||||
|
||||
Domaincheck ist das kleinste vorhandene Modul und eignet sich als Vorlage. Die realen Dateien:
|
||||
Domaincheck ist ein kleines, überschaubares Modul und eignet sich als Vorlage. Die realen Dateien:
|
||||
|
||||
**Backend** (`apps/api/src/domaincheck/`):
|
||||
|
||||
@@ -448,7 +504,8 @@ Domaincheck ist das kleinste vorhandene Modul und eignet sich als Vorlage. Die r
|
||||
3. `domaincheck.controller.ts` — `@Controller('modules/domaincheck')` mit `@UseModule('domaincheck')`
|
||||
auf Klassenebene, ein `@Post('check')`-Handler.
|
||||
4. `domaincheck.seed.ts` — die `seedDomaincheckModule()`-Funktion mit dem Manifest (`slug`, `name`,
|
||||
`version`, `category`, `description`, `isSystem`).
|
||||
`version`, `category`, `description`, `isSystem`). Die Version kommt aus dem Modul-Changelog, siehe
|
||||
[Modulversion und Modul-Changelog pflegen](#modulversion-und-modul-changelog-pflegen).
|
||||
5. `domaincheck.module.ts` — bindet Controller/Service zusammen, importiert
|
||||
`ModuleRegistryModule`, ruft in `onModuleInit()` den Seed auf.
|
||||
6. Eintrag des neuen Moduls in `apps/api/src/app.module.ts` unter `imports`.
|
||||
@@ -488,10 +545,36 @@ diesem globalen `fetch` ignoriert. Gemessen und dokumentiert in
|
||||
`apps/api/src/proxmox/proxmox-client.service.ts` (zweites, unabhängig davon konstruiertes
|
||||
Auftreten mit demselben Befund).
|
||||
|
||||
### Eigene Module
|
||||
|
||||
Neben den eingebauten Modulen gibt es die „Eigenen Module“: Seitenleisten-Einträge, die eine
|
||||
externe `https`-Seite im Rahmen des Portals anzeigen. Sie sind keine Zeilen der Tabelle `Module`,
|
||||
hängen deshalb an keiner Mandanten-Aktivierung und keinem Grant und sind für alle angemeldeten
|
||||
Benutzer sichtbar. Die Teile:
|
||||
|
||||
- **Backend** `apps/api/src/custom-modules/` (`@Controller('custom-modules')`): Es gibt gemeinsame
|
||||
Einträge (nur Administratoren legen sie an, ändern und löschen sie) und persönliche Einträge
|
||||
(nur der Besitzer sieht und ändert sie; für fremde Kennungen antwortet die API immer mit 404). Die
|
||||
Rollenentscheidung trifft der Dienst, weil sie vom Eintrag abhängt, nicht von der Route — deshalb
|
||||
tragen die Routen weder `@Roles` noch `@UseModule`. Die Kategorien kommen aus
|
||||
`apps/api/src/module-categories/`.
|
||||
- **Frontend**: die Rahmenseite `modules/custom/[id]` (bewusst ohne `ModuleAccessGate`; der
|
||||
statische Ordner `custom` hat im App Router Vorrang vor `[category]/[moduleSlug]`), die
|
||||
Bausteine `apps/web/src/components/modules/custom-module-*.tsx`, die Pflege der
|
||||
gemeinsamen Einträge für Administratoren unter `/admin/custom-modules` und die persönlichen
|
||||
Einträge unter `/settings/custom-modules`.
|
||||
- **Wächter**: `apps/web/src/messages/module-categories.spec.ts` prüft, dass jede Modulkategorie
|
||||
einen Anzeigenamen in beiden Sprachen hat; `module-layouts.test.tsx` führt `custom` als einzige
|
||||
begründete Ausnahme von der Layout-Pflicht.
|
||||
|
||||
### Eine Kachel zum Modul
|
||||
|
||||
Ein Modul kann zusätzlich als Kachel auf dem Dashboard erscheinen. Seit
|
||||
`quick-260922-m1h` sind dafür **drei** Stellen nötig (vorher waren es sieben):
|
||||
Ein Modul kann zusätzlich als Kachel auf dem Dashboard erscheinen. `WIDGET_TYPES` kennt zwölf
|
||||
Kachel-Typen: `clock`, `search`, `calendar`, `note`, `calculator`, `favorites`, `stopwatch`,
|
||||
`picture-frame`, `xframe`, `proxmox`, `reminder` und `nextcloud-status`. Davon sind nur `proxmox` und
|
||||
`nextcloud-status` an ein Modul gebunden; alle übrigen (auch `reminder`) sind Plattform-Kacheln, die
|
||||
immer sichtbar sind. Seit `quick-260922-m1h` sind für eine neue Kachel **drei** Stellen nötig
|
||||
(vorher waren es sieben):
|
||||
|
||||
1. **Die Kachel-Komponente schreiben** — `apps/web/src/components/dashboard/widgets/<name>-widget.tsx`,
|
||||
nimmt die `WidgetProps` aus `widget-registry.tsx` (`instanceId`, `config`, `isEditMode`) entgegen.
|
||||
@@ -538,6 +621,14 @@ den Proxmox-Hosts. Und Zeilen, die im Ansichtsmodus Links sind, werden im Bearbe
|
||||
schlichten Elementen ohne Ziel: Links stehen im Abbruch-Selektor von `dashboard-grid.tsx`, eine Kachel
|
||||
aus Links ließe sich sonst kaum noch ziehen.
|
||||
|
||||
**Zweites Beispiel: Nextcloud-Status (`quick-261002-k67`).** Die zweite modulgebundene Kachel
|
||||
folgt demselben Muster: `WIDGET_MODULE_SLUGS` trägt `'nextcloud-status': 'nextcloud-status'`, die
|
||||
Komponente ist `apps/web/src/components/dashboard/widgets/nextcloud-status-widget.tsx`, und die
|
||||
Daten kommen über die Endpunkte des Moduls (`/modules/nextcloud-status/*`, im Frontend gebündelt in
|
||||
`apps/web/src/lib/nextcloud-status-api.ts`, das sich an `proxmox-api.ts` orientiert). Der Katalog-
|
||||
Test `widget-catalog-modal.test.tsx` prüft, dass beide Modul-Kacheln nur bei Zugriff auf das jeweilige
|
||||
Modul erscheinen.
|
||||
|
||||
## Mandantentrennung
|
||||
|
||||
Der tatsächliche Mechanismus ist `TenantGuard` (`apps/api/src/tenant/tenant.guard.ts`), global als
|
||||
@@ -547,8 +638,10 @@ SUPER_ADMIN einen Wechsel per `x-tenant-id`-Header, und setzt anschließend AUSS
|
||||
`req.tenantId` (260911-e2s). Die Bindung an den Mandanten geschieht dienst-intern, je
|
||||
Service-Methode neu, über das Bindungshilfsmittel `forTenant()`
|
||||
(`apps/api/src/prisma/prisma-tenant.extension.ts`), das vor **jeder** Query in einer Transaktion
|
||||
`SELECT set_config('app.current_tenant', $1, true)` ausführt — der Guard selbst erzeugt keinen
|
||||
Prisma-Client mehr und veröffentlicht keinen auf dem Anfrageobjekt.
|
||||
eine einzige `SELECT set_config(...)`-Anweisung ausführt. Sie setzt drei Werte zugleich:
|
||||
`app.current_tenant` (der Mandant), `app.current_user` (der optionale dritte Parameter `userId`,
|
||||
leer, wenn keiner übergeben wird) und `app.system_context` (wird dabei auf leer gesetzt). Der Guard
|
||||
selbst erzeugt keinen Prisma-Client mehr und veröffentlicht keinen auf dem Anfrageobjekt.
|
||||
|
||||
> Ein früherer Entwurf veröffentlichte zusätzlich einen gebundenen Prisma-Client auf dem
|
||||
> Anfrageobjekt, dupliziert in einer gleichnamigen, nie in `app.module.ts` registrierten
|
||||
@@ -589,6 +682,34 @@ Bei den RLS-geschützten Tabellen greift die DB-seitige Absicherung zusätzlich,
|
||||
läuft tatsächlich über einen dienst-intern per `forTenant()` gebundenen Client und nicht über
|
||||
den globalen, ungebundenen `PrismaService`.
|
||||
|
||||
**Systemkontext für Hintergrunddienste:** Ein Dienst, der ohne Anfrage einmal über **alle** Mandanten
|
||||
lesen muss und danach je Mandant gebunden handelt (zum Beispiel die Hintergrunddienste von DKV, Proxmox,
|
||||
Nextcloud-Status, Erinnerungs-Mails und Ausschreibungs-Zusammenfassung, der LDAP-Abgleich und das
|
||||
Ausschreibungs-Matching), nutzt `forSystem()` aus derselben Datei. Es setzt `app.system_context`
|
||||
auf `'true'` und leert dabei `app.current_tenant` und `app.current_user`. Geöffnet ist nur das
|
||||
**Lesen** (eine zusätzliche `FOR SELECT`-Regel); jedes Schreiben scheitert weiterhin an der
|
||||
Mandantenregel. Ein Anfrageweg darf `forSystem()` nie aufrufen — es würde an jeder Mandantenregel
|
||||
vorbeilesen. Welche Dateien es rufen dürfen, steht mit der genauen Zahl der Aufrufe je Datei in
|
||||
`FORSYSTEM_ALLOWED_CALL_SITES` in `rls-access-inventory.spec.ts`; jeder weitere Aufruf macht
|
||||
diesen Test rot.
|
||||
|
||||
**Wächter-Tests für die Mandantentrennung** (`apps/api/src/prisma/`): Sie lesen Schema,
|
||||
Migrationen und Quelltext und werden rot, sobald etwas auseinanderläuft:
|
||||
|
||||
- `rls-coverage.spec.ts` — misst aus `schema.prisma` und den Migrationen, welche Modelle eine
|
||||
`tenantId` tragen und ob für sie eine RLS-Regel existiert; bleibt auch für künftige Modelle gültig.
|
||||
- `rls-access-inventory.spec.ts` — ermittelt alle Zugriffe auf Modelle im Quelltext (gebunden,
|
||||
ungebunden, gemischt, systemgebunden) und vergleicht sie mit der Bestandsaufnahme in
|
||||
`docs/mandantentrennung-zugriffsklassifikation.md`. Eine neue Fundstelle ohne Eintrag oder ein
|
||||
Eintrag ohne Fundstelle lässt den Test fehlschlagen — deshalb gehört die Doku-Zeile in dieselbe
|
||||
Änderung wie der neue Zugriff.
|
||||
- `rls-app-role.spec.ts` — prüft die Migration der Datenbankrolle `tessera_app` (ohne Superuser- und
|
||||
BYPASSRLS-Recht) rein textuell.
|
||||
- `rls-preflight.spec.ts` — prüft `apps/api/scripts/rls-preflight.mjs` in der Betriebsart
|
||||
`--print-plan`, also ohne Datenbankverbindung. Das Skript selbst misst gegen eine Datenbankrolle
|
||||
(Verbindung aus `TESSERA_PREFLIGHT_DATABASE_URL`), ob die Mandantentrennung unter ihr tatsächlich
|
||||
greift; es liest nur und schreibt nichts.
|
||||
|
||||
## Berechtigungen
|
||||
|
||||
Rollen kommen aus dem Prisma-`enum Role { SUPER_ADMIN, ADMIN, USER }` und stecken im JWT — nie aus
|
||||
@@ -607,7 +728,7 @@ Dekorators auf Handler oder Controller.
|
||||
Für den Modulzugriff kommt das oben beschriebene Grant-Modell hinzu: `Group` (mandantenintern,
|
||||
optional an ein AD-Objekt über `ldapDn`/`ldapObjectGuid` gebunden), `GroupMembership`
|
||||
(Benutzer-zu-Gruppe, `source: MANUAL | LDAP`) und `ModuleGrant` (Gruppe **oder** Benutzer, XOR,
|
||||
kein Rechtestufen-Feld). Die Schreibseite dafür ist
|
||||
mit Freigabestufe `level` = `USE` oder `MANAGE`). Die Schreibseite dafür ist
|
||||
`apps/api/src/groups/module-grants.controller.ts`, ausschließlich für ADMIN/SUPER_ADMIN:
|
||||
|
||||
| Route | Zweck |
|
||||
@@ -636,12 +757,22 @@ Der Workflow für eine Schemaänderung:
|
||||
(`apps/api/Dockerfile`) setzt als `CMD`:
|
||||
|
||||
```
|
||||
prisma migrate deploy --schema apps/api/prisma/schema.prisma && node apps/api/dist/main.js
|
||||
sh apps/api/scripts/migrate-and-start.sh
|
||||
```
|
||||
|
||||
Ein neu gestarteter `api`-Container wendet also jede noch ausstehende Migration selbst an, bevor
|
||||
die Anwendung überhaupt hochfährt — es gibt keinen separaten manuellen Migrationsschritt beim
|
||||
Deployment.
|
||||
Das Skript führt zuerst `prisma migrate deploy --schema apps/api/prisma/schema.prisma` aus und
|
||||
startet danach die API (`node apps/api/dist/main.js`, per `exec`, damit Signale wie `SIGTERM`
|
||||
den Node-Prozess erreichen). Ein neu gestarteter `api`-Container wendet also jede noch ausstehende
|
||||
Migration selbst an, bevor die Anwendung überhaupt hochfährt — es gibt keinen separaten manuellen
|
||||
Migrationsschritt beim Deployment.
|
||||
|
||||
Das Skript trennt dabei die Verbindung für die Migration von der Verbindung für den Betrieb: Für
|
||||
`prisma migrate deploy` gilt `TESSERA_MIGRATE_DATABASE_URL`, falls gesetzt (Rechte des
|
||||
Tabelleneigentümers für DDL), sonst `DATABASE_URL`; die API selbst läuft immer mit `DATABASE_URL`.
|
||||
Ohne gesetzte Migrationsverbindung nutzen beide Schritte `DATABASE_URL` — der bisherige Ablauf,
|
||||
unverändert für lokale Entwicklung und CI. Die Hintergründe zur Rollentrennung stehen in
|
||||
`docs/mandantentrennung-datenbankrolle.md`; `apps/api/src/prisma/start-script.spec.ts` prüft das
|
||||
Skript in der Betriebsart `--print-plan` ohne Datenbank.
|
||||
|
||||
RLS-Policies werden nicht von Prisma selbst verwaltet, sondern als reines SQL innerhalb regulärer
|
||||
Migrationsdateien mitgeliefert (`CREATE POLICY ...` in `migration.sql`) — siehe
|
||||
@@ -669,6 +800,19 @@ gegen Services, Controller-Logik und React-Komponenten. Guard-artige Spezifikati
|
||||
gegen bereits einmal aufgetretene Fehler — wiederkehrende Fallstricke werden in diesem Projekt
|
||||
durch einen Test abgesichert, nicht nur durch einen Kommentar.
|
||||
|
||||
**Wächter-Tests (Regressionsschutz):** Mehrere Tests sind keine Funktionstests, sondern halten eine
|
||||
Regel fest, die einmal verletzt wurde. Die wichtigsten:
|
||||
|
||||
| Test | Was er absichert |
|
||||
|---|---|
|
||||
| `apps/web/src/app/(portal)/modules/module-layouts.test.tsx` | Jedes Modulverzeichnis hat ein `layout.tsx` mit `ModuleAccessGate` (Ausnahme: `custom` und dynamische `[...]`-Ordner) |
|
||||
| `apps/web/src/messages/umlaut-guard.spec.ts` | Keine Ersatzschreibung („fuer“, „loeschen“) in `de.json`/`en.json` |
|
||||
| `apps/web/src/messages/tenderRadar-parity.spec.ts` | `de.json` und `en.json` haben im Namensraum `tenderRadar` dieselben Schlüssel |
|
||||
| `apps/web/src/messages/module-categories.spec.ts` | Jede Modulkategorie hat einen Anzeigenamen in beiden Sprachen |
|
||||
| `apps/web/src/components/dashboard/widget-registry.test.tsx` | `WIDGET_TYPES`, Registry und Größenvorgaben der Kacheln sind deckungsgleich |
|
||||
| `apps/api/src/module-registry/module-changelog.spec.ts` | Jedes Seed-Modul hat einen Changelog, die Version stimmt mit dem obersten Eintrag |
|
||||
| `apps/api/src/prisma/rls-*.spec.ts` | Mandantentrennung: Abdeckung, Zugriffsinventar, Datenbankrolle, Preflight (siehe [Mandantentrennung](#mandantentrennung)) |
|
||||
|
||||
**Desktop-Modul (`apps/api/src/desktop`):**
|
||||
|
||||
```bash
|
||||
|
||||
Reference in New Issue
Block a user