weitere updates für instanzierbarkeit und sauberes testing
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
39
docs/lib-standards.md
Normal 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.
|
||||
@@ -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):
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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`.
|
||||
|
||||
Reference in New Issue
Block a user