All checks were successful
continuous-integration/drone/push Build is passing
782 lines
32 KiB
Python
782 lines
32 KiB
Python
import asyncio
|
||
import os
|
||
import time
|
||
from typing import Dict, Any, Optional, List
|
||
|
||
import httpx
|
||
from fastapi import Depends, FastAPI, HTTPException, Request
|
||
from fastapi.middleware.cors import CORSMiddleware
|
||
from fastapi.responses import JSONResponse
|
||
from fastapi.openapi.utils import get_openapi
|
||
from pydantic import BaseModel, Field
|
||
from typing import Any
|
||
|
||
from shared.config import settings
|
||
from services.user_service.schemas import UserCreate, UserLogin, UserResponse, UserUpdate, Token
|
||
|
||
# Импортируем схемы для экстренных контактов
|
||
class EmergencyContactBase(BaseModel):
|
||
name: str = Field(..., min_length=1, max_length=100)
|
||
phone_number: str = Field(..., min_length=5, max_length=20)
|
||
relationship: Optional[str] = Field(None, max_length=50)
|
||
notes: Optional[str] = Field(None, max_length=500)
|
||
|
||
|
||
class EmergencyContactCreate(EmergencyContactBase):
|
||
pass
|
||
|
||
|
||
class EmergencyContactUpdate(BaseModel):
|
||
name: Optional[str] = Field(None, min_length=1, max_length=100)
|
||
phone_number: Optional[str] = Field(None, min_length=5, max_length=20)
|
||
relationship: Optional[str] = Field(None, max_length=50)
|
||
notes: Optional[str] = Field(None, max_length=500)
|
||
|
||
|
||
class EmergencyContactResponse(EmergencyContactBase):
|
||
id: int
|
||
uuid: str
|
||
user_id: int
|
||
|
||
class Config:
|
||
from_attributes = True
|
||
|
||
app = FastAPI(title="API Gateway", version="1.0.0", openapi_url="/api/openapi.json")
|
||
|
||
# CORS middleware
|
||
app.add_middleware(
|
||
CORSMiddleware,
|
||
allow_origins=settings.CORS_ORIGINS,
|
||
allow_credentials=True,
|
||
allow_methods=["*"],
|
||
allow_headers=["*"],
|
||
)
|
||
|
||
# Service registry
|
||
SERVICES = {
|
||
"users": os.getenv("USER_SERVICE_URL", "http://localhost:8001"),
|
||
"emergency": os.getenv("EMERGENCY_SERVICE_URL", "http://localhost:8002"),
|
||
"location": os.getenv("LOCATION_SERVICE_URL", "http://localhost:8003"),
|
||
"calendar": os.getenv("CALENDAR_SERVICE_URL", "http://localhost:8004"),
|
||
"notifications": os.getenv("NOTIFICATION_SERVICE_URL", "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 if request.client else "127.0.0.1"
|
||
|
||
|
||
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: Optional[bytes] = None,
|
||
params: Optional[dict] = None,
|
||
user_create: Optional[UserCreate] = None,
|
||
user_update: Optional[UserUpdate] = None,
|
||
user_login: Optional[UserLogin] = None,
|
||
emergency_contact_create: Optional[EmergencyContactCreate] = None,
|
||
emergency_contact_update: Optional[EmergencyContactUpdate] = None
|
||
):
|
||
"""Proxy request to microservice"""
|
||
url = f"{service_url}{path}"
|
||
|
||
# Для отладки
|
||
print(f"Proxy request to: {url}, method: {method}")
|
||
|
||
if body:
|
||
print(f"Request body: {body.decode('utf-8')[:100]}...")
|
||
|
||
# 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,
|
||
)
|
||
|
||
# Для отладки
|
||
print(f"Response status: {response.status_code}")
|
||
if response.status_code >= 400:
|
||
print(f"Error response: {response.text}")
|
||
|
||
return response
|
||
except httpx.TimeoutException:
|
||
print(f"Timeout error for {url}")
|
||
raise HTTPException(status_code=504, detail="Service timeout")
|
||
except httpx.ConnectError:
|
||
print(f"Connection error for {url}")
|
||
raise HTTPException(status_code=503, detail="Service unavailable")
|
||
except Exception as e:
|
||
print(f"Proxy error for {url}: {str(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)
|
||
|
||
|
||
# Authentication routes
|
||
@app.post("/api/v1/auth/register", response_model=UserResponse, tags=["Authentication"], summary="Register a new user")
|
||
async def register_user(user_create: UserCreate, request: Request):
|
||
"""Register a new user"""
|
||
async with httpx.AsyncClient(timeout=30.0) as client:
|
||
try:
|
||
response = await client.post(
|
||
f"{SERVICES['users']}/api/v1/auth/register",
|
||
json=user_create.model_dump(),
|
||
headers={
|
||
"Content-Type": "application/json",
|
||
"Accept": "application/json"
|
||
}
|
||
)
|
||
|
||
if response.status_code == 200:
|
||
return response.json()
|
||
elif response.status_code == 400:
|
||
# Handle specific registration errors
|
||
try:
|
||
error_json = response.json()
|
||
error_detail = error_json.get("detail", "Registration failed")
|
||
if "Email already registered" in error_detail:
|
||
raise HTTPException(
|
||
status_code=400,
|
||
detail=f"Email {user_create.email} is already registered. Please use a different email or try logging in."
|
||
)
|
||
elif "Username already taken" in error_detail:
|
||
raise HTTPException(
|
||
status_code=400,
|
||
detail=f"Username {user_create.username} is already taken. Please choose a different username."
|
||
)
|
||
else:
|
||
raise HTTPException(status_code=400, detail=error_detail)
|
||
except ValueError:
|
||
# JSON parsing failed
|
||
raise HTTPException(
|
||
status_code=400,
|
||
detail="Registration failed. Please check your input data."
|
||
)
|
||
else:
|
||
error_detail = "Registration failed"
|
||
try:
|
||
error_json = response.json()
|
||
error_detail = error_json.get("detail", error_detail)
|
||
except:
|
||
pass
|
||
raise HTTPException(status_code=response.status_code, detail=error_detail)
|
||
|
||
except HTTPException:
|
||
raise
|
||
except Exception as e:
|
||
raise HTTPException(
|
||
status_code=500,
|
||
detail=f"Registration service error: {str(e)}"
|
||
)
|
||
|
||
|
||
@app.post("/api/v1/auth/login", response_model=Token, tags=["Authentication"], summary="Login user")
|
||
async def login_user(user_login: UserLogin, request: Request):
|
||
"""Login user"""
|
||
client_ip = get_client_ip(request)
|
||
|
||
# Дополнительное логирование для отладки
|
||
try:
|
||
request_body = await request.body()
|
||
print(f"RAW request body from {client_ip}: {request_body}")
|
||
print(f"Request headers: {dict(request.headers)}")
|
||
except:
|
||
pass
|
||
|
||
print(f"Login request from {client_ip}: {user_login.model_dump(exclude={'password'})}")
|
||
print(f"Full login data: {user_login.model_dump()}")
|
||
|
||
async with httpx.AsyncClient(timeout=30.0) as client:
|
||
try:
|
||
login_data = user_login.model_dump()
|
||
print(f"Sending login data to user service: {login_data}")
|
||
|
||
response = await client.post(
|
||
f"{SERVICES['users']}/api/v1/auth/login",
|
||
json=login_data,
|
||
headers={
|
||
"Content-Type": "application/json",
|
||
"Accept": "application/json"
|
||
}
|
||
)
|
||
|
||
print(f"User service response: status={response.status_code}")
|
||
if response.status_code >= 400:
|
||
print(f"Error response body: {response.text}")
|
||
|
||
if response.status_code == 200:
|
||
return response.json()
|
||
elif response.status_code == 422:
|
||
# Detailed handling for validation errors
|
||
try:
|
||
error_json = response.json()
|
||
print(f"Validation error details: {error_json}")
|
||
# Return more detailed validation errors
|
||
if "detail" in error_json:
|
||
detail = error_json["detail"]
|
||
if isinstance(detail, list):
|
||
# FastAPI validation errors
|
||
formatted_errors = []
|
||
for error in detail:
|
||
field = error.get("loc", ["unknown"])[-1]
|
||
msg = error.get("msg", "Invalid value")
|
||
formatted_errors.append(f"{field}: {msg}")
|
||
raise HTTPException(
|
||
status_code=422,
|
||
detail=f"Validation errors: {'; '.join(formatted_errors)}"
|
||
)
|
||
else:
|
||
raise HTTPException(status_code=422, detail=detail)
|
||
else:
|
||
raise HTTPException(status_code=422, detail="Invalid input data")
|
||
except ValueError as ve:
|
||
print(f"JSON parse error: {ve}")
|
||
raise HTTPException(status_code=422, detail="Invalid request format")
|
||
else:
|
||
error_detail = response.text
|
||
try:
|
||
error_json = response.json()
|
||
error_detail = error_json.get("detail", error_detail)
|
||
except:
|
||
pass
|
||
raise HTTPException(status_code=response.status_code, detail=error_detail)
|
||
|
||
except HTTPException:
|
||
raise
|
||
except Exception as e:
|
||
print(f"Login service error: {str(e)}")
|
||
raise HTTPException(status_code=500, detail=f"Login error: {str(e)}")
|
||
|
||
|
||
# Debug endpoint to analyze raw request data
|
||
@app.post("/api/v1/debug/login", tags=["Debug"], summary="Debug login request data")
|
||
async def debug_login_request(request: Request):
|
||
"""Debug endpoint to analyze raw request data from mobile app"""
|
||
try:
|
||
client_ip = get_client_ip(request)
|
||
headers = dict(request.headers)
|
||
body = await request.body()
|
||
|
||
debug_info = {
|
||
"client_ip": client_ip,
|
||
"headers": headers,
|
||
"raw_body": body.decode('utf-8', errors='ignore') if body else None,
|
||
"content_length": len(body) if body else 0,
|
||
"content_type": headers.get('content-type', 'unknown')
|
||
}
|
||
|
||
print(f"DEBUG LOGIN from {client_ip}:")
|
||
print(f"Headers: {headers}")
|
||
print(f"Raw body: {body}")
|
||
|
||
# Try to parse as JSON
|
||
try:
|
||
if body:
|
||
import json
|
||
json_data = json.loads(body)
|
||
debug_info["parsed_json"] = json_data
|
||
print(f"Parsed JSON: {json_data}")
|
||
except Exception as e:
|
||
debug_info["json_parse_error"] = str(e)
|
||
print(f"JSON parse error: {e}")
|
||
|
||
return debug_info
|
||
|
||
except Exception as e:
|
||
print(f"Debug endpoint error: {str(e)}")
|
||
return {"error": str(e)}
|
||
|
||
|
||
# Alternative login endpoint for mobile app debugging
|
||
@app.post("/api/v1/auth/login-flexible", tags=["Authentication"], summary="Flexible login for debugging")
|
||
async def login_flexible(request: Request):
|
||
"""Flexible login endpoint that accepts various data formats"""
|
||
try:
|
||
client_ip = get_client_ip(request)
|
||
body = await request.body()
|
||
|
||
print(f"Flexible login from {client_ip}")
|
||
print(f"Raw request: {body}")
|
||
|
||
# Try to parse JSON
|
||
import json
|
||
try:
|
||
data = json.loads(body) if body else {}
|
||
except:
|
||
return {"error": "Invalid JSON format", "status": 422}
|
||
|
||
print(f"Parsed data: {data}")
|
||
|
||
# Extract login fields with flexible names
|
||
email = data.get('email') or data.get('Email') or data.get('EMAIL')
|
||
username = data.get('username') or data.get('Username') or data.get('user_name') or data.get('login')
|
||
password = data.get('password') or data.get('Password') or data.get('PASSWORD') or data.get('pass')
|
||
|
||
print(f"Extracted: email={email}, username={username}, password={'***' if password else None}")
|
||
|
||
# Validation
|
||
if not password:
|
||
return {"error": "Password is required", "status": 422}
|
||
|
||
if not email and not username:
|
||
return {"error": "Either email or username must be provided", "status": 422}
|
||
|
||
# Create proper login data
|
||
login_data = {
|
||
"email": email,
|
||
"username": username,
|
||
"password": password
|
||
}
|
||
|
||
# Forward to user service
|
||
async with httpx.AsyncClient(timeout=30.0) as client:
|
||
response = await client.post(
|
||
f"{SERVICES['users']}/api/v1/auth/login",
|
||
json=login_data,
|
||
headers={
|
||
"Content-Type": "application/json",
|
||
"Accept": "application/json"
|
||
}
|
||
)
|
||
|
||
print(f"User service response: {response.status_code} - {response.text}")
|
||
|
||
if response.status_code == 200:
|
||
return response.json()
|
||
else:
|
||
try:
|
||
error_data = response.json()
|
||
return {"error": error_data.get("detail", "Login failed"), "status": response.status_code}
|
||
except:
|
||
return {"error": response.text, "status": response.status_code}
|
||
|
||
except Exception as e:
|
||
print(f"Flexible login error: {str(e)}")
|
||
return {"error": str(e), "status": 500}
|
||
|
||
|
||
# Utility endpoints
|
||
@app.get("/api/v1/auth/check-email", tags=["Authentication"], summary="Check if email is available")
|
||
async def check_email_availability(email: str):
|
||
"""Check if email is available for registration"""
|
||
async with httpx.AsyncClient(timeout=10.0) as client:
|
||
try:
|
||
# Make request to user service to check if email exists
|
||
response = await client.get(
|
||
f"{SERVICES['users']}/api/v1/check-email",
|
||
params={"email": email}
|
||
)
|
||
if response.status_code == 200:
|
||
return response.json()
|
||
else:
|
||
return {"available": True, "message": "Email is available"}
|
||
except Exception:
|
||
return {"available": True, "message": "Unable to check availability"}
|
||
|
||
|
||
@app.get("/api/v1/auth/generate-test-data", tags=["Development"], summary="Generate test user data")
|
||
async def generate_test_user_data():
|
||
"""Generate unique test user data for development"""
|
||
import random
|
||
import string
|
||
|
||
# Generate unique email with timestamp
|
||
from datetime import datetime
|
||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||
random_suffix = ''.join(random.choices(string.ascii_lowercase + string.digits, k=4))
|
||
|
||
return {
|
||
"email": f"test_{timestamp}_{random_suffix}@example.com",
|
||
"username": f"testuser_{timestamp}_{random_suffix}",
|
||
"password": "TestPassword123!",
|
||
"first_name": "Test",
|
||
"last_name": "User",
|
||
"phone": f"+123456{random.randint(1000, 9999)}"
|
||
}
|
||
|
||
|
||
# User Service routes
|
||
@app.post("/api/v1/users/register", operation_id="user_register", response_model=UserResponse, tags=["Users"], summary="Register a new user")
|
||
@app.get("/api/v1/users/me", operation_id="user_me_get", response_model=UserResponse, tags=["Users"], summary="Get current user profile")
|
||
@app.patch("/api/v1/users/me", operation_id="user_me_patch", response_model=UserResponse, tags=["Users"], summary="Update user profile")
|
||
@app.put("/api/v1/users/me", operation_id="user_me_put", response_model=UserResponse, tags=["Users"], summary="Update user profile")
|
||
@app.get("/api/v1/users/me/emergency-contacts", operation_id="user_emergency_contacts_get", response_model=List[EmergencyContactResponse], tags=["Emergency Contacts"], summary="Get all emergency contacts")
|
||
@app.post("/api/v1/users/me/emergency-contacts", operation_id="user_emergency_contacts_post", response_model=EmergencyContactResponse, tags=["Emergency Contacts"], summary="Create a new emergency contact")
|
||
@app.get("/api/v1/users/me/emergency-contacts/{contact_id}", operation_id="user_emergency_contact_get", response_model=EmergencyContactResponse, tags=["Emergency Contacts"], summary="Get emergency contact by ID")
|
||
@app.delete("/api/v1/users/me/emergency-contacts/{contact_id}", operation_id="user_emergency_contact_delete", tags=["Emergency Contacts"], summary="Delete emergency contact")
|
||
@app.patch("/api/v1/users/me/emergency-contacts/{contact_id}", operation_id="user_emergency_contact_patch", response_model=EmergencyContactResponse, tags=["Emergency Contacts"], summary="Update emergency contact")
|
||
@app.get("/api/v1/users/dashboard", operation_id="user_dashboard_get", tags=["Users"])
|
||
@app.post("/api/v1/users/me/change-password", operation_id="user_change_password", tags=["Users"])
|
||
@app.get("/api/v1/profile", operation_id="user_profile_get", response_model=UserResponse, tags=["Users"])
|
||
@app.put("/api/v1/profile", operation_id="user_profile_update", response_model=UserResponse, tags=["Users"])
|
||
async def user_service_proxy(
|
||
request: Request,
|
||
user_create: Optional[UserCreate] = None,
|
||
user_login: Optional[UserLogin] = None,
|
||
user_update: Optional[UserUpdate] = None,
|
||
emergency_contact_create: Optional[EmergencyContactCreate] = None,
|
||
emergency_contact_update: Optional[EmergencyContactUpdate] = None
|
||
):
|
||
"""Proxy requests to User Service"""
|
||
body = await request.body()
|
||
print(f"User service proxy: {request.url.path}, method: {request.method}")
|
||
try:
|
||
response = await proxy_request(
|
||
SERVICES["users"],
|
||
request.url.path,
|
||
request.method,
|
||
dict(request.headers),
|
||
body,
|
||
dict(request.query_params),
|
||
user_create=user_create,
|
||
user_login=user_login,
|
||
user_update=user_update,
|
||
emergency_contact_create=emergency_contact_create,
|
||
emergency_contact_update=emergency_contact_update
|
||
)
|
||
|
||
try:
|
||
response_json = response.json()
|
||
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"]
|
||
},
|
||
)
|
||
except Exception as e:
|
||
print(f"Error processing JSON response: {str(e)}")
|
||
print(f"Response text: {response.text}")
|
||
return JSONResponse(
|
||
status_code=500,
|
||
content={"detail": f"Error processing response: {str(e)}"},
|
||
)
|
||
except Exception as e:
|
||
print(f"Error in user_service_proxy: {str(e)}")
|
||
return JSONResponse(
|
||
status_code=500,
|
||
content={"detail": f"Proxy error: {str(e)}"},
|
||
)
|
||
|
||
|
||
# Emergency Service routes
|
||
@app.api_route("/api/v1/emergency/alerts", methods=["POST"], operation_id="emergency_alerts_post")
|
||
@app.api_route("/api/v1/emergency/alerts", methods=["GET"], operation_id="emergency_alerts_get")
|
||
@app.api_route("/api/v1/emergency/alerts/my", methods=["GET"], operation_id="emergency_alerts_my_get")
|
||
@app.api_route("/api/v1/emergency/alerts/nearby", methods=["GET"], operation_id="emergency_alerts_nearby_get")
|
||
@app.api_route("/api/v1/emergency/alerts/{alert_id}", methods=["GET"], operation_id="emergency_alert_get")
|
||
@app.api_route("/api/v1/emergency/alerts/{alert_id}", methods=["PATCH"], operation_id="emergency_alert_patch")
|
||
@app.api_route("/api/v1/emergency/alerts/{alert_id}", methods=["DELETE"], operation_id="emergency_alert_delete")
|
||
@app.api_route("/api/v1/emergency/alerts/{alert_id}/cancel", methods=["PATCH"], operation_id="emergency_alert_cancel")
|
||
@app.api_route("/api/v1/emergency/reports", methods=["POST"], operation_id="emergency_reports_post")
|
||
@app.api_route("/api/v1/emergency/reports", methods=["GET"], operation_id="emergency_reports_get")
|
||
@app.api_route("/api/v1/emergency/reports/nearby", methods=["GET"], operation_id="emergency_reports_nearby_get")
|
||
@app.api_route("/api/v1/emergency/reports/{report_id}", methods=["GET"], operation_id="emergency_report_get")
|
||
@app.api_route("/api/v1/emergency/reports/{report_id}", methods=["PATCH"], operation_id="emergency_report_patch")
|
||
@app.api_route("/api/v1/emergency/reports/{report_id}", methods=["DELETE"], operation_id="emergency_report_delete")
|
||
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/locations/update", methods=["POST"], operation_id="location_update_post")
|
||
@app.api_route("/api/v1/locations/last", methods=["GET"], operation_id="location_last_get")
|
||
@app.api_route("/api/v1/locations/history", methods=["GET"], operation_id="location_history_get")
|
||
@app.api_route("/api/v1/locations/users/nearby", methods=["GET"], operation_id="location_users_nearby_get")
|
||
@app.api_route("/api/v1/locations/safe-places", methods=["GET"], operation_id="location_safe_places_get")
|
||
@app.api_route("/api/v1/locations/safe-places", methods=["POST"], operation_id="location_safe_places_post")
|
||
@app.api_route("/api/v1/locations/safe-places/{place_id}", methods=["GET"], operation_id="location_safe_place_get")
|
||
@app.api_route("/api/v1/locations/safe-places/{place_id}", methods=["PATCH"], operation_id="location_safe_place_patch")
|
||
@app.api_route("/api/v1/locations/safe-places/{place_id}", methods=["DELETE"], operation_id="location_safe_place_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/calendar/entries", methods=["GET"], operation_id="calendar_entries_get")
|
||
@app.api_route("/api/v1/calendar/entries", methods=["POST"], operation_id="calendar_entries_post")
|
||
@app.api_route("/api/v1/calendar/entries/{entry_id}", methods=["GET"], operation_id="calendar_entry_get")
|
||
@app.api_route("/api/v1/calendar/entries/{entry_id}", methods=["PUT"], operation_id="calendar_entry_put")
|
||
@app.api_route("/api/v1/calendar/entries/{entry_id}", methods=["DELETE"], operation_id="calendar_entry_delete")
|
||
@app.api_route("/api/v1/calendar/cycle-overview", methods=["GET"], operation_id="calendar_cycle_overview_get")
|
||
@app.api_route("/api/v1/calendar/insights", methods=["GET"], operation_id="calendar_insights_get")
|
||
@app.api_route("/api/v1/calendar/reminders", methods=["GET"], operation_id="calendar_reminders_get")
|
||
@app.api_route("/api/v1/calendar/reminders", methods=["POST"], operation_id="calendar_reminders_post")
|
||
@app.api_route("/api/v1/calendar/settings", methods=["GET"], operation_id="calendar_settings_get")
|
||
@app.api_route("/api/v1/calendar/settings", methods=["PUT"], operation_id="calendar_settings_put")
|
||
# Мобильное API для календаря
|
||
@app.api_route("/api/v1/entry", methods=["POST"], operation_id="mobile_calendar_entry_post")
|
||
@app.api_route("/api/v1/entries", methods=["GET"], operation_id="mobile_calendar_entries_get")
|
||
@app.api_route("/api/v1/entries", methods=["POST"], operation_id="mobile_calendar_entries_post")
|
||
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/notifications/devices", methods=["GET"], operation_id="notifications_devices_get")
|
||
@app.api_route("/api/v1/notifications/devices", methods=["POST"], operation_id="notifications_devices_post")
|
||
@app.api_route("/api/v1/notifications/devices/{device_id}", methods=["DELETE"], operation_id="notifications_device_delete")
|
||
@app.api_route("/api/v1/notifications/devices/{device_id}", methods=["GET"], operation_id="notifications_device_get")
|
||
@app.api_route("/api/v1/notifications/preferences", methods=["GET"], operation_id="notifications_preferences_get")
|
||
@app.api_route("/api/v1/notifications/preferences", methods=["POST"], operation_id="notifications_preferences_post")
|
||
@app.api_route("/api/v1/notifications/test", methods=["POST"], operation_id="notifications_test_post")
|
||
@app.api_route("/api/v1/notifications/history", methods=["GET"], operation_id="notifications_history_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/auth/register, /api/v1/auth/login",
|
||
"users": "/api/v1/users/me, /api/v1/users/dashboard",
|
||
"emergency": "/api/v1/emergency/alerts, /api/v1/emergency/reports",
|
||
"location": "/api/v1/locations/update, /api/v1/locations/safe-places",
|
||
"calendar": "/api/v1/calendar/entries, /api/v1/calendar/cycle-overview",
|
||
"notifications": "/api/v1/notifications/devices, /api/v1/notifications/history",
|
||
},
|
||
"docs": "/docs",
|
||
}
|
||
|
||
|
||
# Переопределение схемы OpenAPI
|
||
def custom_openapi():
|
||
if app.openapi_schema:
|
||
return app.openapi_schema
|
||
|
||
openapi_schema = get_openapi(
|
||
title="Women's Safety App API Gateway",
|
||
version="1.0.0",
|
||
description="API Gateway для Women's Safety App с поддержкой микросервисной архитектуры",
|
||
routes=app.routes,
|
||
)
|
||
|
||
# Добавление примеров запросов для маршрутов
|
||
if "paths" in openapi_schema:
|
||
if "/api/v1/auth/register" in openapi_schema["paths"]:
|
||
request_example = {
|
||
"username": "user123",
|
||
"email": "user@example.com",
|
||
"password": "Password123!",
|
||
"full_name": "John Doe",
|
||
"phone_number": "+7123456789"
|
||
}
|
||
path_item = openapi_schema["paths"]["/api/v1/auth/register"]
|
||
for method in path_item:
|
||
if "requestBody" in path_item[method]:
|
||
path_item[method]["requestBody"]["content"]["application/json"]["example"] = request_example
|
||
|
||
if "/api/v1/auth/login" in openapi_schema["paths"]:
|
||
login_example = {
|
||
"username": "user123",
|
||
"password": "Password123!"
|
||
}
|
||
path_item = openapi_schema["paths"]["/api/v1/auth/login"]
|
||
for method in path_item:
|
||
if "requestBody" in path_item[method]:
|
||
path_item[method]["requestBody"]["content"]["application/json"]["example"] = login_example
|
||
|
||
app.openapi_schema = openapi_schema
|
||
return app.openapi_schema
|
||
|
||
app.openapi = custom_openapi
|
||
|
||
if __name__ == "__main__":
|
||
import uvicorn
|
||
import os
|
||
|
||
port = int(os.environ.get("PORT", 8000))
|
||
uvicorn.run(app, host="0.0.0.0", port=port)
|