157 lines
5.9 KiB
Python
157 lines
5.9 KiB
Python
"""
|
|
MAX Mini App (WebApp) auth endpoint.
|
|
|
|
/api/v1/max/auth:
|
|
- Принимает init_data от MAX Bridge (window.WebApp.initData)
|
|
- Валидирует init_data и извлекает данные пользователя MAX
|
|
- Проксирует max_user_id в n8n для получения unified_id/контакта
|
|
- Создаёт сессию в Redis (аналогично Telegram — без SMS)
|
|
"""
|
|
|
|
import logging
|
|
from typing import Optional
|
|
|
|
from fastapi import APIRouter, HTTPException
|
|
from fastapi.encoders import jsonable_encoder
|
|
from pydantic import BaseModel
|
|
|
|
from ..services.max_auth import extract_max_user, MaxAuthError
|
|
from ..config import settings
|
|
from . import n8n_proxy
|
|
from . import session as session_api
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
router = APIRouter(prefix="/api/v1/max", tags=["MAX"])
|
|
|
|
|
|
class MaxAuthRequest(BaseModel):
|
|
init_data: str
|
|
session_token: Optional[str] = None
|
|
phone: Optional[str] = None
|
|
|
|
|
|
class MaxAuthResponse(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:
|
|
import uuid
|
|
return f"sess-{uuid.uuid4()}"
|
|
|
|
|
|
@router.post("/auth", response_model=MaxAuthResponse)
|
|
async def max_auth(request: MaxAuthRequest):
|
|
"""
|
|
Авторизация пользователя через MAX WebApp (Mini App).
|
|
"""
|
|
init_data = request.init_data or ""
|
|
phone = (request.phone or "").strip()
|
|
logger.info(
|
|
"[MAX] POST /api/v1/max/auth: init_data длина=%s, phone=%s, session_token=%s",
|
|
len(init_data),
|
|
bool(phone),
|
|
bool(request.session_token),
|
|
)
|
|
if not init_data:
|
|
logger.warning("[MAX] init_data пустой")
|
|
raise HTTPException(status_code=400, detail="init_data обязателен")
|
|
|
|
bot_configured = bool((getattr(settings, "max_bot_token", None) or "").strip())
|
|
webhook_configured = bool((getattr(settings, "n8n_max_auth_webhook", None) or "").strip())
|
|
logger.info("[MAX] Конфиг: MAX_BOT_TOKEN=%s, N8N_MAX_AUTH_WEBHOOK=%s", bot_configured, webhook_configured)
|
|
|
|
try:
|
|
max_user = extract_max_user(request.init_data)
|
|
except MaxAuthError as e:
|
|
logger.warning("[MAX] Ошибка валидации initData: %s", e)
|
|
raise HTTPException(status_code=400, detail=str(e))
|
|
|
|
max_user_id = max_user["max_user_id"]
|
|
logger.info("[MAX] MAX user валиден: id=%s, username=%s", max_user_id, max_user.get("username"))
|
|
|
|
session_token = request.session_token or _generate_session_token()
|
|
|
|
n8n_payload = {
|
|
"max_user_id": max_user_id,
|
|
"username": max_user.get("username"),
|
|
"first_name": max_user.get("first_name"),
|
|
"last_name": max_user.get("last_name"),
|
|
"session_token": session_token,
|
|
"form_id": "ticket_form",
|
|
"init_data": request.init_data,
|
|
}
|
|
if phone:
|
|
n8n_payload["phone"] = phone
|
|
|
|
logger.info("[MAX] Валидация OK → вызов n8n webhook (max_user_id=%s)", max_user_id)
|
|
try:
|
|
class _DummyRequest:
|
|
def __init__(self, payload: dict):
|
|
self._payload = payload
|
|
async def json(self):
|
|
return self._payload
|
|
|
|
n8n_response = await n8n_proxy.proxy_max_auth(_DummyRequest(n8n_payload)) # type: ignore[arg-type]
|
|
n8n_data = jsonable_encoder(n8n_response)
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.exception("[MAX] Ошибка вызова n8n MAX auth webhook: %s", e)
|
|
raise HTTPException(status_code=500, detail=f"Ошибка обращения к n8n: {str(e)}")
|
|
|
|
logger.info("[MAX] 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 {}
|
|
_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("[MAX] n8n: need_contact=true — возвращаем need_contact, фронт закроет приложение")
|
|
return MaxAuthResponse(success=False, need_contact=True)
|
|
|
|
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_res = n8n_data.get("phone") or _result_dict.get("phone")
|
|
has_drafts = n8n_data.get("has_drafts") or _result_dict.get("has_drafts")
|
|
|
|
if not unified_id:
|
|
logger.info("[MAX] n8n не вернул unified_id (юзер не в базе) — возвращаем need_contact=true. Ответ: %s", n8n_data)
|
|
return MaxAuthResponse(success=False, need_contact=True)
|
|
|
|
session_request = session_api.SessionCreateRequest(
|
|
session_token=session_token,
|
|
unified_id=unified_id,
|
|
phone=phone_res or phone or "",
|
|
contact_id=contact_id or "",
|
|
ttl_hours=24,
|
|
chat_id=str(max_user_id) if max_user_id else None,
|
|
)
|
|
|
|
try:
|
|
await session_api.create_session(session_request)
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.exception("[MAX] Ошибка создания сессии в Redis")
|
|
raise HTTPException(status_code=500, detail=f"Ошибка создания сессии: {str(e)}")
|
|
|
|
return MaxAuthResponse(
|
|
success=True,
|
|
session_token=session_token,
|
|
unified_id=unified_id,
|
|
contact_id=contact_id,
|
|
phone=phone_res or phone,
|
|
has_drafts=has_drafts,
|
|
)
|