From 3501eb4dd194d28645d087ffd5169cdfa4c4055f Mon Sep 17 00:00:00 2001 From: Schalli Date: Wed, 9 Sep 2026 08:46:33 +0200 Subject: [PATCH] =?UTF-8?q?docs:=20Anleitungen=20fuer=20Kollegen=20?= =?UTF-8?q?=E2=80=94=20Anwender,=20Administration,=20Betrieb,=20Entwicklun?= =?UTF-8?q?g?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) Claude-Session: https://claude.ai/code/session_01FYZcd3SSmo14QTqWx2KKzU --- .planning/WINDOWS.md | 19 +- docs/README.md | 63 +++++ docs/anleitung-administration.md | 240 ++++++++++++++++ docs/anleitung-anwender.md | 166 +++++++++++ docs/anleitung-betrieb.md | 330 ++++++++++++++++++++++ docs/anleitung-entwicklung.md | 453 +++++++++++++++++++++++++++++++ 6 files changed, 1268 insertions(+), 3 deletions(-) create mode 100644 docs/README.md create mode 100644 docs/anleitung-administration.md create mode 100644 docs/anleitung-anwender.md create mode 100644 docs/anleitung-betrieb.md create mode 100644 docs/anleitung-entwicklung.md diff --git a/.planning/WINDOWS.md b/.planning/WINDOWS.md index f22fc92..8e6d66b 100644 --- a/.planning/WINDOWS.md +++ b/.planning/WINDOWS.md @@ -1,10 +1,10 @@ --- schema_version: 1 -open_count: 0 +open_count: 1 waived_count: 1 fixed_count: 15 -total_count: 16 -last_updated: 2026-09-09T06:25:18.145Z +total_count: 17 +last_updated: 2026-09-09T06:42:22.801Z --- # 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 | | 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 | +| 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 [ @@ -225,6 +226,18 @@ last_updated: 2026-09-09T06:25:18.145Z "reason": "", "recorded_at": "2026-09-07T13:22:33.916Z", "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 } ] ```` diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..cbda21e --- /dev/null +++ b/docs/README.md @@ -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. diff --git a/docs/anleitung-administration.md b/docs/anleitung-administration.md new file mode 100644 index 0000000..3630dd2 --- /dev/null +++ b/docs/anleitung-administration.md @@ -0,0 +1,240 @@ + +# 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. | diff --git a/docs/anleitung-anwender.md b/docs/anleitung-anwender.md new file mode 100644 index 0000000..719d8c2 --- /dev/null +++ b/docs/anleitung-anwender.md @@ -0,0 +1,166 @@ + +# 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. diff --git a/docs/anleitung-betrieb.md b/docs/anleitung-betrieb.md new file mode 100644 index 0000000..526dfc1 --- /dev/null +++ b/docs/anleitung-betrieb.md @@ -0,0 +1,330 @@ + +# 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 /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:@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. + + + +**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. diff --git a/docs/anleitung-entwicklung.md b/docs/anleitung-entwicklung.md new file mode 100644 index 0000000..5943349 --- /dev/null +++ b/docs/anleitung-entwicklung.md @@ -0,0 +1,453 @@ + +# 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 \ + | 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@: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 {children}; +} +``` + +**Regel für neue Module:** Ein neues fest verdrahtetes Modulverzeichnis unter `modules//` +braucht **immer** ein eigenes `layout.tsx` mit `ModuleAccessGate`, genau wie sein Backend-Controller +`@UseModule('')` 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 + ``` +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.