""" Auth middleware — verifies the stack's hnf_session cookie using the shared CENTRAL_AUTH_SECRET. Replaces the old per-app JWT/users system. """ import os from fastapi import Depends, HTTPException, Request, status from jose import JWTError, jwt CENTRAL_AUTH_SECRET = os.getenv("CENTRAL_AUTH_SECRET", "") JWT_ALGORITHM = "HS256" APP_SLUG = os.getenv("APP_SLUG", "forecasting") async def get_current_user(request: Request) -> dict: token = request.cookies.get("hnf_session") if not token: raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Not authenticated") try: payload = jwt.decode(token, CENTRAL_AUTH_SECRET, algorithms=[JWT_ALGORITHM]) except JWTError: raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid session") apps = payload.get("apps", []) if APP_SLUG not in apps: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="No permission for this app") prefix = f"{APP_SLUG}:" raw_caps = payload.get("caps", []) if isinstance(raw_caps, list): caps = [c[len(prefix):] for c in raw_caps if c.startswith(prefix)] else: caps = [] is_admin = payload.get("is_admin", False) return { "id": 0, "username": payload.get("sub", ""), "email": payload.get("sub", ""), "display_name": payload.get("name", ""), "name": payload.get("name", ""), "is_admin": is_admin, "caps": caps, "role": "admin" if is_admin else "user", } def has_cap(user: dict, cap: str) -> bool: return user.get("is_admin", False) or cap in user.get("caps", []) def require_cap(cap: str): async def checker(user: dict = Depends(get_current_user)): if not has_cap(user, cap): raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail=f"Missing capability: {cap}", ) return user return checker # Keep get_admin_user for routes that require admin async def get_admin_user(user: dict = Depends(get_current_user)) -> dict: if not user.get("is_admin", False): raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Admin access required") return user