This commit is contained in:
Bastian Wagner
2026-07-16 15:23:53 +02:00
parent 543e8273a7
commit c03b2e17f5
114 changed files with 7631 additions and 506 deletions

121
docs/admin.md Normal file
View File

@@ -0,0 +1,121 @@
# Adminbereich
Der Adminbereich liegt im Frontend unter `/admin` und verwendet dieselbe
serverseitige Authentifizierung, CSRF-Pruefung und Permission-Logik wie die
restliche Anwendung. Angular blendet Navigation und Aktionen nur fuer passende
Permissions ein; verbindlich prueft immer das Backend.
## Permissions
Die administrativen Permissions sind fest in
`apps/backend/src/roles/permissions.ts` definiert:
- `users.read`: Benutzer anzeigen, suchen und filtern
- `users.manage`: Benutzer aktivieren, deaktivieren und Rollen zuweisen
- `roles.read`: Rollen und Permissions anzeigen
- `roles.manage`: Rollen anlegen, bearbeiten und loeschen
- `sessions.manage`: Sessions anderer Benutzer anzeigen und beenden
- `audit.read`: administratives Audit-Log anzeigen
Benutzer haben keine direkten Permissions. Effektive Permissions werden aus
allen Rollen abgeleitet. Rollen- und Benutzerverwaltung laufen ueber Services;
Controller greifen nicht direkt auf TypeORM-Repositories zu.
## Systemrollen
Es gibt mindestens die Systemrollen `admin` und `user`.
- `admin` ist geschuetzt, nicht loeschbar und muss administrative Permissions
behalten.
- `user` ist geschuetzt, nicht loeschbar und Standardrolle fuer neue Benutzer.
- Eigene Rollen koennen angelegt, umbenannt, geloescht und mit bekannten
Permissions versehen werden.
Eine Rolle kann nur geloescht werden, wenn sie keinem Benutzer mehr zugewiesen
ist. Andernfalls antwortet die API mit `ROLE_STILL_ASSIGNED`.
## Letzter aktiver Administrator
Die Anwendung darf nie ohne aktiven Administrator enden. Sicherheitskritische
Aktionen laufen transaktional und halten einen MySQL-Lock
`business_app_admin_integrity`:
- Benutzer deaktivieren
- Rollen eines Benutzers aendern
- Adminrolle entfernen
- Adminrolle in ihren Permissions veraendern
- Rolle loeschen
Wenn eine Aktion den letzten aktiven Administrator entfernen wuerde, antwortet
die API mit HTTP 409 und `LAST_ACTIVE_ADMIN_REQUIRED`.
## Benutzerstatus und Sessions
Benutzer werden weiterhin ausschliesslich ueber OIDC angelegt. Name und E-Mail
kommen vom Identity Provider und sind im Adminbereich nicht editierbar.
Beim Deaktivieren wird der Benutzer sofort inaktiv gesetzt und alle aktiven
Sessions des Benutzers werden widerrufen. Neue OIDC-Logins deaktivierter Benutzer
werden trotz erfolgreicher IdP-Authentifizierung abgewiesen. Beim erneuten
Aktivieren darf sich der Benutzer wieder anmelden; alte Sessions werden nicht
wiederhergestellt.
Admin-Session-Endpunkte geben nur eine sichere, gekuerzte Session-Referenz,
Zeitpunkte, User-Agent, IP-Annaeherung und Status zurueck. Tokens und rohe
Session-Secrets werden nie ausgegeben.
## API-Endpunkte
Benutzer:
- `GET /api/admin/users`
- `GET /api/admin/users/:id`
- `PATCH /api/admin/users/:id/deactivate`
- `PATCH /api/admin/users/:id/activate`
- `POST /api/admin/users/:id/roles/:roleId`
- `DELETE /api/admin/users/:id/roles/:roleId`
- `GET /api/admin/users/:id/sessions`
- `DELETE /api/admin/users/:userId/sessions/:sessionId`
- `DELETE /api/admin/users/:id/sessions`
Rollen:
- `GET /api/admin/roles`
- `GET /api/admin/roles/:id`
- `POST /api/admin/roles`
- `PUT /api/admin/roles/:id`
- `DELETE /api/admin/roles/:id`
Audit:
- `GET /api/audit-log`
## Audit-Log
Administrative Aenderungen werden auditierbar protokolliert, darunter:
- `USER_ACTIVATED`
- `USER_DEACTIVATED`
- `USER_ROLE_ASSIGNED`
- `USER_ROLE_REMOVED`
- `ROLE_CREATED`
- `ROLE_UPDATED`
- `ROLE_DELETED`
- `ROLE_PERMISSIONS_UPDATED`
- `SESSION_REVOKED`
- `ALL_USER_SESSIONS_REVOKED`
Gespeichert werden fachliche IDs, Request-ID und minimale Metadaten. Tokens,
Secrets und vollstaendige Sessiondaten werden nicht geloggt.
## Migration
Die Admin-Erweiterung fuegt `roles.description` hinzu. Vor dem Deployment muss
die Migration ausgefuehrt werden:
```bash
npm run migration:run
```
Der normale App-Start fuehrt Migrationen weiterhin nicht automatisch aus,
sondern meldet fehlende Migrationen ueber die Readiness-Pruefung.

View File

@@ -24,7 +24,9 @@ geschnitten:
- `sessions`: serverseitige Sessions, CSRF-Token und Session-Listen
- `roles`: Code-definierte Permissions und rollenbasierte Rechteverwaltung
- `users`: lokale Benutzerprofile und Einstellungen
- `admin`: Angular-Routen unter `/admin` fuer Benutzer-, Rollen-, Session- und Audit-Verwaltung
- `items`: Beispiel-Fachmodul
- `notifications`: persoenliche In-App-Benachrichtigungen, Admin-Erzeugung und interner Service
- `audit`: Audit-Log fuer administrative Nachvollziehbarkeit
- `health`: Liveness- und Readiness-Endpunkte
- `database`: TypeORM-Konfiguration, Entities und Migrationen
@@ -116,5 +118,12 @@ Systemrollen:
- `admin`: alle Permissions
- `user`: Basisrechte fuer Items-Lesen und eigene Sessions
Die Standardrolle `user` enthaelt ausserdem
`notifications.readOwn` und `notifications.updateOwn`, damit angemeldete
Benutzer ihre eigenen In-App-Benachrichtigungen verwalten koennen.
Der erste erfolgreich angemeldete Benutzer wird automatisch Admin. Danach
erhalten neue Benutzer initial die Rolle `user`.
Details zu Admin-Endpunkten, Systemrollen und dem transaktionalen Schutz des
letzten aktiven Administrators stehen in [Adminbereich](admin.md).

View File

@@ -30,14 +30,15 @@ separat ausgefuehrt.
## OIDC
| Variable | Bedeutung |
| ------------------------- | ---------------------------------------------------- |
| `OIDC_ISSUER` | Exakter Issuer des Providers |
| `OIDC_CLIENT_ID` | Client ID |
| `OIDC_CLIENT_SECRET` | Client Secret |
| `OIDC_SCOPES` | Scopes, typischerweise `openid profile email` |
| `OIDC_ALLOWED_ALGORITHMS` | Erlaubte ID-Token-Signaturalgorithmen, z. B. `RS256` |
| `OIDC_HTTP_TIMEOUT_MS` | Timeout fuer IdP-HTTP-Requests |
| Variable | Bedeutung |
| ------------------------- | ------------------------------------------------------- |
| `OIDC_ISSUER` | Exakter Issuer des Providers |
| `OIDC_CLIENT_ID` | Client ID |
| `OIDC_CLIENT_SECRET` | Client Secret |
| `OIDC_SCOPES` | Scopes, typischerweise `openid profile email` |
| `OIDC_LOGOUT_URL` | Optionale IdP-Logout-URL, falls Discovery keine liefert |
| `OIDC_ALLOWED_ALGORITHMS` | Erlaubte ID-Token-Signaturalgorithmen, z. B. `RS256` |
| `OIDC_HTTP_TIMEOUT_MS` | Timeout fuer IdP-HTTP-Requests |
Der Callback ist immer:
@@ -45,6 +46,11 @@ Der Callback ist immer:
<APP_BASE_URL>/api/auth/callback
```
Beim App-Logout wird zuerst die lokale Session widerrufen. Danach leitet die App
zum OIDC `end_session_endpoint` aus Discovery weiter. Wenn der Provider diesen
Endpoint nicht publiziert, kann `OIDC_LOGOUT_URL` gesetzt werden. Die App sendet
`client_id`, `post_logout_redirect_uri` und, falls vorhanden, `id_token_hint`.
## Sessions und CSRF
| Variable | Bedeutung |
@@ -93,6 +99,7 @@ OIDC_ISSUER=https://idp.example.com/realms/internal
OIDC_CLIENT_ID=business-app
OIDC_CLIENT_SECRET=<secret>
OIDC_SCOPES=openid profile email
OIDC_LOGOUT_URL=https://idp.example.com/realms/internal/protocol/openid-connect/logout
OIDC_ALLOWED_ALGORITHMS=RS256
OIDC_HTTP_TIMEOUT_MS=5000
SESSION_COOKIE_NAME=app_session

114
docs/design-system.md Normal file
View File

@@ -0,0 +1,114 @@
# Designsystem
Das Frontend verwendet ein eigenes, schlankes Designsystem ohne externe
UI-Library. Es besteht aus zentralen CSS Custom Properties, globalen
Grundklassen und wenigen Angular-UI-Komponenten unter
`apps/frontend/src/app/shared/ui`.
## Prinzipien
- sachliche Business-Oberflaeche statt Marketing-Optik
- mobile first, keine globale Mindestbreite
- klare Hierarchie durch Typografie, Abstand und Rahmen
- Farben immer semantisch ueber Tokens
- sichtbare Fokuszustaende und grosse Touch-Flaechen
- Komponenten nur dort, wo sie Verhalten oder Wiederverwendung bringen
## Tokens
Die Tokens liegen in `apps/frontend/src/styles/_tokens.scss` und werden ueber
`apps/frontend/src/styles.scss` eingebunden.
Wichtige Gruppen:
- Farben: `--color-primary`, `--color-danger`, `--color-surface`,
`--color-text-primary`, `--color-border`, `--color-focus`
- Abstaende: `--space-1` bis `--space-9`
- Typografie: `--font-size-xs` bis `--font-size-2xl`, `--line-height-*`,
`--font-weight-*`
- Layout: `--container-width`, `--sidebar-width`, `--header-height`,
`--touch-target`, `--input-height`, `--button-height`
- Oberflaeche: `--radius-*`, `--shadow-*`, `--z-*`, `--transition-*`
Feature-Komponenten duerfen keine direkten Hex-Farben enthalten. Neue Farben
werden zuerst als semantische Tokens angelegt.
## Globale Klassen
Globale Klassen sind bewusst begrenzt:
- Layout: `.ui-page`, `.ui-page-header`, `.ui-grid`, `.ui-card`,
`.ui-toolbar`, `.ui-actions`
- Formulare: `.ui-form`, `.ui-form-field`, `.ui-control`, `.ui-checkbox`,
`.ui-field-error`
- Buttons: `.ui-button`, `.ui-icon-button`
- Tabellen: `.ui-table-wrap`, `.ui-table`
- Status: `.ui-badge`, `.ui-badge--success`, `.ui-badge--warning`,
`.ui-badge--danger`, `.ui-badge--info`
- Utilities: `.visually-hidden`, `.truncate`, `.stack`, `.cluster`,
`.full-width`, `.text-muted`
Keine neuen Utility-Klassen einfuehren, wenn eine lokale Klasse oder bestehende
UI-Komponente ausreicht.
## Angular-Komponenten
Wiederverwendbare UI-Bausteine:
- `UiButtonComponent`
- `UiIconComponent`
- `UiIconButtonComponent`
- `UiFormFieldComponent`
- `UiStatusBadgeComponent`
- `UiConfirmDialogComponent`
- `ToastService` und `UiToastHostComponent`
- `UiEmptyStateComponent`
- `UiLoadingStateComponent`
- `UiPaginationComponent`
- `UiPageHeaderComponent`
Neue Feature-Seiten sollen diese Bausteine bevorzugen, wenn sie Button-, Badge-,
Dialog-, Toast-, Empty-, Loading- oder Pagination-Verhalten brauchen.
## Responsive Regeln
- Mobile Layouts sind einspaltig.
- Aktionen duerfen mobil untereinander stehen und volle Breite nutzen.
- Business-Listen werden mobil als Karten dargestellt.
- Tabellen liegen in `.ui-table-wrap`, wenn eine echte Tabelle sinnvoll bleibt.
- Touch-Ziele orientieren sich an `--touch-target`.
- Breakpoints werden in rem formuliert und nicht nach Geraetetyp benannt.
## Accessibility
- native HTML-Elemente vor ARIA verwenden
- interaktive Elemente sind Buttons oder Links
- sichtbare Fokuszustaende nicht entfernen
- Labels ersetzen Placeholder nicht
- Fehlertexte stehen direkt am Feld
- Status ist nicht nur Farbe, sondern auch Text/Marker
- Dialoge setzen Fokus, schliessen per Escape und geben Fokus zurueck
- Navigation und Drawer sind per Tastatur bedienbar
- Animationen respektieren `prefers-reduced-motion`
## Entwicklungsseite
Die interne Referenzseite liegt unter:
```text
/dev/design-system
```
Sie ist mit `devOnlyGuard` geschuetzt und im Production-Modus nicht matchbar.
Sie ersetzt kein Storybook, sondern zeigt die vorhandenen Tokens und Komponenten
innerhalb der echten Anwendung.
## Regeln fuer neue UI
1. Bestehende UI-Komponenten oder globale Klassen wiederverwenden.
2. Keine direkte Hex-Farbe in Feature-Komponenten.
3. Keine neue UI-Library einfuehren.
4. Keine tiefen Selektoren, kein `::ng-deep`, kein unkontrolliertes
`!important`.
5. Keine klickbaren `div`-Elemente als Ersatz fuer Buttons oder Links.
6. Verhalten mit Vitest testen, insbesondere Accessibility-relevante Zustaende.

View File

@@ -38,6 +38,11 @@ Der OIDC Provider muss als Redirect URI diese URL erlauben:
http://localhost:3000/api/auth/callback
```
Falls der Provider RP-Initiated Logout validiert, muss ausserdem
`http://localhost:4200` beziehungsweise die konfigurierte `FRONTEND_BASE_URL` als
Post-Logout-Redirect erlaubt sein. Wenn Discovery keinen `end_session_endpoint`
liefert, setze `OIDC_LOGOUT_URL`.
## Datenbank vorbereiten
Die Anwendung fuehrt Migrationen beim normalen Start nicht automatisch aus.

146
docs/notifications.md Normal file
View File

@@ -0,0 +1,146 @@
# Benachrichtigungen
Das Modul `apps/backend/src/notifications` stellt persoenliche In-App-
Benachrichtigungen bereit. Es gibt bewusst keinen E-Mail-Versand, keine Push
Notifications, keine WebSockets, keine Server-Sent Events, keine Queue, kein
Redis und keine Hintergrundjobs. Neue Benachrichtigungen werden beim Laden der
Anwendung und per Frontend-Polling abgerufen.
## Datenmodell
Die Entity `NotificationEntity` wird in der Tabelle `notifications` gespeichert.
Jede Benachrichtigung gehoert genau einem Benutzer.
Felder:
- `id`: UUID
- `userId`: Foreign Key auf `users.id`
- `type`: technischer Typ aus `NotificationType`
- `title`: kurzer Plain-Text-Titel, maximal 150 Zeichen
- `message`: Plain-Text-Nachricht, maximal 1000 Zeichen
- `link`: optionale interne Angular-Route, maximal 500 Zeichen
- `metadata`: optionale JSON-Metadaten, maximal 4096 Bytes serialisiert
- `readAt`: `null`, solange ungelesen
- `createdAt`: UTC-Erstellungszeit
- `deletedAt`: Soft Delete
Indizes:
- `user_id, created_at`
- `user_id, read_at`
- `user_id, deleted_at`
Die Migration liegt unter
`apps/backend/src/database/migrations/1720000001000-AddNotifications.ts` und wird
nicht automatisch beim App-Start ausgefuehrt.
## Typen
Benachrichtigungstypen sind Code, keine Datenbankdaten:
- `system`
- `item.created`
- `item.updated`
- `user.role-changed`
Neue Module erweitern `apps/backend/src/notifications/notification-types.ts`
und nutzen anschliessend `NotificationsService`.
## Permissions
- `notifications.readOwn`: eigene Benachrichtigungen lesen
- `notifications.updateOwn`: eigene Benachrichtigungen markieren oder loeschen
- `notifications.manage`: administrativ Benachrichtigungen erzeugen
Die Systemrolle `user` erhaelt `notifications.readOwn` und
`notifications.updateOwn`. Die Rolle `admin` erhaelt ueber `allPermissions`
zusaetzlich `notifications.manage`.
## API
Alle Endpunkte liegen unter dem bestehenden API-Praefix `/api`.
- `GET /notifications`: eigene Benachrichtigungen, mit `page`, `pageSize` und
`status=all|read|unread`
- `GET /notifications/unread-count`: Anzahl ungelesener eigener
Benachrichtigungen
- `PATCH /notifications/:id/read`: idempotent als gelesen markieren
- `PATCH /notifications/:id/unread`: idempotent als ungelesen markieren
- `PATCH /notifications/read-all`: alle eigenen Benachrichtigungen als gelesen
markieren
- `DELETE /notifications/:id`: Soft Delete einer eigenen Benachrichtigung
- `POST /admin/notifications`: administrative Erzeugung mit
`notifications.manage`
Normale Benutzer koennen keine fremde `userId` uebergeben. Der aktuelle Benutzer
wird serverseitig aus der Session bestimmt.
## Interner Service
Andere Backend-Module erzeugen Benachrichtigungen ueber
`NotificationsService`, nicht direkt ueber ein Repository:
```ts
await notifications.createForUser({
userId,
type: NotificationType.ItemCreated,
title: 'Neuer Eintrag',
message: 'Der Eintrag "Beispiel" wurde erstellt.',
link: '/items/123',
metadata: { itemId: '123' },
});
```
Verfuegbare Methoden:
- `createForUser`
- `createForUsers`
- `getForCurrentUser`
- `getUnreadCount`
- `markAsRead`
- `markAsUnread`
- `markAllAsRead`
- `softDelete`
`createForUsers` begrenzt Bulk-Erzeugung auf 100 Zielbenutzer.
## Integrationen
`ItemsService` benachrichtigt beim Erstellen eines Items den ersten aktiven
Administrator, sofern dieser nicht der Ersteller ist. Das ist eine bewusst kleine
Beispielregel und keine vollstaendige fachliche Eskalationslogik.
`UsersService` benachrichtigt betroffene Benutzer nach erfolgreicher
Rollenaenderung. Fehler beim Erzeugen werden geloggt; die bereits erfolgreiche
sicherheitsrelevante Rollenaenderung wird dadurch nicht unkontrolliert
zurueckgerollt.
Administrative Erzeugung wird im Audit-Log als `NOTIFICATION_CREATED`
protokolliert. Geloggt werden Actor, Zielbenutzer, Notification-ID, Typ und
Request-ID, nicht die vollstaendige Nachricht oder Metadata.
## Frontend
`NotificationStore` verwendet Angular Signals fuer lokalen State und RxJS fuer
HTTP und Polling. Header-Panel und Seite `/notifications` verwenden denselben
Store, damit keine doppelten Requests fuer dieselben Daten entstehen.
Polling:
- Standardintervall: 60 Sekunden ueber `NOTIFICATION_POLL_INTERVAL_MS`
- nur bei angemeldetem Benutzer
- pausiert bei unsichtbarem Tab
- aktualisiert sofort beim Sichtbarwerden
- verhindert ueberlappende Count-Requests
- stoppt und leert State beim Logout
Links werden im Frontend nur navigiert, wenn sie interne relative Routen sind.
Titel und Nachricht werden normal interpoliert und nicht per `innerHTML`
gerendert.
## Spaetere Echtzeitkommunikation
Wenn spaeter echte Echtzeitkommunikation noetig wird, sollte das als separate
Architekturentscheidung erfolgen. Dann waeren Transport, Skalierung,
Authentifizierung, Backpressure und Betrieb gemeinsam zu entscheiden, statt
WebSockets oder Queues nebenbei in das In-App-Modul einzubauen.

View File

@@ -20,6 +20,12 @@ Der Login nutzt Authorization Code Flow mit PKCE:
Erlaubte Signaturalgorithmen werden ueber `OIDC_ALLOWED_ALGORITHMS` gesetzt.
`none` ist explizit verboten.
Beim Logout wird zuerst die lokale Session widerrufen und das Session-/CSRF-Cookie
geloescht. Anschliessend redirectet das Backend zum OIDC
`end_session_endpoint` aus Discovery oder zur optionalen `OIDC_LOGOUT_URL`. Wenn
die Session ein ID-Token enthaelt, wird es nur als `id_token_hint` an den IdP
gegeben und nicht an das Frontend ausgeliefert.
## Sessions
Sessions liegen in MySQL. Das Session-Cookie enthaelt keine Tokens oder
@@ -60,6 +66,18 @@ Backend-Controller schuetzen Endpunkte mit:
Angular nutzt Permissions nur fuer Navigation und Darstellung. Eine versteckte
Schaltflaeche ist keine Sicherheitsgrenze.
Administrative Benutzer-, Rollen- und Session-Aktionen sind unter `/api/admin/*`
mit `users.read`, `users.manage`, `roles.read`, `roles.manage`,
`sessions.manage` und `audit.read` geschuetzt. Aktionen, die den letzten aktiven
Administrator entfernen koennten, laufen transaktional und antworten bei Verstoss
mit `LAST_ACTIVE_ADMIN_REQUIRED`.
Benachrichtigungen sind benutzerbezogene Daten. Normale Notification-Endpunkte
bestimmen den Benutzer ausschliesslich aus der serverseitig aufgeloesten Session.
Benutzer-IDs werden fuer eigene Benachrichtigungen nicht als Query-Parameter oder
Body-Feld akzeptiert. Fremde oder geloeschte Notification-IDs werden als nicht
gefunden behandelt.
## Benutzerstatus
Deaktivierte Benutzer werden trotz erfolgreichem IdP-Login abgewiesen. Aktive