
- Implemented comprehensive multi-tenant data isolation using database-level security - Built JWT authentication system with role-based access control (Super Admin, Org Admin, User, Viewer) - Created RESTful API endpoints for user and organization operations - Added complete audit logging for all data modifications with IP tracking - Implemented API rate limiting and input validation with security middleware - Built webhook processing engine with async event handling and retry logic - Created external API call handlers with circuit breaker pattern and error handling - Implemented data synchronization between external services and internal data - Added integration health monitoring and status tracking - Created three mock external services (User Management, Payment, Communication) - Implemented idempotency for webhook processing to handle duplicates gracefully - Added comprehensive security headers and XSS/CSRF protection - Set up Alembic database migrations with proper SQLite configuration - Included extensive documentation and API examples Architecture features: - Multi-tenant isolation at database level - Circuit breaker pattern for external API resilience - Async background task processing - Complete audit trail with user context - Role-based permission system - Webhook signature verification - Request validation and sanitization - Health monitoring endpoints Co-Authored-By: Claude <noreply@anthropic.com>
213 lines
8.0 KiB
Python
213 lines
8.0 KiB
Python
import httpx
|
|
import asyncio
|
|
from typing import Dict, Any, Optional, List
|
|
from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type
|
|
import logging
|
|
from app.core.config import settings
|
|
from app.integrations.external_apis.circuit_breaker import (
|
|
user_service_circuit_breaker,
|
|
payment_service_circuit_breaker,
|
|
communication_service_circuit_breaker
|
|
)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class ExternalAPIClient:
|
|
def __init__(self, base_url: str, api_key: Optional[str] = None, timeout: int = 30):
|
|
self.base_url = base_url.rstrip('/')
|
|
self.api_key = api_key
|
|
self.timeout = timeout
|
|
|
|
@retry(
|
|
stop=stop_after_attempt(3),
|
|
wait=wait_exponential(multiplier=1, min=4, max=10),
|
|
retry=retry_if_exception_type((httpx.RequestError, httpx.HTTPStatusError))
|
|
)
|
|
async def _make_request(
|
|
self,
|
|
method: str,
|
|
endpoint: str,
|
|
data: Optional[Dict[str, Any]] = None,
|
|
params: Optional[Dict[str, Any]] = None,
|
|
headers: Optional[Dict[str, str]] = None
|
|
) -> Dict[str, Any]:
|
|
"""Make HTTP request with retry logic"""
|
|
|
|
url = f"{self.base_url}{endpoint}"
|
|
|
|
# Prepare headers
|
|
request_headers = {
|
|
"Content-Type": "application/json",
|
|
"User-Agent": "MultiTenant-SaaS-Platform/1.0"
|
|
}
|
|
|
|
if self.api_key:
|
|
request_headers["Authorization"] = f"Bearer {self.api_key}"
|
|
|
|
if headers:
|
|
request_headers.update(headers)
|
|
|
|
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
|
logger.info(f"Making {method} request to {url}")
|
|
|
|
response = await client.request(
|
|
method=method,
|
|
url=url,
|
|
json=data,
|
|
params=params,
|
|
headers=request_headers
|
|
)
|
|
|
|
# Raise exception for HTTP error status codes
|
|
response.raise_for_status()
|
|
|
|
return response.json()
|
|
|
|
async def get(self, endpoint: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
"""Make GET request"""
|
|
return await self._make_request("GET", endpoint, params=params)
|
|
|
|
async def post(self, endpoint: str, data: Dict[str, Any]) -> Dict[str, Any]:
|
|
"""Make POST request"""
|
|
return await self._make_request("POST", endpoint, data=data)
|
|
|
|
async def put(self, endpoint: str, data: Dict[str, Any]) -> Dict[str, Any]:
|
|
"""Make PUT request"""
|
|
return await self._make_request("PUT", endpoint, data=data)
|
|
|
|
async def delete(self, endpoint: str) -> Dict[str, Any]:
|
|
"""Make DELETE request"""
|
|
return await self._make_request("DELETE", endpoint)
|
|
|
|
|
|
class UserServiceClient(ExternalAPIClient):
|
|
def __init__(self):
|
|
super().__init__(
|
|
base_url=settings.EXTERNAL_USER_SERVICE_URL,
|
|
api_key="user-service-api-key"
|
|
)
|
|
self.circuit_breaker = user_service_circuit_breaker
|
|
|
|
async def create_user(self, user_data: Dict[str, Any]) -> Dict[str, Any]:
|
|
"""Create user in external service"""
|
|
def _create_user():
|
|
return asyncio.run(self.post("/users", user_data))
|
|
|
|
return self.circuit_breaker.call(_create_user)
|
|
|
|
async def get_user(self, user_id: str) -> Dict[str, Any]:
|
|
"""Get user from external service"""
|
|
def _get_user():
|
|
return asyncio.run(self.get(f"/users/{user_id}"))
|
|
|
|
return self.circuit_breaker.call(_get_user)
|
|
|
|
async def update_user(self, user_id: str, user_data: Dict[str, Any]) -> Dict[str, Any]:
|
|
"""Update user in external service"""
|
|
def _update_user():
|
|
return asyncio.run(self.put(f"/users/{user_id}", user_data))
|
|
|
|
return self.circuit_breaker.call(_update_user)
|
|
|
|
async def delete_user(self, user_id: str) -> Dict[str, Any]:
|
|
"""Delete user from external service"""
|
|
def _delete_user():
|
|
return asyncio.run(self.delete(f"/users/{user_id}"))
|
|
|
|
return self.circuit_breaker.call(_delete_user)
|
|
|
|
async def sync_users(self, organization_id: int) -> List[Dict[str, Any]]:
|
|
"""Sync users from external service"""
|
|
def _sync_users():
|
|
return asyncio.run(self.get(f"/organizations/{organization_id}/users"))
|
|
|
|
return self.circuit_breaker.call(_sync_users)
|
|
|
|
|
|
class PaymentServiceClient(ExternalAPIClient):
|
|
def __init__(self):
|
|
super().__init__(
|
|
base_url=settings.EXTERNAL_PAYMENT_SERVICE_URL,
|
|
api_key="payment-service-api-key"
|
|
)
|
|
self.circuit_breaker = payment_service_circuit_breaker
|
|
|
|
async def create_subscription(self, subscription_data: Dict[str, Any]) -> Dict[str, Any]:
|
|
"""Create subscription in payment service"""
|
|
def _create_subscription():
|
|
return asyncio.run(self.post("/subscriptions", subscription_data))
|
|
|
|
return self.circuit_breaker.call(_create_subscription)
|
|
|
|
async def get_subscription(self, subscription_id: str) -> Dict[str, Any]:
|
|
"""Get subscription from payment service"""
|
|
def _get_subscription():
|
|
return asyncio.run(self.get(f"/subscriptions/{subscription_id}"))
|
|
|
|
return self.circuit_breaker.call(_get_subscription)
|
|
|
|
async def cancel_subscription(self, subscription_id: str) -> Dict[str, Any]:
|
|
"""Cancel subscription in payment service"""
|
|
def _cancel_subscription():
|
|
return asyncio.run(self.delete(f"/subscriptions/{subscription_id}"))
|
|
|
|
return self.circuit_breaker.call(_cancel_subscription)
|
|
|
|
async def process_payment(self, payment_data: Dict[str, Any]) -> Dict[str, Any]:
|
|
"""Process payment"""
|
|
def _process_payment():
|
|
return asyncio.run(self.post("/payments", payment_data))
|
|
|
|
return self.circuit_breaker.call(_process_payment)
|
|
|
|
async def get_billing_history(self, organization_id: int) -> List[Dict[str, Any]]:
|
|
"""Get billing history for organization"""
|
|
def _get_billing_history():
|
|
return asyncio.run(self.get(f"/organizations/{organization_id}/billing"))
|
|
|
|
return self.circuit_breaker.call(_get_billing_history)
|
|
|
|
|
|
class CommunicationServiceClient(ExternalAPIClient):
|
|
def __init__(self):
|
|
super().__init__(
|
|
base_url=settings.EXTERNAL_COMMUNICATION_SERVICE_URL,
|
|
api_key="communication-service-api-key"
|
|
)
|
|
self.circuit_breaker = communication_service_circuit_breaker
|
|
|
|
async def send_email(self, email_data: Dict[str, Any]) -> Dict[str, Any]:
|
|
"""Send email via communication service"""
|
|
def _send_email():
|
|
return asyncio.run(self.post("/emails", email_data))
|
|
|
|
return self.circuit_breaker.call(_send_email)
|
|
|
|
async def send_sms(self, sms_data: Dict[str, Any]) -> Dict[str, Any]:
|
|
"""Send SMS via communication service"""
|
|
def _send_sms():
|
|
return asyncio.run(self.post("/sms", sms_data))
|
|
|
|
return self.circuit_breaker.call(_send_sms)
|
|
|
|
async def send_notification(self, notification_data: Dict[str, Any]) -> Dict[str, Any]:
|
|
"""Send push notification"""
|
|
def _send_notification():
|
|
return asyncio.run(self.post("/notifications", notification_data))
|
|
|
|
return self.circuit_breaker.call(_send_notification)
|
|
|
|
async def get_delivery_status(self, message_id: str) -> Dict[str, Any]:
|
|
"""Get message delivery status"""
|
|
def _get_delivery_status():
|
|
return asyncio.run(self.get(f"/messages/{message_id}/status"))
|
|
|
|
return self.circuit_breaker.call(_get_delivery_status)
|
|
|
|
async def get_communication_history(self, organization_id: int) -> List[Dict[str, Any]]:
|
|
"""Get communication history for organization"""
|
|
def _get_communication_history():
|
|
return asyncio.run(self.get(f"/organizations/{organization_id}/communications"))
|
|
|
|
return self.circuit_breaker.call(_get_communication_history) |