REFACTOR ALL APIs
This commit is contained in:
@@ -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"]
|
||||
|
||||
48
services/demo_session/app/api/demo_accounts.py
Normal file
48
services/demo_session/app/api/demo_accounts.py
Normal 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
|
||||
89
services/demo_session/app/api/demo_operations.py
Normal file
89
services/demo_session/app/api/demo_operations.py
Normal 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
|
||||
131
services/demo_session/app/api/demo_sessions.py
Normal file
131
services/demo_session/app/api/demo_sessions.py
Normal 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))
|
||||
@@ -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"
|
||||
}
|
||||
Reference in New Issue
Block a user