mirror of
https://github.com/blackboxprogramming/BlackRoad-Operating-System.git
synced 2026-03-17 07:57:19 -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:
387
backend/app/routers/api_health.py
Normal file
387
backend/app/routers/api_health.py
Normal file
@@ -0,0 +1,387 @@
|
||||
"""
|
||||
API Health Check Router
|
||||
|
||||
Comprehensive health check endpoint for all external API integrations.
|
||||
Provides status monitoring for Railway, Vercel, Stripe, Twilio, Slack, Discord, Sentry, and more.
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import BaseModel
|
||||
from typing import Dict, List, Optional, Any
|
||||
from datetime import datetime
|
||||
import asyncio
|
||||
import os
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(prefix="/api/health", tags=["health"])
|
||||
|
||||
|
||||
class APIHealthStatus(BaseModel):
|
||||
"""Health status for a single API"""
|
||||
name: str
|
||||
status: str # connected, not_configured, error
|
||||
message: str
|
||||
last_checked: str
|
||||
configuration: Dict[str, bool]
|
||||
error: Optional[str] = None
|
||||
|
||||
|
||||
class SystemHealthStatus(BaseModel):
|
||||
"""Overall system health status"""
|
||||
status: str # healthy, degraded, unhealthy
|
||||
timestamp: str
|
||||
total_apis: int
|
||||
connected_apis: int
|
||||
not_configured_apis: int
|
||||
error_apis: int
|
||||
apis: Dict[str, APIHealthStatus]
|
||||
|
||||
|
||||
async def check_api_status(name: str, check_func) -> Dict[str, Any]:
|
||||
"""Check individual API status"""
|
||||
try:
|
||||
result = await check_func()
|
||||
return {
|
||||
"name": name,
|
||||
"status": "connected" if result.get("connected") else "not_configured",
|
||||
"message": result.get("message", ""),
|
||||
"last_checked": datetime.utcnow().isoformat(),
|
||||
"configuration": {
|
||||
k: v for k, v in result.items()
|
||||
if k.endswith("_configured") or k == "connected"
|
||||
},
|
||||
"error": None
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Health check failed for {name}: {e}")
|
||||
return {
|
||||
"name": name,
|
||||
"status": "error",
|
||||
"message": f"Health check failed: {str(e)}",
|
||||
"last_checked": datetime.utcnow().isoformat(),
|
||||
"configuration": {},
|
||||
"error": str(e)
|
||||
}
|
||||
|
||||
|
||||
@router.get("/all", response_model=SystemHealthStatus)
|
||||
async def check_all_apis():
|
||||
"""
|
||||
Comprehensive health check for all external APIs.
|
||||
|
||||
Checks connectivity and configuration for:
|
||||
- GitHub API
|
||||
- Railway API
|
||||
- Vercel API
|
||||
- Stripe API
|
||||
- Twilio API (SMS & WhatsApp)
|
||||
- Slack API
|
||||
- Discord API
|
||||
- Sentry API
|
||||
- OpenAI API
|
||||
- Hugging Face API
|
||||
- DigitalOcean API
|
||||
- AWS S3
|
||||
"""
|
||||
|
||||
# Import API clients
|
||||
from .railway import get_railway_status
|
||||
from .vercel import get_vercel_status
|
||||
from .stripe import get_stripe_status
|
||||
from .twilio import get_twilio_status
|
||||
from .slack import get_slack_status
|
||||
from .discord import get_discord_status
|
||||
from .sentry import get_sentry_status
|
||||
|
||||
# Define all API checks
|
||||
api_checks = {
|
||||
"railway": get_railway_status,
|
||||
"vercel": get_vercel_status,
|
||||
"stripe": get_stripe_status,
|
||||
"twilio": get_twilio_status,
|
||||
"slack": get_slack_status,
|
||||
"discord": get_discord_status,
|
||||
"sentry": get_sentry_status,
|
||||
}
|
||||
|
||||
# Add checks for existing APIs
|
||||
api_checks.update({
|
||||
"github": lambda: check_github_status(),
|
||||
"openai": lambda: check_openai_status(),
|
||||
"huggingface": lambda: check_huggingface_status(),
|
||||
"digitalocean": lambda: check_digitalocean_status(),
|
||||
"aws": lambda: check_aws_status(),
|
||||
})
|
||||
|
||||
# Run all checks concurrently
|
||||
tasks = [
|
||||
check_api_status(name, func)
|
||||
for name, func in api_checks.items()
|
||||
]
|
||||
|
||||
results = await asyncio.gather(*tasks, return_exceptions=True)
|
||||
|
||||
# Process results
|
||||
apis = {}
|
||||
connected_count = 0
|
||||
not_configured_count = 0
|
||||
error_count = 0
|
||||
|
||||
for result in results:
|
||||
if isinstance(result, Exception):
|
||||
error_count += 1
|
||||
continue
|
||||
|
||||
apis[result["name"]] = result
|
||||
|
||||
if result["status"] == "connected":
|
||||
connected_count += 1
|
||||
elif result["status"] == "not_configured":
|
||||
not_configured_count += 1
|
||||
else:
|
||||
error_count += 1
|
||||
|
||||
# Determine overall system health
|
||||
total_apis = len(apis)
|
||||
if connected_count == total_apis:
|
||||
overall_status = "healthy"
|
||||
elif connected_count > 0:
|
||||
overall_status = "degraded"
|
||||
else:
|
||||
overall_status = "unhealthy"
|
||||
|
||||
return SystemHealthStatus(
|
||||
status=overall_status,
|
||||
timestamp=datetime.utcnow().isoformat(),
|
||||
total_apis=total_apis,
|
||||
connected_apis=connected_count,
|
||||
not_configured_apis=not_configured_count,
|
||||
error_apis=error_count,
|
||||
apis=apis
|
||||
)
|
||||
|
||||
|
||||
@router.get("/summary")
|
||||
async def get_health_summary():
|
||||
"""Get a quick summary of API health"""
|
||||
health = await check_all_apis()
|
||||
|
||||
return {
|
||||
"status": health.status,
|
||||
"timestamp": health.timestamp,
|
||||
"summary": {
|
||||
"total": health.total_apis,
|
||||
"connected": health.connected_apis,
|
||||
"not_configured": health.not_configured_apis,
|
||||
"errors": health.error_apis
|
||||
},
|
||||
"connected_apis": [
|
||||
name for name, api in health.apis.items()
|
||||
if api.status == "connected"
|
||||
],
|
||||
"not_configured_apis": [
|
||||
name for name, api in health.apis.items()
|
||||
if api.status == "not_configured"
|
||||
],
|
||||
"error_apis": [
|
||||
name for name, api in health.apis.items()
|
||||
if api.status == "error"
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@router.get("/{api_name}")
|
||||
async def check_specific_api(api_name: str):
|
||||
"""Check health of a specific API"""
|
||||
api_checks = {
|
||||
"railway": lambda: __import__("app.routers.railway", fromlist=["get_railway_status"]).get_railway_status(),
|
||||
"vercel": lambda: __import__("app.routers.vercel", fromlist=["get_vercel_status"]).get_vercel_status(),
|
||||
"stripe": lambda: __import__("app.routers.stripe", fromlist=["get_stripe_status"]).get_stripe_status(),
|
||||
"twilio": lambda: __import__("app.routers.twilio", fromlist=["get_twilio_status"]).get_twilio_status(),
|
||||
"slack": lambda: __import__("app.routers.slack", fromlist=["get_slack_status"]).get_slack_status(),
|
||||
"discord": lambda: __import__("app.routers.discord", fromlist=["get_discord_status"]).get_discord_status(),
|
||||
"sentry": lambda: __import__("app.routers.sentry", fromlist=["get_sentry_status"]).get_sentry_status(),
|
||||
"github": check_github_status,
|
||||
"openai": check_openai_status,
|
||||
"huggingface": check_huggingface_status,
|
||||
"digitalocean": check_digitalocean_status,
|
||||
"aws": check_aws_status,
|
||||
}
|
||||
|
||||
if api_name.lower() not in api_checks:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"API '{api_name}' not found. Available APIs: {', '.join(api_checks.keys())}"
|
||||
)
|
||||
|
||||
check_func = api_checks[api_name.lower()]
|
||||
result = await check_api_status(api_name, check_func)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
# Helper functions for existing APIs
|
||||
|
||||
async def check_github_status():
|
||||
"""Check GitHub API status"""
|
||||
github_token = os.getenv("GITHUB_TOKEN")
|
||||
if not github_token:
|
||||
return {
|
||||
"connected": False,
|
||||
"message": "GitHub token not configured",
|
||||
"token_configured": False
|
||||
}
|
||||
|
||||
import httpx
|
||||
try:
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(
|
||||
"https://api.github.com/user",
|
||||
headers={"Authorization": f"token {github_token}"},
|
||||
timeout=10.0
|
||||
)
|
||||
response.raise_for_status()
|
||||
return {
|
||||
"connected": True,
|
||||
"message": "GitHub API connected successfully",
|
||||
"token_configured": True
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
"connected": False,
|
||||
"message": f"GitHub API connection failed: {str(e)}",
|
||||
"token_configured": True
|
||||
}
|
||||
|
||||
|
||||
async def check_openai_status():
|
||||
"""Check OpenAI API status"""
|
||||
openai_key = os.getenv("OPENAI_API_KEY")
|
||||
if not openai_key:
|
||||
return {
|
||||
"connected": False,
|
||||
"message": "OpenAI API key not configured",
|
||||
"key_configured": False
|
||||
}
|
||||
|
||||
import httpx
|
||||
try:
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(
|
||||
"https://api.openai.com/v1/models",
|
||||
headers={"Authorization": f"Bearer {openai_key}"},
|
||||
timeout=10.0
|
||||
)
|
||||
response.raise_for_status()
|
||||
return {
|
||||
"connected": True,
|
||||
"message": "OpenAI API connected successfully",
|
||||
"key_configured": True
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
"connected": False,
|
||||
"message": f"OpenAI API connection failed: {str(e)}",
|
||||
"key_configured": True
|
||||
}
|
||||
|
||||
|
||||
async def check_huggingface_status():
|
||||
"""Check Hugging Face API status"""
|
||||
hf_token = os.getenv("HUGGINGFACE_TOKEN")
|
||||
if not hf_token:
|
||||
return {
|
||||
"connected": False,
|
||||
"message": "Hugging Face token not configured",
|
||||
"token_configured": False
|
||||
}
|
||||
|
||||
import httpx
|
||||
try:
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(
|
||||
"https://huggingface.co/api/whoami-v2",
|
||||
headers={"Authorization": f"Bearer {hf_token}"},
|
||||
timeout=10.0
|
||||
)
|
||||
response.raise_for_status()
|
||||
return {
|
||||
"connected": True,
|
||||
"message": "Hugging Face API connected successfully",
|
||||
"token_configured": True
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
"connected": False,
|
||||
"message": f"Hugging Face API connection failed: {str(e)}",
|
||||
"token_configured": True
|
||||
}
|
||||
|
||||
|
||||
async def check_digitalocean_status():
|
||||
"""Check DigitalOcean API status"""
|
||||
do_token = os.getenv("DIGITALOCEAN_TOKEN")
|
||||
if not do_token:
|
||||
return {
|
||||
"connected": False,
|
||||
"message": "DigitalOcean token not configured",
|
||||
"token_configured": False
|
||||
}
|
||||
|
||||
import httpx
|
||||
try:
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(
|
||||
"https://api.digitalocean.com/v2/account",
|
||||
headers={"Authorization": f"Bearer {do_token}"},
|
||||
timeout=10.0
|
||||
)
|
||||
response.raise_for_status()
|
||||
return {
|
||||
"connected": True,
|
||||
"message": "DigitalOcean API connected successfully",
|
||||
"token_configured": True
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
"connected": False,
|
||||
"message": f"DigitalOcean API connection failed: {str(e)}",
|
||||
"token_configured": True
|
||||
}
|
||||
|
||||
|
||||
async def check_aws_status():
|
||||
"""Check AWS S3 status"""
|
||||
aws_key = os.getenv("AWS_ACCESS_KEY_ID")
|
||||
aws_secret = os.getenv("AWS_SECRET_ACCESS_KEY")
|
||||
|
||||
if not aws_key or not aws_secret:
|
||||
return {
|
||||
"connected": False,
|
||||
"message": "AWS credentials not configured",
|
||||
"key_configured": bool(aws_key),
|
||||
"secret_configured": bool(aws_secret)
|
||||
}
|
||||
|
||||
try:
|
||||
import boto3
|
||||
from botocore.exceptions import ClientError
|
||||
|
||||
s3 = boto3.client('s3')
|
||||
s3.list_buckets()
|
||||
|
||||
return {
|
||||
"connected": True,
|
||||
"message": "AWS S3 connected successfully",
|
||||
"key_configured": True,
|
||||
"secret_configured": True
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
"connected": False,
|
||||
"message": f"AWS S3 connection failed: {str(e)}",
|
||||
"key_configured": True,
|
||||
"secret_configured": True
|
||||
}
|
||||
308
backend/app/routers/discord.py
Normal file
308
backend/app/routers/discord.py
Normal file
@@ -0,0 +1,308 @@
|
||||
"""
|
||||
Discord API Integration Router
|
||||
|
||||
Provides endpoints for sending messages, managing channels, and interacting with Discord servers.
|
||||
"""
|
||||
|
||||
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/discord", tags=["discord"])
|
||||
|
||||
# Discord API configuration
|
||||
DISCORD_BOT_TOKEN = os.getenv("DISCORD_BOT_TOKEN")
|
||||
DISCORD_WEBHOOK_URL = os.getenv("DISCORD_WEBHOOK_URL")
|
||||
|
||||
|
||||
class DiscordMessage(BaseModel):
|
||||
"""Discord message model"""
|
||||
content: str
|
||||
embeds: Optional[List[Dict]] = None
|
||||
username: Optional[str] = None
|
||||
avatar_url: Optional[str] = None
|
||||
|
||||
|
||||
class DiscordEmbed(BaseModel):
|
||||
"""Discord embed model"""
|
||||
title: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
color: Optional[int] = 0x00ff00
|
||||
url: Optional[str] = None
|
||||
timestamp: Optional[str] = None
|
||||
fields: Optional[List[Dict]] = None
|
||||
|
||||
|
||||
class DiscordClient:
|
||||
"""Discord REST API client"""
|
||||
|
||||
def __init__(self, token: Optional[str] = None):
|
||||
self.token = token or DISCORD_BOT_TOKEN
|
||||
self.base_url = "https://discord.com/api/v10"
|
||||
|
||||
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="Discord bot token not configured"
|
||||
)
|
||||
|
||||
return {
|
||||
"Authorization": f"Bot {self.token}",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
async def _request(
|
||||
self,
|
||||
method: str,
|
||||
endpoint: str,
|
||||
json_data: Optional[Dict] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Make API request"""
|
||||
headers = self._get_headers()
|
||||
url = f"{self.base_url}{endpoint}"
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
try:
|
||||
response = await client.request(
|
||||
method,
|
||||
url,
|
||||
headers=headers,
|
||||
json=json_data,
|
||||
timeout=30.0
|
||||
)
|
||||
response.raise_for_status()
|
||||
|
||||
# Some endpoints return 204 No Content
|
||||
if response.status_code == 204:
|
||||
return {"success": True}
|
||||
|
||||
return response.json()
|
||||
|
||||
except httpx.HTTPStatusError as e:
|
||||
logger.error(f"Discord API error: {e.response.text}")
|
||||
raise HTTPException(
|
||||
status_code=e.response.status_code,
|
||||
detail=f"Discord API error: {e.response.text}"
|
||||
)
|
||||
except httpx.HTTPError as e:
|
||||
logger.error(f"Discord API request failed: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail=f"Discord API request failed: {str(e)}"
|
||||
)
|
||||
|
||||
async def send_message(
|
||||
self,
|
||||
channel_id: str,
|
||||
content: str,
|
||||
embeds: Optional[List[Dict]] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Send a message to a channel"""
|
||||
data = {"content": content}
|
||||
if embeds:
|
||||
data["embeds"] = embeds
|
||||
|
||||
return await self._request("POST", f"/channels/{channel_id}/messages", json_data=data)
|
||||
|
||||
async def get_channel(self, channel_id: str) -> Dict[str, Any]:
|
||||
"""Get channel information"""
|
||||
return await self._request("GET", f"/channels/{channel_id}")
|
||||
|
||||
async def get_guild(self, guild_id: str) -> Dict[str, Any]:
|
||||
"""Get guild (server) information"""
|
||||
return await self._request("GET", f"/guilds/{guild_id}")
|
||||
|
||||
async def list_guild_channels(self, guild_id: str) -> List[Dict[str, Any]]:
|
||||
"""List channels in a guild"""
|
||||
return await self._request("GET", f"/guilds/{guild_id}/channels")
|
||||
|
||||
async def get_user(self, user_id: str) -> Dict[str, Any]:
|
||||
"""Get user information"""
|
||||
return await self._request("GET", f"/users/{user_id}")
|
||||
|
||||
async def get_current_user(self) -> Dict[str, Any]:
|
||||
"""Get current bot user information"""
|
||||
return await self._request("GET", "/users/@me")
|
||||
|
||||
|
||||
async def send_webhook_message(
|
||||
content: str,
|
||||
embeds: Optional[List[Dict]] = None,
|
||||
username: Optional[str] = "BlackRoad OS",
|
||||
avatar_url: Optional[str] = None
|
||||
):
|
||||
"""Send message via webhook (doesn't require bot token)"""
|
||||
if not DISCORD_WEBHOOK_URL:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="Discord webhook URL not configured"
|
||||
)
|
||||
|
||||
data = {
|
||||
"content": content,
|
||||
"username": username
|
||||
}
|
||||
if embeds:
|
||||
data["embeds"] = embeds
|
||||
if avatar_url:
|
||||
data["avatar_url"] = avatar_url
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
try:
|
||||
response = await client.post(
|
||||
DISCORD_WEBHOOK_URL,
|
||||
json=data,
|
||||
timeout=10.0
|
||||
)
|
||||
response.raise_for_status()
|
||||
return {"success": True, "message": "Message sent via webhook"}
|
||||
|
||||
except httpx.HTTPError as e:
|
||||
logger.error(f"Discord webhook error: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail=f"Discord webhook failed: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
# Initialize client
|
||||
discord_client = DiscordClient()
|
||||
|
||||
|
||||
@router.get("/status")
|
||||
async def get_discord_status():
|
||||
"""Get Discord API connection status"""
|
||||
if not DISCORD_BOT_TOKEN:
|
||||
return {
|
||||
"connected": False,
|
||||
"message": "Discord bot token not configured. Set DISCORD_BOT_TOKEN environment variable.",
|
||||
"webhook_configured": bool(DISCORD_WEBHOOK_URL)
|
||||
}
|
||||
|
||||
try:
|
||||
# Test API connection by getting bot user info
|
||||
user = await discord_client.get_current_user()
|
||||
return {
|
||||
"connected": True,
|
||||
"message": "Discord API connected successfully",
|
||||
"bot_username": user.get("username"),
|
||||
"bot_id": user.get("id"),
|
||||
"webhook_configured": bool(DISCORD_WEBHOOK_URL)
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
"connected": False,
|
||||
"message": f"Discord API connection failed: {str(e)}",
|
||||
"webhook_configured": bool(DISCORD_WEBHOOK_URL)
|
||||
}
|
||||
|
||||
|
||||
@router.post("/channels/{channel_id}/messages")
|
||||
async def send_message(channel_id: str, content: str, embeds: Optional[List[Dict]] = None):
|
||||
"""Send a message to a Discord channel"""
|
||||
try:
|
||||
result = await discord_client.send_message(
|
||||
channel_id=channel_id,
|
||||
content=content,
|
||||
embeds=embeds
|
||||
)
|
||||
return {
|
||||
"success": True,
|
||||
"message_id": result.get("id"),
|
||||
"channel_id": result.get("channel_id")
|
||||
}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error sending message: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to send message: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.post("/webhook")
|
||||
async def send_webhook(message: DiscordMessage):
|
||||
"""Send message via incoming webhook"""
|
||||
try:
|
||||
result = await send_webhook_message(
|
||||
content=message.content,
|
||||
embeds=message.embeds,
|
||||
username=message.username or "BlackRoad OS",
|
||||
avatar_url=message.avatar_url
|
||||
)
|
||||
return result
|
||||
except HTTPException:
|
||||
raise
|
||||
|
||||
|
||||
@router.get("/channels/{channel_id}")
|
||||
async def get_channel(channel_id: str):
|
||||
"""Get channel information"""
|
||||
try:
|
||||
channel = await discord_client.get_channel(channel_id)
|
||||
return channel
|
||||
except HTTPException:
|
||||
raise
|
||||
|
||||
|
||||
@router.get("/guilds/{guild_id}")
|
||||
async def get_guild(guild_id: str):
|
||||
"""Get guild (server) information"""
|
||||
try:
|
||||
guild = await discord_client.get_guild(guild_id)
|
||||
return guild
|
||||
except HTTPException:
|
||||
raise
|
||||
|
||||
|
||||
@router.get("/guilds/{guild_id}/channels")
|
||||
async def list_channels(guild_id: str):
|
||||
"""List channels in a guild"""
|
||||
try:
|
||||
channels = await discord_client.list_guild_channels(guild_id)
|
||||
return {
|
||||
"channels": channels,
|
||||
"count": len(channels)
|
||||
}
|
||||
except HTTPException:
|
||||
raise
|
||||
|
||||
|
||||
@router.get("/users/{user_id}")
|
||||
async def get_user(user_id: str):
|
||||
"""Get user information"""
|
||||
try:
|
||||
user = await discord_client.get_user(user_id)
|
||||
return user
|
||||
except HTTPException:
|
||||
raise
|
||||
|
||||
|
||||
@router.get("/users/@me")
|
||||
async def get_current_user():
|
||||
"""Get current bot user information"""
|
||||
try:
|
||||
user = await discord_client.get_current_user()
|
||||
return user
|
||||
except HTTPException:
|
||||
raise
|
||||
|
||||
|
||||
@router.get("/health")
|
||||
async def discord_health_check():
|
||||
"""Discord API health check endpoint"""
|
||||
return {
|
||||
"service": "discord",
|
||||
"status": "operational" if DISCORD_BOT_TOKEN else "not_configured",
|
||||
"webhook_status": "operational" if DISCORD_WEBHOOK_URL else "not_configured",
|
||||
"timestamp": datetime.utcnow().isoformat()
|
||||
}
|
||||
393
backend/app/routers/railway.py
Normal file
393
backend/app/routers/railway.py
Normal file
@@ -0,0 +1,393 @@
|
||||
"""
|
||||
Railway API Integration Router
|
||||
|
||||
Provides endpoints for managing Railway deployments, projects, and services.
|
||||
Railway is a deployment platform that simplifies infrastructure management.
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Depends, 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/railway", tags=["railway"])
|
||||
|
||||
# Railway API configuration
|
||||
RAILWAY_API_URL = "https://backboard.railway.app/graphql"
|
||||
RAILWAY_TOKEN = os.getenv("RAILWAY_TOKEN")
|
||||
|
||||
|
||||
class RailwayProject(BaseModel):
|
||||
"""Railway project model"""
|
||||
id: str
|
||||
name: str
|
||||
description: Optional[str] = None
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
|
||||
class RailwayService(BaseModel):
|
||||
"""Railway service model"""
|
||||
id: str
|
||||
name: str
|
||||
project_id: str
|
||||
status: str
|
||||
created_at: datetime
|
||||
|
||||
|
||||
class RailwayDeployment(BaseModel):
|
||||
"""Railway deployment model"""
|
||||
id: str
|
||||
service_id: str
|
||||
status: str
|
||||
created_at: datetime
|
||||
url: Optional[str] = None
|
||||
environment: str = "production"
|
||||
|
||||
|
||||
class DeploymentCreate(BaseModel):
|
||||
"""Create deployment request"""
|
||||
project_id: str
|
||||
service_id: str
|
||||
environment: Optional[str] = "production"
|
||||
|
||||
|
||||
class RailwayVariable(BaseModel):
|
||||
"""Environment variable"""
|
||||
key: str
|
||||
value: str
|
||||
|
||||
|
||||
class RailwayClient:
|
||||
"""Railway GraphQL API client"""
|
||||
|
||||
def __init__(self, token: Optional[str] = None):
|
||||
self.token = token or RAILWAY_TOKEN
|
||||
self.api_url = RAILWAY_API_URL
|
||||
|
||||
async def _graphql_request(self, query: str, variables: Optional[Dict] = None) -> Dict[str, Any]:
|
||||
"""Execute GraphQL request"""
|
||||
if not self.token:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="Railway API token not configured"
|
||||
)
|
||||
|
||||
headers = {
|
||||
"Authorization": f"Bearer {self.token}",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
payload = {
|
||||
"query": query,
|
||||
"variables": variables or {}
|
||||
}
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
try:
|
||||
response = await client.post(
|
||||
self.api_url,
|
||||
json=payload,
|
||||
headers=headers,
|
||||
timeout=30.0
|
||||
)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
|
||||
if "errors" in data:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"GraphQL error: {data['errors']}"
|
||||
)
|
||||
|
||||
return data.get("data", {})
|
||||
|
||||
except httpx.HTTPError as e:
|
||||
logger.error(f"Railway API error: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail=f"Railway API request failed: {str(e)}"
|
||||
)
|
||||
|
||||
async def get_projects(self) -> List[Dict[str, Any]]:
|
||||
"""Get all projects"""
|
||||
query = """
|
||||
query {
|
||||
projects {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
name
|
||||
description
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
result = await self._graphql_request(query)
|
||||
edges = result.get("projects", {}).get("edges", [])
|
||||
return [edge["node"] for edge in edges]
|
||||
|
||||
async def get_project(self, project_id: str) -> Optional[Dict[str, Any]]:
|
||||
"""Get project by ID"""
|
||||
query = """
|
||||
query($id: String!) {
|
||||
project(id: $id) {
|
||||
id
|
||||
name
|
||||
description
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
"""
|
||||
result = await self._graphql_request(query, {"id": project_id})
|
||||
return result.get("project")
|
||||
|
||||
async def get_services(self, project_id: str) -> List[Dict[str, Any]]:
|
||||
"""Get services for a project"""
|
||||
query = """
|
||||
query($projectId: String!) {
|
||||
project(id: $projectId) {
|
||||
services {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
name
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
result = await self._graphql_request(query, {"projectId": project_id})
|
||||
edges = result.get("project", {}).get("services", {}).get("edges", [])
|
||||
return [edge["node"] for edge in edges]
|
||||
|
||||
async def get_deployments(self, service_id: str) -> List[Dict[str, Any]]:
|
||||
"""Get deployments for a service"""
|
||||
query = """
|
||||
query($serviceId: String!) {
|
||||
service(id: $serviceId) {
|
||||
deployments {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
status
|
||||
createdAt
|
||||
url
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
result = await self._graphql_request(query, {"serviceId": service_id})
|
||||
edges = result.get("service", {}).get("deployments", {}).get("edges", [])
|
||||
return [edge["node"] for edge in edges]
|
||||
|
||||
async def trigger_deployment(self, service_id: str) -> Dict[str, Any]:
|
||||
"""Trigger a new deployment"""
|
||||
query = """
|
||||
mutation($serviceId: String!) {
|
||||
serviceDeploy(serviceId: $serviceId) {
|
||||
id
|
||||
status
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
"""
|
||||
result = await self._graphql_request(query, {"serviceId": service_id})
|
||||
return result.get("serviceDeploy", {})
|
||||
|
||||
async def set_variables(
|
||||
self,
|
||||
project_id: str,
|
||||
environment_id: str,
|
||||
variables: Dict[str, str]
|
||||
) -> bool:
|
||||
"""Set environment variables"""
|
||||
query = """
|
||||
mutation($projectId: String!, $environmentId: String!, $variables: String!) {
|
||||
variableCollectionUpsert(
|
||||
input: {
|
||||
projectId: $projectId
|
||||
environmentId: $environmentId
|
||||
variables: $variables
|
||||
}
|
||||
)
|
||||
}
|
||||
"""
|
||||
import json
|
||||
variables_json = json.dumps(variables)
|
||||
|
||||
result = await self._graphql_request(
|
||||
query,
|
||||
{
|
||||
"projectId": project_id,
|
||||
"environmentId": environment_id,
|
||||
"variables": variables_json
|
||||
}
|
||||
)
|
||||
return bool(result.get("variableCollectionUpsert"))
|
||||
|
||||
|
||||
# Initialize client
|
||||
railway_client = RailwayClient()
|
||||
|
||||
|
||||
@router.get("/status")
|
||||
async def get_railway_status():
|
||||
"""Get Railway API connection status"""
|
||||
if not RAILWAY_TOKEN:
|
||||
return {
|
||||
"connected": False,
|
||||
"message": "Railway API token not configured. Set RAILWAY_TOKEN environment variable."
|
||||
}
|
||||
|
||||
try:
|
||||
# Try to fetch projects as a health check
|
||||
projects = await railway_client.get_projects()
|
||||
return {
|
||||
"connected": True,
|
||||
"message": "Railway API connected successfully",
|
||||
"project_count": len(projects)
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
"connected": False,
|
||||
"message": f"Railway API connection failed: {str(e)}"
|
||||
}
|
||||
|
||||
|
||||
@router.get("/projects", response_model=List[Dict[str, Any]])
|
||||
async def list_projects():
|
||||
"""List all Railway projects"""
|
||||
try:
|
||||
projects = await railway_client.get_projects()
|
||||
return 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"""
|
||||
project = await railway_client.get_project(project_id)
|
||||
if not project:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Project not found"
|
||||
)
|
||||
return project
|
||||
|
||||
|
||||
@router.get("/projects/{project_id}/services")
|
||||
async def list_services(project_id: str):
|
||||
"""List services in a project"""
|
||||
try:
|
||||
services = await railway_client.get_services(project_id)
|
||||
return services
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching services: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to fetch services: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/services/{service_id}/deployments")
|
||||
async def list_deployments(service_id: str):
|
||||
"""List deployments for a service"""
|
||||
try:
|
||||
deployments = await railway_client.get_deployments(service_id)
|
||||
return 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.post("/services/{service_id}/deploy")
|
||||
async def deploy_service(service_id: str):
|
||||
"""Trigger a new deployment for a service"""
|
||||
try:
|
||||
deployment = await railway_client.trigger_deployment(service_id)
|
||||
return {
|
||||
"success": True,
|
||||
"deployment": deployment,
|
||||
"message": "Deployment triggered successfully"
|
||||
}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error triggering deployment: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to trigger deployment: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.post("/projects/{project_id}/variables")
|
||||
async def update_variables(
|
||||
project_id: str,
|
||||
environment_id: str,
|
||||
variables: List[RailwayVariable]
|
||||
):
|
||||
"""Update environment variables for a project"""
|
||||
try:
|
||||
variables_dict = {var.key: var.value for var in variables}
|
||||
success = await railway_client.set_variables(
|
||||
project_id,
|
||||
environment_id,
|
||||
variables_dict
|
||||
)
|
||||
|
||||
if success:
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Variables updated successfully"
|
||||
}
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Failed to update variables"
|
||||
)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error updating variables: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to update variables: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/health")
|
||||
async def railway_health_check():
|
||||
"""Railway API health check endpoint"""
|
||||
return {
|
||||
"service": "railway",
|
||||
"status": "operational" if RAILWAY_TOKEN else "not_configured",
|
||||
"timestamp": datetime.utcnow().isoformat()
|
||||
}
|
||||
380
backend/app/routers/sentry.py
Normal file
380
backend/app/routers/sentry.py
Normal file
@@ -0,0 +1,380 @@
|
||||
"""
|
||||
Sentry Error Tracking Integration Router
|
||||
|
||||
Provides endpoints for error tracking, performance monitoring, and release management.
|
||||
Sentry is an application monitoring and error tracking platform.
|
||||
"""
|
||||
|
||||
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/sentry", tags=["sentry"])
|
||||
|
||||
# Sentry API configuration
|
||||
SENTRY_AUTH_TOKEN = os.getenv("SENTRY_AUTH_TOKEN")
|
||||
SENTRY_ORG = os.getenv("SENTRY_ORG")
|
||||
SENTRY_DSN = os.getenv("SENTRY_DSN")
|
||||
|
||||
|
||||
class SentryError(BaseModel):
|
||||
"""Sentry error/event model"""
|
||||
message: str
|
||||
level: str = "error" # debug, info, warning, error, fatal
|
||||
tags: Optional[Dict[str, str]] = None
|
||||
extra: Optional[Dict[str, Any]] = None
|
||||
|
||||
|
||||
class SentryRelease(BaseModel):
|
||||
"""Sentry release model"""
|
||||
version: str
|
||||
projects: List[str]
|
||||
ref: Optional[str] = None
|
||||
|
||||
|
||||
class SentryClient:
|
||||
"""Sentry REST API client"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
auth_token: Optional[str] = None,
|
||||
org: Optional[str] = None
|
||||
):
|
||||
self.auth_token = auth_token or SENTRY_AUTH_TOKEN
|
||||
self.org = org or SENTRY_ORG
|
||||
self.base_url = "https://sentry.io/api/0"
|
||||
|
||||
def _get_headers(self) -> Dict[str, str]:
|
||||
"""Get API request headers"""
|
||||
if not self.auth_token:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="Sentry auth token not configured"
|
||||
)
|
||||
|
||||
return {
|
||||
"Authorization": f"Bearer {self.auth_token}",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
async def _request(
|
||||
self,
|
||||
method: str,
|
||||
endpoint: str,
|
||||
json_data: Optional[Dict] = None,
|
||||
params: Optional[Dict] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Make API request"""
|
||||
headers = self._get_headers()
|
||||
url = f"{self.base_url}{endpoint}"
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
try:
|
||||
response = await client.request(
|
||||
method,
|
||||
url,
|
||||
headers=headers,
|
||||
json=json_data,
|
||||
params=params,
|
||||
timeout=30.0
|
||||
)
|
||||
response.raise_for_status()
|
||||
|
||||
# Handle 204 No Content
|
||||
if response.status_code == 204:
|
||||
return {"success": True}
|
||||
|
||||
return response.json()
|
||||
|
||||
except httpx.HTTPStatusError as e:
|
||||
logger.error(f"Sentry API error: {e.response.text}")
|
||||
raise HTTPException(
|
||||
status_code=e.response.status_code,
|
||||
detail=f"Sentry API error: {e.response.text}"
|
||||
)
|
||||
except httpx.HTTPError as e:
|
||||
logger.error(f"Sentry API request failed: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail=f"Sentry API request failed: {str(e)}"
|
||||
)
|
||||
|
||||
async def get_projects(self) -> List[Dict[str, Any]]:
|
||||
"""Get all projects in organization"""
|
||||
if not self.org:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Sentry organization not configured"
|
||||
)
|
||||
|
||||
return await self._request("GET", f"/organizations/{self.org}/projects/")
|
||||
|
||||
async def get_issues(
|
||||
self,
|
||||
project: str,
|
||||
limit: int = 25
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""Get issues for a project"""
|
||||
if not self.org:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Sentry organization not configured"
|
||||
)
|
||||
|
||||
params = {"limit": limit}
|
||||
return await self._request(
|
||||
"GET",
|
||||
f"/projects/{self.org}/{project}/issues/",
|
||||
params=params
|
||||
)
|
||||
|
||||
async def get_events(
|
||||
self,
|
||||
project: str,
|
||||
limit: int = 25
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""Get events for a project"""
|
||||
if not self.org:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Sentry organization not configured"
|
||||
)
|
||||
|
||||
params = {"limit": limit}
|
||||
return await self._request(
|
||||
"GET",
|
||||
f"/projects/{self.org}/{project}/events/",
|
||||
params=params
|
||||
)
|
||||
|
||||
async def create_release(
|
||||
self,
|
||||
version: str,
|
||||
projects: List[str],
|
||||
ref: Optional[str] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Create a new release"""
|
||||
if not self.org:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Sentry organization not configured"
|
||||
)
|
||||
|
||||
data = {
|
||||
"version": version,
|
||||
"projects": projects
|
||||
}
|
||||
if ref:
|
||||
data["ref"] = ref
|
||||
|
||||
return await self._request(
|
||||
"POST",
|
||||
f"/organizations/{self.org}/releases/",
|
||||
json_data=data
|
||||
)
|
||||
|
||||
async def list_releases(
|
||||
self,
|
||||
project: str,
|
||||
limit: int = 25
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""List releases for a project"""
|
||||
if not self.org:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Sentry organization not configured"
|
||||
)
|
||||
|
||||
params = {"limit": limit}
|
||||
return await self._request(
|
||||
"GET",
|
||||
f"/projects/{self.org}/{project}/releases/",
|
||||
params=params
|
||||
)
|
||||
|
||||
async def get_stats(
|
||||
self,
|
||||
project: str,
|
||||
stat: str = "received"
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""Get project statistics"""
|
||||
if not self.org:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Sentry organization not configured"
|
||||
)
|
||||
|
||||
params = {"stat": stat}
|
||||
return await self._request(
|
||||
"GET",
|
||||
f"/projects/{self.org}/{project}/stats/",
|
||||
params=params
|
||||
)
|
||||
|
||||
|
||||
# Initialize client
|
||||
sentry_client = SentryClient()
|
||||
|
||||
|
||||
@router.get("/status")
|
||||
async def get_sentry_status():
|
||||
"""Get Sentry API connection status"""
|
||||
if not SENTRY_AUTH_TOKEN:
|
||||
return {
|
||||
"connected": False,
|
||||
"message": "Sentry auth token not configured. Set SENTRY_AUTH_TOKEN environment variable.",
|
||||
"org_configured": bool(SENTRY_ORG),
|
||||
"dsn_configured": bool(SENTRY_DSN)
|
||||
}
|
||||
|
||||
try:
|
||||
# Test API connection
|
||||
projects = await sentry_client.get_projects()
|
||||
return {
|
||||
"connected": True,
|
||||
"message": "Sentry API connected successfully",
|
||||
"organization": SENTRY_ORG,
|
||||
"project_count": len(projects),
|
||||
"dsn_configured": bool(SENTRY_DSN)
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
"connected": False,
|
||||
"message": f"Sentry API connection failed: {str(e)}",
|
||||
"org_configured": bool(SENTRY_ORG),
|
||||
"dsn_configured": bool(SENTRY_DSN)
|
||||
}
|
||||
|
||||
|
||||
@router.get("/projects")
|
||||
async def list_projects():
|
||||
"""List all Sentry projects"""
|
||||
try:
|
||||
projects = await sentry_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}/issues")
|
||||
async def list_issues(project: str, limit: int = 25):
|
||||
"""List issues for a project"""
|
||||
try:
|
||||
issues = await sentry_client.get_issues(project, limit)
|
||||
return {
|
||||
"issues": issues,
|
||||
"count": len(issues)
|
||||
}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching issues: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to fetch issues: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/projects/{project}/events")
|
||||
async def list_events(project: str, limit: int = 25):
|
||||
"""List events for a project"""
|
||||
try:
|
||||
events = await sentry_client.get_events(project, limit)
|
||||
return {
|
||||
"events": events,
|
||||
"count": len(events)
|
||||
}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching events: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to fetch events: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.post("/releases")
|
||||
async def create_release(release: SentryRelease):
|
||||
"""Create a new release"""
|
||||
try:
|
||||
result = await sentry_client.create_release(
|
||||
version=release.version,
|
||||
projects=release.projects,
|
||||
ref=release.ref
|
||||
)
|
||||
return {
|
||||
"success": True,
|
||||
"release": result
|
||||
}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating release: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to create release: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/projects/{project}/releases")
|
||||
async def list_releases(project: str, limit: int = 25):
|
||||
"""List releases for a project"""
|
||||
try:
|
||||
releases = await sentry_client.list_releases(project, limit)
|
||||
return {
|
||||
"releases": releases,
|
||||
"count": len(releases)
|
||||
}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching releases: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to fetch releases: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/projects/{project}/stats")
|
||||
async def get_stats(project: str, stat: str = "received"):
|
||||
"""Get project statistics"""
|
||||
try:
|
||||
stats = await sentry_client.get_stats(project, stat)
|
||||
return {
|
||||
"stats": stats,
|
||||
"stat_type": stat
|
||||
}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching stats: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to fetch stats: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/health")
|
||||
async def sentry_health_check():
|
||||
"""Sentry API health check endpoint"""
|
||||
return {
|
||||
"service": "sentry",
|
||||
"status": "operational" if SENTRY_AUTH_TOKEN else "not_configured",
|
||||
"timestamp": datetime.utcnow().isoformat()
|
||||
}
|
||||
281
backend/app/routers/slack.py
Normal file
281
backend/app/routers/slack.py
Normal file
@@ -0,0 +1,281 @@
|
||||
"""
|
||||
Slack API Integration Router
|
||||
|
||||
Provides endpoints for sending messages, managing channels, and interacting with Slack workspaces.
|
||||
"""
|
||||
|
||||
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/slack", tags=["slack"])
|
||||
|
||||
# Slack API configuration
|
||||
SLACK_BOT_TOKEN = os.getenv("SLACK_BOT_TOKEN")
|
||||
SLACK_WEBHOOK_URL = os.getenv("SLACK_WEBHOOK_URL")
|
||||
|
||||
|
||||
class SlackMessage(BaseModel):
|
||||
"""Slack message model"""
|
||||
channel: str
|
||||
text: str
|
||||
blocks: Optional[List[Dict]] = None
|
||||
thread_ts: Optional[str] = None
|
||||
|
||||
|
||||
class WebhookMessage(BaseModel):
|
||||
"""Webhook message model"""
|
||||
text: str
|
||||
username: Optional[str] = "BlackRoad OS"
|
||||
icon_emoji: Optional[str] = ":robot_face:"
|
||||
|
||||
|
||||
class SlackClient:
|
||||
"""Slack Web API client"""
|
||||
|
||||
def __init__(self, token: Optional[str] = None):
|
||||
self.token = token or SLACK_BOT_TOKEN
|
||||
self.base_url = "https://slack.com/api"
|
||||
|
||||
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="Slack bot token not configured"
|
||||
)
|
||||
|
||||
return {
|
||||
"Authorization": f"Bearer {self.token}",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
async def _request(
|
||||
self,
|
||||
method: str,
|
||||
endpoint: str,
|
||||
json_data: Optional[Dict] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Make API request"""
|
||||
headers = self._get_headers()
|
||||
url = f"{self.base_url}/{endpoint}"
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
try:
|
||||
response = await client.request(
|
||||
method,
|
||||
url,
|
||||
headers=headers,
|
||||
json=json_data,
|
||||
timeout=30.0
|
||||
)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
|
||||
# Slack returns 200 with ok:false for errors
|
||||
if not data.get("ok", False):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Slack API error: {data.get('error', 'Unknown error')}"
|
||||
)
|
||||
|
||||
return data
|
||||
|
||||
except httpx.HTTPStatusError as e:
|
||||
logger.error(f"Slack API error: {e.response.text}")
|
||||
raise HTTPException(
|
||||
status_code=e.response.status_code,
|
||||
detail=f"Slack API error: {e.response.text}"
|
||||
)
|
||||
except httpx.HTTPError as e:
|
||||
logger.error(f"Slack API request failed: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail=f"Slack API request failed: {str(e)}"
|
||||
)
|
||||
|
||||
async def post_message(
|
||||
self,
|
||||
channel: str,
|
||||
text: str,
|
||||
blocks: Optional[List[Dict]] = None,
|
||||
thread_ts: Optional[str] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Post a message to a channel"""
|
||||
data = {
|
||||
"channel": channel,
|
||||
"text": text
|
||||
}
|
||||
if blocks:
|
||||
data["blocks"] = blocks
|
||||
if thread_ts:
|
||||
data["thread_ts"] = thread_ts
|
||||
|
||||
return await self._request("POST", "chat.postMessage", json_data=data)
|
||||
|
||||
async def list_channels(self) -> Dict[str, Any]:
|
||||
"""List public channels"""
|
||||
return await self._request("GET", "conversations.list")
|
||||
|
||||
async def get_user_info(self, user_id: str) -> Dict[str, Any]:
|
||||
"""Get user information"""
|
||||
return await self._request("POST", "users.info", json_data={"user": user_id})
|
||||
|
||||
async def upload_file(
|
||||
self,
|
||||
channels: str,
|
||||
content: str,
|
||||
filename: str,
|
||||
title: Optional[str] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Upload a file"""
|
||||
data = {
|
||||
"channels": channels,
|
||||
"content": content,
|
||||
"filename": filename
|
||||
}
|
||||
if title:
|
||||
data["title"] = title
|
||||
|
||||
return await self._request("POST", "files.upload", json_data=data)
|
||||
|
||||
|
||||
async def send_webhook_message(text: str, username: str = "BlackRoad OS", icon_emoji: str = ":robot_face:"):
|
||||
"""Send message via webhook (doesn't require bot token)"""
|
||||
if not SLACK_WEBHOOK_URL:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="Slack webhook URL not configured"
|
||||
)
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
try:
|
||||
response = await client.post(
|
||||
SLACK_WEBHOOK_URL,
|
||||
json={
|
||||
"text": text,
|
||||
"username": username,
|
||||
"icon_emoji": icon_emoji
|
||||
},
|
||||
timeout=10.0
|
||||
)
|
||||
response.raise_for_status()
|
||||
return {"success": True, "message": "Message sent via webhook"}
|
||||
|
||||
except httpx.HTTPError as e:
|
||||
logger.error(f"Slack webhook error: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail=f"Slack webhook failed: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
# Initialize client
|
||||
slack_client = SlackClient()
|
||||
|
||||
|
||||
@router.get("/status")
|
||||
async def get_slack_status():
|
||||
"""Get Slack API connection status"""
|
||||
if not SLACK_BOT_TOKEN:
|
||||
return {
|
||||
"connected": False,
|
||||
"message": "Slack bot token not configured. Set SLACK_BOT_TOKEN environment variable.",
|
||||
"webhook_configured": bool(SLACK_WEBHOOK_URL)
|
||||
}
|
||||
|
||||
try:
|
||||
# Test API connection
|
||||
result = await slack_client._request("POST", "auth.test")
|
||||
return {
|
||||
"connected": True,
|
||||
"message": "Slack API connected successfully",
|
||||
"team": result.get("team"),
|
||||
"user": result.get("user"),
|
||||
"webhook_configured": bool(SLACK_WEBHOOK_URL)
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
"connected": False,
|
||||
"message": f"Slack API connection failed: {str(e)}",
|
||||
"webhook_configured": bool(SLACK_WEBHOOK_URL)
|
||||
}
|
||||
|
||||
|
||||
@router.post("/messages")
|
||||
async def post_message(message: SlackMessage):
|
||||
"""Post a message to a Slack channel"""
|
||||
try:
|
||||
result = await slack_client.post_message(
|
||||
channel=message.channel,
|
||||
text=message.text,
|
||||
blocks=message.blocks,
|
||||
thread_ts=message.thread_ts
|
||||
)
|
||||
return {
|
||||
"success": True,
|
||||
"ts": result.get("ts"),
|
||||
"channel": result.get("channel")
|
||||
}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error posting message: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to post message: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.post("/webhook")
|
||||
async def send_webhook(message: WebhookMessage):
|
||||
"""Send message via incoming webhook"""
|
||||
try:
|
||||
result = await send_webhook_message(
|
||||
text=message.text,
|
||||
username=message.username,
|
||||
icon_emoji=message.icon_emoji
|
||||
)
|
||||
return result
|
||||
except HTTPException:
|
||||
raise
|
||||
|
||||
|
||||
@router.get("/channels")
|
||||
async def list_channels():
|
||||
"""List Slack channels"""
|
||||
try:
|
||||
result = await slack_client.list_channels()
|
||||
return {
|
||||
"channels": result.get("channels", []),
|
||||
"count": len(result.get("channels", []))
|
||||
}
|
||||
except HTTPException:
|
||||
raise
|
||||
|
||||
|
||||
@router.get("/users/{user_id}")
|
||||
async def get_user(user_id: str):
|
||||
"""Get user information"""
|
||||
try:
|
||||
result = await slack_client.get_user_info(user_id)
|
||||
return result.get("user", {})
|
||||
except HTTPException:
|
||||
raise
|
||||
|
||||
|
||||
@router.get("/health")
|
||||
async def slack_health_check():
|
||||
"""Slack API health check endpoint"""
|
||||
return {
|
||||
"service": "slack",
|
||||
"status": "operational" if SLACK_BOT_TOKEN else "not_configured",
|
||||
"webhook_status": "operational" if SLACK_WEBHOOK_URL else "not_configured",
|
||||
"timestamp": datetime.utcnow().isoformat()
|
||||
}
|
||||
328
backend/app/routers/stripe.py
Normal file
328
backend/app/routers/stripe.py
Normal file
@@ -0,0 +1,328 @@
|
||||
"""
|
||||
Stripe API Integration Router
|
||||
|
||||
Provides endpoints for payment processing, subscriptions, and billing.
|
||||
Stripe is a payment processing platform for online businesses.
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, HTTPException, status
|
||||
from pydantic import BaseModel, EmailStr
|
||||
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/stripe", tags=["stripe"])
|
||||
|
||||
# Stripe API configuration
|
||||
STRIPE_API_URL = "https://api.stripe.com/v1"
|
||||
STRIPE_SECRET_KEY = os.getenv("STRIPE_SECRET_KEY")
|
||||
STRIPE_PUBLISHABLE_KEY = os.getenv("STRIPE_PUBLISHABLE_KEY")
|
||||
|
||||
|
||||
class PaymentIntent(BaseModel):
|
||||
"""Payment intent model"""
|
||||
amount: int # Amount in cents
|
||||
currency: str = "usd"
|
||||
description: Optional[str] = None
|
||||
metadata: Optional[Dict[str, str]] = None
|
||||
|
||||
|
||||
class Customer(BaseModel):
|
||||
"""Customer model"""
|
||||
email: EmailStr
|
||||
name: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
metadata: Optional[Dict[str, str]] = None
|
||||
|
||||
|
||||
class StripeClient:
|
||||
"""Stripe REST API client"""
|
||||
|
||||
def __init__(self, secret_key: Optional[str] = None):
|
||||
self.secret_key = secret_key or STRIPE_SECRET_KEY
|
||||
self.base_url = STRIPE_API_URL
|
||||
|
||||
def _get_headers(self) -> Dict[str, str]:
|
||||
"""Get API request headers"""
|
||||
if not self.secret_key:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="Stripe API key not configured"
|
||||
)
|
||||
|
||||
return {
|
||||
"Authorization": f"Bearer {self.secret_key}",
|
||||
"Content-Type": "application/x-www-form-urlencoded"
|
||||
}
|
||||
|
||||
async def _request(
|
||||
self,
|
||||
method: str,
|
||||
endpoint: str,
|
||||
data: Optional[Dict] = None,
|
||||
params: Optional[Dict] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Make API request"""
|
||||
headers = self._get_headers()
|
||||
url = f"{self.base_url}{endpoint}"
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
try:
|
||||
response = await client.request(
|
||||
method,
|
||||
url,
|
||||
headers=headers,
|
||||
data=data,
|
||||
params=params,
|
||||
timeout=30.0
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
except httpx.HTTPStatusError as e:
|
||||
logger.error(f"Stripe API error: {e.response.text}")
|
||||
error_data = e.response.json().get("error", {})
|
||||
raise HTTPException(
|
||||
status_code=e.response.status_code,
|
||||
detail=error_data.get("message", "Stripe API error")
|
||||
)
|
||||
except httpx.HTTPError as e:
|
||||
logger.error(f"Stripe API request failed: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail=f"Stripe API request failed: {str(e)}"
|
||||
)
|
||||
|
||||
async def create_payment_intent(
|
||||
self,
|
||||
amount: int,
|
||||
currency: str = "usd",
|
||||
description: Optional[str] = None,
|
||||
metadata: Optional[Dict] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Create a payment intent"""
|
||||
data = {
|
||||
"amount": amount,
|
||||
"currency": currency
|
||||
}
|
||||
if description:
|
||||
data["description"] = description
|
||||
if metadata:
|
||||
for key, value in metadata.items():
|
||||
data[f"metadata[{key}]"] = value
|
||||
|
||||
return await self._request("POST", "/payment_intents", data=data)
|
||||
|
||||
async def get_payment_intent(self, payment_intent_id: str) -> Dict[str, Any]:
|
||||
"""Get payment intent details"""
|
||||
return await self._request("GET", f"/payment_intents/{payment_intent_id}")
|
||||
|
||||
async def create_customer(
|
||||
self,
|
||||
email: str,
|
||||
name: Optional[str] = None,
|
||||
description: Optional[str] = None,
|
||||
metadata: Optional[Dict] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Create a customer"""
|
||||
data = {"email": email}
|
||||
if name:
|
||||
data["name"] = name
|
||||
if description:
|
||||
data["description"] = description
|
||||
if metadata:
|
||||
for key, value in metadata.items():
|
||||
data[f"metadata[{key}]"] = value
|
||||
|
||||
return await self._request("POST", "/customers", data=data)
|
||||
|
||||
async def get_customer(self, customer_id: str) -> Dict[str, Any]:
|
||||
"""Get customer details"""
|
||||
return await self._request("GET", f"/customers/{customer_id}")
|
||||
|
||||
async def list_customers(self, limit: int = 10) -> Dict[str, Any]:
|
||||
"""List customers"""
|
||||
params = {"limit": limit}
|
||||
return await self._request("GET", "/customers", params=params)
|
||||
|
||||
async def create_subscription(
|
||||
self,
|
||||
customer_id: str,
|
||||
price_id: str,
|
||||
metadata: Optional[Dict] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Create a subscription"""
|
||||
data = {
|
||||
"customer": customer_id,
|
||||
"items[0][price]": price_id
|
||||
}
|
||||
if metadata:
|
||||
for key, value in metadata.items():
|
||||
data[f"metadata[{key}]"] = value
|
||||
|
||||
return await self._request("POST", "/subscriptions", data=data)
|
||||
|
||||
async def list_products(self, limit: int = 10) -> Dict[str, Any]:
|
||||
"""List products"""
|
||||
params = {"limit": limit}
|
||||
return await self._request("GET", "/products", params=params)
|
||||
|
||||
async def list_prices(self, limit: int = 10) -> Dict[str, Any]:
|
||||
"""List prices"""
|
||||
params = {"limit": limit}
|
||||
return await self._request("GET", "/prices", params=params)
|
||||
|
||||
async def get_balance(self) -> Dict[str, Any]:
|
||||
"""Get account balance"""
|
||||
return await self._request("GET", "/balance")
|
||||
|
||||
|
||||
# Initialize client
|
||||
stripe_client = StripeClient()
|
||||
|
||||
|
||||
@router.get("/status")
|
||||
async def get_stripe_status():
|
||||
"""Get Stripe API connection status"""
|
||||
if not STRIPE_SECRET_KEY:
|
||||
return {
|
||||
"connected": False,
|
||||
"message": "Stripe API key not configured. Set STRIPE_SECRET_KEY environment variable."
|
||||
}
|
||||
|
||||
try:
|
||||
# Try to fetch balance as a health check
|
||||
await stripe_client.get_balance()
|
||||
return {
|
||||
"connected": True,
|
||||
"message": "Stripe API connected successfully",
|
||||
"publishable_key_configured": bool(STRIPE_PUBLISHABLE_KEY)
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
"connected": False,
|
||||
"message": f"Stripe API connection failed: {str(e)}"
|
||||
}
|
||||
|
||||
|
||||
@router.post("/payment-intents")
|
||||
async def create_payment_intent(payment: PaymentIntent):
|
||||
"""Create a payment intent"""
|
||||
try:
|
||||
intent = await stripe_client.create_payment_intent(
|
||||
amount=payment.amount,
|
||||
currency=payment.currency,
|
||||
description=payment.description,
|
||||
metadata=payment.metadata
|
||||
)
|
||||
return {
|
||||
"success": True,
|
||||
"payment_intent": intent,
|
||||
"client_secret": intent.get("client_secret")
|
||||
}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating payment intent: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to create payment intent: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/payment-intents/{payment_intent_id}")
|
||||
async def get_payment_intent(payment_intent_id: str):
|
||||
"""Get payment intent details"""
|
||||
try:
|
||||
intent = await stripe_client.get_payment_intent(payment_intent_id)
|
||||
return intent
|
||||
except HTTPException:
|
||||
raise
|
||||
|
||||
|
||||
@router.post("/customers")
|
||||
async def create_customer(customer: Customer):
|
||||
"""Create a customer"""
|
||||
try:
|
||||
result = await stripe_client.create_customer(
|
||||
email=customer.email,
|
||||
name=customer.name,
|
||||
description=customer.description,
|
||||
metadata=customer.metadata
|
||||
)
|
||||
return {
|
||||
"success": True,
|
||||
"customer": result
|
||||
}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating customer: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to create customer: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/customers/{customer_id}")
|
||||
async def get_customer(customer_id: str):
|
||||
"""Get customer details"""
|
||||
try:
|
||||
customer = await stripe_client.get_customer(customer_id)
|
||||
return customer
|
||||
except HTTPException:
|
||||
raise
|
||||
|
||||
|
||||
@router.get("/customers")
|
||||
async def list_customers(limit: int = 10):
|
||||
"""List customers"""
|
||||
try:
|
||||
result = await stripe_client.list_customers(limit)
|
||||
return result
|
||||
except HTTPException:
|
||||
raise
|
||||
|
||||
|
||||
@router.get("/products")
|
||||
async def list_products(limit: int = 10):
|
||||
"""List products"""
|
||||
try:
|
||||
result = await stripe_client.list_products(limit)
|
||||
return result
|
||||
except HTTPException:
|
||||
raise
|
||||
|
||||
|
||||
@router.get("/prices")
|
||||
async def list_prices(limit: int = 10):
|
||||
"""List prices"""
|
||||
try:
|
||||
result = await stripe_client.list_prices(limit)
|
||||
return result
|
||||
except HTTPException:
|
||||
raise
|
||||
|
||||
|
||||
@router.get("/balance")
|
||||
async def get_balance():
|
||||
"""Get account balance"""
|
||||
try:
|
||||
balance = await stripe_client.get_balance()
|
||||
return balance
|
||||
except HTTPException:
|
||||
raise
|
||||
|
||||
|
||||
@router.get("/health")
|
||||
async def stripe_health_check():
|
||||
"""Stripe API health check endpoint"""
|
||||
return {
|
||||
"service": "stripe",
|
||||
"status": "operational" if STRIPE_SECRET_KEY else "not_configured",
|
||||
"timestamp": datetime.utcnow().isoformat()
|
||||
}
|
||||
259
backend/app/routers/twilio.py
Normal file
259
backend/app/routers/twilio.py
Normal file
@@ -0,0 +1,259 @@
|
||||
"""
|
||||
Twilio API Integration Router
|
||||
|
||||
Provides endpoints for SMS, voice calls, and WhatsApp messaging.
|
||||
Twilio is a cloud communications platform.
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, HTTPException, status
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional, Dict, Any
|
||||
from datetime import datetime
|
||||
import httpx
|
||||
import base64
|
||||
import os
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(prefix="/api/twilio", tags=["twilio"])
|
||||
|
||||
# Twilio API configuration
|
||||
TWILIO_ACCOUNT_SID = os.getenv("TWILIO_ACCOUNT_SID")
|
||||
TWILIO_AUTH_TOKEN = os.getenv("TWILIO_AUTH_TOKEN")
|
||||
TWILIO_PHONE_NUMBER = os.getenv("TWILIO_PHONE_NUMBER")
|
||||
|
||||
|
||||
class SMSMessage(BaseModel):
|
||||
"""SMS message model"""
|
||||
to: str
|
||||
message: str
|
||||
from_number: Optional[str] = None
|
||||
|
||||
|
||||
class WhatsAppMessage(BaseModel):
|
||||
"""WhatsApp message model"""
|
||||
to: str
|
||||
message: str
|
||||
|
||||
|
||||
class TwilioClient:
|
||||
"""Twilio REST API client"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
account_sid: Optional[str] = None,
|
||||
auth_token: Optional[str] = None,
|
||||
phone_number: Optional[str] = None
|
||||
):
|
||||
self.account_sid = account_sid or TWILIO_ACCOUNT_SID
|
||||
self.auth_token = auth_token or TWILIO_AUTH_TOKEN
|
||||
self.phone_number = phone_number or TWILIO_PHONE_NUMBER
|
||||
self.base_url = f"https://api.twilio.com/2010-04-01/Accounts/{self.account_sid}"
|
||||
|
||||
def _get_headers(self) -> Dict[str, str]:
|
||||
"""Get API request headers with basic auth"""
|
||||
if not self.account_sid or not self.auth_token:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="Twilio credentials not configured"
|
||||
)
|
||||
|
||||
# Create basic auth header
|
||||
credentials = f"{self.account_sid}:{self.auth_token}"
|
||||
encoded = base64.b64encode(credentials.encode()).decode()
|
||||
|
||||
return {
|
||||
"Authorization": f"Basic {encoded}",
|
||||
"Content-Type": "application/x-www-form-urlencoded"
|
||||
}
|
||||
|
||||
async def _request(
|
||||
self,
|
||||
method: str,
|
||||
endpoint: str,
|
||||
data: Optional[Dict] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Make API request"""
|
||||
headers = self._get_headers()
|
||||
url = f"{self.base_url}{endpoint}"
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
try:
|
||||
response = await client.request(
|
||||
method,
|
||||
url,
|
||||
headers=headers,
|
||||
data=data,
|
||||
timeout=30.0
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
except httpx.HTTPStatusError as e:
|
||||
logger.error(f"Twilio API error: {e.response.text}")
|
||||
raise HTTPException(
|
||||
status_code=e.response.status_code,
|
||||
detail=f"Twilio API error: {e.response.text}"
|
||||
)
|
||||
except httpx.HTTPError as e:
|
||||
logger.error(f"Twilio API request failed: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail=f"Twilio API request failed: {str(e)}"
|
||||
)
|
||||
|
||||
async def send_sms(
|
||||
self,
|
||||
to: str,
|
||||
body: str,
|
||||
from_: Optional[str] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Send SMS message"""
|
||||
data = {
|
||||
"To": to,
|
||||
"From": from_ or self.phone_number,
|
||||
"Body": body
|
||||
}
|
||||
|
||||
if not data["From"]:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Twilio phone number not configured"
|
||||
)
|
||||
|
||||
return await self._request("POST", "/Messages.json", data=data)
|
||||
|
||||
async def send_whatsapp(
|
||||
self,
|
||||
to: str,
|
||||
body: str
|
||||
) -> Dict[str, Any]:
|
||||
"""Send WhatsApp message"""
|
||||
data = {
|
||||
"To": f"whatsapp:{to}",
|
||||
"From": f"whatsapp:{self.phone_number}",
|
||||
"Body": body
|
||||
}
|
||||
|
||||
if not self.phone_number:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Twilio phone number not configured"
|
||||
)
|
||||
|
||||
return await self._request("POST", "/Messages.json", data=data)
|
||||
|
||||
async def get_message(self, message_sid: str) -> Dict[str, Any]:
|
||||
"""Get message details"""
|
||||
return await self._request("GET", f"/Messages/{message_sid}.json")
|
||||
|
||||
async def list_messages(self, limit: int = 20) -> Dict[str, Any]:
|
||||
"""List messages"""
|
||||
data = {"PageSize": limit}
|
||||
return await self._request("GET", "/Messages.json", data=data)
|
||||
|
||||
|
||||
# Initialize client
|
||||
twilio_client = TwilioClient()
|
||||
|
||||
|
||||
@router.get("/status")
|
||||
async def get_twilio_status():
|
||||
"""Get Twilio API connection status"""
|
||||
if not TWILIO_ACCOUNT_SID or not TWILIO_AUTH_TOKEN:
|
||||
return {
|
||||
"connected": False,
|
||||
"message": "Twilio credentials not configured. Set TWILIO_ACCOUNT_SID and TWILIO_AUTH_TOKEN."
|
||||
}
|
||||
|
||||
try:
|
||||
# Try to list messages as a health check
|
||||
await twilio_client.list_messages(limit=1)
|
||||
return {
|
||||
"connected": True,
|
||||
"message": "Twilio API connected successfully",
|
||||
"phone_number_configured": bool(TWILIO_PHONE_NUMBER)
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
"connected": False,
|
||||
"message": f"Twilio API connection failed: {str(e)}"
|
||||
}
|
||||
|
||||
|
||||
@router.post("/sms")
|
||||
async def send_sms(message: SMSMessage):
|
||||
"""Send SMS message"""
|
||||
try:
|
||||
result = await twilio_client.send_sms(
|
||||
to=message.to,
|
||||
body=message.message,
|
||||
from_=message.from_number
|
||||
)
|
||||
return {
|
||||
"success": True,
|
||||
"message": result,
|
||||
"sid": result.get("sid")
|
||||
}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error sending SMS: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to send SMS: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.post("/whatsapp")
|
||||
async def send_whatsapp(message: WhatsAppMessage):
|
||||
"""Send WhatsApp message"""
|
||||
try:
|
||||
result = await twilio_client.send_whatsapp(
|
||||
to=message.to,
|
||||
body=message.message
|
||||
)
|
||||
return {
|
||||
"success": True,
|
||||
"message": result,
|
||||
"sid": result.get("sid")
|
||||
}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error sending WhatsApp: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to send WhatsApp: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/messages/{message_sid}")
|
||||
async def get_message(message_sid: str):
|
||||
"""Get message details"""
|
||||
try:
|
||||
message = await twilio_client.get_message(message_sid)
|
||||
return message
|
||||
except HTTPException:
|
||||
raise
|
||||
|
||||
|
||||
@router.get("/messages")
|
||||
async def list_messages(limit: int = 20):
|
||||
"""List messages"""
|
||||
try:
|
||||
result = await twilio_client.list_messages(limit)
|
||||
return result
|
||||
except HTTPException:
|
||||
raise
|
||||
|
||||
|
||||
@router.get("/health")
|
||||
async def twilio_health_check():
|
||||
"""Twilio API health check endpoint"""
|
||||
return {
|
||||
"service": "twilio",
|
||||
"status": "operational" if (TWILIO_ACCOUNT_SID and TWILIO_AUTH_TOKEN) else "not_configured",
|
||||
"timestamp": datetime.utcnow().isoformat()
|
||||
}
|
||||
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