From 50b589aeff3820d000b741335e46e437c985efb7 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 26 Dec 2025 12:02:24 +0100 Subject: [PATCH 01/16] fix(ui): Initialize latestVersion in Upgrade component mount MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The upgrade modal was displaying "0.0.0" as the target version because $latestVersion was not initialized during component mount. The Blade template rendered before Alpine's x-init could populate the value. Fixed by calling get_latest_version_of_coolify() in mount() to ensure the version is available at initial render time. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Haiku 4.5 --- app/Livewire/Upgrade.php | 1 + 1 file changed, 1 insertion(+) diff --git a/app/Livewire/Upgrade.php b/app/Livewire/Upgrade.php index 7948ad6a9..25cedc71b 100644 --- a/app/Livewire/Upgrade.php +++ b/app/Livewire/Upgrade.php @@ -24,6 +24,7 @@ class Upgrade extends Component public function mount() { $this->currentVersion = config('constants.coolify.version'); + $this->latestVersion = get_latest_version_of_coolify(); $this->devMode = isDev(); } From c2a3be152e2ef462e2dce187872dd54a0a6d92fd Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Tue, 31 Mar 2026 17:28:24 +0200 Subject: [PATCH 02/16] test(upgrade): add mount tests for cached and fallback versions Covers Upgrade Livewire component mount behavior for: - initializing latest version from cached versions data - falling back to 0.0.0 when versions cache is unavailable --- tests/Feature/UpgradeComponentTest.php | 37 ++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 tests/Feature/UpgradeComponentTest.php diff --git a/tests/Feature/UpgradeComponentTest.php b/tests/Feature/UpgradeComponentTest.php new file mode 100644 index 000000000..612e989ae --- /dev/null +++ b/tests/Feature/UpgradeComponentTest.php @@ -0,0 +1,37 @@ + '4.0.0-beta.998']); + + Cache::shouldReceive('remember') + ->once() + ->with('coolify:versions:all', 3600, Mockery::type(\Closure::class)) + ->andReturn([ + 'coolify' => [ + 'v4' => [ + 'version' => '4.0.0-beta.999', + ], + ], + ]); + + Livewire::test(Upgrade::class) + ->assertSet('currentVersion', '4.0.0-beta.998') + ->assertSet('latestVersion', '4.0.0-beta.999') + ->set('isUpgradeAvailable', true) + ->assertSee('4.0.0-beta.998') + ->assertSee('4.0.0-beta.999'); +}); + +it('falls back to 0.0.0 during mount when cached versions data is unavailable', function () { + Cache::shouldReceive('remember') + ->once() + ->with('coolify:versions:all', 3600, Mockery::type(\Closure::class)) + ->andReturn(null); + + Livewire::test(Upgrade::class) + ->assertSet('latestVersion', '0.0.0'); +}); From 3f564f9b2efe2c55655671ab80cd13eedf8c6be0 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Sun, 5 Apr 2026 18:08:06 +0200 Subject: [PATCH 03/16] fix(user-deletion): handle GitHub app sources across team cleanup Limit team cleanup to apps owned by the deleted team and nullify cross-team application source references before deleting team-owned sources. Adds feature tests covering user deletion with GitHub app-backed applications, preserving system-wide apps, and nullifying external source links. --- app/Models/Team.php | 6 +- app/Models/User.php | 17 ++ .../Feature/UserDeletionWithGithubAppTest.php | 166 ++++++++++++++++++ 3 files changed, 187 insertions(+), 2 deletions(-) create mode 100644 tests/Feature/UserDeletionWithGithubAppTest.php diff --git a/app/Models/Team.php b/app/Models/Team.php index 8a54a9dee..479bf4e00 100644 --- a/app/Models/Team.php +++ b/app/Models/Team.php @@ -76,8 +76,10 @@ protected static function booted() foreach ($keys as $key) { $key->delete(); } - $sources = $team->sources(); - foreach ($sources as $source) { + // Only delete sources owned by this team, not system-wide ones from other teams + $teamSources = GithubApp::where('team_id', $team->id)->get() + ->merge(GitlabApp::where('team_id', $team->id)->get()); + foreach ($teamSources as $source) { $source->delete(); } $tags = Tag::whereTeamId($team->id)->get(); diff --git a/app/Models/User.php b/app/Models/User.php index 3199d2024..7a9600ec4 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -176,6 +176,23 @@ private static function finalizeTeamDeletion(User $user, Team $team) $project->forceDelete(); } + // Detach applications from other teams that reference this team's sources, + // so the GithubApp/GitlabApp deleting guard doesn't block team deletion + $githubAppIds = GithubApp::where('team_id', $team->id)->pluck('id'); + $gitlabAppIds = GitlabApp::where('team_id', $team->id)->pluck('id'); + + if ($githubAppIds->isNotEmpty()) { + Application::where('source_type', GithubApp::class) + ->whereIn('source_id', $githubAppIds) + ->update(['source_id' => null, 'source_type' => null]); + } + + if ($gitlabAppIds->isNotEmpty()) { + Application::where('source_type', GitlabApp::class) + ->whereIn('source_id', $gitlabAppIds) + ->update(['source_id' => null, 'source_type' => null]); + } + $team->members()->detach($user->id); $team->delete(); } diff --git a/tests/Feature/UserDeletionWithGithubAppTest.php b/tests/Feature/UserDeletionWithGithubAppTest.php new file mode 100644 index 000000000..d8986e378 --- /dev/null +++ b/tests/Feature/UserDeletionWithGithubAppTest.php @@ -0,0 +1,166 @@ +rootTeam = Team::factory()->create(['id' => 0, 'name' => 'Root Team']); + $this->adminUser = User::factory()->create(); + $this->rootTeam->members()->attach($this->adminUser->id, ['role' => 'owner']); + $this->actingAs($this->adminUser); + session(['currentTeam' => $this->rootTeam]); +}); + +it('deletes a user whose team has a github app with applications', function () { + // Create the user to be deleted with their own team + $targetUser = User::factory()->create(); + $targetTeam = $targetUser->teams()->first(); // created by User::created event + + // Create a private key for the team + $privateKey = PrivateKey::factory()->create(['team_id' => $targetTeam->id]); + + // Create a server and destination for the team + $server = Server::factory()->create([ + 'team_id' => $targetTeam->id, + 'private_key_id' => $privateKey->id, + ]); + $destination = StandaloneDocker::factory()->create(['server_id' => $server->id]); + + // Create a project and environment + $project = Project::factory()->create(['team_id' => $targetTeam->id]); + $environment = Environment::factory()->create(['project_id' => $project->id]); + + // Create a GitHub App owned by the target team + $githubApp = GithubApp::create([ + 'name' => 'Test GitHub App', + 'team_id' => $targetTeam->id, + 'private_key_id' => $privateKey->id, + 'api_url' => 'https://api.github.com', + 'html_url' => 'https://github.com', + 'is_public' => false, + ]); + + // Create an application that uses the GitHub App as its source + $application = Application::factory()->create([ + 'environment_id' => $environment->id, + 'destination_id' => $destination->id, + 'destination_type' => StandaloneDocker::class, + 'source_id' => $githubApp->id, + 'source_type' => GithubApp::class, + ]); + + // Delete the user — this should NOT throw a GithubApp exception + $targetUser->delete(); + + // Assert user is deleted + expect(User::find($targetUser->id))->toBeNull(); + + // Assert the GitHub App is deleted + expect(GithubApp::find($githubApp->id))->toBeNull(); + + // Assert the application is deleted + expect(Application::find($application->id))->toBeNull(); +}); + +it('does not delete system-wide github apps when deleting a different team', function () { + // Create a system-wide GitHub App owned by the root team + $rootPrivateKey = PrivateKey::factory()->create(['team_id' => $this->rootTeam->id]); + $systemGithubApp = GithubApp::create([ + 'name' => 'System GitHub App', + 'team_id' => $this->rootTeam->id, + 'private_key_id' => $rootPrivateKey->id, + 'api_url' => 'https://api.github.com', + 'html_url' => 'https://github.com', + 'is_public' => false, + 'is_system_wide' => true, + ]); + + // Create a target user with their own team + $targetUser = User::factory()->create(); + $targetTeam = $targetUser->teams()->first(); + + // Create an application on the target team that uses the system-wide GitHub App + $privateKey = PrivateKey::factory()->create(['team_id' => $targetTeam->id]); + $server = Server::factory()->create([ + 'team_id' => $targetTeam->id, + 'private_key_id' => $privateKey->id, + ]); + $destination = StandaloneDocker::factory()->create(['server_id' => $server->id]); + $project = Project::factory()->create(['team_id' => $targetTeam->id]); + $environment = Environment::factory()->create(['project_id' => $project->id]); + + $application = Application::factory()->create([ + 'environment_id' => $environment->id, + 'destination_id' => $destination->id, + 'destination_type' => StandaloneDocker::class, + 'source_id' => $systemGithubApp->id, + 'source_type' => GithubApp::class, + ]); + + // Delete the target user — should NOT throw or delete the system-wide GitHub App + $targetUser->delete(); + + // Assert user is deleted + expect(User::find($targetUser->id))->toBeNull(); + + // Assert the system-wide GitHub App still exists + expect(GithubApp::find($systemGithubApp->id))->not->toBeNull(); +}); + +it('nullifies source references on other teams apps when deleting a user', function () { + // Create the user to be deleted with their own team + $targetUser = User::factory()->create(); + $targetTeam = $targetUser->teams()->first(); + + // Create a GitHub App owned by the target team + $targetPrivateKey = PrivateKey::factory()->create(['team_id' => $targetTeam->id]); + $githubApp = GithubApp::create([ + 'name' => 'Target GitHub App', + 'team_id' => $targetTeam->id, + 'private_key_id' => $targetPrivateKey->id, + 'api_url' => 'https://api.github.com', + 'html_url' => 'https://github.com', + 'is_public' => false, + ]); + + // Create an application on the ADMIN's team that uses the target team's GitHub App + $adminPrivateKey = PrivateKey::factory()->create(['team_id' => $this->rootTeam->id]); + $adminServer = Server::factory()->create([ + 'team_id' => $this->rootTeam->id, + 'private_key_id' => $adminPrivateKey->id, + ]); + $adminDestination = StandaloneDocker::factory()->create(['server_id' => $adminServer->id]); + $adminProject = Project::factory()->create(['team_id' => $this->rootTeam->id]); + $adminEnvironment = Environment::factory()->create(['project_id' => $adminProject->id]); + + $otherTeamApp = Application::factory()->create([ + 'environment_id' => $adminEnvironment->id, + 'destination_id' => $adminDestination->id, + 'destination_type' => StandaloneDocker::class, + 'source_id' => $githubApp->id, + 'source_type' => GithubApp::class, + ]); + + // Delete the target user — should succeed, nullifying the source reference + $targetUser->delete(); + + // Assert user is deleted + expect(User::find($targetUser->id))->toBeNull(); + + // Assert the other team's application still exists but source is nullified + $otherTeamApp->refresh(); + expect($otherTeamApp)->not->toBeNull(); + expect($otherTeamApp->source_id)->toBeNull(); + expect($otherTeamApp->source_type)->toBeNull(); +}); From 17ba325924be852f49fb7ce313899ab9d341986a Mon Sep 17 00:00:00 2001 From: rosslh Date: Mon, 6 Apr 2026 15:09:54 -0400 Subject: [PATCH 04/16] fix(ui): make dashboard add buttons visible in light mode The "+" icon buttons next to "Projects" and "Servers" headings used text-white without a dark: prefix, making them invisible on light backgrounds. Changed to text-black dark:text-white so the icon is visible in both themes. Fixes #9454 --- resources/views/livewire/dashboard.blade.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/views/livewire/dashboard.blade.php b/resources/views/livewire/dashboard.blade.php index 908c4a98a..26a404b17 100644 --- a/resources/views/livewire/dashboard.blade.php +++ b/resources/views/livewire/dashboard.blade.php @@ -15,7 +15,7 @@ -
+ :style="panelStyles" class="absolute top-full z-50 mt-1 min-w-max max-w-[calc(100vw-1rem)] md:top-0 md:mt-6" x-cloak>
+ class="border border-neutral-300 bg-white p-1 shadow-sm dark:border-coolgray-300 dark:bg-coolgray-200"> {{ $slot }}
diff --git a/resources/views/components/forms/checkbox.blade.php b/resources/views/components/forms/checkbox.blade.php index b291759a8..29717b9b8 100644 --- a/resources/views/components/forms/checkbox.blade.php +++ b/resources/views/components/forms/checkbox.blade.php @@ -11,12 +11,12 @@ ])
$fullWidth, 'dark:hover:bg-coolgray-100 cursor-pointer' => !$disabled, ])> -
@if (!isCloud()) -
+
diff --git a/resources/views/livewire/notifications/telegram.blade.php b/resources/views/livewire/notifications/telegram.blade.php index 1c83caf70..f87a13c37 100644 --- a/resources/views/livewire/notifications/telegram.blade.php +++ b/resources/views/livewire/notifications/telegram.blade.php @@ -41,7 +41,7 @@

Deployments

-
+
@@ -49,7 +49,7 @@ id="telegramNotificationsDeploymentSuccessThreadId" />
-
+
@@ -57,7 +57,7 @@ id="telegramNotificationsDeploymentFailureThreadId" />
-
+
@@ -71,7 +71,7 @@

Backups

-
+
@@ -80,7 +80,7 @@
-
+
@@ -94,7 +94,7 @@

Scheduled Tasks

-
+
@@ -103,7 +103,7 @@
-
+
@@ -117,7 +117,7 @@

Server

-
+
@@ -126,7 +126,7 @@
-
+
@@ -135,7 +135,7 @@
-
+
@@ -144,7 +144,7 @@
-
+
@@ -153,7 +153,7 @@
-
+
@@ -162,7 +162,7 @@
-
+
@@ -171,7 +171,7 @@
-
+
diff --git a/resources/views/livewire/project/application/general.blade.php b/resources/views/livewire/project/application/general.blade.php index d743e346e..caf105dbf 100644 --- a/resources/views/livewire/project/application/general.blade.php +++ b/resources/views/livewire/project/application/general.blade.php @@ -276,7 +276,7 @@ class="underline" href="https://coolify.io/docs/knowledge-base/docker/registry" helper="It is calculated together with the Base Directory:
{{ Str::start($baseDirectory . $dockerComposeLocation, '/') }}" x-model="composeLocation" @blur="normalizeComposeLocation()" />
-
+
@if ($buildPack !== 'dockercompose') -
+
@endif -
+
HTTP Basic Authentication
-
+
@@ -543,7 +543,7 @@ class="flex items-start gap-2 p-4 mb-4 text-sm rounded-lg bg-blue-50 dark:bg-blu @endif -
+