refactor(backend): enforce route→service→repo layered architecture

- introduce custom repository exception hierarchy (DuplicateEntryError,
  IntegrityConstraintError, InvalidInputError) replacing raw ValueError
- eliminate all direct repository imports and raw SQL from route layer
- add UserService, SessionService, OrganizationService to service layer
- add get_stats/get_org_distribution service methods replacing admin inline SQL
- fix timing side-channel in authenticate_user via dummy bcrypt check
- replace SHA-256 client secret fallback with explicit InvalidClientError
- replace assert with InvalidGrantError in authorization code exchange
- replace N+1 token revocation loops with bulk UPDATE statements
- rename oauth account token fields (drop misleading 'encrypted' suffix)
- add Alembic migration 0003 for token field column rename
- add 45 new service/repository tests; 975 passing, 94% coverage
This commit is contained in:
2026-02-27 09:32:57 +01:00
parent 0646c96b19
commit 98b455fdc3
62 changed files with 2933 additions and 1728 deletions

View File

@@ -1,12 +1,12 @@
from fastapi import Depends, Header, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from fastapi.security.utils import get_authorization_scheme_param
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.auth import TokenExpiredError, TokenInvalidError, get_token_data
from app.core.database import get_db
from app.models.user import User
from app.repositories.user import user_repo
# OAuth2 configuration
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/login")
@@ -32,9 +32,8 @@ async def get_current_user(
# Decode token and get user ID
token_data = get_token_data(token)
# Get user from database
result = await db.execute(select(User).where(User.id == token_data.user_id))
user = result.scalar_one_or_none()
# Get user from database via repository
user = await user_repo.get(db, id=str(token_data.user_id))
if not user:
raise HTTPException(
@@ -144,8 +143,7 @@ async def get_optional_current_user(
try:
token_data = get_token_data(token)
result = await db.execute(select(User).where(User.id == token_data.user_id))
user = result.scalar_one_or_none()
user = await user_repo.get(db, id=str(token_data.user_id))
if not user or not user.is_active:
return None
return user

View File

@@ -15,9 +15,9 @@ from sqlalchemy.ext.asyncio import AsyncSession
from app.api.dependencies.auth import get_current_user
from app.core.database import get_db
from app.crud.organization import organization as organization_crud
from app.models.user import User
from app.models.user_organization import OrganizationRole
from app.services.organization_service import organization_service
def require_superuser(current_user: User = Depends(get_current_user)) -> User:
@@ -81,7 +81,7 @@ class OrganizationPermission:
return current_user
# Get user's role in organization
user_role = await organization_crud.get_user_role_in_org(
user_role = await organization_service.get_user_role_in_org(
db, user_id=current_user.id, organization_id=organization_id
)
@@ -123,7 +123,7 @@ async def require_org_membership(
if current_user.is_superuser:
return current_user
user_role = await organization_crud.get_user_role_in_org(
user_role = await organization_service.get_user_role_in_org(
db, user_id=current_user.id, organization_id=organization_id
)

View File

@@ -0,0 +1,41 @@
# app/api/dependencies/services.py
"""FastAPI dependency functions for service singletons."""
from app.services import oauth_provider_service
from app.services.auth_service import AuthService
from app.services.oauth_service import OAuthService
from app.services.organization_service import OrganizationService, organization_service
from app.services.session_service import SessionService, session_service
from app.services.user_service import UserService, user_service
def get_auth_service() -> AuthService:
"""Return the AuthService singleton for dependency injection."""
from app.services.auth_service import AuthService as _AuthService
return _AuthService()
def get_user_service() -> UserService:
"""Return the UserService singleton for dependency injection."""
return user_service
def get_organization_service() -> OrganizationService:
"""Return the OrganizationService singleton for dependency injection."""
return organization_service
def get_session_service() -> SessionService:
"""Return the SessionService singleton for dependency injection."""
return session_service
def get_oauth_service() -> OAuthService:
"""Return OAuthService for dependency injection."""
return OAuthService()
def get_oauth_provider_service():
"""Return the oauth_provider_service module for dependency injection."""
return oauth_provider_service