Spaces:
Sleeping
Sleeping
| """Trivial passcode auth for the educator dashboard. | |
| The frontend stores the passcode in an httpOnly cookie set by /api/admin/login. | |
| Protected endpoints depend on `require_admin` to validate the cookie. | |
| """ | |
| from __future__ import annotations | |
| import secrets | |
| from fastapi import Cookie, HTTPException, status | |
| from config import ADMIN_PASSCODE | |
| COOKIE_NAME = "admin_session" | |
| def _expected_token() -> str: | |
| """A deterministic-but-opaque token derived from the passcode. | |
| Avoids storing the raw passcode in the cookie. We don't need full session | |
| management here — the passcode itself is the credential. | |
| """ | |
| if not ADMIN_PASSCODE: | |
| return "" | |
| # 32-char hex digest is plenty for a single-tenant educator template. | |
| import hashlib | |
| return hashlib.sha256(ADMIN_PASSCODE.encode("utf-8")).hexdigest() | |
| def verify_passcode(passcode: str) -> bool: | |
| if not ADMIN_PASSCODE: | |
| return False | |
| return secrets.compare_digest(passcode, ADMIN_PASSCODE) | |
| def issue_token() -> str: | |
| return _expected_token() | |
| def require_admin(admin_session: str | None = Cookie(default=None)) -> None: | |
| if not ADMIN_PASSCODE: | |
| raise HTTPException( | |
| status_code=status.HTTP_503_SERVICE_UNAVAILABLE, | |
| detail="ADMIN_PASSCODE is not configured on the server.", | |
| ) | |
| if not admin_session or not secrets.compare_digest(admin_session, _expected_token()): | |
| raise HTTPException( | |
| status_code=status.HTTP_401_UNAUTHORIZED, | |
| detail="Admin authentication required.", | |
| ) | |