Files
aiform_prod/backend/app/api/session.py
AI Assistant 3621ae6021 feat: Session persistence with Redis + Draft management fixes
- Implement session management API (/api/v1/session/create, verify, logout)
- Add session restoration from localStorage on page reload
- Fix session_id priority when loading drafts (use current, not old from DB)
- Add unified_id and claim_id to wizard payload sent to n8n
- Add Docker volume for frontend HMR (Hot Module Replacement)
- Add comprehensive session logging for debugging

Components updated:
- backend/app/api/session.py (NEW) - Session management endpoints
- backend/app/main.py - Include session router
- frontend/src/components/form/Step1Phone.tsx v2.0 - Create session after SMS
- frontend/src/pages/ClaimForm.tsx v3.8 - Session restoration & priority fix
- frontend/src/components/form/StepWizardPlan.tsx v1.4 - Add unified_id/claim_id
- docker-compose.yml - Add frontend volume for live reload

Session flow:
1. User verifies phone -> session created in Redis (24h TTL)
2. session_token saved to localStorage
3. Page reload -> session restored automatically
4. Draft selected -> current session_id used (not old from DB)
5. Wizard submit -> unified_id, claim_id, session_id sent to n8n
6. Logout -> session removed from Redis & localStorage

Fixes:
- Session token not persisting after page reload
- unified_id missing in n8n webhook payload
- Old session_id from draft overwriting current session
- Frontend changes requiring container rebuild
2025-11-20 18:31:42 +03:00

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)}")