4abfcbc293
Add OperatorUser SQLAlchemy model (operator_users table, auto-created by create_all) with email uniqueness, default active/must_change_password/ failed_login_count, and sessions_valid_from truncated to whole seconds. Add backend/operator_auth.py with feature flag, cookie constants, _ROLE_RANK map, role_at_least(), and _norm_email() helpers. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
36 lines
1.3 KiB
Python
36 lines
1.3 KiB
Python
# backend/operator_auth.py
|
|
"""Operator authentication: the deny-by-default gate, session cookie, login +
|
|
lockout, and the small data helpers shared by the routes and the CLI. Reuses the
|
|
argon2 hasher (auth_passwords) and the HMAC signer (auth_cookies).
|
|
|
|
The flag and SessionLocal are read as module globals at call time so tests can
|
|
monkeypatch them."""
|
|
import os
|
|
import time
|
|
import uuid
|
|
from datetime import datetime, timedelta
|
|
|
|
from backend.models import OperatorUser
|
|
from backend.auth_passwords import hash_password, verify_password, generate_password
|
|
|
|
# Feature flag — OFF by default. When off, the gate and require_role both pass
|
|
# everything through and the app behaves exactly as it does today.
|
|
OPERATOR_AUTH_ENABLED = os.getenv("OPERATOR_AUTH_ENABLED", "false").lower() in ("1", "true", "yes")
|
|
|
|
COOKIE_NAME = "tv_session"
|
|
COOKIE_MAX_AGE = 60 * 60 * 24 * 30 # 30 days ("remember this device")
|
|
MAX_LOGIN_FAILURES = 5
|
|
LOCK_MINUTES = 15
|
|
|
|
# Role ladder — a rank map so checks read naturally and 'operator' slots in later.
|
|
_ROLE_RANK = {"operator": 10, "admin": 20, "superadmin": 30}
|
|
|
|
|
|
def role_at_least(role: str, minimum: str) -> bool:
|
|
"""True iff `role` ranks at or above `minimum`. Unknown roles rank as 0."""
|
|
return _ROLE_RANK.get(role, 0) >= _ROLE_RANK[minimum]
|
|
|
|
|
|
def _norm_email(email: str) -> str:
|
|
return (email or "").strip().lower()
|