idconvert/backend/dependencies.py

95 lines
3.5 KiB
Python

"""FastAPI dependency injection wiring.
All service and repository instances are created here. No business logic.
"""
from __future__ import annotations
from typing import Optional
import structlog
from fastapi import Depends, Header, HTTPException
from config import settings
from export.validator import ExportValidator
from export.scanner import IDExportScanner
from repositories.user_repository import UserRepository
from repositories.conversion_repository import ConversionRepository
from services.scan_service import ScanService
from services.conversion_service import ConversionService
from services.user_service import UserService
from services.payment_service import PaymentService
log = structlog.get_logger()
# ── Singletons (stateless services safe to share) ─────────────────────────────
_validator = ExportValidator()
_scanner = IDExportScanner()
_scan_service = ScanService(_validator, _scanner)
_user_repo = UserRepository()
_conversion_repo = ConversionRepository()
_user_service = UserService(_user_repo)
_payment_service = PaymentService()
_conversion_service = ConversionService(_scan_service, _conversion_repo)
# ── Service getters ───────────────────────────────────────────────────────────
def get_scan_service() -> ScanService:
return _scan_service
def get_conversion_service() -> ConversionService:
return _conversion_service
def get_user_service() -> UserService:
return _user_service
def get_payment_service() -> PaymentService:
return _payment_service
# ── Admin token ───────────────────────────────────────────────────────────────
def get_admin_token() -> str:
"""Return the PocketBase admin token from settings.
This token is used for server-side writes (creating conversion/purchase
records) that must bypass normal user permission rules.
"""
return getattr(settings, 'pocketbase_admin_token', '')
# ── Auth dependencies ─────────────────────────────────────────────────────────
async def get_optional_user(
authorization: Optional[str] = Header(None),
) -> Optional[dict]:
"""Extract and validate the Bearer token. Returns user dict or None.
Used on endpoints where auth is optional (e.g. scan — no credits needed).
"""
if not authorization or not authorization.startswith('Bearer '):
return None
token = authorization.split(' ', 1)[1]
try:
user = await _user_repo.get_by_token(token)
return user
except Exception:
return None
async def get_current_user_or_401(
authorization: Optional[str] = Header(None),
) -> dict:
"""Extract and validate the Bearer token. Raises 401 if missing or invalid.
Used on endpoints that require authentication (convert, users/me, payments).
"""
if not authorization or not authorization.startswith('Bearer '):
raise HTTPException(401, 'Authentication required')
token = authorization.split(' ', 1)[1]
try:
return await _user_repo.get_by_token(token)
except Exception:
raise HTTPException(401, 'Invalid or expired token')