Files
aiform_prod/backend/app/api/telegram_auth.py
AI Assistant 2e45786e46 feat: Telegram Mini App integration and UX improvements
- Добавлена полная интеграция с Telegram Mini App (динамическая загрузка SDK)
- Отдельный компактный дизайн для Telegram Mini App
- Добавлен loader при инициализации (предотвращает мелькание SMS-авторизации)
- Улучшена навигация: кнопки "Назад" и "К списку заявок" теперь сохраняют авторизацию
- Telegram Mini App: кнопка "Выход" просто закрывает приложение
- Telegram Mini App: заявки "В работе" скрыты из списка
- Веб-версия: для заявок "В работе" добавлена кнопка "Просмотреть в Telegram" (ссылка на @klientprav_bot)
- Telegram Mini App: кнопки действий в черновиках расположены вертикально
- Веб-версия: убрано отображение номера телефона в приветствии
- Исправлена проблема с возвратом к списку черновиков (не требует повторной SMS-авторизации)
- Заблокировано удаление и редактирование заявок со статусом "В работе"
- Добавлена документация по Telegram Mini App интеграции
2026-01-29 16:12:48 +03:00

155 lines
6.5 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: str
unified_id: str
contact_id: Optional[str] = None
phone: Optional[str] = None
has_drafts: 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 как минимум unified_id
unified_id = n8n_data.get("unified_id") or (n8n_data.get("result") or {}).get("unified_id")
contact_id = n8n_data.get("contact_id") or n8n_data.get("result", {}).get("contact_id")
phone = n8n_data.get("phone") or n8n_data.get("result", {}).get("phone")
has_drafts = n8n_data.get("has_drafts")
if not unified_id:
logger.error("[TG] n8n не вернул unified_id. Полный ответ: %s", n8n_data)
raise HTTPException(status_code=500, detail="n8n не вернул unified_id для Telegram пользователя")
# 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,
)
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,
)