diff --git a/admin/audit_log.php b/admin/audit_log.php new file mode 100644 index 0000000..0af636a --- /dev/null +++ b/admin/audit_log.php @@ -0,0 +1,227 @@ +requireAdmin(); +I18n::init(); + +$db = Database::getInstance(); +$appTitle = $db->getSetting('app_title', 'UniFi Voucher System'); + +$filterAction = trim($_GET['action'] ?? ''); +$filterUser = trim($_GET['user_id'] ?? ''); +$page = max(1, (int)($_GET['page'] ?? 1)); +$perPage = 50; +$offset = ($page - 1) * $perPage; + +$where = []; +$params = []; +if ($filterAction !== '') { $where[] = 'a.action = ?'; $params[] = $filterAction; } +if ($filterUser !== '') { $where[] = 'a.user_id = ?'; $params[] = (int)$filterUser; } +$whereStr = $where ? 'WHERE ' . implode(' AND ', $where) : ''; + +$total = (int)$db->fetchOne("SELECT COUNT(*) as c FROM audit_log a $whereStr", $params)['c']; +$pages = max(1, (int)ceil($total / $perPage)); +$logs = $db->fetchAll( + "SELECT a.*, u.name as user_name, u.email as user_email + FROM audit_log a LEFT JOIN users u ON a.user_id = u.id + $whereStr ORDER BY a.created_at DESC LIMIT ? OFFSET ?", + array_merge($params, [$perPage, $offset]) +); + +// Distinct actions for filter +$actions = $db->fetchAll("SELECT DISTINCT action FROM audit_log ORDER BY action"); + +// User list for filter +$users = $db->fetchAll("SELECT id, name FROM users WHERE is_active = 1 ORDER BY name"); + +$currentPage = 'audit_log'; +$adminBase = ''; + +$actionLabels = [ + 'voucher_created' => '🎫 Voucher erstellt', + 'voucher_bulk' => '🎫 Bulk Voucher', + 'user_login' => '🔐 Login', + 'user_logout' => '🚪 Logout', + 'user_created' => '👤 Benutzer erstellt', + 'user_updated' => '👤 Benutzer geändert', + 'user_deleted' => '👤 Benutzer gelöscht', + 'site_added' => '🌐 Site hinzugefügt', + 'site_updated' => '🌐 Site geändert', + 'site_deleted' => '🌐 Site gelöscht', + 'settings_saved' => '⚙️ Einstellungen gespeichert', + 'password_reset' => '🔑 Passwort-Reset', + 'template_created' => '📋 Profil erstellt', + 'template_updated' => '📋 Profil geändert', + 'template_deleted' => '📋 Profil gelöscht', +]; +?> + + + + + + <?= __('audit_title') ?> - <?= htmlspecialchars($appTitle) ?> + + + + + + + +
+
+
+
+
+ + +
+
+ + +
+ + Zurücksetzen +
+
+
+ + +
+
+ + Einträge +
+ +

+ +
+ + + + + + + + + + + + + + + + + + + + + + + +
+
+ +
+ + + + + +
+ + + System/Anonym + +
+ + : + + - + + + +
+
+ + 1): ?> + + + + +
+ + + + + diff --git a/admin/index.php b/admin/index.php index c6493c6..e432205 100644 --- a/admin/index.php +++ b/admin/index.php @@ -1,1029 +1,406 @@ -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'], - Crypto::decrypt($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' ?>
- -
-
-
- - - - +requireAdmin(); +$db = Database::getInstance(); +$appTitle = $db->getSetting('app_title', 'UniFi Voucher System'); +I18n::init(); + +// AJAX: Statistiken +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 = []; + + if ($syncFirst) { + foreach ($sites as $site) { + try { + $ctrl = new UniFiController($site['unifi_controller_url'], $site['unifi_username'], Crypto::decrypt($site['unifi_password']), $site['site_id']); + $ctrl->syncVouchersToDatabase($db, $site['id']); + } catch (Exception $e) { + $syncErrors[$site['id']] = $e->getMessage(); + } + } + $db->execute("INSERT INTO settings (setting_key,setting_value) VALUES ('last_cron_sync',NOW()) ON DUPLICATE KEY UPDATE setting_value=NOW()"); + } + + foreach ($sites as $site) { + $ss = $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)($ss['total']??0), 'valid' => (int)($ss['valid']??0), 'used' => (int)($ss['used']??0), 'expired' => (int)($ss['expired']??0)], 'error' => $syncErrors[$site['id']] ?? null]; + $totalStats['total'] += (int)($ss['total']??0); + $totalStats['valid'] += (int)($ss['valid']??0); + $totalStats['used'] += (int)($ss['used']??0); + $totalStats['expired'] += (int)($ss['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; +} + +$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'], +]; + +$sites = $db->fetchAll("SELECT * FROM sites WHERE is_active=1"); + +$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"); + +$siteStats = []; +foreach ($sites as $site) { + $siteStats[$site['id']] = $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']]); +} + +$lastCronSync = $db->getSetting('last_cron_sync', ''); + +$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]; +} + +$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"); + +$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"); + +$currentPage = 'dashboard'; +?> + + + + + + <?= __('dashboard_title') ?> – <?= htmlspecialchars($appTitle) ?> + + + + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
🟢
+
+
+
+
+
+
+
+
🟡
+
+
+
+
+
+
+
🔴
+
+
+
+
+
+
+
📊
+
+
+
+
+
+ +
+
+

🔴

+ +
+
+
+ 0,'valid'=>0,'used'=>0,'expired'=>0]; + ?> +
+
+
+ + + +
+
+
+
+
+
+ +
+ + +
+ +

+ +
+ +
+
+
+ +
+

📊

+
+
+
+
+ +
+
+

🏆

+
+ +

+ +
    + +
  • + + +
  • + +
+ +
+
+ +
+

📋

Live
+
+ +

+ + + + + + + + + + + + + +
+ + +
+ +
+
+
+ + + +
+ + + + diff --git a/admin/settings.php b/admin/settings.php index f03725e..fbe7e77 100644 --- a/admin/settings.php +++ b/admin/settings.php @@ -1,863 +1,566 @@ -requireAdmin(); - -$db = Database::getInstance(); -$appTitle = $db->getSetting('app_title', 'UniFi Voucher System'); - -// AJAX: SMTP-Test-E-Mail senden -if (isset($_POST['ajax_smtp_test'])) { - header('Content-Type: application/json'); - if (!$auth->validateCsrfToken($_POST['csrf_token'] ?? '')) { - echo json_encode(['success' => false, 'message' => 'Ungültiges Token']); - exit; - } - $to = trim($_POST['test_email'] ?? ''); - if (!filter_var($to, FILTER_VALIDATE_EMAIL)) { - echo json_encode(['success' => false, 'message' => 'Ungültige E-Mail-Adresse']); - exit; - } - $mailer = new Mailer(); - $ok = $mailer->sendTestEmail($to); - echo json_encode([ - 'success' => $ok, - 'message' => $ok ? "Test-E-Mail wurde an {$to} gesendet." : 'Versand fehlgeschlagen. Prüfen Sie die SMTP-Einstellungen und den PHP-Fehlerlog.' - ]); - exit; -} - -$error = ''; -$success = ''; - -// 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

-
- - -
- > - -
-
-
- - -
-
- - -
-
-
- - -
-
-
- - -
-
- - -
-
-
-
- - -
-
- - -
-
- -
- -
-

SMTP testen

-
-
- - -
- -
- -
-
- - - - - - - -
-

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 +requireAdmin(); +I18n::init(); + +$db = Database::getInstance(); +$appTitle = $db->getSetting('app_title', 'UniFi Voucher System'); + +// AJAX: SMTP-Test-E-Mail senden +if (isset($_POST['ajax_smtp_test'])) { + header('Content-Type: application/json'); + if (!$auth->validateCsrfToken($_POST['csrf_token'] ?? '')) { + echo json_encode(['success' => false, 'message' => __('error_csrf')]); + exit; + } + $to = trim($_POST['test_email'] ?? ''); + if (!filter_var($to, FILTER_VALIDATE_EMAIL)) { + echo json_encode(['success' => false, 'message' => __('error_email_invalid')]); + exit; + } + $mailer = new Mailer(); + $ok = $mailer->sendTestEmail($to); + echo json_encode([ + 'success' => $ok, + 'message' => $ok ? "Test-E-Mail wurde an {$to} gesendet." : 'Versand fehlgeschlagen. Prüfen Sie die SMTP-Einstellungen.' + ]); + exit; +} + +$error = ''; +$success = ''; + +// Einstellungen speichern +if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['save_settings'])) { + if (!$auth->validateCsrfToken($_POST['csrf_token'] ?? '')) { + $error = __('error_csrf'); + } else { + try { + $settings = []; + $formType = $_POST['form_type'] ?? ''; + + 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'; + } + + if ($formType === 'defaults') { + $expMin = (int)($_POST['default_expire_minutes'] ?? 480); + $defDev = (int)($_POST['default_max_uses'] ?? 1); + $maxDev = (int)($_POST['max_uses_limit'] ?? 10); + if ($expMin < 1) $expMin = 480; + if ($defDev < 1) $defDev = 1; + if ($maxDev < 1) $maxDev = 10; + $settings['default_expire_minutes'] = (string)$expMin; + $settings['default_max_uses'] = (string)$defDev; + $settings['max_uses_limit'] = (string)$maxDev; + } + + 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'] ?? ''); + } + + 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'] ?? ''); + } + + 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'] ?? ''); + } + + 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 = __('settings_saved'); + } catch (Exception $e) { + $error = 'Fehler: ' . $e->getMessage(); + } + } +} + +// Cron-Token generieren/löschen +if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['generate_cron_token'])) { + if (!$auth->validateCsrfToken($_POST['csrf_token'] ?? '')) { + $error = __('error_csrf'); + } else { + $db->setSetting('cron_token', bin2hex(random_bytes(32))); + $success = 'Neuer Cron-Token wurde generiert!'; + } +} +if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['delete_cron_token'])) { + if (!$auth->validateCsrfToken($_POST['csrf_token'] ?? '')) { + $error = __('error_csrf'); + } else { + $db->setSetting('cron_token', ''); + $success = 'Cron-Token wurde gelöscht!'; + } +} + +// Passwort ändern +if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['change_password'])) { + if (!$auth->validateCsrfToken($_POST['csrf_token'] ?? '')) { + $error = __('error_csrf'); + } else { + try { + $user = $auth->getCurrentUser(); + if (!password_verify($_POST['current_password'], $user['password_hash'])) { + throw new Exception('Aktuelles Passwort ist falsch'); + } + if (strlen($_POST['new_password']) < 8) { + throw new Exception(__('settings_pw_minlength')); + } + if ($_POST['new_password'] !== $_POST['confirm_password']) { + throw new Exception('Passwörter stimmen nicht überein'); + } + $db->query("UPDATE users SET password_hash = ? WHERE id = ?", + [password_hash($_POST['new_password'], PASSWORD_DEFAULT), $user['id']]); + $success = __('settings_pw_changed'); + } catch (Exception $e) { + $error = $e->getMessage(); + } + } +} + +$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; + +$cs = [ + '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', ''), + 'public_access' => $db->getSetting('public_access', '0'), + 'default_expire_minutes' => $db->getSetting('default_expire_minutes', '480'), + 'default_max_uses' => $db->getSetting('default_max_uses', '1'), + 'max_uses_limit' => $db->getSetting('max_uses_limit', '10'), + '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', ''), +]; + +$currentPage = 'settings'; +$adminBase = ''; +?> + + + + + + <?= __('settings_title') ?> - <?= htmlspecialchars($appTitle) ?> + + + + + + + + + + + + + + + +
+ + +
+ + +
+
+ + + + + + + + +
+ + +
+

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

+

Diese Werte werden als Vorgabe im Voucher-Formular verwendet.

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

Gültigkeits-Referenz

+

+ 60 Min = 1 Stunde  |  480 Min = 8 Stunden  |  + 1440 Min = 1 Tag  |  10080 Min = 1 Woche  |  + 43200 Min = 30 Tage +

+
+ +
+
+ + +
+

+
+

Was macht der Cron-Job?

+

Der Cron-Job synchronisiert automatisch alle Voucher von Ihren UniFi Controllern in die lokale Datenbank.

+
+
+ +

Kein Token konfiguriert.

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

Crontab-Eintrag (alle 30 Min)

+
+ */30 * * * * curl -s "" > /dev/null 2>&1 +
+
+ +
+ + +
+ +

Letzte Synchronisation:

+
+ + +
+

Microsoft 365

+
+

Azure AD App

+

Redirect URI: /m365_callback.php

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

SMTP

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

SMTP testen

+
+
+ +
+ +
+ + +
+

E-Mail Templates

+
+ + +
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: tiny.cloud/signup

+
+
Für WYSIWYG-Editor in Anleitungen
+
+

Druck-Template

+

Platzhalter:

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

System-Information

+ + + + +
PHP Version:
Datenbank:
Version:2.1.0
+
+ + +
+

+
+ +
+
+
+ +
+
+
+ + + + + + + diff --git a/admin/sites.php b/admin/sites.php index a041d41..83c37e8 100644 --- a/admin/sites.php +++ b/admin/sites.php @@ -1,691 +1,360 @@ -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, Crypto::encrypt($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, Crypto::encrypt($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 +requireAdmin(); +$db = Database::getInstance(); +$appTitle = $db->getSetting('app_title', 'UniFi Voucher System'); +I18n::init(); + +$error = ''; +$success = ''; + +// Edit site +if ($_SERVER['REQUEST_METHOD']==='POST' && isset($_POST['edit_site'])) { + if (!$auth->validateCsrfToken($_POST['csrf_token']??'')) { + $error = __('error_csrf'); + } 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(__('error_fill_all')); + if (!empty($password)) { + $test = UniFiController::testConnection($controllerUrl,$username,$password,$siteIdStr); + if ($test !== true) throw new Exception('Verbindung fehlgeschlagen: '.$test); + $db->execute("UPDATE sites SET name=?,site_id=?,unifi_controller_url=?,unifi_username=?,unifi_password=?,public_access=? WHERE id=?", + [$name,$siteIdStr,$controllerUrl,$username,Crypto::encrypt($password),$publicAccess,$siteId]); + } else { + $db->execute("UPDATE sites SET name=?,site_id=?,unifi_controller_url=?,unifi_username=?,public_access=? WHERE id=?", + [$name,$siteIdStr,$controllerUrl,$username,$publicAccess,$siteId]); + } + $auth->writeAuditLog($_SESSION['user_id'],'site_edit','site',$siteId,"Site {$name} aktualisiert"); + $success = __('sites_updated'); + } catch (Exception $e) { $error = $e->getMessage(); } + } +} + +// Add site +if ($_SERVER['REQUEST_METHOD']==='POST' && isset($_POST['add_site'])) { + if (!$auth->validateCsrfToken($_POST['csrf_token']??'')) { + $error = __('error_csrf'); + } 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(__('error_fill_all')); + $test = UniFiController::testConnection($controllerUrl,$username,$password,$siteId); + if ($test !== true) throw new Exception('Verbindung fehlgeschlagen: '.$test); + $newId = $db->execute("INSERT INTO sites (name,site_id,unifi_controller_url,unifi_username,unifi_password,public_access) VALUES (?,?,?,?,?,?)", + [$name,$siteId,$controllerUrl,$username,Crypto::encrypt($password),$publicAccess]); + $auth->writeAuditLog($_SESSION['user_id'],'site_create','site',$newId,"Site {$name} erstellt"); + $success = __('sites_added'); + } catch (Exception $e) { $error = $e->getMessage(); } + } +} + +// Delete site +if (isset($_GET['delete']) && isset($_GET['token'])) { + if ($auth->validateCsrfToken($_GET['token'])) { + $delId = (int)$_GET['delete']; + $db->query("DELETE FROM sites WHERE id=?", [$delId]); + $auth->writeAuditLog($_SESSION['user_id'],'site_delete','site',$delId,'Site gelöscht'); + $success = __('sites_deleted'); + } else { $error = __('error_csrf'); } +} + +// Toggle site +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) { + $db->query("UPDATE sites SET is_active=? WHERE id=?", [$site['is_active']?0:1,(int)$_GET['toggle']]); + $success = 'Site-Status aktualisiert!'; + } + } else { $error = __('error_csrf'); } +} + +$sites = $db->fetchAll("SELECT * FROM sites ORDER BY name"); +$currentPage = 'sites'; +?> + + + + + + <?= __('sites_title') ?> – <?= htmlspecialchars($appTitle) ?> + + + + + + +
+ + +
+ + + +
+ +

+
+ +
+ +
+
+
+
+
ID:
+
+
+ + + + + + + + +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ + + + + + + + +
+
+ +
+ + + + + + + + + + +
+ + + + diff --git a/admin/templates.php b/admin/templates.php new file mode 100644 index 0000000..fe9512e --- /dev/null +++ b/admin/templates.php @@ -0,0 +1,312 @@ +requireAdmin(); +I18n::init(); + +$db = Database::getInstance(); +$appTitle = $db->getSetting('app_title', 'UniFi Voucher System'); + +$error = ''; +$success = ''; + +// Profil hinzufügen +if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['add_template'])) { + if (!$auth->validateCsrfToken($_POST['csrf_token'] ?? '')) { + $error = __('error_csrf'); + } else { + try { + $name = trim($_POST['name'] ?? ''); + $maxUses = (int)($_POST['max_uses'] ?? 1); + $expireMin = (int)($_POST['expire_minutes'] ?? 480); + $description = trim($_POST['description'] ?? ''); + + if (empty($name)) throw new Exception(__('error_name_req')); + if ($maxUses < 1) $maxUses = 1; + if ($expireMin < 1) $expireMin = 60; + + $db->execute( + "INSERT INTO voucher_templates (name, max_uses, expire_minutes, description, created_by) VALUES (?, ?, ?, ?, ?)", + [$name, $maxUses, $expireMin, $description, $_SESSION['user_id']] + ); + $success = __('templates_added'); + } catch (Exception $e) { + $error = $e->getMessage(); + } + } +} + +// Profil bearbeiten +if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['edit_template'])) { + if (!$auth->validateCsrfToken($_POST['csrf_token'] ?? '')) { + $error = __('error_csrf'); + } else { + try { + $id = (int)$_POST['template_id']; + $name = trim($_POST['name'] ?? ''); + $maxUses = (int)($_POST['max_uses'] ?? 1); + $expireMin = (int)($_POST['expire_minutes'] ?? 480); + $description = trim($_POST['description'] ?? ''); + $isActive = isset($_POST['is_active']) ? 1 : 0; + + if (empty($name)) throw new Exception(__('error_name_req')); + + $db->execute( + "UPDATE voucher_templates SET name=?, max_uses=?, expire_minutes=?, description=?, is_active=? WHERE id=?", + [$name, $maxUses, $expireMin, $description, $isActive, $id] + ); + $success = __('templates_updated'); + } catch (Exception $e) { + $error = $e->getMessage(); + } + } +} + +// Profil löschen +if (isset($_GET['delete']) && isset($_GET['token'])) { + if ($auth->validateCsrfToken($_GET['token'])) { + $db->execute("DELETE FROM voucher_templates WHERE id = ?", [(int)$_GET['delete']]); + $success = __('templates_deleted'); + } else { + $error = __('error_csrf'); + } +} + +$templates = $db->fetchAll("SELECT t.*, u.name as creator FROM voucher_templates t LEFT JOIN users u ON t.created_by = u.id ORDER BY t.is_active DESC, t.name"); + +$currentPage = 'templates'; +$adminBase = ''; +?> + + + + + + <?= __('templates_title') ?> - <?= htmlspecialchars($appTitle) ?> + + + + + + + +
+ + +
+ + +
+
+

+ Profile +
+ +
+ +

+

+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + +
+ + + = 1440 && $m % 1440 === 0) { + $durLabel = ($m / 1440) . ' Tag' . ($m / 1440 > 1 ? 'e' : ''); + } elseif ($m >= 60 && $m % 60 === 0) { + $durLabel = ($m / 60) . ' Std.'; + } else { + $durLabel = $m . ' Min.'; + } + ?> + + + + + + + + + + +
+
+ +
+ + + + + + + + + + + + + diff --git a/admin/users.php b/admin/users.php index 7532305..197d918 100644 --- a/admin/users.php +++ b/admin/users.php @@ -1,769 +1,458 @@ -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 +requireAdmin(); +$db = Database::getInstance(); +$mailer = new Mailer(); +$appTitle = $db->getSetting('app_title', 'UniFi Voucher System'); +$smtpEnabled = $db->getSetting('smtp_enabled', '0') === '1'; +I18n::init(); + +$error = ''; +$success = ''; + +// Send password reset link +if (isset($_GET['send_reset']) && isset($_GET['token'])) { + if ($auth->validateCsrfToken($_GET['token'])) { + $targetUser = $db->fetchOne("SELECT * FROM users WHERE id=? AND is_active=1 AND password_hash IS NOT NULL", [(int)$_GET['send_reset']]); + if ($targetUser) { + try { + $db->execute("DELETE FROM password_reset_tokens WHERE user_id=?", [$targetUser['id']]); + $token = bin2hex(random_bytes(32)); + $expiresAt = date('Y-m-d H:i:s', strtotime('+1 hour')); + $db->execute("INSERT INTO password_reset_tokens (user_id, token, expires_at) VALUES (?,?,?)", [$targetUser['id'], $token, $expiresAt]); + $systemUrl = rtrim($db->getSetting('system_url', ''), '/'); + if (empty($systemUrl)) { + $protocol = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS']==='on' ? 'https' : 'http'; + $scriptPath = dirname(dirname($_SERVER['SCRIPT_NAME'])); + $systemUrl = $protocol . '://' . $_SERVER['HTTP_HOST'] . ($scriptPath==='/'?'':$scriptPath); + } + $resetUrl = $systemUrl . '/reset_password.php?token=' . $token; + $mailer->sendRaw($targetUser['email'], $appTitle . ' – Passwort zurücksetzen', + "Hallo {$targetUser['name']},\n\nEin Administrator hat für Sie einen Passwort-Reset-Link erstellt:\n\n{$resetUrl}\n\n(Gültig für 1 Stunde)\n\n{$appTitle}"); + $success = 'Passwort-Reset-Link wurde an ' . htmlspecialchars($targetUser['email']) . ' gesendet.'; + } catch (Exception $e) { + $error = 'Fehler beim Senden: ' . $e->getMessage(); + } + } else { + $error = 'Benutzer nicht gefunden oder kein lokales Passwort.'; + } + } else { + $error = __('error_csrf'); + } +} + +// Edit user +if ($_SERVER['REQUEST_METHOD']==='POST' && isset($_POST['edit_user'])) { + if (!$auth->validateCsrfToken($_POST['csrf_token']??'')) { + $error = __('error_csrf'); + } else { + try { + $userId = (int)$_POST['user_id']; + $isAdmin = isset($_POST['is_admin']) ? 1 : 0; + $siteIds = $_POST['site_ids'] ?? []; + $oldUser = $db->fetchOne("SELECT * FROM users WHERE id=?", [$userId]); + $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]); + $db->query("UPDATE users SET is_admin=? WHERE id=?", [$isAdmin, $userId]); + $db->query("DELETE FROM user_site_access WHERE user_id=?", [$userId]); + $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']; + } + } + $changes = []; + if ($oldUser['is_admin'] != $isAdmin) { + $changes[] = $isAdmin ? 'Sie wurden zum Administrator ernannt' : 'Ihre Administrator-Rechte wurden entfernt'; + } + $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 && !$oldUser['is_admin']) $changes[] = 'Sie haben nun Zugriff auf alle Sites'; + if (!empty($changes)) $mailer->sendUserNotification($oldUser['email'], $oldUser['name'], $changes); + $success = __('users_updated') . (!empty($changes) ? ' '.__('users_notified') : ''); + $auth->writeAuditLog($_SESSION['user_id'], 'user_edit', 'user', $userId, implode('; ', $changes) ?: 'Keine Änderungen'); + } catch (Exception $e) { $error = $e->getMessage(); } + } +} + +// Add user +if ($_SERVER['REQUEST_METHOD']==='POST' && isset($_POST['add_user'])) { + if (!$auth->validateCsrfToken($_POST['csrf_token']??'')) { + $error = __('error_csrf'); + } 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(__('error_fill_all')); + if (!filter_var($email, FILTER_VALIDATE_EMAIL)) throw new Exception(__('error_email_invalid')); + if (strlen($password) < 8) throw new Exception(__('settings_pw_minlength')); + if ($db->fetchOne("SELECT id FROM users WHERE email=?", [$email])) throw new Exception('E-Mail bereits vorhanden'); + $userId = $auth->registerUser($email, $name, $password, $isAdmin); + if (!$userId) throw new Exception('Benutzer konnte nicht erstellt werden'); + if (!$isAdmin && !empty($siteIds)) { + foreach ($siteIds as $siteId) { + $db->execute("INSERT INTO user_site_access (user_id, site_id) VALUES (?,?)", [$userId, $siteId]); + } + } + $auth->writeAuditLog($_SESSION['user_id'], 'user_create', 'user', $userId, "Benutzer {$name} erstellt"); + $success = __('users_added'); + } catch (Exception $e) { $error = $e->getMessage(); } + } +} + +// Delete user +if (isset($_GET['delete']) && isset($_GET['token'])) { + if ($auth->validateCsrfToken($_GET['token'])) { + $deleteId = (int)$_GET['delete']; + if ($deleteId === (int)$_SESSION['user_id']) { + $error = 'Sie können sich nicht selbst löschen'; + } else { + $db->query("DELETE FROM users WHERE id=?", [$deleteId]); + $auth->writeAuditLog($_SESSION['user_id'], 'user_delete', 'user', $deleteId, 'Benutzer gelöscht'); + $success = __('users_deleted'); + } + } else { $error = __('error_csrf'); } +} + +// Toggle user active +if (isset($_GET['toggle']) && isset($_GET['token'])) { + if ($auth->validateCsrfToken($_GET['token'])) { + $toggleId = (int)$_GET['toggle']; + if ($toggleId === (int)$_SESSION['user_id']) { + $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!'; + } + } + } else { $error = __('error_csrf'); } +} + +$users = $db->fetchAll("SELECT * FROM users ORDER BY name"); +$sites = $db->fetchAll("SELECT * FROM sites WHERE is_active=1 ORDER BY name"); +$userSiteAccess = []; +foreach ($users as $user) { + $userSiteAccess[$user['id']] = $db->fetchAll("SELECT s.id, s.name FROM sites s INNER JOIN user_site_access usa ON s.id=usa.site_id WHERE usa.user_id=?", [$user['id']]); +} +$currentPage = 'users'; +?> + + + + + + <?= __('users_title') ?> – <?= htmlspecialchars($appTitle) ?> + + + + + + +
+ + +
+ + +
+

+
+ +

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + +
+
+
+ +
+
+ + + + + + + + + +
+ + + + diff --git a/admin/vouchers.php b/admin/vouchers.php index a13551b..46af577 100644 --- a/admin/vouchers.php +++ b/admin/vouchers.php @@ -1,1073 +1,434 @@ -requireAdmin(); - -$db = Database::getInstance(); -$appTitle = $db->getSetting('app_title', 'UniFi Voucher System'); - -// CSV-Export -if (isset($_GET['export_csv']) && isset($_GET['site_id'])) { - if (!$auth->validateCsrfToken($_GET['token'] ?? '')) { - http_response_code(403); - exit('Ungültiges Token'); - } - $siteId = (int)$_GET['site_id']; - $site = $db->fetchOne("SELECT * FROM sites WHERE id = ? AND is_active = 1", [$siteId]); - if (!$site) { http_response_code(404); exit('Site nicht gefunden'); } - - $rows = $db->fetchAll( - "SELECT voucher_code, voucher_name, max_uses, expire_minutes, status, used_count, created_at, expires_at - FROM vouchers WHERE site_id = ? ORDER BY created_at DESC", - [$siteId] - ); - - $filename = 'vouchers_' . preg_replace('/[^a-z0-9]/i', '_', $site['name']) . '_' . date('Ymd_His') . '.csv'; - header('Content-Type: text/csv; charset=UTF-8'); - header('Content-Disposition: attachment; filename="' . $filename . '"'); - header('Cache-Control: no-cache'); - - $out = fopen('php://output', 'w'); - fprintf($out, chr(0xEF).chr(0xBB).chr(0xBF)); // UTF-8 BOM für Excel - fputcsv($out, ['Code', 'Name', 'Max. Geräte', 'Gültigkeit (Min)', 'Status', 'Genutzt', 'Erstellt', 'Läuft ab'], ';'); - foreach ($rows as $r) { - fputcsv($out, [ - $r['voucher_code'], $r['voucher_name'], $r['max_uses'], - $r['expire_minutes'], $r['status'], $r['used_count'], - $r['created_at'], $r['expires_at'] ?? '' - ], ';'); - } - fclose($out); - exit; -} - -// AJAX: Voucher abrufen (immer aus DB, optional vorher Live-Sync) -if (isset($_GET['ajax_get_vouchers']) && isset($_GET['site_id'])) { - header('Content-Type: application/json'); - - 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'], - Crypto::decrypt($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'], - Crypto::decrypt($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.

-
-
-
-
- - -
- - -
- - - - +requireAdmin(); +$db = Database::getInstance(); +$appTitle = $db->getSetting('app_title', 'UniFi Voucher System'); +I18n::init(); + +// CSV-Export +if (isset($_GET['export_csv']) && isset($_GET['site_id'])) { + if (!$auth->validateCsrfToken($_GET['token'] ?? '')) { http_response_code(403); exit(__('error_csrf')); } + $siteId = (int)$_GET['site_id']; + $site = $db->fetchOne("SELECT * FROM sites WHERE id=? AND is_active=1", [$siteId]); + if (!$site) { http_response_code(404); exit; } + $rows = $db->fetchAll("SELECT voucher_code,voucher_name,max_uses,expire_minutes,status,used_count,created_at,expires_at FROM vouchers WHERE site_id=? ORDER BY created_at DESC", [$siteId]); + $filename = 'vouchers_' . preg_replace('/[^a-z0-9]/i','_',$site['name']) . '_' . date('Ymd_His') . '.csv'; + header('Content-Type: text/csv; charset=UTF-8'); + header('Content-Disposition: attachment; filename="' . $filename . '"'); + $out = fopen('php://output','w'); + fprintf($out, chr(0xEF).chr(0xBB).chr(0xBF)); + fputcsv($out,['Code','Name','Max. Geräte','Gültigkeit (Min)','Status','Genutzt','Erstellt','Läuft ab'],';'); + foreach ($rows as $r) { + fputcsv($out,[$r['voucher_code'],$r['voucher_name'],$r['max_uses'],$r['expire_minutes'],$r['status'],$r['used_count'],$r['created_at'],$r['expires_at']??''],';'); + } + fclose($out); exit; +} + +// AJAX: Vouchers laden +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'=>__('error_site_not_found')]); exit; } + if ($syncFirst) { + try { + $ctrl = new UniFiController($site['unifi_controller_url'],$site['unifi_username'],Crypto::decrypt($site['unifi_password']),$site['site_id']); + $ctrl->syncVouchersToDatabase($db,$siteId); + $db->execute("INSERT INTO settings (setting_key,setting_value) VALUES ('last_cron_sync',NOW()) ON DUPLICATE KEY UPDATE setting_value=NOW()"); + } catch (Exception $e) { error_log("Sync error: ".$e->getMessage()); } + } + $dbVouchers = $db->fetchAll("SELECT * FROM vouchers WHERE site_id=? ORDER BY created_at DESC", [$siteId]); + $vouchers = []; + foreach ($dbVouchers as $v) { + $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' => strtotime($v['created_at']), + 'expire_time' => $v['expires_at'] ? strtotime($v['expires_at']) : 0, + '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'=>__('error_csrf')]); exit; } + try { + $voucherId = $_POST['voucher_id']; + $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'=>__('error_site_not_found')]); exit; } + $ctrl = new UniFiController($site['unifi_controller_url'],$site['unifi_username'],Crypto::decrypt($site['unifi_password']),$site['site_id']); + if ($ctrl->deleteVoucher($voucherId)) { + $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; +} + +$sites = $db->fetchAll("SELECT * FROM sites WHERE is_active=1 ORDER BY name"); +$siteStats = []; +foreach ($sites as $site) { + $siteStats[$site['id']] = $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']]); +} +$lastCronSync = $db->getSetting('last_cron_sync',''); +$currentPage = 'vouchers'; +?> + + + + + + <?= __('vouchers_title') ?> – <?= htmlspecialchars($appTitle) ?> + + + + + + +
+ +

+ +
+ + +
+
+

+ : +
+
+
+ + + +
+
+
+ + + +
+
+

Vouchers

+ +
+
+
+

+
+
+
+ + + + +
+ + + + diff --git a/assets/global.css b/assets/global.css new file mode 100644 index 0000000..27e408f --- /dev/null +++ b/assets/global.css @@ -0,0 +1,471 @@ +/* === DARK MODE CSS VARIABLES === */ +:root { + --bg-body: #f5f7fa; + --bg-card: #ffffff; + --bg-header: #ffffff; + --bg-sidebar: #ffffff; + --bg-hover: #f8f9fa; + --bg-input: #ffffff; + --bg-table-head: #f8f9fa; + --bg-badge-info: #d1ecf1; + --text-primary: #333333; + --text-secondary: #666666; + --text-muted: #999999; + --text-badge-info: #0c5460; + --border-color: #e0e0e0; + --border-hover: #667eea; + --shadow: rgba(0,0,0,0.05); + --shadow-lg: rgba(0,0,0,0.3); + --accent: #667eea; + --accent-hover: #5568d3; + --danger: #dc3545; + --success: #28a745; + --warning: #ffc107; + --code-bg: #f8f9fa; + --input-border: #e0e0e0; + --input-focus: #667eea; + --modal-overlay: rgba(0,0,0,0.5); + --scrollbar-track: #f1f1f1; + --scrollbar-thumb: #c1c1c1; + --toast-bg: #ffffff; + --stat-sub: #f0f0f0; +} + +[data-theme="dark"] { + --bg-body: #0f1117; + --bg-card: #1a1d27; + --bg-header: #1a1d27; + --bg-sidebar: #1a1d27; + --bg-hover: #22273a; + --bg-input: #22273a; + --bg-table-head: #22273a; + --bg-badge-info: #0c3d47; + --text-primary: #e8eaf0; + --text-secondary: #a0a8b8; + --text-muted: #6b7280; + --text-badge-info: #7dd3eb; + --border-color: #2e3347; + --border-hover: #667eea; + --shadow: rgba(0,0,0,0.3); + --shadow-lg: rgba(0,0,0,0.6); + --accent: #7c8ff5; + --accent-hover: #667eea; + --danger: #ef4444; + --success: #22c55e; + --warning: #f59e0b; + --code-bg: #22273a; + --input-border: #2e3347; + --input-focus: #7c8ff5; + --modal-overlay: rgba(0,0,0,0.7); + --scrollbar-track: #1a1d27; + --scrollbar-thumb: #3a3f5a; + --toast-bg: #1a1d27; + --stat-sub: #22273a; +} + +/* === DARK MODE OVERRIDES FOR COMMON ELEMENTS === */ +[data-theme="dark"] body { + background: var(--bg-body); + color: var(--text-primary); +} + +[data-theme="dark"] .header { + background: var(--bg-header) !important; + border-color: var(--border-color) !important; +} + +[data-theme="dark"] .sidebar { + background: var(--bg-sidebar) !important; + border-color: var(--border-color) !important; +} + +[data-theme="dark"] .sidebar-nav a { + color: var(--text-secondary) !important; +} + +[data-theme="dark"] .sidebar-nav a:hover, +[data-theme="dark"] .sidebar-nav a.active { + background: var(--bg-hover) !important; + color: var(--accent) !important; +} + +[data-theme="dark"] .card, +[data-theme="dark"] .stat-card, +[data-theme="dark"] .site-status-card, +[data-theme="dark"] .site-card { + background: var(--bg-card) !important; + border-color: var(--border-color) !important; +} + +[data-theme="dark"] .card-header, +[data-theme="dark"] .modal-header { + border-color: var(--border-color) !important; +} + +[data-theme="dark"] .card-title, +[data-theme="dark"] .page-title, +[data-theme="dark"] .modal-title, +[data-theme="dark"] .stat-card-value, +[data-theme="dark"] h1, [data-theme="dark"] h2, [data-theme="dark"] h3 { + color: var(--text-primary) !important; +} + +[data-theme="dark"] .page-subtitle, +[data-theme="dark"] .stat-card-title, +[data-theme="dark"] .stat-card-sub, +[data-theme="dark"] label { + color: var(--text-secondary) !important; +} + +[data-theme="dark"] .table th { + background: var(--bg-table-head) !important; + color: var(--text-secondary) !important; +} + +[data-theme="dark"] .table td { + color: var(--text-primary) !important; + border-color: var(--border-color) !important; +} + +[data-theme="dark"] .table tr:hover { + background: var(--bg-hover) !important; +} + +[data-theme="dark"] input[type="text"], +[data-theme="dark"] input[type="email"], +[data-theme="dark"] input[type="password"], +[data-theme="dark"] input[type="number"], +[data-theme="dark"] input[type="url"], +[data-theme="dark"] select, +[data-theme="dark"] textarea { + background: var(--bg-input) !important; + border-color: var(--input-border) !important; + color: var(--text-primary) !important; +} + +[data-theme="dark"] input::placeholder, +[data-theme="dark"] textarea::placeholder { + color: var(--text-muted) !important; +} + +[data-theme="dark"] input:focus, +[data-theme="dark"] select:focus, +[data-theme="dark"] textarea:focus { + border-color: var(--input-focus) !important; +} + +[data-theme="dark"] .btn-secondary { + background: var(--bg-hover) !important; + color: var(--text-secondary) !important; + border-color: var(--border-color) !important; +} + +[data-theme="dark"] .btn-secondary:hover { + background: var(--border-color) !important; + color: var(--text-primary) !important; +} + +[data-theme="dark"] code { + background: var(--code-bg) !important; + color: var(--accent) !important; +} + +[data-theme="dark"] .modal-content { + background: var(--bg-card) !important; +} + +[data-theme="dark"] .badge-info { + background: var(--bg-badge-info) !important; + color: var(--text-badge-info) !important; +} + +[data-theme="dark"] .stat-card-icon { + filter: brightness(0.8); +} + +[data-theme="dark"] .info-box { + background: #0c2340 !important; + border-color: #1a4a7a !important; +} + +[data-theme="dark"] .info-box h4 { color: #60a5fa !important; } +[data-theme="dark"] .info-box p { color: #93c5fd !important; } + +[data-theme="dark"] .placeholder-info { + background: #1a1500 !important; + border-color: #44390a !important; +} + +[data-theme="dark"] .placeholder-info h4 { color: #d4a017 !important; } +[data-theme="dark"] .placeholder-info code { background: #2a2000 !important; } + +[data-theme="dark"] .alert-error { + background: #2d0a0a !important; + border-color: #7f1d1d !important; + color: #fca5a5 !important; +} + +[data-theme="dark"] .alert-success { + background: #052e16 !important; + border-color: #14532d !important; + color: #86efac !important; +} + +[data-theme="dark"] .user-menu { + background: var(--bg-hover) !important; +} + +[data-theme="dark"] .filter-btn { + background: var(--bg-card) !important; + border-color: var(--border-color) !important; + color: var(--text-primary) !important; +} + +[data-theme="dark"] .filter-btn.active { + background: var(--accent) !important; + border-color: var(--accent) !important; + color: white !important; +} + +[data-theme="dark"] .tab-navigation { + background: var(--bg-table-head) !important; + border-color: var(--border-color) !important; +} + +[data-theme="dark"] .tab-button { + color: var(--text-secondary) !important; +} + +[data-theme="dark"] .tab-button.active { + background: var(--bg-card) !important; + color: var(--accent) !important; +} + +[data-theme="dark"] .section-divider { + border-color: var(--border-color) !important; +} + +[data-theme="dark"] .site-stat { + background: var(--bg-hover) !important; +} + +[data-theme="dark"] .live-badge { + background: #052e16 !important; + color: #86efac !important; +} + +[data-theme="dark"] .live-badge.loading { + background: #1a0e00 !important; + color: #fcd34d !important; +} + +[data-theme="dark"] .live-badge.error { + background: #2d0a0a !important; + color: #fca5a5 !important; +} + +/* === DARK MODE TOGGLE BUTTON === */ +.dark-mode-toggle { + background: none; + border: 2px solid var(--border-color); + border-radius: 8px; + width: 38px; + height: 38px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + font-size: 18px; + transition: all 0.2s; + color: var(--text-secondary); + flex-shrink: 0; +} +.dark-mode-toggle:hover { + border-color: var(--accent); + color: var(--accent); + background: var(--bg-hover); +} + +/* === LANGUAGE SWITCHER === */ +.lang-switcher { + display: flex; + align-items: center; + gap: 4px; + background: var(--bg-hover); + border: 1px solid var(--border-color); + border-radius: 8px; + padding: 4px; + flex-shrink: 0; +} +.lang-btn { + padding: 4px 8px; + border: none; + background: none; + border-radius: 5px; + cursor: pointer; + font-size: 12px; + font-weight: 600; + color: var(--text-muted); + transition: all 0.2s; +} +.lang-btn.active { + background: var(--accent); + color: white; +} +.lang-btn:hover:not(.active) { + color: var(--accent); +} + +/* === TOAST NOTIFICATIONS === */ +#toast-container { + position: fixed; + bottom: 30px; + right: 30px; + z-index: 9999; + display: flex; + flex-direction: column; + gap: 10px; + pointer-events: none; +} +.toast { + background: var(--toast-bg); + padding: 14px 20px; + border-radius: 12px; + box-shadow: 0 10px 40px var(--shadow-lg); + display: flex; + align-items: center; + gap: 12px; + min-width: 300px; + max-width: 400px; + transform: translateX(120%); + transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); + pointer-events: all; + border: 1px solid var(--border-color); +} +.toast.show { transform: translateX(0); } +.toast.success { border-left: 4px solid var(--success); } +.toast.error { border-left: 4px solid var(--danger); } +.toast.info { border-left: 4px solid var(--accent); } +.toast.warning { border-left: 4px solid var(--warning); } +.toast-icon { font-size: 18px; flex-shrink: 0; } +.toast.success .toast-icon { color: var(--success); } +.toast.error .toast-icon { color: var(--danger); } +.toast.info .toast-icon { color: var(--accent); } +.toast.warning .toast-icon { color: var(--warning); } +.toast-body { flex: 1; } +.toast-title { font-weight: 600; font-size: 14px; color: var(--text-primary); } +.toast-msg { font-size: 13px; color: var(--text-secondary); margin-top: 2px; } +.toast-close { + background: none; border: none; cursor: pointer; + color: var(--text-muted); font-size: 18px; padding: 0; line-height: 1; + transition: color 0.2s; +} +.toast-close:hover { color: var(--text-primary); } + +/* === MOBILE RESPONSIVE === */ +.mobile-menu-btn { + display: none; + background: none; + border: 2px solid var(--border-color); + border-radius: 8px; + width: 38px; + height: 38px; + cursor: pointer; + align-items: center; + justify-content: center; + font-size: 16px; + color: var(--text-secondary); + transition: all 0.2s; + flex-shrink: 0; +} +.mobile-menu-btn:hover { + border-color: var(--accent); + color: var(--accent); +} + +.sidebar-overlay { + display: none; + position: fixed; + top: 0; left: 0; right: 0; bottom: 0; + background: rgba(0,0,0,0.5); + z-index: 199; +} +.sidebar-overlay.active { display: block; } + +@media (max-width: 768px) { + .mobile-menu-btn { display: flex; } + + .sidebar { + transform: translateX(-100%); + transition: transform 0.3s ease; + z-index: 200; + } + .sidebar.mobile-open { transform: translateX(0); } + + .main-content { + margin-left: 0 !important; + padding: 15px !important; + } + + .header { + padding: 0 15px !important; + } + + .stats-grid { + grid-template-columns: repeat(2, 1fr) !important; + } + + .site-status { + grid-template-columns: 1fr !important; + } + + .sites-grid { + grid-template-columns: 1fr !important; + } + + .page-header { + flex-direction: column; + align-items: flex-start !important; + gap: 15px; + } + + .table-container { + overflow-x: auto; + } + + .header-right { + gap: 8px !important; + } + + .user-menu span { display: none; } + + .form-grid { + grid-template-columns: 1fr !important; + } + + .tab-navigation { + flex-wrap: wrap; + } + + .filter-row { + flex-wrap: wrap; + } +} + +@media (max-width: 480px) { + .stats-grid { + grid-template-columns: 1fr !important; + } + .lang-switcher { display: none; } +} + +/* === SCROLLBAR STYLING === */ +::-webkit-scrollbar { width: 6px; height: 6px; } +::-webkit-scrollbar-track { background: var(--scrollbar-track); } +::-webkit-scrollbar-thumb { background: var(--scrollbar-thumb); border-radius: 3px; } +::-webkit-scrollbar-thumb:hover { background: var(--text-muted); } + +/* === SMOOTH TRANSITIONS === */ +body, .card, .sidebar, .header, input, select, textarea, .btn { + transition: background-color 0.2s, border-color 0.2s, color 0.2s; +} diff --git a/assets/global.js b/assets/global.js new file mode 100644 index 0000000..1f388e6 --- /dev/null +++ b/assets/global.js @@ -0,0 +1,117 @@ +/* === DARK MODE === */ +(function() { + const saved = localStorage.getItem('theme') || 'light'; + document.documentElement.setAttribute('data-theme', saved); +})(); + +function toggleDarkMode() { + const html = document.documentElement; + const current = html.getAttribute('data-theme') || 'light'; + const next = current === 'dark' ? 'light' : 'dark'; + html.setAttribute('data-theme', next); + localStorage.setItem('theme', next); + updateDarkModeBtn(); +} + +function updateDarkModeBtn() { + const btn = document.getElementById('darkModeBtn'); + if (!btn) return; + const isDark = document.documentElement.getAttribute('data-theme') === 'dark'; + btn.textContent = isDark ? '☀️' : '🌙'; + btn.title = isDark ? 'Light Mode' : 'Dark Mode'; +} + +document.addEventListener('DOMContentLoaded', updateDarkModeBtn); + +/* === TOAST NOTIFICATIONS === */ +(function() { + let container = null; + + function getContainer() { + if (!container) { + container = document.getElementById('toast-container'); + if (!container) { + container = document.createElement('div'); + container.id = 'toast-container'; + document.body.appendChild(container); + } + } + return container; + } + + const icons = { + success: '✓', + error: '✕', + info: 'ℹ', + warning: '⚠' + }; + + window.showToast = function(type, title, message, duration) { + duration = duration || 4000; + const c = getContainer(); + const el = document.createElement('div'); + el.className = 'toast ' + type; + el.innerHTML = ` + ${icons[type] || 'ℹ'} +
+
${title}
+ ${message ? `
${message}
` : ''} +
+ + `; + c.appendChild(el); + requestAnimationFrame(() => { + requestAnimationFrame(() => el.classList.add('show')); + }); + setTimeout(() => { + el.classList.remove('show'); + setTimeout(() => el.remove(), 350); + }, duration); + return el; + }; +})(); + +/* === MOBILE SIDEBAR === */ +function toggleMobileSidebar() { + const sidebar = document.querySelector('.sidebar'); + const overlay = document.querySelector('.sidebar-overlay'); + if (!sidebar) return; + sidebar.classList.toggle('mobile-open'); + if (overlay) overlay.classList.toggle('active'); +} + +function closeMobileSidebar() { + const sidebar = document.querySelector('.sidebar'); + const overlay = document.querySelector('.sidebar-overlay'); + if (sidebar) sidebar.classList.remove('mobile-open'); + if (overlay) overlay.classList.remove('active'); +} + +document.addEventListener('DOMContentLoaded', function() { + const overlay = document.querySelector('.sidebar-overlay'); + if (overlay) overlay.addEventListener('click', closeMobileSidebar); + + document.addEventListener('keydown', function(e) { + if (e.key === 'Escape') closeMobileSidebar(); + }); +}); + +/* === LANGUAGE SWITCHER === */ +function switchLanguage(lang) { + fetch('?set_lang=' + lang, { method: 'GET' }).then(() => location.reload()); +} + +/* === CLIPBOARD === */ +function copyToClipboard(text, successMsg) { + navigator.clipboard.writeText(text).then(() => { + showToast('success', successMsg || 'Kopiert!', ''); + }).catch(() => { + const ta = document.createElement('textarea'); + ta.value = text; + document.body.appendChild(ta); + ta.select(); + document.execCommand('copy'); + document.body.removeChild(ta); + showToast('success', successMsg || 'Kopiert!', ''); + }); +} diff --git a/database.sql b/database.sql index f0686d8..128a520 100644 --- a/database.sql +++ b/database.sql @@ -1,118 +1,138 @@ --- 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; - -CREATE TABLE IF NOT EXISTS `login_attempts` ( - `id` INT PRIMARY KEY AUTO_INCREMENT, - `ip_address` VARCHAR(45) NOT NULL, - `email` VARCHAR(255) NOT NULL, - `attempted_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - INDEX `idx_ip` (`ip_address`), - INDEX `idx_email` (`email`), - INDEX `idx_attempted` (`attempted_at`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - -CREATE TABLE IF NOT EXISTS `audit_log` ( - `id` INT PRIMARY KEY AUTO_INCREMENT, - `user_id` INT, - `action` VARCHAR(100) NOT NULL, - `entity_type` VARCHAR(50), - `entity_id` VARCHAR(100), - `details` TEXT, - `ip_address` VARCHAR(45), - `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE SET NULL, - INDEX `idx_user` (`user_id`), - INDEX `idx_action` (`action`), - INDEX `idx_created` (`created_at`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - --- Migration für bestehende Installationen: --- Neue Tabellen werden automatisch erstellt (CREATE TABLE IF NOT EXISTS) \ No newline at end of file +-- 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 `voucher_templates` ( + `id` INT PRIMARY KEY AUTO_INCREMENT, + `name` VARCHAR(255) NOT NULL, + `max_uses` INT NOT NULL DEFAULT 1, + `expire_minutes` INT NOT NULL DEFAULT 480, + `description` VARCHAR(500), + `is_active` TINYINT(1) DEFAULT 1, + `created_by` INT, + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + FOREIGN KEY (`created_by`) REFERENCES `users`(`id`) ON DELETE SET NULL +) 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; + +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; + +CREATE TABLE IF NOT EXISTS `login_attempts` ( + `id` INT PRIMARY KEY AUTO_INCREMENT, + `ip_address` VARCHAR(45) NOT NULL, + `email` VARCHAR(255) NOT NULL, + `attempted_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + INDEX `idx_ip` (`ip_address`), + INDEX `idx_email` (`email`), + INDEX `idx_attempted` (`attempted_at`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE IF NOT EXISTS `audit_log` ( + `id` INT PRIMARY KEY AUTO_INCREMENT, + `user_id` INT, + `action` VARCHAR(100) NOT NULL, + `entity_type` VARCHAR(50), + `entity_id` VARCHAR(100), + `details` TEXT, + `ip_address` VARCHAR(45), + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE SET NULL, + INDEX `idx_user` (`user_id`), + INDEX `idx_action` (`action`), + INDEX `idx_created` (`created_at`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE IF NOT EXISTS `password_reset_tokens` ( + `id` INT PRIMARY KEY AUTO_INCREMENT, + `user_id` INT NOT NULL, + `token` VARCHAR(128) NOT NULL, + `expires_at` TIMESTAMP NOT NULL, + `used` TINYINT(1) DEFAULT 0, + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE, + UNIQUE KEY `unique_token` (`token`), + INDEX `idx_expires` (`expires_at`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- Migrations für bestehende Installationen: +-- ALTER TABLE vouchers ADD COLUMN IF NOT EXISTS `status` ENUM('valid', 'used', 'expired') DEFAULT 'valid'; +-- ALTER TABLE vouchers ADD COLUMN IF NOT EXISTS `used_count` INT DEFAULT 0; +-- ALTER TABLE vouchers ADD COLUMN IF NOT EXISTS `expires_at` TIMESTAMP NULL; +-- ALTER TABLE vouchers ADD COLUMN IF NOT EXISTS `synced_from_unifi` TINYINT(1) DEFAULT 0; +-- ALTER TABLE vouchers ADD COLUMN IF NOT EXISTS `last_sync` TIMESTAMP NULL; diff --git a/forgot_password.php b/forgot_password.php new file mode 100644 index 0000000..aff3b62 --- /dev/null +++ b/forgot_password.php @@ -0,0 +1,133 @@ +isLoggedIn()) { header('Location: index.php'); exit; } +I18n::init(); + +$db = Database::getInstance(); +$appTitle = $db->getSetting('app_title', 'UniFi Voucher System'); +$logoUrl = $db->getSetting('logo_url', ''); +$systemUrl = rtrim($db->getSetting('system_url', ''), '/'); + +$error = ''; +$success = ''; + +if ($_SERVER['REQUEST_METHOD'] === 'POST') { + $email = trim($_POST['email'] ?? ''); + + if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { + $error = __('error_email_invalid'); + } else { + $user = $db->fetchOne("SELECT * FROM users WHERE email = ? AND is_active = 1 AND password_hash IS NOT NULL", [$email]); + + // Always show success (don't reveal whether email exists) + if ($user) { + try { + // Delete old tokens for this user + $db->execute("DELETE FROM password_reset_tokens WHERE user_id = ?", [$user['id']]); + + // Generate token + $token = bin2hex(random_bytes(32)); + $expiresAt = date('Y-m-d H:i:s', strtotime('+1 hour')); + + $db->execute( + "INSERT INTO password_reset_tokens (user_id, token, expires_at) VALUES (?, ?, ?)", + [$user['id'], $token, $expiresAt] + ); + + // Send email + $resetUrl = $systemUrl . '/reset_password.php?token=' . $token; + $mailer = new Mailer(); + $subject = $appTitle . ' – Passwort zurücksetzen'; + $body = "Hallo {$user['name']},\n\n" . + "Sie haben eine Passwort-Rücksetzung angefordert.\n\n" . + "Klicken Sie auf den folgenden Link, um Ihr Passwort zurückzusetzen (gültig für 1 Stunde):\n\n" . + $resetUrl . "\n\n" . + "Falls Sie dies nicht angefordert haben, ignorieren Sie diese E-Mail.\n\n" . + $appTitle; + $mailer->sendRaw($user['email'], $subject, $body); + + // Audit log + $db->execute( + "INSERT INTO audit_log (user_id, action, entity_type, entity_id, details, ip_address) VALUES (?, 'password_reset', 'user', ?, 'Reset-Link angefordert', ?)", + [$user['id'], $user['id'], $_SERVER['REMOTE_ADDR'] ?? ''] + ); + } catch (Exception $e) { + // Silent – don't reveal errors to user + } + } + + $success = __('reset_success'); + } +} +?> + + + + + + <?= __('reset_title') ?> – <?= htmlspecialchars($appTitle) ?> + + + + + +
+ + + +

+ + +

+

+ + +
+ + +
+ + + +
+
+ + +
+ +
+ + + +
+ + + diff --git a/includes/Auth.php b/includes/Auth.php index 4bb0b79..af855eb 100644 --- a/includes/Auth.php +++ b/includes/Auth.php @@ -38,6 +38,7 @@ class Auth { $this->clearLoginAttempts($ip, $email); $this->setUserSession($user); $this->updateLastLogin($user['id']); + $this->writeAuditLog($user['id'], 'user_login', 'user', $user['id'], 'Login erfolgreich'); return true; } @@ -45,6 +46,17 @@ class Auth { return false; } + public function writeAuditLog($userId, $action, $entityType = null, $entityId = null, $details = null) { + try { + $this->db->execute( + "INSERT INTO audit_log (user_id, action, entity_type, entity_id, details, ip_address) VALUES (?, ?, ?, ?, ?, ?)", + [$userId, $action, $entityType, $entityId !== null ? (string)$entityId : null, $details, $_SERVER['REMOTE_ADDR'] ?? ''] + ); + } catch (\Exception $e) { + // audit_log table may not exist on old installs + } + } + private function isRateLimited($ip, $email) { try { $count = $this->db->fetchOne( diff --git a/includes/I18n.php b/includes/I18n.php new file mode 100644 index 0000000..9114407 --- /dev/null +++ b/includes/I18n.php @@ -0,0 +1,53 @@ + $v) { + $text = str_replace('{' . $k . '}', (string)$v, $text); + } + return $text; + } + + public static function getLanguage(): string { + return self::$language; + } + + public static function getAvailable(): array { + return ['de' => 'Deutsch', 'en' => 'English']; + } +} + +function __($key, array $replace = []): string { + return I18n::t($key, $replace); +} diff --git a/includes/Mailer.php b/includes/Mailer.php index 9d0a5d5..9fe53de 100644 --- a/includes/Mailer.php +++ b/includes/Mailer.php @@ -26,6 +26,10 @@ class Mailer { $this->fromName = $this->db->getSetting('smtp_from_name', $this->db->getSetting('app_title', 'UniFi Voucher System')); } + public function sendRaw($to, $subject, $plainBody) { + return $this->send($to, $subject, $plainBody, false); + } + public function send($to, $subject, $body, $isHtml = false) { if (!$this->smtpEnabled || empty($this->smtpHost)) { // Fallback auf PHP mail() diff --git a/includes/admin_nav.php b/includes/admin_nav.php new file mode 100644 index 0000000..88d5f53 --- /dev/null +++ b/includes/admin_nav.php @@ -0,0 +1,127 @@ +getSetting('favicon_url', '') : ''; +$currentUser = isset($auth) ? $auth->getCurrentUser() : null; +$lang = I18n::getLanguage(); +?> + + + + + + + + + + + + + + +
+
+ +
+ + +
+
+
+ +
+ $label): ?> + + +
+ + + + + + + + + +
+
+
+
+ +
+
+ + + +
+ diff --git a/index.php b/index.php index eaf5979..113923c 100644 --- a/index.php +++ b/index.php @@ -1,729 +1,668 @@ -= $maxRequests) { - $_SESSION['voucher_create_times'] = $timestamps; - return true; - } - - $timestamps[] = $now; - $_SESSION['voucher_create_times'] = $timestamps; - return false; -} - -// Settings laden -$appTitle = $db->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->validateCsrfToken($_POST['csrf_token'] ?? '')) { - // CSRF wird jetzt fuer ALLE geprueft – auch fuer anonyme oeffentliche - // Erstellung (Token wird per Session auch ohne Login vergeben). - $error = 'Ungültiges Sicherheits-Token'; - } elseif (!$auth->isLoggedIn() && isVoucherRateLimited()) { - // Einfacher Session-basierter Throttle gegen Missbrauch/Spam im - // oeffentlichen Modus (kein Login = kein Benutzerkontext). - $error = 'Zu viele Anfragen. Bitte warten Sie einen Moment.'; - } 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'], - Crypto::decrypt($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 -
-
- -
-
-
-
- -
- - -
- - - - -

- - -
- - - -
-
-
✓ Ihr Zugangs-Code
-
-
- Der Code ist 8 Stunden ab Erstellung gültig -
-
-
-
QR-Code scannen zum Verbinden
-
-
- - -
- -

- - -
- -
- -
- -
- -
- - - - -
- - - -

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

-
- - -
- - - - - - -
- - -
- -
- - -
- -
- - -
- - - - - -
- - -
- -

- - -
- -
- - -
- - - - - - += $maxRequests) { + $_SESSION['voucher_create_times'] = $timestamps; + return true; + } + $timestamps[] = $now; + $_SESSION['voucher_create_times'] = $timestamps; + return false; +} + +$appTitle = $db->getSetting('app_title', 'UniFi Voucher System'); +$logoUrl = $db->getSetting('logo_url', ''); +$instructionHeader = $db->getSetting('instruction_header', ''); +$instructionText = $db->getSetting('instruction_text', ''); +$publicAccess = $db->getSetting('public_access', 0); +$smtpEnabled = $db->getSetting('smtp_enabled', '0') === '1'; +$printTemplate = $db->getSetting('print_template', '

{APP_TITLE}

WLAN Zugangscode

{VOUCHER_CODE}

Gültig bis: {EXPIRY_DATE} {EXPIRY_TIME}

Standort: {SITE_NAME}

Maximale Geräte: {MAX_USES}


{INSTRUCTIONS}
'); +$defaultExpire = max(1, (int)$db->getSetting('default_expire_minutes', 480)); +$defaultMaxUses = max(1, (int)$db->getSetting('default_max_uses', 1)); +$maxUsesLimit = max(1, (int)$db->getSetting('max_uses_limit', 10)); + +if (!$publicAccess && !$auth->isLoggedIn()) { + header('Location: login.php'); + exit; +} + +try { + $templates = $db->fetchAll("SELECT * FROM voucher_templates WHERE is_active = 1 ORDER BY name"); +} catch (Exception $e) { + $templates = []; +} + +$error = ''; +$success = ''; +$voucherCode = ''; +$voucherCreated = false; +$voucherData = []; +$bulkVouchers = []; +$bulkCreated = false; + +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 { + $sites = $db->fetchAll("SELECT * FROM sites WHERE is_active = 1 AND public_access = 1 ORDER BY name"); +} + +$autoSelectSite = (count($sites) === 1) ? $sites[0]['id'] : 0; + +// Helper: create one voucher and save to DB +function doCreateVoucher($db, $site, $voucherName, $maxUses, $expireMinutes, $userId) { + $datum = date('Y-m-d'); + $fullName = $datum . '_' . $voucherName; + $controller = new UniFiController( + $site['unifi_controller_url'], + $site['unifi_username'], + Crypto::decrypt($site['unifi_password']), + $site['site_id'] + ); + $voucher = $controller->createVoucher($fullName, $maxUses, $expireMinutes); + if (!is_array($voucher) || empty($voucher['formatted_code'])) { + throw new Exception(__('error_voucher_invalid')); + } + $db->execute( + "INSERT INTO vouchers (site_id, user_id, voucher_code, voucher_name, max_uses, expire_minutes, unifi_voucher_id) + VALUES (?, ?, ?, ?, ?, ?, ?)", + [$site['id'], $userId, $voucher['code'], $fullName, $maxUses, $expireMinutes, $voucher['unifi_id'] ?? null] + ); + $expiryTs = time() + ($expireMinutes * 60); + return [ + 'code' => $voucher['formatted_code'], + 'site_name' => $site['name'], + 'max_uses' => $maxUses, + 'expire_min' => $expireMinutes, + 'expiry_date' => date('d.m.Y', $expiryTs), + 'expiry_time' => date('H:i', $expiryTs), + ]; +} + +// Single voucher +if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['create_voucher'])) { + if (!$publicAccess && !$auth->isLoggedIn()) { + $error = __('error_login_req'); + } elseif (!$auth->validateCsrfToken($_POST['csrf_token'] ?? '')) { + // CSRF fuer ALLE (auch anonyme oeffentliche Erstellung) + $error = __('error_csrf'); + } elseif (!$auth->isLoggedIn() && isVoucherRateLimited()) { + $error = 'Zu viele Anfragen. Bitte warten Sie einen Moment.'; + } else { + try { + $siteId = (int)($_POST['site_id'] ?? 0); + $voucherName = trim((string)($_POST['voucher_name'] ?? '')); + $maxUses = (int)($_POST['max_uses'] ?? $defaultMaxUses); + $expireMinutes = max(1, (int)($_POST['expire_minutes'] ?? $defaultExpire)); + $sendEmail = isset($_POST['send_email']) && !empty($_POST['recipient_email']); + $recipientEmail= trim((string)($_POST['recipient_email'] ?? '')); + + if (empty($voucherName)) throw new Exception(__('error_name_req')); + if ($maxUses < 1 || $maxUses > $maxUsesLimit) throw new Exception(__('error_devices_range', ['max' => $maxUsesLimit])); + if ($siteId <= 0) throw new Exception(__('error_site_req')); + if ($sendEmail && !filter_var($recipientEmail, FILTER_VALIDATE_EMAIL)) throw new Exception(__('error_email_invalid')); + + if ($auth->isLoggedIn() && !$auth->hasAccessToSite($siteId)) throw new Exception(__('error_site_no_perm')); + + $site = $db->fetchOne("SELECT * FROM sites WHERE id = ? AND is_active = 1", [$siteId]); + if (!$site) throw new Exception(__('error_site_not_found')); + + $userId = $auth->isLoggedIn() ? ($_SESSION['user_id'] ?? null) : null; + $voucherData = doCreateVoucher($db, $site, $voucherName, $maxUses, $expireMinutes, $userId); + $voucherCode = $voucherData['code']; + $voucherCreated = true; + + if ($sendEmail && !empty($recipientEmail)) { + $mailer->sendVoucherEmail($recipientEmail, $voucherCode, $site['name'], $maxUses); + $success = 'Voucher erstellt. E-Mail versendet.'; + } else { + $success = 'Voucher erfolgreich erstellt!'; + } + } catch (Exception $e) { + $error = 'Fehler: ' . $e->getMessage(); + } + } +} + +// Bulk voucher creation +if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['create_bulk'])) { + if (!$publicAccess && !$auth->isLoggedIn()) { + $error = __('error_login_req'); + } elseif (!$auth->validateCsrfToken($_POST['csrf_token'] ?? '')) { + // CSRF fuer ALLE (auch anonyme oeffentliche Erstellung) + $error = __('error_csrf'); + } elseif (!$auth->isLoggedIn() && isVoucherRateLimited()) { + $error = 'Zu viele Anfragen. Bitte warten Sie einen Moment.'; + } else { + try { + $siteId = (int)($_POST['site_id'] ?? 0); + $voucherName = trim((string)($_POST['voucher_name'] ?? '')); + $maxUses = (int)($_POST['max_uses'] ?? $defaultMaxUses); + $expireMinutes = max(1, (int)($_POST['expire_minutes'] ?? $defaultExpire)); + $bulkCount = max(1, min(20, (int)($_POST['bulk_count'] ?? 1))); + + if (empty($voucherName)) throw new Exception(__('error_name_req')); + if ($maxUses < 1 || $maxUses > $maxUsesLimit) throw new Exception(__('error_devices_range', ['max' => $maxUsesLimit])); + if ($siteId <= 0) throw new Exception(__('error_site_req')); + if ($auth->isLoggedIn() && !$auth->hasAccessToSite($siteId)) throw new Exception(__('error_site_no_perm')); + + $site = $db->fetchOne("SELECT * FROM sites WHERE id = ? AND is_active = 1", [$siteId]); + if (!$site) throw new Exception(__('error_site_not_found')); + + $userId = $auth->isLoggedIn() ? ($_SESSION['user_id'] ?? null) : null; + + for ($i = 0; $i < $bulkCount; $i++) { + $bulkVouchers[] = doCreateVoucher($db, $site, $voucherName . '_' . ($i + 1), $maxUses, $expireMinutes, $userId); + } + + $bulkCreated = true; + $success = str_replace('{count}', $bulkCount, __('bulk_success')); + } catch (Exception $e) { + $error = 'Fehler: ' . $e->getMessage(); + } + } +} + +$currentUser = $auth->isLoggedIn() ? $auth->getCurrentUser() : null; + +// Build print HTML for each voucher +function buildPrintCard($template, $data, $instructionHeader, $instructionText, $appTitle) { + $instructions = $instructionHeader || $instructionText + ? htmlspecialchars($instructionHeader) . "\n" . $instructionText + : ''; + return str_replace( + ['{VOUCHER_CODE}', '{SITE_NAME}', '{MAX_USES}', '{APP_TITLE}', '{INSTRUCTIONS}', '{EXPIRY_DATE}', '{EXPIRY_TIME}'], + [$data['code'], htmlspecialchars($data['site_name']), $data['max_uses'], htmlspecialchars($appTitle), $instructions, $data['expiry_date'], $data['expiry_time']], + $template + ); +} +?> + + + + + + <?= htmlspecialchars($appTitle) ?> + + + + + + + + + +
+
+ $label): ?> + + +
+ +
+ + +
+
👋 htmlspecialchars($currentUser['name'])]) ?>
+
+ isAdmin()): ?> + ⚙️ + + +
+
+ +
+
+
+ 🔐 +
+
+ + +
+ + + + + +

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

+
+
+ + +
+ +
+ + + +

+ +
+ $bv): ?> +
+ +
+ +
+ +
+ + + + + + + + + + + $bv): ?> + + + + + + + + +
#
+ +
+

Code anklicken zum Kopieren

+
+ +
+ +
+ +
+
+ + +
+
📶
+


+ isAdmin()): ?> + + + + +

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

+
+
+ + + +
+ +
+ + + + diff --git a/lang/de.php b/lang/de.php new file mode 100644 index 0000000..c96ba91 --- /dev/null +++ b/lang/de.php @@ -0,0 +1,289 @@ + 'Dashboard', + 'nav_sites' => 'Sites verwalten', + 'nav_users' => 'Benutzer verwalten', + 'nav_vouchers' => 'Live Vouchers', + 'nav_templates' => 'Voucher-Profile', + 'nav_audit_log' => 'Audit-Log', + 'nav_settings' => 'Einstellungen', + 'nav_update' => 'System-Update', + 'nav_back' => 'Zurück zur Startseite', + 'nav_administration'=> 'Administration', + + // Common buttons + 'btn_save' => 'Speichern', + 'btn_cancel' => 'Abbrechen', + 'btn_add' => 'Hinzufügen', + 'btn_edit' => 'Bearbeiten', + 'btn_delete' => 'Löschen', + 'btn_create' => 'Erstellen', + 'btn_close' => 'Schließen', + 'btn_back' => 'Zurück', + 'btn_login' => 'Anmelden', + 'btn_logout' => 'Abmelden', + 'btn_refresh' => 'Aktualisieren', + 'btn_export_csv' => 'CSV exportieren', + 'btn_print' => 'Ausdrucken', + 'btn_send' => 'Senden', + 'btn_test' => 'Testen', + 'btn_generate' => 'Generieren', + 'btn_copy' => 'Kopieren', + 'btn_new_code' => 'Weiteren Code erstellen', + + // Common labels + 'label_name' => 'Name', + 'label_email' => 'E-Mail', + 'label_password' => 'Passwort', + 'label_status' => 'Status', + 'label_actions' => 'Aktionen', + 'label_created' => 'Erstellt', + 'label_site' => 'Standort', + 'label_sites' => 'Sites', + 'label_role' => 'Rolle', + 'label_code' => 'Code', + 'label_note' => 'Notiz', + 'label_usage' => 'Nutzung', + 'label_expires' => 'Gültigkeit', + 'label_devices' => 'Geräte', + 'label_duration' => 'Dauer', + 'label_description' => 'Beschreibung', + 'label_language' => 'Sprache', + + // Status texts + 'status_active' => 'Aktiv', + 'status_inactive' => 'Inaktiv', + 'status_valid' => 'Gültig', + 'status_used' => 'Verwendet', + 'status_expired' => 'Abgelaufen', + 'status_admin' => 'Admin', + 'status_user' => 'Benutzer', + 'status_public' => 'Öffentlich', + 'status_all' => 'Alle', + + // Dashboard + 'dashboard_title' => 'Dashboard', + 'dashboard_subtitle'=> 'Übersicht über Ihr UniFi Voucher System', + 'dashboard_active_sites' => 'Aktive Sites', + 'dashboard_users' => 'Benutzer', + 'dashboard_valid' => 'Gültige Vouchers', + 'dashboard_used' => 'Verwendet', + 'dashboard_expired' => 'Abgelaufen', + 'dashboard_total' => 'Gesamt', + 'dashboard_live_refresh' => 'Live aktualisieren', + 'dashboard_trend' => 'Voucher-Trend (Letzte 7 Tage)', + 'dashboard_top_users'=> 'Top 5 Benutzer (Letzte 30 Tage)', + 'dashboard_recent' => 'Letzte Vouchers (aus Datenbank)', + 'dashboard_creator' => 'Ersteller', + 'dashboard_public' => 'Öffentlich', + 'dashboard_vouchers_per_site' => 'Live: Vouchers pro Site', + 'dashboard_no_data' => 'Noch keine Daten verfügbar', + 'dashboard_no_vouchers' => 'Noch keine Vouchers erstellt', + + // Voucher creation + 'voucher_name_label' => 'Voucher-Name *', + 'voucher_name_hint' => 'z.B. Besprechung UL, Vertretername Firma XY', + 'voucher_devices_label' => 'Wie viele Geräte dürfen sich einloggen? *', + 'voucher_site_label' => 'Standort *', + 'voucher_site_select' => 'Bitte wählen...', + 'voucher_email_send' => 'Code per E-Mail versenden', + 'voucher_email_label' => 'E-Mail-Adresse des Empfängers', + 'voucher_email_hint' => 'gast@example.com', + 'voucher_create_btn' => 'Voucher erstellen', + 'voucher_creating' => 'Erstelle Voucher...', + 'voucher_success_title' => '✓ Ihr Zugangs-Code', + 'voucher_validity' => 'Gültig für {minutes} Minuten ab Erstellung', + 'voucher_qr_label' => 'QR-Code scannen zum Verbinden', + 'voucher_print_btn' => 'Code ausdrucken', + 'voucher_no_sites' => 'Keine verfügbaren Sites gefunden.', + 'voucher_no_sites_admin'=> 'Klicken Sie hier, um Sites anzulegen', + 'voucher_no_sites_user' => 'Bitte kontaktieren Sie Ihren Administrator.', + 'voucher_template_select'=> '-- Kein Profil (manuell) --', + 'voucher_template_label'=> 'Schnellprofil (optional)', + + // Bulk creation + 'bulk_tab' => 'Einzeln', + 'bulk_tab_bulk' => 'Mehrere auf einmal', + 'bulk_quantity' => 'Anzahl Vouchers', + 'bulk_quantity_hint'=> 'Wie viele Vouchers sollen erstellt werden?', + 'bulk_name_prefix' => 'Name / Präfix', + 'bulk_create_btn' => '{count} Vouchers erstellen', + 'bulk_creating' => 'Erstelle {count} Vouchers...', + 'bulk_success' => '{count} Vouchers erfolgreich erstellt!', + 'bulk_print_all' => 'Alle ausdrucken', + 'bulk_results' => 'Erstellte Vouchers ({count})', + + // Templates + 'templates_title' => 'Voucher-Profile', + 'templates_subtitle'=> 'Vordefinierte Voucher-Konfigurationen', + 'templates_add' => 'Neues Profil', + 'templates_name' => 'Profilname', + 'templates_devices' => 'Max. Geräte', + 'templates_duration'=> 'Gültigkeit (Minuten)', + 'templates_desc' => 'Beschreibung (optional)', + 'templates_none' => 'Noch keine Profile vorhanden.', + 'templates_add_hint'=> 'Erstellen Sie Profile für häufig verwendete Voucher-Konfigurationen.', + 'templates_edit' => 'Profil bearbeiten', + 'templates_added' => 'Profil erfolgreich erstellt!', + 'templates_updated' => 'Profil erfolgreich aktualisiert!', + 'templates_deleted' => 'Profil erfolgreich gelöscht!', + + // Users + 'users_title' => 'Benutzer verwalten', + 'users_add' => 'Neuer Benutzer', + 'users_all' => 'Alle Benutzer', + 'users_site_access' => 'Site-Zugriffe', + 'users_last_login' => 'Letzter Login', + 'users_never' => 'Noch nie', + 'users_you' => 'Sie', + 'users_all_sites' => 'Alle Sites', + 'users_none' => 'Keine', + 'users_add_title' => 'Neuen Benutzer anlegen', + 'users_edit_title' => 'Benutzer bearbeiten', + 'users_admin_check' => 'Administrator-Rechte', + 'users_admin_hint' => 'Admins haben Zugriff auf alle Sites und Einstellungen', + 'users_password_hint'=> 'Mindestens 8 Zeichen', + 'users_site_hint' => 'Wählen Sie die Sites, auf die dieser Benutzer Zugriff haben soll', + 'users_no_sites' => 'Keine Sites verfügbar. Bitte zuerst Sites anlegen.', + 'users_added' => 'Benutzer erfolgreich erstellt!', + 'users_updated' => 'Benutzer erfolgreich aktualisiert!', + 'users_deleted' => 'Benutzer erfolgreich gelöscht!', + 'users_notified' => ' Benachrichtigung wurde versendet.', + 'users_none_found' => 'Noch keine Benutzer vorhanden', + 'users_save' => 'Benutzer anlegen', + 'users_save_edit' => 'Änderungen speichern', + 'users_reset_pw' => 'Passwort-Reset-Link senden', + + // Sites + 'sites_title' => 'Sites verwalten', + 'sites_add' => 'Neue Site hinzufügen', + 'sites_add_title' => 'Neue Site hinzufügen', + 'sites_edit_title' => 'Site bearbeiten', + 'sites_name' => 'Site-Name *', + 'sites_site_id' => 'UniFi Site ID *', + 'sites_controller' => 'Controller URL *', + 'sites_username' => 'Benutzername *', + 'sites_password' => 'Passwort *', + 'sites_password_edit'=> 'Neues Passwort', + 'sites_password_hint'=> 'Nur ausfüllen wenn Sie das Passwort ändern möchten', + 'sites_public' => 'Öffentlicher Zugriff (ohne Login nutzbar)', + 'sites_none' => 'Noch keine Sites konfiguriert. Fügen Sie Ihre erste Site hinzu!', + 'sites_added' => 'Site erfolgreich hinzugefügt!', + 'sites_updated' => 'Site erfolgreich aktualisiert!', + 'sites_deleted' => 'Site erfolgreich gelöscht!', + 'sites_testing' => 'Verbindung wird getestet...', + 'sites_deactivate' => 'Deaktivieren', + 'sites_activate' => 'Aktivieren', + + // Voucher admin list + 'vouchers_title' => 'Live Voucher-Verwaltung', + 'vouchers_subtitle' => 'Vouchers werden direkt vom UniFi Controller abgerufen', + 'vouchers_select_site'=> 'Site auswählen', + 'vouchers_select_hint'=> '-- Site auswählen --', + 'vouchers_last_sync'=> 'Letzte Sync', + 'vouchers_no_sites' => 'Keine aktiven Sites vorhanden', + 'vouchers_search' => 'Suchen (Code, Name...)', + 'vouchers_filter_all'=> 'Alle', + 'vouchers_filter_valid'=> 'Gültig', + 'vouchers_filter_used'=> 'Verwendet', + 'vouchers_filter_expired'=> 'Abgelaufen', + 'vouchers_none' => 'Keine Vouchers gefunden.', + 'vouchers_per_page' => 'pro Seite', + 'vouchers_page_of' => 'Seite {current} von {total}', + + // Audit Log + 'audit_title' => 'Audit-Log', + 'audit_subtitle' => 'Alle System-Aktionen im Überblick', + 'audit_action' => 'Aktion', + 'audit_user' => 'Benutzer', + 'audit_details' => 'Details', + 'audit_ip' => 'IP-Adresse', + 'audit_time' => 'Zeitpunkt', + 'audit_entity' => 'Objekt', + 'audit_filter_all' => 'Alle Aktionen', + 'audit_none' => 'Keine Einträge gefunden.', + 'audit_filter' => 'Filter', + + // Settings + 'settings_title' => 'Einstellungen', + 'settings_subtitle' => 'System-Konfiguration und Personalisierung', + 'settings_tab_general' => 'Allgemein', + 'settings_tab_defaults' => 'Voucher-Standards', + 'settings_tab_cron' => 'Cron-Sync', + 'settings_tab_m365' => 'Microsoft 365', + 'settings_tab_smtp' => 'SMTP', + 'settings_tab_templates_email' => 'Templates', + 'settings_tab_system' => 'System', + 'settings_tab_password' => 'Passwort', + 'settings_saved' => 'Einstellungen erfolgreich gespeichert!', + 'settings_app_title' => 'Anwendungs-Titel *', + 'settings_logo_url' => 'Logo-URL', + 'settings_favicon_url' => 'Favicon-URL', + 'settings_favicon_hint' => 'Icon im Browser-Tab (.ico, .png, .svg)', + 'settings_instr_header' => 'Anleitung - Überschrift', + 'settings_instr_text' => 'Anleitung - Text', + 'settings_public_access'=> 'Öffentlicher Zugriff', + 'settings_default_expire' => 'Standard-Gültigkeit (Minuten)', + 'settings_default_expire_hint'=> 'Standard-Ablaufzeit für neue Vouchers (480 = 8 Stunden)', + 'settings_default_devices' => 'Standard-Geräteanzahl', + 'settings_default_devices_hint'=> 'Vorausgefüllter Wert im Voucher-Formular', + 'settings_max_devices' => 'Maximale Geräteanzahl', + 'settings_max_devices_hint' => 'Maximaler Wert, den ein Benutzer auswählen kann', + 'settings_pw_current' => 'Aktuelles Passwort', + 'settings_pw_new' => 'Neues Passwort', + 'settings_pw_confirm' => 'Passwort bestätigen', + 'settings_pw_changed' => 'Passwort erfolgreich geändert!', + 'settings_pw_minlength' => 'Mindestens 8 Zeichen', + + // Login / Auth + 'login_title' => 'Anmelden', + 'login_subtitle' => 'Melden Sie sich an, um fortzufahren', + 'login_email' => 'E-Mail', + 'login_password' => 'Passwort', + 'login_btn' => 'Anmelden', + 'login_ms' => 'Mit Microsoft anmelden', + 'login_local' => 'Mit Benutzername und Passwort anmelden', + 'login_back' => '← Zurück zur Code-Erstellung', + 'login_forgot' => 'Passwort vergessen?', + 'login_error_empty' => 'Bitte E-Mail und Passwort eingeben', + 'login_error_rate' => 'Zu viele Fehlversuche. Bitte warten Sie 10 Minuten.', + 'login_error_creds' => 'Ungültige E-Mail oder Passwort', + + // Password Reset + 'reset_title' => 'Passwort vergessen', + 'reset_subtitle' => 'Geben Sie Ihre E-Mail ein – wir senden Ihnen einen Reset-Link.', + 'reset_email_label' => 'E-Mail-Adresse', + 'reset_send_btn' => 'Reset-Link senden', + 'reset_success' => 'Falls ein Konto mit dieser E-Mail existiert, erhalten Sie in Kürze eine E-Mail.', + 'reset_back_login' => '← Zurück zum Login', + 'reset_new_pw' => 'Neues Passwort festlegen', + 'reset_new_pw_label'=> 'Neues Passwort', + 'reset_confirm_label'=> 'Passwort bestätigen', + 'reset_set_btn' => 'Passwort festlegen', + 'reset_invalid' => 'Ungültiger oder abgelaufener Reset-Link.', + 'reset_done' => 'Ihr Passwort wurde erfolgreich geändert.', + + // Errors + 'error_csrf' => 'Ungültiges Sicherheits-Token', + 'error_login_req' => 'Sie müssen angemeldet sein', + 'error_no_permission'=> 'Keine Berechtigung für diese Aktion', + 'error_not_found' => 'Nicht gefunden', + 'error_name_req' => 'Bitte geben Sie einen Namen ein', + 'error_site_req' => 'Bitte wählen Sie einen Standort', + 'error_devices_range'=> 'Anzahl der Geräte muss zwischen 1 und {max} liegen', + 'error_email_invalid'=> 'Ungültige E-Mail-Adresse', + 'error_site_no_perm'=> 'Keine Berechtigung für diese Site', + 'error_site_not_found'=> 'Site nicht gefunden', + 'error_voucher_invalid'=> 'UniFi hat keinen gültigen Voucher zurückgegeben', + 'error_connection' => 'Verbindung zum UniFi Controller fehlgeschlagen', + 'error_fill_all' => 'Bitte füllen Sie alle Pflichtfelder aus', + + // Hello / User + 'hello' => 'Hallo, {name}', + 'minutes_short' => 'Min.', + 'hours_short' => 'Std.', + 'never' => 'Noch nie', + 'unknown' => 'Unbekannt', + 'or' => 'oder', +]; diff --git a/lang/en.php b/lang/en.php new file mode 100644 index 0000000..42b4b56 --- /dev/null +++ b/lang/en.php @@ -0,0 +1,289 @@ + 'Dashboard', + 'nav_sites' => 'Manage Sites', + 'nav_users' => 'Manage Users', + 'nav_vouchers' => 'Live Vouchers', + 'nav_templates' => 'Voucher Profiles', + 'nav_audit_log' => 'Audit Log', + 'nav_settings' => 'Settings', + 'nav_update' => 'System Update', + 'nav_back' => 'Back to Home', + 'nav_administration'=> 'Administration', + + // Common buttons + 'btn_save' => 'Save', + 'btn_cancel' => 'Cancel', + 'btn_add' => 'Add', + 'btn_edit' => 'Edit', + 'btn_delete' => 'Delete', + 'btn_create' => 'Create', + 'btn_close' => 'Close', + 'btn_back' => 'Back', + 'btn_login' => 'Sign In', + 'btn_logout' => 'Sign Out', + 'btn_refresh' => 'Refresh', + 'btn_export_csv' => 'Export CSV', + 'btn_print' => 'Print', + 'btn_send' => 'Send', + 'btn_test' => 'Test', + 'btn_generate' => 'Generate', + 'btn_copy' => 'Copy', + 'btn_new_code' => 'Create Another Code', + + // Common labels + 'label_name' => 'Name', + 'label_email' => 'Email', + 'label_password' => 'Password', + 'label_status' => 'Status', + 'label_actions' => 'Actions', + 'label_created' => 'Created', + 'label_site' => 'Location', + 'label_sites' => 'Sites', + 'label_role' => 'Role', + 'label_code' => 'Code', + 'label_note' => 'Note', + 'label_usage' => 'Usage', + 'label_expires' => 'Expires', + 'label_devices' => 'Devices', + 'label_duration' => 'Duration', + 'label_description' => 'Description', + 'label_language' => 'Language', + + // Status texts + 'status_active' => 'Active', + 'status_inactive' => 'Inactive', + 'status_valid' => 'Valid', + 'status_used' => 'Used', + 'status_expired' => 'Expired', + 'status_admin' => 'Admin', + 'status_user' => 'User', + 'status_public' => 'Public', + 'status_all' => 'All', + + // Dashboard + 'dashboard_title' => 'Dashboard', + 'dashboard_subtitle'=> 'Overview of your UniFi Voucher System', + 'dashboard_active_sites' => 'Active Sites', + 'dashboard_users' => 'Users', + 'dashboard_valid' => 'Valid Vouchers', + 'dashboard_used' => 'Used', + 'dashboard_expired' => 'Expired', + 'dashboard_total' => 'Total', + 'dashboard_live_refresh' => 'Live Refresh', + 'dashboard_trend' => 'Voucher Trend (Last 7 Days)', + 'dashboard_top_users'=> 'Top 5 Users (Last 30 Days)', + 'dashboard_recent' => 'Recent Vouchers (from Database)', + 'dashboard_creator' => 'Creator', + 'dashboard_public' => 'Public', + 'dashboard_vouchers_per_site' => 'Live: Vouchers per Site', + 'dashboard_no_data' => 'No data available yet', + 'dashboard_no_vouchers' => 'No vouchers created yet', + + // Voucher creation + 'voucher_name_label' => 'Voucher Name *', + 'voucher_name_hint' => 'e.g. Meeting Room 3, Guest John Doe', + 'voucher_devices_label' => 'How many devices may connect? *', + 'voucher_site_label' => 'Location *', + 'voucher_site_select' => 'Please select...', + 'voucher_email_send' => 'Send code via email', + 'voucher_email_label' => 'Recipient email address', + 'voucher_email_hint' => 'guest@example.com', + 'voucher_create_btn' => 'Create Voucher', + 'voucher_creating' => 'Creating Voucher...', + 'voucher_success_title' => '✓ Your Access Code', + 'voucher_validity' => 'Valid for {minutes} minutes from creation', + 'voucher_qr_label' => 'Scan QR code to connect', + 'voucher_print_btn' => 'Print Code', + 'voucher_no_sites' => 'No available sites found.', + 'voucher_no_sites_admin'=> 'Click here to create sites', + 'voucher_no_sites_user' => 'Please contact your administrator.', + 'voucher_template_select'=> '-- No Profile (manual) --', + 'voucher_template_label'=> 'Quick Profile (optional)', + + // Bulk creation + 'bulk_tab' => 'Single', + 'bulk_tab_bulk' => 'Create Multiple', + 'bulk_quantity' => 'Number of Vouchers', + 'bulk_quantity_hint'=> 'How many vouchers should be created?', + 'bulk_name_prefix' => 'Name / Prefix', + 'bulk_create_btn' => 'Create {count} Vouchers', + 'bulk_creating' => 'Creating {count} vouchers...', + 'bulk_success' => '{count} vouchers created successfully!', + 'bulk_print_all' => 'Print All', + 'bulk_results' => 'Created Vouchers ({count})', + + // Templates + 'templates_title' => 'Voucher Profiles', + 'templates_subtitle'=> 'Predefined voucher configurations', + 'templates_add' => 'New Profile', + 'templates_name' => 'Profile Name', + 'templates_devices' => 'Max. Devices', + 'templates_duration'=> 'Validity (Minutes)', + 'templates_desc' => 'Description (optional)', + 'templates_none' => 'No profiles found.', + 'templates_add_hint'=> 'Create profiles for frequently used voucher configurations.', + 'templates_edit' => 'Edit Profile', + 'templates_added' => 'Profile created successfully!', + 'templates_updated' => 'Profile updated successfully!', + 'templates_deleted' => 'Profile deleted successfully!', + + // Users + 'users_title' => 'Manage Users', + 'users_add' => 'New User', + 'users_all' => 'All Users', + 'users_site_access' => 'Site Access', + 'users_last_login' => 'Last Login', + 'users_never' => 'Never', + 'users_you' => 'You', + 'users_all_sites' => 'All Sites', + 'users_none' => 'None', + 'users_add_title' => 'Create New User', + 'users_edit_title' => 'Edit User', + 'users_admin_check' => 'Administrator Rights', + 'users_admin_hint' => 'Admins have access to all sites and settings', + 'users_password_hint'=> 'At least 8 characters', + 'users_site_hint' => 'Select the sites this user should have access to', + 'users_no_sites' => 'No sites available. Please create sites first.', + 'users_added' => 'User created successfully!', + 'users_updated' => 'User updated successfully!', + 'users_deleted' => 'User deleted successfully!', + 'users_notified' => ' Notification sent.', + 'users_none_found' => 'No users found', + 'users_save' => 'Create User', + 'users_save_edit' => 'Save Changes', + 'users_reset_pw' => 'Send Password Reset Link', + + // Sites + 'sites_title' => 'Manage Sites', + 'sites_add' => 'Add New Site', + 'sites_add_title' => 'Add New Site', + 'sites_edit_title' => 'Edit Site', + 'sites_name' => 'Site Name *', + 'sites_site_id' => 'UniFi Site ID *', + 'sites_controller' => 'Controller URL *', + 'sites_username' => 'Username *', + 'sites_password' => 'Password *', + 'sites_password_edit'=> 'New Password', + 'sites_password_hint'=> 'Leave blank to keep current password', + 'sites_public' => 'Public Access (usable without login)', + 'sites_none' => 'No sites configured yet. Add your first site!', + 'sites_added' => 'Site added successfully!', + 'sites_updated' => 'Site updated successfully!', + 'sites_deleted' => 'Site deleted successfully!', + 'sites_testing' => 'Testing connection...', + 'sites_deactivate' => 'Deactivate', + 'sites_activate' => 'Activate', + + // Voucher admin list + 'vouchers_title' => 'Live Voucher Management', + 'vouchers_subtitle' => 'Vouchers are fetched directly from the UniFi Controller', + 'vouchers_select_site'=> 'Select Site', + 'vouchers_select_hint'=> '-- Select Site --', + 'vouchers_last_sync'=> 'Last Sync', + 'vouchers_no_sites' => 'No active sites available', + 'vouchers_search' => 'Search (code, name...)', + 'vouchers_filter_all'=> 'All', + 'vouchers_filter_valid'=> 'Valid', + 'vouchers_filter_used'=> 'Used', + 'vouchers_filter_expired'=> 'Expired', + 'vouchers_none' => 'No vouchers found.', + 'vouchers_per_page' => 'per page', + 'vouchers_page_of' => 'Page {current} of {total}', + + // Audit Log + 'audit_title' => 'Audit Log', + 'audit_subtitle' => 'All system actions at a glance', + 'audit_action' => 'Action', + 'audit_user' => 'User', + 'audit_details' => 'Details', + 'audit_ip' => 'IP Address', + 'audit_time' => 'Time', + 'audit_entity' => 'Entity', + 'audit_filter_all' => 'All Actions', + 'audit_none' => 'No entries found.', + 'audit_filter' => 'Filter', + + // Settings + 'settings_title' => 'Settings', + 'settings_subtitle' => 'System configuration and customization', + 'settings_tab_general' => 'General', + 'settings_tab_defaults' => 'Voucher Defaults', + 'settings_tab_cron' => 'Cron Sync', + 'settings_tab_m365' => 'Microsoft 365', + 'settings_tab_smtp' => 'SMTP', + 'settings_tab_templates_email' => 'Templates', + 'settings_tab_system' => 'System', + 'settings_tab_password' => 'Password', + 'settings_saved' => 'Settings saved successfully!', + 'settings_app_title' => 'Application Title *', + 'settings_logo_url' => 'Logo URL', + 'settings_favicon_url' => 'Favicon URL', + 'settings_favicon_hint' => 'Browser tab icon (.ico, .png, .svg)', + 'settings_instr_header' => 'Instructions - Headline', + 'settings_instr_text' => 'Instructions - Text', + 'settings_public_access'=> 'Public Access', + 'settings_default_expire' => 'Default Validity (Minutes)', + 'settings_default_expire_hint'=> 'Default expiry time for new vouchers (480 = 8 hours)', + 'settings_default_devices' => 'Default Device Count', + 'settings_default_devices_hint'=> 'Pre-filled value in the voucher form', + 'settings_max_devices' => 'Maximum Device Count', + 'settings_max_devices_hint' => 'Maximum value a user can select', + 'settings_pw_current' => 'Current Password', + 'settings_pw_new' => 'New Password', + 'settings_pw_confirm' => 'Confirm Password', + 'settings_pw_changed' => 'Password changed successfully!', + 'settings_pw_minlength' => 'At least 8 characters', + + // Login / Auth + 'login_title' => 'Sign In', + 'login_subtitle' => 'Sign in to continue', + 'login_email' => 'Email', + 'login_password' => 'Password', + 'login_btn' => 'Sign In', + 'login_ms' => 'Sign in with Microsoft', + 'login_local' => 'Sign in with username and password', + 'login_back' => '← Back to Code Creation', + 'login_forgot' => 'Forgot password?', + 'login_error_empty' => 'Please enter email and password', + 'login_error_rate' => 'Too many failed attempts. Please wait 10 minutes.', + 'login_error_creds' => 'Invalid email or password', + + // Password Reset + 'reset_title' => 'Forgot Password', + 'reset_subtitle' => 'Enter your email – we will send you a reset link.', + 'reset_email_label' => 'Email Address', + 'reset_send_btn' => 'Send Reset Link', + 'reset_success' => 'If an account with this email exists, you will receive an email shortly.', + 'reset_back_login' => '← Back to Login', + 'reset_new_pw' => 'Set New Password', + 'reset_new_pw_label'=> 'New Password', + 'reset_confirm_label'=> 'Confirm Password', + 'reset_set_btn' => 'Set Password', + 'reset_invalid' => 'Invalid or expired reset link.', + 'reset_done' => 'Your password has been changed successfully.', + + // Errors + 'error_csrf' => 'Invalid security token', + 'error_login_req' => 'You must be logged in', + 'error_no_permission'=> 'No permission for this action', + 'error_not_found' => 'Not found', + 'error_name_req' => 'Please enter a name', + 'error_site_req' => 'Please select a location', + 'error_devices_range'=> 'Device count must be between 1 and {max}', + 'error_email_invalid'=> 'Invalid email address', + 'error_site_no_perm'=> 'No permission for this site', + 'error_site_not_found'=> 'Site not found', + 'error_voucher_invalid'=> 'UniFi did not return a valid voucher', + 'error_connection' => 'Connection to UniFi Controller failed', + 'error_fill_all' => 'Please fill in all required fields', + + // Hello / User + 'hello' => 'Hello, {name}', + 'minutes_short' => 'min.', + 'hours_short' => 'hrs.', + 'never' => 'Never', + 'unknown' => 'Unknown', + 'or' => 'or', +]; diff --git a/login.php b/login.php index ce8f884..59b981b 100644 --- a/login.php +++ b/login.php @@ -1,46 +1,41 @@ isLoggedIn()) { - header('Location: index.php'); - exit; - } + if ($auth->isLoggedIn()) { header('Location: index.php'); exit; } } catch (Exception $e) { die('Fehler beim Initialisieren: ' . $e->getMessage()); } -$error = ''; +I18n::init(); + +$error = ''; $success = ''; -// Login-Verarbeitung if ($_SERVER['REQUEST_METHOD'] === 'POST') { try { - $email = trim($_POST['email'] ?? ''); + $email = trim($_POST['email'] ?? ''); $password = $_POST['password'] ?? ''; if (empty($email) || empty($password)) { - $error = 'Bitte E-Mail und Passwort eingeben'; + $error = __('login_error_empty'); } else { $result = $auth->login($email, $password); if ($result === true) { header('Location: index.php'); exit; } elseif ($result === 'rate_limited') { - $error = 'Zu viele Fehlversuche. Bitte warten Sie 10 Minuten.'; + $error = __('login_error_rate'); } else { - $error = 'Ungültige E-Mail oder Passwort'; + $error = __('login_error_creds'); } } } catch (Exception $e) { @@ -49,46 +44,36 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { } try { - $db = Database::getInstance(); + $db = Database::getInstance(); $appTitle = $db->getSetting('app_title', 'UniFi Voucher System'); - $logoUrl = $db->getSetting('logo_url', ''); + $logoUrl = $db->getSetting('logo_url', ''); - // M365 aktiviert prüfen - ALLE drei Felder müssen ausgefüllt sein - $m365ClientId = $db->getSetting('m365_client_id', ''); + $m365ClientId = $db->getSetting('m365_client_id', ''); $m365ClientSecret = $db->getSetting('m365_client_secret', ''); - $m365TenantId = $db->getSetting('m365_tenant_id', ''); + $m365TenantId = $db->getSetting('m365_tenant_id', ''); + $m365Enabled = !empty($m365ClientId) && !empty($m365ClientSecret) && !empty($m365TenantId); + $publicAccess = $db->getSetting('public_access', 0); + $smtpEnabled = $db->getSetting('smtp_enabled', '0') === '1'; - $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']; + $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, + 'client_id' => $m365ClientId, 'response_type' => 'code', - 'redirect_uri' => $redirectUri, + 'redirect_uri' => $redirectUri, 'response_mode' => 'query', - 'scope' => 'openid profile email User.Read', - 'state' => bin2hex(random_bytes(16)) + '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) { @@ -96,239 +81,117 @@ try { } ?> - + - Login - <?= htmlspecialchars($appTitle) ?> + <?= __('login_title') ?> – <?= htmlspecialchars($appTitle) ?> + + -
- - - -

- +
+
+ $label): ?> + + +
+ +
-

Melden Sie sich an, um fortzufahren

+ + + + +
+ diff --git a/reset_password.php b/reset_password.php new file mode 100644 index 0000000..346667e --- /dev/null +++ b/reset_password.php @@ -0,0 +1,152 @@ +isLoggedIn()) { header('Location: index.php'); exit; } +I18n::init(); + +$db = Database::getInstance(); +$appTitle = $db->getSetting('app_title', 'UniFi Voucher System'); +$logoUrl = $db->getSetting('logo_url', ''); + +$token = trim($_GET['token'] ?? ''); +$error = ''; +$success = ''; +$valid = false; +$tokenRow = null; + +if (empty($token)) { + $error = __('reset_invalid'); +} else { + $tokenRow = $db->fetchOne( + "SELECT prt.*, u.email, u.name FROM password_reset_tokens prt + JOIN users u ON prt.user_id = u.id + WHERE prt.token = ? AND prt.used = 0 AND prt.expires_at > NOW()", + [$token] + ); + if (!$tokenRow) { + $error = __('reset_invalid'); + } else { + $valid = true; + } +} + +if ($valid && $_SERVER['REQUEST_METHOD'] === 'POST') { + $newPw = $_POST['new_password'] ?? ''; + $confirm= $_POST['confirm_password'] ?? ''; + + if (strlen($newPw) < 8) { + $error = __('settings_pw_minlength'); + $valid = true; // keep form visible + } elseif ($newPw !== $confirm) { + $error = 'Passwörter stimmen nicht überein'; + $valid = true; + } else { + $hash = password_hash($newPw, PASSWORD_DEFAULT); + $db->execute("UPDATE users SET password_hash = ? WHERE id = ?", [$hash, $tokenRow['user_id']]); + $db->execute("UPDATE password_reset_tokens SET used = 1 WHERE token = ?", [$token]); + + $db->execute( + "INSERT INTO audit_log (user_id, action, entity_type, entity_id, details, ip_address) VALUES (?, 'password_reset', 'user', ?, 'Passwort erfolgreich geändert', ?)", + [$tokenRow['user_id'], $tokenRow['user_id'], $_SERVER['REMOTE_ADDR'] ?? ''] + ); + + $success = __('reset_done'); + $valid = false; + } +} +?> + + + + + + <?= __('reset_new_pw') ?> – <?= htmlspecialchars($appTitle) ?> + + + + + +
+ + + +
+ + +

+ + +
+ + +
+ + + +

Für

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