fix(auth): last-admin guard counts only ACTIVE admins (prevents lockout)

set_user_role (demote) and admin_delete_user counted ALL admins incl. suspended
ones, so with e.g. 1 active + 1 suspended admin you could demote/delete the only
ACTIVE admin — leaving a single suspended admin who can never sign in → permanent
admin lockout needing DB surgery. Now both guards mirror the (already-correct)
suspend guard: block only when the target is an active admin AND it's the last one
(`not target.is_suspended and count_admins(active_only=True) <= 1`) — which also
correctly still allows removing a SUSPENDED admin while one active admin remains.
This commit is contained in:
npeter83 2026-07-11 21:26:39 +02:00
parent 7e562c6cb5
commit f5fac09833

View file

@ -166,7 +166,12 @@ def set_user_role(
raise HTTPException(status_code=400, detail="The demo account's role can't be changed.")
if target.id == admin.id:
raise HTTPException(status_code=400, detail="You can't change your own role.")
if target.role == "admin" and role == "user" and count_admins(db) <= 1:
if (
target.role == "admin"
and role == "user"
and not target.is_suspended
and count_admins(db, active_only=True) <= 1
):
raise HTTPException(status_code=400, detail="Can't remove the last admin.")
changed = target.role != role
target.role = role
@ -233,7 +238,7 @@ def admin_delete_user(
raise HTTPException(
status_code=400, detail="Delete your own account from Settings → Account."
)
if target.role == "admin" and count_admins(db) <= 1:
if target.role == "admin" and not target.is_suspended and count_admins(db, active_only=True) <= 1:
raise HTTPException(status_code=400, detail="Can't delete the last admin.")
purge_user(db, target, background)
return {"deleted": user_id}