Fix sites freeze bug, add features and shorten README
Bug fixes: - UniFiController: add CURLOPT_TIMEOUT (10s) and CURLOPT_CONNECTTIMEOUT (5s) to login() and apiRequest() — prevents page freeze when controller unreachable - UniFiController: fix login response validation for UniFi OS API which returns a user object instead of meta.rc=ok - admin/sites.php: add JS loading state on form submit to give visual feedback - login.php: handle new 'rate_limited' return value from Auth::login() New features: - Database: in-memory settings cache eliminates redundant DB queries per request - Auth: login rate limiting (10 attempts per 10 min per IP/email) via login_attempts table - admin/vouchers.php: CSV export with UTF-8 BOM for Excel compatibility - admin/vouchers.php: client-side pagination (50 per page) - index.php: QR code display after voucher creation (qrcodejs CDN) - Mailer: sendTestEmail() method - admin/settings.php: SMTP test button with AJAX handler - database.sql: add login_attempts and audit_log tables Readme: condensed from ~420 to ~220 lines, removed duplicated sections, M365 Azure Portal walkthrough, contribution guidelines, update/migration section https://claude.ai/code/session_01UsuvFAmmeagtQa14QA4iaq
This commit is contained in:
parent
3fd9b2190a
commit
73967caefa
11 changed files with 460 additions and 450 deletions
423
Readme.md
423
Readme.md
|
|
@ -1,372 +1,140 @@
|
|||
# UniFi Voucher Management System
|
||||
|
||||
Ein professionelles, webbasiertes System zur Verwaltung von WLAN-Vouchers für UniFi Controller mit Multi-Site-Unterstützung, Benutzerverwaltung und Microsoft 365 Integration.
|
||||
Webbasiertes System zur Verwaltung von WLAN-Vouchers für UniFi OS mit Multi-Site-Unterstützung, Benutzerverwaltung und Microsoft 365 Integration.
|
||||
|
||||
## ✨ Features
|
||||
## Features
|
||||
|
||||
### Kern-Funktionen
|
||||
- 🎫 **Voucher-Erstellung**: Einfache Erstellung von zeitbegrenzten WLAN-Zugangscodes
|
||||
- 🏢 **Multi-Site-Support**: Verwaltung mehrerer UniFi Sites/Standorte
|
||||
- 👥 **Benutzerverwaltung**: Granulare Zugriffskontrolle auf Site-Ebene
|
||||
- 🔐 **Authentifizierung**: Lokale Accounts und Microsoft 365 OAuth
|
||||
- 📊 **Admin-Dashboard**: Übersichtliche Statistiken und Historie
|
||||
- 🌐 **Öffentlicher Zugriff**: Optional ohne Login nutzbar
|
||||
- 🎨 **Modernes Design**: Responsives, helles und professionelles UI
|
||||
- **Voucher-Erstellung** mit QR-Code-Anzeige und E-Mail-Versand
|
||||
- **Multi-Site-Support** – mehrere UniFi-Standorte verwalten
|
||||
- **Benutzerverwaltung** mit granularer Site-Zugriffskontrolle
|
||||
- **Authentifizierung** via lokale Accounts oder Microsoft 365 OAuth
|
||||
- **CSV-Export** aller Vouchers pro Site
|
||||
- **Admin-Dashboard** mit Live-Statistiken und Sync-Funktion
|
||||
- **Öffentlicher Zugriff** – optional ohne Login nutzbar
|
||||
- CSRF-Schutz, bcrypt-Passwörter, Prepared Statements, Login-Rate-Limiting
|
||||
|
||||
### Sicherheit
|
||||
- CSRF-Schutz für alle Formulare
|
||||
- Password-Hashing mit bcrypt
|
||||
- Session-Management mit konfigurierbaren Timeouts
|
||||
- SQL-Injection-Schutz durch Prepared Statements
|
||||
- Rollenbasierte Zugriffskontrolle (Admin/User)
|
||||
## Anforderungen
|
||||
|
||||
## 📋 Anforderungen
|
||||
- PHP 7.4+, MySQL 5.7+ / MariaDB 10.2+, Apache/Nginx
|
||||
- PHP-Extensions: PDO, PDO_MySQL, cURL, mbstring, JSON
|
||||
- **UniFi Network Application 7.0+ mit UniFi OS** (z.B. UDM, UDR, UniFi OS Server)
|
||||
|
||||
### Server-Anforderungen
|
||||
- PHP 7.4 oder höher
|
||||
- MySQL 5.7+ oder MariaDB 10.2+
|
||||
- Apache/Nginx Webserver
|
||||
- PHP-Extensions:
|
||||
- PDO
|
||||
- PDO_MySQL
|
||||
- cURL
|
||||
- mbstring
|
||||
- JSON
|
||||
## Installation
|
||||
|
||||
### UniFi Controller
|
||||
- UniFi Network Application 7.0+ mit UniFi OS (z.B. UDM, UDR, UniFi OS Server)
|
||||
- API-Zugriff aktiviert
|
||||
- Lokaler Admin-Account oder dedizierter API-User
|
||||
|
||||
## 🚀 Installation
|
||||
|
||||
### Schritt 1: Dateien hochladen
|
||||
```bash
|
||||
# Repository klonen oder ZIP herunterladen
|
||||
git clone https://github.com/friloo/unifi-voucher-tool.git
|
||||
cd unifi-voucher-tool
|
||||
|
||||
# Dateien auf den Webserver hochladen
|
||||
# Stellen Sie sicher, dass der Webserver-User Schreibrechte hat
|
||||
```
|
||||
|
||||
### Schritt 2: Ordnerstruktur
|
||||
1. Dateien auf den Webserver hochladen
|
||||
2. `http://ihre-domain.de/install.php` öffnen
|
||||
3. Den 5-Schritte-Assistenten durchlaufen:
|
||||
- **Schritt 1:** Datenbank-Verbindungsdaten
|
||||
- **Schritt 2:** Administrator-Account (Name, E-Mail, Passwort)
|
||||
- **Schritt 3:** Allgemeine Einstellungen (Titel, Logo, öffentlicher Zugriff)
|
||||
- **Schritt 4:** Microsoft 365 Integration (optional)
|
||||
- **Schritt 5:** Installation abschließen
|
||||
4. `install.php` nach erfolgreicher Installation löschen
|
||||
|
||||
```
|
||||
/
|
||||
├── config.php (wird vom Installer erstellt)
|
||||
├── install.php
|
||||
├── index.php
|
||||
├── login.php
|
||||
├── logout.php
|
||||
├── database.sql
|
||||
├── .htaccess (wird vom Installer erstellt)
|
||||
├── includes/
|
||||
│ ├── Database.php
|
||||
│ ├── Auth.php
|
||||
│ └── UniFiController.php
|
||||
└── admin/
|
||||
├── index.php
|
||||
├── sites.php
|
||||
├── users.php
|
||||
├── vouchers.php
|
||||
└── settings.php
|
||||
```
|
||||
## Sites konfigurieren
|
||||
|
||||
### Schritt 3: Installation durchführen
|
||||
1. **Administration → Sites verwalten → Neue Site hinzufügen**
|
||||
2. Felder ausfüllen:
|
||||
- **Name:** Anzeigename (z.B. „Hauptgebäude")
|
||||
- **Site ID:** UniFi Site ID (meist `default`)
|
||||
- **Controller URL:** `https://unifi.example.com:11443`
|
||||
- **Benutzername / Passwort:** UniFi Admin-Zugangsdaten
|
||||
3. **Verbindung testen** klicken, dann speichern
|
||||
|
||||
1. Öffnen Sie `http://ihre-domain.de/install.php` im Browser
|
||||
2. Folgen Sie dem 5-Schritte-Installations-Assistenten:
|
||||
## Voucher erstellen
|
||||
|
||||
#### Schritt 1: Datenbank-Konfiguration
|
||||
- Datenbank-Host (meist `localhost`)
|
||||
- Datenbankname (z.B. `unifi_voucher`)
|
||||
- Datenbank-Benutzer
|
||||
- Datenbank-Passwort
|
||||
1. Startseite öffnen (Login je nach Konfiguration optional)
|
||||
2. Voucher-Name, Anzahl Geräte und Standort wählen
|
||||
3. **Voucher erstellen** – Code und QR-Code werden sofort angezeigt
|
||||
4. Code per E-Mail senden oder ausdrucken
|
||||
|
||||
#### Schritt 2: Administrator-Account
|
||||
- Name
|
||||
- E-Mail-Adresse
|
||||
- Passwort (min. 8 Zeichen)
|
||||
|
||||
#### Schritt 3: Allgemeine Einstellungen
|
||||
- Anwendungs-Titel
|
||||
- Logo-URL (optional)
|
||||
- Anleitung für Benutzer
|
||||
- Öffentlicher Zugriff aktivieren (optional)
|
||||
|
||||
#### Schritt 4: Microsoft 365 Integration (optional)
|
||||
- Client ID
|
||||
- Client Secret
|
||||
- Tenant ID
|
||||
|
||||
#### Schritt 5: Installation abschließen
|
||||
|
||||
Nach erfolgreicher Installation wird automatisch:
|
||||
- Die Datenbank erstellt und initialisiert
|
||||
- Die `config.php` Datei generiert
|
||||
- Die `.htaccess` für URL-Rewriting erstellt
|
||||
- Der Admin-Account angelegt
|
||||
|
||||
### Schritt 4: Installation sichern
|
||||
|
||||
Nach erfolgreicher Installation:
|
||||
```bash
|
||||
# install.php umbenennen oder löschen
|
||||
mv install.php install.php.bak
|
||||
|
||||
# Oder komplett entfernen
|
||||
rm install.php
|
||||
```
|
||||
|
||||
## 🎯 Erste Schritte
|
||||
|
||||
### 1. Als Administrator anmelden
|
||||
- Öffnen Sie `http://ihre-domain.de/login.php`
|
||||
- Melden Sie sich mit Ihren Admin-Zugangsdaten an
|
||||
|
||||
### 2. Sites konfigurieren
|
||||
1. Navigieren Sie zu **Administration** → **Sites verwalten**
|
||||
2. Klicken Sie auf **Neue Site hinzufügen**
|
||||
3. Geben Sie folgende Daten ein:
|
||||
- **Name**: Anzeigename (z.B. "Hauptgebäude")
|
||||
- **Site ID**: UniFi Site ID (z.B. "default")
|
||||
- **Controller URL**: URL Ihres UniFi Controllers (z.B. "https://unifi.example.com:11443")
|
||||
- **Benutzername**: UniFi Admin-Username
|
||||
- **Passwort**: UniFi Admin-Passwort
|
||||
- **Öffentlicher Zugriff**: Aktivieren für Login-freie Nutzung
|
||||
|
||||
4. Klicken Sie auf **Verbindung testen**, um die Einstellungen zu überprüfen
|
||||
5. Speichern Sie die Site
|
||||
|
||||
### 3. Benutzer anlegen
|
||||
1. Navigieren Sie zu **Administration** → **Benutzer verwalten**
|
||||
2. Klicken Sie auf **Neuer Benutzer**
|
||||
3. Geben Sie die Benutzerdaten ein:
|
||||
- Name
|
||||
- E-Mail
|
||||
- Passwort
|
||||
- Admin-Rechte (optional)
|
||||
4. Wählen Sie die Sites aus, auf die der Benutzer Zugriff haben soll
|
||||
5. Speichern Sie den Benutzer
|
||||
|
||||
### 4. Vouchers erstellen
|
||||
1. Gehen Sie zur Startseite
|
||||
2. Wählen Sie eine Site aus
|
||||
3. Geben Sie einen Voucher-Namen ein
|
||||
4. Legen Sie die Anzahl der Geräte fest (1-10)
|
||||
5. Klicken Sie auf **Voucher erstellen**
|
||||
6. Der Code wird sofort angezeigt und ist 8 Stunden gültig
|
||||
|
||||
## 🔧 Konfiguration
|
||||
## Konfiguration
|
||||
|
||||
### config.php
|
||||
Die Datei wird automatisch erstellt, kann aber manuell angepasst werden:
|
||||
|
||||
Wird automatisch durch den Installer erstellt:
|
||||
```php
|
||||
<?php
|
||||
define('DB_HOST', 'localhost');
|
||||
define('DB_NAME', 'unifi_voucher');
|
||||
define('DB_USER', 'username');
|
||||
define('DB_PASS', 'password');
|
||||
|
||||
define('SESSION_LIFETIME', 3600); // 1 Stunde
|
||||
|
||||
define('SESSION_LIFETIME', 3600);
|
||||
date_default_timezone_set('Europe/Berlin');
|
||||
```
|
||||
|
||||
### Microsoft 365 OAuth einrichten
|
||||
### Microsoft 365 OAuth (optional)
|
||||
1. Im [Azure Portal](https://portal.azure.com) eine App-Registrierung anlegen
|
||||
2. Umleitungs-URI: `https://ihre-domain.de/m365_callback.php`
|
||||
3. API-Berechtigungen: `User.Read`, `email`, `profile`, `openid`
|
||||
4. Client ID, Client Secret und Tenant ID in **Administration → Einstellungen** eintragen
|
||||
|
||||
1. **Azure AD App registrieren**:
|
||||
- Gehen Sie zu https://portal.azure.com
|
||||
- Navigieren Sie zu "Azure Active Directory" → "App-Registrierungen"
|
||||
- Klicken Sie auf "Neue Registrierung"
|
||||
- Name: "UniFi Voucher System"
|
||||
- Unterstützte Kontotypen: "Nur Konten in diesem Organisationsverzeichnis"
|
||||
- Umleitungs-URI: `https://ihre-domain.de/login.php`
|
||||
### Cron-Job (empfohlen)
|
||||
Automatische Synchronisation alle 30 Minuten:
|
||||
```bash
|
||||
*/30 * * * * curl -s "https://ihre-domain.de/cron_sync.php?token=IHR_CRON_TOKEN"
|
||||
```
|
||||
Den Token finden Sie unter **Administration → Einstellungen → Cron**.
|
||||
|
||||
2. **API-Berechtigungen**:
|
||||
- Microsoft Graph → Delegierte Berechtigungen
|
||||
- `User.Read`
|
||||
- `email`
|
||||
- `profile`
|
||||
- `openid`
|
||||
## Sicherheit
|
||||
|
||||
3. **Client Secret erstellen**:
|
||||
- Gehen Sie zu "Zertifikate & Geheimnisse"
|
||||
- Erstellen Sie ein neues Client-Geheimnis
|
||||
- Notieren Sie den Wert (nur einmal sichtbar!)
|
||||
|
||||
4. **In System eintragen**:
|
||||
- Administration → Einstellungen
|
||||
- Microsoft 365 Bereich ausfüllen
|
||||
- Client ID, Client Secret und Tenant ID eintragen
|
||||
|
||||
## 🔐 Sicherheitsempfehlungen
|
||||
|
||||
### Server-Konfiguration
|
||||
```apache
|
||||
# .htaccess zusätzliche Sicherheit
|
||||
<Files "config.php">
|
||||
Order Allow,Deny
|
||||
Deny from all
|
||||
</Files>
|
||||
|
||||
<FilesMatch "\.(sql|md)$">
|
||||
# .htaccess – sensible Dateien sperren
|
||||
<FilesMatch "^(config\.php|database\.sql|.*\.md)$">
|
||||
Order Allow,Deny
|
||||
Deny from all
|
||||
</FilesMatch>
|
||||
```
|
||||
|
||||
### Datenbank-Benutzer
|
||||
Erstellen Sie einen dedizierten Datenbankbenutzer nur für diese Anwendung:
|
||||
|
||||
```sql
|
||||
-- Dedizierter Datenbank-Benutzer
|
||||
CREATE USER 'unifi_voucher'@'localhost' IDENTIFIED BY 'sicheres_passwort';
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON unifi_voucher.* TO 'unifi_voucher'@'localhost';
|
||||
FLUSH PRIVILEGES;
|
||||
```
|
||||
|
||||
### HTTPS erzwingen
|
||||
```apache
|
||||
# In .htaccess hinzufügen
|
||||
RewriteEngine On
|
||||
RewriteCond %{HTTPS} off
|
||||
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
|
||||
```
|
||||
|
||||
### Regelmäßige Updates
|
||||
- PHP und MySQL aktuell halten
|
||||
- Sicherheitspatches zeitnah einspielen
|
||||
- Passwörter regelmäßig ändern
|
||||
|
||||
## 📚 Verwendung
|
||||
|
||||
### Für Endbenutzer
|
||||
|
||||
**Voucher erstellen:**
|
||||
1. Startseite öffnen (Login optional je nach Konfiguration)
|
||||
2. Voucher-Name eingeben
|
||||
3. Anzahl Geräte wählen
|
||||
4. Standort auswählen
|
||||
5. Code erstellen und notieren
|
||||
|
||||
**Code verwenden:**
|
||||
1. Mit dem WLAN verbinden
|
||||
2. Browser öffnet automatisch Anmeldeseite
|
||||
3. Voucher-Code eingeben
|
||||
4. Zugang für 8 Stunden
|
||||
|
||||
### Für Administratoren
|
||||
|
||||
**Sites verwalten:**
|
||||
- Neue Standorte hinzufügen
|
||||
- Verbindungen testen
|
||||
- Sites deaktivieren
|
||||
- Zugangsdaten aktualisieren
|
||||
|
||||
**Benutzer verwalten:**
|
||||
- Neue Benutzer anlegen
|
||||
- Berechtigungen zuweisen
|
||||
- Sites-Zugriff konfigurieren
|
||||
- Admin-Rechte vergeben
|
||||
|
||||
**Historie einsehen:**
|
||||
- Alle erstellten Vouchers
|
||||
- Filterfunktionen nach Site/Benutzer/Datum
|
||||
- Export-Funktion (optional)
|
||||
|
||||
## 🐛 Problembehandlung
|
||||
|
||||
### Häufige Probleme
|
||||
## Problembehandlung
|
||||
|
||||
**Login funktioniert nicht:**
|
||||
- Prüfen Sie die Datenbankverbindung
|
||||
- Stellen Sie sicher, dass Sessions funktionieren
|
||||
- Überprüfen Sie die PHP-Session-Konfiguration
|
||||
- Datenbankverbindung und PHP-Session-Konfiguration prüfen
|
||||
|
||||
**UniFi-Verbindung schlägt fehl:**
|
||||
- Testen Sie die Controller-URL im Browser
|
||||
- Prüfen Sie Benutzername und Passwort
|
||||
- Stellen Sie sicher, dass cURL aktiviert ist
|
||||
- Prüfen Sie SSL-Zertifikate (CURLOPT_SSL_VERIFYPEER)
|
||||
- Controller-URL im Browser testen
|
||||
- Port 11443 für UniFi OS verwenden (nicht 8443)
|
||||
- Benutzername, Passwort und Site ID prüfen
|
||||
- cURL-Extension muss aktiviert sein
|
||||
|
||||
**UniFi OS: Verbindung schlägt fehl (HTTP 404 oder 401):**
|
||||
- Stellen Sie sicher, dass Sie Port 11443 verwenden (nicht 8443)
|
||||
- UniFi OS erfordert den Pfad `/proxy/network/api/s/{site}/...` für alle API-Aufrufe
|
||||
- Der Login-Endpunkt lautet `/api/auth/login` (nicht `/api/login`)
|
||||
- Ältere UniFi Network Controller ohne UniFi OS werden ab Version 2.1.0 nicht mehr unterstützt
|
||||
- Bei anhaltenden 401-Fehlern: Prüfen Sie, ob der UniFi-Account lokale API-Rechte besitzt
|
||||
**UniFi OS: HTTP 404 oder 401:**
|
||||
- Login-Endpunkt ist `/api/auth/login` (nicht `/api/login`)
|
||||
- API-Pfade benötigen Präfix `/proxy/network/api/s/{site}/...`
|
||||
- Älterer UniFi Network Controller (ohne UniFi OS) wird ab Version 2.1.0 nicht mehr unterstützt
|
||||
|
||||
**Voucher werden nicht erstellt:**
|
||||
- Überprüfen Sie die UniFi Controller Logs
|
||||
- Prüfen Sie API-Berechtigungen
|
||||
- Stellen Sie sicher, dass die Site-ID korrekt ist
|
||||
- UniFi Controller Logs prüfen
|
||||
- API-Berechtigungen des Admin-Accounts prüfen
|
||||
- Site ID korrekt? (zu finden in der UniFi Controller URL)
|
||||
|
||||
**Microsoft 365 Login funktioniert nicht:**
|
||||
- Prüfen Sie die Redirect URI
|
||||
- Überprüfen Sie Client ID und Secret
|
||||
- Stellen Sie sicher, dass API-Berechtigungen erteilt wurden
|
||||
- Redirect URI in Azure AD prüfen
|
||||
- Client ID, Secret und Tenant ID kontrollieren
|
||||
|
||||
### Debugging aktivieren
|
||||
## API-Dokumentation
|
||||
|
||||
In `config.php` hinzufügen:
|
||||
```php
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', 1);
|
||||
ini_set('log_errors', 1);
|
||||
ini_set('error_log', '/pfad/zu/error.log');
|
||||
```
|
||||
|
||||
## 🔄 Update/Migration
|
||||
|
||||
### Von der alten Version migrieren
|
||||
Das System ist eine komplette Neuentwicklung. Migration erfordert:
|
||||
|
||||
1. **Daten-Export** aus dem alten System (falls vorhanden)
|
||||
2. **Neue Installation** gemäß dieser Anleitung durchführen
|
||||
3. **Sites manuell neu anlegen**
|
||||
4. **Benutzer neu erstellen**
|
||||
|
||||
### Updates einspielen
|
||||
```bash
|
||||
# Backup erstellen
|
||||
mysqldump -u username -p database_name > backup.sql
|
||||
cp -r /var/www/html/voucher /backup/voucher-$(date +%Y%m%d)
|
||||
|
||||
# Neue Dateien hochladen (config.php nicht überschreiben!)
|
||||
# Datenbank-Updates ausführen falls vorhanden
|
||||
```
|
||||
|
||||
## 📝 API-Dokumentation
|
||||
|
||||
### UniFi OS API Endpoints
|
||||
|
||||
> **Hinweis:** Ab Version 2.1.0 verwendet dieses Tool die UniFi OS API (Port 11443).
|
||||
> Ältere Installationen mit dem klassischen UniFi Network Controller (Port 8443) müssen
|
||||
> auf UniFi OS migrieren oder weiterhin Version 2.0.x verwenden.
|
||||
|
||||
**Login:**
|
||||
**Login (UniFi OS):**
|
||||
```
|
||||
POST /api/auth/login
|
||||
Body: {"username": "admin", "password": "password"}
|
||||
Response-Header: X-CSRF-Token: <token>
|
||||
```
|
||||
|
||||
> Der `X-CSRF-Token`-Wert aus dem Login-Response-Header wird automatisch extrahiert und
|
||||
> bei allen nachfolgenden POST-Anfragen als `X-CSRF-Token`-Header mitgesendet.
|
||||
|
||||
**Voucher erstellen:**
|
||||
```
|
||||
POST /proxy/network/api/s/{site_id}/cmd/hotspot
|
||||
Headers: X-CSRF-Token: <token>
|
||||
Body: {
|
||||
"cmd": "create-voucher",
|
||||
"expire": 480,
|
||||
"n": 1,
|
||||
"note": "Voucher Name",
|
||||
"quota": 1
|
||||
}
|
||||
X-CSRF-Token: <token>
|
||||
Body: {"cmd": "create-voucher", "expire": 480, "n": 1, "note": "Name", "quota": 1}
|
||||
```
|
||||
|
||||
**Vouchers abrufen:**
|
||||
|
|
@ -377,53 +145,18 @@ GET /proxy/network/api/s/{site_id}/stat/voucher
|
|||
**Voucher löschen:**
|
||||
```
|
||||
POST /proxy/network/api/s/{site_id}/cmd/hotspot
|
||||
Headers: X-CSRF-Token: <token>
|
||||
X-CSRF-Token: <token>
|
||||
Body: {"cmd": "delete-voucher", "_id": "<voucher_id>"}
|
||||
```
|
||||
|
||||
## 🤝 Mitwirken
|
||||
## Roadmap
|
||||
|
||||
Contributions sind willkommen! Bitte:
|
||||
|
||||
1. Forken Sie das Repository
|
||||
2. Erstellen Sie einen Feature-Branch (`git checkout -b feature/AmazingFeature`)
|
||||
3. Committen Sie Ihre Änderungen (`git commit -m 'Add some AmazingFeature'`)
|
||||
4. Pushen Sie den Branch (`git push origin feature/AmazingFeature`)
|
||||
5. Öffnen Sie einen Pull Request
|
||||
|
||||
## 📄 Lizenz
|
||||
|
||||
Dieses Projekt steht unter der MIT-Lizenz. Siehe `LICENSE` Datei für Details.
|
||||
|
||||
## 👨💻 Autor
|
||||
|
||||
**Friederich Loheide**
|
||||
|
||||
## 🙏 Danksagungen
|
||||
|
||||
- UniFi Controller API Dokumentation
|
||||
- Microsoft Graph API
|
||||
- Bootstrap und FontAwesome Icons
|
||||
|
||||
## 📞 Support
|
||||
|
||||
Bei Fragen oder Problemen:
|
||||
- Erstellen Sie ein Issue auf GitHub
|
||||
- E-Mail an support@example.com
|
||||
|
||||
## 🗺️ Roadmap
|
||||
|
||||
Geplante Features:
|
||||
- [ ] Voucher-Templates
|
||||
- [ ] Voucher-Templates (vordefinierte Laufzeiten)
|
||||
- [ ] Bulk-Voucher-Erstellung
|
||||
- [ ] QR-Code-Generierung
|
||||
- [ ] SMS-Versand von Codes
|
||||
- [ ] Erweiterte Reporting-Funktionen
|
||||
- [ ] REST API für externe Integration
|
||||
- [ ] Docker-Container
|
||||
- [ ] Mehrsprachigkeit
|
||||
|
||||
---
|
||||
|
||||
**Version:** 2.1.0
|
||||
**Letztes Update:** April 2026
|
||||
**Version:** 2.1.0 | **Autor:** Friederich Loheide | **Letztes Update:** April 2026
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ ini_set('display_errors', 1);
|
|||
require_once __DIR__ . '/../config.php';
|
||||
require_once __DIR__ . '/../includes/Database.php';
|
||||
require_once __DIR__ . '/../includes/Auth.php';
|
||||
require_once __DIR__ . '/../includes/Mailer.php';
|
||||
|
||||
$auth = new Auth();
|
||||
$auth->requireAdmin();
|
||||
|
|
@ -12,6 +13,27 @@ $auth->requireAdmin();
|
|||
$db = Database::getInstance();
|
||||
$appTitle = $db->getSetting('app_title', 'UniFi Voucher System');
|
||||
|
||||
// AJAX: SMTP-Test-E-Mail senden
|
||||
if (isset($_POST['ajax_smtp_test'])) {
|
||||
header('Content-Type: application/json');
|
||||
if (!$auth->validateCsrfToken($_POST['csrf_token'] ?? '')) {
|
||||
echo json_encode(['success' => false, 'message' => 'Ungültiges Token']);
|
||||
exit;
|
||||
}
|
||||
$to = trim($_POST['test_email'] ?? '');
|
||||
if (!filter_var($to, FILTER_VALIDATE_EMAIL)) {
|
||||
echo json_encode(['success' => false, 'message' => 'Ungültige E-Mail-Adresse']);
|
||||
exit;
|
||||
}
|
||||
$mailer = new Mailer();
|
||||
$ok = $mailer->sendTestEmail($to);
|
||||
echo json_encode([
|
||||
'success' => $ok,
|
||||
'message' => $ok ? "Test-E-Mail wurde an {$to} gesendet." : 'Versand fehlgeschlagen. Prüfen Sie die SMTP-Einstellungen und den PHP-Fehlerlog.'
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$error = '';
|
||||
$success = '';
|
||||
|
||||
|
|
@ -542,6 +564,20 @@ $faviconUrl = $db->getSetting('favicon_url', '');
|
|||
</div>
|
||||
<button type="submit" name="save_settings" class="btn btn-primary"><i class="fas fa-save"></i> Speichern</button>
|
||||
</form>
|
||||
|
||||
<div style="margin-top: 25px; padding-top: 25px; border-top: 1px solid #e0e0e0;">
|
||||
<h3 style="margin-bottom: 15px;">SMTP testen</h3>
|
||||
<div style="display: flex; gap: 10px; align-items: flex-end;">
|
||||
<div style="flex: 1;">
|
||||
<label for="smtpTestEmail">Test-E-Mail senden an</label>
|
||||
<input type="email" id="smtpTestEmail" placeholder="empfaenger@example.com" style="margin-top: 6px;">
|
||||
</div>
|
||||
<button onclick="testSmtp()" class="btn btn-secondary" id="smtpTestBtn">
|
||||
<i class="fas fa-paper-plane"></i> Testen
|
||||
</button>
|
||||
</div>
|
||||
<span id="smtpTestResult" style="display: block; margin-top: 10px; font-size: 13px;"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TEIL 1 ENDET HIER - Fortsetzung in TEIL 2 -->
|
||||
|
|
@ -749,6 +785,32 @@ $faviconUrl = $db->getSetting('favicon_url', '');
|
|||
});
|
||||
}
|
||||
|
||||
// SMTP testen
|
||||
async function testSmtp() {
|
||||
const email = document.getElementById('smtpTestEmail').value.trim();
|
||||
const btn = document.getElementById('smtpTestBtn');
|
||||
const result = document.getElementById('smtpTestResult');
|
||||
if (!email) { result.textContent = 'Bitte eine E-Mail-Adresse eingeben.'; result.style.color = '#c33'; return; }
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Sende...';
|
||||
result.textContent = '';
|
||||
const fd = new FormData();
|
||||
fd.append('ajax_smtp_test', '1');
|
||||
fd.append('csrf_token', '<?= $auth->getCsrfToken() ?>');
|
||||
fd.append('test_email', email);
|
||||
try {
|
||||
const res = await fetch('settings.php', { method: 'POST', body: fd });
|
||||
const data = await res.json();
|
||||
result.textContent = data.message;
|
||||
result.style.color = data.success ? '#3c3' : '#c33';
|
||||
} catch(e) {
|
||||
result.textContent = 'Fehler beim Senden.';
|
||||
result.style.color = '#c33';
|
||||
}
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<i class="fas fa-paper-plane"></i> Testen';
|
||||
}
|
||||
|
||||
// Cron-Job testen
|
||||
async function testCronJob() {
|
||||
const btn = document.getElementById('testCronBtn');
|
||||
|
|
|
|||
|
|
@ -654,6 +654,22 @@ $currentUser = $auth->getCurrentUser();
|
|||
document.getElementById('editSiteModal').classList.add('active');
|
||||
}
|
||||
|
||||
// Loading-State bei Formular-Absenden (verhindert Freeze-Eindruck)
|
||||
document.getElementById('addSiteForm').addEventListener('submit', function() {
|
||||
const btn = this.querySelector('button[name="add_site"]');
|
||||
if (btn) {
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Verbindung wird getestet...';
|
||||
}
|
||||
});
|
||||
document.getElementById('editSiteForm').addEventListener('submit', function() {
|
||||
const btn = this.querySelector('button[name="edit_site"]');
|
||||
if (btn) {
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Verbindung wird getestet...';
|
||||
}
|
||||
});
|
||||
|
||||
// Modal schließen bei Klick außerhalb
|
||||
document.getElementById('addSiteModal').addEventListener('click', function(e) {
|
||||
if (e.target === this) {
|
||||
|
|
|
|||
|
|
@ -13,6 +13,41 @@ $auth->requireAdmin();
|
|||
$db = Database::getInstance();
|
||||
$appTitle = $db->getSetting('app_title', 'UniFi Voucher System');
|
||||
|
||||
// CSV-Export
|
||||
if (isset($_GET['export_csv']) && isset($_GET['site_id'])) {
|
||||
if (!$auth->validateCsrfToken($_GET['token'] ?? '')) {
|
||||
http_response_code(403);
|
||||
exit('Ungültiges Token');
|
||||
}
|
||||
$siteId = (int)$_GET['site_id'];
|
||||
$site = $db->fetchOne("SELECT * FROM sites WHERE id = ? AND is_active = 1", [$siteId]);
|
||||
if (!$site) { http_response_code(404); exit('Site nicht gefunden'); }
|
||||
|
||||
$rows = $db->fetchAll(
|
||||
"SELECT voucher_code, voucher_name, max_uses, expire_minutes, status, used_count, created_at, expires_at
|
||||
FROM vouchers WHERE site_id = ? ORDER BY created_at DESC",
|
||||
[$siteId]
|
||||
);
|
||||
|
||||
$filename = 'vouchers_' . preg_replace('/[^a-z0-9]/i', '_', $site['name']) . '_' . date('Ymd_His') . '.csv';
|
||||
header('Content-Type: text/csv; charset=UTF-8');
|
||||
header('Content-Disposition: attachment; filename="' . $filename . '"');
|
||||
header('Cache-Control: no-cache');
|
||||
|
||||
$out = fopen('php://output', 'w');
|
||||
fprintf($out, chr(0xEF).chr(0xBB).chr(0xBF)); // UTF-8 BOM für Excel
|
||||
fputcsv($out, ['Code', 'Name', 'Max. Geräte', 'Gültigkeit (Min)', 'Status', 'Genutzt', 'Erstellt', 'Läuft ab'], ';');
|
||||
foreach ($rows as $r) {
|
||||
fputcsv($out, [
|
||||
$r['voucher_code'], $r['voucher_name'], $r['max_uses'],
|
||||
$r['expire_minutes'], $r['status'], $r['used_count'],
|
||||
$r['created_at'], $r['expires_at'] ?? ''
|
||||
], ';');
|
||||
}
|
||||
fclose($out);
|
||||
exit;
|
||||
}
|
||||
|
||||
// AJAX: Voucher abrufen (immer aus DB, optional vorher Live-Sync)
|
||||
if (isset($_GET['ajax_get_vouchers']) && isset($_GET['site_id'])) {
|
||||
header('Content-Type: application/json');
|
||||
|
|
@ -672,6 +707,9 @@ $faviconUrl = $db->getSetting('favicon_url', '');
|
|||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2 class="card-title" id="voucherListTitle">Vouchers</h2>
|
||||
<a id="csvExportBtn" style="display:none;" class="btn btn-secondary btn-small" href="#">
|
||||
<i class="fas fa-download"></i> CSV exportieren
|
||||
</a>
|
||||
</div>
|
||||
<div class="card-body" style="padding: 0;">
|
||||
<div id="voucherContent">
|
||||
|
|
@ -694,6 +732,8 @@ $faviconUrl = $db->getSetting('favicon_url', '');
|
|||
let currentSiteId = null;
|
||||
let allVouchers = [];
|
||||
let currentFilter = 'all';
|
||||
let currentPage = 1;
|
||||
const PAGE_SIZE = 50;
|
||||
|
||||
// Toast Notification anzeigen
|
||||
function showToast(type, title, message) {
|
||||
|
|
@ -761,11 +801,17 @@ $faviconUrl = $db->getSetting('favicon_url', '');
|
|||
|
||||
if (result.success) {
|
||||
allVouchers = result.vouchers;
|
||||
currentPage = 1;
|
||||
document.getElementById('voucherListTitle').textContent = `Vouchers - ${result.site_name} (${result.count})`;
|
||||
updateStats();
|
||||
renderVouchers();
|
||||
document.getElementById('statsContainer').style.display = 'block';
|
||||
|
||||
// CSV-Button aktualisieren
|
||||
const csvBtn = document.getElementById('csvExportBtn');
|
||||
csvBtn.style.display = 'inline-flex';
|
||||
csvBtn.href = `vouchers.php?export_csv=1&site_id=${siteId}&token=${csrfToken}`;
|
||||
|
||||
// Sync-Info anzeigen
|
||||
if (result.last_sync) {
|
||||
showToast('success', syncFirst ? 'Synchronisiert' : 'Geladen',
|
||||
|
|
@ -810,12 +856,19 @@ $faviconUrl = $db->getSetting('favicon_url', '');
|
|||
// Filter setzen
|
||||
function setFilter(filter) {
|
||||
currentFilter = filter;
|
||||
currentPage = 1;
|
||||
document.querySelectorAll('.filter-btn').forEach(btn => {
|
||||
btn.classList.toggle('active', btn.dataset.filter === filter);
|
||||
});
|
||||
renderVouchers();
|
||||
}
|
||||
|
||||
function setPage(page) {
|
||||
currentPage = page;
|
||||
renderVouchers();
|
||||
document.querySelector('.card:last-of-type')?.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}
|
||||
|
||||
// Vouchers rendern
|
||||
function renderVouchers() {
|
||||
let vouchers = allVouchers;
|
||||
|
|
@ -824,6 +877,11 @@ $faviconUrl = $db->getSetting('favicon_url', '');
|
|||
vouchers = allVouchers.filter(v => v.status === currentFilter);
|
||||
}
|
||||
|
||||
const totalPages = Math.ceil(vouchers.length / PAGE_SIZE);
|
||||
if (currentPage > totalPages && totalPages > 0) currentPage = totalPages;
|
||||
const pageStart = (currentPage - 1) * PAGE_SIZE;
|
||||
const pageVouchers = vouchers.slice(pageStart, pageStart + PAGE_SIZE);
|
||||
|
||||
if (vouchers.length === 0) {
|
||||
document.getElementById('voucherContent').innerHTML = `
|
||||
<div class="empty-state">
|
||||
|
|
@ -867,7 +925,7 @@ $faviconUrl = $db->getSetting('favicon_url', '');
|
|||
<tbody>
|
||||
`;
|
||||
|
||||
vouchers.forEach(voucher => {
|
||||
pageVouchers.forEach(voucher => {
|
||||
const createDate = new Date(voucher.create_time * 1000);
|
||||
const expireDate = new Date(voucher.expire_time * 1000);
|
||||
const now = new Date();
|
||||
|
|
@ -938,11 +996,20 @@ $faviconUrl = $db->getSetting('favicon_url', '');
|
|||
`;
|
||||
});
|
||||
|
||||
html += `
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
`;
|
||||
html += `</tbody></table></div>`;
|
||||
|
||||
// Paginierung
|
||||
if (totalPages > 1) {
|
||||
html += `<div style="display:flex;align-items:center;justify-content:space-between;padding:15px 25px;border-top:1px solid #e0e0e0;">`;
|
||||
html += `<span style="font-size:13px;color:#666;">Seite ${currentPage} von ${totalPages} (${vouchers.length} Einträge)</span>`;
|
||||
html += `<div style="display:flex;gap:6px;">`;
|
||||
html += `<button class="btn btn-secondary btn-small" onclick="setPage(${currentPage - 1})" ${currentPage <= 1 ? 'disabled' : ''}><i class="fas fa-chevron-left"></i></button>`;
|
||||
for (let p = Math.max(1, currentPage - 2); p <= Math.min(totalPages, currentPage + 2); p++) {
|
||||
html += `<button class="btn btn-small ${p === currentPage ? 'btn-primary' : 'btn-secondary'}" onclick="setPage(${p})">${p}</button>`;
|
||||
}
|
||||
html += `<button class="btn btn-secondary btn-small" onclick="setPage(${currentPage + 1})" ${currentPage >= totalPages ? 'disabled' : ''}><i class="fas fa-chevron-right"></i></button>`;
|
||||
html += `</div></div>`;
|
||||
}
|
||||
|
||||
document.getElementById('voucherContent').innerHTML = html;
|
||||
}
|
||||
|
|
|
|||
28
database.sql
28
database.sql
|
|
@ -88,3 +88,31 @@ CREATE TABLE IF NOT EXISTS `sessions` (
|
|||
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE,
|
||||
INDEX `idx_expires` (`expires_at`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `login_attempts` (
|
||||
`id` INT PRIMARY KEY AUTO_INCREMENT,
|
||||
`ip_address` VARCHAR(45) NOT NULL,
|
||||
`email` VARCHAR(255) NOT NULL,
|
||||
`attempted_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX `idx_ip` (`ip_address`),
|
||||
INDEX `idx_email` (`email`),
|
||||
INDEX `idx_attempted` (`attempted_at`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `audit_log` (
|
||||
`id` INT PRIMARY KEY AUTO_INCREMENT,
|
||||
`user_id` INT,
|
||||
`action` VARCHAR(100) NOT NULL,
|
||||
`entity_type` VARCHAR(50),
|
||||
`entity_id` VARCHAR(100),
|
||||
`details` TEXT,
|
||||
`ip_address` VARCHAR(45),
|
||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE SET NULL,
|
||||
INDEX `idx_user` (`user_id`),
|
||||
INDEX `idx_action` (`action`),
|
||||
INDEX `idx_created` (`created_at`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- Migration für bestehende Installationen:
|
||||
-- Neue Tabellen werden automatisch erstellt (CREATE TABLE IF NOT EXISTS)
|
||||
|
|
@ -23,20 +23,63 @@ class Auth {
|
|||
|
||||
// Benutzer einloggen
|
||||
public function login($email, $password) {
|
||||
$ip = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
|
||||
|
||||
if ($this->isRateLimited($ip, $email)) {
|
||||
return 'rate_limited';
|
||||
}
|
||||
|
||||
$user = $this->db->fetchOne(
|
||||
"SELECT * FROM users WHERE email = ? AND is_active = 1",
|
||||
[$email]
|
||||
);
|
||||
|
||||
if ($user && password_verify($password, $user['password_hash'])) {
|
||||
$this->clearLoginAttempts($ip, $email);
|
||||
$this->setUserSession($user);
|
||||
$this->updateLastLogin($user['id']);
|
||||
return true;
|
||||
}
|
||||
|
||||
$this->recordLoginAttempt($ip, $email);
|
||||
return false;
|
||||
}
|
||||
|
||||
private function isRateLimited($ip, $email) {
|
||||
try {
|
||||
$count = $this->db->fetchOne(
|
||||
"SELECT COUNT(*) as cnt FROM login_attempts
|
||||
WHERE (ip_address = ? OR email = ?) AND attempted_at > DATE_SUB(NOW(), INTERVAL 10 MINUTE)",
|
||||
[$ip, $email]
|
||||
);
|
||||
return $count && (int)$count['cnt'] >= 10;
|
||||
} catch (\Exception $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private function recordLoginAttempt($ip, $email) {
|
||||
try {
|
||||
$this->db->query(
|
||||
"INSERT INTO login_attempts (ip_address, email) VALUES (?, ?)",
|
||||
[$ip, $email]
|
||||
);
|
||||
} catch (\Exception $e) {
|
||||
// Tabelle existiert noch nicht – ignorieren
|
||||
}
|
||||
}
|
||||
|
||||
private function clearLoginAttempts($ip, $email) {
|
||||
try {
|
||||
$this->db->query(
|
||||
"DELETE FROM login_attempts WHERE ip_address = ? OR email = ?",
|
||||
[$ip, $email]
|
||||
);
|
||||
} catch (\Exception $e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
// Microsoft 365 Login
|
||||
public function loginWithMicrosoft($microsoftUser) {
|
||||
// Zuerst nach Microsoft ID suchen
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
class Database {
|
||||
private static $instance = null;
|
||||
private $pdo;
|
||||
private $settingsCache = [];
|
||||
|
||||
private function __construct() {
|
||||
try {
|
||||
|
|
@ -58,8 +59,13 @@ class Database {
|
|||
|
||||
// Settings-Helper
|
||||
public function getSetting($key, $default = null) {
|
||||
if (array_key_exists($key, $this->settingsCache)) {
|
||||
return $this->settingsCache[$key] ?? $default;
|
||||
}
|
||||
$result = $this->fetchOne("SELECT setting_value FROM settings WHERE setting_key = ?", [$key]);
|
||||
return $result ? $result['setting_value'] : $default;
|
||||
$value = $result ? $result['setting_value'] : null;
|
||||
$this->settingsCache[$key] = $value;
|
||||
return $value ?? $default;
|
||||
}
|
||||
|
||||
public function setSetting($key, $value) {
|
||||
|
|
@ -68,5 +74,6 @@ class Database {
|
|||
ON DUPLICATE KEY UPDATE setting_value = VALUES(setting_value)",
|
||||
[$key, $value]
|
||||
);
|
||||
$this->settingsCache[$key] = $value;
|
||||
}
|
||||
}
|
||||
|
|
@ -201,6 +201,13 @@ class Mailer {
|
|||
return $this->send($to, $subject, $body, $isHtml);
|
||||
}
|
||||
|
||||
public function sendTestEmail($to) {
|
||||
$appTitle = $this->db->getSetting('app_title', 'UniFi Voucher System');
|
||||
$subject = '[Test] E-Mail-Konfiguration – ' . $appTitle;
|
||||
$body = "Dies ist eine Test-E-Mail von {$appTitle}.\n\nDie SMTP-Konfiguration ist korrekt eingerichtet.";
|
||||
return $this->send($to, $subject, $body, false);
|
||||
}
|
||||
|
||||
public function sendUserNotification($to, $userName, $changes) {
|
||||
$appTitle = $this->db->getSetting('app_title', 'UniFi Voucher System');
|
||||
|
||||
|
|
|
|||
|
|
@ -36,6 +36,8 @@ class UniFiController {
|
|||
CURLOPT_SSL_VERIFYPEER => false,
|
||||
CURLOPT_COOKIEJAR => $this->cookieFile,
|
||||
CURLOPT_COOKIEFILE => $this->cookieFile,
|
||||
CURLOPT_TIMEOUT => 10,
|
||||
CURLOPT_CONNECTTIMEOUT => 5,
|
||||
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
|
||||
CURLOPT_HEADERFUNCTION => function($ch, $header) {
|
||||
$parts = explode(':', $header, 2);
|
||||
|
|
@ -60,8 +62,12 @@ class UniFiController {
|
|||
|
||||
$data = json_decode($response, true);
|
||||
|
||||
if (!isset($data['meta']['rc']) || $data['meta']['rc'] !== 'ok') {
|
||||
throw new Exception("Login fehlgeschlagen: Ungültige Antwort");
|
||||
// UniFi OS gibt ein User-Objekt zurück (unique_id/email), die alte API meta.rc = ok
|
||||
$isUnifiOs = is_array($data) && (isset($data['unique_id']) || isset($data['email']));
|
||||
$isOldApi = isset($data['meta']['rc']) && $data['meta']['rc'] === 'ok';
|
||||
|
||||
if (!$isUnifiOs && !$isOldApi) {
|
||||
throw new Exception("Login fehlgeschlagen: Ungültige Antwort vom Controller");
|
||||
}
|
||||
|
||||
return true;
|
||||
|
|
@ -79,6 +85,8 @@ class UniFiController {
|
|||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_SSL_VERIFYPEER => false,
|
||||
CURLOPT_COOKIEFILE => $this->cookieFile,
|
||||
CURLOPT_TIMEOUT => 10,
|
||||
CURLOPT_CONNECTTIMEOUT => 5,
|
||||
CURLOPT_HTTPHEADER => array_filter([
|
||||
'Content-Type: application/json',
|
||||
($method === 'POST' && $this->csrfToken !== null)
|
||||
|
|
|
|||
35
index.php
35
index.php
|
|
@ -173,6 +173,9 @@ $autoSelectSite = (count($sites) === 1) ? $sites[0]['id'] : 0;
|
|||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title><?= htmlspecialchars($appTitle) ?></title>
|
||||
<?php if ($voucherCreated): ?>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js" integrity="sha512-CNgIRecGo7nphbeZ04Sc13ka07paqdeTu0WR1IM4kNcpmBAUSHSe2keRB6Q5pBUtIxCY7bQMsVB0ANBpd6JDg==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
||||
<?php endif; ?>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
|
|
@ -431,6 +434,21 @@ $autoSelectSite = (count($sites) === 1) ? $sites[0]['id'] : 0;
|
|||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
.qr-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin: 20px 0 0;
|
||||
}
|
||||
.qr-wrapper canvas, .qr-wrapper img {
|
||||
border: 6px solid white;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.qr-label {
|
||||
font-size: 12px;
|
||||
opacity: 0.8;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
/* Print Styles */
|
||||
@media print {
|
||||
|
|
@ -492,6 +510,10 @@ $autoSelectSite = (count($sites) === 1) ? $sites[0]['id'] : 0;
|
|||
<div class="voucher-info">
|
||||
Der Code ist 8 Stunden ab Erstellung gültig
|
||||
</div>
|
||||
<div class="qr-wrapper no-print">
|
||||
<div id="qrcode"></div>
|
||||
<div class="qr-label">QR-Code scannen zum Verbinden</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if ($instructionHeader || $instructionText): ?>
|
||||
|
|
@ -603,6 +625,19 @@ $autoSelectSite = (count($sites) === 1) ? $sites[0]['id'] : 0;
|
|||
</div>
|
||||
|
||||
<script>
|
||||
<?php if ($voucherCreated): ?>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
new QRCode(document.getElementById('qrcode'), {
|
||||
text: '<?= addslashes($voucherCode) ?>',
|
||||
width: 160,
|
||||
height: 160,
|
||||
colorDark: '#ffffff',
|
||||
colorLight: 'transparent',
|
||||
correctLevel: QRCode.CorrectLevel.M
|
||||
});
|
||||
});
|
||||
<?php endif; ?>
|
||||
|
||||
function toggleEmailField() {
|
||||
const checkbox = document.getElementById('send_email');
|
||||
const emailField = document.getElementById('email_field');
|
||||
|
|
|
|||
|
|
@ -31,13 +31,17 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||
|
||||
if (empty($email) || empty($password)) {
|
||||
$error = 'Bitte E-Mail und Passwort eingeben';
|
||||
} elseif ($auth->login($email, $password)) {
|
||||
// Nach erfolgreichem Login zu index.php
|
||||
} else {
|
||||
$result = $auth->login($email, $password);
|
||||
if ($result === true) {
|
||||
header('Location: index.php');
|
||||
exit;
|
||||
} elseif ($result === 'rate_limited') {
|
||||
$error = 'Zu viele Fehlversuche. Bitte warten Sie 10 Minuten.';
|
||||
} else {
|
||||
$error = 'Ungültige E-Mail oder Passwort';
|
||||
}
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$error = 'Login-Fehler: ' . $e->getMessage();
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue