Role isolation, impersonation, account ownership, loading spinners, Docker fixes

- Enforce portal role isolation: admin/reseller/user can only auth on their own port
- Admin/reseller impersonation: Login As with cookie handoff + Return banner in user panel
- Account ownership: admin can reassign accounts to resellers, DNS NS follows
- accounts/update: ownership change cascades package + NS to new owner
- users.php endpoint: admin list/filter by role (reseller dropdown)
- Docker launch fix: uDockerUpdateParams defined before call
- Nova.loading() spinners: login, SSL, PHP switch/save, backup create, docker launch/actions
- Logo consistency: gradient CPX text on all login pages, novacpx_logo_html() in all sidebars
- BackupManager: fix DB class name, table name, column name
- DNSManager: fix settings keys (ns1_hostname/ns2_hostname)
- docker.php: resolve account_id from user uid for all actions
- Auth: impersonate sets cookie + stores return_token for seamless round-trip

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-09 02:56:45 +00:00
parent f75f124725
commit 537d52dafa
16 changed files with 618 additions and 230 deletions
+23 -16
View File
@@ -7,6 +7,13 @@ $body = json_decode(file_get_contents('php://input'), true) ?? [];
$dm = new DockerManager();
$isAdmin = $role === 'admin';
// Resolve the hosting account_id for the current non-admin user
$_userAccountId = 0;
if (!$isAdmin) {
$acctRow = DB::getInstance()->fetchOne("SELECT id FROM accounts WHERE user_id = ? LIMIT 1", [$currentUser['uid']]);
$_userAccountId = $acctRow ? (int)$acctRow['id'] : 0;
}
match ($action) {
// ── Engine ──────────────────────────────────────────────────────────────
@@ -30,21 +37,21 @@ match ($action) {
})(),
// ── Containers ──────────────────────────────────────────────────────────
'containers' => (function() use ($dm, $currentUser, $isAdmin, $role) {
'containers' => (function() use ($dm, $currentUser, $isAdmin, $role, $_userAccountId) {
$accountId = $isAdmin ? (isset($_GET['account_id']) ? (int)$_GET['account_id'] : null)
: ($currentUser['account_id'] ?? null);
: ($_userAccountId ?? null);
if ($role === 'reseller') $accountId = null; // resellers see their customers' containers below
$list = $dm->listContainers($accountId);
Response::success(['containers' => $list]);
})(),
'container-action' => (function() use ($dm, $body, $currentUser, $isAdmin, $role) {
'container-action' => (function() use ($dm, $body, $currentUser, $isAdmin, $role, $_userAccountId) {
$cid = $body['container_id'] ?? '';
$act = $body['action'] ?? '';
if (!$cid || !$act) Response::error('container_id and action required');
// Access check for non-admins
if (!$isAdmin) {
$acctId = $currentUser['account_id'] ?? 0;
$acctId = $_userAccountId ?? 0;
$row = DB::getInstance()->fetchOne("SELECT id FROM docker_containers WHERE container_id=? AND account_id=?", [$cid, $acctId]);
if (!$row) Response::error('Container not found', 404);
}
@@ -53,12 +60,12 @@ match ($action) {
Response::success(['output' => $out]);
})(),
'container-remove' => (function() use ($dm, $body, $currentUser, $isAdmin) {
'container-remove' => (function() use ($dm, $body, $currentUser, $isAdmin, $_userAccountId) {
$cid = $body['container_id'] ?? '';
$force = (bool)($body['force'] ?? false);
if (!$cid) Response::error('container_id required');
if (!$isAdmin) {
$acctId = $currentUser['account_id'] ?? 0;
$acctId = $_userAccountId ?? 0;
$row = DB::getInstance()->fetchOne("SELECT id FROM docker_containers WHERE container_id=? AND account_id=?", [$cid, $acctId]);
if (!$row) Response::error('Container not found', 404);
}
@@ -67,12 +74,12 @@ match ($action) {
Response::success(null, 'Container removed');
})(),
'container-logs' => (function() use ($dm, $currentUser, $isAdmin) {
'container-logs' => (function() use ($dm, $currentUser, $isAdmin, $_userAccountId) {
$cid = $_GET['container_id'] ?? '';
$lines = min((int)($_GET['lines'] ?? 100), 500);
if (!$cid) Response::error('container_id required');
if (!$isAdmin) {
$acctId = $currentUser['account_id'] ?? 0;
$acctId = $_userAccountId ?? 0;
$row = DB::getInstance()->fetchOne("SELECT id FROM docker_containers WHERE container_id=? AND account_id=?", [$cid, $acctId]);
if (!$row) Response::error('Container not found', 404);
}
@@ -88,12 +95,12 @@ match ($action) {
Response::success(['inspect' => $data]);
})(),
'container-run' => (function() use ($dm, $body, $currentUser, $isAdmin, $role) {
'container-run' => (function() use ($dm, $body, $currentUser, $isAdmin, $role, $_userAccountId) {
if ($isAdmin) {
$accountId = (int)($body['account_id'] ?? 0);
if (!$accountId) Response::error('account_id required');
} else {
$accountId = $currentUser['account_id'] ?? 0;
$accountId = $_userAccountId ?? 0;
}
if (!$accountId) Response::error('No account context');
$image = $body['image'] ?? '';
@@ -140,14 +147,14 @@ match ($action) {
})(),
// ── Compose Stacks ───────────────────────────────────────────────────────
'stacks' => (function() use ($dm, $currentUser, $isAdmin) {
'stacks' => (function() use ($dm, $currentUser, $isAdmin, $_userAccountId) {
$accountId = $isAdmin ? (isset($_GET['account_id']) ? (int)$_GET['account_id'] : null)
: ($currentUser['account_id'] ?? null);
: ($_userAccountId ?? null);
Response::success(['stacks' => $dm->listStacks($accountId)]);
})(),
'stack-create' => (function() use ($dm, $body, $currentUser, $isAdmin) {
$accountId = $isAdmin ? ($body['account_id'] ?? null) : ($currentUser['account_id'] ?? null);
'stack-create' => (function() use ($dm, $body, $currentUser, $isAdmin, $_userAccountId) {
$accountId = $isAdmin ? ($body['account_id'] ?? null) : ($_userAccountId ?? null);
$name = $body['name'] ?? '';
$yaml = $body['compose_yaml'] ?? '';
if (!$name || !$yaml) Response::error('name and compose_yaml required');
@@ -195,8 +202,8 @@ match ($action) {
Response::success(['catalog' => DockerManager::getCatalog()]);
})(),
'launch' => (function() use ($dm, $body, $currentUser, $isAdmin) {
$accountId = $isAdmin ? (int)($body['account_id'] ?? 0) : ($currentUser['account_id'] ?? 0);
'launch' => (function() use ($dm, $body, $currentUser, $isAdmin, $_userAccountId) {
$accountId = $isAdmin ? (int)($body['account_id'] ?? 0) : ($_userAccountId ?? 0);
if (!$accountId) Response::error('account_id required');
$appKey = $body['app_key'] ?? '';
$params = $body['params'] ?? [];