Add subcription feature 3
This commit is contained in:
@@ -51,7 +51,8 @@ class HeaderManager:
|
||||
'accept',
|
||||
'accept-language',
|
||||
'user-agent',
|
||||
'x-internal-service' # Required for internal service-to-service ML/alert triggers
|
||||
'x-internal-service', # Required for internal service-to-service ML/alert triggers
|
||||
'stripe-signature' # Required for Stripe webhook signature verification
|
||||
]
|
||||
|
||||
def __init__(self):
|
||||
|
||||
@@ -25,7 +25,7 @@ from app.middleware.rate_limiting import APIRateLimitMiddleware
|
||||
from app.middleware.subscription import SubscriptionMiddleware
|
||||
from app.middleware.demo_middleware import DemoMiddleware
|
||||
from app.middleware.read_only_mode import ReadOnlyModeMiddleware
|
||||
from app.routes import auth, tenant, nominatim, subscription, demo, pos, geocoding, poi_context, webhooks
|
||||
from app.routes import auth, tenant, registration, nominatim, subscription, demo, pos, geocoding, poi_context, webhooks
|
||||
|
||||
# Initialize logger
|
||||
logger = structlog.get_logger()
|
||||
@@ -40,36 +40,61 @@ try:
|
||||
except Exception as e:
|
||||
logger.debug(f"Could not check file descriptor limits: {e}")
|
||||
|
||||
# Redis client for SSE streaming
|
||||
# Global Redis client for SSE streaming
|
||||
redis_client = None
|
||||
|
||||
|
||||
class GatewayService(StandardFastAPIService):
|
||||
"""Gateway Service with standardized monitoring setup"""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
# Initialize HeaderManager early
|
||||
header_manager.initialize()
|
||||
logger.info("HeaderManager initialized")
|
||||
|
||||
# Initialize Redis during service creation so it's available when needed
|
||||
try:
|
||||
# We need to run the async initialization in a sync context
|
||||
import asyncio
|
||||
try:
|
||||
# Check if there's already a running event loop
|
||||
loop = asyncio.get_running_loop()
|
||||
# If there is, we'll initialize Redis later in on_startup
|
||||
self.redis_initialized = False
|
||||
self.redis_client = None
|
||||
except RuntimeError:
|
||||
# No event loop running, safe to run the async function
|
||||
import asyncio
|
||||
import nest_asyncio
|
||||
nest_asyncio.apply() # Allow nested event loops
|
||||
|
||||
async def init_redis():
|
||||
await initialize_redis(settings.REDIS_URL, db=0, max_connections=50)
|
||||
return await get_redis_client()
|
||||
|
||||
self.redis_client = asyncio.run(init_redis())
|
||||
self.redis_initialized = True
|
||||
logger.info("Connected to Redis for SSE streaming")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to initialize Redis during service creation: {e}")
|
||||
self.redis_initialized = False
|
||||
self.redis_client = None
|
||||
|
||||
async def on_startup(self, app):
|
||||
"""Custom startup logic for Gateway"""
|
||||
global redis_client
|
||||
|
||||
# Initialize HeaderManager
|
||||
header_manager.initialize()
|
||||
logger.info("HeaderManager initialized")
|
||||
|
||||
# Initialize Redis
|
||||
try:
|
||||
await initialize_redis(settings.REDIS_URL, db=0, max_connections=50)
|
||||
redis_client = await get_redis_client()
|
||||
logger.info("Connected to Redis for SSE streaming")
|
||||
|
||||
# Add API rate limiting middleware with Redis client
|
||||
app.add_middleware(APIRateLimitMiddleware, redis_client=redis_client)
|
||||
logger.info("API rate limiting middleware enabled")
|
||||
|
||||
# NOTE: SubscriptionMiddleware and AuthMiddleware are instantiated without redis_client
|
||||
# They will gracefully degrade (skip Redis-dependent features) when redis_client is None
|
||||
# For future enhancement: consider using lifespan context to inject redis_client into middleware
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to connect to Redis: {e}")
|
||||
# Initialize Redis if not already done during service creation
|
||||
if not self.redis_initialized:
|
||||
try:
|
||||
await initialize_redis(settings.REDIS_URL, db=0, max_connections=50)
|
||||
self.redis_client = await get_redis_client()
|
||||
redis_client = self.redis_client # Update global variable
|
||||
self.redis_initialized = True
|
||||
logger.info("Connected to Redis for SSE streaming")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to connect to Redis during startup: {e}")
|
||||
|
||||
# Register custom metrics for gateway-specific operations
|
||||
if self.telemetry_providers and self.telemetry_providers.app_metrics:
|
||||
@@ -103,6 +128,23 @@ service = GatewayService(
|
||||
# Create FastAPI app
|
||||
app = service.create_app()
|
||||
|
||||
# Add API rate limiting middleware with Redis client - this needs to be done after app creation
|
||||
# but before other middleware that might depend on it
|
||||
# Wait for Redis to be initialized if not already done
|
||||
if not hasattr(service, 'redis_client') or not service.redis_client:
|
||||
# Wait briefly for Redis initialization to complete
|
||||
import time
|
||||
time.sleep(1)
|
||||
# Check again after allowing time for initialization
|
||||
if hasattr(service, 'redis_client') and service.redis_client:
|
||||
app.add_middleware(APIRateLimitMiddleware, redis_client=service.redis_client)
|
||||
logger.info("API rate limiting middleware enabled")
|
||||
else:
|
||||
logger.warning("Redis client not available for API rate limiting middleware")
|
||||
else:
|
||||
app.add_middleware(APIRateLimitMiddleware, redis_client=service.redis_client)
|
||||
logger.info("API rate limiting middleware enabled")
|
||||
|
||||
# Add gateway-specific middleware (in REVERSE order of execution)
|
||||
# Execution order: RequestIDMiddleware -> DemoMiddleware -> AuthMiddleware -> ReadOnlyModeMiddleware -> SubscriptionMiddleware -> APIRateLimitMiddleware -> RateLimitMiddleware -> LoggingMiddleware
|
||||
app.add_middleware(LoggingMiddleware)
|
||||
@@ -115,6 +157,7 @@ app.add_middleware(RequestIDMiddleware)
|
||||
|
||||
# Include routers
|
||||
app.include_router(auth.router, prefix="/api/v1/auth", tags=["authentication"])
|
||||
app.include_router(registration.router, prefix="/api/v1", tags=["registration"])
|
||||
app.include_router(tenant.router, prefix="/api/v1/tenants", tags=["tenants"])
|
||||
app.include_router(subscription.router, prefix="/api/v1", tags=["subscriptions"])
|
||||
# Notification routes are now handled by tenant router at /api/v1/tenants/{tenant_id}/notifications/*
|
||||
@@ -122,9 +165,9 @@ app.include_router(nominatim.router, prefix="/api/v1/nominatim", tags=["location
|
||||
app.include_router(geocoding.router, prefix="/api/v1/geocoding", tags=["geocoding"])
|
||||
app.include_router(pos.router, prefix="/api/v1/pos", tags=["pos"])
|
||||
app.include_router(demo.router, prefix="/api/v1", tags=["demo"])
|
||||
app.include_router(webhooks.router, prefix="/api/v1", tags=["webhooks"])
|
||||
# Also include webhooks at /webhooks prefix to support direct webhook URLs like /webhooks/stripe
|
||||
app.include_router(webhooks.router, prefix="/webhooks", tags=["webhooks-external"])
|
||||
# Include webhooks at the root level to handle /api/v1/webhooks/*
|
||||
# Webhook routes are defined with full /api/v1/webhooks/* paths for consistency
|
||||
app.include_router(webhooks.router, prefix="", tags=["webhooks"])
|
||||
|
||||
|
||||
# ================================================================
|
||||
@@ -257,7 +300,7 @@ def _determine_event_type(event_data: dict) -> str:
|
||||
# SERVER-SENT EVENTS (SSE) ENDPOINT
|
||||
# ================================================================
|
||||
|
||||
@app.get("/api/events")
|
||||
@app.get("/api/v1/events")
|
||||
async def events_stream(
|
||||
request: Request,
|
||||
tenant_id: str,
|
||||
|
||||
@@ -34,10 +34,15 @@ PUBLIC_ROUTES = [
|
||||
"/api/v1/auth/register",
|
||||
"/api/v1/auth/refresh",
|
||||
"/api/v1/auth/verify",
|
||||
"/api/v1/auth/start-registration", # Registration step 1 - SetupIntent creation
|
||||
"/api/v1/auth/complete-registration", # Registration step 2 - Completion after 3DS
|
||||
"/api/v1/auth/verify-email", # Email verification
|
||||
"/api/v1/nominatim/search",
|
||||
"/api/v1/plans",
|
||||
"/api/v1/demo/accounts",
|
||||
"/api/v1/demo/sessions"
|
||||
"/api/v1/demo/sessions",
|
||||
"/api/v1/webhooks/stripe", # Stripe webhook endpoint - bypasses auth for signature verification
|
||||
"/api/v1/webhooks/generic" # Generic webhook endpoint
|
||||
]
|
||||
|
||||
# Routes accessible with demo session (no JWT required, just demo session header)
|
||||
@@ -74,7 +79,7 @@ class AuthMiddleware(BaseHTTPMiddleware):
|
||||
logger.info(f"Auth check - path: {request.url.path}, demo_header: {demo_session_header}, demo_query: {demo_session_query}, has_demo_state: {hasattr(request.state, 'is_demo_session')}")
|
||||
|
||||
# For SSE endpoint with demo_session_id in query params, validate it here
|
||||
if request.url.path == "/api/events" and demo_session_query and not hasattr(request.state, "is_demo_session"):
|
||||
if request.url.path == "/api/v1/events" and demo_session_query and not hasattr(request.state, "is_demo_session"):
|
||||
logger.info(f"SSE endpoint with demo_session_id query param: {demo_session_query}")
|
||||
# Validate demo session via demo-session service using JWT service token
|
||||
import httpx
|
||||
@@ -240,14 +245,14 @@ class AuthMiddleware(BaseHTTPMiddleware):
|
||||
"""
|
||||
Extract JWT token from Authorization header or query params for SSE.
|
||||
|
||||
For SSE endpoints (/api/events), browsers' EventSource API cannot send
|
||||
For SSE endpoints (/api/v1/events), browsers' EventSource API cannot send
|
||||
custom headers, so we must accept token as query parameter.
|
||||
For all other routes, token must be in Authorization header (more secure).
|
||||
|
||||
Security note: Query param tokens are logged. Use short expiry and filter logs.
|
||||
"""
|
||||
# SSE endpoint exception: token in query param (EventSource API limitation)
|
||||
if request.url.path == "/api/events":
|
||||
if request.url.path == "/api/v1/events":
|
||||
token = request.query_params.get("token")
|
||||
if token:
|
||||
logger.debug("Token extracted from query param for SSE endpoint")
|
||||
|
||||
@@ -50,6 +50,7 @@ DEMO_ALLOWED_OPERATIONS = {
|
||||
"/api/v1/tenants/batch/sales-summary",
|
||||
"/api/v1/tenants/batch/production-summary",
|
||||
"/api/v1/auth/me/onboarding/complete", # Allow completing onboarding (no-op for demos)
|
||||
"/api/v1/tenants/*/notifications/send", # Allow notifications (ML insights, alerts, etc.)
|
||||
# Note: Forecast generation is explicitly blocked (see DEMO_BLOCKED_PATHS)
|
||||
],
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ READ_ONLY_WHITELIST_PATTERNS = [
|
||||
r'^/api/v1/tenants/.*/procurement/ml/insights/.*', # Allow ML insights (supplier analysis, price forecasting)
|
||||
r'^/api/v1/tenants/.*/forecasting/ml/insights/.*', # Allow ML insights (rules generation)
|
||||
r'^/api/v1/tenants/.*/forecasting/operations/.*', # Allow forecasting operations
|
||||
r'^/api/v1/webhooks/.*', # Webhook endpoints - no tenant context
|
||||
]
|
||||
|
||||
|
||||
@@ -55,7 +56,7 @@ class ReadOnlyModeMiddleware(BaseHTTPMiddleware):
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=5.0) as client:
|
||||
response = await client.get(
|
||||
f"{self.tenant_service_url}/api/v1/subscriptions/{tenant_id}/status",
|
||||
f"{self.tenant_service_url}/api/v1/tenants/{tenant_id}/subscriptions/status",
|
||||
headers={"Authorization": authorization}
|
||||
)
|
||||
|
||||
|
||||
@@ -178,6 +178,7 @@ class SubscriptionMiddleware(BaseHTTPMiddleware):
|
||||
r'/api/v1/auth/.*',
|
||||
r'/api/v1/subscriptions/.*', # Subscription management itself
|
||||
r'/api/v1/tenants/[^/]+/members.*', # Basic tenant info
|
||||
r'/api/v1/webhooks/.*', # Webhook endpoints - no tenant context
|
||||
r'/docs.*',
|
||||
r'/openapi\.json',
|
||||
# Training monitoring endpoints (WebSocket and status checks)
|
||||
|
||||
116
gateway/app/routes/registration.py
Normal file
116
gateway/app/routes/registration.py
Normal file
@@ -0,0 +1,116 @@
|
||||
"""
|
||||
Registration routes for API Gateway - Handles registration-specific endpoints
|
||||
These endpoints don't require a tenant ID and are used during the registration flow
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Request, Response, HTTPException
|
||||
from fastapi.responses import JSONResponse
|
||||
import httpx
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from app.core.config import settings
|
||||
from app.core.header_manager import header_manager
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter()
|
||||
|
||||
# ================================================================
|
||||
# REGISTRATION ENDPOINTS - Direct routing to tenant service
|
||||
# These endpoints are called during registration before a tenant exists
|
||||
# ================================================================
|
||||
|
||||
@router.post("/registration-payment-setup")
|
||||
async def proxy_registration_payment_setup(request: Request):
|
||||
"""Proxy registration payment setup request to tenant service"""
|
||||
return await _proxy_to_tenant_service(request, "/api/v1/tenants/registration-payment-setup")
|
||||
|
||||
@router.post("/verify-and-complete-registration")
|
||||
async def proxy_verify_and_complete_registration(request: Request):
|
||||
"""Proxy verification and registration completion to tenant service"""
|
||||
return await _proxy_to_tenant_service(request, "/api/v1/tenants/verify-and-complete-registration")
|
||||
|
||||
@router.post("/payment-customers/create")
|
||||
async def proxy_registration_customer_create(request: Request):
|
||||
"""Proxy registration customer creation to tenant service"""
|
||||
return await _proxy_to_tenant_service(request, "/api/v1/payment-customers/create")
|
||||
|
||||
@router.get("/setup-intents/{setup_intent_id}/verify")
|
||||
async def proxy_registration_setup_intent_verify(request: Request, setup_intent_id: str):
|
||||
"""Proxy registration setup intent verification to tenant service"""
|
||||
return await _proxy_to_tenant_service(request, f"/api/v1/tenants/setup-intents/{setup_intent_id}/verify")
|
||||
|
||||
|
||||
# ================================================================
|
||||
# PROXY HELPER FUNCTIONS
|
||||
# ================================================================
|
||||
|
||||
async def _proxy_to_tenant_service(request: Request, target_path: 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, Stripe-Signature",
|
||||
"Access-Control-Allow-Credentials": "true",
|
||||
"Access-Control-Max-Age": "86400"
|
||||
}
|
||||
)
|
||||
|
||||
try:
|
||||
url = f"{settings.TENANT_SERVICE_URL}{target_path}"
|
||||
|
||||
# Use unified HeaderManager for consistent header forwarding
|
||||
headers = header_manager.get_all_headers_for_proxy(request)
|
||||
|
||||
# Debug logging
|
||||
logger.info(f"Forwarding registration request to {url}")
|
||||
|
||||
# Get request body if present
|
||||
body = None
|
||||
if request.method in ["POST", "PUT", "PATCH"]:
|
||||
body = await request.body()
|
||||
|
||||
# Add query parameters
|
||||
params = dict(request.query_params)
|
||||
|
||||
timeout_config = httpx.Timeout(
|
||||
connect=30.0,
|
||||
read=60.0,
|
||||
write=30.0,
|
||||
pool=30.0
|
||||
)
|
||||
|
||||
async with httpx.AsyncClient(timeout=timeout_config) as client:
|
||||
response = await client.request(
|
||||
method=request.method,
|
||||
url=url,
|
||||
headers=headers,
|
||||
content=body,
|
||||
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=content
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error proxying registration request to {settings.TENANT_SERVICE_URL}{target_path}: {e}")
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail="Internal gateway error"
|
||||
)
|
||||
@@ -24,6 +24,8 @@ async def create_tenant(request: Request):
|
||||
"""Proxy tenant creation to tenant service"""
|
||||
return await _proxy_to_tenant_service(request, "/api/v1/tenants/register")
|
||||
|
||||
|
||||
|
||||
@router.get("/{tenant_id}")
|
||||
async def get_tenant(request: Request, tenant_id: str = Path(...)):
|
||||
"""Get specific tenant details"""
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
"""
|
||||
Webhook routes for API Gateway - Handles webhook endpoints
|
||||
|
||||
Route Configuration Notes:
|
||||
- Stripe configures webhook URL as: https://domain.com/api/v1/webhooks/stripe
|
||||
- Gateway receives /api/v1/webhooks/* routes and proxies to tenant service at /webhooks/*
|
||||
- Gateway routes use /api/v1 prefix, but tenant service routes use /webhooks/* prefix
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Request, Response
|
||||
from fastapi import APIRouter, Request, Response, HTTPException
|
||||
from fastapi.responses import JSONResponse
|
||||
import httpx
|
||||
import logging
|
||||
@@ -16,16 +21,20 @@ router = APIRouter()
|
||||
|
||||
# ================================================================
|
||||
# WEBHOOK ENDPOINTS - Direct routing to tenant service
|
||||
# All routes use /api/v1 prefix for consistency
|
||||
# ================================================================
|
||||
|
||||
@router.post("/stripe")
|
||||
# Stripe webhook endpoint
|
||||
@router.post("/api/v1/webhooks/stripe")
|
||||
async def proxy_stripe_webhook(request: Request):
|
||||
"""Proxy Stripe webhook requests to tenant service"""
|
||||
"""Proxy Stripe webhook requests to tenant service (path: /webhooks/stripe)"""
|
||||
logger.info("Received Stripe webhook at /api/v1/webhooks/stripe")
|
||||
return await _proxy_to_tenant_service(request, "/webhooks/stripe")
|
||||
|
||||
@router.post("/generic")
|
||||
# Generic webhook endpoint
|
||||
@router.post("/api/v1/webhooks/generic")
|
||||
async def proxy_generic_webhook(request: Request):
|
||||
"""Proxy generic webhook requests to tenant service"""
|
||||
"""Proxy generic webhook requests to tenant service (path: /webhooks/generic)"""
|
||||
return await _proxy_to_tenant_service(request, "/webhooks/generic")
|
||||
|
||||
# ================================================================
|
||||
|
||||
Reference in New Issue
Block a user