mirror of
https://github.com/myronblair/novacpx
synced 2026-06-30 17:50:41 -05:00
Add account edit modal — package, PHP version, email
- 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 <noreply@anthropic.com>
This commit is contained in:
@@ -88,6 +88,46 @@ match ($action) {
|
|||||||
Response::success($result, 'Account created successfully');
|
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) {
|
'suspend' => (function() use ($db, $body, $ownerClause) {
|
||||||
$id = (int)($body['id'] ?? 0);
|
$id = (int)($body['id'] ?? 0);
|
||||||
$acct = $db->fetchOne(
|
$acct = $db->fetchOne(
|
||||||
|
|||||||
@@ -743,16 +743,16 @@
|
|||||||
|
|
||||||
function renderAccountTable(accts) {
|
function renderAccountTable(accts) {
|
||||||
if (!accts.length) return '<div class="empty" style="padding:2rem">No accounts found.</div>';
|
if (!accts.length) return '<div class="empty" style="padding:2rem">No accounts found.</div>';
|
||||||
return `<table class="table"><thead><tr><th>Username</th><th>Domain</th><th>Reseller</th><th>Package</th><th>Disk</th><th>Status</th><th>Created</th><th>Actions</th></tr></thead><tbody>
|
return `<table class="table"><thead><tr><th>Username</th><th>Domain</th><th>Package</th><th>PHP</th><th>Status</th><th>Created</th><th>Actions</th></tr></thead><tbody>
|
||||||
${accts.map(a => `<tr>
|
${accts.map(a => `<tr>
|
||||||
<td><strong>${a.username}</strong></td>
|
<td><strong>${a.username}</strong></td>
|
||||||
<td>${a.domain}</td>
|
<td>${a.domain}</td>
|
||||||
<td>${a.reseller_username || '<span class="text-muted">admin</span>'}</td>
|
<td>${a.package_name || '<span class="text-muted">—</span>'}</td>
|
||||||
<td>${a.package_name || '—'}</td>
|
<td class="text-muted text-sm">${a.php_version || '—'}</td>
|
||||||
<td>${a.disk_usage_mb || 0} MB</td>
|
|
||||||
<td>${Nova.badge(a.status, a.status==='active'?'green':a.status==='suspended'?'yellow':'red')}</td>
|
<td>${Nova.badge(a.status, a.status==='active'?'green':a.status==='suspended'?'yellow':'red')}</td>
|
||||||
<td class="text-muted text-sm">${Nova.relTime(a.created_at)}</td>
|
<td class="text-muted text-sm">${Nova.relTime(a.created_at)}</td>
|
||||||
<td style="display:flex;gap:.25rem">
|
<td style="display:flex;gap:.25rem;flex-wrap:wrap">
|
||||||
|
<button class="btn btn-xs btn-primary" onclick="adminEditAccount(${a.id})">Edit</button>
|
||||||
${a.status==='active'
|
${a.status==='active'
|
||||||
? `<button class="btn btn-xs btn-warning" onclick="adminSuspend(${a.id},'${a.username}')">Suspend</button>`
|
? `<button class="btn btn-xs btn-warning" onclick="adminSuspend(${a.id},'${a.username}')">Suspend</button>`
|
||||||
: `<button class="btn btn-xs btn-success" onclick="adminUnsuspend(${a.id})">Unsuspend</button>`}
|
: `<button class="btn btn-xs btn-success" onclick="adminUnsuspend(${a.id})">Unsuspend</button>`}
|
||||||
@@ -792,6 +792,54 @@
|
|||||||
}, true);
|
}, 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 = `<option value="">— No package —</option>` +
|
||||||
|
pkgs.map(p => `<option value="${p.id}" ${a.package_id == p.id ? 'selected' : ''}>${Nova.escHtml(p.name)}</option>`).join('');
|
||||||
|
const phpOpts = ['8.3','8.2','8.1','7.4'].map(v =>
|
||||||
|
`<option value="${v}" ${a.php_version === v ? 'selected' : ''}>PHP ${v}</option>`).join('');
|
||||||
|
|
||||||
|
Nova.modal(`Edit Account — ${Nova.escHtml(a.username)}`,
|
||||||
|
`<div style="display:grid;grid-template-columns:1fr 1fr;gap:.75rem">
|
||||||
|
<div class="form-group"><label class="form-label">Email</label>
|
||||||
|
<input id="ae-email" class="form-control" type="email" value="${Nova.escHtml(a.email || '')}"></div>
|
||||||
|
<div class="form-group"><label class="form-label">Domain</label>
|
||||||
|
<input class="form-control" value="${Nova.escHtml(a.domain)}" disabled title="Domain cannot be changed"></div>
|
||||||
|
<div class="form-group"><label class="form-label">Package</label>
|
||||||
|
<select id="ae-pkg" class="form-control">${pkgOpts}</select></div>
|
||||||
|
<div class="form-group"><label class="form-label">PHP Version</label>
|
||||||
|
<select id="ae-php" class="form-control">${phpOpts}</select></div>
|
||||||
|
</div>`,
|
||||||
|
`<button class="btn btn-ghost" onclick="this.closest('.modal-overlay').remove()">Cancel</button>
|
||||||
|
<button class="btn btn-primary" onclick="adminEditAccountSave(${id})">Save Changes</button>`
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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 ─────────────────────────────────────────────────────────
|
// ── Create Account ─────────────────────────────────────────────────────────
|
||||||
async function createAccount() {
|
async function createAccount() {
|
||||||
const pkgRes = await Nova.api('packages', 'list');
|
const pkgRes = await Nova.api('packages', 'list');
|
||||||
|
|||||||
@@ -743,16 +743,16 @@
|
|||||||
|
|
||||||
function renderAccountTable(accts) {
|
function renderAccountTable(accts) {
|
||||||
if (!accts.length) return '<div class="empty" style="padding:2rem">No accounts found.</div>';
|
if (!accts.length) return '<div class="empty" style="padding:2rem">No accounts found.</div>';
|
||||||
return `<table class="table"><thead><tr><th>Username</th><th>Domain</th><th>Reseller</th><th>Package</th><th>Disk</th><th>Status</th><th>Created</th><th>Actions</th></tr></thead><tbody>
|
return `<table class="table"><thead><tr><th>Username</th><th>Domain</th><th>Package</th><th>PHP</th><th>Status</th><th>Created</th><th>Actions</th></tr></thead><tbody>
|
||||||
${accts.map(a => `<tr>
|
${accts.map(a => `<tr>
|
||||||
<td><strong>${a.username}</strong></td>
|
<td><strong>${a.username}</strong></td>
|
||||||
<td>${a.domain}</td>
|
<td>${a.domain}</td>
|
||||||
<td>${a.reseller_username || '<span class="text-muted">admin</span>'}</td>
|
<td>${a.package_name || '<span class="text-muted">—</span>'}</td>
|
||||||
<td>${a.package_name || '—'}</td>
|
<td class="text-muted text-sm">${a.php_version || '—'}</td>
|
||||||
<td>${a.disk_usage_mb || 0} MB</td>
|
|
||||||
<td>${Nova.badge(a.status, a.status==='active'?'green':a.status==='suspended'?'yellow':'red')}</td>
|
<td>${Nova.badge(a.status, a.status==='active'?'green':a.status==='suspended'?'yellow':'red')}</td>
|
||||||
<td class="text-muted text-sm">${Nova.relTime(a.created_at)}</td>
|
<td class="text-muted text-sm">${Nova.relTime(a.created_at)}</td>
|
||||||
<td style="display:flex;gap:.25rem">
|
<td style="display:flex;gap:.25rem;flex-wrap:wrap">
|
||||||
|
<button class="btn btn-xs btn-primary" onclick="adminEditAccount(${a.id})">Edit</button>
|
||||||
${a.status==='active'
|
${a.status==='active'
|
||||||
? `<button class="btn btn-xs btn-warning" onclick="adminSuspend(${a.id},'${a.username}')">Suspend</button>`
|
? `<button class="btn btn-xs btn-warning" onclick="adminSuspend(${a.id},'${a.username}')">Suspend</button>`
|
||||||
: `<button class="btn btn-xs btn-success" onclick="adminUnsuspend(${a.id})">Unsuspend</button>`}
|
: `<button class="btn btn-xs btn-success" onclick="adminUnsuspend(${a.id})">Unsuspend</button>`}
|
||||||
@@ -792,6 +792,54 @@
|
|||||||
}, true);
|
}, 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 = `<option value="">— No package —</option>` +
|
||||||
|
pkgs.map(p => `<option value="${p.id}" ${a.package_id == p.id ? 'selected' : ''}>${Nova.escHtml(p.name)}</option>`).join('');
|
||||||
|
const phpOpts = ['8.3','8.2','8.1','7.4'].map(v =>
|
||||||
|
`<option value="${v}" ${a.php_version === v ? 'selected' : ''}>PHP ${v}</option>`).join('');
|
||||||
|
|
||||||
|
Nova.modal(`Edit Account — ${Nova.escHtml(a.username)}`,
|
||||||
|
`<div style="display:grid;grid-template-columns:1fr 1fr;gap:.75rem">
|
||||||
|
<div class="form-group"><label class="form-label">Email</label>
|
||||||
|
<input id="ae-email" class="form-control" type="email" value="${Nova.escHtml(a.email || '')}"></div>
|
||||||
|
<div class="form-group"><label class="form-label">Domain</label>
|
||||||
|
<input class="form-control" value="${Nova.escHtml(a.domain)}" disabled title="Domain cannot be changed"></div>
|
||||||
|
<div class="form-group"><label class="form-label">Package</label>
|
||||||
|
<select id="ae-pkg" class="form-control">${pkgOpts}</select></div>
|
||||||
|
<div class="form-group"><label class="form-label">PHP Version</label>
|
||||||
|
<select id="ae-php" class="form-control">${phpOpts}</select></div>
|
||||||
|
</div>`,
|
||||||
|
`<button class="btn btn-ghost" onclick="this.closest('.modal-overlay').remove()">Cancel</button>
|
||||||
|
<button class="btn btn-primary" onclick="adminEditAccountSave(${id})">Save Changes</button>`
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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 ─────────────────────────────────────────────────────────
|
// ── Create Account ─────────────────────────────────────────────────────────
|
||||||
async function createAccount() {
|
async function createAccount() {
|
||||||
const pkgRes = await Nova.api('packages', 'list');
|
const pkgRes = await Nova.api('packages', 'list');
|
||||||
|
|||||||
Reference in New Issue
Block a user