100 lines
3.6 KiB
Python
100 lines
3.6 KiB
Python
import asyncio
|
|
import logging
|
|
from dataclasses import dataclass
|
|
from typing import Any
|
|
|
|
from aio_pika.abc import AbstractChannel
|
|
from motor.motor_asyncio import AsyncIOMotorClient
|
|
from redis.asyncio import Redis
|
|
|
|
from gapido_auth.application.auth_service import AuthConfig, AuthService
|
|
from gapido_auth.application.security import JwtTokenCodec
|
|
from gapido_auth.config import Settings
|
|
from gapido_auth.infrastructure.mongo_repositories import (
|
|
MongoRefreshSessionRepository,
|
|
MongoUserRepository,
|
|
)
|
|
from gapido_auth.infrastructure.rabbitmq import RabbitMqSmsPublisher
|
|
from gapido_auth.infrastructure.redis_otp_store import RedisOtpStore
|
|
from gapido_auth.infrastructure.sms_provider import get_sms_template
|
|
|
|
logger = logging.getLogger(__name__)
|
|
STARTUP_RETRY_ATTEMPTS = 30
|
|
STARTUP_RETRY_DELAY_SECONDS = 2
|
|
|
|
|
|
@dataclass(slots=True)
|
|
class AppContainer:
|
|
"""Runtime dependencies that need explicit shutdown after the gRPC server stops."""
|
|
|
|
auth_service: AuthService
|
|
mongo_client: AsyncIOMotorClient[Any]
|
|
redis: Redis
|
|
|
|
|
|
async def build_auth_service(settings: Settings, rabbitmq_channel: AbstractChannel) -> AppContainer:
|
|
"""Create repositories, adapters, policies, and the AuthService use-case object."""
|
|
|
|
mongo_client: AsyncIOMotorClient[Any] = AsyncIOMotorClient(
|
|
settings.mongo_uri,
|
|
serverSelectionTimeoutMS=2000,
|
|
)
|
|
db = mongo_client[settings.mongo_db_name]
|
|
users = MongoUserRepository(db)
|
|
refresh_sessions = MongoRefreshSessionRepository(db)
|
|
|
|
redis = Redis.from_url(settings.redis_url, decode_responses=False)
|
|
await _wait_for_state_stores(users, refresh_sessions, redis, settings.admin_mobile)
|
|
|
|
otp_store = RedisOtpStore(redis)
|
|
sms_publisher = RabbitMqSmsPublisher(rabbitmq_channel)
|
|
token_codec = JwtTokenCodec(
|
|
secret_key=settings.jwt_secret_key,
|
|
issuer=settings.jwt_issuer,
|
|
access_ttl_seconds=settings.access_token_ttl_seconds,
|
|
)
|
|
auth_service = AuthService(
|
|
users=users,
|
|
refresh_sessions=refresh_sessions,
|
|
otp_store=otp_store,
|
|
sms_publisher=sms_publisher,
|
|
token_codec=token_codec,
|
|
config=AuthConfig(
|
|
otp_secret=settings.jwt_secret_key,
|
|
otp_ttl_seconds=settings.otp_ttl_seconds,
|
|
otp_max_attempts=settings.otp_max_attempts,
|
|
otp_request_limit=settings.otp_request_limit,
|
|
otp_request_window_seconds=settings.otp_request_window_seconds,
|
|
refresh_token_ttl_seconds=settings.refresh_token_ttl_seconds,
|
|
sms_template=get_sms_template(settings),
|
|
),
|
|
)
|
|
return AppContainer(auth_service=auth_service, mongo_client=mongo_client, redis=redis)
|
|
|
|
|
|
async def _wait_for_state_stores(
|
|
users: MongoUserRepository,
|
|
refresh_sessions: MongoRefreshSessionRepository,
|
|
redis: Redis,
|
|
admin_mobile: str,
|
|
) -> None:
|
|
"""Wait for MongoDB and Redis before exposing the gRPC server."""
|
|
|
|
for attempt in range(1, STARTUP_RETRY_ATTEMPTS + 1):
|
|
try:
|
|
await users.create_indexes()
|
|
await refresh_sessions.create_indexes()
|
|
await users.ensure_admin_user(admin_mobile)
|
|
await redis.ping()
|
|
return
|
|
except Exception:
|
|
if attempt >= STARTUP_RETRY_ATTEMPTS:
|
|
logger.exception("State stores did not become ready during auth-service startup")
|
|
raise
|
|
logger.info(
|
|
"Waiting for MongoDB/Redis before auth-service startup attempt=%s/%s",
|
|
attempt,
|
|
STARTUP_RETRY_ATTEMPTS,
|
|
)
|
|
await asyncio.sleep(STARTUP_RETRY_DELAY_SECONDS)
|