major update

This commit is contained in:
2026-03-04 15:56:58 +01:00
parent 16759a2732
commit 8f4dd5840d
478 changed files with 24313 additions and 8201 deletions

View File

@@ -0,0 +1,42 @@
# Systemüberblick
Letzte Aktualisierung: 2026-02-24
## Ziel
In 10 Minuten verstehen, was die Software tut, wie sie aufgebaut ist und welche Kernprinzipien immer gelten.
## Was ist IMVS?
IMVS ist eine mandantenfähige Admin-Software für Immobilienmakler-Prozesse mit klarer Rollen-/Rechtestruktur.
Kernmodule:
- Mandanten, Abteilungen, Benutzer, Rollen, Berechtigungen
- Adressbuch und globale Suche
- API (`/api/v1`)
- Scheduler (geplante Aufgaben)
- Optional Microsoft-SSO
## Kernprinzipien
- Multi-Tenant by default: Daten und Sichtbarkeit sind tenant-gebunden.
- Security first: Auth, Permission und Tenant-Scope sind Pflicht, nicht optional.
- Schichtentrennung: `pages` -> `Service` -> `Repository`.
- API-Verträge sind UUID-first.
## Projektkarte
- `config/` Konfiguration
- `db/` Schema/Updates
- `lib/` Fachlogik
- `pages/` Actions + Views
- `templates/` Partials/Layout
- `web/` CSS/JS/Assets
- `docs/` Entwicklerdokumentation
## Danach
1. Setup und erster Login: `/docs/01-setup-und-erster-login.md`
2. Begriffe sicher verstehen: `/docs/02-domain-glossar.md`
3. Architekturfluss verinnerlichen: `/docs/03-architektur-request-flow.md`

View File

@@ -0,0 +1,52 @@
# Setup und erster Login
Letzte Aktualisierung: 2026-02-24
## Ziel
Lokal starten, einloggen, Basisfunktion prüfen.
## Voraussetzungen
- Docker + Docker Compose
- Zugriff auf das Repository
## Schritte
1. Umgebung anlegen und Container starten:
```bash
cp .env.example .env
docker compose up --build -d
```
2. App öffnen:
- `http://localhost:8080`
- `http://localhost:8081` (phpMyAdmin)
3. Mit Seed-User einloggen (Fresh-Install):
- E-Mail: `demo@user.com`
- Passwort: `Demo123`
4. Schnellcheck:
- Login funktioniert
- `Admin -> Benutzer` lädt
- `Admin -> Einstellungen` lädt
## Typisches Problem
Bei altem DB-Volume weichen Seed-Daten ab.
Fresh-Reset (löscht lokale DB-Daten):
```bash
docker compose down -v
docker compose up --build -d
```
## Danach
- Begriffe: `/docs/02-domain-glossar.md`
- Tagesworkflow: `/docs/lokale-entwicklung.md`

32
docs/02-domain-glossar.md Normal file
View File

@@ -0,0 +1,32 @@
# Domain-Glossar
Letzte Aktualisierung: 2026-02-24
## Ziel
Alle Kernbegriffe kurz und einheitlich verstehen.
## Begriffe
- `Tenant`: Mandant; isolierter Daten- und Rechteraum.
- `Department`: Organisationseinheit innerhalb eines Tenants.
- `Role`: Bündel von Berechtigungen.
- `Permission`: einzelne Berechtigung (z. B. `users.view`).
- `Policy` / `Ability`: zentrale Autorisierungsregel.
- `Tenant-Scope`: Regel, ob Zugriff im Ziel-Tenant erlaubt ist.
- `Action`: Request-Einstieg in `pages/**`.
- `Service`: Fachlogik in `lib/Service/**`.
- `Repository`: SQL und Mapping in `lib/Repository/**`.
- `Factory`: erstellt/verdrahtet Service-Objekte (z. B. `UserServicesFactory`).
- `Gateway`: schmale Schnittstelle/Fassade zu Domain-Funktionen oder Querschnitt (z. B. `PermissionGateway`).
- `AppContainer`: zentrale Service-Registrierung und -Auflösung (`app(Foo::class)`).
- `UUID-first`: APIs geben externe UUIDs aus, keine internen IDs.
## Wichtig
Wenn ein Begriff unklar ist, zuerst hier klären und dann implementieren.
## Danach
- Architekturfluss: `/docs/03-architektur-request-flow.md`
- Sicherheitskontext: `/docs/06-security-tenant-scope.md`

View File

@@ -0,0 +1,86 @@
# Architektur und Request-Flow
Letzte Aktualisierung: 2026-03-03
## Ziel
Verstehen, wo Änderungen hingehören und wo nicht.
## Request-Flow
1. `web/index.php` lädt Bootstrap, registriert den `AppContainer` und setzt globale Service-Auflösung.
2. `RequestContext` initialisiert `request_id` und Basis-Metadaten; `X-Request-Id` wird gesetzt.
3. Guard/Sicherheitslayer laufen.
4. Action in `pages/**` nimmt Input an.
5. Action delegiert an Service.
6. Service nutzt Repository für Datenzugriff.
7. Response wird als HTML/JSON ausgegeben.
API-Ergänzung:
1. `ApiBootstrap::init()` startet API-Audit (`ApiAuditService`) und API-System-Audit (`ApiSystemAuditReporter`).
2. `ApiResponse::send()` finalisiert beide Reporter (inkl. `request_id`-Korrelation).
3. System-Protokolle schreiben nur selektive `api.request`-Events (kein Voll-Request-Logging).
## Schichtregeln
- `pages/**`: Input, Auth/AuthZ, Service-Aufruf, Response.
- `lib/Service/**`: Business-Regeln und Orchestrierung.
- `lib/Repository/**`: SQL, Filter, Mapping.
- `templates/**`: Rendering ohne Business-Logik.
- Request-Daten in Actions nur über `requestInput()` (keine Input-Superglobals).
- Validierungsfehler zentral über `FormErrors`; API-Fehler über `ApiResponse::validationFromFormErrors(...)`.
- Template-AuthZ konsumiert nur vorbereitete `$viewAuth`-Daten (kein `can('...')` im Template).
- Service-Auflösung in Actions/Helpern über `app(Foo::class)`.
- Fachliche Audit-Events laufen zentral über `SystemAuditService`.
- Persistierte Status/Outcome-Werte laufen über die zentrale Taxonomie-Schicht `lib/Domain/Taxonomy/*` (Enums + DB-Checks).
- `pages/**/data().php` sind GET-only (`gridRequireGetRequest()`) und normalisieren `limit`/`offset`/`order`/`dir` über Grid-Helper.
## Einfaches ASCII-Beispiel
Beispiel: `GET /admin/users/data`
```text
Browser
-> web/index.php (Bootstrap + AppContainer)
-> pages/admin/users/data().php (Action)
-> Guard + AuthorizationService (Auth/AuthZ)
-> UserAccountService (Fachlogik)
-> UserListQueryRepository (SQL + Filter + Paging)
-> DB
<- { data, total } JSON
```
Factory/Gateway im selben Bild:
```text
AppContainer
-> UserServicesFactory
-> createUserAccountService() -> UserAccountService
Guard::requireAbilityOrForbidden(ops....)
-> app(AuthorizationService::class)
-> AccessPolicyFactory -> *AuthorizationPolicy
-> PermissionGateway -> PermissionService -> PermissionRepository -> DB
Front Controller (web/index.php)
-> app(UiAccessService::class)
-> UiCapabilityMap::LAYOUT -> $viewAuth['layout']
Action
-> app(UiAccessService::class)
-> UiCapabilityMap::* -> $viewAuth['page']
-> templates/** (nur Darstellung)
```
## Absolute No-Go's
- SQL in View-Dateien
- Business-Logik in Actions
- Tenant-Scope als UI-Only-Check
## Vertiefung
- Vollständiger Kompass: `/docs/architektur.md`
- Regeln für `lib/**`: `/docs/lib-standards.md`
- RBAC-Rechte erweitern: `/docs/rbac-permissions-playbook.md`

View File

@@ -0,0 +1,35 @@
# Erste Änderung (UI)
Letzte Aktualisierung: 2026-02-24
## Ziel
Eine kleine, sichere UI-Änderung durchziehen, ohne Architekturbruch.
## Standardablauf
1. Zielseite und Datenquelle identifizieren (`pages/admin/**`).
2. Falls neue Daten nötig: Endpoint/Repository erweitern.
3. UI in `*.phtml` anpassen (nur Rendering).
4. Texte über `t('...')` ergänzen.
5. Manuell testen (Liste, Filter, Save, Rechte).
## Done-Kriterien
- Kein SQL in Views
- Keine Business-Logik in Templates
- Rechte-/Scope-Verhalten unverändert korrekt
## Checks
```bash
docker compose exec php vendor/bin/phpunit
docker compose exec php vendor/bin/phpstan analyse -c phpstan.neon --no-progress
```
Bei JS-Änderungen zusätzlich Browser-Smoke mit DevTools-Console (keine JS-Errors).
## Vertiefung
- Detailleitfaden: `/docs/erste-aenderung.md`
- UI-Standards: `/docs/entwickler-checkliste.md`

View File

@@ -0,0 +1,34 @@
# Erste Änderung (Backend)
Letzte Aktualisierung: 2026-02-24
## Ziel
Eine kleine Backend-Änderung korrekt über Repository und Service umsetzen.
## Standardablauf
1. SQL/Leselogik im Repository erweitern.
2. Business-Regel im Service ergänzen.
3. Action anpassen und Response stabil halten.
4. Bei API-Änderungen OpenAPI aktualisieren.
## Regeln
- Repository kennt keine HTTP-/Session-Logik.
- Service kennt keine Superglobals/Redirects.
- Externe API bleibt UUID-first.
- Service-Auflösung in `pages/**` via `app(Foo::class)`.
- Keine direkten `new ...Factory()` in `lib/Service/**` außerhalb `*Factory.php`.
## Checks
```bash
docker compose exec php vendor/bin/phpunit
docker compose exec php vendor/bin/phpstan analyse -c phpstan.neon --no-progress
```
## Vertiefung
- `lib/**`-Regeln: `/docs/lib-standards.md`
- API-Prozess: `/docs/07-api-erweitern.md`

View File

@@ -0,0 +1,31 @@
# Security und Tenant-Scope
Letzte Aktualisierung: 2026-02-24
## Ziel
Die wichtigsten Sicherheitsregeln verstehen, bevor Features erweitert werden.
## Pflichtmodell pro Request
1. Authentifizierung (eingeloggt?)
2. Permission-Check (darf der User das?)
3. Tenant-Scope-Check (darf er es im Ziel-Tenant?)
## Muss-Regeln
- Keine sicherheitsrelevanten Checks nur im Frontend.
- Denials je Endpunkt-Semantik als `403` oder `404`.
- Sensible Daten nicht in Fehlern oder Logs leaken.
## Quick-Checklist
- Permission vorhanden und geprüft?
- Tenant-Bindung serverseitig geprüft?
- CSRF bei Schreibaktionen aktiv?
- API-Fehlerformat konsistent?
## Vertiefung
- Vollständiges Modell: `/docs/sicherheitsmodell.md`
- Rate Limits: `/docs/anfragelimits.md`

32
docs/07-api-erweitern.md Normal file
View File

@@ -0,0 +1,32 @@
# API erweitern
Letzte Aktualisierung: 2026-02-24
## Ziel
API-Änderungen konsistent ausrollen (Code + Spec + Test).
## Ablauf
1. Endpoint in `pages/api/v1/**` anpassen.
2. Service/Repository bei Bedarf erweitern.
3. OpenAPI in `/docs/openapi.yaml` im selben Change aktualisieren.
4. Endpoint per cURL/Postman gegenprüfen.
## API-Regeln
- UUID-first nach außen
- Einheitliche Fehlerstruktur (`error`, optional `errors`)
- Auth/Scope-Regeln unverändert einhalten
## Minimaltest
```bash
curl -H "Authorization: Bearer <TOKEN>" \
http://localhost:8080/api/v1/me
```
## Vertiefung
- API-Referenz: `/docs/api.md`
- Sicherheitsdetails: `/docs/sicherheitsmodell.md`

View File

@@ -0,0 +1,36 @@
# Advanced: Scheduler und SSO
Letzte Aktualisierung: 2026-02-24
## Ziel
Fortgeschrittene, betriebskritische Themen strukturiert einordnen.
## Scheduler
- Globaler Runner verarbeitet fällige Whitelist-Jobs.
- Neue Jobs werden über Handler + Registry integriert.
- Parallelität wird über Locking begrenzt.
Vertiefung: `/docs/geplante-aufgaben.md`
## User Lifecycle
- Automatisierte Deaktivierung/Löschung nach Policy.
- Audit/Restore-Pfade sind separat abgesichert.
Vertiefung: `/docs/benutzer-lifecycle-policy.md`
## Microsoft SSO
- Login über OIDC-Flow mit Tenant-Bezug.
- Tenant-SSO-Konfiguration und Secret-Handling sind strikt abgesichert.
Vertiefung:
- `/docs/auth-sso-smoke-test.md`
- `/docs/sicherheitsmodell.md`
## Betriebsregel
Diese Themen nur mit Runbook + manuellen Smoke-Tests ändern.

View File

@@ -1,141 +1,68 @@
# Anfragelimits (Rate Limiting)
Letzte Aktualisierung: 2026-02-23
Dieses Dokument beschreibt das aktuelle zentrale Rate-Limiting für Login und API.
Letzte Aktualisierung: 2026-02-24
## Ziel
- Brute-Force auf Login reduzieren.
- API-Missbrauch (Burst/Spam) abfangen.
- Einheitliches Verhalten mit `429` + `Retry-After`.
Brute-Force und API-Spam begrenzen, ohne Verfügbarkeit zu gefährden.
## Technischer Aufbau
## Technischer Kern
### Storage
- Service: `lib/Service/Security/RateLimiterService.php`
- Repository: `lib/Repository/Security/RateLimitRepository.php`
- Storage: `request_rate_limits`
- `scope`, `subject_hash`, `hits`, `window_started_at`, `blocked_until`
Tabelle: `request_rate_limits`
Wichtig:
Spalten:
- `scope`: Technischer Bereich (z. B. `login.password.email_ip`)
- `subject_hash`: SHA-256 Hash des Subjects (keine Klartexte)
- `hits`: Treffer im aktuellen Fenster
- `window_started_at`: Beginn des aktuellen Zeitfensters (UTC)
- `blocked_until`: Block-Ende (UTC), `NULL` wenn nicht geblockt
- Subject wird gehasht (SHA-256), kein Klartext-Key.
- Verhalten ist fail-open bei Storage-Fehlern.
Schema:
- Tabelle wird in `db/init/init.sql` mit angelegt.
- Für Bestandsumgebungen sollte ein idempotentes SQL-Update bereitgestellt werden.
## API-Limits (global)
### Service/Repository
Durchgesetzt in `lib/Http/ApiBootstrap.php`:
- Service: `/lib/Service/Security/RateLimiterService.php`
- Repository: `/lib/Repository/Security/RateLimitRepository.php`
1. IP-only (`api.ip`)
- 120 Requests / 60s
- Block 120s
2. Token+IP (`api.token_ip`)
- 300 Requests / 60s
- Block 120s
Der Service ist absichtlich **fail-open**:
- Wenn DB/Storage fehlschlägt, wird kein harter Lock erzeugt.
- Requests laufen weiter, um Verfügbarkeit nicht zu brechen.
Bei Block:
## API Rate Limiting
- HTTP `429`
- `Retry-After`
- Body `{"error":"rate_limit_exceeded"}`
Integration:
- `/lib/Http/ApiBootstrap.php` (vor Auth)
## Web-Login-Limits
Aktuelle Limits:
In `pages/auth/login().php`:
1. Bearer vorhanden (`token + ip`)
- Scope: `api.token_ip`
- Key: `<selector>|<ip>`
- Limit: `300` Hits pro `60` Sekunden
- Block: `120` Sekunden
1. Resolve-Phase pro IP (`login.resolve.ip`)
- 25 Versuche / 600s
- Block 300s
2. Passwortphase pro E-Mail+IP (`login.password.email_ip`)
- 5 Fehlversuche / 900s
- Block 900s
2. Kein Bearer (`ip-only`)
- Scope: `api.ip`
- Key: `<ip>`
- Limit: `120` Hits pro `60` Sekunden
- Block: `120` Sekunden
Erfolgreicher Passwort-Login setzt den Passwort-Scope zurück.
Bei überschreitung:
- Response: `429`
- Body: `{"error":"rate_limit_exceeded"}`
- Header: `Retry-After: <sekunden>`
## API-Login-Limits
## Login Rate Limiting
In `pages/api/v1/auth/login().php` (zusätzlich zu globalen API-Limits):
Integration:
- `/pages/auth/login().php`
- `/pages/api/v1/auth/login().php` (public Login ohne Bearer)
1. IP (`api.auth.login.ip`)
- 10 Fehlversuche / 600s
- Block 300s
2. E-Mail+IP (`api.auth.login.email_ip`)
- 5 Fehlversuche / 900s
- Block 900s
Aktuelle Limits:
Erfolgreicher API-Login setzt `api.auth.login.email_ip` zurück.
1. Schritt `resolve_email` (IP)
- Scope: `login.resolve.ip`
- Key: `<ip>`
- Limit: `25` Hits pro `600` Sekunden
- Block: `300` Sekunden
## Smoke-Test
2. Schritt `login_password` (email + ip)
- Scope: `login.password.email_ip`
- Key: `<lowercase-email>|<ip>`
- Limit: `5` Fehlversuche pro `900` Sekunden
- Block: `900` Sekunden
Verhalten:
- Bei Block: HTTP `429` + `Retry-After`.
- UI zeigt generische Meldung: `Too many login attempts. Please wait and try again.`
- Bei erfolgreichem Passwort-Login wird der Passwort-Scope für diesen Key zurückgesetzt.
### API-Login (`/api/v1/auth/login`)
Zusätzlich zum allgemeinen API-Limit in `ApiBootstrap` gelten eigene Login-Limits:
1. Schritt `login_ip` (IP)
- Scope: `api.auth.login.ip`
- Key: `<ip>`
- Limit: `10` Fehlversuche pro `600` Sekunden
- Block: `300` Sekunden
2. Schritt `login_email_ip` (email + ip)
- Scope: `api.auth.login.email_ip`
- Key: `<lowercase-email>|<ip>`
- Limit: `5` Fehlversuche pro `900` Sekunden
- Block: `900` Sekunden
Verhalten:
- Endpoint bleibt public (kein Bearer erforderlich).
- Bei Block: HTTP `429` + `Retry-After`.
- Bei erfolgreichem API-Login wird `api.auth.login.email_ip` für den Key zurückgesetzt.
## Datenfluss pro Request
1. Key bilden (`scope` + Subject)
2. Subject hashen (`sha256`)
3. Bestehenden Datensatz lesen
4. Falls `blocked_until > now`: direkt blocken
5. Fenster prüfen (`window_started_at + windowSeconds`)
6. Hits erhöhen
7. Bei `hits > max`: `blocked_until = now + blockSeconds`
8. Ergebnis liefern (`allowed`, `retry_after`)
## Operative Hinweise
- Zeitbasis ist UTC (`gmdate`).
- Tabelle kann wachsen; in V1 gibt es noch keinen automatischen Cleanup für alte Rate-Limit-Zeilen.
- Bei Bedarf kann ein Wartungsjob alte/obsolete Zeilen periodisch löschen.
## Smoke-Test (manuell)
API:
1. Schnell >120 Requests/min ohne Bearer gegen `/api/v1/*`
2. Erwartung: `429` mit `Retry-After`
Login:
1. Mehrere falsche Passwortversuche für dieselbe Email+IP
2. Erwartung nach 5 Fehlversuchen: `429` und Sperre für 15 Minuten
## Erweiterungen (nächster Schritt)
- Limits in Settings pflegbar machen (statt Hardcode).
- Optional stricter Mode in Production (fail-closed nur für API).
- Cleanup-Job für `request_rate_limits`.
- Optional Logging/Stats für geblockte Requests (z. B. im Admin-Stats Modul).
1. API ohne Token >120 Requests/min gegen `/api/v1/*` -> `429` erwartet.
2. Web-Login mit falschem Passwort >5 pro E-Mail+IP -> `429` erwartet.
3. API-Login mit falschen Credentials wiederholt -> `429` erwartet.

View File

@@ -1,6 +1,6 @@
# REST-API (v1)
Letzte Aktualisierung: 2026-02-21
Letzte Aktualisierung: 2026-02-25
## Single Source of Truth
@@ -15,6 +15,19 @@ Diese Datei ist die verbindliche technische Referenz für:
- Statuscodes
- Auth-Header
## Fehlerhülle (API-Standard)
Fehlerantworten nutzen ein einheitliches Schema:
- `ok: false`
- `request_id: <uuid>`
- `error_code: <stable_machine_code>`
- `details: { ... }`
- kein Legacy-Feld `error` oder `errors`
`request_id` wird zusaetzlich als Header `X-Request-Id` geliefert.
Die `request_id` kommt zentral aus `RequestContext` und korreliert API-Fehler mit Audit-Einträgen.
## API-Doku im Admin
- UI: `/admin/api-docs`

View File

@@ -1,58 +1,95 @@
# Architektur-Kompass
Letzte Aktualisierung: 2026-02-23
Letzte Aktualisierung: 2026-02-25
Kurze, verbindliche Architekturübersicht für den aktuellen Stand.
## Ziel
## Begriffe (kurz)
Verbindliche Kurzregeln für Architekturentscheidungen.
- `Tenant`: Mandant, isolierter Daten- und Berechtigungsraum.
- `Department`: optionale organisatorische Untereinheit innerhalb eines Tenants.
- `Role`: Bündel von Berechtigungen.
- `Permission`: einzelne Berechtigung wie `users.view`.
- `Policy`: zentrale Autorisierungsregel für eine konkrete Fähigkeit.
- `Ability`: technischer Name einer Policy-Regel, z. B. `admin.users.edit.submit`.
- `AuthorizationDecision`: Ergebnis einer Policy (`allow/deny`, HTTP-Status, Fehlercode, Attribute).
- `Tenant-Scope`: Regel, ob ein User eine Ressource im Ziel-Tenant sehen oder ändern darf.
- `Request`: einzelner HTTP-Aufruf.
- `Action`: Datei unter `pages/**`, verarbeitet den Request.
- `Service`: Fachlogik unter `lib/Service/**`.
- `Repository`: SQL- und Mapping-Schicht unter `lib/Repository/**`.
- `UUID-first`: externe API gibt nur UUIDs aus, keine internen IDs.
- `Contract-Test`: Test für Architektur- und Schnittstellenregeln.
## Systembild
## Zielbild (Stand heute)
- Server-gerenderte Admin-App + REST-API unter `/api/v1`.
- Multi-Tenant-Isolation ist Standard.
- Autorisierung läuft zentral über Policies.
- Externe API-Verträge sind UUID-first.
- Server-gerenderte App plus REST-API unter `/api/v1`.
- Strikte Multi-Tenant-Isolation.
- Zentrale Autorisierung über `AuthorizationService` und Policies.
- API-Verträge sind UUID-first.
## Request-Flow
## Request-Flow (verbindlich)
1. `web/index.php` lädt Bootstrap, Routing und Konfiguration.
2. Firewall, Session, Locale und Access-Control laufen.
3. Action unter `pages/**` nimmt Request an.
4. Action delegiert Fachlogik an Services.
5. Services nutzen Repositories für Datenzugriff.
6. Response wird als HTML, JSON oder Datei ausgegeben.
1. `web/index.php` bootstrappt Routing/Konfiguration und initialisiert den `AppContainer`.
2. Guard/Firewall/Session/Locale greifen.
3. Action in `pages/**` nimmt Input an.
4. Action delegiert an Service.
5. Service nutzt Repository für Datenzugriff.
6. Response wird als HTML/JSON/Datei ausgegeben.
## Schichtregeln (MUSS)
- `pages/**`: Input, Auth/AuthZ, Service-Aufruf, Response. Keine duplizierte Business-Logik.
- `lib/Service/**`: Fachlogik und Orchestrierung. Keine Superglobals, keine Header/Redirect-Ausgabe.
- `lib/Repository/**`: SQL, Filter, Paging, Mapping. Keine HTTP-, Session- oder Policy-Logik.
- `templates/**`: nur Rendering.
- `pages/**`: Input, Auth/AuthZ, Orchestrierung, Response.
- `lib/Service/**`: Fachlogik und Anwendungsfluss.
- `lib/Repository/**`: SQL, Filter, Paging, Mapping.
- `templates/**`: Rendering ohne Business-Logik.
- Templates konsumieren nur vom Action-Layer vorbereitete View-Daten (`$viewAuth`, Form/ViewModel), keine direkten Permission-Helper.
- Service-Auflösung in Actions/Helpers nur über `app(Foo::class)`.
- In Actions bevorzugt direkte Klassen-Bindings (`app(Service::class)`) statt Factory-Ketten.
- In `pages/admin/**` keine `Factory::class`-Referenzen.
- Request-Input in Actions nur über `requestInput()` lesen.
- Validierungsfehler über `FormErrors` führen und für APIs mit `ApiResponse::validationFromFormErrors(...)` ausgeben.
- `Flash` nur für Redirect-/Outcome-Meldungen verwenden, nicht als primäre Validation-Struktur.
- Listen-Endpoints in `pages/**/data().php` sind GET-only (`gridRequireGetRequest()`).
- Grid-Query-Parameter (`limit`, `offset`, `order`, `dir`) in `data().php` über `gridQueryLimitOffset()` und `gridQueryOrderDir()` normalisieren.
## Sicherheits- und API-Regeln (MUSS)
## Instanziierungsregeln (MUSS)
- Autorisierung nur zentral über Policies.
- Tenant-Scope nicht lokal nachbauen.
- Scope-Denials pro Endpunkt-Semantik als `403` oder `404`.
- API-Fehler einheitlich: `error`, optional `errors`.
- OpenAPI wird bei API-Änderungen im selben Merge aktualisiert.
- Composition Root: `web/index.php` + `lib/App/registerContainer.php`
(→ Details: `/docs/di-container.md`).
- `new ...Factory()` nur im Composition Root und in `*Factory.php`.
- In `lib/Service/**` außerhalb von `*Factory.php` keine direkten `new ...Factory()`.
- In `lib/Service/**` außerhalb von `*Factory.php` keine direkten `new ...Service()`, `new ...Gateway()` oder `new ...Repository()`.
- Legacy-Helper (`accessServicesFactory()`, `userServicesFactory()` usw.) sind entfernt und werden nicht mehr genutzt.
## Merge-Mindestchecks
## API- und Security-Regeln (MUSS)
- `docker compose exec php vendor/bin/phpunit`
- `docker compose exec php vendor/bin/phpstan analyse -c phpstan.neon --no-progress`
- Tenant-Scope und Permission nur serverseitig erzwingen.
- Denials je Endpunkt als `403` oder `404` modellieren.
- API-Fehlerformat konsistent halten (`error`, optional `errors`).
- OpenAPI bei API-Änderungen im selben Merge aktualisieren.
## RBAC-UI-Standard (MUSS)
- Ability-Entscheidungen laufen serverseitig über `AuthorizationService`.
- Für Layout/Partials werden globale Capabilities zentral über `UiAccessService::layoutCapabilities()` + `UiCapabilityMap::LAYOUT` bereitgestellt.
- Für einzelne Views setzen Actions explizit `$viewAuth['page']` (bevorzugt über `UiCapabilityMap` + `UiAccessService::pageCapabilities()`).
- In `templates/**` keine direkten `can('...')`- oder Permission-Key-Checks.
## Mindestchecks vor Merge
```bash
docker compose exec php vendor/bin/phpunit
docker compose exec php vendor/bin/phpstan analyse -c phpstan.neon --no-progress
```
Bei JS-Änderungen zusätzlich Browser-Smoke mit DevTools-Console (keine JS-Errors).
Struktur-Gates zusätzlich:
```bash
(rg 'new\s+[^\s(]+Factory\(' lib/Service || true) | wc -l
(rg --glob '!**/*Factory.php' 'new\s+[^\s(]+(Service|Gateway|Repository)\(' lib/Service || true) | wc -l
(rg "\b(accessServicesFactory|directoryServicesFactory|userServicesFactory|authServicesFactory|tenantServicesFactory|settingServicesFactory|userRepositoryFactory|authRepositoryFactory|authGatewayFactory)\(" pages lib templates web || true) | wc -l
(rg -n 'new\s+[^\s(]+(Service|Gateway|Repository)\(' pages -g '*.php' || true) | wc -l
(rg -n '\bDB::' pages -g '*.php' || true) | wc -l
(rg -n 'app\([^\n]*Factory::class\)->create' pages/admin -g '*.php' || true) | wc -l
(rg -n 'Factory::class' pages/admin -g '*.php' || true) | wc -l
```
Architektur-Contract:
```bash
docker compose exec php vendor/bin/phpunit tests/Architecture/CoreStarterkitContractTest.php
```
## Vertiefung
- `/docs/lib-standards.md`
- `/docs/sicherheitsmodell.md`
- `/docs/rbac-permissions-playbook.md`

View File

@@ -0,0 +1,56 @@
# Betriebscheck mit `bin/doctor.php`
Letzte Aktualisierung: 2026-02-25
## Ziel
`bin/doctor.php` ist der schnelle Gesundheitscheck fuer eine Instanz.
Er prueft unter anderem:
- ENV-Validierung (Pflichtkeys + Formate)
- AppContainer-Bootstrap
- Datenbankverbindung + Kern-Tabellen
- Schreibbarkeit von `APP_STORAGE_PATH`
- RBAC-Basisrechte
- Admin-Rollen-Zuweisung
- Scheduler-Heartbeat (Warnung, falls alt/fehlend)
## Aufruf
Lokal (wenn PHP + DB direkt erreichbar sind):
```bash
php bin/doctor.php
```
Im Docker-Setup:
```bash
docker compose exec php sh -lc "php bin/doctor.php"
```
## Exit-Code
- `0`: keine harten Fehler
- `1`: mindestens ein `FAIL`-Check
`WARN` fuehrt nicht zu Exit `1`, sollte aber geprueft werden.
## Typischer Einsatz im Betrieb
1. Direkt nach Neuinstallation / Deployment.
2. Bei 4xx/5xx-Symptomen als erster Schritt.
3. Nach Konfigurations- oder RBAC-Aenderungen.
## Beispielausgabe
```text
Minty Doctor Report
===================
[OK] Environment validation: all required env keys and formats are valid
[OK] Database connectivity: connection established
[WARN] Scheduler heartbeat: last heartbeat 900s ago (result=ok)
Summary: ok=7 warn=1 fail=0
```

65
docs/codex-prompts.md Normal file
View File

@@ -0,0 +1,65 @@
# Codex Prompt-Standards
Letzte Aktualisierung: 2026-03-04
## Ziel
Wiederverwendbare Prompt-Muster fuer konsistente Planung und Implementierung mit den Skills `core-guardrails` und `starterkit-planner`.
## Prompt: Implementierung (strict)
```text
Nutze die Skills core-guardrails und starterkit-planner.
Implementiere folgende Aufgabe strikt nach den Starterkit-Guardrails:
<aufgabe>
Pflicht:
1) Schichtgrenzen, Input/Validation, RBAC/UI-Contracts einhalten.
2) Minimalen Scope aendern, keine impliziten Nebenwirkungen.
3) Relevante Quality Gates ausfuehren und Ergebnis berichten.
4) Bei Regelkonflikt klar als Blocker markieren statt Workaround.
```
## Prompt: Refactor / Zentralisierung
```text
Nutze die Skills core-guardrails und starterkit-planner.
Ziel: Duplikate reduzieren und Standards zentralisieren, ohne Verhalten zu brechen.
Aufgabe:
<aufgabe>
Liefere:
1) Entscheidungsmatrix (Optionen + Tradeoffs)
2) Gewaehlten Ansatz mit Begruendung
3) Decision-complete Umsetzungsplan
4) Test- und Migrationsabsicherung
```
## Prompt: Architekturplan (decision complete)
```text
Nutze die Skills core-guardrails und starterkit-planner.
Erstelle einen decision-complete Plan fuer:
<vorhaben>
Format ist verpflichtend:
1) Ziel + Success Criteria
2) In/Out-of-Scope
3) Oeffentliche Interfaces / Contracts
4) Implementierungsschritte
5) Tests / Abnahme
6) Risiken / Migration
7) Annahmen / Defaults
```
## Out of Scope: Extensions
Die Extension-Mechanik (Ablage, Hooks, Lifecycle) ist aktuell nicht standardisiert.
Regel:
1. Keine impliziten Extension-Standards erfinden.
2. Erweiterungen explizit als Out-of-Scope kennzeichnen.
3. Nur dokumentieren, was bereits verbindlich im Repo festgelegt ist.

106
docs/di-container.md Normal file
View File

@@ -0,0 +1,106 @@
# Dependency Injection Container
## Was ist das und wozu brauchen wir es?
Statt dass jede Action-Datei ihre Services selbst zusammenbaut
(`new UserAccountService(new UserRepo(), new AuditRepo(), ...)`),
gibt es einen zentralen "Service-Speicher": den **AppContainer**.
Der Container weiß, wie er jeden Service bauen muss —
aber er baut ihn erst, wenn er zum ersten Mal gebraucht wird (lazy).
Wird der gleiche Service in derselben Anfrage nochmal angefragt,
bekommt man dieselbe Instanz zurück (Singleton pro Request).
## Services abrufen — der `app()` Helper
In jeder Action-Datei (`pages/**/*.php`) steht der Container über
die globale Hilfsfunktion `app()` zur Verfügung:
```php
$userService = app(\MintyPHP\Service\User\UserAccountService::class);
$authService = app(\MintyPHP\Service\Access\AuthorizationService::class);
```
Den vollständigen Klassennamen (FQN) als Schlüssel verwenden —
so gibt es keine Namenskollisionen und IDEs können Autocomplete nutzen.
`app()` wirft eine `RuntimeException` wenn der Service nicht registriert ist.
Das ist absichtlich laut, damit fehlende Registrierungen sofort auffallen.
## Aufbau: Registrars
Beim Start der Anwendung läuft `lib/App/registerContainer.php`.
Es erstellt den Container und ruft 8 **Registrars** nacheinander auf:
| Registrar | Was er registriert |
|---|---|
| `RepositoryFactoryRegistrar` | Alle Repository-Factories (SQL-Schicht) |
| `ServiceFactoryRegistrar` | Alle Service-Factories (Business-Logik, mit Abhängigkeiten) |
| `AccessRegistrar` | RBAC-spezifische Services (Roles, Permissions, Gateways) |
| `AuthRegistrar` | Auth-spezifische Services (Login, SSO, API-Tokens) |
| `DirectoryRegistrar` | LDAP/Entra-Directory-Services |
| `UserRegistrar` | User-spezifische Services und Gateways |
| `SettingsRegistrar` | Settings-Services und Gateways |
| `AppServicesRegistrar` | Konkrete Top-Level-Services, die direkt in Actions genutzt werden |
Jeder Registrar implementiert das Interface `ContainerRegistrar::register(AppContainer $container)`.
## Wie Abhängigkeiten aufgelöst werden
```php
// In ServiceFactoryRegistrar:
$container->set(UserServicesFactory::class, static fn (AppContainer $c) =>
new UserServicesFactory(
$c->get(AuditServicesFactory::class), // wird lazy aufgelöst
$c->get(UserRepositoryFactory::class),
$c->get(UserGatewayFactory::class),
$c->get(DatabaseSessionRepository::class)
)
);
```
Der Container übergibt sich selbst als `$c` an die Factory-Funktion —
so werden alle Abhängigkeiten automatisch und lazy aus dem Container gezogen.
Zirkuläre Abhängigkeiten werden über `static fn (): Foo => $c->get(Foo::class)` aufgelöst
(der Aufruf wird verzögert, bis er wirklich gebraucht wird).
## Neuen Service registrieren
**1. Service und ggf. Factory erstellen** (wie bisher in `lib/Service/`)
**2. Im passenden Registrar eintragen:**
```php
// In AppServicesRegistrar (für Services, die direkt in Actions genutzt werden):
$container->set(MeinNeuerService::class, static fn (AppContainer $c) =>
new MeinNeuerService(
$c->get(UserServicesFactory::class),
$c->get(SettingGateway::class)
)
);
```
**3. In der Action nutzen:**
```php
$meinService = app(\MintyPHP\Service\MeinNeuerService::class);
```
**Faustregel: welcher Registrar?**
- Neue *Repository-Factory*`RepositoryFactoryRegistrar`
- Neue *Service-Factory* (mit Abhängigkeiten) → `ServiceFactoryRegistrar`
- Konkreter *Service*, der direkt in Actions per `app()` abgerufen wird → `AppServicesRegistrar`
- Klar einer Domain zugeordnet (Auth, Access, Directory, User, Settings) → der passende Domain-Registrar
## Wichtige Regeln
- **Nie** `new SomeService(...)` direkt in Action-Dateien schreiben —
immer `app(SomeService::class)` nutzen.
- **Nie** Factory-Ketten in Actions (`app(SomeFactory::class)->createService()`) —
den konkreten Service direkt registrieren und per `app()` holen.
- Der Container lebt **pro HTTP-Anfrage** — er wird in `web/index.php` initialisiert
und über `setAppContainer()` im globalen State abgelegt.
- In CLI-Skripten (`bin/`) wird der Container separat initialisiert (s. `bin/scheduler-run.php`).
- In `lib/Service/**` außerhalb von `*Factory.php` niemals `new ...Factory()`,
`new ...Service()` oder `new ...Repository()` direkt instanziieren.

View File

@@ -16,3 +16,4 @@ Die Docker-Dokumentation ist in zwei klare Pfade getrennt:
- Für tägliche Entwicklung immer mit `docker-compose.yml` arbeiten.
- Für Serverbetrieb ausschließlich `docker-compose.prod.yml` verwenden.
- Änderungen an Domain/TLS nur in der Produktivdoku und den Produktivdateien pflegen.
- Nach Start oder Deployment einmal `docker compose exec php sh -lc "php bin/doctor.php"` ausführen.

View File

@@ -1,6 +1,6 @@
# Docker lokal
Letzte Aktualisierung: 2026-02-21
Letzte Aktualisierung: 2026-03-03
## Ziel
@@ -35,6 +35,21 @@ docker compose up --build -d
docker compose logs -f nginx php
```
## Security Header in Dev
Der lokale Nginx liefert bewusst dieselbe Security-Header-Baseline wie Produktion,
mit einer Ausnahme:
- Kein `Strict-Transport-Security` (HSTS) im Dev-HTTP-Setup.
Aktive Header in Dev:
- `X-Content-Type-Options: nosniff`
- `X-Frame-Options: SAMEORIGIN`
- `Referrer-Policy: strict-origin-when-cross-origin`
- `Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=(), usb=()`
- `X-Permitted-Cross-Domain-Policies: none`
## Nützliche Befehle
Neustart:

View File

@@ -1,6 +1,6 @@
# Docker produktiv
Letzte Aktualisierung: 2026-02-21
Letzte Aktualisierung: 2026-03-03
## Ziel
@@ -40,6 +40,22 @@ Zertifikate ablegen:
- `/docker/nginx/certs/fullchain.pem`
- `/docker/nginx/certs/privkey.pem`
### Security Header Baseline
Die produktive Nginx-Konfiguration setzt folgende Header serverseitig:
- `X-Content-Type-Options: nosniff`
- `X-Frame-Options: SAMEORIGIN`
- `Referrer-Policy: strict-origin-when-cross-origin`
- `Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=(), usb=()`
- `X-Permitted-Cross-Domain-Policies: none`
- `Strict-Transport-Security: max-age=31536000`
Hinweise:
- HSTS wird nur im HTTPS-Serverblock gesetzt.
- `includeSubDomains` und `preload` sind absichtlich nicht gesetzt.
## 3) Produktion starten
```bash

View File

@@ -1,62 +1,61 @@
# Dokumentation erweitern
Letzte Aktualisierung: 2026-02-21
Letzte Aktualisierung: 2026-02-24
## Ziel
Diese Seite beschreibt den Standardprozess, um die Projektdokumentation sauber, konsistent und dauerhaft wartbar zu erweitern.
Neue Seiten schnell, konsistent und langfristig wartbar ergänzen.
## Single Source of Truth
## Informationsarchitektur (verbindlich)
- Für Doku-Navigation und Doku-Suche ist **`/docs/index.md`** die einzige Quelle.
- Nur Dateien, die dort eingetragen sind, erscheinen in:
- `/admin/docs/...`
- globaler Suche (Kategorie `Documentation`)
- Vollsuche `/search`
- Der sichtbare Name im Docs-Aside und in der Suche kommt aus dem **H1-Titel** der Datei.
`/docs/index.md` ist die einzige Navigationsquelle für:
## Standardablauf für neue Doku-Dateien
- Admin-Doku (`/admin/docs/...`)
- Docs-Suche
- Vollsuche (`/search`)
1. Neue Datei unter `docs/*.md` anlegen (Slug nur `a-z`, `0-9`, `-`).
2. Erste Zeile als H1 setzen (`# Titel`).
3. Direkt darunter Datum setzen:
- `Letzte Aktualisierung: YYYY-MM-DD`
4. Datei in `docs/index.md` unter passender Kategorie eintragen:
- Format: ``/docs/<slug>.md``
5. Kurzbeschreibung in `docs/index.md` ergänzen (1 Zeile, klarer Nutzen).
6. Relevante Querverweise in bestehenden Docs ergänzen.
Hauptbereiche in genau dieser Reihenfolge:
## Struktur- und Stilregeln
1. Lernpfad (chronologisch)
2. Konzepte (Explanation)
3. Aufgabenanleitungen (How-to)
4. Referenz (Reference)
5. Betrieb (Operations)
6. Mitwirken (Contributor Guide)
- Pro Datei genau ein H1.
- H1 ist der kanonische Anzeigename (Navigation + Suche).
- Abschnittsstruktur über H2/H3 (nicht direkt mit H4 starten).
- Ein Abschnitt = eine klare Aussage.
- Befehle in Codeblöcken mit realen, lauffähigen Beispielen.
- Dateipfade immer als klickbare Code-Referenz (`/path/to/file`).
- Keine veralteten Relikte stehen lassen ("Legacy", alte Namen, alte Routen).
## Format pro Seite (kurz und klar)
## Wichtig für die Suche
Jede neue Seite soll kompakt bleiben:
- Doku-Suche indexiert `docs/*.md` live.
- Treffer entstehen aus:
- Dokumenttitel
- Abschnittsüberschriften (H1/H2/H3)
- Abschnittstext
- Links springen auf `admin/docs/{slug}#anchor`.
- Aussagekräftige H2/H3-Überschriften verbessern Suchtreffer deutlich.
- genau ein H1
- `Letzte Aktualisierung: YYYY-MM-DD`
- `## Ziel` (1-2 Sätze)
- maximal 3-6 H2-Abschnitte
- kurze Listen statt Fließtextblöcke
- konkrete Befehle oder Pfade
Richtwert: zuerst kurz, Details als Verweis auf Referenzseiten.
## Standardablauf
1. Datei in `docs/*.md` anlegen (`a-z`, `0-9`, `-`).
2. Seite im passenden Bereich in `docs/index.md` eintragen.
3. Aussagekräftige H2/H3-Überschriften setzen (für Suche).
4. Relevante Querverweise ergänzen.
5. Datum aktualisieren.
## Qualitätscheck vor Merge
1. Ist die Datei in `docs/index.md` eingetragen?
2. Ist `Letzte Aktualisierung` vorhanden und korrekt?
3. Stimmen alle genannten Pfade/Routen/Permissions mit dem Code?
4. Gibt es mindestens einen konkreten, praxisnahen Beispielaufruf?
5. Sind neue Begriffe konsistent zu bestehender Terminologie?
1. Seite ist in `docs/index.md` eingetragen.
2. Struktur ist kurz, klar, ohne Redundanz.
3. Pfade, Routen, Permissions stimmen mit dem Code.
4. Instanziierungsregeln sind konsistent mit `/docs/architektur.md` und `/docs/lib-standards.md`.
5. Mindestens ein konkretes Beispiel ist enthalten.
6. Begriffe sind konsistent mit `/docs/02-domain-glossar.md`.
## Häufige Fehler
- Datei erstellt, aber nicht in `docs/index.md` registriert.
- Überschrift/Slug später umbenannt, alte Links nicht angepasst.
- Allgemeine Aussagen ohne konkrete Beispiele.
- API-/Permission-Änderung im Code, aber Doku nicht nachgezogen.
- Seite existiert, ist aber nicht im Index registriert.
- Lernpfad wird mit Referenztext überladen.
- Lange Detailtexte ohne klare Handlungsschritte.
- API-/Permission-Änderung im Code ohne Doku-Update.

View File

@@ -1,89 +1,90 @@
# Entwickler-Checkliste
Letzte Aktualisierung: 2026-02-23
Letzte Aktualisierung: 2026-03-04
## Ziel
Kurze Definition of Done vor Merge.
## Vor dem Coden
- [ ] Betroffene Schicht klar? (`Repository`, `Service`, `pages`, `templates`, `web/js`)
- [ ] Permission- und Tenant-Scope-Auswirkungen verstanden?
- [ ] Bestehendes Partial/Helper wiederverwendbar?
- [ ] Bei `lib/**`-Änderungen Regeln in `/docs/lib-standards.md` geprüft
- [ ] Betroffene Schichten klar (`Repository`, `Service`, `pages`, `templates`, `web/js`)
- [ ] Permission- und Tenant-Scope-Auswirkungen geklärt
- [ ] Relevante bestehende Partials/Helper geprüft
## Beim Implementieren
- [ ] Keine DB-Calls in Views (`*.phtml`)
- [ ] SQL nur im Repository
- [ ] Business-Regeln nur im Service
- [ ] Actions bleiben schlank (Input/Guard/Orchestrierung)
- [ ] Neue UI-Texte mit `t('...')`
- [ ] Kein SQL in `*.phtml`
- [ ] SQL nur in `lib/Repository/**`
- [ ] Business-Logik nur in `lib/Service/**`
- [ ] Neue UI-Texte über `t('...')`
- [ ] Listen-Filter konsistent: IDs/Enums als Select/MultiSelect statt Freitext
- [ ] Für persistierte Status/Outcomes nur zentrale Taxonomien aus `lib/Domain/Taxonomy/*` nutzen (keine lokalen String-Literale)
- [ ] Für neue/angepasste Liste: `filter-schema.php` angelegt/aktualisiert
- [ ] Listen-Titlebar über `templates/partials/app-list-titlebar.phtml` (kein inline `<div class="app-list-titlebar">`)
- [ ] Listen-Tabs über `templates/partials/app-list-tabs.phtml` (kein inline `<div class="app-list-tabs">`)
- [ ] Purge-Titlebar-Action über `templates/partials/app-list-purge-action.phtml` (kein inline Purge-Form/Button)
- [ ] Detail-Formulare mit Standardverhalten markieren (`data-standard-detail-form="1"`)
- [ ] Detail-Titlebar nutzt Unsaved/Shortcut-Contract (`data-detail-unsaved-message`, `data-detail-save-primary`)
- [ ] Detail-Aktionen nutzen Action-Policy-Contract (`data-detail-action-policy`, `data-detail-action-kind`, `data-detail-confirm-message`) statt inline `confirm(...)`
- [ ] Detail-Validierungsfehler über `templates/partials/app-details-validation-summary.phtml` (kein inline Error-Loop)
- [ ] Für Detail-Validation-Summary strukturierte Feldfehler setzen (`$validationSummaryErrors = $errorBag->toArray()`)
- [ ] Toolbar über `renderGridFilterToolbar(...)`, kein manuelles Filter-HTML
- [ ] Listen über `initStandardListPage(...)` initialisiert (kein direkter `createServerGrid(...)`-Standardpfad)
- [ ] Für Listen `searchToolbarFilterSchema` + `drawerToolbarFilterSchema` + `filterChipMeta` aus `index().php` bereitgestellt
- [ ] Keine direkten `gridFiltersFromSchema(...)`- oder `initListFilterExperience(...)`-Aufrufe in Listen-Templates
- [ ] Bei Filter-Drawer-Seiten: `filters.mode: 'drawer'` im Wrapper gesetzt
- [ ] Bei Search-only-Listen: kein Drawer-Markup, Quick Search bleibt live
- [ ] Aktive Filter-Chips vorhanden (`data-active-filter-chips`) und Remove/Clear funktionieren
- [ ] Kein Legacy-Toggle in Listen (`data-toolbar-toggle`, `data-filter-overflow`, `data-filter-toggle`)
- [ ] Falls Save-Filter vorhanden: gespeicherter Zustand stammt aus Applied State (`gridConfig.baseUrl()`)
- [ ] Service-Auflösung in `pages/**`/`templates/**`/`lib/Support/**` nur via `app(Foo::class)`
- [ ] Keine `new ...Factory()` in `lib/Service/**` außerhalb `*Factory.php`
- [ ] Keine direkten `new ...Service()`, `new ...Gateway()`, `new ...Repository()` in `lib/Service/**` außerhalb `*Factory.php`
## UI/UX-Konsistenz
## Sicherheits- und Scope-Checks
- [ ] Edit-Seite nutzt `app-details-titlebar`
- [ ] Titelzeile enthält nur Primäraktionen (`Save`, `Save & close`)
- [ ] Sekundäraktionen liegen im Aside-Block `Actions` (nicht als verstecktes Titlebar-Dropdown)
- [ ] Erster Tab = `Master data`
- [ ] Status über `app-visibility-status-field`
- [ ] Delete über `app-danger-zone-delete-field` (falls relevant)
- [ ] Aside-Meta über Audit/IDs-Partial (falls relevant)
- [ ] Bei Tenant-Zusatzfeldern: Definitionen nur über eigene Actions, nicht über Tenant-Hauptsave
- [ ] Bei User-Zusatzfeldern: Felder tenantweise gruppiert, Scope serverseitig geprüft, Werte optional in V1
- [ ] `is_filterable` nur für `select`, `multiselect`, `boolean`, `date` verwenden
- [ ] Bei Theme-Logik: Tenant-Overrides (`default_theme`, `allow_user_theme`) gegen globale Settings geprüft
- [ ] Bei Tenant-SSO: `tenants.sso_manage` geprüft und Secrets niemals im Klartext rendern/loggen
- [ ] Bei API-Audit: nur Metadaten loggen (keine Bodies/Tokens), Redaction aktiv, Permission `api_audit.view` geprüft
- [ ] Rechte serverseitig geprüft
- [ ] Tenant-Scope serverseitig geprüft
- [ ] Bei RBAC-Änderungen Playbook angewendet (`/docs/rbac-permissions-playbook.md`)
- [ ] Schreibaktionen mit POST + CSRF abgesichert
- [ ] Confirm-Dialoge über Datencontract (`data-confirm-message` / `data-detail-confirm-message`), keine inline `confirm(...)`
- [ ] Keine sensiblen Daten in Fehlern/Logs
- [ ] Für `pages/**/data().php`: GET-only + Query-Normalisierung (`gridRequireGetRequest`, `gridParseFilters`)
- [ ] Für `pages/**/data().php`: Filter über `gridParseFilters(...)` statt Inline-Parsing
- [ ] Bei MultiSelect-Filtern: CSV-Query (`foo=a,b`) + serverseitige `IN`-Filter mit Limit
- [ ] Keine Listen-Alias-Keys (`*_id` neben `*_ids`) neu einführen
- [ ] Input nur über `requestInput()` und Validation über `FormErrors` (`/docs/request-input-validation.md`)
## Assets
## Qualitätschecks
- [ ] Neue CSS-Datei im passenden Ordner (`components`, `layout`, `pages`, `vendor-overrides`)
- [ ] Falls nötig in `config/assets.php` als Gruppe registriert
- [ ] Seitenstil per `Buffer::set('style_groups', ...)` aktiviert
## Tests und Checks
- [ ] PHPUnit:
- [ ] `docker compose exec php vendor/bin/phpunit --bootstrap vendor/autoload.php`
- [ ] PHPStan:
- [ ] `docker compose exec php vendor/bin/phpstan analyse -c phpstan.neon --no-progress`
- [ ] ESLint:
- [ ] `npx eslint web/js`
- [ ] i18n-Test:
- [ ] `docker compose exec php vendor/bin/phpunit --bootstrap vendor/autoload.php tests/I18n/TranslationKeysTest.php`
- [ ] Bei Microsoft-SSO-Änderungen:
- [ ] Für Bestandsumgebungen passendes idempotentes SQL-Update bereitgestellt und ausgeführt
- [ ] `APP_CRYPTO_KEY` in `.env` gesetzt (32 Byte key, hex oder base64)
- [ ] Wenn `phone`/`mobile`/`avatar` Sync genutzt wird: Graph `User.Read` in Entra verfügbar und consented
- [ ] Profil-Sync-Felder im Tenant-SSO-Tab geprüft (`sync_profile_on_login`, `sync_profile_fields`)
- [ ] Login Smoke-Test (`login?tenant=<slug>` -> Microsoft Start/Callback)
- [ ] Bei API-Audit-Änderungen:
- [ ] Für Bestandsumgebungen passendes idempotentes SQL-Update bereitgestellt und ausgeführt
- [ ] Audit-Logging für 2xx/4xx/5xx manuell geprüft
- [ ] Purge-Action (`admin/api-audit/purge`) prüft Permission + POST + CSRF
- [ ] Bei Global-Search-Änderungen:
- [ ] SQL-Resource in `lib/Support/Search/SearchSqlResourceProvider.php` ergänzt (`resources`, ggf. `tenantScopeFilters`)
- [ ] UI-Meta in `lib/Support/Search/SearchUiMetaProvider.php` ergänzt (`listUrl`, `iconForKey`)
- [ ] Mapping in `lib/Support/Search/SearchItemMapperProvider.php` ergänzt (`mapPreviewItem`, `mapResultItem`)
- [ ] Spezialressourcen in `lib/Support/Search/SearchSpecialResourceProvider.php` ergänzt (falls `docs`/`hotkeys`-ähnlich)
- [ ] `lib/Support/SearchConfig.php` nur als Fassade konsistent delegierend belassen
- [ ] Aside-Search-Eintrag in `templates/partials/app-main-aside.phtml` mit passendem `data-search-key` vorhanden
- [ ] Permission + Tenant-Scope geprüft (keine unerlaubten Treffer)
- [ ] Preview (`admin/search/data`) und Vollsuche (`search?search=...`) manuell geprüft
- [ ] `docker compose exec php vendor/bin/phpunit`
- [ ] `docker compose exec php vendor/bin/phpstan analyse -c phpstan.neon --no-progress`
- [ ] `docker compose exec php vendor/bin/phpunit tests/Architecture/CoreStarterkitContractTest.php`
- [ ] bei JS-Änderungen: Browser-Smoke mit DevTools-Console (keine JS-Errors)
- [ ] bei API-Änderungen: `docs/openapi.yaml` aktualisiert
- [ ] Struktur-Gates geprüft (`rg` für Instanziierungsregeln)
- [ ] `pages/**`: kein `new ...Service|Gateway|Repository`, kein `DB::`
- [ ] `pages/admin/**`: keine `app(...Factory::class)->create...`-Ketten
- [ ] `pages/admin/**`: keine `Factory::class`-Referenzen
## Manuelle Smoke-Tests
- [ ] Login/Logout
- [ ] Eine Admin-Liste mit Filter + Sortierung
- [ ] Eine Edit-Seite mit Save + Danger-Zone-Aktion (falls vorhanden)
- [ ] Tenant-gebundene Sicht (Address Book / Users / Departments)
- [ ] Global Search
- [ ] betroffene Admin-Liste + Filter
- [ ] betroffene Edit-Seite + Save
- [ ] relevante Tenant-Sicht
- [ ] ggf. Global Search / API-Flow
## Abschluss
- [ ] Dokumentation aktualisiert (`README.md`, `docs/*` bei Bedarf, inkl. `docs/frontend-javascript.md`)
- [ ] Keine toten Imports/unused Code hinzugefügt
- [ ] Änderung ist für den nächsten Entwickler in 1-2 Minuten nachvollziehbar
- [ ] Doku aktualisiert (`README.md`, `docs/*`)
- [ ] keine toten Imports/ungenutzten Helfer
- [ ] Änderung in 1-2 Minuten nachvollziehbar
## Periodisch (bei Feature-Entfernung / vor Release)
## Vertiefung
- [ ] Ungenutzte Composer-Pakete prüfen:
- [ ] `docker compose exec php vendor/bin/composer-unused`
- `/docs/lib-standards.md`
- `/docs/sicherheitsmodell.md`
- `/docs/rbac-permissions-playbook.md`
- `/docs/dokumentation-erweitern.md`

View File

@@ -1,6 +1,6 @@
# Leitfaden für die erste Änderung
Letzte Aktualisierung: 2026-02-22
Letzte Aktualisierung: 2026-02-24
## Ziel
@@ -17,7 +17,9 @@ Ein neuer Entwickler soll den ersten Change strukturiert und reproduzierbar umse
- Schreiboperation → `lib/Repository/User/UserWriteRepository.php`
3. Grid-Spalte in `pages/admin/<modul>/index(default).phtml` ergänzen (z. B. `pages/admin/users/index(default).phtml`)
4. i18n-Key für Spaltennamen setzen
5. `php -l` + manueller UI-Test
5. Service-Auflösung in Action-Dateien nur über `app(Foo::class)` nutzen (kein `new ...Factory()` in `pages/**`)
6. In `data().php`: GET-only (`gridRequireGetRequest()`) und Grid-Querys über `gridQueryLimitOffset()`/`gridQueryOrderDir()` normalisieren
7. `php -l` + manueller UI-Test
### Done-Kriterien
@@ -25,6 +27,7 @@ Ein neuer Entwickler soll den ersten Change strukturiert und reproduzierbar umse
- Neue Werte im Endpoint dokumentiert/benannt
- Sortierung und Mapping im Grid konsistent
- Bei neuen Filtern: Query-Param, Repository-Filter und Grid-State konsistent
- Instanziierungsstandard eingehalten (`app(...)`, keine Legacy-Helper)
## Beispiel 2: API-Response erweitern
@@ -32,7 +35,7 @@ Ein neuer Entwickler soll den ersten Change strukturiert und reproduzierbar umse
1. Endpoint in `pages/api/v1/...` anpassen (z. B. `pages/api/v1/me/index().php` oder `pages/api/v1/users/show($id).php`)
2. Permission- und Tenant-Scope-Prüfungen beibehalten
3. Optional Service/Repository ergänzen
3. Optional Service/Repository ergänzen, Instanziierung über `app(...)`/Factory-Standards
4. `docs/openapi.yaml` aktualisieren
5. `docs/api.md` mit Beispiel aktualisieren
6. Bei neuen Berechtigungen: `PermissionService` + `init.sql` synchron halten, für Bestandsumgebungen idempotentes SQL-Update in `db/updates/*.sql` bereitstellen

View File

@@ -10,7 +10,6 @@ Diese Seite bringt neue Entwickler in unter 45 Minuten zu einem lauffähigen lok
- Docker + Docker Compose
- Git
- Node.js + npm (für ESLint)
- Zugriff auf dieses Repository
## 1) Projekt lokal starten
@@ -69,9 +68,10 @@ curl -H "Authorization: Bearer <TOKEN>" \
```bash
docker compose exec php vendor/bin/phpunit
docker compose exec php vendor/bin/phpstan analyse -c phpstan.neon --no-progress
npx eslint web/js
```
Bei JS-Änderungen zusätzlich Browser-Smoke mit DevTools-Console (keine JS-Errors).
## 6) Nächste Doku-Schritte
- Täglicher Workflow: `/docs/lokale-entwicklung.md`

View File

@@ -14,6 +14,20 @@ UI zeigt alte Daten/Assets oder Verhalten wirkt stale.
docker compose restart php nginx
```
## Erstdiagnose mit Doctor
### Symptom
Unklar, ob das Problem aus ENV, DB, Storage, RBAC oder Scheduler kommt.
### Lösung
```bash
docker compose exec php sh -lc "php bin/doctor.php"
```
Bei `FAIL` zuerst diese Punkte beheben, danach erneut ausführen.
## Login/API verhalten sich unerwartet
### Symptom

View File

@@ -1,6 +1,6 @@
# Frontend JavaScript
Letzte Aktualisierung: 2026-02-21
Letzte Aktualisierung: 2026-03-04
## Ordnerstruktur
@@ -84,18 +84,185 @@ Hinweise:
- `order` nur bekannte Sort-Key-Spalten
- Bei Search-/Filter-Änderung wird auf Seite 1 zurückgesetzt.
## Linting
### Filter UX 2.0 (Globaler Standard)
Prüfen mit:
Für Listen gilt standardmäßig:
```bash
npx eslint web/js
1. Quick Search sichtbar außerhalb des Drawers.
2. Restliche Filter im rechten Drawer (`data-filter-drawer-*`).
3. Drawer arbeitet `apply-first` (Draft bis `Apply`).
4. Aktive Filter werden als Chips angezeigt (`data-active-filter-chips`).
5. Filter-Reset erfolgt über Chips (`Clear all filters`), nicht im Drawer.
JS-Verkabelung:
- Standard-Entrypoint ist `initStandardListPage(...)` aus `web/js/core/app-grid-factory.js`.
- Der Wrapper kapselt intern `createServerGrid(...)` + (bei `mode: 'drawer'`) `initListFilterExperience(...)`.
- Bei Apply/Clear wird URL mit `pushState` fortgeschrieben.
- Save-Filter-Usecases lesen den angewandten Zustand über `gridConfig.baseUrl()`.
- Chip-State-Logik ist zentral im Wrapper verdrahtet (Default: `app-list-filter-state.js`).
Search-only-Ausnahme:
- Listen ohne zusätzliche Filter initialisieren über denselben Wrapper mit `mode: 'search-only'` (ohne Drawer/Chips).
### Standard-List-API
Für Listen-Templates gilt:
- `initStandardListPage({ grid, filters, hooks })` verwenden.
- `grid.filterSchema` serverseitig aus `filter-schema.php` (`gridSchemaClientFilters(...)`) übergeben.
- Optional zusätzliche Spezialfilter über `grid.extraFilters`.
- Bei Drawer-Listen `filters.mode: 'drawer'`, bei Search-only `filters.mode: 'search-only'`.
Regel:
- keine direkten Aufrufe von `gridFiltersFromSchema(...)` in Listen-Templates
- keine direkten Aufrufe von `initListFilterExperience(...)` in Listen-Templates
- keine inline `filters: [...]`-Arrays in Listen-Templates
- MultiSelect-Filter immer als CSV-Key (`*_ids`, `actions`, `event_types` etc.)
Serverseitig passendes Template-Markup:
- normale Select-Filter über `selectFilter(...)`
- MultiSelect-Filter über `multiSelectFilter(...)`
### List Titlebar Standard
Für Listen-Titlebars gilt:
- Titlebar über `templates/partials/app-list-titlebar.phtml` rendern.
- Titel über `$listTitle` setzen.
- Aktionen über `$listTitleActionsHtml` als Slot (captured HTML) übergeben.
Beispiel:
```php
<?php
$listTitle = t('Users');
ob_start();
?>
<a role="button" class="primary" href="admin/users/create"><?php e(t('Create user')); ?></a>
<?php
$listTitleActionsHtml = ob_get_clean();
require templatePath('partials/app-list-titlebar.phtml');
?>
```
Auto-fix (nur für sichere Rule-Fixes):
### List Tabs Standard
```bash
npx eslint web/js --fix
Für Tenant- oder Kontext-Tabs in Listen gilt:
- Tabs über `templates/partials/app-list-tabs.phtml` rendern.
- Tab-Items über `$listTabsItems` mit `href`, `label`, `active` übergeben.
- Optional `id` über `$listTabsId` setzen.
### List Purge Action Standard
Für Purge-Buttons in Titlebar-Actions gilt:
- Purge-Markup über `templates/partials/app-list-purge-action.phtml` rendern.
- Formular/CSRF/Button/Confirm werden zentral übergeben via:
- `$listPurgeEnabled`
- `$listPurgeFormId`
- `$listPurgeAction`
- `$listPurgeConfirmMessage`
- `$listPurgeButtonLabel`
### Aside Grouped Navigation + Persisted Details State
Für gruppierte Sidebar-Navigation mit einklappbaren `details` gilt:
- Zentraler Storage-Helper: `web/js/core/app-details-open-state.js` (`initPersistedDetailsGroup(...)`).
- Standardgruppen über `details[data-details-key]`.
- Persistenz-Aktivierung über:
- `data-details-storage="..."` (allgemein, z. B. Detailseiten)
- `data-aside-details-storage="..."` (Aside-Panels)
- Optionales Aktiv-Verhalten im Aside über `data-aside-details-open-active="1"`:
- aktive Gruppe bleibt immer offen
- effektive Open-Keys = `persistedOpenKeys activeOpenKeys`
Admin-Aside-Standard:
- Storage-Key: `aside-admin-sections-v1`
- Gruppenkeys:
- `admin-organization`
- `admin-roles-permissions`
- `admin-automation`
- `admin-monitoring`
- `admin-logs`
- `admin-system`
### Detail Page Standard
Für Create/Edit-Detailseiten gilt:
- Hauptformular über `data-standard-detail-form="1"` explizit opt-in markieren.
- Initialisierung läuft zentral über `initStandardDetailPage(...)` aus `web/js/core/app-detail-page-factory.js`.
- Auto-Init erfolgt über `web/js/components/app-standard-detail-page.js` (in `app-init.js` eingebunden).
Standardverhalten:
- Dirty-State über serialisierte `FormData`-Diffs (initial vs. aktuell).
- `beforeunload`-Warnung nur bei ungespeicherten Änderungen.
- Back/Cancel-Link in `.app-details-titlebar h1 a` fragt bei Dirty-State per Confirm.
- `Cmd/Ctrl+S` triggert primären Save/Create-Button (`data-detail-save-primary="1"`).
- Submit-Lock/Confirm laufen zentral über die Detail-Action-Policy.
- Bei Dirty-State wird in der Titlebar automatisch ein kleiner Hinweis mit Feldanzahl gezeigt.
### Detail Action Policy
Für standardisierte Detailseiten gilt zusätzlich:
- `initStandardDetailPage(...)` bindet intern `initDetailActionPolicy(...)` aus `web/js/core/app-details-action-policy.js`.
- Aktivierung pro Seite über `data-detail-action-policy="1"` auf `.app-details-titlebar` (Default im Shared-Partial aktiv).
- Confirm nicht mehr inline per `onclick/onsubmit`, sondern über `data-detail-confirm-message`.
- Aktionssemantik über `data-detail-action-kind` (z. B. `save`, `save-close`, `delete`, `danger`).
- Optionaler Lock-Override über `data-detail-submit-lock="none"` (Default: `form`).
### Global Confirm Actions
Für allgemeine Form-/Button-Confirms außerhalb der Detail-Policy gilt:
- zentral über `web/js/components/app-confirm-actions.js`
- Confirm-Text über `data-confirm-message`
- keine inline `onclick="return confirm(...)"` / `onsubmit="return confirm(...)"` verwenden
### Detail Validation Summary Standard
Für serverseitige Formularfehler in Create/Edit-Templates gilt:
- Fehlerausgabe über `templates/partials/app-details-validation-summary.phtml`.
- Keine inline `<div class="notice" data-variant="error">`-Blöcke mit `foreach ($errors as $error)` mehr.
- Standardtitel kommt aus `t('Please review the following errors')`.
- In Action-Handlern strukturierte Feld-Errors als `$validationSummaryErrors = $errorBag->toArray();` bereitstellen.
Template-Beispiel:
```php
<?php
$validationSummaryErrors = $validationSummaryErrors ?? ($errors ?? []);
require templatePath('partials/app-details-validation-summary.phtml');
?>
```
Hinweis: `--fix` behebt format-/syntaxnahe Themen (z. B. fehlende Klammern), aber nicht automatisch alle Logikprobleme.
Optional strukturierte Einträge:
- `['message' => '...', 'fieldName' => 'email']`
- `['message' => '...', 'fieldId' => 'email']`
- `['email' => ['Required']]` (FormErrors-ähnliche Map)
JS-Verhalten über `initStandardDetailPage(...)`:
- Klick auf Validation-Link fokussiert das passende Feld.
- Bei vorhandener Summary wird initial das erste betroffene Feld (oder Summary) fokussiert.
- Betroffene Felder werden zentral mit `aria-invalid="true"` und `aria-describedby` (Summary-ID) markiert.
## Laufzeit-Check (ohne Node)
Prüfen im Browser:
1. DevTools öffnen (Console, optional Preserve log aktivieren).
2. Kernseiten laden (`/admin/users`, `/address-book`, Audit-Listen).
3. Filter setzen/zurücksetzen, Doppelklick-Navigation und Bulk-Aktionen testen.
4. Erwartung: keine JavaScript-Errors und keine Unhandled Promise Rejections.

View File

@@ -1,232 +1,74 @@
# Geplante Aufgaben
Letzte Aktualisierung: 2026-02-21
Letzte Aktualisierung: 2026-02-24
## Ziel
Zentrale Verwaltung geplanter Aufgaben im Admin-Bereich.
Scheduler-Betrieb und Job-Erweiterung kurz und verlässlich dokumentieren.
- Ein zentraler Runner (`bin/scheduler-run.php`) läuft minütlich.
- Produktion typischerweise per Cron.
- Lokale Docker-Umgebung über den Compose-Service `scheduler`.
- Die konkreten Jobs sind Whitelist-basiert (kein frei ausführbarer Bash-Text aus DB).
- V1 startet mit `user_lifecycle_run`.
## Kernkomponenten
## Tabellen
- Runner: `bin/scheduler-run.php`
- Orchestrierung: `lib/Service/Scheduler/SchedulerRunService.php`
- Job-Konfiguration: `lib/Service/Scheduler/ScheduledJobService.php`
- Registry: `lib/Service/Scheduler/ScheduledJobRegistry.php`
- Schedule-Berechnung: `lib/Service/Scheduler/ScheduleCalculator.php`
- `scheduled_jobs`
- Konfiguration pro Job (enabled, schedule, timezone, next/last run, letzter Fehler).
- `scheduled_job_runs`
- Historie einzelner Runs (trigger, status, Dauer, error/result summary).
- `scheduler_runtime_status`
- Einzeilige Runtime-Health des globalen Runners (Heartbeat, letzter Runner-Status, letzter Fehlercode).
## Datenmodell
Run-Log-Retention:
- `scheduled_jobs`: Job-Konfiguration und letzter Laufstatus
- `scheduled_job_runs`: Historie einzelner Läufe
- `scheduler_runtime_status`: Heartbeat/Runner-Status (eine Zeile, `id = 1`)
- 90 Tage (Purge in Admin verfügbar).
Retention:
- Run-Logs: 90 Tage (`ScheduledJobService::purgeRunsExpired`)
- System-Audit: konfigurierbar über `system_audit_retention_days` (Job `system_audit_purge`)
## Laufmodell
1. Runner startet und synchronisiert Registry-Jobs (`ensureSystemJobs`).
2. Globaler MySQL-Lock wird gesetzt (`scheduled_jobs_runner`).
3. Fällige Jobs (`enabled=1`, `next_run_at <= now`) werden abgearbeitet.
4. Jeder Lauf wird als `success`/`failed`/`skipped` protokolliert.
5. `next_run_at` wird neu berechnet.
6. Heartbeat wird aktualisiert, Lock wird freigegeben.
## Scheduling
Unterstützt:
- `hourly` (1..24)
- `daily` (1..365 + Uhrzeit)
- `weekly` (1..52 + Uhrzeit + Wochentage)
`catchup_once` steuert Nachholverhalten:
- `1`: genau ein Nachhol-Lauf, danach normal weiter
- `0`: kein Backlog-Sturm, verpasste Fenster werden übersprungen
## Rechte
- `jobs.view`: Bereich ansehen
- `jobs.manage`: Job-Konfiguration speichern
- `jobs.run_now`: Job manuell ausführen
- `jobs.view`: Übersicht sehen
- `jobs.manage`: Job speichern
- `jobs.run_now`: Job manuell starten
- Purge der Run-Logs: `settings.update`
Purge der Run-Logs bleibt unter `settings.update`.
Registrierte Core-Jobs:
## Betriebsmodell
- `user_lifecycle_run`
- `system_audit_purge`
Cron-Eintrag (Beispiel):
## Neuen Job registrieren (kurz)
```bash
* * * * * docker compose exec php php bin/scheduler-run.php
```
1. Handler in `lib/Service/Scheduler/Handler/*` erstellen (`ScheduledJobHandlerInterface`).
2. Job-Key + Handler in `ScheduledJobRegistry` ergänzen.
3. Abhängigkeiten in `SchedulerServicesFactory` verdrahten.
Der Runner:
1. sichert sich globalen Lock (`GET_LOCK('scheduled_jobs_runner', 0)`) nur ein Runner aktiv
2. holt globale Due-Jobs (`enabled=1`, `next_run_at <= now`)
3. führt Jobs kontrolliert aus
4. schreibt Heartbeat in `scheduler_runtime_status`
5. schreibt Run-Log und aktualisiert Job-Status
6. berechnet `next_run_at` neu
7. gibt den Lock frei
## Runner-Heartbeat
- `scheduler_runtime_status` enthält genau eine Zeile (`id = 1`).
- Jeder Aufruf von `bin/scheduler-run.php` aktualisiert diese Zeile:
- `last_heartbeat_at`
- `last_result` (`ok`, `lock_not_acquired`, `unexpected_error`)
- `last_error_code` (optional)
- Dadurch wächst die Tabelle nicht mit der Zeit; sie ist ein laufendes Statusfenster.
Verwendung in Admin/Stats:
- Kachel **Cron runner active** ist `Aktiv`, wenn `last_heartbeat_at` jünger als 3 Minuten ist.
- Ist der Heartbeat älter oder fehlt, zeigt die Kachel `Inaktiv`.
- Manuelle Runs aus der UI (`Run now`) zählen nicht als Cron-Liveness.
## Scheduling (V1)
Unterstützte Typen:
- `hourly` (Intervall 1..24)
- `daily` (Intervall 1..365 + Uhrzeit)
- `weekly` (Intervall 1..52 + Uhrzeit + Wochentage)
`catchup_once`:
- Wenn ein Lauf verpasst wurde, wird genau ein Run nachgeholt.
- Danach wird normal in die Zukunft weitergeplant (kein Backlog-Sturm).
## Neuen Job registrieren (Entwickler-Guide)
### Architektur-überblick
Jeder Job ist eine eigene Handler-Klasse, die `ScheduledJobHandlerInterface` implementiert.
Die Registry ist eine Map von `job_key` zu Handler-Instanz sie enthält keine
job-spezifische Logik.
```
lib/Service/Scheduler/
Handler/
ScheduledJobHandlerInterface.php ← Vertrag für alle Handler
UserLifecycleJobHandler.php ← Referenz-Implementierung
ScheduledJobRegistry.php ← Handler-Map (einzige Datei die bei neuen Jobs angefasst wird)
SchedulerServicesFactory.php ← verdrahtet Abhängigkeiten zentral
```
### Schritt-für-Schritt
**1. Handler-Klasse erstellen**
Datei: `lib/Service/Scheduler/Handler/MeinJobHandler.php`
```php
<?php
namespace MintyPHP\Service\Scheduler\Handler;
use MintyPHP\Service\MeinBereich\MeinService;
class MeinJobHandler implements ScheduledJobHandlerInterface
{
public function __construct(private readonly MeinService $meinService)
{
}
public function definition(): array
{
return [
'label' => 'Mein Job',
'description' => 'Kurze Beschreibung was der Job macht',
'default_enabled' => 1,
'default_timezone' => defined('APP_TIMEZONE') ? (string) APP_TIMEZONE : 'UTC',
'default_schedule_type' => 'daily',
'default_schedule_interval' => 1,
'default_schedule_time' => '03:00',
'default_schedule_weekdays_csv' => null,
'default_catchup_once' => 1,
'allowed_schedule_types' => ['daily', 'weekly'],
];
}
public function execute(?int $actorUserId): array
{
$result = $this->meinService->run($actorUserId);
if (!($result['ok'] ?? false)) {
$errorCode = (string) ($result['error'] ?? 'job_failed');
$status = $errorCode === 'lock_not_acquired' ? 'skipped' : 'failed';
return [
'status' => $status,
'error_code' => $errorCode,
'error_message' => null,
'result' => [],
];
}
return [
'status' => 'success',
'error_code' => null,
'error_message' => null,
'result' => [
'processed_count' => (int) ($result['processed_count'] ?? 0),
'duration_ms' => (int) ($result['duration_ms'] ?? 0),
],
];
}
}
```
**2. Job in der Registry eintragen**
Datei: `lib/Service/Scheduler/ScheduledJobRegistry.php`
```php
// Konstante hinzufügen:
public const MEIN_JOB = 'mein_job';
// In __construct() eine Zeile hinzufügen:
$this->handlers = [
self::USER_LIFECYCLE_RUN => new UserLifecycleJobHandler($userLifecycleService),
self::MEIN_JOB => new MeinJobHandler($meinService), // ← neu
];
```
**3. Factory-Verdrahtung ergänzen**
Datei: `lib/Service/Scheduler/SchedulerServicesFactory.php`
- `MeinService` instanziieren bzw. bereitstellen
- beim Erzeugen von `ScheduledJobRegistry` den Service mit übergeben
Danach legt `ensureSystemJobs()` den Job beim nächsten Scheduler-Aufruf automatisch
in der Datenbank an.
### Regeln für Handler
**`definition()`**
- Kein `job_key`-Feld der Array-Key in der Registry-Map ist der Job-Key.
- `default_timezone`: Am besten `APP_TIMEZONE`-Konstante verwenden, Fallback `'UTC'`.
- `allowed_schedule_types`: Nur Typen erlauben, die der Job sinnvoll unterstützt.
Ein Job der stundlich keinen Sinn ergibt, sollte `hourly` nicht listen.
**`execute()`**
- Muss immer ein Array mit `status`, `error_code`, `error_message`, `result` zurückgeben.
- `status` darf nur `'success'`, `'failed'` oder `'skipped'` sein.
- `error_message` darf maximal 255 Zeichen lang sein (VARCHAR-Limit in DB).
- `result` wird als JSON im Run-Log gespeichert nur JSON-serialisierbare Werte.
- Wenn der zugrundeliegende Service einen eigenen Lock hat und `lock_not_acquired` meldet,
sollte der Handler `status = 'skipped'` zurückgeben (kein echter Fehler).
**Namenskonvention**
- Dateiname: `MeinJobHandler.php` (PascalCase + `Handler`-Suffix)
- Konstante in Registry: `SCREAMING_SNAKE_CASE` (z.B. `AUDIT_PURGE_RUN`)
- Job-Key in DB: `snake_case` (z.B. `audit_purge_run`)
### Referenz-Implementierung
`lib/Service/Scheduler/Handler/UserLifecycleJobHandler.php` ist die vollständige
Referenz-Implementierung mit internem Service-Lock, Fehlerbehandlung und
job-spezifischem `result`-Payload. Der Objektgraph wird über
`lib/Service/Scheduler/SchedulerServicesFactory.php` gebaut.
---
## Registrierte Jobs
| job_key | Handler-Klasse | Default-Schedule |
|----------------------|-----------------------------|---------------------|
| `user_lifecycle_run` | `UserLifecycleJobHandler` | täglich 02:15 |
---
Danach wird der Job bei `ensureSystemJobs()` automatisch in `scheduled_jobs` angelegt.
## Troubleshooting
- `lock_not_acquired`:
- Ein anderer Runner läuft bereits.
- Job bleibt auf `running`:
- stale-running Schutz erlaubt nach 2 Stunden wieder neue Runs (Schwellwert in `ScheduledJobRepository->markRunning`).
- Kein fälliger Job:
- `next_run_at`, `enabled`, `timezone` und schedule Konfiguration prüfen.
- `lock_not_acquired`: ein anderer Runner läuft.
- Job bleibt `running`: stale-running Schutz greift nach 2 Stunden.
- Job läuft nicht: `enabled`, `next_run_at`, `timezone`, Schedule prüfen.

View File

@@ -1,149 +1,71 @@
# Globale Suche erweitern
Letzte Aktualisierung: 2026-02-21
Letzte Aktualisierung: 2026-02-24
Diese Doku erklärt, wie neue Entitäten sauber in die globale Suche integriert werden.
## Ziel
## Überblick
Neue Search-Resource korrekt in Aside-Preview und Vollsuche integrieren.
Es gibt zwei Suchausgaben mit derselben Datenquelle:
## Architektur
1. **Aside-Preview** (`admin/search/data`)
- zeigt je Resource Count + bis zu 5 Preview-Einträge.
2. **Vollseite Suche** (`search/data` -> `/search`)
- zeigt gemischte Trefferliste über alle Resources.
Zwei Endpunkte teilen dieselbe Konfiguration:
Die zentrale Fassade liegt in:
- Aside-Preview: `pages/admin/search/data().php`
- Vollsuche: `pages/search/data().php`
- `/lib/Support/SearchConfig.php`
Zentrale Fassade:
Die eigentliche Logik ist in Provider aufgeteilt:
- `lib/Support/SearchConfig.php`
- Laufzeit-Orchestrierung: `lib/Service/Search/SearchDataService.php`
- SQL-Ausführung: `lib/Repository/Search/SearchQueryRepository.php`
- `/lib/Support/Search/SearchSqlResourceProvider.php`
- `/lib/Support/Search/SearchUiMetaProvider.php`
- `/lib/Support/Search/SearchItemMapperProvider.php`
- `/lib/Support/Search/SearchSpecialResourceProvider.php`
- `/lib/Support/Search/SearchQueryNormalizer.php`
Provider:
## Relevante Dateien
- SQL-Definitionen: `SearchSqlResourceProvider`
- UI-Meta (Icons/URLs): `SearchUiMetaProvider`
- Mapping: `SearchItemMapperProvider`
- Spezialquellen (`docs`, `hotkeys`): `SearchSpecialResourceProvider`
- `/lib/Support/SearchConfig.php`
- stabile Orchestrierungs-Fassade (kompatible Methoden nach außen)
- `/lib/Support/Search/SearchSqlResourceProvider.php`
- Resource-Definitionen (SQL + Permission + Label) + Tenant-Filter
- `/lib/Support/Search/SearchUiMetaProvider.php`
- URL- und Icon-Mapping pro Resource-Key
- `/lib/Support/Search/SearchItemMapperProvider.php`
- Mapping SQL-Row -> Preview-/Result-Item
- `/lib/Support/Search/SearchSpecialResourceProvider.php`
- Spezialressourcen (`docs`, `hotkeys`)
- `/lib/Support/Search/SearchQueryNormalizer.php`
- Query-Normalisierung für LIKE/Score
- `/pages/admin/search/data().php`
- Aside-Preview API (Counts + Preview)
- `/pages/search/data().php`
- Volltext-Treffer API
- `/templates/partials/app-main-aside.phtml`
- Sichtbarer Eintrag im Such-Panel (`data-search-key=...`)
- `/web/js/components/app-global-search.js`
- Render-Logik (generisch; meist keine Anpassung nötig)
- `/lib/Service/Docs/DocsCatalogService.php`
- Zentrale Katalog-/Anchor-Logik für Entwicklerdoku (Single Source of Truth)
## Laufzeitpfad
## Standard-Vorgehen für neue Resource
1. `pages/admin/search/data().php` oder `pages/search/data().php`
2. `SearchDataService` baut Scope/Permission + Resource-Iteration
3. `SearchQueryRepository` führt `countSql`/`previewSql`/`resultSql` aus
4. `SearchConfig` mapped auf API/UI-Items
### 1) Resource in `SearchSqlResourceProvider::resources()` anlegen
## Standardablauf für neue Resource
Neue Struktur mit:
1. Resource in `SearchSqlResourceProvider::resources()` ergänzen:
- `key`, `label`, `permission`
- `countSql`, `previewSql`, `resultSql`
2. Bei Tenant-Daten `tenantScopeFilters()` ergänzen.
3. UI ergänzen:
- `iconForKey()`
- `listUrl()`
4. Mapping ergänzen:
- `mapPreviewItem()`
- `mapResultItem()`
5. Aside-Eintrag in `templates/partials/app-main-aside.phtml` ergänzen (`data-search-key`).
6. i18n-Labels prüfen.
- `key` (z. B. `scheduled-jobs`)
- `label` (übersetzbar via `t(...)`)
- `permission` (z. B. `PermissionService::JOBS_VIEW`)
- `countSql` + `countParams`
- `previewSql` + `previewParams`
- `resultSql` + `resultParams`
## Wichtige Regeln
Regeln:
- Permission immer serverseitig in der Resource setzen.
- Tenant-Scope nicht nur im UI lösen.
- Preview immer mit `limit ?` und ohne sensitive Daten.
- SQL immer mit `... like ? escape '\\'`.
- Bei tenant-sensitiven Entitäten `{{tenantFilter}}` nutzen und in `SearchSqlResourceProvider::tenantScopeFilters()` ergänzen.
- Preview-Query immer mit `limit ?`.
## Docs-Resource
### 2) URL-/Icon-/Mapping ergänzen
Die Entwicklerdoku läuft als Spezialresource `docs`:
- `SearchUiMetaProvider::iconForKey()` erweitern
- `SearchUiMetaProvider::listUrl()` erweitern
- `SearchItemMapperProvider::mapPreviewItem()` ergänzen (falls Spezialformat nötig)
- `SearchItemMapperProvider::mapResultItem()` ergänzen (falls Spezialformat nötig)
Ziel:
- Preview und Volltreffer sollen sprechende Labels zeigen.
- URLs sollen direkt auf sinnvolle Zielseiten gehen.
### 3) Eintrag im Aside-Search-Panel anlegen
In `/templates/partials/app-main-aside.phtml`:
- `<li data-search-key="..." data-search-base="...">` ergänzen
- Anzeige per Permission guarden (z. B. `$canViewJobs`)
Wichtig:
- `data-search-key` muss exakt dem `key` in `SearchConfig` entsprechen.
### 4) i18n prüfen
Neue Labels in:
- `/i18n/default_de.json`
- `/i18n/default_en.json`
## Sicherheits- und Qualitätsregeln
- Permission in `SearchConfig` setzen, nicht nur im Aside.
- Tenant-Scope für tenant-bezogene Daten nicht vergessen.
- Keine sensiblen Daten im Preview-Label.
- Bei optionalen Modulen nur dann Resource aktivieren, wenn Schema vorhanden ist (oder Migration als Pflicht voraussetzen).
## Kurzes Beispiel: `scheduled-jobs`
Integration umfasst:
- Resource in `SearchSqlResourceProvider::resources()`
- Mapping auf `admin/scheduled-jobs/edit/{id}`
- Icon `bi-calendar-check`
- Aside-Search-Item mit `data-search-key="scheduled-jobs"`
- Permission `jobs.view`
## Doku-Resource (`docs`) im System
Die Entwicklerdoku ist als eigene Search-Resource integriert:
- Key: `docs`
- Quelle: `DocsCatalogService::search()`
- Permission: `docs.view`
- Quelle: `docs/*.md` (über `DocsCatalogService`, kein `openapi.yaml`)
- Aside: eigener Block `Documentation` mit Count + Preview
- Vollsuche: Treffer vom Typ `Documentation`
- Ziel-URLs: `admin/docs/{slug}#anchor` (direkter Abschnitt)
- Ziel: `admin/docs/{slug}#anchor`
Wichtig:
## Check vor Merge
- Anchor-Ermittlung für Docs-Seite und Suche läuft über denselben Service.
- Dadurch bleiben Hash-Links stabil und konsistent.
## Checkliste vor Merge
1. `php -l /lib/Support/SearchConfig.php`
2. `php -l /lib/Support/Search/*.php`
3. `php -l /templates/partials/app-main-aside.phtml`
3. Global Search manuell testen:
- Sidebar-Suche zeigt Count + Preview
- Klick auf Resource öffnet richtige Liste
- `/search?search=...` zeigt Volltreffer
4. Permission-Test:
- ohne Permission kein Eintrag/keine Treffer
5. Tenant-Scope-Test (falls relevant)
6. Bei `docs` zusätzlich:
- Preview-Link springt auf richtigen Abschnitt
- Vollsuche zeigt `Documentation`-Treffer mit sinnvollem Snippet
1. `php -l lib/Support/SearchConfig.php`
2. `php -l lib/Support/Search/*.php`
3. Preview testen (`admin/search/data?q=...`)
4. Vollsuche testen (`/search?search=...`)
5. Permission/Scope mit reduziertem User prüfen

View File

@@ -1,88 +1,63 @@
# Importe
Letzte Aktualisierung: 2026-02-22
Letzte Aktualisierung: 2026-02-24
Diese Seite beschreibt den V1 CSV-Import unter `/admin/imports`.
## Ziel
## Technischer Aufbau
CSV-Import (V1) für `users` und `departments` kurz und belastbar dokumentieren.
- Das Import-Modul ist instanzbasiert über `lib/Service/Import/ImportServicesFactory.php` verdrahtet.
- `ImportService` orchestriert Analyze/Preview/Commit.
- `ImportStateStoreService` kapselt den tokenbasierten Session-State (`admin_imports_v1`, TTL 2h).
- `CsvReaderService` und `ImportTempFileService` sind eigene Instanzservices.
- Import-Audit läuft über `ImportAuditService` + `ImportAuditRunRepository`.
## Architektur
- Einstieg: `/admin/imports`
- Orchestrierung: `ImportService`
- Upload/Temp: `ImportTempFileService`
- CSV-Analyse/Stream: `CsvReaderService`
- State (tokenbasiert, Session): `ImportStateStoreService`
- Audit: `ImportAuditService`
## Scope (V1)
- Profile: `users`, `departments`.
- Create-only: bestehende Einträge werden übersprungen.
- CSV mit `,` oder `;`, UTF-8 (BOM wird toleriert).
- Limits: 10MB und 20.000 Datenzeilen.
- Fehlerausgabe nur in der UI (kein Download in V1).
- Profile: `users`, `departments`
- Create-only: bestehende Datensätze werden übersprungen
- CSV-Delimiter: `,` oder `;`
- BOM wird toleriert
- Limits:
- 10 MB Upload
- 20.000 Datenzeilen
- max. 500 Fehlerzeilen in der UI
## Permissions
## Berechtigungen
- `imports.view`: Zugriff auf die Import-Seite.
- `imports.audit.view`: Zugriff auf Import-Protokolle unter `/admin/import-audit`.
- `users.import`: User-Import ausführen.
- `users.import_assignments`: Tenant/Role/Department aus CSV verwenden.
- `departments.import`: Department-Import ausführen.
- `imports.view`
- `users.import`
- `departments.import`
- `users.import_assignments` (nur wenn Assignment-Spalten gemappt werden)
- `imports.audit.view` (Audit lesen)
- `settings.update` (Audit-Purge)
## Ablauf
1. Upload
- Entität wählen (`Users` oder `Departments`) und CSV hochladen.
- Header/Delimiter/Zeilenlimit werden direkt geprüft.
2. Mapping
- CSV-Header auf Zielfelder mappen.
- Pflichtziele sind profilabhängig.
3. Preview (Dry run)
- Keine DB-Schreibvorgänge.
- Summary + erste Fehler (maximal 500 Einträge).
4. Commit
- Zeilenweise Verarbeitung (`best effort`).
- Gültige Zeilen werden erstellt, fehlerhafte reportet.
1. Analyze: Upload + Header/Delimiter/Limit prüfen.
2. Mapping: CSV-Spalten auf Zielfelder mappen.
3. Preview: Dry-Run ohne DB-Write.
4. Commit: zeilenweise Verarbeitung mit Fehleraggregation.
## Assignment-Regeln
## Assignment-Regeln (wichtig)
- Users: optional `tenant`, `role`, `department` (jeweils 1:1).
- Departments: optional `tenant`.
- Identifier-Auflösung:
- Users: UUID zuerst, ID als Fallback.
- Departments: `tenant` ist UUID-only (kein ID-Fallback).
- Nur aktive Datensätze sind gültig.
- Department muss zum Tenant passen.
- Fehlen Assignment-Werte, greifen Defaults aus Settings.
- Ohne `users.import_assignments` wird ein Mapping mit Assignment-Spalten sofort abgelehnt.
- Users: optionale `tenant`/`role`/`department`-Zuordnung.
- Users: Auflösung über UUID, numerische ID als Fallback.
- Departments: `tenant` ist UUID-only.
- Ohne gültige Assignment-Werte greifen Defaults aus Settings.
- Out-of-scope Tenants werden blockiert (`assignment_out_of_scope`).
## Duplikate und Skip-Verhalten
## Temp-Dateien und State
- In-File-Duplikate: erste Zeile gewinnt, spätere `duplicate_in_file`.
- Users:
- bestehende E-Mail: `email_exists`
- Departments:
- bestehender Code: `code_exists`
- bestehende Kombination aus Tenant + Description: `department_exists`
- Temp-Pfad: `APP_STORAGE_PATH/imports/tmp`
- Alte Temp-Dateien werden opportunistisch abgeräumt (24h)
- Import-State-Token läuft nach 2h ab
## Temporäre Dateien
## Audit
- Uploads liegen unter `APP_STORAGE_PATH/imports/tmp`.
- Dateinamen werden randomisiert.
- Cleanup entfernt alte Temp-Dateien opportunistisch (älter als 24h).
## Import-Audit (V1)
- Pro Commit-Lauf wird ein Audit-Run gespeichert (`import_audit_runs`).
- Analyze/Preview werden bewusst nicht protokolliert.
- Gespeichert werden nur Metadaten:
- Profil (`users`/`departments`), Status (`success`/`partial`/`failed`)
- Laufzeit, Counter (`rows_total`, `created`, `skipped`, `failed`)
- User, aktueller Tenant, Dateiname (basename), gemappte Felder
- aggregierte Fehlercodes (kein Zeileninhalt)
- Keine CSV-Zeilenpayload oder PII-Details im Audit.
- Retention: 90 Tage, bereinigbar über `/admin/import-audit` (Purge-Action mit `settings.update`).
## Fehlercodes (V1)
- Struktur: `upload_missing`, `upload_too_large`, `invalid_file_type`, `invalid_csv_header`, `empty_csv`, `max_rows_exceeded`, `mapping_required_missing`, `assignment_permission_required`
- Zeilenvalidierung: `invalid_email`, `invalid_tenant_uuid`, `duplicate_in_file`, `email_exists`, `code_exists`, `department_exists`, `invalid_locale`, `invalid_active`, `invalid_hire_date`, `tenant_not_found`, `role_not_found`, `department_not_found`, `department_tenant_mismatch`, `assignment_out_of_scope`, `create_failed`, `unexpected_error`
- Nur Commit-Läufe werden protokolliert (nicht Analyze/Preview).
- Gespeichert: Metadaten, Counter, Fehlercodes, User/Tenant-Kontext.
- Retention: 90 Tage.

View File

@@ -1,75 +1,93 @@
# Dokumentation
Letzte Aktualisierung: 2026-02-23
Letzte Aktualisierung: 2026-03-04
Diese Dokumente sind für Entwickler gedacht, die die Anwendung warten oder erweitern.
Diese Dokumentation folgt einem klaren Lernpfad (einfach -> fortgeschritten) und trennt Lernen von Nachschlagen.
## Start hier (neue Entwickler)
## Lernpfad (chronologisch)
1. `/docs/erste-schritte.md`
- Setup in 30-45 Minuten, erster Login, erster Smoke-Test.
2. `/docs/lokale-entwicklung.md`
- Täglicher Workflow, Schema-Updates und Qualitätschecks.
3. `/docs/erste-aenderung.md`
- Schritt-für-Schritt für den ersten kleinen Code-Change.
4. `/docs/dokumentation-erweitern.md`
- Wie neue Doku-Dateien sauber erstellt, einsortiert und suchbar gemacht werden.
5. `/docs/entwickler-checkliste.md`
- Finale Checkliste vor Merge/Review.
1. `/docs/00-systemueberblick.md`
- Produktbild, Kernmodule und Lernziel in 10 Minuten.
2. `/docs/01-setup-und-erster-login.md`
- Lokales Setup, erster Login, erster Smoke-Test.
3. `/docs/02-domain-glossar.md`
- Zentrale Begriffe (Tenant, Role, Permission, Scope, Policy).
4. `/docs/03-architektur-request-flow.md`
- Request-Flow, Schichtmodell, Do/Don't.
5. `/docs/04-erste-aenderung-ui.md`
- Erste sichere UI-Änderung im Admin-Bereich.
6. `/docs/05-erste-aenderung-backend.md`
- Erste Backend-/Service-/Repository-Änderung.
7. `/docs/06-security-tenant-scope.md`
- Sicherheitsgrundlagen, Tenant-Scope, Pflichtchecks.
8. `/docs/07-api-erweitern.md`
- API-Änderung sauber mit OpenAPI und Tests umsetzen.
9. `/docs/08-advanced-scheduler-sso.md`
- Fortgeschrittene Themen: Scheduler, Lifecycle, SSO.
## Architektur und Regeln
## Konzepte (Explanation)
- `/docs/architektur.md`
- Startpunkt mit Glossar, Zielbild, Schichtregeln und Sicherheitsleitplanken.
- Architektur-Kompass mit verbindlichen Leitplanken.
- `/docs/di-container.md`
- Wie der AppContainer funktioniert und neue Services registriert werden.
- `/docs/sicherheitsmodell.md`
- Auth, Permissions, Tenant-Scope, Public/API-Zugriffe.
- `/docs/konventionen.md`
- Kurze, verbindliche Arbeitsregeln für Code, API und Qualität.
- `/docs/lib-standards.md`
- Konkrete Do/Don't-Regeln für `lib/**`.
## Frontend
- `/docs/frontend-javascript.md`
- Frontend-JS-Aufbau, Init-Flow, Grid-Integration und URL-State-Sync.
- `/docs/frontend-css.md`
- CSS-Struktur, Layering, Vendor-Overrides und Asset-Einbindung.
## Fach- und Feature-Dokumentation
- `/docs/benutzerdefinierte-felder.md`
- Tenant/User-Zusatzfelder (Typen, Pflege, Filterbarkeit).
- `/docs/importe.md`
- CSV-Import (V1), Mapping, Fehlercodes, Permission-Modell und Import-Audit.
- Vollständiges Sicherheitsmodell (Auth, Permission, Scope).
- `/docs/einstellungen-speicherung.md`
- Source of truth für Settings (DB) und Datei-Cache-Verhalten.
- `/docs/anfragelimits.md`
- Login/API-Limits, Scopes, Block-Verhalten.
- `/docs/benutzer-lifecycle-policy.md`
- Automatische Benutzer-Deaktivierung/Löschung (Policy, Cron, manueller Run).
- `/docs/geplante-aufgaben.md`
- Zentrale geplante Aufgaben (Scheduler), Job-Konfiguration und Runner-Betrieb.
- `/docs/globale-suche.md`
- Globale Suche erweitern (Resource, Permission, Mapping, Aside-Integration).
- Warum Settings in DB + Datei-Cache koexistieren.
## API Referenz
## Aufgabenanleitungen (How-to)
- `/docs/globale-suche.md`
- Neue Resource in der globalen Suche integrieren.
- `/docs/importe.md`
- Importfluss, Regeln und Fehlercodes.
- `/docs/benutzerdefinierte-felder.md`
- Tenant/User-Zusatzfelder korrekt erweitern.
- `/docs/geplante-aufgaben.md`
- Neue Scheduler-Jobs registrieren und betreiben.
- `/docs/auth-sso-smoke-test.md`
- SSO-Funktionalität manuell verifizieren.
- `/docs/anfragelimits.md`
- Rate-Limits verstehen und testen.
- `/docs/rbac-permissions-playbook.md`
- RBAC-Rechte sauber erweitern (neue Permission, neue Ability, UI/API-Fälle).
- `/docs/request-input-validation.md`
- Einheitliches Request/Input- und Validation-Muster für Web + API.
## Referenz (Reference)
- `/docs/api.md`
- API Quick Reference, Auth, Flows, Beispielaufrufe.
- `/docs/openapi.yaml`
- API Source of truth für Swagger UI.
## Betriebswissen
- API-Referenz-Einstieg.
- `/docs/konfiguration.md`
- Alle ENV-Variablen mit Beschreibung, Standardwerten und Produktions-Checkliste.
- `/docs/fehlerbehebung.md`
- Häufige Fehlerbilder und schnelle Lösungen.
- `/docs/auth-sso-smoke-test.md`
- Manueller Smoke-Runbook für Login, Microsoft-Start/Callback, Tenant-SSO-Save und Logout.
- ENV-Variablen und Produktions-Defaults.
- `/docs/konventionen.md`
- Projektweite Konventionen (kurz und verbindlich).
- `/docs/lib-standards.md`
- Regeln speziell für `lib/**`.
- `/docs/benutzer-lifecycle-policy.md`
- Policy- und Laufzeitreferenz für User Lifecycle.
## Betrieb (Operations)
- `/docs/docker-lokal.md`
- Schneller lokaler Docker-Start inkl. typischer Befehle und Troubleshooting.
- Lokaler Docker-Betrieb.
- `/docs/docker-produktiv.md`
- Produktionsbetrieb mit Domain, TLS und `docker-compose.prod.yml`.
- Produktivbetrieb mit Docker.
- `/docs/docker-betrieb.md`
- Einstieg und Navigation zwischen lokaler und produktiver Docker-Doku.
- Übersicht und Entscheidungshilfe für Dev/Prod.
- `/docs/betriebscheck-doctor.md`
- Schneller Systemcheck mit `bin/doctor.php`.
- `/docs/fehlerbehebung.md`
- Häufige Fehler und schnelle Lösungen.
## Mitwirken (Contributor Guide)
- `/docs/lokale-entwicklung.md`
- Tages-Workflow für Entwicklung und Checks.
- `/docs/entwickler-checkliste.md`
- Definition of Done vor Merge.
- `/docs/codex-prompts.md`
- Standard-Prompts für Skill-basierte Planung und Implementierung.
- `/docs/dokumentation-erweitern.md`
- Regeln für konsistente, wachsende Doku.

View File

@@ -15,6 +15,7 @@ Vorlage: `.env.example` — niemals echte Credentials committen.
| `APP_URL` | `http://localhost:8080` | Basis-URL der Anwendung (für Links in E-Mails und Redirects) |
| `APP_ENV` | `dev` | Laufzeitumgebung (`dev` oder `prod`) — beeinflusst Fehlerausgabe und Caching |
| `APP_DEBUG` | `true` | Debug-Modus aktivieren (`true`/`false`) — in Produktion auf `false` setzen |
| `APP_CONFIG_VALIDATE` | `true` | Boot-Validierung für ENV-Konfiguration aktivieren/deaktivieren (`true`/`false`) |
| `APP_TIMEZONE` | `Europe/Berlin` | PHP-Zeitzone für `date_default_timezone_set()` |
| `APP_STORAGE_PATH` | `./storage` | Pfad zum Storage-Verzeichnis für Uploads und Caches |
| `APP_CRYPTO_KEY` | _(leer)_ | Schlüssel für symmetrische Verschlüsselung (64-stelliger Hex-Key **oder** Base64 mit 32 Byte) — **Pflichtfeld in Produktion** |
@@ -101,3 +102,29 @@ Folgende Variablen **müssen** vor Go-Live angepasst werden:
- [ ] `FIREWALL_REVERSE_PROXY=true` falls hinter Nginx/Traefik
- [ ] Alle DB-Credentials auf sichere Werte setzen
- [ ] Alle SMTP-Credentials auf echte Werte setzen
---
## Boot-Validierung (Fail-Fast)
Beim Start validiert die Anwendung die zentrale ENV-Konfiguration sofort in `config/config.php`.
Bei fehlenden oder ungültigen Pflichtwerten stoppt der Boot-Prozess mit klarer Fehlermeldung.
Geprüft werden unter anderem:
- Pflicht-Keys wie `APP_ENV`, `DB_*`, `CACHE_SERVERS`, `SESSION_NAME`, `FIREWALL_*`
- Typen/Formate (`bool`, `int`, `float`, `timezone`, `URL`)
- Konsistenz (`APP_LOCALE` muss in `APP_LOCALES` enthalten sein)
- Production-Regeln:
- `APP_URL` muss gesetzt sein
- `APP_CRYPTO_KEY` muss gesetzt und gültig sein (64 Hex oder Base64/32 Byte)
- `APP_DEBUG=false`
- `TENANT_SCOPE_STRICT=true`
Nur für Notfälle kann die Validierung temporär abgeschaltet werden:
```env
APP_CONFIG_VALIDATE=false
```
Empfehlung: im Normalbetrieb immer `true` lassen.

View File

@@ -1,6 +1,6 @@
# Konventionen (kurz und verbindlich)
Letzte Aktualisierung: 2026-02-23
Letzte Aktualisierung: 2026-02-26
## 1) Code
@@ -14,6 +14,18 @@ Letzte Aktualisierung: 2026-02-23
- Fachlogik nur in `lib/Service/**`.
- `pages/**` bleibt dünn: Input, Auth/AuthZ, Service-Aufruf, Response.
- Neue Domain-Logik als Instanz-Service mit Factory-Verdrahtung.
- Service-Instanzen in Actions/Helpern nur via `app(Foo::class)`.
- `new ...Factory()` nur im Composition Root (`web/index.php` + `lib/App/registerContainer.php`) oder in `*Factory.php`.
- In `lib/Service/**` außerhalb von `*Factory.php` keine direkten `new ...Factory()`, `new ...Service()`, `new ...Gateway()` oder `new ...Repository()`.
- Alte Service-Helper (`accessServicesFactory()`, `userServicesFactory()` usw.) nicht mehr verwenden.
- `pages/**/data().php` nur GET (`gridRequireGetRequest()`), Query-Normalisierung ausschließlich über `gridParseFilters(...)`.
- Für jede Liste ein `filter-schema.php` im selben Verzeichnis; `data().php`, `index().php` und `index(default).phtml` verwenden dieses Schema.
- Filter-Toolbar nur über `renderGridFilterToolbar(...)`, keine manuellen Filter-Inputs/-Selects.
- Client-Filterbindung nur über `gridFiltersFromSchema(...)` (kein inline `filters: [...]`).
- Für Listen gilt Filter UX 2.0: Quick Search sichtbar, restliche Filter im Drawer, aktive Filter-Chips.
- Search-only-Listen (nur Quick Search) verwenden keinen Drawer und bleiben im Live-Modus.
- `data-filter-overflow`, `data-filter-toggle` und `data-toolbar-toggle` sind Legacy und in Listen-Templates nicht mehr erlaubt.
- Listen-IDs nur als CSV-Parameter mit pluralem Key (`*_ids`), keine Alias-Fallbacks.
## 3) Autorisierung
@@ -38,4 +50,4 @@ Letzte Aktualisierung: 2026-02-23
- `docker compose exec php vendor/bin/phpunit`
- `docker compose exec php vendor/bin/phpstan analyse -c phpstan.neon --no-progress`
- bei JS-Änderungen: `npx eslint web/js`
- bei JS-Änderungen: Browser-Smoke mit DevTools-Console (keine JS-Errors)

View File

@@ -1,6 +1,6 @@
# lib-Standards
Letzte Aktualisierung: 2026-02-23
Letzte Aktualisierung: 2026-03-03
Praktische Regeln nur für `lib/**`. Ergänzt `docs/architektur.md`.
@@ -14,26 +14,114 @@ Praktische Regeln nur für `lib/**`. Ergänzt `docs/architektur.md`.
- Verantwortlich für Fachlogik und Orchestrierung.
- Keine Superglobals (`$_POST`, `$_GET`, `$_SESSION`).
- Keine Header- oder Redirect-Ausgabe.
- Keine direkten `DB::...`-Aufrufe im Service.
- Abhängigkeiten per Constructor Injection.
- DB-Locks und Transaktionen über Repository-Schicht (z. B. `DatabaseSessionRepository`).
- Audit-Hooks für fachliche Schreibaktionen laufen in Services (`SystemAuditService`), nicht in Templates.
- API-Request-System-Audit läuft zentral über `ApiSystemAuditReporter` (Lifecycle in `ApiBootstrap` + `ApiResponse`), nicht pro Endpoint.
- Scheduler-Run-/Job-Ereignisse werden zentral im `SchedulerRunService` in `SystemAuditService` geschrieben.
- CLI-System-Audit für zentrale Commands wird am Entrypoint (`bin/**`) geschrieben, mit minimaler Metadata (ohne Argumentwerte).
## 3) Instanziierung
- Domain-Abhängigkeiten mit `new` nur in Factories/Bootstrap.
- Composition Root ist `web/index.php` + `lib/App/registerContainer.php`
(→ Details: `/docs/di-container.md`).
- Domain-Abhängigkeiten mit `new` nur im Composition Root und in `*Factory.php`.
- In `pages/**`, `templates/**`, `lib/Support/**` und Gateway-/Service-Fallbacks nur über `app(Foo::class)` auflösen.
- In Actions bevorzugt direkte Service/Gateway-Bindings (`app(Service::class)`) statt `app(...Factory::class)->create...()`.
- In `pages/admin/**` keine `Factory::class`-Referenzen; nur direkte `app(Service::class)` / `app(Repository::class)`.
- In `lib/Service/**` außerhalb von `*Factory.php` keine direkten `new ...Factory()`.
- In `lib/Service/**` außerhalb von `*Factory.php` keine direkten `new ...Service()`, `new ...Gateway()` oder `new ...Repository()`.
- Legacy-Service-Helper (`accessServicesFactory()`, `userServicesFactory()` usw.) werden nicht mehr verwendet.
- Für neue Domain-Logik keine statischen Utility-Klassen.
### 3.1) Schnell-Gates (lokal)
```bash
(rg 'new\s+[^\s(]+Factory\(' lib/Service || true) | wc -l
(rg --glob '!**/*Factory.php' 'new\s+[^\s(]+(Service|Gateway|Repository)\(' lib/Service || true) | wc -l
(rg "\b(accessServicesFactory|directoryServicesFactory|userServicesFactory|authServicesFactory|tenantServicesFactory|settingServicesFactory|userRepositoryFactory|authRepositoryFactory|authGatewayFactory)\(" pages lib templates web || true) | wc -l
(rg -n 'new\s+[^\s(]+(Service|Gateway|Repository)\(' pages -g '*.php' || true) | wc -l
(rg -n '\bDB::' pages -g '*.php' || true) | wc -l
(rg -n 'app\([^\n]*Factory::class\)->create' pages/admin -g '*.php' || true) | wc -l
(rg -n 'Factory::class' pages/admin -g '*.php' || true) | wc -l
```
Erwartung jeweils: `0`.
### 3.2) Architektur-Contract-Test
- Verbindlicher Guardrail: `tests/Architecture/CoreStarterkitContractTest.php`
- Deckt die Hard-Cut-Regeln ab:
- keine direkten `new ...Service|Gateway|Repository` in `pages/**`
- kein `DB::` in `pages/**`
- keine `app(...Factory::class)->create...`-Ketten in `pages/admin/**`
- keine Input-Superglobals in `pages/**`
- kein `ApiResponse::readJsonBody(...)` in `pages/api/v1/**`
## 4) Autorisierung und Scope
- Zentral über `AuthorizationService` und Policy-Abilities.
- Keine verteilten Einzelchecks als primäre Zugriffslogik.
- UI-Capabilities zentral über `UiAccessService` auflösen.
- `$viewAuth['layout']` wird zentral im Front Controller aufgebaut (`web/index.php`), nicht in Templates.
- Templates nutzen nur `$viewAuth['layout']` bzw. `$viewAuth['page']`, keine `can('...')`-Helper.
## 5) API-Contract
### 4.1) Erweiterung neuer Permissions (Starter-Kit)
1. Permission-Key in `PermissionService` + Migration/Seed ergänzen.
2. Ability in passender Policy ergänzen (oder neue Operation-Ability).
3. Bei UI-Bedarf Mapping zentral in `UiCapabilityMap` ergänzen.
4. Action setzt benötigte `$viewAuth['page']`-Flags.
5. Template konsumiert nur `$viewAuth`.
6. Architektur-/Service-Tests und Doku aktualisieren.
Details und Sonderfälle: `/docs/rbac-permissions-playbook.md`
## 5) `data().php`-Standard
- `pages/**/data().php` sind ausschließlich GET-Endpunkte (`gridRequireGetRequest()`).
- Query-Normalisierung zentral über `gridParseFilters($query, gridSchemaQuery($filterSchema))`.
- Pro Liste gibt es ein `filter-schema.php` im selben Verzeichnis (Single Source of Truth).
- Schema-Typen: `int`, `number`, `bool`, `string`, `enum`, `uuid`, `date`, `csv_ids`, `csv_uuid`, `csv_strings`, `order`, `dir`.
- Keine Alias-Parameter (`*_id`) in Listen-Querys; IDs immer als CSV über `*_ids`.
- Bei JSON-Listenantworten immer konsistent: `{'data': [...], 'total': <int>}`.
- Für MultiSelect-Filter in Listen:
- Toolbar-Rendering zentral über `renderGridFilterToolbar(...)`.
- Select-/MultiSelect-/Text-/Date-/Hidden-Filter werden aus dem Schema gerendert (kein dupliziertes HTML in Templates).
- Grid-Filterbindung clientseitig zentral über `gridFiltersFromSchema(...)`.
- Grid-Filter verwenden CSV-Parameter (z. B. `event_types`, `actor_user_ids`), keine `[]`-Query-Arrays.
- Repository-Filter über `IN (???)` und serverseitige Limitierung/Validierung.
## 5.1) Status/Outcome-Taxonomien (Backed Enums)
- Persistierte Status-/Outcome-Domänen liegen zentral in `lib/Domain/Taxonomy/*`.
- Jede Taxonomie liefert einheitlich: `values()`, `tryNormalize()`, `normalizeOr()`, `labelToken()`, `badgeVariant()`.
- Filter-Schemas lesen Allowed-Listen aus `Enum::values()` oder `gridEnumSanitizer(Enum::class)`.
- Data-Endpunkte erzeugen Badge/Label über Enum-Cases, nicht über lokale `if/elseif`-Mappings.
- In Services/Repositories keine verstreuten Literal-Listen für diese Domänen (`success`, `failed`, ...).
- DB-seitig sind die gleichen Domänen per `CHECK`-Constraints abgesichert (`db/init/init.sql` + `db/updates/2026-03-03-status-taxonomy-hard-cut.sql`).
## 6) API-Contract
- Extern UUID-first.
- Interne IDs nur intern auflösen und mappen.
## 6) Definition of Done für `lib/**`
## 7) Input + Validation in `pages/**`
- Request-Daten nur über `requestInput()` lesen.
- Kein direktes `$_POST`, `$_GET`, `$_FILES`, `$_SERVER['REQUEST_METHOD']` in `pages/**`.
- Validierungsfehler intern über `FormErrors` aufbauen (`formErrors()`, `formErrorsFrom()`).
- API-Validation-Antworten einheitlich über `ApiResponse::validationFromFormErrors(...)`.
- Web-Templates bekommen weiterhin kompakte Listen (`$errors`) aus `FormErrors::toFlatList()`.
- `Flash` nicht als primäre Validation-Struktur verwenden; nur für Redirect-/Outcome-Meldungen.
Kurz-How-to: `/docs/request-input-validation.md`
## 8) Definition of Done für `lib/**`
1. Schichtgrenzen eingehalten.
2. AuthZ zentral.
3. Unit-Tests ergänzt, bei Strukturänderung zusätzlich Contract-Test.
4. `phpunit` und `phpstan` grün.
3. Instanziierungsregeln aus Abschnitt 3 eingehalten (inkl. Gate-Checks).
4. Unit-Tests ergänzt, bei Strukturänderung zusätzlich Contract-Test.
5. `phpunit` und `phpstan` grün.

View File

@@ -1,10 +1,10 @@
# Lokale Entwicklung
Letzte Aktualisierung: 2026-02-21
Letzte Aktualisierung: 2026-02-24
## Ziel
Pragmatischer Tages-Workflow für Entwicklung, Tests und Migrationen.
Kompakter Tagesworkflow für Entwicklung, Tests und schema-nahe Änderungen.
## Start/Stop
@@ -13,58 +13,52 @@ docker compose up -d
docker compose down
```
Hinweis: `docker compose up -d` startet auch den Service `scheduler` (globaler Minutely-Runner).
Hinweis: `up -d` startet auch den Scheduler-Service.
Bei größeren Refactors oder merkwürdigem Laufzeitverhalten:
Bei inkonsistentem Laufzeitverhalten:
```bash
docker compose restart php nginx
```
## Datenbank und Schema-Updates
## Schema-Stand
- Basis-Schema + Seeds: `db/init/init.sql`
- Das Repo nutzt aktuell ein konsolidiertes Init-Schema (keine separaten Migrationsdateien in `db/init/`).
- Für Bestandsumgebungen Schema-/Permission-Änderungen als idempotentes SQL-Update in `db/updates/*.sql` bereitstellen und manuell ausführen.
- Für Bestandsumgebungen: idempotente SQL-Updates in `db/updates/*.sql`
Beispiel (bestehende Umgebung, SQL-Update manuell ausführen):
```bash
docker compose exec -T db sh -lc \
'mariadb -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" < /tmp/release-update.sql'
```
## Typischer Implementierungsablauf
## Umsetzungsablauf
1. Requirement klären
2. Repository anpassen (SQL)
3. Service anpassen (Business-Regeln)
4. Action/Page anpassen
5. i18n-Keys ergänzen (`i18n/default_de.json`, `i18n/default_en.json`)
6. Doku anpassen (`docs/*`)
5. i18n ergänzen
6. Doku aktualisieren
## Pflicht-Checks vor Commit
## Pflichtchecks vor Commit
```bash
docker compose exec php vendor/bin/phpunit
docker compose exec php vendor/bin/phpstan analyse -c phpstan.neon --no-progress
npx eslint web/js
```
Optional gezielt:
Bei JS-Änderungen zusätzlich Browser-Smoke mit DevTools-Console (keine JS-Errors).
## Struktur-Gates (empfohlen)
```bash
docker compose exec php vendor/bin/phpunit tests/I18n/TranslationKeysTest.php
(rg 'new\s+[^\s(]+Factory\(' lib/Service || true) | wc -l
(rg --glob '!**/*Factory.php' 'new\s+[^\s(]+(Service|Gateway|Repository)\(' lib/Service || true) | wc -l
(rg "\b(accessServicesFactory|directoryServicesFactory|userServicesFactory|authServicesFactory|tenantServicesFactory|settingServicesFactory|userRepositoryFactory|authRepositoryFactory|authGatewayFactory)\(" pages lib templates web || true) | wc -l
```
## API-spezifisch
- OpenAPI aktualisieren: `docs/openapi.yaml`
- API-Quickref aktualisieren: `docs/api.md`
- Bei Security-Änderungen auch `docs/sicherheitsmodell.md` aktualisieren
- `docs/openapi.yaml` aktualisieren
- bei Bedarf `docs/api.md` ergänzen
- bei Security-Änderungen `docs/sicherheitsmodell.md` aktualisieren
## Bei Unsicherheit
## Vertiefung
- Architektur zuerst lesen: `/docs/architektur.md`
- Sicherheitsregeln prüfen: `/docs/sicherheitsmodell.md`
- Abschluss immer gegen `/docs/entwickler-checkliste.md`
- `/docs/entwickler-checkliste.md`
- `/docs/03-architektur-request-flow.md`

View File

@@ -62,10 +62,16 @@ paths:
examples:
invalid_credentials:
value:
error: invalid_credentials
ok: false
request_id: 550e8400-e29b-41d4-a716-446655440000
error_code: invalid_credentials
details: {}
account_inactive:
value:
error: account_inactive
ok: false
request_id: 550e8400-e29b-41d4-a716-446655440000
error_code: account_inactive
details: {}
'403':
$ref: '#/components/responses/Forbidden'
'409':
@@ -1108,7 +1114,10 @@ components:
schema:
$ref: '#/components/schemas/ErrorResponse'
example:
error: unauthorized
ok: false
request_id: 550e8400-e29b-41d4-a716-446655440000
error_code: unauthorized
details: {}
Forbidden:
description: Forbidden
content:
@@ -1122,7 +1131,10 @@ components:
schema:
$ref: '#/components/schemas/ErrorResponse'
example:
error: not_found
ok: false
request_id: 550e8400-e29b-41d4-a716-446655440000
error_code: not_found
details: {}
ValidationError:
description: Validation failed
content:
@@ -1130,10 +1142,13 @@ components:
schema:
$ref: '#/components/schemas/ErrorResponse'
example:
error: validation_error
errors:
field:
- invalid_value
ok: false
request_id: 550e8400-e29b-41d4-a716-446655440000
error_code: validation_error
details:
errors:
field:
- invalid_value
Conflict:
description: Conflict
content:
@@ -1151,7 +1166,11 @@ components:
schema:
$ref: '#/components/schemas/ErrorResponse'
example:
error: rate_limit_exceeded
ok: false
request_id: 550e8400-e29b-41d4-a716-446655440000
error_code: rate_limit_exceeded
details:
retry_after: 60
ServerError:
description: Internal server error
content:
@@ -1167,16 +1186,19 @@ components:
schemas:
ErrorResponse:
type: object
required: [error]
required: [ok, request_id, error_code, details]
properties:
error:
ok:
type: boolean
example: false
request_id:
type: string
errors:
format: uuid
error_code:
type: string
details:
type: object
additionalProperties:
type: array
items:
type: string
additionalProperties: true
GenericDataResponse:
type: object
required: [data]
@@ -1333,6 +1355,11 @@ components:
user_inactivity_delete_days:
type: integer
nullable: true
system_audit_enabled:
type: boolean
system_audit_retention_days:
type: integer
nullable: true
microsoft_shared_client_id:
type: string
microsoft_authority:
@@ -1395,6 +1422,11 @@ components:
user_inactivity_delete_days:
type: integer
nullable: true
system_audit_enabled:
type: boolean
system_audit_retention_days:
type: integer
nullable: true
microsoft_shared_client_id:
type: string
microsoft_authority:

View File

@@ -0,0 +1,78 @@
# RBAC Permission Playbook
Letzte Aktualisierung: 2026-02-25
Ziel: Neue Rechte konsistent und ohne Legacy-Pfade ergänzen.
## Entscheidungshilfe
1. Nur UI an bestehende Ability hängen: 3 Schritte.
2. Neue Ability auf bestehende Permission: 4 Schritte.
3. Ganz neue Permission (inkl. DB): 7 Schritte.
## Fall A: Ganz neue Permission
1. Permission-Key in `PermissionService` ergänzen.
Datei: `lib/Service/Access/PermissionService.php`
2. DB-Update für Bestandsinstanzen anlegen (idempotent).
Datei: `db/updates/YYYY-MM-DD-*.sql`
3. `db/init/init.sql` für Neuinstallationen synchron ergänzen.
4. Ability in passender Policy ergänzen:
`ABILITY_*`, `supports()`, Mapping in `authorize()`.
5. Action/API auf Ability umstellen:
Web: `Guard::requireAbility(...)` oder `AuthorizationService`.
API: `ApiResponse::requireAbility(...)`.
6. Falls UI betroffen: `UiCapabilityMap` ergänzen und in Action nach `$viewAuth` auflösen.
7. Tests + Doku aktualisieren.
Beispiel (Core): `system_audit.view` + `system_audit.purge` mit Abilities
`ops.admin.system_audit.view` und `ops.admin.system_audit.purge`.
## Fall B: Neue Ability auf bestehende Permission
1. Ability in passender Policy ergänzen und auf vorhandenen Permission-Key mappen.
2. Action/API auf neue Ability ziehen.
3. Optional UI-Mapping in `UiCapabilityMap` + `$viewAuth`.
4. Tests/Doku nachziehen.
## Fall C: Nur UI-Freigabe erweitern (ohne neue Permission)
1. Capability-Key in `UiCapabilityMap` ergänzen.
2. In Action mit `UiAccessService::pageCapabilities(...)` oder `layoutCapabilities()` auflösen.
3. Template nur über `$viewAuth['page']` bzw. `$viewAuth['layout']` steuern.
## Fall D: API-only Endpoint
1. Ability festlegen (Policy/OperationsPolicy).
2. Endpoint mit `ApiResponse::requireAbility(...)` schützen.
3. OpenAPI aktualisieren.
4. API-Tests ergänzen.
## Fall E: Scope-sensitive Ability (Ressourcenbezug)
1. Immer Kontext übergeben, z. B.:
`actor_user_id`, `target_user_id`, `target_tenant_id`, `input`.
2. Wenn Capabilities benötigt werden:
`AuthorizationDecision::attribute('capabilities', [])` nutzen.
3. Für reine Bool-Checks `AuthorizationService::allow(...)` verwenden.
## Verbindliche Regeln
1. Keine Permission-Checks in Templates (`can(...)` verboten).
2. Keine alten Wrapper (`Guard::requirePermission*`, `ApiResponse::requirePermission`).
3. UI nur über `$viewAuth`.
4. Policy-/Ability-Namen sind der technische Vertrag, nicht der Permission-Key im Template.
## Minimal-Checkliste vor Merge
```bash
rg -n "(?<!::)\\bcan\\(" templates pages -g '*.phtml' -P
rg -n "Guard::requirePermission\\(|Guard::requirePermissionOrForbidden\\(|ApiResponse::requirePermission\\(" pages lib tests web templates -g '*.php'
docker compose exec php vendor/bin/phpunit
docker compose exec php vendor/bin/phpstan analyse -c phpstan.neon --no-progress
```
Erwartung:
1. Die beiden `rg`-Checks liefern `0` Treffer.
2. `phpunit` und `phpstan` sind grün.

View File

@@ -0,0 +1,58 @@
# Request Input + Validation
Letzte Aktualisierung: 2026-02-25
Kurzstandard fuer neue Actions/Endpoints.
## Regeln
- Input immer ueber `requestInput()`.
- Validierungsfehler intern immer als `FormErrors`.
- API-Validation immer mit `ApiResponse::validationFromFormErrors(...)`.
- Web-Templates bekommen kompakte Fehlerlisten (`$errors`) aus `toFlatList()`.
- `Flash` ist fuer Redirect-/Outcome-Meldungen (z. B. Success) gedacht, nicht als primaerer Validation-Container.
- Bei Redirect-POST-Formen: `FormErrors` intern sammeln und erst am Redirect-Rand transportieren (z. B. `flashFormErrors(...)`).
## Web-Beispiel (Action)
```php
$request = requestInput();
$errorBag = formErrors();
if ($request->isMethod('POST')) {
if (!Session::checkCsrfToken()) {
$errorBag->addGlobal(t('Form expired, please try again'));
}
if (!$errorBag->hasAny()) {
$result = $service->save($request->bodyAll());
$errorBag->merge($result['errors'] ?? []);
}
}
$errors = $errorBag->toFlatList();
```
## API-Beispiel (Endpoint)
```php
$request = requestInput();
$errors = formErrors();
if ($request->bodyString('name') === '') {
$errors->add('name', 'required');
}
if ($errors->hasAny()) {
ApiResponse::validationFromFormErrors($errors);
}
```
## Migration-Gates
```bash
rg -n '\\$_POST|\\$_GET|\\$_FILES|REQUEST_METHOD' pages -g '*.php'
rg -n 'ApiResponse::readJsonBody\\(' pages/api/v1 -g '*.php'
```
Erwartung jeweils: `0` Treffer.

View File

@@ -1,141 +1,113 @@
# Sicherheitsmodell
Letzte Aktualisierung: 2026-02-23
Letzte Aktualisierung: 2026-03-03
## 1) Grundprinzip
## Ziel
Die Anwendung kombiniert mehrere Sicherheitslayer:
Kurze, verbindliche Übersicht der Sicherheitsmechanik für Web und API.
1. Request-Grenze (Nginx + Minty Firewall)
2. Session/Auth-Guard
3. Permission-Checks
4. Tenant-Scope-Checks
5. Endpoint-spezifische Schutzlogik (z. B. Avatar-Dateien, Exporte, Actions)
## Sicherheitskette pro Request
Kein einzelner Layer ist ausreichend; die Kombination ist der Schutz.
1. Firewall/Request-Grenze
2. Authentifizierung
3. Permission-Check
4. Tenant-Scope-Check
5. Endpoint-spezifische Regeln (z. B. Exporte, Media, Audit)
## 2) Authentifizierung
Kein Layer allein reicht aus.
- Session-basierter Login.
- Optionales Remember-Me über `user_remember_tokens`.
- Auto-Login aus Cookie erfolgt früh in `/web/index.php`.
- Zentraler Login-Entry ist `login` (optional `login?tenant=<slug>` für Tenant-Vorauswahl).
- Unknown-Email-Verhalten im Login bleibt generisch (keine Tenant-/Account-Details vor erfolgreicher Auflösung).
- Auth-/SSO-Kern ist instanzbasiert verdrahtet über `lib/Service/Auth/AuthServicesFactory.php`
(`AuthService`, `RememberMeService`, `SsoUserLinkService`, `TenantSsoService`, `MicrosoftOidcService`).
## Public vs. Protected
Kritisch:
- Public-Routen kommen aus `config/routes.php` (`public => true`).
- Zusätzlich sind Präfixe technisch immer public (`branding/`, `flash/`, `auth/microsoft/`, `api/`) in `lib/Http/AccessControl.php`.
- Alles andere erfordert Login.
- Tokens sind gehasht gespeichert.
- Tokens können zentral ablaufen gelassen werden (Settings-Gefahrenzone).
API-Sonderfall:
## 3) Public vs. Protected Routes
- `pages/api/v1/auth/login().php` nutzt `ApiBootstrap::init(false)` (public Login-Endpunkt).
- Geschützte API-Endpunkte nutzen `ApiBootstrap::init()`.
- Public Routes werden in `/config/routes.php` mit `public => true` definiert.
- Daraus wird zur Laufzeit `APP_PUBLIC_PATHS` aufgebaut.
- Zusätzlich gibt es technische Always-Public-Präfixe in `lib/Http/AccessControl.php`
(u. a. `api/`, `auth/microsoft/`, `branding/`, `flash/`).
- Alles andere ist loginpflichtig.
## Auth und Login
API-Besonderheit:
- Web-Login ist session-basiert (`pages/auth/login().php`).
- Remember-Me-Autologin läuft früh in `web/index.php`.
- Multi-Tenant-Login wählt Tenant-Kontext serverseitig.
- Microsoft-SSO läuft über OIDC (`auth/microsoft/start` + `auth/microsoft/callback`) mit `state`, `nonce`, PKCE und ID-Token-Validierung.
- `/api/v1/auth/login` ist bewusst public (ohne Bearer-Header).
- Laufzeitverhalten: `pages/api/v1/auth/login().php` nutzt `ApiBootstrap::init(false)`.
- Alle anderen geschützten API-Endpunkte bleiben bei `ApiBootstrap::init()` (Auth erforderlich).
## Permission und Tenant-Scope
## 4) Permission-System
- Rechte werden serverseitig geprüft (`PermissionService` + Policies).
- UI-Sichtbarkeit wird ebenfalls serverseitig vorbereitet (`UiAccessService` + `$viewAuth`), nicht im Template selbst entschieden.
- Globaler Scope-Bypass ist explizit: `tenant.scope.global`.
- Beispiel für harte Feature-Gates:
- `users.access_pdf`
- `custom_fields.manage`, `custom_fields.edit_values`
- `tenants.sso_manage`
- `api_audit.view`
- Berechtigungen sind feingranular (`permissions` + `role_permissions`).
- Rollen vergeben Berechtigungen an User (`user_roles`).
- Actions prüfen Berechtigungen explizit.
## API-Schutz
Empfehlung:
- Zentrale API-Rate-Limits laufen in `lib/Http/ApiBootstrap.php`.
- API-Audit läuft zentral über `ApiAuditService`:
- loggt Metadaten (Pfad, Status, Dauer, Kontext)
- redigiert sensible Query-Parameter
- speichert keine Request-/Response-Bodies
- Rechte für View/Edit/Delete getrennt halten.
- Admin-only Endpunkte immer serverseitig absichern, nicht nur im UI verstecken.
- Onboarding-PDFs für Benutzer laufen über eigenes Recht `users.access_pdf` (Single + Bulk).
- Tenant-Zusatzfelder sind getrennt abgesichert:
- `custom_fields.manage` für Definitionen/Optionen im Tenant
- `custom_fields.edit_values` für Wertepflege am User.
- `field_key` wird serverseitig erzeugt/stabil gehalten (kein editierbares UI-Feld), damit tenant-weite Eindeutigkeit erhalten bleibt.
- Theme-Wechsel für User wird tenant-spezifisch abgesichert:
- effektive Regel kommt aus `current_tenant.allow_user_theme` (Fallback global `app_theme_user`)
- Endpoint `admin/users/theme` blockt bei deaktivierter Regel mit `403` (`theme_disabled`).
- Globaler Tenant-Scope-Bypass ist explizit und separat:
- nur Permission `tenant.scope.global`.
- `settings.update` oder `tenants.update` alleine geben **keinen** Scope-Bypass.
- Tenant-Microsoft-SSO ist separat abgesichert:
- `tenants.sso_manage` für Pflege der Tenant-SSO-Konfiguration.
- Shared-App-Credentials bleiben unter `settings.update`.
- Secrets werden nur verschlüsselt gespeichert (`APP_CRYPTO_KEY` + AES-256-GCM), nie im Klartext gerendert.
- Profil-Sync ist tenant-spezifisch konfigurierbar (`sync_profile_on_login`, `sync_profile_fields`).
- Erlaubte Sync-Felder: `first_name`, `last_name`, `phone`, `mobile`, `avatar`.
- Graph-basierte Felder (`phone/mobile/avatar`) laufen fail-open: Login bleibt erfolgreich, auch wenn Graph nicht verfügbar ist.
## Edge-Hardening (Nginx)
## 5) Tenant Scope
- Security-Header werden zentral im Nginx gesetzt.
- Baseline:
- `X-Content-Type-Options: nosniff`
- `X-Frame-Options: SAMEORIGIN`
- `Referrer-Policy: strict-origin-when-cross-origin`
- `Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=(), usb=()`
- `X-Permitted-Cross-Domain-Policies: none`
- Produktion ergänzt:
- `Strict-Transport-Security: max-age=31536000`
- Lokale Entwicklung setzt bewusst kein HSTS, um HTTP-/localhost-Workflows nicht zu beeinflussen.
- Datenzugriff für tenant-gebundene Inhalte erfolgt nur bei Tenant-Match.
- Departments sind genau einem Tenant zugeordnet (`departments.tenant_id`).
- Beim Speichern von User-Zuordnungen werden Departments serverseitig gegen die aktuell zugewiesenen User-Tenants gefiltert; ungültige Department-Zuordnungen werden entfernt.
- User-Zusatzfeldwerte werden auf Tenant-Wechsel bereinigt (Werte außerhalb der aktuell zugewiesenen Tenants werden gelöscht).
- Filterbare Zusatzfelder sind auf sichere Typen begrenzt (`select`, `multiselect`, `boolean`, `date`); freie Texttypen werden nicht als dynamische Filter zugelassen.
- Theme-Auflösung ist tenant-spezifisch: `default_theme` und `allow_user_theme` werden je `current_tenant` aufgelöst; ohne Tenant-Snapshot greifen globale Defaults.
- SSO-Mapping bleibt tenant-scoped:
- Primär über externe Identität (`provider + tid + oid`).
- Fallback einmalig über E-Mail (create-or-link), danach feste Identity-Linkung.
- Domain-Allowlist (falls gesetzt) wird im Callback serverseitig erzwungen.
- Profil-Sync überschreibt nur konfigurierte Felder und ignoriert leere Quelle-Werte;
`email` bleibt nach JIT-Create unverändert (insert-only).
- Lokaler/SSO-Login ist nur mit mindestens einem aktiven User-Tenant erlaubt (kein Sonderfall ohne Tenant).
- Strict/Non-Strict Verhalten über `TENANT_SCOPE_STRICT`.
- Inaktiv-Status von Tenants beeinflusst Sichtbarkeit (je nach Kontext).
## System-Audit (fachliche Aktionen)
## 6) Verbotene Zugriffe
- Separater Audit-Stream: `system_audit_log` (unabhängig von `api_audit_log`).
- Scope V2:
- Web: Auth-Events + Admin-Write-Aktionen.
- API: selektive `api.request`-Events (Write-Requests, `>=500`, Security-Endpunkte bei `>=400`).
- Scheduler: `scheduler.run` (Runner-Zusammenfassung) und `scheduler.job.run` (pro Joblauf).
- CLI: `cli.command` für zentrale Betriebsbefehle (z. B. `bin/doctor.php`).
- Privacy-Default:
- keine Request-/Response-Bodies
- keine Klartext-Secrets/Passwörter/Tokens/E-Mails
- IP/User-Agent nur gehasht (HMAC)
- Globaler Schalter über Settings:
- `system_audit_enabled` (hard-off: keine neuen Events)
- `system_audit_retention_days` (Retention/Purge)
- RBAC:
- `system_audit.view`
- `system_audit.purge`
- Bei fehlender Permission/Tenant-Bindung: forbidden flow (403 Seite) statt stilles Durchrutschen.
- Keine sensitiven Details in Fehlermeldungen leaken.
## Schreibschutz
## 7) Datei-/Media-Endpunkte
- Web-Schreibaktionen laufen mit CSRF-Token.
- Kritische Actions sind POST-only + serverseitige Validierung.
- API nutzt Bearer-Auth und eigenes Schutzmodell (kein Form-CSRF).
- Avatar/Favicon/Logo Endpunkte prüfen Session + Zugriffskontext.
- Storage liegt außerhalb direkter Business-Logik; Zugriff nur über kontrollierte Endpunkte/Symlinks.
## Betriebschecks
## 8) CSRF und Form-Schutz
- Permission-Set und Rollen regelmäßig prüfen.
- Tenant-Entzug/Scope-Verhalten testen.
- Rate-Limits und `429`-Verhalten bei Login/API testen.
- Audit-Streams bewusst getrennt nutzen:
- `system_audit_log`: fachliche/sicherheitsrelevante Ereignisse (summary-level)
- `api_audit_log`: API-Request-Telemetrie (Status, Dauer, Pfad)
- `import_audit_runs`, `user_lifecycle_audit_log`, `scheduled_job_runs`: Domänen-/Betriebsdetails
- Audit-Retention prüfen:
- API-Audit: 90 Tage
- Import-Audit: 90 Tage
- User-Lifecycle-Audit: 365 Tage
- System-Audit: konfigurierbar (Default 365 Tage)
- Schreibaktionen nutzen CSRF-Token.
- Delete/Status-Aktionen nur per POST und serverseitiger Validierung.
- Bulk-Download für Onboarding-PDF (`admin/users/access-pdf-bulk`) ist POST+CSRF, Scope bleibt aktiv.
- OIDC-Loginfluss nutzt `state`, `nonce` und PKCE; Callback validiert zusätzlich `aud`, `iss`, `tid`, Signatur (JWKS), `exp/nbf`.
## Vertiefung
## 9) Operative Checks
Regelmäßig prüfen:
- Inaktive Rollen/Permissions werden nicht mehr aktiv zugewiesen/ausgewertet.
- Tenant-Entzüge invalidieren Sichtkontext erwartungsgemäß.
- Remember-Tokens können zentral invalidiert werden.
- Mail-Log enthält Fehlerdaten für Auditing.
- User-Lifecycle-Policy (falls aktiv) läuft täglich und setzt inaktive Accounts automatisch auf `inactive`
bzw. löscht sie später (2-stufig), privilegierte Admins sind ausgenommen.
- Delete schreibt vorher zwingend ein Audit-Snapshot (fail-closed).
- Snapshot ist verschlüsselt gespeichert (`snapshot_enc`) und enthält keine Secrets/Tokens.
- Restore ist separat berechtigt (`users.lifecycle_restore`) und stellt bewusst keine Assignments wieder her.
- Geplante Aufgaben (Scheduler) sind Whitelist-basiert:
- keine frei ausführbaren Shell-Kommandos aus der Datenbank.
- zentrale Rechte: `jobs.view`, `jobs.manage`, `jobs.run_now`.
- Runner-Lock verhindert parallele Mehrfach-Ausführung.
## 10) API-Audit
- Alle `/api/v1/*` Requests werden zentral mit Metadaten in `api_audit_log` protokolliert.
- V1 speichert keine Request-/Response-Bodies, nur Metadaten (Pfad, Status, Dauer, Scope-Kontext).
- Query-Parameter werden redacted gespeichert (z. B. Token/Secret/Password-Felder).
- Zugriff auf die Audit-UI ist über Permission `api_audit.view` abgesichert.
- Retention ist auf 90 Tage ausgelegt (Bereinigung per Admin-Action oder Cron).
## 11) User-Lifecycle-Audit
- Lifecycle-Audit-UI ist über `user_lifecycle_audit.view` abgesichert.
- Restore aus Delete-Snapshot ist getrennt über `users.lifecycle_restore` abgesichert.
- Ohne gültigen Snapshot wird kein Auto-Delete ausgeführt (fail-closed).
- Retention für Lifecycle-Audit-Einträge: 365 Tage.
- `/docs/anfragelimits.md`
- `/docs/06-security-tenant-scope.md`
- `/docs/rbac-permissions-playbook.md`