diff --git a/panel/api/endpoints/system.php b/panel/api/endpoints/system.php index b7d1fba..0b059aa 100644 --- a/panel/api/endpoints/system.php +++ b/panel/api/endpoints/system.php @@ -302,9 +302,16 @@ match ($action) { $diskFree = disk_free_space('/'); $diskPct = $diskTotal > 0 ? round((($diskTotal - $diskFree) / $diskTotal) * 100, 1) : 0; - // Services + // Services — dynamic list based on configured servers + $webSetting = $db->fetchOne("SELECT `value` FROM settings WHERE `key`='web_server'")['value'] ?? 'apache'; + $ftpSetting = $db->fetchOne("SELECT `value` FROM settings WHERE `key`='ftp_server'")['value'] ?? 'proftpd'; + $dnsSetting = $db->fetchOne("SELECT `value` FROM settings WHERE `key`='dns_server'")['value'] ?? 'bind9'; + $webSvc = match($webSetting) { 'nginx' => 'nginx', 'apache' => 'apache2', default => 'apache2' }; + $ftpSvc = match($ftpSetting) { 'vsftpd' => 'vsftpd', 'pureftpd' => 'pure-ftpd', default => 'proftpd' }; + $dnsSvc = match($dnsSetting) { 'powerdns' => 'pdns', 'nsd' => 'nsd', 'none' => null, default => 'named' }; + $svcList = array_filter([$webSvc,'mysql','postfix','dovecot',$ftpSvc,$dnsSvc,'fail2ban']); $services = []; - foreach (['apache2','nginx','mysql','postfix','dovecot','proftpd','named','fail2ban'] as $svc) { + foreach ($svcList as $svc) { $active = trim(shell_exec("systemctl is-active $svc 2>/dev/null") ?: ''); if ($active) $services[$svc] = $active; } @@ -329,7 +336,9 @@ match ($action) { Auth::getInstance()->require('admin'); $svc = preg_replace('/[^a-z0-9\-_]/', '', $body['service'] ?? ''); $cmd = $body['command'] ?? 'status'; - $allowed = ['apache2','nginx','mysql','postfix','dovecot','proftpd','named','fail2ban','php7.4-fpm','php8.1-fpm','php8.2-fpm','php8.3-fpm']; + $allowed = ['apache2','nginx','lighttpd','caddy','mysql','mariadb','postgresql','postfix','dovecot', + 'proftpd','vsftpd','pure-ftpd','named','bind9','pdns','nsd','fail2ban', + 'php7.4-fpm','php8.1-fpm','php8.2-fpm','php8.3-fpm']; if (!in_array($svc, $allowed)) Response::error("Service not managed: $svc"); if (!in_array($cmd, ['start','stop','restart','reload','status'])) Response::error("Invalid command"); @@ -390,15 +399,31 @@ match ($action) { $value = $body['value'] ?? ''; $allowed = ['web_server','mail_server','ftp_server','dns_server','whmcs_api_key','whmcs_enabled','ns1_hostname','ns2_hostname']; if (!in_array($key, $allowed)) Response::error("Invalid setting key: $key"); + + // Save before switching so the new value is in DB $db->execute("INSERT INTO settings (`key`,`value`) VALUES (?,?) ON DUPLICATE KEY UPDATE `value`=VALUES(`value`)", [$key, $value]); - // For server switches, run install/reload scripts - if (in_array($key, ['web_server','ftp_server','dns_server','mail_server'])) { - $script = "/opt/novacpx/bin/switch-{$key}.sh"; - if (is_executable($script)) { - shell_exec("sudo {$script} " . escapeshellarg($value) . " > /var/log/novacpx/switch-{$key}.log 2>&1 &"); + // Inline service switching — stop all alternatives, start the chosen one + if ($key === 'web_server') { + $webSvcs = ['apache2','nginx','lighttpd','caddy']; + foreach ($webSvcs as $s) { shell_exec("systemctl stop $s 2>/dev/null; systemctl disable $s 2>/dev/null"); } + $startSvc = match($value) { 'nginx' => 'nginx', 'apache' => 'apache2', default => 'apache2' }; + shell_exec("systemctl enable $startSvc 2>/dev/null && systemctl start $startSvc 2>/dev/null"); + } elseif ($key === 'ftp_server') { + foreach (['proftpd','vsftpd','pure-ftpd'] as $s) { shell_exec("systemctl stop $s 2>/dev/null; systemctl disable $s 2>/dev/null"); } + $startSvc = match($value) { 'vsftpd' => 'vsftpd', 'pureftpd' => 'pure-ftpd', default => 'proftpd' }; + if (trim(shell_exec("dpkg -l $startSvc 2>/dev/null | grep -c '^ii'") ?: '0') > 0) { + shell_exec("systemctl enable $startSvc 2>/dev/null && systemctl start $startSvc 2>/dev/null"); + } + } elseif ($key === 'dns_server') { + foreach (['named','bind9','pdns','nsd'] as $s) { shell_exec("systemctl stop $s 2>/dev/null; systemctl disable $s 2>/dev/null"); } + if ($value !== 'none') { + $startSvc = match($value) { 'powerdns' => 'pdns', 'nsd' => 'nsd', default => 'named' }; + shell_exec("systemctl enable $startSvc 2>/dev/null && systemctl start $startSvc 2>/dev/null"); } } + // mail_server: postfix + dovecot are always running; mail_server setting controls config template only + audit("settings.{$key}", $value); Response::success(null, "Setting saved: {$key} = {$value}"); })(), @@ -471,5 +496,77 @@ match ($action) { else Response::error("CyberMail returned HTTP {$code}: " . substr($resp, 0, 200)); })(), + // ── Database engine management ──────────────────────────────────────────── + 'db-engines' => (function() use ($db) { + Auth::getInstance()->require('admin'); + $engines = []; + foreach (['mysql','mariadb','postgresql'] as $eng) { + $svc = match($eng) { + 'mysql' => 'mysql', + 'mariadb' => 'mariadb', + 'postgresql' => 'postgresql', + }; + $active = trim(shell_exec("systemctl is-active $svc 2>/dev/null") ?: ''); + $enabled = trim(shell_exec("systemctl is-enabled $svc 2>/dev/null") ?: ''); + $pkgCheck = match($eng) { + 'mysql' => 'dpkg -l mysql-server 2>/dev/null | grep -c "^ii"', + 'mariadb' => 'dpkg -l mariadb-server 2>/dev/null | grep -c "^ii"', + 'postgresql' => 'dpkg -l postgresql 2>/dev/null | grep -c "^ii"', + }; + $installed = (int)trim(shell_exec($pkgCheck) ?: '0') > 0; + $version = ''; + if ($installed) { + $version = match($eng) { + 'mysql' => trim(shell_exec("mysql --version 2>/dev/null | grep -oP '[0-9]+\.[0-9]+\.[0-9]+'") ?: ''), + 'mariadb' => trim(shell_exec("mariadb --version 2>/dev/null | grep -oP '[0-9]+\.[0-9]+\.[0-9]+'") ?: ''), + 'postgresql' => trim(shell_exec("psql --version 2>/dev/null | grep -oP '[0-9]+\.[0-9]+'") ?: ''), + }; + } + $engines[$eng] = [ + 'installed' => $installed, + 'active' => $active === 'active', + 'enabled' => $enabled === 'enabled', + 'version' => $version, + ]; + } + $active_db = $db->fetchOne("SELECT `value` FROM settings WHERE `key` = 'active_db_engine'")['value'] ?? 'mysql'; + Response::success(['engines' => $engines, 'active_engine' => $active_db]); + })(), + + 'db-engine-action' => (function() use ($db, $body) { + Auth::getInstance()->require('admin'); + $engine = preg_replace('/[^a-z]/', '', $body['engine'] ?? ''); + $action = $body['action'] ?? ''; + if (!in_array($engine, ['mysql','mariadb','postgresql'])) Response::error("Invalid engine"); + if (!in_array($action, ['install','remove','start','stop','restart','set-active'])) Response::error("Invalid action"); + + $out = ''; + if ($action === 'install') { + $pkg = match($engine) { + 'mysql' => 'mysql-server', + 'mariadb' => 'mariadb-server', + 'postgresql' => 'postgresql postgresql-contrib', + }; + $out = shell_exec("DEBIAN_FRONTEND=noninteractive apt-get install -y $pkg 2>&1"); + shell_exec("systemctl enable $engine && systemctl start $engine 2>/dev/null"); + } elseif ($action === 'remove') { + $pkg = match($engine) { + 'mysql' => 'mysql-server mysql-client', + 'mariadb' => 'mariadb-server mariadb-client', + 'postgresql' => 'postgresql postgresql-contrib', + }; + shell_exec("systemctl stop $engine 2>/dev/null || true"); + $out = shell_exec("apt-get remove -y $pkg 2>&1"); + } elseif ($action === 'set-active') { + $db->execute("INSERT INTO settings (`key`,`value`) VALUES ('active_db_engine',?) ON DUPLICATE KEY UPDATE `value`=VALUES(`value`)", [$engine]); + audit('settings.active_db_engine', $engine); + Response::success(null, "Active database engine set to $engine"); + } else { + shell_exec("systemctl $action $engine 2>/dev/null"); + } + audit("db-engine.$action", $engine); + Response::success(['output' => substr($out ?: '', -1000)], ucfirst($action) . " $engine done"); + })(), + default => Response::error("Unknown system action: $action", 404), }; diff --git a/panel/public/assets/js/admin.js b/panel/public/assets/js/admin.js index 4f3a842..9f6372a 100644 --- a/panel/public/assets/js/admin.js +++ b/panel/public/assets/js/admin.js @@ -1597,24 +1597,81 @@ ${ips.length ? ` // ── MySQL/DB Manager ─────────────────────────────────────────────────────── async function mysqlManager() { - const res = await Nova.api('databases','list',{params:{account_id:0}}); - const dbs = res?.data || []; - return ` + const [engRes, dbRes] = await Promise.all([ + Nova.api('system','db-engines'), + Nova.api('databases','list',{params:{account_id:0}}), + ]); + const eng = engRes?.data?.engines || {}; + const actE = engRes?.data?.active_engine || 'mysql'; + const dbs = dbRes?.data || []; + + const engineCard = (id, label, icon) => { + const e = eng[id] || {}; + const statusColor = e.active ? 'green' : (e.installed ? 'red' : 'default'); + const statusText = !e.installed ? 'Not Installed' : (e.active ? 'Running' : 'Stopped'); + return `
-
Databases
- ${dbs.length ? ` - ${dbs.map(d => ` - - - - - - - `).join('')} -
DatabaseUserTypeAccountSizeActions
${d.db_name}${d.db_user}${Nova.badge(d.db_type,'default')}${d.username||'—'}${d.size||'—'}
` - : '
No databases.
'} +
+ ${icon} ${label} + ${Nova.badge(statusText, statusColor)} + ${e.version ? `v${e.version}` : ''} +
+
+
+ ${!e.installed + ? `` + : ` + + + + ` + } +
+ ${e.installed && id !== 'postgresql' ? `phpMyAdmin ↗` : ''} + ${e.installed && id === 'postgresql' ? `pgAdmin ↗` : ''} +
+
`; + }; + + const dbTable = dbs.length ? ` + +${dbs.map(d=>` + + + + + +`).join('')} +
DatabaseUserTypeAccountActions
${Nova.escHtml(d.db_name)}${Nova.escHtml(d.db_user||'—')}${Nova.badge(d.db_type||'mysql','default')}${Nova.escHtml(d.username||'—')}
` : '
No databases yet.
'; + + return ` + + +
+ ${engineCard('mysql', 'MySQL', '🐬')} + ${engineCard('mariadb', 'MariaDB', '🦭')} + ${engineCard('postgresql','PostgreSQL','🐘')} +
+ +
+
Active EngineUsed for new account databases
+
+ + + Currently: ${Nova.badge(actE,'green')} +
+
+ +
+
All Databases${dbs.length} total
+ ${dbTable}
`; } + window.adminDropDB = (id, name) => { Nova.confirm(`Drop database ${name}? ALL DATA WILL BE LOST.`, async () => { const r = await Nova.api('databases','drop',{method:'POST',body:{id}}); @@ -1622,6 +1679,25 @@ ${ips.length ? ` else Nova.toast(r?.message,'error'); }, true); }; + window.dbEngineAction = (engine, action) => { + const labels = {install:`Installing ${engine}…`,remove:`Removing ${engine}…`,start:`Starting…`,stop:`Stopping…`,restart:`Restarting…`}; + const doIt = async () => { + Nova.toast(labels[action]||'Working…','info'); + const r = await Nova.api('system','db-engine-action',{method:'POST',body:{engine,action}}); + Nova.toast(r?.message||(r?.success?'Done':'Failed'), r?.success?'success':'error'); + if (r?.success) adminPage('mysql-manager'); + }; + if (['install','remove'].includes(action)) { + Nova.confirm(`${action === 'install' ? 'Install' : 'Remove'} ${engine}?`, doIt, action === 'remove'); + } else { doIt(); } + }; + window.dbSetActive = async () => { + const engine = document.getElementById('db-active-engine')?.value; + if (!engine) return; + const r = await Nova.api('system','db-engine-action',{method:'POST',body:{engine,action:'set-active'}}); + Nova.toast(r?.message||(r?.success?'Active engine updated':'Failed'), r?.success?'success':'error'); + if (r?.success) adminPage('mysql-manager'); + }; // ── Mail Server ──────────────────────────────────────────────────────────── async function mailServer() { @@ -1660,21 +1736,28 @@ ${ips.length ? ` // ── FTP Server ──────────────────────────────────────────────────────────── async function ftpServer() { - const r = await Nova.api('system','stats'); - const ftpStatus = r?.data?.services?.proftpd || 'unknown'; + const [sRes, optsRes] = await Promise.all([ + Nova.api('system','stats'), + Nova.api('system','server-options'), + ]); + const svcs = sRes?.data?.services || {}; + const ftpConf = optsRes?.data?.ftp_server || 'proftpd'; + const svcName = ftpConf === 'vsftpd' ? 'vsftpd' : (ftpConf === 'pureftpd' ? 'pure-ftpd' : 'proftpd'); + const label = ftpConf === 'vsftpd' ? 'vsftpd' : (ftpConf === 'pureftpd' ? 'Pure-FTPd' : 'ProFTPD'); + const status = svcs[svcName] || 'unknown'; return `
- FTP Server (ProFTPD) - ${Nova.badge(ftpStatus, ftpStatus==='active'?'green':'red')} + FTP Server (${label}) + ${Nova.badge(status, status==='active'?'green':'red')}
- - + +
-

ProFTPD uses virtual users stored in /etc/proftpd/novacpx-users.passwd

+

Active FTP server: ${label} — change in Server Options.

FTP connections use SFTP on port 22 or passive FTP on ports 20/21.

Per-account FTP management is available in each account's FTP page.