Files
crm.clientright.ru/ticket_form/backend/app/api/session.py

194 lines
6.7 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.

"""
Session management API endpoints
Обеспечивает управление сессиями пользователей через Redis:
- Верификация существующей сессии
- Logout (удаление сессии)
"""
import json
import logging
from datetime import datetime, timedelta
from typing import Optional, Dict, Any
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
import redis.asyncio as redis
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/v1/session", tags=["session"])
# Redis connection (используем существующее подключение)
redis_client: Optional[redis.Redis] = None
def init_redis(redis_conn: redis.Redis):
"""Initialize Redis connection"""
global redis_client
redis_client = redis_conn
class SessionVerifyRequest(BaseModel):
session_token: str
class SessionVerifyResponse(BaseModel):
success: bool
valid: bool
unified_id: Optional[str] = None
phone: Optional[str] = None
contact_id: Optional[str] = None
verified_at: Optional[str] = None
expires_in_seconds: Optional[int] = None
class SessionLogoutRequest(BaseModel):
session_token: str
class SessionLogoutResponse(BaseModel):
success: bool
message: str
@router.post("/verify", response_model=SessionVerifyResponse)
async def verify_session(request: SessionVerifyRequest):
"""
Проверить валидность сессии по session_token
Используется при загрузке страницы, чтобы восстановить сессию пользователя.
Если сессия валидна - возвращаем unified_id, phone и другие данные.
"""
try:
if not redis_client:
raise HTTPException(status_code=500, detail="Redis connection not initialized")
session_key = f"session:{request.session_token}"
logger.info(f"🔍 Проверка сессии: {session_key}")
# Получаем данные сессии из Redis
session_data_raw = await redis_client.get(session_key)
if not session_data_raw:
logger.info(f"❌ Сессия не найдена или истекла: {session_key}")
return SessionVerifyResponse(
success=True,
valid=False
)
# Парсим данные сессии
session_data = json.loads(session_data_raw)
# Получаем TTL (оставшееся время жизни)
ttl = await redis_client.ttl(session_key)
logger.info(f"✅ Сессия валидна: unified_id={session_data.get('unified_id')}, TTL={ttl}s")
return SessionVerifyResponse(
success=True,
valid=True,
unified_id=session_data.get('unified_id'),
phone=session_data.get('phone'),
contact_id=session_data.get('contact_id'),
verified_at=session_data.get('verified_at'),
expires_in_seconds=ttl if ttl > 0 else None
)
except json.JSONDecodeError as e:
logger.error(f"❌ Ошибка парсинга данных сессии: {e}")
return SessionVerifyResponse(
success=True,
valid=False
)
except Exception as e:
logger.exception("❌ Ошибка проверки сессии")
raise HTTPException(status_code=500, detail=f"Ошибка проверки сессии: {str(e)}")
@router.post("/logout", response_model=SessionLogoutResponse)
async def logout_session(request: SessionLogoutRequest):
"""
Выход из сессии (удаление session_token из Redis)
Используется при клике на кнопку "Выход".
"""
try:
if not redis_client:
raise HTTPException(status_code=500, detail="Redis connection not initialized")
session_key = f"session:{request.session_token}"
logger.info(f"🚪 Выход из сессии: {session_key}")
# Удаляем сессию из Redis
deleted = await redis_client.delete(session_key)
if deleted > 0:
logger.info(f"✅ Сессия удалена: {session_key}")
return SessionLogoutResponse(
success=True,
message="Выход выполнен успешно"
)
else:
logger.info(f"⚠️ Сессия не найдена (возможно, уже удалена): {session_key}")
return SessionLogoutResponse(
success=True,
message="Сессия уже завершена"
)
except Exception as e:
logger.exception("❌ Ошибка при выходе из сессии")
raise HTTPException(status_code=500, detail=f"Ошибка при выходе: {str(e)}")
class SessionCreateRequest(BaseModel):
session_token: str
unified_id: str
phone: str
contact_id: str
ttl_hours: int = 24
@router.post("/create")
async def create_session(request: SessionCreateRequest):
"""
Создать новую сессию (вызывается после успешной SMS верификации)
Обычно вызывается из Step1Phone после получения данных от n8n.
"""
try:
if not redis_client:
raise HTTPException(status_code=500, detail="Redis connection not initialized")
session_key = f"session:{request.session_token}"
session_data = {
'unified_id': request.unified_id,
'phone': request.phone,
'contact_id': request.contact_id,
'verified_at': datetime.utcnow().isoformat(),
'expires_at': (datetime.utcnow() + timedelta(hours=request.ttl_hours)).isoformat()
}
# Сохраняем в Redis с TTL
await redis_client.setex(
session_key,
request.ttl_hours * 3600, # TTL в секундах
json.dumps(session_data)
)
logger.info(f"✅ Сессия создана: {session_key}, unified_id={request.unified_id}, TTL={request.ttl_hours}h")
return {
'success': True,
'session_token': request.session_token,
'expires_in_seconds': request.ttl_hours * 3600
}
except Exception as e:
logger.exception("❌ Ошибка создания сессии")
raise HTTPException(status_code=500, detail=f"Ошибка создания сессии: {str(e)}")