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=>` + `).join('')} @@ -4301,6 +4302,13 @@ window.dockerStackAct = async (id, action) => { } }; +window.dockerStackReinstall = (id) => Nova.confirm('Reinstall this stack? Latest images will be pulled and containers restarted. Data volumes are preserved.', async () => { + Nova.toast('Reinstalling stack…', 'info', 15000); + const r = await Nova.api('docker', 'stack-reinstall', { method: 'POST', body: { stack_id: id } }); + Nova.toast(r?.success ? 'Stack reinstalled' : (r?.message||'Reinstall failed'), r?.success?'success':'error'); + if (r?.success) dockerLoadTab('stacks'); +}, true); + window.dockerStackRemove = (id) => Nova.confirm('Remove this stack? Docker Compose down will be run first.', async () => { const r = await Nova.api('docker', 'stack-remove', { method: 'DELETE', body: { stack_id: id } }); Nova.toast(r?.success ? 'Stack removed' : (r?.message||'Failed'), r?.success?'success':'error'); diff --git a/panel/public/assets/js/user.js b/panel/public/assets/js/user.js index bbe8b5a..29266a0 100644 --- a/panel/public/assets/js/user.js +++ b/panel/public/assets/js/user.js @@ -1155,6 +1155,7 @@ ${stacks.map(s=>` ? `` : ``} + `).join('')} @@ -1193,10 +1194,22 @@ window.uStackLogs = async (stackId, name) => { Nova.modal(`Logs: ${name}`, `
${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();