Auth: multibot TG MAX logging fix 500

This commit is contained in:
Fedor
2026-02-27 07:48:16 +03:00
parent b3a7396d32
commit 62fc57f108
6 changed files with 417 additions and 78 deletions

View File

@@ -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:

View File

@@ -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

View File

@@ -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)

View File

@@ -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