From dbdc237fa1c44f2d6e3d624b5abcd5ce09463d38 Mon Sep 17 00:00:00 2001
From: friloo <49183588+friloo@users.noreply.github.com>
Date: Tue, 21 Apr 2026 17:45:58 +0200
Subject: [PATCH] Initial Upload
---
Readme.md | 406 ++++++++++++++
admin/index.php | 1027 ++++++++++++++++++++++++++++++++++
admin/settings.php | 799 ++++++++++++++++++++++++++
admin/sites.php | 671 ++++++++++++++++++++++
admin/users.php | 767 +++++++++++++++++++++++++
admin/vouchers.php | 1004 +++++++++++++++++++++++++++++++++
config.php | 13 +
cron_sync.php | 230 ++++++++
cron_test.php | 72 +++
database.sql | 90 +++
includes/Auth.php | 201 +++++++
includes/Database.php | 72 +++
includes/Mailer.php | 243 ++++++++
includes/UniFiController.php | 329 +++++++++++
index.php | 654 ++++++++++++++++++++++
install.php | 461 +++++++++++++++
login.php | 329 +++++++++++
login_simple.php | 328 +++++++++++
logout.php | 10 +
m365_callback.php | 118 ++++
m365_debug.php | 81 +++
test.php | 90 +++
22 files changed, 7995 insertions(+)
create mode 100644 Readme.md
create mode 100644 admin/index.php
create mode 100644 admin/settings.php
create mode 100644 admin/sites.php
create mode 100644 admin/users.php
create mode 100644 admin/vouchers.php
create mode 100644 config.php
create mode 100644 cron_sync.php
create mode 100644 cron_test.php
create mode 100644 database.sql
create mode 100644 includes/Auth.php
create mode 100644 includes/Database.php
create mode 100644 includes/Mailer.php
create mode 100644 includes/UniFiController.php
create mode 100644 index.php
create mode 100644 install.php
create mode 100644 login.php
create mode 100644 login_simple.php
create mode 100644 logout.php
create mode 100644 m365_callback.php
create mode 100644 m365_debug.php
create mode 100644 test.php
diff --git a/Readme.md b/Readme.md
new file mode 100644
index 0000000..a1fc0d5
--- /dev/null
+++ b/Readme.md
@@ -0,0 +1,406 @@
+# 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.
+
+## ✨ 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
+
+### 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
+
+### 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
+
+### UniFi Controller
+- UniFi Network Controller 6.0 oder höher
+- 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/ihr-username/unifi-voucher-system.git
+cd unifi-voucher-system
+
+# Dateien auf den Webserver hochladen
+# Stellen Sie sicher, dass der Webserver-User Schreibrechte hat
+```
+
+### Schritt 2: Ordnerstruktur
+
+```
+/
+├── 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
+```
+
+### Schritt 3: Installation durchführen
+
+1. Öffnen Sie `http://ihre-domain.de/install.php` im Browser
+2. Folgen Sie dem 5-Schritte-Installations-Assistenten:
+
+#### Schritt 1: Datenbank-Konfiguration
+- Datenbank-Host (meist `localhost`)
+- Datenbankname (z.B. `unifi_voucher`)
+- Datenbank-Benutzer
+- Datenbank-Passwort
+
+#### 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:8443")
+ - **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
+
+### config.php
+Die Datei wird automatisch erstellt, kann aber manuell angepasst werden:
+
+```php
+
+ Order Allow,Deny
+ Deny from all
+
+
+
+ Order Allow,Deny
+ Deny from all
+
+```
+
+### Datenbank-Benutzer
+Erstellen Sie einen dedizierten Datenbankbenutzer nur für diese Anwendung:
+
+```sql
+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
+
+**Login funktioniert nicht:**
+- Prüfen Sie die Datenbankverbindung
+- Stellen Sie sicher, dass Sessions funktionieren
+- Überprüfen Sie die PHP-Session-Konfiguration
+
+**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)
+
+**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
+
+**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
+
+### Debugging aktivieren
+
+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 Controller API Endpoints
+
+**Login:**
+```
+POST /api/login
+Body: {"username": "admin", "password": "password"}
+```
+
+**Voucher erstellen:**
+```
+POST /api/s/{site_id}/cmd/hotspot
+Body: {
+ "cmd": "create-voucher",
+ "expire": 480,
+ "n": 1,
+ "note": "Voucher Name",
+ "quota": 1
+}
+```
+
+**Vouchers abrufen:**
+```
+GET /api/s/{site_id}/stat/voucher
+```
+
+## 🤝 Mitwirken
+
+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
+- [ ] Bulk-Voucher-Erstellung
+- [ ] QR-Code-Generierung
+- [ ] SMS-Versand von Codes
+- [ ] Erweiterte Reporting-Funktionen
+- [ ] REST API für externe Integration
+- [ ] Docker-Container
+- [ ] Mehrsprachigkeit
+
+---
+
+**Version:** 2.0.0
+**Letztes Update:** Januar 2026
\ No newline at end of file
diff --git a/admin/index.php b/admin/index.php
new file mode 100644
index 0000000..d167e3c
--- /dev/null
+++ b/admin/index.php
@@ -0,0 +1,1027 @@
+requireAdmin();
+
+$db = Database::getInstance();
+$appTitle = $db->getSetting('app_title', 'UniFi Voucher System');
+
+// AJAX: Statistiken abrufen (immer aus DB, optional vorher Live-Sync)
+if (isset($_GET['ajax_stats'])) {
+ header('Content-Type: application/json');
+
+ $syncFirst = isset($_GET['sync']) && $_GET['sync'] == '1';
+
+ try {
+ $sites = $db->fetchAll("SELECT * FROM sites WHERE is_active = 1");
+ $siteData = [];
+ $totalStats = [
+ 'total' => 0,
+ 'valid' => 0,
+ 'used' => 0,
+ 'expired' => 0
+ ];
+ $syncErrors = [];
+
+ // Bei sync=1: Erst alle Sites live synchronisieren
+ if ($syncFirst) {
+ foreach ($sites as $site) {
+ try {
+ $controller = new UniFiController(
+ $site['unifi_controller_url'],
+ $site['unifi_username'],
+ $site['unifi_password'],
+ $site['site_id']
+ );
+ $controller->syncVouchersToDatabase($db, $site['id']);
+ } catch (Exception $e) {
+ $syncErrors[$site['id']] = $e->getMessage();
+ }
+ }
+
+ // Last sync time aktualisieren
+ $db->execute(
+ "INSERT INTO settings (setting_key, setting_value) VALUES ('last_cron_sync', NOW())
+ ON DUPLICATE KEY UPDATE setting_value = NOW()"
+ );
+ }
+
+ // Immer aus Datenbank abrufen
+ foreach ($sites as $site) {
+ $siteStats = $db->fetchOne(
+ "SELECT
+ COUNT(*) as total,
+ SUM(CASE WHEN status = 'valid' THEN 1 ELSE 0 END) as valid,
+ SUM(CASE WHEN status = 'used' THEN 1 ELSE 0 END) as used,
+ SUM(CASE WHEN status = 'expired' THEN 1 ELSE 0 END) as expired
+ FROM vouchers WHERE site_id = ?",
+ [$site['id']]
+ );
+
+ $siteData[] = [
+ 'site_id' => $site['id'],
+ 'site_name' => $site['name'],
+ 'stats' => [
+ 'total' => (int)($siteStats['total'] ?? 0),
+ 'valid' => (int)($siteStats['valid'] ?? 0),
+ 'used' => (int)($siteStats['used'] ?? 0),
+ 'expired' => (int)($siteStats['expired'] ?? 0)
+ ],
+ 'error' => $syncErrors[$site['id']] ?? null
+ ];
+
+ $totalStats['total'] += (int)($siteStats['total'] ?? 0);
+ $totalStats['valid'] += (int)($siteStats['valid'] ?? 0);
+ $totalStats['used'] += (int)($siteStats['used'] ?? 0);
+ $totalStats['expired'] += (int)($siteStats['expired'] ?? 0);
+ }
+
+ $lastSync = $db->getSetting('last_cron_sync', '');
+
+ echo json_encode([
+ 'success' => true,
+ 'synced' => $syncFirst,
+ 'sites' => $siteData,
+ 'total' => $totalStats,
+ 'last_sync' => $lastSync ? date('d.m.Y H:i:s', strtotime($lastSync)) : null,
+ 'timestamp' => date('H:i:s')
+ ]);
+ } catch (Exception $e) {
+ echo json_encode(['success' => false, 'message' => $e->getMessage()]);
+ }
+ exit;
+}
+
+// Basis-Statistiken (aus Datenbank für initiale Anzeige)
+$stats = [
+ 'total_sites' => $db->fetchOne("SELECT COUNT(*) as count FROM sites WHERE is_active = 1")['count'],
+ 'total_users' => $db->fetchOne("SELECT COUNT(*) as count FROM users WHERE is_active = 1")['count'],
+ 'total_vouchers_today' => $db->fetchOne("SELECT COUNT(*) as count FROM vouchers WHERE DATE(created_at) = CURDATE()")['count'],
+ 'total_vouchers_month' => $db->fetchOne("SELECT COUNT(*) as count FROM vouchers WHERE MONTH(created_at) = MONTH(CURDATE()) AND YEAR(created_at) = YEAR(CURDATE())")['count'],
+];
+
+// Sites für Live-Anzeige mit gecachten Statistiken
+$sites = $db->fetchAll("SELECT * FROM sites WHERE is_active = 1");
+
+// Voucher-Statistiken aus DB (gecached durch Cron)
+$voucherStats = $db->fetchOne(
+ "SELECT
+ COUNT(*) as total,
+ SUM(CASE WHEN status = 'valid' THEN 1 ELSE 0 END) as valid,
+ SUM(CASE WHEN status = 'used' THEN 1 ELSE 0 END) as used,
+ SUM(CASE WHEN status = 'expired' THEN 1 ELSE 0 END) as expired
+ FROM vouchers"
+);
+
+// Pro-Site-Statistiken aus DB
+$siteStats = [];
+foreach ($sites as $site) {
+ $siteStat = $db->fetchOne(
+ "SELECT
+ COUNT(*) as total,
+ SUM(CASE WHEN status = 'valid' THEN 1 ELSE 0 END) as valid,
+ SUM(CASE WHEN status = 'used' THEN 1 ELSE 0 END) as used,
+ SUM(CASE WHEN status = 'expired' THEN 1 ELSE 0 END) as expired
+ FROM vouchers WHERE site_id = ?",
+ [$site['id']]
+ );
+ $siteStats[$site['id']] = $siteStat;
+}
+
+// Letzte Synchronisation
+$lastCronSync = $db->getSetting('last_cron_sync', '');
+
+// Vouchers für Diagramm (letzte 7 Tage)
+$chartData = [];
+for ($i = 6; $i >= 0; $i--) {
+ $date = date('Y-m-d', strtotime("-$i days"));
+ $count = $db->fetchOne(
+ "SELECT COUNT(*) as count FROM vouchers WHERE DATE(created_at) = ?",
+ [$date]
+ )['count'];
+
+ $chartData[] = [
+ 'date' => date('d.m', strtotime($date)),
+ 'count' => $count
+ ];
+}
+
+// Top 5 Benutzer (meiste Vouchers)
+$topUsers = $db->fetchAll(
+ "SELECT u.name, u.email, COUNT(v.id) as voucher_count
+ FROM users u
+ LEFT JOIN vouchers v ON u.id = v.user_id
+ WHERE v.created_at >= DATE_SUB(NOW(), INTERVAL 30 DAY)
+ GROUP BY u.id
+ ORDER BY voucher_count DESC
+ LIMIT 5"
+);
+
+// Letzte Vouchers (aus DB)
+$recentVouchers = $db->fetchAll(
+ "SELECT v.*, s.name as site_name, u.name as user_name
+ FROM vouchers v
+ LEFT JOIN sites s ON v.site_id = s.id
+ LEFT JOIN users u ON v.user_id = u.id
+ ORDER BY v.created_at DESC
+ LIMIT 10"
+);
+
+$currentUser = $auth->getCurrentUser();
+?>
+
+
+
+
+
+ Administration - = htmlspecialchars($appTitle) ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
= $stats['total_sites'] ?>
+
+
+
+
+
= $stats['total_users'] ?>
+
+
+
+
+
= (int)($voucherStats['valid'] ?? 0) ?>
+
Letzte Sync: = $lastCronSync ? date('H:i', strtotime($lastCronSync)) : 'nie' ?>
+
+
+
+
+
= (int)($voucherStats['used'] ?? 0) ?>
+
Quota ausgeschöpft
+
+
+
+
+
= (int)($voucherStats['expired'] ?? 0) ?>
+
Im Controller
+
+
+
+
+
= (int)($voucherStats['total'] ?? 0) ?>
+
Alle Sites
+
+
+
+
+
+
+
+
+ 0, 'valid' => 0, 'used' => 0, 'expired' => 0];
+ ?>
+
+
+
+
+
= (int)$ss['valid'] ?>
+
Gültig
+
+
+
= (int)$ss['used'] ?>
+
Verwendet
+
+
+
= (int)$ss['expired'] ?>
+
Abgelaufen
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Noch keine Daten verfügbar
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Noch keine Vouchers erstellt
+
+
+
+
+
+ Erstellt
+ Code
+ Name
+ Site
+ Status
+ Ersteller
+
+
+
+
+
+ = date('d.m.Y H:i', strtotime($voucher['created_at'])) ?>
+ = htmlspecialchars($voucher['voucher_code']) ?>
+ = htmlspecialchars($voucher['voucher_name']) ?>
+ = htmlspecialchars($voucher['site_name']) ?>
+
+
+ = $statusText ?>
+
+ = $voucher['user_name'] ? htmlspecialchars($voucher['user_name']) : 'Öffentlich ' ?>
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/admin/settings.php b/admin/settings.php
new file mode 100644
index 0000000..c95027d
--- /dev/null
+++ b/admin/settings.php
@@ -0,0 +1,799 @@
+requireAdmin();
+
+$db = Database::getInstance();
+$appTitle = $db->getSetting('app_title', 'UniFi Voucher System');
+
+$error = '';
+$success = '';
+
+// Einstellungen speichern
+if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['save_settings'])) {
+ if (!$auth->validateCsrfToken($_POST['csrf_token'] ?? '')) {
+ $error = 'Ungültiges Sicherheits-Token';
+ } else {
+ try {
+ $settings = [];
+ $formType = $_POST['form_type'] ?? '';
+
+ // Allgemeine Einstellungen
+ if ($formType === 'general') {
+ $settings['app_title'] = trim($_POST['app_title'] ?? '');
+ $settings['logo_url'] = trim($_POST['logo_url'] ?? '');
+ $settings['favicon_url'] = trim($_POST['favicon_url'] ?? '');
+ $settings['instruction_header'] = trim($_POST['instruction_header'] ?? '');
+ $settings['instruction_text'] = $_POST['instruction_text'] ?? '';
+ $settings['public_access'] = isset($_POST['public_access']) ? '1' : '0';
+ }
+
+ // M365 Einstellungen
+ if ($formType === 'm365') {
+ $settings['m365_client_id'] = trim($_POST['m365_client_id'] ?? '');
+ $settings['m365_client_secret'] = trim($_POST['m365_client_secret'] ?? '');
+ $settings['m365_tenant_id'] = trim($_POST['m365_tenant_id'] ?? '');
+ }
+
+ // SMTP Einstellungen
+ if ($formType === 'smtp') {
+ $settings['smtp_enabled'] = isset($_POST['smtp_enabled']) ? '1' : '0';
+ $settings['smtp_host'] = trim($_POST['smtp_host'] ?? '');
+ $settings['smtp_port'] = trim($_POST['smtp_port'] ?? '587');
+ $settings['smtp_username'] = trim($_POST['smtp_username'] ?? '');
+ if (!empty($_POST['smtp_password'])) {
+ $settings['smtp_password'] = trim($_POST['smtp_password']);
+ }
+ $settings['smtp_encryption'] = trim($_POST['smtp_encryption'] ?? 'tls');
+ $settings['smtp_from_email'] = trim($_POST['smtp_from_email'] ?? '');
+ $settings['smtp_from_name'] = trim($_POST['smtp_from_name'] ?? '');
+ }
+
+ // E-Mail Templates
+ if ($formType === 'templates') {
+ $settings['email_voucher_subject'] = trim($_POST['email_voucher_subject'] ?? '');
+ $settings['email_voucher_body'] = $_POST['email_voucher_body'] ?? '';
+ $settings['email_user_notification_subject'] = trim($_POST['email_user_notification_subject'] ?? '');
+ $settings['email_user_notification_body'] = $_POST['email_user_notification_body'] ?? '';
+ $settings['system_url'] = trim($_POST['system_url'] ?? '');
+ }
+
+ // System Einstellungen
+ if ($formType === 'system') {
+ $settings['tinymce_api_key'] = trim($_POST['tinymce_api_key'] ?? '');
+ $settings['print_template'] = $_POST['print_template'] ?? '';
+ }
+
+ foreach ($settings as $key => $value) {
+ $db->setSetting($key, $value);
+ }
+
+ $success = 'Einstellungen erfolgreich gespeichert!';
+
+ } catch (Exception $e) {
+ $error = 'Fehler: ' . $e->getMessage();
+ }
+ }
+}
+
+// Cron-Token generieren
+if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['generate_cron_token'])) {
+ if (!$auth->validateCsrfToken($_POST['csrf_token'] ?? '')) {
+ $error = 'Ungültiges Sicherheits-Token';
+ } else {
+ try {
+ // Sicheren Token generieren
+ $newToken = bin2hex(random_bytes(32));
+ $db->setSetting('cron_token', $newToken);
+ $success = 'Neuer Cron-Token wurde generiert!';
+ } catch (Exception $e) {
+ $error = 'Fehler: ' . $e->getMessage();
+ }
+ }
+}
+
+// Cron-Token löschen
+if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['delete_cron_token'])) {
+ if (!$auth->validateCsrfToken($_POST['csrf_token'] ?? '')) {
+ $error = 'Ungültiges Sicherheits-Token';
+ } else {
+ try {
+ $db->setSetting('cron_token', '');
+ $success = 'Cron-Token wurde gelöscht!';
+ } catch (Exception $e) {
+ $error = 'Fehler: ' . $e->getMessage();
+ }
+ }
+}
+
+// Passwort ändern
+if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['change_password'])) {
+ if (!$auth->validateCsrfToken($_POST['csrf_token'] ?? '')) {
+ $error = 'Ungültiges Sicherheits-Token';
+ } else {
+ try {
+ $currentPassword = $_POST['current_password'];
+ $newPassword = $_POST['new_password'];
+ $confirmPassword = $_POST['confirm_password'];
+
+ $user = $auth->getCurrentUser();
+
+ if (!password_verify($currentPassword, $user['password_hash'])) {
+ throw new Exception('Aktuelles Passwort ist falsch');
+ }
+
+ if (strlen($newPassword) < 8) {
+ throw new Exception('Neues Passwort muss mindestens 8 Zeichen lang sein');
+ }
+
+ if ($newPassword !== $confirmPassword) {
+ throw new Exception('Passwörter stimmen nicht überein');
+ }
+
+ $newHash = password_hash($newPassword, PASSWORD_DEFAULT);
+ $db->query("UPDATE users SET password_hash = ? WHERE id = ?", [$newHash, $user['id']]);
+
+ $success = 'Passwort erfolgreich geändert!';
+
+ } catch (Exception $e) {
+ $error = $e->getMessage();
+ }
+ }
+}
+
+// Aktuelle Einstellungen laden
+$protocol = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http';
+$host = $_SERVER['HTTP_HOST'];
+$scriptPath = dirname($_SERVER['SCRIPT_NAME'], 2);
+$scriptPath = $scriptPath === '/' ? '' : $scriptPath;
+$autoDetectedUrl = $protocol . '://' . $host . $scriptPath;
+
+$currentSettings = [
+ 'app_title' => $db->getSetting('app_title', 'UniFi Voucher System'),
+ 'logo_url' => $db->getSetting('logo_url', ''),
+ 'favicon_url' => $db->getSetting('favicon_url', ''),
+ 'instruction_header' => $db->getSetting('instruction_header', 'So verwenden Sie Ihren Code'),
+ 'instruction_text' => $db->getSetting('instruction_text', 'Verbinden Sie sich mit dem WLAN und geben Sie den Code ein.'),
+ 'public_access' => $db->getSetting('public_access', '0'),
+ 'm365_client_id' => $db->getSetting('m365_client_id', ''),
+ 'm365_client_secret' => $db->getSetting('m365_client_secret', ''),
+ 'm365_tenant_id' => $db->getSetting('m365_tenant_id', ''),
+ 'smtp_enabled' => $db->getSetting('smtp_enabled', '0'),
+ 'smtp_host' => $db->getSetting('smtp_host', ''),
+ 'smtp_port' => $db->getSetting('smtp_port', '587'),
+ 'smtp_username' => $db->getSetting('smtp_username', ''),
+ 'smtp_password' => $db->getSetting('smtp_password', ''),
+ 'smtp_encryption' => $db->getSetting('smtp_encryption', 'tls'),
+ 'smtp_from_email' => $db->getSetting('smtp_from_email', ''),
+ 'smtp_from_name' => $db->getSetting('smtp_from_name', ''),
+ 'system_url' => $db->getSetting('system_url', $autoDetectedUrl),
+ 'email_voucher_subject' => $db->getSetting('email_voucher_subject', '{APP_TITLE} - Ihr WLAN-Zugang'),
+ 'email_voucher_body' => $db->getSetting('email_voucher_body', "Hallo,\n\nIhr Code: {VOUCHER_CODE}\n\nGültigkeit: 8h\nGeräte: {MAX_USES}\nSite: {SITE_NAME}"),
+ 'email_user_notification_subject' => $db->getSetting('email_user_notification_subject', '{APP_TITLE} - Berechtigungen geändert'),
+ 'email_user_notification_body' => $db->getSetting('email_user_notification_body', "Hallo {USER_NAME},\n\n{CHANGES}"),
+ 'tinymce_api_key' => $db->getSetting('tinymce_api_key', ''),
+ 'print_template' => $db->getSetting('print_template', '{APP_TITLE} WLAN Code {VOUCHER_CODE}
Gültig bis: {EXPIRY_DATE} {EXPIRY_TIME}
Site: {SITE_NAME}
Geräte: {MAX_USES}
{INSTRUCTIONS}
'),
+ 'cron_token' => $db->getSetting('cron_token', ''),
+ 'last_cron_sync' => $db->getSetting('last_cron_sync', '')
+];
+
+$currentUser = $auth->getCurrentUser();
+$faviconUrl = $db->getSetting('favicon_url', '');
+?>
+
+
+
+
+
+ Einstellungen - = htmlspecialchars($appTitle) ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
= htmlspecialchars($error) ?>
+
+
+
+
= htmlspecialchars($success) ?>
+
+
+
+
+ Allgemein
+ Cron-Sync
+ Microsoft 365
+ SMTP
+ Templates
+ System
+ Passwort
+
+
+
+
+
Allgemeine Einstellungen
+
+
+
+
+
+
Automatische Voucher-Synchronisation
+
+
+
Was macht der Cron-Job?
+
Der Cron-Job synchronisiert automatisch alle Voucher von Ihren UniFi Controllern in die lokale Datenbank.
+ Dadurch werden Dashboard und Voucher-Übersicht sofort beim Öffnen angezeigt, ohne auf die API warten zu müssen.
+
+
+
+
+
Cron-Token
+
+
+
+
Kein Token konfiguriert. Generieren Sie einen Token, um den Cron-Job zu aktivieren.
+
+
+
+ Token generieren
+
+
+
+
+
+
Token ist aktiv
+
+ = htmlspecialchars($currentSettings['cron_token']) ?>
+
+
+
+ Token kopieren
+
+
+
+
+ Neu generieren
+
+
+
+
+
+ Löschen
+
+
+
+
+
+
+
+
+
Cron-Job einrichten
+
+
+
+
+
+
+
Crontab-Eintrag (Linux/Mac)
+
Fügen Sie diese Zeile in Ihre Crontab ein (crontab -e):
+
+ */30 * * * * curl -s "= htmlspecialchars($cronUrl) ?>" > /dev/null 2>&1
+
+
Dies führt die Synchronisation alle 30 Minuten aus.
+
+
+
+
Windows Task Scheduler
+
Erstellen Sie eine geplante Aufgabe mit diesem Befehl:
+
+ powershell -Command "Invoke-WebRequest -Uri '= htmlspecialchars($cronUrl) ?>' -UseBasicParsing"
+
+
+
+
+
+
Status
+
+
+
+ Letzte Synchronisation:
+
+
+ = date('d.m.Y H:i:s', strtotime($currentSettings['last_cron_sync'])) ?>
+
+ Noch nie ausgeführt
+
+
+
+
+ Token-Status:
+
+
+ Aktiv
+
+ Nicht konfiguriert
+
+
+
+
+
+
+
+
+ Jetzt manuell ausführen
+
+
+
+
+
+
+
+
+
Microsoft 365
+
+
Azure AD App
+
Redirect URI: = $protocol . '://' . $host . $scriptPath ?>/m365_callback.php
+
+
+
+
+
+ Client ID
+
+
+
+ Client Secret
+
+
+
+ Tenant ID
+
+
+ Speichern
+
+
+
+
+
+
SMTP
+
+
+
+
+ >
+ SMTP aktivieren
+
+
+
+ Verschlüsselung
+
+ >TLS
+ >SSL
+ >Keine
+
+
+
+
+ Speichern
+
+
+
+
+
+
+
+
+
+
+
E-Mail Templates
+
+
+
+
+
+
+
+
+ Voucher E-Mail
+
+
Platzhalter:
+
+
{VOUCHER_CODE}
+
{SITE_NAME}
+
{MAX_USES}
+
{APP_TITLE}
+
{INSTRUCTIONS}
+
+
+
+
+ Betreff
+
+
+
+
+ E-Mail Text
+ = htmlspecialchars($currentSettings['email_voucher_body']) ?>
+
+
+
+
+ Benutzer-Benachrichtigung
+
+
Platzhalter:
+
+
{USER_NAME}
+
{CHANGES}
+
{APP_TITLE}
+
{SYSTEM_URL}
+
+
+
+
+ Betreff
+
+
+
+
+ E-Mail Text
+ = htmlspecialchars($currentSettings['email_user_notification_body']) ?>
+
+
+ Speichern
+
+
+
+
+
+
System & Erweitert
+
+
+
+
+
+
TinyMCE API Key
+
Kostenlosen API Key erhalten: tiny.cloud/signup
+ Ohne Key wird eine eingeschränkte Version geladen.
+
+
+
+
+
+
+ Druck-Template
+
+
Platzhalter:
+
+
{VOUCHER_CODE}
+
{EXPIRY_DATE}
+
{EXPIRY_TIME}
+
{SITE_NAME}
+
{MAX_USES}
+
{APP_TITLE}
+
{INSTRUCTIONS}
+
+
+
+
+
+ Speichern
+
+
+
+
+
System-Information
+
+ PHP Version: = phpversion() ?>
+ Datenbank: = DB_NAME ?>
+ Installiert: = date('d.m.Y H:i', filectime(__DIR__ . '/../config.php')) ?>
+ Version: 2.0.0
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/admin/sites.php b/admin/sites.php
new file mode 100644
index 0000000..3b2e14c
--- /dev/null
+++ b/admin/sites.php
@@ -0,0 +1,671 @@
+requireAdmin();
+
+$db = Database::getInstance();
+$appTitle = $db->getSetting('app_title', 'UniFi Voucher System');
+
+$error = '';
+$success = '';
+
+// Site bearbeiten
+if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['edit_site'])) {
+ if (!$auth->validateCsrfToken($_POST['csrf_token'] ?? '')) {
+ $error = 'Ungültiges Sicherheits-Token';
+ } else {
+ try {
+ $siteId = (int)$_POST['site_id'];
+ $name = trim($_POST['name']);
+ $siteIdStr = trim($_POST['site_id_str']);
+ $controllerUrl = trim($_POST['controller_url']);
+ $username = trim($_POST['username']);
+ $password = $_POST['password'];
+ $publicAccess = isset($_POST['public_access']) ? 1 : 0;
+
+ if (empty($name) || empty($siteIdStr) || empty($controllerUrl) || empty($username)) {
+ throw new Exception('Bitte füllen Sie alle Pflichtfelder aus');
+ }
+
+ // Wenn neues Passwort, Verbindung testen
+ if (!empty($password)) {
+ $testResult = UniFiController::testConnection($controllerUrl, $username, $password, $siteIdStr);
+ if ($testResult !== true) {
+ throw new Exception('Verbindung fehlgeschlagen: ' . $testResult);
+ }
+
+ // Mit neuem Passwort aktualisieren
+ $db->execute(
+ "UPDATE sites SET name = ?, site_id = ?, unifi_controller_url = ?, unifi_username = ?, unifi_password = ?, public_access = ? WHERE id = ?",
+ [$name, $siteIdStr, $controllerUrl, $username, $password, $publicAccess, $siteId]
+ );
+ } else {
+ // Ohne Passwort-Änderung
+ $db->execute(
+ "UPDATE sites SET name = ?, site_id = ?, unifi_controller_url = ?, unifi_username = ?, public_access = ? WHERE id = ?",
+ [$name, $siteIdStr, $controllerUrl, $username, $publicAccess, $siteId]
+ );
+ }
+
+ $success = 'Site erfolgreich aktualisiert!';
+
+ } catch (Exception $e) {
+ $error = $e->getMessage();
+ }
+ }
+}
+
+// Site hinzufügen
+if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['add_site'])) {
+ if (!$auth->validateCsrfToken($_POST['csrf_token'] ?? '')) {
+ $error = 'Ungültiges Sicherheits-Token';
+ } else {
+ try {
+ $name = trim($_POST['name']);
+ $siteId = trim($_POST['site_id']);
+ $controllerUrl = trim($_POST['controller_url']);
+ $username = trim($_POST['username']);
+ $password = $_POST['password'];
+ $publicAccess = isset($_POST['public_access']) ? 1 : 0;
+
+ if (empty($name) || empty($siteId) || empty($controllerUrl) || empty($username)) {
+ throw new Exception('Bitte füllen Sie alle Pflichtfelder aus');
+ }
+
+ // Verbindung testen
+ $testResult = UniFiController::testConnection($controllerUrl, $username, $password, $siteId);
+ if ($testResult !== true) {
+ throw new Exception('Verbindung fehlgeschlagen: ' . $testResult);
+ }
+
+ $db->execute(
+ "INSERT INTO sites (name, site_id, unifi_controller_url, unifi_username, unifi_password, public_access)
+ VALUES (?, ?, ?, ?, ?, ?)",
+ [$name, $siteId, $controllerUrl, $username, $password, $publicAccess]
+ );
+
+ $success = 'Site erfolgreich hinzugefügt!';
+
+ } catch (Exception $e) {
+ $error = $e->getMessage();
+ }
+ }
+}
+
+// Site löschen
+if (isset($_GET['delete']) && isset($_GET['token'])) {
+ if ($auth->validateCsrfToken($_GET['token'])) {
+ $db->query("DELETE FROM sites WHERE id = ?", [(int)$_GET['delete']]);
+ $success = 'Site erfolgreich gelöscht!';
+ } else {
+ $error = 'Ungültiges Sicherheits-Token';
+ }
+}
+
+// Site aktivieren/deaktivieren
+if (isset($_GET['toggle']) && isset($_GET['token'])) {
+ if ($auth->validateCsrfToken($_GET['token'])) {
+ $site = $db->fetchOne("SELECT is_active FROM sites WHERE id = ?", [(int)$_GET['toggle']]);
+ if ($site) {
+ $newStatus = $site['is_active'] ? 0 : 1;
+ $db->query("UPDATE sites SET is_active = ? WHERE id = ?", [$newStatus, (int)$_GET['toggle']]);
+ $success = 'Site-Status aktualisiert!';
+ }
+ }
+}
+
+// Alle Sites abrufen
+$sites = $db->fetchAll("SELECT * FROM sites ORDER BY name");
+$currentUser = $auth->getCurrentUser();
+?>
+
+
+
+
+
+ Sites verwalten - = htmlspecialchars($appTitle) ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
= htmlspecialchars($error) ?>
+
+
+
+
= htmlspecialchars($success) ?>
+
+
+
+
+
+
+
Noch keine Sites konfiguriert. Fügen Sie Ihre erste Site hinzu!
+
+
+
+
+
+
+
+
+
+
+
+ = htmlspecialchars($site['unifi_controller_url']) ?>
+
+
+
+ = htmlspecialchars($site['unifi_username']) ?>
+
+
+
+ Erstellt: = date('d.m.Y', strtotime($site['created_at'])) ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/admin/users.php b/admin/users.php
new file mode 100644
index 0000000..84cb827
--- /dev/null
+++ b/admin/users.php
@@ -0,0 +1,767 @@
+requireAdmin();
+
+$db = Database::getInstance();
+$mailer = new Mailer();
+$appTitle = $db->getSetting('app_title', 'UniFi Voucher System');
+
+$error = '';
+$success = '';
+
+// Benutzer bearbeiten
+if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['edit_user'])) {
+ if (!$auth->validateCsrfToken($_POST['csrf_token'] ?? '')) {
+ $error = 'Ungültiges Sicherheits-Token';
+ } else {
+ try {
+ $userId = (int)$_POST['user_id'];
+ $isAdmin = isset($_POST['is_admin']) ? 1 : 0;
+ $siteIds = $_POST['site_ids'] ?? [];
+
+ // Alten Status abrufen
+ $oldUser = $db->fetchOne("SELECT * FROM users WHERE id = ?", [$userId]);
+ $oldIsAdmin = $oldUser['is_admin'];
+ $oldSites = $db->fetchAll("SELECT s.name FROM sites s INNER JOIN user_site_access usa ON s.id = usa.site_id WHERE usa.user_id = ?", [$userId]);
+
+ // Admin-Status aktualisieren
+ $db->query("UPDATE users SET is_admin = ? WHERE id = ?", [$isAdmin, $userId]);
+
+ // Alte Site-Zugriffe löschen (nur wenn nicht Admin)
+ $db->query("DELETE FROM user_site_access WHERE user_id = ?", [$userId]);
+
+ // Neue Site-Zugriffe zuweisen (nur wenn nicht Admin)
+ $newSites = [];
+ if (!$isAdmin && !empty($siteIds)) {
+ foreach ($siteIds as $siteId) {
+ $db->execute(
+ "INSERT INTO user_site_access (user_id, site_id) VALUES (?, ?)",
+ [$userId, $siteId]
+ );
+ $site = $db->fetchOne("SELECT name FROM sites WHERE id = ?", [$siteId]);
+ if ($site) {
+ $newSites[] = $site['name'];
+ }
+ }
+ }
+
+ // E-Mail-Benachrichtigung vorbereiten
+ $changes = [];
+
+ if ($oldIsAdmin != $isAdmin) {
+ if ($isAdmin) {
+ $changes[] = "Sie wurden zum Administrator ernannt";
+ } else {
+ $changes[] = "Ihre Administrator-Rechte wurden entfernt";
+ }
+ }
+
+ // Site-Änderungen erkennen
+ $oldSiteNames = array_column($oldSites, 'name');
+ $addedSites = array_diff($newSites, $oldSiteNames);
+ $removedSites = array_diff($oldSiteNames, $newSites);
+
+ if (!empty($addedSites)) {
+ $changes[] = "Zugriff gewährt auf: " . implode(', ', $addedSites);
+ }
+
+ if (!empty($removedSites)) {
+ $changes[] = "Zugriff entfernt von: " . implode(', ', $removedSites);
+ }
+
+ if ($isAdmin && !$oldIsAdmin) {
+ $changes[] = "Sie haben nun Zugriff auf alle Sites";
+ }
+
+ // E-Mail senden wenn Änderungen vorliegen
+ if (!empty($changes)) {
+ $mailer->sendUserNotification($oldUser['email'], $oldUser['name'], $changes);
+ }
+
+ $success = 'Benutzer erfolgreich aktualisiert!' . (!empty($changes) ? ' Benachrichtigung wurde versendet.' : '');
+
+ } catch (Exception $e) {
+ $error = $e->getMessage();
+ }
+ }
+}
+
+// Benutzer hinzufügen
+if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['add_user'])) {
+ if (!$auth->validateCsrfToken($_POST['csrf_token'] ?? '')) {
+ $error = 'Ungültiges Sicherheits-Token';
+ } else {
+ try {
+ $email = trim($_POST['email']);
+ $name = trim($_POST['name']);
+ $password = $_POST['password'];
+ $isAdmin = isset($_POST['is_admin']) ? 1 : 0;
+ $siteIds = $_POST['site_ids'] ?? [];
+
+ if (empty($email) || empty($name) || empty($password)) {
+ throw new Exception('Bitte füllen Sie alle Pflichtfelder aus');
+ }
+
+ if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
+ throw new Exception('Ungültige E-Mail-Adresse');
+ }
+
+ if (strlen($password) < 8) {
+ throw new Exception('Passwort muss mindestens 8 Zeichen lang sein');
+ }
+
+ // Prüfen ob E-Mail bereits existiert
+ $existing = $db->fetchOne("SELECT id FROM users WHERE email = ?", [$email]);
+ if ($existing) {
+ throw new Exception('Ein Benutzer mit dieser E-Mail existiert bereits');
+ }
+
+ // Benutzer anlegen
+ $userId = $auth->registerUser($email, $name, $password, $isAdmin);
+
+ if (!$userId) {
+ throw new Exception('Benutzer konnte nicht erstellt werden');
+ }
+
+ // Site-Zugriffe zuweisen (nur wenn nicht Admin)
+ if (!$isAdmin && !empty($siteIds)) {
+ foreach ($siteIds as $siteId) {
+ $db->execute(
+ "INSERT INTO user_site_access (user_id, site_id) VALUES (?, ?)",
+ [$userId, $siteId]
+ );
+ }
+ }
+
+ $success = 'Benutzer erfolgreich erstellt!';
+
+ } catch (Exception $e) {
+ $error = $e->getMessage();
+ }
+ }
+}
+
+// Benutzer löschen
+if (isset($_GET['delete']) && isset($_GET['token'])) {
+ if ($auth->validateCsrfToken($_GET['token'])) {
+ $deleteId = (int)$_GET['delete'];
+ $currentUserId = $_SESSION['user_id'];
+
+ if ($deleteId === $currentUserId) {
+ $error = 'Sie können sich nicht selbst löschen';
+ } else {
+ $db->query("DELETE FROM users WHERE id = ?", [$deleteId]);
+ $success = 'Benutzer erfolgreich gelöscht!';
+ }
+ } else {
+ $error = 'Ungültiges Sicherheits-Token';
+ }
+}
+
+// Benutzer aktivieren/deaktivieren
+if (isset($_GET['toggle']) && isset($_GET['token'])) {
+ if ($auth->validateCsrfToken($_GET['token'])) {
+ $toggleId = (int)$_GET['toggle'];
+ $currentUserId = $_SESSION['user_id'];
+
+ if ($toggleId === $currentUserId) {
+ $error = 'Sie können sich nicht selbst deaktivieren';
+ } else {
+ $user = $db->fetchOne("SELECT is_active FROM users WHERE id = ?", [$toggleId]);
+ if ($user) {
+ $newStatus = $user['is_active'] ? 0 : 1;
+ $db->query("UPDATE users SET is_active = ? WHERE id = ?", [$newStatus, $toggleId]);
+ $success = 'Benutzer-Status aktualisiert!';
+ }
+ }
+ }
+}
+
+// Alle Benutzer und Sites abrufen
+$users = $db->fetchAll("SELECT * FROM users ORDER BY name");
+$sites = $db->fetchAll("SELECT * FROM sites WHERE is_active = 1 ORDER BY name");
+
+// Site-Zugriffe für jeden Benutzer abrufen
+$userSiteAccess = [];
+foreach ($users as $user) {
+ $userSiteAccess[$user['id']] = $db->fetchAll(
+ "SELECT s.name FROM sites s
+ INNER JOIN user_site_access usa ON s.id = usa.site_id
+ WHERE usa.user_id = ?",
+ [$user['id']]
+ );
+}
+
+$currentUser = $auth->getCurrentUser();
+?>
+
+
+
+
+
+ Benutzer verwalten - = htmlspecialchars($appTitle) ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
= htmlspecialchars($error) ?>
+
+
+
+
= htmlspecialchars($success) ?>
+
+
+
+
+
+
+
+
+
Noch keine Benutzer vorhanden
+
+
+
+
+
+ Name
+ E-Mail
+ Rolle
+ Status
+ Site-Zugriffe
+ Letzter Login
+ Aktionen
+
+
+
+
+
+
+ = htmlspecialchars($user['name']) ?>
+
+ Sie
+
+
+ = htmlspecialchars($user['email']) ?>
+
+
+ Admin
+
+ Benutzer
+
+
+
+
+ Aktiv
+
+ Inaktiv
+
+
+
+
+ Alle Sites
+
+
+ = htmlspecialchars($site['name']) ?>
+
+
+ Keine
+
+
+
+
+ = date('d.m.Y H:i', strtotime($user['last_login'])) ?>
+
+ Noch nie
+
+
+
+
+ ])"
+ class="btn btn-secondary btn-small">
+
+
+
+
+
+
+
+
+
+ ])"
+ class="btn btn-secondary btn-small">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Name
+
+
+
+
+
+
+
+
+
+ Änderungen speichern
+
+
+ Abbrechen
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/admin/vouchers.php b/admin/vouchers.php
new file mode 100644
index 0000000..b2b4e5d
--- /dev/null
+++ b/admin/vouchers.php
@@ -0,0 +1,1004 @@
+requireAdmin();
+
+$db = Database::getInstance();
+$appTitle = $db->getSetting('app_title', 'UniFi Voucher System');
+
+// 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');
+
+ try {
+ $siteId = (int)$_GET['site_id'];
+ $syncFirst = isset($_GET['sync']) && $_GET['sync'] == '1';
+ $site = $db->fetchOne("SELECT * FROM sites WHERE id = ? AND is_active = 1", [$siteId]);
+
+ if (!$site) {
+ echo json_encode(['success' => false, 'message' => 'Site nicht gefunden oder inaktiv']);
+ exit;
+ }
+
+ // Bei sync=1: Erst Live-Daten holen und in DB speichern
+ if ($syncFirst) {
+ try {
+ $controller = new UniFiController(
+ $site['unifi_controller_url'],
+ $site['unifi_username'],
+ $site['unifi_password'],
+ $site['site_id']
+ );
+ $controller->syncVouchersToDatabase($db, $siteId);
+
+ // Last sync time aktualisieren
+ $db->execute(
+ "INSERT INTO settings (setting_key, setting_value) VALUES ('last_cron_sync', NOW())
+ ON DUPLICATE KEY UPDATE setting_value = NOW()"
+ );
+ } catch (Exception $e) {
+ // Sync-Fehler loggen, aber trotzdem DB-Daten zurückgeben
+ error_log("Sync error for site {$siteId}: " . $e->getMessage());
+ }
+ }
+
+ // Immer aus Datenbank abrufen
+ $dbVouchers = $db->fetchAll(
+ "SELECT * FROM vouchers WHERE site_id = ? ORDER BY created_at DESC",
+ [$siteId]
+ );
+
+ $vouchers = [];
+ foreach ($dbVouchers as $v) {
+ $expireTime = $v['expires_at'] ? strtotime($v['expires_at']) : 0;
+ $createTime = strtotime($v['created_at']);
+
+ $vouchers[] = [
+ '_id' => $v['unifi_voucher_id'] ?? $v['id'],
+ 'code' => str_replace('-', '', $v['voucher_code']),
+ 'formatted_code' => $v['voucher_code'],
+ 'note' => $v['voucher_name'],
+ 'quota' => (int)$v['max_uses'],
+ 'used' => (int)($v['used_count'] ?? 0),
+ 'duration' => (int)$v['expire_minutes'],
+ 'create_time' => $createTime,
+ 'expire_time' => $expireTime,
+ 'status' => $v['status'] ?? 'valid',
+ 'db_id' => $v['id']
+ ];
+ }
+
+ $lastSync = $db->getSetting('last_cron_sync', '');
+
+ echo json_encode([
+ 'success' => true,
+ 'vouchers' => $vouchers,
+ 'site_name' => $site['name'],
+ 'count' => count($vouchers),
+ 'synced' => $syncFirst,
+ 'last_sync' => $lastSync ? date('d.m.Y H:i:s', strtotime($lastSync)) : null
+ ]);
+ } catch (Exception $e) {
+ echo json_encode(['success' => false, 'message' => 'Fehler: ' . $e->getMessage()]);
+ }
+ exit;
+}
+
+// AJAX: Voucher löschen
+if (isset($_POST['ajax_delete']) && isset($_POST['voucher_id']) && isset($_POST['site_id'])) {
+ header('Content-Type: application/json');
+
+ if (!$auth->validateCsrfToken($_POST['csrf_token'] ?? '')) {
+ echo json_encode(['success' => false, 'message' => 'Ungültiges Sicherheits-Token']);
+ exit;
+ }
+
+ try {
+ $voucherId = $_POST['voucher_id']; // UniFi _id (String)
+ $siteId = (int)$_POST['site_id'];
+
+ $site = $db->fetchOne("SELECT * FROM sites WHERE id = ? AND is_active = 1", [$siteId]);
+
+ if (!$site) {
+ echo json_encode(['success' => false, 'message' => 'Site nicht gefunden oder inaktiv']);
+ exit;
+ }
+
+ $controller = new UniFiController(
+ $site['unifi_controller_url'],
+ $site['unifi_username'],
+ $site['unifi_password'],
+ $site['site_id']
+ );
+
+ $result = $controller->deleteVoucher($voucherId);
+
+ if ($result) {
+ // Auch aus Datenbank löschen
+ $db->execute("DELETE FROM vouchers WHERE unifi_voucher_id = ? AND site_id = ?", [$voucherId, $siteId]);
+ echo json_encode(['success' => true, 'message' => 'Voucher erfolgreich gelöscht!']);
+ } else {
+ echo json_encode(['success' => false, 'message' => 'Voucher konnte nicht gelöscht werden']);
+ }
+ } catch (Exception $e) {
+ echo json_encode(['success' => false, 'message' => 'Fehler: ' . $e->getMessage()]);
+ }
+ exit;
+}
+
+// Alle aktiven Sites abrufen
+$sites = $db->fetchAll("SELECT * FROM sites WHERE is_active = 1 ORDER BY name");
+
+// Voucher-Statistiken aus DB pro Site
+$siteStats = [];
+foreach ($sites as $site) {
+ $stats = $db->fetchOne(
+ "SELECT
+ COUNT(*) as total,
+ SUM(CASE WHEN status = 'valid' THEN 1 ELSE 0 END) as valid,
+ SUM(CASE WHEN status = 'used' THEN 1 ELSE 0 END) as used,
+ SUM(CASE WHEN status = 'expired' THEN 1 ELSE 0 END) as expired
+ FROM vouchers WHERE site_id = ?",
+ [$site['id']]
+ );
+ $siteStats[$site['id']] = $stats;
+}
+
+// Letzte Synchronisation
+$lastCronSync = $db->getSetting('last_cron_sync', '');
+
+$currentUser = $auth->getCurrentUser();
+$faviconUrl = $db->getSetting('favicon_url', '');
+?>
+
+
+
+
+
+ Live Voucher-Verwaltung - = htmlspecialchars($appTitle) ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Keine aktiven Sites vorhanden
+
Bitte fügen Sie zuerst eine Site hinzu oder aktivieren Sie eine vorhandene.
+
+ Sites verwalten
+
+
+
+
+
+
+
+
+
+ -- Site auswählen --
+ 0];
+ ?>
+
+ = htmlspecialchars($site['name']) ?> (= (int)$ss['total'] ?> Vouchers)
+
+
+
+
+ Aktualisieren
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Bitte wählen Sie eine Site aus, um die Vouchers zu laden.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/config.php b/config.php
new file mode 100644
index 0000000..978061b
--- /dev/null
+++ b/config.php
@@ -0,0 +1,13 @@
+ false,
+ 'message' => 'Fatal Error: ' . $error['message'],
+ 'file' => $error['file'],
+ 'line' => $error['line']
+ ];
+ outputResponse($response, $isCli);
+ }
+});
+
+set_exception_handler(function($e) use ($isCli) {
+ $response = [
+ 'success' => false,
+ 'message' => 'Exception: ' . $e->getMessage(),
+ 'file' => $e->getFile(),
+ 'line' => $e->getLine()
+ ];
+ outputResponse($response, $isCli);
+ exit;
+});
+
+// ============================================
+// INCLUDES LADEN
+// ============================================
+
+require_once __DIR__ . '/config.php';
+require_once __DIR__ . '/includes/Database.php';
+require_once __DIR__ . '/includes/UniFiController.php';
+
+$db = Database::getInstance();
+
+// ============================================
+// TOKEN VALIDIERUNG
+// ============================================
+
+$cronToken = $db->getSetting('cron_token', '');
+
+// Token aus GET oder CLI-Argument holen
+$providedToken = '';
+if ($isCli && isset($argv[1])) {
+ $providedToken = $argv[1];
+} else {
+ $providedToken = $_GET['token'] ?? '';
+}
+
+// Prüfen ob Token konfiguriert und korrekt ist
+if (empty($cronToken)) {
+ outputResponse([
+ 'success' => false,
+ 'message' => 'Kein Cron-Token konfiguriert. Bitte im Admin-Bereich unter Einstellungen generieren.'
+ ], $isCli);
+ exit;
+}
+
+if ($providedToken !== $cronToken) {
+ outputResponse([
+ 'success' => false,
+ 'message' => 'Ungültiger Token'
+ ], $isCli);
+ exit;
+}
+
+// ============================================
+// SYNCHRONISATION STARTEN
+// ============================================
+
+logMessage("Starte Voucher-Synchronisation...", $isCli);
+
+$startTime = microtime(true);
+$results = [];
+$totalStats = [
+ 'sites_processed' => 0,
+ 'sites_failed' => 0,
+ 'vouchers_total' => 0,
+ 'vouchers_new' => 0,
+ 'vouchers_updated' => 0,
+ 'vouchers_valid' => 0,
+ 'vouchers_used' => 0,
+ 'vouchers_expired' => 0
+];
+
+try {
+ // Alle aktiven Sites abrufen
+ $sites = $db->fetchAll("SELECT * FROM sites WHERE is_active = 1");
+
+ if (empty($sites)) {
+ outputResponse([
+ 'success' => true,
+ 'message' => 'Keine aktiven Sites gefunden',
+ 'results' => []
+ ], $isCli);
+ exit;
+ }
+
+ logMessage("Gefunden: " . count($sites) . " aktive Site(s)", $isCli);
+
+ foreach ($sites as $site) {
+ logMessage("Synchronisiere Site: {$site['name']} ({$site['site_id']})...", $isCli);
+
+ $siteResult = [
+ 'site_id' => $site['id'],
+ 'site_name' => $site['name'],
+ 'stats' => null,
+ 'error' => null
+ ];
+
+ try {
+ $controller = new UniFiController(
+ $site['unifi_controller_url'],
+ $site['unifi_username'],
+ $site['unifi_password'],
+ $site['site_id']
+ );
+
+ $stats = $controller->syncVouchersToDatabase($db, $site['id']);
+ $siteResult['stats'] = $stats;
+
+ // Gesamtstatistiken aktualisieren
+ $totalStats['sites_processed']++;
+ $totalStats['vouchers_total'] += $stats['total'];
+ $totalStats['vouchers_new'] += $stats['new'];
+ $totalStats['vouchers_updated'] += $stats['updated'];
+ $totalStats['vouchers_valid'] += $stats['valid'];
+ $totalStats['vouchers_used'] += $stats['used'];
+ $totalStats['vouchers_expired'] += $stats['expired'];
+
+ logMessage(" OK - {$stats['total']} Voucher(s), {$stats['new']} neu, {$stats['updated']} aktualisiert", $isCli);
+
+ } catch (Exception $e) {
+ $siteResult['error'] = $e->getMessage();
+ $totalStats['sites_failed']++;
+ logMessage(" FEHLER - " . $e->getMessage(), $isCli);
+ }
+
+ $results[] = $siteResult;
+ }
+
+ // Letzte Sync-Zeit speichern
+ $db->execute(
+ "INSERT INTO settings (setting_key, setting_value) VALUES ('last_cron_sync', NOW())
+ ON DUPLICATE KEY UPDATE setting_value = NOW()"
+ );
+
+ $duration = round(microtime(true) - $startTime, 2);
+
+ $response = [
+ 'success' => true,
+ 'message' => "Synchronisation abgeschlossen in {$duration}s",
+ 'duration' => $duration,
+ 'total' => $totalStats,
+ 'results' => $results,
+ 'timestamp' => date('Y-m-d H:i:s')
+ ];
+
+ logMessage("Synchronisation abgeschlossen in {$duration}s", $isCli);
+ logMessage("Gesamt: {$totalStats['vouchers_total']} Voucher(s), {$totalStats['vouchers_new']} neu, {$totalStats['sites_failed']} Fehler", $isCli);
+
+} catch (Exception $e) {
+ $response = [
+ 'success' => false,
+ 'message' => 'Kritischer Fehler: ' . $e->getMessage()
+ ];
+ logMessage("KRITISCHER FEHLER: " . $e->getMessage(), $isCli);
+}
+
+outputResponse($response, $isCli);
diff --git a/cron_test.php b/cron_test.php
new file mode 100644
index 0000000..ab6067a
--- /dev/null
+++ b/cron_test.php
@@ -0,0 +1,72 @@
+ 1, 'message' => 'PHP läuft']);
+
+// Test 1: Config laden
+try {
+ require_once __DIR__ . '/config.php';
+ echo "\n" . json_encode(['step' => 2, 'message' => 'Config geladen']);
+} catch (Exception $e) {
+ echo "\n" . json_encode(['step' => 2, 'error' => $e->getMessage()]);
+ exit;
+}
+
+// Test 2: Database laden
+try {
+ require_once __DIR__ . '/includes/Database.php';
+ echo "\n" . json_encode(['step' => 3, 'message' => 'Database.php geladen']);
+} catch (Exception $e) {
+ echo "\n" . json_encode(['step' => 3, 'error' => $e->getMessage()]);
+ exit;
+}
+
+// Test 3: DB-Verbindung
+try {
+ $db = Database::getInstance();
+ echo "\n" . json_encode(['step' => 4, 'message' => 'DB-Verbindung OK']);
+} catch (Exception $e) {
+ echo "\n" . json_encode(['step' => 4, 'error' => $e->getMessage()]);
+ exit;
+}
+
+// Test 4: UniFiController laden
+try {
+ require_once __DIR__ . '/includes/UniFiController.php';
+ echo "\n" . json_encode(['step' => 5, 'message' => 'UniFiController.php geladen']);
+} catch (Exception $e) {
+ echo "\n" . json_encode(['step' => 5, 'error' => $e->getMessage()]);
+ exit;
+}
+
+// Test 5: Spalten prüfen
+try {
+ $columns = $db->fetchAll("SHOW COLUMNS FROM vouchers");
+ $columnNames = array_column($columns, 'Field');
+ echo "\n" . json_encode(['step' => 6, 'message' => 'Voucher-Spalten', 'columns' => $columnNames]);
+
+ // Prüfen ob neue Spalten existieren
+ $required = ['status', 'used_count', 'expires_at', 'synced_from_unifi', 'last_sync'];
+ $missing = array_diff($required, $columnNames);
+
+ if (!empty($missing)) {
+ echo "\n" . json_encode(['step' => 7, 'warning' => 'Fehlende Spalten', 'missing' => array_values($missing)]);
+ } else {
+ echo "\n" . json_encode(['step' => 7, 'message' => 'Alle Spalten vorhanden']);
+ }
+} catch (Exception $e) {
+ echo "\n" . json_encode(['step' => 6, 'error' => $e->getMessage()]);
+ exit;
+}
+
+// Test 6: Token prüfen
+try {
+ $token = $db->getSetting('cron_token', '');
+ echo "\n" . json_encode(['step' => 8, 'message' => 'Token Status', 'has_token' => !empty($token)]);
+} catch (Exception $e) {
+ echo "\n" . json_encode(['step' => 8, 'error' => $e->getMessage()]);
+ exit;
+}
+
+echo "\n" . json_encode(['final' => 'Alle Tests erfolgreich!']);
diff --git a/database.sql b/database.sql
new file mode 100644
index 0000000..1015ffd
--- /dev/null
+++ b/database.sql
@@ -0,0 +1,90 @@
+-- UniFi Voucher Management System - Datenbankstruktur
+
+CREATE TABLE IF NOT EXISTS `settings` (
+ `id` INT PRIMARY KEY AUTO_INCREMENT,
+ `setting_key` VARCHAR(100) UNIQUE NOT NULL,
+ `setting_value` TEXT,
+ `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+
+CREATE TABLE IF NOT EXISTS `sites` (
+ `id` INT PRIMARY KEY AUTO_INCREMENT,
+ `name` VARCHAR(255) NOT NULL,
+ `site_id` VARCHAR(100) NOT NULL,
+ `unifi_controller_url` VARCHAR(255) NOT NULL,
+ `unifi_username` VARCHAR(100) NOT NULL,
+ `unifi_password` VARCHAR(255) NOT NULL,
+ `is_active` TINYINT(1) DEFAULT 1,
+ `public_access` TINYINT(1) DEFAULT 0,
+ `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ INDEX `idx_active` (`is_active`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+
+CREATE TABLE IF NOT EXISTS `users` (
+ `id` INT PRIMARY KEY AUTO_INCREMENT,
+ `email` VARCHAR(255) UNIQUE NOT NULL,
+ `name` VARCHAR(255),
+ `password_hash` VARCHAR(255),
+ `is_admin` TINYINT(1) DEFAULT 0,
+ `is_active` TINYINT(1) DEFAULT 1,
+ `microsoft_id` VARCHAR(255) UNIQUE,
+ `last_login` TIMESTAMP NULL,
+ `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ INDEX `idx_email` (`email`),
+ INDEX `idx_microsoft` (`microsoft_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+
+CREATE TABLE IF NOT EXISTS `user_site_access` (
+ `id` INT PRIMARY KEY AUTO_INCREMENT,
+ `user_id` INT NOT NULL,
+ `site_id` INT NOT NULL,
+ `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE,
+ FOREIGN KEY (`site_id`) REFERENCES `sites`(`id`) ON DELETE CASCADE,
+ UNIQUE KEY `unique_user_site` (`user_id`, `site_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+
+CREATE TABLE IF NOT EXISTS `vouchers` (
+ `id` INT PRIMARY KEY AUTO_INCREMENT,
+ `site_id` INT NOT NULL,
+ `user_id` INT,
+ `voucher_code` VARCHAR(50) NOT NULL,
+ `voucher_name` VARCHAR(255) NOT NULL,
+ `max_uses` INT NOT NULL,
+ `expire_minutes` INT NOT NULL,
+ `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ `unifi_voucher_id` VARCHAR(100),
+ `status` ENUM('valid', 'used', 'expired') DEFAULT 'valid',
+ `used_count` INT DEFAULT 0,
+ `expires_at` TIMESTAMP NULL,
+ `synced_from_unifi` TINYINT(1) DEFAULT 0,
+ `last_sync` TIMESTAMP NULL,
+ FOREIGN KEY (`site_id`) REFERENCES `sites`(`id`) ON DELETE CASCADE,
+ FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE SET NULL,
+ INDEX `idx_site` (`site_id`),
+ INDEX `idx_created` (`created_at`),
+ INDEX `idx_unifi_id` (`unifi_voucher_id`),
+ INDEX `idx_status` (`status`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+
+-- Migration für bestehende Tabellen (falls bereits vorhanden):
+-- ALTER TABLE vouchers ADD COLUMN `status` ENUM('valid', 'used', 'expired') DEFAULT 'valid';
+-- ALTER TABLE vouchers ADD COLUMN `used_count` INT DEFAULT 0;
+-- ALTER TABLE vouchers ADD COLUMN `expires_at` TIMESTAMP NULL;
+-- ALTER TABLE vouchers ADD COLUMN `synced_from_unifi` TINYINT(1) DEFAULT 0;
+-- ALTER TABLE vouchers ADD COLUMN `last_sync` TIMESTAMP NULL;
+-- ALTER TABLE vouchers ADD INDEX `idx_unifi_id` (`unifi_voucher_id`);
+-- ALTER TABLE vouchers ADD INDEX `idx_status` (`status`);
+
+CREATE TABLE IF NOT EXISTS `sessions` (
+ `id` VARCHAR(128) PRIMARY KEY,
+ `user_id` INT NOT NULL,
+ `data` TEXT,
+ `expires_at` TIMESTAMP NOT NULL,
+ `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE,
+ INDEX `idx_expires` (`expires_at`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
\ No newline at end of file
diff --git a/includes/Auth.php b/includes/Auth.php
new file mode 100644
index 0000000..02d261b
--- /dev/null
+++ b/includes/Auth.php
@@ -0,0 +1,201 @@
+db = Database::getInstance();
+ } catch (Exception $e) {
+ die("Datenbankverbindung fehlgeschlagen: " . $e->getMessage());
+ }
+
+ // Session-Konfiguration
+ if (session_status() === PHP_SESSION_NONE) {
+ ini_set('session.cookie_httponly', 1);
+ ini_set('session.use_strict_mode', 1);
+ ini_set('session.cookie_samesite', 'Lax');
+
+ if (!session_start()) {
+ die("Session konnte nicht gestartet werden");
+ }
+ }
+ }
+
+ // Benutzer einloggen
+ public function login($email, $password) {
+ $user = $this->db->fetchOne(
+ "SELECT * FROM users WHERE email = ? AND is_active = 1",
+ [$email]
+ );
+
+ if ($user && password_verify($password, $user['password_hash'])) {
+ $this->setUserSession($user);
+ $this->updateLastLogin($user['id']);
+ return true;
+ }
+
+ return false;
+ }
+
+ // Microsoft 365 Login
+ public function loginWithMicrosoft($microsoftUser) {
+ // Zuerst nach Microsoft ID suchen
+ $user = $this->db->fetchOne(
+ "SELECT * FROM users WHERE microsoft_id = ? AND is_active = 1",
+ [$microsoftUser['id']]
+ );
+
+ if (!$user) {
+ // Prüfen ob E-Mail bereits existiert (ohne Microsoft ID)
+ $existingUser = $this->db->fetchOne(
+ "SELECT * FROM users WHERE email = ? AND is_active = 1",
+ [$microsoftUser['email']]
+ );
+
+ if ($existingUser) {
+ // Benutzer existiert bereits ohne Microsoft ID - verknüpfen
+ $this->db->query(
+ "UPDATE users SET microsoft_id = ?, name = ? WHERE id = ?",
+ [$microsoftUser['id'], $microsoftUser['name'], $existingUser['id']]
+ );
+ $user = $this->db->fetchOne("SELECT * FROM users WHERE id = ?", [$existingUser['id']]);
+ } else {
+ // Komplett neuer Benutzer - anlegen
+ $userId = $this->db->execute(
+ "INSERT INTO users (email, name, microsoft_id, is_active) VALUES (?, ?, ?, 1)",
+ [$microsoftUser['email'], $microsoftUser['name'], $microsoftUser['id']]
+ );
+
+ $user = $this->db->fetchOne("SELECT * FROM users WHERE id = ?", [$userId]);
+ }
+ } else {
+ // Microsoft-Benutzer existiert bereits - Name aktualisieren falls geändert
+ $this->db->query(
+ "UPDATE users SET name = ? WHERE id = ?",
+ [$microsoftUser['name'], $user['id']]
+ );
+ }
+
+ $this->setUserSession($user);
+ $this->updateLastLogin($user['id']);
+ return true;
+ }
+
+ // Session setzen
+ private function setUserSession($user) {
+ $_SESSION['user_id'] = $user['id'];
+ $_SESSION['user_email'] = $user['email'];
+ $_SESSION['user_name'] = $user['name'];
+ $_SESSION['is_admin'] = (bool)$user['is_admin'];
+ $_SESSION['login_time'] = time();
+
+ // CSRF-Token generieren
+ if (!isset($_SESSION['csrf_token'])) {
+ $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
+ }
+ }
+
+ // Letzten Login aktualisieren
+ private function updateLastLogin($userId) {
+ $this->db->query(
+ "UPDATE users SET last_login = NOW() WHERE id = ?",
+ [$userId]
+ );
+ }
+
+ // Ausloggen
+ public function logout() {
+ $_SESSION = [];
+
+ if (isset($_COOKIE[session_name()])) {
+ setcookie(session_name(), '', time() - 3600, '/');
+ }
+
+ session_destroy();
+ }
+
+ // Prüfen ob eingeloggt
+ public function isLoggedIn() {
+ return isset($_SESSION['user_id']) && isset($_SESSION['login_time']);
+ }
+
+ // Prüfen ob Admin
+ public function isAdmin() {
+ return $this->isLoggedIn() && isset($_SESSION['is_admin']) && $_SESSION['is_admin'] === true;
+ }
+
+ // Aktuellen Benutzer abrufen
+ public function getCurrentUser() {
+ if (!$this->isLoggedIn()) {
+ return null;
+ }
+
+ return $this->db->fetchOne(
+ "SELECT * FROM users WHERE id = ?",
+ [$_SESSION['user_id']]
+ );
+ }
+
+ // Prüfen ob Benutzer Zugriff auf Site hat
+ public function hasAccessToSite($siteId) {
+ if ($this->isAdmin()) {
+ return true;
+ }
+
+ if (!$this->isLoggedIn()) {
+ return false;
+ }
+
+ $access = $this->db->fetchOne(
+ "SELECT id FROM user_site_access WHERE user_id = ? AND site_id = ?",
+ [$_SESSION['user_id'], $siteId]
+ );
+
+ return $access !== false;
+ }
+
+ // CSRF-Token validieren
+ public function validateCsrfToken($token) {
+ return isset($_SESSION['csrf_token']) && hash_equals($_SESSION['csrf_token'], $token);
+ }
+
+ // CSRF-Token abrufen
+ public function getCsrfToken() {
+ if (!isset($_SESSION['csrf_token'])) {
+ $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
+ }
+ return $_SESSION['csrf_token'];
+ }
+
+ // Benutzer registrieren (nur für Admins)
+ public function registerUser($email, $name, $password, $isAdmin = false) {
+ // Prüfen ob E-Mail bereits existiert
+ $existing = $this->db->fetchOne("SELECT id FROM users WHERE email = ?", [$email]);
+ if ($existing) {
+ return false;
+ }
+
+ $passwordHash = password_hash($password, PASSWORD_DEFAULT);
+
+ return $this->db->execute(
+ "INSERT INTO users (email, name, password_hash, is_admin, is_active) VALUES (?, ?, ?, ?, 1)",
+ [$email, $name, $passwordHash, $isAdmin ? 1 : 0]
+ );
+ }
+
+ // Admin-Zugriff erforderlich
+ public function requireAdmin() {
+ if (!$this->isAdmin()) {
+ header('Location: /index.php?error=access_denied');
+ exit;
+ }
+ }
+
+ // Login erforderlich
+ public function requireLogin() {
+ if (!$this->isLoggedIn()) {
+ header('Location: /login.php');
+ exit;
+ }
+ }
+}
\ No newline at end of file
diff --git a/includes/Database.php b/includes/Database.php
new file mode 100644
index 0000000..68a252f
--- /dev/null
+++ b/includes/Database.php
@@ -0,0 +1,72 @@
+pdo = new PDO(
+ "mysql:host=" . DB_HOST . ";dbname=" . DB_NAME . ";charset=utf8mb4",
+ DB_USER,
+ DB_PASS,
+ [
+ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
+ PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
+ PDO::ATTR_EMULATE_PREPARES => false
+ ]
+ );
+ } catch (PDOException $e) {
+ die("Datenbankverbindung fehlgeschlagen: " . $e->getMessage());
+ }
+ }
+
+ public static function getInstance() {
+ if (self::$instance === null) {
+ self::$instance = new self();
+ }
+ return self::$instance;
+ }
+
+ public function getConnection() {
+ return $this->pdo;
+ }
+
+ // Helper-Methode für Queries
+ public function query($sql, $params = []) {
+ $stmt = $this->pdo->prepare($sql);
+ $stmt->execute($params);
+ return $stmt;
+ }
+
+ // Helper für einzelnen Datensatz
+ public function fetchOne($sql, $params = []) {
+ $stmt = $this->query($sql, $params);
+ return $stmt->fetch();
+ }
+
+ // Helper für mehrere Datensätze
+ public function fetchAll($sql, $params = []) {
+ $stmt = $this->query($sql, $params);
+ return $stmt->fetchAll();
+ }
+
+ // Helper für Insert/Update mit Rückgabe der ID
+ public function execute($sql, $params = []) {
+ $this->query($sql, $params);
+ return $this->pdo->lastInsertId();
+ }
+
+ // Settings-Helper
+ public function getSetting($key, $default = null) {
+ $result = $this->fetchOne("SELECT setting_value FROM settings WHERE setting_key = ?", [$key]);
+ return $result ? $result['setting_value'] : $default;
+ }
+
+ public function setSetting($key, $value) {
+ $this->query(
+ "INSERT INTO settings (setting_key, setting_value) VALUES (?, ?)
+ ON DUPLICATE KEY UPDATE setting_value = VALUES(setting_value)",
+ [$key, $value]
+ );
+ }
+}
\ No newline at end of file
diff --git a/includes/Mailer.php b/includes/Mailer.php
new file mode 100644
index 0000000..8b2e655
--- /dev/null
+++ b/includes/Mailer.php
@@ -0,0 +1,243 @@
+db = Database::getInstance();
+ $this->loadSettings();
+ }
+
+ private function loadSettings() {
+ $this->smtpEnabled = $this->db->getSetting('smtp_enabled', '0') === '1';
+ $this->smtpHost = $this->db->getSetting('smtp_host', '');
+ $this->smtpPort = (int)$this->db->getSetting('smtp_port', '587');
+ $this->smtpUsername = $this->db->getSetting('smtp_username', '');
+ $this->smtpPassword = $this->db->getSetting('smtp_password', '');
+ $this->smtpEncryption = $this->db->getSetting('smtp_encryption', 'tls');
+ $this->fromEmail = $this->db->getSetting('smtp_from_email', 'noreply@' . $_SERVER['HTTP_HOST']);
+ $this->fromName = $this->db->getSetting('smtp_from_name', $this->db->getSetting('app_title', 'UniFi Voucher System'));
+ }
+
+ public function send($to, $subject, $body, $isHtml = false) {
+ if (!$this->smtpEnabled || empty($this->smtpHost)) {
+ // Fallback auf PHP mail()
+ return $this->sendWithPhpMail($to, $subject, $body);
+ }
+
+ return $this->sendWithSmtp($to, $subject, $body, $isHtml);
+ }
+
+ private function sendWithPhpMail($to, $subject, $body) {
+ $headers = "From: {$this->fromName} <{$this->fromEmail}>\r\n";
+ $headers .= "Reply-To: {$this->fromEmail}\r\n";
+ $headers .= "Content-Type: text/plain; charset=UTF-8\r\n";
+
+ return mail($to, $subject, $body, $headers);
+ }
+
+ private function sendWithSmtp($to, $subject, $body, $isHtml = false) {
+ try {
+ // Verbindung aufbauen
+ $socket = $this->connectToSmtp();
+
+ // EHLO
+ $this->smtpCommand($socket, "EHLO " . $_SERVER['HTTP_HOST']);
+
+ // STARTTLS wenn nötig
+ if ($this->smtpEncryption === 'tls') {
+ $this->smtpCommand($socket, "STARTTLS");
+ stream_socket_enable_crypto($socket, true, STREAM_CRYPTO_METHOD_TLS_CLIENT);
+ $this->smtpCommand($socket, "EHLO " . $_SERVER['HTTP_HOST']);
+ }
+
+ // AUTH LOGIN
+ $this->smtpCommand($socket, "AUTH LOGIN");
+ $this->smtpCommand($socket, base64_encode($this->smtpUsername));
+ $this->smtpCommand($socket, base64_encode($this->smtpPassword));
+
+ // MAIL FROM
+ $this->smtpCommand($socket, "MAIL FROM:<{$this->fromEmail}>");
+
+ // RCPT TO
+ $this->smtpCommand($socket, "RCPT TO:<{$to}>");
+
+ // DATA
+ $this->smtpCommand($socket, "DATA");
+
+ // Headers
+ $message = "From: {$this->fromName} <{$this->fromEmail}>\r\n";
+ $message .= "To: {$to}\r\n";
+ $message .= "Subject: =?UTF-8?B?" . base64_encode($subject) . "?=\r\n";
+ $message .= "MIME-Version: 1.0\r\n";
+
+ if ($isHtml) {
+ $message .= "Content-Type: text/html; charset=UTF-8\r\n";
+ } else {
+ $message .= "Content-Type: text/plain; charset=UTF-8\r\n";
+ }
+
+ $message .= "\r\n";
+
+ // Body - bei Plain Text Zeilenumbrüche konvertieren
+ if (!$isHtml) {
+ $body = nl2br($body, false); // Für Plain Text
+ $body = str_replace(' ', "\r\n", $body);
+ }
+
+ $message .= $body;
+ $message .= "\r\n.\r\n";
+
+ fwrite($socket, $message);
+ $response = fgets($socket);
+
+ // QUIT
+ $this->smtpCommand($socket, "QUIT");
+ fclose($socket);
+
+ return strpos($response, '250') === 0;
+
+ } catch (Exception $e) {
+ error_log("SMTP Error: " . $e->getMessage());
+ return false;
+ }
+ }
+
+ private function connectToSmtp() {
+ $context = stream_context_create([
+ 'ssl' => [
+ 'verify_peer' => false,
+ 'verify_peer_name' => false,
+ 'allow_self_signed' => true
+ ]
+ ]);
+
+ if ($this->smtpEncryption === 'ssl') {
+ $host = 'ssl://' . $this->smtpHost;
+ } else {
+ $host = $this->smtpHost;
+ }
+
+ $socket = stream_socket_client(
+ $host . ':' . $this->smtpPort,
+ $errno,
+ $errstr,
+ 30,
+ STREAM_CLIENT_CONNECT,
+ $context
+ );
+
+ if (!$socket) {
+ throw new Exception("SMTP Connection failed: $errstr ($errno)");
+ }
+
+ // Willkommensnachricht lesen
+ fgets($socket);
+
+ return $socket;
+ }
+
+ private function smtpCommand($socket, $command) {
+ fwrite($socket, $command . "\r\n");
+ $response = fgets($socket);
+
+ // Prüfen auf Fehler (4xx oder 5xx)
+ if (preg_match('/^[45]/', $response)) {
+ throw new Exception("SMTP Error: $response");
+ }
+
+ return $response;
+ }
+
+ // Vordefinierte E-Mail-Templates
+ public function sendVoucherEmail($to, $voucherCode, $siteName, $maxUses) {
+ $appTitle = $this->db->getSetting('app_title', 'UniFi Voucher System');
+ $instructionHeader = $this->db->getSetting('instruction_header', '');
+ $instructionText = $this->db->getSetting('instruction_text', '');
+
+ // System-URL aus Einstellungen oder automatisch erkennen
+ $systemUrl = $this->db->getSetting('system_url', '');
+ if (empty($systemUrl)) {
+ $protocol = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http';
+ $host = $_SERVER['HTTP_HOST'];
+ $scriptPath = dirname($_SERVER['SCRIPT_NAME']);
+ $scriptPath = $scriptPath === '/' ? '' : $scriptPath;
+ $systemUrl = $protocol . '://' . $host . $scriptPath;
+ }
+
+ // Template aus Datenbank laden
+ $subjectTemplate = $this->db->getSetting('email_voucher_subject', '{APP_TITLE} - Ihr WLAN-Zugang');
+ $bodyTemplate = $this->db->getSetting('email_voucher_body', "Hallo,\n\nIhr WLAN-Zugangscode lautet:\n\n{VOUCHER_CODE} \n\nGültigkeit: 8 Stunden ab Erstellung\nMaximale Geräte: {MAX_USES}\nStandort: {SITE_NAME}\n\n{INSTRUCTIONS}\n\nMit freundlichen Grüßen\n{APP_TITLE}");
+
+ // Anleitung formatieren
+ $instructions = '';
+ if ($instructionText) {
+ $instructions = $instructionHeader . "\n" . $instructionText;
+ }
+
+ // Platzhalter ersetzen
+ $placeholders = [
+ '{VOUCHER_CODE}' => $voucherCode,
+ '{SITE_NAME}' => $siteName,
+ '{MAX_USES}' => $maxUses,
+ '{APP_TITLE}' => $appTitle,
+ '{INSTRUCTIONS}' => $instructions,
+ '{SYSTEM_URL}' => $systemUrl
+ ];
+
+ $subject = str_replace(array_keys($placeholders), array_values($placeholders), $subjectTemplate);
+ $body = str_replace(array_keys($placeholders), array_values($placeholders), $bodyTemplate);
+
+ // HTML oder Plain Text prüfen
+ $isHtml = strip_tags($body) !== $body;
+
+ return $this->send($to, $subject, $body, $isHtml);
+ }
+
+ public function sendUserNotification($to, $userName, $changes) {
+ $appTitle = $this->db->getSetting('app_title', 'UniFi Voucher System');
+
+ // System-URL aus Einstellungen oder automatisch erkennen
+ $systemUrl = $this->db->getSetting('system_url', '');
+ if (empty($systemUrl)) {
+ $protocol = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http';
+ $host = $_SERVER['HTTP_HOST'];
+ $scriptPath = dirname($_SERVER['SCRIPT_NAME']);
+ $scriptPath = $scriptPath === '/' ? '' : $scriptPath;
+ $systemUrl = $protocol . '://' . $host . $scriptPath;
+ }
+
+ // Template aus Datenbank laden
+ $subjectTemplate = $this->db->getSetting('email_user_notification_subject', '{APP_TITLE} - Ihre Berechtigungen wurden geändert');
+ $bodyTemplate = $this->db->getSetting('email_user_notification_body', "Hallo {USER_NAME},\n\nEin Administrator hat Ihre Berechtigungen im {APP_TITLE} geändert:\n\n{CHANGES}\n\nSie können sich unter folgender Adresse anmelden:\n{SYSTEM_URL}\n\nMit freundlichen Grüßen\n{APP_TITLE}");
+
+ // Änderungen formatieren
+ $changesText = '';
+ foreach ($changes as $change) {
+ $changesText .= "• $change\n";
+ }
+
+ // Platzhalter ersetzen
+ $placeholders = [
+ '{USER_NAME}' => $userName,
+ '{CHANGES}' => $changesText,
+ '{APP_TITLE}' => $appTitle,
+ '{SYSTEM_URL}' => $systemUrl
+ ];
+
+ $subject = str_replace(array_keys($placeholders), array_values($placeholders), $subjectTemplate);
+ $body = str_replace(array_keys($placeholders), array_values($placeholders), $bodyTemplate);
+
+ // HTML oder Plain Text prüfen
+ $isHtml = strip_tags($body) !== $body;
+
+ return $this->send($to, $subject, $body, $isHtml);
+ }
+}
\ No newline at end of file
diff --git a/includes/UniFiController.php b/includes/UniFiController.php
new file mode 100644
index 0000000..f279bb0
--- /dev/null
+++ b/includes/UniFiController.php
@@ -0,0 +1,329 @@
+controllerUrl = rtrim($controllerUrl, '/');
+ $this->username = $username;
+ $this->password = $password;
+ $this->siteId = $siteId;
+ $this->cookieFile = tempnam(sys_get_temp_dir(), 'UNIFI_');
+ }
+
+ public function __destruct() {
+ if (file_exists($this->cookieFile)) {
+ unlink($this->cookieFile);
+ }
+ }
+
+ // Login zum Controller
+ private function login() {
+ $ch = curl_init();
+
+ curl_setopt_array($ch, [
+ CURLOPT_URL => $this->controllerUrl . "/api/login",
+ CURLOPT_POST => true,
+ CURLOPT_POSTFIELDS => json_encode([
+ 'username' => $this->username,
+ 'password' => $this->password
+ ]),
+ CURLOPT_RETURNTRANSFER => true,
+ CURLOPT_SSL_VERIFYPEER => false,
+ CURLOPT_COOKIEJAR => $this->cookieFile,
+ CURLOPT_COOKIEFILE => $this->cookieFile,
+ CURLOPT_HTTPHEADER => ['Content-Type: application/json']
+ ]);
+
+ $response = curl_exec($ch);
+ $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
+ curl_close($ch);
+
+ if ($httpCode !== 200) {
+ throw new Exception("Login fehlgeschlagen: HTTP $httpCode");
+ }
+
+ $data = json_decode($response, true);
+
+ if (!isset($data['meta']['rc']) || $data['meta']['rc'] !== 'ok') {
+ throw new Exception("Login fehlgeschlagen: Ungültige Antwort");
+ }
+
+ return true;
+ }
+
+ // API-Request ausführen
+ private function apiRequest($endpoint, $data = null, $method = 'POST') {
+ $this->login();
+
+ $ch = curl_init();
+ $url = $this->controllerUrl . $endpoint;
+
+ $options = [
+ CURLOPT_URL => $url,
+ CURLOPT_RETURNTRANSFER => true,
+ CURLOPT_SSL_VERIFYPEER => false,
+ CURLOPT_COOKIEFILE => $this->cookieFile,
+ CURLOPT_HTTPHEADER => ['Content-Type: application/json']
+ ];
+
+ if ($method === 'POST' && $data !== null) {
+ $options[CURLOPT_POST] = true;
+ $options[CURLOPT_POSTFIELDS] = json_encode($data);
+ }
+
+ curl_setopt_array($ch, $options);
+
+ $response = curl_exec($ch);
+ $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
+ $error = curl_error($ch);
+ curl_close($ch);
+
+ if ($error) {
+ throw new Exception("cURL Fehler: $error");
+ }
+
+ if ($httpCode !== 200) {
+ throw new Exception("API Request fehlgeschlagen: HTTP $httpCode");
+ }
+
+ return json_decode($response, true);
+ }
+
+ // Voucher erstellen
+ public function createVoucher($voucherName, $maxUses, $expireMinutes = 480) {
+ $data = [
+ 'cmd' => 'create-voucher',
+ 'expire' => (int)$expireMinutes,
+ 'n' => 1,
+ 'note' => $voucherName,
+ 'quota' => (int)$maxUses
+ ];
+
+ $response = $this->apiRequest("/api/s/{$this->siteId}/cmd/hotspot", $data);
+
+ if (!isset($response['data'][0]['create_time'])) {
+ throw new Exception("Voucher konnte nicht erstellt werden");
+ }
+
+ // Voucher-Code abrufen
+ $vouchers = $this->getVouchers();
+
+ if (empty($vouchers)) {
+ throw new Exception("Voucher-Code konnte nicht abgerufen werden");
+ }
+
+ // Neuesten Voucher zurückgeben
+ $latestVoucher = reset($vouchers);
+
+ return [
+ 'code' => $latestVoucher['code'],
+ 'formatted_code' => $this->formatVoucherCode($latestVoucher['code']),
+ 'unifi_id' => $latestVoucher['_id'] ?? null,
+ 'create_time' => $latestVoucher['create_time'] ?? null
+ ];
+ }
+
+ // Alle Voucher abrufen
+ public function getVouchers() {
+ $response = $this->apiRequest("/api/s/{$this->siteId}/stat/voucher");
+ return $response['data'] ?? [];
+ }
+
+ // Voucher mit formatierten Details abrufen
+ public function getVouchersWithDetails() {
+ $vouchers = $this->getVouchers();
+ $result = [];
+
+ foreach ($vouchers as $voucher) {
+ $createTime = isset($voucher['create_time']) ? $voucher['create_time'] : 0;
+ $duration = isset($voucher['duration']) ? $voucher['duration'] : 0; // in Minuten
+ $expireTime = $createTime + ($duration * 60);
+ $now = time();
+
+ // Status bestimmen
+ $status = 'valid';
+ $usedCount = isset($voucher['used']) ? $voucher['used'] : 0;
+ $quota = isset($voucher['quota']) ? $voucher['quota'] : 0;
+
+ if ($now > $expireTime) {
+ $status = 'expired';
+ } elseif ($quota > 0 && $usedCount >= $quota) {
+ $status = 'used';
+ }
+
+ $result[] = [
+ '_id' => $voucher['_id'] ?? '',
+ 'code' => $voucher['code'] ?? '',
+ 'formatted_code' => $this->formatVoucherCode($voucher['code'] ?? ''),
+ 'note' => $voucher['note'] ?? '',
+ 'quota' => $quota,
+ 'used' => $usedCount,
+ 'duration' => $duration,
+ 'create_time' => $createTime,
+ 'expire_time' => $expireTime,
+ 'status' => $status,
+ 'status_expires' => isset($voucher['status_expires']) ? $voucher['status_expires'] : null,
+ 'for_hotspot' => isset($voucher['for_hotspot']) ? $voucher['for_hotspot'] : false,
+ 'qos_overwrite' => isset($voucher['qos_overwrite']) ? $voucher['qos_overwrite'] : false,
+ 'qos_usage_quota' => isset($voucher['qos_usage_quota']) ? $voucher['qos_usage_quota'] : null,
+ 'qos_rate_max_up' => isset($voucher['qos_rate_max_up']) ? $voucher['qos_rate_max_up'] : null,
+ 'qos_rate_max_down' => isset($voucher['qos_rate_max_down']) ? $voucher['qos_rate_max_down'] : null
+ ];
+ }
+
+ // Nach Erstellungsdatum sortieren (neueste zuerst)
+ usort($result, function($a, $b) {
+ return $b['create_time'] - $a['create_time'];
+ });
+
+ return $result;
+ }
+
+ // Voucher-Code formatieren (xxxxx-xxxxx-xxxxx)
+ private function formatVoucherCode($code) {
+ $chunks = str_split($code, 5);
+ return implode('-', $chunks);
+ }
+
+ // Voucher löschen
+ public function deleteVoucher($voucherId) {
+ $data = [
+ 'cmd' => 'delete-voucher',
+ '_id' => $voucherId
+ ];
+
+ $response = $this->apiRequest("/api/s/{$this->siteId}/cmd/hotspot", $data);
+
+ return isset($response['meta']['rc']) && $response['meta']['rc'] === 'ok';
+ }
+
+ // Verbindung testen
+ public static function testConnection($controllerUrl, $username, $password, $siteId) {
+ try {
+ $controller = new self($controllerUrl, $username, $password, $siteId);
+ $controller->login();
+ return true;
+ } catch (Exception $e) {
+ return $e->getMessage();
+ }
+ }
+
+ /**
+ * Synchronisiert alle Voucher vom UniFi Controller in die Datenbank
+ * @param Database $db Datenbank-Instanz
+ * @param int $dbSiteId Die Site-ID in der lokalen Datenbank
+ * @return array Statistiken über die Synchronisation
+ */
+ public function syncVouchersToDatabase($db, $dbSiteId) {
+ $vouchers = $this->getVouchersWithDetails();
+ $stats = [
+ 'total' => count($vouchers),
+ 'new' => 0,
+ 'updated' => 0,
+ 'deleted' => 0,
+ 'valid' => 0,
+ 'used' => 0,
+ 'expired' => 0
+ ];
+
+ // Alle aktuellen UniFi-IDs sammeln
+ $unifiIds = [];
+
+ foreach ($vouchers as $voucher) {
+ $unifiIds[] = $voucher['_id'];
+
+ // Status zählen
+ $stats[$voucher['status']]++;
+
+ // Prüfen ob Voucher bereits existiert
+ $existing = $db->fetchOne(
+ "SELECT id, status, used_count FROM vouchers WHERE unifi_voucher_id = ? AND site_id = ?",
+ [$voucher['_id'], $dbSiteId]
+ );
+
+ $expiresAt = date('Y-m-d H:i:s', $voucher['expire_time']);
+ $createdAt = date('Y-m-d H:i:s', $voucher['create_time']);
+
+ if ($existing) {
+ // Voucher aktualisieren
+ $db->execute(
+ "UPDATE vouchers SET
+ status = ?,
+ used_count = ?,
+ expires_at = ?,
+ last_sync = NOW()
+ WHERE id = ?",
+ [$voucher['status'], $voucher['used'], $expiresAt, $existing['id']]
+ );
+ $stats['updated']++;
+ } else {
+ // Neuen Voucher einfügen
+ $db->execute(
+ "INSERT INTO vouchers
+ (site_id, voucher_code, voucher_name, max_uses, expire_minutes,
+ unifi_voucher_id, status, used_count, expires_at, created_at,
+ synced_from_unifi, last_sync)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, NOW())",
+ [
+ $dbSiteId,
+ $voucher['formatted_code'],
+ $voucher['note'] ?: 'Importiert aus UniFi',
+ $voucher['quota'],
+ $voucher['duration'],
+ $voucher['_id'],
+ $voucher['status'],
+ $voucher['used'],
+ $expiresAt,
+ $createdAt
+ ]
+ );
+ $stats['new']++;
+ }
+ }
+
+ // Voucher die nicht mehr im Controller existieren als gelöscht markieren
+ if (!empty($unifiIds)) {
+ $placeholders = implode(',', array_fill(0, count($unifiIds), '?'));
+ $params = array_merge($unifiIds, [$dbSiteId]);
+
+ // Alle Voucher mit UniFi-ID die nicht mehr existieren auf "expired" setzen
+ $deleted = $db->execute(
+ "UPDATE vouchers SET status = 'expired', last_sync = NOW()
+ WHERE unifi_voucher_id IS NOT NULL
+ AND unifi_voucher_id NOT IN ($placeholders)
+ AND site_id = ?
+ AND status != 'expired'",
+ $params
+ );
+ $stats['deleted'] = $deleted;
+ }
+
+ return $stats;
+ }
+
+ /**
+ * Holt Live-Statistiken für das Dashboard
+ * @return array
+ */
+ public function getLiveStats() {
+ $vouchers = $this->getVouchersWithDetails();
+
+ $stats = [
+ 'total' => count($vouchers),
+ 'valid' => 0,
+ 'used' => 0,
+ 'expired' => 0,
+ 'vouchers' => $vouchers
+ ];
+
+ foreach ($vouchers as $voucher) {
+ $stats[$voucher['status']]++;
+ }
+
+ return $stats;
+ }
+}
\ No newline at end of file
diff --git a/index.php b/index.php
new file mode 100644
index 0000000..140c627
--- /dev/null
+++ b/index.php
@@ -0,0 +1,654 @@
+getSetting('app_title', 'UniFi Voucher System');
+$logoUrl = $db->getSetting('logo_url', '');
+$instructionHeader = $db->getSetting('instruction_header', 'So verwenden Sie Ihren Code');
+$instructionText = $db->getSetting('instruction_text', '');
+$publicAccess = $db->getSetting('public_access', 0);
+$printTemplate = $db->getSetting('print_template', '
+
{APP_TITLE}
+
WLAN Zugangscode
+
{VOUCHER_CODE}
+
Gültig bis: {EXPIRY_DATE} um {EXPIRY_TIME} Uhr
+
Standort: {SITE_NAME}
+
Maximale Geräte: {MAX_USES}
+
+
+ {INSTRUCTIONS}
+
+
');
+
+// Prüfen ob Login erforderlich - aber nur wenn nicht auf login.php
+if (!$publicAccess && !$auth->isLoggedIn()) {
+ header('Location: login.php');
+ exit;
+}
+
+$error = '';
+$success = '';
+$voucherCode = '';
+$voucherCreated = false;
+$voucherData = [];
+
+// Sites abrufen
+if ($auth->isLoggedIn()) {
+ if ($auth->isAdmin()) {
+ $sites = $db->fetchAll("SELECT * FROM sites WHERE is_active = 1 ORDER BY name");
+ } else {
+ $sites = $db->fetchAll(
+ "SELECT s.* FROM sites s
+ INNER JOIN user_site_access usa ON s.id = usa.site_id
+ WHERE s.is_active = 1 AND usa.user_id = ?
+ ORDER BY s.name",
+ [$_SESSION['user_id']]
+ );
+ }
+} else {
+ // Öffentlicher Zugriff - nur Sites mit public_access
+ $sites = $db->fetchAll("SELECT * FROM sites WHERE is_active = 1 AND public_access = 1 ORDER BY name");
+}
+
+// Voucher erstellen
+// WICHTIG: create_voucher kommt jetzt aus einem hidden input, nicht vom Button (disabled Buttons werden teils nicht gesendet)
+if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['create_voucher'])) {
+ if (!$publicAccess && !$auth->isLoggedIn()) {
+ $error = 'Sie müssen angemeldet sein';
+ } elseif ($auth->isLoggedIn() && !$auth->validateCsrfToken($_POST['csrf_token'] ?? '')) {
+ $error = 'Ungültiges Sicherheits-Token';
+ } else {
+ try {
+ $siteId = (int)($_POST['site_id'] ?? 0);
+ $voucherName = trim((string)($_POST['voucher_name'] ?? ''));
+ $maxUses = (int)($_POST['max_uses'] ?? 0);
+ $sendEmail = isset($_POST['send_email']) && !empty($_POST['recipient_email']);
+ $recipientEmail = trim((string)($_POST['recipient_email'] ?? ''));
+
+ // Validierung
+ if (empty($voucherName)) {
+ throw new Exception('Bitte geben Sie einen Voucher-Namen ein');
+ }
+
+ if ($maxUses < 1 || $maxUses > 10) {
+ throw new Exception('Anzahl der Geräte muss zwischen 1 und 10 liegen');
+ }
+
+ if ($siteId <= 0) {
+ throw new Exception('Bitte wählen Sie einen Standort');
+ }
+
+ if ($sendEmail && !filter_var($recipientEmail, FILTER_VALIDATE_EMAIL)) {
+ throw new Exception('Ungültige E-Mail-Adresse');
+ }
+
+ // Site-Zugriff prüfen
+ if ($auth->isLoggedIn() && !$auth->hasAccessToSite($siteId)) {
+ throw new Exception('Keine Berechtigung für diese Site');
+ }
+
+ // Site-Daten abrufen
+ $site = $db->fetchOne("SELECT * FROM sites WHERE id = ? AND is_active = 1", [$siteId]);
+
+ if (!$site) {
+ throw new Exception('Site nicht gefunden');
+ }
+
+ // Voucher-Namen formatieren
+ $datum = date('Y-m-d');
+ $fullVoucherName = $datum . '_' . $voucherName;
+
+ // UniFi Controller initialisieren
+ $controller = new UniFiController(
+ $site['unifi_controller_url'],
+ $site['unifi_username'],
+ $site['unifi_password'],
+ $site['site_id']
+ );
+
+ // Voucher erstellen
+ $voucher = $controller->createVoucher($fullVoucherName, $maxUses, 480);
+
+ if (!is_array($voucher) || empty($voucher['formatted_code']) || empty($voucher['code'])) {
+ throw new Exception('UniFi hat keinen gültigen Voucher zurückgegeben');
+ }
+
+ $voucherCode = $voucher['formatted_code'];
+
+ // In Datenbank speichern
+ $userId = $auth->isLoggedIn() ? ($_SESSION['user_id'] ?? null) : null;
+ $db->execute(
+ "INSERT INTO vouchers (site_id, user_id, voucher_code, voucher_name, max_uses, expire_minutes, unifi_voucher_id)
+ VALUES (?, ?, ?, ?, ?, 480, ?)",
+ [$siteId, $userId, $voucher['code'], $fullVoucherName, $maxUses, ($voucher['unifi_id'] ?? null)]
+ );
+
+ // Voucher-Daten für Druck speichern
+ $expiryTimestamp = time() + (480 * 60);
+ $voucherData = [
+ 'code' => $voucherCode,
+ 'site_name' => $site['name'],
+ 'max_uses' => $maxUses,
+ 'expiry_date' => date('d.m.Y', $expiryTimestamp),
+ 'expiry_time' => date('H:i', $expiryTimestamp)
+ ];
+
+ // E-Mail versenden falls gewünscht
+ if ($sendEmail && !empty($recipientEmail)) {
+ if ($mailer->sendVoucherEmail($recipientEmail, $voucherCode, $site['name'], $maxUses)) {
+ $success .= ' E-Mail wurde erfolgreich versendet!';
+ } else {
+ $success .= ' (E-Mail konnte nicht versendet werden)';
+ }
+ }
+
+ $voucherCreated = true;
+ $success = 'Voucher erfolgreich erstellt!' . ($sendEmail ? ' E-Mail wurde versendet.' : '');
+ } catch (Exception $e) {
+ $error = 'Fehler: ' . $e->getMessage();
+ }
+ }
+}
+
+$currentUser = $auth->isLoggedIn() ? $auth->getCurrentUser() : null;
+
+// Auto-select wenn nur eine Site
+$autoSelectSite = (count($sites) === 1) ? $sites[0]['id'] : 0;
+?>
+
+
+
+
+
+ = htmlspecialchars($appTitle) ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
= htmlspecialchars($appTitle) ?>
+
+
+
= htmlspecialchars($error) ?>
+
+
+
+
+
+
✓ Ihr Zugangs-Code
+
= htmlspecialchars($voucherCode) ?>
+
+ Der Code ist 8 Stunden ab Erstellung gültig
+
+
+
+
+
+
+
= htmlspecialchars($instructionHeader) ?>
+
+
+
= $instructionText ?>
+
+
+
+
+
+
+ Weiteren Code erstellen
+
+
+
+
+
+
+ Code ausdrucken
+
+
+
+
+
+
+
+
+
+
+ isLoggedIn()): ?>
+
+
+
+
+ Voucher-Name *
+
+
+
+
+ Wie viele Geräte dürfen sich einloggen? *
+
+
+
+
+ Standort *
+
+ 1): ?>
+ Bitte wählen...
+
+
+ >
+ = htmlspecialchars($site['name']) ?>
+
+
+
+
+
+
+
+
+
+
+
+
+ Code per E-Mail versenden
+
+
+
+
+ E-Mail-Adresse des Empfängers
+
+
+
+
+
+
+ Voucher erstellen
+
+
+
+
+
+
+
= htmlspecialchars($instructionHeader) ?>
+
+
+
= $instructionText ?>
+
+
+
+
+
+
+
+
+
+
+
diff --git a/install.php b/install.php
new file mode 100644
index 0000000..ba103b0
--- /dev/null
+++ b/install.php
@@ -0,0 +1,461 @@
+setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
+
+ // Datenbank erstellen falls nicht vorhanden
+ $pdo->exec("CREATE DATABASE IF NOT EXISTS `$db_name` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci");
+ $pdo->exec("USE `$db_name`");
+
+ // Tabellen erstellen
+ $sql = file_get_contents(__DIR__ . '/database.sql');
+ $pdo->exec($sql);
+
+ $_SESSION['install_db'] = [
+ 'host' => $db_host,
+ 'name' => $db_name,
+ 'user' => $db_user,
+ 'pass' => $db_pass
+ ];
+
+ } catch (PDOException $e) {
+ $errors[] = "Datenbankfehler: " . $e->getMessage();
+ $step = 1;
+ }
+}
+
+// Step 2: Admin-Account erstellen
+if ($step === 3 && $_SERVER['REQUEST_METHOD'] === 'POST') {
+ $admin_email = filter_var($_POST['admin_email'] ?? '', FILTER_VALIDATE_EMAIL);
+ $admin_name = $_POST['admin_name'] ?? '';
+ $admin_password = $_POST['admin_password'] ?? '';
+ $admin_password_confirm = $_POST['admin_password_confirm'] ?? '';
+
+ if (!$admin_email) {
+ $errors[] = "Ungültige E-Mail-Adresse";
+ $step = 2;
+ } elseif (strlen($admin_password) < 8) {
+ $errors[] = "Passwort muss mindestens 8 Zeichen lang sein";
+ $step = 2;
+ } elseif ($admin_password !== $admin_password_confirm) {
+ $errors[] = "Passwörter stimmen nicht überein";
+ $step = 2;
+ } else {
+ $_SESSION['install_admin'] = [
+ 'email' => $admin_email,
+ 'name' => $admin_name,
+ 'password' => password_hash($admin_password, PASSWORD_DEFAULT)
+ ];
+ }
+}
+
+// Step 3: Allgemeine Einstellungen
+if ($step === 4 && $_SERVER['REQUEST_METHOD'] === 'POST') {
+ $app_title = $_POST['app_title'] ?? 'UniFi Voucher System';
+ $logo_url = $_POST['logo_url'] ?? '';
+ $instruction_header = $_POST['instruction_header'] ?? '';
+ $instruction_text = $_POST['instruction_text'] ?? '';
+ $public_access = isset($_POST['public_access']) ? 1 : 0;
+
+ // Microsoft 365 OAuth (optional)
+ $m365_client_id = $_POST['m365_client_id'] ?? '';
+ $m365_client_secret = $_POST['m365_client_secret'] ?? '';
+ $m365_tenant_id = $_POST['m365_tenant_id'] ?? '';
+
+ $_SESSION['install_settings'] = [
+ 'app_title' => $app_title,
+ 'logo_url' => $logo_url,
+ 'instruction_header' => $instruction_header,
+ 'instruction_text' => $instruction_text,
+ 'public_access' => $public_access,
+ 'm365_client_id' => $m365_client_id,
+ 'm365_client_secret' => $m365_client_secret,
+ 'm365_tenant_id' => $m365_tenant_id
+ ];
+}
+
+// Step 4: Installation abschließen
+if ($step === 5 && $_SERVER['REQUEST_METHOD'] === 'POST') {
+ try {
+ $db = $_SESSION['install_db'];
+ $admin = $_SESSION['install_admin'];
+ $settings = $_SESSION['install_settings'];
+
+ $pdo = new PDO("mysql:host={$db['host']};dbname={$db['name']};charset=utf8mb4", $db['user'], $db['pass']);
+ $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
+
+ // Admin-User erstellen
+ $stmt = $pdo->prepare("INSERT INTO users (email, name, password_hash, is_admin, is_active) VALUES (?, ?, ?, 1, 1)");
+ $stmt->execute([$admin['email'], $admin['name'], $admin['password']]);
+
+ // Settings speichern
+ $settingsData = [
+ 'app_title' => $settings['app_title'],
+ 'logo_url' => $settings['logo_url'],
+ 'instruction_header' => $settings['instruction_header'],
+ 'instruction_text' => $settings['instruction_text'],
+ 'public_access' => $settings['public_access'],
+ 'm365_client_id' => $settings['m365_client_id'],
+ 'm365_client_secret' => $settings['m365_client_secret'],
+ 'm365_tenant_id' => $settings['m365_tenant_id']
+ ];
+
+ $stmt = $pdo->prepare("INSERT INTO settings (setting_key, setting_value) VALUES (?, ?) ON DUPLICATE KEY UPDATE setting_value = VALUES(setting_value)");
+ foreach ($settingsData as $key => $value) {
+ $stmt->execute([$key, $value]);
+ }
+
+ // config.php erstellen
+ $configContent = "\n";
+ $htaccess .= " Order Allow,Deny\n";
+ $htaccess .= " Deny from all\n";
+ $htaccess .= "\n\n";
+ $htaccess .= "DirectoryIndex index.php\n";
+ file_put_contents(__DIR__ . '/.htaccess', $htaccess);
+
+ $success = true;
+
+ // Session-Daten löschen
+ unset($_SESSION['install_db'], $_SESSION['install_admin'], $_SESSION['install_settings']);
+
+ } catch (Exception $e) {
+ $errors[] = "Fehler bei der Installation: " . $e->getMessage();
+ $step = 4;
+ }
+}
+?>
+
+
+
+
+
+ UniFi Voucher System - Installation
+
+
+
+
+
🚀 UniFi Voucher System
+
Installation
+
+
+
+
+
+
+
= htmlspecialchars($error) ?>
+
+
+
+
+
+
+ ✓ Installation erfolgreich abgeschlossen!
+ Sie können sich jetzt mit Ihren Admin-Zugangsdaten anmelden.
+
+
Zum Login
+
+
+
+ Schritt 1: Datenbank-Konfiguration
+
+
+
+
+
+
+ Datenbank-Benutzer
+
+
+
+
+ Datenbank-Passwort
+
+
+
+ Weiter →
+
+
+
+
+
+ Schritt 2: Administrator-Account
+
+
+ Name
+
+
+
+
+ E-Mail
+
+
+
+
+
+
+ Passwort bestätigen
+
+
+
+ Weiter →
+
+
+
+
+
+ Schritt 3: Allgemeine Einstellungen
+
+
+ Anwendungs-Titel
+
+
+
+
+ Logo-URL (optional)
+
+
+
+
+ Anleitung Überschrift
+
+
+
+
+ Anleitung Text
+ Verbinden Sie sich mit dem WLAN und geben Sie den Code auf der Anmeldeseite ein.
+
+
+
+
+ Öffentlicher Zugriff auf Code-Erstellung
+
+
+
+
Microsoft 365 Login (Optional)
+
+ Client ID
+
+
+
+ Client Secret
+
+
+
+ Tenant ID
+
+
+
Leer lassen, wenn M365-Login nicht verwendet werden soll
+
+
+ Weiter →
+
+
+
+
+
+ Schritt 4: Installation abschließen
+
+
+ Klicken Sie auf "Installation abschließen", um die Einrichtung zu beenden.
+ Die Datenbank und alle notwendigen Dateien werden erstellt.
+
+
+ Installation abschließen
+
+
+
+
+
\ No newline at end of file
diff --git a/login.php b/login.php
new file mode 100644
index 0000000..2536e30
--- /dev/null
+++ b/login.php
@@ -0,0 +1,329 @@
+isLoggedIn()) {
+ header('Location: index.php');
+ exit;
+ }
+} catch (Exception $e) {
+ die('Fehler beim Initialisieren: ' . $e->getMessage());
+}
+
+$error = '';
+$success = '';
+
+// Login-Verarbeitung
+if ($_SERVER['REQUEST_METHOD'] === 'POST') {
+ try {
+ $email = trim($_POST['email'] ?? '');
+ $password = $_POST['password'] ?? '';
+
+ if (empty($email) || empty($password)) {
+ $error = 'Bitte E-Mail und Passwort eingeben';
+ } elseif ($auth->login($email, $password)) {
+ // Nach erfolgreichem Login zu index.php
+ header('Location: index.php');
+ exit;
+ } else {
+ $error = 'Ungültige E-Mail oder Passwort';
+ }
+ } catch (Exception $e) {
+ $error = 'Login-Fehler: ' . $e->getMessage();
+ }
+}
+
+try {
+ $db = Database::getInstance();
+ $appTitle = $db->getSetting('app_title', 'UniFi Voucher System');
+ $logoUrl = $db->getSetting('logo_url', '');
+
+ // M365 aktiviert prüfen - ALLE drei Felder müssen ausgefüllt sein
+ $m365ClientId = $db->getSetting('m365_client_id', '');
+ $m365ClientSecret = $db->getSetting('m365_client_secret', '');
+ $m365TenantId = $db->getSetting('m365_tenant_id', '');
+
+ $m365Enabled = !empty($m365ClientId) &&
+ !empty($m365ClientSecret) &&
+ !empty($m365TenantId);
+
+ $publicAccess = $db->getSetting('public_access', 0);
+
+ // M365 OAuth URL generieren falls aktiviert
+ $m365LoginUrl = '';
+ if ($m365Enabled) {
+ // Dynamische Redirect URI basierend auf aktuellem Pfad
+ $protocol = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http';
+ $host = $_SERVER['HTTP_HOST'];
+ $scriptPath = dirname($_SERVER['SCRIPT_NAME']);
+ $scriptPath = $scriptPath === '/' ? '' : $scriptPath;
+ $redirectUri = $protocol . '://' . $host . $scriptPath . '/m365_callback.php';
+
+ $params = [
+ 'client_id' => $m365ClientId,
+ 'response_type' => 'code',
+ 'redirect_uri' => $redirectUri,
+ 'response_mode' => 'query',
+ 'scope' => 'openid profile email User.Read',
+ 'state' => bin2hex(random_bytes(16))
+ ];
+
+ $_SESSION['m365_state'] = $params['state'];
+
+ $m365LoginUrl = "https://login.microsoftonline.com/$m365TenantId/oauth2/v2.0/authorize?" . http_build_query($params);
+ }
+
+ // Prüfen ob alternative Login-Form (Benutzername/Passwort) angezeigt werden soll
+ $showLocalLogin = isset($_GET['local']) && $_GET['local'] === '1';
+
+} catch (Exception $e) {
+ die('Datenbankfehler: ' . $e->getMessage());
+}
+?>
+
+
+
+
+
+ Login - = htmlspecialchars($appTitle) ?>
+
+
+
+
+
+
+
diff --git a/login_simple.php b/login_simple.php
new file mode 100644
index 0000000..602704b
--- /dev/null
+++ b/login_simple.php
@@ -0,0 +1,328 @@
+getMessage();
+}
+
+try {
+ if (!file_exists(__DIR__ . '/includes/Database.php')) {
+ throw new Exception('includes/Database.php nicht gefunden');
+ }
+ require_once __DIR__ . '/includes/Database.php';
+} catch (Exception $e) {
+ $loadErrors[] = "Database: " . $e->getMessage();
+}
+
+try {
+ if (!file_exists(__DIR__ . '/includes/Auth.php')) {
+ throw new Exception('includes/Auth.php nicht gefunden');
+ }
+ require_once __DIR__ . '/includes/Auth.php';
+} catch (Exception $e) {
+ $loadErrors[] = "Auth: " . $e->getMessage();
+}
+
+// Wenn Ladefehler aufgetreten sind, zeige sie an
+if (!empty($loadErrors)) {
+ die('Fehler beim Laden der Dateien ' . implode(' ', $loadErrors) . ' ');
+}
+
+// Ab hier normal weiter
+try {
+ $auth = new Auth();
+} catch (Exception $e) {
+ die('Fehler bei Auth-Initialisierung ' . $e->getMessage() . '
');
+}
+
+// Wenn bereits eingeloggt, weiterleiten
+if ($auth->isLoggedIn()) {
+ header('Location: index.php');
+ exit;
+}
+
+$error = '';
+$success = '';
+
+// Login-Verarbeitung
+if ($_SERVER['REQUEST_METHOD'] === 'POST') {
+ try {
+ $email = $_POST['email'] ?? '';
+ $password = $_POST['password'] ?? '';
+
+ if (empty($email) || empty($password)) {
+ $error = 'Bitte E-Mail und Passwort eingeben';
+ } elseif ($auth->login($email, $password)) {
+ header('Location: index.php');
+ exit;
+ } else {
+ $error = 'Ungültige E-Mail oder Passwort';
+ }
+ } catch (Exception $e) {
+ $error = 'Login-Fehler: ' . $e->getMessage();
+ }
+}
+
+try {
+ $db = Database::getInstance();
+ $appTitle = $db->getSetting('app_title', 'UniFi Voucher System');
+ $logoUrl = $db->getSetting('logo_url', '');
+ $m365Enabled = !empty($db->getSetting('m365_client_id')) &&
+ !empty($db->getSetting('m365_client_secret')) &&
+ !empty($db->getSetting('m365_tenant_id'));
+ $publicAccess = $db->getSetting('public_access', 0);
+
+ // M365 OAuth URL generieren falls aktiviert
+ $m365LoginUrl = '';
+ if ($m365Enabled) {
+ $clientId = $db->getSetting('m365_client_id');
+ $tenantId = $db->getSetting('m365_tenant_id');
+
+ // Dynamische Redirect URI basierend auf aktuellem Pfad
+ $protocol = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http';
+ $host = $_SERVER['HTTP_HOST'];
+ $scriptPath = dirname($_SERVER['SCRIPT_NAME']);
+ $scriptPath = $scriptPath === '/' ? '' : $scriptPath;
+ $redirectUri = $protocol . '://' . $host . $scriptPath . '/m365_callback.php';
+
+ $params = [
+ 'client_id' => $clientId,
+ 'response_type' => 'code',
+ 'redirect_uri' => $redirectUri,
+ 'response_mode' => 'query',
+ 'scope' => 'openid profile email User.Read',
+ 'state' => bin2hex(random_bytes(16))
+ ];
+
+ $_SESSION['m365_state'] = $params['state'];
+
+ $m365LoginUrl = "https://login.microsoftonline.com/$tenantId/oauth2/v2.0/authorize?" . http_build_query($params);
+ }
+} catch (Exception $e) {
+ die('Datenbankfehler ' . $e->getMessage() . '
');
+}
+?>
+
+
+
+
+
+ Login - = htmlspecialchars($appTitle) ?>
+
+
+
+
+
+
+
+
= htmlspecialchars($appTitle) ?>
+
+
+
Melden Sie sich an, um fortzufahren
+
+
+
= htmlspecialchars($error) ?>
+
+
+
+
= htmlspecialchars($success) ?>
+
+
+
+
+ E-Mail
+
+
+
+
+ Passwort
+
+
+
+ Anmelden
+
+
+
+
oder
+
+ 🔷 Mit Microsoft 365 anmelden
+
+
+
+
+
← Zurück zur Code-Erstellung
+
+
+
+
+ System-Status:
+ PHP Version: = phpversion() ?>
+ Session Status: = session_status() === PHP_SESSION_ACTIVE ? 'Aktiv' : 'Inaktiv' ?>
+ Eingeloggt: = $auth->isLoggedIn() ? 'Ja' : 'Nein' ?>
+
+
+
+
\ No newline at end of file
diff --git a/logout.php b/logout.php
new file mode 100644
index 0000000..e31ebac
--- /dev/null
+++ b/logout.php
@@ -0,0 +1,10 @@
+logout();
+
+header('Location: index.php');
+exit;
\ No newline at end of file
diff --git a/m365_callback.php b/m365_callback.php
new file mode 100644
index 0000000..2e148d5
--- /dev/null
+++ b/m365_callback.php
@@ -0,0 +1,118 @@
+getSetting('m365_client_id', '');
+$clientSecret = $db->getSetting('m365_client_secret', '');
+$tenantId = $db->getSetting('m365_tenant_id', '');
+
+if (empty($clientId) || empty($clientSecret) || empty($tenantId)) {
+ die('Microsoft 365 ist nicht konfiguriert. Bitte kontaktieren Sie Ihren Administrator. Zurück zum Login ');
+}
+
+// Dynamische Redirect URI
+$protocol = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http';
+$host = $_SERVER['HTTP_HOST'];
+$scriptPath = dirname($_SERVER['SCRIPT_NAME']);
+$scriptPath = $scriptPath === '/' ? '' : $scriptPath;
+$redirectUri = $protocol . '://' . $host . $scriptPath . '/m365_callback.php';
+
+// Fehlerbehandlung
+if (isset($_GET['error'])) {
+ $error = htmlspecialchars($_GET['error']);
+ $errorDesc = htmlspecialchars($_GET['error_description'] ?? 'Unbekannter Fehler');
+ die("Microsoft 365 Login-Fehler: $error $errorDescZurück zum Login ");
+}
+
+// Authorization Code erhalten
+if (isset($_GET['code'])) {
+ $code = $_GET['code'];
+
+ // Token anfordern
+ $tokenUrl = "https://login.microsoftonline.com/$tenantId/oauth2/v2.0/token";
+
+ $postData = [
+ 'client_id' => $clientId,
+ 'client_secret' => $clientSecret,
+ 'code' => $code,
+ 'redirect_uri' => $redirectUri,
+ 'grant_type' => 'authorization_code',
+ 'scope' => 'openid profile email User.Read'
+ ];
+
+ $ch = curl_init($tokenUrl);
+ curl_setopt($ch, CURLOPT_POST, true);
+ curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($postData));
+ curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+ curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/x-www-form-urlencoded']);
+
+ $response = curl_exec($ch);
+ $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
+ curl_close($ch);
+
+ if ($httpCode !== 200) {
+ die("Fehler beim Token-Abruf (HTTP $httpCode): " . htmlspecialchars($response) . "Zurück zum Login ");
+ }
+
+ $tokenData = json_decode($response, true);
+
+ if (!isset($tokenData['access_token'])) {
+ die("Kein Access Token erhalten: " . htmlspecialchars($response) . "Zurück zum Login ");
+ }
+
+ $accessToken = $tokenData['access_token'];
+
+ // Benutzer-Informationen abrufen
+ $userUrl = 'https://graph.microsoft.com/v1.0/me';
+
+ $ch = curl_init($userUrl);
+ curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+ curl_setopt($ch, CURLOPT_HTTPHEADER, [
+ 'Authorization: Bearer ' . $accessToken,
+ 'Content-Type: application/json'
+ ]);
+
+ $userResponse = curl_exec($ch);
+ $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
+ curl_close($ch);
+
+ if ($httpCode !== 200) {
+ die("Fehler beim Abrufen der Benutzer-Daten (HTTP $httpCode): " . htmlspecialchars($userResponse) . "Zurück zum Login ");
+ }
+
+ $userData = json_decode($userResponse, true);
+
+ if (!isset($userData['id']) || !isset($userData['mail'])) {
+ die("Ungültige Benutzer-Daten erhalten: " . htmlspecialchars($userResponse) . "Zurück zum Login ");
+ }
+
+ // Benutzer einloggen oder anlegen
+ $microsoftUser = [
+ 'id' => $userData['id'],
+ 'email' => $userData['mail'] ?? $userData['userPrincipalName'],
+ 'name' => $userData['displayName'] ?? $userData['givenName'] . ' ' . $userData['surname']
+ ];
+
+ try {
+ $auth->loginWithMicrosoft($microsoftUser);
+ header('Location: index.php');
+ exit;
+ } catch (Exception $e) {
+ die("Login-Fehler: " . $e->getMessage() . "Zurück zum Login ");
+ }
+
+} else {
+ // Keine Authorization Code - Redirect zu Microsoft Login
+ die("Kein Authorization Code erhalten.Zurück zum Login ");
+}
+?>
\ No newline at end of file
diff --git a/m365_debug.php b/m365_debug.php
new file mode 100644
index 0000000..4df698f
--- /dev/null
+++ b/m365_debug.php
@@ -0,0 +1,81 @@
+getSetting('m365_client_id', '');
+$clientSecret = $db->getSetting('m365_client_secret', '');
+$tenantId = $db->getSetting('m365_tenant_id', '');
+
+// Dynamische URLs
+$protocol = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http';
+$host = $_SERVER['HTTP_HOST'];
+$scriptPath = dirname($_SERVER['SCRIPT_NAME']);
+$scriptPath = $scriptPath === '/' ? '' : $scriptPath;
+$redirectUri = $protocol . '://' . $host . $scriptPath . '/m365_callback.php';
+
+?>
+
+
+
+
+
+ M365 Debug
+
+
+
+ 🔍 Microsoft 365 OAuth Debug
+
+
+
1. Konfiguration Status
+
Client ID: = !empty($clientId) ? '✓ Gesetzt ' : '✗ Fehlt ' ?>
+
Client Secret: = !empty($clientSecret) ? '✓ Gesetzt ' : '✗ Fehlt ' ?>
+
Tenant ID: = !empty($tenant
\ No newline at end of file
diff --git a/test.php b/test.php
new file mode 100644
index 0000000..472b59c
--- /dev/null
+++ b/test.php
@@ -0,0 +1,90 @@
+System Test";
+
+// 1. PHP Version
+echo "
1. PHP Version ";
+echo "PHP Version: " . phpversion() . "
";
+
+// 2. Config.php vorhanden?
+echo "
2. Config.php Check ";
+if (file_exists('config.php')) {
+ echo "✓ config.php existiert
";
+ require_once 'config.php';
+ echo "✓ config.php geladen
";
+ echo "DB_HOST: " . DB_HOST . "
";
+ echo "DB_NAME: " . DB_NAME . "
";
+} else {
+ echo "✗ config.php nicht gefunden!
";
+}
+
+// 3. Datenbankverbindung
+echo "
3. Datenbankverbindung ";
+try {
+ $pdo = new PDO(
+ "mysql:host=" . DB_HOST . ";dbname=" . DB_NAME . ";charset=utf8mb4",
+ DB_USER,
+ DB_PASS
+ );
+ echo "✓ Datenbankverbindung erfolgreich
";
+
+ // Tabellen prüfen
+ $tables = $pdo->query("SHOW TABLES")->fetchAll(PDO::FETCH_COLUMN);
+ echo "✓ Gefundene Tabellen: " . implode(", ", $tables) . "
";
+
+} catch (PDOException $e) {
+ echo "✗ Datenbankfehler: " . $e->getMessage() . "
";
+}
+
+// 4. Includes prüfen
+echo "
4. Include-Dateien ";
+$files = ['includes/Database.php', 'includes/Auth.php', 'includes/UniFiController.php'];
+foreach ($files as $file) {
+ if (file_exists($file)) {
+ echo "✓ $file existiert
";
+ try {
+ require_once $file;
+ echo "✓ $file geladen
";
+ } catch (Exception $e) {
+ echo "✗ Fehler beim Laden von $file: " . $e->getMessage() . "
";
+ }
+ } else {
+ echo "✗ $file nicht gefunden!
";
+ }
+}
+
+// 5. Database-Klasse testen
+echo "
5. Database-Klasse ";
+try {
+ $db = Database::getInstance();
+ echo "✓ Database::getInstance() erfolgreich
";
+
+ $result = $db->fetchOne("SELECT COUNT(*) as count FROM users");
+ echo "✓ Anzahl Benutzer: " . $result['count'] . "
";
+
+} catch (Exception $e) {
+ echo "✗ Database-Fehler: " . $e->getMessage() . "
";
+}
+
+// 6. Auth-Klasse testen
+echo "
6. Auth-Klasse ";
+try {
+ $auth = new Auth();
+ echo "✓ Auth-Klasse initialisiert
";
+ echo "Eingeloggt: " . ($auth->isLoggedIn() ? 'Ja' : 'Nein') . "
";
+
+} catch (Exception $e) {
+ echo "✗ Auth-Fehler: " . $e->getMessage() . "
";
+}
+
+// 7. Session-Test
+echo "
7. Session ";
+echo "Session Status: " . session_status() . " (1=disabled, 2=active)
";
+echo "Session ID: " . session_id() . "
";
+
+echo "
";
+echo "
✓ Test abgeschlossen ";
+echo "
Zum Login | Zur Startseite
";
+?>
\ No newline at end of file