fix(teams): allow deletion with unused private keys (#11499)

This commit is contained in:
Andras Bacsai 2026-08-25 11:18:59 +02:00 committed by GitHub
parent c5b7f726b0
commit ac4f5af7c0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 64 additions and 8 deletions

View file

@ -275,13 +275,22 @@ public function invitations()
return $this->hasMany(TeamInvitation::class);
}
public function isEmpty()
/**
* @return array<string, int>
*/
public function deletionBlockers(): array
{
if ($this->projects()->count() === 0 && $this->servers()->count() === 0 && $this->privateKeys()->count() === 0 && $this->sources()->count() === 0) {
return true;
}
return array_filter([
'projects' => $this->projects()->count(),
'servers' => $this->servers()->count(),
'sources' => GithubApp::query()->where('team_id', $this->id)->count()
+ GitlabApp::query()->where('team_id', $this->id)->count(),
]);
}
return false;
public function isEmpty(): bool
{
return $this->deletionBlockers() === [];
}
public function projects()

View file

@ -1,4 +1,12 @@
<div>
@php
$deletionBlockers = currentTeam()->deletionBlockers();
$blockerDetails = [
'projects' => ['label' => 'project', 'route' => 'project.index'],
'servers' => ['label' => 'server', 'route' => 'server.index'],
'sources' => ['label' => 'Git source', 'route' => 'source.all'],
];
@endphp
<x-slot:title>
Team Danger Zone | Coolify
</x-slot>
@ -34,7 +42,7 @@ class="rounded-lg border border-red-300 bg-red-50 p-4 ring-1 ring-inset ring-red
{{ wireNavigate() }} href="{{ route('subscription.show') }}">subscription</a>
before deleting this team.
</p>
@elseif(currentTeam()->isEmpty())
@elseif($deletionBlockers === [])
<p class="mt-2 max-w-2xl text-[13px] leading-5 text-neutral-600 dark:text-fg-dim">
Permanently delete <strong class="font-semibold text-black dark:text-fg">{{ currentTeam()->name }}</strong>
from Coolify. This action cannot be undone.
@ -45,7 +53,20 @@ class="rounded-lg border border-red-300 bg-red-50 p-4 ring-1 ring-inset ring-red
</ul>
@else
<p class="mt-2 text-[13px] leading-5 text-neutral-600 dark:text-fg-dim">
Remove or move every resource owned by this team before deleting it.
This team still owns:
</p>
<ul class="mt-2 space-y-1 text-[13px] text-neutral-600 dark:text-fg-dim">
@foreach ($deletionBlockers as $type => $count)
<li>
<a class="font-medium text-coollabs hover:underline dark:text-warning"
{{ wireNavigate() }} href="{{ route($blockerDetails[$type]['route']) }}">
{{ $count }} {{ str($blockerDetails[$type]['label'])->plural($count) }}
</a>
</li>
@endforeach
</ul>
<p class="mt-2 text-[13px] leading-5 text-neutral-600 dark:text-fg-dim">
Remove or move these resources before deleting the team.
</p>
@endif
</div>
@ -57,7 +78,7 @@ class="rounded-lg border border-red-300 bg-red-50 p-4 ring-1 ring-inset ring-red
auth()->user()->teams()->count() > 1 &&
!auth()->user()->currentTeam()->personal_team &&
!currentTeam()->subscription &&
currentTeam()->isEmpty())
$deletionBlockers === [])
<x-modal-confirmation title="Confirm Team Deletion?" buttonTitle="Delete team"
isErrorButton submitAction="delete"
:actions="['The current team will be permanently deleted from Coolify and the database.']"

View file

@ -59,6 +59,32 @@
->assertSuccessful();
});
test('unused private keys do not block team deletion', function () {
$privateKey = PrivateKey::factory()->create([
'team_id' => $this->teamToDelete->id,
'description' => 'Created by Coolify',
]);
expect($this->teamToDelete->isEmpty())->toBeTrue();
app(DeleteTeam::class)->handle($this->teamToDelete, $this->owner);
expect(Team::find($this->teamToDelete->id))->toBeNull()
->and(PrivateKey::find($privateKey->id))->toBeNull();
});
test('the danger zone names and links blocking resource types', function () {
Project::factory()->count(2)->create(['team_id' => $this->teamToDelete->id]);
$this->actingAs($this->owner);
session(['currentTeam' => $this->teamToDelete]);
Livewire::test(DangerZone::class)
->assertSee('This team still owns:')
->assertSee('2 projects')
->assertSeeHtml('href="'.route('project.index').'"');
});
test('a team with a running application cannot be deleted', function () {
$server = Server::factory()->create(['team_id' => $this->teamToDelete->id]);
$destination = StandaloneDocker::query()->where('server_id', $server->id)->firstOrFail();