feat: admin view for deleting users

This commit is contained in:
Andras Bacsai 2024-05-21 14:29:06 +02:00
parent 7feb788ed3
commit 98b6aec203
12 changed files with 271 additions and 19 deletions

View File

@ -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.';
@ -442,7 +442,8 @@ private function old_way()
if ($database_id) {
$service_db = ServiceDatabase::where('id', $database_id)->first();
if ($service_db) {
$uuid = $service_db->service->uuid;
$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) {
@ -459,6 +460,7 @@ private function old_way()
}
}
}
}
} else {
$database = $databases->where('uuid', $uuid)->first();
if ($database) {

View File

@ -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()) {

View File

@ -0,0 +1,117 @@
<?php
namespace App\Livewire\Team;
use App\Models\Team;
use App\Models\User;
use Livewire\Component;
class AdminView extends Component
{
public $users;
public ?string $search = "";
public function mount()
{
if (!isInstanceAdmin()) {
return redirect()->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');
}
}

View File

@ -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 &&

View File

@ -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());
}
}

View File

@ -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()

View File

@ -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

View File

@ -0,0 +1,42 @@
<?php
namespace Database\Seeders;
use App\Models\Team;
use App\Models\User;
use Illuminate\Database\Seeder;
class TestTeamSeeder extends Seeder
{
public function run(): void
{
// User has 2 teams, 1 personal, 1 other where it is the owner and no other members are in the team
$user = User::factory()->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']);
}
}

View File

@ -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) {

View File

@ -15,6 +15,12 @@
href="{{ route('team.member.index') }}">
<button>Members</button>
</a>
@if (isInstanceAdmin())
<a class="{{ request()->routeIs('team.admin-view') ? 'dark:text-white' : '' }}"
href="{{ route('team.admin-view') }}">
<button>Admin View</button>
</a>
@endif
<div class="flex-1"></div>
</nav>
</div>

View File

@ -0,0 +1,29 @@
<div>
<x-team.navbar />
<form wire:submit="submitSearch" class="flex flex-col gap-2 lg:flex-row">
<x-forms.input wire:model="search" placeholder="Search for a user" />
<x-forms.button type="submit">Search</x-forms.button>
</form>
<h3 class="pt-4">Users</h3>
<div class="flex flex-col gap-2 ">
@forelse ($users as $user)
<div class="flex items-center justify-center gap-2 bg-white box-without-bg dark:bg-coolgray-100">
<div>{{ $user->name }}</div>
<div>{{ $user->email }}</div>
<div class="flex-1"></div>
<div class="flex items-center justify-center gap-2 mx-4 text-xs font-bold ">
<x-modal-confirmation isErrorButton action="delete({{ $user->id }})" buttonTitle="Delete">
This will delete all resources (application, databases, services, configurations, servers,
private keys, tags, etc.) from Coolify and <span
class="font-bold text-red-500 dark:text-warning">from the server (if it's reachable)</span>.
<br> <br>
It is not reversible. <br><br>
<div class="font-bold text-red-500 dark:text-white">Think twice!</div>
</x-modal-confirmation>
</div>
</div>
@empty
<div>No users found other than the root.</div>
@endforelse
</div>
</div>

View File

@ -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');