docs: Anleitungen fuer Kollegen — Anwender, Administration, Betrieb, Entwicklung
Tessera CI/CD / Lint & Type Check (push) Successful in 45s
Tessera CI/CD / Tests (push) Successful in 54s
Tessera CI/CD / Build & Publish Images (push) Successful in 7s

Bisher gab es fuer Kollegen keine Dokumentation: im Projekt lagen nur das
CI/CD-Runbook und die Arbeitsanweisungen fuer die Entwicklung. Diese Luecke
schliessen vier Anleitungen plus eine Einstiegsseite unter docs/.

Alle vier wurden gegen den Quelltext geschrieben, nicht aus der Planung
abgeleitet, und anschliessend unabhaengig gegengeprueft: jede zitierte
Beschriftung ist woertlich aus de.json belegt, jede beschriebene Funktion im
Code nachgewiesen, alle Befehle und Pfade gegen die echten Compose-Dateien,
Dockerfiles und package.json-Skripte verifiziert. Die Gegenpruefung fand keine
falsche Aussage.

Die Einstiegsseite hebt die drei Punkte hervor, die in der Praxis am meisten
Zeit gekostet haben: Anmeldung ueber den Benutzernamen statt der E-Mail,
der Unterschied zwischen aktiviert und freigegeben, und dass ein blosses
'up -d' die laufenden Container nicht ersetzt.

Nebenbefund beim Schreiben des Betriebshandbuchs, als #17 im Ledger erfasst:
user-files/ ist in keiner Compose-Datei als Volume eingebunden — hochgeladene
Profilbilder und DKV-Exporte ueberleben kein --force-recreate. Noch ohne
Schaden, da bisher kein Nutzer ein Profilbild hinterlegt hat.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FYZcd3SSmo14QTqWx2KKzU
This commit is contained in:
2026-09-09 08:46:33 +02:00
parent cfb85cd129
commit 3501eb4dd1
6 changed files with 1268 additions and 3 deletions
+16 -3
View File
@@ -1,10 +1,10 @@
--- ---
schema_version: 1 schema_version: 1
open_count: 0 open_count: 1
waived_count: 1 waived_count: 1
fixed_count: 15 fixed_count: 15
total_count: 16 total_count: 17
last_updated: 2026-09-09T06:25:18.145Z last_updated: 2026-09-09T06:42:22.801Z
--- ---
# Broken Windows Ledger # Broken Windows Ledger
@@ -31,6 +31,7 @@ last_updated: 2026-09-09T06:25:18.145Z
| 14 | 15 | unmet-truth | apps/web/src/app/(portal)/admin/modules/grants/page.tsx | | Die Suche in der Freigaben-Matrix macht die Matrix unbenutzbar, sobald sie etwas findet: derselbe Suchbegriff filtert BEIDE Achsen unabhaengig voneinander (page.tsx:137 filtert Module ueber m.name, page.tsx:145 filtert Gruppen ueber internalName/name). Ein Begriff, der nur eine Achse trifft, leert die andere vollstaendig — es bleibt nie ein Kaestchen zum Klicken uebrig. Am 2026-09-07 im Browser auf alpha gemessen: Suche 'Claude_VT' bzw. 'Vertrieb' laesst die Gruppenspalte stehen, entfernt aber jede Modulzeile; Suche 'Cert' laesst die Modulzeile stehen, entfernt aber jede Gruppenspalte. Damit scheitert genau der Zweck der Suche — in einer grossen Matrix die Kreuzung Modul x Gruppe finden. Belege: .planning/phases/16-ad-gruppen-synchronisation/uat-2026-09-07/befund-matrix-suche-gruppenname.png und befund-matrix-suche-modulname.png | fixed | | 2026-09-07T12:46:56.172Z | 2026-09-09T06:25:17.928Z | | 14 | 15 | unmet-truth | apps/web/src/app/(portal)/admin/modules/grants/page.tsx | | Die Suche in der Freigaben-Matrix macht die Matrix unbenutzbar, sobald sie etwas findet: derselbe Suchbegriff filtert BEIDE Achsen unabhaengig voneinander (page.tsx:137 filtert Module ueber m.name, page.tsx:145 filtert Gruppen ueber internalName/name). Ein Begriff, der nur eine Achse trifft, leert die andere vollstaendig — es bleibt nie ein Kaestchen zum Klicken uebrig. Am 2026-09-07 im Browser auf alpha gemessen: Suche 'Claude_VT' bzw. 'Vertrieb' laesst die Gruppenspalte stehen, entfernt aber jede Modulzeile; Suche 'Cert' laesst die Modulzeile stehen, entfernt aber jede Gruppenspalte. Damit scheitert genau der Zweck der Suche — in einer grossen Matrix die Kreuzung Modul x Gruppe finden. Belege: .planning/phases/16-ad-gruppen-synchronisation/uat-2026-09-07/befund-matrix-suche-gruppenname.png und befund-matrix-suche-modulname.png | fixed | | 2026-09-07T12:46:56.172Z | 2026-09-09T06:25:17.928Z |
| 15 | 16 | unmet-truth | apps/api/src/ldap/ldap.service.ts | | Der Sync reicht rohe Techniktexte an den Administrator durch und laesst echte AD-Konten still liegen. Am 2026-09-07 auf alpha gegen das echte AD gemessen (Lauf 14:42): zehn Fehlerzeilen unter den drei Zahlenzeilen, davon zwei Sorten. (1) Vier Konten scheitern mit der woertlichen Prisma-Meldung 'Invalid prisma.user.create() invocation: Unique constraint failed on the fields: (email)' — CN=uvertrieb_ro, uvertrieb_rw, uvertrieb_ro_ss, usoftware_rw aus OU=CTL_PWS_Gruppen teilen sich offenbar eine E-Mail-Adresse. Sie werden dadurch NIE importiert, ohne dass der Administrator erfaehrt warum oder was er tun soll. (2) Sechs Eintraege melden englisch 'no username mapped (check sAMAccountName mapping)' — korrekt uebersprungene Kontakte/Ressourcen ohne sAMAccountName, aber die Meldung liest sich wie ein Fehler und ist nicht uebersetzt. Beides braucht eine verstaendliche deutsche Meldung; die E-Mail-Kollision zusaetzlich eine Entscheidung, ob solche Konten ohne E-Mail angelegt oder bewusst uebersprungen werden. Beleg: .planning/phases/16-ad-gruppen-synchronisation/uat-2026-09-07/windows6a-sync-zahlenzeilen.png | fixed | | 2026-09-07T12:47:32.058Z | 2026-09-09T06:25:18.145Z | | 15 | 16 | unmet-truth | apps/api/src/ldap/ldap.service.ts | | Der Sync reicht rohe Techniktexte an den Administrator durch und laesst echte AD-Konten still liegen. Am 2026-09-07 auf alpha gegen das echte AD gemessen (Lauf 14:42): zehn Fehlerzeilen unter den drei Zahlenzeilen, davon zwei Sorten. (1) Vier Konten scheitern mit der woertlichen Prisma-Meldung 'Invalid prisma.user.create() invocation: Unique constraint failed on the fields: (email)' — CN=uvertrieb_ro, uvertrieb_rw, uvertrieb_ro_ss, usoftware_rw aus OU=CTL_PWS_Gruppen teilen sich offenbar eine E-Mail-Adresse. Sie werden dadurch NIE importiert, ohne dass der Administrator erfaehrt warum oder was er tun soll. (2) Sechs Eintraege melden englisch 'no username mapped (check sAMAccountName mapping)' — korrekt uebersprungene Kontakte/Ressourcen ohne sAMAccountName, aber die Meldung liest sich wie ein Fehler und ist nicht uebersetzt. Beides braucht eine verstaendliche deutsche Meldung; die E-Mail-Kollision zusaetzlich eine Entscheidung, ob solche Konten ohne E-Mail angelegt oder bewusst uebersprungen werden. Beleg: .planning/phases/16-ad-gruppen-synchronisation/uat-2026-09-07/windows6a-sync-zahlenzeilen.png | fixed | | 2026-09-07T12:47:32.058Z | 2026-09-09T06:25:18.145Z |
| 16 | 14 | unmet-truth | apps/api/src/tenders/tenders.controller.ts | | Das Postfach im Ausschreibungs-Radar hat keinen Verbindungstest, obwohl die Faehigkeit fertig vorliegt. Beide Inbox-Provider bringen testConnection() mit (imap.provider.ts:343, exchange-inbox.provider.ts:294), und das DKV-Modul nutzt sie ueber POST /dkv/test-connection samt Knopf 'Verbindung testen' im InboxConfigForm. Beim Ausschreibungs-Radar fehlt beides: der Controller kennt zu email-config nur GET (:343) und PUT (:357), kein Test-Endpunkt, und das Formular unter Meine Quellen hat keinen Knopf. Historie geprueft: der Knopf war nie vorhanden (git log -S testConnection im tender-radar-Frontend ist leer), es ist also eine Luecke, keine Regression. Folge fuer den Betrieb: ein Tippfehler in der EWS-Endpunkt-URL oder falsche Zugangsdaten fallen erst auf, wenn dauerhaft nichts ankommt — und dann ist nicht unterscheidbar, ob die Verbindung scheitert oder schlicht keine Alarm-Mail da war. Das trifft besonders WINDOWS #12, dessen ganzer Zweck der Beleg des handgeschriebenen NTLM/SOAP-Wegs ist. Aufgefallen am 2026-09-07 beim Einrichten des Postfachs. | fixed | | 2026-09-07T13:22:33.916Z | 2026-09-09T05:20:09.851Z | | 16 | 14 | unmet-truth | apps/api/src/tenders/tenders.controller.ts | | Das Postfach im Ausschreibungs-Radar hat keinen Verbindungstest, obwohl die Faehigkeit fertig vorliegt. Beide Inbox-Provider bringen testConnection() mit (imap.provider.ts:343, exchange-inbox.provider.ts:294), und das DKV-Modul nutzt sie ueber POST /dkv/test-connection samt Knopf 'Verbindung testen' im InboxConfigForm. Beim Ausschreibungs-Radar fehlt beides: der Controller kennt zu email-config nur GET (:343) und PUT (:357), kein Test-Endpunkt, und das Formular unter Meine Quellen hat keinen Knopf. Historie geprueft: der Knopf war nie vorhanden (git log -S testConnection im tender-radar-Frontend ist leer), es ist also eine Luecke, keine Regression. Folge fuer den Betrieb: ein Tippfehler in der EWS-Endpunkt-URL oder falsche Zugangsdaten fallen erst auf, wenn dauerhaft nichts ankommt — und dann ist nicht unterscheidbar, ob die Verbindung scheitert oder schlicht keine Alarm-Mail da war. Das trifft besonders WINDOWS #12, dessen ganzer Zweck der Beleg des handgeschriebenen NTLM/SOAP-Wegs ist. Aufgefallen am 2026-09-07 beim Einrichten des Postfachs. | fixed | | 2026-09-07T13:22:33.916Z | 2026-09-09T05:20:09.851Z |
| 17 | 6 | unmet-truth | docker-compose.yml | | Hochgeladene Dateien ueberleben kein Neuerstellen der Container. Der Code legt sie unter user-files/ ab (user.controller.ts:40 und :266 fuer Profilbilder, dazu die DKV-Exporte), aber KEINE der Compose-Dateien mountet dieses Verzeichnis — weder im Repository (docker-compose.yml, .prod.yml, .dev.yml haben nur das Volume pgdata) noch in der abweichenden Datei auf dem Server /opt/tessera/docker-compose.yml. Am 2026-09-09 gemessen: 'docker inspect' auf tessera-api-1 meldet ueberhaupt keinen Mount, /app/user-files liegt damit nur in der beschreibbaren Container-Schicht und ist bei jedem 'up -d --force-recreate' weg. Aufgefallen beim Schreiben des Betriebshandbuchs. KEIN Schaden entstanden: aktuell hat kein Nutzer ein Profilbild hinterlegt (avatarPath ueberall NULL), und die bisherigen Neuerstellungen trafen einen leeren Ordner. Die Luecke schlaegt zu, sobald der erste Nutzer ein Bild hochlaedt oder ein DKV-Export aufgehoben werden soll. Behebung: ein benanntes Volume oder Bind-Mount fuer user-files in beiden Compose-Dateien; die Server-Datei muss zusaetzlich von Hand ergaenzt werden, weil sie vom Repository abweicht. | open | | 2026-09-09T06:42:22.801Z | |
````json ````json
[ [
@@ -225,6 +226,18 @@ last_updated: 2026-09-09T06:25:18.145Z
"reason": "", "reason": "",
"recorded_at": "2026-09-07T13:22:33.916Z", "recorded_at": "2026-09-07T13:22:33.916Z",
"resolved_at": "2026-09-09T05:20:09.851Z" "resolved_at": "2026-09-09T05:20:09.851Z"
},
{
"id": 17,
"kind": "unmet-truth",
"phase": "6",
"file": "docker-compose.yml",
"line": null,
"description": "Hochgeladene Dateien ueberleben kein Neuerstellen der Container. Der Code legt sie unter user-files/ ab (user.controller.ts:40 und :266 fuer Profilbilder, dazu die DKV-Exporte), aber KEINE der Compose-Dateien mountet dieses Verzeichnis — weder im Repository (docker-compose.yml, .prod.yml, .dev.yml haben nur das Volume pgdata) noch in der abweichenden Datei auf dem Server /opt/tessera/docker-compose.yml. Am 2026-09-09 gemessen: 'docker inspect' auf tessera-api-1 meldet ueberhaupt keinen Mount, /app/user-files liegt damit nur in der beschreibbaren Container-Schicht und ist bei jedem 'up -d --force-recreate' weg. Aufgefallen beim Schreiben des Betriebshandbuchs. KEIN Schaden entstanden: aktuell hat kein Nutzer ein Profilbild hinterlegt (avatarPath ueberall NULL), und die bisherigen Neuerstellungen trafen einen leeren Ordner. Die Luecke schlaegt zu, sobald der erste Nutzer ein Bild hochlaedt oder ein DKV-Export aufgehoben werden soll. Behebung: ein benanntes Volume oder Bind-Mount fuer user-files in beiden Compose-Dateien; die Server-Datei muss zusaetzlich von Hand ergaenzt werden, weil sie vom Repository abweicht.",
"status": "open",
"reason": "",
"recorded_at": "2026-09-09T06:42:22.801Z",
"resolved_at": null
} }
] ]
```` ````
+63
View File
@@ -0,0 +1,63 @@
# Tessera — Anleitungen
Tessera ist eine Plattform, auf der verschiedene Arbeitswerkzeuge — genannt
**Module** — an einer Stelle zusammenlaufen. Statt zwischen mehreren Anwendungen
zu wechseln, meldet man sich einmal an und findet alles in derselben Oberfläche:
ein einstellbares Dashboard, eine Seitenleiste mit den freigeschalteten Modulen
und einen Marktplatz, über den weitere hinzukommen.
Diese Sammlung richtet sich an vier verschiedene Leserkreise. Suchen Sie sich den
passenden heraus — die Anleitungen überschneiden sich bewusst kaum.
| Anleitung | Für wen | Worum es geht |
|-----------|---------|---------------|
| [Für Anwender](anleitung-anwender.md) | alle, die mit Tessera arbeiten | Anmelden, Dashboard einrichten, Module benutzen |
| [Für Administratoren](anleitung-administration.md) | wer Tessera einrichtet | Benutzer, Gruppen, AD-Anbindung, Freigaben, SMTP |
| [Für den Betrieb](anleitung-betrieb.md) | wer die Server betreut | Installieren, neue Fassungen einspielen, Sicherungen, Fehlersuche |
| [Für Entwickler](anleitung-entwicklung.md) | wer an Tessera mitbaut | Aufbau, Modulsystem, Berechtigungen, Konventionen |
Daneben liegt das [CI/CD-Runbook](ci-cd-setup.md), das die Einrichtung der
Bau-Pipeline in Gitea beschreibt. Es richtet sich an dieselben Leute wie die
Betriebsanleitung, deckt aber nur den Weg vom Quelltext zum fertigen Abbild ab.
---
## Die drei Dinge, die am häufigsten Zeit kosten
Wenn Sie nur wenig lesen wollen — diese drei Punkte haben in der Praxis am
meisten Verwirrung gestiftet:
**1. Die Anmeldung läuft über den Benutzernamen, nicht über die E-Mail-Adresse.**
Das Feld heißt „Benutzername". Wer stattdessen seine E-Mail-Adresse einträgt,
kommt nicht hinein — ohne dass eine hilfreiche Meldung erscheint. Es sieht aus
wie ein kaputter Login, ist aber nur das falsche Feld.
**2. „Aktiviert" und „freigegeben" sind zwei verschiedene Dinge.**
Ein Modul wird zuerst für das Unternehmen aktiviert und danach einzelnen Gruppen
oder Personen freigegeben. Sehen Sie ein Modul im Marktplatz, aber nicht in Ihrer
Seitenleiste, fehlt die zweite Stufe — wenden Sie sich an Ihre Administration.
Details in der [Administrationsanleitung](anleitung-administration.md).
**3. Beim Ausrollen genügt `docker compose up -d` nicht.**
Ohne `--force-recreate` laufen die alten Container weiter, obwohl ein neues
Abbild heruntergeladen wurde — ohne jede Fehlermeldung. Das Einspielen wirkt
erfolgreich, ist es aber nicht. Der genaue Ablauf samt Kontrollbefehl steht in
der [Betriebsanleitung](anleitung-betrieb.md).
---
## Zum Stand dieser Anleitungen
Sie wurden gegen den tatsächlichen Quelltext geschrieben, nicht aus der Planung
abgeleitet. Beschriftungen von Schaltflächen und Feldern sind wörtlich aus den
Sprachdateien der Oberfläche übernommen, damit sie zu dem passen, was auf dem
Bildschirm steht.
Zwei Einschränkungen, die Sie kennen sollten:
- Wo eine Aussage sich nicht aus dem Quelltext belegen ließ — etwa eine
Einstellung, die von Hand auf dem Server ergänzt wurde — steht ein Hinweis im
Text statt einer Vermutung.
- Tessera wird derzeit ausschließlich intern eingesetzt. Die Trennung mehrerer
Mandanten ist in der Architektur angelegt, aber nicht Gegenstand dieser
Anleitungen.
+240
View File
@@ -0,0 +1,240 @@
<!-- generated-by: gsd-doc-writer -->
# Tessera – Administrationshandbuch
Dieses Handbuch richtet sich an Kolleginnen und Kollegen, die Tessera für ihre Organisation konfigurieren: Benutzer und Gruppen verwalten, die Anbindung an das Active Directory (AD) betreuen, Module freischalten und den Mailversand einrichten. Es setzt keine Programmierkenntnisse voraus, aber Grundwissen über AD-Gruppen, LDAP und SMTP-Server.
## Inhaltsverzeichnis
1. [Rollen und was sie dürfen](#1-rollen-und-was-sie-dürfen)
2. [Benutzerverwaltung](#2-benutzerverwaltung)
3. [Gruppen](#3-gruppen)
4. [AD-/LDAP-Anbindung](#4-ad--ldap-anbindung)
5. [Modulverwaltung und Freigaben-Matrix](#5-modulverwaltung-und-freigaben-matrix)
6. [SMTP](#6-smtp)
7. [Mandanten](#7-mandanten)
8. [Fehlersuche für Administratoren](#8-fehlersuche-für-administratoren)
---
## 1. Rollen und was sie dürfen
Tessera kennt drei Rollen:
| Rolle | Anzeige im Header | Was sie zusätzlich zu USER darf |
|---|---|---|
| **USER** | Benutzer | Zugriff nur auf Module, die ihm direkt oder über eine Gruppe freigegeben wurden. Sieht keinen Administrationsbereich. |
| **ADMIN** | Admin | Voller Zugriff auf den Administrationsbereich des **eigenen** Mandanten: Benutzer, Gruppen, LDAP, SMTP, Module und die Freigaben-Matrix. Sieht und ändert nur Benutzer/Daten des eigenen Mandanten. |
| **SUPER_ADMIN** | Super-Admin | Alles, was ADMIN darf, zusätzlich mandantenübergreifend (sieht alle Benutzer aller Mandanten) sowie exklusiv: die Mandantenverwaltung unter „Mandanten“. Kann als einzige Rolle die Rolle SUPER_ADMIN an einen anderen Benutzer vergeben. |
Wichtige Details, die im Code tatsächlich so umgesetzt sind:
- Der Menüpunkt „Administrator“ im Header erscheint nur für ADMIN und SUPER_ADMIN. Ein Benutzer mit der Rolle USER hat keinen sichtbaren Einstiegspunkt in den Administrationsbereich.
- Innerhalb des Administrationsbereichs ist „Mandanten“ der einzige Navigationspunkt, der ausschließlich für SUPER_ADMIN sichtbar ist; alle anderen Punkte (Benutzer, Module, LDAP, SMTP, Gruppen) sind für ADMIN und SUPER_ADMIN gleichermaßen erreichbar.
- **ADMIN und SUPER_ADMIN haben immer Zugriff auf alle für den Mandanten aktivierten Module** – unabhängig von der Freigaben-Matrix (siehe Kapitel 5). Die Matrix regelt ausschließlich, welche Module ein Benutzer mit der Rolle USER sehen darf.
- Ein ADMIN kann einem Benutzer nicht die Rolle SUPER_ADMIN zuweisen – weder beim Anlegen noch beim Bearbeiten. Nur ein SUPER_ADMIN darf das.
## 2. Benutzerverwaltung
Der Bereich **Administrator → Benutzer** zeigt eine Tabelle mit Benutzername, E-Mail, Anzeigename, Rolle und Status. Ein ADMIN sieht dabei ausschließlich die Benutzer des eigenen Mandanten, ein SUPER_ADMIN sieht alle.
### Benutzer anlegen
Über „Benutzer erstellen“ öffnet sich ein Formular mit den Feldern Benutzername, E-Mail, Passwort (mindestens 8 Zeichen), Anzeigename und Rolle. Bei der manuellen Anlage sind Benutzername, E-Mail und Passwort Pflichtfelder. Ein ADMIN kann in der Rollen-Auswahl nur USER oder ADMIN wählen; die Option SUPER_ADMIN erscheint ausschließlich, wenn der eingeloggte Benutzer selbst SUPER_ADMIN ist.
Ein neu angelegter Benutzer wird automatisch Mitglied der als **Standardgruppe** markierten Gruppe (siehe Kapitel 3) – dieser Schritt läuft unsichtbar im Hintergrund, sobald das Konto erstellt wird.
### Benutzer bearbeiten
Beim Bearbeiten lassen sich Benutzername, E-Mail, Anzeigename und Rolle ändern; das Passwortfeld ist optional und wird nur überschrieben, wenn ein neuer Wert eingegeben wird.
### Lokale vs. verzeichnisgeführte Konten
Ein Konto, das über die AD-Anbindung importiert oder synchronisiert wurde, hat kein lokales Passwort – die Anmeldung läuft ausschließlich über eine Prüfung gegen das Verzeichnis (siehe Kapitel 4). Konten, die über das Formular „Benutzer erstellen“ manuell angelegt wurden, haben ein lokales Passwort und melden sich unabhängig von jeder LDAP-Konfiguration an.
Seit Kurzem ist die E-Mail-Adresse eines Benutzers **optional**: Wenn beim Import eine Adresse bereits einem anderen Konto gehört, wird das Konto trotzdem angelegt bzw. aktualisiert – nur eben ohne diese Adresse. Anmeldung und Zugriff funktionieren für ein solches Konto normal, lediglich Benachrichtigungen per E-Mail (z. B. Passwort-Reset) erreichen es nicht. In der Tabelle wird eine fehlende Adresse als „–“ angezeigt. Bei der manuellen Anlage über das Formular ist eine E-Mail-Adresse weiterhin Pflicht.
### Deaktivieren und Löschen
Die Benutzerliste zeigt einen Status „Aktiv“/„Inaktiv“ an, dieser lässt sich aber **nicht** über einen Schalter im Formular umschalten – im Bearbeiten-Dialog gibt es dafür kein Feld. Ein Konto wird auf zwei Wegen inaktiv:
- automatisch durch die LDAP-Synchronisation, wenn der zugehörige Verzeichniseintrag nicht mehr gefunden wird (siehe Kapitel 4, Abschnitt Synchronisation);
- durch den Button „Benutzer löschen“ in der Tabelle – dieser löscht den Benutzer jedoch **endgültig** aus der Datenbank, es handelt sich nicht um ein reversibles Deaktivieren.
Der eigene Account lässt sich nicht löschen; der Löschen-Button ist für die eigene Zeile deaktiviert.
### Details zu Gruppen und Modulzugriff
Über den Button „Details“ neben einem Benutzer öffnet sich ein Dialog mit zwei Abschnitten:
- **Gruppenmitgliedschaften** – rein informativ (Chips mit Quelle „LDAP“ oder „Manuell“); Änderungen daran erfolgen ausschließlich unter „Gruppen“.
- **Modul-Zugriff** – eine Tabelle je aktiviertem Modul mit der Spalte „Über Gruppe(n)“ (zeigt, über welche Gruppenmitgliedschaft(en) der Zugriff vererbt wird) und einer Checkbox „Direkt“, mit der zusätzlich zur Gruppenvererbung ein direkter Zugriff für genau diesen Benutzer gesetzt oder entzogen werden kann.
## 3. Gruppen
Der Bereich **Administrator → Gruppen** verwaltet Gruppen, über die Modulzugriff vergeben wird (siehe Kapitel 5). Es gibt zwei Arten von Gruppen:
- **Manuelle Gruppen**: über „Gruppe erstellen“ mit einem frei wählbaren Namen angelegt. Name und Mitglieder lassen sich jederzeit bearbeiten.
- **AD-gebundene Gruppen**: entstehen ausschließlich über den Import im LDAP-Bereich (Kapitel 4, „AD-Gruppen importieren“) – sie können nicht über den „Gruppe erstellen“-Dialog angelegt werden. Der Dialog weist bei einer Neuanlage per Hinweistext auf den LDAP-Bereich hin.
Die Tabelle unterscheidet beide Typen über ein Badge in der Spalte „AD-Bindung“ („AD-gebunden“ bzw. „Manuell“).
### Interner Name
Bei einer AD-gebundenen Gruppe ist das Namensfeld gesperrt: Der Name wird bei jeder Synchronisation automatisch mit dem AD-Wert überschrieben. Stattdessen lässt sich ein zusätzliches Feld **„Interner Name“** setzen. Dieser interne Name wird von der Synchronisation **nie** verändert oder überschrieben. Solange er gesetzt ist, zeigt die Oberfläche (Gruppentabelle, Freigaben-Matrix) diesen internen Namen anstelle des AD-Namens an; ist er leer, erscheint stattdessen der AD-Name. Damit lässt sich eine Gruppe intern anders benennen, ohne dass der nächste Sync-Lauf diese Bezeichnung wieder zurücksetzt.
Der AD-DN der Gruppe wird im Bearbeiten-Dialog zur Nachvollziehbarkeit schreibgeschützt mit angezeigt.
### Mitglieder
Über „Mitglieder“ lässt sich pro Gruppe eine Mitgliederliste öffnen. Mitglieder mit Quelle „LDAP“ (durch den AD-Sync eingetragen) lassen sich hier **nicht** manuell entfernen – der Entfernen-Button ist für sie deaktiviert, da diese Mitgliedschaft bei der nächsten Synchronisation ohnehin wieder aus dem AD nachgeführt wird. Manuelle Mitgliedschaften (Quelle „Manuell“) lassen sich jederzeit frei hinzufügen und entfernen.
### Standardgruppe
Genau eine Gruppe pro Mandant kann als **Standardgruppe** markiert werden (Stern-Symbol in der Tabelle). Diese Markierung hat zwei konkrete Auswirkungen:
1. **Jeder neu angelegte Benutzer** – ob manuell oder per LDAP-Import/-Sync – wird automatisch Mitglied dieser Gruppe.
2. Beim Aktivieren eines Moduls bietet der Dialog „Sofort freigeben“ an, das Modul direkt für die Standardgruppe freizugeben (siehe Kapitel 5).
Wird die Markierung auf eine andere Gruppe umgehängt, geschieht das serverseitig als exklusiver Wechsel: Die vorherige Standardgruppe verliert die Markierung automatisch. Ein Mandant ohne jede Gruppe erhält beim ersten Bedarf automatisch eine Gruppe „Alle Benutzer“ als Standardgruppe – dieser Automatismus greift aber nur, wenn tatsächlich noch keine einzige Gruppe existiert, nicht wenn lediglich keine Gruppe als Standard markiert ist.
### Gruppe löschen
Der Löschen-Dialog zeigt vorab an, wie viele Mitglieder und Modul-Freigaben von der Löschung betroffen sind, und weist darauf hin, dass betroffene Benutzer den Zugriff verlieren, sofern sie ihn nicht anderweitig (direkt oder über eine andere Gruppe) haben. Dies gilt auch für AD-gebundene Gruppen – auch sie lassen sich manuell löschen.
## 4. AD-/LDAP-Anbindung
Der Bereich **Administrator → LDAP** bündelt alles rund um die Anbindung an ein Active Directory bzw. einen LDAP-Verzeichnisdienst. Er ist in mehrere Abschnitte gegliedert.
### Verbindungseinstellungen
| Feld | Bedeutung |
|---|---|
| **Server-URL** | Die Adresse des Verzeichnisdienstes, z. B. `ldap://ldap.example.com` oder `ldaps://...` für eine verschlüsselte Verbindung. Pflichtfeld. |
| **Basis-DN** | Mehrzeiliges Feld – **ein DN pro Zeile**. Die Basis-DN(s) legen den Umfang der Synchronisation fest: Nur was unterhalb dieser DN(s) im Verzeichnis liegt, wird überhaupt betrachtet. Mehrere Zeilen bedeuten mehrere gleichrangige Suchwurzeln. |
| **Bind-DN** | Der DN, mit dem sich Tessera am Verzeichnis anmeldet, um zu suchen (z. B. `cn=admin,dc=example,dc=com`). Leer gelassen wird ein anonymer Bind versucht. |
| **Bind-Passwort** | Das Passwort zum Bind-DN. Wird verschlüsselt gespeichert; beim erneuten Öffnen des Formulars wird es nie im Klartext angezeigt oder erneut mitgesendet, solange kein neuer Wert eingetragen wird. |
| **Suchfilter** | LDAP-Suchfilter für die Benutzersuche, standardmäßig `(objectClass=person)`. |
| **TLS-Zertifikat nicht prüfen (unsicher)** | Nur relevant bei `ldaps://` mit einem Zertifikat einer internen/selbstsignierten Zertifizierungsstelle (typischer Fehler: „unable to verify the first certificate“). Schaltet die Zertifikatsprüfung ab – nur in vertrauenswürdigen Netzen verwenden. |
Über „Verbindung testen“ lässt sich die Konfiguration prüfen, **bevor** sie gespeichert wird – die aktuell im Formular eingetragenen Werte werden dafür verwendet (ein leeres Bind-Passwort-Feld greift dabei auf das bereits gespeicherte Passwort zurück, falls eine Konfiguration existiert).
### Feld-Zuordnung (LDAP-Feld → Tessera-Feld)
Legt fest, welches LDAP-Attribut auf welches Tessera-Feld gemappt wird. Beim erstmaligen Anlegen der Konfiguration werden automatisch drei Standard-Zuordnungen angelegt, die sich nicht entfernen lassen:
| LDAP-Feld | Tessera-Feld |
|---|---|
| `displayName` | Anzeigename |
| `mail` | E-Mail |
| `sAMAccountName` | Benutzername |
Zusätzliche, selbst definierte Zuordnungen lassen sich über „Zuordnung hinzufügen“ ergänzen und über „Entfernen“ wieder löschen.
### Import-Filter (Gruppen/OUs)
Eine **zusätzliche, optionale** Einschränkung, unabhängig von der Feld-Zuordnung: Ohne Auswahl werden alle Benutzer unter den konfigurierten Basis-DN(s) synchronisiert. Über „Gruppen/OUs suchen“ lässt sich das Verzeichnis nach Gruppen und Organisationseinheiten durchsuchen; ausgewählte Einträge grenzen die Synchronisation weiter ein. DNs lassen sich auch manuell eintragen. Diese Auswahl muss über „Filter speichern“ separat gesichert werden.
### AD-Gruppen importieren
Legt ausgewählte AD-Gruppen als eigenständige Tessera-Gruppen an. Das ist der **einzige** Weg, eine AD-gebundene Gruppe zu erzeugen (siehe Kapitel 3). Wichtig zu wissen:
- Nach dem Import wird die Gruppe bei jeder weiteren Synchronisation automatisch nachgeführt: Ein Umbenennen der Gruppe im AD wird übernommen, ihre Löschung im AD führt (nach einer zusätzlichen Absicherung gegen bloße Verschiebung außerhalb des konfigurierten Bereichs) auch zur Löschung der Tessera-Gruppe.
- Bereits importierte Gruppen werden in der Trefferliste als „Bereits importiert“ markiert und lassen sich nicht ein zweites Mal auswählen.
- Kollidiert der Name der zu importierenden AD-Gruppe mit einer bereits vorhandenen lokalen Gruppe, schlägt der Import für diesen Eintrag fehl; die bestehende lokale Gruppe muss zunächst umbenannt oder mit einem internen Namen versehen werden.
- Die Mitgliedschaften der importierten Gruppe werden nicht sofort befüllt, sondern erst beim nächsten Sync-Lauf (manuell oder nach Intervall).
### Einzelbenutzer suchen und importieren
Sucht gezielt einzelne AD-Benutzer über einen Freitext (Name, Benutzername oder E-Mail) und importiert nur die ausgewählten Treffer. Bereits vorhandene Benutzer werden als „Bereits importiert“ markiert und übersprungen – es entsteht dadurch kein Doppelimport, auch wenn später eine vollständige Synchronisation über den Import-Filter läuft.
### Benutzer ausschließen (Denylist)
Eine Liste einzelner Benutzernamen, die **nie** importiert werden – typischerweise technische Dienstkonten wie `administrator`, `krbtgt`, `guest` oder `ldap$`. Sie wirkt zusätzlich zum Gruppen-/OU-Filter, also unabhängig davon, ob der Eintrag im gefilterten Suchbereich läge. Die Liste muss über „Ausschlussliste speichern“ gesichert werden.
### Synchronisation
Zwei Zeitpunkte für den Sync-Lauf:
- **Sync-Intervall (Minuten)** – der Wert **0** bedeutet: automatische Synchronisation ist deaktiviert. Ein Wert größer 0 lässt Tessera in diesem Abstand automatisch synchronisieren.
- **„Jetzt synchronisieren“** – löst unabhängig vom Intervall sofort einen Sync-Lauf aus.
**Wichtiges, im Code verifiziertes Verhalten:** Der Schalter „Aktiviert/Deaktiviert“ in diesem Abschnitt (Feld `isActive` der LDAP-Konfiguration) ist **kein** „Auto-Sync aus“-Schalter. Er entscheidet serverseitig, ob sich Benutzer, deren Konto kein lokales Passwort hat (also alle per LDAP importierten Konten), überhaupt noch **anmelden** dürfen. Wird dieser Schalter deaktiviert, werden nicht etwa nur zukünftige Sync-Läufe pausiert – jeder verzeichnisgeführte Benutzer wird sofort von der Anmeldung ausgesperrt, unabhängig vom Sync-Intervall. Um ausschließlich die automatische Synchronisation abzuschalten, ohne bestehende Anmeldungen zu blockieren, ist stattdessen das Sync-Intervall auf 0 zu setzen.
Nach einem Lauf zeigt Tessera einen Ergebnisbericht mit mehreren Zeilen:
1. **Erstellt / aktualisiert / deaktiviert** – wie viele Benutzerkonten neu angelegt, aktualisiert bzw. deaktiviert wurden. Deaktiviert werden Konten, deren zugehöriger AD-Eintrag im aktuellen Lauf nicht mehr gefunden wurde (der Benutzer bleibt dabei in der Datenbank erhalten, wird aber inaktiv gesetzt, siehe Kapitel 2).
2. **Gruppenmitgliedschaften** – wie viele Mitgliedschaften über AD-gebundene Gruppen hinzugefügt bzw. entfernt wurden.
3. **AD-Gruppen** – wie viele Gruppen neu übernommen (adoptiert), umbenannt oder gelöscht wurden.
Darüber hinaus können folgende Zusatzabschnitte erscheinen:
- **Standardgruppen-Markierung musste neu vergeben werden (n×)** – erscheint in Gelb/Amber, wenn im selben Lauf eine AD-gebundene Gruppe gelöscht wurde, die zugleich als Standardgruppe markiert war. Die Markierung wird dabei automatisch an eine andere Gruppe im Mandanten übergeben, damit nie ein Mandant ganz ohne Standardgruppe dasteht.
- **Konten ohne E-Mail-Adresse** – listet Konten auf, die in diesem Lauf ohne E-Mail-Adresse angelegt oder aktualisiert wurden, weil die im AD hinterlegte Adresse bereits zu einem anderen Konto gehört. Anmeldung und Zugriff funktionieren für diese Konten normal, nur Benachrichtigungen per E-Mail erreichen sie nicht (siehe Kapitel 2).
- **Ohne Anmeldenamen übersprungen** – Verzeichniseinträge ohne Anmeldename (`sAMAccountName`). Das ist ein normaler, kein fehlerhafter Zustand – typischerweise Kontakte oder Verteilerlisten im Verzeichnis, aus denen bewusst kein Tessera-Konto entsteht.
- **Fehler / unerwartete Fehler** – Einträge, bei denen ein technischer Fehler auftrat. Die genaue technische Meldung steht ausschließlich im Server-Protokoll, im Bericht erscheint nur der betroffene Eintrag.
Schlägt der Sync-Lauf als Ganzes fehl (z. B. weil die Verbindung nicht zustande kommt), erscheint statt eines Berichts mit lauter Nullen eine eindeutige Fehlermeldung, dass die Synchronisation nicht ausgeführt werden konnte.
## 5. Modulverwaltung und Freigaben-Matrix
Der Zugriff auf ein Modul läuft zweistufig:
1. **Aktivierung für den Mandanten** (Administrator → Module): Ein Modul muss zunächst für den Mandanten aktiviert werden. Ohne Aktivierung ist es für niemanden im Mandanten nutzbar, unabhängig von jeder Freigabe.
2. **Freigabe pro Gruppe oder Benutzer** (Freigaben-Matrix bzw. Benutzer-Detaildialog): Erst danach lässt sich steuern, welche Gruppen oder einzelnen Benutzer mit der Rolle USER Zugriff erhalten.
ADMIN und SUPER_ADMIN benötigen dafür keine Freigabe – sie haben, sobald ein Modul mandantenweit aktiviert ist, automatisch Zugriff darauf. Die Freigaben-Matrix betrifft ausschließlich die Rolle USER.
### Modul aktivieren
Beim Umschalten eines deaktivierten Moduls auf „Aktiviert“ öffnet sich ein Bestätigungsdialog mit zwei Optionen:
- **„Sofort freigeben“** – aktiviert das Modul für den Mandanten und gibt es zugleich für die als Standardgruppe markierte Gruppe frei (siehe Kapitel 3). Diese Option ist deaktiviert, solange keine Gruppe als Standardgruppe markiert ist; ein Hinweistext erklärt das im Dialog.
- **„Später konfigurieren“** – aktiviert das Modul nur für den Mandanten, ohne irgendeine Freigabe zu setzen. Benutzer mit der Rolle USER sehen das Modul dann erst, nachdem eine Freigabe manuell über die Freigaben-Matrix erteilt wurde.
Ein bereits aktiviertes Modul lässt sich direkt über den Schalter wieder deaktivieren, ohne Rückfrage.
### Freigaben-Matrix
Unter „Module → Freigaben-Matrix“ erscheint eine Tabelle: Zeilen sind die aktivierten Module (nach Kategorie gruppiert), Spalten sind die Gruppen des Mandanten (angezeigt mit ihrem internen Namen, falls gesetzt, sonst mit dem regulären Namen). Jede Zelle ist eine Checkbox, die die Freigabe für genau diese Kombination aus Modul und Gruppe an- oder abschaltet – Änderungen werden sofort gespeichert. Über das Suchfeld lässt sich sowohl nach Modul- als auch nach Gruppennamen filtern; ein Treffer auf nur einer Achse leert die andere Achse nicht.
Ein Hinweistext unterhalb der Matrix erinnert daran, dass ADMIN und SUPER_ADMIN immer Zugriff auf alle aktiven Module haben und diese Matrix nur die Rolle USER betrifft.
## 6. SMTP
Unter **Administrator → SMTP** wird der Mailversand konfiguriert: Host, Port, Verschlüsselung (Keine, STARTTLS oder SSL-TLS), Benutzername, Passwort und die Absenderadresse. Das Passwortfeld wird aus Sicherheitsgründen nie mit dem gespeicherten Wert vorbefüllt – es bleibt beim Laden immer leer und wird nur mitgesendet, wenn tatsächlich ein neuer Wert eingegeben wurde.
Über „Test-E-Mail an“ lässt sich optional eine echte Testnachricht an eine beliebige Adresse verschicken, um die Konfiguration vor dem produktiven Einsatz zu prüfen.
Ohne funktionierende SMTP-Konfiguration versendet Tessera keine E-Mails. Das betrifft insbesondere:
- den Versand von Passwort-Reset-Mails an Benutzer, die ihr Passwort vergessen haben;
- E-Mail-Benachrichtigungen einzelner Module, die auf Mailversand angewiesen sind.
Die Anmeldung selbst, der laufende Betrieb und die LDAP-Synchronisation sind von der SMTP-Konfiguration unabhängig und funktionieren auch ohne sie.
## 7. Mandanten
Der Bereich **Administrator → Mandanten** ist ausschließlich für SUPER_ADMIN sichtbar und zugänglich (siehe Kapitel 1). Er bietet:
- eine Übersicht aller Mandanten mit Name, Slug, Status (Aktiv/Inaktiv) und Anzahl der zugehörigen Benutzer;
- „Mandant erstellen“ mit den Feldern Name und Slug (Slug nur bei der Neuanlage editierbar, danach fest);
- „Bearbeiten“, worüber sich ausschließlich der Name ändern lässt;
- einen Schalter, um einen Mandanten zwischen Aktiv und Inaktiv umzuschalten;
- „Löschen“ mit Bestätigungsdialog.
Ein Mandant lässt sich **nicht löschen, solange er noch aktive Benutzer hat** – ein Löschversuch scheitert in diesem Fall mit einem entsprechenden Hinweis. Die Benutzer müssen zuvor deaktiviert oder einem anderen Mandanten zugeordnet werden.
## 8. Fehlersuche für Administratoren
| Symptom | Wahrscheinliche Ursache |
|---|---|
| Alle Benutzer aus dem Active Directory können sich plötzlich nicht mehr anmelden, obwohl der Sync-Lauf zuvor erfolgreich war. | Der Schalter „Aktiviert/Deaktiviert“ im Sync-Bereich der LDAP-Konfiguration wurde ausgeschaltet. Dieser Schalter sperrt sofort jede Anmeldung verzeichnisgeführter Konten – er ist **nicht** gleichbedeutend mit einer pausierten Synchronisation (siehe Kapitel 4). Zum Aussetzen nur der automatischen Synchronisation stattdessen das Sync-Intervall auf 0 setzen und den Schalter aktiviert lassen. |
| „Verbindung testen“ oder ein Sync-Lauf schlägt mit einem Bind-/Authentifizierungsfehler fehl (z. B. einer Meldung, die auf einen fehlgeschlagenen Sicherheitskontext hinweist). | Häufigste Ursache ist eine vertauschte Eingabe von Bind-DN und Basis-DN, oder ein falsches Bind-Passwort. Beide Felder in den Verbindungseinstellungen prüfen. |
| Ein AD-Benutzer wird zwar synchronisiert, aber ohne E-Mail-Adresse angelegt oder aktualisiert; Passwort-Reset-Mails erreichen ihn nicht. | Normales, dokumentiertes Verhalten seit E-Mail optional ist: Die im AD hinterlegte Adresse gehört bereits zu einem anderen Konto und wird deshalb nicht übernommen. Der Ergebnisbericht des Sync-Laufs listet solche Fälle unter „Konten ohne E-Mail-Adresse“ auf. Die Kollision im betroffenen Konto (dem Halter der Adresse) prüfen und die Adresse dort ggf. korrigieren. |
| Eine AD-Gruppe erscheint im Sync-Bericht als gelöscht, obwohl sie im Verzeichnis noch existiert. | Die Gruppe wurde vermutlich außerhalb der konfigurierten Basis-DN(s) verschoben. Tessera prüft vor einer Löschung zwar zusätzlich die Domänenwurzel, kann eine Verschiebung aber nur innerhalb der erreichbaren Suchbereiche erkennen. Basis-DN(s) und Import-Filter prüfen. |
| Ein Benutzer mit Rolle USER sieht ein Modul nicht, obwohl es für den Mandanten aktiviert ist. | Für dieses Modul fehlt eine Freigabe – weder direkt (Benutzer-Details) noch über eine Gruppe (Freigaben-Matrix). Prüfen, ob der Benutzer Mitglied der vorgesehenen Gruppe ist und ob die Matrix-Zelle für Modul/Gruppe gesetzt ist. |
| Ein neu importierter/erstellter Benutzer hat unerwartet Zugriff auf ein Modul, das eigentlich niemandem freigegeben sein sollte. | Der Benutzer wurde automatisch Mitglied der Standardgruppe (siehe Kapitel 3), und dieser Gruppe wurde beim Aktivieren eines Moduls über „Sofort freigeben“ Zugriff erteilt. Freigaben-Matrix prüfen und ggf. die Standardgruppen-Freigabe für das betreffende Modul entfernen. |
| Der Name einer AD-gebundenen Gruppe „springt“ nach jedem Sync-Lauf auf den AD-Namen zurück, obwohl ein anderer Name gewünscht ist. | Erwartetes Verhalten: Der Anzeigename einer AD-gebundenen Gruppe wird bei jedem Sync mit dem AD-Wert überschrieben. Für einen dauerhaft abweichenden Anzeigenamen den „Internen Namen“ im Bearbeiten-Dialog der Gruppe setzen – dieses Feld wird von der Synchronisation nie berührt. |
| Ein AD-Gruppen-Import schlägt für eine bestimmte Gruppe mit einem Namenskonflikt fehl. | Es existiert bereits eine lokale (manuelle) Gruppe mit demselben Namen. Diese lokale Gruppe umbenennen oder – falls es sich tatsächlich um dieselbe Gruppe handeln soll – vor dem Import einen internen Namen dafür vergeben, dann erneut importieren. |
| Eine Test- oder Benachrichtigungs-E-Mail kommt nicht an. | SMTP-Konfiguration unter Administrator → SMTP prüfen (Host, Port, Verschlüsselungsart, Zugangsdaten, Absenderadresse) und über „Verbindung testen“ mit einer Test-Empfängeradresse erneut prüfen. |
| Ein Mandant lässt sich nicht löschen. | Der Mandant hat noch mindestens einen aktiven Benutzer. Alle Benutzer des Mandanten zunächst deaktivieren oder umziehen, dann erneut löschen. |
+166
View File
@@ -0,0 +1,166 @@
<!-- generated-by: gsd-doc-writer -->
# Tessera — Anleitung für Anwender
Diese Anleitung richtet sich an alle Kolleginnen und Kollegen, die Tessera im Arbeitsalltag nutzen. Sie beschreibt die Oberfläche und alle Module so, wie sie tatsächlich am Bildschirm erscheinen.
## Inhaltsverzeichnis
1. [Was ist Tessera?](#was-ist-tessera)
2. [Anmeldung](#anmeldung)
3. [Aufbau der Oberfläche](#aufbau-der-oberfläche)
4. [Dashboard](#dashboard)
5. [Marktplatz](#marktplatz)
6. [Die Module](#die-module)
- [Ausschreibungs-Radar](#ausschreibungs-radar)
- [DKV-Rechnung](#dkv-rechnung)
- [Zertifikat-Manager](#zertifikat-manager)
- [Domaincheck](#domaincheck)
7. [Persönliche Einstellungen](#persönliche-einstellungen)
8. [Häufige Stolpersteine](#häufige-stolpersteine)
---
## Was ist Tessera?
Tessera ist das zentrale Portal, in dem alle Arbeitswerkzeuge Ihres Unternehmens an einem Ort zusammenlaufen. Statt zwischen einzelnen Programmen zu wechseln, finden Sie jedes freigeschaltete Werkzeug — ein sogenanntes „Modul" — in der Seitenleiste und öffnen es mit einem Klick. Ihr persönliches Dashboard zeigt Ihnen zusätzlich frei zusammenstellbare Kacheln mit Uhrzeit, Kalender, Notizen und anderen Helfern für den täglichen Gebrauch.
## Anmeldung
Rufen Sie die Anmeldeseite auf. Auf der linken Seite sehen Sie das Tessera-Logo, rechts das Anmeldeformular mit den Feldern **Benutzername** und **Passwort**.
**Wichtig:** Melden Sie sich immer mit Ihrem **Benutzernamen** an — nicht mit Ihrer E-Mail-Adresse. Das Anmeldefeld heißt zwar oft wie eine E-Mail-Eingabe aus, akzeptiert aber ausschließlich den Benutzernamen. Wird stattdessen eine E-Mail-Adresse eingegeben, meldet das System schlicht „Benutzername oder Passwort ungültig" — es sieht dann so aus, als sei die Anmeldung grundsätzlich kaputt, obwohl nur das falsche Feld befüllt wurde. Fragen Sie im Zweifel Ihren Administrator nach Ihrem Benutzernamen.
Über den Link „Passwort vergessen?" können Sie ein neues Passwort per E-Mail anfordern — dieser Weg funktioniert nur für Konten, deren Passwort lokal in Tessera verwaltet wird (siehe [Persönliche Einstellungen](#persönliche-einstellungen)).
Falls Ihr Administrator beim Anlegen Ihres Kontos eine Passwort-Änderung erzwungen hat, werden Sie nach der Anmeldung automatisch aufgefordert, ein neues Passwort zu vergeben, bevor Sie fortfahren können.
## Aufbau der Oberfläche
Die Portal-Oberfläche gliedert sich in drei feste Bereiche:
**Kopfleiste (oben)**
Links steht das Tessera-Logo, in der Mitte der aktuelle Seitentitel. Rechts finden Sie zwei Bedienelemente:
- Einen Schalter zum Umschalten zwischen hellem und dunklem Erscheinungsbild (siehe [Persönliche Einstellungen](#persönliche-einstellungen)).
- Ihr Benutzersymbol (Avatar oder Ihr Anfangsbuchstabe). Ein Klick öffnet das **Benutzermenü** mit:
- Ihrem Namen und Ihrer Rolle (Benutzer, Admin oder Super-Admin),
- **Einstellungen** — führt zu Ihren persönlichen Einstellungen,
- **Administrator** — erscheint nur, wenn Sie Admin- oder Super-Admin-Rechte haben,
- **Abmelden**.
**Seitenleiste (links)**
Ganz oben stehen zwei feste Einträge: **Dashboard** (Ihre Startseite) und **Marktplatz**. Darunter folgt ein Suchfeld „Module suchen…", mit dem Sie die Modulliste filtern können, und darunter die Liste der für Sie freigegebenen Module, gruppiert nach **Kategorien**. Ein Klick auf eine Kategorie klappt sie auf und zeigt die einzelnen Module darin. Sind für Sie noch keine Module aktiv, steht dort „Keine Module".
Unten in der Seitenleiste finden Sie die Sprachumschaltung (Deutsch/English) sowie Ihren Namen mit Rolle. Über den Pfeil-Button am unteren Rand können Sie die Seitenleiste ein- und wieder ausklappen — im eingeklappten Zustand bleiben nur die Symbole sichtbar, das spart Platz auf kleineren Bildschirmen.
## Dashboard
Das Dashboard ist Ihre persönliche Startseite und öffnet sich automatisch nach der Anmeldung. Es zeigt ein Raster aus Kacheln — den **Widgets**. Ist noch kein Widget platziert, sehen Sie nur das Tessera-Symbol mit dem Hinweis „Keine Widgets aktiv".
**Widgets hinzufügen und anordnen:** Oben rechts auf dem Dashboard befindet sich der Schalter **„Dashboard bearbeiten"**. Sobald der Bearbeitungsmodus aktiv ist:
- Erscheint der Button **„Widget hinzufügen"**, der eine Auswahl aller verfügbaren Widget-Typen als Kachel-Katalog öffnet. Ein Klick auf einen Eintrag fügt das Widget sofort dem Dashboard hinzu.
- Können Sie bestehende Widgets per Ziehen an eine neue Position verschieben.
- Können Sie Widgets an der Ecke in der Größe ziehen (jeder Widget-Typ hat eine Mindestgröße, damit der Inhalt lesbar bleibt).
- Erscheint an jedem Widget ein Symbol zum Entfernen.
Ihre Änderungen werden über **„Änderungen speichern"** übernommen. Verlassen Sie den Bearbeitungsmodus, ist das Dashboard wieder fest — Verschieben und Größenänderung sind dann gesperrt, damit Sie es im normalen Gebrauch nicht versehentlich verstellen.
**Verfügbare Widgets:**
| Widget | Zweck |
|---|---|
| Uhr | Zeigt die aktuelle Uhrzeit an (optional mit Datum) |
| Suchleiste | Schnellsuche im Web über frei konfigurierbare Suchanbieter |
| Kalender | Zeigt kommende Termine aus Ihren verbundenen Kalenderquellen |
| Notizen | Freitext-Notizen mit Markdown-Formatierung |
| Taschenrechner | Grundrechenarten, auch per Tastatur bedienbar |
| Favoriten | Schnellzugriff auf mehrere selbst gepflegte Links, als Liste oder Kachelansicht |
| Link | Schnellzugriff auf genau einen einzelnen Link |
| Stoppuhr | Zeitmessung mit Rundenzeiten |
Für Suchleiste, Kalender, Favoriten und Link gibt es zusätzliche Einstellungen (z. B. eigene Suchanbieter, Kalenderquellen, hinterlegte Links) — diese finden Sie unter **Einstellungen > Dashboard**, siehe [Persönliche Einstellungen](#persönliche-einstellungen).
## Marktplatz
Über den Eintrag **Marktplatz** in der Seitenleiste sehen Sie alle Module, die Tessera grundsätzlich anbietet — unabhängig davon, ob Sie sie bereits nutzen können. Jede Modulkachel zeigt Name, Version und einen Status:
- **Aktiviert** — das Modul wurde vom Administrator für Ihr Unternehmen freigeschaltet.
- **Verfügbar** — das Modul existiert, ist aber noch nicht aktiviert.
Über die Suche und die Filter (Status, Kategorie) lässt sich die Liste eingrenzen. Ein Klick auf eine Kachel öffnet die Detailseite mit einer ausführlicheren Beschreibung.
**Aktivieren und Freigeben sind zwei unterschiedliche Dinge:** Nur Administratoren können ein Modul für das gesamte Unternehmen **aktivieren**. Ob Sie persönlich das Modul danach auch sehen und öffnen können, hängt zusätzlich davon ab, ob Ihnen oder Ihrer Gruppe der Zugriff **freigegeben** wurde. Ein Modul kann also für das Unternehmen aktiv sein, ohne dass Sie selbst Zugriff darauf haben. Versuchen Sie in diesem Fall, das Modul zu öffnen, erscheint statt des Moduls die Meldung „Kein Zugriff auf dieses Modul" mit dem Hinweis „Sie haben für dieses Modul keine Freigabe. Wenden Sie sich an Ihren Administrator." In diesem Fall wenden Sie sich an Ihren Administrator, damit er Ihnen (direkt oder über eine Gruppe) die Freigabe für das Modul erteilt.
## Die Module
Aktuell stehen in Tessera vier Module zur Verfügung. Je nachdem, welche für Sie freigegeben sind, sehen Sie sie in der Seitenleiste unter ihrer jeweiligen Kategorie.
### Ausschreibungs-Radar
Das Ausschreibungs-Radar durchsucht öffentliche Ausschreibungen und zeigt sie Ihnen als Trefferliste.
**Trefferliste:** Die Hauptseite zeigt eine Tabelle mit Titel, Auftraggeber, Frist, Wert, Veröffentlichungsdatum und einer Spalte „Triage". Über den Button **„Jetzt abrufen"** (Zahnrad-Symbol daneben) können fällige Quellen sofort abgerufen werden, statt auf den nächsten automatischen Durchlauf zu warten. Ein Hinweisbanner erklärt die aktuelle Datenabdeckung: Erfasst werden derzeit nur EU-weite Oberschwellen-Ausschreibungen aus der zentralen DÖE-Quelle; kleinere Unterschwellen-Vergaben einzelner Vergabestellen fehlen noch, weshalb eine kurze Trefferliste kein Fehler ist. Einige Portale lassen laut ihren Nutzungsbedingungen keinen automatisierten Zugriff zu und müssen daher manuell beobachtet werden — auch das zeigt das Banner an.
**Filter:** Über das Filterfeld können Sie nach Titel oder Auftraggeber suchen, nach Frist, Postleitzahl, Bundesland, Branche (CPV-Code) und Auftragswert eingrenzen sowie nach „nur noch offene" und „nur Favoriten/Merkliste" filtern.
**Suchprofile:** Sie können die aktuell eingestellten Filter unter einem eigenen Namen als Suchprofil speichern, später wieder laden, umbenennen oder löschen. Für jedes Suchprofil lässt sich zusätzlich ein **Sofort-Alert** per E-Mail einschalten, der Sie bei neuen Treffern dieses Profils sofort benachrichtigt.
**Merken/Gelesen:** In der Trefferliste kann jede Ausschreibung als gelesen/ungelesen markiert und als Favorit gesetzt (markiert mit ★) oder wieder entfernt werden.
**Meine Quellen:** Über das Zahnrad-Symbol neben „Jetzt abrufen" gelangen Sie zu „Meine Quellen" — Ihrem persönlichen Einstellungsbereich für dieses Modul, mit drei Abschnitten:
- **Mein Postfach** — verbinden Sie Ihr eigenes E-Mail-Postfach (IMAP oder Exchange), aus dem eingehende Ausschreibungs-Benachrichtigungen automatisch erfasst werden.
- **Meine Feeds** — eigene RSS-Feeds, die Sie zusätzlich beobachten möchten.
- **Benachrichtigung** — das Intervall für die Sammel-Mail (Digest), die neue Treffer aus Ihren Suchprofilen zusammenfasst: täglich, wöchentlich oder aus.
Ausschreibungen, die über Ihr Postfach oder Ihre Feeds hereinkommen, erscheinen anschließend in der Trefferliste **aller** Kolleginnen und Kollegen Ihres Unternehmens — es gibt keine getrennte Sichtbarkeit je Postfach.
Administratoren sehen zusätzlich einen Link zu den plattformweiten Einstellungen (Abrufintervall, plattformweite RSS-Feeds), die für alle gleich gelten.
### DKV-Rechnung
Das Modul prüft automatisch ein Postfach auf eingehende DKV-Tankkarten-Rechnungen und verarbeitet sie.
- **Posteingang:** Über **„Jetzt prüfen"** lässt sich der Posteingang sofort abfragen, statt auf den nächsten automatischen Lauf zu warten. Die **Verarbeitungshistorie** zeigt Datum/Zeit, Rechnungsnummer, betroffene Fahrzeuge, Anzahl Transaktionen und Status jeder verarbeiteten Rechnung. Darunter listet **„Letzte Exportdateien"** die erzeugten Exporte zum Download.
- **Fahrzeuge:** Im Tab „Fahrzeuge" pflegen Sie die Fahrzeug-Stammdaten (Kennzeichen, Marke, Modell, Fahrer) — einzeln oder per CSV-Import (Format: `Kennzeichen;Marke;Modell;Fahrer`). Beim Import wählen Sie zwischen „Zusammenführen" (neue Fahrzeuge ergänzen, vorhandene bleiben) und „Ersetzen" (alle vorhandenen Fahrzeuge werden ersetzt — vor dieser Aktion erscheint eine Sicherheitsabfrage, da sie nicht rückgängig gemacht werden kann).
- **Moduleinstellungen:** Hier wird die Verbindung zum Postfach konfiguriert (Protokoll, Host, Zugangsdaten, Abrufintervall) sowie der Export-Empfänger, an den die verarbeiteten Rechnungen weitergeleitet werden.
### Zertifikat-Manager
Ein Werkzeug rund um SSL/TLS-Zertifikate mit vier Reitern:
- **Analysieren** — lädt ein Zertifikat (Datei oder eingefügter PEM-Text) und zeigt dessen Details, inklusive Einordnung als Root-CA, Zwischen-CA oder Endzertifikat.
- **Aufteilen** — zerlegt eine Fullchain- oder P7B-Datei in ihre einzelnen Zertifikate.
- **Zusammenführen** — fügt mehrere einzelne Zertifikate zu einer Kette zusammen.
- **Konvertieren** — wandelt ein Zertifikat in ein anderes Format um.
Unterstützte Dateiformate sind unter anderem `.pem`, `.crt`, `.cer`, `.der`, `.pfx`, `.p12`, `.p7b` und `.p7c`. Passwortgeschützte PFX/P12-Dateien verlangen die Eingabe des zugehörigen Passworts. Ergebnisse lassen sich einzeln oder gesammelt als ZIP herunterladen.
### Domaincheck
Ein einfaches Werkzeug, um zu prüfen, ob eine Internet-Domain verfügbar ist. Geben Sie einen Domain-Namen ein (z. B. `beispiel.de`) und klicken Sie auf „Prüfen" — das Ergebnis zeigt für die geprüften Endungen jeweils „Verfügbar" oder „Registriert" an, ergänzt um alternative Vorschläge.
## Persönliche Einstellungen
Öffnen Sie **Einstellungen** über das Benutzermenü oben rechts. Der Bereich gliedert sich in zwei Kategorien in der linken Unterleiste:
**Allgemein > Konto:**
- **Profilbild:** Laden Sie ein Bild hoch (PNG, JPEG oder WebP, maximal 2 MB) oder löschen Sie das vorhandene wieder.
- **Akzentfarbe:** Passt die Hauptfarbe der Oberfläche an Ihren Geschmack an; über „Zurücksetzen" kehren Sie zur Standardfarbe zurück.
- **Passwort ändern:** Nur sichtbar und nutzbar, wenn Ihr Konto **lokal** in Tessera verwaltet wird. Wird Ihr Konto stattdessen über das Verzeichnis (LDAP/Active Directory) verwaltet, zeigt Tessera stattdessen den Hinweis „Ihr Passwort wird über das Verzeichnis (LDAP) verwaltet. Eine Änderung ist hier nicht möglich." — in diesem Fall ändern Sie Ihr Passwort über die üblichen Firmenwege (z. B. Windows-Anmeldung), nicht in Tessera.
**Dashboard > Widgets:** Hier finden Sie für jedes auf Ihrem Dashboard platzierte Widget die zugehörigen Einstellungen, zum Beispiel eigene Suchanbieter für die Suchleiste.
**Dashboard > Kalender:** Hier verwalten Sie die Kalenderquellen, aus denen das Kalender-Widget seine Termine bezieht — Quellen hinzufügen, die Verbindung testen und nicht mehr benötigte Quellen wieder entfernen.
**Erscheinungsbild (Hell/Dunkel/System):** Der Schalter oben rechts in der Kopfleiste wechselt bei jedem Klick reihum zwischen Hell, Dunkel und System (folgt der Einstellung Ihres Betriebssystems).
**Sprache:** Unten in der Seitenleiste finden Sie die Sprachumschaltung zwischen Deutsch und Englisch.
## Häufige Stolpersteine
- **Die Anmeldung schlägt fehl, obwohl Passwort und E-Mail stimmen.** Prüfen Sie, ob Sie im Feld „Benutzername" tatsächlich Ihren Benutzernamen eingegeben haben — nicht Ihre E-Mail-Adresse. Das ist mit Abstand der häufigste Grund für eine scheinbar kaputte Anmeldung.
- **Ein Modul wird im Marktplatz als „Aktiviert" angezeigt, lässt sich aber nicht öffnen.** Das Modul ist für Ihr Unternehmen freigeschaltet, aber Ihnen persönlich (oder Ihrer Gruppe) wurde noch keine Freigabe erteilt. Wenden Sie sich an Ihren Administrator.
- **Im Bereich „Passwort ändern" fehlt das Formular komplett.** Ihr Konto wird über das Verzeichnis (LDAP) verwaltet — das ist kein Fehler, sondern Absicht. Ändern Sie Ihr Passwort in diesem Fall über den üblichen Firmenweg.
- **Nach der Anmeldung werden Sie sofort zur Passwort-Änderung gezwungen.** Das ist eine Sicherheitsmaßnahme, die der Administrator beim Anlegen Ihres Kontos aktiviert hat — vergeben Sie einfach ein neues Passwort, um fortzufahren.
- **Die Seitenleiste zeigt „Keine Module".** Für Sie sind noch keine Module freigegeben. Das ist normal für neu angelegte Konten — wenden Sie sich an Ihren Administrator.
- **Im Ausschreibungs-Radar erscheinen nur sehr wenige Treffer.** Aktuell werden nur EU-weite Oberschwellen-Ausschreibungen erfasst; kleinere Unterschwellen-Vergaben fehlen noch. Das Hinweisbanner auf der Modulseite erklärt das.
- **Zahlen, die Sie über „Meine Quellen" im Ausschreibungs-Radar eingebracht haben, tauchen in der Trefferliste aller Kollegen auf.** Das ist beabsichtigt — die Trefferliste ist für das ganze Unternehmen gemeinsam, nicht postfachbezogen getrennt.
+330
View File
@@ -0,0 +1,330 @@
<!-- generated-by: gsd-doc-writer -->
# Tessera – Betriebshandbuch
Anleitung für den laufenden Betrieb von Tessera: Erstinstallation, neue Fassungen
einspielen, Datenbank-Migrationen, Sicherung/Wiederherstellung und Fehlersuche.
Richtet sich an Kolleg:innen, die die Docker-Container auf dem Server betreiben.
Für den Aufbau der CI/CD-Pipeline (Gitea Actions, act_runner) siehe
[`docs/ci-cd-setup.md`](./ci-cd-setup.md) – dieses Dokument behandelt nur den
Betrieb der bereits laufenden Installation, nicht deren automatisierten Build.
## Inhaltsverzeichnis
1. [Überblick der Dienste](#1-überblick-der-dienste)
2. [Erstinstallation](#2-erstinstallation)
3. [Konfiguration](#3-konfiguration)
4. [Neue Fassung einspielen](#4-neue-fassung-einspielen)
5. [Datenbank-Migrationen](#5-datenbank-migrationen)
6. [Sicherung und Wiederherstellung](#6-sicherung-und-wiederherstellung)
7. [Protokolle und Fehlersuche](#7-protokolle-und-fehlersuche)
8. [Abgrenzung zur CI/CD-Pipeline](#8-abgrenzung-zur-cicd-pipeline)
---
## 1. Überblick der Dienste
Tessera besteht aus drei Containern, definiert in `docker-compose.yml` (Basis) und
`docker-compose.prod.yml` (Produktionsvariante mit fertigen Images statt lokalem Build):
| Dienst | Image / Build | Host-Port | Zweck |
|--------|---------------|-----------|-------|
| `web` | `apps/web/Dockerfile` (Basis) bzw. `git.vicolab.de/schalli/tessera-ctl/web:latest` (Prod) | 3000 | Next.js-Frontend (Portal, Dashboard) |
| `api` | `apps/api/Dockerfile` (Basis) bzw. `git.vicolab.de/schalli/tessera-ctl/api:latest` (Prod) | 3001 | NestJS-Backend (REST-API, Prisma/PostgreSQL-Zugriff) |
| `db` | `postgres:16-alpine` | kein Host-Port | PostgreSQL-Datenbank |
Zusätzlich existiert `docker-compose.dev.yml` (Bind-Mounts für Live-Reload,
Mailhog, OpenLDAP, phpLDAPadmin) für die lokale Entwicklung sowie
`docker-compose.ci.yml` für den Gitea `act_runner` – beide sind für den
Produktivbetrieb ohne Belang und werden hier nicht weiter behandelt.
**Netzwerke** (siehe `docker-compose.yml`):
- `frontend-net` – nur `web`, darüber ist der Browser-Zugriff erreichbar (Port 3000).
- `backend-net` – `web` und `api`, Kommunikation zwischen Frontend-Server und Backend.
- `data-net` – `api` und `db`, als `internal: true` markiert. Die Datenbank ist damit
aus dem Host-Netzwerk grundsätzlich nicht erreichbar; Zugriff nur über
`docker compose exec db ...` von innen.
**Startreihenfolge:** `db` → `api` → `web`, erzwungen über `depends_on` mit
`condition: service_healthy`:
- `db`: Healthcheck `pg_isready -U tessera` (Intervall 5 s, 5 Versuche).
- `api`: Healthcheck `wget --spider http://localhost:3001/health` (Intervall 10 s,
3 Versuche, `start_period` 10 s in der Basis- / 20 s in der Prod-Variante). Der
`/health`-Endpunkt (`apps/api/src/health/health.controller.ts`) prüft nur, dass
der NestJS-Prozess antwortet – **keine** Datenbankverbindung. Ein "healthy"
API-Container sagt also nichts darüber aus, ob Prisma tatsächlich verbunden ist.
- `web`: hat selbst **keinen** Healthcheck. `docker compose ps` zeigt `web` daher nie
als "healthy" an, nur als laufend – das ist normal.
Der Browser spricht ausschließlich mit `web` (Port 3000). Aufrufe unter
`/api-proxy/*` werden von Next.js serverseitig auf `API_INTERNAL_URL`
(`http://api:3001` im Docker-Netz) umgeschrieben (siehe `apps/web/next.config.ts`).
Der Browser erreicht die API also nie direkt und muss auch keine eigene CORS-Freigabe
für die API-Origin haben – Port 3001 muss von außen in der Regel nicht erreichbar
sein, ist es in den mitgelieferten Compose-Dateien aber (Host-Port 3001 ist
gemappt).
## 2. Erstinstallation
Voraussetzung: Docker und Docker Compose (Plugin, `docker compose ...`) sind auf dem
Zielsystem installiert.
1. Repository auf den Server holen:
```bash
git clone <gitea-url>/schalli/tessera-ctl.git
cd tessera-ctl
```
2. `.env` aus der Vorlage anlegen:
```bash
cp .env.prod.example .env
```
Die Vorlage `.env.prod.example` enthält die für den Produktivbetrieb relevanten
Variablennamen; `.env.example` ist die schlankere Entwicklungsvariante. Welche
Variablen zu setzen sind, steht in Kapitel [3. Konfiguration](#3-konfiguration).
Mindestens erforderlich, bevor irgendetwas startet:
- `DB_PASSWORD` und dazu passend `DATABASE_URL`
- `JWT_SECRET`
- `TESSERA_ADMIN_EMAIL`, `TESSERA_ADMIN_PASSWORD`
- `TESSERA_ENCRYPTION_KEY` (siehe Warnhinweis unten – ohne diesen Wert startet
nichts)
3. Encryption-Key erzeugen und eintragen:
```bash
openssl rand -hex 32
```
Das Ergebnis (64 Hex-Zeichen) als `TESSERA_ENCRYPTION_KEY=` in die `.env`
eintragen. Dieser Schlüssel verschlüsselt alle bei Tessera hinterlegten
Zugangsdaten (LDAP-Bind-Passwort, SMTP, Kalender- sowie DKV-/Ausschreibungs-Postfächer).
**Ohne diesen Wert startet die API nicht** – `docker-compose.prod.yml` verwendet
die Compose-Syntax `${TESSERA_ENCRYPTION_KEY:?...}`, wodurch bereits `docker
compose` selbst mit einer Fehlermeldung abbricht, bevor ein Container startet,
falls die Variable fehlt. Zusätzlich prüft `CryptoService`
(`apps/api/src/crypto/crypto.service.ts`) beim Hochfahren der API die Länge
(muss genau 64 Hex-Zeichen sein) und wirft andernfalls einen Fehler.
Diesen Schlüssel getrennt von jedem Datenbank-Dump aufbewahren (siehe Kapitel 6)
– geht er verloren, sind alle gespeicherten Zugangsdaten unwiderruflich
unbrauchbar und müssen von Hand neu eingegeben werden.
4. Container starten:
```bash
docker compose -f docker-compose.prod.yml pull
docker compose -f docker-compose.prod.yml up -d
```
Beim ersten Start seedet die API (`AdminSeedService`,
`apps/api/src/user/admin-seed.service.ts`) automatisch den initialen
Super-Admin-Account aus `TESSERA_ADMIN_USER` / `_EMAIL` / `_PASSWORD` sowie
den Mandanten `default`. Das passiert nur, wenn noch **kein** Benutzer mit
diesem Nutzernamen existiert – bei einer bereits befüllten Datenbank wird der
Seed-Schritt stillschweigend übersprungen, auch wenn sich die Passwort-Variable
in der `.env` danach ändert.
5. Prüfen, ob alles läuft:
```bash
docker compose -f docker-compose.prod.yml ps
curl -s http://localhost:3001/health
```
`api` sollte als `healthy` markiert sein, die `/health`-Abfrage liefert
`{"status":"ok",...}`. Danach ist die Oberfläche unter Port 3000 erreichbar.
## 3. Konfiguration
Alle Variablen werden über `.env` im Projektverzeichnis eingelesen (Docker Compose
liest diese Datei automatisch). Werte unten sind **Platzhalter**, keine echten
Zugangsdaten.
| Variable | Pflicht in Prod? | Default (Prod-Compose) | Zweck |
|----------|:---:|---|---|
| `DB_PASSWORD` | ja | – | Passwort des PostgreSQL-Benutzers `tessera`, an den `db`-Container durchgereicht. |
| `DATABASE_URL` | ja | – | Vollständiger Prisma-Connection-String, z. B. `postgresql://tessera:<passwort>@db:5432/tessera`. Muss zum `DB_PASSWORD` passen. |
| `JWT_SECRET` | ja | – | Signiert/prüft die JWT-Auth-Cookies. Wird **sowohl** an `api` **als auch** an `web` durchgereicht – beide müssen denselben Wert erhalten, sonst schlägt die Anmeldung fehl. In der Basis-Compose (Dev) gibt es einen unsicheren Default (`tessera-dev-jwt-secret-change-in-production`), in Prod nicht. |
| `TESSERA_ENCRYPTION_KEY` | ja | – | AES-256-GCM-Schlüssel (64 Hex-Zeichen) für gespeicherte Drittanbieter-Zugangsdaten. Siehe Kapitel 2. `CALENDAR_ENCRYPTION_KEY` ist der alte Variablenname und wird weiterhin akzeptiert (mit Warnung im Log). |
| `TESSERA_ADMIN_USER` | nein | `admin` | Benutzername des initialen Super-Admin, nur beim allerersten Start relevant. |
| `TESSERA_ADMIN_EMAIL` | ja (für den Seed) | – | E-Mail des initialen Super-Admin. |
| `TESSERA_ADMIN_PASSWORD` | ja (für den Seed) | – | Initiales Passwort des Super-Admin. |
| `TESSERA_FORCE_CHANGE` | nein | `true` | Erzwingt Passwortwechsel beim ersten Login des geseedeten Admin-Accounts. |
| `TESSERA_SMTP_HOST` / `_PORT` / `_SECURE` / `_USER` / `_PASSWORD` / `_FROM` | nein (aber ohne Host kein Mailversand) | Host leer, Port `587`, `_SECURE=false` | SMTP-Relay für ausgehende Mails (Passwort-Reset, Benachrichtigungen). |
| `APP_URL` | empfohlen | `http://localhost:3001` (für `NEXT_PUBLIC_API_URL`) / `http://localhost:3000` (für `TESSERA_APP_URL`) | Öffentliche Basis-URL der Web-Oberfläche. Wird serverseitig u. a. für in E-Mails generierte Links verwendet (`TESSERA_APP_URL`). |
| `API_INTERNAL_URL` | fest verdrahtet | `http://api:3001` | Adresse, unter der `web` die API **innerhalb** des Docker-Netzes erreicht; dorthin schreibt Next.js die `/api-proxy/*`-Rewrites um. In der Regel nicht ändern. |
Hinweis zu `NEXT_PUBLIC_API_URL`: Diese Variable wird beim Image-Build bereits fest
auf `/api-proxy` gesetzt (`ENV NEXT_PUBLIC_API_URL=/api-proxy` in
`apps/web/Dockerfile`, vor `pnpm build`). Next.js baut `NEXT_PUBLIC_*`-Variablen zur
Build-Zeit in das an den Browser ausgelieferte JavaScript ein; der zur Laufzeit über
Compose gesetzte Wert (`${APP_URL:-http://localhost:3001}`) hat auf das bereits
gebaute Frontend-Bundle daher keine sichtbare Wirkung mehr. Praktisch reicht es, dass
`APP_URL` für `TESSERA_APP_URL` (serverseitig, z. B. Mail-Links) korrekt gesetzt ist.
<!-- VERIFY: Ob und wie CORS_ORIGIN, NODE_ENV oder weitere in main.ts gelesene Variablen auf dem tatsächlichen Server zusätzlich gesetzt sind, lässt sich aus dem Repository allein nicht feststellen. -->
**Wichtig – Konfigurationsdrift auf dem Server:** Auf dem Produktivserver wurde die
laufende Compose-Datei in der Vergangenheit direkt von Hand angepasst und ist damit
nicht mehr zwingend identisch mit `docker-compose.prod.yml` in diesem Repository.
Änderungen an `docker-compose*.yml` im Git-Repo wirken sich **nicht** automatisch
auf die laufende Installation aus – der Deploy-Schritt zieht ausschließlich neue
Images, die Compose-Datei selbst muss bei Bedarf separat auf dem Server aktualisiert
werden. Vor grösseren Änderungen an der Compose-Struktur die auf dem Server
tatsächlich liegende Datei prüfen, nicht blind von der Repo-Version ausgehen.
## 4. Neue Fassung einspielen
Das ist der wichtigste Ablauf im Tagesgeschäft. Zwei Befehle:
```bash
docker compose -f docker-compose.prod.yml pull
docker compose -f docker-compose.prod.yml up -d --force-recreate api web
```
**Verifizierte Falle:** `docker compose up -d` **ohne** `--force-recreate` ersetzt
einen bereits laufenden Container **nicht**, wenn Compose der Meinung ist, an der
Service-Definition habe sich nichts geändert – auch wenn `pull` gerade ein neues
Image unter demselben Tag (`:latest`) heruntergeladen hat. Der alte Container läuft
dann unverändert mit dem alten Code weiter, ohne Fehlermeldung. Das Deployment
wirkt erfolgreich, ist es aber nicht. Dieser Fehler ist dem Team schon mehrfach
passiert. Deshalb: nach jedem `pull` **immer** `--force-recreate` verwenden (nur
`api` und `web` betrifft das – `db` soll normalerweise nicht neu erstellt werden,
sonst würde sie kurz neu starten).
**Kontrolle, ob es wirklich funktioniert hat:** Startzeitpunkt des Containers mit dem
Erstellungszeitpunkt des zugehörigen Images vergleichen. Der Container muss
**nach** dem Image erzeugt worden sein:
```bash
docker inspect -f '{{.State.StartedAt}}' tessera-api-1
docker image inspect -f '{{.Created}}' git.vicolab.de/schalli/tessera-ctl/api:latest
docker inspect -f '{{.State.StartedAt}}' tessera-web-1
docker image inspect -f '{{.Created}}' git.vicolab.de/schalli/tessera-ctl/web:latest
```
(Container-Namen mit `docker compose -f docker-compose.prod.yml ps` prüfen, falls
sie auf dem Server abweichen.) Liegt `StartedAt` **vor** `Created` des Images, läuft
noch die alte Version – dann `--force-recreate` nachholen.
## 5. Datenbank-Migrationen
Ein separater Migrationsschritt ist **nicht** nötig. Der `api`-Container führt
Prisma-Migrationen bei jedem Start automatisch aus, bevor der eigentliche Prozess
hochfährt (`apps/api/Dockerfile`, `CMD`):
```
prisma migrate deploy --schema apps/api/prisma/schema.prisma && node apps/api/dist/main.js
```
Das heisst: Sobald ein neues Image (mit neuen Migrationen im Ordner
`apps/api/prisma/migrations/`) per `pull` + `--force-recreate` eingespielt wird,
laufen ausstehende Migrationen beim nächsten Start automatisch. Schlägt eine
Migration fehl, startet `node apps/api/dist/main.js` erst gar nicht – der Container
bleibt dann im Neustart-Loop bzw. terminiert, sichtbar in `docker compose logs api`.
Kontrolle, ob eine Migration tatsächlich angekommen ist:
```bash
docker compose -f docker-compose.prod.yml exec db \
psql -U tessera -d tessera -c \
"SELECT migration_name, finished_at FROM _prisma_migrations ORDER BY finished_at DESC LIMIT 5;"
```
Der Name der zuletzt erwarteten Migration lässt sich mit dem Ordnernamen in
`apps/api/prisma/migrations/` abgleichen (Namensschema
`YYYYMMDDHHMMSS_beschreibung`).
## 6. Sicherung und Wiederherstellung
Es gibt aktuell **kein** eingebautes Backup-Skript und kein Makefile-Target dafür im
Repository. Sicherung erfolgt manuell über die Postgres-Bordmittel.
### Datenbank sichern
```bash
docker compose -f docker-compose.prod.yml exec db \
pg_dump -U tessera -d tessera -F c -f /tmp/tessera-$(date +%Y%m%d).dump
docker compose -f docker-compose.prod.yml cp db:/tmp/tessera-$(date +%Y%m%d).dump ./
```
### Datenbank wiederherstellen
```bash
docker compose -f docker-compose.prod.yml cp ./tessera-YYYYMMDD.dump db:/tmp/restore.dump
docker compose -f docker-compose.prod.yml exec db \
pg_restore -U tessera -d tessera --clean --if-exists /tmp/restore.dump
```
Vor einer Wiederherstellung `api` stoppen, damit keine Schreibzugriffe während des
Restores stattfinden.
### Was sonst noch an Zustand existiert
- **PostgreSQL-Daten**: liegen im benannten Docker-Volume `pgdata`
(`docker-compose.prod.yml`, Mount `pgdata:/var/lib/postgresql/data` im
`db`-Container). Dieses Volume ist der einzige daürhafte Datenspeicher, der über
ein `docker volume`-Backup zusätzlich gesichert werden könnte.
- **Verschlüsselungsschlüssel** `TESSERA_ENCRYPTION_KEY`: liegt nur in `.env` auf
dem Host, **nicht** im Datenbank-Dump. Getrennt sichern (siehe Kapitel 2/3) – ohne
ihn sind alle per `pg_dump` gesicherten verschlüsselten Zugangsdaten wertlos.
- **Hochgeladene Dateien** (Avatare unter `user-files/avatars/`, generierte
DKV-Exporte unter `user-files/`, siehe `apps/api/src/user/user.controller.ts` und
`apps/api/src/dkv/dkv-export.service.ts`): Diese Dateien landen im Verzeichnis
`user-files/` **innerhalb** des `api`-Containers. Weder `docker-compose.yml` noch
`docker-compose.prod.yml` mounten dafür ein Docker-Volume oder ein Host-Verzeichnis
– der Ordner existiert ausschließlich in der beschreibbaren Container-Schicht.
**Das bedeutet: Bei jedem `--force-recreate` von `api` gehen Avatare und DKV-Exporte
verloren**, sofern auf dem Server nicht zusätzlich (außerhalb des Repo-Standes)
ein Bind-Mount ergänzt wurde. Vor einem Redeploy prüfen, ob auf dem Server ein
solcher Mount existiert (`docker inspect tessera-api-1` → `Mounts`); falls
nicht, gelten Avatare/Exporte als nicht persistent und sollten bei Bedarf vorher
manuell aus dem Container kopiert werden (`docker compose cp api:/app/user-files
./user-files-backup`).
## 7. Protokolle und Fehlersuche
Logs ansehen:
```bash
docker compose -f docker-compose.prod.yml logs -f api
docker compose -f docker-compose.prod.yml logs -f web
docker compose -f docker-compose.prod.yml logs -f db
```
**Gesunder Start sieht so aus:** `db` wird `healthy`, danach startet `api` und
protokolliert die durchgelaufenen Prisma-Migrationen sowie zuletzt
`Tessera API running on port 3001` (aus `apps/api/src/main.ts`). Erst danach startet
`web`, weil `depends_on: api: condition: service_healthy` das erzwingt.
| Symptom | Wahrscheinliche Ursache | Prüfen / Beheben |
|---|---|---|
| `docker compose up` bricht sofort ab, ohne dass ein Container startet, mit einer Meldung zu `TESSERA_ENCRYPTION_KEY` | Variable fehlt in `.env` – Compose selbst verweigert die Variablen-Interpolation (`${TESSERA_ENCRYPTION_KEY:?...}`) | `TESSERA_ENCRYPTION_KEY` setzen (siehe Kapitel 2), danach erneut starten. |
| `api`-Container startet und beendet sich sofort wieder, Log zeigt `TESSERA_ENCRYPTION_KEY is not set` oder `must be a 64-character hex string` | Key ist zwar irgendwo gesetzt, aber leer oder falsch lang | Mit `openssl rand -hex 32` neu erzeugen, Länge prüfen (64 Zeichen). |
| `api` wird nie `healthy`, Log zeigt Verbindungsfehler von Prisma/PostgreSQL | `db` ist noch nicht bereit, `DATABASE_URL` falsch, oder `DB_PASSWORD`/`DATABASE_URL` passen nicht zusammen | `docker compose ps` – ist `db` `healthy`? `DATABASE_URL` gegen `DB_PASSWORD` abgleichen. |
| Login funktioniert nicht, obwohl `api` und `web` laufen | `JWT_SECRET` bei `web` und `api` unterschiedlich, z. B. nach halbherzigem Neustart nur eines Containers | Beide Container mit demselben `JWT_SECRET` neu erstellen (`--force-recreate api web`). |
| Neue Version scheint nicht anzukommen, obwohl `pull` gelaufen ist | Klassische `up -d`-Falle ohne `--force-recreate` (siehe Kapitel 4) | `StartedAt` des Containers gegen `Created` des Images vergleichen, ggf. `--force-recreate` nachholen. |
| Initialer Admin-Login funktioniert nicht nach Änderung von `TESSERA_ADMIN_PASSWORD` | Seed läuft nur, wenn der Benutzername noch **nicht** existiert; bestehende Accounts werden nicht überschrieben | Passwort über die Anwendung selbst (bzw. direkt in der Datenbank) ändern, nicht über die `.env`-Variable. |
| Mails werden nicht versendet | `TESSERA_SMTP_HOST` leer (Prod-Default) | SMTP-Variablen vollständig setzen und Container neu erstellen. |
| Avatare/DKV-Exporte nach einem Deploy verschwunden | `user-files/` ist nicht als Volume gemountet, siehe Kapitel 6 | Vor `--force-recreate` sichern, langfristig einen Bind-Mount für `user-files/` ergänzen. |
## 8. Abgrenzung zur CI/CD-Pipeline
Dieses Dokument beschreibt den manuellen Betrieb: Erstinstallation, Redeploy,
Migrations-Kontrolle, Backup und Fehlersuche auf einer bereits eingerichteten
Installation.
Wie die Gitea-Actions-Pipeline (Runner-Registrierung, `.gitea/workflows/ci.yml`,
Berechtigungen des `act_runner`-Docker-Socket-Mounts) aufgesetzt wird, ist bewusst
nicht Teil dieses Dokuments – das steht vollständig in
[`docs/ci-cd-setup.md`](./ci-cd-setup.md). Die Grenze zwischen beiden Dokumenten:
Sobald ein neues Image lokal vorliegt oder in einer Registry verfügbar ist, beginnt
dieses Betriebshandbuch (Kapitel 4); alles davor – wie das Image entsteht – gehört
in das CI/CD-Runbook.
+453
View File
@@ -0,0 +1,453 @@
<!-- generated-by: gsd-doc-writer -->
# Tessera — Anleitung für Entwickler
Diese Anleitung richtet sich an Entwicklerinnen und Entwickler, die neu zu Tessera stoßen. Sie
kennen TypeScript, aber nichts von diesem Repository. Ziel ist, Sie von einem frischen Checkout
bis zu einem eigenen, lauffähigen Modul zu bringen — alle Angaben sind aus dem tatsächlichen
Code geprüft, nicht aus einer geplanten Architektur abgeleitet.
## Inhaltsverzeichnis
1. [Aufbau des Monorepos](#aufbau-des-monorepos)
2. [Lokale Entwicklungsumgebung](#lokale-entwicklungsumgebung)
3. [Architektur im Überblick](#architektur-im-überblick)
4. [Das Modulsystem](#das-modulsystem)
5. [Mandantentrennung](#mandantentrennung)
6. [Berechtigungen](#berechtigungen)
7. [Datenbank und Migrationen](#datenbank-und-migrationen)
8. [Tests](#tests)
9. [Konventionen und Fallstricke](#konventionen-und-fallstricke)
---
## Aufbau des Monorepos
Tessera ist ein pnpm-Workspace (`pnpm-workspace.yaml`), orchestriert über Turborepo
(`turbo.json`). Der Paketmanager ist mit `packageManager: "pnpm@9.15.0"` in der Root-`package.json`
fest verankert.
```
apps/
api/ @tessera/api — NestJS-Backend
web/ @tessera/web — Next.js-Frontend
desktop/ — — Tauri-Wrapper, früher Stand (nur Cargo-Projekt + eine setup.html)
packages/
shared/ @tessera/shared — geteilte Konstanten/Typen, derzeit sehr klein (APP_NAME, HealthResponse)
module-sdk/ — — TypeScript-Interfaces für den Modul-Vertrag (TesseraModule, ModuleManifest)
```
`apps/desktop` besteht bislang nur aus dem Tauri-Grundgerüst (`src-tauri/`) und einer einzelnen
`setup.html` — dort ist noch keine eigentliche Anwendung zu finden. `packages/shared` ist ebenfalls
minimal; es enthält aktuell nur eine Konstante und ein Health-Interface, keine DTOs.
**Root-Skripte** (`package.json`, laufen über Turborepo durch alle Workspaces):
| Skript | Bedeutung |
|---|---|
| `pnpm dev` | `turbo dev` — startet alle `dev`-Tasks (bei API/Web persistent, ungecached) |
| `pnpm build` | `turbo build` |
| `pnpm lint` | `turbo lint` |
| `pnpm test` | `turbo test` |
| `pnpm type-check` | `turbo type-check` |
Linting/Formatierung laufen über **Biome** (`biome.json`, Zeilenlänge 100, 2 Spaces, `organizeImports`
aktiv) — es gibt kein ESLint/Prettier im Projekt. Testrunner ist **Vitest** in beiden Apps
(`apps/api/vitest.config.ts`, `apps/web/vitest.config.ts`); für `apps/web` läuft die
`jsdom`-Umgebung mit `@testing-library/react`.
## Lokale Entwicklungsumgebung
### Voraussetzungen
- Docker und Docker Compose
- pnpm 9.x (`packageManager` in `package.json` pinnt `pnpm@9.15.0`)
Die produktiven `Dockerfile`s ziehen `node:24-alpine` — das ist die verbindliche Node-Version für
Container-Builds.
### Umgebungsvariablen
Kopieren Sie `.env.example` nach `.env`. Zwei Werte sind praxisrelevant:
- `DB_PASSWORD` — Postgres-Passwort, Default in Compose ist `tessera_dev`, falls nicht gesetzt.
- `TESSERA_ENCRYPTION_KEY` — verschlüsselt alle gespeicherten Zugangsdaten (LDAP-Bind, Kalender-
und Postfach-Logins). **Pflichtfeld**, der Stack startet ohne diesen Wert nicht. Erzeugen mit
`openssl rand -hex 32`. Geht der Wert verloren, sind alle gespeicherten Zugangsdaten
unwiederbringlich — der Schlüssel gehört zu jedem Datenbank-Backup dazu, aber getrennt davon
aufbewahrt.
Alle übrigen Variablen (JWT-Secret, SMTP für den lokalen `mailhog`, Admin-Zugangsdaten) haben in
`docker-compose.yml`/`docker-compose.dev.yml` brauchbare Entwicklungs-Defaults.
### Stack starten
```bash
docker compose -f docker-compose.yml -f docker-compose.dev.yml up --build
```
Das startet:
- **web** — Next.js mit Turbopack (`next dev --turbopack`), Port `3000`
- **api** — NestJS mit Watch-Modus (`nest start --watch`), Port `3001`
- **db** — Postgres 16 (Alpine), **ohne Host-Port**
- **mailhog** — SMTP-Testserver, UI auf `8025`, SMTP auf `1025`
- **openldap** / **phpldapadmin** — für LDAP-Sync-Entwicklung, Ports `389`/`636` bzw. `6443`
`docker compose up` ohne `--build`/`--force-recreate` baut bestehende Images **nicht** neu — nach
Änderungen an Dockerfiles oder Dependencies muss `--build` explizit mitgegeben werden.
### Datenbank vom Host erreichen (wichtig)
Der `db`-Service hat **keinen Host-Port** — `docker-compose.yml` exponiert für `db` bewusst nichts
nach außen, nur die internen Container-Netze. Ein `psql` oder `prisma`-Aufruf **vom Host** kann sich
also nicht über `localhost:5432` verbinden. Stattdessen über die Container-IP:
```bash
docker compose ps # Namen des db-Containers ermitteln
docker inspect <db-container-name> \
| grep -A1 '"Networks"' | grep IPAddress # Container-IP im internen Netz
```
Verbindung dann mit den Zugangsdaten aus Compose (`tessera` / `tessera_dev` im Dev-Setup):
```
postgresql://tessera:tessera_dev@<container-ip>:5432/tessera
```
Das ist relevant, sobald Sie `prisma migrate dev`, `prisma studio` oder ein manuelles `psql` **vom
Host aus** statt aus dem `api`-Container heraus ausführen wollen.
## Architektur im Überblick
**Frontend** (`apps/web/src/app`, Next.js App Router):
```
(auth)/ — /login, /reset-password — öffentliche Routen
(portal)/ — alles hinter Login: /admin, /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).
**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`.
**Weg einer Anfrage** (Beispiel: eine Modulseite lädt Daten):
1. Eine Server- oder Client-Komponente unter `apps/web/src/app/(portal)/...` ruft die API über
`fetch` gegen `NEXT_PUBLIC_API_URL` (Browser) bzw. `API_INTERNAL_URL` (Server-Komponenten,
zeigt intern auf `http://api:3001`) auf.
2. Die Anfrage trifft in `apps/api/src/main.ts` auf die globale `ValidationPipe` und läuft dann
durch die drei global registrierten `APP_GUARD`s aus `app.module.ts`, in genau dieser
Reihenfolge: `JwtAuthGuard` (Auth) → `TenantGuard` (setzt `req.tenantId`/`req.tenantPrisma`
aus dem JWT) → `RolesGuard` (prüft `@Roles()`).
3. Trägt der Controller zusätzlich `@UseModule('slug')`, prüft anschließend `ModuleGuard`
(`apps/api/src/module-registry/module.guard.ts`) Modulzugriff über `ModuleAccessService`.
4. Der Controller ruft den zugehörigen Service auf, der über `PrismaService`
(`apps/api/src/prisma/prisma.service.ts`) oder — für mandantensensible Tabellen — über den
tenant-gescopten Client aus `req.tenantPrisma` auf Postgres zugreift.
5. Die Antwort geht als JSON zurück; das Frontend rendert sie in der jeweiligen Server- oder
Client-Komponente.
## Das Modulsystem
Module sind das zentrale Organisationsprinzip von Tessera: fachliche Werkzeuge (Domaincheck,
Zertifikat-Manager, DKV-Rechnung, Ausschreibungs-Radar), die im Marktplatz erscheinen, pro Mandant
aktiviert und dann einzelnen Gruppen oder Benutzern freigegeben werden.
### Registrierung
Jedes Modul-`Module` (NestJS) seedet sich beim Start selbst in die Datenbanktabelle `Module` — über
`OnModuleInit` und eine `seed*Module()`-Funktion, siehe
`apps/api/src/domaincheck/domaincheck.seed.ts`:
```ts
await moduleRegistryService.seedModule({
slug: 'domaincheck',
name: 'Domaincheck',
version: '1.0.0',
category: 'domain-tools',
description: { de: '...', en: '...' },
isSystem: true,
});
```
`ModuleRegistryService.seedModule` (`apps/api/src/module-registry/module-registry.service.ts`)
macht daraus ein Upsert auf `slug` — bei jedem API-Start wird der Registry-Eintrag aktualisiert,
nicht dupliziert. Ein Modul erscheint im Marktplatz (`GET /modules`, `GET /modules/catalog`), sobald
dieser Seed einmal gelaufen ist — unabhängig von der Mandanten-Aktivierung.
### Zweistufiges Zugriffsmodell
Zugriff auf ein Modul besteht aus zwei unabhängigen Stufen:
1. **Mandanten-Aktivierung** (`TenantModuleActivation`) — ein Admin schaltet das Modul für den
gesamten Mandanten frei/aus (`POST /modules/:moduleId/activate|deactivate`, nur ADMIN/
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.
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
Backend, `GET /modules/active` (Sidebar) und `GET /modules/catalog` (Marktplatz) — damit keine
dieser Stellen unabhängig voneinander driften kann.
### Vom Backend-Endpunkt zur Seite im Portal
Ein Modul-Controller schützt seine Routen mit dem `@UseModule(slug)`-Dekorator:
```ts
@Controller('modules/domaincheck')
@UseModule('domaincheck')
export class DomaincheckController { ... }
```
`UseModule` (`apps/api/src/module-registry/module.guard.ts`) setzt Metadaten und hängt
`ModuleGuard` als `CanActivate` ein. **Ohne diesen Dekorator gibt `ModuleGuard` bewusst `true`
zurück** — die Durchsetzung hängt vollständig am Dekorator, jeder neue Modul-Controller muss ihn
tragen.
Im Frontend gibt es zwei Wege, wie eine Modulseite unter `/modules/...` erreichbar ist:
- **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`).
Beide Wege rendern denselben Baustein: die Server-Komponente `ModuleAccessGate`
(`apps/web/src/components/modules/module-access-gate.tsx`). Sie ruft `checkModuleAccess(moduleSlug)`
auf, was `GET /modules/active` mit dem Session-Cookie anfragt — dieselbe
`ModuleAccessService`-Auflösung, die auch Sidebar und `ModuleGuard` benutzen. Nur bei explizit
`true` werden die `children` gerendert; jeder andere Ausgang (verweigert, oder die Prüfung wirft
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
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:
```ts
// apps/web/src/app/(portal)/modules/dkv-fleet/layout.tsx
export default function DkvFleetLayout({ children }: { children: ReactNode }) {
return <ModuleAccessGate moduleSlug="dkv-fleet">{children}</ModuleAccessGate>;
}
```
**Regel für neue Module:** Ein neues fest verdrahtetes Modulverzeichnis unter `modules/<slug>/`
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.
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
Modul-Komponente nur, wenn ihr Slug in `MODULE_REGISTRY`
(`apps/web/src/lib/module-loader.ts`) als lazy-geladenes `dynamic()`-Import gelistet ist —
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:
**Backend** (`apps/api/src/domaincheck/`):
1. `domaincheck.service.ts` — die eigentliche fachliche Logik.
2. `dto/check-domain.dto.ts` — Validierung des Request-Body per `class-validator`.
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`).
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`.
**Frontend** (`apps/web/src/app/(portal)/modules/domaincheck/`):
1. `page.tsx` — die eigentliche Modulseite (Client-Komponente mit den Formular-/Ergebnis-Teilen).
2. `layout.tsx` — `ModuleAccessGate moduleSlug="domaincheck"` um `{children}`.
3. `actions.ts` — Server Actions, die die Backend-Route aufrufen.
4. `components/` — `DomainInput.tsx`, `ResultList.tsx`.
5. Eintrag in `MODULE_REGISTRY` (`apps/web/src/lib/module-loader.ts`) mit dem `dynamic()`-Import
auf `page.tsx`.
6. Übersetzungsschlüssel in `apps/web/src/messages/de.json` **und** `en.json` (siehe
[Konventionen und Fallstricke](#konventionen-und-fallstricke)).
Für ein Modul mit Unterrouten (Einstellungsseite, Verwaltungsansicht) orientieren Sie sich an
`dkv-fleet` oder `tender-radar` — beide haben zusätzliche `settings/page.tsx` bzw. weitere
Unterverzeichnisse, die vom selben `layout.tsx` mitgedeckt werden.
## Mandantentrennung
Der tatsächliche Mechanismus ist `TenantGuard` (`apps/api/src/tenant/tenant.guard.ts`), global als
`APP_GUARD` in `app.module.ts` registriert — er läuft nach `JwtAuthGuard`, weil `req.user` erst
dann gesetzt ist. `TenantGuard` liest `tenantId` aus dem JWT-Claim des Anfragenden, erlaubt
SUPER_ADMIN einen Wechsel per `x-tenant-id`-Header, und setzt anschließend `req.tenantId` sowie
`req.tenantPrisma` — einen über `forTenant()`
(`apps/api/src/prisma/prisma-tenant.extension.ts`) erzeugten Prisma-Client, der vor **jeder** Query
in einer Transaktion `SELECT set_config('app.current_tenant', $1, true)` ausführt.
> Im Code existiert daneben eine gleichnamige `TenantMiddleware`
> (`apps/api/src/tenant/tenant.middleware.ts`) mit identischer Logik. Sie ist in `app.module.ts`
> nirgends über `.apply(...).forRoutes(...)` eingebunden — der tatsächlich aktive Mechanismus ist
> ausschließlich `TenantGuard`. Vereinzelte Code-Kommentare verweisen noch auf „TenantMiddleware“;
> gemeint ist in jedem Fall der Guard.
`app.current_tenant` wird von **Postgres Row-Level-Security** ausgewertet. RLS-Policies sind aber
**nicht** auf allen Tabellen aktiv — aktuell nur auf `User`, `PasswordResetToken`, `LdapConfig`,
`LdapFieldMapping`, `Group`, `GroupMembership` und `ModuleGrant` (siehe die Migrationen
`20260618112133_rls_policies` und `20260804130918_groups_rls_policies`). Alle übrigen
mandantenbezogenen Tabellen — u. a. `DkvVehicleMaster`, `DkvInvoiceHistory`, `CalendarSource`,
`Tender`, `TenderSavedSearch`, `FavoriteLink` — tragen zwar eine `tenantId`-Spalte, aber **keine**
RLS-Policy.
**Was ein Entwickler nie vergessen darf:** Bei jeder Query gegen eine Tabelle ohne RLS-Policy muss
`tenantId` **manuell** in die `where`-Klausel — die Datenbank filtert hier nichts von selbst. Das
ist im Code auch der gelebte Stil: `DkvService.loadConfig()`
(`apps/api/src/dkv/dkv.service.ts`) etwa nutzt den plain `PrismaService` (nicht
`req.tenantPrisma`) und filtert explizit mit `where: { tenantId }`. Wer bei einer solchen Tabelle
das `tenantId`-Filter vergisst, liest oder schreibt mandantenübergreifend — ohne dass RLS das
auffängt. Bei den sieben RLS-geschützten Tabellen greift die DB-seitige Absicherung zusätzlich,
vorausgesetzt die Query läuft tatsächlich über den `tenantPrisma`-Client aus `req.tenantPrisma`
und nicht über den globalen `PrismaService`.
## Berechtigungen
Rollen kommen aus dem Prisma-`enum Role { SUPER_ADMIN, ADMIN, USER }` und stecken im JWT — nie aus
Body oder Query-Parametern, sondern ausschließlich `req.user.role`. Rollenschutz auf
Controller-Ebene läuft über zwei Dekoratoren
(`apps/api/src/auth/decorators/roles.decorator.ts`, `apps/api/src/auth/guards/roles.guard.ts`):
```ts
@Roles(Role.ADMIN, Role.SUPER_ADMIN)
```
`RolesGuard` ist ebenfalls global als `APP_GUARD` registriert; ohne `@Roles()`-Metadaten lässt er
jede Anfrage durch — die Einschränkung entsteht ausschließlich durch das explizite Setzen des
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
`apps/api/src/groups/module-grants.controller.ts`, ausschließlich für ADMIN/SUPER_ADMIN:
| Route | Zweck |
|---|---|
| `GET /module-grants/matrix` | Module × Gruppen-Matrix bestehender Grants |
| `GET /module-grants/users/:userId` | Gruppenmitgliedschaften + geerbte/direkte Modulzugriffe eines Benutzers |
| `POST /module-grants` | Grant anlegen |
| `DELETE /module-grants` | Grant entziehen (Ziel im Body, nicht im Pfad) |
## Datenbank und Migrationen
Prisma ist die einzige Zugriffsschicht (`apps/api/prisma/schema.prisma`, Provider `postgresql`).
Der Workflow für eine Schemaänderung:
1. `schema.prisma` anpassen.
2. Migration erzeugen (im `api`-Container oder mit Zugriff auf die DB — siehe
[Datenbank vom Host erreichen](#datenbank-vom-host-erreichen-wichtig)):
```bash
pnpm --filter @tessera/api exec prisma migrate dev --name <beschreibender-name>
```
3. `prisma generate` läuft automatisch als `postinstall`-Skript von `@tessera/api`
(`"postinstall": "test -f prisma/schema.prisma && prisma generate || true"`), muss also nach
`pnpm install` nicht separat aufgerufen werden.
**Migrationen laufen automatisch beim API-Start.** Das produktive `Dockerfile`
(`apps/api/Dockerfile`) setzt als `CMD`:
```
prisma migrate deploy --schema apps/api/prisma/schema.prisma && node apps/api/dist/main.js
```
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.
RLS-Policies werden nicht von Prisma selbst verwaltet, sondern als reines SQL innerhalb regulärer
Migrationsdateien mitgeliefert (`CREATE POLICY ...` in `migration.sql`) — siehe
[Mandantentrennung](#mandantentrennung) für den aktuellen Stand, welche Tabellen das betrifft.
## Tests
Beide Apps nutzen **Vitest**, aber mit unterschiedlicher Umgebung:
- `apps/api` — `environment: 'node'`, sucht `src/**/*.spec.ts`, `passWithNoTests: true`.
```bash
pnpm --filter @tessera/api test # einmalig
pnpm --filter @tessera/api test:watch # Watch-Modus
```
- `apps/web` — `environment: 'jsdom'` mit `@testing-library/react`,
`setupFiles: ['./src/test/setup.ts']`.
```bash
pnpm --filter @tessera/web test
```
`pnpm test` im Root führt über Turborepo beide Suiten aus. Es gibt kein separates
End-to-End-Test-Setup (kein Playwright-Config im Repository) — Tests sind Unit-/Integrationstests
gegen Services, Controller-Logik und React-Komponenten. Guard-artige Spezifikationen wie
`module.guard.spec.ts` oder die i18n-Wächter (siehe unten) sind das Vorbild für Regressionsschutz
gegen bereits einmal aufgetretene Fehler — wiederkehrende Fallstricke werden in diesem Projekt
durch einen Test abgesichert, nicht nur durch einen Kommentar.
## Konventionen und Fallstricke
**NestJS-Routenreihenfolge:** NestJS matcht Routen in Deklarationsreihenfolge. Eine statische Route
wie `@Get('source-config')` **muss vor** einem `@Get(':id')`-Platzhalter derselben Klasse stehen —
sonst interpretiert der Platzhalter den literalen Pfadteil als `id` und "beschattet" die statische
Route (404 auf die eigentlich vorhandene Route). Das betrifft ausschließlich denselben HTTP-Verb:
ein `GET :id` kann niemals eine `POST`-Route beschatten. `apps/api/src/tenders/tenders.controller.ts`
dokumentiert das an jeder betroffenen Stelle explizit im Kommentar (`source-config`, `rss-feeds`,
`email-config`, `coverage`, `denylisted-portals`, `triage`, `saved-searches`,
`notification-pref` — alle vor `@Get(':id')` deklariert) und `module-grants.controller.ts` hält
`matrix` bewusst vor `users/:userId`. **Unit-Tests fangen diesen Fehler nicht** — sie rufen
üblicherweise die Handler-Methode direkt auf, nicht den tatsächlichen Routing-Mechanismus. Bei
jedem neuen `@Get(':id')`/`@Put(':id')`/`@Delete(':id')` in einem Controller mit weiteren statischen
GET-Routen: statische Routen zuerst deklarieren.
**i18n — Schlüsselparität zwischen de.json und en.json:** Jeder benutzersichtbare Text gehört in
beide Sprachdateien, `apps/web/src/messages/de.json` und `apps/web/src/messages/en.json`. Ein
strukturelle Wächter-Test, `apps/web/src/messages/tenderRadar-parity.spec.ts`, prüft für den
`tenderRadar`-Namensraum automatisiert, dass beide Dateien exakt denselben (rekursiv
aufgeschlüsselten) Schlüsselsatz besitzen und jeder Blattwert eine nicht-leere Zeichenkette ist —
ein Schlüssel, der nur in einer Sprache ergänzt wird, lässt den Test fehlschlagen. Zusätzlich prüft
`apps/web/src/messages/umlaut-guard.spec.ts` ausschließlich das geparste JSON von `de.json`/`en.json`
gegen ein Wörterbuch aus `umlaut-dictionary.ts`: keine ae/oe/ue/ss-Ersatzschreibweise
(„fuer“, „loeschen“) darf mehr vorkommen, außer sie steht auf einer Allowlist korrekter deutscher
Wörter, die zufällig `ae/oe/ue/ss` enthalten (z. B. „Passwörter“, „ausschließen“). Beide Wächter
lesen bewusst nur das geparste JSON, nie den Quellcode-Baum — ein repo-weiter Grep würde am
Wörterbuch selbst scheitern, weil dessen Schlüssel notwendigerweise die falschen Schreibweisen
enthalten.
**Tailwind 4 — der `dark:`-Selektor muss explizit an `.dark` gebunden werden:** Tailwind 4 bindet
`dark:` standardmäßig an `prefers-color-scheme`, also an die Betriebssystem-Einstellung. Tessera
schaltet den Modus aber über `next-themes` mit `attribute="class"` um — der Benutzer wählt
hell/dunkel im Portal, unabhängig vom System. `apps/web/src/app/globals.css` bindet den Selektor
deshalb explizit an die `.dark`-Klasse:
```css
@custom-variant dark (&:where(.dark, .dark *));
```
Fehlt diese Zeile, schalten die Farbtoken unter `.dark` weiter unten in derselben Datei zwar
korrekt um, aber **jede einzelne `dark:`-Utility im Quellcode bleibt wirkungslos**, sobald System-
und Portal-Einstellung nicht zufällig übereinstimmen. Der Fehler fällt dabei nicht sofort auf, weil
Hintergrund- und Textfarbe über die CSS-Variablen laufen, nicht über `dark:`-Utilities — die
Oberfläche wird also grundsätzlich dunkel, nur Feinheiten (Status-, Warn- und Fehlerfarben,
Hinweisboxen, Badges, wie im Projekt bereits an über 100 Stellen betroffen) bleiben falsch. Jede neue
`dark:`-Utility-Klasse im Projekt setzt voraus, dass diese Zeile in `globals.css` unverändert bleibt.