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)