From e746e212cb4a128cb49395ea118f9e10a2ab4c2f Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Wed, 25 Jun 2025 12:14:35 +0200 Subject: [PATCH] refactor(user): streamline user deletion process and enhance team management logic --- app/Livewire/Team/AdminView.php | 73 +++++--------------------- app/Livewire/Team/Invitations.php | 8 ++- app/Models/TeamInvitation.php | 4 ++ app/Models/User.php | 86 +++++++++++++++++++++++++++++++ 4 files changed, 105 insertions(+), 66 deletions(-) diff --git a/app/Livewire/Team/AdminView.php b/app/Livewire/Team/AdminView.php index cfb47d9d8..6d6915ae2 100644 --- a/app/Livewire/Team/AdminView.php +++ b/app/Livewire/Team/AdminView.php @@ -3,7 +3,6 @@ namespace App\Livewire\Team; use App\Models\InstanceSettings; -use App\Models\Team; use App\Models\User; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Hash; @@ -53,30 +52,12 @@ class AdminView extends Component } } - private function finalizeDeletion(User $user, Team $team) - { - $servers = $team->servers; - foreach ($servers as $server) { - $resources = $server->definedResources(); - foreach ($resources as $resource) { - $resource->forceDelete(); - } - $server->forceDelete(); - } - - $projects = $team->projects; - foreach ($projects as $project) { - $project->forceDelete(); - } - $team->members()->detach($user->id); - $team->delete(); - } - public function delete($id, $password) { if (! isInstanceAdmin()) { return redirect()->route('dashboard'); } + if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) { if (! Hash::check($password, Auth::user()->password)) { $this->addError('password', 'The provided password is incorrect.'); @@ -84,52 +65,22 @@ class AdminView extends Component return; } } + if (! auth()->user()->isInstanceAdmin()) { return $this->dispatch('error', 'You are not authorized to delete users'); } + $user = User::find($id); - $teams = $user->teams; - foreach ($teams as $team) { - $user_alone_in_team = $team->members->count() === 1; - if ($team->id === 0) { - if ($user_alone_in_team) { - return $this->dispatch('error', 'User is alone in the root team, cannot delete'); - } - } - if ($user_alone_in_team) { - $this->finalizeDeletion($user, $team); - - continue; - } - 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) { - $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) { - $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->finalizeDeletion($user, $team); - } - - continue; - } - } else { - $team->members()->detach($user->id); - } + if (! $user) { + return $this->dispatch('error', 'User not found'); + } + + try { + $user->delete(); + $this->getUsers(); + } catch (\Exception $e) { + return $this->dispatch('error', $e->getMessage()); } - $user->delete(); - $this->getUsers(); } public function render() diff --git a/app/Livewire/Team/Invitations.php b/app/Livewire/Team/Invitations.php index 6dc3fce25..3af0e0e92 100644 --- a/app/Livewire/Team/Invitations.php +++ b/app/Livewire/Team/Invitations.php @@ -16,11 +16,9 @@ class Invitations extends Component { try { $invitation = TeamInvitation::ownedByCurrentTeam()->findOrFail($invitation_id); - $user = User::whereEmail($invitation->email)->firstOrFail(); - $emailVerified = $user->hasVerifiedEmail(); - $forcePasswordReset = $user->force_password_reset; - if ($emailVerified === false && $forcePasswordReset === true) { - $user->delete(); + $user = User::whereEmail($invitation->email)->first(); + if (filled($user)) { + $user->deleteIfNotVerifiedAndForcePasswordReset(); } $invitation->delete(); diff --git a/app/Models/TeamInvitation.php b/app/Models/TeamInvitation.php index bc1a90d58..0fea1806b 100644 --- a/app/Models/TeamInvitation.php +++ b/app/Models/TeamInvitation.php @@ -33,6 +33,10 @@ class TeamInvitation extends Model return true; } else { $this->delete(); + $user = User::whereEmail($this->email)->first(); + if (filled($user)) { + $user->deleteIfNotVerifiedAndForcePasswordReset(); + } return false; } diff --git a/app/Models/User.php b/app/Models/User.php index f9515ad09..0e420585d 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -72,6 +72,92 @@ class User extends Authenticatable implements SendsEmail $new_team = Team::create($team); $user->teams()->attach($new_team, ['role' => 'owner']); }); + + static::deleting(function (User $user) { + + $teams = $user->teams; + foreach ($teams as $team) { + $user_alone_in_team = $team->members->count() === 1; + + // Prevent deletion if user is alone in root team + if ($team->id === 0 && $user_alone_in_team) { + throw new \Exception('User is alone in the root team, cannot delete'); + } + + if ($user_alone_in_team) { + static::finalizeTeamDeletion($user, $team); + // Delete any pending team invitations for this user + TeamInvitation::whereEmail($user->email)->delete(); + + continue; + } + + // Load the user's role for this team + $userRole = $team->members->where('id', $user->id)->first()?->pivot?->role; + + if ($userRole === 'owner') { + $found_other_owner_or_admin = $team->members->filter(function ($member) use ($user) { + return ($member->pivot->role === 'owner' || $member->pivot->role === 'admin') && $member->id !== $user->id; + })->first(); + + if ($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) { + $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 { + static::finalizeTeamDeletion($user, $team); + } + + continue; + } + } else { + $team->members()->detach($user->id); + } + } + }); + } + + /** + * Finalize team deletion by cleaning up all associated resources + */ + private static function finalizeTeamDeletion(User $user, Team $team) + { + $servers = $team->servers; + foreach ($servers as $server) { + $resources = $server->definedResources(); + foreach ($resources as $resource) { + $resource->forceDelete(); + } + $server->forceDelete(); + } + + $projects = $team->projects; + foreach ($projects as $project) { + $project->forceDelete(); + } + + $team->members()->detach($user->id); + $team->delete(); + } + + /** + * Delete the user if they are not verified and have a force password reset. + * This is used to clean up users that have been invited, does not accepted the invitation (and did not verify their email and have a force password reset). + */ + public function deleteIfNotVerifiedAndForcePasswordReset() + { + if ($this->hasVerifiedEmail() === false && $this->force_password_reset === true) { + $this->delete(); + } } public function recreate_personal_team()