177 lines
8.0 KiB
Python
177 lines
8.0 KiB
Python
"""
|
||
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,
|
||
)
|
||
|