Fix multiple user panel 500 errors

- domains: VhostManager::create() called with array instead of 4 params
- PHPManager: VhostManager not required; pool writes use sudo tee (permission);
  updateConfig creates pool if missing instead of throwing
- DatabaseManager: MySQL ops used SQLite panel PDO; add dedicated mysqlPdo()
  using MariaDB socket auth
- BackupManager: column name is size_mb not size; diskUsage returns float
- DB.php: add LAST_INSERT_ID() → last_insert_rowid() translation
- user.js: SSL issue/submit used Nova.api (JSON) but endpoint streams SSE;
  add _sslStream() helper matching admin panel behavior
- schema/migration: add enc_password column to email_accounts

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-09 18:32:00 +00:00
parent 563386b8b8
commit c22e1fd067
8 changed files with 107 additions and 56 deletions
+8 -7
View File
@@ -46,9 +46,10 @@ class BackupManager {
}
}
$size = file_exists($filepath) ? filesize($filepath) : 0;
$this->db->prepare("UPDATE backups SET status='complete', size=? WHERE id=?")->execute([$size, $backupId]);
return ['id' => $backupId, 'filename' => $filename, 'size' => $size];
$bytes = file_exists($filepath) ? filesize($filepath) : 0;
$sizeMb = round($bytes / 1048576, 2);
$this->db->prepare("UPDATE backups SET status='complete', size_mb=? WHERE id=?")->execute([$sizeMb, $backupId]);
return ['id' => $backupId, 'filename' => $filename, 'size_mb' => $sizeMb];
} catch (RuntimeException $e) {
$this->db->prepare("UPDATE backups SET status='failed' WHERE id=?")->execute([$backupId]);
@@ -147,14 +148,14 @@ class BackupManager {
}
// ── Disk usage ────────────────────────────────────────────────────────────
public function diskUsage(int $accountId = 0): int {
public function diskUsage(int $accountId = 0): float {
if ($accountId) {
$stmt = $this->db->prepare("SELECT COALESCE(SUM(size),0) FROM backups WHERE account_id=? AND status='complete'");
$stmt = $this->db->prepare("SELECT COALESCE(SUM(size_mb),0) FROM backups WHERE account_id=? AND status='complete'");
$stmt->execute([$accountId]);
} else {
$stmt = $this->db->query("SELECT COALESCE(SUM(size),0) FROM backups WHERE status='complete'");
$stmt = $this->db->query("SELECT COALESCE(SUM(size_mb),0) FROM backups WHERE status='complete'");
}
return (int)$stmt->fetchColumn();
return (float)$stmt->fetchColumn();
}
private function getAccount(int $id): array {
+3
View File
@@ -93,6 +93,9 @@ class DB {
$sql
);
// LAST_INSERT_ID() → last_insert_rowid()
$sql = preg_replace('/\bLAST_INSERT_ID\(\)/i', 'last_insert_rowid()', $sql);
// IFNULL → COALESCE (SQLite supports both but be safe)
$sql = preg_replace('/\bIFNULL\s*\(/i', 'COALESCE(', $sql);
+23 -6
View File
@@ -4,10 +4,26 @@
*/
class DatabaseManager {
private static function mysqlPdo(): PDO {
$cfg = @parse_ini_file('/etc/novacpx/config.ini', true) ?: [];
$host = $cfg['mysql']['host'] ?? '127.0.0.1';
$port = $cfg['mysql']['port'] ?? '3306';
$user = $cfg['mysql']['root_user'] ?? 'root';
$pass = $cfg['mysql']['root_pass'] ?? '';
$opts = [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION];
if ($pass === '' && $host === '127.0.0.1') {
// Try socket auth (MariaDB/MySQL root with no password)
try {
return new PDO("mysql:unix_socket=/var/run/mysqld/mysqld.sock", $user, '', $opts);
} catch (PDOException $e) { /* fall through to TCP */ }
}
return new PDO("mysql:host={$host};port={$port}", $user, $pass, $opts);
}
public static function createMySQL(int $accountId, string $dbName, string $dbUser, string $dbPass): int {
self::validateName($dbName); self::validateName($dbUser);
$db = DB::getInstance();
$pdo = $db->pdo();
$pdo = self::mysqlPdo();
$pdo->exec("CREATE DATABASE IF NOT EXISTS `{$dbName}` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci");
$pdo->exec("CREATE USER IF NOT EXISTS '{$dbUser}'@'localhost' IDENTIFIED BY " . $pdo->quote($dbPass));
@@ -35,7 +51,7 @@ class DatabaseManager {
public static function drop(string $dbName, string $dbUser, string $type = 'mysql'): void {
if ($type === 'mysql') {
$pdo = DB::getInstance()->pdo();
$pdo = self::mysqlPdo();
$pdo->exec("DROP DATABASE IF EXISTS `{$dbName}`");
$pdo->exec("DROP USER IF EXISTS '{$dbUser}'@'localhost'");
$pdo->exec("FLUSH PRIVILEGES");
@@ -51,7 +67,7 @@ class DatabaseManager {
$dbe = $db->fetchOne("SELECT * FROM `databases` WHERE id = ?", [$id]);
if (!$dbe) throw new RuntimeException("Database not found");
if ($dbe['db_type'] === 'mysql') {
$pdo = $db->pdo();
$pdo = self::mysqlPdo();
$pdo->exec("ALTER USER '{$dbe['db_user']}'@'localhost' IDENTIFIED BY " . $pdo->quote($newPass));
} else {
shell_exec("sudo -u postgres psql -c \"ALTER USER {$dbe['db_user']} WITH PASSWORD " . escapeshellarg($newPass) . "\" 2>/dev/null");
@@ -61,11 +77,12 @@ class DatabaseManager {
public static function getSize(string $dbName, string $type = 'mysql'): float {
if ($type === 'mysql') {
$row = DB::getInstance()->fetchOne(
$stmt = self::mysqlPdo()->prepare(
"SELECT ROUND(SUM(data_length + index_length) / 1024 / 1024, 2) AS size
FROM information_schema.tables WHERE table_schema = ?",
[$dbName]
FROM information_schema.tables WHERE table_schema = ?"
);
$stmt->execute([$dbName]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
return (float)($row['size'] ?? 0);
}
$out = shell_exec("sudo -u postgres psql -t -c \"SELECT pg_size_pretty(pg_database_size('{$dbName}'))\" 2>/dev/null");
+14 -5
View File
@@ -2,16 +2,25 @@
/**
* PHPManager — per-account PHP-FPM pools + version switching
*/
if (!class_exists('VhostManager')) require_once __DIR__ . '/VhostManager.php';
class PHPManager {
private static string $poolDir = '/etc/php/{ver}/fpm/pool.d';
private static function writeFile(string $path, string $content): void {
$tmp = tempnam('/tmp', 'ncpx_pool_');
file_put_contents($tmp, $content);
shell_exec("sudo tee " . escapeshellarg($path) . " > /dev/null < " . escapeshellarg($tmp));
@unlink($tmp);
}
public static function createPool(string $username, string $phpVer): void {
$poolFile = str_replace('{ver}', $phpVer, self::$poolDir) . "/{$username}.conf";
$homeDir = "/home/{$username}";
$sock = "/run/php/php{$phpVer}-fpm-{$username}.sock";
file_put_contents($poolFile, "[{$username}]
self::writeFile($poolFile, "[{$username}]
user = {$username}
group = www-data
listen = {$sock}
@@ -39,7 +48,7 @@ php_value[max_execution_time] = 30
public static function removePool(string $username): void {
foreach (['7.4','8.1','8.2','8.3'] as $ver) {
$file = str_replace('{ver}', $ver, self::$poolDir) . "/{$username}.conf";
if (file_exists($file)) { unlink($file); self::reloadFPM($ver); }
if (file_exists($file)) { shell_exec("sudo rm -f " . escapeshellarg($file)); self::reloadFPM($ver); }
}
}
@@ -70,9 +79,9 @@ php_value[max_execution_time] = 30
if (!$acct) throw new RuntimeException("Account not found");
$poolFile = str_replace('{ver}', $acct['php_version'], self::$poolDir) . "/{$acct['username']}.conf";
if (!file_exists($poolFile)) throw new RuntimeException("PHP-FPM pool not found");
if (!file_exists($poolFile)) self::createPool($acct['username'], $acct['php_version']);
$content = file_get_contents($poolFile);
$content = file_get_contents($poolFile) ?: '';
$map = [
'memory_limit' => 'php_value[memory_limit]',
'max_execution_time' => 'php_value[max_execution_time]',
@@ -84,7 +93,7 @@ php_value[max_execution_time] = 30
$content = preg_replace("/{$iniKey}\s*=.*/", "{$iniKey} = {$cfg[$key]}", $content);
}
}
file_put_contents($poolFile, $content);
self::writeFile($poolFile, $content);
self::reloadFPM($acct['php_version']);
$db->execute(