feat(sms): add selectable provider strategy
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
from functools import lru_cache
|
||||
from typing import Literal
|
||||
|
||||
from pydantic import Field
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
@@ -22,9 +23,14 @@ class Settings(BaseSettings):
|
||||
otp_request_limit: int = 3
|
||||
otp_request_window_seconds: int = 300
|
||||
|
||||
sms_provider: Literal["kavenegar", "sms_ir"] = "kavenegar"
|
||||
|
||||
kavenegar_api_key: str = "replace-with-real-key"
|
||||
kavenegar_login_template: str = "login-otp"
|
||||
|
||||
sms_ir_api_key: str = "replace-with-real-key"
|
||||
sms_ir_verify_template_id: int = 570574
|
||||
|
||||
admin_mobile: str = "989120000000"
|
||||
grpc_host: str = "0.0.0.0"
|
||||
grpc_port: int = 50051
|
||||
@@ -35,4 +41,3 @@ class Settings(BaseSettings):
|
||||
@lru_cache
|
||||
def get_settings() -> Settings:
|
||||
return Settings()
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ from gapido_auth.infrastructure.mongo_repositories import (
|
||||
)
|
||||
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
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
@@ -53,8 +54,7 @@ async def build_auth_service(settings: Settings, rabbitmq_channel: AbstractChann
|
||||
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=settings.kavenegar_login_template,
|
||||
sms_template=get_sms_template(settings),
|
||||
),
|
||||
)
|
||||
return AppContainer(auth_service=auth_service, mongo_client=mongo_client, redis=redis)
|
||||
|
||||
|
||||
56
src/gapido_auth/infrastructure/sms_ir_client.py
Normal file
56
src/gapido_auth/infrastructure/sms_ir_client.py
Normal file
@@ -0,0 +1,56 @@
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
|
||||
from gapido_auth.domain.errors import ExternalServiceError
|
||||
from gapido_auth.domain.ports import SmsClient
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SmsIrSmsClient(SmsClient):
|
||||
_endpoint = "https://api.sms.ir/v1/send/verify"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
api_key: str,
|
||||
timeout_seconds: float = 10.0,
|
||||
transport: httpx.AsyncBaseTransport | None = None,
|
||||
) -> None:
|
||||
self._api_key = api_key
|
||||
self._timeout_seconds = timeout_seconds
|
||||
self._transport = transport
|
||||
|
||||
async def send_otp(self, mobile: str, code: str, template: str) -> None:
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json",
|
||||
"x-api-key": self._api_key,
|
||||
}
|
||||
payload = {
|
||||
"mobile": mobile,
|
||||
"templateId": int(template),
|
||||
"parameters": [{"name": "OTP", "value": code}],
|
||||
}
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient(
|
||||
timeout=self._timeout_seconds, transport=self._transport
|
||||
) as client:
|
||||
response = await client.post(self._endpoint, json=payload, headers=headers)
|
||||
except httpx.HTTPError as exc:
|
||||
logger.warning("SMS.ir network error for mobile=%s", mobile, exc_info=True)
|
||||
raise ExternalServiceError("SMS.ir network error") from exc
|
||||
|
||||
if response.status_code != 200:
|
||||
logger.warning("SMS.ir HTTP error status=%s mobile=%s", response.status_code, mobile)
|
||||
raise ExternalServiceError("SMS.ir HTTP error")
|
||||
|
||||
data: dict[str, Any] = response.json()
|
||||
if str(data.get("status", "")) != "1":
|
||||
logger.warning("SMS.ir API error status=%s mobile=%s", data.get("status"), mobile)
|
||||
raise ExternalServiceError("SMS.ir API error")
|
||||
|
||||
logger.info("OTP SMS sent successfully to mobile=%s via SMS.ir", mobile)
|
||||
|
||||
26
src/gapido_auth/infrastructure/sms_provider.py
Normal file
26
src/gapido_auth/infrastructure/sms_provider.py
Normal file
@@ -0,0 +1,26 @@
|
||||
import httpx
|
||||
|
||||
from gapido_auth.config import Settings
|
||||
from gapido_auth.domain.ports import SmsClient
|
||||
from gapido_auth.infrastructure.kavenegar_client import KavenegarSmsClient
|
||||
from gapido_auth.infrastructure.sms_ir_client import SmsIrSmsClient
|
||||
|
||||
|
||||
def create_sms_client(
|
||||
settings: Settings,
|
||||
transport: httpx.AsyncBaseTransport | None = None,
|
||||
) -> SmsClient:
|
||||
match settings.sms_provider:
|
||||
case "kavenegar":
|
||||
return KavenegarSmsClient(settings.kavenegar_api_key, transport=transport)
|
||||
case "sms_ir":
|
||||
return SmsIrSmsClient(settings.sms_ir_api_key, transport=transport)
|
||||
|
||||
|
||||
def get_sms_template(settings: Settings) -> str:
|
||||
match settings.sms_provider:
|
||||
case "kavenegar":
|
||||
return settings.kavenegar_login_template
|
||||
case "sms_ir":
|
||||
return str(settings.sms_ir_verify_template_id)
|
||||
|
||||
@@ -8,12 +8,13 @@ from aio_pika.abc import AbstractIncomingMessage
|
||||
|
||||
from gapido_auth.config import get_settings
|
||||
from gapido_auth.domain.entities import SmsJob
|
||||
from gapido_auth.infrastructure.kavenegar_client import KavenegarSmsClient
|
||||
from gapido_auth.domain.ports import SmsClient
|
||||
from gapido_auth.infrastructure.rabbitmq import (
|
||||
SMS_ROUTING_KEY,
|
||||
connect_robust,
|
||||
declare_sms_topology,
|
||||
)
|
||||
from gapido_auth.infrastructure.sms_provider import create_sms_client
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
MAX_RETRIES = 3
|
||||
@@ -21,7 +22,7 @@ MAX_RETRIES = 3
|
||||
|
||||
async def handle_message(
|
||||
message: AbstractIncomingMessage,
|
||||
client: KavenegarSmsClient,
|
||||
client: SmsClient,
|
||||
exchange: Any,
|
||||
dlx: Any,
|
||||
) -> None:
|
||||
@@ -68,9 +69,9 @@ async def main() -> None:
|
||||
channel = await connection.channel()
|
||||
await channel.set_qos(prefetch_count=20)
|
||||
exchange, queue, dlx = await declare_sms_topology(channel)
|
||||
client = KavenegarSmsClient(settings.kavenegar_api_key)
|
||||
client = create_sms_client(settings)
|
||||
await queue.consume(lambda message: handle_message(message, client, exchange, dlx))
|
||||
logger.info("SMS worker started")
|
||||
logger.info("SMS worker started with provider=%s", settings.sms_provider)
|
||||
await asyncio.Future()
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user