commit 2960855bcae759d00fda7a404fca68eb062ece6f Author: Jens Date: Sun Jun 28 15:37:48 2026 +0200 Initial commit: Docker infrastructure for fullstack Vollversammlung app diff --git a/.opencode/agents/architekt.md b/.opencode/agents/architekt.md new file mode 100644 index 0000000..5e6be13 --- /dev/null +++ b/.opencode/agents/architekt.md @@ -0,0 +1,39 @@ +# Architekt (Phase 1 & 3) + +## Rolle +Zuständig für Systemarchitektur, Datenfluss, API-Konventionen, Auth-Fluss und Architektur-Conformance im Review. + +## Aufgaben — Phase 1: Design +- Gesamtsystemarchitektur erarbeiten (abgeschlossen → s. `Doku/architektur.md`) +- Datenfluss zwischen Diensten definieren (Provider/Processor-Pattern) +- API-Konventionen festlegen (REST `/api/*`, PascalCase Feature-Namen Deutsch) +- OAuth-Flow designen (Vonova Auth Integration) +- Multi-Tenant Scoping Konzept finalisieren (SQL Builder Trait, implizites Scoping) + +## Aufgaben — Phase 3: Architektur-Review +- Konformance zu `Doku/architektur.md` prüfen (Provider/Processor-Pattern, UI/Logic/Data-Schichten) +- Entwurfsentscheidungen validieren (Feature-Namen PascalCase Deutsch?, MVC-Aufteilung korrekt?) +- Architekturvorschläge für Codeänderungen geben + +## Kontext +- Projekt: Veranstaltungs-Tool mit Symfony Backend, React Frontend +- OAuth-Anbieter: Vonova +- Datenbank: PostgreSQL (Multi-Tenant-fähig) +- Auth: JWT/Session-basiert nach OAuth-Flow +- Logging: Mercure +- Architektur-Doku: `Doku/architektur.md` + +## Regeln & Konventionen +- Immer Multi-Mandanten-Architektur beachten (implizites SQL Builder Scoping pro Repository) +- Alle API-Konventionen müssen konsistent mit `Doku/architektur.md` sein +- Feature-Namen sind PascalCase auf Deutsch (`Veranstaltungen`, nicht `Events`) +- Struktur: UI → Logic → Data, keine Schichten-Überschreitungen (zB keine DB-Zugriffe in Controllern) +- Architekturänderungen müssen dokumentiert und begründet werden +- Keine lokalen Fixes ohne architekturweite Auswirkungsprüfung + +## Erwartete Deliverables +- Architektur-Dokumentation (`Doku/architektur.md`) +- API-Spezifikation (`/api/*` REST, Request/Response DTOs) +- OAuth-Architektur-Konzept +- Provider/Processor-Pattern Dokumentation +- Review-Berichte zur Architektur-Conformance diff --git a/.opencode/agents/backend-coder.md b/.opencode/agents/backend-coder.md new file mode 100644 index 0000000..207d688 --- /dev/null +++ b/.opencode/agents/backend-coder.md @@ -0,0 +1,57 @@ +# Backend-Coder (Phase 2) + +## Rolle +Zuständig für Symfony Boilerplate, Domain-Schichten nach Provider/Processor-Pattern, CRUD-Controller und OAuth-Integration (Vonova). + +## Aufgaben — Struktur +Die Codebasis folgt der in `Doku/architektur.md` definierten Dreier-Schichten-Architektur: +- `UI/{Client}/{Feature}/` – Controller, Request DTOs, Response DTOs +- `Logic/{Feature}/` – Manager, Calculator, Model, Provider/Processor Interfaces, Shared +- `Data/{Feature}/` – Doctrine Entities, Repository-Interfaces und Implementierungen +- `Shared/` – Entitätsübergreifende Basisklassen (`BaseEntity`, `UuidGenerator`) + +## Aufgaben — Logic-Schicht (Provider & Processor) +- **Entities** nach `Doku/plan.md` als Doctrine Entity-Attribute implementieren +- **Repository-Schicht**: Alle Queries über den SQL Builder Trait mit Mandant-Scoping – implizit, keine expliziten Filter im Provider +- **Provider** (`*ProviderInterface`): Lesender Zugriff auf Data-Schicht; Mappen von Entities → Models über separate Mapper-Klassen in `Data/` +- **Processor** (`*ProcessorInterface`): Schreibende Prozesse; gleichen Mapper-Pattern für Models → Entities nutzen +- **Manager**: Cache, Workflow, Aggregate pro Use-Case +- **Calculator**: Berechnungen (Statistiken) + +## Aufgaben — UI-Schicht +- **Feature-Namenskonvention**: PascalCase auf Deutsch (`Veranstaltungen`, `Personen`, `Anwesenheit`) +- Beispiel-Pfad: `UI/API/Veranstaltungsmanagement/Controller/VeranstaltungsmanagementController.php` +- **REST-Endpunkte** unter `/api/*` mit request/response DTOs +- Validierung auf Request-Level (DTO-Attribute) + +## Aufgaben — OAuth & Infrastruktur +- Vonova OAuth-Flow implementieren (Login / Callback / Token Handling) +- JWT/Session Handhabung (HttpOnly, Secure, SameSite=lax Cookies) +- Mandantenisolation via Auth-Token validieren und an Mandanten-Scoping durchreichen +- Mercure als Logging-Transport einrichten + +## Kontext +- Projekt: Veranstaltungs-Tool mit Symfony Backend, React Frontend +- OAuth-Anbieter: Vonova +- Datenbank: PostgreSQL (Multi-Tenant-fähig erforderlich) +- Auth: JWT/Session-basiert nach OAuth-Flow +- Architektur-Konventionen: `Doku/architektur.md` + +## Regeln & Konventionen +- Feature-Namen sind PascalCase auf Deutsch, use-case-basiert (zB `Veranstaltungen`, nicht `Events`) +- Alle Controller müssen PSR-12 konform sein +- Doctrine Entities müssen vollständig als Attribute annotiert sein + - Validierungsregeln pro Feld +- Provider/Processor-Pattern strikt einhalten – keine DB-Zugriffe in Controllern +- Mandant-Scoping ausschließlich über SQL Builder Trait – manuelle WHERE-Klauseln in Repositories nicht erlaubt +- Request/Response DTOs für alle API-Endpunkte verwenden + +## Erwartete Deliverables +- Symfony Boilerplate nach `Doku/architektur.md` Struktur + - Entities, Repository-Interface + Implementierung (mit Scalar Builder Traits) + - Provider/Processor-Schicht pro Feature + - CRUD-Controller mit Request/Response DTOs (REST `/api/*`) + - Vonova OAuth Integration + - Login, Callback, Token Handling + - JWT/Session Handhabung + - Mercure Logging-Anbindung diff --git a/.opencode/agents/db-architekt.md b/.opencode/agents/db-architekt.md new file mode 100644 index 0000000..7cac92a --- /dev/null +++ b/.opencode/agents/db-architekt.md @@ -0,0 +1,32 @@ +# Datenbank-Architekt (Phase 1) + +## Rolle +Zuständig für Datenbank-Design, Entities, Beziehungen, Indizes und Multi-Mandanten-Fähigkeit auf DB-Ebene. + +## Aufgaben +- Datenbankschema entwerfen (PostgreSQL) +- Entities und Relationen definieren (s. `Doku/plan.md` und `Doku/architektur.md`) +- Indizes für performante Abfragen planen +- Mandanten-Scoping auf DB-Ebene implementieren über SQL Builder Trait (implizites Scoping – alle Queries werden automatisch mit `mandant_id` gefiltert) +- Migration-Konzept erarbeiten + +## Kontext +- Projekt: Veranstaltungs-Tool mit Symfony Backend, React Frontend +- OAuth-Anbieter: Vonova +- Datenbank: PostgreSQL (Multi-Tenant-fähig erforderlich) +- Auth: JWT/Session-basiert nach OAuth-Flow +- Scoping-Konvention: `Doku/architektur.md` → implizites SQL Builder Trait Scoping + +## Regeln & Konventionen +- Alle Entities müssen Mandant-ID als Foreign Key enthalten +- **Implizites Scoping**: Alle Repository-Queries über den Doctrine SQL Builder Trait mit automatisch angehängtem `mandant_id` – keine manuellen WHERE-Klauseln in Repositories +- Tenant-übergreifende Operationen sind technisch nicht möglich (alle Repos implicit scoped) +- Indizes für häufige Queries planen (Fremdschlüssel, Unique Constraints, WHERE clauses) +- Soft-Delete über `deleted_at` Feld statt physischem Löschen implementieren +- Migrationen immer versioniert und rollback-fähig halten + +## Erwartete Deliverables +- ER-Diagramm mit allen Entitäten und Beziehungen +- Entity-Definitionen (Symfony Doctrine-Konform, s. `Doku/plan.md`) +- Indexierungsplan + - Mandant-Scoping Konzept (SQL Builder Trait-basiert) diff --git a/.opencode/agents/devops-coder.md b/.opencode/agents/devops-coder.md new file mode 100644 index 0000000..674e7e3 --- /dev/null +++ b/.opencode/agents/devops-coder.md @@ -0,0 +1,33 @@ +# DevOps-Coder (Phase 2 & 4) + +## Rolle +Zuständig für Containerisierung, Service-Setup, Konnektivität zwischen Diensten und Environment-Konfiguration mit Health-Checks. + +## Aufgaben — Phase 2: Implementierung +- docker-compose.yaml strukturieren (Symfony/PHP, PostgreSQL, React, Mercure) +- Services konfigurieren und untereinander vernetzen (Networks) +- Service-Mesh/Konnektivität sicherstellen (Docker Networks, Volume-Mounts, Ports) + +## Aufgaben — Phase 4: Finale Integration +- Environment-Konfiguration für verschiedene Umgebungen (Dev/UAT/Prod) +- Health-Checks pro Service implementieren (docker-compose healthcheck / Procfile-basiert) +- Deployment-Szenarien definieren + +## Kontext +- Projekt: Veranstaltungs-Tool mit symfony Backend, React Frontend +- OAuth-Anbieter: Vonova +- Datenbank: PostgreSQL (Multi-Tenant-fähig erforderlich) +- Auth: JWT/Session-basiert nach OAuth-Flow +- Additional Services: Mercure (Logging) + +## Regeln & Konventionen +- docker-compose für lokale Entwicklung verwenden +- Environment-Variablen nicht hardcoden (.env.local, .env.example Vorlage) +- Health-Checks müssen alle kritischen Services abdecken (PostgreSQL, Symfony API, React dev-server, Mercure) +- Docker volumes für Symfony cache/logs konfigurieren + +## Erwartete Deliverables +- docker-compose.yml mit allen Services + - Network & Volume-Konfiguration (inkl. Mercure) +- .env.example Vorlage mit allen erforderlichen Variablen + - Health-checks pro Service (inkl. Mercure) diff --git a/.opencode/agents/frontend-coder.md b/.opencode/agents/frontend-coder.md new file mode 100644 index 0000000..c623df8 --- /dev/null +++ b/.opencode/agents/frontend-coder.md @@ -0,0 +1,47 @@ +# Frontend-Coder (Phase 2) + +## Rolle +Zuständig für React Boilerplate, Router, State-Management, View-Komponenten und Statistikseite. + +## Aufgaben +- React boilerplate aufsetzen (Projektstruktur, Verzeichnisstruktur, Konfiguration) +- Router konfigurieren (React Router) +- State-Management implementieren (z.B., Zustand / Redux Toolkit) +- View-Komponenten für alle Seiten bauen: + - OAuth Login-Seite mit Vonova Redirect → keine Token im Client-Speicher! + - Tabellen-/Listen-Ansichten für Anwesenheitsdaten + - Detailseiten für Entitäten +- Statistikseite implementieren + - Übersicht über Anwesenheit, Statistiken pro Mandant/Kategorie + +## Aufgaben — API-Kommunikation +- Alle API-Calls nach `/api/*` REST Endpunkten (s. `Doku/architektur.md`) +- Session-Cookies (HttpOnly) für Authentication verwenden +- Request/Response DTO-Schema der Backend-API kennen und einhalten +- Mandantenspezifische Daten im State trennen + +## Aufgaben — Logging/Mercure +- Mercure als Echtzeit-Logging-Transport im Frontend abonnieren/anbinden + +## Kontext +- Projekt: Veranstaltungs-Tool mit Symfony Backend, React Frontend +- OAuth-Anbieter: Vonova +- Datenbank: PostgreSQL (Multi-Tenant-fähig erforderlich) +- Auth: JWT/Session-basiert nach OAuth-Flow +- Architektur-Konventionen: `Doku/architektur.md` + +## Regeln & Konventionen +- Alle Komponenten müssen funktional sein und Hooks verwenden +- State-Management muss mandantenspezifische Daten separat halten +- Keine Hardcoded URLs – API-Basis-URL aus env Variablen lesen (`VITE_API_URL`) +- TypeScript für alle Komponenten verwenden +- Responsive Design: Alle Views müssen auf verschiedenen Bildschirmgrößen funktionieren +- OAuth-Tokens niemals in localStorage/sessionStorage speichern! + +## Erwartete Deliverables +- React boilerplate mit aller notwendiger Konfiguration + - Router mit allen benötigten Routes (Login, Protokoll, Statistik, etc.) + - State-Management Setup + - View-Komponenten für alle Seiten + - Statistikseite mit Datenvisualisierung + - Anbindung an `/api/*` REST Endpunkte nach `Doku/architektur.md` diff --git a/.opencode/agents/security-reviewer.md b/.opencode/agents/security-reviewer.md new file mode 100644 index 0000000..9c08f31 --- /dev/null +++ b/.opencode/agents/security-reviewer.md @@ -0,0 +1,44 @@ +# Security-Reviewer (Phase 3) + +## Rolle +Zuständig für OAuth-Integrationssicherheit, Datenisolation, JWT/Session-Handling und SQL-Injection-Prüfung. + +## Aufgaben +- OAuth-Integration reviewen: + - Token-Speicherung sicher? (nicht im localStorage!) + - Refresh-Token-Handhabung korrekt implementiert? + - PKCE-Flow für Vonova OAuth eingehalten? +- Datenisolation prüfen: + - Alle Queries mandantenbezogen über SQL Builder Trait gefiltert? + - Keine IDOR-Lücken in REST `/api/*` Endpunkten? + - Subquery-Injection gegen Mandantengrenzen möglich? +- JWT/Session-Handling auditieren: + - Tokens kurzlebig und sicher gespeichert? (HttpOnly Cookies?) + - Session-Fixierung verhindert? + - Token-Revocation implementiert? +- SQL-Injection prüfen: + - Alle Doctrine Queries verwenden parametrisierte Werte? + - Native/Query-Builder-Nutzung korrekt? + - Keine raw-sql ohne Parameterisierung? + +## Kontext +- Projekt: Veranstaltungs-Tool mit Symfony Backend, React Frontend +- OAuth-Anbieter: Vonova +- Datenbank: PostgreSQL (Multi-Tenant-fähig erforderlich) +- Auth: JWT/Session-basiert nach OAuth-Flow +- Architektur-Konventionen: `Doku/architektur.md` + +## Regeln & Konventionen +- Keine hardcoded secrets oder tokens in code + - Alle credentials über env vars beziehen +- OAuth-Tokens niemals im Client-Speicher (localStorage, sessionStorage) speichern +- Session-Cookies müssen HttpOnly, Secure und SameSite=lax sein +- Input validation auf jeder Schicht: Frontend + API + DB-Level +- Regelmäßige dependency-security-checks durchführen + +## Erwartete Deliverables +- OAuth-Security-Review-Bericht (Token Handling, PKCE, Refresh-Token) +- Datenisolation-Audit (IDOR, SQL Builder Trait Scoping, Mandantengrenzen) + - JWT/Session-Handling Audit +- SQL-Injection-Analyse +- Security-Recommendations mit Priorisierung diff --git a/.opencode/agents/tester.md b/.opencode/agents/tester.md new file mode 100644 index 0000000..4e601d6 --- /dev/null +++ b/.opencode/agents/tester.md @@ -0,0 +1,45 @@ +# Tester (Phase 3 & 4) + +## Rolle +Zuständig für Unit Tests, Functional Tests, E2E-Szenarien und Integrationstests sowie globale Workflow-Validierung. + +## Aufgaben — Phase 3: Qualitätssicherung +- Unit Tests schreiben für alle Services und Business Logic + - Doctrine Entities (Validation, Relationships) + - Symfony Services (OAuth Logic, Mandanten-Scoping per SQL Builder Trait) + - Provider/Processor-Koordination pro Use-Case +- Functional Tests schreiben (Symfony PHPUnit / functional testing) + - CRUD-Operationen via REST `/api/*` testen (Request/Response DTOs validieren) + - OAuth-Integration testen + - Mandantenisolisation testen (Daten pro Mandant isoliert?) +- E2E-Szenarien vorbereiten: + - Anwesenheitsprotokollierung (vollständiger Workflow vom Login bis zur Protokolleintragung) + - Mandantenisolation (echter Multi-Tenant-Szenario via API-Aufrufe) + +## Aufgaben — Phase 4: Finale Integration +- Integrationstests durchführen (End-to-end workflows validieren) + - Gesamt-Workflow von OAuth Login bis Anwesenheitsprotokollierung + - Environment-spezifische Tests (Dev/UAT/Prod Konfigurationen prüfen) +- Alle Testlücken dokumentieren + +## Kontext +- Projekt: Veranstaltungs-Tool mit Symfony Backend, React Frontend +- OAuth-Anbieter: Vonova +- Datenbank: PostgreSQL (Multi-Tenant-fähig erforderlich) +- Auth: JWT/Session-basiert nach OAuth-Flow +- Architektur-Konventionen: `Doku/architektur.md` + +## Regeln & Konventionen +- Testabdeckung für alle kritischen Pfade > 80% (Anwesenheitsprotokollierung, Mandantenisolation, OAuth-Flows) +- Functional Tests nutzen Symfony WebTestCase / functional testing framework +- E2E-Szenarien in playwright/cypress implementieren (Login → API-Aufruf → DB-Validierung) + - Pro Test: Ein einziger klarer Szenario mit assertierbaren Ergebnissen + - Tests müssen Mandantenisolation explizit prüfen +- Provider/Processor-Koordination testen + +## Erwartete Deliverables +- Unit Tests für alle Services, Controller und Entities + - Functional Tests mit allen CRUD- und OAuth-Szenarien + - E2E-Szenarien (Anwesenheitsprotokoll Workflow, Mandantendisolation) +- Integrationstests +- Testabdeckungsreport diff --git a/.opencode/opencode.json b/.opencode/opencode.json new file mode 100644 index 0000000..45f023a --- /dev/null +++ b/.opencode/opencode.json @@ -0,0 +1,33 @@ +{ + "$schema": "https://opencode.ai/config.json", + "agent": { + "architekt": { + "description": "Architektur-Design und Architektur-Review", + "path": ".opencode/agents/architekt.md" + }, + "db-architekt": { + "description": "Datenbank-Design, Entities, Indizes und Mandanten-Scoping", + "path": ".opencode/agents/db-architekt.md" + }, + "devops-coder": { + "description": "Docker-Konfiguration, Services, Health-Checks und Environment-Einrichtung", + "path": ".opencode/agents/devops-coder.md" + }, + "backend-coder": { + "description": "Symfony Boilerplate, Entities, CRUD-Controller und OAuth-Integration", + "path": ".opencode/agents/backend-coder.md" + }, + "frontend-coder": { + "description": "React boilerplate, Router, State-Management und View-Komponenten", + "path": ".opencode/agents/frontend-coder.md" + }, + "tester": { + "description": "Unit-, Functional-, E2E- und Integrationstests", + "path": ".opencode/agents/tester.md" + }, + "security-reviewer": { + "description": "OAuth-Sicherheit, Datenisolation, JWT/Session-Handling und SQL-Injection-Prüfung", + "path": ".opencode/agents/security-reviewer.md" + } + } +} diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..e8428a6 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,40 @@ +# Vollversammlung – Agent Instructions + +## Project +Mandantenfähiges Veranstaltungs-Tool mit Anwesenheitsverwaltung und Statistik. +- Backend: Symfony (manuelle Controller, keine API Platform) +- Frontend: React +- DB: PostgreSQL (Multi-Tenant, Scoping per MandantID) +- Auth: externer OAuth (Vonova), kein lokaler Login +- Logging: Mercure +- Infrastruktur: Docker / docker-compose + +## Structure (planned) +``` +vollversammlung/ +├── .opencode/ ← OpenCode agents + config (opencode.json) +├── Doku/ ← Planungsdokumente, Architektur +├── api/ ← Symfony Backend +└── frontend/ ← React Frontend +``` + +## Key constraints the repo doesn't make obvious +- Personen haben **keine** email/benutzername-Felder – OAuth liefert Identität extern +- Mandantenisolation ist core: alle Daten pro MandantID scoped, keine Überschneidung +- Auth-Tokens niemals im Client-Speicher (localStorage/sessionStorage) +- Session-Cookies: HttpOnly, Secure, SameSite=lax +- Input-Validierung auf jeder Schicht: Frontend + API + DB-Level + +## Agent config +Agents are defined in `.opencode/opencode.json` and `.opencode/agents/*.md`. They cover six roles: architekt, db-architekt, backend-coder, frontend-coder, tester, devops-coder. Loading agent instructions from `.opencode/` is the primary configuration surface – no other OpenCode config files exist at root level. + +## Referenzen +- `Doku/architektur.md` → Backend Architektur mit struktur & Konventionen (Provider/Processor-Pattern, Multi-Tenant Scoping) +- `Doku/plan.md` → Domain-Details und Entity-Spezifikationen (Veranstaltung, Personen, Anwesenheit) +- `Doku/agents_planung.md` → 4-Phasen-Agent-Arbeitsverteilung + +## What to do on first session in api/ or frontend/ +1. Read `.opencode/opencode.json` for agent definitions +2. Read `Doku/architektur.md` und `Doku/plan.md` für Architektur und Domain-Anforderungen +3. Read the relevant agent file from `.opencode/agents/` fur implementierung-Konventionen +4. Prüfe ob Infrastruktur eingerichtet ist (docker-compose.yaml, .env.example) vor Code-Schreiben diff --git a/Doku/agents_planung.md b/Doku/agents_planung.md new file mode 100644 index 0000000..239b0ff --- /dev/null +++ b/Doku/agents_planung.md @@ -0,0 +1,31 @@ +# Agents-Planung – Veranstaltungs-Tool + +## Phase 1: Design + +| Rolle | Aufgaben | +|---|---| +| **Architekt** | Systemarchitektur, Datenfluss, API-Konventionen, Auth-Fluss | +| **Datenbank-Architekt** | Entities, Beziehungen, Indizes, Mandanten-Scoping auf DB-Ebene | + +## Phase 2: Implementierung + +| Rolle | Aufgaben | +|---|---| +| **DevOps-Coder** | docker-compose.yaml, Services, Konnektivitat | +| **Backend-Coder** | Symfony boilerplate, Entities, CRUD-Controller, OAuth-Integration | +| **Frontend-Coder** | React boilerplate, Router, State-Management, View-Komponenten, Statistikseite | + +## Phase 3: Qualität + +| Rolle | Aufgaben | +|---|---| +| **Tester** | Unit Tests, Functional Tests, E2E-Szenarien (Anwesenheitsprotokollierung, Mandantenisolation) | +| **Security-Reviewer** | OAuth-Integration, Datenisolation, JWT/Session handling, SQL-Injection Check | +| **Architekt (Review)** | Architektur-Conformance, Entwurfsentscheidungen nachprufen | + +## Phase 4: Finale Integration + +| Rolle | Aufgaben | +|---|---| +| **DevOps-Coder** | Environment-Konfiguration, Health-Checks | +| **Tester** | Integrationstests, Gesamt-Workflow validieren | diff --git a/Doku/architektur.md b/Doku/architektur.md new file mode 100644 index 0000000..fee7f30 --- /dev/null +++ b/Doku/architektur.md @@ -0,0 +1,68 @@ +# Backend-Architektur – Veranstaltungs-Tool (Konzept) + +## Ziel +Mandantenfähiges Backend mit 3 Schichten pro fachlicher Domäne und mehreren Clients/Entry-Points. + +## Struktur + +``` +api/ +├── public/index.php ← Entrypoint +└── src/ + ├── UI/{ClientName}/{FeatureName}/ ← pro Client + Use-Case (PascalCase, deutsch) + │ ├── Controller/ REST-Endpunkte (/api/*), Commands etc. + │ ├── Request/ Request-DTOs + │ └── Response/ Response-DTOs + ├── Logic/{FeatureName}/ ← Business Rules pro Use-Case + │ ├── Manager/ Cache, Workflow, Aggregate + │ ├── Calculator/ Berechnungen (Statistiken) + │ ├── Model/ Feature-Modelle + │ ├── Provider/*ProviderInterface Interfaces für Data-Layer + │ ├── Processor/*ProcessorInterface Interface für Verarbeitungslogik + │ └── Shared/ Feature-spezifisches Shared (wenn notwendig) + ├── Data/{FeatureName}/ ← Domain-Objects, DB-Zugriff pro Use-Case + │ ├── Entity/ Doctrine Entities + │ └── Repository/ Repo-Interfaces + Implementierungen + └── Shared/ ← Nur wirklich entitätsübergreifende Basics + ├── BaseEntity.php + └── UuidGenerator.php +``` + +## Client-Namenskonvention +- `API` → REST-Schnittstelle (UI/API/*) +- `Console` → Symfony Commands (UI/Console/*) + +## Feature-Namenskonvention +Use-Case-basiert, PascalCode auf Deutsch. ZB: +- `Personen` (nicht Personenliste / PersonController) +- `Veranstaltungen` (nicht VeranstaltungenIndex) +- `Anwesenheit` (nicht AnwesenheitsstatusErfassung) + +Beispiel-Pfad: `UI/API/Personen/Controller/PersonenController.php` + +**Model/** pro Feature – Ordner mit ggf. mehreren Models definiert die Feature-Grenze. + +## Entry-Points +Mehrere Clients via Router-basierte Routing: +- `/api/*` → REST-Controller (`UI/API/{Feature}/Controller/`) +- Console → Symfony Commands (`UI/Console/{Feature}/Command/`) +- Messages → Messenger Handler + +## Entscheidungen + +### Provider und Processoren +**Provider** implementieren die `*ProviderInterface`. Sie rufen Daten ab – per Doctrine Repositories/Entities oder über API-Aufrufe. Das Mapping zwischen Entities und Model-Objekten erfolgt in separaten **Mapper**-Klassen, die in **Data/** liegen. Provider binden so die Data-Schicht an die Logic-Schicht. + +**Processoren** funktionieren analog für schreibende Prozesse (`*ProcessorInterface`). + +### Shared-Klassen +Echt entitätsbergreifende Basis-Klassen (`BaseEntity`, `UuidGenerator`) leben in `src/Shared/`. Feature-spezifisches Shared liegt innerhalb des Features, zB: +``` +src/Logic/Veranstaltungsmanagement/Shared/JsonSerializer.php +``` + +### Multi-Tenant Scoping +Alle Repository-Queries werden per **Doctrine SQL Builder Trait** mit `mandant_id` gefiltert. Der Scope wird automatisch angehängt – sowohl lesend als auch schreibend. Tenantübergreifende Operationen sind technisch nicht möglich, da alle Repos implicit scopen. Das schützt vor unbeabsichtigten Mandanten-Überschneidungen. Bei Bedarf kann eine explizite WithoutScope-Schicht eingeführt werden. + +### Dependency Injection +Nur explizit über `services.yaml`, wenn notwendig. Standardmäßig wird Auto-Wiring mit Konventionen verwendet (`*ProviderInterface`, `*ProcessorInterface`). diff --git a/Doku/infrastruktur_planung.md b/Doku/infrastruktur_planung.md new file mode 100644 index 0000000..6d314b2 --- /dev/null +++ b/Doku/infrastruktur_planung.md @@ -0,0 +1,339 @@ +# 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= +OAUTH_CLIENT_SECRET= +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= + +DATABASE_URL="postgresql://vv_admin:@postgres-db:5432/vollversammlung?serverVersion=16&charset=utf8mb4" + +MERCURE_PUBLISHER_JWT_KEY= +MERCURE_SUBSCRIBER_JWT_KEY= +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= +OAUTH_CLIENT_SECRET= +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* diff --git a/Doku/plan.md b/Doku/plan.md new file mode 100644 index 0000000..eb1ed20 --- /dev/null +++ b/Doku/plan.md @@ -0,0 +1,57 @@ +# Veranstaltungs-Tool – Planungsdokumentation + +## Ziel +Mandantenfähiges Tool zur Verwaltung von Veranstaltungen mit Anwesenheitsverwaltung und Statistik. + +## Tech Stack +- Backend: Symfony (manuelle Controller, keine API Platform) +- Frontend: React +- Datenbank: PostgreSQL +- Logging: Mercure +- Deployment: Docker / docker-compose +- Auth: OAuth (extern, kein lokaler Login) + +## Mandantenfähigkeit +- Daten werden pro MandantID gescoped +- Personen sind mandant-isoliert (keine Überschneidung) + +## Veranstaltungen (CRUD via API) +- Titel +- Datum +- Beschreibung +- max_teilnehmer + +Anlegen über: UI oder Import-API + +## Personenverwaltung (Admin) +- Anlegen / Entfernen über UI oder Import-API +- Felder: vorname, nachname, geschlecht +- Kein lokaler Login (OAuth extern) +- Keine email / benutzername-Felder + +## Anwesenheit +- Manuelle Markierung im UI +- Status: anwesend / abwesend +- Alle Änderungen werden geloggt (wer, wann, was) + +## Statistikseite +- Anwesenheitszahlen pro Veranstaltung +- Anwesenheitsquote +- Geschlechterquote +- Dashboard-Ansicht als separate Statistik-Seite + +## Projektstruktur +``` +vollversammlung/ +├── docker-compose.yaml +├── api/ (Symfony Backend Repo) +└── frontend/ (React Frontend Repo) + +## Nächste Schritte +1. Boilerplate symfony erstellen +2. Boilerplate react erstellen +3. Docker compose konfigurieren +4. Datenmodelle implementieren +5. CRUD-Endpoints entwickeln +6. Frontend-Seiten aufbauen +7. Statistikseite implementieren diff --git a/api/.env.example b/api/.env.example new file mode 100644 index 0000000..0ca0236 --- /dev/null +++ b/api/.env.example @@ -0,0 +1,47 @@ +# ============================================= +# Symphony Backend – Environment Konfiguration +# Kopiere diese Datei nach .env.local und passe die Werte an. +# ============================================= + +# ------------------- +# Symfony Basics +# ------------------- +APP_ENV=dev +APP_DEBUG=1 +APP_SECRET=__CHANGE_ME__ + +# ------------------- +# PostgreSQL DB +# ------------------- +DATABASE_URL="postgresql://symfony:secret@db:5432/symfony?serverVersion=16&charset=utf8mb4" + +# ------------------- +# Mercure Logging +# ------------------- +MERCURE_JWT_SECRET=__CHANGE_ME_MERCURE_JWT_SECRET__ +MERCURE_PUBLISHER_JWT_KEY=__CHANGE_ME_PUBLISHER_KEY__ +MERCURE_SUBSCRIBER_JWT_KEY=__CHANGE_ME_SUBSCRIBER_KEY__ +MERCURE_URL="http://mercure-hub/.well-known/mercure" +MERCURE_PUBLIC_URL="${MERCURE_PUBLIC_URL:-http://localhost:3000/.well-known/mercure}" + +# ------------------- +# OAuth (Vonova Provider) +# ------------------- +OAUTH_PROVIDER="vonova" +OAUTH_PROVIDER_NAME="${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=__CHANGE_ME_OAUTH_CLIENT_ID__ +OAUTH_CLIENT_SECRET=__CHANGE_ME_OAUTH_CLIENT_SECRET__ +OAUTH_REDIRECT_URI="http://localhost:8080/api/oauth/callback" +OAUTH_SCOPES="read profile mandant_id" + +# ------------------- +# Mandant-Scope (Standard/Tenant für lokale Entwicklung) +# ------------------- +TENANT_ID="" + +# ------------------- +# Mail (Mailpit im Dev-Stack) +# ------------------- +MAILER_DSN="smtp://mailpit:1025" diff --git a/api/Dockerfile.dev b/api/Dockerfile.dev new file mode 100644 index 0000000..105e966 --- /dev/null +++ b/api/Dockerfile.dev @@ -0,0 +1,54 @@ +FROM php:8.3-fpm-alpine + +# Install system dependencies +RUN apk add --no-cache \ + imagemagick \ + imagemagick-dev \ + git \ + curl \ + oniguruma-dev \ + libpng-dev \ + libjpeg-turbo-dev \ + libxml2-dev \ + zip \ + unzip + +# Install PHP extensions +RUN docker-php-ext-install \ + pdo_pgsql \ + gd \ + intl \ + mbregex \ + xml + +# Install Composer +COPY --from=composer:latest /usr/bin/composer /usr/bin/composer + +# Create app directory +WORKDIR /app + +# Copy entrypoint script +COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh +RUN chmod +x /usr/local/bin/docker-entrypoint.sh + +# Set up Symfony directory structure +RUN mkdir -p \ + src/UI/{API,Console}/{Controller,Request,Response,Command} \ + src/Logic/{Manager,Calculator,Model,Provider,Processor,Shared} \ + src/Data/{Entity,Repository} \ + public/files \ + var/cache \ + var/log \ + var/sessions + +# Install Symfony runtime and framework dependencies +RUN composer config allow-plugins true +RUN composer require symfony/framework-bundle symfony/asset symfony/mime symfony/console \ + symfony/dotenv symfony/flex symfony/runtime symfony/twig-bundle symfony/validator \ + symfony/doctrine-messenger symfony/messenger twig symfony/mercure symfony/routing \ + symfony/serializer php-http/discovery + +EXPOSE 9000 + +ENTRYPOINT ["docker-entrypoint.sh"] +CMD ["php-fpm", "-F"] diff --git a/api/Dockerfile.prod b/api/Dockerfile.prod new file mode 100644 index 0000000..2207303 --- /dev/null +++ b/api/Dockerfile.prod @@ -0,0 +1,71 @@ +FROM php:8.3-fpm-alpine AS base + +# Install system dependencies (minimal for production) +RUN apk add --no-cache \ + curl \ + oniguruma-dev \ + libpng-dev \ + libjpeg-turbo-dev \ + libxml2-dev \ + zip \ + unzip \ + imagemagick \ + ttf-freefont + +# Install PHP extensions (production-safe with --enable-option-checking) +RUN docker-php-ext-configure gd --with-jpeg \ + && docker-php-ext-install \ + pdo_pgsql \ + gd \ + intl \ + mbstring \ + xmlrpc \ + opcache + +# Enable OPcache for production +RUN echo "opcache.memory_consumption=256" > /usr/local/etc/php/conf.d/opcache.ini \ + && echo "opcache.max_accelerated_files=32531" >> /usr/local/etc/php/conf.d/opcache.ini \ + && echo "opcache.validate_timestamps=0" >> /usr/local/etc/php/conf.d/opcache.ini \ + && echo "opcache.revalidate_freq=0" >> /usr/local/etc/php/conf.d/opcache.ini + +# Install Composer (production stage) +FROM composer:latest AS composer + +# --------------- +# Build stage – install dependencies +# --------------- +FROM base AS build + +WORKDIR /app + +COPY composer.json composer.lock ./ +RUN composer install --no-dev --optimize-autoloader --classmap-authoritative + +COPY . . +RUN chmod +x bin/console \ + && APP_ENV=prod APP_DEBUG=0 COMPOSER_DEV_MODE=0 composer dump-autoload --classmap-authoritative --optimize \ + && php bin/console cache:clear --env=prod \ + && php bin/console assets:install public --env=prod + +# --------------- +# Final production image +# --------------- +FROM base AS production + +LABEL maintainer="devops@vollversammlung" \ + description="Vollversammlung Symfony Production Image" + +WORKDIR /app + +COPY --from=composer /usr/bin/composer /usr/bin/composer +COPY --from=build /app . + +# Create necessary directories with correct permissions +RUN chown -R www-data:www-data var/cache var/log var/sessions public/files \ + && find var/ -type d -exec chmod 750 {} \; \ + && find var/ -type f -exec chmod 640 {} \; + +EXPOSE 9000 + +ENTRYPOINT ["docker-entrypoint.sh"] +CMD ["php-fpm", "-F"] diff --git a/api/docker-entrypoint.sh b/api/docker-entrypoint.sh new file mode 100644 index 0000000..36482d4 --- /dev/null +++ b/api/docker-entrypoint.sh @@ -0,0 +1,86 @@ +#!/bin/sh +set -e + +echo "========================================" +echo " Vollversammlung - Symfony Startup" +echo "========================================" + +# ---------------------------- +# Wait for database availability +# ---------------------------- +wait_for_db() { + echo "Waiting for PostgreSQL to be ready..." + max_tries=30 + try=0 + while ! nc -z db 5432 2>/dev/null; do + sleep 1 + try=$((try + 1)) + if [ "$try" -ge "$max_tries" ]; then + echo "ERROR: PostgreSQL did not become ready in time." + exit 1 + fi + done + echo "PostgreSQL is ready." +} + +wait_for_db + +# ---------------------------- +# Run Doctrine Migrations +# ---------------------------- +run_migrations() { + if [ -f "bin/console" ]; then + echo "Running Doctrine Migrations..." + php bin/console doctrine:migrations:migrate --no-interaction --all-or-nothing || { + echo "WARNING: Migration failed, continuing startup." + } + fi +} + +run_migrations + +# ---------------------------- +# Configure Mercure for logging +# ---------------------------- +configure_mercure() { + if [ -n "${MERCURE_JWT_SECRET}" ]; then + export MERCURE_PUBLISHER_JWT_KEY="${MERCURE_JWT_SECRET}" + echo "Mercure Publisher JWT: configured" + + export MERCURE_SUBSCRIBER_JWT_KEY="${MERCURE_JWT_SECRET}" + echo "Mercure Subscriber JWT: configured" + else + echo "WARNING: MERCURE_JWT_SECRET not set – Mercure logging may not work." + fi + + if [ -n "${MERCURE_URL}" ]; then + echo "Mercure URL: ${MERCURE_URL}" + fi +} + +configure_mercure + +# ---------------------------- +# Clear cache for current environment +# ---------------------------- +clear_cache() { + if [ -f "bin/console" ]; then + echo "Clearing Symfony cache (env=${APP_ENV:-dev})..." + php bin/console cache:clear --env="${APP_ENV:-dev}" --no-warmup || true + fi +} + +clear_cache + +# ---------------------------- +# Start PHP-FPM or dev server +# ---------------------------- +echo "" +echo "========================================" +if [ "${Symfony_USE_DEV_SERVER:-1}" = "1" ] && [ "${APP_ENV:-dev}" = "dev" ]; then + echo " Starting Symfony Dev Server on :8000" + exec php bin/console server:run 0.0.0.0:8000 --no-reload +else + echo " Starting PHP-FPM on port 9000" + exec php-fpm -F +fi diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..c07dd14 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,147 @@ +services: + postgres-db: + image: postgres:16-alpine + container_name: vv-postgres-db + environment: + POSTGRES_DB: vollversammlung + POSTGRES_USER: vv_admin + POSTGRES_PASSWORD_FILE: /run/secrets/db_password + volumes: + - pg_data:/var/lib/postgresql/data + networks: + - vollversammlung_net + healthcheck: + test: ["CMD-SHELL", "pg_isready -U vv_admin -d vollversammlung"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 30s + secrets: + - db_password + + symfony-fpm: + build: + context: ./api + dockerfile: Dockerfile.dev + container_name: vv-symfony-fpm + volumes: + - ./api/src:/app/src + - ./api/public:/app/public + - symfony_var:/app/var + environment: + DATABASE_URL: "postgresql://vv_admin:${DB_PASSWORD}@postgres-db:5432/vollversammlung?serverVersion=16&charset=utf8mb4" + MERCURE_PUBLISHER_JWT_KEY: ${MERCURE_PUBLISHER_JWT_KEY} + MERCURE_SUBSCRIBER_JWT_KEY: ${MERCURE_SUBSCRIBER_JWT_KEY} + MERCURE_URL: "http://mercure-hub/.well-known/mercure" + MERCURE_PUBLIC_URL: "${MERCURE_PUBLIC_URL:-http://localhost:3000/.well-known/mercure}" + APP_ENV: "${APP_ENV:-dev}" + APP_DEBUG: "${APP_DEBUG:-1}" + APP_SECRET: ${APP_SECRET} + OAUTH_PROVIDER_NAME: "${OAUTH_PROVIDER_NAME:-vonova}" + OAUTH_AUTHORIZATION_ENDPOINT: "${OAUTH_AUTHORIZATION_ENDPOINT:-https://oauth.vonova.de/oauth/authorize}" + OAUTH_TOKEN_ENDPOINT: "${OAUTH_TOKEN_ENDPOINT:-https://oauth.vonova.de/oauth/token}" + OAUTH_CLIENT_ID: "${OAUTH_CLIENT_ID}" + OAUTH_CLIENT_SECRET: "${OAUTH_CLIENT_SECRET}" + OAUTH_REDIRECT_URI: "${OAUTH_REDIRECT_URI:-http://localhost:8080/api/oauth/callback}" + OAUTH_SCOPES: "${OAUTH_SCOPES:-read profile mandant_id}" + MAILER_DSN: "${MAILER_DSN:-smtp://mailpit:1025}" + networks: + - vollversammlung_net + depends_on: + postgres-db: + condition: service_healthy + healthcheck: + test: ["CMD-SHELL", "curl -sf http://localhost:8000/api/health || exit 1"] + interval: 15s + timeout: 5s + retries: 3 + start_period: 40s + + nginx: + image: nginx:alpine + container_name: vv-nginx + ports: + - "8080:80" + volumes: + - ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro + networks: + - vollversammlung_net + depends_on: + symfony-fpm: + condition: service_healthy + mercure-hub: + condition: service_healthy + healthcheck: + test: ["CMD-SHELL", "curl -sf http://localhost:8080/api/health || exit 1"] + interval: 15s + timeout: 5s + retries: 3 + + mercure-hub: + image: dunglas/mercure + container_name: vv-mercure-hub + ports: + - "3000:80" + environment: + SERVER_NAME: ":80" + MERCURE_PUBLISHER_JWT_KEY: ${MERCURE_PUBLISHER_JWT_KEY} + MERCURE_SUBSCRIBER_JWT_KEY: ${MERCURE_SUBSCRIBER_JWT_KEY} + MERCURE_EXTRA_DIRECTIVES: | + $merge_headers { + "Access-Control-Allow-Origin" "*"; + } + networks: + - vollversammlung_net + healthcheck: + test: ["CMD-SHELL", "curl -sf http://localhost/.well-known/mercure || exit 1"] + interval: 15s + timeout: 5s + retries: 3 + + mailpit: + image: axllent/mailpit + container_name: vv-mailpit + ports: + - "8025:8025" + networks: + - vollversammlung_net + healthcheck: + test: ["CMD-SHELL", "nc -z localhost 1025 || exit 1"] + interval: 15s + timeout: 5s + retries: 3 + + vite-dev: + build: + context: ./frontend + dockerfile: Dockerfile.dev + container_name: vv-vite-dev + volumes: + - ./frontend/src:/app/src + - ./frontend/public:/app/public + ports: + - "5173:5173" + environment: + VITE_API_URL: "http://nginx:80/api" + VITE_MERCURE_URL: "http://mercure-hub/.well-known/mercure" + VITE_API_URL: ${VITE_API_URL:-http://nginx:80/api} + VITE_MERCURE_URL: ${VITE_MERCURE_URL:-http://mercure-hub/.well-known/mercure} + VITE_OAUTH_AUTHORIZATION_ENDPOINT: "${VITE_OAUTH_AUTHORIZATION_ENDPOINT:-https://oauth.vonova.de/oauth/authorize}" + VITE_OAUTH_TOKEN_ENDPOINT: "${VITE_OAUTH_TOKEN_ENDPOINT:-https://oauth.vonova.de/oauth/token}" + networks: + - vollversammlung_net + depends_on: + nginx: + condition: service_healthy + +networks: + vollversammlung_net: + driver: bridge + +volumes: + pg_data: + symfony_var: + +secrets: + db_password: + file: ./secrets/db_password.txt diff --git a/frontend/.env.example b/frontend/.env.example new file mode 100644 index 0000000..0b3f39b --- /dev/null +++ b/frontend/.env.example @@ -0,0 +1,28 @@ +# ============================================= +# React Frontend – Environment Konfiguration +# Kopiere diese Datei nach .env.local und passe die Werte an. +# ============================================= + +# ------------------- +# API Backend URL +# ------------------- +VITE_API_URL="http://localhost:8080/api" + +# ------------------- +# Mercure (SSE/Logging) +# ------------------- +VITE_MERCURE_URL="http://localhost:3000/.well-known/mercure" + +# ------------------- +# OAuth Endpunkte (Vonova Provider) +# ------------------- +VITE_OAUTH_AUTHORIZATION_ENDPOINT="https://oauth.vonova.de/oauth/authorize" +VITE_OAUTH_TOKEN_ENDPOINT="https://oauth.vonova.de/oauth/token" +VITE_OAUTH_CLIENT_ID=__CHANGE_ME_OAUTH_CLIENT_ID__ +VITE_OAUTH_REDIRECT_URI="http://localhost:5173/oauth/callback" + +# ------------------- +# Manifest / App Metadata +# ------------------- +VITE_APP_NAME="Vollversammlung" +VITE_APP_VERSION="0.1.0" diff --git a/frontend/Dockerfile.dev b/frontend/Dockerfile.dev new file mode 100644 index 0000000..a931b36 --- /dev/null +++ b/frontend/Dockerfile.dev @@ -0,0 +1,67 @@ +# --------------- +# Base: install deps for dev server +# --------------- +FROM node:20-alpine AS base-dev + +ARG user=viteuser +ARG group=node +ARG uid=1000 +ARG gid=1000 + +RUN apk add --no-cache \ + tini \ + bash + +RUN addgroup -g ${gid} ${group} && \ + adduser -u ${uid} -G ${group} -s /bin/sh -D ${user} + +WORKDIR /app + +COPY package.json package-lock.json* ./ +RUN npm ci || npm install + +# --------------- +# Production build stage +# --------------- +FROM node:20-alpine AS build + +WORKDIR /app + +ARG VITE_API_URL="http://nginx:80/api" +ARG VITE_MERCURE_URL="http://mercure-hub/.well-known/mercure" +ARG VITE_OAUTH_AUTHORIZATION_ENDPOINT="https://oauth.vonova.de/oauth/authorize" +ARG VITE_OAUTH_TOKEN_ENDPOINT="https://oauth.vonova.de/oauth/token" + +ENV VITE_API_URL=${VITE_API_URL} +ENV VITE_MERCURE_URL=${VITE_MERCURE_URL} +ENV VITE_OAUTH_AUTHORIZATION_ENDPOINT=${VITE_OAUTH_AUTHORIZATION_ENDPOINT} +ENV VITE_OAUTH_TOKEN_ENDPOINT=${VITE_OAUTH_TOKEN_ENDPOINT} + +COPY --from=base-dev /app/node_modules ./node_modules +COPY . . + +RUN npm run build --if-present + +# --------------- +# Development stage +# --------------- +FROM base-dev AS dev + +LABEL maintainer="devops@vollversammlung" \ + description="Vollversammlung React Vite Dev Image" + +EXPOSE 5173 + +CMD ["tini", "--", "npx", "vite", "--host", "0.0.0.0"] + +# --------------- +# Production static serve (nginx will be added later) +# --------------- +FROM nginx:alpine AS production-static + +COPY --from=build /app/dist /usr/share/nginx/html + +ARG VITE_API_URL="http://nginx:80/api" +RUN printf "server {\n listen 80;\n root /usr/share/nginx/html;\n try_files \$uri \$uri/ /index.html;\n}" > /etc/nginx/conf.d/default.conf + +EXPOSE 80 diff --git a/nginx/default.conf b/nginx/default.conf new file mode 100644 index 0000000..6fa0fe4 --- /dev/null +++ b/nginx/default.conf @@ -0,0 +1,69 @@ +server { + listen 80; + server_name localhost; + + # Security headers + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-XSS-Protection "1; mode=block" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + + # CORS for API endpoints + location /api/ { + # Forward tenant ID from client + proxy_set_header X-Mandant-ID $http_x_mandant_id; + + # Proxy headers + 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 $scheme; + proxy_set_header X-Forwarded-Port $server_port; + + # Disable buffering for streaming responses (Mercure) + proxy_buffering off; + + # Pass to Symfony FPM + location ~ ^/api/(.+)\.php(/|$) { + fastcgi_pass symfony-fpm:9000; + fastcgi_split_path_info ^(.+.php)(/.*)$; + include fastcgi_params; + fastcgi_param SCRIPT_FILENAME /app/$fastcgi_script_name; + fastcgi_param DOCUMENT_ROOT /app; + } + + # API catch-all - rewrite to index.php for Symfony routing + try_files $uri /api/index.php$is_args$args; + } + + # Mercure Hub + location /.well-known/mercure { + proxy_pass https://mercure-hub/.well-known/mercure; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # Mercure needs WebSocket support + upgrade_http 2; + } + + # Static assets (frontend build output) + location /assets/ { + alias /app/public/assets/; + expires 7d; + add_header Cache-Control "public, immutable"; + } + + # Health check endpoint + location /api/health { + access_log off; + proxy_pass http://symfony-fpm:8000/api/health; + proxy_set_header Host localhost:8000; + } + + # Frontend SPA fallback (static files served from public/) + location / { + root /app/public; + try_files $uri $uri/ /index.html; + } +}