""" SMS Service для отправки кодов верификации (SigmaSMS) """ import httpx import random import logging from typing import Optional from ..config import settings from .redis_service import redis_service logger = logging.getLogger(__name__) class SMSService: """Сервис для работы с SMS через SigmaSMS API""" def __init__(self): self.api_url = settings.sms_api_url self.login = settings.sms_login self.password = settings.sms_password self.token = settings.sms_token self.sender = settings.sms_sender self.enabled = settings.sms_enabled async def _get_token(self) -> Optional[str]: """Получить JWT токен для API""" try: async with httpx.AsyncClient() as client: response = await client.post( f"{self.api_url}login", json={ "username": self.login, "password": self.password }, timeout=10.0 ) if response.status_code == 200: data = response.json() return data.get("token") else: logger.error(f"Failed to get SMS token: {response.status_code}") return self.token # Используем токен из .env как fallback except Exception as e: logger.error(f"Error getting SMS token: {e}") return self.token def generate_code(self) -> str: """Генерировать 6-значный код""" return str(random.randint(100000, 999999)) async def send_sms(self, phone: str, message: str) -> bool: """ Отправить SMS Args: phone: Номер телефона (формат: +79001234567) message: Текст сообщения Returns: bool: True если отправлено успешно """ if not self.enabled: logger.warning("SMS отправка отключена в конфигурации") return False try: # Получаем актуальный токен token = await self._get_token() if not token: logger.error("No SMS token available") return False # Очищаем номер телефона clean_phone = phone.replace("+", "").replace("-", "").replace(" ", "") # Отправляем SMS async with httpx.AsyncClient() as client: response = await client.post( f"{self.api_url}sendings", headers={ "Authorization": f"Bearer {token}" }, json={ "recipient": clean_phone, "type": "sms", "payload": { "sender": self.sender, "text": message } }, timeout=15.0 ) if response.status_code in [200, 201]: logger.info(f"✅ SMS sent to {phone}") return True else: logger.error(f"Failed to send SMS: {response.status_code} - {response.text}") return False except Exception as e: logger.error(f"Error sending SMS: {e}") return False async def send_verification_code(self, phone: str) -> Optional[str]: """ Отправить код верификации на телефон Args: phone: Номер телефона Returns: str: Код верификации (для отладки) или None при ошибке """ # Проверка rate limiting (не больше 1 SMS в минуту на номер) rate_limit_key = f"sms_rate:{phone}" if await redis_service.exists(rate_limit_key): ttl = await redis_service.client.ttl(f"{settings.redis_prefix}{rate_limit_key}") logger.warning(f"Rate limit for {phone}, retry in {ttl} seconds") return None # Генерируем код code = self.generate_code() # Сохраняем код в Redis на 10 минут verification_key = f"sms_verify:{phone}" await redis_service.set(verification_key, code, expire=600) # 10 минут # Устанавливаем rate limit на 60 секунд await redis_service.set(rate_limit_key, "1", expire=60) # Формируем сообщение message = f"Ваш код подтверждения: {code}. Действителен 10 минут." # Отправляем SMS success = await self.send_sms(phone, message) if success: logger.info(f"Verification code sent to {phone}") return code # Возвращаем для отладки else: # Удаляем код если не удалось отправить await redis_service.delete(verification_key) return None async def verify_code(self, phone: str, code: str) -> bool: """ Проверить код верификации Args: phone: Номер телефона code: Код для проверки Returns: bool: True если код верный """ verification_key = f"sms_verify:{phone}" stored_code = await redis_service.get(verification_key) if not stored_code: logger.warning(f"No verification code found for {phone}") return False if stored_code == code: # Удаляем код после успешной проверки await redis_service.delete(verification_key) logger.info(f"✅ Code verified for {phone}") return True else: logger.warning(f"Invalid code for {phone}") return False # Глобальный экземпляр sms_service = SMSService()