From 39942929a7b82f9c057844b40f522eccc08573c1 Mon Sep 17 00:00:00 2001 From: Myron Blair Date: Sat, 20 Jun 2026 05:23:42 +0000 Subject: [PATCH] fix: global exception handler (prevents 502), transaction rollback on account create, CORS for reverse proxy - set_exception_handler in api/index.php prevents uncaught exceptions from crashing PHP-FPM - AccountManager::create() wrapped in DB transaction with rollback + Linux user cleanup on failure - CORS origin regex updated to allow requests from port 443 (NPM reverse proxy) - index.html written via sudo tee instead of file_put_contents (www-data permission fix) - chpasswd now called with sudo prefix Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_01LP9Q4kfCAYAjJnsbHBrViZ --- panel/api/index.php | 12 ++++++-- panel/lib/AccountManager.php | 59 +++++++++++++++++++++--------------- 2 files changed, 44 insertions(+), 27 deletions(-) diff --git a/panel/api/index.php b/panel/api/index.php index 70a771b..25f127c 100644 --- a/panel/api/index.php +++ b/panel/api/index.php @@ -9,14 +9,22 @@ define('NOVACPX_API', __DIR__); define('NOVACPX_LIB', NOVACPX_ROOT . '/lib'); header('Content-Type: application/json'); + +// Global exception handler — prevents uncaught exceptions from crashing PHP-FPM (502) +set_exception_handler(function (Throwable $e) { + http_response_code(500); + echo json_encode(['success' => false, 'message' => $e->getMessage(), 'errors' => []]); + exit; +}); + $_ver = file_get_contents(NOVACPX_ROOT . '/VERSION') ?: file_get_contents('/opt/novacpx-src/VERSION') ?: '1.0.0'; header('X-NovaCPX-Version: ' . trim($_ver)); -// CORS for same-origin panel requests (ports 8880/8881/8882/8883) +// CORS for same-origin panel requests (ports 8880/8881/8882/8883 and HTTPS via reverse proxy on 443) $origin = $_SERVER['HTTP_ORIGIN'] ?? ''; -if (preg_match('#^https?://[^/]+:(888[0-3])$#', $origin)) { +if (preg_match('#^https?://[^/]+(:(888[0-3]))?$#', $origin)) { header("Access-Control-Allow-Origin: $origin"); header('Access-Control-Allow-Credentials: true'); header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS'); diff --git a/panel/lib/AccountManager.php b/panel/lib/AccountManager.php index 1ea2480..2c2b071 100644 --- a/panel/lib/AccountManager.php +++ b/panel/lib/AccountManager.php @@ -25,41 +25,50 @@ class AccountManager { $docRoot = "{$homeDir}/public_html"; $password = $data['password'] ?? bin2hex(random_bytes(8)); - // Create Linux user + // Create Linux user and home directory first self::shell("useradd -m -d {$homeDir} -s /sbin/nologin -G www-data " . escapeshellarg($username)); - self::shell("echo " . escapeshellarg("{$username}:{$password}") . " | chpasswd"); + self::shell("echo " . escapeshellarg("{$username}:{$password}") . " | sudo chpasswd"); self::shell("sudo mkdir -p {$docRoot} {$homeDir}/logs {$homeDir}/tmp"); self::shell("sudo chown -R {$username}:www-data {$homeDir}"); - self::shell("sudo chmod 750 {$homeDir}"); self::shell("sudo chmod 775 {$docRoot}"); + self::shell("sudo chmod 750 {$homeDir}"); + self::shell("sudo chmod 775 {$docRoot}"); - // Default index page - file_put_contents("{$docRoot}/index.html", - "

Welcome to {$domain}

Hosted by NovaCPX

" - ); + // Default index page (write as root via sudo tee) + $html = "

Welcome to {$domain}

Hosted by NovaCPX

"; + self::shell("sudo tee " . escapeshellarg("{$docRoot}/index.html") . " > /dev/null << 'HTMLEOF'\n{$html}\nHTMLEOF"); - // Save account to DB - $acctId = (int)$db->insert( - "INSERT INTO accounts (user_id, username, domain, home_dir, package_id, php_version, web_server) VALUES (?,?,?,?,?,?,?)", - [$userId, $username, $domain, $homeDir, $pkgId ?: null, $phpVer, $webSrv] - ); + // Wrap all DB writes in a transaction so partial failures leave no orphans + $db->beginTransaction(); + try { + $acctId = (int)$db->insert( + "INSERT INTO accounts (user_id, username, domain, home_dir, package_id, php_version, web_server) VALUES (?,?,?,?,?,?,?)", + [$userId, $username, $domain, $homeDir, $pkgId ?: null, $phpVer, $webSrv] + ); - // Save domain - $db->insert( - "INSERT INTO domains (account_id, domain, type, document_root) VALUES (?,?,?,?)", - [$acctId, $domain, 'main', $docRoot] - ); + $db->insert( + "INSERT INTO domains (account_id, domain, type, document_root) VALUES (?,?,?,?)", + [$acctId, $domain, 'main', $docRoot] + ); - // Create web vhost - VhostManager::create($username, $domain, $docRoot, $phpVer); + // Create web vhost + VhostManager::create($username, $domain, $docRoot, $phpVer); - // Create DNS zone - DNSManager::createZone($acctId, $domain); + // Create DNS zone + DNSManager::createZone($acctId, $domain); - // Auto-provision SPF, DKIM, DMARC records - self::provisionEmailDNS($acctId, $domain); + // Auto-provision SPF, DKIM, DMARC records + self::provisionEmailDNS($acctId, $domain); - // Create PHP-FPM pool - PHPManager::createPool($username, $phpVer); + // Create PHP-FPM pool + PHPManager::createPool($username, $phpVer); + + $db->commit(); + } catch (Throwable $e) { + $db->rollBack(); + // Clean up Linux user if DB failed + self::shell("userdel -r " . escapeshellarg($username) . " 2>/dev/null || true"); + throw $e; + } novacpx_log('info', "Account created: $username ($domain)"); return ['account_id' => $acctId, 'username' => $username, 'domain' => $domain, 'home_dir' => $homeDir];