diff --git a/app/Actions/Service/DeployServiceApplication.php b/app/Actions/Service/DeployServiceApplication.php index f79437a26..363166bcc 100644 --- a/app/Actions/Service/DeployServiceApplication.php +++ b/app/Actions/Service/DeployServiceApplication.php @@ -22,35 +22,42 @@ public function handle(ServiceApplication $serviceApplication, bool $pullLatestI $service->isConfigurationChanged(save: true); $workdir = $service->workdir(); + $composeFile = "{$workdir}/docker-compose.yml"; + $safeWorkdir = escapeshellarg($workdir); + $safeComposeFile = escapeshellarg($composeFile); + $safeProjectName = escapeshellarg($service->uuid); + $safeComposeServiceName = escapeshellarg($composeServiceName); + $commands = collect([ - "echo 'Saved configuration files to {$workdir}.'", - "touch {$workdir}/.env", + 'echo '.escapeshellarg("Saved configuration files to {$workdir}."), + 'touch '.escapeshellarg("{$workdir}/.env"), ]); if ($pullLatestImages) { $commands->push('echo Pulling image for service.'); - $commands->push("docker compose --project-directory {$workdir} -f {$workdir}/docker-compose.yml --project-name {$service->uuid} pull {$composeServiceName}"); + $commands->push("docker compose --project-directory {$safeWorkdir} -f {$safeComposeFile} --project-name {$safeProjectName} pull {$safeComposeServiceName}"); } if ($service->networks()->count() > 0) { $commands->push('echo Creating Docker network.'); - $commands->push("docker network inspect {$service->uuid} >/dev/null 2>&1 || docker network create --attachable {$service->uuid}"); + $commands->push("docker network inspect {$safeProjectName} >/dev/null 2>&1 || docker network create --attachable {$safeProjectName}"); } - $upCommand = "docker compose --project-directory {$workdir} -f {$workdir}/docker-compose.yml --project-name {$service->uuid} up -d --no-deps"; + $upCommand = "docker compose --project-directory {$safeWorkdir} -f {$safeComposeFile} --project-name {$safeProjectName} up -d --no-deps"; if ($forceRebuild) { $upCommand .= ' --build'; } - $upCommand .= " {$composeServiceName}"; + $upCommand .= " {$safeComposeServiceName}"; $commands->push('echo Starting service container.'); $commands->push($upCommand); - $commands->push("docker network connect {$service->uuid} coolify-proxy >/dev/null 2>&1 || true"); + $commands->push("docker network connect {$safeProjectName} coolify-proxy >/dev/null 2>&1 || true"); if (data_get($service, 'connect_to_docker_network')) { - $compose = data_get($service, 'docker_compose', []); - $network = $service->destination->network; - $commands->push("docker network connect --alias {$composeServiceName}-{$service->uuid} {$network} {$composeServiceName}-{$service->uuid} >/dev/null 2>&1 || true"); + $network = escapeshellarg($service->destination->network); + $containerName = escapeshellarg("{$composeServiceName}-{$service->uuid}"); + $networkAlias = escapeshellarg("{$composeServiceName}-{$service->uuid}"); + $commands->push("docker network connect --alias {$networkAlias} {$network} {$containerName} >/dev/null 2>&1 || true"); } return remote_process($commands->toArray(), $service->server, type_uuid: $service->uuid, callEventOnFinish: 'ServiceStatusChanged'); diff --git a/app/Actions/Service/RestartServiceApplication.php b/app/Actions/Service/RestartServiceApplication.php index f5a7883e5..c83cbe660 100644 --- a/app/Actions/Service/RestartServiceApplication.php +++ b/app/Actions/Service/RestartServiceApplication.php @@ -15,7 +15,7 @@ public function handle(ServiceApplication $serviceApplication): void { $service = $serviceApplication->service; $server = $service->destination->server; - $containerName = $serviceApplication->name.'-'.$service->uuid; + $containerName = escapeshellarg($serviceApplication->name.'-'.$service->uuid); instant_remote_process([ "docker restart {$containerName}", diff --git a/app/Actions/Service/StopServiceApplication.php b/app/Actions/Service/StopServiceApplication.php index d34f1f3c8..cc1afbb96 100644 --- a/app/Actions/Service/StopServiceApplication.php +++ b/app/Actions/Service/StopServiceApplication.php @@ -15,7 +15,7 @@ public function handle(ServiceApplication $serviceApplication): void { $service = $serviceApplication->service; $server = $service->destination->server; - $containerName = $serviceApplication->name.'-'.$service->uuid; + $containerName = escapeshellarg($serviceApplication->name.'-'.$service->uuid); instant_remote_process([ "docker stop {$containerName}", diff --git a/app/Actions/Service/UpdateServiceApplicationFromApi.php b/app/Actions/Service/UpdateServiceApplicationFromApi.php index 8a2ce6ecd..7bc04dfdb 100644 --- a/app/Actions/Service/UpdateServiceApplicationFromApi.php +++ b/app/Actions/Service/UpdateServiceApplicationFromApi.php @@ -9,12 +9,12 @@ class UpdateServiceApplicationFromApi { - public function execute(ServiceApplication $serviceApplication, Request $request, string $teamId): ?JsonResponse + public function execute(ServiceApplication $serviceApplication, Request $request, string $teamId, array $payload): ?JsonResponse { $forceDomainOverride = $request->boolean('force_domain_override'); - if ($request->has('url')) { - $urlRaw = $request->input('url'); + if (array_key_exists('url', $payload)) { + $urlRaw = $payload['url']; if ($urlRaw !== null && ! is_string($urlRaw)) { return response()->json([ 'message' => 'Validation failed.', @@ -59,38 +59,38 @@ public function execute(ServiceApplication $serviceApplication, Request $request $serviceApplication->fqdn = $parsed['normalized']; } - if ($request->has('human_name')) { - $serviceApplication->human_name = $request->input('human_name'); + if (array_key_exists('human_name', $payload)) { + $serviceApplication->human_name = $payload['human_name']; } - if ($request->has('description')) { - $serviceApplication->description = $request->input('description'); + if (array_key_exists('description', $payload)) { + $serviceApplication->description = $payload['description']; } - if ($request->has('image')) { - $serviceApplication->image = $request->input('image'); + if (array_key_exists('image', $payload)) { + $serviceApplication->image = $payload['image']; } - if ($request->has('exclude_from_status')) { - $serviceApplication->exclude_from_status = $request->boolean('exclude_from_status'); + if (array_key_exists('exclude_from_status', $payload)) { + $serviceApplication->exclude_from_status = filter_var($payload['exclude_from_status'], FILTER_VALIDATE_BOOLEAN); } - if ($request->has('is_gzip_enabled')) { - $serviceApplication->is_gzip_enabled = $request->boolean('is_gzip_enabled'); + if (array_key_exists('is_gzip_enabled', $payload)) { + $serviceApplication->is_gzip_enabled = filter_var($payload['is_gzip_enabled'], FILTER_VALIDATE_BOOLEAN); } - if ($request->has('is_stripprefix_enabled')) { - $serviceApplication->is_stripprefix_enabled = $request->boolean('is_stripprefix_enabled'); + if (array_key_exists('is_stripprefix_enabled', $payload)) { + $serviceApplication->is_stripprefix_enabled = filter_var($payload['is_stripprefix_enabled'], FILTER_VALIDATE_BOOLEAN); } - if ($request->has('is_log_drain_enabled')) { - $enabled = $request->boolean('is_log_drain_enabled'); + if (array_key_exists('is_log_drain_enabled', $payload)) { + $enabled = filter_var($payload['is_log_drain_enabled'], FILTER_VALIDATE_BOOLEAN); $server = $serviceApplication->service->destination->server; if ($enabled && ! $server->isLogDrainEnabled()) { return response()->json([ 'message' => 'Validation failed.', 'errors' => [ - 'is_log_drain_enabled' => 'Log drain is not enabled on the server for this service.', + 'is_log_drain_enabled' => ['Log drain is not enabled on the server for this service.'], ], ], 422); } diff --git a/app/Http/Controllers/Api/ServiceApplicationsController.php b/app/Http/Controllers/Api/ServiceApplicationsController.php index 75a48d112..de07f2e33 100644 --- a/app/Http/Controllers/Api/ServiceApplicationsController.php +++ b/app/Http/Controllers/Api/ServiceApplicationsController.php @@ -306,6 +306,11 @@ public function update(Request $request, UpdateServiceApplicationFromApi $update $this->authorize('update', $serviceApplication); + $payload = $request->json()->all(); + if (empty($payload)) { + $payload = $request->request->all(); + } + $allowedFields = [ 'url', 'human_name', @@ -328,9 +333,9 @@ public function update(Request $request, UpdateServiceApplicationFromApi $update 'is_stripprefix_enabled' => 'sometimes|boolean', ]; - $validator = Validator::make($request->all(), $validationRules); + $validator = Validator::make($payload, $validationRules); - $extraFields = array_diff(array_keys($request->all()), $allowedFields); + $extraFields = array_diff(array_keys($payload), $allowedFields); if ($validator->fails() || ! empty($extraFields)) { $errors = $validator->errors(); foreach ($extraFields as $field) { @@ -343,7 +348,7 @@ public function update(Request $request, UpdateServiceApplicationFromApi $update ], 422); } - $response = $updateServiceApplicationFromApi->execute($serviceApplication, $request, $teamId); + $response = $updateServiceApplicationFromApi->execute($serviceApplication, $request, $teamId, $payload); if ($response instanceof JsonResponse) { return $response; } @@ -450,8 +455,9 @@ public function logs_by_uuid(Request $request): JsonResponse } $containerName = $serviceApplication->name.'-'.$serviceApplication->service->uuid; + $safeContainerName = escapeshellarg($containerName); - $status = getContainerStatus($server, $containerName); + $status = getContainerStatus($server, $safeContainerName); if ($status !== 'running') { return response()->json([ 'message' => 'Service application container is not running.', diff --git a/templates/service-templates-latest.json b/templates/service-templates-latest.json index 19c213764..0a2dfda9d 100644 --- a/templates/service-templates-latest.json +++ b/templates/service-templates-latest.json @@ -31,7 +31,7 @@ "category": "finance", "logo": "svgs/actualbudget.png", "minversion": "0.0.0", - "template_last_updated_at": "2025-08-17T18:23:57+02:00", + "template_last_updated_at": "2026-04-06T11:35:16-05:00", "port": "5006" }, "affine": { @@ -64,7 +64,7 @@ "category": "productivity", "logo": "svgs/alexandrie.svg", "minversion": "0.0.0", - "template_last_updated_at": "2026-01-27T21:03:32+01:00", + "template_last_updated_at": "2026-04-05T13:36:24+02:00", "port": "8200" }, "anythingllm": { @@ -175,7 +175,7 @@ "category": "ai", "logo": "svgs/argilla.png", "minversion": "0.0.0", - "template_last_updated_at": "2025-08-17T18:23:57+02:00", + "template_last_updated_at": "2026-04-06T11:35:16-05:00", "port": "6900" }, "audiobookshelf": { @@ -192,7 +192,7 @@ "category": "media", "logo": "svgs/audiobookshelf.svg", "minversion": "0.0.0", - "template_last_updated_at": "2025-08-17T18:23:57+02:00", + "template_last_updated_at": "2026-05-12T09:09:36+02:00", "port": "80" }, "authentik": { @@ -231,7 +231,7 @@ "category": "database", "logo": "svgs/autobase.svg", "minversion": "0.0.0", - "template_last_updated_at": "2026-01-19T18:13:51+01:00", + "template_last_updated_at": "2026-04-06T11:35:16-05:00", "port": "80" }, "babybuddy": { @@ -280,7 +280,7 @@ "category": "monitoring", "logo": "svgs/beszel.svg", "minversion": "0.0.0", - "template_last_updated_at": "2026-02-21T23:17:23+05:30" + "template_last_updated_at": "2026-04-24T02:22:08+05:30" }, "beszel": { "documentation": "https://github.com/henrygd/beszel?tab=readme-ov-file#getting-started?utm_source=coolify.io", @@ -296,7 +296,7 @@ "category": "monitoring", "logo": "svgs/beszel.svg", "minversion": "0.0.0", - "template_last_updated_at": "2026-02-21T23:17:23+05:30", + "template_last_updated_at": "2026-04-24T02:21:33+05:30", "port": "8090" }, "bitcoin-core": { @@ -326,7 +326,7 @@ "category": "backend", "logo": "svgs/bluesky.svg", "minversion": "0.0.0", - "template_last_updated_at": "2026-02-03T22:12:21+01:00", + "template_last_updated_at": "2026-05-09T19:19:48+05:30", "port": "3000" }, "bookstack": { @@ -377,7 +377,7 @@ "category": "finance", "logo": "svgs/budge.png", "minversion": "0.0.0", - "template_last_updated_at": "2025-08-17T18:23:57+02:00" + "template_last_updated_at": "2026-04-06T11:35:16-05:00" }, "budibase": { "documentation": "https://docs.budibase.com/docs/docker-compose?utm_source=coolify.io", @@ -438,7 +438,7 @@ "category": "media", "logo": "svgs/calibre-web-automated-with-downloader.png", "minversion": "0.0.0", - "template_last_updated_at": "2026-01-08T21:32:32+01:00", + "template_last_updated_at": "2026-04-06T11:35:16-05:00", "port": "8083" }, "calibre-web": { @@ -475,7 +475,7 @@ "category": "security", "logo": "svgs/cap-captcha.png", "minversion": "0.0.0", - "template_last_updated_at": null, + "template_last_updated_at": "2026-04-23T01:07:14+05:30", "port": "3000" }, "cap": { @@ -493,7 +493,7 @@ "category": "media", "logo": "svgs/cap.svg", "minversion": "0.0.0", - "template_last_updated_at": "2025-10-14T23:42:45+02:00", + "template_last_updated_at": "2026-04-06T11:35:16-05:00", "port": "5679" }, "castopod": { @@ -552,7 +552,7 @@ "category": "helpdesk", "logo": "svgs/chaskiq.png", "minversion": "0.0.0", - "template_last_updated_at": "2025-08-17T18:23:57+02:00", + "template_last_updated_at": "2026-04-06T11:35:16-05:00", "port": "3000" }, "chatwoot": { @@ -573,7 +573,7 @@ "category": "helpdesk", "logo": "svgs/chatwoot.svg", "minversion": "0.0.0", - "template_last_updated_at": "2025-08-17T18:23:57+02:00", + "template_last_updated_at": "2026-05-27T09:31:29-03:00", "port": "3000" }, "checkmate": { @@ -605,7 +605,7 @@ "category": "storage", "logo": "svgs/chibisafe.svg", "minversion": "0.0.0", - "template_last_updated_at": "2026-01-12T22:51:57+01:00", + "template_last_updated_at": "2026-04-06T11:35:16-05:00", "port": "80" }, "chroma": { @@ -704,7 +704,7 @@ "category": "automation", "logo": "svgs/cloudflare-ddns.svg", "minversion": "0.0.0", - "template_last_updated_at": null + "template_last_updated_at": "2026-05-18T15:42:31+10:00" }, "cloudflared": { "documentation": "https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/?utm_source=coolify.io", @@ -803,7 +803,7 @@ "category": "backend", "logo": "svgs/convex.svg", "minversion": "0.0.0", - "template_last_updated_at": "2025-11-10T14:16:14+01:00", + "template_last_updated_at": "2026-06-12T10:45:52+02:00", "port": "6791" }, "cryptgeon": { @@ -903,7 +903,7 @@ "category": "cms", "logo": "svgs/directus.svg", "minversion": "0.0.0", - "template_last_updated_at": "2025-08-17T18:23:57+02:00", + "template_last_updated_at": "2026-04-03T19:25:51+05:30", "port": "8055" }, "directus": { @@ -919,7 +919,7 @@ "category": "cms", "logo": "svgs/directus.svg", "minversion": "0.0.0", - "template_last_updated_at": "2025-08-17T18:23:57+02:00", + "template_last_updated_at": "2026-04-03T19:26:06+05:30", "port": "8055" }, "diun": { @@ -968,7 +968,7 @@ "category": "productivity", "logo": "svgs/docmost.png", "minversion": "0.0.0", - "template_last_updated_at": "2025-09-27T04:00:56+02:00", + "template_last_updated_at": "2026-05-15T13:36:02+02:00", "port": "3000" }, "documenso": { @@ -1044,7 +1044,7 @@ "category": "productivity", "logo": "svgs/dolibarr.png", "minversion": "0.0.0", - "template_last_updated_at": "2025-12-18T10:27:24+01:00", + "template_last_updated_at": "2026-04-06T11:35:16-05:00", "port": "80" }, "dozzle-with-auth": { @@ -1093,7 +1093,7 @@ "category": "devtools", "logo": "svgs/drizzle.jpeg", "minversion": "0.0.0", - "template_last_updated_at": "2025-08-17T18:23:57+02:00", + "template_last_updated_at": "2026-04-06T11:35:16-05:00", "port": "4983" }, "drupal-with-postgresql": { @@ -1161,7 +1161,7 @@ "category": "monitoring", "logo": "svgs/elasticsearch.svg", "minversion": "0.0.0", - "template_last_updated_at": "2026-01-27T21:34:40+05:30", + "template_last_updated_at": "2026-04-09T00:43:59-05:00", "port": "5601" }, "elasticsearch": { @@ -1248,7 +1248,7 @@ "category": "Networking", "logo": "svgs/emqx-enterprise.svg", "minversion": "0.0.0", - "template_last_updated_at": null, + "template_last_updated_at": "2026-04-27T09:25:48+03:00", "port": "18083" }, "ente-photos-with-s3": { @@ -1322,7 +1322,7 @@ "category": "productivity", "logo": "svgs/espocrm.svg", "minversion": "0.0.0", - "template_last_updated_at": "2026-03-16T18:23:47+02:00", + "template_last_updated_at": "2026-04-06T11:35:16-05:00", "port": "80" }, "evolution-api": { @@ -1436,7 +1436,7 @@ "category": "finance", "logo": "svgs/firefly.svg", "minversion": "0.0.0", - "template_last_updated_at": "2025-08-17T18:23:57+02:00", + "template_last_updated_at": "2026-04-06T11:35:16-05:00", "port": "8080" }, "firefox": { @@ -1625,7 +1625,7 @@ "category": "productivity", "logo": "svgs/formbricks.png", "minversion": "0.0.0", - "template_last_updated_at": "2026-01-27T21:12:57+01:00", + "template_last_updated_at": "2026-04-06T11:35:16-05:00", "port": "3000" }, "foundryvtt": { @@ -1642,7 +1642,7 @@ "category": "games", "logo": "svgs/foundryvtt.png", "minversion": "0.0.0", - "template_last_updated_at": "2025-08-17T18:23:57+02:00", + "template_last_updated_at": "2026-04-06T11:35:16-05:00", "port": "30000" }, "freescout": { @@ -1658,7 +1658,7 @@ "category": "helpdesk", "logo": "svgs/freescout.png", "minversion": "0.0.0", - "template_last_updated_at": "2025-08-17T18:23:57+02:00", + "template_last_updated_at": "2026-04-06T11:35:16-05:00", "port": "80" }, "freshrss-with-mariadb": { @@ -1672,7 +1672,7 @@ "category": "RSS", "logo": "svgs/freshrss.png", "minversion": "0.0.0", - "template_last_updated_at": "2025-08-17T18:23:57+02:00", + "template_last_updated_at": "2026-04-06T11:35:16-05:00", "port": "80" }, "freshrss-with-mysql": { @@ -1686,7 +1686,7 @@ "category": "RSS", "logo": "svgs/freshrss.png", "minversion": "0.0.0", - "template_last_updated_at": "2025-08-17T18:23:57+02:00", + "template_last_updated_at": "2026-04-06T11:35:16-05:00", "port": "80" }, "freshrss-with-postgresql": { @@ -1700,7 +1700,7 @@ "category": "RSS", "logo": "svgs/freshrss.png", "minversion": "0.0.0", - "template_last_updated_at": "2026-01-04T17:41:25+01:00", + "template_last_updated_at": "2026-04-06T11:35:16-05:00", "port": "80" }, "freshrss": { @@ -1714,7 +1714,7 @@ "category": "RSS", "logo": "svgs/freshrss.png", "minversion": "0.0.0", - "template_last_updated_at": "2025-08-17T18:23:57+02:00", + "template_last_updated_at": "2026-04-06T11:35:16-05:00", "port": "80" }, "garage": { @@ -1732,7 +1732,7 @@ "category": "storage", "logo": "svgs/garage.svg", "minversion": "0.0.0", - "template_last_updated_at": "2025-12-10T12:57:34+01:00", + "template_last_updated_at": "2026-05-31T23:57:46+02:00", "port": "3900" }, "getoutline": { @@ -1746,7 +1746,7 @@ "category": "productivity", "logo": "svgs/getoutline.jpeg", "minversion": "0.0.0", - "template_last_updated_at": "2025-12-16T14:12:15+07:00", + "template_last_updated_at": "2026-05-09T19:19:29+05:30", "port": "3000" }, "ghost": { @@ -1779,7 +1779,7 @@ "category": "devtools", "logo": "svgs/gitea.svg", "minversion": "0.0.0", - "template_last_updated_at": null + "template_last_updated_at": "2026-06-06T00:11:24+02:00" }, "gitea-with-mariadb": { "documentation": "https://docs.gitea.com?utm_source=coolify.io", @@ -1910,7 +1910,7 @@ "category": "productivity", "logo": "svgs/glance.png", "minversion": "0.0.0", - "template_last_updated_at": "2025-08-18T19:18:07+02:00", + "template_last_updated_at": "2026-04-06T11:35:16-05:00", "port": "8080" }, "glances": { @@ -2000,7 +2000,7 @@ "category": "messaging", "logo": "svgs/gotify.png", "minversion": "0.0.0", - "template_last_updated_at": "2025-10-08T11:43:21+02:00", + "template_last_updated_at": "2026-04-06T11:35:16-05:00", "port": "80" }, "gowa": { @@ -2017,7 +2017,7 @@ "category": "messaging", "logo": "svgs/gowa.svg", "minversion": "0.0.0", - "template_last_updated_at": "2025-08-17T18:23:57+02:00", + "template_last_updated_at": "2026-04-06T11:35:16-05:00", "port": "3000" }, "grafana-with-postgresql": { @@ -2080,7 +2080,7 @@ "category": null, "logo": "svgs/grimmory.svg", "minversion": "0.0.0", - "template_last_updated_at": null, + "template_last_updated_at": "2026-04-03T18:59:26+05:30", "port": "80" }, "grist": { @@ -2114,7 +2114,7 @@ "category": "productivity", "logo": "svgs/grocy.svg", "minversion": "0.0.0", - "template_last_updated_at": "2025-08-17T18:23:57+02:00" + "template_last_updated_at": "2026-05-12T09:10:44+02:00" }, "hatchet": { "documentation": "https://docs.hatchet.run/self-hosting/docker-compose?utm_source=coolify.io", @@ -2129,7 +2129,7 @@ "category": "automation", "logo": "svgs/hatchet.svg", "minversion": "0.0.0", - "template_last_updated_at": "2026-01-16T22:27:56+01:00", + "template_last_updated_at": "2026-04-06T11:35:16-05:00", "port": "80" }, "healthchecks": { @@ -2148,7 +2148,7 @@ "category": "monitoring", "logo": "svgs/healthchecks.webp", "minversion": "0.0.0", - "template_last_updated_at": null, + "template_last_updated_at": "2026-05-25T13:06:59-04:00", "port": "80000" }, "heimdall": { @@ -2183,7 +2183,7 @@ "category": "ai", "logo": "svgs/hermes-agent.png", "minversion": "0.0.0", - "template_last_updated_at": null, + "template_last_updated_at": "2026-05-29T12:14:17+05:30", "port": "8787" }, "heyform": { @@ -2218,7 +2218,7 @@ "category": "productivity", "logo": "svgs/homarr.svg", "minversion": "0.0.0", - "template_last_updated_at": "2025-10-07T12:19:45+02:00", + "template_last_updated_at": "2026-05-09T19:30:07+05:30", "port": "7575" }, "home-assistant": { @@ -2253,7 +2253,7 @@ "category": "productivity", "logo": "svgs/homebox.svg", "minversion": "0.0.0", - "template_last_updated_at": "2025-08-17T18:23:57+02:00", + "template_last_updated_at": "2026-04-06T11:35:16-05:00", "port": "7745" }, "homepage": { @@ -2361,7 +2361,7 @@ "category": "automation", "logo": "svgs/inngest.png", "minversion": "0.0.0", - "template_last_updated_at": null, + "template_last_updated_at": "2026-07-02T13:25:47+02:00", "port": "8288" }, "invoice-ninja": { @@ -2378,7 +2378,7 @@ "category": "finance", "logo": "svgs/invoiceninja.png", "minversion": "0.0.0", - "template_last_updated_at": "2025-08-17T18:23:57+02:00", + "template_last_updated_at": "2026-04-06T11:35:16-05:00", "port": "9000" }, "it-tools": { @@ -2410,7 +2410,7 @@ "category": "media", "logo": "svgs/jellyfin.svg", "minversion": "0.0.0", - "template_last_updated_at": "2025-08-17T18:23:57+02:00", + "template_last_updated_at": "2026-05-12T08:58:05+02:00", "port": "8096" }, "jenkins": { @@ -2443,7 +2443,7 @@ "category": "productivity", "logo": "svgs/jitsi.svg", "minversion": "0.0.0", - "template_last_updated_at": "2025-08-17T18:23:57+02:00", + "template_last_updated_at": "2026-04-24T09:40:01+05:30", "port": "80" }, "joomla-with-mariadb": { @@ -2672,7 +2672,7 @@ "category": "ai", "logo": "svgs/langfuse.svg", "minversion": "0.0.0", - "template_last_updated_at": "2026-03-28T16:21:57+01:00", + "template_last_updated_at": "2026-04-23T18:08:40+02:00", "port": "3000" }, "leantime": { @@ -2713,7 +2713,7 @@ "category": "ai", "logo": "svgs/librechat.svg", "minversion": "0.0.0", - "template_last_updated_at": "2025-08-17T18:23:57+02:00", + "template_last_updated_at": "2026-04-03T18:29:44+05:30", "port": "3080" }, "libreoffice": { @@ -2881,7 +2881,7 @@ "category": "auth", "logo": "svgs/logto_dark.svg", "minversion": "0.0.0", - "template_last_updated_at": "2025-08-17T18:23:57+02:00" + "template_last_updated_at": "2026-04-01T13:19:47Z" }, "lowcoder": { "documentation": "https://docs.lowcoder.cloud/?utm_source=coolify.io", @@ -3005,7 +3005,7 @@ "category": "messaging", "logo": "svgs/mattermost.svg", "minversion": "0.0.0", - "template_last_updated_at": "2025-09-04T16:02:48+03:30", + "template_last_updated_at": "2026-04-06T13:57:40-05:00", "port": "8065" }, "mealie": { @@ -3021,7 +3021,7 @@ "category": "productivity", "logo": "svgs/mealie.png", "minversion": "0.0.0", - "template_last_updated_at": "2025-08-17T18:23:57+02:00", + "template_last_updated_at": "2026-05-12T09:22:15+02:00", "port": "9000" }, "mediawiki": { @@ -3149,7 +3149,7 @@ "category": "games", "logo": "svgs/minecraft.svg", "minversion": "0.0.0", - "template_last_updated_at": "2025-08-17T18:23:57+02:00", + "template_last_updated_at": "2026-04-01T15:06:17-05:00", "port": "25565" }, "miniflux": { @@ -3166,7 +3166,7 @@ "category": "RSS", "logo": "svgs/miniflux.svg", "minversion": "0.0.0", - "template_last_updated_at": "2025-08-17T18:23:57+02:00", + "template_last_updated_at": "2026-04-06T11:35:16-05:00", "port": "8080" }, "mixpost": { @@ -3242,7 +3242,7 @@ "category": "automation", "logo": "svgs/n8n.png", "minversion": "0.0.0", - "template_last_updated_at": "2026-03-08T01:08:18-05:00", + "template_last_updated_at": "2026-03-30T20:40:07+02:00", "port": "5678" }, "n8n-with-postgresql": { @@ -3261,7 +3261,7 @@ "category": "automation", "logo": "svgs/n8n.png", "minversion": "0.0.0", - "template_last_updated_at": "2026-02-27T22:41:52-05:00", + "template_last_updated_at": "2026-03-30T20:40:07+02:00", "port": "5678" }, "n8n": { @@ -3280,7 +3280,7 @@ "category": "automation", "logo": "svgs/n8n.png", "minversion": "0.0.0", - "template_last_updated_at": "2026-02-27T22:41:52-05:00", + "template_last_updated_at": "2026-03-30T20:40:07+02:00", "port": "5678" }, "navidrome": { @@ -3328,7 +3328,7 @@ "category": "vpn", "logo": "svgs/netbird.png", "minversion": "0.0.0", - "template_last_updated_at": "2025-11-16T10:33:10+05:30" + "template_last_updated_at": "2026-04-09T00:00:38+05:30" }, "newapi": { "documentation": "https://docs.newapi.pro/en/getting-started/?utm_source=coolify.io", @@ -3360,7 +3360,7 @@ "category": "proxy", "logo": "svgs/pangolin-logo.png", "minversion": "0.0.0", - "template_last_updated_at": "2025-11-12T11:59:36-03:00" + "template_last_updated_at": "2026-04-06T11:35:16-05:00" }, "next-image-transformation": { "documentation": "https://github.com/coollabsio/next-image-transformation?utm_source=coolify.io", @@ -3392,7 +3392,7 @@ "category": "storage", "logo": "svgs/nextcloud.svg", "minversion": "0.0.0", - "template_last_updated_at": "2025-08-17T18:23:57+02:00", + "template_last_updated_at": "2026-04-07T23:32:57+05:30", "port": "80" }, "nextcloud-with-mysql": { @@ -3409,7 +3409,7 @@ "category": "storage", "logo": "svgs/nextcloud.svg", "minversion": "0.0.0", - "template_last_updated_at": "2025-08-17T18:23:57+02:00", + "template_last_updated_at": "2026-04-07T23:32:57+05:30", "port": "80" }, "nextcloud-with-postgres": { @@ -3426,7 +3426,7 @@ "category": "storage", "logo": "svgs/nextcloud.svg", "minversion": "0.0.0", - "template_last_updated_at": "2025-08-17T18:23:57+02:00", + "template_last_updated_at": "2026-04-07T23:32:57+05:30", "port": "80" }, "nextcloud": { @@ -3443,7 +3443,7 @@ "category": "storage", "logo": "svgs/nextcloud.svg", "minversion": "0.0.0", - "template_last_updated_at": "2025-08-17T18:23:57+02:00", + "template_last_updated_at": "2026-04-07T23:32:57+05:30", "port": "80" }, "nexus-arm": { @@ -3554,7 +3554,7 @@ "category": "productivity", "logo": "svgs/nocodb.svg", "minversion": "0.0.0", - "template_last_updated_at": "2025-08-17T18:23:57+02:00", + "template_last_updated_at": "2026-04-06T11:35:16-05:00", "port": "8080" }, "nodebb": { @@ -3621,7 +3621,7 @@ "category": "productivity", "logo": "svgs/odoo.svg", "minversion": "0.0.0", - "template_last_updated_at": "2025-08-17T18:23:57+02:00", + "template_last_updated_at": "2026-04-06T11:35:16-05:00", "port": "8069" }, "ollama-with-open-webui": { @@ -3712,7 +3712,7 @@ "category": "email", "logo": "svgs/openarchiver.svg", "minversion": "0.0.0", - "template_last_updated_at": "2026-01-12T23:41:36+01:00", + "template_last_updated_at": "2026-05-09T19:35:15+05:30", "port": "3000" }, "open-webui": { @@ -3770,7 +3770,7 @@ "category": "monitoring", "logo": "svgs/openobserve.svg", "minversion": "0.0.0", - "template_last_updated_at": null, + "template_last_updated_at": "2026-05-19T16:40:18+05:30", "port": "5080" }, "openpanel": { @@ -3789,7 +3789,7 @@ "category": "analytics", "logo": "svgs/openpanel.svg", "minversion": "0.0.0", - "template_last_updated_at": "2026-01-27T20:48:57+01:00", + "template_last_updated_at": "2026-04-06T11:35:16-05:00", "port": "3000" }, "opnform": { @@ -3810,7 +3810,7 @@ "category": "productivity", "logo": "svg/opnform.svg", "minversion": "0.0.0", - "template_last_updated_at": "2025-11-24T10:13:08+01:00", + "template_last_updated_at": "2026-04-06T11:35:16-05:00", "port": "80" }, "orangehrm": { @@ -3828,7 +3828,7 @@ "category": "productivity", "logo": "svgs/orangehrm.svg", "minversion": "0.0.0", - "template_last_updated_at": "2025-08-17T18:23:57+02:00", + "template_last_updated_at": "2026-04-06T11:35:16-05:00", "port": "80" }, "organizr": { @@ -3857,7 +3857,7 @@ "category": "helpdesk", "logo": "svgs/osticket.png", "minversion": "0.0.0", - "template_last_updated_at": "2025-08-17T18:23:57+02:00", + "template_last_updated_at": "2026-04-06T11:35:16-05:00", "port": "80" }, "overseerr": { @@ -3891,7 +3891,7 @@ "category": "storage", "logo": "svgs/owncloud.svg", "minversion": "0.0.0", - "template_last_updated_at": "2025-08-17T18:23:57+02:00", + "template_last_updated_at": "2026-06-02T12:12:42+03:00", "port": "8080" }, "pairdrop": { @@ -3918,7 +3918,7 @@ "category": "games", "logo": "svgs/default.webp", "minversion": "0.0.0", - "template_last_updated_at": "2025-11-15T21:55:50+01:00" + "template_last_updated_at": "2026-04-06T11:35:16-05:00" }, "paperless": { "documentation": "https://docs.paperless-ngx.com/configuration/?utm_source=coolify.io", @@ -3959,7 +3959,7 @@ "category": "finance", "logo": "svgs/paymenter.svg", "minversion": "0.0.0", - "template_last_updated_at": "2025-11-23T12:08:21+01:00", + "template_last_updated_at": "2026-04-06T11:35:16-05:00", "port": "80" }, "penpot-with-s3": { @@ -4075,7 +4075,7 @@ "category": "productivity", "logo": "svgs/plane.svg", "minversion": "0.0.0", - "template_last_updated_at": "2026-02-24T02:34:35+05:30", + "template_last_updated_at": "2026-04-24T00:12:17+05:30", "port": "80" }, "plex": { @@ -4108,7 +4108,7 @@ "category": "email", "logo": "svgs/plunk.svg", "minversion": "0.0.0", - "template_last_updated_at": "2025-08-17T18:23:57+02:00", + "template_last_updated_at": "2026-04-06T11:35:16-05:00", "port": "3000" }, "pocket-id-with-postgresql": { @@ -4265,7 +4265,7 @@ "category": "proxy", "logo": "svgs/hoppscotch.png", "minversion": "0.0.0", - "template_last_updated_at": "2025-10-13T19:24:05+02:00", + "template_last_updated_at": "2026-04-06T11:35:16-05:00", "port": "9159" }, "qbittorrent": { @@ -4351,7 +4351,7 @@ "category": "productivity", "logo": "svgs/rallly.svg", "minversion": "0.0.0", - "template_last_updated_at": "2025-08-17T18:23:57+02:00", + "template_last_updated_at": "2026-03-19T16:11:50+01:00", "port": "3000" }, "reactive-resume": { @@ -4428,7 +4428,7 @@ "category": "productivity", "logo": "svgs/redmine.svg", "minversion": "0.0.0", - "template_last_updated_at": "2026-01-16T22:27:56+01:00", + "template_last_updated_at": "2026-04-06T11:35:16-05:00", "port": "3000" }, "rivet-engine": { @@ -4446,7 +4446,7 @@ "category": "development", "logo": "svgs/rivet.svg", "minversion": "0.0.0", - "template_last_updated_at": "2025-10-22T16:07:42+02:00", + "template_last_updated_at": "2026-04-01T19:58:30+02:00", "port": "6420" }, "rocketchat": { @@ -4502,7 +4502,7 @@ "category": "productivity", "logo": "svgs/ryot.svg", "minversion": "0.0.0", - "template_last_updated_at": "2025-08-17T18:23:57+02:00", + "template_last_updated_at": "2026-05-12T08:34:57+02:00", "port": "8000" }, "satisfactory": { @@ -4621,7 +4621,7 @@ "category": "storage", "logo": "svgs/sftpgo.png", "minversion": "0.0.0", - "template_last_updated_at": "2026-01-12T23:44:06+01:00", + "template_last_updated_at": "2026-04-06T11:35:16-05:00", "port": "8080" }, "shlink": { @@ -4660,7 +4660,7 @@ "category": "monitoring", "logo": "svgs/signoz.svg", "minversion": "0.0.0", - "template_last_updated_at": "2026-01-11T17:51:10+01:00", + "template_last_updated_at": "2026-04-06T11:35:16-05:00", "port": "8080" }, "silverbullet": { @@ -4675,7 +4675,7 @@ "category": "productivity", "logo": "svgs/silverbullet.png", "minversion": "0.0.0", - "template_last_updated_at": "2026-01-05T15:42:12+01:00", + "template_last_updated_at": "2026-04-06T11:35:16-05:00", "port": "3000" }, "siyuan": { @@ -4759,7 +4759,7 @@ "category": "devtools", "logo": "svgs/soketi-app-manager.svg", "minversion": "0.0.0", - "template_last_updated_at": "2026-01-16T22:27:17+01:00", + "template_last_updated_at": "2026-04-06T11:35:16-05:00", "port": "8080" }, "soketi": { @@ -4898,7 +4898,7 @@ "category": "backend", "logo": "svgs/supabase.svg", "minversion": "4.0.0-beta.228", - "template_last_updated_at": "2026-01-20T19:10:07+01:00", + "template_last_updated_at": "2026-04-05T20:21:11+02:00", "port": "8000" }, "superset-with-postgresql": { @@ -5176,7 +5176,7 @@ "category": "productivity", "logo": "svgs/twenty.svg", "minversion": "0.0.0", - "template_last_updated_at": "2026-01-08T18:51:23+01:00", + "template_last_updated_at": "2026-04-16T23:18:19+05:30", "port": "3000" }, "typesense": { @@ -5224,7 +5224,7 @@ "category": "devtools", "logo": "svgs/unleash.svg", "minversion": "0.0.0", - "template_last_updated_at": "2026-01-04T19:45:35+01:00", + "template_last_updated_at": "2026-04-06T11:35:16-05:00", "port": "4242" }, "unleash-without-database": { @@ -5241,7 +5241,7 @@ "category": "devtools", "logo": "svgs/unleash.svg", "minversion": "0.0.0", - "template_last_updated_at": "2025-08-17T18:23:57+02:00", + "template_last_updated_at": "2026-04-06T11:35:16-05:00", "port": "4242" }, "unstructured": { @@ -5339,7 +5339,7 @@ "category": "email", "logo": "svgs/usesend.svg", "minversion": "0.0.0", - "template_last_updated_at": "2025-12-07T17:32:23+11:00", + "template_last_updated_at": "2026-04-06T11:35:16-05:00", "port": "3000" }, "vaultwarden": { @@ -5774,7 +5774,7 @@ "category": "storage", "logo": "svgs/cells.svg", "minversion": "0.0.0", - "template_last_updated_at": "2026-02-13T18:47:54+01:00", + "template_last_updated_at": "2026-04-06T11:35:16-05:00", "port": "8080" } } diff --git a/templates/service-templates.json b/templates/service-templates.json index e5c56c5dc..4d97d8d5e 100644 --- a/templates/service-templates.json +++ b/templates/service-templates.json @@ -31,7 +31,7 @@ "category": "finance", "logo": "svgs/actualbudget.png", "minversion": "0.0.0", - "template_last_updated_at": "2025-08-17T18:23:57+02:00", + "template_last_updated_at": "2026-04-06T11:35:16-05:00", "port": "5006" }, "affine": { @@ -64,7 +64,7 @@ "category": "productivity", "logo": "svgs/alexandrie.svg", "minversion": "0.0.0", - "template_last_updated_at": "2026-01-27T21:03:32+01:00", + "template_last_updated_at": "2026-04-05T13:36:24+02:00", "port": "8200" }, "anythingllm": { @@ -175,7 +175,7 @@ "category": "ai", "logo": "svgs/argilla.png", "minversion": "0.0.0", - "template_last_updated_at": "2025-08-17T18:23:57+02:00", + "template_last_updated_at": "2026-04-06T11:35:16-05:00", "port": "6900" }, "audiobookshelf": { @@ -192,7 +192,7 @@ "category": "media", "logo": "svgs/audiobookshelf.svg", "minversion": "0.0.0", - "template_last_updated_at": "2025-08-17T18:23:57+02:00", + "template_last_updated_at": "2026-05-12T09:09:36+02:00", "port": "80" }, "authentik": { @@ -231,7 +231,7 @@ "category": "database", "logo": "svgs/autobase.svg", "minversion": "0.0.0", - "template_last_updated_at": "2026-01-19T18:13:51+01:00", + "template_last_updated_at": "2026-04-06T11:35:16-05:00", "port": "80" }, "babybuddy": { @@ -280,7 +280,7 @@ "category": "monitoring", "logo": "svgs/beszel.svg", "minversion": "0.0.0", - "template_last_updated_at": "2026-02-21T23:17:23+05:30" + "template_last_updated_at": "2026-04-24T02:22:08+05:30" }, "beszel": { "documentation": "https://github.com/henrygd/beszel?tab=readme-ov-file#getting-started?utm_source=coolify.io", @@ -296,7 +296,7 @@ "category": "monitoring", "logo": "svgs/beszel.svg", "minversion": "0.0.0", - "template_last_updated_at": "2026-02-21T23:17:23+05:30", + "template_last_updated_at": "2026-04-24T02:21:33+05:30", "port": "8090" }, "bitcoin-core": { @@ -326,7 +326,7 @@ "category": "backend", "logo": "svgs/bluesky.svg", "minversion": "0.0.0", - "template_last_updated_at": "2026-02-03T22:12:21+01:00", + "template_last_updated_at": "2026-05-09T19:19:48+05:30", "port": "3000" }, "bookstack": { @@ -377,7 +377,7 @@ "category": "finance", "logo": "svgs/budge.png", "minversion": "0.0.0", - "template_last_updated_at": "2025-08-17T18:23:57+02:00" + "template_last_updated_at": "2026-04-06T11:35:16-05:00" }, "budibase": { "documentation": "https://docs.budibase.com/docs/docker-compose?utm_source=coolify.io", @@ -438,7 +438,7 @@ "category": "media", "logo": "svgs/calibre-web-automated-with-downloader.png", "minversion": "0.0.0", - "template_last_updated_at": "2026-01-08T21:32:32+01:00", + "template_last_updated_at": "2026-04-06T11:35:16-05:00", "port": "8083" }, "calibre-web": { @@ -475,7 +475,7 @@ "category": "security", "logo": "svgs/cap-captcha.png", "minversion": "0.0.0", - "template_last_updated_at": null, + "template_last_updated_at": "2026-04-23T01:07:14+05:30", "port": "3000" }, "cap": { @@ -493,7 +493,7 @@ "category": "media", "logo": "svgs/cap.svg", "minversion": "0.0.0", - "template_last_updated_at": "2025-10-14T23:42:45+02:00", + "template_last_updated_at": "2026-04-06T11:35:16-05:00", "port": "5679" }, "castopod": { @@ -552,7 +552,7 @@ "category": "helpdesk", "logo": "svgs/chaskiq.png", "minversion": "0.0.0", - "template_last_updated_at": "2025-08-17T18:23:57+02:00", + "template_last_updated_at": "2026-04-06T11:35:16-05:00", "port": "3000" }, "chatwoot": { @@ -573,7 +573,7 @@ "category": "helpdesk", "logo": "svgs/chatwoot.svg", "minversion": "0.0.0", - "template_last_updated_at": "2025-08-17T18:23:57+02:00", + "template_last_updated_at": "2026-05-27T09:31:29-03:00", "port": "3000" }, "checkmate": { @@ -605,7 +605,7 @@ "category": "storage", "logo": "svgs/chibisafe.svg", "minversion": "0.0.0", - "template_last_updated_at": "2026-01-12T22:51:57+01:00", + "template_last_updated_at": "2026-04-06T11:35:16-05:00", "port": "80" }, "chroma": { @@ -704,7 +704,7 @@ "category": "automation", "logo": "svgs/cloudflare-ddns.svg", "minversion": "0.0.0", - "template_last_updated_at": null + "template_last_updated_at": "2026-05-18T15:42:31+10:00" }, "cloudflared": { "documentation": "https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/?utm_source=coolify.io", @@ -803,7 +803,7 @@ "category": "backend", "logo": "svgs/convex.svg", "minversion": "0.0.0", - "template_last_updated_at": "2025-11-10T14:16:14+01:00", + "template_last_updated_at": "2026-06-12T10:45:52+02:00", "port": "6791" }, "cryptgeon": { @@ -903,7 +903,7 @@ "category": "cms", "logo": "svgs/directus.svg", "minversion": "0.0.0", - "template_last_updated_at": "2025-08-17T18:23:57+02:00", + "template_last_updated_at": "2026-04-03T19:25:51+05:30", "port": "8055" }, "directus": { @@ -919,7 +919,7 @@ "category": "cms", "logo": "svgs/directus.svg", "minversion": "0.0.0", - "template_last_updated_at": "2025-08-17T18:23:57+02:00", + "template_last_updated_at": "2026-04-03T19:26:06+05:30", "port": "8055" }, "diun": { @@ -968,7 +968,7 @@ "category": "productivity", "logo": "svgs/docmost.png", "minversion": "0.0.0", - "template_last_updated_at": "2025-09-27T04:00:56+02:00", + "template_last_updated_at": "2026-05-15T13:36:02+02:00", "port": "3000" }, "documenso": { @@ -1044,7 +1044,7 @@ "category": "productivity", "logo": "svgs/dolibarr.png", "minversion": "0.0.0", - "template_last_updated_at": "2025-12-18T10:27:24+01:00", + "template_last_updated_at": "2026-04-06T11:35:16-05:00", "port": "80" }, "dozzle-with-auth": { @@ -1093,7 +1093,7 @@ "category": "devtools", "logo": "svgs/drizzle.jpeg", "minversion": "0.0.0", - "template_last_updated_at": "2025-08-17T18:23:57+02:00", + "template_last_updated_at": "2026-04-06T11:35:16-05:00", "port": "4983" }, "drupal-with-postgresql": { @@ -1161,7 +1161,7 @@ "category": "monitoring", "logo": "svgs/elasticsearch.svg", "minversion": "0.0.0", - "template_last_updated_at": "2026-01-27T21:34:40+05:30", + "template_last_updated_at": "2026-04-09T00:43:59-05:00", "port": "5601" }, "elasticsearch": { @@ -1248,7 +1248,7 @@ "category": "Networking", "logo": "svgs/emqx-enterprise.svg", "minversion": "0.0.0", - "template_last_updated_at": null, + "template_last_updated_at": "2026-04-27T09:25:48+03:00", "port": "18083" }, "ente-photos-with-s3": { @@ -1322,7 +1322,7 @@ "category": "productivity", "logo": "svgs/espocrm.svg", "minversion": "0.0.0", - "template_last_updated_at": "2026-03-16T18:23:47+02:00", + "template_last_updated_at": "2026-04-06T11:35:16-05:00", "port": "80" }, "evolution-api": { @@ -1436,7 +1436,7 @@ "category": "finance", "logo": "svgs/firefly.svg", "minversion": "0.0.0", - "template_last_updated_at": "2025-08-17T18:23:57+02:00", + "template_last_updated_at": "2026-04-06T11:35:16-05:00", "port": "8080" }, "firefox": { @@ -1625,7 +1625,7 @@ "category": "productivity", "logo": "svgs/formbricks.png", "minversion": "0.0.0", - "template_last_updated_at": "2026-01-27T21:12:57+01:00", + "template_last_updated_at": "2026-04-06T11:35:16-05:00", "port": "3000" }, "foundryvtt": { @@ -1642,7 +1642,7 @@ "category": "games", "logo": "svgs/foundryvtt.png", "minversion": "0.0.0", - "template_last_updated_at": "2025-08-17T18:23:57+02:00", + "template_last_updated_at": "2026-04-06T11:35:16-05:00", "port": "30000" }, "freescout": { @@ -1658,7 +1658,7 @@ "category": "helpdesk", "logo": "svgs/freescout.png", "minversion": "0.0.0", - "template_last_updated_at": "2025-08-17T18:23:57+02:00", + "template_last_updated_at": "2026-04-06T11:35:16-05:00", "port": "80" }, "freshrss-with-mariadb": { @@ -1672,7 +1672,7 @@ "category": "RSS", "logo": "svgs/freshrss.png", "minversion": "0.0.0", - "template_last_updated_at": "2025-08-17T18:23:57+02:00", + "template_last_updated_at": "2026-04-06T11:35:16-05:00", "port": "80" }, "freshrss-with-mysql": { @@ -1686,7 +1686,7 @@ "category": "RSS", "logo": "svgs/freshrss.png", "minversion": "0.0.0", - "template_last_updated_at": "2025-08-17T18:23:57+02:00", + "template_last_updated_at": "2026-04-06T11:35:16-05:00", "port": "80" }, "freshrss-with-postgresql": { @@ -1700,7 +1700,7 @@ "category": "RSS", "logo": "svgs/freshrss.png", "minversion": "0.0.0", - "template_last_updated_at": "2026-01-04T17:41:25+01:00", + "template_last_updated_at": "2026-04-06T11:35:16-05:00", "port": "80" }, "freshrss": { @@ -1714,7 +1714,7 @@ "category": "RSS", "logo": "svgs/freshrss.png", "minversion": "0.0.0", - "template_last_updated_at": "2025-08-17T18:23:57+02:00", + "template_last_updated_at": "2026-04-06T11:35:16-05:00", "port": "80" }, "garage": { @@ -1732,7 +1732,7 @@ "category": "storage", "logo": "svgs/garage.svg", "minversion": "0.0.0", - "template_last_updated_at": "2025-12-10T12:57:34+01:00", + "template_last_updated_at": "2026-05-31T23:57:46+02:00", "port": "3900" }, "getoutline": { @@ -1746,7 +1746,7 @@ "category": "productivity", "logo": "svgs/getoutline.jpeg", "minversion": "0.0.0", - "template_last_updated_at": "2025-12-16T14:12:15+07:00", + "template_last_updated_at": "2026-05-09T19:19:29+05:30", "port": "3000" }, "ghost": { @@ -1779,7 +1779,7 @@ "category": "devtools", "logo": "svgs/gitea.svg", "minversion": "0.0.0", - "template_last_updated_at": null + "template_last_updated_at": "2026-06-06T00:11:24+02:00" }, "gitea-with-mariadb": { "documentation": "https://docs.gitea.com?utm_source=coolify.io", @@ -1910,7 +1910,7 @@ "category": "productivity", "logo": "svgs/glance.png", "minversion": "0.0.0", - "template_last_updated_at": "2025-08-18T19:18:07+02:00", + "template_last_updated_at": "2026-04-06T11:35:16-05:00", "port": "8080" }, "glances": { @@ -2000,7 +2000,7 @@ "category": "messaging", "logo": "svgs/gotify.png", "minversion": "0.0.0", - "template_last_updated_at": "2025-10-08T11:43:21+02:00", + "template_last_updated_at": "2026-04-06T11:35:16-05:00", "port": "80" }, "gowa": { @@ -2017,7 +2017,7 @@ "category": "messaging", "logo": "svgs/gowa.svg", "minversion": "0.0.0", - "template_last_updated_at": "2025-08-17T18:23:57+02:00", + "template_last_updated_at": "2026-04-06T11:35:16-05:00", "port": "3000" }, "grafana-with-postgresql": { @@ -2080,7 +2080,7 @@ "category": null, "logo": "svgs/grimmory.svg", "minversion": "0.0.0", - "template_last_updated_at": null, + "template_last_updated_at": "2026-04-03T18:59:26+05:30", "port": "80" }, "grist": { @@ -2114,7 +2114,7 @@ "category": "productivity", "logo": "svgs/grocy.svg", "minversion": "0.0.0", - "template_last_updated_at": "2025-08-17T18:23:57+02:00" + "template_last_updated_at": "2026-05-12T09:10:44+02:00" }, "hatchet": { "documentation": "https://docs.hatchet.run/self-hosting/docker-compose?utm_source=coolify.io", @@ -2129,7 +2129,7 @@ "category": "automation", "logo": "svgs/hatchet.svg", "minversion": "0.0.0", - "template_last_updated_at": "2026-01-16T22:27:56+01:00", + "template_last_updated_at": "2026-04-06T11:35:16-05:00", "port": "80" }, "healthchecks": { @@ -2148,7 +2148,7 @@ "category": "monitoring", "logo": "svgs/healthchecks.webp", "minversion": "0.0.0", - "template_last_updated_at": null, + "template_last_updated_at": "2026-05-25T13:06:59-04:00", "port": "80000" }, "heimdall": { @@ -2183,7 +2183,7 @@ "category": "ai", "logo": "svgs/hermes-agent.png", "minversion": "0.0.0", - "template_last_updated_at": null, + "template_last_updated_at": "2026-05-29T12:14:17+05:30", "port": "8787" }, "heyform": { @@ -2218,7 +2218,7 @@ "category": "productivity", "logo": "svgs/homarr.svg", "minversion": "0.0.0", - "template_last_updated_at": "2025-10-07T12:19:45+02:00", + "template_last_updated_at": "2026-05-09T19:30:07+05:30", "port": "7575" }, "home-assistant": { @@ -2253,7 +2253,7 @@ "category": "productivity", "logo": "svgs/homebox.svg", "minversion": "0.0.0", - "template_last_updated_at": "2025-08-17T18:23:57+02:00", + "template_last_updated_at": "2026-04-06T11:35:16-05:00", "port": "7745" }, "homepage": { @@ -2361,7 +2361,7 @@ "category": "automation", "logo": "svgs/inngest.png", "minversion": "0.0.0", - "template_last_updated_at": null, + "template_last_updated_at": "2026-07-02T13:25:47+02:00", "port": "8288" }, "invoice-ninja": { @@ -2378,7 +2378,7 @@ "category": "finance", "logo": "svgs/invoiceninja.png", "minversion": "0.0.0", - "template_last_updated_at": "2025-08-17T18:23:57+02:00", + "template_last_updated_at": "2026-04-06T11:35:16-05:00", "port": "9000" }, "it-tools": { @@ -2410,7 +2410,7 @@ "category": "media", "logo": "svgs/jellyfin.svg", "minversion": "0.0.0", - "template_last_updated_at": "2025-08-17T18:23:57+02:00", + "template_last_updated_at": "2026-05-12T08:58:05+02:00", "port": "8096" }, "jenkins": { @@ -2443,7 +2443,7 @@ "category": "productivity", "logo": "svgs/jitsi.svg", "minversion": "0.0.0", - "template_last_updated_at": "2025-08-17T18:23:57+02:00", + "template_last_updated_at": "2026-04-24T09:40:01+05:30", "port": "80" }, "joomla-with-mariadb": { @@ -2672,7 +2672,7 @@ "category": "ai", "logo": "svgs/langfuse.svg", "minversion": "0.0.0", - "template_last_updated_at": "2026-03-28T16:21:57+01:00", + "template_last_updated_at": "2026-04-23T18:08:40+02:00", "port": "3000" }, "leantime": { @@ -2713,7 +2713,7 @@ "category": "ai", "logo": "svgs/librechat.svg", "minversion": "0.0.0", - "template_last_updated_at": "2025-08-17T18:23:57+02:00", + "template_last_updated_at": "2026-04-03T18:29:44+05:30", "port": "3080" }, "libreoffice": { @@ -2881,7 +2881,7 @@ "category": "auth", "logo": "svgs/logto_dark.svg", "minversion": "0.0.0", - "template_last_updated_at": "2025-08-17T18:23:57+02:00" + "template_last_updated_at": "2026-04-01T13:19:47Z" }, "lowcoder": { "documentation": "https://docs.lowcoder.cloud/?utm_source=coolify.io", @@ -3005,7 +3005,7 @@ "category": "messaging", "logo": "svgs/mattermost.svg", "minversion": "0.0.0", - "template_last_updated_at": "2025-09-04T16:02:48+03:30", + "template_last_updated_at": "2026-04-06T13:57:40-05:00", "port": "8065" }, "mealie": { @@ -3021,7 +3021,7 @@ "category": "productivity", "logo": "svgs/mealie.png", "minversion": "0.0.0", - "template_last_updated_at": "2025-08-17T18:23:57+02:00", + "template_last_updated_at": "2026-05-12T09:22:15+02:00", "port": "9000" }, "mediawiki": { @@ -3149,7 +3149,7 @@ "category": "games", "logo": "svgs/minecraft.svg", "minversion": "0.0.0", - "template_last_updated_at": "2025-08-17T18:23:57+02:00", + "template_last_updated_at": "2026-04-01T15:06:17-05:00", "port": "25565" }, "miniflux": { @@ -3166,7 +3166,7 @@ "category": "RSS", "logo": "svgs/miniflux.svg", "minversion": "0.0.0", - "template_last_updated_at": "2025-08-17T18:23:57+02:00", + "template_last_updated_at": "2026-04-06T11:35:16-05:00", "port": "8080" }, "mixpost": { @@ -3242,7 +3242,7 @@ "category": "automation", "logo": "svgs/n8n.png", "minversion": "0.0.0", - "template_last_updated_at": "2026-03-08T01:08:18-05:00", + "template_last_updated_at": "2026-03-30T20:40:07+02:00", "port": "5678" }, "n8n-with-postgresql": { @@ -3261,7 +3261,7 @@ "category": "automation", "logo": "svgs/n8n.png", "minversion": "0.0.0", - "template_last_updated_at": "2026-02-27T22:41:52-05:00", + "template_last_updated_at": "2026-03-30T20:40:07+02:00", "port": "5678" }, "n8n": { @@ -3280,7 +3280,7 @@ "category": "automation", "logo": "svgs/n8n.png", "minversion": "0.0.0", - "template_last_updated_at": "2026-02-27T22:41:52-05:00", + "template_last_updated_at": "2026-03-30T20:40:07+02:00", "port": "5678" }, "navidrome": { @@ -3328,7 +3328,7 @@ "category": "vpn", "logo": "svgs/netbird.png", "minversion": "0.0.0", - "template_last_updated_at": "2025-11-16T10:33:10+05:30" + "template_last_updated_at": "2026-04-09T00:00:38+05:30" }, "newapi": { "documentation": "https://docs.newapi.pro/en/getting-started/?utm_source=coolify.io", @@ -3360,7 +3360,7 @@ "category": "proxy", "logo": "svgs/pangolin-logo.png", "minversion": "0.0.0", - "template_last_updated_at": "2025-11-12T11:59:36-03:00" + "template_last_updated_at": "2026-04-06T11:35:16-05:00" }, "next-image-transformation": { "documentation": "https://github.com/coollabsio/next-image-transformation?utm_source=coolify.io", @@ -3392,7 +3392,7 @@ "category": "storage", "logo": "svgs/nextcloud.svg", "minversion": "0.0.0", - "template_last_updated_at": "2025-08-17T18:23:57+02:00", + "template_last_updated_at": "2026-04-07T23:32:57+05:30", "port": "80" }, "nextcloud-with-mysql": { @@ -3409,7 +3409,7 @@ "category": "storage", "logo": "svgs/nextcloud.svg", "minversion": "0.0.0", - "template_last_updated_at": "2025-08-17T18:23:57+02:00", + "template_last_updated_at": "2026-04-07T23:32:57+05:30", "port": "80" }, "nextcloud-with-postgres": { @@ -3426,7 +3426,7 @@ "category": "storage", "logo": "svgs/nextcloud.svg", "minversion": "0.0.0", - "template_last_updated_at": "2025-08-17T18:23:57+02:00", + "template_last_updated_at": "2026-04-07T23:32:57+05:30", "port": "80" }, "nextcloud": { @@ -3443,7 +3443,7 @@ "category": "storage", "logo": "svgs/nextcloud.svg", "minversion": "0.0.0", - "template_last_updated_at": "2025-08-17T18:23:57+02:00", + "template_last_updated_at": "2026-04-07T23:32:57+05:30", "port": "80" }, "nexus-arm": { @@ -3554,7 +3554,7 @@ "category": "productivity", "logo": "svgs/nocodb.svg", "minversion": "0.0.0", - "template_last_updated_at": "2025-08-17T18:23:57+02:00", + "template_last_updated_at": "2026-04-06T11:35:16-05:00", "port": "8080" }, "nodebb": { @@ -3621,7 +3621,7 @@ "category": "productivity", "logo": "svgs/odoo.svg", "minversion": "0.0.0", - "template_last_updated_at": "2025-08-17T18:23:57+02:00", + "template_last_updated_at": "2026-04-06T11:35:16-05:00", "port": "8069" }, "ollama-with-open-webui": { @@ -3712,7 +3712,7 @@ "category": "email", "logo": "svgs/openarchiver.svg", "minversion": "0.0.0", - "template_last_updated_at": "2026-01-12T23:41:36+01:00", + "template_last_updated_at": "2026-05-09T19:35:15+05:30", "port": "3000" }, "open-webui": { @@ -3770,7 +3770,7 @@ "category": "monitoring", "logo": "svgs/openobserve.svg", "minversion": "0.0.0", - "template_last_updated_at": null, + "template_last_updated_at": "2026-05-19T16:40:18+05:30", "port": "5080" }, "openpanel": { @@ -3789,7 +3789,7 @@ "category": "analytics", "logo": "svgs/openpanel.svg", "minversion": "0.0.0", - "template_last_updated_at": "2026-01-27T20:48:57+01:00", + "template_last_updated_at": "2026-04-06T11:35:16-05:00", "port": "3000" }, "opnform": { @@ -3810,7 +3810,7 @@ "category": "productivity", "logo": "svg/opnform.svg", "minversion": "0.0.0", - "template_last_updated_at": "2025-11-24T10:13:08+01:00", + "template_last_updated_at": "2026-04-06T11:35:16-05:00", "port": "80" }, "orangehrm": { @@ -3828,7 +3828,7 @@ "category": "productivity", "logo": "svgs/orangehrm.svg", "minversion": "0.0.0", - "template_last_updated_at": "2025-08-17T18:23:57+02:00", + "template_last_updated_at": "2026-04-06T11:35:16-05:00", "port": "80" }, "organizr": { @@ -3857,7 +3857,7 @@ "category": "helpdesk", "logo": "svgs/osticket.png", "minversion": "0.0.0", - "template_last_updated_at": "2025-08-17T18:23:57+02:00", + "template_last_updated_at": "2026-04-06T11:35:16-05:00", "port": "80" }, "overseerr": { @@ -3891,7 +3891,7 @@ "category": "storage", "logo": "svgs/owncloud.svg", "minversion": "0.0.0", - "template_last_updated_at": "2025-08-17T18:23:57+02:00", + "template_last_updated_at": "2026-06-02T12:12:42+03:00", "port": "8080" }, "pairdrop": { @@ -3918,7 +3918,7 @@ "category": "games", "logo": "svgs/default.webp", "minversion": "0.0.0", - "template_last_updated_at": "2025-11-15T21:55:50+01:00" + "template_last_updated_at": "2026-04-06T11:35:16-05:00" }, "paperless": { "documentation": "https://docs.paperless-ngx.com/configuration/?utm_source=coolify.io", @@ -3959,7 +3959,7 @@ "category": "finance", "logo": "svgs/paymenter.svg", "minversion": "0.0.0", - "template_last_updated_at": "2025-11-23T12:08:21+01:00", + "template_last_updated_at": "2026-04-06T11:35:16-05:00", "port": "80" }, "penpot-with-s3": { @@ -4075,7 +4075,7 @@ "category": "productivity", "logo": "svgs/plane.svg", "minversion": "0.0.0", - "template_last_updated_at": "2026-02-24T02:34:35+05:30", + "template_last_updated_at": "2026-04-24T00:12:17+05:30", "port": "80" }, "plex": { @@ -4108,7 +4108,7 @@ "category": "email", "logo": "svgs/plunk.svg", "minversion": "0.0.0", - "template_last_updated_at": "2025-08-17T18:23:57+02:00", + "template_last_updated_at": "2026-04-06T11:35:16-05:00", "port": "3000" }, "pocket-id-with-postgresql": { @@ -4265,7 +4265,7 @@ "category": "proxy", "logo": "svgs/hoppscotch.png", "minversion": "0.0.0", - "template_last_updated_at": "2025-10-13T19:24:05+02:00", + "template_last_updated_at": "2026-04-06T11:35:16-05:00", "port": "9159" }, "qbittorrent": { @@ -4351,7 +4351,7 @@ "category": "productivity", "logo": "svgs/rallly.svg", "minversion": "0.0.0", - "template_last_updated_at": "2025-08-17T18:23:57+02:00", + "template_last_updated_at": "2026-03-19T16:11:50+01:00", "port": "3000" }, "reactive-resume": { @@ -4428,7 +4428,7 @@ "category": "productivity", "logo": "svgs/redmine.svg", "minversion": "0.0.0", - "template_last_updated_at": "2026-01-16T22:27:56+01:00", + "template_last_updated_at": "2026-04-06T11:35:16-05:00", "port": "3000" }, "rivet-engine": { @@ -4446,7 +4446,7 @@ "category": "development", "logo": "svgs/rivet.svg", "minversion": "0.0.0", - "template_last_updated_at": "2025-10-22T16:07:42+02:00", + "template_last_updated_at": "2026-04-01T19:58:30+02:00", "port": "6420" }, "rocketchat": { @@ -4502,7 +4502,7 @@ "category": "productivity", "logo": "svgs/ryot.svg", "minversion": "0.0.0", - "template_last_updated_at": "2025-08-17T18:23:57+02:00", + "template_last_updated_at": "2026-05-12T08:34:57+02:00", "port": "8000" }, "satisfactory": { @@ -4621,7 +4621,7 @@ "category": "storage", "logo": "svgs/sftpgo.png", "minversion": "0.0.0", - "template_last_updated_at": "2026-01-12T23:44:06+01:00", + "template_last_updated_at": "2026-04-06T11:35:16-05:00", "port": "8080" }, "shlink": { @@ -4660,7 +4660,7 @@ "category": "monitoring", "logo": "svgs/signoz.svg", "minversion": "0.0.0", - "template_last_updated_at": "2026-01-11T17:51:10+01:00", + "template_last_updated_at": "2026-04-06T11:35:16-05:00", "port": "8080" }, "silverbullet": { @@ -4675,7 +4675,7 @@ "category": "productivity", "logo": "svgs/silverbullet.png", "minversion": "0.0.0", - "template_last_updated_at": "2026-01-05T15:42:12+01:00", + "template_last_updated_at": "2026-04-06T11:35:16-05:00", "port": "3000" }, "siyuan": { @@ -4759,7 +4759,7 @@ "category": "devtools", "logo": "svgs/soketi-app-manager.svg", "minversion": "0.0.0", - "template_last_updated_at": "2026-01-16T22:27:17+01:00", + "template_last_updated_at": "2026-04-06T11:35:16-05:00", "port": "8080" }, "soketi": { @@ -4898,7 +4898,7 @@ "category": "backend", "logo": "svgs/supabase.svg", "minversion": "4.0.0-beta.228", - "template_last_updated_at": "2026-01-20T19:10:07+01:00", + "template_last_updated_at": "2026-04-05T20:21:11+02:00", "port": "8000" }, "superset-with-postgresql": { @@ -5176,7 +5176,7 @@ "category": "productivity", "logo": "svgs/twenty.svg", "minversion": "0.0.0", - "template_last_updated_at": "2026-01-08T18:51:23+01:00", + "template_last_updated_at": "2026-04-16T23:18:19+05:30", "port": "3000" }, "typesense": { @@ -5224,7 +5224,7 @@ "category": "devtools", "logo": "svgs/unleash.svg", "minversion": "0.0.0", - "template_last_updated_at": "2026-01-04T19:45:35+01:00", + "template_last_updated_at": "2026-04-06T11:35:16-05:00", "port": "4242" }, "unleash-without-database": { @@ -5241,7 +5241,7 @@ "category": "devtools", "logo": "svgs/unleash.svg", "minversion": "0.0.0", - "template_last_updated_at": "2025-08-17T18:23:57+02:00", + "template_last_updated_at": "2026-04-06T11:35:16-05:00", "port": "4242" }, "unstructured": { @@ -5339,7 +5339,7 @@ "category": "email", "logo": "svgs/usesend.svg", "minversion": "0.0.0", - "template_last_updated_at": "2025-12-07T17:32:23+11:00", + "template_last_updated_at": "2026-04-06T11:35:16-05:00", "port": "3000" }, "vaultwarden": { @@ -5774,7 +5774,7 @@ "category": "storage", "logo": "svgs/cells.svg", "minversion": "0.0.0", - "template_last_updated_at": "2026-02-13T18:47:54+01:00", + "template_last_updated_at": "2026-04-06T11:35:16-05:00", "port": "8080" } } diff --git a/tests/Feature/Security/CommandInjectionSecurityTest.php b/tests/Feature/Security/CommandInjectionSecurityTest.php index 45101635b..0234a9539 100644 --- a/tests/Feature/Security/CommandInjectionSecurityTest.php +++ b/tests/Feature/Security/CommandInjectionSecurityTest.php @@ -996,6 +996,35 @@ }); }); +describe('service application lifecycle command escaping', function () { + test('service application deploy action escapes docker command arguments', function () { + $source = file_get_contents(app_path('Actions/Service/DeployServiceApplication.php')); + + expect($source)->toContain('escapeshellarg($workdir)') + ->and($source)->toContain('escapeshellarg($composeFile)') + ->and($source)->toContain('escapeshellarg($service->uuid)') + ->and($source)->toContain('escapeshellarg($composeServiceName)') + ->and($source)->toContain('escapeshellarg($service->destination->network)'); + }); + + test('service application restart and stop actions escape container names', function (string $path) { + $source = file_get_contents(app_path($path)); + + expect($source)->toContain('escapeshellarg($serviceApplication->name'); + })->with([ + 'restart action' => 'Actions/Service/RestartServiceApplication.php', + 'stop action' => 'Actions/Service/StopServiceApplication.php', + ]); + + test('service application logs endpoint escapes container name before docker helpers', function () { + $source = file_get_contents(app_path('Http/Controllers/Api/ServiceApplicationsController.php')); + + expect($source)->toContain('$safeContainerName = escapeshellarg($containerName)') + ->and($source)->toContain('getContainerStatus($server, $safeContainerName)') + ->and($source)->toContain('getContainerLogs($server, $containerName, $lines)'); + }); +}); + describe('install/build/start command validation', function () { test('rejects semicolon injection in install_command', function () { $rules = sharedDataApplications(); diff --git a/tests/Feature/ServiceApplicationsApiTest.php b/tests/Feature/ServiceApplicationsApiTest.php index d4f12350e..d65967bd8 100644 --- a/tests/Feature/ServiceApplicationsApiTest.php +++ b/tests/Feature/ServiceApplicationsApiTest.php @@ -20,20 +20,13 @@ beforeEach(function () { Queue::fake(); - InstanceSettings::updateOrCreate(['id' => 0]); + InstanceSettings::forceCreate(['id' => 0, 'is_api_enabled' => true]); $this->team = Team::factory()->create(); $this->user = User::factory()->create(); $this->team->members()->attach($this->user->id, ['role' => 'owner']); - $plainTextToken = Str::random(40); - $token = $this->user->tokens()->create([ - 'name' => 'test-token', - 'token' => hash('sha256', $plainTextToken), - 'abilities' => ['*'], - 'team_id' => $this->team->id, - ]); - $this->bearerToken = $token->getKey().'|'.$plainTextToken; + $this->bearerToken = createBearerTokenForServiceApplicationsApiTest($this->user, $this->team); $this->server = Server::factory()->create(['team_id' => $this->team->id]); $this->server->settings->update([ @@ -46,6 +39,19 @@ $this->environment = Environment::factory()->create(['project_id' => $this->project->id]); }); +function createBearerTokenForServiceApplicationsApiTest(User $user, Team $team, array $abilities = ['*']): string +{ + $plainTextToken = Str::random(40); + $token = $user->tokens()->create([ + 'name' => 'test-token', + 'token' => hash('sha256', $plainTextToken), + 'abilities' => $abilities, + 'team_id' => $team->id, + ]); + + return $token->getKey().'|'.$plainTextToken; +} + function createServiceWithApplicationForApiTest(object $ctx): object { $service = Service::factory()->create([ @@ -108,6 +114,32 @@ function createServiceWithoutApplicationsForApiTest(object $ctx): Service $response->assertStatus(404); $response->assertJsonFragment(['message' => 'Service not found.']); }); + + test('does not list applications from another team', function () { + $otherTeam = Team::factory()->create(); + $otherServer = Server::factory()->create(['team_id' => $otherTeam->id]); + $otherServer->settings->update([ + 'is_reachable' => true, + 'is_usable' => true, + 'force_disabled' => false, + ]); + + $otherCtx = (object) [ + 'server' => $otherServer, + 'destination' => StandaloneDocker::where('server_id', $otherServer->id)->first(), + 'environment' => Environment::factory()->create([ + 'project_id' => Project::factory()->create(['team_id' => $otherTeam->id])->id, + ]), + ]; + $ctx = createServiceWithApplicationForApiTest($otherCtx); + + $response = $this->withHeaders([ + 'Authorization' => 'Bearer '.$this->bearerToken, + ])->getJson("/api/v1/services/{$ctx->service->uuid}/applications"); + + $response->assertStatus(404); + $response->assertJsonFragment(['message' => 'Service not found.']); + }); }); describe('GET /api/v1/services/{uuid}/applications/{app_uuid}', function () { @@ -149,7 +181,7 @@ function createServiceWithoutApplicationsForApiTest(object $ctx): Service 'human_name' => 'x', ], ['Accept' => 'application/json', 'Content-Type' => 'application/json']); - $response->assertStatus(400); + $response->assertStatus(401); }); test('updates human_name', function () { @@ -191,13 +223,51 @@ function createServiceWithoutApplicationsForApiTest(object $ctx): Service $response->assertStatus(422); expect((string) $response->json('errors.is_log_drain_enabled.0'))->toContain('Log drain'); }); + + test('read-only token cannot update a service application', function () { + $ctx = createServiceWithApplicationForApiTest($this); + $readOnlyToken = createBearerTokenForServiceApplicationsApiTest($this->user, $this->team, ['read']); + + $response = $this->withHeaders([ + 'Authorization' => 'Bearer '.$readOnlyToken, + ])->patchJson("/api/v1/services/{$ctx->service->uuid}/applications/{$ctx->serviceApplication->uuid}", [ + 'human_name' => 'Blocked', + ]); + + $response->assertStatus(403); + }); + + test('returns 409 for domain conflicts unless forced', function () { + $ctx = createServiceWithApplicationForApiTest($this); + $ctx->serviceApplication->update(['fqdn' => 'http://used.example.com']); + $otherApplication = ServiceApplication::create([ + 'name' => 'admin', + 'service_id' => $ctx->service->id, + 'image' => 'nginx:alpine', + ]); + + $this->withHeaders([ + 'Authorization' => 'Bearer '.$this->bearerToken, + ])->patchJson("/api/v1/services/{$ctx->service->uuid}/applications/{$otherApplication->uuid}", [ + 'url' => 'http://used.example.com', + ])->assertStatus(409); + + $this->withHeaders([ + 'Authorization' => 'Bearer '.$this->bearerToken, + ])->patchJson("/api/v1/services/{$ctx->service->uuid}/applications/{$otherApplication->uuid}?force_domain_override=true", [ + 'url' => 'http://used.example.com', + ])->assertStatus(200); + + $otherApplication->refresh(); + expect($otherApplication->fqdn)->toBe('http://used.example.com'); + }); }); describe('POST /api/v1/services/{uuid}/applications/{app_uuid}/restart', function () { test('returns 400 without valid token', function () { $response = $this->postJson('/api/v1/services/some-uuid/applications/some-app/restart'); - $response->assertStatus(400); + $response->assertStatus(401); }); test('queues restart for a service application', function () { @@ -214,6 +284,17 @@ function createServiceWithoutApplicationsForApiTest(object $ctx): Service }); describe('POST /api/v1/services/{uuid}/applications/{app_uuid}/start', function () { + test('write token cannot start a service application', function () { + $ctx = createServiceWithApplicationForApiTest($this); + $writeToken = createBearerTokenForServiceApplicationsApiTest($this->user, $this->team, ['write']); + + $response = $this->withHeaders([ + 'Authorization' => 'Bearer '.$writeToken, + ])->postJson("/api/v1/services/{$ctx->service->uuid}/applications/{$ctx->serviceApplication->uuid}/start"); + + $response->assertStatus(403); + }); + test('queues deploy for a service application', function () { $ctx = createServiceWithApplicationForApiTest($this);