feat: admin view for deleting users
This commit is contained in:
parent
7feb788ed3
commit
98b6aec203
@ -24,9 +24,9 @@ class GetContainersStatus
|
|||||||
|
|
||||||
public function handle(Server $server)
|
public function handle(Server $server)
|
||||||
{
|
{
|
||||||
if (isDev()) {
|
// if (isDev()) {
|
||||||
$server = Server::find(0);
|
// $server = Server::find(0);
|
||||||
}
|
// }
|
||||||
$this->server = $server;
|
$this->server = $server;
|
||||||
if (!$this->server->isFunctional()) {
|
if (!$this->server->isFunctional()) {
|
||||||
return 'Server is not ready.';
|
return 'Server is not ready.';
|
||||||
@ -154,7 +154,7 @@ private function sentinel()
|
|||||||
if ($isPublic) {
|
if ($isPublic) {
|
||||||
$foundTcpProxy = $containers->filter(function ($value, $key) use ($uuid) {
|
$foundTcpProxy = $containers->filter(function ($value, $key) use ($uuid) {
|
||||||
if ($this->server->isSwarm()) {
|
if ($this->server->isSwarm()) {
|
||||||
// TODO: fix this with sentinel
|
// TODO: fix this with sentinel
|
||||||
return data_get($value, 'Spec.Name') === "coolify-proxy_$uuid";
|
return data_get($value, 'Spec.Name') === "coolify-proxy_$uuid";
|
||||||
} else {
|
} else {
|
||||||
return data_get($value, 'name') === "$uuid-proxy";
|
return data_get($value, 'name') === "$uuid-proxy";
|
||||||
@ -316,7 +316,7 @@ private function sentinel()
|
|||||||
$this->server->proxyType();
|
$this->server->proxyType();
|
||||||
$foundProxyContainer = $containers->filter(function ($value, $key) {
|
$foundProxyContainer = $containers->filter(function ($value, $key) {
|
||||||
if ($this->server->isSwarm()) {
|
if ($this->server->isSwarm()) {
|
||||||
// TODO: fix this with sentinel
|
// TODO: fix this with sentinel
|
||||||
return data_get($value, 'Spec.Name') === 'coolify-proxy_traefik';
|
return data_get($value, 'Spec.Name') === 'coolify-proxy_traefik';
|
||||||
} else {
|
} else {
|
||||||
return data_get($value, 'name') === 'coolify-proxy';
|
return data_get($value, 'name') === 'coolify-proxy';
|
||||||
@ -442,19 +442,21 @@ private function old_way()
|
|||||||
if ($database_id) {
|
if ($database_id) {
|
||||||
$service_db = ServiceDatabase::where('id', $database_id)->first();
|
$service_db = ServiceDatabase::where('id', $database_id)->first();
|
||||||
if ($service_db) {
|
if ($service_db) {
|
||||||
$uuid = $service_db->service->uuid;
|
$uuid = data_get($service_db, 'service.uuid');
|
||||||
$isPublic = data_get($service_db, 'is_public');
|
if ($uuid) {
|
||||||
if ($isPublic) {
|
$isPublic = data_get($service_db, 'is_public');
|
||||||
$foundTcpProxy = $containers->filter(function ($value, $key) use ($uuid) {
|
if ($isPublic) {
|
||||||
if ($this->server->isSwarm()) {
|
$foundTcpProxy = $containers->filter(function ($value, $key) use ($uuid) {
|
||||||
return data_get($value, 'Spec.Name') === "coolify-proxy_$uuid";
|
if ($this->server->isSwarm()) {
|
||||||
} else {
|
return data_get($value, 'Spec.Name') === "coolify-proxy_$uuid";
|
||||||
return data_get($value, 'Name') === "/$uuid-proxy";
|
} 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));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -52,6 +52,9 @@ class Index extends Component
|
|||||||
|
|
||||||
public function mount()
|
public function mount()
|
||||||
{
|
{
|
||||||
|
if (auth()->user()?->isMember() && auth()->user()->currentTeam()->show_boarding === true) {
|
||||||
|
return redirect()->route('dashboard');
|
||||||
|
}
|
||||||
$this->privateKeyName = generate_random_name();
|
$this->privateKeyName = generate_random_name();
|
||||||
$this->remoteServerName = generate_random_name();
|
$this->remoteServerName = generate_random_name();
|
||||||
if (isDev()) {
|
if (isDev()) {
|
||||||
|
117
app/Livewire/Team/AdminView.php
Normal file
117
app/Livewire/Team/AdminView.php
Normal 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');
|
||||||
|
}
|
||||||
|
}
|
@ -8,6 +8,18 @@
|
|||||||
class Environment extends Model
|
class Environment extends Model
|
||||||
{
|
{
|
||||||
protected $guarded = [];
|
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()
|
public function isEmpty()
|
||||||
{
|
{
|
||||||
return $this->applications()->count() == 0 &&
|
return $this->applications()->count() == 0 &&
|
||||||
|
@ -25,6 +25,11 @@ protected static function booted()
|
|||||||
static::deleting(function ($project) {
|
static::deleting(function ($project) {
|
||||||
$project->environments()->delete();
|
$project->environments()->delete();
|
||||||
$project->settings()->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()
|
public function environment_variables()
|
||||||
@ -55,6 +60,7 @@ public function applications()
|
|||||||
return $this->hasManyThrough(Application::class, Environment::class);
|
return $this->hasManyThrough(Application::class, Environment::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public function postgresqls()
|
public function postgresqls()
|
||||||
{
|
{
|
||||||
return $this->hasManyThrough(StandalonePostgresql::class, Environment::class);
|
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();
|
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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -26,6 +26,34 @@ protected static function booted()
|
|||||||
throw new \Exception('You are not allowed to update this team.');
|
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()
|
public function routeNotificationForDiscord()
|
||||||
|
@ -95,6 +95,9 @@ function currentTeam()
|
|||||||
|
|
||||||
function showBoarding(): bool
|
function showBoarding(): bool
|
||||||
{
|
{
|
||||||
|
if (auth()->user()?->isMember()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return currentTeam()->show_boarding ?? false;
|
return currentTeam()->show_boarding ?? false;
|
||||||
}
|
}
|
||||||
function refreshSession(?Team $team = null): void
|
function refreshSession(?Team $team = null): void
|
||||||
|
42
database/seeders/TestTeamSeeder.php
Normal file
42
database/seeders/TestTeamSeeder.php
Normal 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']);
|
||||||
|
}
|
||||||
|
}
|
@ -41,7 +41,7 @@ option {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.button {
|
.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) {
|
button[isError]:not(:disabled) {
|
||||||
|
@ -15,6 +15,12 @@
|
|||||||
href="{{ route('team.member.index') }}">
|
href="{{ route('team.member.index') }}">
|
||||||
<button>Members</button>
|
<button>Members</button>
|
||||||
</a>
|
</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>
|
<div class="flex-1"></div>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
29
resources/views/livewire/team/admin-view.blade.php
Normal file
29
resources/views/livewire/team/admin-view.blade.php
Normal 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>
|
@ -82,7 +82,7 @@
|
|||||||
|
|
||||||
use App\Livewire\Tags\Index as TagsIndex;
|
use App\Livewire\Tags\Index as TagsIndex;
|
||||||
use App\Livewire\Tags\Show as TagsShow;
|
use App\Livewire\Tags\Show as TagsShow;
|
||||||
|
use App\Livewire\Team\AdminView as TeamAdminView;
|
||||||
use App\Livewire\Waitlist\Index as WaitlistIndex;
|
use App\Livewire\Waitlist\Index as WaitlistIndex;
|
||||||
use App\Models\ScheduledDatabaseBackupExecution;
|
use App\Models\ScheduledDatabaseBackupExecution;
|
||||||
use Illuminate\Support\Facades\Storage;
|
use Illuminate\Support\Facades\Storage;
|
||||||
@ -160,6 +160,7 @@
|
|||||||
Route::prefix('team')->group(function () {
|
Route::prefix('team')->group(function () {
|
||||||
Route::get('/', TeamIndex::class)->name('team.index');
|
Route::get('/', TeamIndex::class)->name('team.index');
|
||||||
Route::get('/members', TeamMemberIndex::class)->name('team.member.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');
|
Route::get('/command-center', CommandCenterIndex::class)->name('command-center');
|
||||||
|
Loading…
Reference in New Issue
Block a user