Add new frontend - fix 16

This commit is contained in:
Urtzi Alfaro
2025-07-23 07:26:04 +02:00
parent e6b0be0c95
commit 1d35912459
14 changed files with 588 additions and 169 deletions

View File

@@ -17,7 +17,7 @@ from app.core.service_discovery import ServiceDiscovery
from app.middleware.auth import AuthMiddleware
from app.middleware.logging import LoggingMiddleware
from app.middleware.rate_limit import RateLimitMiddleware
from app.routes import auth, training, forecasting, data, tenant, notification, nominatim
from app.routes import auth, training, forecasting, data, tenant, notification, nominatim, user
from shared.monitoring.logging import setup_logging
from shared.monitoring.metrics import MetricsCollector
@@ -56,6 +56,7 @@ app.add_middleware(AuthMiddleware)
# Include routers
app.include_router(auth.router, prefix="/api/v1/auth", tags=["authentication"])
app.include_router(auth.router, prefix="/api/v1/user", tags=["user"])
app.include_router(training.router, prefix="/api/v1/training", tags=["training"])
app.include_router(forecasting.router, prefix="/api/v1/forecasting", tags=["forecasting"])
app.include_router(data.router, prefix="/api/v1/data", tags=["data"])

View File

@@ -210,20 +210,6 @@ async def change_password(request: Request):
"""Proxy password change to auth service"""
return await auth_proxy.forward_request("POST", "change-password", request)
# ================================================================
# USER MANAGEMENT ENDPOINTS - Proxied to auth service
# ================================================================
@router.get("/users/me")
async def get_current_user(request: Request):
"""Proxy get current user to auth service"""
return await auth_proxy.forward_request("GET", "../users/me", request)
@router.put("/users/me")
async def update_current_user(request: Request):
"""Proxy update current user to auth service"""
return await auth_proxy.forward_request("PUT", "../users/me", request)
# ================================================================
# CATCH-ALL ROUTE for any other auth endpoints
# ================================================================

View File

@@ -1,6 +1,6 @@
"""Data service routes for API Gateway - Authentication handled by gateway middleware"""
from fastapi import APIRouter, Request, HTTPException
from fastapi import APIRouter, Request, Response, HTTPException
from fastapi.responses import StreamingResponse
import httpx
import logging
@@ -10,23 +10,37 @@ from app.core.config import settings
logger = logging.getLogger(__name__)
router = APIRouter()
@router.api_route("/sales/{path:path}", methods=["GET", "POST", "PUT", "DELETE"])
@router.api_route("/sales/{path:path}", methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"])
async def proxy_sales(request: Request, path: str):
"""Proxy sales data requests to data service"""
return await _proxy_request(request, f"/api/v1/sales/{path}")
@router.api_route("/weather/{path:path}", methods=["GET", "POST"])
@router.api_route("/weather/{path:path}", methods=["GET", "POST", "OPTIONS"])
async def proxy_weather(request: Request, path: str):
"""Proxy weather requests to data service"""
return await _proxy_request(request, f"/api/v1/weather/{path}")
@router.api_route("/traffic/{path:path}", methods=["GET", "POST"])
@router.api_route("/traffic/{path:path}", methods=["GET", "POST", "OPTIONS"])
async def proxy_traffic(request: Request, path: str):
"""Proxy traffic requests to data service"""
return await _proxy_request(request, f"/api/v1/traffic/{path}")
async def _proxy_request(request: Request, target_path: str):
"""Proxy request to data service with user context"""
# 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" # Cache preflight for 24 hours
}
)
try:
url = f"{settings.DATA_SERVICE_URL}{target_path}"

View File

@@ -12,18 +12,32 @@ from app.core.config import settings
logger = logging.getLogger(__name__)
router = APIRouter()
@router.api_route("/forecasts/{path:path}", methods=["GET", "POST", "PUT", "DELETE"])
@router.api_route("/forecasts/{path:path}", methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"])
async def proxy_forecasts(request: Request, path: str):
"""Proxy forecast requests to forecasting service"""
return await _proxy_request(request, f"/api/v1/forecasts/{path}")
@router.api_route("/predictions/{path:path}", methods=["GET", "POST"])
@router.api_route("/predictions/{path:path}", methods=["GET", "POST", "OPTIONS"])
async def proxy_predictions(request: Request, path: str):
"""Proxy prediction requests to forecasting service"""
return await _proxy_request(request, f"/api/v1/predictions/{path}")
async def _proxy_request(request: Request, target_path: str):
"""Proxy request to forecasting service with user context"""
# 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" # Cache preflight for 24 hours
}
)
try:
url = f"{settings.FORECASTING_SERVICE_URL}{target_path}"

View File

@@ -14,6 +14,19 @@ async def proxy_nominatim_search(request: Request):
"""
Proxies requests to the Nominatim geocoding search API.
"""
# 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" # Cache preflight for 24 hours
}
)
try:
# Construct the internal Nominatim URL
# All query parameters from the client request are forwarded

194
gateway/app/routes/user.py Normal file
View File

@@ -0,0 +1,194 @@
# ================================================================
# gateway/app/routes/user.py
# ================================================================
"""
Authentication routes for API Gateway
"""
import logging
import httpx
from fastapi import APIRouter, Request, Response, HTTPException, status
from fastapi.responses import JSONResponse
from typing import Dict, Any
import json
from app.core.config import settings
from app.core.service_discovery import ServiceDiscovery
from shared.monitoring.metrics import MetricsCollector
logger = logging.getLogger(__name__)
router = APIRouter()
# Initialize service discovery and metrics
service_discovery = ServiceDiscovery()
metrics = MetricsCollector("gateway")
# Auth service configuration
AUTH_SERVICE_URL = settings.AUTH_SERVICE_URL or "http://auth-service:8000"
class UserProxy:
"""Authentication service proxy with enhanced error handling"""
def __init__(self):
self.client = httpx.AsyncClient(
timeout=httpx.Timeout(30.0),
limits=httpx.Limits(max_connections=100, max_keepalive_connections=20)
)
async def forward_request(
self,
method: str,
path: str,
request: Request
) -> Response:
"""Forward request to auth service with proper 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" # Cache preflight for 24 hours
}
)
try:
# Get auth service URL (with service discovery if available)
auth_url = await self._get_auth_service_url()
target_url = f"{auth_url}/api/v1/user/{path}"
# Prepare headers (remove hop-by-hop headers)
headers = self._prepare_headers(dict(request.headers))
# Get request body
body = await request.body()
# Forward request
logger.info(f"Forwarding {method} {path} to auth service")
response = await self.client.request(
method=method,
url=target_url,
headers=headers,
content=body,
params=dict(request.query_params)
)
# Record metrics
metrics.increment_counter("gateway_auth_requests_total")
metrics.increment_counter(
"gateway_auth_responses_total",
labels={"status_code": str(response.status_code)}
)
# Prepare response headers
response_headers = self._prepare_response_headers(dict(response.headers))
return Response(
content=response.content,
status_code=response.status_code,
headers=response_headers,
media_type=response.headers.get("content-type")
)
except httpx.TimeoutException:
logger.error(f"Timeout forwarding {method} {path} to auth service")
metrics.increment_counter("gateway_auth_errors_total", labels={"error": "timeout"})
raise HTTPException(
status_code=status.HTTP_504_GATEWAY_TIMEOUT,
detail="Authentication service timeout"
)
except httpx.ConnectError:
logger.error(f"Connection error forwarding {method} {path} to auth service")
metrics.increment_counter("gateway_auth_errors_total", labels={"error": "connection"})
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="Authentication service unavailable"
)
except Exception as e:
logger.error(f"Error forwarding {method} {path} to auth service: {e}")
metrics.increment_counter("gateway_auth_errors_total", labels={"error": "unknown"})
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Internal gateway error"
)
async def _get_auth_service_url(self) -> str:
"""Get auth service URL with service discovery"""
try:
# Try service discovery first
service_url = await service_discovery.get_service_url("auth-service")
if service_url:
return service_url
except Exception as e:
logger.warning(f"Service discovery failed: {e}")
# Fall back to configured URL
return AUTH_SERVICE_URL
def _prepare_headers(self, headers: Dict[str, str]) -> Dict[str, str]:
"""Prepare headers for forwarding (remove hop-by-hop headers)"""
# Remove hop-by-hop headers
hop_by_hop_headers = {
'connection', 'keep-alive', 'proxy-authenticate',
'proxy-authorization', 'te', 'trailers', 'upgrade'
}
filtered_headers = {
k: v for k, v in headers.items()
if k.lower() not in hop_by_hop_headers
}
# Add gateway identifier
filtered_headers['X-Forwarded-By'] = 'bakery-gateway'
filtered_headers['X-Gateway-Version'] = '1.0.0'
return filtered_headers
def _prepare_response_headers(self, headers: Dict[str, str]) -> Dict[str, str]:
"""Prepare response headers"""
# Remove server-specific headers
filtered_headers = {
k: v for k, v in headers.items()
if k.lower() not in {'server', 'date'}
}
# Add CORS headers if needed
if settings.CORS_ORIGINS:
filtered_headers['Access-Control-Allow-Origin'] = '*'
filtered_headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE, OPTIONS'
filtered_headers['Access-Control-Allow-Headers'] = 'Content-Type, Authorization'
return filtered_headers
# Initialize proxy
user_proxy = UserProxy()
# ================================================================
# USER MANAGEMENT ENDPOINTS - Proxied to auth service
# ================================================================
@router.get("/me")
async def get_current_user(request: Request):
"""Proxy get current user to auth service"""
return await user_proxy.forward_request("GET", "/me", request)
@router.put("/me")
async def update_current_user(request: Request):
"""Proxy update current user to auth service"""
return await user_proxy.forward_request("PUT", "/me", request)
# ================================================================
# CATCH-ALL ROUTE for any other user endpoints
# ================================================================
@router.api_route("/user/{path:path}", methods=["GET", "POST", "PUT", "DELETE", "PATCH"])
async def proxy_auth_requests(path: str, request: Request):
"""Catch-all proxy for auth requests"""
return await user_proxy.forward_request(request.method, path, request)