diff --git a/panel/api/endpoints/docker.php b/panel/api/endpoints/docker.php index 0fc6ee6..32e99e1 100644 --- a/panel/api/endpoints/docker.php +++ b/panel/api/endpoints/docker.php @@ -193,20 +193,42 @@ match ($action) { Response::success($result, 'Stack created'); })(), - 'stack-action' => (function() use ($dm, $body, $isAdmin) { - if (!$isAdmin) Response::error('Admin only', 403); + 'stack-action' => (function() use ($dm, $body, $isAdmin, $_userAccountId) { $id = (int)($body['stack_id'] ?? 0); $act = $body['action'] ?? ''; if (!$id || !$act) Response::error('stack_id and action required'); + // Non-admins can only act on their own stacks + if (!$isAdmin) { + $stack = DB::getInstance()->fetchOne("SELECT account_id FROM docker_compose_stacks WHERE id = ?", [$id]); + if (!$stack || (int)$stack['account_id'] !== (int)$_userAccountId) Response::error('Not found', 404); + } $out = $dm->composeAction($id, $act); audit("docker.stack.{$act}", "stack:{$id}"); Response::success(['output' => $out]); })(), - 'stack-remove' => (function() use ($dm, $body, $isAdmin) { - if (!$isAdmin) Response::error('Admin only', 403); + 'stack-reinstall' => (function() use ($dm, $body, $isAdmin, $_userAccountId) { $id = (int)($body['stack_id'] ?? 0); if (!$id) Response::error('stack_id required'); + if (!$isAdmin) { + $stack = DB::getInstance()->fetchOne("SELECT account_id FROM docker_compose_stacks WHERE id = ?", [$id]); + if (!$stack || (int)$stack['account_id'] !== (int)$_userAccountId) Response::error('Not found', 404); + } + // Pull latest images, bring down (removing volumes), then start fresh + $dm->composeAction($id, 'pull'); + $dm->composeAction($id, 'down'); + $out = $dm->composeAction($id, 'up'); + audit('docker.stack.reinstall', "stack:{$id}"); + Response::success(['output' => $out], 'Stack reinstalled'); + })(), + + 'stack-remove' => (function() use ($dm, $body, $isAdmin, $_userAccountId) { + $id = (int)($body['stack_id'] ?? 0); + if (!$id) Response::error('stack_id required'); + if (!$isAdmin) { + $stack = DB::getInstance()->fetchOne("SELECT account_id FROM docker_compose_stacks WHERE id = ?", [$id]); + if (!$stack || (int)$stack['account_id'] !== (int)$_userAccountId) Response::error('Not found', 404); + } $dm->removeStack($id); audit('docker.stack.remove', "stack:{$id}"); Response::success(null, 'Stack removed'); diff --git a/panel/public/assets/js/admin.js b/panel/public/assets/js/admin.js index b74715b..73edc98 100644 --- a/panel/public/assets/js/admin.js +++ b/panel/public/assets/js/admin.js @@ -4143,6 +4143,7 @@ ${stacks.map(s=>`
${Nova.escHtml(r?.data?.output||'No logs available')}`);
};
+window.uStackReinstall = (stackId, name) => Nova.confirm(
+ `Reinstall "${name}"? This will pull the latest images, restart all containers, and reset to a fresh state. Your data volumes will be preserved.`,
+ async () => {
+ Nova.loading(`Reinstalling ${name}…`);
+ const r = await Nova.api('docker', 'stack-reinstall', { method: 'POST', body: { stack_id: stackId } });
+ Nova.loadingDone();
+ Nova.toast(r?.success ? `${name} reinstalled` : (r?.message || 'Reinstall failed'), r?.success ? 'success' : 'error');
+ if (r?.success) await uDockerReloadStacks();
+ },
+ true
+);
+
window.uStackRemove = async (stackId, name) => {
if (!confirm(`Remove app "${name}"? This will stop and delete its containers and data.`)) return;
Nova.loading('Removing app…');
- const r = await Nova.api('docker', 'remove-stack', { method: 'POST', body: { stack_id: stackId } });
+ const r = await Nova.api('docker', 'stack-remove', { method: 'POST', body: { stack_id: stackId } });
Nova.loadingDone();
Nova.toast(r?.success ? 'App removed' : (r?.message||'Failed'), r?.success?'success':'error');
if (r?.success) await uDockerReloadStacks();