diff --git a/app/Livewire/Settings/Users.php b/app/Livewire/Settings/Users.php index 9acbf6c..9d0b0a9 100644 --- a/app/Livewire/Settings/Users.php +++ b/app/Livewire/Settings/Users.php @@ -6,6 +6,7 @@ use App\Models\AuditEvent; use App\Models\User; use App\Services\SessionService; use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\DB; use Livewire\Attributes\On; use Livewire\Component; @@ -57,19 +58,36 @@ class Users extends Component #[On('userRemoved')] public function remove(int $userId, SessionService $sessions): void { - $user = User::find($userId); - if (! $user || ! $this->canRemove($user)) { + $self = Auth::id(); + + // Atomic: lock the user rows + recount inside the transaction so two concurrent + // deletes can't both pass the "more than one account" check and strand the + // install with zero users (a lock-out). The last account is never removable. + $email = DB::transaction(function () use ($userId, $self, $sessions): ?string { + if (User::lockForUpdate()->count() <= 1) { + return null; + } + + $user = User::find($userId); + if (! $user || $user->getKey() === $self) { + return null; + } + + $email = $user->email; + + // Kill every session + rotate the remember_token BEFORE deleting, so a + // stolen remember-me cookie can never resurrect the account between the + // session wipe and the row delete. + $sessions->logoutUserEverywhere($user); + $user->delete(); + + return $email; + }); + + if ($email === null) { return; } - $email = $user->email; - - // Kill every session + rotate the remember_token BEFORE deleting, so a - // stolen remember-me cookie can never resurrect the account between the - // session wipe and the row delete. - $sessions->logoutUserEverywhere($user); - $user->delete(); - $this->audit('user.delete', $email); $this->dispatch('notify', message: __('accounts.remove_notify')); }