Преглед на файлове

adding toggle to rate limit
Moving config to not-versioned file

Josef Straßl преди 1 месец
родител
ревизия
25c9d9d188
променени са 9 файла, в които са добавени 67 реда и са изтрити 14 реда
  1. 2 0
      .gitignore
  2. 11 3
      README.md
  3. 6 5
      config/app.sample.php
  4. 14 0
      config/mail.sample.php
  5. 2 0
      docs/AI_OVERVIEW.md
  6. 10 0
      docs/OPERATIONS.md
  7. 3 0
      docs/RATE_LIMITING.md
  8. 12 5
      src/App/Bootstrap.php
  9. 7 1
      src/Security/RateLimiter.php

+ 2 - 0
.gitignore

@@ -0,0 +1,2 @@
+config/app.local.php
+config/mail.local.php

+ 11 - 3
README.md

@@ -40,20 +40,28 @@ Schlankes PHP-Flatfile-Projekt für einen digitalen Mitgliedsantrag (deutsches F
 2. Apache verwenden (mit aktiviertem `mod_rewrite`) und `AllowOverride All` für das Projekt sicherstellen.
 3. Document Root auf das Projekt-Root setzen.
 4. Schreibrechte für `storage/` sicherstellen (mind. Webserver-User).
-5. Konfiguration anpassen:
+5. Lokale Konfiguration aus Samples erzeugen:
+   - `cp config/app.sample.php config/app.php`
+   - `cp config/mail.sample.php config/mail.php`
+6. Konfiguration anpassen:
    - `config/app.php` (Admin-Passwort, Kontakt, Disclaimer, Retention, Rate Limit)
    - `config/mail.php` (Absender, Empfänger)
-6. Admin-Hash setzen:
+7. Admin-Hash setzen:
    - Auf Server: `php -r "echo password_hash('DEIN-PASSWORT', PASSWORD_DEFAULT), PHP_EOL;"`
    - Ergebnis in `config/app.php -> admin.password_hash`
    - Danach `password_plain_fallback` entfernen/leer setzen.
-7. Cronjob einrichten (täglich):
+8. Cronjob einrichten (täglich):
    - `php /pfad/zum/projekt/bin/cleanup.php`
 
+Hinweis:
+- `config/*.sample.php` sind versionskontrollierte Vorlagen.
+- `config/app.php` und `config/mail.php` sind lokale Produktivdateien und werden per `.gitignore` nicht versioniert.
+
 ## Sicherheitshinweise
 
 - CSRF aktiv auf POST-Endpunkten.
 - Honeypot + Rate Limit aktiv.
+- Rate Limit fuer Tests deaktivierbar ueber `config/app.php -> rate_limit.enabled = false`.
 - Uploads werden auf Typ, MIME und Größe geprüft.
 - Interne Ordner (`config`, `src`, `storage`, `bin`, `docs`) werden per `.htaccess` blockiert.
 

+ 6 - 5
config/app.php → config/app.sample.php

@@ -5,12 +5,12 @@ declare(strict_types=1);
 $root = dirname(__DIR__);
 
 return [
-    'project_name' => 'Feuerwehr Freising Mitgliedsantrag',
+    'project_name' => 'Feuerwehr Mitgliedsantrag',
     'base_url' => '/',
-    'contact_email' => 'josef.strassl@feuerwehr-freising.de',
+    'contact_email' => 'kontakt@example.org',
     'disclaimer' => [
         'title' => 'Wichtiger Hinweis',
-        'text' => "Bitte lesen Sie diesen Hinweis vor Beginn sorgfältig.\n\nMit dem Fortfahren bestätigen Sie, dass Ihre Angaben vollständig und wahrheitsgemäß sind.\nIhre Daten werden ausschließlich zur Bearbeitung des Mitgliedsantrags verwendet.",
+        'text' => "Bitte lesen Sie diesen Hinweis vor Beginn sorgfaeltig.\n\nMit dem Fortfahren bestaetigen Sie, dass Ihre Angaben vollstaendig und wahrheitsgemaess sind.\nIhre Daten werden ausschliesslich zur Bearbeitung des Mitgliedsantrags verwendet.",
         'accept_label' => 'Hinweis gelesen, weiter zum Antrag',
     ],
     'retention' => [
@@ -28,14 +28,15 @@ return [
         ],
     ],
     'rate_limit' => [
+        'enabled' => true,
         'requests' => 30,
         'window_seconds' => 300,
     ],
     'admin' => [
         // Hash mit password_hash('DEIN-PASSWORT', PASSWORD_DEFAULT) erzeugen.
         'password_hash' => '',
-        // Fallback nur für initiales Setup, danach löschen.
-        'password_plain_fallback' => 'testing',
+        // Fallback nur fuer initiales Setup, danach loeschen.
+        'password_plain_fallback' => '',
         'session_timeout_seconds' => 3600,
     ],
     'storage' => [

+ 14 - 0
config/mail.sample.php

@@ -0,0 +1,14 @@
+<?php
+
+declare(strict_types=1);
+
+return [
+    'from' => 'antrag@example.org',
+    'recipients' => [
+        'verein@example.org',
+    ],
+    'subjects' => [
+        'admin' => 'Neuer Mitgliedsantrag',
+        'applicant' => 'Bestaetigung deines Mitgliedsantrags',
+    ],
+];

+ 2 - 0
docs/AI_OVERVIEW.md

@@ -58,6 +58,8 @@ Digitaler Mitgliedsantrag für Feuerwehrverein mit Flatfile-Speicherung und Admi
 - Retention-Tage: `config/app.php` + Cron `bin/cleanup.php`
 - Rate-Limit-Parameter: `config/app.php -> rate_limit` (Details: `docs/RATE_LIMITING.md`)
 - Disclaimer-Startseite: `config/app.php -> disclaimer` + `index.php`
+- Versionskontrollierte Config-Vorlagen: `config/app.sample.php`, `config/mail.sample.php`
+- Lokale Runtime-Configs (nicht versioniert): `config/app.php`, `config/mail.php`
 
 ## Harte Regeln
 

+ 10 - 0
docs/OPERATIONS.md

@@ -20,6 +20,15 @@ php /pfad/zum/projekt/bin/cleanup.php
 - Drafts: `config/app.php -> retention.draft_days` (Default 14)
 - Submissions: `config/app.php -> retention.submission_days` (Default 90)
 
+## Konfiguration (Sample-Setup)
+
+- Versionierte Vorlagen: `config/app.sample.php`, `config/mail.sample.php`
+- Lokale Runtime-Dateien: `config/app.php`, `config/mail.php`
+- Erstsetup:
+  - `cp config/app.sample.php config/app.php`
+  - `cp config/mail.sample.php config/mail.php`
+- `config/app.php` und `config/mail.php` sind lokal und sollen nicht in Git versioniert werden.
+
 ## Logs
 
 - `storage/logs/cleanup.log`
@@ -34,6 +43,7 @@ php /pfad/zum/projekt/bin/cleanup.php
 - Persistenz: `storage/rate_limit/`
 - Detaillierte Doku: `docs/RATE_LIMITING.md`
 - Bei erhöhten `429`-Antworten zuerst `requests/window_seconds` prüfen und gegen reale Nutzerlast kalibrieren.
+- Für Tests kann das Limiting global deaktiviert werden: `rate_limit.enabled = false`.
 
 ## Backup
 

+ 3 - 0
docs/RATE_LIMITING.md

@@ -16,6 +16,8 @@ Schützt die API gegen Spam, Bot-Traffic und Missbrauch durch zu viele Anfragen
 
 In `config/app.php`:
 
+- `rate_limit.enabled`
+Globaler Schalter (`true`/`false`). Bei `false` lässt der Limiter alle Requests durch.
 - `rate_limit.requests`
 Maximal erlaubte Requests pro Zeitfenster
 - `rate_limit.window_seconds`
@@ -23,6 +25,7 @@ Länge des Zeitfensters in Sekunden
 
 Default:
 
+- `enabled = true`
 - `requests = 30`
 - `window_seconds = 300` (5 Minuten)
 

+ 12 - 5
src/App/Bootstrap.php

@@ -90,20 +90,27 @@ final class Bootstrap
     /** @return array<string, mixed> */
     private static function loadConfigArray(string $path, string $name): array
     {
-        if (!is_file($path)) {
-            self::log('php_fatal', sprintf('Missing config file: %s (%s)', $name, $path));
-            return [];
+        $pathToLoad = $path;
+        if (!is_file($pathToLoad)) {
+            $samplePath = preg_replace('/\.php$/', '.sample.php', $pathToLoad) ?: ($pathToLoad . '.sample');
+            if (is_file($samplePath)) {
+                $pathToLoad = $samplePath;
+                self::log('app', sprintf('Using sample config for %s: %s', $name, $samplePath));
+            } else {
+                self::log('php_fatal', sprintf('Missing config file: %s (%s)', $name, $path));
+                return [];
+            }
         }
 
         try {
-            $loaded = require $path;
+            $loaded = require $pathToLoad;
         } catch (\Throwable $e) {
             self::log('php_fatal', sprintf('Failed loading config %s: %s', $name, $e->getMessage()));
             return [];
         }
 
         if (!is_array($loaded)) {
-            self::log('php_fatal', sprintf('Config %s must return an array (%s)', $name, $path));
+            self::log('php_fatal', sprintf('Config %s must return an array (%s)', $name, $pathToLoad));
             return [];
         }
 

+ 7 - 1
src/Security/RateLimiter.php

@@ -9,19 +9,25 @@ use App\App\Bootstrap;
 final class RateLimiter
 {
     private string $storageDir;
+    private bool $enabled;
 
     public function __construct()
     {
         $app = Bootstrap::config('app');
+        $this->enabled = (bool) ($app['rate_limit']['enabled'] ?? true);
         $this->storageDir = (string) ($app['storage']['rate_limit'] ?? Bootstrap::rootPath() . '/storage/rate_limit');
 
-        if (!is_dir($this->storageDir)) {
+        if ($this->enabled && !is_dir($this->storageDir)) {
             mkdir($this->storageDir, 0775, true);
         }
     }
 
     public function allow(string $key, int $limit, int $windowSeconds): bool
     {
+        if (!$this->enabled) {
+            return true;
+        }
+
         $hash = hash('sha256', $key);
         $path = $this->storageDir . '/' . $hash . '.json';
         $now = time();