initial
This commit is contained in:
120
docs/architecture.md
Normal file
120
docs/architecture.md
Normal file
@@ -0,0 +1,120 @@
|
||||
# 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
|
||||
- `items`: Beispiel-Fachmodul
|
||||
- `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
|
||||
|
||||
Der erste erfolgreich angemeldete Benutzer wird automatisch Admin. Danach
|
||||
erhalten neue Benutzer initial die Rolle `user`.
|
||||
111
docs/configuration.md
Normal file
111
docs/configuration.md
Normal file
@@ -0,0 +1,111 @@
|
||||
# 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_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
|
||||
```
|
||||
|
||||
## 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_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.
|
||||
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.
|
||||
88
docs/getting-started.md
Normal file
88
docs/getting-started.md
Normal file
@@ -0,0 +1,88 @@
|
||||
# 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
|
||||
```
|
||||
|
||||
## 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;
|
||||
}
|
||||
117
docs/security.md
Normal file
117
docs/security.md
Normal file
@@ -0,0 +1,117 @@
|
||||
# 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.
|
||||
|
||||
## 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.
|
||||
|
||||
## 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