""" FastAPI shared dependencies — injected via Depends(). JWT scope enforcement: - "user" scope (browser login): full access to all endpoints - "node" scope (Ed25519 daemon auth): read-only group access + node operations Node-scoped tokens CANNOT create/delete groups or manage membership. """ from fastapi import Depends, Header, HTTPException, status from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from meshbay_hub.auth import decode_access_token from meshbay_hub.db.engine import get_db from meshbay_hub.db.models import User _admin_usernames: set[str] = set() def set_admin_usernames(usernames: list[str]) -> None: global _admin_usernames _admin_usernames = set(usernames) async def _decode_token(authorization: str = Header(...)) -> dict: """Decode and verify JWT bearer token. Returns full payload.""" try: scheme, token = authorization.split(None, 1) if scheme.lower() != "bearer": raise ValueError return decode_access_token(token) except Exception: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid or expired token", headers={"WWW-Authenticate": "Bearer"}, ) async def get_current_user( payload: dict = Depends(_decode_token), db: AsyncSession = Depends(get_db), ) -> User: """ Verify the JWT bearer token and return the User from the database. Accepts both user-scoped and node-scoped tokens. """ result = await db.execute( select(User).where(User.id == payload["sub"])) user = result.scalar_one_or_none() if user is None: raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="User not found") if user.status != "active": raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=f"Account {user.status}") return user async def require_user_scope( payload: dict = Depends(_decode_token), current_user: User = Depends(get_current_user), ) -> User: """Reject node-scoped tokens — only browser (user-scope) can mutate groups.""" if payload.get("scope") == "node": raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Node-scoped token cannot perform this operation — use browser", ) return current_user def user_is_admin(user: User) -> bool: """Admin by DB role or by the config allow-list. Use inside a handler that already depends on `require_moderator` but has to draw the admin line for one field (see `admin_patch_user`).""" return user.role == "admin" or user.username in _admin_usernames def user_is_moderator(user: User) -> bool: return user.role in ("moderator", "admin") or user.username in _admin_usernames async def require_moderator( current_user: User = Depends(get_current_user), ) -> User: if not user_is_moderator(current_user): raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Moderator access required") return current_user async def require_admin( current_user: User = Depends(get_current_user), ) -> User: if not user_is_admin(current_user): raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Admin access required") return current_user