340 lines
13 KiB
Markdown
340 lines
13 KiB
Markdown
# Infrastruktur-Planung — Vollversammlung
|
||
|
||
## Inhaltsverzeichnis
|
||
|
||
1. [docker-compose.yaml – Services](#1-dockercomposeyaml--services)
|
||
2. [Netzwerk & Volumes](#2-netzwerk--volumes)
|
||
3. [api/ Verzeichnis – Vorbereitungen](#3-apiverzeichnis--vorbereitungen)
|
||
4. [frontend/ Verzeichnis – Vorbereitungen](#4-frontend-verzeichnis--vorbereitungen)
|
||
5. [Multi-Tenant Scoping – Infrastruktur-Ansatz](#6-multi-tenant-scoping--infrastruktur-ansatz)
|
||
6. [OAuth (Vonova) – Infrastruktur](#7-oauth-vonova--konfiguration)
|
||
7. [Lokaler Start](#8-lokaler-start)
|
||
8. [Health Checks](#9-health-checks)
|
||
9. [.env.example Konfigurationen](#10-envexample-konfigurationen)
|
||
10. [Checkliste – Nächste Schritte](#11-checkliste--nächste-schritte-zur-implementierung)
|
||
|
||
---
|
||
|
||
## 1. docker-compose.yaml – Services
|
||
|
||
Sechs Services, alle über `vollversammlung_net` vernetzt:
|
||
|
||
| Service | Image | Externer Port | Internes Ziel | Rolle |
|
||
|---------|-------|---------------|--------------|-------|
|
||
| **postgres-db** | postgres:16-alpine | kein (intern) | :5432 | PostgreSQL-Datenbank, MandantID-gescoped |
|
||
| **symfony-fpm** | php:8.3-fpm-alpine (custom) | kein (über nginx) | :9000 | PHP-FPM Backend-Runtime |
|
||
| **nginx** | nginx:alpine | **8080** → :80 | Reverse Proxy, routet /api/* zu symfony-fpm, statisch zu frontend |
|
||
| **mercure-hub** | dunglas/mercure | **3000** → :80 | Event-Streaming für Logging in Echtzeit |
|
||
| **mailpit** | axllent/mailpit | **8025** (UI), intern :1025 (SMTP) | E-Mail Testing |
|
||
| **vite-dev** | node:22-alpine | **5173** → :5173 | React Dev Server mit Hot Module Replacement |
|
||
|
||
---
|
||
|
||
## 2. Netzwerk & Volumes
|
||
|
||
### Netzwerk
|
||
|
||
```yaml
|
||
networks:
|
||
vollversammlung_net:
|
||
driver: bridge
|
||
```
|
||
|
||
Alle Services joined auf `vollversammlung_net`. Nur nginx, mercure-hub, vite-dev und mailpit haben externe Port-Mappings. postgres-db und symfony-fpm sind ausschließlich intern erreichbar.
|
||
|
||
### Volumes
|
||
|
||
**Named Volumes:**
|
||
|
||
| Name | Mount-Punkt (symfony-fpm) / Ziel | Zweck |
|
||
|------|-----------------------------------|-------|
|
||
| `pg_data` | `/var/lib/postgresql/data` (post-db) | Persistente PostgreSQL-Daten |
|
||
| `symfony_var` | `/app/var/` | Symfony cache, logs |
|
||
|
||
**Bind-mounts (Development only):**
|
||
|
||
| Host-Pfad | Container | Mount-Pfad | Zweck |
|
||
|-----------|-----------|-----------|-------|
|
||
| `./api/src` | symfony-fpm | `/app/src` | Live-Coding PHP-Code |
|
||
| `./frontend/src` | vite-dev | `/app/src` | Live-Coding React-Code |
|
||
|
||
---
|
||
|
||
## 3. api/ Verzeichnis – Vorbereitungen
|
||
|
||
### Dateistruktur
|
||
|
||
```
|
||
api/
|
||
├── .env.example (Commit-Sicherheit, keine Secrets)
|
||
├── .env.local (gitignored - lokale Secrets)
|
||
├── .dockerignore (Docker Build-Kontext filtering)
|
||
├── composer.json (Symfony Flex setup)
|
||
├── Dockerfile.dev (php:8.3-fpm-alpine Base)
|
||
├── Dockerfile.prod (Multi-stage build für Produktion)
|
||
├── docker-entrypoint.sh (Healthcheck + Init-Script)
|
||
├── public/
|
||
│ └── index.php (Symfony Entry Point über FPM)
|
||
├── config/
|
||
│ ├── packages/ (framework.yaml, mercure.yaml, doctrine.yaml etc.)
|
||
│ └── routes/ (API Routes YAML)
|
||
├── migrations/ (Doctrine Migration-Dateien)
|
||
├── src/
|
||
│ ├── Controller/ (Manuelle Controller – kein API Platform!)
|
||
│ ├── Entity/ (Domain Entities mit MandantID-Eigenschaft)
|
||
│ ├── Repository/ (Doctrine Repositories mit Tenant-Scoping)
|
||
│ ├── Service/ (Business Logic Services)
|
||
│ ├── Middleware/ (MandantId-Middleware + OAuth-Processor)
|
||
│ └── Kernel.php
|
||
├── templates/ (Twig Templates)
|
||
├── var/ (cache, logs – via Volume gemountet)
|
||
├── var/cache → symfony_var Volume
|
||
├── var/log → symfony_var Volume
|
||
└── bin/console (Symfony Command-Wrapper)
|
||
```
|
||
|
||
### PHP-Erweiterungen (Dockerfile.dev)
|
||
|
||
- `pdo_pgsql` – Doctrine DBAL-Konnektivität
|
||
- `gd` – Bildverarbeitung
|
||
- `intl` – Internationalisierung
|
||
- `mbregex` – Multibyte-Regex
|
||
|
||
---
|
||
|
||
## 4. frontend/ Verzeichnis – Vorbereitungen
|
||
|
||
### Dateistruktur
|
||
|
||
```
|
||
frontend/
|
||
├── .env.example (Commit-Sicherheit)
|
||
├── .env.local (gitignored - lokale Secrets)
|
||
├── .dockerignore (Build-Kontext filtering)
|
||
├── package.json (React + Vite + TypeScript Dependencies)
|
||
├── vite.config.ts (Dev Server + Proxy zu nginx/symfony-fpm)
|
||
├── tsconfig.json (TypeScript Konfiguration)
|
||
├── Dockerfile.dev (node:22-alpine Base)
|
||
├── public/ (Statische Assets – favicon, manifest etc.)
|
||
└── src/
|
||
├── components/ (UI-Komponenten – z.B. Anwesenheitsliste, Statistiken)
|
||
├── pages/ (Seiten – Dashboard, Veranstaltungsdetail etc.)
|
||
├── stores/ (Zustandsmanagement – KEINE OAuth-Tokens!)
|
||
├── utils/ (HTTP-Client, Auth-Helfer etc.)
|
||
├── App.tsx (Root-Komponente)
|
||
└── main.tsx (Entry Point)
|
||
```
|
||
|
||
### Vite Proxy-Konfiguration (vite.config.ts)
|
||
|
||
- `/api/*` → `http://nginx:80/api/*` (Reverse-Proxy auf Symfony)
|
||
- `/.well-known/mercure` → `http://mercure-hub:3000/.well-known/mercure`
|
||
|
||
Damit kommuniziert der Vite Dev Server (`localhost:5173`) nahtlos mit Backend-Services.
|
||
|
||
---
|
||
|
||
## 5. Multi-Tenant Scoping – Infrastruktur-Ansatz
|
||
|
||
**Kein Schema-per-Tenant.** Stattdessen Row-Level-Scoping über `mandant_id` in ALLEN Tabellen.
|
||
|
||
| Schicht | Mechanismus |
|
||
|---------|-------------|
|
||
| **Client** | MandantID per HTTP Header `X-Mandant-ID` an API senden. Token NUR in HttpOnly, Secure, SameSite=lax Cookies. |
|
||
| **Nginx** | Forwarded `X-Mandant-ID` Header via `proxy_set_header` an symfony-fpm weiter. |
|
||
| **Symfony Middleware** | Extrahiert MandantID aus Cookie/Header → setzt als RequestAttribute. |
|
||
| **Doctrine Layer** | Alle Entities erben Base-Entity mit `mandant_id`. Repositories scopen automatisch `WHERE mandant_id = :mandantId`. |
|
||
|
||
### Security-Garantie
|
||
|
||
- Tenant-überschreitende Operationen technisch unmöglich ohne explizites `WithoutScope`-Attribut (standardmäßig nicht gewährt).
|
||
- Keine email/benutzername-Felder in Personen-Entity – Identität stammt ausschließlich aus OAuth.
|
||
|
||
---
|
||
|
||
## 6. OAuth (Vonova) – Konfiguration
|
||
|
||
### Backend Environment Variables (.env.local api/)
|
||
|
||
```
|
||
OAUTH_PROVIDER_NAME=vonova
|
||
OAUTH_AUTHORIZATION_ENDPOINT=https://oauth.vonova.de/oauth/authorize
|
||
OAUTH_TOKEN_ENDPOINT=https://oauth.vonova.de/oauth/token
|
||
OAUTH_CLIENT_ID=<from Vonova admin>
|
||
OAUTH_CLIENT_SECRET=<from Vonova admin>
|
||
OAUTH_REDIRECT_URI=http://localhost:8080/api/oauth/callback
|
||
OAUTH_SCOPES=read profile mandant_id
|
||
```
|
||
|
||
### Frontend Environment Variables (frontend/.env.example)
|
||
|
||
```
|
||
VITE_OAUTH_AUTHORIZATION_ENDPOINT=https://oauth.vonova.de/oauth/authorize
|
||
VITE_OAUTH_TOKEN_ENDPOINT=https://oauth.vonova.de/oauth/token
|
||
```
|
||
|
||
### OAuth-Flow (Kurzbeschreibung)
|
||
|
||
1. Frontend leitet User zu Vonova Authorization Endpoint weiter.
|
||
2. Vonova authentifiziert User, gibt Authorisierungscode an `OAUTH_REDIRECT_URI` zurück.
|
||
3. Backend tauscht Code gegen Access-Token am Token Endpoint ein.
|
||
4. Backend extrahiert MandantID und Profil-Informationen aus dem Token-Payload.
|
||
5. Backend generiert Session-Cookie (HttpOnly, Secure, SameSite=lax).
|
||
6. Folge-Requests enthalten Cookie automatisch → authentifizierte Api-Zugriffe.
|
||
|
||
**Wichtig:** Tokens werden NIEMALS auf dem Client gespeichert (kein localStorage/sessionStorage).
|
||
|
||
---
|
||
|
||
## 7. Lokaler Start
|
||
|
||
### Setup
|
||
|
||
```bash
|
||
# Projekt rootVerzeichnis:
|
||
|
||
# Env-Templates kopieren (keine Secrets generieren – Werte aus .env.example übernehmen):
|
||
cp api/.env.example api/.env.local
|
||
cp frontend/.env.example frontend/.env.local
|
||
|
||
# APP_SECRET und JWT-Keys in .env.local ergänzen:
|
||
docker compose exec symfony-fpm php bin/console security:encode-password
|
||
# → Output als APP_SECRET eintragen
|
||
|
||
# Mercure-JWT-Keys manuell generieren (Mindestlänge 32 Zeichen, kryptografisch sicher):
|
||
# z.B.: openssl rand -base64 64
|
||
```
|
||
|
||
### Services starten
|
||
|
||
```bash
|
||
# Alle Services im Hintergrund starten:
|
||
docker compose up -d
|
||
|
||
# Database-Migrations ausführen (erster Start):
|
||
docker compose exec symfony-fpm php bin/console doctrine:migrations:migrate --no-interaction
|
||
```
|
||
|
||
### Verfügbare Endpunkte
|
||
|
||
| URL | Service |
|
||
|-----|---------|
|
||
| `http://localhost:8080/` | Frontend + API (via nginx) |
|
||
| `http://localhost:3000/` | Mercure Hub Debug Interface |
|
||
| `http://localhost:5173/` | Vite Dev Server (Hot Reload) |
|
||
| `http://localhost:8025/` | Mailpit E-Mail Testing UI |
|
||
|
||
---
|
||
|
||
## 8. Health Checks
|
||
|
||
Jeder kritische Service verfügt über einen `healthcheck` in docker-compose.yaml:
|
||
|
||
| Service | Check-Methode | Command |
|
||
|---------|---------------|---------|
|
||
| **postgres-db** | `pg_isready` | `pg_isready -U vv_admin -d vollversammlung` |
|
||
| **mercure-hub** | HTTP-Well-Known-Path | `curl -sf http://localhost/.well-known/mercure` |
|
||
| **symfony-fpm** | API Health Endpoint *(vom Backend-Coder zu implementieren)* | `curl -sf http://localhost:8000/api/health` |
|
||
| **nginx** | Proxy-Pass zu symfony-fpm health | `curl -sf http://localhost:8080/api/health` |
|
||
|
||
---
|
||
|
||
## 9. .env.example Konfigurationen
|
||
|
||
### api/.env.example
|
||
|
||
```ini
|
||
# =========================================
|
||
# Vollversammlung – Backend Environment
|
||
# Kopieren nach .env.local und anpassen!
|
||
# =========================================
|
||
|
||
APP_ENV=dev
|
||
APP_DEBUG=1
|
||
APP_SECRET=<symfony secret:generate oder security:encode-password>
|
||
|
||
DATABASE_URL="postgresql://vv_admin:<password>@postgres-db:5432/vollversammlung?serverVersion=16&charset=utf8mb4"
|
||
|
||
MERCURE_PUBLISHER_JWT_KEY=<openssl rand -base64 64>
|
||
MERCURE_SUBSCRIBER_JWT_KEY=<openssl rand -base64 64>
|
||
MERCURE_URL=http://mercure-hub/.well-known/mercure
|
||
MERCURE_PUBLIC_URL=http://localhost:3000/.well-known/mercure
|
||
|
||
MAILER_DSN=smtp://mailpit:1025
|
||
|
||
# =========================================
|
||
# Vonova OAuth
|
||
# =========================================
|
||
OAUTH_PROVIDER_NAME=vonova
|
||
OAUTH_AUTHORIZATION_ENDPOINT=https://oauth.vonova.de/oauth/authorize
|
||
OAUTH_TOKEN_ENDPOINT=https://oauth.vonova.de/oauth/token
|
||
OAUTH_CLIENT_ID=<from vonova admin>
|
||
OAUTH_CLIENT_SECRET=<from vonova admin>
|
||
OAUTH_REDIRECT_URI=http://localhost:8080/api/oauth/callback
|
||
OAUTH_SCOPES=read profile mandant_id
|
||
```
|
||
|
||
### frontend/.env.example
|
||
|
||
```ini
|
||
# =========================================
|
||
# Vollversammlung – Frontend Environment
|
||
# Kopieren nach .env.local und anpassen!
|
||
# =========================================
|
||
|
||
VITE_API_URL=http://localhost:8080/api
|
||
VITE_MERCURE_URL=http://localhost:3000/.well-known/mercure
|
||
VITE_OAUTH_AUTHORIZATION_ENDPOINT=https://oauth.vonova.de/oauth/authorize
|
||
VITE_OAUTH_TOKEN_ENDPOINT=https://oauth.vonova.de/oauth/token
|
||
```
|
||
|
||
---
|
||
|
||
## 10. Multi-Environment Konfiguration
|
||
|
||
| Variable | dev (.env.local) | uat/staging (.env.staging) | prod (.env.prod) |
|
||
|----------|------------------|----------------------------|-------------------|
|
||
| `APP_ENV` | `dev` | `uat` | `prod` |
|
||
| `APP_DEBUG` | `1` | `0` | `0` |
|
||
| `CORS_ALLOW_ORIGIN` | `http://localhost:5173` | `https://uat.company.com` | `https://www.company.com` |
|
||
| `MAILER_DSN` | `smtp://mailpit:1025` | prod SMTP credentials | prod SMTP credentials |
|
||
| `DATABASE_URL` | localhost via docker | externe PG-Instanz | externe PG-Instanz |
|
||
| `MERCURE_PUBLISHER_JWT_KEY` | dev-key (openssl rand) | Vault-sekret | Vault/AWS Secrets Manager |
|
||
| `TRUSTED_HOSTS` | `^localhost$` | `^uat\.example\.com$` | `^www\.example\.com$` |
|
||
| `TRUSTED_PROXIES` | – | `127.0.0.1,10.0.0.0/8` | Load Balancer IPs |
|
||
|
||
---
|
||
|
||
## 11. Checkliste — Nächste Schritte zur Implementierung
|
||
|
||
### Infrastruktur (DevOps-Coder)
|
||
- [ ] `docker-compose.yaml` mit allen 6 Services schreiben
|
||
- [ ] Networks & Volumes definieren
|
||
- [ ] Health-Checks pro Service implementieren
|
||
- [ ] nginx Konfiguration für Proxy-Routing erstellen
|
||
- [ ] `.env.example` in api/ und frontend/ anlegen
|
||
- [ ] Dockerfiles (dev + prod) pro Service erstellen
|
||
|
||
### Backend (Backend-Coder)
|
||
- [ ] Symfony-Projektstruktur in `api/` aufsetzen
|
||
- [ ] `composer.json` mit Symfony Flex konfigurieren
|
||
- [ ] Entity-Base für Multi-Tenant Scoping erstellen (`mandant_id` column)
|
||
- [ ] Doctrine Repositories mit automatischem Tenant-Scoping implementieren
|
||
- [ ] MandantId-Middleware schreiben (Cookie/Header → RequestAttribute)
|
||
- [ ] Vonova OAuth-Flow implementieren (Authorization Code Grant + PKCE)
|
||
- [ ] Session-Cookie Generation nach OAuth-Flow (HttpOnly, Secure, SameSite=lax)
|
||
- [ ] `/api/health` Endpoint erstellen
|
||
- [ ] Migrationen für alle Templates schreiben
|
||
|
||
### Frontend (Frontend-Coder)
|
||
- [ ] React/Vite/TypeScript Setup in `frontend/`
|
||
- [ ] OAuth Login-Flow im Browser implementieren (Authorization Code + PKCE mit redirect)
|
||
- [ ] Auth-Stores ohne token storage (nur Cookie-basiert)
|
||
- [ ] HTTP-Client mit MandantID-Header-Injection schreiben
|
||
- [ ] Mercure-Client für Event-Subscription einrichten
|
||
- [ ] Key-Pages: Dashboard, Veranstaltungsdetail, Anwesenheitsübersicht
|
||
|
||
---
|
||
|
||
*Stand: Juni 2026 — Infrastruktur-Planung Vollversammlung*
|