Spielerpositionen, Statistiken, Fahrgemeinschaften, Spielfeld-Visualisierung

- PlayerPosition Enum (7 Handball-Positionen) mit Label/ShortLabel
- Spielerstatistik pro Spiel (Tore, Würfe, TW-Paraden, Bemerkung)
- Position-Dropdown in Spieler-Editor und Event-Stats-Formular
- Statistik-Seite: TW zuerst, Trennlinie, Feldspieler, Position-Badges
- Spielfeld-SVG mit Ampel-Performance (grün/gelb/rot)
- Anklickbare Spieler im Spielfeld öffnen Detail-Modal
- Fahrgemeinschaften (Anbieten, Zuordnen, Zurückziehen)
- Übersetzungen in allen 6 Sprachen (de, en, pl, ru, ar, tr)
- .gitignore für Laravel hinzugefügt
- Demo-Daten mit Positionen und Statistiken

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Rhino
2026-03-02 11:47:34 +01:00
parent 2e24a40d68
commit ad60e7a9f9
46 changed files with 2041 additions and 86 deletions

126
CLAUDE.md
View File

@@ -3,6 +3,7 @@
## 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.
Deployment: Shared Hosting (all-inkl.com, FTP only, kein SSH).
## Architektur
@@ -10,14 +11,14 @@ PHP 8.2+, SQLite/MySQL, Blade + Alpine.js + Tailwind CSS, Quill.js WYSIWYG-Edito
- `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/Http/Middleware/` — InstallerMiddleware, SetLocaleMiddleware, SecurityHeadersMiddleware, ActiveUserMiddleware, DsgvoConsentMiddleware, StaffMiddleware, AdminOnlyMiddleware
- `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
- `resources/views/` — Blade-Templates (layouts: admin, guest, app, installer)
- `lang/{de,en,pl,ru,ar,tr}/` — 6 Sprachen: Deutsch, Englisch, Polnisch, Russisch, Arabisch, Tuerkisch
- `database/migrations/` — Nummeriert mit Prefix 0001-0035
- `database/seeders/` — AdminSeeder (benotigt ADMIN_EMAIL + ADMIN_PASSWORD in .env)
- `database/seeders/` — AdminSeeder, DemoSeeder, FaqSeeder
### Wichtige Patterns
- **Settings**: Key-Value Store in `settings` Tabelle. `Setting::get($key)` mit 1-Stunden-Cache, `Setting::set($key, $value)` mit Cache-Invalidierung
@@ -27,43 +28,92 @@ PHP 8.2+, SQLite/MySQL, Blade + Alpine.js + Tailwind CSS, Quill.js WYSIWYG-Edito
- **Rollen**: Admin, Coach, ParentRep, User (Enum `UserRole`)
- **Soft-Deletes**: User und Player (7 Tage Wiederherstellung)
- **Activity-Log**: `ActivityLog::log()` / `ActivityLog::logWithChanges()` fuer Audit-Trail
- **User-Model**: Nutzt `Notifiable` Trait (erforderlich fuer Passwort-Reset-Notifications)
- **.env-Manipulation**: `updateEnvValues()` Helper in InstallerController und SettingsController
### 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)
- **Alle Uebersetzungsschluessel muessen in ALLEN 6 Sprachen hinzugefuegt werden**
### Installer (5-Step Wizard)
1. Systemcheck (PHP-Version, Extensions, Berechtigungen)
2. Datenbank (SQLite/MySQL Konfiguration)
3. Einstellungen (App-Name, Admin-Account)
4. E-Mail (SMTP-Konfiguration mit Verbindungstest, oder Log-Modus zum Ueberspringen)
5. Abschluss (Finalize, Demo-Daten, Lizenz)
- `InstallerMiddleware` prueft `storage/installed` Datei
- Setup-Token-Schutz: Token in `storage/setup-token`, SHA256 im Laravel-Log
- SMTP-Test nutzt `Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport`
- PW-Reset-Standardtexte fuer 6 Sprachen in `getDefaultPasswordResetTexts()`
- Session-basierte Datenpersistenz zwischen Schritten
### 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}')`
3. ResetPasswordNotification laedt Custom-Template 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
### Admin Settings Tabs
- **Allgemein**: App-Name, Slogan, Favicon, Saison
- **E-Mail**: SMTP-Konfiguration mit Verbindungstest (schreibt in .env, `Artisan::call('config:clear')`)
- **Rechtliches**: Impressum + Datenschutz + PW-Reset-E-Mail pro Sprache (Quill-Editor mit Flaggen-Selector)
- **Event-Standards**: Min-Werte fuer Spieler, Catering, Zeitnehmer pro Event-Typ
- **Dateikategorien**: CRUD fuer File-Categories
- **Sichtbarkeit**: Feature-Toggles
- **Lizenz**: License-Key-Validierung via SupportApiService
- **Wartung**: Demo-Daten loeschen, Factory-Reset
### Security
### Lazy Quill-Initialisierung (Rechtliches-Tab)
- Quill-Editoren werden erst bei Klick auf die Sprach-Flagge erstellt (`initLocaleEditors(locale)`)
- Verhindert Datenverlust: Nicht-initialisierte Locales behalten ihre Server-Werte in Hidden-Inputs
- `$watch('legalLocale')` + `$nextTick()` fuer verzoegerte Erstellung
- `syncEditors()` synchronisiert nur tatsaechlich initialisierte Editoren
### Route-Struktur (Middleware)
- Installer: `throttle:10,1`, ohne SetLocaleMiddleware/ActiveUserMiddleware
- Oeffentlich: Login, Register, Legal-Pages, Locale-Switch
- User-Bereich: `auth` Middleware
- Admin-Bereich: `auth` + `admin` + `prefix('admin')`
- Staff-Routes (Admin + Coach): `staff` Middleware
- Admin-Only-Routes: `admin-only` Middleware (Settings, Locations, Support)
## Security
### Middleware-Stack
- CSP und Permissions-Policy via SecurityHeadersMiddleware (inkl. COOP-Header)
- `StaffMiddleware` — prueft Admin oder Coach Rolle
- `AdminOnlyMiddleware` — prueft explizit Admin-Rolle (Route-Level-Schutz)
- Rate-Limiting auf Auth-Routes, User-Actions, Geocoding, Installer
### Schutzmassnahmen
- 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
- **File-Autorisierung**: `authorizeFileAccess()` in FileController prueft Team-Zugehoerigkeit
- **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
- **SSRF-Schutz**: GeocodingService mit Host-Whitelist, HTTPS-only, DNS-Rebinding-Check, Timeout 5s
- **Installer-Sicherheit**: Admin-Passwort sofort gehasht, 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
- **HTML-Sanitisierung**: Alle User-Content-Ausgaben durch HtmlSanitizerService geschuetzt
- **Invitation-Tokens**: SHA-256 gehasht in DB gespeichert
- **E-Mail-Normalisierung**: Lowercase-Vergleich bei Login und Password-Reset
- **Team-Scoping**: Coach/ParentRep sehen nur ihre zugewiesenen Teams
### Security-Audit-Historie (Maerz 2026)
6 Audits durchgefuehrt, Score: 6.5 → 9.5/10
- 95 Findings insgesamt, 88 behoben, 3 akzeptierte Risiken, 4 verbleibende Niedrig/Info
- Akzeptierte Risiken: MIME-Spoofing (F05), Enum-Reuse (F20), CSP unsafe-inline (W04, CDN-Abhaengigkeit)
## Conventions
- Controller-Methoden: resourceful (index, create, store, show, edit, update, destroy)
- Blade-Components: `<x-layouts.admin>`, `<x-layouts.guest>`, `<x-layouts.app>`
- Blade-Components: `<x-layouts.admin>`, `<x-layouts.guest>`, `<x-layouts.app>`, `<x-layouts.installer>`
- Alpine.js fuer interaktive UI-Elemente
- Quill.js v1.3.7 fuer WYSIWYG-Editoren (via CDN)
- Quill.js v1.3.7 fuer WYSIWYG-Editoren (via CDN mit SRI-Hashes)
- Keine Tests vorhanden — bei Aenderungen manuell testen
- Commit-Sprache: Deutsch oder Englisch
- Alle Uebersetzungsschluessel muessen in ALLEN 6 Sprachen hinzugefuegt werden
@@ -84,38 +134,14 @@ Immer in allen 6 Dateien: `lang/de/`, `lang/en/`, `lang/pl/`, `lang/ru/`, `lang/
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
5. In `update()` mit Whitelist 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
### Neue Admin-Route hinzufuegen
1. Route in `routes/web.php` im passenden Middleware-Block:
- Alle Admin-Panel-Nutzer: direkt unter `admin` Prefix
- Staff (Admin + Coach): unter `staff` Middleware
- Nur Admin: unter `admin-only` Middleware
2. Controller-Methode mit Autorisierungs-Pruefung
3. View erstellen
4. Translation-Keys fuer alle 6 Sprachen