Implement RADIUS user creation and update functionality in CPE provisioning

This commit is contained in:
2025-11-18 08:25:26 +01:00
parent b408f7fcf2
commit 9e56b8ee98
3 changed files with 316 additions and 34 deletions

View File

@@ -530,6 +530,7 @@ class CpeprovisioningController extends mfBaseController {
"CPE_PROV_API_GET_URL" => $this->getUrl("Cpeprovisioning", "apiGet"), "CPE_PROV_API_GET_URL" => $this->getUrl("Cpeprovisioning", "apiGet"),
"CPE_PROV_API_SAVE_URL" => $this->getUrl("Cpeprovisioning", "apiSave"), "CPE_PROV_API_SAVE_URL" => $this->getUrl("Cpeprovisioning", "apiSave"),
"CPE_PROV_API_TEST_ACS_VLAN_URL" => $this->getUrl("Cpeprovisioning", "getAcsVlan"), "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"), "CPE_PROV_PRINT_PDF_URL" => $this->getUrl("Cpeprovisioning", "printPDF"),
"ORDER_URL" => $this->getUrl("Order"), "ORDER_URL" => $this->getUrl("Order"),
"NETWORKS" => NetworkModel::getAll(), "NETWORKS" => NetworkModel::getAll(),
@@ -598,13 +599,17 @@ class CpeprovisioningController extends mfBaseController {
$attrs = $product->product->attributes; $attrs = $product->product->attributes;
$vlanPublicDefault = $term->getPop()->vlan_public ?? $attrs['vlan_default_public']->value ?? null; // First, check if any VLAN is explicitly saved (checked in frontend)
$vlanNatDefault = $term->getPop()->vlan_nat ?? $attrs['vlan_default_nat']->value ?? null; // The saved values take priority over defaults
$vlanIpv6Default = $term->getPop()->vlan_ipv6 ?? $attrs['vlan_default_ipv6']->value ?? null; $assignedVlan = $cpe->vlan_public ?? $cpe->vlan_nat ?? $cpe->vlan_ipv6;
// For the test, we just return the first available VLAN that would be assigned. // If no VLAN is explicitly saved, fall back to defaults
// The logic can be expanded if a specific type is requested. if (!$assignedVlan) {
$assignedVlan = $vlanPublicDefault ?? $vlanNatDefault ?? $vlanIpv6Default; $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) { if ($assignedVlan) {
self::returnJson(['success' => true, 'vlan_id' => $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) { private function fixCpeData($data) {
if (!$data) return []; if (!$data) return [];
$data->shipping = (bool)$data->shipping; $data->shipping = (bool)$data->shipping;

View File

@@ -162,6 +162,14 @@ const TT_IBAN_VALIDATOR_BASEURL = "https://rest.sepatools.eu/validate_iban/";
const TT_IBAN_VALIDATOR_USER = ""; const TT_IBAN_VALIDATOR_USER = "";
const TT_IBAN_VALIDATOR_PASS = ""; 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 * Preorder settings

View File

@@ -111,6 +111,7 @@ Vue.component('Cpeprovisioning', {
<tt-button text="In Radius anlegen" <tt-button text="In Radius anlegen"
@click="createRadiusUser(item)" @click="createRadiusUser(item)"
:disabled="!isValidMac(item.cpe_data.mac)" :disabled="!isValidMac(item.cpe_data.mac)"
:loading="item.isCreatingRadius"
sm sm
additional-class="btn-primary" /> additional-class="btn-primary" />
<tt-button text="ACS Auto VLAN Zuweisung testen" <tt-button text="ACS Auto VLAN Zuweisung testen"
@@ -244,6 +245,7 @@ Vue.component('Cpeprovisioning', {
...item, ...item,
isDirty: false, isDirty: false,
isSaving: false, isSaving: false,
isCreatingRadius: false,
pop_name: item.pop_name || 'N/A', pop_name: item.pop_name || 'N/A',
owner_address: `${item.owner_street || ''} ${item.owner_housenumber || ''}, ${item.owner_zip || ''} ${item.owner_city || ''}`, owner_address: `${item.owner_street || ''} ${item.owner_housenumber || ''}, ${item.owner_zip || ''} ${item.owner_city || ''}`,
owner_phone: item.owner_phone || '', owner_phone: item.owner_phone || '',
@@ -296,36 +298,27 @@ Vue.component('Cpeprovisioning', {
isVlanSelected(item) { isVlanSelected(item) {
return item.vlans && Object.values(item.vlans).some(v => v.checked); return item.vlans && Object.values(item.vlans).some(v => v.checked);
}, },
createRadiusUser(item) { async createRadiusUser(item) {
const message = { // Disable button during request
type: "INITIATE_CREATE_RADIUS_USER", this.$set(item, 'isCreatingRadius', true);
payload: {
customerName: item.customer,
address: item.owner_full_address,
servicePin: item.spin,
routerMac: item.cpe_data.mac,
customerNumber: item.owner_customer_number
}
};
if (window.chrome && chrome.runtime && chrome.runtime.sendMessage) { try {
try { const { data } = await axios.post(window.TT_CONFIG.CPE_PROV_API_CREATE_RADIUS_USER_URL, {
chrome.runtime.sendMessage(this.extensionId, message, (response) => { mac: item.cpe_data.mac
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)'); if (data.success) {
} else { window.notify('success', `RADIUS User erfolgreich angelegt! Kundennr: ${data.data.customer_number}`);
console.log("Erweiterung hat geantwortet:", response); console.log('RADIUS User created:', data.data);
window.notify('success', 'Radius Anlage an Erweiterung übergeben.'); } else {
} window.notify('error', data.message || 'Fehler beim Anlegen des RADIUS Users.');
});
} catch (e) {
console.error("Fehler beim Senden an die Erweiterung:", e);
window.notify('error', 'Fehler beim Senden an die Erweiterung.');
} }
} else { } catch (error) {
console.warn("Chrome Extension Messaging API nicht verfügbar."); const errorMsg = error.response?.data?.message || 'Ein unerwarteter Fehler ist aufgetreten.';
window.notify('warning', 'Chrome Messaging API nicht gefunden.'); window.notify('error', errorMsg);
console.error('Error creating RADIUS user:', error);
} finally {
this.$set(item, 'isCreatingRadius', false);
} }
}, },
async testAcsVlan(item) { async testAcsVlan(item) {