from fastapi import FastAPI, HTTPException, Request, Depends from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse import httpx import time from typing import Dict from shared.config import settings import asyncio app = FastAPI(title="API Gateway", version="1.0.0") # CORS middleware app.add_middleware( CORSMiddleware, allow_origins=settings.CORS_ORIGINS, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # Service registry SERVICES = { "users": "http://localhost:8001", "emergency": "http://localhost:8002", "location": "http://localhost:8003", "calendar": "http://localhost:8004", "notifications": "http://localhost:8005" } # Rate limiting (simple in-memory implementation) request_counts: Dict[str, Dict[str, int]] = {} RATE_LIMIT_REQUESTS = 100 # requests per minute RATE_LIMIT_WINDOW = 60 # seconds def get_client_ip(request: Request) -> str: """Get client IP address""" x_forwarded_for = request.headers.get("X-Forwarded-For") if x_forwarded_for: return x_forwarded_for.split(",")[0].strip() return request.client.host def is_rate_limited(client_ip: str) -> bool: """Check if client is rate limited""" current_time = int(time.time()) window_start = current_time - RATE_LIMIT_WINDOW if client_ip not in request_counts: request_counts[client_ip] = {} # Clean old entries request_counts[client_ip] = { timestamp: count for timestamp, count in request_counts[client_ip].items() if int(timestamp) > window_start } # Count requests in current window total_requests = sum(request_counts[client_ip].values()) if total_requests >= RATE_LIMIT_REQUESTS: return True # Add current request timestamp_key = str(current_time) request_counts[client_ip][timestamp_key] = request_counts[client_ip].get(timestamp_key, 0) + 1 return False async def proxy_request(service_url: str, path: str, method: str, headers: dict, body: bytes = None, params: dict = None): """Proxy request to microservice""" url = f"{service_url}{path}" # Remove hop-by-hop headers filtered_headers = { k: v for k, v in headers.items() if k.lower() not in ["host", "connection", "upgrade", "proxy-connection", "proxy-authenticate", "proxy-authorization", "te", "trailers", "transfer-encoding"] } async with httpx.AsyncClient(timeout=30.0) as client: try: response = await client.request( method=method, url=url, headers=filtered_headers, content=body, params=params ) return response except httpx.TimeoutException: raise HTTPException(status_code=504, detail="Service timeout") except httpx.ConnectError: raise HTTPException(status_code=503, detail="Service unavailable") except Exception as e: raise HTTPException(status_code=500, detail=f"Proxy error: {str(e)}") @app.middleware("http") async def rate_limiting_middleware(request: Request, call_next): """Rate limiting middleware""" client_ip = get_client_ip(request) # Skip rate limiting for health checks if request.url.path.endswith("/health"): return await call_next(request) if is_rate_limited(client_ip): return JSONResponse( status_code=429, content={"detail": "Rate limit exceeded"} ) return await call_next(request) # User Service routes @app.api_route("/api/v1/register", methods=["POST"]) @app.api_route("/api/v1/login", methods=["POST"]) @app.api_route("/api/v1/profile", methods=["GET", "PUT"]) async def user_service_proxy(request: Request): """Proxy requests to User Service""" body = await request.body() response = await proxy_request( SERVICES["users"], request.url.path, request.method, dict(request.headers), body, dict(request.query_params) ) return JSONResponse( status_code=response.status_code, content=response.json(), headers={k: v for k, v in response.headers.items() if k.lower() not in ["content-length", "transfer-encoding"]} ) # Emergency Service routes @app.api_route("/api/v1/alert", methods=["POST"]) @app.api_route("/api/v1/alert/{alert_id}/respond", methods=["POST"]) @app.api_route("/api/v1/alert/{alert_id}/resolve", methods=["PUT"]) @app.api_route("/api/v1/alerts/my", methods=["GET"]) @app.api_route("/api/v1/alerts/active", methods=["GET"]) async def emergency_service_proxy(request: Request): """Proxy requests to Emergency Service""" body = await request.body() response = await proxy_request( SERVICES["emergency"], request.url.path, request.method, dict(request.headers), body, dict(request.query_params) ) return JSONResponse( status_code=response.status_code, content=response.json(), headers={k: v for k, v in response.headers.items() if k.lower() not in ["content-length", "transfer-encoding"]} ) # Location Service routes @app.api_route("/api/v1/update-location", methods=["POST"]) @app.api_route("/api/v1/user-location/{user_id}", methods=["GET"]) @app.api_route("/api/v1/nearby-users", methods=["GET"]) @app.api_route("/api/v1/location-history", methods=["GET"]) @app.api_route("/api/v1/location", methods=["DELETE"]) async def location_service_proxy(request: Request): """Proxy requests to Location Service""" body = await request.body() response = await proxy_request( SERVICES["location"], request.url.path, request.method, dict(request.headers), body, dict(request.query_params) ) return JSONResponse( status_code=response.status_code, content=response.json(), headers={k: v for k, v in response.headers.items() if k.lower() not in ["content-length", "transfer-encoding"]} ) # Calendar Service routes @app.api_route("/api/v1/entries", methods=["GET", "POST"]) @app.api_route("/api/v1/entries/{entry_id}", methods=["DELETE"]) @app.api_route("/api/v1/cycle-overview", methods=["GET"]) @app.api_route("/api/v1/insights", methods=["GET"]) async def calendar_service_proxy(request: Request): """Proxy requests to Calendar Service""" body = await request.body() response = await proxy_request( SERVICES["calendar"], request.url.path, request.method, dict(request.headers), body, dict(request.query_params) ) return JSONResponse( status_code=response.status_code, content=response.json(), headers={k: v for k, v in response.headers.items() if k.lower() not in ["content-length", "transfer-encoding"]} ) # Notification Service routes @app.api_route("/api/v1/register-device", methods=["POST"]) @app.api_route("/api/v1/send-notification", methods=["POST"]) @app.api_route("/api/v1/device-token", methods=["DELETE"]) @app.api_route("/api/v1/my-devices", methods=["GET"]) async def notification_service_proxy(request: Request): """Proxy requests to Notification Service""" body = await request.body() response = await proxy_request( SERVICES["notifications"], request.url.path, request.method, dict(request.headers), body, dict(request.query_params) ) return JSONResponse( status_code=response.status_code, content=response.json(), headers={k: v for k, v in response.headers.items() if k.lower() not in ["content-length", "transfer-encoding"]} ) @app.get("/api/v1/health") async def gateway_health_check(): """Gateway health check""" return {"status": "healthy", "service": "api-gateway"} @app.get("/api/v1/services-status") async def check_services_status(): """Check status of all microservices""" service_status = {} async def check_service(name: str, url: str): try: async with httpx.AsyncClient(timeout=5.0) as client: response = await client.get(f"{url}/api/v1/health") service_status[name] = { "status": "healthy" if response.status_code == 200 else "unhealthy", "response_time_ms": response.elapsed.total_seconds() * 1000, "url": url } except Exception as e: service_status[name] = { "status": "unhealthy", "error": str(e), "url": url } # Check all services concurrently tasks = [check_service(name, url) for name, url in SERVICES.items()] await asyncio.gather(*tasks) all_healthy = all(status["status"] == "healthy" for status in service_status.values()) return { "gateway_status": "healthy", "all_services_healthy": all_healthy, "services": service_status } @app.get("/") async def root(): """Root endpoint with API information""" return { "service": "Women Safety App API Gateway", "version": "1.0.0", "status": "running", "endpoints": { "auth": "/api/v1/register, /api/v1/login", "profile": "/api/v1/profile", "emergency": "/api/v1/alert, /api/v1/alerts/*", "location": "/api/v1/update-location, /api/v1/nearby-users", "calendar": "/api/v1/entries, /api/v1/cycle-overview", "notifications": "/api/v1/register-device, /api/v1/send-notification" }, "docs": "/docs" } if __name__ == "__main__": import uvicorn uvicorn.run(app, host="0.0.0.0", port=8000)