Coverage for src/alprina_cli/api/main.py: 69%

64 statements  

« prev     ^ index     » next       coverage.py v7.11.3, created at 2025-11-14 11:27 +0100

1""" 

2Alprina API - Main Application 

3FastAPI-based REST API for security scanning. 

4""" 

5 

6from fastapi import FastAPI, HTTPException, Depends, Header, Request 

7from fastapi.responses import JSONResponse 

8from fastapi.middleware.cors import CORSMiddleware 

9from typing import Optional 

10import sys 

11import os 

12from pathlib import Path 

13 

14# Add parent directory to path to import existing modules 

15sys.path.insert(0, str(Path(__file__).parent.parent)) 

16 

17from ..security_engine import run_agent, run_local_scan, AGENTS_AVAILABLE 

18from ..agent_bridge import SecurityAgentBridge 

19 

20# Initialize FastAPI app 

21app = FastAPI( 

22 title="Alprina API", 

23 description="AI-powered security scanning and vulnerability detection", 

24 version="1.0.0", 

25 docs_url="/docs", 

26 redoc_url="/redoc", 

27) 

28 

29# CORS configuration - allow specific origins for production 

30# Note: Can't use allow_origins=["*"] with allow_credentials=True 

31CORS_ORIGINS = os.getenv("CORS_ORIGINS", "https://alprina.com,https://www.alprina.com,http://localhost:3000") 

32 

33# Split comma-separated origins 

34origins = [origin.strip() for origin in CORS_ORIGINS.split(",")] 

35 

36# Add CORS middleware 

37app.add_middleware( 

38 CORSMiddleware, 

39 allow_origins=origins, # Specific origins (not wildcard) 

40 allow_credentials=True, # Required for Authorization headers 

41 allow_methods=["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"], 

42 allow_headers=["*"], 

43 expose_headers=["*"], 

44) 

45 

46# Import route modules 

47from .routes import scan, agents, auth, scans, device_auth, polar_webhooks, billing, subscription, alerts, insights, github_webhooks, team, dashboard, badge, cron 

48 

49# Import services for startup/shutdown 

50from .services.neon_service import neon_service 

51 

52# Include routers 

53app.include_router(scan.router, prefix="/v1", tags=["Scanning"]) 

54app.include_router(agents.router, prefix="/v1", tags=["Agents"]) 

55app.include_router(auth.router, prefix="/v1", tags=["Authentication"]) 

56app.include_router(scans.router, prefix="/v1", tags=["Scan Management"]) 

57app.include_router(device_auth.router, prefix="/v1", tags=["Device Authorization"]) 

58app.include_router(polar_webhooks.router, prefix="/v1", tags=["Billing & Webhooks"]) 

59app.include_router(github_webhooks.router, prefix="/v1", tags=["GitHub Integration"]) 

60app.include_router(billing.router, tags=["Billing"]) 

61app.include_router(badge.router, prefix="/v1/badge", tags=["Security Badge"]) 

62app.include_router(subscription.router, prefix="/v1", tags=["Subscription Management"]) 

63app.include_router(alerts.router, prefix="/v1", tags=["Alerts & Notifications"]) 

64app.include_router(insights.router, prefix="/v1", tags=["Security Insights"]) 

65app.include_router(team.router, prefix="/v1", tags=["Team Management"]) 

66app.include_router(dashboard.router, prefix="/v1", tags=["Dashboard"]) 

67app.include_router(cron.router, prefix="/v1", tags=["Cron Jobs"]) 

68 

69 

70# Startup and shutdown events 

71@app.on_event("startup") 

72async def startup_event(): 

73 """Initialize services on startup.""" 

74 from loguru import logger 

75 logger.info("🚀 Starting Alprina API...") 

76 

77 # Initialize Neon connection pool 

78 if neon_service.is_enabled(): 

79 try: 

80 await neon_service.get_pool() 

81 logger.info("✅ Neon connection pool initialized") 

82 except Exception as e: 

83 logger.error(f"❌ Failed to initialize Neon connection pool: {e}") 

84 else: 

85 logger.warning("⚠️ Neon service not enabled (DATABASE_URL not set)") 

86 

87 

88@app.on_event("shutdown") 

89async def shutdown_event(): 

90 """Clean up resources on shutdown.""" 

91 from loguru import logger 

92 logger.info("🛑 Shutting down Alprina API...") 

93 

94 # Close Neon connection pool 

95 if neon_service.is_enabled(): 

96 await neon_service.close() 

97 logger.info("✅ Neon connection pool closed") 

98 

99 

100@app.get("/") 

101async def root(): 

102 """API root endpoint.""" 

103 return { 

104 "name": "Alprina API", 

105 "version": "1.0.0", 

106 "description": "AI-powered security scanning", 

107 "docs": "/docs", 

108 "security_engine": "active" if AGENTS_AVAILABLE else "fallback", 

109 "endpoints": { 

110 "scan_code": "POST /v1/scan/code", 

111 "list_agents": "GET /v1/agents", 

112 "register": "POST /v1/auth/register", 

113 "login": "POST /v1/auth/login", 

114 "documentation": "GET /docs" 

115 } 

116 } 

117 

118 

119@app.get("/health") 

120async def health_check(): 

121 """Health check endpoint.""" 

122 return { 

123 "status": "healthy", 

124 "security_engine": "active" if AGENTS_AVAILABLE else "fallback", 

125 "version": "1.0.0" 

126 } 

127 

128 

129# Error handlers 

130@app.exception_handler(HTTPException) 

131async def http_exception_handler(request: Request, exc: HTTPException): 

132 """Handle HTTP exceptions.""" 

133 return JSONResponse( 

134 status_code=exc.status_code, 

135 content={ 

136 "error": { 

137 "code": exc.status_code, 

138 "message": exc.detail, 

139 "documentation_url": "https://docs.alprina.com/errors" 

140 } 

141 } 

142 ) 

143 

144 

145@app.exception_handler(Exception) 

146async def general_exception_handler(request: Request, exc: Exception): 

147 """Handle general exceptions.""" 

148 return JSONResponse( 

149 status_code=500, 

150 content={ 

151 "error": { 

152 "code": "server_error", 

153 "message": "An internal server error occurred", 

154 "documentation_url": "https://docs.alprina.com/errors/server_error" 

155 } 

156 } 

157 ) 

158 

159 

160# Production server configuration 

161if __name__ == "__main__": 

162 import uvicorn 

163 

164 uvicorn.run( 

165 "alprina_cli.api.main:app", 

166 host="0.0.0.0", 

167 port=8000, 

168 timeout_keep_alive=600, # 10 minutes for long-running AI scans 

169 timeout_notify=570, # Notify client at 9.5 minutes 

170 reload=False, # Disable reload in production 

171 )