From 9e56b8ee989778f8eb5af364900a53056a93f159 Mon Sep 17 00:00:00 2001 From: Luca Haid Date: Tue, 18 Nov 2025 08:25:26 +0100 Subject: [PATCH] Implement RADIUS user creation and update functionality in CPE provisioning --- .../CpeprovisioningController.php | 293 +++++++++++++++++- config/config.sample.php | 8 + .../pages/Cpeprovisioning/Cpeprovisioning.js | 49 ++- 3 files changed, 316 insertions(+), 34 deletions(-) diff --git a/application/Cpeprovisioning/CpeprovisioningController.php b/application/Cpeprovisioning/CpeprovisioningController.php index 3ca148fe2..beec047a9 100644 --- a/application/Cpeprovisioning/CpeprovisioningController.php +++ b/application/Cpeprovisioning/CpeprovisioningController.php @@ -530,6 +530,7 @@ class CpeprovisioningController extends mfBaseController { "CPE_PROV_API_GET_URL" => $this->getUrl("Cpeprovisioning", "apiGet"), "CPE_PROV_API_SAVE_URL" => $this->getUrl("Cpeprovisioning", "apiSave"), "CPE_PROV_API_TEST_ACS_VLAN_URL" => $this->getUrl("Cpeprovisioning", "getAcsVlan"), + "CPE_PROV_API_CREATE_RADIUS_USER_URL" => $this->getUrl("Cpeprovisioning", "createRadiusUser"), "CPE_PROV_PRINT_PDF_URL" => $this->getUrl("Cpeprovisioning", "printPDF"), "ORDER_URL" => $this->getUrl("Order"), "NETWORKS" => NetworkModel::getAll(), @@ -598,13 +599,17 @@ class CpeprovisioningController extends mfBaseController { $attrs = $product->product->attributes; - $vlanPublicDefault = $term->getPop()->vlan_public ?? $attrs['vlan_default_public']->value ?? null; - $vlanNatDefault = $term->getPop()->vlan_nat ?? $attrs['vlan_default_nat']->value ?? null; - $vlanIpv6Default = $term->getPop()->vlan_ipv6 ?? $attrs['vlan_default_ipv6']->value ?? null; + // First, check if any VLAN is explicitly saved (checked in frontend) + // The saved values take priority over defaults + $assignedVlan = $cpe->vlan_public ?? $cpe->vlan_nat ?? $cpe->vlan_ipv6; - // For the test, we just return the first available VLAN that would be assigned. - // The logic can be expanded if a specific type is requested. - $assignedVlan = $vlanPublicDefault ?? $vlanNatDefault ?? $vlanIpv6Default; + // If no VLAN is explicitly saved, fall back to defaults + if (!$assignedVlan) { + $vlanPublicDefault = $term->getPop()->vlan_public ?? $attrs['vlan_default_public']->value ?? null; + $vlanNatDefault = $term->getPop()->vlan_nat ?? $attrs['vlan_default_nat']->value ?? null; + $vlanIpv6Default = $term->getPop()->vlan_ipv6 ?? $attrs['vlan_default_ipv6']->value ?? null; + $assignedVlan = $vlanPublicDefault ?? $vlanNatDefault ?? $vlanIpv6Default; + } if ($assignedVlan) { self::returnJson(['success' => true, 'vlan_id' => $assignedVlan]); @@ -618,6 +623,282 @@ class CpeprovisioningController extends mfBaseController { } } + protected function createRadiusUserAction() { + $apiKey = $_SERVER['HTTP_X_API_KEY'] ?? null; + $isApiCall = defined('TT_CPE_PROV_ACS_API_KEY') && $apiKey && $apiKey === TT_CPE_PROV_ACS_API_KEY; + $isLoggedInUser = $this->me && $this->me->id; + + if (!$isApiCall && !$isLoggedInUser) { + http_response_code(403); + self::returnJson(['success' => false, 'message' => 'Forbidden']); + return; + } + + try { + $p = json_decode(file_get_contents('php://input'), true); + $mac = $p['mac'] ?? null; + + if (empty($mac)) { + throw new Exception("MAC address is required."); + } + + // Normalize MAC address format to uppercase with colons + $mac = strtoupper(str_replace(['-', '.'], ':', $mac)); + + // Look up CPE provisioning entry + $cpe = CpeprovisioningModel::getFirst(['mac' => $mac]); + if (!$cpe || !$cpe->termination_id) { + throw new Exception("No active provisioning entry found for this MAC address."); + } + + $term = new Termination($cpe->termination_id); + $product = $cpe->orderproduct; + $order = new Order($cpe->order_id); + + if (!$term->id || !$product->id || !$order->id) { + throw new Exception("Could not load termination, product, or order details."); + } + + // Gather all data needed for RADIUS user + $customerNumber = $order->owner->customer_number ?? $order->partner_number ?? ''; + $ontSn = $term->getWorkflowValue("ont_sn", "string") ?? ''; + $wifiKey = $cpe->wifi_pass ?? ''; + + // Check if RADIUS credentials are configured + if (!defined('TT_RADIUS_URL') || !defined('TT_RADIUS_USERNAME') || !defined('TT_RADIUS_PASSWORD')) { + throw new Exception("RADIUS server credentials are not configured."); + } + + $radiusUrl = TT_RADIUS_URL; + $radiusUsername = TT_RADIUS_USERNAME; + $radiusPassword = TT_RADIUS_PASSWORD; + + // Step 1: Check if RADIUS user already exists + $existingUser = $this->checkRadiusUserExists($mac); + + if ($existingUser) { + $this->log->info("RADIUS user {$mac} already exists, skipping creation and updating details only."); + } else { + // Step 2: Login to RADIUS server + $cookieFile = tempnam(sys_get_temp_dir(), 'radius_cookie_'); + + // Generate random password for RADIUS user + $radiusUserPassword = substr(str_shuffle('abcdefghijklmnopqrstuvwxyz0123456789'), 0, 8); + + $loginResult = $this->radiusHttpRequest( + $radiusUrl . '/login.php', + [ + 'username' => $radiusUsername, + 'password' => $radiusPassword, + 'submit' => 'Login' + ], + $cookieFile + ); + + if (strpos($loginResult, 'Login') === false && strpos($loginResult, 'logout') === false) { + throw new Exception("Failed to login to RADIUS server."); + } + + // Step 3: Create user with MAC address + $createResult = $this->radiusHttpRequest( + $radiusUrl . '/add_user.php', + [ + 'user' => $mac, + 'pass' => $radiusUserPassword, + 'submit' => ' Anlegen ' + ], + $cookieFile + ); + + // Check if user creation was successful + if (strpos($createResult, 'already exists') !== false) { + // Somehow it exists now (race condition?), continue to update + $this->log->info("RADIUS user {$mac} already exists, updating details."); + } elseif (strpos($createResult, 'error') !== false || strpos($createResult, 'Error') !== false) { + throw new Exception("Failed to create RADIUS user: User creation returned an error."); + } else { + $this->log->info("RADIUS user {$mac} created successfully."); + } + } + + // Step 4: Login to RADIUS server for update (in case we skipped creation) + $cookieFile = tempnam(sys_get_temp_dir(), 'radius_cookie_'); + $loginResult = $this->radiusHttpRequest( + $radiusUrl . '/login.php', + [ + 'username' => $radiusUsername, + 'password' => $radiusPassword, + 'submit' => 'Login' + ], + $cookieFile + ); + + if (strpos($loginResult, 'Login') === false && strpos($loginResult, 'logout') === false) { + throw new Exception("Failed to login to RADIUS server for update."); + } + + // Step 5: Update user details + $updateData = [ + 'userid' => '', + 'user' => $mac, + 'Cleartext-Password' => '', + 'Custnum' => (strpos($customerNumber, '7000') === 0) ? $customerNumber : '', + 'Custnume' => (strpos($customerNumber, '7000') === 0) ? '' : $customerNumber, + 'Hotspot_Info' => '', + 'ont_sn' => $ontSn, + 'Wifikey' => $wifiKey, + 'Mikrotik-Group' => '', + 'Framed-Pool' => '', + 'Pool-Name' => '', + 'Framed-IP-Address' => '', + 'Framed-IP-Netmask' => '', + 'Framed-Route' => '', + 'MS-Primary-DNS-Server' => '195.191.252.62', + 'MS-Secondary-DNS-Server' => '193.105.204.194', + 'DHCP-IP-Address-Lease-Time' => '', + 'MaxLogins' => '', + 'Valid-From' => '', + 'Valid-To' => '', + 'Hotspot_Duration' => '', + 'Hotspot_Duration_Multiplicant' => '86400', + 'Rate-Limit-Down' => '', + 'Rate-Limit-Up' => '', + 'ContractDown' => '', + 'ContractUp' => '', + 'Rate-Limit-Down-Burst' => '', + 'Rate-Limit-Up-Burst' => '', + 'Rate-Limit-Burst-Sec' => '', + 'Rate-Limit-Down-Thresh' => '', + 'Rate-Limit-Up-Thresh' => '', + 'Session-Timeout' => '', + 'timeout_max' => '', + 'Mikrotik-Recv-Limit' => '', + 'transfer_max' => '', + 'cisco-avpair[vrf]' => '', + 'cisco-avpair[interface]' => '', + 'submit' => 'Update' + ]; + + $updateResult = $this->radiusHttpRequest( + $radiusUrl . '/edit_user.php', + $updateData, + $cookieFile + ); + + // Clean up cookie file + @unlink($cookieFile); + + // Check if update was successful + if (strpos($updateResult, 'error') !== false || strpos($updateResult, 'Error') !== false) { + throw new Exception("Failed to update RADIUS user details."); + } + + $this->log->info("Successfully created/updated RADIUS user for MAC: {$mac}, Customer: {$customerNumber}"); + + self::returnJson([ + 'success' => true, + 'message' => 'RADIUS user created/updated successfully.', + 'data' => [ + 'mac' => $mac, + 'customer_number' => $customerNumber, + 'ont_sn' => $ontSn, + 'wifi_key' => $wifiKey + ] + ]); + + } catch (Exception $e) { + // Clean up cookie file on error + if (isset($cookieFile) && file_exists($cookieFile)) { + @unlink($cookieFile); + } + + $this->log->error("Failed to create RADIUS user: " . $e->getMessage()); + http_response_code(400); + self::returnJson(['success' => false, 'message' => $e->getMessage()]); + } + } + + /** + * Check if a RADIUS user exists by username (MAC address) + * + * @param string $username The username/MAC address to check + * @return bool True if user exists, false otherwise + */ + private function checkRadiusUserExists($username) { + try { + if (!defined('TT_RADIUS_API_URL')) { + // Fallback to default if not configured + $apiUrl = 'http://radius.xinon.at/api.php'; + } else { + $apiUrl = TT_RADIUS_API_URL; + } + + // Query the RADIUS API to check if user exists + $queryParams = http_build_query(['username' => $username]); + $url = $apiUrl . '?' . $queryParams; + + $opts = [ + "http" => [ + "method" => "GET", + "header" => "Authorization: Basic " . base64_encode(TT_RADIUS_USERNAME . ":" . TT_RADIUS_PASSWORD), + "timeout" => 10 + ] + ]; + + $context = stream_context_create($opts); + $response = @file_get_contents($url, false, $context); + + if ($response === false) { + $this->log->warning("Could not check if RADIUS user exists: API request failed"); + return false; + } + + $data = json_decode($response, true); + + // If we get results back, the user exists + if (is_array($data) && count($data) > 0) { + return true; + } + + return false; + + } catch (Exception $e) { + $this->log->error("Error checking RADIUS user existence: " . $e->getMessage()); + return false; + } + } + + private function radiusHttpRequest($url, $postData, $cookieFile) { + $ch = curl_init($url); + + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($postData)); + curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); + curl_setopt($ch, CURLOPT_COOKIEJAR, $cookieFile); + curl_setopt($ch, CURLOPT_COOKIEFILE, $cookieFile); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); + curl_setopt($ch, CURLOPT_TIMEOUT, 30); + + $result = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + + if (curl_errno($ch)) { + $error = curl_error($ch); + curl_close($ch); + throw new Exception("HTTP request failed: " . $error); + } + + curl_close($ch); + + if ($httpCode !== 200) { + throw new Exception("HTTP request returned status code: " . $httpCode); + } + + return $result; + } + private function fixCpeData($data) { if (!$data) return []; $data->shipping = (bool)$data->shipping; diff --git a/config/config.sample.php b/config/config.sample.php index 42c653a8c..b9d39bc6b 100644 --- a/config/config.sample.php +++ b/config/config.sample.php @@ -162,6 +162,14 @@ const TT_IBAN_VALIDATOR_BASEURL = "https://rest.sepatools.eu/validate_iban/"; const TT_IBAN_VALIDATOR_USER = ""; const TT_IBAN_VALIDATOR_PASS = ""; +// CPE Provisioning ACS API +const TT_CPE_PROV_ACS_API_KEY = "your-acs-api-key-here"; +const TT_CPE_PROV_ALLOW_COUNTS_IP = ["192.168.1.100"]; // IPs allowed to access count endpoint + +// RADIUS Server Configuration +const TT_RADIUS_URL = "http://radius.xinon.at"; +const TT_RADIUS_USERNAME = "admin"; +const TT_RADIUS_PASSWORD = "savemanfb545aw"; /* * Preorder settings diff --git a/public/js/pages/Cpeprovisioning/Cpeprovisioning.js b/public/js/pages/Cpeprovisioning/Cpeprovisioning.js index ac3150a99..306b0cb3b 100644 --- a/public/js/pages/Cpeprovisioning/Cpeprovisioning.js +++ b/public/js/pages/Cpeprovisioning/Cpeprovisioning.js @@ -111,6 +111,7 @@ Vue.component('Cpeprovisioning', { v.checked); }, - createRadiusUser(item) { - const message = { - type: "INITIATE_CREATE_RADIUS_USER", - payload: { - customerName: item.customer, - address: item.owner_full_address, - servicePin: item.spin, - routerMac: item.cpe_data.mac, - customerNumber: item.owner_customer_number - } - }; + async createRadiusUser(item) { + // Disable button during request + this.$set(item, 'isCreatingRadius', true); - if (window.chrome && chrome.runtime && chrome.runtime.sendMessage) { - try { - chrome.runtime.sendMessage(this.extensionId, message, (response) => { - if (chrome.runtime.lastError) { - console.warn("Senden an Erweiterung fehlgeschlagen:", chrome.runtime.lastError.message); - window.notify('warning', 'Daten konnten nicht an die Erweiterung gesendet werden. (Drücke STRG + ALT + E zum Konfigurieren)'); - } else { - console.log("Erweiterung hat geantwortet:", response); - window.notify('success', 'Radius Anlage an Erweiterung übergeben.'); - } - }); - } catch (e) { - console.error("Fehler beim Senden an die Erweiterung:", e); - window.notify('error', 'Fehler beim Senden an die Erweiterung.'); + try { + const { data } = await axios.post(window.TT_CONFIG.CPE_PROV_API_CREATE_RADIUS_USER_URL, { + mac: item.cpe_data.mac + }); + + if (data.success) { + window.notify('success', `RADIUS User erfolgreich angelegt! Kundennr: ${data.data.customer_number}`); + console.log('RADIUS User created:', data.data); + } else { + window.notify('error', data.message || 'Fehler beim Anlegen des RADIUS Users.'); } - } else { - console.warn("Chrome Extension Messaging API nicht verfügbar."); - window.notify('warning', 'Chrome Messaging API nicht gefunden.'); + } catch (error) { + const errorMsg = error.response?.data?.message || 'Ein unerwarteter Fehler ist aufgetreten.'; + window.notify('error', errorMsg); + console.error('Error creating RADIUS user:', error); + } finally { + this.$set(item, 'isCreatingRadius', false); } }, async testAcsVlan(item) {