diff --git a/application/Radius/RadiusController.php b/application/Radius/RadiusController.php index 7c8e96be3..097c61e22 100644 --- a/application/Radius/RadiusController.php +++ b/application/Radius/RadiusController.php @@ -3,6 +3,11 @@ use PHPMailer\PHPMailer\PHPMailer; use PHPMailer\PHPMailer\Exception; class RadiusController extends mfBaseController { + // FritzBox API configuration + private const FRITZBOX_API_URL = "http://acs.xinon.at:5000"; + private const FRITZBOX_API_KEY = "2H9zWrgxPEJL9MZ1yTGtWh16cPCu0AsQ"; + private const FRITZBOX_PORT = "9090"; + private User $me; private bool $isApiCall = false; @@ -113,15 +118,14 @@ class RadiusController extends mfBaseController { $ip = $externalIp ?: $managementIp; if (!$ip) self::sendError("Could not determine device IP"); - $url = "http://acs.xinon.at:5000/run-speedtest"; - $apiKey = "2H9zWrgxPEJL9MZ1yTGtWh16cPCu0AsQ"; + $url = self::FRITZBOX_API_URL . "/run-speedtest"; $data = json_encode(['ip' => $ip]); $opts = [ "http" => [ "method" => "POST", "header" => "Content-Type: application/json\r\n" . - "X-API-Key: " . $apiKey . "\r\n" . + "X-API-Key: " . self::FRITZBOX_API_KEY . "\r\n" . "Content-Length: " . strlen($data) . "\r\n", "content" => $data ] @@ -445,12 +449,11 @@ class RadiusController extends mfBaseController { $creds = $acs->createRemoteUser($resolvedId); if (!$creds) self::sendError("Could not obtain credentials for FritzBox"); - $url = "http://acs.xinon.at:5000/read-fritz-eventlog"; - $apiKey = "2H9zWrgxPEJL9MZ1yTGtWh16cPCu0AsQ"; + $url = self::FRITZBOX_API_URL . "/read-fritz-eventlog"; $data = json_encode([ 'fritz_ip' => $creds['ip'], - 'fritz_port' => "9090", + 'fritz_port' => self::FRITZBOX_PORT, 'fritz_user' => $creds['username'], 'fritz_pass' => $creds['password'] ]); @@ -459,7 +462,7 @@ class RadiusController extends mfBaseController { "http" => [ "method" => "POST", "header" => "Content-Type: application/json\r\n" . - "X-API-Key: " . $apiKey . "\r\n" . + "X-API-Key: " . self::FRITZBOX_API_KEY . "\r\n" . "Content-Length: " . strlen($data) . "\r\n", "content" => $data, "timeout" => 60 @@ -484,11 +487,12 @@ class RadiusController extends mfBaseController { } } - protected function genieacsNetworkStructureAction() { + protected function genieacsFritzboxWlanKeyAction() { try { $input = json_decode(file_get_contents('php://input'), true); $deviceId = $input['deviceId'] ?? null; - $this->log->debug("genieacsNetworkStructureAction", ['deviceId' => $deviceId]); + $forceRefresh = $input['forceRefresh'] ?? false; + $this->log->debug("genieacsFritzboxWlanKeyAction", ['deviceId' => $deviceId, 'forceRefresh' => $forceRefresh]); if (!$deviceId) self::sendError("Device ID is required"); @@ -497,15 +501,98 @@ class RadiusController extends mfBaseController { $resolvedId = $this->resolveDeviceId($deviceId, $acs); if (!$resolvedId) self::sendError("Device not found in GenieACS"); + // Check cache first (1 hour TTL = 3600s) + $cacheKey = "fritzbox_wlan_" . $resolvedId; + if (!$forceRefresh) { + $cached = $acs->getCachePublic($cacheKey, 3600); + if ($cached) { + $this->log->debug("WLAN Key: returning cached data"); + self::returnJson(['success' => true, 'wlan' => $cached, 'cached' => true]); + return; + } + } + $creds = $acs->createRemoteUser($resolvedId); if (!$creds) self::sendError("Could not obtain credentials for FritzBox"); - $url = "http://acs.xinon.at:5000/read-fritz"; - $apiKey = "2H9zWrgxPEJL9MZ1yTGtWh16cPCu0AsQ"; - + $url = self::FRITZBOX_API_URL . "/read-fritz-wlan-key"; + $data = json_encode([ 'fritz_ip' => $creds['ip'], - 'fritz_port' => "9090", + 'fritz_port' => self::FRITZBOX_PORT, + 'fritz_user' => $creds['username'], + 'fritz_pass' => $creds['password'] + ]); + + $opts = [ + "http" => [ + "method" => "POST", + "header" => "Content-Type: application/json\r\n" . + "X-API-Key: " . self::FRITZBOX_API_KEY . "\r\n" . + "Content-Length: " . strlen($data) . "\r\n", + "content" => $data, + "timeout" => 60 + ] + ]; + + $context = stream_context_create($opts); + $response = file_get_contents($url, false, $context); + + if ($response) { + $json = json_decode($response, true); + if ($json && isset($json['wlan'])) { + // Also fetch LAN/DHCP config from GenieACS device data + $device = $acs->getDevice($resolvedId); + $lanConfig = $this->extractLanConfig($device); + + $result = array_merge($json['wlan'], ['lan' => $lanConfig]); + + // Cache the response + $acs->setCachePublic($cacheKey, $result); + self::returnJson(['success' => true, 'wlan' => $result, 'cached' => false]); + return; + } + } + + self::sendError("Failed to fetch WLAN key data"); + } catch (Exception $e) { + $this->log->debug("WLAN Key Error", ['error' => $e->getMessage()]); + self::sendError("Error: " . $e->getMessage()); + } + } + + protected function genieacsNetworkStructureAction() { + try { + $input = json_decode(file_get_contents('php://input'), true); + $deviceId = $input['deviceId'] ?? null; + $forceRefresh = $input['forceRefresh'] ?? false; + $this->log->debug("genieacsNetworkStructureAction", ['deviceId' => $deviceId, 'forceRefresh' => $forceRefresh]); + + if (!$deviceId) self::sendError("Device ID is required"); + + $acs = $this->getGenieACS(); + + $resolvedId = $this->resolveDeviceId($deviceId, $acs); + if (!$resolvedId) self::sendError("Device not found in GenieACS"); + + // Check cache first (15 min TTL = 900s) + $cacheKey = "fritzbox_network_" . $resolvedId; + if (!$forceRefresh) { + $cached = $acs->getCachePublic($cacheKey, 900); + if ($cached) { + self::returnJson(['root' => $cached, 'cached' => true]); + return; + } + } + + $creds = $acs->createRemoteUser($resolvedId); + if (!$creds) self::sendError("Could not obtain credentials for FritzBox"); + + $url = self::FRITZBOX_API_URL . "/read-fritz"; + + $data = json_encode([ + 'fritz_ip' => $creds['ip'], + 'fritz_port' => self::FRITZBOX_PORT, 'fritz_user' => $creds['username'], 'fritz_pass' => $creds['password'], 'page' => 'netDev' @@ -515,7 +602,7 @@ class RadiusController extends mfBaseController { "http" => [ "method" => "POST", "header" => "Content-Type: application/json\r\n" . - "X-API-Key: " . $apiKey . "\r\n" . + "X-API-Key: " . self::FRITZBOX_API_KEY . "\r\n" . "Content-Length: " . strlen($data) . "\r\n", "content" => $data, "timeout" => 60 @@ -623,7 +710,10 @@ class RadiusController extends mfBaseController { $sortChildren($fbox); - self::returnJson(['root' => $fbox]); + // Cache the processed result + $acs->setCachePublic($cacheKey, $fbox); + + self::returnJson(['root' => $fbox, 'cached' => false]); } } @@ -653,9 +743,31 @@ class RadiusController extends mfBaseController { } } - - - + + /** + * Extract LAN/DHCP configuration from GenieACS device data + */ + private function extractLanConfig($device) { + if (!is_array($device)) return null; + + $getParam = function($key) use ($device) { + if (isset($device[$key]['value'][0])) { + return $device[$key]['value'][0]; + } + return null; + }; + + return [ + // LAN IP Configuration + 'ip' => $getParam('InternetGatewayDevice.LANDevice.1.LANHostConfigManagement.IPInterface.1.IPInterfaceIPAddress'), + 'subnet' => $getParam('InternetGatewayDevice.LANDevice.1.LANHostConfigManagement.IPInterface.1.IPInterfaceSubnetMask'), + // DHCP Configuration + 'dhcp_start' => $getParam('InternetGatewayDevice.LANDevice.1.LANHostConfigManagement.MinAddress'), + 'dhcp_end' => $getParam('InternetGatewayDevice.LANDevice.1.LANHostConfigManagement.MaxAddress'), + 'dns_servers' => $getParam('InternetGatewayDevice.LANDevice.1.LANHostConfigManagement.DNSServers'), + ]; + } + private function getVendor($mac) { $mac = strtoupper(str_replace([':', '-', '.'], '', $mac)); diff --git a/lib/GenieACS/GenieACS.php b/lib/GenieACS/GenieACS.php index 6905065b9..a38428776 100644 --- a/lib/GenieACS/GenieACS.php +++ b/lib/GenieACS/GenieACS.php @@ -235,10 +235,10 @@ class GenieACS { return $result; } - private function getCache($key) { + private function getCache($key, $ttl = 3300) { $file = TEMP_DIR . "/RadiusCache/" . md5($key) . ".json"; if (file_exists($file)) { - if (filemtime($file) < (time() - 1800)) { + if (filemtime($file) < (time() - $ttl)) { @unlink($file); return null; } @@ -247,6 +247,14 @@ class GenieACS { return null; } + public function getCachePublic($key, $ttl = 3300) { + return $this->getCache($key, $ttl); + } + + public function setCachePublic($key, $data) { + $this->setCache($key, $data); + } + private function setCache($key, $data) { $dir = TEMP_DIR . "/RadiusCache/"; if (!is_dir($dir)) @mkdir($dir, 0777, true); diff --git a/public/js/pages/Radius/Radius.css b/public/js/pages/Radius/Radius.css index aaae5f43c..512a01cd1 100644 --- a/public/js/pages/Radius/Radius.css +++ b/public/js/pages/Radius/Radius.css @@ -246,3 +246,170 @@ @media (max-width: 700px) { .tt-scope .router-actions-grid { grid-template-columns: 1fr; } } .tt-scope .action-btn { display: flex; align-items: center; padding: 8px 16px; } .tt-scope .action-btn i { width: 20px; flex-shrink: 0; text-align: center; margin-right: 10px; } + +/* ===== Network Config Modal ===== */ +.tt-scope .network-config-modal { } + +.tt-scope .cache-badge { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 2px 8px; + background: rgba(255,255,255,0.7); + border: 1px solid #b8d9f0; + border-radius: 999px; + font-size: 10px; + font-weight: 600; + color: #0b3a57; + margin-left: auto; +} + +.tt-scope .cache-badge i { font-size: 9px; } + +.tt-scope .config-action-btn { + background: rgba(255,255,255,0.6); + border: 1px solid #b8d9f0; + border-radius: 6px; + padding: 4px 8px; + cursor: pointer; + color: var(--accent); + font-size: 12px; + transition: all 0.15s ease; + display: flex; + align-items: center; + justify-content: center; +} + +.tt-scope .config-action-btn:hover { + background: rgba(255,255,255,0.9); + border-color: var(--accent); +} + +.tt-scope .config-action-btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.tt-scope .config-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 12px; +} + +@media (max-width: 700px) { + .tt-scope .config-grid { grid-template-columns: 1fr; } +} + +.tt-scope .config-card { + background: linear-gradient(145deg, #ffffff 0%, #f8fbfd 100%); + border: 1px solid #e3eef5; + border-radius: 10px; + overflow: hidden; + transition: box-shadow 0.2s ease, border-color 0.2s ease; +} + +.tt-scope .config-card:hover { + box-shadow: 0 4px 16px rgba(0, 83, 132, 0.08); + border-color: #cce4f5; +} + +.tt-scope .config-card-header { + display: flex; + align-items: center; + gap: 8px; + padding: 10px 14px; + background: linear-gradient(135deg, #e8f4fb 0%, #dceef8 100%); + border-bottom: 1px solid #d6e8f5; + font-size: 13px; + font-weight: 700; + color: #0b3a57; +} + +.tt-scope .config-card-header > i:first-child { + font-size: 14px; + color: var(--accent); +} + +.tt-scope .config-card-header .status-dot { + width: 8px; + height: 8px; + border-radius: 50%; +} + +.tt-scope .config-card-header .status-dot.active { + background: #10b981; + box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.2); +} + +.tt-scope .config-card-header .status-dot.inactive { + background: #ef4444; + box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.2); +} + +.tt-scope .config-card-body { + padding: 10px 14px; +} + +.tt-scope .config-row { + display: flex; + align-items: center; + justify-content: space-between; + padding: 6px 0; + gap: 12px; +} + +.tt-scope .config-row:not(:last-child) { + border-bottom: 1px dashed #e8eef3; +} + +.tt-scope .config-row.highlight { + background: #f8fbfd; + margin: 0 -14px; + padding: 8px 14px; + border-bottom: 1px solid #e8eef3; +} + +.tt-scope .config-row.highlight:first-child { + margin-top: -10px; +} + +.tt-scope .config-label { + font-size: 12px; + color: #667085; + font-weight: 500; + flex-shrink: 0; +} + +.tt-scope .config-value { + font-family: var(--mono); + font-size: 12px; + color: #1a2b3c; + font-weight: 600; + text-align: right; + word-break: break-all; +} + +.tt-scope .config-value.with-copy { + display: flex; + align-items: center; + gap: 6px; + justify-content: flex-end; +} + +.tt-scope .config-value.small { + font-size: 11px; + font-weight: 500; +} + +.tt-scope .config-value code { + background: none; + padding: 0; + font-family: inherit; +} + +.tt-scope .config-value code.password { + font-size: 13px; + font-weight: 700; + color: var(--accent); + letter-spacing: 0.5px; +} diff --git a/public/js/pages/Radius/RadiusRouterManager.js b/public/js/pages/Radius/RadiusRouterManager.js index 95c92c04d..13336cf43 100644 --- a/public/js/pages/Radius/RadiusRouterManager.js +++ b/public/js/pages/Radius/RadiusRouterManager.js @@ -78,6 +78,10 @@ const RadiusRouterManager = { Ereignisprotokoll + @@ -178,6 +182,13 @@ const RadiusRouterManager = {
+
+ (Cache) + + +
@@ -214,6 +225,119 @@ const RadiusRouterManager = {
Keine Ereignisse verfügbar.
+ + + +
+ +
+ +
+
+ + WLAN + + Cache + + +
+
+
+ SSID +
+ {{ wlanKeyData.ssid || '-' }} + +
+
+
+ SSID 5GHz +
+ {{ wlanKeyData.ssid_secondary }} + +
+
+
+ Passwort +
+ {{ wlanKeyData.psk || '-' }} + +
+
+
+ Sicherheit + {{ wlanKeyData.wpa_type?.toUpperCase() || 'WPA2' }} +
+
+
+ + +
+
+ + LAN +
+
+
+ Router-IP +
+ {{ wlanKeyData.lan?.ip || '-' }} + +
+
+
+ Subnetz + {{ wlanKeyData.lan?.subnet || '-' }} +
+
+
+ + +
+
+ + DHCP +
+
+
+ Bereich + + {{ wlanKeyData.lan.dhcp_start }} - {{ wlanKeyData.lan.dhcp_end }} + + - +
+
+ DNS + {{ wlanKeyData.lan?.dns_servers || '-' }} +
+
+
+ + +
+
+ + Gerät +
+
+
+ Modell + {{ wlanKeyData.device_name }} +
+
+ WLAN-Geräte + {{ wlanKeyData.known_devices_count }} +
+
+
+
+
+
Keine Daten verfügbar.
+
+
`, data: () => ({ @@ -238,12 +362,18 @@ const RadiusRouterManager = { showNetworkStructureModal: false, networkStructureLoading: false, + networkStructureCached: false, rootDevice: null, showEventLogModal: false, eventLogLoading: false, eventLogData: null, - refreshLoading: false + refreshLoading: false, + + showWlanKeyModal: false, + wlanKeyLoading: false, + wlanKeyData: null, + wlanKeyDataCached: false }), watch: { show: { @@ -439,19 +569,22 @@ const RadiusRouterManager = { this.remoteAccessLoading = false; } }, - async openNetworkStructure() { + async openNetworkStructure(forceRefresh = false) { if (!this.routerDevice || !this.routerDevice.deviceId) return; this.showNetworkStructureModal = true; this.networkStructureLoading = true; this.rootDevice = null; + this.networkStructureCached = false; try { const { data } = await axios.post(`${window.TT_CONFIG.BASE_PATH}/Radius/genieacsNetworkStructure`, { - deviceId: this.routerDevice.deviceId + deviceId: this.routerDevice.deviceId, + forceRefresh: forceRefresh }); if (data.root) { this.rootDevice = data.root; + this.networkStructureCached = data.cached === true; } } catch (error) { console.error(error); @@ -482,6 +615,63 @@ const RadiusRouterManager = { } finally { this.eventLogLoading = false; } + }, + async openWlanKeyModal(forceRefresh = false) { + if (!this.routerDevice || !this.routerDevice.deviceId) return; + this.showWlanKeyModal = true; + this.wlanKeyLoading = true; + this.wlanKeyData = null; + this.wlanKeyDataCached = false; + + try { + const { data } = await axios.post(`${window.TT_CONFIG.BASE_PATH}/Radius/genieacsFritzboxWlanKey`, { + deviceId: this.routerDevice.deviceId, + forceRefresh: forceRefresh + }); + + if (data.success && data.wlan) { + this.wlanKeyData = data.wlan; + this.wlanKeyDataCached = data.cached === true; + } else { + throw new Error(data.message || "Keine WLAN-Daten gefunden"); + } + } catch (error) { + console.error(error); + window.notify('error', error.response?.data?.message || 'Fehler beim Laden der WLAN-Daten'); + } finally { + this.wlanKeyLoading = false; + } + }, + copyAllNetworkConfig() { + if (!this.wlanKeyData) return; + + const d = this.wlanKeyData; + const lines = [ + '=== WLAN ===', + `SSID: ${d.ssid || '-'}`, + d.ssid_secondary && d.ssid_secondary !== d.ssid ? `SSID 5GHz: ${d.ssid_secondary}` : null, + `Passwort: ${d.psk || '-'}`, + `Sicherheit: ${d.wpa_type?.toUpperCase() || 'WPA2'}`, + d.ap_enabled !== undefined ? `WLAN aktiv: ${d.ap_enabled ? 'Ja' : 'Nein'}` : null, + '', + '=== LAN ===', + `Router-IP: ${d.lan?.ip || '-'}`, + `Subnetz: ${d.lan?.subnet || '-'}`, + '', + '=== DHCP ===', + `Bereich: ${d.lan?.dhcp_start && d.lan?.dhcp_end ? `${d.lan.dhcp_start} - ${d.lan.dhcp_end}` : '-'}`, + `DNS: ${d.lan?.dns_servers || '-'}`, + '', + '=== Gerät ===', + d.device_name ? `Modell: ${d.device_name}` : null, + d.known_devices_count !== undefined ? `WLAN-Geräte: ${d.known_devices_count}` : null, + ].filter(Boolean).join('\n'); + + navigator.clipboard.writeText(lines).then(() => { + window.notify('success', 'Netzwerk-Konfiguration kopiert'); + }).catch(() => { + window.notify('error', 'Kopieren fehlgeschlagen'); + }); } } };