generated from bastian/boilerplate
Initial commit
This commit is contained in:
121
docs/admin.md
Normal file
121
docs/admin.md
Normal 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.
|
||||
129
docs/architecture.md
Normal file
129
docs/architecture.md
Normal file
@@ -0,0 +1,129 @@
|
||||
# Architektur
|
||||
|
||||
Das Repository ist ein modularer Monolith mit getrennten Workspaces fuer
|
||||
Frontend, Backend und API-Client. Ausgeliefert wird eine einzelne Node.js-App,
|
||||
die die gebaute Angular-Anwendung statisch mitliefert.
|
||||
|
||||
## Workspaces
|
||||
|
||||
```text
|
||||
apps/frontend
|
||||
apps/backend
|
||||
packages/api-client
|
||||
```
|
||||
|
||||
Es wird bewusst kein Nx eingesetzt. Workspace-Grenzen bleiben durch npm-Scripts,
|
||||
TypeScript-Projekte und klare Importpfade nachvollziehbar.
|
||||
|
||||
## Backend
|
||||
|
||||
Das Backend liegt in `apps/backend` und basiert auf NestJS. Es ist nach Features
|
||||
geschnitten:
|
||||
|
||||
- `auth`: OIDC Login, Callback, Logout und Authentifizierung
|
||||
- `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
|
||||
- `common`: Fehlerformat, Request Context, Validierung und HTTP-Helfer
|
||||
|
||||
Controller enthalten keine Datenbanklogik. Sie validieren den HTTP-Rand,
|
||||
deklarieren Permissions und delegieren an Services. Services enthalten
|
||||
Fachlogik. Datenbankzugriffe laufen ueber Repository-Klassen oder klar benannte
|
||||
Persistence-Services.
|
||||
|
||||
Globale Backend-Bausteine:
|
||||
|
||||
- `ApiExceptionFilter` normalisiert Fehlerantworten.
|
||||
- `ThrottlerGuard` setzt Rate Limits.
|
||||
- `CsrfGuard` schuetzt schreibende Requests.
|
||||
- `PermissionsGuard` prueft Rollen und Permissions.
|
||||
- `RequestIdMiddleware` setzt Request-Korrelation.
|
||||
- `helmet` setzt Security Header.
|
||||
- `pino-http` loggt mit Redaction fuer Cookies, Tokens und Secrets.
|
||||
|
||||
## Frontend
|
||||
|
||||
Das Frontend liegt in `apps/frontend` und nutzt Angular Standalone Components.
|
||||
Der Shell-Aufbau steckt in `src/app/layout/app-shell.ts`. Feature-Seiten liegen
|
||||
unter `src/app/features`.
|
||||
|
||||
Design- und Implementierungsregeln:
|
||||
|
||||
- mobile-first HTML und SCSS
|
||||
- keine UI-Komponentenbibliothek
|
||||
- Signals fuer lokalen UI-State
|
||||
- RxJS fuer HTTP- und API-Flows
|
||||
- Angular Permissions nur fuer Darstellung und Navigation, nicht als
|
||||
Sicherheitsgrenze
|
||||
|
||||
Der `permissionGuard` verhindert unpassende Navigation im Frontend. Die
|
||||
verbindliche Autorisierung findet immer im Backend statt.
|
||||
|
||||
## API-Client
|
||||
|
||||
`packages/api-client` enthaelt den vom Frontend genutzten Angular-Service und die
|
||||
DTO-Typen. Der Code ist als generiert markiert. Er wird mit folgendem Befehl neu
|
||||
geschrieben:
|
||||
|
||||
```bash
|
||||
npm run api:generate
|
||||
```
|
||||
|
||||
Der Produktionsbuild ruft diesen Schritt vor Backend- und Frontend-Builds auf.
|
||||
Bei API-Aenderungen muss der Generator synchron zur Backend-API angepasst werden.
|
||||
|
||||
## Runtime-Aufbau
|
||||
|
||||
Im Produktionsbuild entsteht:
|
||||
|
||||
```text
|
||||
apps/backend/dist/main.js
|
||||
apps/backend/dist/public/index.html
|
||||
apps/backend/dist/public/assets...
|
||||
```
|
||||
|
||||
NestJS liefert:
|
||||
|
||||
- `/api/*` als JSON API
|
||||
- `/api/docs` als Swagger UI, wenn aktiviert
|
||||
- `/health/live` fuer Liveness
|
||||
- `/health/ready` fuer Readiness inklusive Datenbank- und Migrationspruefung
|
||||
- alle anderen Pfade als Angular SPA Fallback
|
||||
|
||||
## Datenmodell und Migrationen
|
||||
|
||||
TypeORM laeuft mit:
|
||||
|
||||
- `synchronize: false`
|
||||
- `migrationsRun: false`
|
||||
- expliziten Migrationen unter `apps/backend/src/database/migrations`
|
||||
|
||||
Der normale App-Start fuehrt keine Migrationen aus. Stattdessen prueft
|
||||
`MigrationHealthService`, ob Migrationen fehlen. Dadurch wird verhindert, dass
|
||||
ein App-Rollout unbemerkt ein Schema veraendert.
|
||||
|
||||
## Autorisierung
|
||||
|
||||
Permissions werden in `apps/backend/src/roles/permissions.ts` definiert. Rollen
|
||||
sind Datenbankdaten, Benutzer erhalten Permissions nur ueber Rollen.
|
||||
|
||||
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).
|
||||
118
docs/configuration.md
Normal file
118
docs/configuration.md
Normal file
@@ -0,0 +1,118 @@
|
||||
# Konfiguration
|
||||
|
||||
Die Runtime-Konfiguration wird ueber Umgebungsvariablen geladen und mit Zod in
|
||||
`apps/backend/src/config/env.ts` validiert. `.env.example` ist die Referenz fuer
|
||||
lokale Entwicklung und Deployment-Vorlagen.
|
||||
|
||||
## Allgemein
|
||||
|
||||
| Variable | Bedeutung |
|
||||
| ------------------- | -------------------------------------------------------------------------- |
|
||||
| `NODE_ENV` | `development`, `test` oder `production` |
|
||||
| `PORT` | HTTP-Port des Backends |
|
||||
| `APP_BASE_URL` | Externe Basis-URL der App; Grundlage fuer OIDC Callback |
|
||||
| `FRONTEND_BASE_URL` | Frontend-Origin fuer Redirects und CORS; faellt auf `APP_BASE_URL` zurueck |
|
||||
| `TRUST_PROXY` | Express Trust Proxy, wenn hinter Reverse Proxy |
|
||||
|
||||
## Datenbank
|
||||
|
||||
| Variable | Bedeutung |
|
||||
| ------------------- | --------------------------- |
|
||||
| `DATABASE_HOST` | MySQL Host |
|
||||
| `DATABASE_PORT` | MySQL Port, Standard `3306` |
|
||||
| `DATABASE_NAME` | Datenbankname |
|
||||
| `DATABASE_USER` | Datenbankbenutzer |
|
||||
| `DATABASE_PASSWORD` | Datenbankpasswort |
|
||||
| `DATABASE_SSL` | TLS fuer MySQL-Verbindung |
|
||||
|
||||
Die Datenbank muss MySQL 8 und `utf8mb4` unterstuetzen. Migrationen werden
|
||||
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_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:
|
||||
|
||||
```text
|
||||
<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 |
|
||||
| ---------------------------------- | --------------------------------------------------------- |
|
||||
| `SESSION_COOKIE_NAME` | Name des signierten Session-Cookies |
|
||||
| `SESSION_IDLE_TIMEOUT_SECONDS` | Inaktivitaetsablauf |
|
||||
| `SESSION_ABSOLUTE_TIMEOUT_SECONDS` | Absoluter Ablauf unabhaengig von Aktivitaet |
|
||||
| `SESSION_SECRET` | Secret fuer Cookie-Signatur, mindestens 32 Zeichen |
|
||||
| `SESSION_ENCRYPTION_KEY` | Secret fuer Token-Verschluesselung, mindestens 32 Zeichen |
|
||||
| `CSRF_HEADER_NAME` | Header fuer CSRF-Token, Standard `X-CSRF-Token` |
|
||||
|
||||
In Produktion duerfen diese Secrets keine Beispielwerte enthalten. Die
|
||||
Konfiguration bricht bei offensichtlichen Platzhaltern ab.
|
||||
|
||||
## CORS, Logging, Swagger und Rate Limits
|
||||
|
||||
| Variable | Bedeutung |
|
||||
| ------------------------------------- | ---------------------------------------- |
|
||||
| `CORS_ORIGINS` | Kommagetrennte erlaubte Origins |
|
||||
| `LOG_LEVEL` | Pino Log Level |
|
||||
| `SWAGGER_ENABLED` | Swagger UI und JSON aktivieren |
|
||||
| `RATE_LIMIT_WINDOW_SECONDS` | Globales IP-Zeitfenster |
|
||||
| `RATE_LIMIT_MAX_REQUESTS` | Globale Requests pro IP und Zeitfenster |
|
||||
| `RATE_LIMIT_SENSITIVE_WINDOW_SECONDS` | Zeitfenster fuer sensible Endpunkte |
|
||||
| `RATE_LIMIT_SENSITIVE_MAX_REQUESTS` | Requests pro IP auf sensiblen Endpunkten |
|
||||
|
||||
`SWAGGER_ENABLED` sollte in Produktion nur bewusst aktiviert werden. Das
|
||||
eingebaute Rate Limiting ist In-Memory, pro Prozess und nicht fuer horizontale
|
||||
Skalierung koordiniert. Es ist fuer genau eine Containerinstanz ausgelegt.
|
||||
|
||||
## Beispiel fuer Produktion
|
||||
|
||||
```env
|
||||
NODE_ENV=production
|
||||
PORT=3000
|
||||
APP_BASE_URL=https://business.example.com
|
||||
FRONTEND_BASE_URL=https://business.example.com
|
||||
TRUST_PROXY=true
|
||||
DATABASE_HOST=mysql.internal
|
||||
DATABASE_PORT=3306
|
||||
DATABASE_NAME=business_app
|
||||
DATABASE_USER=business_app
|
||||
DATABASE_PASSWORD=<secret>
|
||||
DATABASE_SSL=true
|
||||
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
|
||||
SESSION_IDLE_TIMEOUT_SECONDS=28800
|
||||
SESSION_ABSOLUTE_TIMEOUT_SECONDS=604800
|
||||
SESSION_SECRET=<secret>
|
||||
SESSION_ENCRYPTION_KEY=<secret>
|
||||
CORS_ORIGINS=https://business.example.com
|
||||
CSRF_HEADER_NAME=X-CSRF-Token
|
||||
LOG_LEVEL=info
|
||||
SWAGGER_ENABLED=false
|
||||
RATE_LIMIT_WINDOW_SECONDS=60
|
||||
RATE_LIMIT_MAX_REQUESTS=300
|
||||
RATE_LIMIT_SENSITIVE_WINDOW_SECONDS=60
|
||||
RATE_LIMIT_SENSITIVE_MAX_REQUESTS=10
|
||||
```
|
||||
139
docs/deployment.md
Normal file
139
docs/deployment.md
Normal file
@@ -0,0 +1,139 @@
|
||||
# Deployment und Betrieb
|
||||
|
||||
Die Anwendung wird als ein Docker-Image ausgeliefert. Das Image enthaelt das
|
||||
gebaute NestJS-Backend und das gebaute Angular-Frontend.
|
||||
|
||||
## Release-Build
|
||||
|
||||
Vor jedem Release:
|
||||
|
||||
```bash
|
||||
npm ci
|
||||
npm run lint
|
||||
npm run format:check
|
||||
npm run typecheck
|
||||
npm test
|
||||
npm run build
|
||||
docker build -t registry.example.com/business-app:<tag> .
|
||||
```
|
||||
|
||||
`npm run build` fuehrt aus:
|
||||
|
||||
1. API-Client generieren
|
||||
2. API-Client bauen
|
||||
3. Backend bauen
|
||||
4. Frontend bauen
|
||||
5. Frontend nach `apps/backend/dist/public` kopieren
|
||||
|
||||
Das Dockerfile baut erneut im Container und erzeugt anschliessend ein schlankes
|
||||
Runtime-Image mit Non-Root-User.
|
||||
|
||||
## Runtime-Konfiguration
|
||||
|
||||
Konfiguration erfolgt ueber Umgebungsvariablen. Nutze `.env.example` nur als
|
||||
Vorlage. Production-Secrets gehoeren in Secret Management, CI/CD Variables oder
|
||||
eine geschuetzte Server-Konfiguration.
|
||||
|
||||
Wichtige Produktionswerte:
|
||||
|
||||
- `NODE_ENV=production`
|
||||
- `APP_BASE_URL=https://app.example.com`
|
||||
- `FRONTEND_BASE_URL=https://app.example.com`
|
||||
- `TRUST_PROXY=true`, wenn hinter Reverse Proxy
|
||||
- `DATABASE_SSL=true`, wenn die Datenbank TLS erzwingt
|
||||
- `SWAGGER_ENABLED=false`, ausser bewusst anders entschieden
|
||||
- starke Werte fuer `SESSION_SECRET`, `SESSION_ENCRYPTION_KEY`, `OIDC_CLIENT_SECRET`
|
||||
|
||||
## Migrationen
|
||||
|
||||
Migrationen werden nicht beim App-Start ausgefuehrt. Fuehre sie vor dem neuen
|
||||
App-Container aus, mit demselben Image und derselben Konfiguration:
|
||||
|
||||
```bash
|
||||
docker run --rm --env-file .env registry.example.com/business-app:<tag> node apps/backend/dist/database/run-migrations.js
|
||||
```
|
||||
|
||||
Danach den App-Container starten oder aktualisieren. Wenn Migrationen fehlen,
|
||||
schlaegt der App-Start fehl beziehungsweise `/health/ready` bleibt nicht bereit.
|
||||
|
||||
## Container starten
|
||||
|
||||
Ein einfaches Compose-Beispiel liegt in `compose.yml`:
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
Der Container:
|
||||
|
||||
- laeuft als Non-Root-User
|
||||
- verwendet ein read-only Root Filesystem
|
||||
- nutzt `/tmp` als tmpfs
|
||||
- dropt Linux Capabilities
|
||||
- prueft `/health/ready` als Healthcheck
|
||||
|
||||
Compose baut oder nutzt das Image `internal-business-app:latest`. Fuer echte
|
||||
Deployments sollte ein versioniertes Registry-Image verwendet werden.
|
||||
|
||||
## Reverse Proxy
|
||||
|
||||
Nginx oder ein Load Balancer sollte TLS terminieren und alle Routen an den
|
||||
App-Container weiterleiten. Die Angular-Dateien werden nicht separat durch Nginx
|
||||
ausgeliefert. Ein Beispiel liegt in `docs/nginx-example.conf`.
|
||||
|
||||
Wichtig:
|
||||
|
||||
- WebSocket-Konfiguration ist fuer dieses Boilerplate nicht erforderlich.
|
||||
- `X-Forwarded-Proto` und `X-Forwarded-For` sollten gesetzt werden.
|
||||
- `APP_BASE_URL` muss zur externen HTTPS-URL passen.
|
||||
- Wenn `TRUST_PROXY=true` gesetzt ist, muss der Proxy vertrauenswuerdig sein.
|
||||
|
||||
## Healthchecks
|
||||
|
||||
- `/health/live`: Prozess lebt.
|
||||
- `/health/ready`: Datenbank erreichbar, Migration-Check initialisiert und keine
|
||||
offenen Migrationen.
|
||||
|
||||
Readiness ist der richtige Check fuer Rolling Deployments und Container
|
||||
Orchestrierung.
|
||||
|
||||
## TeamCity-Beispiel
|
||||
|
||||
1. Checkout
|
||||
2. `npm ci`
|
||||
3. `npm run lint`
|
||||
4. `npm run format:check`
|
||||
5. `npm run typecheck`
|
||||
6. `npm test`
|
||||
7. `npm run build`
|
||||
8. `docker build -t <registry>/<image>:<build-number> .`
|
||||
9. `docker push <registry>/<image>:<build-number>`
|
||||
10. Migration-Job mit neuem Image ausfuehren
|
||||
11. App-Service auf neues Image aktualisieren
|
||||
12. `/health/ready` pruefen
|
||||
|
||||
## Rollback
|
||||
|
||||
Ein Rollback ist nur dann einfach, wenn die Datenbankmigrationen rueckwaerts
|
||||
kompatibel geplant wurden. Fuer riskante Schema-Aenderungen sollte das Expand-
|
||||
Contract-Muster verwendet werden:
|
||||
|
||||
1. Neue Spalten oder Tabellen hinzufuegen, alte weiter bedienen.
|
||||
2. Anwendung umstellen.
|
||||
3. Daten migrieren.
|
||||
4. Alte Spalten oder Pfade in einem spaeteren Release entfernen.
|
||||
|
||||
## Betrieb
|
||||
|
||||
Beobachte mindestens:
|
||||
|
||||
- HTTP-Fehlerraten und Latenzen
|
||||
- Healthcheck-Status
|
||||
- Datenbankverbindungen
|
||||
- Login-Fehler vom OIDC Provider
|
||||
- `MIGRATION_MISSING`
|
||||
- Rate-Limit-Treffer, insbesondere `RATE_LIMIT_EXCEEDED` auf Login- und Admin-Endpunkten
|
||||
- Audit-Log fuer administrative Aktionen
|
||||
|
||||
Logs sollten zentral gesammelt werden. Request IDs helfen dabei, Frontend-Fehler,
|
||||
Backend-Logs und Audit-Eintraege zusammenzufuehren.
|
||||
114
docs/design-system.md
Normal file
114
docs/design-system.md
Normal 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.
|
||||
119
docs/development.md
Normal file
119
docs/development.md
Normal file
@@ -0,0 +1,119 @@
|
||||
# Entwicklung
|
||||
|
||||
Diese Datei beschreibt die normalen Entwicklungsablaeufe im Monorepo.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm ci
|
||||
```
|
||||
|
||||
Abhaengigkeiten werden nur im Root installiert. Workspace-spezifische Befehle
|
||||
werden ueber Root-Scripts oder `npm --workspace ...` ausgefuehrt.
|
||||
|
||||
## Lokaler Start
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Alternativ getrennt:
|
||||
|
||||
```bash
|
||||
npm run dev:frontend
|
||||
npm run dev:backend
|
||||
```
|
||||
|
||||
Das Frontend nutzt den Proxy in `apps/frontend/proxy.conf.json`, damit `/api`
|
||||
lokal an das Backend weitergereicht wird.
|
||||
|
||||
## Feature-Workflow
|
||||
|
||||
1. Backend-DTOs und Entities modellieren.
|
||||
2. Repository fuer Datenzugriff erstellen oder erweitern.
|
||||
3. Service mit Fachlogik implementieren.
|
||||
4. Controller nur als HTTP-Rand und Permission-Deklaration verwenden.
|
||||
5. Permission in `roles/permissions.ts` ergaenzen, falls noetig.
|
||||
6. Migration erzeugen und kontrollieren.
|
||||
7. API-Client-Generator aktualisieren.
|
||||
8. Frontend-Route, Navigation und Page bauen.
|
||||
9. Tests fuer Verhalten schreiben.
|
||||
10. Qualitaetsbefehle ausfuehren.
|
||||
|
||||
## Migrationen
|
||||
|
||||
Status anzeigen:
|
||||
|
||||
```bash
|
||||
npm run migration:status
|
||||
```
|
||||
|
||||
Migration erzeugen:
|
||||
|
||||
```bash
|
||||
npm run migration:generate
|
||||
```
|
||||
|
||||
Migration ausfuehren:
|
||||
|
||||
```bash
|
||||
npm run migration:run
|
||||
```
|
||||
|
||||
Generierte Migrationen muessen reviewed werden. Sie duerfen keine
|
||||
versehentlichen Datenverluste, falsche Defaults oder umgebungsspezifische Namen
|
||||
enthalten.
|
||||
|
||||
## API-Client
|
||||
|
||||
Nach API-Aenderungen:
|
||||
|
||||
```bash
|
||||
npm run api:generate
|
||||
```
|
||||
|
||||
Der Client unter `packages/api-client/src` ist generiert. Aendere stattdessen den
|
||||
Generator oder ersetze ihn spaeter bewusst durch einen OpenAPI-basierten
|
||||
Generator.
|
||||
|
||||
## Tests
|
||||
|
||||
```bash
|
||||
npm test
|
||||
npm run test:backend
|
||||
npm run test:frontend
|
||||
```
|
||||
|
||||
Tests sollen Verhalten pruefen, nicht nur Existenz. Backend-Tests fuer Services
|
||||
sollen fachliche Regeln, Fehlerfaelle, Permissions und Datenbankinteraktionen
|
||||
abdecken. Frontend-Tests sollen sichtbares Verhalten, Guards, Interaktionen und
|
||||
API-Fehlerpfade abdecken.
|
||||
|
||||
Integrationstests gegen MySQL muessen eine separate Testdatenbank verwenden,
|
||||
deren Name eindeutig `test` enthaelt. Tests duerfen niemals gegen
|
||||
Produktionsdatenbanken laufen.
|
||||
|
||||
## Qualitaet vor Abschluss
|
||||
|
||||
```bash
|
||||
npm run lint
|
||||
npm run format:check
|
||||
npm run typecheck
|
||||
npm test
|
||||
npm run build
|
||||
docker build .
|
||||
```
|
||||
|
||||
Diese Befehle sind die Mindestpruefung fuer Merge und Release.
|
||||
|
||||
## Fehlerformat
|
||||
|
||||
Backend-Fehler werden ueber `ApiExceptionFilter` in ein konsistentes Format
|
||||
gebracht. Neue fachliche Fehler sollten `ApiError` und `ErrorCode` verwenden,
|
||||
damit Frontend und Logs stabil bleiben.
|
||||
|
||||
## Logging
|
||||
|
||||
`pino-http` redigiert sensible Header und Token-Felder. Neue Logs duerfen keine
|
||||
Secrets, Session-IDs, Cookies, Access Tokens, Refresh Tokens oder ID Tokens
|
||||
enthalten.
|
||||
93
docs/getting-started.md
Normal file
93
docs/getting-started.md
Normal file
@@ -0,0 +1,93 @@
|
||||
# Getting Started
|
||||
|
||||
Diese Anleitung bringt eine lokale Entwicklungsumgebung fuer das Boilerplate zum
|
||||
Laufen. Sie setzt voraus, dass MySQL 8 und ein OIDC Provider bereits verfuegbar
|
||||
sind.
|
||||
|
||||
## Voraussetzungen
|
||||
|
||||
- Node.js `24.18.0` aus `.nvmrc`
|
||||
- npm `11.x`
|
||||
- Docker fuer Image-Builds und spaetere Auslieferung
|
||||
- MySQL 8 mit `utf8mb4`
|
||||
- OIDC Client mit Authorization Code Flow, PKCE und Discovery Endpoint
|
||||
|
||||
Das Repository ist ein npm-Workspace-Monorepo. Abhaengigkeiten werden immer aus
|
||||
dem Root installiert.
|
||||
|
||||
```bash
|
||||
npm ci
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
## Lokale Konfiguration
|
||||
|
||||
Trage in `.env` mindestens folgende Werte ein:
|
||||
|
||||
- `DATABASE_HOST`, `DATABASE_PORT`, `DATABASE_NAME`, `DATABASE_USER`, `DATABASE_PASSWORD`
|
||||
- `OIDC_ISSUER`, `OIDC_CLIENT_ID`, `OIDC_CLIENT_SECRET`
|
||||
- `SESSION_SECRET` mit mindestens 32 zufaelligen Zeichen
|
||||
- `SESSION_ENCRYPTION_KEY` mit mindestens 32 zufaelligen Zeichen
|
||||
- `APP_BASE_URL=http://localhost:3000`
|
||||
- `FRONTEND_BASE_URL=http://localhost:4200`
|
||||
- `CORS_ORIGINS=http://localhost:4200,http://localhost:3000`
|
||||
|
||||
Der OIDC Provider muss als Redirect URI diese URL erlauben:
|
||||
|
||||
```text
|
||||
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.
|
||||
Fuehre sie bewusst aus:
|
||||
|
||||
```bash
|
||||
npm run migration:status
|
||||
npm run migration:run
|
||||
```
|
||||
|
||||
Wenn Migrationen fehlen, verweigert das Backend den Start beziehungsweise
|
||||
`/health/ready` bleibt nicht bereit.
|
||||
|
||||
## Entwicklung starten
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Das startet:
|
||||
|
||||
- Frontend: `http://localhost:4200`
|
||||
- Backend: `http://localhost:3000`
|
||||
- API: `http://localhost:3000/api`
|
||||
- Health: `http://localhost:3000/health/live` und `/health/ready`
|
||||
- Swagger, falls `SWAGGER_ENABLED=true`: `http://localhost:3000/api/docs`
|
||||
|
||||
Das Frontend proxyt `/api` ueber `apps/frontend/proxy.conf.json` an das Backend.
|
||||
Dadurch kann lokal mit Cookie-basierter Authentifizierung gearbeitet werden.
|
||||
|
||||
## Erster Login
|
||||
|
||||
1. Oeffne `http://localhost:4200`.
|
||||
2. Melde dich ueber den OIDC Provider an.
|
||||
3. Der erste lokal angelegte Benutzer erhaelt automatisch die Rollen `user` und
|
||||
`admin`.
|
||||
4. Weitere Benutzer erhalten initial die Rolle `user`.
|
||||
|
||||
Systemrollen und Permissions werden beim Login synchronisiert. Permissions sind
|
||||
im Code definiert und werden nicht frei in der UI angelegt.
|
||||
|
||||
## Haefige Probleme
|
||||
|
||||
- `Ungueltige Konfiguration`: `.env` verletzt das Schema in `apps/backend/src/config/env.ts`.
|
||||
- `MIGRATION_MISSING`: `npm run migration:run` ausfuehren.
|
||||
- `UNAUTHORIZED`: Session abgelaufen, Benutzer deaktiviert oder OIDC-Konfiguration falsch.
|
||||
- `CSRF_INVALID`: Schreibender Request ohne `X-CSRF-Token`; im Angular-Client erledigt das der Interceptor.
|
||||
- OIDC Callback schlaegt fehl: Redirect URI, Issuer, Client Secret und erlaubte Algorithmen pruefen.
|
||||
32
docs/nginx-example.conf
Normal file
32
docs/nginx-example.conf
Normal file
@@ -0,0 +1,32 @@
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
server_name app.example.com;
|
||||
|
||||
ssl_certificate /etc/letsencrypt/live/app.example.com/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/app.example.com/privkey.pem;
|
||||
|
||||
client_max_body_size 1m;
|
||||
proxy_connect_timeout 5s;
|
||||
proxy_send_timeout 60s;
|
||||
proxy_read_timeout 60s;
|
||||
|
||||
add_header X-Content-Type-Options nosniff always;
|
||||
add_header Referrer-Policy strict-origin-when-cross-origin always;
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto https;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
proxy_set_header X-Request-ID $request_id;
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name app.example.com;
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
146
docs/notifications.md
Normal file
146
docs/notifications.md
Normal 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.
|
||||
135
docs/security.md
Normal file
135
docs/security.md
Normal file
@@ -0,0 +1,135 @@
|
||||
# Security-Modell
|
||||
|
||||
Das Boilerplate trennt Browser, Backend und Identity Provider strikt. Der Browser
|
||||
bekommt keine OIDC-Tokens. Das Backend ist fuer Authentifizierung,
|
||||
Autorisierung, CSRF und Session-Verwaltung verbindlich.
|
||||
|
||||
## OIDC
|
||||
|
||||
Der Login nutzt Authorization Code Flow mit PKCE:
|
||||
|
||||
1. Browser ruft `/api/auth/login` auf.
|
||||
2. Backend erzeugt `state`, `nonce`, `code_verifier` und leitet zum IdP weiter.
|
||||
3. IdP ruft `/api/auth/callback` mit `code` und `state` auf.
|
||||
4. Backend tauscht den Code gegen Tokens.
|
||||
5. Backend validiert ID Token, Issuer, Audience, Nonce und Algorithmus.
|
||||
6. Backend laedt optional UserInfo.
|
||||
7. Backend legt oder aktualisiert den lokalen Benutzer.
|
||||
8. Backend erzeugt eine serverseitige Session.
|
||||
|
||||
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
|
||||
Benutzerdaten, sondern nur eine signierte Session-ID.
|
||||
|
||||
Gespeichert werden unter anderem:
|
||||
|
||||
- verschluesselte OIDC-Tokens
|
||||
- Hash des CSRF-Tokens
|
||||
- Ablaufzeiten fuer Idle Timeout und absolutes Timeout
|
||||
- User-Agent und IP fuer Anzeige und Audit-Kontext
|
||||
- Revocation-Zeitpunkt
|
||||
|
||||
Die Token-Verschluesselung nutzt `SESSION_ENCRYPTION_KEY`. Cookie-Signaturen
|
||||
nutzen `SESSION_SECRET`. Beide Werte muessen in Produktion echte Secrets sein.
|
||||
|
||||
## CSRF
|
||||
|
||||
Schreibende Methoden `POST`, `PUT`, `PATCH` und `DELETE` brauchen ein gueltiges
|
||||
CSRF-Token. Das Frontend liest das CSRF-Cookie und sendet es als
|
||||
`X-CSRF-Token`. Das Backend vergleicht nur den Hash gegen die Session.
|
||||
|
||||
Public Routes sind vom CSRF-Guard ausgenommen, sofern sie mit `@Public()`
|
||||
markiert sind.
|
||||
|
||||
## Rollen und Permissions
|
||||
|
||||
Permissions werden in `apps/backend/src/roles/permissions.ts` definiert. Rollen
|
||||
sind Datenbankdaten und referenzieren diese Permissions. Benutzer haben keine
|
||||
direkten Permissions.
|
||||
|
||||
Backend-Controller schuetzen Endpunkte mit:
|
||||
|
||||
```ts
|
||||
@RequirePermissions(Permission.ItemsRead)
|
||||
```
|
||||
|
||||
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
|
||||
Sessions deaktivierter Benutzer werden beim Session-Resolve unbrauchbar gemacht.
|
||||
|
||||
## Cookies und Proxy
|
||||
|
||||
In Produktion sollte TLS vor der Anwendung terminiert werden, zum Beispiel mit
|
||||
Nginx. Setze `TRUST_PROXY=true`, wenn die Anwendung hinter einem vertrauenswuerdigen
|
||||
Reverse Proxy laeuft und korrekte Forwarded Header benoetigt.
|
||||
|
||||
`APP_BASE_URL` muss die externe URL der Anwendung enthalten, weil daraus die OIDC
|
||||
Callback URL gebaut wird.
|
||||
|
||||
## Logging und Redaction
|
||||
|
||||
Das Backend redigiert sensible Werte in Standard-HTTP-Logs:
|
||||
|
||||
- `Authorization`
|
||||
- `Cookie`
|
||||
- `X-CSRF-Token`
|
||||
- `Set-Cookie`
|
||||
- Access-, Refresh- und ID-Tokens
|
||||
|
||||
Neue Logs duerfen keine Secrets, Tokens, Session-IDs oder vollstaendige Cookies
|
||||
enthalten. Fuer Auditing sollten fachliche IDs und Request IDs verwendet werden.
|
||||
|
||||
## Swagger
|
||||
|
||||
`SWAGGER_ENABLED` ist in Produktion standardmaessig `false`. Wenn Swagger in
|
||||
Produktion aktiviert wird, sollte der Zugriff ueber Netzwerkregeln oder Reverse
|
||||
Proxy zusaetzlich eingeschraenkt werden.
|
||||
|
||||
## Rate Limiting
|
||||
|
||||
Das konfigurierte Rate Limiting ist fuer eine einzelne Containerinstanz ohne
|
||||
Redis ausgelegt. Die Zaehler liegen im Prozessspeicher. Bei horizontaler
|
||||
Skalierung muss ein gemeinsamer Store eingefuehrt oder der Schutz an den Reverse
|
||||
Proxy verlagert werden.
|
||||
|
||||
Es gibt zwei fruehe Ebenen:
|
||||
|
||||
- Globales IP-Limit fuer alle Requests, bevor Guards und Controller laufen.
|
||||
- Strengeres IP-Limit fuer sensible Endpunkte, ebenfalls bevor Session-Resolve
|
||||
und fachliche Datenbankzugriffe laufen.
|
||||
|
||||
Sensible Endpunkte werden mit `@SensitiveRateLimit()` markiert. Dazu gehoeren
|
||||
Login, OIDC Callback, Logout, Session-Revoke sowie Benutzer- und
|
||||
Rollenverwaltung. Bei Ueberschreitung antwortet die API mit HTTP 429,
|
||||
`RATE_LIMIT_EXCEEDED`, `Retry-After` und `X-RateLimit-*` Headern.
|
||||
|
||||
Ein zusaetzliches Benutzer-Limit waere technisch moeglich, wuerde in diesem
|
||||
Projekt aber erst nach dem serverseitigen Session-Resolve greifen. Fuer die
|
||||
geforderte fruehe Lastreduktion ist deshalb das globale IP-Limit die verbindliche
|
||||
Schutzschicht.
|
||||
97
docs/using-as-template.md
Normal file
97
docs/using-as-template.md
Normal file
@@ -0,0 +1,97 @@
|
||||
# Als Projektvorlage Verwenden
|
||||
|
||||
Dieses Boilerplate ist bewusst nah an einer realen internen Business-Anwendung.
|
||||
Beim Start eines neuen Projekts solltest du zuerst Namen, Branding,
|
||||
Runtime-Konfiguration und Demo-Fachlichkeit ersetzen, aber die Sicherheits- und
|
||||
Betriebsentscheidungen beibehalten.
|
||||
|
||||
## Empfohlener Start
|
||||
|
||||
1. Repository kopieren oder als Template verwenden.
|
||||
2. Paketnamen und Beschreibungen in `package.json`, `apps/*/package.json` und
|
||||
`packages/api-client/package.json` anpassen.
|
||||
3. UI-Name `Business App` in `apps/frontend/src/app/layout/app-shell.ts` ersetzen.
|
||||
4. Swagger-Titel und Beschreibung in `apps/backend/src/main.ts` ersetzen.
|
||||
5. Docker-Image-Namen in README, CI und Compose auf den Projektnamen aendern.
|
||||
6. `.env.example` auf die Zielumgebung zuschneiden, aber keine Secrets eintragen.
|
||||
7. Lizenz, Repository-URL, Ownership und Open-Source-Metadaten bewusst setzen.
|
||||
|
||||
## Was du behalten solltest
|
||||
|
||||
- Backend-for-Frontend Authentifizierung: OIDC-Tokens bleiben im Backend.
|
||||
- Server-seitige Sessions in MySQL.
|
||||
- CSRF-Schutz fuer alle schreibenden Requests.
|
||||
- TypeORM-Migrationen statt `synchronize`.
|
||||
- Controller delegieren an Services; Datenbankzugriff liegt in Repositories oder
|
||||
klar benannten Persistence-Services.
|
||||
- Permissions sind Code, Rollen sind Daten.
|
||||
- Keine UI-Library als versteckte Projektbindung.
|
||||
- Striktes TypeScript ohne `any`.
|
||||
|
||||
## Demo-Fachlichkeit entfernen oder ersetzen
|
||||
|
||||
Das Modul `items` ist ein Beispiel fuer ein einfaches fachliches CRUD-Modul mit
|
||||
optimistischer Versionierung und Soft Delete. Fuer ein neues Projekt gibt es zwei
|
||||
saubere Optionen:
|
||||
|
||||
- Behalten und in das erste echte Fachmodul umbenennen.
|
||||
- Entfernen und anhand der Struktur ein neues Modul erstellen.
|
||||
|
||||
Wenn du `items` entfernst, pruefe mindestens:
|
||||
|
||||
- Backend-Import in `apps/backend/src/app.module.ts`
|
||||
- Entity-Export in `apps/backend/src/database/entities.ts`
|
||||
- Initiale Migration und neue Migration fuer Schema-Aenderungen
|
||||
- Permissions in `apps/backend/src/roles/permissions.ts`
|
||||
- Routen in `apps/frontend/src/app/app.routes.ts`
|
||||
- Navigation in `apps/frontend/src/app/layout/app-shell.ts`
|
||||
- API-Client-Generator in `scripts/generate-api-client.mjs`
|
||||
- Frontend-Pages und Tests unter `apps/frontend/src/app/features/items`
|
||||
|
||||
## Ein neues Feature anlegen
|
||||
|
||||
Backend:
|
||||
|
||||
1. Feature-Ordner unter `apps/backend/src/<feature>` anlegen.
|
||||
2. Entity, DTOs, Repository, Service, Controller und Module erstellen.
|
||||
3. Controller nur mit Services verdrahten.
|
||||
4. Datenbankzugriff im Repository kapseln.
|
||||
5. Permissions in `roles/permissions.ts` ergaenzen.
|
||||
6. Controller-Methoden mit `@RequirePermissions(...)` schuetzen.
|
||||
7. Entity in `database/entities.ts` aufnehmen.
|
||||
8. Migration erzeugen und pruefen.
|
||||
9. Verhaltenstests fuer Service oder Controller schreiben.
|
||||
|
||||
Frontend:
|
||||
|
||||
1. Page unter `apps/frontend/src/app/features/<feature>` erstellen.
|
||||
2. Route in `app.routes.ts` anlegen.
|
||||
3. Navigation in `app-shell.ts` ergaenzen.
|
||||
4. Permission im Route-`data` und in der Navigation konsistent setzen.
|
||||
5. API-Client ueber `packages/api-client` verwenden.
|
||||
6. Verhaltenstests fuer relevante UI-Logik schreiben.
|
||||
|
||||
API-Client:
|
||||
|
||||
Aktuell schreibt `scripts/generate-api-client.mjs` den Client reproduzierbar aus
|
||||
einer gepflegten Vorlage. Wenn die API waechst, muss der Generator erweitert oder
|
||||
durch eine echte OpenAPI-Codegenerierung ersetzt werden. Der Client selbst ist
|
||||
generierter Code und sollte nicht manuell editiert werden.
|
||||
|
||||
## Open-Source-Veroeffentlichung
|
||||
|
||||
Vor einer Veroeffentlichung als Open-Source-Projekt:
|
||||
|
||||
- `license` in `package.json` bewusst setzen.
|
||||
- `LICENSE` und optional `NOTICE` hinzufuegen.
|
||||
- Namen, Screenshots, Domaenen, Beispiel-Registry und interne Hinweise entfernen.
|
||||
- Beispiel-Secrets in `.env.example` nur als Platzhalter belassen.
|
||||
- Pruefen, ob alle Dependencies und generierten Artefakte lizenzkompatibel sind.
|
||||
- Sicherheitsmodell in README und `docs/security.md` aktuell halten.
|
||||
|
||||
## Projektgrenzen
|
||||
|
||||
Dieses Boilerplate ist fuer eine einzelne deploybare Business-Anwendung gebaut.
|
||||
Es ist kein Microservice-Framework, kein Nx-Workspace und keine Multi-Tenant
|
||||
Plattform. Wenn ein Projekt diese Grenzen braucht, sollte die Architektur zuerst
|
||||
bewusst erweitert werden, statt sie indirekt ueber Feature-Code einzuschleppen.
|
||||
Reference in New Issue
Block a user