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

@ -1,5 +1,4 @@
"""Global admin-controlled app state (e.g. pausing background sync, first-run setup)."""
import hashlib
import logging
import secrets
@ -7,6 +6,7 @@ from sqlalchemy.orm import Session
from app.config import settings
from app.models import AppState
from app.security import hash_token
log = logging.getLogger("siftlode.state")
@ -73,16 +73,12 @@ def mark_configured(db: Session) -> None:
log.info("Instance marked configured; setup wizard disabled.")
def _hash_token(raw: str) -> str:
return hashlib.sha256(raw.encode()).hexdigest()
def rotate_setup_token(db: Session) -> str:
"""Generate a fresh one-time setup token, persist only its hash, and return the raw token
(logged at startup so the admin can open the wizard)."""
raw = secrets.token_urlsafe(24)
row = _row(db)
row.setup_token_hash = _hash_token(raw)
row.setup_token_hash = hash_token(raw)
db.add(row)
db.commit()
return raw
@ -93,7 +89,7 @@ def check_setup_token(db: Session, raw: str | None) -> bool:
if not raw:
return False
stored = _row(db).setup_token_hash
return stored is not None and secrets.compare_digest(stored, _hash_token(raw))
return stored is not None and secrets.compare_digest(stored, hash_token(raw))
def get_maintenance_batch(db: Session) -> int: