Files
aiform_dev/backend/app/services/sms_service.py
AI Assistant 0f82eef08d 🚀 MVP: FastAPI + React форма с SMS верификацией
 Инфраструктура: PostgreSQL, Redis, RabbitMQ, S3
 Backend: SMS сервис + API endpoints
 Frontend: React форма (3 шага) + SMS верификация
2025-10-24 16:19:58 +03:00

181 lines
6.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
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()