Unifi-Voucher-Tool/updater
Claude 6e19958a37
Security-, Bugfix- und UX-Überarbeitung auf Basis des Code-Reviews
Sicherheit:
- Bulk-Erstellung serverseitig auf eingeloggte Nutzer beschränkt;
  expire_minutes wird validiert (anonym: nur Default/Template-Werte,
  eingeloggt: max. 1 Jahr)
- IP-basiertes Rate-Limit über neue Tabelle request_throttle
  (Voucher-Erstellung + Passwort-Reset-Anfragen), Session-Fallback
  für Alt-Installationen; Migration 0002
- session_regenerate_id() nach Login, Secure-Cookie-Flag bei HTTPS
- Admin-/Aktiv-Status wird pro Request live aus der DB geprüft
  (Rechteentzug & Deaktivierung wirken sofort); Schutz vor
  Selbst-Degradierung im Benutzer-Edit
- Alle state-ändernden Admin-Aktionen von GET auf POST umgestellt
  (kein CSRF-Token mehr in URLs)
- login_simple.php (Legacy, Debug-Leak) entfernt; cron_test.php nur
  noch für Admins; .htaccess auf Apache-2.4-Syntax inkl. cron_test.php
- M365 Client Secret wird nicht mehr ins Formular zurückgegeben
- Updater: Zip-Slip-/Pfad-Traversal-Schutz, Backup vor dem Anwenden
  mit automatischem Rollback bei Fehlern, AuditLogger-Bug behoben
- cron_sync: Token-Vergleich mit hash_equals; login_attempts-Pruning
- CSV-Export gegen Excel-Formula-Injection abgesichert

Bugfixes:
- M365-Login: Fallback auf userPrincipalName, wenn Graph kein 'mail'
  liefert (Nutzer ohne Exchange-Postfach konnten sich nie anmelden)
- PRG-Pattern überall: F5 erzeugt keine Duplikat-Voucher und
  wiederholt keine Admin-Aktionen (Session-Flash-Messages)
- QR-Code nicht mehr invertiert (schwarz auf weiß, scanbar)
- Bulk-Erstellung nutzt den UniFi 'n'-Parameter: 1 API-Call statt
  n× Login + Voucherlisten-Abruf; exaktes Code-Matching per
  create_time statt "global neuester Voucher"
- Mailer: doppelte Zeilenumbrüche behoben, AUTH nur mit Credentials,
  SMTP-Dot-Stuffing, CLI-sicherer EHLO-Host
- forgot_password: System-URL-Auto-Detect (Reset-Link war sonst
  relativ/kaputt) + Rate-Limit
- Audit-Log-Labels an tatsächliche Action-Keys angepasst;
  Voucher-Erstellung (einzeln & bulk) wird jetzt auditiert
- Site-Edit testet die Verbindung auch ohne Passwortänderung

UX/UI:
- Alert-/Badge-Styles zentral in global.css mit Dark-Mode-Variablen
  (vorher 7× dupliziert mit hart codierten Hellfarben)
- Sticky-Formulare + Tab-Erhalt nach Validierungsfehlern (Bulk),
  Settings kehren nach dem Speichern zum aktiven Tab zurück
- Gültigkeit menschenlesbar (z.B. "8 Stunden" statt "480 Minuten")
- Voucher-Name-Default "Gast/Guest" im öffentlichen Modus
- Favicon auch auf Login-/öffentlichen Seiten
- Verbindungstest-Button pro Site-Karte (Health-Check)
- i18n-Pass: Confirm-Dialoge, Toasts, Fehl-/Erfolgsmeldungen in de/en
- Sprachumschalter ohne fetch+reload (kein Re-Submit-Dialog)
- A11y: Esc schließt Modals, aria-live für Toasts, aria-labels auf
  Icon-Buttons; APP_KEY-Warnbanner im Dashboard
- Dashboard-Sync: set_time_limit passend zur Site-Anzahl;
  Voucher-Sync mit Map statt SELECT pro Voucher

Tooling:
- GitHub-Actions-Workflow: PHP-Lint aller Dateien + de/en-Key-Parität

https://claude.ai/code/session_01KKVpVPJjrTKGoRgpJcySD4
2026-06-09 19:43:13 +00:00
..
migrations Security-, Bugfix- und UX-Überarbeitung auf Basis des Code-Reviews 2026-06-09 19:43:13 +00:00
storage Updater-System (OpenNIT-Modell) im isolierten updater/-Ordner 2026-06-05 18:51:51 +00:00
templates Updater-Migration für Feature-Tabellen + README um neue Features erweitern 2026-06-05 19:16:55 +00:00
AuditLogger.php Updater-System (OpenNIT-Modell) im isolierten updater/-Ordner 2026-06-05 18:51:51 +00:00
bootstrap.php Updater-System (OpenNIT-Modell) im isolierten updater/-Ordner 2026-06-05 18:51:51 +00:00
MigrationRunner.php Updater-System (OpenNIT-Modell) im isolierten updater/-Ordner 2026-06-05 18:51:51 +00:00
README.md Security-, Bugfix- und UX-Überarbeitung auf Basis des Code-Reviews 2026-06-09 19:43:13 +00:00
routes.php Updater-System (OpenNIT-Modell) im isolierten updater/-Ordner 2026-06-05 18:51:51 +00:00
UpdateController.php Security-, Bugfix- und UX-Überarbeitung auf Basis des Code-Reviews 2026-06-09 19:43:13 +00:00
UpdateManager.php Security-, Bugfix- und UX-Überarbeitung auf Basis des Code-Reviews 2026-06-09 19:43:13 +00:00
UpdaterFactory.php Updater-System (OpenNIT-Modell) im isolierten updater/-Ordner 2026-06-05 18:51:51 +00:00

Updater (OpenVoucherTool)

Auto-Update-System nach dem OpenNIT-Modell: zieht Quellcode + DB-Migrationen aus einem zentralen Repository über einen HTTP-Update-Proxy nach.

[diese App]  ←HTTPS→  [Update-Proxy]  ←pull→  [Git-Repo]
  • Versions-Identität: 40-stelliger Git-Commit-SHA in updater/storage/.version
  • Channels: stablehttps://update.loheide.eu/openvouchertool, developmenthttps://update.loheide.eu/openvouchertool-development
  • User-Agent: OpenVoucherTool-Updater/1.0
  • DB-Treiber: MySQL/MariaDB (Migrations-Tracking-Tabelle _updater_migrations)

Aufruf

Admin-Oberfläche: /admin/update.php (nur für angemeldete Admins).

Endpoint Methode Zweck
admin/update.php GET Admin-UI
admin/update.php?action=check GET Update-Prüfung (JSON)
admin/update.php?action=progress GET Fortschritt (JSON)
admin/update.php?action=migrations GET Migrations-Status (JSON)
admin/update.php action=install POST Update installieren (+csrf_token)
admin/update.php action=set_channel POST Channel wählen (+csrf_token)

Optional: In der Admin-Navigation (admin/index.php) einen Link zu update.php ergänzen, damit die Seite auffindbar ist. Das ist bewusst nicht automatisch geschehen (Isolations-Prinzip siehe unten).

Isolation

Der Updater liegt vollständig in updater/ (eigener Namespace Updater\) plus zwei dünne, klar markierte Anknüpfungen:

  1. Front-Controller-Hook in index.php (Maintenance-Check, markiert mit // Updater maintenance hook).
  2. Entry-Shim admin/update.php (lädt nur Basis + Bootstrap, delegiert an den Controller).

Eigene Settings (updater/storage/updater-settings.json), eigener Autoloader (updater/bootstrap.php), eigener AuditLogger (schreibt in die vorhandene audit_log-Tabelle), eigenes Migrations-System (updater/migrations/ + Tabelle _updater_migrations). Es werden keine Projekt-Klassen erweitert \Database und \Auth werden nur per Injection genutzt.

Geschützte Pfade (werden bei Updates nie überschrieben)

config.php, .htaccess, .env*, .git/, .gitignore, public/uploads/, vendor/, composer.lock, updater/storage/.

Rückbau (restlos entfernen)

  1. In index.php den Block „Updater maintenance hook" (die Zeilen 28, beginnend mit $maintenanceFile = … bis zum schließenden }) entfernen.
  2. rm -r updater/
  3. rm admin/update.php
  4. (optional) In der Datenbank: DROP TABLE _updater_migrations;
  5. (optional, falls vorhanden) Laufzeitdateien sind bereits in updater/ und damit mit Schritt 2 weg. Nichts liegt außerhalb.

Danach ist keine Spur des Updaters mehr im Bestandscode der Beweis für die Isolation.

Smoke-Test

# 1) Syntax aller Updater-Dateien
php -l updater/UpdateManager.php
php -l updater/MigrationRunner.php
php -l updater/UpdateController.php
php -l updater/UpdaterFactory.php
php -l updater/AuditLogger.php

# 2) Admin-Seite aufrufen (eingeloggt als Admin):
#    https://<host>/admin/update.php
#    -> "Auf Updates prüfen" klicken. Erwartung: Proxy-Antwort oder klare
#       Fehlermeldung (wenn Proxy/Channel nicht erreichbar).

# 3) Maintenance-Mode manuell testen:
touch updater/storage/.maintenance     # index.php zeigt jetzt 503-Wartungsseite
rm    updater/storage/.maintenance     # wieder normal

Hinweis: Ein vollständiger Installations-Durchlauf (action=install) setzt einen erreichbaren Update-Proxy unter den oben genannten URLs voraus.

Sicherheits-Hinweise & Limitierungen

  • Keine Paket-Signatur: Die Integrität der Updates hängt derzeit allein an TLS zur Update-Proxy-URL. Der Updater validiert Zip-Einträge und Dateipfade gegen Pfad-Traversal und legt vor dem Anwenden ein Backup an (updater/storage/.backup-last), das bei Fehlern automatisch zurückgespielt wird. Eine kryptografische Signaturprüfung der Pakete (z.B. signierte SHA-256-Manifeste) erfordert serverseitige Unterstützung des Update-Proxys und steht noch aus.
  • Rollback: Schlägt das Anwenden des Updates oder eine Migration fehl, werden die überschriebenen Dateien aus dem Backup wiederhergestellt. Datenbank-Migrationen werden dabei nicht automatisch rückgängig gemacht (jede Migration läuft aber in einer eigenen Transaktion).