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:
@@ -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)
|
||||
@@ -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
119
docs/anfragelimits.md
Normal 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
42
docs/api.md
Normal 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
190
docs/architektur.md
Normal 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`
|
||||
110
docs/benutzer-lifecycle-policy.md
Normal file
110
docs/benutzer-lifecycle-policy.md
Normal 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`
|
||||
82
docs/benutzerdefinierte-felder.md
Normal file
82
docs/benutzerdefinierte-felder.md
Normal 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`.
|
||||
@@ -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
18
docs/docker-betrieb.md
Normal 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
65
docs/docker-lokal.md
Normal 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
77
docs/docker-produktiv.md
Normal 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://...`.
|
||||
62
docs/dokumentation-erweitern.md
Normal file
62
docs/dokumentation-erweitern.md
Normal 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.
|
||||
60
docs/einstellungen-speicherung.md
Normal file
60
docs/einstellungen-speicherung.md
Normal 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).
|
||||
88
docs/entwickler-checkliste.md
Normal file
88
docs/entwickler-checkliste.md
Normal 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
48
docs/erste-aenderung.md
Normal 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
80
docs/erste-schritte.md
Normal 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
83
docs/fehlerbehebung.md
Normal 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
78
docs/frontend-css.md
Normal 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
101
docs/frontend-javascript.md
Normal 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
224
docs/geplante-aufgaben.md
Normal 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
149
docs/globale-suche.md
Normal 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
80
docs/importe.md
Normal 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
71
docs/index.md
Normal 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
103
docs/konfiguration.md
Normal 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
126
docs/konventionen.md
Normal 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).
|
||||
70
docs/lokale-entwicklung.md
Normal file
70
docs/lokale-entwicklung.md
Normal 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
1534
docs/openapi.yaml
Normal file
File diff suppressed because it is too large
Load Diff
128
docs/sicherheitsmodell.md
Normal file
128
docs/sicherheitsmodell.md
Normal 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.
|
||||
@@ -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.
|
||||
@@ -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. logged‑in).
|
||||
- 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.
|
||||
Reference in New Issue
Block a user