refactor(auth): centralize token hashing, email validation & admin gating

- app/utils.py: shared valid_email() + now_utc() (one email regex, was duplicated
  in auth.py and admin.py with a looser "@"-in check elsewhere).
- security.hash_token(): one SHA-256 token hasher (was duplicated in auth.py + state.py).
- auth.admin_user dependency + count_admins() helper, replacing the inline role
  checks and the last-admin count query repeated across admin.py/me.py/tags.py.
This commit is contained in:
npeter83 2026-06-26 03:15:36 +02:00
parent 8ef748c7b8
commit 63701f5344
7 changed files with 84 additions and 65 deletions

View file

@ -3,6 +3,7 @@ from sqlalchemy import func, select
from sqlalchemy.orm import Session
from app.auth import (
count_admins,
current_user,
google_enabled,
has_read_scope,
@ -111,13 +112,11 @@ def delete_my_account(
(that would lock everyone out of the admin surfaces)."""
if user.is_demo:
raise HTTPException(status_code=403, detail="The shared demo account can't be deleted.")
if user.role == "admin":
admins = db.scalar(select(func.count()).select_from(User).where(User.role == "admin")) or 0
if admins <= 1:
raise HTTPException(
status_code=400,
detail="You're the only admin — promote another admin before deleting your account.",
)
if user.role == "admin" and count_admins(db) <= 1:
raise HTTPException(
status_code=400,
detail="You're the only admin — promote another admin before deleting your account.",
)
user_id = user.id
# Full erasure (cascades) + access-request cleanup + Google-grant revocation; shared with the
# admin delete path. Capture the id first — `user` is detached once purge_user deletes the row.