add composer-unused, comprehensive docs, and project restructure

- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-22 15:27:35 +01:00
parent 3eb9cc0ac4
commit 25370a1a55
389 changed files with 40506 additions and 8071 deletions

View File

@@ -1,719 +0,0 @@
# IMVS - Architektur und Grundprinzipien
## Inhaltsverzeichnis
1. [Projektübersicht](#1-projektübersicht)
2. [Framework: MintyPHP](#2-framework-mintyphp)
3. [Verzeichnisstruktur](#3-verzeichnisstruktur)
4. [Datenbankarchitektur](#4-datenbankarchitektur)
5. [PHP-Architektur](#5-php-architektur)
6. [Frontend-Architektur](#6-frontend-architektur)
7. [Template-System](#7-template-system)
8. [Authentifizierung und Autorisierung](#8-authentifizierung-und-autorisierung)
9. [Internationalisierung](#9-internationalisierung)
10. [Best Practices](#10-best-practices)
---
## 1. Projektübersicht
**Projektname:** IMVS
**Framework:** MintyPHP v3+
**PHP-Version:** 8.5+
**Datenbank:** MySQL 5.7+
**Lizenz:** MIT
### Kernfeatures
- Multi-Tenant-Architektur
- Rollen- und Berechtigungssystem
- Mehrsprachigkeit (DE/EN)
- Dark/Light Theme
- E-Mail-Integration mit Templates
---
## 2. Framework: MintyPHP
### Philosophie
MintyPHP ist ein leichtgewichtiges PHP-Framework mit folgenden Prinzipien:
- **Easy to learn** - Einfach für PHP-Entwickler zu erlernen
- **Secure by design** - Sicherheit als Grundprinzip
- **Lightweight** - Minimaler Overhead
- **Single variable scope** - Eine Variable-Scope für alle Layer
- **SQL required** - Direktes SQL statt ORM
- **PHP as templating** - PHP selbst als Template-Sprache
### Kern-Komponenten
| Komponente | Beschreibung |
|------------|--------------|
| `Router` | URL-Routing zu Pages |
| `DB` | Datenbankzugriff mit Prepared Statements |
| `Auth` | Basis-Authentifizierung |
| `Session` | Session-Management |
| `Buffer` | Output-Buffering für Templates |
| `I18n` | Internationalisierung |
| `Cache` | Memcached-Integration |
| `Firewall` | DDoS-Schutz |
### Router-Konvention
```
URL: /admin/users/edit/abc-123
Datei: pages/admin/users/edit($id).php
Variable: $id = 'abc-123'
```
---
## 3. Verzeichnisstruktur
```
ImmobilienMaklerVerkaufssoftware/
├── config/ # Konfigurationsdateien
│ ├── config.php # Hauptkonfiguration
│ ├── router.php # Route-Definitionen
│ └── settings.php # App-Einstellungen (generiert)
├── db/ # Datenbank
│ └── init/init.sql # Schema und Seeds
├── docker/ # Docker-Konfiguration
├── docs/ # Dokumentation
├── i18n/ # Sprachdateien
│ ├── default_de.json
│ └── default_en.json
├── lib/ # Geschäftslogik
│ ├── Http/ # HTTP-Utilities
│ ├── Repository/ # Datenbankschicht
│ ├── Service/ # Business-Logic
│ └── Support/ # Helper und Guards
├── pages/ # Seiten (Controller)
│ ├── admin/ # Geschützte Admin-Seiten
│ ├── auth/ # Authentifizierung
│ ├── error/ # Fehlerseiten
│ └── page/ # CMS-Seiten
├── storage/ # Dateiablage
│ ├── branding/ # Logos, Favicons
│ ├── tenants/ # Tenant-Dateien
│ └── users/ # User-Avatare
├── templates/ # View-Templates
│ ├── partials/ # Wiederverwendbare Teile
│ └── emails/ # E-Mail-Templates
├── tests/ # PHPUnit Tests
├── vendor/ # Composer Dependencies
└── web/ # Public Root
├── css/ # Stylesheets
├── js/ # JavaScript-Module
├── vendor/ # Frontend-Libraries
└── index.php # Entry Point
```
---
## 4. Datenbankarchitektur
### Entity-Relationship-Diagramm
```
┌─────────────┐ ┌─────────────────┐ ┌─────────────┐
│ USERS │────<│ USER_TENANTS │>────│ TENANTS │
└─────────────┘ └─────────────────┘ └─────────────┘
│ │
│ ┌─────────────────┐ │
└────────────<│ USER_ROLES │ │
│ └─────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────┐ │
│ │ ROLES │ │
│ └─────────────┘ │
│ │ │
│ ┌─────────────────┐ │
│ │ROLE_PERMISSIONS │ │
│ └─────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────┐ │
│ │ PERMISSIONS │ │
│ └─────────────┘ │
│ │
│ ┌─────────────────┐ ┌──────┴──────┐
└────────────<│USER_DEPARTMENTS │>────│ DEPARTMENTS │
└─────────────────┘ └─────────────┘
┌──────┴──────┐
│TENANT_DEPTS │
└─────────────┘
```
### Haupttabellen
#### users
```sql
id INT PRIMARY KEY AUTO_INCREMENT
uuid VARCHAR(36) UNIQUE
first_name VARCHAR(100)
last_name VARCHAR(100)
email VARCHAR(255) UNIQUE
password VARCHAR(255) -- bcrypt hash
locale VARCHAR(10) -- 'de', 'en'
theme VARCHAR(20) -- 'light', 'dark'
active TINYINT DEFAULT 1
primary_tenant_id INT FK -> tenants.id
current_tenant_id INT FK -> tenants.id
created_by INT FK -> users.id
modified_by INT FK -> users.id
created DATETIME
modified DATETIME
```
#### tenants
```sql
id INT PRIMARY KEY AUTO_INCREMENT
uuid VARCHAR(36) UNIQUE
description VARCHAR(255) -- Name/Firma
address VARCHAR(255)
postal_code VARCHAR(20)
city VARCHAR(100)
country VARCHAR(100)
vat_id VARCHAR(50) -- MwSt-ID
tax_number VARCHAR(50)
phone VARCHAR(50)
email VARCHAR(255)
website VARCHAR(255)
created_by INT FK -> users.id
modified_by INT FK -> users.id
created DATETIME
modified DATETIME
```
#### roles
```sql
id INT PRIMARY KEY AUTO_INCREMENT
uuid VARCHAR(36) UNIQUE
description VARCHAR(255) -- Rollenname
created_by INT FK -> users.id
modified_by INT FK -> users.id
created DATETIME
modified DATETIME
```
#### permissions
```sql
id INT PRIMARY KEY AUTO_INCREMENT
key VARCHAR(100) UNIQUE -- z.B. 'users.view'
description VARCHAR(255)
created DATETIME
```
#### departments
```sql
id INT PRIMARY KEY AUTO_INCREMENT
uuid VARCHAR(36) UNIQUE
description VARCHAR(255) -- Abteilungsname
created_by INT FK -> users.id
modified_by INT FK -> users.id
created DATETIME
modified DATETIME
```
### Verknüpfungstabellen
| Tabelle | Beschreibung |
|---------|--------------|
| `user_tenants` | User ↔ Tenant (M:N) |
| `user_roles` | User ↔ Role (M:N) |
| `user_departments` | User ↔ Department (M:N) |
| `role_permissions` | Role ↔ Permission (M:N) |
| `tenant_departments` | Tenant ↔ Department (M:N) |
### Vordefinierte Permissions
```
users.view, users.create, users.update, users.delete
users.self_update, users.update_assignments
tenants.view, tenants.create, tenants.update, tenants.delete
departments.view, departments.create, departments.update, departments.delete
roles.view, roles.create, roles.update, roles.delete
permissions.view, permissions.create, permissions.update, permissions.delete
settings.view, settings.update
mail_log.view
stats.view
```
---
## 5. PHP-Architektur
### Schichten-Architektur
```
┌─────────────────────────────────────────────┐
│ Pages │
│ (Controller/Actions) │
├─────────────────────────────────────────────┤
│ Services │
│ (Business Logic, Validation) │
├─────────────────────────────────────────────┤
│ Repositories │
│ (Data Access, SQL) │
├─────────────────────────────────────────────┤
│ DB │
│ (MintyPHP Database) │
└─────────────────────────────────────────────┘
```
### Service-Layer (`lib/Service/`)
Services implementieren die Geschäftslogik:
```php
namespace MintyPHP\Service;
class UserService
{
// Listen
public static function list(): array
public static function listPaged(array $options): array
// Finder
public static function findByUuid(string $uuid): ?array
public static function findById(int $id): ?array
public static function findByEmail(string $email): ?array
// CRUD
public static function createFromAdmin(array $input, int $currentUserId): array
public static function updateFromAdmin(int $userId, array $input, int $currentUserId): array
public static function deleteByUuid(string $uuid, int $currentUserId): array
// Bulk-Operationen
public static function deleteByUuids(array $uuids, int $currentUserId): array
public static function setActiveByUuids(array $uuids, bool $active, int $currentUserId): array
}
```
**Wichtige Services:**
| Service | Verantwortung |
|---------|---------------|
| `AuthService` | Login, Logout, Session-Management |
| `UserService` | Benutzerverwaltung |
| `TenantService` | Mandantenverwaltung |
| `RoleService` | Rollenverwaltung |
| `PermissionService` | Berechtigungsprüfung und -verwaltung |
| `DepartmentService` | Abteilungsverwaltung |
| `MailService` | E-Mail-Versand |
| `SettingService` | App-Einstellungen |
### Repository-Layer (`lib/Repository/`)
Repositories kapseln Datenbankzugriffe:
```php
namespace MintyPHP\Repository;
class UserRepository
{
public static function list(): array
{
return DB::select('SELECT * FROM users ORDER BY last_name, first_name');
}
public static function find(int $id): ?array
{
return DB::selectOne('SELECT * FROM users WHERE id = ?', [$id]);
}
public static function create(array $data): int
{
return DB::insert('users', $data);
}
public static function update(int $id, array $data): bool
{
return DB::update('users', $data, ['id' => $id]) > 0;
}
}
```
### Support-Layer (`lib/Support/`)
#### Guard - Zugriffskontrolle
```php
namespace MintyPHP\Support;
class Guard
{
// Nur für angemeldete Benutzer
public static function requireLogin(string $redirect = 'login'): void
// Spezifische Berechtigung erforderlich
public static function requirePermission(string $permissionKey, string $redirect = 'admin'): void
// Berechtigung oder 403 (für AJAX)
public static function requirePermissionOrForbidden(string $permissionKey): void
}
```
#### Flash - Session-Nachrichten
```php
namespace MintyPHP\Support;
class Flash
{
public static function success(string $message, ?string $scope = null, ?string $key = null): string
public static function error(string $message, ?string $scope = null, ?string $key = null): string
public static function info(string $message, ?string $scope = null, ?string $key = null): string
public static function peek(?string $scope = null): array
public static function has(): bool
}
```
### Helper-Funktionen (`lib/Support/helpers/`)
```php
// HTML-Escape
e($string);
// Asset-URL
asset('css/style.css');
// Lokalisierte URL
lurl('admin/users');
// Übersetzung
t('Hello');
t('Welcome %s', $name);
// App-Einstellung
appSetting('app_title');
// Berechtigung prüfen
can('users.view');
```
---
## 6. Frontend-Architektur
### CSS-Struktur (`web/css/`)
```
web/css/
├── base/
│ ├── variables.base.css # CSS-Variablen (Basis + Light/Dark)
│ ├── variables.theme-dark-green.css # Theme-Overrides
│ └── variables.contrast.css # High-Contrast Overrides
├── layout/
│ ├── app-shell.css # Haupt-Layout
│ ├── app-topbar.css # Header
│ ├── app-sidebar.css # Navigation
│ └── app-aside-icon-bar.css
├── components/
│ ├── app-buttons.css
│ ├── app-forms.css
│ ├── app-list-table.css
│ ├── app-badges.css
│ ├── app-tabs.css
│ ├── app-flash.css
│ └── vendor-*.css # Third-Party
└── pages/
└── app-login.css
```
### JavaScript-Module (`web/js/`)
ES-Module-Architektur:
```
web/js/
├── app-init.js # Haupt-Initialisierung
├── core/
│ └── app-grid-factory.js # Grid/Tabellen-Generator
├── components/
│ ├── app-tenant-switcher.js
│ ├── app-theme-toggle.js
│ ├── app-bulk-selection.js
│ ├── app-flash-auto-dismiss.js
│ └── ...
└── pages/
├── app-users-list.js
└── app-list-utils.js
```
### Modul-Pattern
```javascript
// web/js/components/app-tenant-switcher.js
const switchTenant = async (link) => {
const tenantId = link.dataset.switchTenant;
// ...
};
const initTenantSwitcher = () => {
const tenantLinks = document.querySelectorAll('[data-switch-tenant]');
tenantLinks.forEach(link => {
link.addEventListener('click', (e) => {
e.preventDefault();
switchTenant(link);
});
});
};
// Auto-Init bei DOM Ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initTenantSwitcher);
} else {
initTenantSwitcher();
}
```
### Import in app-init.js
```javascript
// web/js/app-init.js
import './components/app-tenant-switcher.js';
import './components/app-theme-toggle.js';
import './components/app-flash-auto-dismiss.js';
// ...
```
---
## 7. Template-System
### Struktur
```
templates/
├── default.phtml # Haupt-Layout
├── login.phtml # Login-Layout
├── error.phtml # Fehler-Layout
├── partials/
│ ├── app-topbar.phtml
│ ├── app-sidebar.phtml
│ ├── app-aside.phtml
│ ├── app-flash.phtml
│ └── app-footer.phtml
└── emails/
├── de/
│ ├── access_info.html
│ └── reset_code.html
└── en/
└── ...
```
### Template-Rendering
```php
<!-- templates/default.phtml -->
<!DOCTYPE html>
<html lang="<?php e(I18n::$locale); ?>">
<head>
<title><?php e(Buffer::get('title') ?? appTitle()); ?></title>
<link rel="stylesheet" href="<?php e(asset('css/app.css')); ?>">
</head>
<body>
<?php require 'partials/app-topbar.phtml'; ?>
<main>
<?php require 'partials/app-flash.phtml'; ?>
<?php echo Buffer::get('html'); ?>
</main>
<script type="module" src="<?php e(asset('js/app-init.js')); ?>"></script>
</body>
</html>
```
### Page mit View
```php
// pages/admin/users/index().php
use MintyPHP\Buffer;
use MintyPHP\Support\Guard;
Guard::requirePermissionOrForbidden('users.view');
$users = UserService::list();
Buffer::set('title', t('Users'));
// View wird automatisch geladen: pages/admin/users/index(default).phtml
```
---
## 8. Authentifizierung und Autorisierung
### Login-Ablauf
```
1. POST /login
├─> AuthService::login(email, password)
│ ├─> Auth::login() - Passwort-Verifikation
│ ├─> $_SESSION['user'] setzen
│ ├─> PermissionService::getUserPermissions()
│ │ └─> $_SESSION['permissions'] setzen
│ └─> AuthService::loadTenantDataIntoSession()
│ ├─> $_SESSION['available_tenants']
│ └─> $_SESSION['current_tenant']
└─> Router::redirect('admin')
```
### Berechtigungsprüfung
```php
// In Page
Guard::requirePermissionOrForbidden(PermissionService::USERS_VIEW);
// Im Template
<?php if (can('users.create')): ?>
<a href="admin/users/create">Neuer Benutzer</a>
<?php endif; ?>
```
### Session-Struktur
```php
$_SESSION = [
'user' => [
'id' => 1,
'uuid' => 'abc-123',
'email' => 'user@example.com',
'first_name' => 'Max',
'last_name' => 'Mustermann',
'locale' => 'de',
'theme' => 'dark',
],
'permissions' => [
'user_id' => 1,
'keys' => ['users.view', 'users.create', ...],
],
'available_tenants' => [
['id' => 1, 'uuid' => '...', 'description' => 'Tenant A'],
['id' => 2, 'uuid' => '...', 'description' => 'Tenant B'],
],
'current_tenant' => [
'id' => 1,
'uuid' => '...',
'description' => 'Tenant A',
],
];
```
---
## 9. Internationalisierung
### Sprachdateien
```json
// i18n/default_de.json
{
"Login": "Anmelden",
"Users": "Benutzer",
"At least %d characters": "Mindestens %d Zeichen"
}
// i18n/default_en.json
{
"Login": "Login",
"Users": "Users",
"At least %d characters": "At least %d characters"
}
```
### Verwendung
```php
// Einfache Übersetzung
t('Login')
// Mit Parametern
t('At least %d characters', 12)
// Im Template
<?php e(t('Users')); ?>
```
### Sprachauflösung
Priorität:
1. URL-Präfix (`/de/admin`, `/en/admin`)
2. Session (`$_SESSION['user']['locale']`)
3. Cookie (`locale=de`)
4. Default (`APP_LOCALE`)
---
## 10. Best Practices
### Service-Rückgabewerte
```php
// Erfolg
return [
'ok' => true,
'uuid' => $uuid,
'id' => $id,
];
// Fehler
return [
'ok' => false,
'error' => 'validation_error',
'errors' => ['email' => 'Email ist erforderlich'],
'form' => $sanitizedInput,
];
```
### Datenbankzugriff
```php
// RICHTIG - Prepared Statements
$user = DB::selectOne('SELECT * FROM users WHERE email = ?', [$email]);
// FALSCH - SQL-Injection
$user = DB::selectOne("SELECT * FROM users WHERE email = '$email'");
```
### Template-Ausgabe
```php
// RICHTIG - Escape
<?php e($user['email']); ?>
// FALSCH - XSS-Risiko
<?php echo $user['email']; ?>
```
### Passwort-Anforderungen
- Mindestens 12 Zeichen
- Mindestens 1 Großbuchstabe
- Mindestens 1 Kleinbuchstabe
- Mindestens 1 Ziffer
- Mindestens 1 Sonderzeichen
### Code-Organisation
1. **Services** - Geschäftslogik und Validierung
2. **Repositories** - Nur Datenbankzugriff, keine Logik
3. **Guards** - Zugriffskontrolle
4. **Helpers** - Wiederverwendbare Funktionen
5. **Pages** - Koordination, keine Logik
---
## Weiterführende Dokumentation
- [MintyPHP Framework](https://github.com/mevdschee/php-crud-api)
- [Docker Setup](../docker-compose.yml)
- [Database Schema](../db/init/init.sql)

View File

@@ -1,25 +0,0 @@
# Search + Upload TODO (Internal)
Goal: Unified search across DB data and file contents (PDF, DOCX), starting with MariaDB FULLTEXT and private file storage.
## MVP Steps
1) Create `documents` table (uuid, original_name, mime, size, storage_path, content_text, created).
2) Add FULLTEXT index on `content_text` (and later title/metadata fields if needed).
3) Build `FileUploadService`:
- Validate size/type (PDF/DOCX).
- Store outside `/web` (e.g. `storage/uploads/{uuid}/file.ext`).
- Extract text:
- PDF: `pdftotext` (poppler-utils).
- DOCX: unzip + parse `word/document.xml` (fallback: LibreOffice headless).
- Save `content_text` + metadata.
4) Add upload action + form (admin area).
5) Add search endpoint:
- Search DB entities.
- Search `documents.content_text`.
- Merge results into one list.
## Future Enhancements
- Async extraction (queue/worker) for large files.
- Dedicated search engine (Meilisearch/Typesense) when scale/relevance needs grow.
- Result ranking with field weighting (title > body).
- Optional language detection / stemming.

119
docs/anfragelimits.md Normal file
View File

@@ -0,0 +1,119 @@
# Anfragelimits (Rate Limiting)
Letzte Aktualisierung: 2026-02-21
Dieses Dokument beschreibt das aktuelle zentrale Rate-Limiting für Login und API.
## Ziel
- Brute-Force auf Login reduzieren.
- API-Missbrauch (Burst/Spam) abfangen.
- Einheitliches Verhalten mit `429` + `Retry-After`.
## Technischer Aufbau
### Storage
Tabelle: `request_rate_limits`
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
Schema:
- Tabelle wird in `db/init/init.sql` mit angelegt.
- Für Bestandsumgebungen sollte ein idempotentes SQL-Update bereitgestellt werden.
### Service/Repository
- Service: `/lib/Service/Security/RateLimiterService.php`
- Repository: `/lib/Repository/Security/RateLimitRepository.php`
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.
## API Rate Limiting
Integration:
- `/lib/Http/ApiBootstrap.php` (vor Auth)
Aktuelle Limits:
1. Bearer vorhanden (`token + ip`)
- Scope: `api.token_ip`
- Key: `<selector>|<ip>`
- Limit: `300` Hits pro `60` Sekunden
- Block: `120` Sekunden
2. Kein Bearer (`ip-only`)
- Scope: `api.ip`
- Key: `<ip>`
- Limit: `120` Hits pro `60` Sekunden
- Block: `120` Sekunden
Bei überschreitung:
- Response: `429`
- Body: `{"error":"rate_limit_exceeded"}`
- Header: `Retry-After: <sekunden>`
## Login Rate Limiting
Integration:
- `/pages/auth/login().php`
Aktuelle Limits:
1. Schritt `resolve_email` (IP)
- Scope: `login.resolve.ip`
- Key: `<ip>`
- Limit: `25` Hits pro `600` Sekunden
- Block: `300` Sekunden
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.
## 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).

42
docs/api.md Normal file
View File

@@ -0,0 +1,42 @@
# REST-API (v1)
Letzte Aktualisierung: 2026-02-21
## Single Source of Truth
Die API-Spezifikation wird zentral gepflegt in:
- `/docs/openapi.yaml`
Diese Datei ist die verbindliche technische Referenz für:
- Endpunkte
- Request-/Response-Schemas
- Statuscodes
- Auth-Header
## API-Doku im Admin
- UI: `/admin/api-docs`
- Zugriff: Permission `api_docs.view`
- Quelle der UI: `/docs/openapi.yaml`
- Swagger UI ist read-only (kein `Try it out`).
## Warum diese Seite kurz ist
Diese Seite bleibt bewusst knapp, damit keine doppelte Pflege entsteht.
Alle fachlichen und technischen API-Details stehen nur in `/docs/openapi.yaml`.
## Änderungsprozess (verbindlich)
1. API-Änderung immer zuerst in `/docs/openapi.yaml` pflegen.
2. `/admin/api-docs` öffnen und Rendering kurz prüfen.
3. API-Request per Client testen (z. B. Yaak/Postman/cURL).
4. Nur falls nötig hier in `api.md` organisatorische Hinweise ergänzen (keine Endpoint-Details).
## Verwandte Dokumente
- Rate Limiting: `/docs/anfragelimits.md`
- Sicherheit/Permissions: `/docs/sicherheitsmodell.md`
- Troubleshooting: `/docs/fehlerbehebung.md`

190
docs/architektur.md Normal file
View File

@@ -0,0 +1,190 @@
# Architektur
Letzte Aktualisierung: 2026-02-21
## 1) Zielbild
Die Anwendung ist eine servergerenderte Multi-Tenant-Admin-Anwendung mit klarer Trennung von:
- 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/**`)
## 2) Request-Lebenszyklus
Zentraler Entry Point ist `/web/index.php`.
Ablauf pro Request:
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
## 3) Routing-Modell
- Route-Definitionen liegen in `/config/routes.php`.
- Registrierung erfolgt in `/config/router.php`.
- `public=true` in `routes.php` steuert, welche Pfade ohne Login erlaubt sind.
MintyPHP-Konvention (Dateinamensrouting):
- `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
- Binarausgaben (z. B. Onboarding-PDF/ZIP) setzen `MINTY_ALLOW_OUTPUT` im Action-Endpoint.
## 4) Schichten und Verantwortungen
### 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
### 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:
- `SettingService::get*` / `SettingService::set*` -> DB
- `appSetting(...)` -> Datei-Cache (`SettingCacheService`)
- 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`

View File

@@ -0,0 +1,110 @@
# Benutzer-Lifecycle-Policy
Letzte Aktualisierung: 2026-02-21
## Ziel
Globale Regeln für automatische Benutzer-Deaktivierung und spätere Löschung.
- Stufe 1: Benutzer wird auf `inactive` gesetzt.
- Stufe 2: Inaktiver Benutzer wird nach weiterer Frist gelöscht (Hard Delete).
## Settings
Die Regeln werden in `settings` gespeichert:
- `user_inactivity_deactivate_days`
- `user_inactivity_delete_days`
Semantik:
- Wertebereich: `0..3650`
- `0` deaktiviert die jeweilige Regel
- Löschung wird nur angewendet, wenn Deaktivierung aktiv ist (`deactivate > 0`)
Defaults:
- Deaktivierung: `180`
- Löschung: `365` (ab Inaktivsetzung)
## Referenzdaten
- Deaktivierung basiert auf `COALESCE(last_login_at, created)`.
- Löschung basiert auf `active_changed_at` (nur wenn gesetzt).
## Privilegierte Benutzer (ausgenommen)
Automatische Lifecycle-Aktionen ignorieren Benutzer mit mindestens einer aktiven Rolle, die eine der Permissions hat:
- `settings.update`
- `tenants.update`
## Ausführung
### Manueller Run (Admin UI)
- Endpoint: `POST /admin/settings/run-user-lifecycle`
- Schutz: Login + `settings.update` + CSRF
### Cron/CLI
- Bevorzugt über den zentralen Scheduler:
- Script: `bin/scheduler-run.php` (führt fällige Jobs aus, inkl. `user_lifecycle_run`)
- Beispiel (minütlich):
```bash
* * * * * docker compose exec php php bin/scheduler-run.php
```
Exit-Codes:
- `0` erfolgreich
- `1` Fehler/Lock-Konflikt
## Concurrency
Zur Vermeidung paralleler Runs wird ein MySQL-Lock verwendet:
- `GET_LOCK('user_lifecycle_maintenance', 0)`
- `RELEASE_LOCK('user_lifecycle_maintenance')`
## Session-Wirkung
Wenn ein bereits eingeloggter Benutzer automatisch deaktiviert wurde, greift die bestehende Session-Refresh-Logik beim nächsten Request und führt zum Logout.
## Audit-Log (Delete/Restore)
Lifecycle-Events werden in `user_lifecycle_audit_log` protokolliert.
- `deactivate`: erfolgreich ausgeführte automatische Deaktivierung
- `delete`: Löschversuch mit Snapshot
- `restore`: Wiederherstellung aus einem Delete-Event
Wichtige Regeln:
- Delete ist fail-closed: ohne erfolgreich gespeicherten Snapshot wird nicht gelöscht.
- Snapshot wird verschlüsselt in `snapshot_enc` gespeichert.
- Snapshot enthält nur whitelisted Profilfelder (keine Secrets/Tokens).
- Audit-Retention: 365 Tage (`POST /admin/user-lifecycle-audit/purge`).
## Wiederherstellung (ohne Assignments)
Restore ist nur für `action=delete` + `status=success` möglich und nur einmal pro Event.
Konfliktregeln (Hard fail):
- UUID existiert bereits
- E-Mail existiert bereits
Restore-Ergebnis:
- User wird neu angelegt mit Snapshot-Stammdaten
- `active=0`
- zufälliges Passwort
- keine Wiederherstellung von Tenant-/Role-/Department-Zuweisungen
## Rechte
- Ansicht Lifecycle-Protokolle: `user_lifecycle_audit.view`
- Wiederherstellen aus Lifecycle-Protokoll: `users.lifecycle_restore`
- Purge: `settings.update`

View File

@@ -0,0 +1,82 @@
# Benutzerdefinierte Felder (V1) Kurzreferenz
Letzte Aktualisierung: 2026-02-21
## Ziel
Tenant-spezifische Zusatzfelder für User:
- Definitionen und Optionen werden pro Tenant gepflegt.
- Werte werden pro User gespeichert.
- Address-Book-Filter nutzen dynamische Query-Parameter.
## Berechtigungen
- `custom_fields.manage`
- Definitionen/Optionen im Tenant-Tab pflegen.
- `custom_fields.edit_values`
- Werte im User-Tab pflegen.
## Tabellen
- `tenant_custom_field_definitions`
- Felddefinition pro Tenant (Label, Typ, aktiv, filterbar, `field_key` intern).
- `tenant_custom_field_options`
- Optionen für `select`/`multiselect` je Definition.
- `user_custom_field_values`
- Gespeicherter Wert pro `user_id + definition_id` (unique).
- `user_custom_field_value_options`
- N:M-Optionen für Multiselect-Werte.
## Typen und Verhalten
- `text`, `textarea`
- Eingabe am User möglich.
- Nicht als Address-Book-Filter zugelassen.
- `select`, `multiselect`, `boolean`, `date`
- Eingabe am User möglich.
- Optional als Address-Book-Filter (wenn `is_filterable=1`).
## Wichtige Regeln
- `field_key`
- Wird serverseitig aus Label erzeugt und tenant-weit eindeutig gehalten.
- Kein manuelles UI-Feld.
- User-Werte sind in V1 optional (keine Pflichtvalidierung).
- Bei Tenant-Wechsel am User werden Werte außerhalb der aktuellen User-Tenants bereinigt.
## Query-Parameter im Address Book
- `cf_<definition_uuid>`
- scalar/select/boolean
- `cfm_<definition_uuid>`
- multiselect (CSV mit Option-IDs)
- `cfd_<definition_uuid>_from`
- `cfd_<definition_uuid>_to`
- date-range
Unbekannte/manipulierte Parameter werden ignoriert.
## Relevante Pfade
- Tenant-Tab Formular:
- `/pages/admin/tenants/_form.phtml`
- Tenant Actions:
- `/pages/admin/tenants/custom-field-create($id).php`
- `/pages/admin/tenants/custom-field-update($id).php`
- `/pages/admin/tenants/custom-field-delete($id).php`
- User-Werte:
- `/lib/Service/CustomField/UserCustomFieldValueService.php`
- `/lib/Repository/CustomField/*`
- Address-Book-Filter:
- `/pages/address-book/index().php`
- `/pages/address-book/index(default).phtml`
## Troubleshooting (kurz)
- Multiselect speichert nichts:
- Feldname ohne doppeltes `[]` prüfen.
- Optionen sind leer:
- Label-Key für MultiSelect auf `label`/`description` prüfen.
- Filterbar ist deaktiviert:
- Typ ist nicht in `select|multiselect|boolean|date`.

View File

@@ -1,17 +0,0 @@
# Conventions (Short)
## CSS
- All project files use `app-` prefix.
- Vendor overrides use `vendor-` prefix.
- Structure: `base/`, `layout/`, `components/`, `pages/`.
## JS
- All project files use `app-` prefix.
- Vendor scripts stay in `web/vendor`.
- Structure: `core/`, `components/`, `pages/`.
## Partials
- All partials use `app-` prefix (e.g. `app-footer.phtml`).
## PHP (Lib)
- Services in `lib/Service`, repositories in `lib/Repository`.

18
docs/docker-betrieb.md Normal file
View File

@@ -0,0 +1,18 @@
# Docker-Betrieb
Letzte Aktualisierung: 2026-02-21
## Übersicht
Die Docker-Dokumentation ist in zwei klare Pfade getrennt:
1. Lokal entwickeln:
- `/docs/docker-lokal.md`
2. Produktiv betreiben:
- `/docs/docker-produktiv.md`
## Empfehlung
- 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.

65
docs/docker-lokal.md Normal file
View File

@@ -0,0 +1,65 @@
# Docker lokal
Letzte Aktualisierung: 2026-02-21
## Ziel
Diese Anleitung bringt die Anwendung lokal schnell und stabil zum Laufen.
## Voraussetzungen
- Docker Desktop (oder Docker Engine + Compose)
- Port `8080` (App) und `8081` (phpMyAdmin) sind frei
## Start in 4 Schritten
1. ENV-Datei anlegen:
```bash
cp .env.example .env
```
2. Container bauen und starten:
```bash
docker compose up --build -d
```
3. Anwendung öffnen:
- App: `http://localhost:8080`
- phpMyAdmin: `http://localhost:8081`
4. Logs prüfen (optional):
```bash
docker compose logs -f nginx php
```
## Nützliche Befehle
Neustart:
```bash
docker compose restart nginx php
```
Stoppen:
```bash
docker compose down
```
Scheduler-Logs:
```bash
docker compose logs -f scheduler
```
## Häufige lokale Probleme
- Änderungen werden nicht sichtbar:
- `docker compose restart nginx php`
- Datenbank startet nicht:
- DB-Container-Logs prüfen: `docker compose logs -f db`
- Port-Konflikt:
- in `docker-compose.yml` lokale Port-Mappings anpassen

77
docs/docker-produktiv.md Normal file
View File

@@ -0,0 +1,77 @@
# Docker produktiv
Letzte Aktualisierung: 2026-02-21
## Ziel
Diese Anleitung beschreibt den Betrieb mit Domain auf Basis von `/docker-compose.prod.yml`.
## Voraussetzungen
- DNS zeigt auf den Server (`A/AAAA`)
- Ports `80` und `443` sind offen
- TLS-Zertifikate vorhanden
## Produktionsdateien
- Compose: `/docker-compose.prod.yml`
- Nginx: `/docker/nginx/prod.conf`
- PHP: `/docker/php/php.prod.ini`
- ENV-Vorlage: `/.env.prod.example`
## 1) Umgebung vorbereiten
`.env` auf Produktionswerte setzen:
- `APP_URL=https://deine-domain.tld`
- `APP_ENV=prod`
- `APP_DEBUG=false`
- stabile `APP_CRYPTO_KEY`
- starke DB/SMTP-Passwörter
## 2) Nginx auf Domain/TLS setzen
In `/docker/nginx/prod.conf`:
- `server_name` auf echte Domain(s) anpassen
Zertifikate ablegen:
- `/docker/nginx/certs/fullchain.pem`
- `/docker/nginx/certs/privkey.pem`
## 3) Produktion starten
```bash
docker compose -f docker-compose.prod.yml up --build -d
```
Konfiguration validieren:
```bash
docker compose -f docker-compose.prod.yml config
```
## 4) Betrieb prüfen
```bash
docker compose -f docker-compose.prod.yml logs -f nginx php scheduler
```
Prüfen:
- App über `https://...` erreichbar
- Login funktioniert
- Scheduler läuft minütlich (Logausgaben)
## 5) Bestandssysteme
Bei bestehender Datenbank:
- idempotentes SQL-Update manuell ausführen
(Auto-Init über `db/init/init.sql` greift nur beim ersten DB-Setup)
## Hinweise
- `phpmyadmin` ist in Prod absichtlich nicht enthalten.
- Wenn ein externer Reverse-Proxy TLS terminiert, kann `prod.conf` auf HTTP intern reduziert werden; `APP_URL` bleibt trotzdem `https://...`.

View File

@@ -0,0 +1,62 @@
# Dokumentation erweitern
Letzte Aktualisierung: 2026-02-21
## Ziel
Diese Seite beschreibt den Standardprozess, um die Projektdokumentation sauber, konsistent und dauerhaft wartbar zu erweitern.
## Single Source of Truth
- 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.
## Standardablauf für neue Doku-Dateien
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.
## Struktur- und Stilregeln
- 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).
## Wichtig für die Suche
- 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.
## 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?
## 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.

View File

@@ -0,0 +1,60 @@
# Einstellungen: Speicherung (DB + Cache)
Letzte Aktualisierung: 2026-02-21
## Kurzfassung
- **Source of truth ist immer die Tabelle `settings` in der Datenbank.**
- `config/settings.php` ist nur ein **Teil-Cache** für wenige, sehr häufig gelesene UI-Basiswerte.
- Nicht alle Settings müssen oder sollen im Datei-Cache stehen.
## Warum es beides gibt
Die Kombination ist bewusst so gebaut:
1. **DB als Wahrheit**
- Alle Settings werden persistiert in `settings`.
- Validierung und Schreiblogik laufen über `SettingService`.
2. **Datei-Cache für Hot-Path-Reads**
- `config/settings.php` wird durch `SettingCacheService` geschrieben.
- Verwendet wird er über `appSetting(...)` nur für globale Basiswerte wie:
- `app_title`
- `app_locale`
- `app_theme`
- `app_theme_user`
- `app_registration`
- `app_primary_color`
## Was **nicht** im Cache der Datei liegen muss
Sicherheits- oder Integrationswerte werden direkt aus DB gelesen, z. B.:
- SMTP (`smtp_host`, `smtp_port`, `smtp_user`, `smtp_from`, ...)
- Microsoft SSO Shared App Settings
- API-spezifische Policies
- User-Lifecycle-Policies (`user_inactivity_deactivate_days`, `user_inactivity_delete_days`)
- Scheduler-/Job-Konfiguration (`scheduled_jobs`, `scheduled_job_runs`)
Daher ist es korrekt, wenn diese Werte in `settings` (DB) vorhanden sind, aber nicht in `config/settings.php`.
## Laufzeitfluss
1. Admin speichert Settings in `admin/settings`.
2. `SettingService::set*` schreibt in DB.
3. `SettingCacheService::update(...)` aktualisiert den Datei-Cache nur für den definierten Teilbereich.
4. UI-Helfer `appSetting(...)` liest danach aus Datei-Cache.
## Wenn DB und Datei scheinbar nicht zusammenpassen
Das ist oft erwartbar, solange es um nicht-gecachte Keys geht.
Prüfregel:
- Geht es um `appSetting(...)`-Keys? Dann Datei-Cache relevant.
- Geht es um SMTP/SSO/API-Details? Dann DB ist massgeblich.
## Betriebs-Hinweis
Direkte DB-Änderungen an gecachten `appSetting(...)`-Keys werden erst wirksam, wenn der Datei-Cache aktualisiert wurde
(z. B. durch erneutes Speichern in den Settings).

View File

@@ -0,0 +1,88 @@
# Entwickler-Checkliste
Letzte Aktualisierung: 2026-02-22
## Vor dem Coden
- [ ] Betroffene Schicht klar? (`Repository`, `Service`, `pages`, `templates`, `web/js`)
- [ ] Permission- und Tenant-Scope-Auswirkungen verstanden?
- [ ] Bestehendes Partial/Helper wiederverwendbar?
## 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('...')`
## UI/UX-Konsistenz
- [ ] 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
## Assets
- [ ] 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
## 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
## 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
## Periodisch (bei Feature-Entfernung / vor Release)
- [ ] Ungenutzte Composer-Pakete prüfen:
- [ ] `docker compose exec php vendor/bin/composer-unused`

48
docs/erste-aenderung.md Normal file
View File

@@ -0,0 +1,48 @@
# Leitfaden für die erste Änderung
Letzte Aktualisierung: 2026-02-21
## Ziel
Ein neuer Entwickler soll den ersten Change strukturiert und reproduzierbar umsetzen können.
## Beispiel 1: Spalte in Admin-Liste ergänzen
### Schrittfolge
1. Data-Endpoint erweitern (z. B. `pages/admin/users/data().php`)
2. Falls nötig: neue Repository-Query/Count-Methode (z. B. `lib/Repository/User/UserRepository.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
### Done-Kriterien
- Kein SQL in View-Dateien
- Neue Werte im Endpoint dokumentiert/benannt
- Sortierung und Mapping im Grid konsistent
- Bei neuen Filtern: Query-Param, Repository-Filter und Grid-State konsistent
## Beispiel 2: API-Response erweitern
### Schrittfolge
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
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
### Done-Kriterien
- Keine numerischen IDs exponieren, wenn UUID bereits Contract ist
- Fehlercodes konsistent (`ApiResponse::error(...)`)
- Kein Body-Leak in Logs/Audit
- OpenAPI und tatsächlich gelieferte Response-Felder stimmen 1:1
## Finaler Check
Vor PR/Merge diese Liste gegenprüfen:
- `/docs/entwickler-checkliste.md`

80
docs/erste-schritte.md Normal file
View File

@@ -0,0 +1,80 @@
# Erste Schritte
Letzte Aktualisierung: 2026-02-21
## Ziel
Diese Seite bringt neue Entwickler in unter 45 Minuten zu einem lauffähigen lokalen Setup inklusive erstem Smoke-Test.
## Voraussetzungen
- Docker + Docker Compose
- Git
- Node.js + npm (für ESLint)
- Zugriff auf dieses Repository
## 1) Projekt lokal starten
```bash
cp .env.example .env
docker compose up --build -d
```
> Alle ENV-Variablen sind dokumentiert in /docs/konfiguration.md.
> docker compose up startet auch den globalen Scheduler-Runner (Service scheduler) im Hintergrund.
Danach erreichbar:
- App: `http://localhost:8080`
- phpMyAdmin: `http://localhost:8081`
## 2) Login mit Seed-User (Fresh-Install)
Standard-Demo-User (aus `db/init/init.sql`):
- E-Mail: `demo@user.com`
- Passwort: `Demo123`
Wichtig:
- Der Seed-User wird nur beim initialen DB-Setup angelegt.
- Wenn bereits ein bestehendes Docker-Volume (`db_data`) vorhanden ist, kann der lokale Datenstand abweichen.
- Für einen sauberen Fresh-Stand kannst du lokal neu initialisieren (Achtung: löscht lokale DB-Daten):
```bash
docker compose down -v
docker compose up --build -d
```
## 3) Schneller Funktionscheck
1. Login funktioniert
2. Admin-Navigation ist sichtbar
3. `Admin -> Benutzer` lädt
4. `Admin -> Einstellungen` lädt
5. `Admin -> API docs` lädt (mit entsprechender Permission)
## 4) API-Schnelltest
1. In der Admin-UI einen API-Token erstellen (oder vorhandenen nutzen)
2. Beispiel-Call:
```bash
curl -H "Authorization: Bearer <TOKEN>" \
http://localhost:8080/api/v1/me
```
## 5) Qualitätschecks ausführen
```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
```
## 6) Nächste Doku-Schritte
- Täglicher Workflow: `/docs/lokale-entwicklung.md`
- Erster kleiner Change: `/docs/erste-aenderung.md`
- Architekturüberblick: `/docs/architektur.md`
- Sicherheitsmodell: `/docs/sicherheitsmodell.md`

83
docs/fehlerbehebung.md Normal file
View File

@@ -0,0 +1,83 @@
# Fehlerbehebung
Letzte Aktualisierung: 2026-02-21
## App reagiert nicht wie erwartet
### Symptom
UI zeigt alte Daten/Assets oder Verhalten wirkt stale.
### Lösung
```bash
docker compose restart php nginx
```
## Login/API verhalten sich unerwartet
### Symptom
401/403 bei API trotz gültigem Token.
### 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
## Migration oder Schema-Probleme
### Symptom
Neue Felder/Features fehlen lokal.
### Lösung
1. Aktuelles Schema in `db/init/init.sql` prüfen
2. Für Bestandsumgebung idempotentes SQL-Update ausführen
3. Danach Container neu starten:
```bash
docker compose restart php
```
## i18n Fehler
### Symptom
Fehlende Translation-Keys oder Mixed-Language UI.
### Lösung
```bash
docker compose exec php vendor/bin/phpunit --bootstrap vendor/autoload.php tests/I18n/TranslationKeysTest.php
```
## PHPStan/Tests schlagen plötzlich fehl
### Symptom
Fehler nach Refactor, obwohl Seite lädt.
### Lösung
```bash
docker compose exec php vendor/bin/phpstan analyse -c phpstan.neon --no-progress
docker compose exec php vendor/bin/phpunit --bootstrap vendor/autoload.php
```
Dann gezielt betroffene Datei/Service prüfen.
## Swagger UI lädt ungestylt oder inkonsistent
### Symptom
API docs sehen nicht nach App-Theme aus.
### Lösung
1. Prüfen, dass `Buffer::set('style_groups', json_encode(['api-docs']))` gesetzt ist
2. Prüfen, dass `css/vendor-overrides/swagger-ui.css` über `config/assets.php` eingebunden ist
3. Hard-Reload im Browser

78
docs/frontend-css.md Normal file
View File

@@ -0,0 +1,78 @@
# Frontend CSS
Letzte Aktualisierung: 2026-02-21
## Ziel
Diese Seite beschreibt die kompakte Standardstruktur für CSS in diesem Projekt: klar, wartbar und ohne Seiteneffekte.
## Struktur
- `/web/css/app-layers.css`
- zentraler Entrypoint für Layer-Reihenfolge.
- `/web/css/base`
- Variablen, Reset, Typografie, globale Tokens.
- `/web/css/layout`
- Layout-Bausteine (Sidebar, Header, Main-Layout).
- `/web/css/components`
- wiederverwendbare UI-Teile.
- `/web/css/pages`
- seitenbezogene Styles mit engem Scope.
- `/web/css/vendor-overrides`
- gezielte Overrides für externe Libraries.
## Grundregeln
1. Neue Styles immer so lokal wie möglich einordnen:
- erst `components`/`layout`,
- `pages` nur bei echtem Seitenspezifikum.
2. Keine globalen Überschreibungen ohne Scope.
3. Bestehende CSS-Variablen bevorzugen, keine neuen Farbwerte „inline“ verteilen.
4. Vendor-Anpassungen ausschließlich in `/web/css/vendor-overrides`.
## Entscheidungsmatrix: Neue Datei oder bestehende erweitern?
| Situation | Empfehlung | Zielpfad |
| --- | --- | --- |
| Bestehende Komponente bekommt kleine visuelle Erweiterung | Bestehende Datei erweitern | `/web/css/components/<bestehend>.css` |
| Neuer wiederverwendbarer UI-Baustein (mind. auf 2 Seiten) | Neue Komponentendatei anlegen | `/web/css/components/<neu>.css` |
| Änderung betrifft globales Layout (Sidebar, Header, Main) | Bestehende Layout-Datei erweitern | `/web/css/layout/<bestehend>.css` |
| Änderung ist nur für eine konkrete Seite relevant | Neue/ bestehende Seiten-Datei nutzen | `/web/css/pages/<seite>.css` |
| Externe Library muss übersteuert werden (Grid.js, Swagger UI) | Nur Vendor-Override ändern | `/web/css/vendor-overrides/<vendor>.css` |
| Neue Farben/Abstände/Typografie-Tokens werden benötigt | Erst Variablen in Base ergänzen, dann verwenden | `/web/css/base/*` |
Kurzregel:
- **Bestehende Datei erweitern**, wenn Scope und Verantwortung klar gleich bleiben.
- **Neue Datei anlegen**, wenn ein neuer klarer Verantwortungsbereich entsteht.
## Einbindung von Styles
Asset-Gruppen werden in `/config/assets.php` registriert.
Seiten laden bei Bedarf Gruppen über `Buffer::set('style_groups', ...)`.
Beispiel (Action):
```php
Buffer::set('style_groups', json_encode(['api-docs']));
```
## Benennungs- und Scope-Konvention
- Klassen konsistent mit `app-`-Präfix halten, wenn es eigene Komponenten sind.
- Status/Varianten über bestehende Attribute/Klassen abbilden (z. B. `data-variant`), nicht über zusätzliche Einmal-Klassen.
- Selektoren kurz halten, keine unnötig tiefen Ketten.
## Grid/Vendor
- Grid.js-spezifische Anpassungen: `/web/css/vendor-overrides/gridjs.css`
- Swagger UI-spezifische Anpassungen: `/web/css/vendor-overrides/swagger-ui.css`
Regel: Vendor-Overrides nur für Bibliotheks-Selektoren; projektspezifische UI-Stile in `components`/`pages`.
## Checkliste vor Merge
1. Liegt die Datei im richtigen Ordner (`components`, `layout`, `pages` oder `vendor-overrides`)?
2. Ist die Asset-Gruppe in `/config/assets.php` korrekt?
3. Wird die Gruppe auf der Zielseite wirklich geladen (`style_groups`)?
4. Gibt es ungewollte Seiteneffekte auf anderen Seiten?
5. Sind bestehende Variablen/Konventionen eingehalten?

101
docs/frontend-javascript.md Normal file
View File

@@ -0,0 +1,101 @@
# Frontend JavaScript
Letzte Aktualisierung: 2026-02-21
## Ordnerstruktur
- `/web/js/core`
- technische Basismodule (DOM/Factories)
- `/web/js/components`
- wiederverwendbare UI-Features
- `/web/js/pages`
- seitenspezifische Hilfen (z. B. Listeninitialisierung)
## Initialisierung
- `/web/js/app-boot.js`
- wird im `<head>` geladen
- setzt früh UI-States (no-js/js, Sidebar-/Contrast-LocalStorage)
- `/web/js/app-init.js`
- Modul-Entrypoint für Standardseiten
- importiert Komponenten zentral
- `/web/js/app-login-init.js`
- reduzierter Entrypoint für Loginseiten
## Modulkonvention (components/pages)
Empfohlenes Muster:
1. `init...()` Funktion exportieren
2. Früher Guard auf fehlende Elemente
3. Keine stillen Fehler verschlucken
4. Nur DOM/UI-Verhalten, keine Fachlogik
Template für neue Komponenten:
- `/web/js/components/_template.js`
## Defensive DOM-Nutzung
Nicht jede Seite hat jedes Element.
- Vor Zugriff immer Element-Existenz prüfen.
- Optional zentrale Helper (`app-dom.js`) verwenden.
- Module sollen auf nicht-passenden Seiten sauber no-op sein.
## Grid.js-Integration
- Gemeinsame Initialisierung über Core/Page-Utilities.
- Vendor-spezifische CSS-Anpassungen in:
- `/web/css/vendor-overrides/gridjs.css`
- Bei dynamischen Rows mit Lightbox/Tooltips: nach Render Refresh triggern.
### Grid URL State Sync
`createServerGrid(...)` synchronisiert bei `urlSync: true` den Grid-Zustand zentral in die URL.
- Search + Filter (bestehendes Verhalten)
- Sortierung über `order` + `dir`
- Pagination über `page` (1-basiert)
Optionale Konfiguration:
```js
createServerGrid({
// ...
urlSync: true,
urlState: {
pageParam: 'page',
sortOrderParam: 'order',
sortDirParam: 'dir',
cleanFirstPage: true,
syncPage: true,
syncSort: true
}
});
```
Hinweise:
- `page=1` wird bei `cleanFirstPage: true` nicht in der URL gehalten.
- Übernommene URL-Werte werden validiert:
- `page >= 1`
- `dir` nur `asc|desc`
- `order` nur bekannte Sort-Key-Spalten
- Bei Search-/Filter-Änderung wird auf Seite 1 zurückgesetzt.
## Linting
Prüfen mit:
```bash
npx eslint web/js
```
Auto-fix (nur für sichere Rule-Fixes):
```bash
npx eslint web/js --fix
```
Hinweis: `--fix` behebt format-/syntaxnahe Themen (z. B. fehlende Klammern), aber nicht automatisch alle Logikprobleme.

224
docs/geplante-aufgaben.md Normal file
View File

@@ -0,0 +1,224 @@
# Geplante Aufgaben
Letzte Aktualisierung: 2026-02-21
## Ziel
Zentrale Verwaltung geplanter Aufgaben im Admin-Bereich.
- 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`.
## Tabellen
- `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).
Run-Log-Retention:
- 90 Tage (Purge in Admin verfügbar).
## Rechte
- `jobs.view`: Bereich ansehen
- `jobs.manage`: Job-Konfiguration speichern
- `jobs.run_now`: Job manuell ausführen
Purge der Run-Logs bleibt unter `settings.update`.
## Betriebsmodell
Cron-Eintrag (Beispiel):
```bash
* * * * * docker compose exec php php bin/scheduler-run.php
```
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 reine Map von `job_key` zu Handler-Klasse 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)
```
### 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 static 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 static function execute(?int $actorUserId): array
{
$result = 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 handlers() eine Zeile hinzufügen:
private static function handlers(): array
{
return [
self::USER_LIFECYCLE_RUN => UserLifecycleJobHandler::class,
self::MEIN_JOB => MeinJobHandler::class, // ← neu
];
}
```
**3. Fertig.**
Keine anderen Dateien müssen angefasst werden. `ensureSystemJobs()` legt 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.
---
## Registrierte Jobs
| job_key | Handler-Klasse | Default-Schedule |
|----------------------|-----------------------------|---------------------|
| `user_lifecycle_run` | `UserLifecycleJobHandler` | täglich 02:15 |
---
## 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.

149
docs/globale-suche.md Normal file
View File

@@ -0,0 +1,149 @@
# Globale Suche erweitern
Letzte Aktualisierung: 2026-02-21
Diese Doku erklärt, wie neue Entitäten sauber in die globale Suche integriert werden.
## Überblick
Es gibt zwei Suchausgaben mit derselben Datenquelle:
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.
Die zentrale Fassade liegt in:
- `/lib/Support/SearchConfig.php`
Die eigentliche Logik ist in Provider aufgeteilt:
- `/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`
## Relevante Dateien
- `/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)
## Standard-Vorgehen für neue Resource
### 1) Resource in `SearchSqlResourceProvider::resources()` anlegen
Neue Struktur mit:
- `key` (z. B. `scheduled-jobs`)
- `label` (übersetzbar via `t(...)`)
- `permission` (z. B. `PermissionService::JOBS_VIEW`)
- `countSql` + `countParams`
- `previewSql` + `previewParams`
- `resultSql` + `resultParams`
Regeln:
- SQL immer mit `... like ? escape '\\'`.
- Bei tenant-sensitiven Entitäten `{{tenantFilter}}` nutzen und in `SearchSqlResourceProvider::tenantScopeFilters()` ergänzen.
- Preview-Query immer mit `limit ?`.
### 2) URL-/Icon-/Mapping ergänzen
- `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`
- 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)
Wichtig:
- 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

80
docs/importe.md Normal file
View File

@@ -0,0 +1,80 @@
# Importe
Letzte Aktualisierung: 2026-02-21
Diese Seite beschreibt den V1 CSV-Import unter `/admin/imports`.
## 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).
## Permissions
- `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.
## 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.
## Assignment-Regeln
- 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.
## Duplikate und Skip-Verhalten
- 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`
## Temporäre Dateien
- 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`

71
docs/index.md Normal file
View File

@@ -0,0 +1,71 @@
# Dokumentation
Letzte Aktualisierung: 2026-02-21
Diese Dokumente sind für Entwickler gedacht, die die Anwendung warten oder erweitern.
## Start hier (neue Entwickler)
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.
## Architektur und Regeln
- `/docs/architektur.md`
- Request-Lebenszyklus, Schichtentrennung, Frontend-Aufbau.
- `/docs/sicherheitsmodell.md`
- Auth, Permissions, Tenant-Scope, Public/API-Zugriffe.
- `/docs/konventionen.md`
- Coding- und Strukturkonventionen für PHP, Templates, CSS, JS.
## 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.
- `/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).
## API Referenz
- `/docs/api.md`
- API Quick Reference, Auth, Flows, Beispielaufrufe.
- `/docs/openapi.yaml`
- API Source of truth für Swagger UI.
## Betriebswissen
- `/docs/konfiguration.md`
- Alle ENV-Variablen mit Beschreibung, Standardwerten und Produktions-Checkliste.
- `/docs/fehlerbehebung.md`
- Häufige Fehlerbilder und schnelle Lösungen.
- `/docs/docker-lokal.md`
- Schneller lokaler Docker-Start inkl. typischer Befehle und Troubleshooting.
- `/docs/docker-produktiv.md`
- Produktionsbetrieb mit Domain, TLS und `docker-compose.prod.yml`.
- `/docs/docker-betrieb.md`
- Einstieg und Navigation zwischen lokaler und produktiver Docker-Doku.

103
docs/konfiguration.md Normal file
View File

@@ -0,0 +1,103 @@
# Konfiguration (ENV-Variablen)
Letzte Aktualisierung: 2026-02-21
Alle Konfiguration erfolgt über Umgebungsvariablen in der `.env`-Datei.
Vorlage: `.env.example` — niemals echte Credentials committen.
---
## App
| Variable | Standard | Beschreibung |
|---|---|---|
| `APP_NAME` | `App` | Anwendungsname (wird in UI-Titeln und E-Mails verwendet) |
| `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_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** |
| `APP_LOCALE` | `de` | Standard-Sprache der Anwendung |
| `APP_LOCALES` | `de,en` | Komma-getrennte Liste aller unterstützten Sprachen |
| `APP_I18N_DOMAIN` | `default` | Gettext-Domain — bestimmt welche `i18n/default_*.json` geladen wird, normalerweise nicht ändern |
---
## Session
| Variable | Standard | Beschreibung |
|---|---|---|
| `SESSION_NAME` | `app_session` | Name des Session-Cookies im Browser |
---
## Tenant
| Variable | Standard | Beschreibung |
|---|---|---|
| `TENANT_SCOPE_STRICT` | `true` | Erzwingt strikte Tenant-Isolation (`true`/`false`) — in Produktion immer `true` |
---
## Datenbank
| Variable | Standard | Beschreibung |
|---|---|---|
| `DB_HOST` | `db` | Hostname des MySQL/MariaDB-Servers (Docker-Service-Name oder IP) |
| `DB_PORT` | `3306` | Port des Datenbankservers |
| `DB_NAME` | `imvs` | Name der Datenbank |
| `DB_USER` | `imvs` | Datenbankbenutzer |
| `DB_PASS` | `imvs` | Passwort des Datenbankbenutzers |
| `DB_ROOT_PASSWORD` | `imvs_root` | Root-Passwort für Docker-Compose (wird nur vom DB-Container genutzt, nicht von PHP) |
---
## Cache
| Variable | Standard | Beschreibung |
|---|---|---|
| `CACHE_SERVERS` | `memcached:11211` | Memcached-Server als `host:port` (Docker-Service-Name oder IP) |
---
## Firewall
Die Firewall begrenzt gleichzeitige Requests pro IP (Concurrency-Schutz).
| Variable | Standard | Beschreibung |
|---|---|---|
| `FIREWALL_CONCURRENCY` | `10` | Max. gleichzeitige Requests pro IP |
| `FIREWALL_SPINLOCK_SECONDS` | `0.15` | Wartezeit in Sekunden zwischen Concurrency-Prüfungen |
| `FIREWALL_INTERVAL_SECONDS` | `300` | Zeitfenster in Sekunden für die Concurrency-Messung |
| `FIREWALL_CACHE_PREFIX` | `fw_concurrency_` | Prefix für Memcached-Keys der Firewall |
| `FIREWALL_REVERSE_PROXY` | `false` | Auf `true` setzen wenn die App hinter einem Reverse Proxy (Nginx, Traefik) läuft — sonst wird die Proxy-IP statt der echten Client-IP verwendet |
---
## SMTP (E-Mail)
| Variable | Standard | Beschreibung |
|---|---|---|
| `SMTP_HOST` | `smtp.example.com` | Hostname des SMTP-Servers |
| `SMTP_PORT` | `587` | Port des SMTP-Servers (587 = STARTTLS, 465 = SSL) |
| `SMTP_USER` | `no-reply@example.com` | SMTP-Benutzername / Absenderadresse |
| `SMTP_PASS` | _(leer)_ | SMTP-Passwort |
| `SMTP_FROM` | `no-reply@example.com` | Absender-E-Mail-Adresse |
| `SMTP_FROM_NAME` | `App` | Absendername in ausgehenden E-Mails |
| `SMTP_SECURE` | `tls` | Verschlüsselungstyp: `tls` (STARTTLS) oder `ssl` |
---
## Produktions-Checkliste
Folgende Variablen **müssen** vor Go-Live angepasst werden:
- [ ] `APP_ENV=prod`
- [ ] `APP_DEBUG=false`
- [ ] `APP_CRYPTO_KEY` — z. B. 64-stelligen Hex-Key generieren: `openssl rand -hex 32`
- [ ] `APP_URL` — auf die echte Domain setzen
- [ ] `TENANT_SCOPE_STRICT=true`
- [ ] `FIREWALL_REVERSE_PROXY=true` falls hinter Nginx/Traefik
- [ ] Alle DB-Credentials auf sichere Werte setzen
- [ ] Alle SMTP-Credentials auf echte Werte setzen

126
docs/konventionen.md Normal file
View File

@@ -0,0 +1,126 @@
# Konventionen
Letzte Aktualisierung: 2026-02-22
Diese Regeln sind projektweit verbindlich, um Lesbarkeit und Wartbarkeit stabil zu halten.
## 1) Allgemein
- 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) PHP-Schichten
### Repository
- Enthalten SQL und Filterlogik.
- Keine HTTP-/Session-/Render-Logik.
- Prepared Statements und bestehende Query-Utilities nutzen.
### Service
- 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(...)`.
### Action (`pages/**.php`)
- Request lesen, Zugriff prüfen, Service aufrufen, Variablen für View setzen.
- Redirects/Flash-Messages hier zentral steuern.
### Views (`*.phtml`)
- 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).
- Nur dann in `SettingCacheService::update(...)` 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).

View File

@@ -0,0 +1,70 @@
# Lokale Entwicklung
Letzte Aktualisierung: 2026-02-21
## Ziel
Pragmatischer Tages-Workflow für Entwicklung, Tests und Migrationen.
## Start/Stop
```bash
docker compose up -d
docker compose down
```
Hinweis: `docker compose up -d` startet auch den Service `scheduler` (globaler Minutely-Runner).
Bei größeren Refactors oder merkwürdigem Laufzeitverhalten:
```bash
docker compose restart php nginx
```
## Datenbank und Schema-Updates
- 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.
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
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/*`)
## Pflicht-Checks 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:
```bash
docker compose exec php vendor/bin/phpunit tests/I18n/TranslationKeysTest.php
```
## API-spezifisch
- OpenAPI aktualisieren: `docs/openapi.yaml`
- API-Quickref aktualisieren: `docs/api.md`
- Bei Security-Änderungen auch `docs/sicherheitsmodell.md` aktualisieren
## Bei Unsicherheit
- Architektur zuerst lesen: `/docs/architektur.md`
- Sicherheitsregeln prüfen: `/docs/sicherheitsmodell.md`
- Abschluss immer gegen `/docs/entwickler-checkliste.md`

1534
docs/openapi.yaml Normal file

File diff suppressed because it is too large Load Diff

128
docs/sicherheitsmodell.md Normal file
View File

@@ -0,0 +1,128 @@
# Sicherheitsmodell
Letzte Aktualisierung: 2026-02-21
## 1) Grundprinzip
Die Anwendung kombiniert mehrere Sicherheitslayer:
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)
Kein einzelner Layer ist ausreichend; die Kombination ist der Schutz.
## 2) Authentifizierung
- 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).
Kritisch:
- Tokens sind gehasht gespeichert.
- Tokens können zentral ablaufen gelassen werden (Settings-Gefahrenzone).
## 3) Public vs. Protected Routes
- Public Routes werden in `/config/routes.php` mit `public => true` definiert.
- Daraus wird zur Laufzeit `APP_PUBLIC_PATHS` aufgebaut.
- Alles andere ist loginpflichtig.
## 4) Permission-System
- Berechtigungen sind feingranular (`permissions` + `role_permissions`).
- Rollen vergeben Berechtigungen an User (`user_roles`).
- Actions prüfen Berechtigungen explizit.
Empfehlung:
- 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`).
- 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.
## 5) Tenant Scope
- 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).
## 6) Verbotene Zugriffe
- Bei fehlender Permission/Tenant-Bindung: forbidden flow (403 Seite) statt stilles Durchrutschen.
- Keine sensitiven Details in Fehlermeldungen leaken.
## 7) Datei-/Media-Endpunkte
- Avatar/Favicon/Logo Endpunkte prüfen Session + Zugriffskontext.
- Storage liegt außerhalb direkter Business-Logik; Zugriff nur über kontrollierte Endpunkte/Symlinks.
## 8) CSRF und Form-Schutz
- 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`.
## 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.

View File

@@ -1,24 +0,0 @@
# TODO: 2FA (TOTP) Integration
Goal: Add optional TOTP-based 2FA for users (admin-managed, later self-managed).
Proposed library:
- Start with a lightweight TOTP library (e.g. remotemerge/totp-php) or a more established one (e.g. spomky-labs/otphp).
Data model:
- users: totp_secret (encrypted), totp_enabled (tinyint), totp_verified_at (nullable)
- user_backup_codes: user_id, code_hash, created_at, used_at (nullable)
Admin user edit flow:
- Enable 2FA: generate secret + show QR (otpauth URI).
- Verify code to activate.
- Generate backup codes (show once).
Login flow:
- After password: prompt TOTP if totp_enabled.
- Rate-limit attempts.
Security notes:
- Encrypt secret at rest.
- Accept small time drift (+/- 1 step).
- Default to SHA-1 for compatibility.

View File

@@ -1,63 +0,0 @@
# TODO: Editor.js Image Tool Integration
## Goal
Integrate the Editor.js **Image Tool** with proper upload endpoints, storage, and access control.
## Why
- Editor.js expects uploads to respond with `{ success: 1, file: { url: "..." } }`.
- We want private storage with a controlled proxy (like avatars) and optional resizing.
## Open Decisions
- Public access? (e.g. impressum pages should be public)
- Allowed file types (recommended: PNG/JPG/WebP; avoid SVG for security)
- Resize policy (max width + WebP variants?)
## Proposed Structure
### Storage
- `storage/pages/{page_uuid}/assets/{asset_uuid}/original.<ext>`
- Optional variants: `w1280.webp`, `w640.webp`, etc.
### DB
Create `page_assets` table:
- `id`, `uuid`, `page_id`, `locale`
- `file_name`, `mime`, `size`, `width`, `height`
- `created_by`, `created`
### Endpoints
1) **Upload by file**
`pages/page/upload-image(none).phtml`
- Accepts multipart (field name `image`).
- Validates MIME/size.
- Stores file + creates DB record.
- Returns `{ success: 1, file: { url } }`.
2) **Fetch by URL** (optional)
`pages/page/fetch-image(none).phtml`
- Accepts JSON `{ url: "..." }`.
- Downloads, validates, stores like upload.
- Returns same response.
3) **Media proxy**
`pages/page/media(none).phtml?uuid=...`
- Checks permissions (public vs. loggedin).
- Reads storage file, sets headers, returns file.
### Editor.js config
Add `image` tool to `web/js/components/page-editor.js`:
```js
image: {
class: ImageTool,
config: {
endpoints: {
byFile: 'page/upload-image',
byUrl: 'page/fetch-image'
}
}
}
```
## Next Steps
- Decide public vs. private access rules.
- Implement service + migration + endpoints.
- Add tool config and i18n messages.