refactor(user): streamline user deletion process and enhance team management logic

This commit is contained in:
Andras Bacsai
2025-06-25 12:14:35 +02:00
parent 7fb85314e5
commit e746e212cb
4 changed files with 105 additions and 66 deletions

View File

@@ -3,7 +3,6 @@
namespace App\Livewire\Team; namespace App\Livewire\Team;
use App\Models\InstanceSettings; use App\Models\InstanceSettings;
use App\Models\Team;
use App\Models\User; use App\Models\User;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash; 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) public function delete($id, $password)
{ {
if (! isInstanceAdmin()) { if (! isInstanceAdmin()) {
return redirect()->route('dashboard'); return redirect()->route('dashboard');
} }
if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) { if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) {
if (! Hash::check($password, Auth::user()->password)) { if (! Hash::check($password, Auth::user()->password)) {
$this->addError('password', 'The provided password is incorrect.'); $this->addError('password', 'The provided password is incorrect.');
@@ -84,52 +65,22 @@ class AdminView extends Component
return; return;
} }
} }
if (! auth()->user()->isInstanceAdmin()) { if (! auth()->user()->isInstanceAdmin()) {
return $this->dispatch('error', 'You are not authorized to delete users'); return $this->dispatch('error', 'You are not authorized to delete users');
} }
$user = User::find($id); $user = User::find($id);
$teams = $user->teams; if (! $user) {
foreach ($teams as $team) { return $this->dispatch('error', 'User not found');
$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; try {
}
} else {
$team->members()->detach($user->id);
}
}
$user->delete(); $user->delete();
$this->getUsers(); $this->getUsers();
} catch (\Exception $e) {
return $this->dispatch('error', $e->getMessage());
}
} }
public function render() public function render()

View File

@@ -16,11 +16,9 @@ class Invitations extends Component
{ {
try { try {
$invitation = TeamInvitation::ownedByCurrentTeam()->findOrFail($invitation_id); $invitation = TeamInvitation::ownedByCurrentTeam()->findOrFail($invitation_id);
$user = User::whereEmail($invitation->email)->firstOrFail(); $user = User::whereEmail($invitation->email)->first();
$emailVerified = $user->hasVerifiedEmail(); if (filled($user)) {
$forcePasswordReset = $user->force_password_reset; $user->deleteIfNotVerifiedAndForcePasswordReset();
if ($emailVerified === false && $forcePasswordReset === true) {
$user->delete();
} }
$invitation->delete(); $invitation->delete();

View File

@@ -33,6 +33,10 @@ class TeamInvitation extends Model
return true; return true;
} else { } else {
$this->delete(); $this->delete();
$user = User::whereEmail($this->email)->first();
if (filled($user)) {
$user->deleteIfNotVerifiedAndForcePasswordReset();
}
return false; return false;
} }

View File

@@ -72,6 +72,92 @@ class User extends Authenticatable implements SendsEmail
$new_team = Team::create($team); $new_team = Team::create($team);
$user->teams()->attach($new_team, ['role' => 'owner']); $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() public function recreate_personal_team()