mirror of
https://github.com/blackboxprogramming/BlackRoad-Operating-System.git
synced 2026-03-17 06:57:17 -05:00
Add comprehensive multi-API integration support
This commit adds extensive API integration capabilities for deployment, payments, communications, and monitoring to BlackRoad OS. New API Integrations: - Railway API: Cloud deployment management (GraphQL) - Vercel API: Serverless deployment platform (REST) - Stripe API: Payment processing and billing - Twilio API: SMS, Voice, and WhatsApp messaging - Slack API: Team collaboration and notifications - Discord API: Community messaging and notifications - Sentry API: Error tracking and application monitoring Core Features: - Centralized API client manager with health checking - Comprehensive health monitoring endpoint (/api/health/*) - Automatic retry logic and rate limit handling - Unified status monitoring for all integrations Infrastructure: - Railway deployment configuration (railway.json, railway.toml) - Enhanced GitHub Actions workflows: * backend-tests.yml: Comprehensive test suite with PostgreSQL/Redis * railway-deploy.yml: Automated Railway deployment with notifications - Docker build validation in CI/CD pipeline Testing: - Comprehensive test suite for all API integrations - API connectivity verification in CI/CD - Mock-friendly architecture for testing without credentials Configuration: - Updated .env.example with all new API keys - Added stripe and sentry-sdk to requirements.txt - Registered all new routers in main.py - Updated API info endpoint with new integrations Documentation: - API_INTEGRATIONS.md: Complete setup and usage guide - Interactive API docs at /api/docs with all endpoints - Health check endpoints for monitoring All APIs are optional and gracefully handle missing credentials. The system provides clear status messages for configuration requirements.
This commit is contained in:
424
backend/app/routers/vercel.py
Normal file
424
backend/app/routers/vercel.py
Normal file
@@ -0,0 +1,424 @@
|
||||
"""
|
||||
Vercel API Integration Router
|
||||
|
||||
Provides endpoints for managing Vercel deployments, projects, and domains.
|
||||
Vercel is a cloud platform for static sites and serverless functions.
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, HTTPException, status
|
||||
from pydantic import BaseModel
|
||||
from typing import List, Optional, Dict, Any
|
||||
from datetime import datetime
|
||||
import httpx
|
||||
import os
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(prefix="/api/vercel", tags=["vercel"])
|
||||
|
||||
# Vercel API configuration
|
||||
VERCEL_API_URL = "https://api.vercel.com"
|
||||
VERCEL_TOKEN = os.getenv("VERCEL_TOKEN")
|
||||
VERCEL_TEAM_ID = os.getenv("VERCEL_TEAM_ID")
|
||||
|
||||
|
||||
class VercelProject(BaseModel):
|
||||
"""Vercel project model"""
|
||||
id: str
|
||||
name: str
|
||||
framework: Optional[str] = None
|
||||
created_at: int
|
||||
updated_at: int
|
||||
|
||||
|
||||
class VercelDeployment(BaseModel):
|
||||
"""Vercel deployment model"""
|
||||
uid: str
|
||||
name: str
|
||||
url: str
|
||||
state: str
|
||||
created_at: int
|
||||
ready: Optional[int] = None
|
||||
|
||||
|
||||
class VercelDomain(BaseModel):
|
||||
"""Vercel domain model"""
|
||||
name: str
|
||||
verified: bool
|
||||
created_at: int
|
||||
|
||||
|
||||
class DeploymentTrigger(BaseModel):
|
||||
"""Trigger deployment request"""
|
||||
project_id: str
|
||||
git_branch: Optional[str] = "main"
|
||||
|
||||
|
||||
class VercelClient:
|
||||
"""Vercel REST API client"""
|
||||
|
||||
def __init__(self, token: Optional[str] = None, team_id: Optional[str] = None):
|
||||
self.token = token or VERCEL_TOKEN
|
||||
self.team_id = team_id or VERCEL_TEAM_ID
|
||||
self.base_url = VERCEL_API_URL
|
||||
|
||||
def _get_headers(self) -> Dict[str, str]:
|
||||
"""Get API request headers"""
|
||||
if not self.token:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="Vercel API token not configured"
|
||||
)
|
||||
|
||||
return {
|
||||
"Authorization": f"Bearer {self.token}",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
def _get_params(self) -> Dict[str, str]:
|
||||
"""Get query parameters (team ID if configured)"""
|
||||
params = {}
|
||||
if self.team_id:
|
||||
params["teamId"] = self.team_id
|
||||
return params
|
||||
|
||||
async def _request(
|
||||
self,
|
||||
method: str,
|
||||
endpoint: str,
|
||||
**kwargs
|
||||
) -> Dict[str, Any]:
|
||||
"""Make API request"""
|
||||
headers = self._get_headers()
|
||||
params = kwargs.pop("params", {})
|
||||
params.update(self._get_params())
|
||||
|
||||
url = f"{self.base_url}{endpoint}"
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
try:
|
||||
response = await client.request(
|
||||
method,
|
||||
url,
|
||||
headers=headers,
|
||||
params=params,
|
||||
timeout=30.0,
|
||||
**kwargs
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
except httpx.HTTPStatusError as e:
|
||||
logger.error(f"Vercel API error: {e.response.text}")
|
||||
raise HTTPException(
|
||||
status_code=e.response.status_code,
|
||||
detail=f"Vercel API error: {e.response.text}"
|
||||
)
|
||||
except httpx.HTTPError as e:
|
||||
logger.error(f"Vercel API request failed: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail=f"Vercel API request failed: {str(e)}"
|
||||
)
|
||||
|
||||
async def get_projects(self) -> List[Dict[str, Any]]:
|
||||
"""Get all projects"""
|
||||
data = await self._request("GET", "/v9/projects")
|
||||
return data.get("projects", [])
|
||||
|
||||
async def get_project(self, project_id: str) -> Dict[str, Any]:
|
||||
"""Get project by ID or name"""
|
||||
return await self._request("GET", f"/v9/projects/{project_id}")
|
||||
|
||||
async def get_deployments(
|
||||
self,
|
||||
project_id: Optional[str] = None,
|
||||
limit: int = 20
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""Get deployments"""
|
||||
params = {"limit": limit}
|
||||
if project_id:
|
||||
params["projectId"] = project_id
|
||||
|
||||
data = await self._request("GET", "/v6/deployments", params=params)
|
||||
return data.get("deployments", [])
|
||||
|
||||
async def get_deployment(self, deployment_id: str) -> Dict[str, Any]:
|
||||
"""Get deployment by ID"""
|
||||
return await self._request("GET", f"/v13/deployments/{deployment_id}")
|
||||
|
||||
async def create_deployment(
|
||||
self,
|
||||
name: str,
|
||||
git_source: Optional[Dict[str, str]] = None,
|
||||
env_vars: Optional[Dict[str, str]] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Create a new deployment"""
|
||||
payload = {"name": name}
|
||||
|
||||
if git_source:
|
||||
payload["gitSource"] = git_source
|
||||
|
||||
if env_vars:
|
||||
payload["env"] = [
|
||||
{"key": k, "value": v}
|
||||
for k, v in env_vars.items()
|
||||
]
|
||||
|
||||
return await self._request("POST", "/v13/deployments", json=payload)
|
||||
|
||||
async def get_domains(self) -> List[Dict[str, Any]]:
|
||||
"""Get all domains"""
|
||||
data = await self._request("GET", "/v5/domains")
|
||||
return data.get("domains", [])
|
||||
|
||||
async def add_domain(self, name: str, project_id: str) -> Dict[str, Any]:
|
||||
"""Add a domain to a project"""
|
||||
payload = {"name": name}
|
||||
return await self._request(
|
||||
"POST",
|
||||
f"/v10/projects/{project_id}/domains",
|
||||
json=payload
|
||||
)
|
||||
|
||||
async def get_env_vars(self, project_id: str) -> List[Dict[str, Any]]:
|
||||
"""Get environment variables for a project"""
|
||||
data = await self._request("GET", f"/v9/projects/{project_id}/env")
|
||||
return data.get("envs", [])
|
||||
|
||||
async def create_env_var(
|
||||
self,
|
||||
project_id: str,
|
||||
key: str,
|
||||
value: str,
|
||||
target: List[str] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Create environment variable"""
|
||||
payload = {
|
||||
"key": key,
|
||||
"value": value,
|
||||
"type": "encrypted",
|
||||
"target": target or ["production", "preview", "development"]
|
||||
}
|
||||
return await self._request(
|
||||
"POST",
|
||||
f"/v10/projects/{project_id}/env",
|
||||
json=payload
|
||||
)
|
||||
|
||||
|
||||
# Initialize client
|
||||
vercel_client = VercelClient()
|
||||
|
||||
|
||||
@router.get("/status")
|
||||
async def get_vercel_status():
|
||||
"""Get Vercel API connection status"""
|
||||
if not VERCEL_TOKEN:
|
||||
return {
|
||||
"connected": False,
|
||||
"message": "Vercel API token not configured. Set VERCEL_TOKEN environment variable."
|
||||
}
|
||||
|
||||
try:
|
||||
# Try to fetch user info as a health check
|
||||
await vercel_client._request("GET", "/v2/user")
|
||||
return {
|
||||
"connected": True,
|
||||
"message": "Vercel API connected successfully",
|
||||
"team_configured": bool(VERCEL_TEAM_ID)
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
"connected": False,
|
||||
"message": f"Vercel API connection failed: {str(e)}"
|
||||
}
|
||||
|
||||
|
||||
@router.get("/projects")
|
||||
async def list_projects():
|
||||
"""List all Vercel projects"""
|
||||
try:
|
||||
projects = await vercel_client.get_projects()
|
||||
return {"projects": projects, "count": len(projects)}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching projects: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to fetch projects: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/projects/{project_id}")
|
||||
async def get_project(project_id: str):
|
||||
"""Get project details"""
|
||||
try:
|
||||
project = await vercel_client.get_project(project_id)
|
||||
return project
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching project: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to fetch project: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/deployments")
|
||||
async def list_deployments(project_id: Optional[str] = None, limit: int = 20):
|
||||
"""List deployments"""
|
||||
try:
|
||||
deployments = await vercel_client.get_deployments(project_id, limit)
|
||||
return {"deployments": deployments, "count": len(deployments)}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching deployments: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to fetch deployments: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/deployments/{deployment_id}")
|
||||
async def get_deployment(deployment_id: str):
|
||||
"""Get deployment details"""
|
||||
try:
|
||||
deployment = await vercel_client.get_deployment(deployment_id)
|
||||
return deployment
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching deployment: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to fetch deployment: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.post("/deployments")
|
||||
async def create_deployment(
|
||||
name: str,
|
||||
git_repo: Optional[str] = None,
|
||||
git_branch: Optional[str] = "main"
|
||||
):
|
||||
"""Create a new deployment"""
|
||||
try:
|
||||
git_source = None
|
||||
if git_repo:
|
||||
git_source = {
|
||||
"type": "github",
|
||||
"repo": git_repo,
|
||||
"ref": git_branch
|
||||
}
|
||||
|
||||
deployment = await vercel_client.create_deployment(name, git_source)
|
||||
return {
|
||||
"success": True,
|
||||
"deployment": deployment,
|
||||
"message": "Deployment created successfully"
|
||||
}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating deployment: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to create deployment: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/domains")
|
||||
async def list_domains():
|
||||
"""List all domains"""
|
||||
try:
|
||||
domains = await vercel_client.get_domains()
|
||||
return {"domains": domains, "count": len(domains)}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching domains: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to fetch domains: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.post("/projects/{project_id}/domains")
|
||||
async def add_domain(project_id: str, domain: str):
|
||||
"""Add a domain to a project"""
|
||||
try:
|
||||
result = await vercel_client.add_domain(domain, project_id)
|
||||
return {
|
||||
"success": True,
|
||||
"domain": result,
|
||||
"message": "Domain added successfully"
|
||||
}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error adding domain: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to add domain: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/projects/{project_id}/env")
|
||||
async def get_env_vars(project_id: str):
|
||||
"""Get environment variables for a project"""
|
||||
try:
|
||||
env_vars = await vercel_client.get_env_vars(project_id)
|
||||
return {"variables": env_vars, "count": len(env_vars)}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching env vars: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to fetch env vars: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.post("/projects/{project_id}/env")
|
||||
async def create_env_var(
|
||||
project_id: str,
|
||||
key: str,
|
||||
value: str,
|
||||
target: Optional[List[str]] = None
|
||||
):
|
||||
"""Create an environment variable"""
|
||||
try:
|
||||
result = await vercel_client.create_env_var(
|
||||
project_id,
|
||||
key,
|
||||
value,
|
||||
target
|
||||
)
|
||||
return {
|
||||
"success": True,
|
||||
"variable": result,
|
||||
"message": "Environment variable created successfully"
|
||||
}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating env var: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to create env var: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/health")
|
||||
async def vercel_health_check():
|
||||
"""Vercel API health check endpoint"""
|
||||
return {
|
||||
"service": "vercel",
|
||||
"status": "operational" if VERCEL_TOKEN else "not_configured",
|
||||
"timestamp": datetime.utcnow().isoformat()
|
||||
}
|
||||
Reference in New Issue
Block a user