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

@@ -25,8 +25,7 @@ from app.core.auth import decode_token
from app.core.config import settings
from app.core.database import get_db
from app.core.exceptions import AuthenticationError as AuthError
from app.crud import oauth_account
from app.crud.session import session as session_crud
from app.services.session_service import session_service
from app.models.user import User
from app.schemas.oauth import (
OAuthAccountsListResponse,
@@ -82,7 +81,7 @@ async def _create_oauth_login_session(
location_country=device_info.location_country,
)
await session_crud.create_session(db, obj_in=session_data)
await session_service.create_session(db, obj_in=session_data)
logger.info(
f"OAuth login successful: {user.email} via {provider} "
@@ -289,7 +288,7 @@ async def list_accounts(
Returns:
List of linked OAuth accounts
"""
accounts = await oauth_account.get_user_accounts(db, user_id=current_user.id)
accounts = await OAuthService.get_user_accounts(db, user_id=current_user.id)
return OAuthAccountsListResponse(accounts=accounts)
@@ -397,7 +396,7 @@ async def start_link(
)
# Check if user already has this provider linked
existing = await oauth_account.get_user_account_by_provider(
existing = await OAuthService.get_user_account_by_provider(
db, user_id=current_user.id, provider=provider
)
if existing: