Add DEMO feature to the project

This commit is contained in:
Urtzi Alfaro
2025-10-03 14:09:34 +02:00
parent 1243c2ca6d
commit dc8221bd2f
77 changed files with 6251 additions and 1074 deletions

View File

@@ -0,0 +1,5 @@
"""Demo Session API"""
from .routes import router
__all__ = ["router"]

View File

@@ -0,0 +1,254 @@
"""
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"
}

View File

@@ -0,0 +1,76 @@
"""
API Schemas for Demo Session Service
"""
from pydantic import BaseModel, Field
from typing import Optional, Dict, Any
from datetime import datetime
class DemoSessionCreate(BaseModel):
"""Create demo session request"""
demo_account_type: str = Field(..., description="individual_bakery or central_baker")
user_id: Optional[str] = Field(None, description="Optional authenticated user ID")
ip_address: Optional[str] = None
user_agent: Optional[str] = None
class DemoSessionResponse(BaseModel):
"""Demo session response"""
session_id: str
virtual_tenant_id: str
demo_account_type: str
status: str
created_at: datetime
expires_at: datetime
demo_config: Dict[str, Any]
session_token: str
class Config:
from_attributes = True
class DemoSessionExtend(BaseModel):
"""Extend session request"""
session_id: str
class DemoSessionDestroy(BaseModel):
"""Destroy session request"""
session_id: str
class DemoSessionStats(BaseModel):
"""Demo session statistics"""
total_sessions: int
active_sessions: int
expired_sessions: int
destroyed_sessions: int
avg_duration_minutes: float
total_requests: int
class DemoAccountInfo(BaseModel):
"""Public demo account information"""
account_type: str
name: str
email: str
password: str
description: str
features: list[str]
business_model: str
class CloneDataRequest(BaseModel):
"""Request to clone tenant data"""
base_tenant_id: str
virtual_tenant_id: str
session_id: str
class CloneDataResponse(BaseModel):
"""Response from data cloning"""
session_id: str
services_cloned: list[str]
total_records: int
redis_keys: int