from fastapi import FastAPI, Request, Depends, HTTPException, status from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials import structlog from datetime import datetime, timedelta from typing import Dict, Any, Optional, List from pydantic import BaseModel import secrets from app.config import get_settings # Configure logging structlog.configure( processors=[ structlog.stdlib.filter_by_level, structlog.stdlib.add_logger_name, structlog.stdlib.add_log_level, structlog.stdlib.PositionalArgumentsFormatter(), structlog.processors.TimeStamper(fmt="iso"), structlog.processors.StackInfoRenderer(), structlog.processors.format_exc_info, structlog.processors.UnicodeDecoder(), structlog.processors.JSONRenderer() ], context_class=dict, logger_factory=structlog.stdlib.LoggerFactory(), wrapper_class=structlog.stdlib.BoundLogger, cache_logger_on_first_use=True, ) logger = structlog.get_logger() # Create FastAPI application app = FastAPI( title="Gitea Webhook Ambassador (Demo)", description="High-performance webhook service from Gitea to Jenkins - Demo Version", version="1.0.0" ) # Add CORS middleware app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # Security configuration security = HTTPBearer(auto_error=False) # Demo data storage api_keys = { "demo_admin_key": { "name": "Demo Admin Key", "key_hash": "demo_admin_key", "key_prefix": "demo_adm", "created_at": datetime.utcnow(), "last_used": datetime.utcnow(), "is_active": True, "role": "admin" }, "demo_user_key": { "name": "Demo User Key", "key_hash": "demo_user_key", "key_prefix": "demo_usr", "created_at": datetime.utcnow(), "last_used": datetime.utcnow(), "is_active": True, "role": "user" } } trigger_logs = [ { "id": 1, "repository_name": "freeleaps/test-project", "branch_name": "main", "commit_sha": "abc123def456", "job_name": "test-project-build", "status": "success", "error_message": None, "created_at": datetime.utcnow() - timedelta(hours=2) }, { "id": 2, "repository_name": "freeleaps/another-project", "branch_name": "dev", "commit_sha": "def456ghi789", "job_name": "another-project-dev", "status": "success", "error_message": None, "created_at": datetime.utcnow() - timedelta(hours=1) }, { "id": 3, "repository_name": "freeleaps/test-project", "branch_name": "feature/new-feature", "commit_sha": "ghi789jkl012", "job_name": "test-project-feature", "status": "failed", "error_message": "Build timeout", "created_at": datetime.utcnow() - timedelta(minutes=30) } ] project_mappings = { 1: { "repository_name": "freeleaps/test-project", "default_job": "test-project-build", "branch_jobs": [ {"branch": "dev", "job": "test-project-dev"}, {"branch": "staging", "job": "test-project-staging"} ], "branch_patterns": [ {"pattern": "feature/*", "job": "test-project-feature"}, {"pattern": "hotfix/*", "job": "test-project-hotfix"} ], "created_at": datetime.utcnow() - timedelta(days=1), "updated_at": datetime.utcnow() - timedelta(hours=6) } } # Request/response models class HealthResponse(BaseModel): status: str service: str version: str timestamp: datetime jenkins: Dict[str, Any] worker_pool: Dict[str, Any] database: Dict[str, Any] class TriggerLogResponse(BaseModel): id: int repository_name: str branch_name: str commit_sha: str job_name: str status: str error_message: Optional[str] = None created_at: datetime class APIKeyResponse(BaseModel): id: str name: str key_prefix: str created_at: datetime last_used: datetime is_active: bool role: str class ProjectMappingResponse(BaseModel): id: int repository_name: str default_job: str branch_jobs: List[dict] branch_patterns: List[dict] created_at: datetime updated_at: datetime # Authentication functions def verify_api_key(api_key: str): """Verify API key""" for key_id, key_data in api_keys.items(): if key_data["key_hash"] == api_key and key_data["is_active"]: # Update last used time key_data["last_used"] = datetime.utcnow() return key_data return None async def get_current_user( credentials: Optional[HTTPAuthorizationCredentials] = Depends(security) ): """Get current user (supports API key authentication)""" if not credentials: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Authentication token required", headers={"WWW-Authenticate": "Bearer"}, ) token = credentials.credentials # Verify API key api_key_data = verify_api_key(token) if api_key_data: return { "username": api_key_data["name"], "auth_type": "api_key", "role": api_key_data["role"] } # Authentication failed raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid authentication token", headers={"WWW-Authenticate": "Bearer"}, ) # Public endpoints @app.get("/health", response_model=HealthResponse) async def health_check(): """Health check endpoint""" settings = get_settings() return HealthResponse( status="healthy", service="Gitea Webhook Ambassador (Demo)", version=settings.version, timestamp=datetime.utcnow(), jenkins={"status": "connected"}, worker_pool={ "active_workers": 2, "queue_size": 0, "total_processed": len(trigger_logs), "total_failed": len([log for log in trigger_logs if log["status"] == "failed"]) }, database={"status": "connected"} ) @app.get("/") async def root(): """Root path""" return { "name": "Gitea Webhook Ambassador (Demo)", "version": "1.0.0", "description": "High-performance webhook service from Gitea to Jenkins - Demo Version", "endpoints": { "webhook": "/webhook/gitea", "health": "/health", "logs": "/api/logs", "admin": "/api/admin" }, "demo_keys": { "admin": "demo_admin_key", "user": "demo_user_key" } } @app.post("/webhook/gitea") async def handle_gitea_webhook(request: Request): """Handle Gitea webhook request""" try: body = await request.body() # Log webhook request logger.info("Received Gitea webhook", body_size=len(body), headers=dict(request.headers)) # Add new trigger log log_entry = { "id": len(trigger_logs) + 1, "repository_name": "demo-repo", "branch_name": "main", "commit_sha": "demo123", "job_name": "demo-job", "status": "success", "error_message": None, "created_at": datetime.utcnow() } trigger_logs.append(log_entry) return { "success": True, "message": "Webhook received successfully", "data": { "body_size": len(body), "timestamp": datetime.utcnow().isoformat() } } except Exception as e: logger.error("Webhook processing failed", error=str(e)) return JSONResponse( status_code=500, content={ "success": False, "message": "Webhook processing failed", "error": str(e) } ) # Authenticated endpoints @app.get("/api/logs", response_model=List[TriggerLogResponse]) async def get_trigger_logs( repository: Optional[str] = None, branch: Optional[str] = None, limit: int = 100, current_user: dict = Depends(get_current_user) ): """Get trigger logs""" print(f"User {current_user['username']} accessed logs endpoint") filtered_logs = trigger_logs.copy() if repository: filtered_logs = [log for log in filtered_logs if log["repository_name"] == repository] if branch: filtered_logs = [log for log in filtered_logs if log["branch_name"] == branch] # Sort by time descending and limit filtered_logs.sort(key=lambda x: x["created_at"], reverse=True) return filtered_logs[:limit] @app.get("/api/logs/stats") async def get_log_stats(current_user: dict = Depends(get_current_user)): """Get log statistics""" print(f"User {current_user['username']} accessed log statistics") total_logs = len(trigger_logs) successful_logs = len([log for log in trigger_logs if log["status"] == "success"]) failed_logs = len([log for log in trigger_logs if log["status"] == "failed"]) # Logs in the last 24 hours yesterday = datetime.utcnow() - timedelta(days=1) recent_logs = len([log for log in trigger_logs if log["created_at"] >= yesterday]) # Grouped by repository repo_stats = {} for log in trigger_logs: repo = log["repository_name"] repo_stats[repo] = repo_stats.get(repo, 0) + 1 return { "total_logs": total_logs, "successful_logs": successful_logs, "failed_logs": failed_logs, "recent_logs_24h": recent_logs, "repository_stats": [ {"repository": repo, "count": count} for repo, count in repo_stats.items() ] } @app.get("/api/admin/api-keys", response_model=List[APIKeyResponse]) async def list_api_keys(current_user: dict = Depends(get_current_user)): """List all API keys (admin only)""" if current_user["role"] != "admin": raise HTTPException(status_code=403, detail="Admin privileges required") print(f"Admin {current_user['username']} viewed API key list") return [ APIKeyResponse( id=key_id, name=key_data["name"], key_prefix=key_data["key_prefix"], created_at=key_data["created_at"], last_used=key_data["last_used"], is_active=key_data["is_active"], role=key_data["role"] ) for key_id, key_data in api_keys.items() ] @app.get("/api/admin/projects", response_model=List[ProjectMappingResponse]) async def list_project_mappings(current_user: dict = Depends(get_current_user)): """List all project mappings""" print(f"User {current_user['username']} viewed project mappings") return [ ProjectMappingResponse( id=mapping_id, repository_name=mapping_data["repository_name"], default_job=mapping_data["default_job"], branch_jobs=mapping_data["branch_jobs"], branch_patterns=mapping_data["branch_patterns"], created_at=mapping_data["created_at"], updated_at=mapping_data["updated_at"] ) for mapping_id, mapping_data in project_mappings.items() ] @app.get("/api/admin/stats") async def get_admin_stats(current_user: dict = Depends(get_current_user)): """Get admin statistics""" print(f"User {current_user['username']} viewed admin statistics") total_keys = len(api_keys) active_keys = len([key for key in api_keys.values() if key["is_active"]]) # Recently used keys week_ago = datetime.utcnow() - timedelta(days=7) recent_keys = len([ key for key in api_keys.values() if key["last_used"] >= week_ago ]) total_mappings = len(project_mappings) return { "api_keys": { "total": total_keys, "active": active_keys, "recently_used": recent_keys }, "project_mappings": { "total": total_mappings } } # Middleware @app.middleware("http") async def log_requests(request: Request, call_next): """Request logging middleware""" start_time = datetime.utcnow() response = await call_next(request) process_time = (datetime.utcnow() - start_time).total_seconds() response.headers["X-Process-Time"] = str(process_time) return response if __name__ == "__main__": import uvicorn settings = get_settings() print("🚀 Starting Gitea Webhook Ambassador Demo Version") print("=" * 60) print("📋 Demo API Keys:") print(" Admin key: demo_admin_key") print(" User key: demo_user_key") print() print("🔧 Usage examples:") print(" curl -H 'Authorization: Bearer demo_admin_key' http://localhost:8000/api/admin/api-keys") print(" curl -H 'Authorization: Bearer demo_user_key' http://localhost:8000/api/logs") print("=" * 60) uvicorn.run( "app.main_demo:app", host=settings.server.host, port=settings.server.port, reload=settings.server.reload )