Files
aiform_prod/backend/app/services/sms_service.py
AI Assistant 89a182bc7b fix: Интеграция n8n webhook для создания контакта после SMS
- Step1Phone теперь вызывает n8n webhook после SMS верификации
- Webhook создаёт/находит контакт в CRM через CreateWebContact
- Возвращает: contact_id, claim_id, is_new_contact
- Данные сохраняются в formData для дальнейшей работы
- Исправлена нормализация телефона в sms_service (убираем +)
- Отключен rate limiting SMS для тестирования
- Backend подключён к внешнему Redis (crm.clientright.ru:6379)
- Добавлены поля contact_id, is_new_contact в FormData
- Frontend пересобран с новым кодом
2025-11-01 13:31:05 +03:00

197 lines
7.6 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
# DEBUG MODE: Не отправляем реальные SMS, экономим бюджет
if settings.debug or settings.app_env == "development":
logger.info(f"🔧 DEBUG MODE: SMS to {phone} not sent (saving money!)")
logger.info(f"📱 Message would be: {message}")
return True
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 при ошибке
"""
# Нормализуем формат телефона (убираем + если есть)
phone = phone.replace("+", "").replace("-", "").replace(" ", "")
# Проверка 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 если код верный
"""
# Нормализуем формат телефона (убираем + если есть)
phone = phone.replace("+", "").replace("-", "").replace(" ", "")
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} (key: {verification_key})")
return False
logger.info(f"🔍 Comparing codes: stored='{stored_code}' vs input='{code}' (types: {type(stored_code).__name__} vs {type(code).__name__})")
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}: expected '{stored_code}', got '{code}'")
return False
# Глобальный экземпляр
sms_service = SMSService()