REFACTOR ALL APIs

This commit is contained in:
Urtzi Alfaro
2025-10-06 15:27:01 +02:00
parent dc8221bd2f
commit 38fb98bc27
166 changed files with 18454 additions and 13605 deletions

View File

@@ -1,5 +1,7 @@
"""Demo Session API"""
from .routes import router
from .demo_sessions import router as demo_sessions_router
from .demo_accounts import router as demo_accounts_router
from .demo_operations import router as demo_operations_router
__all__ = ["router"]
__all__ = ["demo_sessions_router", "demo_accounts_router", "demo_operations_router"]

View File

@@ -0,0 +1,48 @@
"""
Demo Accounts API - Public demo account information (ATOMIC READ)
"""
from fastapi import APIRouter
from typing import List
import structlog
from app.api.schemas import DemoAccountInfo
from app.core import settings
from shared.routing import RouteBuilder
router = APIRouter(tags=["demo-accounts"])
logger = structlog.get_logger()
route_builder = RouteBuilder('demo')
@router.get(
route_builder.build_base_route("accounts", include_tenant_prefix=False),
response_model=List[DemoAccountInfo]
)
async def get_demo_accounts():
"""Get public demo account information (ATOMIC READ)"""
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

View File

@@ -0,0 +1,89 @@
"""
Demo Operations API - Business operations for demo session management
"""
from fastapi import APIRouter, Depends, HTTPException, Path
import structlog
import jwt
from app.api.schemas import DemoSessionResponse, DemoSessionStats
from app.services import DemoSessionManager, DemoCleanupService
from app.core import get_db, get_redis, RedisClient
from sqlalchemy.ext.asyncio import AsyncSession
from shared.routing import RouteBuilder
router = APIRouter(tags=["demo-operations"])
logger = structlog.get_logger()
route_builder = RouteBuilder('demo')
@router.post(
route_builder.build_resource_action_route("sessions", "session_id", "extend", include_tenant_prefix=False),
response_model=DemoSessionResponse
)
async def extend_demo_session(
session_id: str = Path(...),
db: AsyncSession = Depends(get_db),
redis: RedisClient = Depends(get_redis)
):
"""Extend demo session expiration (BUSINESS OPERATION)"""
try:
session_manager = DemoSessionManager(db, redis)
session = await session_manager.extend_session(session_id)
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.get(
route_builder.build_base_route("stats", include_tenant_prefix=False),
response_model=DemoSessionStats
)
async def get_demo_stats(
db: AsyncSession = Depends(get_db),
redis: RedisClient = Depends(get_redis)
):
"""Get demo session statistics (BUSINESS OPERATION)"""
session_manager = DemoSessionManager(db, redis)
stats = await session_manager.get_session_stats()
return stats
@router.post(
route_builder.build_operations_route("cleanup", include_tenant_prefix=False),
response_model=dict
)
async def run_cleanup(
db: AsyncSession = Depends(get_db),
redis: RedisClient = Depends(get_redis)
):
"""Manually trigger session cleanup (BUSINESS OPERATION - Internal endpoint for CronJob)"""
cleanup_service = DemoCleanupService(db, redis)
stats = await cleanup_service.cleanup_expired_sessions()
return stats

View File

@@ -0,0 +1,131 @@
"""
Demo Sessions API - Atomic CRUD operations on DemoSession model
"""
from fastapi import APIRouter, Depends, HTTPException, Path, Query, Request
from typing import Optional
from uuid import UUID
import structlog
import jwt
from app.api.schemas import DemoSessionCreate, DemoSessionResponse
from app.services import DemoSessionManager
from app.core import get_db, get_redis, RedisClient
from sqlalchemy.ext.asyncio import AsyncSession
from shared.routing import RouteBuilder
router = APIRouter(tags=["demo-sessions"])
logger = structlog.get_logger()
route_builder = RouteBuilder('demo')
@router.post(
route_builder.build_base_route("sessions", include_tenant_prefix=False),
response_model=DemoSessionResponse,
status_code=201
)
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 (ATOMIC)"""
logger.info("Creating demo session", demo_account_type=request.demo_account_type)
try:
ip_address = request.ip_address or http_request.client.host
user_agent = request.user_agent or http_request.headers.get("user-agent", "")
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
)
# Trigger async data cloning job
from app.services.k8s_job_cloner import K8sJobCloner
import asyncio
job_cloner = K8sJobCloner()
asyncio.create_task(
job_cloner.clone_tenant_data(
session.session_id,
"",
str(session.virtual_tenant_id),
request.demo_account_type
)
)
await session_manager.mark_data_cloned(session.session_id)
await session_manager.mark_redis_populated(session.session_id)
# Generate session token
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",
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.get(
route_builder.build_resource_detail_route("sessions", "session_id", include_tenant_prefix=False),
response_model=dict
)
async def get_session_info(
session_id: str = Path(...),
db: AsyncSession = Depends(get_db),
redis: RedisClient = Depends(get_redis)
):
"""Get demo session information (ATOMIC READ)"""
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.delete(
route_builder.build_resource_detail_route("sessions", "session_id", include_tenant_prefix=False),
response_model=dict
)
async def destroy_demo_session(
session_id: str = Path(...),
db: AsyncSession = Depends(get_db),
redis: RedisClient = Depends(get_redis)
):
"""Destroy demo session and cleanup resources (ATOMIC DELETE)"""
try:
session_manager = DemoSessionManager(db, redis)
await session_manager.destroy_session(session_id)
return {"message": "Session destroyed successfully", "session_id": session_id}
except Exception as e:
logger.error("Failed to destroy session", error=str(e))
raise HTTPException(status_code=500, detail=str(e))

View File

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

@@ -10,7 +10,7 @@ import structlog
from contextlib import asynccontextmanager
from app.core import settings, DatabaseManager, RedisClient
from app.api import router
from app.api import demo_sessions, demo_accounts, demo_operations
logger = structlog.get_logger()
@@ -74,7 +74,9 @@ async def global_exception_handler(request: Request, exc: Exception):
# Include routers
app.include_router(router)
app.include_router(demo_sessions.router)
app.include_router(demo_accounts.router)
app.include_router(demo_operations.router)
@app.get("/")