335 lines
9.9 KiB
Python
335 lines
9.9 KiB
Python
import asyncio
|
|
import time
|
|
from typing import Dict
|
|
|
|
import httpx
|
|
from fastapi import Depends, FastAPI, HTTPException, Request
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
from fastapi.responses import JSONResponse
|
|
|
|
from shared.config import settings
|
|
|
|
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)
|