""" Demo Session API Routes """ from fastapi import APIRouter, Depends, HTTPException, Request from sqlalchemy.ext.asyncio import AsyncSession from typing import List import structlog from app.api.schemas import ( DemoSessionCreate, DemoSessionResponse, DemoSessionExtend, DemoSessionDestroy, DemoSessionStats, DemoAccountInfo ) from app.services import DemoSessionManager, DemoDataCloner, DemoCleanupService from app.core import get_db, get_redis, settings, RedisClient logger = structlog.get_logger() router = APIRouter(prefix="/api/demo", tags=["demo"]) @router.get("/accounts", response_model=List[DemoAccountInfo]) async def get_demo_accounts(): """ Get public demo account information Returns credentials for prospects to use """ accounts = [] for account_type, config in settings.DEMO_ACCOUNTS.items(): accounts.append({ "account_type": account_type, "name": config["name"], "email": config["email"], "password": "DemoSanPablo2024!" if "sanpablo" in config["email"] else "DemoLaEspiga2024!", "description": ( "Panadería individual que produce todo localmente" if account_type == "individual_bakery" else "Punto de venta con obrador central" ), "features": ( ["Gestión de Producción", "Recetas", "Inventario", "Previsión de Demanda", "Ventas"] if account_type == "individual_bakery" else ["Gestión de Proveedores", "Inventario", "Ventas", "Pedidos", "Previsión"] ), "business_model": ( "Producción Local" if account_type == "individual_bakery" else "Obrador Central + Punto de Venta" ) }) return accounts @router.post("/session/create", response_model=DemoSessionResponse) async def create_demo_session( request: DemoSessionCreate, http_request: Request, db: AsyncSession = Depends(get_db), redis: RedisClient = Depends(get_redis) ): """ Create a new isolated demo session """ logger.info("Creating demo session", demo_account_type=request.demo_account_type) try: # Get client info ip_address = request.ip_address or http_request.client.host user_agent = request.user_agent or http_request.headers.get("user-agent", "") # Create session session_manager = DemoSessionManager(db, redis) session = await session_manager.create_session( demo_account_type=request.demo_account_type, user_id=request.user_id, ip_address=ip_address, user_agent=user_agent ) # Clone demo data using Kubernetes Job (better architecture) from app.services.k8s_job_cloner import K8sJobCloner job_cloner = K8sJobCloner() # Trigger async cloning job (don't wait for completion) import asyncio asyncio.create_task( job_cloner.clone_tenant_data( session.session_id, "", # base_tenant_id not used in job approach str(session.virtual_tenant_id), request.demo_account_type ) ) # Mark as data cloning started await session_manager.mark_data_cloned(session.session_id) await session_manager.mark_redis_populated(session.session_id) # Generate session token (simple JWT-like format) import jwt from datetime import datetime, timezone session_token = jwt.encode( { "session_id": session.session_id, "virtual_tenant_id": str(session.virtual_tenant_id), "demo_account_type": request.demo_account_type, "exp": session.expires_at.timestamp() }, "demo-secret-key", # In production, use proper secret algorithm="HS256" ) return { "session_id": session.session_id, "virtual_tenant_id": str(session.virtual_tenant_id), "demo_account_type": session.demo_account_type, "status": session.status.value, "created_at": session.created_at, "expires_at": session.expires_at, "demo_config": session.metadata.get("demo_config", {}), "session_token": session_token } except Exception as e: logger.error("Failed to create demo session", error=str(e)) raise HTTPException(status_code=500, detail=f"Failed to create demo session: {str(e)}") @router.post("/session/extend", response_model=DemoSessionResponse) async def extend_demo_session( request: DemoSessionExtend, db: AsyncSession = Depends(get_db), redis: RedisClient = Depends(get_redis) ): """ Extend demo session expiration """ try: session_manager = DemoSessionManager(db, redis) session = await session_manager.extend_session(request.session_id) # Generate new token import jwt session_token = jwt.encode( { "session_id": session.session_id, "virtual_tenant_id": str(session.virtual_tenant_id), "demo_account_type": session.demo_account_type, "exp": session.expires_at.timestamp() }, "demo-secret-key", algorithm="HS256" ) return { "session_id": session.session_id, "virtual_tenant_id": str(session.virtual_tenant_id), "demo_account_type": session.demo_account_type, "status": session.status.value, "created_at": session.created_at, "expires_at": session.expires_at, "demo_config": session.metadata.get("demo_config", {}), "session_token": session_token } except ValueError as e: raise HTTPException(status_code=400, detail=str(e)) except Exception as e: logger.error("Failed to extend session", error=str(e)) raise HTTPException(status_code=500, detail=str(e)) @router.post("/session/destroy") async def destroy_demo_session( request: DemoSessionDestroy, db: AsyncSession = Depends(get_db), redis: RedisClient = Depends(get_redis) ): """ Destroy demo session and cleanup resources """ try: session_manager = DemoSessionManager(db, redis) await session_manager.destroy_session(request.session_id) return {"message": "Session destroyed successfully", "session_id": request.session_id} except Exception as e: logger.error("Failed to destroy session", error=str(e)) raise HTTPException(status_code=500, detail=str(e)) @router.get("/session/{session_id}") async def get_session_info( session_id: str, db: AsyncSession = Depends(get_db), redis: RedisClient = Depends(get_redis) ): """ Get demo session information """ session_manager = DemoSessionManager(db, redis) session = await session_manager.get_session(session_id) if not session: raise HTTPException(status_code=404, detail="Session not found") return session.to_dict() @router.get("/stats", response_model=DemoSessionStats) async def get_demo_stats( db: AsyncSession = Depends(get_db), redis: RedisClient = Depends(get_redis) ): """ Get demo session statistics """ session_manager = DemoSessionManager(db, redis) stats = await session_manager.get_session_stats() return stats @router.post("/cleanup/run") async def run_cleanup( db: AsyncSession = Depends(get_db), redis: RedisClient = Depends(get_redis) ): """ Manually trigger session cleanup Internal endpoint for CronJob """ cleanup_service = DemoCleanupService(db, redis) stats = await cleanup_service.cleanup_expired_sessions() return stats @router.get("/health") async def health_check(redis: RedisClient = Depends(get_redis)): """ Health check endpoint """ redis_ok = await redis.ping() return { "status": "healthy" if redis_ok else "degraded", "redis": "connected" if redis_ok else "disconnected" }