Automated Action 2adbcd0535 Complete multi-tenant SaaS platform with external integrations
- 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>
2025-06-27 21:14:30 +00:00

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)