init commit
This commit is contained in:
295
services/api_gateway/main.py
Normal file
295
services/api_gateway/main.py
Normal file
@@ -0,0 +1,295 @@
|
||||
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)
|
||||
Reference in New Issue
Block a user