- 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 пересобран с новым кодом
197 lines
7.6 KiB
Python
197 lines
7.6 KiB
Python
"""
|
||
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()
|
||
|