diff --git a/.gitattributes b/.gitattributes
deleted file mode 100644
index 7e20944..0000000
--- a/.gitattributes
+++ /dev/null
@@ -1,15 +0,0 @@
-# Zeilenenden nicht anfassen – einige Dateien liegen bewusst mit CRLF vor.
-* -text
-
-# Dateien, die nicht ins Release-ZIP gehoeren.
-# `git archive` (siehe .github/workflows/release.yml) wertet export-ignore aus.
-/.gitattributes export-ignore
-/.gitignore export-ignore
-/.dockerignore export-ignore
-/.github export-ignore
-/docs export-ignore
-/tests export-ignore
-/tools export-ignore
-/phpunit.xml.dist export-ignore
-/phpstan.neon export-ignore
-/composer.lock export-ignore
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index e88d61a..5aaaad9 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -29,40 +29,9 @@ jobs:
php -l "$f"
done
- - name: Validate language files
+ - name: Validate JSON language/migration assets
run: |
- php -r '
- $de = require "lang/de.php"; $en = require "lang/en.php";
- if (!is_array($de) || !is_array($en)) { fwrite(STDERR, "Bad lang file\n"); exit(1); }
- $missingEn = array_diff(array_keys($de), array_keys($en));
- $missingDe = array_diff(array_keys($en), array_keys($de));
- if ($missingEn || $missingDe) {
- fwrite(STDERR, "Fehlend in en: " . implode(", ", $missingEn) . "\n");
- fwrite(STDERR, "Fehlend in de: " . implode(", ", $missingDe) . "\n");
- exit(1);
- }
- echo "lang OK (" . count($de) . " Schluessel)\n";'
-
- - name: Check that every used translation key exists
- run: |
- php -r '
- $de = require "lang/de.php";
- $missing = [];
- $it = new RecursiveIteratorIterator(new RecursiveDirectoryIterator(".", FilesystemIterator::SKIP_DOTS));
- foreach ($it as $file) {
- $path = $file->getPathname();
- if (substr($path, -4) !== ".php") continue;
- if (strpos($path, "/vendor/") !== false || strpos($path, "/tools/") !== false) continue;
- preg_match_all("/__\(\s*\x27([a-z0-9_]+)\x27/", file_get_contents($path), $m);
- foreach ($m[1] as $key) {
- if (!isset($de[$key]) && substr($key, -1) !== "_") { $missing[$key] = $path; }
- }
- }
- if ($missing) {
- foreach ($missing as $key => $path) { fwrite(STDERR, "Unbekannter Schluessel $key in $path\n"); }
- exit(1);
- }
- echo "Alle verwendeten Schluessel vorhanden\n";'
+ php -r 'foreach (glob("lang/*.php") as $f) { $a = require $f; if (!is_array($a)) { fwrite(STDERR, "Bad lang file: $f\n"); exit(1);} } echo "lang OK\n";'
test:
name: Unit Tests & Static Analysis
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
deleted file mode 100644
index 3fc3001..0000000
--- a/.github/workflows/release.yml
+++ /dev/null
@@ -1,152 +0,0 @@
-name: Release-Paket
-
-# Bei jedem Merge nach main entsteht ein installierbares ZIP und landet als
-# Vorab-Release "latest-main" im Repository. Wird ein Tag v* gepusht, wird
-# daraus ein regulaeres Release mit derselben Mechanik.
-on:
- push:
- branches: [ main ]
- tags: [ 'v*' ]
- workflow_dispatch:
-
-jobs:
- package:
- name: ZIP bauen und veröffentlichen
- runs-on: ubuntu-latest
-
- steps:
- - uses: actions/checkout@v4
- with:
- fetch-depth: 0
-
- - name: Version und Dateinamen bestimmen
- id: meta
- run: |
- set -eu
- VERSION="$(tr -d ' \r\n' < VERSION)"
- SHORT_SHA="$(git rev-parse --short HEAD)"
- BUILD_DATE="$(date -u +%Y-%m-%d)"
-
- if [ "${GITHUB_REF_TYPE:-branch}" = "tag" ]; then
- TAG="${GITHUB_REF_NAME}"
- NAME="unifi-voucher-tool-${TAG}"
- TITLE="Version ${TAG}"
- PRERELEASE="false"
- else
- TAG="latest-main"
- NAME="unifi-voucher-tool-${VERSION}+${BUILD_DATE}.${SHORT_SHA}"
- TITLE="Aktueller Stand von main – ${VERSION} (${SHORT_SHA})"
- PRERELEASE="true"
- fi
-
- {
- echo "version=${VERSION}"
- echo "short_sha=${SHORT_SHA}"
- echo "build_date=${BUILD_DATE}"
- echo "tag=${TAG}"
- echo "name=${NAME}"
- echo "title=${TITLE}"
- echo "prerelease=${PRERELEASE}"
- } >> "$GITHUB_OUTPUT"
-
- - name: ZIP erzeugen
- run: |
- set -eu
- mkdir -p dist
- # git archive wertet die export-ignore-Regeln aus .gitattributes aus,
- # docs/, tests/, tools/ und CI-Dateien bleiben also draußen.
- git archive --format=zip -9 \
- --prefix="unifi-voucher-tool/" \
- -o "dist/${{ steps.meta.outputs.name }}.zip" HEAD
- cd dist
- sha256sum "${{ steps.meta.outputs.name }}.zip" > "${{ steps.meta.outputs.name }}.zip.sha256"
- ls -lh
-
- - name: Inhalt kurz prüfen
- run: |
- set -eu
- # Ein paar Dateien muessen enthalten sein, sonst ist das Paket kaputt.
- for required in \
- unifi-voucher-tool/index.php \
- unifi-voucher-tool/install.php \
- unifi-voucher-tool/database.sql \
- unifi-voucher-tool/assets/global.css \
- unifi-voucher-tool/assets/vendor/inter/inter.css \
- unifi-voucher-tool/includes/Ui.php
- do
- if ! unzip -l "dist/${{ steps.meta.outputs.name }}.zip" | grep -q "$required"; then
- echo "Fehlt im Paket: $required" >&2
- exit 1
- fi
- done
- echo "Paket vollständig."
-
- - name: Als Build-Artefakt sichern
- uses: actions/upload-artifact@v3
- continue-on-error: true # Artefakt-Speicher ist optional
- with:
- name: ${{ steps.meta.outputs.name }}
- path: dist/*
- retention-days: 30
-
- - name: Release anlegen bzw. auffrischen
- env:
- # Forgejo stellt den Token automatisch bereit; FORGEJO_TOKEN
- # (persönlicher Token) dient als Ausweichweg.
- TOKEN: ${{ secrets.GITHUB_TOKEN || secrets.FORGEJO_TOKEN }}
- API: ${{ github.server_url }}/api/v1/repos/${{ github.repository }}
- TAG: ${{ steps.meta.outputs.tag }}
- NAME: ${{ steps.meta.outputs.name }}
- TITLE: ${{ steps.meta.outputs.title }}
- PRERELEASE: ${{ steps.meta.outputs.prerelease }}
- VERSION: ${{ steps.meta.outputs.version }}
- SHORT_SHA: ${{ steps.meta.outputs.short_sha }}
- BUILD_DATE: ${{ steps.meta.outputs.build_date }}
- run: |
- set -eu
- if [ -z "${TOKEN:-}" ]; then
- echo "Kein Token vorhanden – Release wird übersprungen." >&2
- exit 0
- fi
- AUTH="Authorization: token ${TOKEN}"
-
- # Rollendes Vorab-Release durch ein frisches ersetzen, damit der
- # Download-Link stabil bleibt und auf den aktuellen Stand zeigt.
- if [ "$TAG" = "latest-main" ]; then
- OLD_ID="$(curl -sf -H "$AUTH" "${API}/releases/tags/${TAG}" \
- | grep -o '"id":[0-9]*' | head -1 | cut -d: -f2 || true)"
- if [ -n "${OLD_ID:-}" ]; then
- curl -sf -X DELETE -H "$AUTH" "${API}/releases/${OLD_ID}" || true
- curl -sf -X DELETE -H "$AUTH" "${API}/tags/${TAG}" || true
- fi
- fi
-
- # Release-Text bewusst ohne Anfuehrungszeichen und Backslashes,
- # damit er ohne jq direkt in den JSON-Body passt (\n bleibt literal).
- BODY="Automatisch gebaut aus Commit ${SHORT_SHA}.\n\n"
- BODY="${BODY}| | |\n|---|---|\n"
- BODY="${BODY}| Version | ${VERSION} |\n"
- BODY="${BODY}| Commit | ${SHORT_SHA} |\n"
- BODY="${BODY}| Gebaut am | ${BUILD_DATE} |\n\n"
- BODY="${BODY}**Neuinstallation:** ZIP entpacken, Dateien auf den Webserver legen, install.php aufrufen.\n\n"
- BODY="${BODY}**Update einer bestehenden Installation:** config.php, uploads/ und updater/storage/ nicht ueberschreiben "
- BODY="${BODY}- oder gleich den eingebauten Updater unter Administration, System-Update verwenden.\n\n"
- BODY="${BODY}Pruefsumme: siehe beigelegte .sha256-Datei."
-
- RELEASE_ID="$(curl -sf -X POST -H "$AUTH" -H 'Content-Type: application/json' \
- -d "{\"tag_name\":\"${TAG}\",\"target_commitish\":\"${GITHUB_SHA}\",\"name\":\"${TITLE}\",\"body\":\"${BODY}\",\"draft\":false,\"prerelease\":${PRERELEASE}}" \
- "${API}/releases" | grep -o '"id":[0-9]*' | head -1 | cut -d: -f2)"
-
- if [ -z "${RELEASE_ID:-}" ]; then
- echo "Release konnte nicht angelegt werden." >&2
- exit 1
- fi
-
- for file in "dist/${NAME}.zip" "dist/${NAME}.zip.sha256"; do
- curl -sf -X POST -H "$AUTH" \
- -F "attachment=@${file}" \
- "${API}/releases/${RELEASE_ID}/assets?name=$(basename "$file")" > /dev/null
- echo "Angehängt: $(basename "$file")"
- done
-
- echo "Release ${TITLE} steht bereit: ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/releases/tag/${TAG}"
diff --git a/.htaccess b/.htaccess
deleted file mode 100644
index 2654739..0000000
--- a/.htaccess
+++ /dev/null
@@ -1,26 +0,0 @@
-# ---------------------------------------------------------------------------
-# Sicherheits-Header und Zugriffsschutz (Apache)
-# Nginx-Entsprechung siehe Readme.md, Abschnitt "Sicherheit".
-# ---------------------------------------------------------------------------
-
-
- Header always set X-Content-Type-Options "nosniff"
- Header always set X-Frame-Options "SAMEORIGIN"
- Header always set Referrer-Policy "strict-origin-when-cross-origin"
- Header always set Permissions-Policy "geolocation=(), microphone=(), camera=()"
-
- # Alle Frontend-Assets liegen lokal; externe Quellen nur fuer hCaptcha,
- # falls es in den Einstellungen aktiviert wurde.
- Header always set Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' https://js.hcaptcha.com https://*.hcaptcha.com; style-src 'self' 'unsafe-inline' https://*.hcaptcha.com; img-src 'self' data: https:; font-src 'self'; connect-src 'self' https://*.hcaptcha.com; frame-src https://*.hcaptcha.com; frame-ancestors 'self'; base-uri 'self'; form-action 'self'"
-
-
-# Kein Verzeichnislisting
-Options -Indexes
-
-# Dateien, die nie direkt ausgeliefert werden sollen
-
- Require all denied
-
-
-# Interne Ordner schuetzen sich ueber eigene .htaccess-Dateien
-# (funktioniert auch bei Installation in einem Unterverzeichnis).
diff --git a/Dockerfile b/Dockerfile
index 9696aa2..3d0f28c 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -16,14 +16,11 @@ RUN { \
echo 'post_max_size=8M'; \
} > /usr/local/etc/php/conf.d/zz-voucher.ini
-# .htaccess auswerten (Sicherheits-Header, Schutz des uploads-Ordners)
-RUN sed -ri 's!!\n\tAllowOverride All!g' /etc/apache2/apache2.conf
-
WORKDIR /var/www/html
COPY . /var/www/html
-# Laufzeit-Verzeichnisse beschreibbar machen
-RUN mkdir -p /var/www/html/updater/storage /var/www/html/uploads \
+# Laufzeit-Verzeichnis des Updaters beschreibbar machen
+RUN mkdir -p /var/www/html/updater/storage \
&& chown -R www-data:www-data /var/www/html
COPY docker/entrypoint.sh /usr/local/bin/entrypoint.sh
diff --git a/Readme.md b/Readme.md
index 46a8a84..af6cb97 100644
--- a/Readme.md
+++ b/Readme.md
@@ -4,15 +4,12 @@
**Webbasiertes WLAN-Voucher-Management für UniFi OS** – mit Multi-Site-Support, Benutzerverwaltung, Microsoft-365-Login und integriertem Auto-Updater.
-Entwickelt von **[Loheide.eu](https://loheide.eu)**
-




-
-
-[](https://git.loheide.cloud/friloo/Unifi-Voucher-Tool)
+
+
@@ -28,8 +25,6 @@ Entwickelt von **[Loheide.eu](https://loheide.eu)**
## ✨ Features
- 🎟️ **Voucher-Erstellung** mit sofortiger QR-Code-Anzeige, Druckvorlage und E-Mail-Versand
-- 🖥️ **Display-Seiten (Kiosk)** – öffentliche Seite je Site, an der Gäste sich mit einem Klick selbst einen Zugang holen
-- 🎨 **Jede Display-Seite eigenständig gestaltbar** – Logo, Hintergrundbild, Akzentfarbe, helle oder dunkle Karte
- 📦 **Bulk-Erstellung** – bis zu 20 Vouchers auf einmal, inkl. Sammeldruck-Layout
- 🧩 **Voucher-Profile/Templates** – vordefinierte Laufzeiten & Gerätelimits per Schnellauswahl
- 🏢 **Multi-Site-Support** – beliebig viele UniFi-Standorte zentral verwalten
@@ -52,11 +47,6 @@ Entwickelt von **[Loheide.eu](https://loheide.eu)**
- 🌍 **Öffentlicher Modus** – optional ohne Login nutzbar (mit CSRF-Schutz & Throttle)
- 🎨 **Einheitliches Design-System** – ein Stylesheet für Frontend, Login und Backend (Tokens, Komponenten, Light/Dark)
- 🏷️ **Login-Seite individualisierbar** – Firmenname, Logo, Texte, Hintergrundbild bzw. Farbverlauf
-- 🖌️ **Eigene Markenfarben** – Akzentfarbe, Verlauf und Eckenradius wirken auf die gesamte Oberfläche
-- ⬆️ **Bild-Upload** für Logo, Favicon und Login-Hintergrund (kein externes Hosting nötig)
-- 🔒 **Keine externen CDNs** – Schrift, Icons, Diagramme und Editor werden lokal ausgeliefert (DSGVO, Offline-Netze)
-- ♿ **Barrierearm** – Kontraste nach WCAG AA, Sprungmarke, aria-Beschriftungen, `prefers-reduced-motion`
-- 📱 **Mobil nutzbar** – Tabellen werden auf schmalen Geräten zu Karten
- 🌗 **Dark Mode** – umschaltbar, Einstellung wird im Browser gespeichert
- 🌐 **Mehrsprachig** – Deutsch / Englisch per Umschalter (`lang/`)
- 📱 **Responsive Admin-Layout** mit Hamburger-Menü & Sidebar-Overlay
@@ -85,18 +75,6 @@ Entwickelt von **[Loheide.eu](https://loheide.eu)**
-### Display-Seite für Gäste
-
-
-
-
-
-
-
-
-
-
-
@@ -114,11 +92,6 @@ Entwickelt von **[Loheide.eu](https://loheide.eu)**
-
-
-
-
-
### REST-API, 2FA & Integrationen
@@ -157,8 +130,8 @@ Entwickelt von **[Loheide.eu](https://loheide.eu)**
## 🚀 Installation
```bash
-git clone https://git.loheide.cloud/friloo/Unifi-Voucher-Tool.git
-cd Unifi-Voucher-Tool
+git clone https://github.com/friloo/unifi-voucher-tool.git
+cd unifi-voucher-tool
```
1. Dateien auf den Webserver hochladen
@@ -228,64 +201,6 @@ Während eines Updates wird die Anwendung kurz in den **Wartungsmodus** versetzt
---
-## 🖥️ Display-Seiten für Gäste
-
-Für Empfang, Lobby oder Tagungsraum lässt sich je Site eine **öffentliche Seite**
-anlegen, die auf einem Bildschirm oder Tablet läuft. Gäste tippen auf einen
-Knopf und bekommen sofort einen eigenen Zugangscode – ohne Anmeldung, ohne
-Personal am Tresen.
-
-**Anlegen:** Administration → **Display-Seiten** → *Display-Seite anlegen*
-
-
-
-
-
-| Einstellung | Wirkung |
-|---|---|
-| Site | für welchen Standort die Codes erzeugt werden |
-| Voucher-Profil | Laufzeit, Geräteanzahl und Bandbreite der Codes (leer = Standardwerte) |
-| Überschrift / Text | was auf dem Bildschirm steht |
-| Codes pro Tag | Obergrenze je Kalendertag (0 = unbegrenzt) |
-| Wartezeit | Abstand zwischen zwei Codes an diesem Display |
-| Anzeigedauer | danach springt der Bildschirm automatisch zurück |
-
-Jede Seite hat einen **eigenen, geheimen Link** (`kiosk.php?k=…`). Er lässt sich
-kopieren, als QR-Code anzeigen (praktisch, um ihn am Tablet zu öffnen) und
-jederzeit erneuern – der alte Link ist dann sofort ungültig. Den Link nicht
-öffentlich verbreiten: wer ihn hat, kann im Rahmen der Limits Codes ziehen.
-
-Auf dem Startbildschirm steht zusätzlich ein QR-Code, der auf dieselbe Seite
-zeigt. Gäste können sie damit **am eigenen Handy** öffnen – praktisch bei
-Bildschirmen ohne Touch.
-
-### Jede Seite eigenständig gestalten
-
-Jede Display-Seite bringt ihr eigenes Erscheinungsbild mit – das Hotel am
-Empfang sieht anders aus als der Tagungsraum nebenan:
-
-| Einstellung | Wirkung |
-|---|---|
-| Logo | eigenes Logo auf der Karte (leer = Logo aus den Einstellungen) |
-| Hintergrundbild | formatfüllend hinter der Karte, z. B. ein Foto des Hauses |
-| Abdunklung | 0–90 % dunkle Ebene über dem Bild, damit die Karte lesbar bleibt |
-| Akzentfarbe | färbt den Knopf dieser Seite (leer = Farbe aus dem Design-Tab) |
-| Karte | hell oder dunkel – auf Fotos wirkt die dunkle Karte meist ruhiger |
-
-Logo und Hintergrund lassen sich direkt hochladen (PNG, JPG, WEBP, GIF, SVG bis
-3 MB) oder als URL hinterlegen; beim Löschen einer Display-Seite verschwinden
-die hochgeladenen Dateien mit.
-
-Die ausgegebenen Codes erscheinen normal in *Live Vouchers*, im *Reporting* und
-im *Audit-Log* (Aktion „Voucher am Display geholt"), sodass jederzeit
-nachvollziehbar bleibt, woher ein Zugang stammt.
-
-> Display-Seiten funktionieren unabhängig vom globalen öffentlichen Modus – der
-> geheime Link ist der Zugang. Webhook-Benachrichtigungen werden für diese Codes
-> bewusst **nicht** ausgelöst, sonst wäre der Slack-Kanal voll.
-
----
-
## ⚙️ Konfiguration
### `config.php`
@@ -334,44 +249,21 @@ gemeinsames Stylesheet: **`assets/global.css`**.
- **Dark Mode** ausschließlich über Tokens – keine `!important`-Overrides mehr
- **Schriftart** Inter (via Google Fonts) mit System-Font-Fallback
-### Markenfarben ohne Code
-
-Unter **Administration → Einstellungen → Design** lassen sich Akzentfarbe
-(hell und dunkel), Markenverlauf und Eckenradius setzen. Abgeleitete Töne –
-Hover, weiche Flächen, Rahmen, Fokusring – berechnet das System per `color-mix`
-aus der Grundfarbe; eine Farbe genügt also. Eine Live-Vorschau zeigt Button,
-Badge, Chip und Logo-Kachel sofort im neuen Ton.
-
-Die Werte landen als schlanker `:root`-Override im Seitenkopf und gelten überall,
-auch auf Login-Seite, Installer und Updater. Wer lieber in CSS arbeitet, kann
-dieselben Variablen weiterhin in `assets/global.css` überschreiben:
+Eigenes Branding lässt sich meist mit wenigen Zeilen umsetzen – z. B. in einer
+eigenen CSS-Datei oder direkt in `assets/global.css`:
```css
:root {
--accent: #0f766e; /* Primärfarbe (Buttons, aktive Navigation) */
+ --accent-hover: #0d5f59;
+ --accent-soft: #e6f4f2; /* Flächen für aktive Zustände */
--brand-gradient: linear-gradient(135deg, #0f766e 0%, #0ea5e9 100%);
--r-lg: 14px; /* Eckenradius für Cards */
}
```
-### Bilder hochladen
-
-Logo, Favicon, Login-Logo und Login-Hintergrund lassen sich direkt hochladen –
-alternativ bleibt das URL-Feld bestehen. Die Dateien landen unter `uploads/`
-(Docker: eigenes Volume, siehe unten). Erlaubt sind PNG, JPG, WEBP, GIF und SVG
-bis 3 MB; SVGs werden vor dem Speichern von Skripten und externen Verweisen
-befreit, und im Upload-Ordner sperrt eine `.htaccess` die PHP-Ausführung.
-
-### Assets ohne Drittanbieter
-
-Schrift (Inter), Icons (Font Awesome), Diagramme (Chart.js), QR-Codes und der
-WYSIWYG-Editor (TinyMCE) liegen unter `assets/vendor/` und kommen vom eigenen
-Server. Das hält Besucher-IPs bei Ihnen – und die Oberfläche funktioniert auch
-dort, wo das Netz keinen Weg nach außen hat. Details und Aktualisierungs-Hinweise:
-[`assets/vendor/README.md`](assets/vendor/README.md).
-
-Alle Asset-URLs tragen einen Versionsstempel (`?v=…`), damit Browser nach einem
-Update nicht die alten Dateien aus dem Cache verwenden.
+Logo und Favicon werden nicht über CSS, sondern unter
+**Administration → Einstellungen → Allgemein** gesetzt.
### Login-Seite individualisieren
@@ -411,30 +303,13 @@ Das Tool ist auf einen sicheren Standardbetrieb ausgelegt:
| **Sessions** | HttpOnly, SameSite, strict mode + absolutes Timeout |
| **Fehler** | `display_errors` aus, `log_errors` an (kein Info-Leak) |
-Mitgeliefert wird eine `.htaccess` im Projektstamm mit Sicherheits-Headern
-(`X-Content-Type-Options`, `X-Frame-Options`, `Referrer-Policy`,
-`Permissions-Policy` und einer Content-Security-Policy). Da alle Assets lokal
-liegen, erlaubt die CSP nur noch die eigene Herkunft – externe Verbindungen
-bleiben lediglich für hCaptcha offen, falls es aktiviert wird. Ordner wie
-`includes/`, `tools/`, `tests/` und `uploads/` schützen sich über eigene
-`.htaccess`-Dateien.
+Empfohlene zusätzliche Härtung am Server:
-> **Apache:** `AllowOverride All` muss für das Verzeichnis gesetzt sein, sonst
-> werden die `.htaccess`-Dateien ignoriert. Das mitgelieferte Docker-Image
-> erledigt das bereits.
-
-Für **Nginx** entspricht das:
-
-```nginx
-add_header X-Content-Type-Options "nosniff" always;
-add_header X-Frame-Options "SAMEORIGIN" always;
-add_header Referrer-Policy "strict-origin-when-cross-origin" always;
-add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; frame-ancestors 'self'" always;
-
-location ~ ^/(includes|tools|tests)/ { deny all; }
-location ~ ^/updater/(storage|migrations)/ { deny all; }
-location ~ ^/(config\.php|database\.sql)$ { deny all; }
-location ^~ /uploads/ { location ~ \.php$ { deny all; } }
+```apache
+# .htaccess – sensible Dateien sperren (wird vom Installer erzeugt)
+
+ Require all denied
+
```
```sql
@@ -557,108 +432,6 @@ Das Schema wird beim ersten Start automatisch in MariaDB geladen; danach den
Installer (`/install.php`) für den Admin-Account aufrufen oder Config per ENV
setzen (`DB_*`, `APP_KEY`).
-Hochgeladene Logos und Hintergründe liegen im Volume `uploads` und überstehen
-damit ein Image-Update. Bei eigener Apache-/Nginx-Installation muss `uploads/`
-für den Webserver beschreibbar sein:
-
-```bash
-chown -R www-data:www-data uploads updater/storage
-```
-
----
-
-## 🧪 Entwicklung
-
-Für Screenshots und einen schnellen Durchlauf aller Seiten gibt es eine
-Demo-Instanz **ohne Datenbank** – `Database` und `Auth` werden durch Stubs mit
-festen Beispieldaten ersetzt:
-
-```bash
-python3 tools/demo/build.py /tmp/uvt-demo
-php -S 127.0.0.1:8123 -t /tmp/uvt-demo &
-
-# Bilder in docs/screenshots neu erzeugen (benötigt headless Chromium)
-CHROME_BIN=/usr/bin/chromium python3 tools/screenshots.py
-```
-
-Details und die verfügbaren Demo-Zustände: [`tools/README.md`](tools/README.md).
-
-Tests und statische Analyse:
-
-```bash
-composer install
-vendor/bin/phpunit # 29 Tests (Crypto, TOTP, API-Keys, Upload, Ui)
-vendor/bin/phpstan analyse # Level 5
-```
-
-Die Versionsnummer steht in der Datei **`VERSION`** im Projektstamm. Sie wird
-im Admin-Bereich unten in der Seitenleiste angezeigt und benennt das
-Release-Paket – für eine neue Version also dort (und im Badge oben) anheben.
-
-Die Pipeline (`.github/workflows/ci.yml`) führt zusätzlich einen
-Syntax-Check über alle PHP-Dateien aus und prüft, ob `lang/de.php` und
-`lang/en.php` dieselben Schlüssel enthalten und jeder im Code verwendete
-Schlüssel existiert. Dieselben Schritte lassen sich lokal ausführen.
-
----
-
-## 📦 Repository & Mitwirken
-
-Der Quellcode liegt auf der eigenen Forgejo-Instanz – **nicht** auf GitHub:
-
-****
-
-```bash
-# HTTPS
-git clone https://git.loheide.cloud/friloo/Unifi-Voucher-Tool.git
-
-# SSH (Port 2222)
-git clone ssh://git@git.loheide.cloud:2222/friloo/Unifi-Voucher-Tool.git
-```
-
-### Fertige Pakete
-
-Jeder Merge nach `main` erzeugt automatisch ein installierbares ZIP
-(`.github/workflows/release.yml`) und hängt es an das rollende Vorab-Release
-**`latest-main`**:
-
-****
-
-Das Paket enthält nur die Laufzeit-Dateien – `docs/`, `tests/`, `tools/` und die
-CI-Konfiguration bleiben draußen (rund 1,4 MB). Wird ein Tag `v*` gepusht,
-entsteht daraus ein reguläres Release mit derselben Mechanik.
-
-> Beim **Update einer bestehenden Installation** `config.php`, `uploads/` und
-> `updater/storage/` nicht überschreiben – oder gleich den eingebauten Updater
-> verwenden, der genau diese Pfade schützt.
-
-Voraussetzung ist ein registrierter **Forgejo-Actions-Runner**; ohne Runner
-bleiben die Workflows in der Warteschlange stehen. Compose-Datei und Anleitung
-dafür liegen in [`tools/runner/`](tools/runner/README.md).
-
-### Mitwirken
-
-Fehlerberichte und Änderungsvorschläge laufen über die **Issues** und **Pull
-Requests** dort. Für die Kommandozeile eignet sich [`tea`](https://gitea.com/gitea/tea),
-die Gitea-/Forgejo-CLI:
-
-```bash
-tea pr create # Pull Request öffnen
-tea issues ls # offene Tickets ansehen
-```
-
-> Die Workflows unter `.github/workflows/` werden von Forgejo Actions
-> mitgelesen; das Badge oben ist bewusst statisch, solange kein Runner
-> registriert ist. `docker-publish.yml` veröffentlicht nach `ghcr.io` und
-> stammt noch aus der GitHub-Zeit – für den Forgejo-Betrieb entweder auf die
-> eigene Registry umstellen oder entfernen.
-
-Der **Auto-Updater** ist davon unabhängig: er zieht seine Pakete über
-`update.loheide.eu` (Channels `stable` und `development`) und nicht direkt aus
-dem Git-Hoster.
-
----
-
## 🗺️ Roadmap
- [x] Voucher-Templates (vordefinierte Laufzeiten)
@@ -674,17 +447,11 @@ dem Git-Hoster.
- [x] Erweiterte Reporting-Funktionen (CSV/PDF) + Health-Endpoint
- [x] 2FA-Recovery-Codes, API-Scopes/Rate-Limit/OpenAPI, Test-Suite (PHPUnit/PHPStan)
- [x] Gemeinsames Design-System für Frontend, Login und Backend
-- [x] Branding über die Oberfläche (Farben, Logo, Login-Seite)
-- [x] Assets lokal ausliefern (keine Drittanbieter-CDNs)
-- [x] Vollständige englische Übersetzung des Admin-Bereichs
-- [x] Display-Seiten: Selbstbedienung für Gäste am Bildschirm
---
Lädt Einstellungen, Sites und Voucher-Profile als JSON. Site-Passwörter bleiben mit dem APP_KEY dieser Installation verschlüsselt – ein Restore auf einer Installation mit anderem APP_KEY kann sie nicht entschlüsseln.
Vorhandene Sites werden anhand von Name + Site-ID aktualisiert, neue hinzugefügt. Profile werden nur angelegt, wenn der Name noch nicht existiert. Der Cron-Token wird nie überschrieben.
SSO, Webhooks, SMS-Versand und Aufbewahrungsfristen.
@@ -108,64 +108,64 @@ $adminBase = '';
-
= __('int_security') ?>
-
= __('int_security_hint') ?>
-
-
+
Sicherheitsrichtlinie
+
Erzwingt Zwei-Faktor-Authentifizierung für alle Administrator-Konten (lokale Accounts). Admins ohne 2FA werden bei der nächsten Aktion zur Einrichtung geleitet.
Bei jeder Anmeldung wird zusätzlich ein Code aus Ihrer Authenticator-App abgefragt.
+ Verbleibende Recovery-Codes: = (int)$auth->backupCodesRemaining($user) ?>
-
-
= __('sec_inactive') ?>
+
Inaktiv
-
= __('sec_step_1') ?>
-
= __('sec_step_2') ?>
-
= __('sec_step_3') ?>
+
Authenticator-App öffnen (Google Authenticator, Authy, Microsoft Authenticator …)