I nearly locked myself out of my own admin panel once. Testing the user-delete button, clicked my own row out of habit, confirmed the modal without thinking. The account was gone. So were all its sessions — obviously — and I had to re-seed the database to get back in.
The fix is two lines, and both belong on the backend, not in a confirm dialog. Frontend confirm dialogs are a suggestion. The API is the law.
Here's the destroy endpoint:
public function destroy(Request $request, User $user): JsonResponse
{
if ($request->user()->is($user)) {
return response()->json(['message' => 'You cannot delete your own account.'], 422);
}
$user->tokens()->delete();
$user->delete();
return response()->json(null, 204);
}
The first line of defense is $request->user()->is($user). Eloquent's is() compares the model's key and table — it answers "is this the same row?" without you writing ->id === ->id and getting it wrong when one side is a string and the other an int. If the authenticated admin is deleting themselves, they get a 422 with a plain-language message. The route-model-bound $user is a real User model here, so is() works directly.
Why 422 and not 403? Because this isn't an authorization failure — the admin is authorized to delete users. The request is well-formed but semantically invalid, the same way submitting an already-taken email is. My API has exactly five failure shapes, and "valid request, invalid idea" maps to 422. The frontend already knows how to render 422s (the 422 error contract from an earlier article), so the message lands in the right banner automatically.
The second line of defense is $user->tokens()->delete() before $user->delete(). When you delete a Sanctum user, their personal access tokens don't disappear with them — token rows live in their own table. A stale token row pointing at a deleted user is usually harmless, but "usually" is not a security posture. Deleting the tokens first means no orphaned tokens, and it also means if that deleted user was logged in on another device, their session dies the same moment their account does. Belt and braces: the tokens are gone, then the user is gone.
Order matters. Tokens first, user second — if $user->delete() throws, you haven't left the user alive with zero tokens, which would be a weird state to debug at 2am.
A common objection: "just hide the delete button on your own row." I do that too — the frontend disables it. But the endpoint is the part that actually protects you, because endpoints get called by scripts, by tests, by the mobile app someone builds next year. The rule lives where it can't be bypassed: in destroy, before anything destructive happens.
These are the small rules that make a starter kit trustworthy — not the features list, but the ways it refuses to shoot itself in the foot. That's the standard I ship in my Laravel + React SaaS Starter Kit: https://kamranofficial.gumroad.com/l/mhekoig — $19 one-time.
Top comments (0)