weitere updates für instanzierbarkeit und sauberes testing

This commit is contained in:
2026-02-24 08:49:40 +01:00
parent 7b53faca37
commit 16759a2732
119 changed files with 6371 additions and 745 deletions

View File

@@ -1,6 +1,6 @@
# Anfragelimits (Rate Limiting)
Letzte Aktualisierung: 2026-02-21
Letzte Aktualisierung: 2026-02-23
Dieses Dokument beschreibt das aktuelle zentrale Rate-Limiting für Login und API.
@@ -64,6 +64,7 @@ Bei überschreitung:
Integration:
- `/pages/auth/login().php`
- `/pages/api/v1/auth/login().php` (public Login ohne Bearer)
Aktuelle Limits:
@@ -84,6 +85,27 @@ Verhalten:
- 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)

View File

@@ -1,201 +1,58 @@
# Architektur
# Architektur-Kompass
Letzte Aktualisierung: 2026-02-22 (geprüft)
Letzte Aktualisierung: 2026-02-23
## 1) Zielbild
Kurze, verbindliche Architekturübersicht für den aktuellen Stand.
Die Anwendung ist eine servergerenderte Multi-Tenant-Admin-Anwendung mit klarer Trennung von:
## Begriffe (kurz)
- HTTP/Request-Flow (`/web/index.php`, `lib/Http/*`)
- Action-Layer (`/pages/**`)
- Business-Logik (`/lib/Service/**`)
- Datenzugriff (`/lib/Repository/**`)
- Rendering (`/templates/**`, `*.phtml`)
- Frontend-Verhalten (`/web/js/**`)
- `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.
## 2) Request-Lebenszyklus
## Zielbild (Stand heute)
Zentraler Entry Point ist `/web/index.php`.
- 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.
Ablauf pro Request:
## Request-Flow (verbindlich)
1. Bootstrap (`vendor/autoload.php`, `config/config.php`, `config/router.php`, Helper)
2. Firewall + Session Start
3. Locale-Ermittlung (URL/Session/Cookie/default)
4. Remember-Me Auto-Login
5. Optionales Tenant-Snapshot-Refresh in Session
6. Redirect auf locale-prefixed URL (falls nötig)
7. Access Guard (`AccessControl`) für Public/Protected Paths
8. Action-Layer aus `pages/**` laden
9. View + Template rendern
10. Session beenden, DB schließen
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.
## 3) Routing-Modell
## Schichtregeln (MUSS)
- Route-Definitionen (Pfad-Aliase) liegen in `/config/routes.php`.
- Registrierung erfolgt in `/config/router.php` (liest nur `path` und `target`; das `public`-Feld wird vom Router nicht ausgewertet).
- Welche Pfade ohne Login erreichbar sind, steuert `lib/Http/AccessControl.php`:
- Hartkodierte `ALWAYS_PUBLIC_PREFIXES` (z. B. `auth/microsoft/`, `api/`, `branding/`, `flash/`)
- `APP_PUBLIC_PATHS`-Konstante (gesetzt in `config/config.php`)
- `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.
MintyPHP-Konvention (Dateinamensrouting):
## Sicherheits- und API-Regeln (MUSS)
- `pages/admin/users/edit($id).php` erwartet URL-Parameter `id`
- `pages/admin/users/data().php` für Datenendpunkte
- `...(none).phtml` für responses ohne Template-Layout
- Binärausgaben (z. B. Onboarding-PDF/ZIP) setzen `MINTY_ALLOW_OUTPUT` im Action-Endpoint.
- 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.
## 4) Schichten und Verantwortungen
## Merge-Mindestchecks
### Action-Layer (`/pages`)
- Eingaben lesen/validieren
- Permissions und Tenant-Scope prüfen
- Services aufrufen
- View-Daten vorbereiten
### Service-Layer (`/lib/Service`)
- Geschäftsregeln
- Cross-Cutting-Logik (z. B. Auth, Settings, Branding)
- Orchestrierung mehrerer Repositories
- Scheduler/Lifecycle sind instanzbasiert über `lib/Service/Scheduler/SchedulerServicesFactory.php` verdrahtet.
- Import/Import-Audit sind instanzbasiert über `lib/Service/Import/ImportServicesFactory.php` verdrahtet.
- User-Domain ist instanzbasiert über `lib/Service/User/UserServicesFactory.php` verdrahtet
(Services: `UserAccountService`, `UserAssignmentService`, `UserTenantContextService`, `UserPasswordService`;
Repositories: `UserReadRepository`, `UserWriteRepository`, `UserListQueryRepository` + Assignment-Repositories).
- Auth-/SSO-Domain ist instanzbasiert über `lib/Service/Auth/AuthServicesFactory.php` verdrahtet
(u. a. `AuthService`, `RememberMeService`, `SsoUserLinkService`, `TenantSsoService`, `MicrosoftOidcService`).
- Auth-Infra-Repositories (`ApiTokenRepository`, `RememberTokenRepository`, `PasswordResetRepository`,
`EmailVerificationRepository`) werden ebenfalls instanzbasiert über die Factory injiziert.
### Repository-Layer (`/lib/Repository`)
- SQL und Mapping
- Filter-/Paging-Mechaniken
- Keine UI-/HTTP-Logik
### Template/View-Layer (`/templates`, `*.phtml`)
- Nur Rendering
- Keine DB-Zugriffe
- Wiederkehrende UI-Teile als Partials
## 5) Datenmodell (fachlich)
Kernobjekte:
- `users`
- `tenants`
- `departments`
- `roles`
- `permissions`
Verknüpfungen:
- `user_tenants`, `user_departments`, `user_roles`
- `role_permissions`
- `tenant_auth_microsoft`, `user_external_identities`
- `tenant_custom_field_definitions`, `tenant_custom_field_options`
- `user_custom_field_values`, `user_custom_field_value_options`
Hinweis:
- `departments` ist 1:1 an `tenants` gebunden über `departments.tenant_id` (kein M:N-Mapping).
- In der User-Organisation wird die Department-Auswahl tenantweise gerendert (ein Multi-Select je zugewiesenem Tenant), gespeichert wird weiterhin flach über `user_departments`.
- Tenant-spezifische User-Zusatzfelder werden separat modelliert:
- Definitionen/Optionen pro Tenant in `tenant_custom_field_*`
- Werte pro User in `user_custom_field_*`
- Filterung im Address Book über dynamische Query-Keys `cf_*`, `cfm_*`, `cfd_*`.
- `field_key` bleibt intern (tenant-weit eindeutig), wird im Tenant-UI automatisch aus dem Label erzeugt und nicht manuell gepflegt.
- `is_filterable` ist fachlich nur für `select`, `multiselect`, `boolean`, `date` aktiv.
- User-Zusatzfeldwerte sind in V1 optional (keine Pflichtvalidierung).
- Tenant-Theme-Overrides werden direkt auf `tenants` gespeichert:
- `tenants.default_theme` (`NULL` = globales `app_theme`)
- `tenants.allow_user_theme` (`NULL` = globales `app_theme_user`).
- Auflösung immer über `$_SESSION['current_tenant']` und damit tenant-spezifisch für Multi-Tenant-User.
- Tenant-SSO für Microsoft Entra ID:
- Konfiguration pro Tenant in `tenant_auth_microsoft`.
- Externe Identitäts-Verknüpfung stabil über `user_external_identities` (`provider + tid + oid`).
- Login-Endpunkte: `login` (optional `?tenant={slug}`), `auth/microsoft/start`, `auth/microsoft/callback`.
- Shared-App-Credentials liegen global in `settings`; optionale Tenant-Overrides sind möglich.
- Profil-Sync wird tenant-spezifisch über `tenant_auth_microsoft.sync_profile_on_login` und
`tenant_auth_microsoft.sync_profile_fields` gesteuert.
- Erlaubte Sync-Felder in V1: `first_name`, `last_name`, `phone`, `mobile`, `avatar`.
- `phone/mobile/avatar` kommen über Microsoft Graph (`/me`, `/me/photo/$value`) und sind fail-open
(Graph-Fehler blockieren den Login nicht).
Sicherheits-/Audit-nahe Tabellen:
- `user_remember_tokens`
- `password_resets`
- `email_verifications`
- `mail_log`
## 6) Sicherheits- und Scope-Modell
- Auth Guard: protected by default
- Permission Checks: feature-spezifisch in Actions/Services
- Tenant Scope: datenabhängig über Tenant-Matches
- Strictness steuerbar über `TENANT_SCOPE_STRICT`
Details siehe:
`/docs/sicherheitsmodell.md`
## 7) Frontend-Architektur
JS-Struktur:
- `/web/js/core` Basismodule (DOM/Grid Factory)
- `/web/js/components` UI-Komponenten
- `/web/js/pages` seitenspezifische Helfer
Bootstrap:
- `app-boot.js` früh (no-js/js state, localStorage UI state)
- `app-init.js` für Default-Template
- `app-login-init.js` für Login-Template
CSS-Struktur:
- Layer-Entrypoint: `/web/css/app-layers.css`
- Basen/Variablen in `/web/css/base`
- Layout in `/web/css/layout`
- Komponenten in `/web/css/components`
- Seitenstile in `/web/css/pages`
- Vendor-Overrides in `/web/css/vendor-overrides`
## 8) Asset-Steuerung
Konfiguration liegt in `/config/assets.php`.
- Template-Gruppen: `default`, `login`
- Seiten-Gruppen optional via `Buffer::set('style_groups', ...)`
- Rendering über Helper: `renderTemplateStyles()` und `renderPageStyles()`
## 9) Erweiterungsstrategie
Pragmatischer Weg für neue Features:
1. Repository erweitern (SQL)
2. Service-Regel bauen
3. Action anbinden
4. View/Partial integrieren
5. JS nur falls interaktiv nötig
6. Tests + Lint/Analyse laufen lassen
So bleibt die Trennung stabil und Refactoring kontrollierbar.
## 10) Settings-Architektur (DB + Datei-Cache)
- `settings` (DB) ist die alleinige fachliche Quelle für globale Settings.
- `config/settings.php` ist ein technischer Teil-Cache für sehr häufig gelesene UI-Basiswerte.
- Zugriffsmuster:
- `SettingGateway` (verdrahtet über `SettingServicesFactory`) -> `SettingService` -> DB
- `appSetting(...)` -> `SettingServicesFactory` -> `SettingCacheService` (Datei-Cache)
- Konsequenz:
- Es ist korrekt, dass nicht alle DB-Settings im Datei-Cache stehen (z. B. SMTP/SSO/API-Details).
- Der Cache muss nur die Keys enthalten, die im Runtime-Helper `appSetting(...)` verwendet werden.
Details:
`/docs/einstellungen-speicherung.md`
- `docker compose exec php vendor/bin/phpunit`
- `docker compose exec php vendor/bin/phpstan analyse -c phpstan.neon --no-progress`

View File

@@ -1,12 +1,13 @@
# Entwickler-Checkliste
Letzte Aktualisierung: 2026-02-22
Letzte Aktualisierung: 2026-02-23
## 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
## Beim Implementieren

View File

@@ -35,7 +35,7 @@ Ein neuer Entwickler soll den ersten Change strukturiert und reproduzierbar umse
3. Optional Service/Repository ergänzen
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 bereitstellen
6. Bei neuen Berechtigungen: `PermissionService` + `init.sql` synchron halten, für Bestandsumgebungen idempotentes SQL-Update in `db/updates/*.sql` bereitstellen
### Done-Kriterien

View File

@@ -1,6 +1,6 @@
# Fehlerbehebung
Letzte Aktualisierung: 2026-02-21
Letzte Aktualisierung: 2026-02-23
## App reagiert nicht wie erwartet
@@ -22,10 +22,11 @@ docker compose restart php nginx
### Checks
1. Authorization Header korrekt gesetzt (`Bearer <selector:secret>`)
2. Token nicht revoked/abgelaufen
3. Token hat erforderliche Permission
4. Bei tenant-scoped Token: Zugriff nur auf passende Tenant-Ressourcen
1. Für geschützte Endpunkte Authorization Header korrekt gesetzt (`Bearer <selector:secret>`)
2. Ausnahme: `/api/v1/auth/login` ist public und benötigt keinen Bearer-Header
3. Token nicht revoked/abgelaufen
4. Token hat erforderliche Permission
5. Bei tenant-scoped Token: Zugriff nur auf passende Tenant-Ressourcen
## Migration oder Schema-Probleme

View File

@@ -1,6 +1,6 @@
# Dokumentation
Letzte Aktualisierung: 2026-02-22
Letzte Aktualisierung: 2026-02-23
Diese Dokumente sind für Entwickler gedacht, die die Anwendung warten oder erweitern.
@@ -20,11 +20,13 @@ Diese Dokumente sind für Entwickler gedacht, die die Anwendung warten oder erwe
## Architektur und Regeln
- `/docs/architektur.md`
- Request-Lebenszyklus, Schichtentrennung, Frontend-Aufbau.
- Startpunkt mit Glossar, Zielbild, Schichtregeln und Sicherheitsleitplanken.
- `/docs/sicherheitsmodell.md`
- Auth, Permissions, Tenant-Scope, Public/API-Zugriffe.
- `/docs/konventionen.md`
- Coding- und Strukturkonventionen für PHP, Templates, CSS, JS.
- Kurze, verbindliche Arbeitsregeln für Code, API und Qualität.
- `/docs/lib-standards.md`
- Konkrete Do/Don't-Regeln für `lib/**`.
## Frontend

View File

@@ -1,129 +1,41 @@
# Konventionen
# Konventionen (kurz und verbindlich)
Letzte Aktualisierung: 2026-02-22
Letzte Aktualisierung: 2026-02-23
Diese Regeln sind projektweit verbindlich, um Lesbarkeit und Wartbarkeit stabil zu halten.
## 1) Code
## 1) Allgemein
- Kleine, fokussierte Änderungen statt Misch-Commits.
- Kein toter Code, keine ungenutzten Helpers.
- ASCII als Standard, Unicode nur wenn Datei es bereits nutzt.
- ASCII als Standard, außer bestehende Datei nutzt bereits Unicode.
- Keine toten Pfade/Codezweige einbauen.
- Wiederholung lieber zentralisieren (Partial/Helper/Utility) als kopieren.
- Kleine, fokussierte Commits/Changesets bevorzugen.
## 2) Schichten
## 2) PHP-Schichten
- SQL nur in `lib/Repository/**`.
- Fachlogik nur in `lib/Service/**`.
- `pages/**` bleibt dünn: Input, Auth/AuthZ, Service-Aufruf, Response.
- Neue Domain-Logik als Instanz-Service mit Factory-Verdrahtung.
### Repository
## 3) Autorisierung
- Enthalten SQL und Filterlogik.
- Keine HTTP-/Session-/Render-Logik.
- Prepared Statements und bestehende Query-Utilities nutzen.
- Nur zentrale Policy-Abilities nutzen.
- Keine verteilten `userHas(...)`-Checks als Hauptlogik.
- Tenant-Scope nicht in Templates oder JavaScript nachbauen.
### Service
## 4) API
- Geschäftsregeln, Validierung, Orchestrierung.
- Keine HTML-Ausgabe.
- Keine direkten View-Annahmen.
- Bei globalen Settings bleibt DB die Quelle (`SettingService`); Datei-Cache nur gezielt für `appSetting(...)`.
- Für neue oder refaktorierte Domain-Module Instanz-Services + Factory verwenden (z. B. `...ServicesFactory`) statt statischer Domain-Methoden.
- Keine domain-spezifischen Service-Locator-Helper in `lib/Support/helpers/app.php`; Composition per Factory im Aufrufer.
- UUID-first für externe Ressourcen.
- Interne IDs nicht nach außen geben.
- Fehlerantworten einheitlich halten (`error`, optional `errors`).
- OpenAPI bei jeder API-Änderung im selben Merge aktualisieren.
### Action (`pages/**.php`)
## 5) Frontend
- Request lesen, Zugriff prüfen, Service aufrufen, Variablen für View setzen.
- Redirects/Flash-Messages hier zentral steuern.
- Templates nur für Rendering.
- UI-Texte über `t('...')`.
- Strukturregeln in `docs/frontend-css.md` und `docs/frontend-javascript.md`.
### Views (`*.phtml`)
## 6) Mindestchecks vor Merge
- Reines Rendering.
- Keine DB-Calls.
- HTML escapen (`e(...)`) außer bewusst gewünscht.
## 3) UI-Konventionen
### Edit-Seiten
- Einheitliche Titelzeile mit `/templates/partials/app-details-titlebar.phtml`.
- In der Titelzeile nur Primäraktionen halten (typisch: `Save`, `Save & close`).
- Sekundäraktionen nicht im Drei-Punkte-Menü verstecken, sondern im Aside als einklappbarer Block `Actions` über `/templates/partials/app-details-aside-actions.phtml`.
- Partial-Naming klar trennen:
- linkes Haupt-Aside: `app-main-aside*`
- rechtes Details-Aside: `app-details-aside*`
- Tab-Reihenfolge:
- erster Tab `Master data`
- später `Visibility`
- optional letzter Tab `Danger zone`
- Statusfeld über `/templates/partials/app-visibility-status-field.phtml`.
- Delete-Aktion über `/templates/partials/app-danger-zone-delete-field.phtml`.
- Aside-Metadaten über:
- `/templates/partials/app-details-aside-audit.phtml`
- `/templates/partials/app-details-aside-ids.phtml`
- Tenant-Zusatzfelder:
- Definitionen werden im Tenant-Tab `Custom fields` über eigene POST-Actions gepflegt (nicht über Tenant-Hauptsave).
- `field_key` wird nicht im UI gepflegt; serverseitig aus Label erzeugt und tenant-weit eindeutig gehalten.
- `is_filterable` nur für `select`, `multiselect`, `boolean`, `date` anbieten.
- User-Werte werden im User-Tab `Custom fields` tenantweise gruppiert gerendert.
- User-Zusatzfelder sind optional; keine Pflichtvalidierung in V1.
### Listen
- Grid.js als Standard.
- Filter/Toolbar konsistent halten.
- Aktionsspalten nur wenn wirklich nötig; sonst Double-Click/Row-Action nutzen.
- Dynamische Address-Book-Filter für Zusatzfelder folgen festen Parametern:
- `cf_<definition_uuid>` (scalar/select/bool)
- `cfm_<definition_uuid>` (multiselect als CSV)
- `cfd_<definition_uuid>_from|to` (date range)
## 4) CSS
- Prefix `app-` für eigene Klassen/Dateien.
- Vendor-Anpassungen in `/web/css/vendor-overrides`.
- Layering über `/web/css/app-layers.css`.
- Theme-/Contrast-Werte über zentrale Variablen in `/web/css/base`.
## 5) JavaScript
- Struktur:
- `core/` für Basismodule
- `components/` für wiederverwendbare UI-Module
- `pages/` für seitenspezifische Logik
- Initialisierung zentral in `app-init.js` bzw. `app-login-init.js`.
- Defensive DOM-Checks (Element kann auf manchen Seiten fehlen).
- Keine Business-Logik im Frontend verstecken.
## 6) i18n
- Alle sichtbaren UI-Texte über `t('...')`.
- Übersetzungs-Keys in `i18n/default_de.json` und `i18n/default_en.json` pflegen.
- Test nutzen: `tests/I18n/TranslationKeysTest.php`.
## 7) Qualitätssicherung
Vor Merge mindestens:
- PHPUnit
- PHPStan
- ESLint (web/js)
- Betroffene Kern-Userflows manuell testen
Checkliste siehe:
`/docs/entwickler-checkliste.md`
### Dependency-Hygiene (periodisch, nicht bei jedem Commit)
Ungenutzte Composer-Pakete prüfen:
```bash
docker compose exec php vendor/bin/composer-unused
```
Ausführen bei: Feature-Entfernung, größerem Refactoring oder vor Releases. Pakete die fälschlicherweise als unused gemeldet werden (z.B. dynamisch geladen), können in `composer.json` unter `extra.unused` ignoriert werden.
## 8) Settings-Konvention
- Neue Settings immer zuerst sauber im `SettingService` modellieren (Validierung + Read/Write).
- Aufrufer nutzen `SettingGateway` aus `SettingServicesFactory`; kein globaler `settingGateway()`-Helper.
- Nur dann in den `SettingCacheService` aufnehmen, wenn der Key wirklich über `appSetting(...)` im Hot Path genutzt wird.
- Keine Sicherheits-/Integrationssettings (SMTP, SSO-Secrets, API-Policies) als Pflicht in den Datei-Cache drücken.
- Bei manuellen DB-Änderungen an gecachten Keys den Cache gezielt aktualisieren (z. B. über Settings-Save-Flow).
- `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`

39
docs/lib-standards.md Normal file
View File

@@ -0,0 +1,39 @@
# lib-Standards
Letzte Aktualisierung: 2026-02-23
Praktische Regeln nur für `lib/**`. Ergänzt `docs/architektur.md`.
## 1) Repository-Regeln
- Verantwortlich für SQL, Filter, Paging und Mapping.
- Keine HTTP-, Session-, Redirect- oder Policy-Logik.
## 2) Service-Regeln
- Verantwortlich für Fachlogik und Orchestrierung.
- Keine Superglobals (`$_POST`, `$_GET`, `$_SESSION`).
- Keine Header- oder Redirect-Ausgabe.
- Abhängigkeiten per Constructor Injection.
## 3) Instanziierung
- Domain-Abhängigkeiten mit `new` nur in Factories/Bootstrap.
- Für neue Domain-Logik keine statischen Utility-Klassen.
## 4) Autorisierung und Scope
- Zentral über `AuthorizationService` und Policy-Abilities.
- Keine verteilten Einzelchecks als primäre Zugriffslogik.
## 5) API-Contract
- Extern UUID-first.
- Interne IDs nur intern auflösen und mappen.
## 6) 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.

View File

@@ -25,7 +25,7 @@ docker compose restart php nginx
- 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 bereitstellen und manuell ausführen.
- Für Bestandsumgebungen Schema-/Permission-Änderungen als idempotentes SQL-Update in `db/updates/*.sql` bereitstellen und manuell ausführen.
Beispiel (bestehende Umgebung, SQL-Update manuell ausführen):

View File

@@ -18,6 +18,8 @@ tags:
- name: tenants
- name: departments
- name: roles
- name: permissions
- name: settings
paths:
/auth/login:
post:
@@ -88,6 +90,133 @@ paths:
$ref: '#/components/responses/Unauthorized'
'429':
$ref: '#/components/responses/RateLimited'
put:
tags: [me]
summary: Update authenticated user profile
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/MeUpdateRequest'
responses:
'200':
description: Updated profile
content:
application/json:
schema:
$ref: '#/components/schemas/MeResponse'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'422':
$ref: '#/components/responses/ValidationError'
'429':
$ref: '#/components/responses/RateLimited'
patch:
tags: [me]
summary: Patch authenticated user profile
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/MeUpdateRequest'
responses:
'200':
description: Updated profile
content:
application/json:
schema:
$ref: '#/components/schemas/MeResponse'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'422':
$ref: '#/components/responses/ValidationError'
'429':
$ref: '#/components/responses/RateLimited'
/me/password:
post:
tags: [me]
summary: Change password for authenticated user
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/MePasswordRequest'
responses:
'204':
description: No Content
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'422':
$ref: '#/components/responses/ValidationError'
'429':
$ref: '#/components/responses/RateLimited'
/me/avatar:
get:
tags: [me]
summary: Get avatar image for authenticated user
parameters:
- $ref: '#/components/parameters/AvatarSize'
responses:
'200':
description: Avatar binary
content:
image/jpeg: {}
image/png: {}
image/webp: {}
'401':
$ref: '#/components/responses/Unauthorized'
'404':
$ref: '#/components/responses/NotFound'
'429':
$ref: '#/components/responses/RateLimited'
post:
tags: [me]
summary: Upload avatar for authenticated user
requestBody:
required: true
content:
multipart/form-data:
schema:
type: object
required: [avatar]
properties:
avatar:
type: string
format: binary
responses:
'201':
$ref: '#/components/responses/DataObject'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'422':
$ref: '#/components/responses/ValidationError'
'429':
$ref: '#/components/responses/RateLimited'
delete:
tags: [me]
summary: Delete avatar for authenticated user
responses:
'204':
description: No Content
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'429':
$ref: '#/components/responses/RateLimited'
/me/tokens:
get:
@@ -350,6 +479,29 @@ paths:
'429':
$ref: '#/components/responses/RateLimited'
/users/avatar/{uuid}:
parameters:
- $ref: '#/components/parameters/UuidPath'
- $ref: '#/components/parameters/AvatarSize'
get:
tags: [users]
summary: Get avatar image by user UUID
responses:
'200':
description: Avatar binary
content:
image/jpeg: {}
image/png: {}
image/webp: {}
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
'429':
$ref: '#/components/responses/RateLimited'
/tenants:
get:
tags: [tenants]
@@ -474,6 +626,29 @@ paths:
'429':
$ref: '#/components/responses/RateLimited'
/tenants/avatar/{uuid}:
parameters:
- $ref: '#/components/parameters/UuidPath'
- $ref: '#/components/parameters/AvatarSize'
get:
tags: [tenants]
summary: Get avatar image by tenant UUID
responses:
'200':
description: Avatar binary
content:
image/jpeg: {}
image/png: {}
image/webp: {}
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
'429':
$ref: '#/components/responses/RateLimited'
/departments:
get:
tags: [departments]
@@ -722,6 +897,112 @@ paths:
'429':
$ref: '#/components/responses/RateLimited'
/permissions:
get:
tags: [permissions]
summary: List permissions
parameters:
- $ref: '#/components/parameters/Limit25'
- $ref: '#/components/parameters/Offset'
- $ref: '#/components/parameters/Search'
- $ref: '#/components/parameters/OrderPermissions'
- $ref: '#/components/parameters/Dir'
- $ref: '#/components/parameters/ActiveFilter'
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/PermissionListResponse'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'429':
$ref: '#/components/responses/RateLimited'
/settings:
get:
tags: [settings]
summary: Get application settings snapshot
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/SettingsResponse'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'429':
$ref: '#/components/responses/RateLimited'
put:
tags: [settings]
summary: Update application settings
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/SettingsUpdateRequest'
responses:
'200':
description: Updated settings snapshot
content:
application/json:
schema:
$ref: '#/components/schemas/SettingsResponse'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'422':
$ref: '#/components/responses/ValidationError'
'429':
$ref: '#/components/responses/RateLimited'
patch:
tags: [settings]
summary: Patch application settings
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/SettingsUpdateRequest'
responses:
'200':
description: Updated settings snapshot
content:
application/json:
schema:
$ref: '#/components/schemas/SettingsResponse'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'422':
$ref: '#/components/responses/ValidationError'
'429':
$ref: '#/components/responses/RateLimited'
/settings/public:
get:
tags: [settings]
summary: Get public app settings for unauthenticated clients
security: []
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/GenericDataResponse'
'429':
$ref: '#/components/responses/RateLimited'
components:
securitySchemes:
BearerAuth:
@@ -777,6 +1058,13 @@ components:
schema:
type: string
description: "Accepted aliases: 1, 0, true, false, active, inactive"
AvatarSize:
name: size
in: query
schema:
type: integer
minimum: 16
maximum: 1024
OrderUsers:
name: order
in: query
@@ -805,6 +1093,13 @@ components:
type: string
enum: [id, uuid, description, code, active, created, modified]
default: description
OrderPermissions:
name: order
in: query
schema:
type: string
enum: [key, description, active, created]
default: key
responses:
Unauthorized:
description: Unauthorized
@@ -930,6 +1225,195 @@ components:
format: date-time
nullable: true
description: UTC expiry timestamp of the token
MeUpdateRequest:
type: object
additionalProperties: false
properties:
first_name:
type: string
last_name:
type: string
profile_description:
type: string
job_title:
type: string
phone:
type: string
mobile:
type: string
short_dial:
type: string
address:
type: string
postal_code:
type: string
city:
type: string
country:
type: string
region:
type: string
locale:
type: string
theme:
type: string
current_tenant_uuid:
type: string
format: uuid
MePasswordRequest:
type: object
additionalProperties: false
required: [current_password, password, password2]
properties:
current_password:
type: string
password:
type: string
password2:
type: string
PermissionListItem:
type: object
properties:
key:
type: string
description:
type: string
active:
type: boolean
is_system:
type: boolean
PermissionListResponse:
type: object
properties:
data:
type: array
items:
$ref: '#/components/schemas/PermissionListItem'
total:
type: integer
limit:
type: integer
offset:
type: integer
SettingsObject:
type: object
properties:
app_title:
type: string
app_locale:
type: string
app_theme:
type: string
app_theme_user_allowed:
type: boolean
app_registration_enabled:
type: boolean
app_primary_color:
type: string
api_token_default_ttl_days:
type: integer
nullable: true
api_token_max_ttl_days:
type: integer
nullable: true
api_cors_allowed_origins:
type: string
default_tenant_id:
type: integer
nullable: true
default_role_id:
type: integer
nullable: true
default_department_id:
type: integer
nullable: true
user_inactivity_deactivate_days:
type: integer
nullable: true
user_inactivity_delete_days:
type: integer
nullable: true
microsoft_shared_client_id:
type: string
microsoft_authority:
type: string
smtp_host:
type: string
smtp_port:
type: integer
nullable: true
smtp_user:
type: string
smtp_secure:
type: string
smtp_from:
type: string
smtp_from_name:
type: string
SettingsResponse:
type: object
required: [data]
properties:
data:
$ref: '#/components/schemas/SettingsObject'
SettingsUpdateRequest:
type: object
additionalProperties: false
properties:
app_title:
type: string
app_locale:
type: string
app_theme:
type: string
app_theme_user_allowed:
type: boolean
app_registration_enabled:
type: boolean
app_primary_color:
type: string
api_token_default_ttl_days:
type: integer
nullable: true
api_token_max_ttl_days:
type: integer
nullable: true
api_cors_allowed_origins:
type: string
default_tenant_id:
type: integer
nullable: true
default_role_id:
type: integer
nullable: true
default_department_id:
type: integer
nullable: true
user_inactivity_deactivate_days:
type: integer
nullable: true
user_inactivity_delete_days:
type: integer
nullable: true
microsoft_shared_client_id:
type: string
microsoft_authority:
type: string
smtp_host:
type: string
smtp_port:
type: integer
nullable: true
smtp_user:
type: string
smtp_password:
type: string
smtp_secure:
type: string
smtp_from:
type: string
smtp_from_name:
type: string
RoleWriteRequest:
type: object
additionalProperties: false
@@ -946,14 +1430,14 @@ components:
DepartmentWriteRequest:
type: object
additionalProperties: false
required: [description, tenant_id]
required: [description, tenant_uuid]
properties:
description:
type: string
minLength: 1
tenant_id:
type: integer
minimum: 1
tenant_uuid:
type: string
format: uuid
code:
type: string
cost_center:
@@ -1066,24 +1550,24 @@ components:
type: string
password2:
type: string
tenant_ids:
tenant_uuids:
type: array
items:
type: integer
minimum: 1
primary_tenant_id:
type: integer
minimum: 1
role_ids:
type: string
format: uuid
primary_tenant_uuid:
type: string
format: uuid
role_uuids:
type: array
items:
type: integer
minimum: 1
department_ids:
type: string
format: uuid
department_uuids:
type: array
items:
type: integer
minimum: 1
type: string
format: uuid
UserUpdateRequest:
type: object
additionalProperties: false
@@ -1133,24 +1617,24 @@ components:
type: string
password2:
type: string
tenant_ids:
tenant_uuids:
type: array
items:
type: integer
minimum: 1
primary_tenant_id:
type: integer
minimum: 1
role_ids:
type: string
format: uuid
primary_tenant_uuid:
type: string
format: uuid
role_uuids:
type: array
items:
type: integer
minimum: 1
department_ids:
type: string
format: uuid
department_uuids:
type: array
items:
type: integer
minimum: 1
type: string
format: uuid
TenantSummary:
type: object
properties:
@@ -1436,6 +1920,8 @@ components:
type: string
email:
type: string
profile_description:
type: string
job_title:
type: string
phone:
@@ -1452,12 +1938,32 @@ components:
type: string
country:
type: string
region:
type: string
hire_date:
type: string
format: date
locale:
type: string
theme:
type: string
email_verified_at:
type: string
nullable: true
last_login_at:
type: string
nullable: true
last_login_provider:
type: string
nullable: true
active:
type: boolean
created:
type: string
modified:
type: string
current_tenant:
$ref: '#/components/schemas/TenantSummary'
primary_tenant:
$ref: '#/components/schemas/TenantSummary'
permissions:
@@ -1520,12 +2026,39 @@ components:
type: string
country:
type: string
region:
type: string
vat_id:
type: string
tax_number:
type: string
phone:
type: string
fax:
type: string
email:
type: string
support_email:
type: string
support_phone:
type: string
billing_email:
type: string
website:
type: string
privacy_url:
type: string
imprint_url:
type: string
primary_color:
type: string
primary_color_use_default:
type: boolean
default_theme:
type: string
allow_user_theme_mode:
type: string
enum: ['', '0', '1']
created:
type: string
modified:
@@ -1574,11 +2107,13 @@ components:
type: string
code:
type: string
cost_center:
type: string
active:
type: boolean
tenant_id:
type: integer
nullable: true
tenant_uuid:
type: string
format: uuid
created:
type: string
modified:
@@ -1625,6 +2160,10 @@ components:
type: string
active:
type: boolean
permission_keys:
type: array
items:
type: string
created:
type: string
modified:

View File

@@ -1,6 +1,6 @@
# Sicherheitsmodell
Letzte Aktualisierung: 2026-02-22
Letzte Aktualisierung: 2026-02-23
## 1) Grundprinzip
@@ -33,8 +33,16 @@ Kritisch:
- 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.
API-Besonderheit:
- `/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).
## 4) Permission-System
- Berechtigungen sind feingranular (`permissions` + `role_permissions`).
@@ -53,6 +61,9 @@ Empfehlung:
- 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`.