""" Telegram Mini App (WebApp) auth endpoint. /api/v1/tg/auth: - Принимает init_data от Telegram WebApp и (опционально) session_token - Валидирует init_data и извлекает данные пользователя Telegram - Проксирует telegram_user_id в n8n для получения unified_id/контакта - Создаёт сессию в Redis через существующий /api/v1/session/create """ import logging from typing import Optional from fastapi import APIRouter, HTTPException from pydantic import BaseModel from ..services.telegram_auth import extract_telegram_user, TelegramAuthError from ..config import settings from . import n8n_proxy from . import session as session_api logger = logging.getLogger(__name__) router = APIRouter(prefix="/api/v1/tg", tags=["Telegram"]) class TelegramAuthRequest(BaseModel): init_data: str session_token: Optional[str] = None class TelegramAuthResponse(BaseModel): success: bool session_token: Optional[str] = None unified_id: Optional[str] = None contact_id: Optional[str] = None phone: Optional[str] = None has_drafts: Optional[bool] = None need_contact: Optional[bool] = None def _generate_session_token() -> str: """Генерирует новый session_token в формате, похожем на текущий веб-флоу.""" import uuid return f"sess-{uuid.uuid4()}" @router.post("/auth", response_model=TelegramAuthResponse) async def telegram_auth(request: TelegramAuthRequest): """ Авторизация пользователя через Telegram WebApp. Ничего не ломает в текущем SMS-флоу: это параллельный способ входа. """ # Логирование: что пришло на бэкенд init_data = request.init_data or "" logger.info( "[TG] POST /api/v1/tg/auth вызван: init_data длина=%s, session_token передан=%s", len(init_data), bool(request.session_token), ) if not init_data: logger.warning("[TG] init_data пустой — запрос отклонён") raise HTTPException(status_code=400, detail="init_data обязателен") bot_token_configured = bool((getattr(settings, "telegram_bot_token", None) or "").strip()) n8n_webhook_configured = bool((getattr(settings, "n8n_tg_auth_webhook", None) or "").strip()) logger.info("[TG] Конфиг: TELEGRAM_BOT_TOKEN задан=%s, N8N_TG_AUTH_WEBHOOK задан=%s", bot_token_configured, n8n_webhook_configured) # 1. Валидация и разбор init_data try: tg_user = extract_telegram_user(request.init_data) except TelegramAuthError as e: logger.warning("[TG] Ошибка валидации initData: %s", e) raise HTTPException(status_code=400, detail=str(e)) telegram_user_id = tg_user["telegram_user_id"] logger.info("[TG] Telegram user валиден: id=%s, username=%s", telegram_user_id, tg_user.get("username")) # 2. Определяем session_token session_token = request.session_token or _generate_session_token() # 3. Вызываем n8n через прокси для маппинга telegram_user_id → unified_id n8n_payload = { "telegram_user_id": telegram_user_id, "username": tg_user.get("username"), "first_name": tg_user.get("first_name"), "last_name": tg_user.get("last_name"), "session_token": session_token, "form_id": "ticket_form", "init_data": request.init_data, # сырая строка из Telegram (подпись уже проверена) } logger.info("[TG] Вызов n8n webhook, payload keys=%s", list(n8n_payload.keys())) # Используем уже существующий n8n_proxy роут (внутренний вызов) try: from fastapi.encoders import jsonable_encoder # Объект с async .json() для proxy_telegram_auth(request), без Pydantic __root__ class _DummyRequest: def __init__(self, payload: dict): self._payload = payload async def json(self): return self._payload dummy_request = _DummyRequest(n8n_payload) n8n_response = await n8n_proxy.proxy_telegram_auth(dummy_request) # type: ignore[arg-type] n8n_data = jsonable_encoder(n8n_response) logger.info("[TG] n8n ответ получен: keys=%s", list(n8n_data.keys()) if isinstance(n8n_data, dict) else type(n8n_data).__name__) except HTTPException: # Пробрасываем HTTPException наверх raise except Exception as e: logger.exception("[TG] Ошибка вызова n8n Telegram auth webhook: %s", e) raise HTTPException(status_code=500, detail=f"Ошибка обращения к n8n: {str(e)}") # Логируем сырой ответ n8n для отладки (ключи и need_contact/unified_id) logger.info("[TG] n8n ответ (ключи): %s", list(n8n_data.keys()) if isinstance(n8n_data, dict) else type(n8n_data).__name__) _result = n8n_data.get("result") _result_dict = _result if isinstance(_result, dict) else {} if _result_dict: logger.info("[TG] n8n result ключи: %s", list(_result_dict.keys())) # Если n8n вернул need_contact — пользователя нет в базе, мини-апп должен закрыться _raw = ( n8n_data.get("need_contact") or _result_dict.get("need_contact") or n8n_data.get("needContact") or _result_dict.get("needContact") ) need_contact = _raw is True or _raw == 1 or (isinstance(_raw, str) and str(_raw).strip().lower() in ("true", "1")) if need_contact: logger.info("[TG] n8n: need_contact=true — возвращаем need_contact, фронт закроет приложение") return TelegramAuthResponse(success=False, need_contact=True) # Ожидаем от n8n как минимум unified_id unified_id = n8n_data.get("unified_id") or _result_dict.get("unified_id") or n8n_data.get("unifiedId") contact_id = n8n_data.get("contact_id") or _result_dict.get("contact_id") or n8n_data.get("contactId") phone = n8n_data.get("phone") or _result_dict.get("phone") has_drafts = n8n_data.get("has_drafts") or _result_dict.get("has_drafts") # Нет unified_id = пользователь не найден в базе → тоже возвращаем need_contact, чтобы фронт закрыл мини-апп if not unified_id: logger.info("[TG] n8n не вернул unified_id (пользователь не в базе) — возвращаем need_contact=true. Ответ n8n: %s", n8n_data) return TelegramAuthResponse(success=False, need_contact=True) # 4. Создаём сессию в Redis через существующий /api/v1/session/create # Для Telegram телефон может быть ещё неизвестен, поэтому передаём пустые строки при отсутствии. session_request = session_api.SessionCreateRequest( session_token=session_token, unified_id=unified_id, phone=phone or "", contact_id=contact_id or "", ttl_hours=24, chat_id=str(telegram_user_id) if telegram_user_id else None, ) try: await session_api.create_session(session_request) except HTTPException: # Если ошибка уже обёрнута в HTTPException — пробрасываем как есть raise except Exception as e: logger.exception("❌ Error creating Redis session for Telegram user") raise HTTPException(status_code=500, detail=f"Ошибка создания сессии: {str(e)}") return TelegramAuthResponse( success=True, session_token=session_token, unified_id=unified_id, contact_id=contact_id, phone=phone, has_drafts=has_drafts, )