# WebApp_Install — Handball Team Manager ## Projekt-Typ Laravel 12 WebApp zur Verwaltung von Handball-Teams, Spielern, Eltern, Terminen und Dateien. PHP 8.2+, SQLite/MySQL, Blade + Alpine.js + Tailwind CSS, Quill.js WYSIWYG-Editor. ## Architektur ### Verzeichnisstruktur - `app/Models/` — Eloquent Models (User, Player, Team, Event, Setting, etc.) - `app/Http/Controllers/Admin/` — Admin-Bereich (UserController, SettingsController, etc.) - `app/Http/Controllers/Auth/` — Login, Register, ForgotPassword, ResetPassword - `app/Http/Middleware/` — InstallerMiddleware (Setup-Token), SetLocaleMiddleware, SecurityHeadersMiddleware, ActiveUserMiddleware, DsgvoConsentMiddleware - `app/Services/` — HtmlSanitizerService (HTMLPurifier), GeocodingService, SupportApiService - `app/Enums/` — UserRole, EventType, EventStatus, ParticipantStatus, CateringStatus - `app/Notifications/` — ResetPasswordNotification (Custom, mit Setting-Template) - `resources/views/` — Blade-Templates (layouts: admin, guest, app) - `lang/{de,en,pl,ru,ar,tr}/` — 6 Sprachen: Deutsch, Englisch, Polnisch, Russisch, Arabisch, Turkisch - `database/migrations/` — Nummeriert mit Prefix 0001-0035 - `database/seeders/` — AdminSeeder (benotigt ADMIN_EMAIL + ADMIN_PASSWORD in .env) ### Wichtige Patterns - **Settings**: Key-Value Store in `settings` Tabelle. `Setting::get($key)` mit 1-Stunden-Cache, `Setting::set($key, $value)` mit Cache-Invalidierung - **Locale-Settings**: Rechtliche Texte und E-Mail-Templates sind locale-suffixed: `impressum_html_de`, `datenschutz_html_en`, `password_reset_email_pl` etc. - **Passwort-Hashing**: User-Model nutzt Laravel Cast `'password' => 'hashed'` — KEIN manuelles Hash::make beim Setzen via `$user->password = $pw` noetig - **HTML-Output**: Alle `{!! !!}`-Ausgaben von User-Content muessen durch `HtmlSanitizerService::sanitize()` gehen - **Rollen**: Admin, Coach, ParentRep, User (Enum `UserRole`) - **Soft-Deletes**: User und Player (7 Tage Wiederherstellung) - **Activity-Log**: `ActivityLog::log()` / `ActivityLog::logWithChanges()` fuer Audit-Trail ### Multi-Language - 6 Sprachen: de, en, pl, ru, ar, tr - Translation-Dateien: `lang/{locale}/admin.php`, `auth_ui.php`, `passwords.php`, `ui.php`, `events.php`, `profile.php`, `validation.php` - Locale wird per SetLocaleMiddleware aus `$user->locale` oder Session gesetzt - RTL-Support fuer Arabisch (ar) ### Password Reset Flow 1. User klickt "Passwort vergessen?" auf Login-Seite 2. ForgotPasswordController sendet Reset-Link via Laravel Password Broker 3. ResetPasswordNotification laedt optionalen Custom-Template-Text aus `Setting::get('password_reset_email_{locale}')` 4. Platzhalter: `{name}`, `{link}`, `{app_name}` 5. Fallback auf Standard-Laravel-Template mit Translation-Keys (`passwords.reset_*`) ### Installer - InstallerMiddleware prueft `storage/installed` Datei - Setup-Token-Schutz: Token in `storage/setup-token`, wird beim ersten Zugriff generiert und in Laravel-Log geschrieben - Nach Installation wird `storage/installed` erstellt ### Security - CSP und Permissions-Policy via SecurityHeadersMiddleware (inkl. COOP-Header) - Honeypot-Feld auf Login/Register-Formularen - Rate-Limiting auf Auth-Routes (`throttle:login`) - DSGVO-Consent-System mit Datei-Upload und Admin-Bestaetigung - Factory Reset benoetigt Passwort + Bestaetigung "RESET-BESTAETIGT" - **File-Autorisierung**: `authorizeFileAccess()` in FileController prueft Team-Zugehoerigkeit und aktive Kategorien - **Settings-Whitelist**: SettingsController akzeptiert nur bekannte Keys + validierte Locale-Suffixe - **SSRF-Schutz**: GeocodingService mit Host-Whitelist (nominatim.openstreetmap.org, photon.komoot.io), HTTPS-only, Timeout 5s - **Installer-Sicherheit**: Admin-Passwort wird sofort gehasht (nicht Klartext in Session), Setup-Token nur als SHA256 geloggt - **Path-Traversal-Schutz**: DSGVO-Datei-Downloads pruefen `str_starts_with('dsgvo/')` - **HTML-Sanitisierung**: Alle `{!! !!}`-Ausgaben (Slogan, Settings-Editor, PWA-Banner) durch `HtmlSanitizerService::sanitize()` geschuetzt ## Conventions - Controller-Methoden: resourceful (index, create, store, show, edit, update, destroy) - Blade-Components: ``, ``, `` - Alpine.js fuer interaktive UI-Elemente - Quill.js v1.3.7 fuer WYSIWYG-Editoren (via CDN) - Keine Tests vorhanden — bei Aenderungen manuell testen - Commit-Sprache: Deutsch oder Englisch - Alle Uebersetzungsschluessel muessen in ALLEN 6 Sprachen hinzugefuegt werden ## Haeufige Aufgaben ### Neuen Translation-Key hinzufuegen Immer in allen 6 Dateien: `lang/de/`, `lang/en/`, `lang/pl/`, `lang/ru/`, `lang/ar/`, `lang/tr/` ### Neues Setting hinzufuegen 1. Migration erstellen: `Setting::firstOrCreate(['key' => '...'], [...])` 2. In SettingsController `edit()` laden 3. In View rendern 4. In `update()` speichern (mit Sanitization fuer HTML) ### Neues locale-spezifisches Setting 1. Keys: `{setting_name}_{locale}` (z.B. `impressum_html_de`) 2. Migration fuer alle 6 Locales 3. In SettingsController `$localeSettings` Array aufnehmen 4. In View mit Sprach-Flaggen-Selector anzeigen 5. In `update()` mit `str_starts_with()` und `updateOrCreate()` behandeln 6. **Wichtig**: Neuen Key zur Whitelist in `SettingsController::update()` hinzufuegen ($allowedLocaleKeys) ## Security Audit & Haertung (Maerz 2026) ### Audit-Report - **Datei**: `security-audit-2026-03.html` — Vollstaendiger HTML-Report mit allen Findings und Fix-Dokumentation - **Score**: 6.5 → 8.5 / 10 nach Haertung - **Ergebnis**: 20 Findings identifiziert, 19 behoben, 1 als akzeptiertes Risiko ### Behobene Schwachstellen (nach Datei) | Datei | Findings | Aenderungen | |-------|----------|-------------| | `FileController.php` | F01 (Kritisch) | `authorizeFileAccess()` — IDOR-Schutz fuer Downloads/Previews | | `admin/settings/edit.blade.php` | F02 (Kritisch) | Alle `{!! !!}` durch `HtmlSanitizerService::sanitize()` | | `layouts/app.blade.php`, `guest.blade.php` | F03 (Kritisch) | Slogan-Ausgabe sanitisiert | | `Admin/SettingsController.php` | F04 (Hoch) | Settings-Key-Whitelist in `update()` | | `Services/GeocodingService.php` | F06 (Hoch) | ALLOWED_HOSTS, HTTPS-only, SHA256-Cache, Timeout | | `EventController.php` | F07 (Hoch) | `accessibleTeamIds()`, EventType-Validierung, int-Cast | | `InstallerController.php` | F08 (Mittel) | Passwort sofort gehasht (`installer.admin_password_hash`) | | `InstallerMiddleware.php` | F09 (Mittel) | Token als SHA256 geloggt, chmod 0600 | | `SecurityHeadersMiddleware.php` | F10, F19 | COOP-Header hinzugefuegt | | `.env.example` | F11 (Mittel) | SESSION_SECURE_COOKIE + SESSION_SAME_SITE Defaults | | `Admin/TeamController.php` | F12 (Mittel) | Ziel-Team aktiv-Pruefung in `updatePlayerTeam()` | | `Admin/ActivityLogController.php` | F13, F16 | whereRaw→Eloquent, `canViewActivityLog()` statt `id !== 1` | | `CommentController.php` | F14 (Mittel) | `e()` entfernt (Doppel-Escaping behoben) | | `pwa-install-banner.blade.php` | F15 (Mittel) | Translation-Ausgabe sanitisiert | | `ProfileController.php` | F17 (Niedrig) | DSGVO Path-Prefix-Check | | `Auth/ResetPasswordController.php` | F18 (Niedrig) | `Password::min(8)->letters()->numbers()` | ### Akzeptierte Restrisiken - **F05** (MIME-Spoofing): UUID-Dateinamen + privater Storage + Autorisierung mitigieren das Risiko - **F20** (EventTimekeeper Enum): CateringStatus und TimekeeperStatus haben identische Werte - **CSP unsafe-inline/eval**: CDN-Abhaengigkeit (Tailwind/Alpine.js/Quill.js) erfordert dies — langfristig lokale Assets empfohlen