- Добавлена полная интеграция с 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 интеграции
155 lines
6.5 KiB
Python
155 lines
6.5 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: 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,
|
||
)
|
||
|