Initial Upload
This commit is contained in:
commit
dbdc237fa1
22 changed files with 7995 additions and 0 deletions
201
includes/Auth.php
Normal file
201
includes/Auth.php
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
<?php
|
||||
class Auth {
|
||||
private $db;
|
||||
|
||||
public function __construct() {
|
||||
try {
|
||||
$this->db = Database::getInstance();
|
||||
} catch (Exception $e) {
|
||||
die("Datenbankverbindung fehlgeschlagen: " . $e->getMessage());
|
||||
}
|
||||
|
||||
// Session-Konfiguration
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
ini_set('session.cookie_httponly', 1);
|
||||
ini_set('session.use_strict_mode', 1);
|
||||
ini_set('session.cookie_samesite', 'Lax');
|
||||
|
||||
if (!session_start()) {
|
||||
die("Session konnte nicht gestartet werden");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Benutzer einloggen
|
||||
public function login($email, $password) {
|
||||
$user = $this->db->fetchOne(
|
||||
"SELECT * FROM users WHERE email = ? AND is_active = 1",
|
||||
[$email]
|
||||
);
|
||||
|
||||
if ($user && password_verify($password, $user['password_hash'])) {
|
||||
$this->setUserSession($user);
|
||||
$this->updateLastLogin($user['id']);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Microsoft 365 Login
|
||||
public function loginWithMicrosoft($microsoftUser) {
|
||||
// Zuerst nach Microsoft ID suchen
|
||||
$user = $this->db->fetchOne(
|
||||
"SELECT * FROM users WHERE microsoft_id = ? AND is_active = 1",
|
||||
[$microsoftUser['id']]
|
||||
);
|
||||
|
||||
if (!$user) {
|
||||
// Prüfen ob E-Mail bereits existiert (ohne Microsoft ID)
|
||||
$existingUser = $this->db->fetchOne(
|
||||
"SELECT * FROM users WHERE email = ? AND is_active = 1",
|
||||
[$microsoftUser['email']]
|
||||
);
|
||||
|
||||
if ($existingUser) {
|
||||
// Benutzer existiert bereits ohne Microsoft ID - verknüpfen
|
||||
$this->db->query(
|
||||
"UPDATE users SET microsoft_id = ?, name = ? WHERE id = ?",
|
||||
[$microsoftUser['id'], $microsoftUser['name'], $existingUser['id']]
|
||||
);
|
||||
$user = $this->db->fetchOne("SELECT * FROM users WHERE id = ?", [$existingUser['id']]);
|
||||
} else {
|
||||
// Komplett neuer Benutzer - anlegen
|
||||
$userId = $this->db->execute(
|
||||
"INSERT INTO users (email, name, microsoft_id, is_active) VALUES (?, ?, ?, 1)",
|
||||
[$microsoftUser['email'], $microsoftUser['name'], $microsoftUser['id']]
|
||||
);
|
||||
|
||||
$user = $this->db->fetchOne("SELECT * FROM users WHERE id = ?", [$userId]);
|
||||
}
|
||||
} else {
|
||||
// Microsoft-Benutzer existiert bereits - Name aktualisieren falls geändert
|
||||
$this->db->query(
|
||||
"UPDATE users SET name = ? WHERE id = ?",
|
||||
[$microsoftUser['name'], $user['id']]
|
||||
);
|
||||
}
|
||||
|
||||
$this->setUserSession($user);
|
||||
$this->updateLastLogin($user['id']);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Session setzen
|
||||
private function setUserSession($user) {
|
||||
$_SESSION['user_id'] = $user['id'];
|
||||
$_SESSION['user_email'] = $user['email'];
|
||||
$_SESSION['user_name'] = $user['name'];
|
||||
$_SESSION['is_admin'] = (bool)$user['is_admin'];
|
||||
$_SESSION['login_time'] = time();
|
||||
|
||||
// CSRF-Token generieren
|
||||
if (!isset($_SESSION['csrf_token'])) {
|
||||
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
|
||||
}
|
||||
}
|
||||
|
||||
// Letzten Login aktualisieren
|
||||
private function updateLastLogin($userId) {
|
||||
$this->db->query(
|
||||
"UPDATE users SET last_login = NOW() WHERE id = ?",
|
||||
[$userId]
|
||||
);
|
||||
}
|
||||
|
||||
// Ausloggen
|
||||
public function logout() {
|
||||
$_SESSION = [];
|
||||
|
||||
if (isset($_COOKIE[session_name()])) {
|
||||
setcookie(session_name(), '', time() - 3600, '/');
|
||||
}
|
||||
|
||||
session_destroy();
|
||||
}
|
||||
|
||||
// Prüfen ob eingeloggt
|
||||
public function isLoggedIn() {
|
||||
return isset($_SESSION['user_id']) && isset($_SESSION['login_time']);
|
||||
}
|
||||
|
||||
// Prüfen ob Admin
|
||||
public function isAdmin() {
|
||||
return $this->isLoggedIn() && isset($_SESSION['is_admin']) && $_SESSION['is_admin'] === true;
|
||||
}
|
||||
|
||||
// Aktuellen Benutzer abrufen
|
||||
public function getCurrentUser() {
|
||||
if (!$this->isLoggedIn()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->db->fetchOne(
|
||||
"SELECT * FROM users WHERE id = ?",
|
||||
[$_SESSION['user_id']]
|
||||
);
|
||||
}
|
||||
|
||||
// Prüfen ob Benutzer Zugriff auf Site hat
|
||||
public function hasAccessToSite($siteId) {
|
||||
if ($this->isAdmin()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!$this->isLoggedIn()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$access = $this->db->fetchOne(
|
||||
"SELECT id FROM user_site_access WHERE user_id = ? AND site_id = ?",
|
||||
[$_SESSION['user_id'], $siteId]
|
||||
);
|
||||
|
||||
return $access !== false;
|
||||
}
|
||||
|
||||
// CSRF-Token validieren
|
||||
public function validateCsrfToken($token) {
|
||||
return isset($_SESSION['csrf_token']) && hash_equals($_SESSION['csrf_token'], $token);
|
||||
}
|
||||
|
||||
// CSRF-Token abrufen
|
||||
public function getCsrfToken() {
|
||||
if (!isset($_SESSION['csrf_token'])) {
|
||||
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
|
||||
}
|
||||
return $_SESSION['csrf_token'];
|
||||
}
|
||||
|
||||
// Benutzer registrieren (nur für Admins)
|
||||
public function registerUser($email, $name, $password, $isAdmin = false) {
|
||||
// Prüfen ob E-Mail bereits existiert
|
||||
$existing = $this->db->fetchOne("SELECT id FROM users WHERE email = ?", [$email]);
|
||||
if ($existing) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$passwordHash = password_hash($password, PASSWORD_DEFAULT);
|
||||
|
||||
return $this->db->execute(
|
||||
"INSERT INTO users (email, name, password_hash, is_admin, is_active) VALUES (?, ?, ?, ?, 1)",
|
||||
[$email, $name, $passwordHash, $isAdmin ? 1 : 0]
|
||||
);
|
||||
}
|
||||
|
||||
// Admin-Zugriff erforderlich
|
||||
public function requireAdmin() {
|
||||
if (!$this->isAdmin()) {
|
||||
header('Location: /index.php?error=access_denied');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
// Login erforderlich
|
||||
public function requireLogin() {
|
||||
if (!$this->isLoggedIn()) {
|
||||
header('Location: /login.php');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
}
|
||||
72
includes/Database.php
Normal file
72
includes/Database.php
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
<?php
|
||||
class Database {
|
||||
private static $instance = null;
|
||||
private $pdo;
|
||||
|
||||
private function __construct() {
|
||||
try {
|
||||
$this->pdo = new PDO(
|
||||
"mysql:host=" . DB_HOST . ";dbname=" . DB_NAME . ";charset=utf8mb4",
|
||||
DB_USER,
|
||||
DB_PASS,
|
||||
[
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
PDO::ATTR_EMULATE_PREPARES => false
|
||||
]
|
||||
);
|
||||
} catch (PDOException $e) {
|
||||
die("Datenbankverbindung fehlgeschlagen: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public static function getInstance() {
|
||||
if (self::$instance === null) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public function getConnection() {
|
||||
return $this->pdo;
|
||||
}
|
||||
|
||||
// Helper-Methode für Queries
|
||||
public function query($sql, $params = []) {
|
||||
$stmt = $this->pdo->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
return $stmt;
|
||||
}
|
||||
|
||||
// Helper für einzelnen Datensatz
|
||||
public function fetchOne($sql, $params = []) {
|
||||
$stmt = $this->query($sql, $params);
|
||||
return $stmt->fetch();
|
||||
}
|
||||
|
||||
// Helper für mehrere Datensätze
|
||||
public function fetchAll($sql, $params = []) {
|
||||
$stmt = $this->query($sql, $params);
|
||||
return $stmt->fetchAll();
|
||||
}
|
||||
|
||||
// Helper für Insert/Update mit Rückgabe der ID
|
||||
public function execute($sql, $params = []) {
|
||||
$this->query($sql, $params);
|
||||
return $this->pdo->lastInsertId();
|
||||
}
|
||||
|
||||
// Settings-Helper
|
||||
public function getSetting($key, $default = null) {
|
||||
$result = $this->fetchOne("SELECT setting_value FROM settings WHERE setting_key = ?", [$key]);
|
||||
return $result ? $result['setting_value'] : $default;
|
||||
}
|
||||
|
||||
public function setSetting($key, $value) {
|
||||
$this->query(
|
||||
"INSERT INTO settings (setting_key, setting_value) VALUES (?, ?)
|
||||
ON DUPLICATE KEY UPDATE setting_value = VALUES(setting_value)",
|
||||
[$key, $value]
|
||||
);
|
||||
}
|
||||
}
|
||||
243
includes/Mailer.php
Normal file
243
includes/Mailer.php
Normal file
|
|
@ -0,0 +1,243 @@
|
|||
<?php
|
||||
class Mailer {
|
||||
private $db;
|
||||
private $smtpEnabled;
|
||||
private $smtpHost;
|
||||
private $smtpPort;
|
||||
private $smtpUsername;
|
||||
private $smtpPassword;
|
||||
private $smtpEncryption;
|
||||
private $fromEmail;
|
||||
private $fromName;
|
||||
|
||||
public function __construct() {
|
||||
$this->db = Database::getInstance();
|
||||
$this->loadSettings();
|
||||
}
|
||||
|
||||
private function loadSettings() {
|
||||
$this->smtpEnabled = $this->db->getSetting('smtp_enabled', '0') === '1';
|
||||
$this->smtpHost = $this->db->getSetting('smtp_host', '');
|
||||
$this->smtpPort = (int)$this->db->getSetting('smtp_port', '587');
|
||||
$this->smtpUsername = $this->db->getSetting('smtp_username', '');
|
||||
$this->smtpPassword = $this->db->getSetting('smtp_password', '');
|
||||
$this->smtpEncryption = $this->db->getSetting('smtp_encryption', 'tls');
|
||||
$this->fromEmail = $this->db->getSetting('smtp_from_email', 'noreply@' . $_SERVER['HTTP_HOST']);
|
||||
$this->fromName = $this->db->getSetting('smtp_from_name', $this->db->getSetting('app_title', 'UniFi Voucher System'));
|
||||
}
|
||||
|
||||
public function send($to, $subject, $body, $isHtml = false) {
|
||||
if (!$this->smtpEnabled || empty($this->smtpHost)) {
|
||||
// Fallback auf PHP mail()
|
||||
return $this->sendWithPhpMail($to, $subject, $body);
|
||||
}
|
||||
|
||||
return $this->sendWithSmtp($to, $subject, $body, $isHtml);
|
||||
}
|
||||
|
||||
private function sendWithPhpMail($to, $subject, $body) {
|
||||
$headers = "From: {$this->fromName} <{$this->fromEmail}>\r\n";
|
||||
$headers .= "Reply-To: {$this->fromEmail}\r\n";
|
||||
$headers .= "Content-Type: text/plain; charset=UTF-8\r\n";
|
||||
|
||||
return mail($to, $subject, $body, $headers);
|
||||
}
|
||||
|
||||
private function sendWithSmtp($to, $subject, $body, $isHtml = false) {
|
||||
try {
|
||||
// Verbindung aufbauen
|
||||
$socket = $this->connectToSmtp();
|
||||
|
||||
// EHLO
|
||||
$this->smtpCommand($socket, "EHLO " . $_SERVER['HTTP_HOST']);
|
||||
|
||||
// STARTTLS wenn nötig
|
||||
if ($this->smtpEncryption === 'tls') {
|
||||
$this->smtpCommand($socket, "STARTTLS");
|
||||
stream_socket_enable_crypto($socket, true, STREAM_CRYPTO_METHOD_TLS_CLIENT);
|
||||
$this->smtpCommand($socket, "EHLO " . $_SERVER['HTTP_HOST']);
|
||||
}
|
||||
|
||||
// AUTH LOGIN
|
||||
$this->smtpCommand($socket, "AUTH LOGIN");
|
||||
$this->smtpCommand($socket, base64_encode($this->smtpUsername));
|
||||
$this->smtpCommand($socket, base64_encode($this->smtpPassword));
|
||||
|
||||
// MAIL FROM
|
||||
$this->smtpCommand($socket, "MAIL FROM:<{$this->fromEmail}>");
|
||||
|
||||
// RCPT TO
|
||||
$this->smtpCommand($socket, "RCPT TO:<{$to}>");
|
||||
|
||||
// DATA
|
||||
$this->smtpCommand($socket, "DATA");
|
||||
|
||||
// Headers
|
||||
$message = "From: {$this->fromName} <{$this->fromEmail}>\r\n";
|
||||
$message .= "To: {$to}\r\n";
|
||||
$message .= "Subject: =?UTF-8?B?" . base64_encode($subject) . "?=\r\n";
|
||||
$message .= "MIME-Version: 1.0\r\n";
|
||||
|
||||
if ($isHtml) {
|
||||
$message .= "Content-Type: text/html; charset=UTF-8\r\n";
|
||||
} else {
|
||||
$message .= "Content-Type: text/plain; charset=UTF-8\r\n";
|
||||
}
|
||||
|
||||
$message .= "\r\n";
|
||||
|
||||
// Body - bei Plain Text Zeilenumbrüche konvertieren
|
||||
if (!$isHtml) {
|
||||
$body = nl2br($body, false); // Für Plain Text
|
||||
$body = str_replace('<br>', "\r\n", $body);
|
||||
}
|
||||
|
||||
$message .= $body;
|
||||
$message .= "\r\n.\r\n";
|
||||
|
||||
fwrite($socket, $message);
|
||||
$response = fgets($socket);
|
||||
|
||||
// QUIT
|
||||
$this->smtpCommand($socket, "QUIT");
|
||||
fclose($socket);
|
||||
|
||||
return strpos($response, '250') === 0;
|
||||
|
||||
} catch (Exception $e) {
|
||||
error_log("SMTP Error: " . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private function connectToSmtp() {
|
||||
$context = stream_context_create([
|
||||
'ssl' => [
|
||||
'verify_peer' => false,
|
||||
'verify_peer_name' => false,
|
||||
'allow_self_signed' => true
|
||||
]
|
||||
]);
|
||||
|
||||
if ($this->smtpEncryption === 'ssl') {
|
||||
$host = 'ssl://' . $this->smtpHost;
|
||||
} else {
|
||||
$host = $this->smtpHost;
|
||||
}
|
||||
|
||||
$socket = stream_socket_client(
|
||||
$host . ':' . $this->smtpPort,
|
||||
$errno,
|
||||
$errstr,
|
||||
30,
|
||||
STREAM_CLIENT_CONNECT,
|
||||
$context
|
||||
);
|
||||
|
||||
if (!$socket) {
|
||||
throw new Exception("SMTP Connection failed: $errstr ($errno)");
|
||||
}
|
||||
|
||||
// Willkommensnachricht lesen
|
||||
fgets($socket);
|
||||
|
||||
return $socket;
|
||||
}
|
||||
|
||||
private function smtpCommand($socket, $command) {
|
||||
fwrite($socket, $command . "\r\n");
|
||||
$response = fgets($socket);
|
||||
|
||||
// Prüfen auf Fehler (4xx oder 5xx)
|
||||
if (preg_match('/^[45]/', $response)) {
|
||||
throw new Exception("SMTP Error: $response");
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
// Vordefinierte E-Mail-Templates
|
||||
public function sendVoucherEmail($to, $voucherCode, $siteName, $maxUses) {
|
||||
$appTitle = $this->db->getSetting('app_title', 'UniFi Voucher System');
|
||||
$instructionHeader = $this->db->getSetting('instruction_header', '');
|
||||
$instructionText = $this->db->getSetting('instruction_text', '');
|
||||
|
||||
// System-URL aus Einstellungen oder automatisch erkennen
|
||||
$systemUrl = $this->db->getSetting('system_url', '');
|
||||
if (empty($systemUrl)) {
|
||||
$protocol = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http';
|
||||
$host = $_SERVER['HTTP_HOST'];
|
||||
$scriptPath = dirname($_SERVER['SCRIPT_NAME']);
|
||||
$scriptPath = $scriptPath === '/' ? '' : $scriptPath;
|
||||
$systemUrl = $protocol . '://' . $host . $scriptPath;
|
||||
}
|
||||
|
||||
// Template aus Datenbank laden
|
||||
$subjectTemplate = $this->db->getSetting('email_voucher_subject', '{APP_TITLE} - Ihr WLAN-Zugang');
|
||||
$bodyTemplate = $this->db->getSetting('email_voucher_body', "Hallo,\n\nIhr WLAN-Zugangscode lautet:\n\n<strong>{VOUCHER_CODE}</strong>\n\nGültigkeit: 8 Stunden ab Erstellung\nMaximale Geräte: {MAX_USES}\nStandort: {SITE_NAME}\n\n{INSTRUCTIONS}\n\nMit freundlichen Grüßen\n{APP_TITLE}");
|
||||
|
||||
// Anleitung formatieren
|
||||
$instructions = '';
|
||||
if ($instructionText) {
|
||||
$instructions = $instructionHeader . "\n" . $instructionText;
|
||||
}
|
||||
|
||||
// Platzhalter ersetzen
|
||||
$placeholders = [
|
||||
'{VOUCHER_CODE}' => $voucherCode,
|
||||
'{SITE_NAME}' => $siteName,
|
||||
'{MAX_USES}' => $maxUses,
|
||||
'{APP_TITLE}' => $appTitle,
|
||||
'{INSTRUCTIONS}' => $instructions,
|
||||
'{SYSTEM_URL}' => $systemUrl
|
||||
];
|
||||
|
||||
$subject = str_replace(array_keys($placeholders), array_values($placeholders), $subjectTemplate);
|
||||
$body = str_replace(array_keys($placeholders), array_values($placeholders), $bodyTemplate);
|
||||
|
||||
// HTML oder Plain Text prüfen
|
||||
$isHtml = strip_tags($body) !== $body;
|
||||
|
||||
return $this->send($to, $subject, $body, $isHtml);
|
||||
}
|
||||
|
||||
public function sendUserNotification($to, $userName, $changes) {
|
||||
$appTitle = $this->db->getSetting('app_title', 'UniFi Voucher System');
|
||||
|
||||
// System-URL aus Einstellungen oder automatisch erkennen
|
||||
$systemUrl = $this->db->getSetting('system_url', '');
|
||||
if (empty($systemUrl)) {
|
||||
$protocol = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http';
|
||||
$host = $_SERVER['HTTP_HOST'];
|
||||
$scriptPath = dirname($_SERVER['SCRIPT_NAME']);
|
||||
$scriptPath = $scriptPath === '/' ? '' : $scriptPath;
|
||||
$systemUrl = $protocol . '://' . $host . $scriptPath;
|
||||
}
|
||||
|
||||
// Template aus Datenbank laden
|
||||
$subjectTemplate = $this->db->getSetting('email_user_notification_subject', '{APP_TITLE} - Ihre Berechtigungen wurden geändert');
|
||||
$bodyTemplate = $this->db->getSetting('email_user_notification_body', "Hallo {USER_NAME},\n\nEin Administrator hat Ihre Berechtigungen im {APP_TITLE} geändert:\n\n{CHANGES}\n\nSie können sich unter folgender Adresse anmelden:\n{SYSTEM_URL}\n\nMit freundlichen Grüßen\n{APP_TITLE}");
|
||||
|
||||
// Änderungen formatieren
|
||||
$changesText = '';
|
||||
foreach ($changes as $change) {
|
||||
$changesText .= "• $change\n";
|
||||
}
|
||||
|
||||
// Platzhalter ersetzen
|
||||
$placeholders = [
|
||||
'{USER_NAME}' => $userName,
|
||||
'{CHANGES}' => $changesText,
|
||||
'{APP_TITLE}' => $appTitle,
|
||||
'{SYSTEM_URL}' => $systemUrl
|
||||
];
|
||||
|
||||
$subject = str_replace(array_keys($placeholders), array_values($placeholders), $subjectTemplate);
|
||||
$body = str_replace(array_keys($placeholders), array_values($placeholders), $bodyTemplate);
|
||||
|
||||
// HTML oder Plain Text prüfen
|
||||
$isHtml = strip_tags($body) !== $body;
|
||||
|
||||
return $this->send($to, $subject, $body, $isHtml);
|
||||
}
|
||||
}
|
||||
329
includes/UniFiController.php
Normal file
329
includes/UniFiController.php
Normal file
|
|
@ -0,0 +1,329 @@
|
|||
<?php
|
||||
class UniFiController {
|
||||
private $controllerUrl;
|
||||
private $username;
|
||||
private $password;
|
||||
private $siteId;
|
||||
private $cookieFile;
|
||||
|
||||
public function __construct($controllerUrl, $username, $password, $siteId) {
|
||||
$this->controllerUrl = rtrim($controllerUrl, '/');
|
||||
$this->username = $username;
|
||||
$this->password = $password;
|
||||
$this->siteId = $siteId;
|
||||
$this->cookieFile = tempnam(sys_get_temp_dir(), 'UNIFI_');
|
||||
}
|
||||
|
||||
public function __destruct() {
|
||||
if (file_exists($this->cookieFile)) {
|
||||
unlink($this->cookieFile);
|
||||
}
|
||||
}
|
||||
|
||||
// Login zum Controller
|
||||
private function login() {
|
||||
$ch = curl_init();
|
||||
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_URL => $this->controllerUrl . "/api/login",
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_POSTFIELDS => json_encode([
|
||||
'username' => $this->username,
|
||||
'password' => $this->password
|
||||
]),
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_SSL_VERIFYPEER => false,
|
||||
CURLOPT_COOKIEJAR => $this->cookieFile,
|
||||
CURLOPT_COOKIEFILE => $this->cookieFile,
|
||||
CURLOPT_HTTPHEADER => ['Content-Type: application/json']
|
||||
]);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
if ($httpCode !== 200) {
|
||||
throw new Exception("Login fehlgeschlagen: HTTP $httpCode");
|
||||
}
|
||||
|
||||
$data = json_decode($response, true);
|
||||
|
||||
if (!isset($data['meta']['rc']) || $data['meta']['rc'] !== 'ok') {
|
||||
throw new Exception("Login fehlgeschlagen: Ungültige Antwort");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// API-Request ausführen
|
||||
private function apiRequest($endpoint, $data = null, $method = 'POST') {
|
||||
$this->login();
|
||||
|
||||
$ch = curl_init();
|
||||
$url = $this->controllerUrl . $endpoint;
|
||||
|
||||
$options = [
|
||||
CURLOPT_URL => $url,
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_SSL_VERIFYPEER => false,
|
||||
CURLOPT_COOKIEFILE => $this->cookieFile,
|
||||
CURLOPT_HTTPHEADER => ['Content-Type: application/json']
|
||||
];
|
||||
|
||||
if ($method === 'POST' && $data !== null) {
|
||||
$options[CURLOPT_POST] = true;
|
||||
$options[CURLOPT_POSTFIELDS] = json_encode($data);
|
||||
}
|
||||
|
||||
curl_setopt_array($ch, $options);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$error = curl_error($ch);
|
||||
curl_close($ch);
|
||||
|
||||
if ($error) {
|
||||
throw new Exception("cURL Fehler: $error");
|
||||
}
|
||||
|
||||
if ($httpCode !== 200) {
|
||||
throw new Exception("API Request fehlgeschlagen: HTTP $httpCode");
|
||||
}
|
||||
|
||||
return json_decode($response, true);
|
||||
}
|
||||
|
||||
// Voucher erstellen
|
||||
public function createVoucher($voucherName, $maxUses, $expireMinutes = 480) {
|
||||
$data = [
|
||||
'cmd' => 'create-voucher',
|
||||
'expire' => (int)$expireMinutes,
|
||||
'n' => 1,
|
||||
'note' => $voucherName,
|
||||
'quota' => (int)$maxUses
|
||||
];
|
||||
|
||||
$response = $this->apiRequest("/api/s/{$this->siteId}/cmd/hotspot", $data);
|
||||
|
||||
if (!isset($response['data'][0]['create_time'])) {
|
||||
throw new Exception("Voucher konnte nicht erstellt werden");
|
||||
}
|
||||
|
||||
// Voucher-Code abrufen
|
||||
$vouchers = $this->getVouchers();
|
||||
|
||||
if (empty($vouchers)) {
|
||||
throw new Exception("Voucher-Code konnte nicht abgerufen werden");
|
||||
}
|
||||
|
||||
// Neuesten Voucher zurückgeben
|
||||
$latestVoucher = reset($vouchers);
|
||||
|
||||
return [
|
||||
'code' => $latestVoucher['code'],
|
||||
'formatted_code' => $this->formatVoucherCode($latestVoucher['code']),
|
||||
'unifi_id' => $latestVoucher['_id'] ?? null,
|
||||
'create_time' => $latestVoucher['create_time'] ?? null
|
||||
];
|
||||
}
|
||||
|
||||
// Alle Voucher abrufen
|
||||
public function getVouchers() {
|
||||
$response = $this->apiRequest("/api/s/{$this->siteId}/stat/voucher");
|
||||
return $response['data'] ?? [];
|
||||
}
|
||||
|
||||
// Voucher mit formatierten Details abrufen
|
||||
public function getVouchersWithDetails() {
|
||||
$vouchers = $this->getVouchers();
|
||||
$result = [];
|
||||
|
||||
foreach ($vouchers as $voucher) {
|
||||
$createTime = isset($voucher['create_time']) ? $voucher['create_time'] : 0;
|
||||
$duration = isset($voucher['duration']) ? $voucher['duration'] : 0; // in Minuten
|
||||
$expireTime = $createTime + ($duration * 60);
|
||||
$now = time();
|
||||
|
||||
// Status bestimmen
|
||||
$status = 'valid';
|
||||
$usedCount = isset($voucher['used']) ? $voucher['used'] : 0;
|
||||
$quota = isset($voucher['quota']) ? $voucher['quota'] : 0;
|
||||
|
||||
if ($now > $expireTime) {
|
||||
$status = 'expired';
|
||||
} elseif ($quota > 0 && $usedCount >= $quota) {
|
||||
$status = 'used';
|
||||
}
|
||||
|
||||
$result[] = [
|
||||
'_id' => $voucher['_id'] ?? '',
|
||||
'code' => $voucher['code'] ?? '',
|
||||
'formatted_code' => $this->formatVoucherCode($voucher['code'] ?? ''),
|
||||
'note' => $voucher['note'] ?? '',
|
||||
'quota' => $quota,
|
||||
'used' => $usedCount,
|
||||
'duration' => $duration,
|
||||
'create_time' => $createTime,
|
||||
'expire_time' => $expireTime,
|
||||
'status' => $status,
|
||||
'status_expires' => isset($voucher['status_expires']) ? $voucher['status_expires'] : null,
|
||||
'for_hotspot' => isset($voucher['for_hotspot']) ? $voucher['for_hotspot'] : false,
|
||||
'qos_overwrite' => isset($voucher['qos_overwrite']) ? $voucher['qos_overwrite'] : false,
|
||||
'qos_usage_quota' => isset($voucher['qos_usage_quota']) ? $voucher['qos_usage_quota'] : null,
|
||||
'qos_rate_max_up' => isset($voucher['qos_rate_max_up']) ? $voucher['qos_rate_max_up'] : null,
|
||||
'qos_rate_max_down' => isset($voucher['qos_rate_max_down']) ? $voucher['qos_rate_max_down'] : null
|
||||
];
|
||||
}
|
||||
|
||||
// Nach Erstellungsdatum sortieren (neueste zuerst)
|
||||
usort($result, function($a, $b) {
|
||||
return $b['create_time'] - $a['create_time'];
|
||||
});
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
// Voucher-Code formatieren (xxxxx-xxxxx-xxxxx)
|
||||
private function formatVoucherCode($code) {
|
||||
$chunks = str_split($code, 5);
|
||||
return implode('-', $chunks);
|
||||
}
|
||||
|
||||
// Voucher löschen
|
||||
public function deleteVoucher($voucherId) {
|
||||
$data = [
|
||||
'cmd' => 'delete-voucher',
|
||||
'_id' => $voucherId
|
||||
];
|
||||
|
||||
$response = $this->apiRequest("/api/s/{$this->siteId}/cmd/hotspot", $data);
|
||||
|
||||
return isset($response['meta']['rc']) && $response['meta']['rc'] === 'ok';
|
||||
}
|
||||
|
||||
// Verbindung testen
|
||||
public static function testConnection($controllerUrl, $username, $password, $siteId) {
|
||||
try {
|
||||
$controller = new self($controllerUrl, $username, $password, $siteId);
|
||||
$controller->login();
|
||||
return true;
|
||||
} catch (Exception $e) {
|
||||
return $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronisiert alle Voucher vom UniFi Controller in die Datenbank
|
||||
* @param Database $db Datenbank-Instanz
|
||||
* @param int $dbSiteId Die Site-ID in der lokalen Datenbank
|
||||
* @return array Statistiken über die Synchronisation
|
||||
*/
|
||||
public function syncVouchersToDatabase($db, $dbSiteId) {
|
||||
$vouchers = $this->getVouchersWithDetails();
|
||||
$stats = [
|
||||
'total' => count($vouchers),
|
||||
'new' => 0,
|
||||
'updated' => 0,
|
||||
'deleted' => 0,
|
||||
'valid' => 0,
|
||||
'used' => 0,
|
||||
'expired' => 0
|
||||
];
|
||||
|
||||
// Alle aktuellen UniFi-IDs sammeln
|
||||
$unifiIds = [];
|
||||
|
||||
foreach ($vouchers as $voucher) {
|
||||
$unifiIds[] = $voucher['_id'];
|
||||
|
||||
// Status zählen
|
||||
$stats[$voucher['status']]++;
|
||||
|
||||
// Prüfen ob Voucher bereits existiert
|
||||
$existing = $db->fetchOne(
|
||||
"SELECT id, status, used_count FROM vouchers WHERE unifi_voucher_id = ? AND site_id = ?",
|
||||
[$voucher['_id'], $dbSiteId]
|
||||
);
|
||||
|
||||
$expiresAt = date('Y-m-d H:i:s', $voucher['expire_time']);
|
||||
$createdAt = date('Y-m-d H:i:s', $voucher['create_time']);
|
||||
|
||||
if ($existing) {
|
||||
// Voucher aktualisieren
|
||||
$db->execute(
|
||||
"UPDATE vouchers SET
|
||||
status = ?,
|
||||
used_count = ?,
|
||||
expires_at = ?,
|
||||
last_sync = NOW()
|
||||
WHERE id = ?",
|
||||
[$voucher['status'], $voucher['used'], $expiresAt, $existing['id']]
|
||||
);
|
||||
$stats['updated']++;
|
||||
} else {
|
||||
// Neuen Voucher einfügen
|
||||
$db->execute(
|
||||
"INSERT INTO vouchers
|
||||
(site_id, voucher_code, voucher_name, max_uses, expire_minutes,
|
||||
unifi_voucher_id, status, used_count, expires_at, created_at,
|
||||
synced_from_unifi, last_sync)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, NOW())",
|
||||
[
|
||||
$dbSiteId,
|
||||
$voucher['formatted_code'],
|
||||
$voucher['note'] ?: 'Importiert aus UniFi',
|
||||
$voucher['quota'],
|
||||
$voucher['duration'],
|
||||
$voucher['_id'],
|
||||
$voucher['status'],
|
||||
$voucher['used'],
|
||||
$expiresAt,
|
||||
$createdAt
|
||||
]
|
||||
);
|
||||
$stats['new']++;
|
||||
}
|
||||
}
|
||||
|
||||
// Voucher die nicht mehr im Controller existieren als gelöscht markieren
|
||||
if (!empty($unifiIds)) {
|
||||
$placeholders = implode(',', array_fill(0, count($unifiIds), '?'));
|
||||
$params = array_merge($unifiIds, [$dbSiteId]);
|
||||
|
||||
// Alle Voucher mit UniFi-ID die nicht mehr existieren auf "expired" setzen
|
||||
$deleted = $db->execute(
|
||||
"UPDATE vouchers SET status = 'expired', last_sync = NOW()
|
||||
WHERE unifi_voucher_id IS NOT NULL
|
||||
AND unifi_voucher_id NOT IN ($placeholders)
|
||||
AND site_id = ?
|
||||
AND status != 'expired'",
|
||||
$params
|
||||
);
|
||||
$stats['deleted'] = $deleted;
|
||||
}
|
||||
|
||||
return $stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Holt Live-Statistiken für das Dashboard
|
||||
* @return array
|
||||
*/
|
||||
public function getLiveStats() {
|
||||
$vouchers = $this->getVouchersWithDetails();
|
||||
|
||||
$stats = [
|
||||
'total' => count($vouchers),
|
||||
'valid' => 0,
|
||||
'used' => 0,
|
||||
'expired' => 0,
|
||||
'vouchers' => $vouchers
|
||||
];
|
||||
|
||||
foreach ($vouchers as $voucher) {
|
||||
$stats[$voucher['status']]++;
|
||||
}
|
||||
|
||||
return $stats;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue