From c96af67ac96c53a72b2970a831045409a5cf066f Mon Sep 17 00:00:00 2001 From: sorcier Date: Wed, 11 Nov 2026 19:31:32 +0100 Subject: [PATCH] =?UTF-8?q?fix(ucp,ha):=20auto-d=C3=A9ploiement=20UCP,=20c?= =?UTF-8?q?heck=20SQLite=20PDO/SQLite3::backup=20HA,=20refresh=20comptes?= =?UTF-8?q?=20UCP=20(v2.5.1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/api.php | 31 ++++++-- public/assets/app.js | 4 +- scripts/nexus-firewall | 11 ++- scripts/update.sh | 4 +- src/ClusterManager.php | 157 +++++++++++++++++++++++++++------------- src/FirewallManager.php | 52 ++++++++++++- src/UcpAuth.php | 1 + ucp/api.php | 1 + ucp/assets/ucp.js | 1 + ucp/index.php | 1 + version.json | 14 ++-- 11 files changed, 201 insertions(+), 76 deletions(-) diff --git a/public/api.php b/public/api.php index d259945..7a24031 100644 --- a/public/api.php +++ b/public/api.php @@ -133,6 +133,9 @@ try { switch ($action) { case 'get_state': + try { + FirewallManager::ensureUcpDeployed(); + } catch (\Throwable) {} $settings = $db->query('SELECT * FROM settings WHERE id = 1')->fetch(); $extensions = $db->query('SELECT * FROM extensions ORDER BY CAST(extension AS INTEGER) ASC, extension ASC')->fetchAll(); $groups = $db->query('SELECT * FROM groups ORDER BY CAST(extension AS INTEGER) ASC, extension ASC')->fetchAll(); @@ -1920,6 +1923,11 @@ try { $ami = AmiClient::fromDatabase($db); $reloadInfo = $ami->reloadPjsipAndDialplan(); + // S'assurer que le VHost UCP & Fail2ban UCP sont déployés même après un git pull manuel + try { + FirewallManager::ensureUcpDeployed(); + } catch (\Throwable) {} + // Actualisation de l'horodatage de modification en base pour la réconciliation cluster try { $db->exec("UPDATE cluster_settings SET updated_at = CURRENT_TIMESTAMP WHERE id = 1"); @@ -2454,18 +2462,24 @@ try { break; case 'get_ucp_users': + try { + FirewallManager::ensureUcpDeployed(); + } catch (\Throwable) {} + $users = UcpManager::listUsers($db); echo json_encode([ - 'success' => true, - 'users' => UcpManager::listUsers($db), + 'success' => true, + 'users' => $users, + 'ucp_users' => $users, ]); break; case 'save_ucp_user': $users = UcpManager::saveUser($db, $payload); echo json_encode([ - 'success' => true, - 'users' => $users, - 'message' => 'Compte utilisateur UCP enregistré avec succès.', + 'success' => true, + 'users' => $users, + 'ucp_users' => $users, + 'message' => 'Compte utilisateur UCP enregistré avec succès.', ]); break; @@ -2473,9 +2487,10 @@ try { $uid = (int)($payload['id'] ?? 0); $users = UcpManager::deleteUser($db, $uid); echo json_encode([ - 'success' => true, - 'users' => $users, - 'message' => 'Compte utilisateur UCP supprimé.', + 'success' => true, + 'users' => $users, + 'ucp_users' => $users, + 'message' => 'Compte utilisateur UCP supprimé.', ]); break; diff --git a/public/assets/app.js b/public/assets/app.js index 5ead48f..b3bef85 100644 --- a/public/assets/app.js +++ b/public/assets/app.js @@ -7017,7 +7017,7 @@ async function saveUcpUser(event) { try { const res = await apiCall('save_ucp_user', payload); - appState.ucp_users = res.ucp_users || []; + appState.ucp_users = res.ucp_users || res.users || []; renderUcpTab(); const ucpBadge = document.getElementById('nav-badge-ucp'); if (ucpBadge) ucpBadge.textContent = appState.ucp_users.length; @@ -7036,7 +7036,7 @@ async function deleteUcpUser(userId, username) { } try { const res = await apiCall('delete_ucp_user', { id: Number(userId) }); - appState.ucp_users = res.ucp_users || []; + appState.ucp_users = res.ucp_users || res.users || []; renderUcpTab(); const ucpBadge = document.getElementById('nav-badge-ucp'); if (ucpBadge) ucpBadge.textContent = appState.ucp_users.length; diff --git a/scripts/nexus-firewall b/scripts/nexus-firewall index 1a119a5..b119030 100755 --- a/scripts/nexus-firewall +++ b/scripts/nexus-firewall @@ -74,8 +74,17 @@ switch ($cmd) { echo ($res['success'] ? "\033[32m[OK]\033[0m " : "\033[31m[ERREUR]\033[0m ") . $res['message'] . "\n"; break; + case 'init-ucp': + case 'sync-ucp': + case 'apply': + $settings = FirewallManager::getSettings(); + $res = FirewallManager::saveSettings($settings); + @unlink('/tmp/nexus_fw_status_cache.json'); + echo ($res['success'] ? "\033[32m[OK]\033[0m " : "\033[31m[ERREUR]\033[0m ") . ($res['message'] ?? '') . "\n"; + break; + default: - echo "Usage: nexus-firewall [status|on|off|ban |unban ]\n"; + echo "Usage: nexus-firewall [status|on|off|init-ucp|ban |unban ]\n"; exit(1); } diff --git a/scripts/update.sh b/scripts/update.sh index 4ed4ca9..421c388 100755 --- a/scripts/update.sh +++ b/scripts/update.sh @@ -158,9 +158,9 @@ fi # 3. Vérification des dépendances Debian echo -e "${C_GRAY}[3/6] Vérification des composants et outils système...${C_RESET}" -which ffmpeg >/dev/null 2>&1 && which rsync >/dev/null 2>&1 && which sox >/dev/null 2>&1 && which sshpass >/dev/null 2>&1 && which keepalived >/dev/null 2>&1 && which arping >/dev/null 2>&1 || { +which sqlite3 >/dev/null 2>&1 && which ffmpeg >/dev/null 2>&1 && which rsync >/dev/null 2>&1 && which sox >/dev/null 2>&1 && which sshpass >/dev/null 2>&1 && which keepalived >/dev/null 2>&1 && which arping >/dev/null 2>&1 || { echo -e " -> Installation de paquets complémentaires..." - (apt-get update -y -qq && apt-get install -y -qq --no-install-recommends acl ffmpeg rsync sox sshpass keepalived iputils-arping) 2>/dev/null || true + (apt-get update -y -qq && apt-get install -y -qq --no-install-recommends sqlite3 acl ffmpeg rsync sox sshpass keepalived iputils-arping) 2>/dev/null || true } # 4. Exécution des migrations SQLite, régénération Dialplan, Nginx & Fail2ban UCP diff --git a/src/ClusterManager.php b/src/ClusterManager.php index a0fb721..3af6e5b 100644 --- a/src/ClusterManager.php +++ b/src/ClusterManager.php @@ -495,8 +495,10 @@ class ClusterManager throw new RuntimeException("Fichier de base de données introuvable : {$dbPath}"); } + $uniq = getmypid() . '_' . bin2hex(random_bytes(4)); + // Snapshot 100% atomique de la base NexusPBX via VACUUM INTO / .backup officiel SQLite - $tmpDbDump = '/tmp/nexus_cluster_db_' . time() . '.sqlite'; + $tmpDbDump = '/tmp/nexus_cluster_db_' . $uniq . '.sqlite'; @unlink($tmpDbDump); try { $this->db->exec("VACUUM INTO " . $this->db->quote($tmpDbDump)); @@ -509,26 +511,44 @@ class ClusterManager @copy($dbPath, $tmpDbDump); } - $dbIntegrity = trim(shell_exec("sqlite3 " . escapeshellarg($tmpDbDump) . " 'PRAGMA integrity_check;' 2>/dev/null") ?? ''); + $dbIntegrity = $this->checkSqliteIntegrity($tmpDbDump); if ($dbIntegrity !== 'ok') { @unlink($tmpDbDump); throw new RuntimeException("Échec de vérification d'intégrité de la base locale avant réplication : {$dbIntegrity}"); } - $tmpArchive = '/tmp/nexus_cluster_sync_' . time() . '.tar.gz'; - $tmpAstdb = '/tmp/astdb_cluster_sync.sqlite3'; + $tmpArchive = '/tmp/nexus_cluster_sync_' . $uniq . '.tar.gz'; + $tmpAstdb = '/tmp/astdb_cluster_sync_' . $uniq . '.sqlite3'; $hasAstdb = false; // Sauvegarde intègre et atomique de la base de contacts Asterisk (astdb.sqlite3) $astdbPath = '/var/lib/asterisk/astdb.sqlite3'; if (file_exists($astdbPath) && is_readable($astdbPath)) { @unlink($tmpAstdb); - shell_exec("sqlite3 " . escapeshellarg($astdbPath) . " \".backup " . escapeshellarg($tmpAstdb) . "\" 2>/dev/null || cp -f " . escapeshellarg($astdbPath) . " " . escapeshellarg($tmpAstdb) . " 2>/dev/null || true"); + try { + if (class_exists(\SQLite3::class)) { + $srcAst = new \SQLite3($astdbPath, SQLITE3_OPEN_READONLY); + $dstAst = new \SQLite3($tmpAstdb); + $srcAst->backup($dstAst); + $srcAst->close(); + $dstAst->close(); + } else { + shell_exec("sqlite3 " . escapeshellarg($astdbPath) . " \".backup " . escapeshellarg($tmpAstdb) . "\" 2>/dev/null || cp -f " . escapeshellarg($astdbPath) . " " . escapeshellarg($tmpAstdb) . " 2>/dev/null || true"); + } + } catch (\Throwable) { + @copy($astdbPath, $tmpAstdb); + } if (file_exists($tmpAstdb) && filesize($tmpAstdb) > 0) { - $integrity = trim(shell_exec("sqlite3 " . escapeshellarg($tmpAstdb) . " 'PRAGMA integrity_check;' 2>/dev/null") ?? ''); + $integrity = $this->checkSqliteIntegrity($tmpAstdb); if ($integrity === 'ok') { // Purger les souscriptions persistantes PJSIP de l'archive (incompatibles entre instances Asterisk) - shell_exec("sqlite3 " . escapeshellarg($tmpAstdb) . " \"DELETE FROM astdb WHERE key LIKE '/subscription_persistence/%';\" 2>/dev/null || true"); + try { + $astPdo = new PDO('sqlite:' . $tmpAstdb); + $astPdo->exec("DELETE FROM astdb WHERE key LIKE '/subscription_persistence/%'"); + $astPdo = null; + } catch (\Throwable) { + shell_exec("sqlite3 " . escapeshellarg($tmpAstdb) . " \"DELETE FROM astdb WHERE key LIKE '/subscription_persistence/%';\" 2>/dev/null || true"); + } $hasAstdb = true; } else { @unlink($tmpAstdb); @@ -537,7 +557,7 @@ class ClusterManager } // Préparation du dossier temporaire de tarage pour isoler database.sqlite - $tmpDbDir = '/tmp/nexus_cluster_tar_' . time(); + $tmpDbDir = '/tmp/nexus_cluster_tar_' . $uniq; @mkdir($tmpDbDir, 0755, true); rename($tmpDbDump, $tmpDbDir . '/database.sqlite'); @@ -552,9 +572,9 @@ class ClusterManager $cmd .= " -C /var/www/nexus-pbx/public avatars"; } if ($hasAstdb) { - $cmd .= " -C /tmp astdb_cluster_sync.sqlite3"; + $cmd .= " -C /tmp " . escapeshellarg(basename($tmpAstdb)); } - shell_exec($cmd); + exec($cmd . " 2>&1", $tarOut, $tarCode); @unlink($tmpDbDir . '/database.sqlite'); @rmdir($tmpDbDir); @@ -562,9 +582,9 @@ class ClusterManager @unlink($tmpAstdb); } - if (!file_exists($tmpArchive) || filesize($tmpArchive) === 0) { + if (!file_exists($tmpArchive) || filesize($tmpArchive) === 0 || $tarCode >= 2) { @unlink($tmpArchive); - throw new RuntimeException("Échec de génération de l'archive de réplication."); + throw new RuntimeException("Échec de génération de l'archive de réplication (tar code {$tarCode})."); } $archiveData = file_get_contents($tmpArchive); @@ -632,6 +652,9 @@ class ClusterManager $resJson = json_decode((string)$response, true); $detail = !empty($resJson['message']) ? " - " . $resJson['message'] : ($err ? " : {$err}" : ""); $lastError = "Échec d'envoi vers {$targetUrl} (HTTP {$httpCode}){$detail}"; + if (in_array($httpCode, [400, 401, 403, 422], true) && !empty($resJson['message'])) { + break; + } } } @@ -671,27 +694,27 @@ class ClusterManager throw new RuntimeException("Paquet de réplication vide ou corrompu."); } + $uniqRecv = getmypid() . '_' . bin2hex(random_bytes(4)); $archiveRaw = base64_decode($data['archive_b64']); - $tmpArchive = '/tmp/nexus_cluster_recv_' . time() . '.tar.gz'; + $tmpArchive = '/tmp/nexus_cluster_recv_' . $uniqRecv . '.tar.gz'; file_put_contents($tmpArchive, $archiveRaw); // Extraction sécurisée dans un dossier temporaire pour validation préalable - $tmpExtractDir = '/tmp/nexus_cluster_extract_' . time(); + $tmpExtractDir = '/tmp/nexus_cluster_extract_' . $uniqRecv; @mkdir($tmpExtractDir, 0755, true); shell_exec("tar -xzf " . escapeshellarg($tmpArchive) . " -C " . escapeshellarg($tmpExtractDir) . " 2>/dev/null || true"); $extractedDb = $tmpExtractDir . '/database.sqlite'; if (!file_exists($extractedDb) || filesize($extractedDb) === 0) { @unlink($tmpArchive); - @rmdir($tmpExtractDir); + shell_exec('rm -rf ' . escapeshellarg($tmpExtractDir) . ' 2>/dev/null || true'); throw new RuntimeException("L'archive de réplication reçue ne contient pas de base de données valide."); } // Contrôle d'intégrité SQLite strict du fichier reçu AVANT toute modification du système local - $check = trim(shell_exec("sqlite3 " . escapeshellarg($extractedDb) . " 'PRAGMA integrity_check;' 2>/dev/null") ?? ''); + $check = $this->checkSqliteIntegrity($extractedDb); if ($check !== 'ok') { - @unlink($extractedDb); - @rmdir($tmpExtractDir); + shell_exec('rm -rf ' . escapeshellarg($tmpExtractDir) . ' 2>/dev/null || true'); @unlink($tmpArchive); throw new RuntimeException("La base de données reçue est corrompue ({$check}) : rejet strict de l'application."); } @@ -709,15 +732,11 @@ class ClusterManager $localSettingsRow = $this->db->query("SELECT local_net, local_networks_json, admin_https_port FROM settings WHERE id = 1")->fetch(PDO::FETCH_ASSOC); } catch (\Throwable $e) {} - // Restauration officielle en ligne via l'API SQLite .restore + // Restauration officielle en ligne via l'API SQLite (SQLite3::backup ou sqlite3 .restore) // Ne détruit aucun descripteur de fichier, ne touche pas aux journaux WAL/SHM, // et n'invalide aucune lecture concurrente dans PHP-FPM. if (file_exists($dbPath)) { - $restoreCmd = "sqlite3 " . escapeshellarg($dbPath) . " \".restore " . escapeshellarg($extractedDb) . "\" 2>&1"; - $restoreOut = shell_exec($restoreCmd); - if (!empty($restoreOut)) { - error_log("[CLUSTER] Avertissement sqlite3 .restore : " . trim($restoreOut)); - } + $this->restoreSqliteDatabase($extractedDb, $dbPath); } else { @copy($extractedDb, $dbPath); @chmod($dbPath, 0660); @@ -728,11 +747,13 @@ class ClusterManager @unlink($extractedDb); // Contrôle d'intégrité immédiat après synchronisation - $postCheck = trim(shell_exec("sqlite3 " . escapeshellarg($dbPath) . " 'PRAGMA integrity_check;' 2>/dev/null") ?? ''); + $postCheck = $this->checkSqliteIntegrity($dbPath); if ($postCheck !== 'ok') { if (file_exists($dbPath . '.pre_cluster_sync.bak')) { - shell_exec("sqlite3 " . escapeshellarg($dbPath) . " \".restore " . escapeshellarg($dbPath . '.pre_cluster_sync.bak') . "\" 2>/dev/null || true"); + $this->restoreSqliteDatabase($dbPath . '.pre_cluster_sync.bak', $dbPath); } + shell_exec('rm -rf ' . escapeshellarg($tmpExtractDir) . ' 2>/dev/null || true'); + @unlink($tmpArchive); throw new RuntimeException("Échec de vérification d'intégrité après synchronisation ({$postCheck}). Restauration préventive effectuée."); } @@ -762,7 +783,13 @@ class ClusterManager } // Restauration de l'astdb (contacts et IP actives des téléphones PJSIP) via le helper root - $extractedAstdb = $tmpExtractDir . '/astdb_cluster_sync.sqlite3'; + $extractedAstdb = $tmpExtractDir . '/astdb_cluster_sync_' . $uniqRecv . '.sqlite3'; + if (!file_exists($extractedAstdb)) { + $candidates = glob($tmpExtractDir . '/astdb_cluster_sync*.sqlite3') ?: []; + if (!empty($candidates)) { + $extractedAstdb = $candidates[0]; + } + } if (file_exists($extractedAstdb)) { shell_exec('sudo -n ' . escapeshellcmd(self::CLUSTER_HELPER_PATH) . ' restore-astdb ' . escapeshellarg($extractedAstdb) . ' 2>&1'); @unlink($extractedAstdb); @@ -825,29 +852,11 @@ class ClusterManager // Synchroniser la configuration Nginx/Fail2ban locale (dont ports et jail UCP répliqués) try { - $fw = FirewallManager::getSettings(); - FirewallManager::ensureUcpFail2banArtifacts(); - FirewallManager::applyNginxConfig( - (int)($fw['admin_https_port'] ?? 8443), - (int)($fw['certbot_strict_port80'] ?? 0), - (int)($fw['ucp_enabled'] ?? 1), - (int)($fw['ucp_https_port'] ?? 8444), - (int)($fw['ucp_http_port'] ?? 8084) - ); - FirewallManager::generateJailLocal( - (string)($fw['fail2ban_whitelist_ips'] ?? ''), - (int)($fw['fail2ban_sip_maxretry'] ?? 3), - (int)($fw['fail2ban_sip_findtime'] ?? 600), - (int)($fw['fail2ban_sip_bantime'] ?? 3600), - (int)($fw['fail2ban_web_maxretry'] ?? 3), - (int)($fw['fail2ban_web_bantime'] ?? 3600), - (int)($fw['admin_https_port'] ?? 8443), - (int)($fw['ucp_enabled'] ?? 1), - (int)($fw['ucp_https_port'] ?? 8444), - (int)($fw['ucp_http_port'] ?? 8084), - (int)($fw['fail2ban_ucp_maxretry'] ?? 3), - (int)($fw['fail2ban_ucp_bantime'] ?? 3600) - ); + if (function_exists('posix_geteuid') && posix_geteuid() !== 0) { + @exec('sudo -n /var/www/nexus-pbx/scripts/nexus-firewall init-ucp >/dev/null 2>&1'); + } else { + FirewallManager::saveSettings(FirewallManager::getSettings()); + } } catch (\Throwable $e) { // Non-bloquant lors de la synchronisation HA } @@ -1110,6 +1119,54 @@ class ClusterManager ]; } + private function checkSqliteIntegrity(string $filePath): string + { + if (!file_exists($filePath) || filesize($filePath) === 0) { + return 'fichier manquant ou vide'; + } + + try { + $pdo = new PDO('sqlite:' . $filePath); + $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + $res = trim((string)$pdo->query('PRAGMA integrity_check;')->fetchColumn()); + $pdo = null; + if ($res !== '') { + return $res; + } + } catch (\Throwable $e) { + $cliOut = trim((string)shell_exec('sqlite3 ' . escapeshellarg($filePath) . " 'PRAGMA integrity_check;' 2>&1")); + return $cliOut !== '' ? $cliOut : $e->getMessage(); + } + + $cliOut = trim((string)shell_exec('sqlite3 ' . escapeshellarg($filePath) . " 'PRAGMA integrity_check;' 2>&1")); + return $cliOut !== '' ? $cliOut : 'réponse integrity_check vide'; + } + + private function restoreSqliteDatabase(string $sourceFile, string $targetFile): void + { + if (class_exists(\SQLite3::class)) { + try { + $src = new \SQLite3($sourceFile, SQLITE3_OPEN_READONLY); + $dst = new \SQLite3($targetFile); + $dst->busyTimeout(5000); + $ok = $src->backup($dst); + $src->close(); + $dst->close(); + if ($ok) { + return; + } + } catch (\Throwable $e) { + error_log('[CLUSTER] Avertissement SQLite3::backup : ' . $e->getMessage()); + } + } + + $restoreCmd = 'sqlite3 ' . escapeshellarg($targetFile) . ' ".restore ' . escapeshellarg($sourceFile) . '" 2>&1'; + $restoreOut = shell_exec($restoreCmd); + if (!empty($restoreOut)) { + error_log('[CLUSTER] Avertissement sqlite3 .restore : ' . trim((string)$restoreOut)); + } + } + private function updateSyncStatus(string $status, string $message): void { $stmt = $this->db->prepare(' diff --git a/src/FirewallManager.php b/src/FirewallManager.php index d9d801d..d52e00e 100644 --- a/src/FirewallManager.php +++ b/src/FirewallManager.php @@ -673,18 +673,62 @@ EOF; public static function ensureUcpFail2banArtifacts(): void { $filterContent = "[Definition]\nfailregex = ^.*\\[NEXUS-UCP-AUTH-FAIL\\]:\\s*.*$\nignoreregex =\n"; - if (!file_exists(self::UCP_FILTER_PATH) || file_get_contents(self::UCP_FILTER_PATH) !== $filterContent) { - @file_put_contents(self::UCP_FILTER_PATH, $filterContent); - @chmod(self::UCP_FILTER_PATH, 0644); + $isNonRoot = function_exists('posix_geteuid') && posix_geteuid() !== 0; + + if (!file_exists(self::UCP_FILTER_PATH) || @file_get_contents(self::UCP_FILTER_PATH) !== $filterContent) { + if (@file_put_contents(self::UCP_FILTER_PATH, $filterContent) !== false) { + @chmod(self::UCP_FILTER_PATH, 0644); + } elseif ($isNonRoot) { + @exec('sudo -n /var/www/nexus-pbx/scripts/nexus-firewall init-ucp >/dev/null 2>&1'); + return; + } } if (!file_exists(self::UCP_SECURITY_LOG)) { - @touch(self::UCP_SECURITY_LOG); + if (@touch(self::UCP_SECURITY_LOG) === false && $isNonRoot) { + @exec('sudo -n /var/www/nexus-pbx/scripts/nexus-firewall init-ucp >/dev/null 2>&1'); + return; + } } @chown(self::UCP_SECURITY_LOG, 'www-data'); @chgrp(self::UCP_SECURITY_LOG, 'asterisk'); @chmod(self::UCP_SECURITY_LOG, 0664); } + /** + * Auto-déploie les VHosts Nginx UCP et le filtre Fail2ban si absents (ex. après git pull). + */ + public static function ensureUcpDeployed(): void + { + $st = self::getSettings(); + if (empty($st['ucp_enabled'])) { + return; + } + $httpsPort = (int)($st['ucp_https_port'] ?? 8444); + $httpPort = (int)($st['ucp_http_port'] ?? 8084); + if ($httpsPort <= 0 && $httpPort <= 0) { + return; + } + + $nginxFile = '/etc/nginx/sites-available/nexus-pbx'; + $nginxConf = is_readable($nginxFile) ? (string)@file_get_contents($nginxFile) : ''; + $hasNginxUcp = str_contains($nginxConf, self::UCP_ROOT_DIR); + $hasFilter = file_exists(self::UCP_FILTER_PATH); + $hasLog = file_exists(self::UCP_SECURITY_LOG); + + if ($hasNginxUcp && $hasFilter && $hasLog) { + return; + } + + if (function_exists('posix_geteuid') && posix_geteuid() !== 0) { + exec('sudo -n /var/www/nexus-pbx/scripts/nexus-firewall init-ucp 2>&1', $out, $code); + if ($code === 0) { + return; + } + } + + self::saveSettings($st); + } + /** * Ouvre/ferme les ports UCP configurés dans UFW / iptables uniquement si un filtrage actif existe. */ diff --git a/src/UcpAuth.php b/src/UcpAuth.php index 7a295fc..1fd7c31 100644 --- a/src/UcpAuth.php +++ b/src/UcpAuth.php @@ -194,3 +194,4 @@ class UcpAuth ]); } } + diff --git a/ucp/api.php b/ucp/api.php index 9cfe27d..f8b30e6 100644 --- a/ucp/api.php +++ b/ucp/api.php @@ -340,3 +340,4 @@ try { echo json_encode(['success' => false, 'message' => $e->getMessage()]); } } + diff --git a/ucp/assets/ucp.js b/ucp/assets/ucp.js index 5461a26..2377833 100644 --- a/ucp/assets/ucp.js +++ b/ucp/assets/ucp.js @@ -829,3 +829,4 @@ if (window.UCP_IS_AUTH) { } }, 15000); } + diff --git a/ucp/index.php b/ucp/index.php index f29b522..def8e1a 100644 --- a/ucp/index.php +++ b/ucp/index.php @@ -412,3 +412,4 @@ $csrfToken = UcpAuth::getCsrfToken(); + diff --git a/version.json b/version.json index 5e6e727..d845092 100644 --- a/version.json +++ b/version.json @@ -1,16 +1,12 @@ { - "version": "2.5.0", + "version": "2.5.1", "codename": "Pulsar UCP", "release_date": "2026-11-11", "min_php_version": "8.2", "changelog": [ - "NexusPBX 2.5.0 « Pulsar UCP » (Issue #4) : Portail Utilisateur Isolé (User Control Panel)", - "Sécurité & Isolation Nginx : VirtualHosts UCP dédiés (/var/www/nexus-pbx/ucp) séparés physiquement de l'Administration", - "Ports UCP configurables depuis l'interface Pare-feu : HTTPS (8444) pour accès direct et HTTP (8084) pour Reverse Proxy, avec interrupteur Activer/Désactiver", - "Fail2ban dédié Portail Utilisateur : nouvelle jail [nexus-ucp], filtre nexus-ucp.conf et journal isolé /var/log/nexus_ucp_security.log (résolution X-Real-IP derrière Proxy)", - "Gestion Admin des Comptes UCP : création/édition/désactivation d'utilisateurs avec liaison d'une ou plusieurs extensions SIP", - "Portail Employé Mode Clair : suivi des postes temps réel, activation NPD (DND) + mise à jour BLF, renvois internes (CF/CFU) et vers répondeur (vm-) sans numéro externe", - "Messagerie Vocale Web : écoute HTML5 (Accept-Ranges), téléchargement WAV, marquage Lu/Non lu (synchro MWI), suppression et modification PIN/email", - "Journal d'appels personnel filtré par extensions liées, annuaire complet avec édition photo (avatar) et email pour ses propres postes, et synchro HA des avatars" + "Correctif v2.5.1 : auto-déploiement automatique Nginx/Fail2ban UCP même après un git pull manuel", + "Correctif v2.5.1 : vérification d'intégrité & restauration SQLite HA via PDO / SQLite3::backup sans dépendance CLI sqlite3 et anti-collision /tmp", + "Correctif v2.5.1 : rafraîchissement instantané de la liste des utilisateurs UCP à la création/suppression sans rechargement manuel de la page", + "NexusPBX 2.5.0 « Pulsar UCP » (Issue #4) : Portail Utilisateur Isolé (User Control Panel)" ] }