From 98b6aec2039462cd63d8d0a8d44b05b86f50db4e Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Tue, 21 May 2024 14:29:06 +0200 Subject: [PATCH] feat: admin view for deleting users --- app/Actions/Docker/GetContainersStatus.php | 36 +++--- app/Livewire/Boarding/Index.php | 3 + app/Livewire/Team/AdminView.php | 117 ++++++++++++++++++ app/Models/Environment.php | 12 ++ app/Models/Project.php | 9 ++ app/Models/Team.php | 28 +++++ bootstrap/helpers/shared.php | 3 + database/seeders/TestTeamSeeder.php | 42 +++++++ resources/css/app.css | 2 +- .../views/components/team/navbar.blade.php | 6 + .../views/livewire/team/admin-view.blade.php | 29 +++++ routes/web.php | 3 +- 12 files changed, 271 insertions(+), 19 deletions(-) create mode 100644 app/Livewire/Team/AdminView.php create mode 100644 database/seeders/TestTeamSeeder.php create mode 100644 resources/views/livewire/team/admin-view.blade.php diff --git a/app/Actions/Docker/GetContainersStatus.php b/app/Actions/Docker/GetContainersStatus.php index 8f4bfdf25..1d240d88a 100644 --- a/app/Actions/Docker/GetContainersStatus.php +++ b/app/Actions/Docker/GetContainersStatus.php @@ -24,9 +24,9 @@ class GetContainersStatus public function handle(Server $server) { - if (isDev()) { - $server = Server::find(0); - } + // if (isDev()) { + // $server = Server::find(0); + // } $this->server = $server; if (!$this->server->isFunctional()) { return 'Server is not ready.'; @@ -154,7 +154,7 @@ private function sentinel() if ($isPublic) { $foundTcpProxy = $containers->filter(function ($value, $key) use ($uuid) { if ($this->server->isSwarm()) { - // TODO: fix this with sentinel + // TODO: fix this with sentinel return data_get($value, 'Spec.Name') === "coolify-proxy_$uuid"; } else { return data_get($value, 'name') === "$uuid-proxy"; @@ -316,7 +316,7 @@ private function sentinel() $this->server->proxyType(); $foundProxyContainer = $containers->filter(function ($value, $key) { if ($this->server->isSwarm()) { - // TODO: fix this with sentinel + // TODO: fix this with sentinel return data_get($value, 'Spec.Name') === 'coolify-proxy_traefik'; } else { return data_get($value, 'name') === 'coolify-proxy'; @@ -442,19 +442,21 @@ private function old_way() if ($database_id) { $service_db = ServiceDatabase::where('id', $database_id)->first(); if ($service_db) { - $uuid = $service_db->service->uuid; - $isPublic = data_get($service_db, 'is_public'); - if ($isPublic) { - $foundTcpProxy = $containers->filter(function ($value, $key) use ($uuid) { - if ($this->server->isSwarm()) { - return data_get($value, 'Spec.Name') === "coolify-proxy_$uuid"; - } else { - return data_get($value, 'Name') === "/$uuid-proxy"; + $uuid = data_get($service_db, 'service.uuid'); + if ($uuid) { + $isPublic = data_get($service_db, 'is_public'); + if ($isPublic) { + $foundTcpProxy = $containers->filter(function ($value, $key) use ($uuid) { + if ($this->server->isSwarm()) { + return data_get($value, 'Spec.Name') === "coolify-proxy_$uuid"; + } else { + return data_get($value, 'Name') === "/$uuid-proxy"; + } + })->first(); + if (!$foundTcpProxy) { + StartDatabaseProxy::run($service_db); + // $this->server->team?->notify(new ContainerRestarted("TCP Proxy for {$service_db->service->name}", $this->server)); } - })->first(); - if (!$foundTcpProxy) { - StartDatabaseProxy::run($service_db); - // $this->server->team?->notify(new ContainerRestarted("TCP Proxy for {$service_db->service->name}", $this->server)); } } } diff --git a/app/Livewire/Boarding/Index.php b/app/Livewire/Boarding/Index.php index 2681b69e0..8f4e87090 100644 --- a/app/Livewire/Boarding/Index.php +++ b/app/Livewire/Boarding/Index.php @@ -52,6 +52,9 @@ class Index extends Component public function mount() { + if (auth()->user()?->isMember() && auth()->user()->currentTeam()->show_boarding === true) { + return redirect()->route('dashboard'); + } $this->privateKeyName = generate_random_name(); $this->remoteServerName = generate_random_name(); if (isDev()) { diff --git a/app/Livewire/Team/AdminView.php b/app/Livewire/Team/AdminView.php new file mode 100644 index 000000000..12546ff1b --- /dev/null +++ b/app/Livewire/Team/AdminView.php @@ -0,0 +1,117 @@ +route('dashboard'); + } + $this->getUsers(); + } + public function submitSearch() + { + if ($this->search !== "") { + $this->users = User::where(function ($query) { + $query->where('name', 'like', "%{$this->search}%") + ->orWhere('email', 'like', "%{$this->search}%"); + })->get()->filter(function ($user) { + return $user->id !== auth()->id(); + }); + } else { + $this->getUsers(); + } + } + public function getUsers() + { + $this->users = User::where('id', '!=', auth()->id())->get(); + // $this->users = User::all(); + } + private function finalizeDeletion(User $user, Team $team) + { + $servers = $team->servers; + foreach ($servers as $server) { + $resources = $server->definedResources(); + foreach ($resources as $resource) { + ray("Deleting resource: " . $resource->name); + $resource->forceDelete(); + } + ray("Deleting server: " . $server->name); + $server->forceDelete(); + } + + $projects = $team->projects; + foreach ($projects as $project) { + ray("Deleting project: " . $project->name); + $project->forceDelete(); + } + $team->members()->detach($user->id); + ray('Deleting team: ' . $team->name); + $team->delete(); + } + public function delete($id) + { + $user = User::find($id); + $teams = $user->teams; + foreach ($teams as $team) { + ray($team->name); + $user_alone_in_team = $team->members->count() === 1; + if ($team->id === 0) { + if ($user_alone_in_team) { + ray('user is alone in the root team, do nothing'); + return $this->dispatch('error', 'User is alone in the root team, cannot delete'); + } + } + if ($user_alone_in_team) { + ray('user is alone in the team'); + $this->finalizeDeletion($user, $team); + continue; + } + ray('user is not alone in the team'); + if ($user->isOwner()) { + $found_other_owner_or_admin = $team->members->filter(function ($member) { + return $member->pivot->role === 'owner' || $member->pivot->role === 'admin'; + })->where('id', '!=', $user->id)->first(); + + if ($found_other_owner_or_admin) { + ray('found other owner or admin'); + $team->members()->detach($user->id); + continue; + } else { + $found_other_member_who_is_not_owner = $team->members->filter(function ($member) { + return $member->pivot->role === 'member'; + })->first(); + if ($found_other_member_who_is_not_owner) { + ray('found other member who is not owner'); + $found_other_member_who_is_not_owner->pivot->role = 'owner'; + $found_other_member_who_is_not_owner->pivot->save(); + $team->members()->detach($user->id); + } else { + // This should never happen as if the user is the only member in the team, the team should be deleted already. + ray('found no other member who is not owner'); + $this->finalizeDeletion($user, $team); + } + continue; + } + } else { + ray('user is not owner'); + $team->members()->detach($user->id); + } + } + ray("Deleting user: " . $user->name); + $user->delete(); + $this->getUsers(); + } + public function render() + { + return view('livewire.team.admin-view'); + } +} diff --git a/app/Models/Environment.php b/app/Models/Environment.php index 7ed9e38e5..a1f3e4190 100644 --- a/app/Models/Environment.php +++ b/app/Models/Environment.php @@ -8,6 +8,18 @@ class Environment extends Model { protected $guarded = []; + + protected static function booted() + { + static::deleting(function ($environment) { + $shared_variables = $environment->environment_variables(); + foreach ($shared_variables as $shared_variable) { + ray('Deleting environment shared variable: ' . $shared_variable->name); + $shared_variable->delete(); + } + + }); + } public function isEmpty() { return $this->applications()->count() == 0 && diff --git a/app/Models/Project.php b/app/Models/Project.php index 2621d3da1..c2be8cc32 100644 --- a/app/Models/Project.php +++ b/app/Models/Project.php @@ -25,6 +25,11 @@ protected static function booted() static::deleting(function ($project) { $project->environments()->delete(); $project->settings()->delete(); + $shared_variables = $project->environment_variables(); + foreach ($shared_variables as $shared_variable) { + ray('Deleting project shared variable: ' . $shared_variable->name); + $shared_variable->delete(); + } }); } public function environment_variables() @@ -55,6 +60,7 @@ public function applications() return $this->hasManyThrough(Application::class, Environment::class); } + public function postgresqls() { return $this->hasManyThrough(StandalonePostgresql::class, Environment::class); @@ -91,4 +97,7 @@ public function resource_count() { return $this->applications()->count() + $this->postgresqls()->count() + $this->redis()->count() + $this->mongodbs()->count() + $this->mysqls()->count() + $this->mariadbs()->count() + $this->keydbs()->count() + $this->dragonflies()->count() + $this->services()->count() + $this->clickhouses()->count(); } + public function databases() { + return $this->postgresqls()->get()->merge($this->redis()->get())->merge($this->mongodbs()->get())->merge($this->mysqls()->get())->merge($this->mariadbs()->get())->merge($this->keydbs()->get())->merge($this->dragonflies()->get())->merge($this->clickhouses()->get()); + } } diff --git a/app/Models/Team.php b/app/Models/Team.php index 29e434a5d..81206019f 100644 --- a/app/Models/Team.php +++ b/app/Models/Team.php @@ -26,6 +26,34 @@ protected static function booted() throw new \Exception('You are not allowed to update this team.'); } }); + + static::deleting(function ($team) { + $keys = $team->privateKeys; + foreach ($keys as $key) { + ray('Deleting key: ' . $key->name); + $key->delete(); + } + $sources = $team->sources(); + foreach ($sources as $source) { + ray('Deleting source: ' . $source->name); + $source->delete(); + } + $tags = Tag::whereTeamId($team->id)->get(); + foreach ($tags as $tag) { + ray('Deleting tag: ' . $tag->name); + $tag->delete(); + } + $shared_variables = $team->environment_variables(); + foreach ($shared_variables as $shared_variable) { + ray('Deleting team shared variable: ' . $shared_variable->name); + $shared_variable->delete(); + } + $s3s = $team->s3s; + foreach ($s3s as $s3) { + ray('Deleting s3: ' . $s3->name); + $s3->delete(); + } + }); } public function routeNotificationForDiscord() diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index de3d50d1a..6453108eb 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -95,6 +95,9 @@ function currentTeam() function showBoarding(): bool { + if (auth()->user()?->isMember()) { + return false; + } return currentTeam()->show_boarding ?? false; } function refreshSession(?Team $team = null): void diff --git a/database/seeders/TestTeamSeeder.php b/database/seeders/TestTeamSeeder.php new file mode 100644 index 000000000..1d660c713 --- /dev/null +++ b/database/seeders/TestTeamSeeder.php @@ -0,0 +1,42 @@ +create([ + 'name' => '1 personal, 1 other team, owner, no other members', + 'email' => '1@example.com', + ]); + $team = Team::create([ + 'name' => "1@example.com", + 'personal_team' => false, + 'show_boarding' => true + ]); + $user->teams()->attach($team, ['role' => 'owner']); + + // User has 2 teams, 1 personal, 1 other where it is the owner and 1 other member is in the team + $user = User::factory()->create([ + 'name' => 'owner: 1 personal, 1 other team, owner, 1 other member', + 'email' => '2@example.com', + ]); + $team = Team::create([ + 'name' => "2@example.com", + 'personal_team' => false, + 'show_boarding' => true + ]); + $user->teams()->attach($team, ['role' => 'owner']); + $user = User::factory()->create([ + 'name' => 'member: 1 personal, 1 other team, owner, 1 other member', + 'email' => '3@example.com', + ]); + $team->members()->attach($user, ['role' => 'member']); + } +} diff --git a/resources/css/app.css b/resources/css/app.css index bb05b783b..cae83b0de 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -41,7 +41,7 @@ option { } .button { - @apply flex items-center justify-center gap-2 px-2 py-1 text-sm text-black normal-case border rounded cursor-pointer bg-neutral-200/50 border-neutral-300 hover:bg-neutral-300 dark:bg-coolgray-200 dark:text-white dark:hover:text-white dark:hover:bg-coolgray-500 dark:border-black hover:text-black disabled:cursor-not-allowed min-w-fit focus:outline-1 dark:disabled:text-neutral-600 disabled:border-none disabled:hover:bg-transparent disabled:bg-transparent disabled:text-neutral-300; + @apply flex items-center justify-center gap-2 px-2 py-1 text-sm text-black normal-case border rounded cursor-pointer bg-neutral-200/50 border-neutral-300 hover:bg-neutral-300 dark:bg-coolgray-200 dark:text-white dark:hover:text-white dark:hover:bg-coolgray-500 dark:border-coolgray-300 hover:text-black disabled:cursor-not-allowed min-w-fit focus:outline-1 dark:disabled:text-neutral-600 disabled:border-none disabled:hover:bg-transparent disabled:bg-transparent disabled:text-neutral-300; } button[isError]:not(:disabled) { diff --git a/resources/views/components/team/navbar.blade.php b/resources/views/components/team/navbar.blade.php index 8bcceeb87..aa88aad51 100644 --- a/resources/views/components/team/navbar.blade.php +++ b/resources/views/components/team/navbar.blade.php @@ -15,6 +15,12 @@ href="{{ route('team.member.index') }}"> + @if (isInstanceAdmin()) + + + + @endif
diff --git a/resources/views/livewire/team/admin-view.blade.php b/resources/views/livewire/team/admin-view.blade.php new file mode 100644 index 000000000..ae9258603 --- /dev/null +++ b/resources/views/livewire/team/admin-view.blade.php @@ -0,0 +1,29 @@ +
+ +
+ + Search + +

Users

+
+ @forelse ($users as $user) +
+
{{ $user->name }}
+
{{ $user->email }}
+
+
+ + This will delete all resources (application, databases, services, configurations, servers, + private keys, tags, etc.) from Coolify and from the server (if it's reachable). +

+ It is not reversible.

+
Think twice!
+
+
+
+ @empty +
No users found other than the root.
+ @endforelse +
+
diff --git a/routes/web.php b/routes/web.php index 8d0651f8f..feb2dd0eb 100644 --- a/routes/web.php +++ b/routes/web.php @@ -82,7 +82,7 @@ use App\Livewire\Tags\Index as TagsIndex; use App\Livewire\Tags\Show as TagsShow; - +use App\Livewire\Team\AdminView as TeamAdminView; use App\Livewire\Waitlist\Index as WaitlistIndex; use App\Models\ScheduledDatabaseBackupExecution; use Illuminate\Support\Facades\Storage; @@ -160,6 +160,7 @@ Route::prefix('team')->group(function () { Route::get('/', TeamIndex::class)->name('team.index'); Route::get('/members', TeamMemberIndex::class)->name('team.member.index'); + Route::get('/admin', TeamAdminView::class)->name('team.admin-view'); }); Route::get('/command-center', CommandCenterIndex::class)->name('command-center');