from sqlalchemy.ext.asyncio import AsyncSession from fastapi import Depends from fastapi.security import OAuth2PasswordBearer from typing import Callable from datetime import datetime from fooder.db import get_db_session from fooder.domain import User from fooder.repository import Repository from fooder.utils.datetime import utc_now from fooder.exc import Unauthorized, NotFound from fooder.utils.jwt import AccessToken class Context: """ Main API context, aggregating dependencies """ def __init__( self, repo: Repository, clock: Callable[[], datetime] = utc_now, ) -> None: self.repo = repo self.clock = clock self._user: User | None = None def set_user(self, user: User) -> None: self._user = user @property def user(self) -> User: if self._user is None: raise Unauthorized() return self._user class ContextDependency: """ Context dependency """ def __call__( self, session: AsyncSession = Depends(get_db_session), ): return Context(repo=Repository(session)) _oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/token") class AuthContextDependency: """ Context dependency for authorized endpoints """ async def __call__( self, token: str = Depends(_oauth2_scheme), session: AsyncSession = Depends(get_db_session), ) -> Context: ctx = Context(repo=Repository(session)) user_id = AccessToken.decode(token).sub try: user = await ctx.repo.user.get_by_id(user_id) except NotFound: raise Unauthorized() ctx.set_user(user) return ctx