fix(accounts): make last-user removal atomic (lock + recount) to prevent a zero-user lock-out race
Codex P2: concurrent deletes could both pass the >1 check. Now the count + delete run in one transaction under a row lock. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>feat/v1-foundation
parent
3399a5017a
commit
79c25fbf83
|
|
@ -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'));
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue