From 68e9184b57962c7c5c8f30e531e9ec69a73290b9 Mon Sep 17 00:00:00 2001 From: Poul Date: Mon, 25 May 2026 11:47:10 +0000 Subject: [PATCH] fix(api/destinations): use whereHas instead of ownedByCurrentTeamAPI for back-compat MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The ownedByCurrentTeamAPI scope was added to StandaloneDocker/SwarmDocker *after* 4.0.0-beta.470 — running containers on that beta hit a BadMethodCallException. Rewrites all team scoping to use whereHas('server', whereTeamId) which works against any v4.x of Coolify (StandaloneDocker.server_id -> Server.team_id has been there since the multi-team change). Also guards attachedTo() with method_exists and falls back to a manual attached-resource check covering applications + every standalone DB relation, so delete() doesn't crash on older versions either. --- .../Api/DestinationsController.php | 62 +++++++++++++++---- 1 file changed, 51 insertions(+), 11 deletions(-) diff --git a/app/Http/Controllers/Api/DestinationsController.php b/app/Http/Controllers/Api/DestinationsController.php index cd6c187b6..5db11624b 100644 --- a/app/Http/Controllers/Api/DestinationsController.php +++ b/app/Http/Controllers/Api/DestinationsController.php @@ -25,6 +25,9 @@ private function transform($d): array ]; } + /** + * Resolve the calling token's team id, or return a 403 response. + */ private function teamIdOrAbort(): int|\Illuminate\Http\JsonResponse { $teamId = getTeamIdFromToken(); @@ -35,16 +38,33 @@ private function teamIdOrAbort(): int|\Illuminate\Http\JsonResponse return $teamId; } + /** + * StandaloneDocker / SwarmDocker scoped to a team via their parent server. + * Uses whereHas instead of the model's ownedByCurrentTeamAPI() scope so the + * controller works on Coolify versions that pre-date that scope being added + * to the destination models (e.g. 4.0.0-beta.470). + */ + private function teamScopedDockers(int $teamId) + { + return [ + 'standalone' => StandaloneDocker::whereHas('server', fn ($q) => $q->whereTeamId($teamId))->get(), + 'swarm' => SwarmDocker::whereHas('server', fn ($q) => $q->whereTeamId($teamId))->get(), + ]; + } + public function index(Request $request) { $teamId = $this->teamIdOrAbort(); if (! is_int($teamId)) { return $teamId; } - $standalone = StandaloneDocker::ownedByCurrentTeamAPI($teamId)->get(); - $swarm = SwarmDocker::ownedByCurrentTeamAPI($teamId)->get(); + $sets = $this->teamScopedDockers($teamId); - return response()->json($standalone->concat($swarm)->map(fn ($d) => $this->transform($d))->values()); + return response()->json( + $sets['standalone']->concat($sets['swarm']) + ->map(fn ($d) => $this->transform($d)) + ->values() + ); } public function index_by_server(Request $request, string $server_uuid) @@ -53,7 +73,7 @@ public function index_by_server(Request $request, string $server_uuid) if (! is_int($teamId)) { return $teamId; } - $server = Server::ownedByCurrentTeamAPI($teamId)->whereUuid($server_uuid)->firstOrFail(); + $server = Server::whereTeamId($teamId)->whereUuid($server_uuid)->firstOrFail(); $list = $server->standaloneDockers->concat($server->swarmDockers); return response()->json($list->map(fn ($d) => $this->transform($d))->values()); @@ -65,8 +85,8 @@ public function show(Request $request, string $uuid) if (! is_int($teamId)) { return $teamId; } - $d = StandaloneDocker::ownedByCurrentTeamAPI($teamId)->whereUuid($uuid)->first() - ?? SwarmDocker::ownedByCurrentTeamAPI($teamId)->whereUuid($uuid)->firstOrFail(); + $d = StandaloneDocker::whereHas('server', fn ($q) => $q->whereTeamId($teamId))->whereUuid($uuid)->first() + ?? SwarmDocker::whereHas('server', fn ($q) => $q->whereTeamId($teamId))->whereUuid($uuid)->firstOrFail(); return response()->json($this->transform($d)); } @@ -77,7 +97,7 @@ public function create(Request $request, string $server_uuid) if (! is_int($teamId)) { return $teamId; } - $server = Server::ownedByCurrentTeamAPI($teamId)->whereUuid($server_uuid)->firstOrFail(); + $server = Server::whereTeamId($teamId)->whereUuid($server_uuid)->firstOrFail(); $allowed = ['name', 'network', 'type']; $extra = array_diff(array_keys($request->all()), $allowed); @@ -118,10 +138,30 @@ public function delete(Request $request, string $uuid) if (! is_int($teamId)) { return $teamId; } - $d = StandaloneDocker::ownedByCurrentTeamAPI($teamId)->whereUuid($uuid)->first() - ?? SwarmDocker::ownedByCurrentTeamAPI($teamId)->whereUuid($uuid)->firstOrFail(); - if ($d->attachedTo()) { - return response()->json(['message' => 'Destination has attached resources, detach first.'], 409); + $d = StandaloneDocker::whereHas('server', fn ($q) => $q->whereTeamId($teamId))->whereUuid($uuid)->first() + ?? SwarmDocker::whereHas('server', fn ($q) => $q->whereTeamId($teamId))->whereUuid($uuid)->firstOrFail(); + + // Guard against deleting destinations with attached resources. attachedTo() + // is recent on the destination models; fall back to a manual check for + // older Coolify versions (e.g. 4.0.0-beta.470). + if (method_exists($d, 'attachedTo')) { + if ($d->attachedTo()) { + return response()->json(['message' => 'Destination has attached resources, detach first.'], 409); + } + } else { + $hasAttached = $d->applications()->exists() + || $d->postgresqls()->exists() + || (method_exists($d, 'mysqls') && $d->mysqls()->exists()) + || (method_exists($d, 'mariadbs') && $d->mariadbs()->exists()) + || (method_exists($d, 'mongodbs') && $d->mongodbs()->exists()) + || (method_exists($d, 'redis') && $d->redis()->exists()) + || (method_exists($d, 'keydbs') && $d->keydbs()->exists()) + || (method_exists($d, 'dragonflies') && $d->dragonflies()->exists()) + || (method_exists($d, 'clickhouses') && $d->clickhouses()->exists()) + || (method_exists($d, 'services') && $d->services()->exists()); + if ($hasAttached) { + return response()->json(['message' => 'Destination has attached resources, detach first.'], 409); + } } $d->delete();