46 lines
1.4 KiB
Python
46 lines
1.4 KiB
Python
"""Lightweight admin gate for the web UI.
|
|
|
|
A single shared PIN (`ADMIN_PIN` in .env) unlocks destructive actions
|
|
(deleting projects/competitors/employees, editing employees) for the browser
|
|
session via an HMAC-signed cookie. No per-user accounts — this is an internal
|
|
localhost tool, not a public app.
|
|
|
|
If `ADMIN_PIN` is empty the gate is OPEN (nothing restricted) so the tool is
|
|
never bricked by a missing setting; set a PIN to actually restrict.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import hmac
|
|
|
|
from starlette.requests import Request
|
|
|
|
from app.config import settings
|
|
|
|
ADMIN_COOKIE = "dld_admin"
|
|
COOKIE_MAX_AGE = 60 * 60 * 8 # 8 hours
|
|
|
|
|
|
def admin_configured() -> bool:
|
|
return bool(settings.admin_pin)
|
|
|
|
|
|
def admin_token() -> str | None:
|
|
"""The cookie value that proves admin: HMAC(pin, marker). None if no PIN."""
|
|
if not settings.admin_pin:
|
|
return None
|
|
return hmac.new(settings.admin_pin.encode(), b"dld-admin-v1", hashlib.sha256).hexdigest()
|
|
|
|
|
|
def pin_ok(pin: str) -> bool:
|
|
"""Constant-time check of a submitted PIN against the configured one."""
|
|
return bool(settings.admin_pin) and hmac.compare_digest((pin or "").strip(), settings.admin_pin)
|
|
|
|
|
|
def request_is_admin(request: Request) -> bool:
|
|
if not settings.admin_pin:
|
|
return True # gate not configured → open
|
|
token = request.cookies.get(ADMIN_COOKIE)
|
|
expected = admin_token()
|
|
return bool(token and expected and hmac.compare_digest(token, expected))
|