major update
This commit is contained in:
42
docs/00-systemueberblick.md
Normal file
42
docs/00-systemueberblick.md
Normal 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`
|
||||
52
docs/01-setup-und-erster-login.md
Normal file
52
docs/01-setup-und-erster-login.md
Normal 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
32
docs/02-domain-glossar.md
Normal 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`
|
||||
86
docs/03-architektur-request-flow.md
Normal file
86
docs/03-architektur-request-flow.md
Normal 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`
|
||||
35
docs/04-erste-aenderung-ui.md
Normal file
35
docs/04-erste-aenderung-ui.md
Normal 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`
|
||||
34
docs/05-erste-aenderung-backend.md
Normal file
34
docs/05-erste-aenderung-backend.md
Normal 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`
|
||||
31
docs/06-security-tenant-scope.md
Normal file
31
docs/06-security-tenant-scope.md
Normal 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
32
docs/07-api-erweitern.md
Normal 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`
|
||||
36
docs/08-advanced-scheduler-sso.md
Normal file
36
docs/08-advanced-scheduler-sso.md
Normal 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.
|
||||
@@ -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.
|
||||
|
||||
15
docs/api.md
15
docs/api.md
@@ -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`
|
||||
|
||||
@@ -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`
|
||||
|
||||
56
docs/betriebscheck-doctor.md
Normal file
56
docs/betriebscheck-doctor.md
Normal 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
65
docs/codex-prompts.md
Normal 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
106
docs/di-container.md
Normal 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.
|
||||
@@ -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.
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
113
docs/importe.md
113
docs/importe.md
@@ -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.
|
||||
|
||||
134
docs/index.md
134
docs/index.md
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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:
|
||||
|
||||
78
docs/rbac-permissions-playbook.md
Normal file
78
docs/rbac-permissions-playbook.md
Normal 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.
|
||||
58
docs/request-input-validation.md
Normal file
58
docs/request-input-validation.md
Normal 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.
|
||||
@@ -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`
|
||||
|
||||
Reference in New Issue
Block a user