From 1f2d254e785f9f9ce59173fc19da3a46e602b8e2 Mon Sep 17 00:00:00 2001 From: Emiliano Gandini Outeda Date: Wed, 27 May 2026 11:31:24 -0300 Subject: [PATCH 1/8] feat: add Annotated type aliases to infrastructure/dependencies.py --- backend/src/infrastructure/dependencies.py | 30 ++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 backend/src/infrastructure/dependencies.py diff --git a/backend/src/infrastructure/dependencies.py b/backend/src/infrastructure/dependencies.py new file mode 100644 index 00000000..58b02c7e --- /dev/null +++ b/backend/src/infrastructure/dependencies.py @@ -0,0 +1,30 @@ +from typing import Annotated, Any + +from fastapi import Depends +from fastapi.security import OAuth2PasswordRequestForm +from sqlalchemy.ext.asyncio import AsyncSession + +from .auth.oauth.dependencies import get_google_provider, get_oauth_state_storage +from .auth.oauth.provider import AbstractOAuthProvider +from .auth.oauth.schemas import OAuthState +from .auth.session.dependencies import ( + get_current_session_data, + get_current_superuser, + get_current_user, + get_optional_user, + get_session_manager, +) +from .auth.session.manager import SessionManager +from .auth.session.schemas import SessionData +from .auth.session.storage import AbstractSessionStorage +from .database.session import async_session + +AsyncSessionDep = Annotated[AsyncSession, Depends(async_session)] +CurrentUserDep = Annotated[dict[str, Any], Depends(get_current_user)] +CurrentSuperUserDep = Annotated[dict[str, Any], Depends(get_current_superuser)] +OptionalUserDep = Annotated[dict[str, Any] | None, Depends(get_optional_user)] +SessionManagerDep = Annotated[SessionManager, Depends(get_session_manager)] +CurrentSessionDataDep = Annotated[SessionData, Depends(get_current_session_data)] +OAuth2FormDep = Annotated[OAuth2PasswordRequestForm, Depends()] +GoogleOAuthProviderDep = Annotated[AbstractOAuthProvider, Depends(get_google_provider)] +OAuthStateStorageDep = Annotated[AbstractSessionStorage[OAuthState], Depends(get_oauth_state_storage)] From 8d00268e520b5497c447dd27ae58affdf4f4dd20 Mon Sep 17 00:00:00 2001 From: Emiliano Gandini Outeda Date: Wed, 27 May 2026 11:31:25 -0300 Subject: [PATCH 2/8] feat: add per-module dependencies.py with service type aliases Create user, tier, rate_limit, and api_keys dependencies.py files. Move inline service factory functions from routes.py into their respective dependencies.py module for consistency. --- backend/src/modules/api_keys/dependencies.py | 12 ++++++++++++ backend/src/modules/rate_limit/dependencies.py | 12 ++++++++++++ backend/src/modules/tier/dependencies.py | 12 ++++++++++++ backend/src/modules/user/dependencies.py | 12 ++++++++++++ 4 files changed, 48 insertions(+) create mode 100644 backend/src/modules/api_keys/dependencies.py create mode 100644 backend/src/modules/rate_limit/dependencies.py create mode 100644 backend/src/modules/tier/dependencies.py create mode 100644 backend/src/modules/user/dependencies.py diff --git a/backend/src/modules/api_keys/dependencies.py b/backend/src/modules/api_keys/dependencies.py new file mode 100644 index 00000000..fd570f22 --- /dev/null +++ b/backend/src/modules/api_keys/dependencies.py @@ -0,0 +1,12 @@ +from typing import Annotated + +from fastapi import Depends + +from .service import APIKeyService + + +def get_api_key_service() -> APIKeyService: + return APIKeyService() + + +APIKeyServiceDep = Annotated[APIKeyService, Depends(get_api_key_service)] diff --git a/backend/src/modules/rate_limit/dependencies.py b/backend/src/modules/rate_limit/dependencies.py new file mode 100644 index 00000000..72a1d098 --- /dev/null +++ b/backend/src/modules/rate_limit/dependencies.py @@ -0,0 +1,12 @@ +from typing import Annotated + +from fastapi import Depends + +from .service import RateLimitService + + +def get_rate_limit_service() -> RateLimitService: + return RateLimitService() + + +RateLimitServiceDep = Annotated[RateLimitService, Depends(get_rate_limit_service)] diff --git a/backend/src/modules/tier/dependencies.py b/backend/src/modules/tier/dependencies.py new file mode 100644 index 00000000..5abb2a91 --- /dev/null +++ b/backend/src/modules/tier/dependencies.py @@ -0,0 +1,12 @@ +from typing import Annotated + +from fastapi import Depends + +from .service import TierService + + +def get_tier_service() -> TierService: + return TierService() + + +TierServiceDep = Annotated[TierService, Depends(get_tier_service)] diff --git a/backend/src/modules/user/dependencies.py b/backend/src/modules/user/dependencies.py new file mode 100644 index 00000000..51f5cbea --- /dev/null +++ b/backend/src/modules/user/dependencies.py @@ -0,0 +1,12 @@ +from typing import Annotated + +from fastapi import Depends + +from .service import UserService + + +def get_user_service() -> UserService: + return UserService() + + +UserServiceDep = Annotated[UserService, Depends(get_user_service)] From 9c2cd452624f24ce7d9c4700ff59b5006bfe0012 Mon Sep 17 00:00:00 2001 From: Emiliano Gandini Outeda Date: Wed, 27 May 2026 11:31:26 -0300 Subject: [PATCH 3/8] refactor(user, tier, rate_limit): use Annotated type aliases Replace inline Annotated/Depends with AsyncSessionDep, CurrentUserDep, CurrentSuperUserDep, and per-module service aliases (UserServiceDep, TierServiceDep, RateLimitServiceDep). --- backend/src/modules/rate_limit/routes.py | 35 +++++------ backend/src/modules/tier/routes.py | 22 +++---- backend/src/modules/user/routes.py | 78 +++++++++++------------- 3 files changed, 58 insertions(+), 77 deletions(-) diff --git a/backend/src/modules/rate_limit/routes.py b/backend/src/modules/rate_limit/routes.py index a768ca89..3556282f 100644 --- a/backend/src/modules/rate_limit/routes.py +++ b/backend/src/modules/rate_limit/routes.py @@ -1,28 +1,21 @@ -from typing import Annotated, Any +from typing import Any -from fastapi import APIRouter, Depends +from fastapi import APIRouter from fastcrud import PaginatedListResponse, compute_offset, paginated_response -from sqlalchemy.ext.asyncio import AsyncSession from ...infrastructure.auth.http_exceptions import DuplicateValueException, HTTPException, NotFoundException -from ...infrastructure.auth.session.dependencies import get_current_superuser -from ...infrastructure.database.session import async_session +from ...infrastructure.dependencies import AsyncSessionDep, CurrentSuperUserDep from ..common.exceptions import ResourceExistsError, ResourceNotFoundError from ..common.utils.error_handler import handle_exception +from .dependencies import RateLimitServiceDep from .schemas import ( RateLimitRead, RateLimitUpdate, ) -from .service import RateLimitService router = APIRouter(tags=["Rate Limits"]) -def get_rate_limit_service() -> RateLimitService: - """Dependency for providing a RateLimitService instance.""" - return RateLimitService() - - @router.get( "/", response_model=PaginatedListResponse[RateLimitRead], @@ -43,8 +36,8 @@ def get_rate_limit_service() -> RateLimitService: response_description="A paginated list of rate limits with their configuration details", ) async def get_rate_limits( - db: Annotated[AsyncSession, Depends(async_session)], - rate_limit_service: Annotated[RateLimitService, Depends(get_rate_limit_service)], + db: AsyncSessionDep, + rate_limit_service: RateLimitServiceDep, page: int = 1, items_per_page: int = 10, ) -> dict[str, Any]: @@ -88,8 +81,8 @@ async def get_rate_limits( ) async def get_rate_limit( name: str, - db: Annotated[AsyncSession, Depends(async_session)], - rate_limit_service: Annotated[RateLimitService, Depends(get_rate_limit_service)], + db: AsyncSessionDep, + rate_limit_service: RateLimitServiceDep, ) -> dict[str, Any] | None: """ Get detailed information about a specific rate limit by name. @@ -138,9 +131,9 @@ async def get_rate_limit( async def update_rate_limit( name: str, values: RateLimitUpdate, - db: Annotated[AsyncSession, Depends(async_session)], - rate_limit_service: Annotated[RateLimitService, Depends(get_rate_limit_service)], - _: Annotated[dict[str, Any], Depends(get_current_superuser)], + db: AsyncSessionDep, + rate_limit_service: RateLimitServiceDep, + _: CurrentSuperUserDep, ) -> dict[str, str]: """ Update an existing rate limit. @@ -187,9 +180,9 @@ async def update_rate_limit( ) async def delete_rate_limit( name: str, - db: Annotated[AsyncSession, Depends(async_session)], - rate_limit_service: Annotated[RateLimitService, Depends(get_rate_limit_service)], - _: Annotated[dict[str, Any], Depends(get_current_superuser)], + db: AsyncSessionDep, + rate_limit_service: RateLimitServiceDep, + _: CurrentSuperUserDep, ) -> dict[str, str]: """ Delete a rate limit. diff --git a/backend/src/modules/tier/routes.py b/backend/src/modules/tier/routes.py index efbecdfd..9613b96c 100644 --- a/backend/src/modules/tier/routes.py +++ b/backend/src/modules/tier/routes.py @@ -1,28 +1,22 @@ -from typing import Annotated, Any +from typing import Any -from fastapi import APIRouter, Depends, HTTPException +from fastapi import APIRouter, HTTPException from fastcrud import PaginatedListResponse, compute_offset, paginated_response -from sqlalchemy.ext.asyncio import AsyncSession from ...infrastructure.auth.http_exceptions import NotFoundException -from ...infrastructure.database.session import async_session +from ...infrastructure.dependencies import AsyncSessionDep from ..common.exceptions import TierNotFoundError from ..common.utils.error_handler import handle_exception +from .dependencies import TierServiceDep from .schemas import TierRead -from .service import TierService router = APIRouter(tags=["Tiers"]) -def get_tier_service() -> TierService: - """Dependency for providing a TierService instance.""" - return TierService() - - @router.get("/", response_model=PaginatedListResponse[TierRead], summary="List tiers") async def get_tiers( - db: Annotated[AsyncSession, Depends(async_session)], - tier_service: Annotated[TierService, Depends(get_tier_service)], + db: AsyncSessionDep, + tier_service: TierServiceDep, page: int = 1, items_per_page: int = 10, ) -> dict: @@ -44,8 +38,8 @@ async def get_tiers( @router.get("/{name}", response_model=TierRead, summary="Get a tier by name") async def get_tier_by_name( name: str, - db: Annotated[AsyncSession, Depends(async_session)], - tier_service: Annotated[TierService, Depends(get_tier_service)], + db: AsyncSessionDep, + tier_service: TierServiceDep, ) -> dict[str, Any]: """Get a tier by name.""" try: diff --git a/backend/src/modules/user/routes.py b/backend/src/modules/user/routes.py index d3b2bde6..e9265cf4 100644 --- a/backend/src/modules/user/routes.py +++ b/backend/src/modules/user/routes.py @@ -1,32 +1,26 @@ -from typing import Annotated, Any +from typing import Any -from fastapi import APIRouter, Depends +from fastapi import APIRouter from fastcrud import PaginatedListResponse, compute_offset, paginated_response -from sqlalchemy.ext.asyncio import AsyncSession from ...infrastructure.auth.http_exceptions import HTTPException -from ...infrastructure.auth.session.dependencies import ( - get_current_superuser, - get_current_user, +from ...infrastructure.dependencies import ( + AsyncSessionDep, + CurrentSuperUserDep, + CurrentUserDep, ) -from ...infrastructure.database.session import async_session from ..common.utils.error_handler import handle_exception +from .dependencies import UserServiceDep from .schemas import ( UserCreate, UserRead, UserTierUpdate, UserUpdate, ) -from .service import UserService router = APIRouter(tags=["Users"]) -def get_user_service() -> UserService: - """Dependency for providing a UserService instance.""" - return UserService() - - @router.post( "/", status_code=201, @@ -52,8 +46,8 @@ def get_user_service() -> UserService: ) async def create_user( user: UserCreate, - db: Annotated[AsyncSession, Depends(async_session)], - user_service: Annotated[UserService, Depends(get_user_service)], + db: AsyncSessionDep, + user_service: UserServiceDep, ) -> dict[str, Any]: """Create a new user account.""" try: @@ -83,9 +77,9 @@ async def create_user( response_description="A paginated list of users with total count and pagination metadata", ) async def get_users( - db: Annotated[AsyncSession, Depends(async_session)], - _: Annotated[dict[str, Any], Depends(get_current_superuser)], - user_service: Annotated[UserService, Depends(get_user_service)], + db: AsyncSessionDep, + _: CurrentSuperUserDep, + user_service: UserServiceDep, page: int = 1, items_per_page: int = 10, ) -> dict[str, Any]: @@ -115,7 +109,7 @@ async def get_users( response_description="The current user's profile data", ) async def get_current_user_profile( - current_user: Annotated[dict[str, Any], Depends(get_current_user)], + current_user: CurrentUserDep, ) -> dict[str, Any]: """Get current authenticated user's profile.""" return current_user @@ -139,8 +133,8 @@ async def get_current_user_profile( ) async def get_user_by_username( username: str, - db: Annotated[AsyncSession, Depends(async_session)], - user_service: Annotated[UserService, Depends(get_user_service)], + db: AsyncSessionDep, + user_service: UserServiceDep, ) -> dict[str, Any]: """Get user profile by username.""" try: @@ -177,9 +171,9 @@ async def get_user_by_username( ) async def get_active_and_inactive_user_by_username( username: str, - db: Annotated[AsyncSession, Depends(async_session)], - _: Annotated[dict[str, Any], Depends(get_current_superuser)], - user_service: Annotated[UserService, Depends(get_user_service)], + db: AsyncSessionDep, + _: CurrentSuperUserDep, + user_service: UserServiceDep, ) -> dict[str, Any]: """Get active and inactive profile by username.""" try: @@ -223,9 +217,9 @@ async def get_active_and_inactive_user_by_username( async def update_user_profile( username: str, values: UserUpdate, - current_user: Annotated[dict[str, Any], Depends(get_current_user)], - db: Annotated[AsyncSession, Depends(async_session)], - user_service: Annotated[UserService, Depends(get_user_service)], + current_user: CurrentUserDep, + db: AsyncSessionDep, + user_service: UserServiceDep, ) -> dict[str, str]: """Update user profile information.""" try: @@ -269,9 +263,9 @@ async def update_user_profile( ) async def delete_user_account( username: str, - current_user: Annotated[dict[str, Any], Depends(get_current_user)], - db: Annotated[AsyncSession, Depends(async_session)], - user_service: Annotated[UserService, Depends(get_user_service)], + current_user: CurrentUserDep, + db: AsyncSessionDep, + user_service: UserServiceDep, ) -> dict[str, str]: """Soft delete a user account.""" try: @@ -326,9 +320,9 @@ async def delete_user_account( ) async def gdpr_delete_user( username: str, - db: Annotated[AsyncSession, Depends(async_session)], - user_service: Annotated[UserService, Depends(get_user_service)], - _: Annotated[dict[str, Any], Depends(get_current_superuser)], + db: AsyncSessionDep, + user_service: UserServiceDep, + _: CurrentSuperUserDep, ) -> dict[str, str]: """GDPR compliant user anonymization (admin only).""" try: @@ -370,9 +364,9 @@ async def gdpr_delete_user( ) async def get_user_rate_limits( username: str, - db: Annotated[AsyncSession, Depends(async_session)], - current_user: Annotated[dict[str, Any], Depends(get_current_user)], - user_service: Annotated[UserService, Depends(get_user_service)], + db: AsyncSessionDep, + current_user: CurrentUserDep, + user_service: UserServiceDep, ) -> dict[str, Any]: """Get rate limits for a user.""" try: @@ -414,9 +408,9 @@ async def get_user_rate_limits( ) async def get_user_tier( username: str, - db: Annotated[AsyncSession, Depends(async_session)], - current_user: Annotated[dict[str, Any], Depends(get_current_user)], - user_service: Annotated[UserService, Depends(get_user_service)], + db: AsyncSessionDep, + current_user: CurrentUserDep, + user_service: UserServiceDep, ) -> dict[str, Any]: """Get detailed tier information for a user.""" try: @@ -459,9 +453,9 @@ async def get_user_tier( async def update_user_tier( username: str, values: UserTierUpdate, - db: Annotated[AsyncSession, Depends(async_session)], - user_service: Annotated[UserService, Depends(get_user_service)], - _: Annotated[dict[str, Any], Depends(get_current_superuser)], + db: AsyncSessionDep, + user_service: UserServiceDep, + _: CurrentSuperUserDep, ) -> dict[str, str]: """Update a user's subscription tier (admin only).""" try: From 35aa8c93907eb913c2839b421e5f93305e418775 Mon Sep 17 00:00:00 2001 From: Emiliano Gandini Outeda Date: Wed, 27 May 2026 11:31:26 -0300 Subject: [PATCH 4/8] refactor(api_keys): use Annotated type aliases Migrate all 8 endpoints from old-style param = Depends() to CurrentUserDep, APIKeyServiceDep, and AsyncSessionDep. Reorder parameters to avoid no-default-after-default issues with key_id: int = Path(...). --- backend/src/modules/api_keys/routes.py | 62 +++++++++++--------------- 1 file changed, 27 insertions(+), 35 deletions(-) diff --git a/backend/src/modules/api_keys/routes.py b/backend/src/modules/api_keys/routes.py index 25a63e38..d94f0398 100644 --- a/backend/src/modules/api_keys/routes.py +++ b/backend/src/modules/api_keys/routes.py @@ -2,34 +2,26 @@ from typing import Any -from fastapi import APIRouter, Depends, HTTPException, Path, Query, status +from fastapi import APIRouter, HTTPException, Path, Query, status from fastcrud import PaginatedListResponse, compute_offset, paginated_response -from sqlalchemy.ext.asyncio import AsyncSession -from ...infrastructure.auth.session.dependencies import get_current_user -from ...infrastructure.database.session import async_session +from ...infrastructure.dependencies import AsyncSessionDep, CurrentUserDep from ..common.exceptions import ( PermissionDeniedError, ResourceNotFoundError, ) from ..common.utils.error_handler import handle_exception -from ..user.models import User +from .dependencies import APIKeyServiceDep from .schemas import ( APIKeyCreate, APIKeyRead, APIKeyUpdate, KeyUsageRead, ) -from .service import APIKeyService router = APIRouter(tags=["API Keys"]) -def get_api_key_service() -> APIKeyService: - """Dependency for providing an APIKeyService instance.""" - return APIKeyService() - - @router.post( "/", status_code=201, @@ -57,9 +49,9 @@ def get_api_key_service() -> APIKeyService: ) async def create_api_key( key_data: APIKeyCreate, - current_user: User = Depends(get_current_user), - api_key_service: APIKeyService = Depends(get_api_key_service), - db: AsyncSession = Depends(async_session), + current_user: CurrentUserDep, + api_key_service: APIKeyServiceDep, + db: AsyncSessionDep, ) -> dict[str, Any]: """Create a new API key for the authenticated user.""" try: @@ -96,9 +88,9 @@ async def create_api_key( response_description="Paginated list of user's API keys", ) async def get_user_api_keys( - current_user: User = Depends(get_current_user), - api_key_service: APIKeyService = Depends(get_api_key_service), - db: AsyncSession = Depends(async_session), + current_user: CurrentUserDep, + api_key_service: APIKeyServiceDep, + db: AsyncSessionDep, active_only: bool = Query(True, description="Return only active keys"), page: int = Query(1, ge=1, description="Page number"), items_per_page: int = Query(50, ge=1, le=100, description="Items per page"), @@ -145,10 +137,10 @@ async def get_user_api_keys( response_description="API key details", ) async def get_api_key( + current_user: CurrentUserDep, + api_key_service: APIKeyServiceDep, + db: AsyncSessionDep, key_id: int = Path(..., description="API key ID"), - current_user: User = Depends(get_current_user), - api_key_service: APIKeyService = Depends(get_api_key_service), - db: AsyncSession = Depends(async_session), ) -> dict[str, Any]: """Get details for a specific API key.""" try: @@ -190,10 +182,10 @@ async def get_api_key( ) async def update_api_key( update_data: APIKeyUpdate, + current_user: CurrentUserDep, + api_key_service: APIKeyServiceDep, + db: AsyncSessionDep, key_id: int = Path(..., description="API key ID"), - current_user: User = Depends(get_current_user), - api_key_service: APIKeyService = Depends(get_api_key_service), - db: AsyncSession = Depends(async_session), ) -> dict[str, Any]: """Update an existing API key.""" try: @@ -236,10 +228,10 @@ async def update_api_key( }, ) async def delete_api_key( + current_user: CurrentUserDep, + api_key_service: APIKeyServiceDep, + db: AsyncSessionDep, key_id: int = Path(..., description="API key ID"), - current_user: User = Depends(get_current_user), - api_key_service: APIKeyService = Depends(get_api_key_service), - db: AsyncSession = Depends(async_session), ) -> None: """Delete (deactivate) an API key.""" try: @@ -282,10 +274,10 @@ async def delete_api_key( response_description="Paginated list of usage records", ) async def get_key_usage( + current_user: CurrentUserDep, + api_key_service: APIKeyServiceDep, + db: AsyncSessionDep, key_id: int = Path(..., description="API key ID"), - current_user: User = Depends(get_current_user), - api_key_service: APIKeyService = Depends(get_api_key_service), - db: AsyncSession = Depends(async_session), page: int = Query(1, ge=1, description="Page number"), items_per_page: int = Query(100, ge=1, le=1000, description="Items per page"), ) -> dict[str, Any]: @@ -340,10 +332,10 @@ async def get_key_usage( response_description="Usage analytics for the API key", ) async def get_key_analytics( + current_user: CurrentUserDep, + api_key_service: APIKeyServiceDep, + db: AsyncSessionDep, key_id: int = Path(..., description="API key ID"), - current_user: User = Depends(get_current_user), - api_key_service: APIKeyService = Depends(get_api_key_service), - db: AsyncSession = Depends(async_session), days: int = Query(30, ge=1, le=365, description="Number of days to analyze"), ) -> dict[str, Any]: """Get usage analytics for an API key.""" @@ -386,9 +378,9 @@ async def get_key_analytics( response_description="Comprehensive API key summary for the user", ) async def get_user_summary( - current_user: User = Depends(get_current_user), - api_key_service: APIKeyService = Depends(get_api_key_service), - db: AsyncSession = Depends(async_session), + current_user: CurrentUserDep, + api_key_service: APIKeyServiceDep, + db: AsyncSessionDep, ) -> dict[str, Any]: """Get comprehensive API key summary for the authenticated user.""" try: From 4c6a687ecd81a2aea9b47350f9b8bf163ecbef35 Mon Sep 17 00:00:00 2001 From: Emiliano Gandini Outeda Date: Wed, 27 May 2026 11:31:27 -0300 Subject: [PATCH 5/8] refactor(auth): use Annotated type aliases Migrate 6 auth endpoints from old-style Depends() and inline Annotated to centralized aliases (OAuth2FormDep, AsyncSessionDep, SessionManagerDep, CurrentSessionDataDep, GoogleOAuthProviderDep, OAuthStateStorageDep). Reorder params to comply with Python no-default-after-default rules. --- backend/src/infrastructure/auth/routes.py | 53 ++++++++++++----------- 1 file changed, 27 insertions(+), 26 deletions(-) diff --git a/backend/src/infrastructure/auth/routes.py b/backend/src/infrastructure/auth/routes.py index ac309e17..289ef18f 100644 --- a/backend/src/infrastructure/auth/routes.py +++ b/backend/src/infrastructure/auth/routes.py @@ -1,25 +1,26 @@ import inspect -from typing import Annotated, Any +from typing import Any -from fastapi import APIRouter, Depends, HTTPException, Query, Request, Response, status +from fastapi import APIRouter, HTTPException, Query, Request, Response, status from fastapi.responses import RedirectResponse -from fastapi.security import OAuth2PasswordRequestForm -from sqlalchemy.ext.asyncio import AsyncSession from ...modules.user.crud import crud_users from ...modules.user.enums import OAuthProvider from ..config.settings import get_settings -from ..database.session import async_session from ..logging import get_logger +from ..dependencies import ( + AsyncSessionDep, + CurrentSessionDataDep, + GoogleOAuthProviderDep, + OAuth2FormDep, + OAuthStateStorageDep, + SessionManagerDep, +) from .http_exceptions import UnauthorizedException -from .oauth.dependencies import get_google_provider, get_oauth_state, get_oauth_state_storage -from .oauth.provider import AbstractOAuthProvider +from .oauth.dependencies import get_oauth_state from .oauth.schemas import OAuthState, OAuthToken from .oauth.services import oauth_account_service -from .session.dependencies import authenticate_user, get_current_session_data, get_session_manager -from .session.manager import SessionManager -from .session.schemas import SessionData -from .session.storage import AbstractSessionStorage +from .session.dependencies import authenticate_user settings = get_settings() logger = get_logger() @@ -52,9 +53,9 @@ async def login( request: Request, response: Response, - form_data: Annotated[OAuth2PasswordRequestForm, Depends()], - db: Annotated[AsyncSession, Depends(async_session)], - session_manager: Annotated[SessionManager, Depends(get_session_manager)], + form_data: OAuth2FormDep, + db: AsyncSessionDep, + session_manager: SessionManagerDep, ) -> dict[str, str]: """Login endpoint to get session cookies. @@ -124,8 +125,8 @@ async def login( async def logout( request: Request, response: Response, - session_data: Annotated[SessionData, Depends(get_current_session_data)], - session_manager: Annotated[SessionManager, Depends(get_session_manager)], + session_data: CurrentSessionDataDep, + session_manager: SessionManagerDep, ) -> dict[str, str]: """Logout endpoint to terminate the session and clear cookies.""" await session_manager.terminate_session(session_data.session_id) @@ -153,8 +154,8 @@ async def logout( async def refresh_csrf_token( request: Request, response: Response, - session_data: Annotated[SessionData, Depends(get_current_session_data)], - session_manager: Annotated[SessionManager, Depends(get_session_manager)], + session_data: CurrentSessionDataDep, + session_manager: SessionManagerDep, ) -> dict[str, str]: """Generate a new CSRF token for the current session.""" csrf_token = await session_manager.regenerate_csrf_token( @@ -201,9 +202,9 @@ async def refresh_csrf_token( ) async def oauth_google_login( request: Request, + oauth_provider: GoogleOAuthProviderDep, + state_storage: OAuthStateStorageDep, redirect_uri: str | None = Query(None), - oauth_provider: AbstractOAuthProvider = Depends(get_google_provider), - state_storage: AbstractSessionStorage[OAuthState] = Depends(get_oauth_state_storage), ) -> dict[str, str]: """ Initiate OAuth login flow for Google. @@ -298,13 +299,13 @@ def _is_provider_valid(provider_value: Any, expected_provider: str) -> bool: async def oauth_google_callback( request: Request, response: Response, + oauth_provider: GoogleOAuthProviderDep, + state_storage: OAuthStateStorageDep, + db: AsyncSessionDep, + session_manager: SessionManagerDep, code: str = Query(...), state: str = Query(...), response_format: str = Query("redirect", description="Response format, either 'redirect' or 'json'"), - oauth_provider: AbstractOAuthProvider = Depends(get_google_provider), - state_storage: AbstractSessionStorage[OAuthState] = Depends(get_oauth_state_storage), - db: AsyncSession = Depends(async_session), - session_manager: SessionManager = Depends(get_session_manager), ): """ Handle OAuth callback from Google. @@ -421,8 +422,8 @@ async def oauth_google_callback( @router.get("/check-auth") async def check_auth( - session_data: Annotated[SessionData | None, Depends(get_current_session_data)], - db: AsyncSession = Depends(async_session), + session_data: CurrentSessionDataDep, + db: AsyncSessionDep, ) -> dict[str, Any]: """ Check if the user is authenticated and return basic user information. From 7ddbb99211d3dc8e8848c6a5ab904c922ecbd982 Mon Sep 17 00:00:00 2001 From: Emiliano Gandini Outeda Date: Wed, 27 May 2026 11:31:28 -0300 Subject: [PATCH 6/8] docs: add DI intro section with Annotated type aliases Add dependency injection overview to endpoints.md (central + per-module alias tables), document the pattern in development.md (custom dependency walkthrough with alias registration), and list the feature in README.md. --- README.md | 1 + docs/user-guide/api/endpoints.md | 58 ++++++++++++++++++++++++++++++++ docs/user-guide/development.md | 43 +++++++++++++++++++---- 3 files changed, 96 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 92fd0e94..d0bcd394 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,7 @@ * ⚡️ Fully async FastAPI + SQLAlchemy 2.0 * 🧱 Pydantic v2 models & validation * 🔐 Server-side sessions + CSRF; OAuth (Google wired, GitHub scaffolded); API keys +* 🏷️ Annotated type aliases for all FastAPI dependencies * 👮 Rate limiter with per-tier, per-path rules * 🧰 FastCRUD for efficient CRUD & pagination * 🧑‍💼 **SQLAdmin**-based admin panel (optional, env-toggled) diff --git a/docs/user-guide/api/endpoints.md b/docs/user-guide/api/endpoints.md index 9562a251..2a9b4b2a 100644 --- a/docs/user-guide/api/endpoints.md +++ b/docs/user-guide/api/endpoints.md @@ -2,6 +2,64 @@ This guide shows the patterns the boilerplate uses for endpoints, so adding new ones stays consistent with the existing modules. +## Dependency Injection + +This boilerplate supports two equivalent ways to inject FastAPI dependencies — you'll see both in the codebase, and either is correct. + +### Traditional style (explicit `Depends()`) + +```python +from fastapi import Depends +from sqlalchemy.ext.asyncio import AsyncSession + +from ...infrastructure.database.session import async_session + + +@router.get("/items") +async def list_items( + db: AsyncSession = Depends(async_session), +) -> list[dict[str, Any]]: + ... +``` + +### Modern style (Annotated type aliases) + +```python +from ...infrastructure.dependencies import AsyncSessionDep + + +@router.get("/items") +async def list_items( + db: AsyncSessionDep, +) -> list[dict[str, Any]]: + ... +``` + +The boilerplate pre-defines aliases for every shared dependency in `infrastructure/dependencies.py`: + +| Alias | Resolves to | +|---|---| +| `AsyncSessionDep` | `Annotated[AsyncSession, Depends(async_session)]` | +| `CurrentUserDep` | `Annotated[dict[str, Any], Depends(get_current_user)]` | +| `CurrentSuperUserDep` | `Annotated[dict[str, Any], Depends(get_current_superuser)]` | +| `OptionalUserDep` | `Annotated[dict[str, Any] \| None, Depends(get_optional_user)]` | +| `SessionManagerDep` | `Annotated[SessionManager, Depends(get_session_manager)]` | +| `CurrentSessionDataDep` | `Annotated[SessionData, Depends(get_current_session_data)]` | +| `OAuth2FormDep` | `Annotated[OAuth2PasswordRequestForm, Depends()]` | +| `GoogleOAuthProviderDep` | `Annotated[AbstractOAuthProvider, Depends(get_google_provider)]` | +| `OAuthStateStorageDep` | `Annotated[AbstractSessionStorage[OAuthState], Depends(get_oauth_state_storage)]` | + +Per-module service aliases live in `modules//dependencies.py`: + +| File | Alias | +|---|---| +| `modules/user/dependencies.py` | `UserServiceDep` | +| `modules/tier/dependencies.py` | `TierServiceDep` | +| `modules/rate_limit/dependencies.py` | `RateLimitServiceDep` | +| `modules/api_keys/dependencies.py` | `APIKeyServiceDep` | + +Both styles produce the same runtime behavior. The alias form reduces repetition and makes route signatures easier to scan. + ## Quick Start A typical endpoint lives in `modules//routes.py` and delegates work to a service: diff --git a/docs/user-guide/development.md b/docs/user-guide/development.md index 7fe460b6..39afe981 100644 --- a/docs/user-guide/development.md +++ b/docs/user-guide/development.md @@ -161,17 +161,19 @@ Order matters — middleware added later runs **earlier** in the request path. T Dependencies belong with the feature they serve. For session-aware dependencies, look at `infrastructure/auth/session/dependencies.py:get_current_user` for a template. -```python -from typing import Annotated +### Define the factory -from fastapi import Depends, Request +```python +# modules/workspace/dependencies.py +from fastapi import Request -from src.infrastructure.auth.session.dependencies import get_current_user +from ...infrastructure.auth.session.dependencies import get_current_user +from ...infrastructure.dependencies import CurrentUserDep def get_workspace( request: Request, - current_user: Annotated[dict, Depends(get_current_user)], + current_user: CurrentUserDep, ) -> str: workspace = request.headers.get("X-Workspace") if not workspace: @@ -180,7 +182,36 @@ def get_workspace( return workspace ``` -Use it as `Depends(get_workspace)` on a route, or in another dependency for chaining. +### Register the alias (optional) + +Add it to the module's `dependencies.py` so routes can use it without typing `Depends(get_workspace)` every time: + +```python +# modules/workspace/dependencies.py (extended) +from typing import Annotated + +from fastapi import Depends + +from ...infrastructure.dependencies import CurrentUserDep + +WorkspaceDep = Annotated[str, Depends(get_workspace)] +``` + +### Use it + +```python +from ..dependencies import WorkspaceDep + + +@router.get("/workspace/items") +async def list_workspace_items( + workspace: WorkspaceDep, + db: AsyncSessionDep, +) -> list[dict[str, Any]]: + ... +``` + +Per-module service aliases follow the same pattern — see the existing `modules/{user,tier,rate_limit,api_keys}/dependencies.py` files for real examples. ## Debugging Tips From 1c33cf037cd0127b056f841bb611f62fc60c387b Mon Sep 17 00:00:00 2001 From: Emiliano Gandini Outeda Date: Wed, 27 May 2026 11:36:23 -0300 Subject: [PATCH 7/8] style: fix import ordering in auth routes --- backend/src/infrastructure/auth/routes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/infrastructure/auth/routes.py b/backend/src/infrastructure/auth/routes.py index 289ef18f..1e4ee15d 100644 --- a/backend/src/infrastructure/auth/routes.py +++ b/backend/src/infrastructure/auth/routes.py @@ -7,7 +7,6 @@ from ...modules.user.crud import crud_users from ...modules.user.enums import OAuthProvider from ..config.settings import get_settings -from ..logging import get_logger from ..dependencies import ( AsyncSessionDep, CurrentSessionDataDep, @@ -16,6 +15,7 @@ OAuthStateStorageDep, SessionManagerDep, ) +from ..logging import get_logger from .http_exceptions import UnauthorizedException from .oauth.dependencies import get_oauth_state from .oauth.schemas import OAuthState, OAuthToken From 59d6ecd852ae66d49f53aedcfdd3ddb1488489e5 Mon Sep 17 00:00:00 2001 From: Igor Benav Date: Fri, 29 May 2026 01:34:03 -0300 Subject: [PATCH 8/8] refactor(deps): group infra aliases; fix /check-auth for anonymous callers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add section separators (Database/Users/Sessions/OAuth) to infrastructure/dependencies.py for readability. Also fix a latent bug surfaced by this migration: /check-auth depended on get_current_session_data, which raises 401 when there is no session and never returns None — so its `if not session_data` branch was dead code and the endpoint returned 401 to anonymous callers, defeating its stated purpose (letting clients check auth status). Switch it to a new OptionalSessionDataDep backed by get_session_from_cookie (returns None instead of raising), so unauthenticated requests now get 200 {authenticated: false}. Update the check_auth tests to override the dependency the endpoint actually uses, and add a regression test that exercises the real no-cookie path. Co-Authored-By: Claude Opus 4.8 (1M context) --- backend/src/infrastructure/auth/routes.py | 3 +- backend/src/infrastructure/dependencies.py | 9 ++++++ .../tests/integration/auth/test_endpoints.py | 28 +++++++++++++++++-- 3 files changed, 36 insertions(+), 4 deletions(-) diff --git a/backend/src/infrastructure/auth/routes.py b/backend/src/infrastructure/auth/routes.py index 1e4ee15d..1714a180 100644 --- a/backend/src/infrastructure/auth/routes.py +++ b/backend/src/infrastructure/auth/routes.py @@ -13,6 +13,7 @@ GoogleOAuthProviderDep, OAuth2FormDep, OAuthStateStorageDep, + OptionalSessionDataDep, SessionManagerDep, ) from ..logging import get_logger @@ -422,7 +423,7 @@ async def oauth_google_callback( @router.get("/check-auth") async def check_auth( - session_data: CurrentSessionDataDep, + session_data: OptionalSessionDataDep, db: AsyncSessionDep, ) -> dict[str, Any]: """ diff --git a/backend/src/infrastructure/dependencies.py b/backend/src/infrastructure/dependencies.py index 58b02c7e..6331304b 100644 --- a/backend/src/infrastructure/dependencies.py +++ b/backend/src/infrastructure/dependencies.py @@ -12,6 +12,7 @@ get_current_superuser, get_current_user, get_optional_user, + get_session_from_cookie, get_session_manager, ) from .auth.session.manager import SessionManager @@ -19,12 +20,20 @@ from .auth.session.storage import AbstractSessionStorage from .database.session import async_session +# Database AsyncSessionDep = Annotated[AsyncSession, Depends(async_session)] + +# Users CurrentUserDep = Annotated[dict[str, Any], Depends(get_current_user)] CurrentSuperUserDep = Annotated[dict[str, Any], Depends(get_current_superuser)] OptionalUserDep = Annotated[dict[str, Any] | None, Depends(get_optional_user)] + +# Sessions SessionManagerDep = Annotated[SessionManager, Depends(get_session_manager)] CurrentSessionDataDep = Annotated[SessionData, Depends(get_current_session_data)] +OptionalSessionDataDep = Annotated[SessionData | None, Depends(get_session_from_cookie)] + +# OAuth OAuth2FormDep = Annotated[OAuth2PasswordRequestForm, Depends()] GoogleOAuthProviderDep = Annotated[AbstractOAuthProvider, Depends(get_google_provider)] OAuthStateStorageDep = Annotated[AbstractSessionStorage[OAuthState], Depends(get_oauth_state_storage)] diff --git a/backend/tests/integration/auth/test_endpoints.py b/backend/tests/integration/auth/test_endpoints.py index d550151e..1677950b 100644 --- a/backend/tests/integration/auth/test_endpoints.py +++ b/backend/tests/integration/auth/test_endpoints.py @@ -15,7 +15,7 @@ ) from src.infrastructure.auth.oauth.provider import AbstractOAuthProvider from src.infrastructure.auth.oauth.schemas import OAuthState -from src.infrastructure.auth.session.dependencies import get_current_session_data, get_session_manager +from src.infrastructure.auth.session.dependencies import get_session_from_cookie, get_session_manager from src.infrastructure.auth.session.manager import SessionManager from src.infrastructure.auth.session.storage import AbstractSessionStorage from src.infrastructure.database.session import async_session @@ -170,7 +170,7 @@ async def test_check_auth_authenticated(client: AsyncClient, db_session: AsyncSe original_deps = app.dependency_overrides.copy() try: - app.dependency_overrides[get_current_session_data] = lambda: mock_session + app.dependency_overrides[get_session_from_cookie] = lambda: mock_session app.dependency_overrides[async_session] = lambda: db_session with patch("src.modules.user.crud.crud_users.get", return_value=mock_user): @@ -192,7 +192,7 @@ async def test_check_auth_not_authenticated(client: AsyncClient): original_deps = app.dependency_overrides.copy() try: - app.dependency_overrides[get_current_session_data] = lambda: None + app.dependency_overrides[get_session_from_cookie] = lambda: None response = await client.get("/api/v1/auth/check-auth") @@ -201,3 +201,25 @@ async def test_check_auth_not_authenticated(client: AsyncClient): assert response.json()["message"] == "Not authenticated" finally: app.dependency_overrides = original_deps + + +@pytest.mark.asyncio +async def test_check_auth_no_session_cookie_returns_unauthenticated(client: AsyncClient): + """A request with no session cookie must get 200 {authenticated: false}, not a 401. + + Regression: /check-auth must answer anonymous callers (its whole purpose). It used to + depend on get_current_session_data, which raises 401 when no session exists, so the + unauthenticated branch was unreachable. Only get_session_manager is overridden here so + the real get_session_from_cookie runs against a request that carries no cookie. + """ + original_deps = app.dependency_overrides.copy() + + try: + app.dependency_overrides[get_session_manager] = lambda: MagicMock() + + response = await client.get("/api/v1/auth/check-auth") + + assert response.status_code == 200 + assert response.json()["authenticated"] is False + finally: + app.dependency_overrides = original_deps