REFACTOR API gateway

This commit is contained in:
Urtzi Alfaro
2025-07-26 18:46:52 +02:00
parent e49893e10a
commit e4885db828
24 changed files with 1049 additions and 1080 deletions

View File

@@ -1,156 +1,200 @@
# gateway/app/routes/tenant.py - COMPLETELY UPDATED
"""
Tenant routes for gateway - FIXED VERSION
Tenant routes for API Gateway - Handles all tenant-scoped endpoints
"""
from fastapi import APIRouter, Request, HTTPException
from fastapi import APIRouter, Request, Response, HTTPException, Path
from fastapi.responses import JSONResponse
import httpx
import logging
from typing import Optional
from app.core.config import settings
logger = logging.getLogger(__name__)
router = APIRouter()
# ================================================================
# TENANT MANAGEMENT ENDPOINTS
# ================================================================
@router.post("/register")
async def create_tenant(request: Request):
"""Proxy tenant creation to tenant service"""
try:
body = await request.body()
# ✅ FIX: Forward all headers AND add user context from gateway auth
headers = dict(request.headers)
headers.pop("host", None) # Remove host header
# ✅ ADD USER CONTEXT FROM GATEWAY AUTHENTICATION
# Gateway middleware already verified the token and added user to request.state
if hasattr(request.state, 'user'):
headers["X-User-ID"] = str(request.state.user.get("user_id"))
headers["X-User-Email"] = request.state.user.get("email", "")
headers["X-User-Role"] = request.state.user.get("role", "user")
# Add tenant ID if it exists
if hasattr(request.state, 'tenant_id') and request.state.tenant_id:
headers["X-Tenant-ID"] = str(request.state.tenant_id)
elif request.state.user.get("tenant_id"):
headers["X-Tenant-ID"] = str(request.state.user.get("tenant_id"))
roles = request.state.user.get("roles", [])
if roles:
headers["X-User-Roles"] = ",".join(roles)
permissions = request.state.user.get("permissions", [])
if permissions:
headers["X-User-Permissions"] = ",".join(permissions)
async with httpx.AsyncClient(timeout=10.0) as client:
response = await client.post(
f"{settings.TENANT_SERVICE_URL}/api/v1/tenants/register",
content=body,
headers=headers
)
return JSONResponse(
status_code=response.status_code,
content=response.json()
)
except httpx.RequestError as e:
logger.error(f"Tenant service unavailable: {e}")
raise HTTPException(
status_code=503,
detail="Tenant service unavailable"
)
return await _proxy_to_tenant_service(request, "/api/v1/tenants/register")
@router.get("/")
async def get_tenants(request: Request):
"""Get tenants"""
@router.get("/{tenant_id}")
async def get_tenant(request: Request, tenant_id: str = Path(...)):
"""Get specific tenant details"""
return await _proxy_to_tenant_service(request, f"/api/v1/tenants/{tenant_id}")
@router.put("/{tenant_id}")
async def update_tenant(request: Request, tenant_id: str = Path(...)):
"""Update tenant details"""
return await _proxy_to_tenant_service(request, f"/api/v1/tenants/{tenant_id}")
@router.get("/{tenant_id}/members")
async def get_tenant_members(request: Request, tenant_id: str = Path(...)):
"""Get tenant members"""
return await _proxy_to_tenant_service(request, f"/api/v1/tenants/{tenant_id}/members")
# ================================================================
# TENANT-SCOPED DATA SERVICE ENDPOINTS
# ================================================================
@router.api_route("/{tenant_id}/sales/{path:path}", methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"])
async def proxy_tenant_sales(request: Request, tenant_id: str = Path(...), path: str = ""):
"""Proxy tenant sales requests to data service"""
target_path = f"/api/v1/tenants/{tenant_id}/sales/{path}".rstrip("/")
return await _proxy_to_data_service(request, target_path)
@router.api_route("/{tenant_id}/weather/{path:path}", methods=["GET", "POST", "OPTIONS"])
async def proxy_tenant_weather(request: Request, tenant_id: str = Path(...), path: str = ""):
"""Proxy tenant weather requests to data service"""
target_path = f"/api/v1/tenants/{tenant_id}/weather/{path}".rstrip("/")
return await _proxy_to_data_service(request, target_path)
@router.api_route("/{tenant_id}/analytics/{path:path}", methods=["GET", "POST", "OPTIONS"])
async def proxy_tenant_analytics(request: Request, tenant_id: str = Path(...), path: str = ""):
"""Proxy tenant analytics requests to data service"""
target_path = f"/api/v1/tenants/{tenant_id}/analytics/{path}".rstrip("/")
return await _proxy_to_data_service(request, target_path)
# ================================================================
# TENANT-SCOPED TRAINING SERVICE ENDPOINTS
# ================================================================
@router.api_route("/{tenant_id}/training/{path:path}", methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"])
async def proxy_tenant_training(request: Request, tenant_id: str = Path(...), path: str = ""):
"""Proxy tenant training requests to training service"""
target_path = f"/api/v1/tenants/{tenant_id}/training/{path}".rstrip("/")
return await _proxy_to_training_service(request, target_path)
@router.api_route("/{tenant_id}/models/{path:path}", methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"])
async def proxy_tenant_models(request: Request, tenant_id: str = Path(...), path: str = ""):
"""Proxy tenant model requests to training service"""
target_path = f"/api/v1/tenants/{tenant_id}/models/{path}".rstrip("/")
return await _proxy_to_training_service(request, target_path)
# ================================================================
# TENANT-SCOPED FORECASTING SERVICE ENDPOINTS
# ================================================================
@router.api_route("/{tenant_id}/forecasts/{path:path}", methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"])
async def proxy_tenant_forecasts(request: Request, tenant_id: str = Path(...), path: str = ""):
"""Proxy tenant forecast requests to forecasting service"""
target_path = f"/api/v1/tenants/{tenant_id}/forecasts/{path}".rstrip("/")
return await _proxy_to_forecasting_service(request, target_path)
@router.api_route("/{tenant_id}/predictions/{path:path}", methods=["GET", "POST", "OPTIONS"])
async def proxy_tenant_predictions(request: Request, tenant_id: str = Path(...), path: str = ""):
"""Proxy tenant prediction requests to forecasting service"""
target_path = f"/api/v1/tenants/{tenant_id}/predictions/{path}".rstrip("/")
return await _proxy_to_forecasting_service(request, target_path)
# ================================================================
# TENANT-SCOPED NOTIFICATION SERVICE ENDPOINTS
# ================================================================
@router.api_route("/{tenant_id}/notifications/{path:path}", methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"])
async def proxy_tenant_notifications(request: Request, tenant_id: str = Path(...), path: str = ""):
"""Proxy tenant notification requests to notification service"""
target_path = f"/api/v1/tenants/{tenant_id}/notifications/{path}".rstrip("/")
return await _proxy_to_notification_service(request, target_path)
# ================================================================
# PROXY HELPER FUNCTIONS
# ================================================================
async def _proxy_to_tenant_service(request: Request, target_path: str):
"""Proxy request to tenant service"""
return await _proxy_request(request, target_path, settings.TENANT_SERVICE_URL)
async def _proxy_to_data_service(request: Request, target_path: str):
"""Proxy request to data service"""
return await _proxy_request(request, target_path, settings.DATA_SERVICE_URL)
async def _proxy_to_training_service(request: Request, target_path: str):
"""Proxy request to training service"""
return await _proxy_request(request, target_path, settings.TRAINING_SERVICE_URL)
async def _proxy_to_forecasting_service(request: Request, target_path: str):
"""Proxy request to forecasting service"""
return await _proxy_request(request, target_path, settings.FORECASTING_SERVICE_URL)
async def _proxy_to_notification_service(request: Request, target_path: str):
"""Proxy request to notification service"""
return await _proxy_request(request, target_path, settings.NOTIFICATION_SERVICE_URL)
async def _proxy_request(request: Request, target_path: str, service_url: str):
"""Generic proxy function with enhanced error handling"""
# Handle OPTIONS requests directly for CORS
if request.method == "OPTIONS":
return Response(
status_code=200,
headers={
"Access-Control-Allow-Origin": settings.CORS_ORIGINS_LIST,
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type, Authorization, X-Tenant-ID",
"Access-Control-Allow-Credentials": "true",
"Access-Control-Max-Age": "86400"
}
)
try:
# ✅ FIX: Same pattern for GET requests
url = f"{service_url}{target_path}"
# Forward headers and add user/tenant context
headers = dict(request.headers)
headers.pop("host", None)
# Add user context from gateway auth
if hasattr(request.state, 'user'):
headers["X-User-ID"] = str(request.state.user.get("user_id"))
headers["X-User-Email"] = request.state.user.get("email", "")
headers["X-User-Role"] = request.state.user.get("role", "user")
if hasattr(request.state, 'tenant_id') and request.state.tenant_id:
headers["X-Tenant-ID"] = str(request.state.tenant_id)
elif request.state.user.get("tenant_id"):
headers["X-Tenant-ID"] = str(request.state.user.get("tenant_id"))
roles = request.state.user.get("roles", [])
if roles:
headers["X-User-Roles"] = ",".join(roles)
async with httpx.AsyncClient(timeout=10.0) as client:
response = await client.get(
f"{settings.TENANT_SERVICE_URL}/api/v1/tenants",
headers=headers
)
return JSONResponse(
status_code=response.status_code,
content=response.json()
)
except httpx.RequestError as e:
logger.error(f"Tenant service unavailable: {e}")
raise HTTPException(
status_code=503,
detail="Tenant service unavailable"
)
# ✅ ADD: Generic proxy function like the data service has
async def _proxy_tenant_request(request: Request, target_path: str, method: str = None):
"""Proxy request to tenant service with user context"""
try:
url = f"{settings.TENANT_SERVICE_URL}{target_path}"
# Forward headers with user context
headers = dict(request.headers)
headers.pop("host", None)
# Add user context from gateway authentication
if hasattr(request.state, 'user'):
headers["X-User-ID"] = str(request.state.user.get("user_id"))
headers["X-User-Email"] = request.state.user.get("email", "")
headers["X-User-Role"] = request.state.user.get("role", "user")
if hasattr(request.state, 'tenant_id') and request.state.tenant_id:
headers["X-Tenant-ID"] = str(request.state.tenant_id)
elif request.state.user.get("tenant_id"):
headers["X-Tenant-ID"] = str(request.state.user.get("tenant_id"))
roles = request.state.user.get("roles", [])
if roles:
headers["X-User-Roles"] = ",".join(roles)
# Get request body if present
body = None
request_method = method or request.method
if request_method in ["POST", "PUT", "PATCH"]:
if request.method in ["POST", "PUT", "PATCH"]:
body = await request.body()
# Add query parameters
params = dict(request.query_params)
async with httpx.AsyncClient(timeout=30.0) as client:
response = await client.request(
method=request_method,
method=request.method,
url=url,
headers=headers,
content=body,
params=dict(request.query_params)
params=params
)
# Handle different response types
if response.headers.get("content-type", "").startswith("application/json"):
try:
content = response.json()
except:
content = {"message": "Invalid JSON response from service"}
else:
content = response.text
return JSONResponse(
status_code=response.status_code,
content=response.json()
content=content
)
except httpx.TimeoutError:
logger.error(f"Timeout calling {service_url}{target_path}")
raise HTTPException(
status_code=504,
detail=f"Service timeout"
)
except httpx.RequestError as e:
logger.error(f"Tenant service unavailable: {e}")
logger.error(f"Request error calling {service_url}{target_path}: {e}")
raise HTTPException(
status_code=503,
detail="Tenant service unavailable"
detail=f"Service unavailable"
)
except Exception as e:
logger.error(f"Unexpected error proxying to {service_url}{target_path}: {e}")
raise HTTPException(
status_code=500,
detail="Internal gateway error"
)