"""Admin auth: single env-based user + JWT bearer tokens.""" from __future__ import annotations import hmac import os import time from typing import Any import jwt from fastapi import HTTPException ALGORITHM = "HS256" DEFAULT_TTL_SECONDS = 60 * 60 * 24 # 24h def _admin_user() -> str: return os.getenv("ADMIN_USER", "admin").strip() or "admin" def _admin_password() -> str: return os.getenv("ADMIN_PASSWORD", "").strip() def _jwt_secret() -> str: secret = os.getenv("ADMIN_JWT_SECRET", "").strip() if not secret: # Dev fallback: derive from password so local stacks boot without extra secret. password = _admin_password() if not password: raise HTTPException( status_code=503, detail="ADMIN_PASSWORD is not configured", ) return f"mapmil-dev:{password}" return secret def admin_credentials_configured() -> bool: return bool(_admin_password()) def verify_credentials(username: str, password: str) -> bool: expected_user = _admin_user() expected_password = _admin_password() if not expected_password: return False user_ok = hmac.compare_digest(username.strip(), expected_user) pass_ok = hmac.compare_digest(password, expected_password) return user_ok and pass_ok def create_access_token(*, username: str, ttl_seconds: int = DEFAULT_TTL_SECONDS) -> str: now = int(time.time()) payload: dict[str, Any] = { "sub": username, "role": "admin", "iat": now, "exp": now + ttl_seconds, } return jwt.encode(payload, _jwt_secret(), algorithm=ALGORITHM) def decode_access_token(token: str) -> dict[str, Any]: try: payload = jwt.decode(token, _jwt_secret(), algorithms=[ALGORITHM]) except jwt.ExpiredSignatureError as exc: raise HTTPException(status_code=401, detail="Token expired") from exc except jwt.InvalidTokenError as exc: raise HTTPException(status_code=401, detail="Invalid token") from exc if payload.get("role") != "admin" or not payload.get("sub"): raise HTTPException(status_code=401, detail="Invalid token") return payload