Auth: multibot TG MAX logging fix 500
This commit is contained in:
@@ -5,6 +5,7 @@
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
import uuid
|
||||
from typing import Optional, Any, Dict
|
||||
|
||||
@@ -57,6 +58,8 @@ async def auth_universal(request: AuthUniversalRequest):
|
||||
if not init_data:
|
||||
raise HTTPException(status_code=400, detail="init_data обязателен")
|
||||
|
||||
logger.debug("[AUTH] init_data length=%s", len(init_data))
|
||||
|
||||
# 1) Извлечь channel_user_id из init_data
|
||||
channel_user_id: Optional[str] = None
|
||||
if channel == "telegram":
|
||||
@@ -77,7 +80,8 @@ async def auth_universal(request: AuthUniversalRequest):
|
||||
if not channel_user_id:
|
||||
raise HTTPException(status_code=400, detail="Не удалось получить channel_user_id из init_data")
|
||||
|
||||
webhook_url = (getattr(settings, "n8n_auth_webhook", None) or "").strip()
|
||||
# URL из settings или напрямую из env (если в config нет поля n8n_auth_webhook)
|
||||
webhook_url = (getattr(settings, "n8n_auth_webhook", None) or os.environ.get("N8N_AUTH_WEBHOOK") or "").strip()
|
||||
if not webhook_url:
|
||||
logger.error("N8N_AUTH_WEBHOOK не задан в .env")
|
||||
raise HTTPException(status_code=503, detail="Сервис авторизации не настроен")
|
||||
@@ -88,6 +92,9 @@ async def auth_universal(request: AuthUniversalRequest):
|
||||
"channel_user_id": channel_user_id,
|
||||
"init_data": init_data,
|
||||
}
|
||||
# При мультиботе (Telegram или MAX) передаём bot_id (из extract_telegram_user / extract_max_user)
|
||||
if user.get("bot_id"):
|
||||
payload["bot_id"] = user["bot_id"]
|
||||
logger.info("[AUTH] Вызов N8N_AUTH_WEBHOOK: channel=%s, channel_user_id=%s", channel, channel_user_id)
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=30.0) as client:
|
||||
@@ -129,8 +136,13 @@ async def auth_universal(request: AuthUniversalRequest):
|
||||
data = {}
|
||||
logger.warning("[AUTH] парсинг: first без json/success/unified_id, data={}")
|
||||
elif isinstance(raw, dict):
|
||||
data = raw
|
||||
logger.info("[AUTH] парсинг: raw — dict, keys=%s", list(data.keys()))
|
||||
# n8n Respond to Webhook может вернуть { "json": { success, phone, ... } }
|
||||
if "json" in raw and isinstance(raw.get("json"), dict):
|
||||
data = raw["json"]
|
||||
logger.info("[AUTH] парсинг: raw — dict с json, data keys=%s", list(data.keys()))
|
||||
else:
|
||||
data = raw
|
||||
logger.info("[AUTH] парсинг: raw — dict, keys=%s", list(data.keys()))
|
||||
else:
|
||||
data = {}
|
||||
logger.warning("[AUTH] парсинг: неизвестный формат raw, data={}")
|
||||
@@ -155,11 +167,13 @@ async def auth_universal(request: AuthUniversalRequest):
|
||||
)
|
||||
if data.get("success") is False:
|
||||
# Ошибка/неуспех без требования контакта — не закрываем приложение, показываем сообщение
|
||||
logger.info("[AUTH] ответ: success=false, need_contact=false → показать ошибку")
|
||||
msg = data.get("message") or "Ошибка авторизации."
|
||||
logger.info("[AUTH] ответ: success=false, need_contact=false → показать ошибку: message=%s", msg)
|
||||
logger.debug("[AUTH] полный data при success=false: %s", data)
|
||||
return AuthUniversalResponse(
|
||||
success=False,
|
||||
need_contact=False,
|
||||
message=(data.get("message") or "Ошибка авторизации."),
|
||||
message=msg,
|
||||
)
|
||||
|
||||
# 4) Успех: unified_id и т.д.
|
||||
@@ -172,13 +186,20 @@ async def auth_universal(request: AuthUniversalRequest):
|
||||
return AuthUniversalResponse(success=False, need_contact=True, message="Контакт не найден.")
|
||||
|
||||
# 5) Записать сессию в Redis по session:{channel}:{channel_user_id} и session:{session_token}
|
||||
_phone = data.get("phone") or ((data.get("result") or {}).get("phone") if isinstance(data.get("result"), dict) else None)
|
||||
_contact_id = data.get("contact_id") or ((data.get("result") or {}).get("contact_id") if isinstance(data.get("result"), dict) else None)
|
||||
if _phone is not None and not isinstance(_phone, str):
|
||||
_phone = str(_phone).strip() or None
|
||||
elif isinstance(_phone, str):
|
||||
_phone = _phone.strip() or None
|
||||
session_data = {
|
||||
"unified_id": unified_id,
|
||||
"phone": data.get("phone") or (data.get("result") or {}).get("phone") if isinstance(data.get("result"), dict) else None,
|
||||
"contact_id": data.get("contact_id") or (data.get("result") or {}).get("contact_id") if isinstance(data.get("result"), dict) else None,
|
||||
"phone": _phone,
|
||||
"contact_id": _contact_id,
|
||||
"has_drafts": data.get("has_drafts", False) or (data.get("result") or {}).get("has_drafts", False) if isinstance(data.get("result"), dict) else False,
|
||||
"chat_id": channel_user_id,
|
||||
}
|
||||
logger.info("[AUTH] session_data: unified_id=%s, phone=%s", unified_id, session_data.get("phone"))
|
||||
try:
|
||||
await session_api.set_session_by_channel_user(channel, channel_user_id, session_data)
|
||||
except HTTPException:
|
||||
|
||||
@@ -210,10 +210,25 @@ class Settings(BaseSettings):
|
||||
# ============================================
|
||||
telegram_bot_token: str = "" # Токен бота для проверки initData WebApp
|
||||
|
||||
def get_telegram_bot_tokens(self) -> List[tuple]:
|
||||
"""Список (bot_id, token) для проверки подписи Telegram initData. Один токен — [('default', token)]."""
|
||||
token = (self.telegram_bot_token or "").strip()
|
||||
if token:
|
||||
return [("default", token)]
|
||||
return []
|
||||
|
||||
# ============================================
|
||||
# MAX (мессенджер) — Mini App auth
|
||||
# ============================================
|
||||
max_bot_token: str = "" # Токен бота MAX для проверки initData WebApp
|
||||
|
||||
def get_max_bot_tokens(self) -> List[tuple]:
|
||||
"""Список (bot_id, token) для проверки подписи MAX initData. Один токен — [('default', token)]."""
|
||||
token = (self.max_bot_token or "").strip()
|
||||
if token:
|
||||
return [("default", token)]
|
||||
return []
|
||||
|
||||
n8n_max_auth_webhook: str = "" # Webhook n8n: max_user_id → unified_id, contact_id, has_drafts
|
||||
n8n_auth_webhook: str = "" # Универсальный auth: channel + channel_user_id + init_data → unified_id, phone, contact_id, has_drafts
|
||||
|
||||
|
||||
@@ -22,12 +22,18 @@ from .services.s3_service import s3_service
|
||||
from .api import sms, claims, policy, upload, draft, events, n8n_proxy, session, documents, banks, telegram_auth, max_auth, auth2, auth_universal, documents_draft_open, profile, support
|
||||
from .api import debug_session
|
||||
|
||||
# Настройка логирования
|
||||
# Настройка логирования (уровень из config: LOG_LEVEL=DEBUG для отладки)
|
||||
import sys
|
||||
_level = getattr(logging, (getattr(get_settings(), "log_level", None) or "INFO").upper(), logging.INFO)
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
level=_level,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||
stream=sys.stdout,
|
||||
)
|
||||
# Применяем уровень ко всем логгерам приложения
|
||||
logging.getLogger("app").setLevel(_level)
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.info("Backend log level: %s", logging.getLevelName(_level))
|
||||
|
||||
DEBUG_SESSION_ID = "2a4d38"
|
||||
# В прод-контейнере гарантированно доступен /app/logs (volume ./backend/logs:/app/logs)
|
||||
|
||||
@@ -29,10 +29,28 @@ def _parse_init_data(init_data: str) -> Dict[str, Any]:
|
||||
return data
|
||||
|
||||
|
||||
def _verify_with_token(parsed: Dict[str, Any], data_check_string: str, received_hash: str, bot_token: str) -> bool:
|
||||
"""Проверяет подпись initData одним MAX ботом. Возвращает True, если подпись верна."""
|
||||
secret_key = hmac.new(
|
||||
key="WebAppData".encode("utf-8"),
|
||||
msg=bot_token.encode("utf-8"),
|
||||
digestmod=hashlib.sha256,
|
||||
).digest()
|
||||
calculated_hash = hmac.new(
|
||||
key=secret_key,
|
||||
msg=data_check_string.encode("utf-8"),
|
||||
digestmod=hashlib.sha256,
|
||||
).hexdigest()
|
||||
return hmac.compare_digest(calculated_hash, received_hash)
|
||||
|
||||
|
||||
def verify_max_init_data(init_data: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Проверяет подпись initData по правилам MAX (аналогично Telegram).
|
||||
|
||||
Поддерживает один бот (MAX_BOT_TOKEN) или несколько (MAX_BOT_TOKENS — JSON).
|
||||
Перебирает токены, пока один не подойдёт; в результат добавляется ключ bot_id.
|
||||
|
||||
- secret_key = HMAC_SHA256("WebAppData", BOT_TOKEN)
|
||||
- data_check_string: пары key=value без hash, сортировка по key, разделитель \n
|
||||
- hex(HMAC_SHA256(secret_key, data_check_string)) === hash из initData
|
||||
@@ -41,9 +59,9 @@ def verify_max_init_data(init_data: str) -> Dict[str, Any]:
|
||||
logger.warning("[MAX] verify_max_init_data: init_data пустой")
|
||||
raise MaxAuthError("init_data is empty")
|
||||
|
||||
bot_token = (getattr(settings, "max_bot_token", None) or "").strip()
|
||||
if not bot_token:
|
||||
logger.warning("[MAX] MAX_BOT_TOKEN не задан в .env")
|
||||
tokens_list = settings.get_max_bot_tokens()
|
||||
if not tokens_list:
|
||||
logger.warning("[MAX] Ни MAX_BOT_TOKEN, ни MAX_BOT_TOKENS не заданы в .env")
|
||||
raise MaxAuthError("MAX bot token is not configured")
|
||||
|
||||
parsed = _parse_init_data(init_data)
|
||||
@@ -54,29 +72,17 @@ def verify_max_init_data(init_data: str) -> Dict[str, Any]:
|
||||
logger.warning("[MAX] В initData отсутствует поле hash")
|
||||
raise MaxAuthError("Missing hash in init_data")
|
||||
|
||||
data_check_items = []
|
||||
for key in sorted(parsed.keys()):
|
||||
value = parsed[key]
|
||||
data_check_items.append(f"{key}={value}")
|
||||
data_check_items = [f"{k}={parsed[k]}" for k in sorted(parsed.keys())]
|
||||
data_check_string = "\n".join(data_check_items)
|
||||
|
||||
secret_key = hmac.new(
|
||||
key="WebAppData".encode("utf-8"),
|
||||
msg=bot_token.encode("utf-8"),
|
||||
digestmod=hashlib.sha256,
|
||||
).digest()
|
||||
for bot_id, bot_token in tokens_list:
|
||||
if _verify_with_token(parsed, data_check_string, received_hash, bot_token):
|
||||
parsed["bot_id"] = bot_id
|
||||
logger.info("[MAX] Подпись MAX initData проверена, bot_id=%s", bot_id)
|
||||
return parsed
|
||||
|
||||
calculated_hash = hmac.new(
|
||||
key=secret_key,
|
||||
msg=data_check_string.encode("utf-8"),
|
||||
digestmod=hashlib.sha256,
|
||||
).hexdigest()
|
||||
|
||||
if not hmac.compare_digest(calculated_hash, received_hash):
|
||||
logger.warning("[MAX] Подпись initData не совпадает")
|
||||
raise MaxAuthError("Invalid init_data hash")
|
||||
|
||||
return parsed
|
||||
logger.warning("[MAX] Подпись initData не совпадает ни с одним из токенов MAX ботов")
|
||||
raise MaxAuthError("Invalid init_data hash")
|
||||
|
||||
|
||||
def extract_max_user(init_data: str) -> Dict[str, Any]:
|
||||
@@ -100,7 +106,7 @@ def extract_max_user(init_data: str) -> Dict[str, Any]:
|
||||
if "id" not in user_obj:
|
||||
raise MaxAuthError("MAX user.id is missing")
|
||||
|
||||
return {
|
||||
result = {
|
||||
"max_user_id": str(user_obj.get("id")),
|
||||
"username": user_obj.get("username"),
|
||||
"first_name": user_obj.get("first_name"),
|
||||
@@ -109,3 +115,6 @@ def extract_max_user(init_data: str) -> Dict[str, Any]:
|
||||
"photo_url": user_obj.get("photo_url"),
|
||||
"raw": user_obj,
|
||||
}
|
||||
if "bot_id" in parsed:
|
||||
result["bot_id"] = parsed["bot_id"]
|
||||
return result
|
||||
|
||||
Reference in New Issue
Block a user