antes de la etiqueta OBTENER PASSWORD ANTENA

This commit is contained in:
DANYDHSV 2026-02-18 17:29:41 -06:00
parent 1d562cad5a
commit 28f700e64c
14 changed files with 6157 additions and 38 deletions

0
.agent/skills/frontend-design/SKILL.md Normal file → Executable file
View File

View File

@ -1,35 +1 @@
{ {"ipserver":"venus.siip.mx","apitoken":"gvcnIJqXdUjneVSjhl6THLlQcYXJyIFCcwHKVba2bvIrNraanCTb5VeoWuJ0TFZ9","unmsApiToken":"4f5219de-cc5b-413d-b2fb-5133d02f3b26","tokencallbell":"g8thcZkXGd3xBj2g3TtYNYFMH1fuesbJ.b6a940ea7d78cf6c9e42f067b21c8ddf96e9fa2a9e307bfd0c7c7c4d7fa38f79","tokenstripe":"sk_test_51OkG0REFY1WEUtgRH6UxBK5pu80Aq5Iy8EcdPnf0cOWzuVLQTpyLCd7CbPzqMsWMafZOHElCxhEHF7g8boURjWlJ00tBwE0W1M","hostServerFTP":"siip.mx","usernameServerFTP":"siip0001","passServerFTP":"$spGiT,[wa)n","ipPuppeteer":"172.16.5.134","portPuppeteer":"4100","idPaymentAdminCRM":"1180","cashPaymentMethodId":true,"courtesyPaymentMethodId":true,"bankTransferPaymentMethodId":true,"paypalPaymentMethodId":true,"creditCardPaypalPaymentMethodId":true,"creditCardStripePaymentMethodId":true,"stripeSubscriptionCreditCardPaymentMethodId":true,"paypalSubscriptionPaymentMethodId":true,"mercadopagoPaymentMethodId":true,"checkPaymentMethodId":true,"customPaymentMethodId":true,"oxxoPayPaymentMethodId":true,"creditDebitCardPaymentMethodId":true,"notificationTypeText":false,"installersDataWhatsApp":"{\"instaladores\":[{\"id\":1019,\"nombre\":\"Mucio Robledo\",\"whatsapp\":\"4181878106\"},{\"id\":1173,\"nombre\":\"Ángel Arvizu\",\"whatsapp\":\"4181878106\"},{\"id\":1172,\"nombre\":\"Juan Rostro\",\"whatsapp\":\"4181878106\"},{\"id\":1015,\"nombre\":\"Daniel Humberto\",\"whatsapp\":\"4181878106\"},{\"id\":1131,\"nombre\":\"Gricelda Avalos\",\"whatsapp\":\"4181817609\"}]}","debugMode":true,"minioEndpoint":"http://172.16.5.134:9002","minioPublicUrl":"https://aws-venus.siip.mx","minioAccessKey":"minioadmin","minioSecretKey":"minioadmin","minioBucket":"vouchers-oxxo","logging_level":true}
"ipserver": "venus.siip.mx",
"apitoken": "gvcnIJqXdUjneVSjhl6THLlQcYXJyIFCcwHKVba2bvIrNraanCTb5VeoWuJ0TFZ9",
"unmsApiToken": "079c28f5-888c-457d-bd7a-0a4202590f75",
"tokencallbell": "g8thcZkXGd3xBj2g3TtYNYFMH1fuesbJ.b6a940ea7d78cf6c9e42f067b21c8ddf96e9fa2a9e307bfd0c7c7c4d7fa38f79",
"tokenstripe": "sk_test_51OkG0REFY1WEUtgRH6UxBK5pu80Aq5Iy8EcdPnf0cOWzuVLQTpyLCd7CbPzqMsWMafZOHElCxhEHF7g8boURjWlJ00tBwE0W1M",
"hostServerFTP": "siip.mx",
"usernameServerFTP": "siip0001",
"passServerFTP": "$spGiT,[wa)n",
"ipPuppeteer": "172.16.5.134",
"portPuppeteer": "4100",
"idPaymentAdminCRM": "1180",
"cashPaymentMethodId": true,
"courtesyPaymentMethodId": true,
"bankTransferPaymentMethodId": true,
"paypalPaymentMethodId": true,
"creditCardPaypalPaymentMethodId": true,
"creditCardStripePaymentMethodId": true,
"stripeSubscriptionCreditCardPaymentMethodId": true,
"paypalSubscriptionPaymentMethodId": true,
"mercadopagoPaymentMethodId": true,
"checkPaymentMethodId": true,
"customPaymentMethodId": true,
"oxxoPayPaymentMethodId": true,
"creditDebitCardPaymentMethodId": true,
"notificationTypeText": false,
"installersDataWhatsApp": "{\"instaladores\":[{\"id\":1019,\"nombre\":\"Mucio Robledo\",\"whatsapp\":\"4181878106\"},{\"id\":1173,\"nombre\":\"Ángel Arvizu\",\"whatsapp\":\"4181878106\"},{\"id\":1172,\"nombre\":\"Juan Rostro\",\"whatsapp\":\"4181878106\"},{\"id\":1015,\"nombre\":\"Daniel Humberto\",\"whatsapp\":\"4181878106\"},{\"id\":1131,\"nombre\":\"Gricelda Avalos\",\"whatsapp\":\"4181817609\"}]}",
"debugMode": true,
"minioEndpoint": "http:\/\/172.16.5.134:9002",
"minioPublicUrl": "https:\/\/aws-venus.siip.mx",
"minioAccessKey": "minioadmin",
"minioSecretKey": "minioadmin",
"minioBucket": "vouchers-oxxo",
"logging_level": true
}

File diff suppressed because one or more lines are too long

0
img/webp/account-balance.webp Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

0
img/webp/client.webp Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

0
img/webp/crm.webp Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

0
img/webp/edit.webp Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

1
invalid_ids.csv Normal file
View File

@ -0,0 +1 @@
10
1 10

View File

@ -292,7 +292,7 @@ $installersData = json_decode($config['installersDataWhatsApp'] ?? '{"instalador
} }
[data-theme="dark"] { [data-theme="dark"] {
--primary: #60a5fa; --primary: #3b82f6;
--primary-hover: #93c5fd; --primary-hover: #93c5fd;
--bg-body: #0f172a; --bg-body: #0f172a;
--bg-card: #1e293b; --bg-card: #1e293b;
@ -1374,8 +1374,8 @@ $installersData = json_decode($config['installersDataWhatsApp'] ?? '{"instalador
<button id="btnCreateOxxoIntent" class="btn btn-primary" style="width: 100%; justify-content: center; background-color: #E20613; border-color: #E20613; color: white; display: flex; align-items: center; gap: 10px; padding: 12px; border-radius: 8px; font-weight: 600; font-size: 1rem; transition: all 0.2s;"> <button id="btnCreateOxxoIntent" class="btn btn-primary" style="width: 100%; justify-content: center; background-color: #E20613; border-color: #E20613; color: white; display: flex; align-items: center; gap: 10px; padding: 12px; border-radius: 8px; font-weight: 600; font-size: 1rem; transition: all 0.2s;">
Generar Ficha OXXO Pay Generar Ficha OXXO Pay
</button> </button>
<a id="btnOxxoCrm" href="#" target="_blank" class="btn btn-uniform" style="text-align: center; display: block; width: 100%; padding: 10px; border-radius: 8px; background: var(--bg-body); border: 1px solid var(--border); color: var(--text-muted); font-size: 0.9rem;"> <a id="btnOxxoCrm" href="#" target="_blank" class="btn btn-uniform" style="text-align: center; display: flex; justify-content: center; width: 100%; padding: 12px; border-radius: 8px; font-size: 1rem;">
<img src="?action=image&file=crm.webp" class="icon-crm" style="vertical-align: middle; margin-right: 5px; width: 16px; opacity: 0.7;"> Ver en CRM <img src="?action=image&file=crm.webp" class="icon-crm" style="vertical-align: middle; margin-right: 5px; width: 16px;"> Ver en CRM
</a> </a>
</div> </div>
</div> </div>

View File

@ -0,0 +1,674 @@
<?php
$initialDir = getcwd();
chdir(__DIR__ . '/../');
require_once __DIR__ . '/../vendor/autoload.php';
use Ubnt\UcrmPluginSdk\Service\UcrmApi;
use Ubnt\UcrmPluginSdk\Service\PluginConfigManager;
use GuzzleHttp\Client;
// -- Configuration --
$customAttributeKey = 'passwordAntenaCliente';
$siteAttributeKey = 'site';
$antenaSectorialAttributeKey = 'antenaSectorial';
$logFile = __DIR__ . '/audit_passwords.log';
// -- Argument Parsing --
$fixOption = null;
foreach ($argv as $arg) {
if (strpos($arg, '--fix=') === 0) {
$fixOption = substr($arg, 6);
}
}
$fixLimit = 0;
if ($fixOption === 'all') {
$fixLimit = PHP_INT_MAX;
} elseif (is_numeric($fixOption)) {
$fixLimit = (int)$fixOption;
}
// -- Helpers --
function logMessage($message)
{
global $logFile;
$timestamp = date('Y-m-d H:i:s');
file_put_contents($logFile, "[$timestamp] $message\n", FILE_APPEND);
fwrite(STDERR, "[$timestamp] $message\n");
}
function isTestEnvironment($config)
{
$ipServer = $config['ipserver'] ?? '';
return ($ipServer === '172.16.5.134' || $ipServer === 'venus.siip.mx' || $ipServer === 'pruebas.internet.mx');
}
// -- Initialization --
$config = PluginConfigManager::create()->loadConfig();
$ipServer = $config['ipserver'] ?? 'localhost';
$apiUrl = "https://$ipServer/crm/api/v1.0/";
$token = $config['apitoken'] ?? '';
if (empty($token)) {
logMessage("Error: API Token is missing in plugin configuration.");
exit(1);
}
// Initialize UCRM Client
$client = new Client([
'base_uri' => $apiUrl,
'verify' => false,
'headers' => [
'X-Auth-App-Key' => $token,
'Content-Type' => 'application/json',
],
]);
$ucrmApi = new UcrmApi($client, $token);
// Initialize UNMS Client
$unmsClient = new Client([
'base_uri' => "https://{$ipServer}/nms/api/v2.1/",
'verify' => false,
'headers' => [
'X-Auth-Token' => $config['unmsApiToken'] ?? ''
]
]);
function fetchAllClients($client)
{
$allClients = [];
$page = 1;
$limit = 500;
do {
try {
$response = $client->get('clients', [
'query' => [
'limit' => $limit,
'offset' => ($page - 1) * $limit,
'isArchived' => 0,
]
]);
$data = json_decode($response->getBody()->getContents(), true);
if (empty($data)) break;
$allClients = array_merge($allClients, $data);
if (count($data) < $limit) break;
$page++;
} catch (\Exception $e) {
logMessage("Error fetching page $page: " . $e->getMessage());
break;
}
} while (true);
return $allClients;
}
function generateStrongPassword($length = 16)
{
$lower = 'abcdefghijkmnopqrstuvwxyz';
$upper = 'ABCDEFGHJKLMNPQRSTUVWXYZ';
$digits = '23456789';
$symbols = '!@#$%&*()-_=+[]{};:,.<>?';
$all = $lower . $upper . $digits . $symbols;
$pwChars = [];
$pwChars[] = $lower[random_int(0, strlen($lower) - 1)];
$pwChars[] = $upper[random_int(0, strlen($upper) - 1)];
$pwChars[] = $digits[random_int(0, strlen($digits) - 1)];
$pwChars[] = $symbols[random_int(0, strlen($symbols) - 1)];
for ($i = count($pwChars); $i < $length; $i++) {
$pwChars[] = $all[random_int(0, strlen($all) - 1)];
}
shuffle($pwChars);
return implode('', $pwChars);
}
/**
* Fix client password AND extract network data (Site, Antena/Sectorial).
* Returns an associative array with 'password', 'siteName', 'deviceInfo'.
*
* Edge cases handled:
* - Client with tag 'NS REPETIDOR' password = 'Este cliente funciona como Repetidor'
* - Service status Ended(2) or Obsolete(5) password = 'Servicio Finalizado'
* - Device disconnected password = 'Antena desconectada al momento de obtener la contraseña'
*/
function fixClientData($clientId, $ucrmApi, $unmsClient, $attributeIds, $config, $currentPassword)
{
$passwordAttributeId = $attributeIds['password'];
$siteAttributeId = $attributeIds['site'] ?? null;
$antenaSectorialAttributeId = $attributeIds['antenaSectorial'] ?? null;
$result = [
'password' => '',
'siteName' => null,
'deviceInfo' => null,
];
try {
$isTest = isTestEnvironment($config);
// ── Edge Case 1: Check client tags for "NS REPETIDOR" ──
try {
$clientData = $ucrmApi->get("clients/$clientId");
$clientTags = $clientData['tags'] ?? [];
foreach ($clientTags as $tag) {
if (stripos($tag['name'] ?? '', 'NS REPETIDOR') !== false) {
$msg = 'Este cliente funciona como Repetidor';
$result['password'] = $msg;
$result['deviceInfo'] = 'REPETIDOR';
if ($currentPassword !== $msg) {
$attrUpdates = [$passwordAttributeId => $msg];
if ($antenaSectorialAttributeId) {
$attrUpdates[$antenaSectorialAttributeId] = 'REPETIDOR';
}
patchClientAttributes($ucrmApi, $clientId, $attrUpdates);
$result['changed'] = true;
} else {
$result['changed'] = false;
}
logMessage("Client $clientId: Tag NS REPETIDOR detected → skipping");
return $result;
}
}
} catch (\Exception $e) {
logMessage("Warning: Could not fetch client tags for $clientId: " . $e->getMessage());
}
// ── Get Services ──
$svcs = $ucrmApi->get('clients/services', ['clientId' => $clientId]);
if (empty($svcs)) {
$msg = '⚠️ Cliente sin servicios/antenas';
$result['password'] = $msg;
if ($currentPassword !== $msg) {
patchClientAttributes($ucrmApi, $clientId, [
$passwordAttributeId => $msg,
]);
$result['changed'] = true;
} else {
$result['changed'] = false;
}
return $result;
}
$allServicePasswords = [];
$numServices = count($svcs);
$siteName = null;
$deviceInfo = null;
foreach ($svcs as $index => $svc) {
$label = ($numServices > 1) ? "Servicio " . ($index + 1) . ":" : "";
$siteId = $svc['unmsClientSiteId'] ?? null;
$serviceStatus = $svc['status'] ?? null;
$passwordValue = "";
// ── Edge Case 2: Service Ended (2) or Obsolete (5) ──
if ($serviceStatus === 2 || $serviceStatus === 5) {
$passwordValue = 'Servicio Finalizado';
logMessage("Client $clientId: Service " . ($index + 1) . " has status $serviceStatus (Ended/Obsolete)");
$allServicePasswords[] = trim("$label $passwordValue");
continue;
}
if (!$siteId) {
$passwordValue = "⚠️ Sin sitio";
} else {
if ($isTest) {
// Test Env Logic: Preserve existing if valid
$foundInCRM = false;
if (!empty($currentPassword)) {
if ($numServices > 1) {
if (preg_match('/Servicio ' . ($index + 1) . ':\s*([^⚠️\s]+)/', $currentPassword, $matches)) {
$passwordValue = $matches[1];
$foundInCRM = true;
}
} else {
if (strpos($currentPassword, '⚠️') === false && strpos($currentPassword, 'Servicio') === false) {
$passwordValue = trim($currentPassword);
$foundInCRM = true;
}
}
}
if (!$foundInCRM) {
$passwordValue = generateStrongPassword(20);
logMessage("Test Env: Generated new password for Client $clientId (Service $index)");
} else {
logMessage("Test Env: Preserved existing password for Client $clientId");
}
} else {
// Production Logic
try {
$respDev = $unmsClient->get("devices?siteId=$siteId");
$devs = json_decode($respDev->getBody()->getContents(), true);
if (empty($devs)) {
$passwordValue = "⚠️ Sin antena";
} else {
$passVault = null;
$firstDeviceId = null;
// ── Edge Case 3: Check if device is disconnected ──
$firstDev = $devs[0] ?? null;
$deviceStatus = $firstDev['overview']['status'] ?? 'unknown';
if ($deviceStatus === 'disconnected') {
$passwordValue = 'Antena desconectada al momento de obtener la contraseña';
logMessage("Client $clientId: Device is disconnected (siteId=$siteId)");
// Still extract network data even if disconnected
if ($firstDev && $index === 0) {
$siteName = $firstDev['identification']['site']['parent']['name'] ?? null;
$apDeviceName = $firstDev['attributes']['apDevice']['name'] ?? null;
$apDeviceId = $firstDev['attributes']['apDevice']['id'] ?? null;
if ($apDeviceName && $apDeviceId) {
try {
$respApDev = $unmsClient->get("devices/$apDeviceId");
$apDevData = json_decode($respApDev->getBody()->getContents(), true);
$apDeviceIP = $apDevData['ipAddress'] ?? '';
$deviceInfo = trim("$apDeviceName $apDeviceIP");
} catch (\Exception $e) {
$deviceInfo = $apDeviceName;
}
} elseif (!$apDeviceName) {
$deviceInfo = 'REPETIDOR';
}
}
$allServicePasswords[] = trim("$label $passwordValue");
continue; // Skip vault/regenerate for this service
}
// $firstDev already set above (line 258)
if ($firstDev && $index === 0) {
// Site name from parent site
$siteName = $firstDev['identification']['site']['parent']['name'] ?? null;
// AP Device name and IP
$apDeviceName = $firstDev['attributes']['apDevice']['name'] ?? null;
$apDeviceId = $firstDev['attributes']['apDevice']['id'] ?? null;
if ($apDeviceName && $apDeviceId) {
// Fetch AP device IP
try {
$respApDev = $unmsClient->get("devices/$apDeviceId");
$apDevData = json_decode($respApDev->getBody()->getContents(), true);
$apDeviceIP = $apDevData['ipAddress'] ?? '';
$deviceInfo = trim("$apDeviceName $apDeviceIP");
} catch (\Exception $e) {
$deviceInfo = $apDeviceName;
logMessage("Warning: Could not fetch AP device IP for $apDeviceId: " . $e->getMessage());
}
} elseif (!$apDeviceName) {
// No apDevice = possibly a repeater
$deviceInfo = 'REPETIDOR';
logMessage("Client $clientId: Device is a repeater (no apDevice)");
}
}
foreach ($devs as $dev) {
$deviceId = $dev['identification']['id'] ?? null;
if (!$deviceId) continue;
if (!$firstDeviceId) $firstDeviceId = $deviceId;
try {
$respVault = $unmsClient->get("vault/$deviceId/credentials");
$vault = json_decode($respVault->getBody()->getContents(), true);
if (isset($vault['credentials'][0]['password'])) {
$passVault = $vault['credentials'][0]['password'];
break;
}
} catch (\Exception $e) {
continue;
}
}
if ($passVault) {
$passwordValue = $passVault;
} elseif ($firstDeviceId) {
// Regenerate on Device
$newPass = generateStrongPassword(16);
try {
$unmsClient->post("vault/$firstDeviceId/credentials/regenerate", [
'json' => [['username' => 'ubnt', 'password' => $newPass, 'readOnly' => true]]
]);
$passwordValue = $newPass;
logMessage("Prod Env: Regenerated password on UNMS Device $firstDeviceId");
} catch (\Exception $e) {
$passwordValue = "⚠️ Error Regenerating: " . $e->getMessage();
}
} else {
$passwordValue = "⚠️ Sin dispositivo válido";
}
}
} catch (\Exception $e) {
$passwordValue = "⚠️ Error API UNMS: " . $e->getMessage();
}
}
}
$allServicePasswords[] = trim("$label $passwordValue");
}
$finalValue = implode(' ', $allServicePasswords);
$result['password'] = $finalValue;
$result['siteName'] = $siteName;
$result['deviceInfo'] = $deviceInfo;
// Build attribute updates (always patch all available fields)
$attrUpdates = [];
// Password: only if changed
if ($finalValue !== $currentPassword) {
$attrUpdates[$passwordAttributeId] = $finalValue;
}
// Site: patch if we got data and attribute ID exists
if ($siteName !== null && $siteAttributeId) {
$attrUpdates[$siteAttributeId] = $siteName;
}
// Antena/Sectorial: patch if we got data and attribute ID exists
if ($deviceInfo !== null && $antenaSectorialAttributeId) {
$attrUpdates[$antenaSectorialAttributeId] = $deviceInfo;
}
if (!empty($attrUpdates)) {
patchClientAttributes($ucrmApi, $clientId, $attrUpdates);
logMessage("Client $clientId: Patched " . count($attrUpdates) . " attribute(s)");
}
$result['changed'] = ($finalValue !== $currentPassword) || !empty($attrUpdates);
return $result;
} catch (\Exception $e) {
$result['password'] = "Error fixing: " . $e->getMessage();
return $result;
}
}
/**
* Patch multiple client custom attributes in a single API call.
*/
function patchClientAttributes($ucrmApi, $clientId, array $attributeUpdates)
{
$attributes = [];
foreach ($attributeUpdates as $attrId => $value) {
if ($attrId && $value !== null) {
$attributes[] = ['customAttributeId' => (int)$attrId, 'value' => (string)$value];
}
}
if (!empty($attributes)) {
$ucrmApi->patch("clients/$clientId", ['attributes' => $attributes]);
}
}
// 0. Resolve Attribute IDs
$customAttributeId = null;
$siteAttributeId = null;
$antenaSectorialAttributeId = null;
try {
$attributes = $ucrmApi->get('custom-attributes', ['attributeType' => 'client']);
foreach ($attributes as $attr) {
match ($attr['key']) {
$customAttributeKey => $customAttributeId = $attr['id'],
$siteAttributeKey => $siteAttributeId = $attr['id'],
$antenaSectorialAttributeKey => $antenaSectorialAttributeId = $attr['id'],
default => null,
};
}
} catch (\Exception $e) {
logMessage("Error fetching attributes: " . $e->getMessage());
exit(1);
}
if (!$customAttributeId) {
logMessage("Error: Custom attribute '$customAttributeKey' not found.");
exit(1);
}
// Log resolved IDs
logMessage("Resolved attribute IDs: password=$customAttributeId, site=" . ($siteAttributeId ?? 'N/A') . ", antenaSectorial=" . ($antenaSectorialAttributeId ?? 'N/A'));
// Build attribute IDs map for fixClientData
$attributeIds = [
'password' => $customAttributeId,
'site' => $siteAttributeId,
'antenaSectorial' => $antenaSectorialAttributeId,
];
// -- Main Execution --
if ($fixOption === null) {
// Check for --file argument
$fileOption = null;
foreach ($argv as $arg) {
if (strpos($arg, '--file=') === 0) {
$fileOption = substr($arg, 7);
}
}
if ($fileOption) {
if ($fileOption[0] !== '/') {
global $initialDir;
$fileOption = $initialDir . '/' . $fileOption;
}
if (!file_exists($fileOption)) {
logMessage("Error: File not found: $fileOption");
exit(1);
}
logMessage("Processing from file: $fileOption");
// Read all lines
$lines = file($fileOption, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
// Process lines
// We need to loop, process, remove, rewrite.
// Ideally, we process the list in memory and rewrite the file after EACH success to satisfy "removing it as we go".
$originalCount = count($lines);
$processedCount = 0;
// Helper to rewrite file
$rewriteFile = function ($remainingLines) use ($fileOption) {
file_put_contents($fileOption, implode("\n", $remainingLines) . "\n");
};
// We allow a header row if it starts with "clientId" or "id"
// If we remove the header, we lose it for subsequent runs? No, we should probably keep header if present?
// Or just assume data.
// Let's assume the user might have a header.
// If the first line is non-numeric, treat as header?
$header = null;
if (!empty($lines)) {
$firstLine = $lines[0];
$parts = explode(',', $firstLine);
if (!is_numeric($parts[0])) {
$header = array_shift($lines); // Remove header from processing list
}
}
$queue = $lines; // Working copy
while (!empty($queue)) {
$line = array_shift($queue); // Take next
if (empty(trim($line))) continue;
$clientId = trim($line);
if (!is_numeric($clientId)) {
logMessage("Skipping invalid line: $line");
continue;
}
// Process Client
$clientId = (int)$clientId;
logMessage("Processing ID from file: $clientId");
// Use fetchAllClients filtered by ID? No, too slow.
// Directly use fixClientPassword which fetches services inside.
// BUT we need the current password to decide if we fix?
// `fixClientPassword` takes `$currentPassword`.
// We can fetch the specific client first.
try {
$clientDataResponse = $ucrmApi->get("clients/$clientId");
$currentAttributes = $clientDataResponse['attributes'] ?? [];
$currentPass = null;
foreach ($currentAttributes as $attr) {
if ($attr['key'] === $customAttributeKey) {
$currentPass = $attr['value'];
break;
}
}
// Determine if valid (same logic as audit)
// Actually, if it's in the list, we assume it's invalid?
// Or should we re-validate? safer to re-validate slightly to get current state.
$fixResult = fixClientData($clientId, $ucrmApi, $unmsClient, $attributeIds, $config, $currentPass);
$newPass = $fixResult['password'];
$siteInfo = $fixResult['siteName'] ? " | Site: {$fixResult['siteName']}" : '';
$devInfo = $fixResult['deviceInfo'] ? " | Antena: {$fixResult['deviceInfo']}" : '';
logMessage("Client $clientId result: $newPass$siteInfo$devInfo");
} catch (\Exception $e) {
logMessage("Error processing Client $clientId: " . $e->getMessage());
// Do we remove it if error?
// User said "que cada que procese un id lo quite".
// Usually specific errors might warrant keeping, but if we want to "resume", maybe we remove logic?
// Let's assuming "Processed" means attempted.
}
// Rewrite file
// Content = Header (if any) + Remaining Queue
$validLines = $queue;
if ($header) {
array_unshift($validLines, $header);
}
$rewriteFile($validLines);
// Sleep slightly to avoid hammering?
// usleep(100000);
}
logMessage("File processing complete.");
exit(0);
}
}
if ($fixLimit > 0) {
logMessage("Fix mode enabled. Limit: " . ($fixOption === 'all' ? 'ALL' : $fixLimit));
}
// 1. Fetch all clients
logMessage("Fetching clients...");
$clients = fetchAllClients($client);
logMessage("Total active clients fetched: " . count($clients));
// 2. Filter & Fix
$results = [];
$fixedCount = 0;
foreach ($clients as $clientData) {
$password = null;
$attributes = $clientData['attributes'] ?? [];
foreach ($attributes as $attr) {
if ($attr['key'] === $customAttributeKey) {
$password = $attr['value'];
break;
}
}
// Validation Logic
$isValid = true;
$reason = "";
// Strict validation for "invalid" detection to trigger fix,
// BUT we need to be careful not to flag "valid" generated strings (like "Servicio 1: ...") as invalid if we just want to audit "missing".
// Facade generates complex strings. If we flag them all as invalid because of spaces, we will loop forever fixing them (if generating same structure).
// Logic: If it contains "Servicio" or "⚠️", it is "Managed".
// Only flag if Empty OR (Length check fail AND Not Managed).
// Detect explicit error states that should be retried (e.g. "⚠️ Error Regenerating...")
$isError = (stripos((string)$password, 'Error') !== false);
$isManaged = (strpos((string)$password, 'Servicio') !== false || strpos((string)$password, '⚠️') !== false);
if (empty($password)) {
$isValid = false;
$reason = "Missing";
} elseif ($isError) {
$isValid = false;
$reason = "Error (Retry)";
} elseif (!$isManaged) {
$len = strlen($password);
if ($len < 8 || $len > 20) {
$isValid = false;
$reason = "Length ($len)";
} elseif (strpos($password, ' ') !== false) {
$isValid = false;
$reason = "Spaces";
}
}
if (!$isValid) {
$newPassword = "";
$actionTaken = "None";
// Fix Logic
if ($fixLimit > 0 && $fixedCount < $fixLimit) {
logMessage("Fixing client {$clientData['id']}...");
$fixResult = fixClientData($clientData['id'], $ucrmApi, $unmsClient, $attributeIds, $config, $password);
$newPassword = $fixResult['password'];
// Basic check if fix resulted in a change/value
if ($fixResult['changed'] ?? false) {
$actionTaken = "Updated";
$fixedCount++;
} else {
$actionTaken = "Attempted (No Change/Error)";
}
}
$results[] = [
'clientId' => $clientData['id'],
'name' => ($clientData['firstName'] ?? '') . ' ' . ($clientData['lastName'] ?? ''),
'userIdent' => $clientData['userIdent'] ?? '',
'currentPassword' => $password ?? '',
'action' => $actionTaken,
'newPassword' => $newPassword
];
}
}
// 3. Output CSV
logMessage("Found " . count($results) . " clients with invalid passwords.");
if ($fixLimit > 0) {
logMessage("Fixed $fixedCount clients.");
}
echo "clientId,name,userIdent,currentPassword,action,newPassword\n";
foreach ($results as $row) {
echo sprintf(
"%s,\"%s\",\"%s\",\"%s\",\"%s\",\"%s\"\n",
$row['clientId'],
str_replace('"', '""', $row['name']),
str_replace('"', '""', $row['userIdent']),
str_replace('"', '""', $row['currentPassword']),
$row['action'],
str_replace('"', '""', $row['newPassword'])
);
}
logMessage("Done.");

View File

@ -0,0 +1,63 @@
<?php
$initialDir = getcwd();
chdir(__DIR__ . '/../');
require_once __DIR__ . '/../vendor/autoload.php';
use Ubnt\UcrmPluginSdk\Service\UcrmApi;
use Ubnt\UcrmPluginSdk\Service\PluginConfigManager;
use GuzzleHttp\Client;
$config = PluginConfigManager::create()->loadConfig();
$ipServer = $config['ipserver'] ?? 'localhost';
$apiUrl = "https://$ipServer/crm/api/v1.0/";
$token = $config['apitoken'] ?? '';
$client = new Client([
'base_uri' => $apiUrl,
'verify' => false,
'headers' => [
'X-Auth-App-Key' => $token,
'Content-Type' => 'application/json',
],
]);
$ucrmApi = new UcrmApi($client, $token);
$ids = [18, 20];
$customAttributeKey = 'passwordAntenaCliente';
foreach ($ids as $id) {
try {
$data = $ucrmApi->get("clients/$id");
echo "Client ID: $id\n";
echo "IsArchived: " . ($data['isArchived'] ? 'Yes' : 'No') . "\n";
echo "All Attributes:\n";
foreach ($data['attributes'] as $attr) {
echo " - {$attr['key']} (ID: {$attr['customAttributeId']}): [{$attr['value']}]\n";
}
flush();
$passVal = null;
$foundKey = false;
foreach ($data['attributes'] as $attr) {
if ($attr['key'] === $customAttributeKey) {
$passVal = $attr['value'];
$foundKey = true;
break;
}
}
if ($foundKey) {
echo "Attribute '$customAttributeKey' FOUND.\n";
echo "Value: [" . $passVal . "]\n";
echo "Type: " . gettype($passVal) . "\n";
echo "Empty? " . (empty($passVal) ? 'Yes' : 'No') . "\n";
} else {
echo "Attribute '$customAttributeKey' NOT FOUND in attributes list.\n";
}
echo "--------------------------------------\n";
} catch (\Exception $e) {
echo "Error fetching $id: " . $e->getMessage() . "\n";
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,166 @@
<?php
$initialDir = getcwd();
chdir(__DIR__ . '/../');
require_once __DIR__ . '/../vendor/autoload.php';
use Ubnt\UcrmPluginSdk\Service\UcrmApi;
use Ubnt\UcrmPluginSdk\Service\PluginConfigManager;
use GuzzleHttp\Client;
// -- Configuration --
$customAttributeKey = 'passwordAntenaCliente';
// -- Initialization --
$config = PluginConfigManager::create()->loadConfig();
$ipServer = $config['ipserver'] ?? 'localhost';
$apiUrl = "https://$ipServer/crm/api/v1.0/";
$token = $config['apitoken'] ?? '';
if (empty($token)) {
fwrite(STDERR, "Error: API Token is missing in plugin configuration.\n");
exit(1);
}
// Initialize UCRM Client
$client = new Client([
'base_uri' => $apiUrl,
'verify' => false,
'headers' => [
'X-Auth-App-Key' => $token,
'Content-Type' => 'application/json',
],
]);
$ucrmApi = new UcrmApi($client, $token);
function fetchAllClients($client)
{
$allClients = [];
$page = 1;
$limit = 500;
do {
try {
$response = $client->get('clients', [
'query' => [
'limit' => $limit,
'offset' => ($page - 1) * $limit,
'isArchived' => 0,
]
]);
$data = json_decode($response->getBody()->getContents(), true);
if (empty($data)) break;
$allClients = array_merge($allClients, $data);
if (count($data) < $limit) break;
$page++;
} catch (\Exception $e) {
fwrite(STDERR, "Error fetching page $page: " . $e->getMessage() . "\n");
break;
}
} while (true);
return $allClients;
}
// 0. Resolve Attribute ID
$customAttributeId = null;
try {
$attributes = $ucrmApi->get('custom-attributes', ['attributeType' => 'client']);
foreach ($attributes as $attr) {
if ($attr['key'] === $customAttributeKey) {
$customAttributeId = $attr['id'];
break;
}
}
} catch (\Exception $e) {
fwrite(STDERR, "Error fetching attributes: " . $e->getMessage() . "\n");
exit(1);
}
if (!$customAttributeId) {
fwrite(STDERR, "Error: Custom attribute '$customAttributeKey' not found.\n");
exit(1);
}
// 1. Fetch all clients
fwrite(STDERR, "Fetching clients...\n");
$clients = fetchAllClients($client);
fwrite(STDERR, "Total active clients fetched: " . count($clients) . "\n");
// 2. Identify Invalid
$results = [];
foreach ($clients as $clientData) {
$password = null;
$attributes = $clientData['attributes'] ?? [];
foreach ($attributes as $attr) {
if ($attr['key'] === $customAttributeKey) {
$password = $attr['value'];
break;
}
}
// Detect explicit error states that should be retried (e.g. "⚠️ Error Regenerating...")
$isError = (stripos((string)$password, 'Error') !== false);
// Detect managed states (valid non-passwords like "⚠️ Sin sitio", "Servicio Finalizado", etc.)
$isManaged = (strpos((string)$password, 'Servicio') !== false || strpos((string)$password, '⚠️') !== false);
$isValid = true;
if (empty($password)) {
$isValid = false;
} elseif ($isError) {
$isValid = false;
} elseif (!$isManaged) {
$len = strlen($password);
if ($len < 8 || $len > 20) {
$isValid = false;
} elseif (strpos($password, ' ') !== false) {
$isValid = false;
}
}
if (!$isValid) {
$results[] = [
'id' => $clientData['id'],
'name' => ($clientData['firstName'] ?? '') . ' ' . ($clientData['lastName'] ?? '')
];
}
}
// 3. Output CSV
// 3. Output CSV
$outputFile = null;
foreach ($argv as $arg) {
if (strpos($arg, '--output=') === 0) {
$outputFile = substr($arg, 9);
}
}
if ($outputFile) {
// Resolve relative path to initial execution directory
if ($outputFile[0] !== '/') {
global $initialDir;
$outputFile = $initialDir . '/' . $outputFile;
}
$outputHandle = fopen($outputFile, 'w');
fwrite(STDERR, "Writing to file: $outputFile\n");
} else {
$outputHandle = STDOUT;
}
foreach ($results as $row) {
fwrite($outputHandle, $row['id'] . "\n");
}
if ($outputFile) {
fclose($outputHandle);
fwrite(STDERR, "Found " . count($results) . " invalid clients written to $outputFile.\n");
} else {
fwrite(STDERR, "Found " . count($results) . " invalid clients.\n");
}

View File

@ -0,0 +1 @@
10
1 10