Files
aiform_prod/backend/app/api/telegram_auth.py

177 lines
8.0 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.

"""
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,
)