commit dbdc237fa1c44f2d6e3d624b5abcd5ce09463d38 Author: friloo <49183588+friloo@users.noreply.github.com> Date: Tue Apr 21 17:45:58 2026 +0200 Initial Upload 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) ?> + + + + + +
+
+
+ Administration +
+
+
+ + Zurück zur Startseite + +
+
+
+
+
Administrator
+
+
+
+
+ + + +
+ + + +
+
+
+
Aktive Sites
+
+ +
+
+
+
+ +
+
+
Benutzer
+
+ +
+
+
+
+ +
+
+
🟢 Gültige Vouchers
+
+ +
+
+
+
Letzte Sync:
+
+ +
+
+
🟡 Verwendet
+
+ +
+
+
+
Quota ausgeschöpft
+
+ +
+
+
🔴 Abgelaufen
+
+ +
+
+
+
Im Controller
+
+ +
+
+
📊 Gesamt
+
+ +
+
+
+
Alle Sites
+
+
+ + +
+
+

🔴 Live: Vouchers pro Site

+ Letzte Sync: +
+
+
+ 0, 'valid' => 0, 'used' => 0, 'expired' => 0]; + ?> +
+
+
+
+ + + +
+
+
+
+
+
Gültig
+
+
+
+
Verwendet
+
+
+
+
Abgelaufen
+
+
+ +
+ + + +
+ +

Keine aktiven Sites konfiguriert.

+ + Site hinzufügen + +
+ +
+
+
+ + +
+
+

📊 Voucher-Trend (Letzte 7 Tage)

+
+
+
+ +
+
+
+ + +
+
+

🏆 Top 5 Benutzer (Letzte 30 Tage)

+
+
+ +
+ +

Noch keine Daten verfügbar

+
+ +
    + $user): ?> +
  • + +
    Vouchers
    +
  • + +
+ +
+
+ + +
+
+

Letzte Vouchers (aus Datenbank)

+ + Live-Ansicht + +
+
+ +
+ +

Noch keine Vouchers erstellt

+
+ + + + + + + + + + + + + + + + + + + + + + + + +
ErstelltCodeNameSiteStatusErsteller
+ + + Ö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) ?> + + + + + + + + + + + + + + + +
+
Administration
+ Zurück +
+ + + +
+ + + +
+ + + +
+ + +
+
+ + + + + + + +
+ + +
+

Allgemeine Einstellungen

+
+ + + +
+ + +
+ +
+
+ + +
+
+ + +
Icon im Browser-Tab (.ico, .png, .svg)
+
+
+ +
+ +
+ + +
+ +
+ + +
+ +
+ > + +
+ +
+ +
+
+
+ + +
+

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 ist aktiv

+
+ +
+
+ +
+ + +
+
+ + +
+
+
+ + +
+ +

Cron-Job einrichten

+ + + +
+ +
+ + +
+
+ +
+

Crontab-Eintrag (Linux/Mac)

+

Fügen Sie diese Zeile in Ihre Crontab ein (crontab -e):

+
+ */30 * * * * curl -s "" > /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 '' -UseBasicParsing" +
+
+ +
+ +

Status

+ + + + + + + + + + +
Letzte Synchronisation: + + + + Noch nie ausgeführt + +
Token-Status: + + Aktiv + + Nicht konfiguriert + +
+ + +
+ + +
+ +
+ + +
+

Microsoft 365

+
+

Azure AD App

+

Redirect URI: /m365_callback.php

+
+
+ + +
+ + +
+
+ + +
+
+ + +
+ +
+
+ + +
+

SMTP

+
+ + +
+ > + +
+
+
+ + +
+
+ + +
+
+
+ + +
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+ +
+
+ + + + + + + +
+

E-Mail Templates

+
+ + + +
+ + +
Wird in E-Mails als Login-Link verwendet. Auto:
+
+ +
+ +

Voucher E-Mail

+
+

Platzhalter:

+
+
{VOUCHER_CODE}
+
{SITE_NAME}
+
{MAX_USES}
+
{APP_TITLE}
+
{INSTRUCTIONS}
+
+
+ +
+ + +
+ +
+ + +
+ +
+ +

Benutzer-Benachrichtigung

+
+

Platzhalter:

+
+
{USER_NAME}
+
{CHANGES}
+
{APP_TITLE}
+
{SYSTEM_URL}
+
+
+ +
+ + +
+ +
+ + +
+ + +
+
+ + +
+

System & Erweitert

+
+ + + +
+

TinyMCE API Key

+

Kostenlosen API Key erhalten: tiny.cloud/signup
+ Ohne Key wird eine eingeschränkte Version geladen.

+
+ +
+ + +
Für WYSIWYG-Editor in Anleitungen und E-Mail-Templates
+
+ +
+ +

Druck-Template

+
+

Platzhalter:

+
+
{VOUCHER_CODE}
+
{EXPIRY_DATE}
+
{EXPIRY_TIME}
+
{SITE_NAME}
+
{MAX_USES}
+
{APP_TITLE}
+
{INSTRUCTIONS}
+
+
+ +
+ + +
HTML/CSS-Code für den Ausdruck von Vouchers
+
+ + +
+ +
+ +

System-Information

+ + + + + +
PHP Version:
Datenbank:
Installiert:
Version:2.0.0
+
+ + +
+

Passwort ändern

+
+ + +
+ + +
+ +
+ + +
Mindestens 8 Zeichen
+
+ +
+ + +
+ + +
+
+
+
+ + + + \ 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) ?> + + + + +
+
+ Administration +
+ + Zurück + +
+ + + +
+ + + +
+ + + +
+ + + +
+
+ +

Noch keine Sites konfiguriert.
Fügen Sie Ihre erste Site hinzu!

+
+
+ +
+ +
+
+
+
+
ID:
+
+
+ + Aktiv + + Inaktiv + + + Öffentlich + +
+
+ +
+
+ + +
+
+ + +
+
+ + Erstellt: +
+
+ +
+ + + + + + + Löschen + +
+
+ +
+ +
+ + + + + + + + + + \ 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) ?> + + + + +
+
+ Administration +
+ + Zurück + +
+ + + +
+ + + +
+ + + +
+ + +
+
+

Alle Benutzer

+
+
+ +
+ +

Noch keine Benutzer vorhanden

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
NameE-MailRolleStatusSite-ZugriffeLetzter LoginAktionen
+ + + Sie + + + + Admin + + Benutzer + + + + Aktiv + + Inaktiv + + + + Alle Sites + + + + + + Keine + + + + + + Noch nie + + + + + + + + + + + + + +
+ +
+
+
+ + + + + + + + + + \ 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) ?> + + + + + + + +
+
+ Administration +
+ + Zurück + +
+ + + +
+ + + +
+ +

Keine aktiven Sites vorhanden

+

Bitte fügen Sie zuerst eine Site hinzu oder aktivieren Sie eine vorhandene.

+ + Sites verwalten + +
+ + +
+
+

Site auswählen

+ Letzte Sync: +
+
+
+ + +
+
+
+ + + +
+
+

Vouchers

+
+
+
+
+ +

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) ?> + + + + +
+
+
👋 Hallo,
+
+
+ isAdmin()): ?> + ⚙️ Administration + + Abmelden +
+
+ +
+
+
+
+
+ + 🔐 Anmelden + +
+
+ + +
+ + + + +

+ + +
+ + + +
+
+
✓ Ihr Zugangs-Code
+
+
+ Der Code ist 8 Stunden ab Erstellung gültig +
+
+ + +
+ +

+ + +
+ +
+ +
+ +
+ +
+ + + + +
+ + + +

Keine verfügbaren Sites gefunden.
+ isAdmin()): ?> + Klicken Sie hier, um Sites anzulegen + + Bitte kontaktieren Sie Ihren Administrator. + +

+
+ + +
+ + + + isLoggedIn()): ?> + + + +
+ + +
+ +
+ + +
+ +
+ + +
+ + + + + +
+ + +
+ +

+ + +
+ +
+ + +
+ + + + + + 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

+ +
+
1
+
2
+
3
+
4
+
5
+
+ + +
+ +
+ +
+ + + +
+ ✓ Installation erfolgreich abgeschlossen!
+ Sie können sich jetzt mit Ihren Admin-Zugangsdaten anmelden. +
+ Zum Login + +
+ +

Schritt 1: Datenbank-Konfiguration

+ +
+ + +
Meist "localhost"
+
+ +
+ + +
Name der Datenbank (wird erstellt falls nicht vorhanden)
+
+ +
+ + +
+ +
+ + +
+ + +
+ + +
+ +

Schritt 2: Administrator-Account

+ +
+ + +
+ +
+ + +
+ +
+ + +
Mindestens 8 Zeichen
+
+ +
+ + +
+ + +
+ + +
+ +

Schritt 3: Allgemeine Einstellungen

+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+

Microsoft 365 Login (Optional)

+
+ + +
+
+ + +
+
+ + +
+
Leer lassen, wenn M365-Login nicht verwendet werden soll
+
+ + +
+ + +
+ +

Schritt 4: Installation abschließen

+ +

+ Klicken Sie auf "Installation abschließen", um die Einrichtung zu beenden. + Die Datenbank und alle notwendigen Dateien werden erstellt. +

+ + +
+ +
+ + \ 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) ?> + + + +
+ + + +

+ + +

Melden Sie sich an, um fortzufahren

+ + +
+ + + +
+ + + + + + + + + + + + Mit Microsoft anmelden + + + Mit Benutzername und Passwort anmelden + + + +
+
+ + +
+ +
+ + +
+ + +
+ + +
oder
+ + + + + + + + Mit Microsoft anmelden + + + + + + ← Zurück zur Code-Erstellung + +
+ + + 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

'); +} + +// 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) ?> + + + +
+ + + +

+ + +

Melden Sie sich an, um fortzufahren

+ + +
+ + + +
+ + +
+
+ + +
+ +
+ + +
+ + +
+ + +
oder
+ + 🔷 Mit Microsoft 365 anmelden + + + + + ← Zurück zur Code-Erstellung + + + +
+ System-Status:
+ PHP Version:
+ Session Status:
+ Eingeloggt: 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
$errorDesc
Zurü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: ✓ Gesetzt' : '✗ Fehlt' ?>

+

Client Secret: ✓ Gesetzt' : '✗ Fehlt' ?>

+

Tenant ID: 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