From a42613168de872b68cc19e326e3927155195ddba Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 11 May 2026 22:22:01 +0200 Subject: [PATCH] fix(applications): store custom nginx config from API correctly Decode base64 custom_nginx_configuration before model assignment so it is not double-encoded, and allow null values when clearing the setting. Add API coverage for create, update, invalid input, and clearing behavior. --- .../Api/ApplicationsController.php | 8 +- app/Models/Application.php | 4 +- templates/service-templates-latest.json | 30 ++-- templates/service-templates.json | 30 ++-- ...icationCustomNginxConfigurationApiTest.php | 150 ++++++++++++++++++ 5 files changed, 187 insertions(+), 35 deletions(-) create mode 100644 tests/Feature/ApplicationCustomNginxConfigurationApiTest.php diff --git a/app/Http/Controllers/Api/ApplicationsController.php b/app/Http/Controllers/Api/ApplicationsController.php index 9919a8054..074269fa0 100644 --- a/app/Http/Controllers/Api/ApplicationsController.php +++ b/app/Http/Controllers/Api/ApplicationsController.php @@ -979,6 +979,9 @@ private function create_application(Request $request, $type) ], ], 422); } + $request->merge([ + 'custom_nginx_configuration' => $customNginxConfiguration, + ]); } $project = Project::whereTeamId($teamId)->whereUuid($request->project_uuid)->first(); @@ -2397,7 +2400,7 @@ public function update_by_uuid(Request $request) } } } - if ($request->has('custom_nginx_configuration')) { + if ($request->has('custom_nginx_configuration') && ! is_null($request->custom_nginx_configuration)) { if (! isBase64Encoded($request->custom_nginx_configuration)) { return response()->json([ 'message' => 'Validation failed.', @@ -2415,6 +2418,9 @@ public function update_by_uuid(Request $request) ], ], 422); } + $request->merge([ + 'custom_nginx_configuration' => $customNginxConfiguration, + ]); } $return = $this->validateDataApplications($request, $server); if ($return instanceof JsonResponse) { diff --git a/app/Models/Application.php b/app/Models/Application.php index 2a364e13f..b5a0f0ea9 100644 --- a/app/Models/Application.php +++ b/app/Models/Application.php @@ -886,8 +886,8 @@ public function status(): Attribute public function customNginxConfiguration(): Attribute { return Attribute::make( - set: fn ($value) => base64_encode($value), - get: fn ($value) => base64_decode($value), + set: fn ($value) => is_null($value) ? null : base64_encode($value), + get: fn ($value) => is_null($value) ? null : base64_decode($value), ); } diff --git a/templates/service-templates-latest.json b/templates/service-templates-latest.json index b57d9d29c..4b21a8798 100644 --- a/templates/service-templates-latest.json +++ b/templates/service-templates-latest.json @@ -1634,6 +1634,20 @@ "minversion": "0.0.0", "port": "2368" }, + "gitea-runner": { + "documentation": "https://github.com/go-gitea/gitea?utm_source=coolify.io", + "slogan": "Gitea Actions runner for docker", + "compose": "c2VydmljZXM6CiAgcnVubmVyOgogICAgaW1hZ2U6ICdkb2NrZXIuaW8vZ2l0ZWEvcnVubmVyOjEuMC4wJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ0dJVEVBX0lOU1RBTkNFX1VSTD0ke0dJVEVBX0lOU1RBTkNFX1VSTH0nCiAgICAgIC0gJ0dJVEVBX1JVTk5FUl9SRUdJU1RSQVRJT05fVE9LRU49JHtHSVRFQV9SVU5ORVJfUkVHSVNUUkFUSU9OX1RPS0VOfScKICAgICAgLSAnR0lURUFfUlVOTkVSX05BTUU9JHtHSVRFQV9SVU5ORVJfTkFNRTotZ2l0ZWEtcnVubmVyfScKICAgICAgLSAnR0lURUFfUlVOTkVSX0xBQkVMUz0ke0dJVEVBX1JVTk5FUl9MQUJFTFM6LXVidW50dS1sYXRlc3Q6ZG9ja2VyOi8vbm9kZToyMn0nCiAgICAgIC0gJ0dJVEVBX1RPS0VOPSR7R0lURUFfVE9LRU59JwogICAgd29ya2luZ19kaXI6IC9kYXRhCiAgICB2b2x1bWVzOgogICAgICAtICdydW5uZXItZGF0YTovZGF0YScKICAgICAgLSAnL3Zhci9ydW4vZG9ja2VyLnNvY2s6L3Zhci9ydW4vZG9ja2VyLnNvY2snCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gInBzIGF1eCB8IGdyZXAgJ1tSXXVubmVyJyA+IC9kZXYvbnVsbCB8fCBleGl0IDEiCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMTUK", + "tags": [ + "gitea", + "actions", + "runner", + "docker" + ], + "category": "devtools", + "logo": "svgs/gitea.svg", + "minversion": "0.0.0" + }, "gitea-with-mariadb": { "documentation": "https://docs.gitea.com?utm_source=coolify.io", "slogan": "Gitea is a self-hosted, lightweight Git service, offering version control, collaboration, and code hosting.", @@ -2587,22 +2601,6 @@ "minversion": "0.0.0", "port": "4000" }, - "litequeen": { - "documentation": "https://litequeen.com/?utm_source=coolify.io", - "slogan": "Lite Queen is an open-source SQLite database management software that runs on your server.", - "compose": "c2VydmljZXM6CiAgbGl0ZXF1ZWVuOgogICAgaW1hZ2U6ICdraXZzZWdyb2IvbGl0ZS1xdWVlbjpsYXRlc3QnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX1VSTF9MSVRFUVVFRU5fODAwMAogICAgdm9sdW1lczoKICAgICAgLSAnbGl0ZXF1ZWVuLWRhdGE6L2hvbWUvbGl0ZXF1ZWVuL2RhdGEnCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL2RhdGFiYXNlcwogICAgICAgIHRhcmdldDogL3NydgogICAgICAgIGlzX2RpcmVjdG9yeTogdHJ1ZQogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICJiYXNoIC1jICc6PiAvZGV2L3RjcC8xMjcuMC4wLjEvODAwMCcgfHwgZXhpdCAxIgogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogNXMKICAgICAgcmV0cmllczogMwo=", - "tags": [ - "sqlite", - "sqlite-database-management", - "self-hosted", - "vps", - "database" - ], - "category": "database", - "logo": "svgs/litequeen.svg", - "minversion": "0.0.0", - "port": "8000" - }, "lobe-chat": { "documentation": "https://github.com/lobehub/lobe-chat?tab=readme-ov-file#b-deploying-with-docker?utm_source=coolify.io", "slogan": "An open-source, modern-design AI chat framework.", diff --git a/templates/service-templates.json b/templates/service-templates.json index ea9fd145a..8dd787f2e 100644 --- a/templates/service-templates.json +++ b/templates/service-templates.json @@ -1634,6 +1634,20 @@ "minversion": "0.0.0", "port": "2368" }, + "gitea-runner": { + "documentation": "https://github.com/go-gitea/gitea?utm_source=coolify.io", + "slogan": "Gitea Actions runner for docker", + "compose": "c2VydmljZXM6CiAgcnVubmVyOgogICAgaW1hZ2U6ICdkb2NrZXIuaW8vZ2l0ZWEvcnVubmVyOjEuMC4wJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ0dJVEVBX0lOU1RBTkNFX1VSTD0ke0dJVEVBX0lOU1RBTkNFX1VSTH0nCiAgICAgIC0gJ0dJVEVBX1JVTk5FUl9SRUdJU1RSQVRJT05fVE9LRU49JHtHSVRFQV9SVU5ORVJfUkVHSVNUUkFUSU9OX1RPS0VOfScKICAgICAgLSAnR0lURUFfUlVOTkVSX05BTUU9JHtHSVRFQV9SVU5ORVJfTkFNRTotZ2l0ZWEtcnVubmVyfScKICAgICAgLSAnR0lURUFfUlVOTkVSX0xBQkVMUz0ke0dJVEVBX1JVTk5FUl9MQUJFTFM6LXVidW50dS1sYXRlc3Q6ZG9ja2VyOi8vbm9kZToyMn0nCiAgICAgIC0gJ0dJVEVBX1RPS0VOPSR7R0lURUFfVE9LRU59JwogICAgd29ya2luZ19kaXI6IC9kYXRhCiAgICB2b2x1bWVzOgogICAgICAtICdydW5uZXItZGF0YTovZGF0YScKICAgICAgLSAnL3Zhci9ydW4vZG9ja2VyLnNvY2s6L3Zhci9ydW4vZG9ja2VyLnNvY2snCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gInBzIGF1eCB8IGdyZXAgJ1tSXXVubmVyJyA+IC9kZXYvbnVsbCB8fCBleGl0IDEiCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMTUK", + "tags": [ + "gitea", + "actions", + "runner", + "docker" + ], + "category": "devtools", + "logo": "svgs/gitea.svg", + "minversion": "0.0.0" + }, "gitea-with-mariadb": { "documentation": "https://docs.gitea.com?utm_source=coolify.io", "slogan": "Gitea is a self-hosted, lightweight Git service, offering version control, collaboration, and code hosting.", @@ -2587,22 +2601,6 @@ "minversion": "0.0.0", "port": "4000" }, - "litequeen": { - "documentation": "https://litequeen.com/?utm_source=coolify.io", - "slogan": "Lite Queen is an open-source SQLite database management software that runs on your server.", - "compose": "c2VydmljZXM6CiAgbGl0ZXF1ZWVuOgogICAgaW1hZ2U6ICdraXZzZWdyb2IvbGl0ZS1xdWVlbjpsYXRlc3QnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fTElURVFVRUVOXzgwMDAKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2xpdGVxdWVlbi1kYXRhOi9ob21lL2xpdGVxdWVlbi9kYXRhJwogICAgICAtCiAgICAgICAgdHlwZTogYmluZAogICAgICAgIHNvdXJjZTogLi9kYXRhYmFzZXMKICAgICAgICB0YXJnZXQ6IC9zcnYKICAgICAgICBpc19kaXJlY3Rvcnk6IHRydWUKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAiYmFzaCAtYyAnOj4gL2Rldi90Y3AvMTI3LjAuMC4xLzgwMDAnIHx8IGV4aXQgMSIKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDVzCiAgICAgIHJldHJpZXM6IDMK", - "tags": [ - "sqlite", - "sqlite-database-management", - "self-hosted", - "vps", - "database" - ], - "category": "database", - "logo": "svgs/litequeen.svg", - "minversion": "0.0.0", - "port": "8000" - }, "lobe-chat": { "documentation": "https://github.com/lobehub/lobe-chat?tab=readme-ov-file#b-deploying-with-docker?utm_source=coolify.io", "slogan": "An open-source, modern-design AI chat framework.", diff --git a/tests/Feature/ApplicationCustomNginxConfigurationApiTest.php b/tests/Feature/ApplicationCustomNginxConfigurationApiTest.php new file mode 100644 index 000000000..358003588 --- /dev/null +++ b/tests/Feature/ApplicationCustomNginxConfigurationApiTest.php @@ -0,0 +1,150 @@ + InstanceSettings::firstOrCreate(['id' => 0])); + + $this->team = Team::factory()->create(); + $this->user = User::factory()->create(); + $this->team->members()->attach($this->user->id, ['role' => 'owner']); + session(['currentTeam' => $this->team]); + + $plainTextToken = Str::random(40); + $token = $this->user->tokens()->create([ + 'name' => 'custom-nginx-api-test-'.Str::random(6), + 'token' => hash('sha256', $plainTextToken), + 'abilities' => ['*'], + 'team_id' => $this->team->id, + ]); + $this->bearerToken = $token->getKey().'|'.$plainTextToken; + + $this->server = Server::factory()->create(['team_id' => $this->team->id]); + $this->destination = StandaloneDocker::where('server_id', $this->server->id)->first(); + $this->project = Project::factory()->create(['team_id' => $this->team->id]); + $this->environment = Environment::factory()->create(['project_id' => $this->project->id]); +}); + +function customNginxApiHeaders(string $bearerToken): array +{ + return [ + 'Authorization' => 'Bearer '.$bearerToken, + 'Content-Type' => 'application/json', + ]; +} + +function customNginxConfig(): string +{ + return <<<'NGINX' +server { + listen 80; + location / { + try_files $uri $uri/ /index.html; + } +} +NGINX; +} + +function makeCustomNginxApplication(array $overrides = []): Application +{ + return Application::factory()->create(array_merge([ + 'environment_id' => test()->environment->id, + 'destination_id' => test()->destination->id, + 'destination_type' => test()->destination->getMorphClass(), + 'build_pack' => 'static', + ], $overrides)); +} + +describe('PATCH /api/v1/applications/{uuid} custom_nginx_configuration', function () { + test('decodes base64 custom nginx configuration before storing it', function () { + $application = makeCustomNginxApplication(); + $configuration = customNginxConfig(); + $encodedConfiguration = base64_encode($configuration); + + $response = $this->withHeaders(customNginxApiHeaders($this->bearerToken)) + ->patchJson("/api/v1/applications/{$application->uuid}", [ + 'custom_nginx_configuration' => $encodedConfiguration, + ]); + + $response->assertOk(); + + $application->refresh(); + expect($application->custom_nginx_configuration)->toBe($configuration); + + $storedConfiguration = DB::table('applications') + ->where('id', $application->id) + ->value('custom_nginx_configuration'); + + expect($storedConfiguration)->toBe(base64_encode($configuration)); + + $this->withHeaders(customNginxApiHeaders($this->bearerToken)) + ->getJson("/api/v1/applications/{$application->uuid}") + ->assertOk() + ->assertJsonPath('custom_nginx_configuration', $configuration); + }); + + test('rejects custom nginx configuration that is not base64 encoded', function () { + $application = makeCustomNginxApplication(); + + $response = $this->withHeaders(customNginxApiHeaders($this->bearerToken)) + ->patchJson("/api/v1/applications/{$application->uuid}", [ + 'custom_nginx_configuration' => customNginxConfig(), + ]); + + $response->assertUnprocessable() + ->assertJsonPath('errors.custom_nginx_configuration', 'The custom_nginx_configuration should be base64 encoded.'); + }); + + test('can clear custom nginx configuration with null', function () { + $application = makeCustomNginxApplication([ + 'custom_nginx_configuration' => customNginxConfig(), + ]); + + $response = $this->withHeaders(customNginxApiHeaders($this->bearerToken)) + ->patchJson("/api/v1/applications/{$application->uuid}", [ + 'custom_nginx_configuration' => null, + ]); + + $response->assertOk(); + + $application->refresh(); + expect($application->custom_nginx_configuration)->toBeNull(); + }); +}); + +describe('POST /api/v1/applications/public custom_nginx_configuration', function () { + test('decodes base64 custom nginx configuration before storing it on create', function () { + $configuration = customNginxConfig(); + + $response = $this->withHeaders(customNginxApiHeaders($this->bearerToken)) + ->postJson('/api/v1/applications/public', [ + 'project_uuid' => $this->project->uuid, + 'environment_uuid' => $this->environment->uuid, + 'server_uuid' => $this->server->uuid, + 'git_repository' => 'https://gitlab.com/coolify/test-static-app', + 'git_branch' => 'main', + 'build_pack' => 'static', + 'ports_exposes' => '80', + 'custom_nginx_configuration' => base64_encode($configuration), + 'autogenerate_domain' => false, + ]); + + $response->assertCreated(); + + $application = Application::where('uuid', $response->json('uuid'))->firstOrFail(); + + expect($application->custom_nginx_configuration)->toBe($configuration); + }); +});