257 lines
10 KiB
Python
257 lines
10 KiB
Python
"""
|
|
Alternative auth endpoint (tg/max/sms) without touching existing flow.
|
|
|
|
/api/v1/auth2/login:
|
|
- platform=tg|max|sms
|
|
- Validates init_data for TG/MAX and calls n8n webhook
|
|
- For SMS: verifies code, calls n8n contact webhook, creates session
|
|
- Returns greeting message
|
|
"""
|
|
|
|
import logging
|
|
from typing import Optional, Literal, Any, Dict
|
|
|
|
from fastapi import APIRouter, HTTPException
|
|
from fastapi.encoders import jsonable_encoder
|
|
from pydantic import BaseModel
|
|
|
|
from ..services.sms_service import sms_service
|
|
from ..services.telegram_auth import extract_telegram_user, TelegramAuthError
|
|
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/auth2", tags=["auth2"])
|
|
|
|
|
|
class Auth2LoginRequest(BaseModel):
|
|
platform: Literal["tg", "max", "sms"]
|
|
init_data: Optional[str] = None
|
|
phone: Optional[str] = None
|
|
code: Optional[str] = None
|
|
session_token: Optional[str] = None
|
|
form_id: str = "ticket_form"
|
|
|
|
|
|
class Auth2LoginResponse(BaseModel):
|
|
success: bool
|
|
greeting: str
|
|
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
|
|
avatar_url: Optional[str] = None
|
|
|
|
|
|
def _generate_session_token() -> str:
|
|
import uuid
|
|
return f"sess-{uuid.uuid4()}"
|
|
|
|
|
|
@router.post("/login", response_model=Auth2LoginResponse)
|
|
async def login(request: Auth2LoginRequest):
|
|
platform = request.platform
|
|
logger.info("[AUTH2] login: platform=%s", platform)
|
|
|
|
if platform == "tg":
|
|
if not request.init_data:
|
|
raise HTTPException(status_code=400, detail="init_data обязателен для tg")
|
|
try:
|
|
tg_user = extract_telegram_user(request.init_data)
|
|
except TelegramAuthError as e:
|
|
raise HTTPException(status_code=400, detail=str(e))
|
|
|
|
session_token = request.session_token or _generate_session_token()
|
|
n8n_payload = {
|
|
"telegram_user_id": tg_user["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": request.form_id,
|
|
"init_data": request.init_data,
|
|
}
|
|
|
|
class _DummyRequest:
|
|
def __init__(self, payload: Dict[str, Any]):
|
|
self._payload = payload
|
|
async def json(self):
|
|
return self._payload
|
|
|
|
n8n_response = await n8n_proxy.proxy_telegram_auth(_DummyRequest(n8n_payload)) # type: ignore[arg-type]
|
|
n8n_data = jsonable_encoder(n8n_response)
|
|
_result = n8n_data.get("result")
|
|
_result_dict = _result if isinstance(_result, dict) else {}
|
|
|
|
_raw_nc = n8n_data.get("need_contact") or _result_dict.get("need_contact") or n8n_data.get("needContact") or _result_dict.get("needContact")
|
|
need_contact = _raw_nc is True or _raw_nc == 1 or (isinstance(_raw_nc, str) and str(_raw_nc).strip().lower() in ("true", "1"))
|
|
if need_contact:
|
|
logger.info("[AUTH2] TG: n8n need_contact — возвращаем need_contact=true")
|
|
return Auth2LoginResponse(success=False, greeting="Привет!", 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 = n8n_data.get("phone") or _result_dict.get("phone")
|
|
has_drafts = n8n_data.get("has_drafts")
|
|
|
|
if not unified_id:
|
|
logger.info("[AUTH2] TG: n8n не вернул unified_id — возвращаем need_contact=true")
|
|
return Auth2LoginResponse(success=False, greeting="Привет!", need_contact=True)
|
|
|
|
await session_api.create_session(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(tg_user["telegram_user_id"]) if tg_user.get("telegram_user_id") is not None else None,
|
|
))
|
|
|
|
first_name = tg_user.get("first_name") or ""
|
|
greeting = f"Привет, {first_name}!" if first_name else "Привет!"
|
|
|
|
return Auth2LoginResponse(
|
|
success=True,
|
|
greeting=greeting,
|
|
session_token=session_token,
|
|
unified_id=unified_id,
|
|
contact_id=contact_id,
|
|
phone=phone,
|
|
has_drafts=has_drafts,
|
|
avatar_url=tg_user.get("photo_url") or None,
|
|
)
|
|
|
|
if platform == "max":
|
|
if not request.init_data:
|
|
raise HTTPException(status_code=400, detail="init_data обязателен для max")
|
|
try:
|
|
max_user = extract_max_user(request.init_data)
|
|
except MaxAuthError as e:
|
|
raise HTTPException(status_code=400, detail=str(e))
|
|
|
|
session_token = request.session_token or _generate_session_token()
|
|
n8n_payload = {
|
|
"max_user_id": max_user["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": request.form_id,
|
|
"init_data": request.init_data,
|
|
}
|
|
|
|
class _DummyRequest:
|
|
def __init__(self, payload: Dict[str, Any]):
|
|
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)
|
|
_result = n8n_data.get("result")
|
|
_result_dict = _result if isinstance(_result, dict) else {}
|
|
|
|
_raw_nc = n8n_data.get("need_contact") or _result_dict.get("need_contact") or n8n_data.get("needContact") or _result_dict.get("needContact")
|
|
need_contact = _raw_nc is True or _raw_nc == 1 or (isinstance(_raw_nc, str) and str(_raw_nc).strip().lower() in ("true", "1"))
|
|
if need_contact:
|
|
logger.info("[AUTH2] MAX: n8n need_contact — возвращаем need_contact=true")
|
|
return Auth2LoginResponse(success=False, greeting="Привет!", 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 = n8n_data.get("phone") or _result_dict.get("phone")
|
|
has_drafts = n8n_data.get("has_drafts")
|
|
|
|
if not unified_id:
|
|
logger.info("[AUTH2] MAX: n8n не вернул unified_id — возвращаем need_contact=true")
|
|
return Auth2LoginResponse(success=False, greeting="Привет!", need_contact=True)
|
|
|
|
await session_api.create_session(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(max_user["max_user_id"]) if max_user.get("max_user_id") is not None else None,
|
|
))
|
|
|
|
first_name = max_user.get("first_name") or ""
|
|
greeting = f"Привет, {first_name}!" if first_name else "Привет!"
|
|
|
|
return Auth2LoginResponse(
|
|
success=True,
|
|
greeting=greeting,
|
|
session_token=session_token,
|
|
unified_id=unified_id,
|
|
contact_id=contact_id,
|
|
phone=phone,
|
|
has_drafts=has_drafts,
|
|
avatar_url=max_user.get("photo_url") or None,
|
|
)
|
|
|
|
if platform == "sms":
|
|
phone = (request.phone or "").strip()
|
|
code = (request.code or "").strip()
|
|
if not phone or not code:
|
|
raise HTTPException(status_code=400, detail="phone и code обязательны для sms")
|
|
|
|
is_valid = await sms_service.verify_code(phone, code)
|
|
if not is_valid:
|
|
raise HTTPException(status_code=400, detail="Неверный код или код истек")
|
|
|
|
class _DummyRequest:
|
|
def __init__(self, payload: Dict[str, Any]):
|
|
self._payload = payload
|
|
async def json(self):
|
|
return self._payload
|
|
|
|
n8n_payload = {
|
|
"phone": phone,
|
|
"session_id": request.session_token or "",
|
|
"form_id": request.form_id,
|
|
}
|
|
|
|
n8n_response = await n8n_proxy.proxy_create_contact(_DummyRequest(n8n_payload)) # type: ignore[arg-type]
|
|
n8n_data = jsonable_encoder(n8n_response)
|
|
if isinstance(n8n_data, list) and n8n_data:
|
|
n8n_data = n8n_data[0]
|
|
|
|
if not n8n_data or not isinstance(n8n_data, dict) or not n8n_data.get("success"):
|
|
raise HTTPException(status_code=500, detail="Ошибка создания контакта в n8n")
|
|
|
|
result = n8n_data.get("result") or n8n_data
|
|
unified_id = result.get("unified_id")
|
|
contact_id = result.get("contact_id")
|
|
phone_res = result.get("phone") or phone
|
|
has_drafts = result.get("has_drafts")
|
|
session_token = result.get("session") or request.session_token or _generate_session_token()
|
|
|
|
if not unified_id:
|
|
raise HTTPException(status_code=500, detail="n8n не вернул unified_id")
|
|
|
|
await session_api.create_session(session_api.SessionCreateRequest(
|
|
session_token=session_token,
|
|
unified_id=unified_id,
|
|
phone=phone_res or "",
|
|
contact_id=contact_id or "",
|
|
ttl_hours=24,
|
|
))
|
|
|
|
return Auth2LoginResponse(
|
|
success=True,
|
|
greeting="Привет!",
|
|
session_token=session_token,
|
|
unified_id=unified_id,
|
|
contact_id=contact_id,
|
|
phone=phone_res,
|
|
has_drafts=has_drafts,
|
|
avatar_url=None,
|
|
)
|
|
|
|
raise HTTPException(status_code=400, detail="Неподдерживаемая платформа")
|