From fe2d3d457ce48fa39a4add2e5e8db06aef73ec7e Mon Sep 17 00:00:00 2001 From: Myron Blair Date: Mon, 8 Jun 2026 20:39:54 +0000 Subject: [PATCH] =?UTF-8?q?Add=20account=20edit=20modal=20=E2=80=94=20pack?= =?UTF-8?q?age,=20PHP=20version,=20email?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - New accounts/update endpoint: updates package_id, php_version, email, and notes; switches PHP-FPM pool when version changes - Edit button on each account row opens pre-populated modal - Modal shows email, package dropdown, PHP version selector; domain is read-only with tooltip explaining it can't change Co-Authored-By: Claude Sonnet 4.6 --- panel/api/endpoints/accounts.php | 40 ++++++++++++++++++++++ panel/assets/js/admin.js | 58 +++++++++++++++++++++++++++++--- panel/public/assets/js/admin.js | 58 +++++++++++++++++++++++++++++--- 3 files changed, 146 insertions(+), 10 deletions(-) diff --git a/panel/api/endpoints/accounts.php b/panel/api/endpoints/accounts.php index 131ff69..ae06383 100644 --- a/panel/api/endpoints/accounts.php +++ b/panel/api/endpoints/accounts.php @@ -88,6 +88,46 @@ match ($action) { Response::success($result, 'Account created successfully'); })(), + 'update' => (function() use ($db, $body, $user, $ownerClause) { + $id = (int)($body['id'] ?? 0); + $acct = $db->fetchOne( + "SELECT a.*, u.email FROM accounts a JOIN users u ON u.id=a.user_id WHERE a.id=? $ownerClause", + [$id] + ); + if (!$acct) Response::error("Account not found", 404); + + $allowed = ['php_version', 'package_id', 'notes']; + $sets = []; $params = []; + foreach ($allowed as $col) { + if (array_key_exists($col, $body)) { + $sets[] = "`$col` = ?"; + $params[] = $body[$col] === '' ? null : $body[$col]; + } + } + // Email lives on users table + if (array_key_exists('email', $body) && filter_var($body['email'], FILTER_VALIDATE_EMAIL)) { + $db->execute("UPDATE users SET email=? WHERE id=?", [$body['email'], $acct['user_id']]); + } + if ($sets) { + $params[] = $id; + $db->execute("UPDATE accounts SET " . implode(', ', $sets) . " WHERE id=?", $params); + } + + // If PHP version changed, update the FPM pool + if (!empty($body['php_version']) && $body['php_version'] !== $acct['php_version']) { + require_once NOVACPX_LIB . '/PHPManager.php'; + try { + PHPManager::removePool($acct['username']); + PHPManager::createPool($acct['username'], $body['php_version']); + } catch (Throwable $e) { + novacpx_log('warn', "PHP pool update failed for {$acct['username']}: " . $e->getMessage()); + } + } + + audit('account.update', "account:$id", array_intersect_key($body, array_flip([...$allowed, 'email']))); + Response::success(null, 'Account updated'); + })(), + 'suspend' => (function() use ($db, $body, $ownerClause) { $id = (int)($body['id'] ?? 0); $acct = $db->fetchOne( diff --git a/panel/assets/js/admin.js b/panel/assets/js/admin.js index aca1082..237656c 100644 --- a/panel/assets/js/admin.js +++ b/panel/assets/js/admin.js @@ -743,16 +743,16 @@ function renderAccountTable(accts) { if (!accts.length) return '
No accounts found.
'; - return ` + return `
UsernameDomainResellerPackageDiskStatusCreatedActions
${accts.map(a => ` - - - + + -
UsernameDomainPackagePHPStatusCreatedActions
${a.username} ${a.domain}${a.reseller_username || 'admin'}${a.package_name || '—'}${a.disk_usage_mb || 0} MB${a.package_name || ''}${a.php_version || '—'} ${Nova.badge(a.status, a.status==='active'?'green':a.status==='suspended'?'yellow':'red')} ${Nova.relTime(a.created_at)} + + ${a.status==='active' ? `` : ``} @@ -792,6 +792,54 @@ }, true); }; + window.adminEditAccount = async (id) => { + const [acctRes, pkgRes] = await Promise.all([ + Nova.api('accounts', 'get', { params: { id } }), + Nova.api('packages', 'list'), + ]); + if (!acctRes?.success) { Nova.toast(acctRes?.message || 'Failed to load account', 'error'); return; } + const a = acctRes.data; + const pkgs = pkgRes?.data || []; + const pkgOpts = `` + + pkgs.map(p => ``).join(''); + const phpOpts = ['8.3','8.2','8.1','7.4'].map(v => + ``).join(''); + + Nova.modal(`Edit Account — ${Nova.escHtml(a.username)}`, + `
+
+
+
+
+
+
+
+
+
`, + ` + ` + ); + }; + + window.adminEditAccountSave = async (id) => { + const body = { + id, + email: document.getElementById('ae-email')?.value?.trim(), + package_id: document.getElementById('ae-pkg')?.value || null, + php_version: document.getElementById('ae-php')?.value, + }; + Nova.loading('Saving…'); + const res = await Nova.api('accounts', 'update', { method: 'POST', body }); + Nova.loadingDone(); + if (res?.success) { + document.querySelector('.modal-overlay')?.remove(); + Nova.toast('Account updated', 'success'); + adminPage('accounts'); + } else { + Nova.toast(res?.message || 'Update failed', 'error'); + } + }; + // ── Create Account ───────────────────────────────────────────────────────── async function createAccount() { const pkgRes = await Nova.api('packages', 'list'); diff --git a/panel/public/assets/js/admin.js b/panel/public/assets/js/admin.js index aca1082..237656c 100644 --- a/panel/public/assets/js/admin.js +++ b/panel/public/assets/js/admin.js @@ -743,16 +743,16 @@ function renderAccountTable(accts) { if (!accts.length) return '
No accounts found.
'; - return ` + return `
UsernameDomainResellerPackageDiskStatusCreatedActions
${accts.map(a => ` - - - + + -
UsernameDomainPackagePHPStatusCreatedActions
${a.username} ${a.domain}${a.reseller_username || 'admin'}${a.package_name || '—'}${a.disk_usage_mb || 0} MB${a.package_name || ''}${a.php_version || '—'} ${Nova.badge(a.status, a.status==='active'?'green':a.status==='suspended'?'yellow':'red')} ${Nova.relTime(a.created_at)} + + ${a.status==='active' ? `` : ``} @@ -792,6 +792,54 @@ }, true); }; + window.adminEditAccount = async (id) => { + const [acctRes, pkgRes] = await Promise.all([ + Nova.api('accounts', 'get', { params: { id } }), + Nova.api('packages', 'list'), + ]); + if (!acctRes?.success) { Nova.toast(acctRes?.message || 'Failed to load account', 'error'); return; } + const a = acctRes.data; + const pkgs = pkgRes?.data || []; + const pkgOpts = `` + + pkgs.map(p => ``).join(''); + const phpOpts = ['8.3','8.2','8.1','7.4'].map(v => + ``).join(''); + + Nova.modal(`Edit Account — ${Nova.escHtml(a.username)}`, + `
+
+
+
+
+
+
+
+
+
`, + ` + ` + ); + }; + + window.adminEditAccountSave = async (id) => { + const body = { + id, + email: document.getElementById('ae-email')?.value?.trim(), + package_id: document.getElementById('ae-pkg')?.value || null, + php_version: document.getElementById('ae-php')?.value, + }; + Nova.loading('Saving…'); + const res = await Nova.api('accounts', 'update', { method: 'POST', body }); + Nova.loadingDone(); + if (res?.success) { + document.querySelector('.modal-overlay')?.remove(); + Nova.toast('Account updated', 'success'); + adminPage('accounts'); + } else { + Nova.toast(res?.message || 'Update failed', 'error'); + } + }; + // ── Create Account ───────────────────────────────────────────────────────── async function createAccount() { const pkgRes = await Nova.api('packages', 'list');