import os from contextlib import asynccontextmanager from datetime import datetime from fastapi import FastAPI, Request, APIRouter from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse from slowapi import _rate_limit_exceeded_handler from slowapi.errors import RateLimitExceeded from sqlalchemy import text from app.middleware.audit import AuditMiddleware from app.core.scheduler import start_scheduler, shutdown_scheduler, scheduler from app.core.rate_limiter import limiter from app.core.deprecation import DeprecationMiddleware @asynccontextmanager async def lifespan(app: FastAPI): """Manage application lifespan events.""" testing = os.environ.get("TESTING", "").lower() in ("true", "1", "yes") scheduler_disabled = os.environ.get("DISABLE_SCHEDULER", "").lower() in ("true", "1", "yes") start_background_jobs = not testing and not scheduler_disabled # Startup security validation if not testing: from app.core.security import validate_jwt_secret_on_startup validate_jwt_secret_on_startup() # Startup if start_background_jobs: start_scheduler() yield # Shutdown if start_background_jobs: shutdown_scheduler() from app.api.auth import router as auth_router from app.api.users import router as users_router from app.api.departments import router as departments_router from app.api.spaces import router as spaces_router from app.api.projects import router as projects_router from app.api.tasks import router as tasks_router from app.api.workload import router as workload_router from app.api.comments import router as comments_router from app.api.notifications import router as notifications_router from app.api.blockers import router as blockers_router from app.api.websocket import router as websocket_router from app.api.audit import router as audit_router from app.api.attachments import router as attachments_router from app.api.triggers import router as triggers_router from app.api.reports import router as reports_router from app.api.health import router as health_router from app.api.custom_fields import router as custom_fields_router from app.api.task_dependencies import router as task_dependencies_router from app.api.admin import encryption_keys as admin_encryption_keys_router from app.api.dashboard import router as dashboard_router from app.api.templates import router as templates_router from app.core.config import settings from app.core.database import get_pool_status, engine from app.core.redis import redis_client from app.services.notification_service import get_redis_fallback_status from app.services.file_storage_service import file_storage_service app = FastAPI( title="Project Control API", description="Cross-departmental project management system API", version="1.0.0", lifespan=lifespan, ) # Initialize rate limiter app.state.limiter = limiter app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler) # CORS middleware app.add_middleware( CORSMiddleware, allow_origins=settings.CORS_ORIGINS, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # Audit middleware - extracts request metadata for audit logging app.add_middleware(AuditMiddleware) # Deprecation middleware - adds deprecation headers to legacy /api/ routes app.add_middleware(DeprecationMiddleware) # ============================================================================= # API Version 1 Router - Primary API namespace # ============================================================================= api_v1_router = APIRouter(prefix="/api/v1") # Include routers under /api/v1/ api_v1_router.include_router(auth_router.router, prefix="/auth", tags=["Authentication"]) api_v1_router.include_router(users_router.router, prefix="/users", tags=["Users"]) api_v1_router.include_router(departments_router.router, prefix="/departments", tags=["Departments"]) api_v1_router.include_router(workload_router, prefix="/workload", tags=["Workload"]) api_v1_router.include_router(dashboard_router, prefix="/dashboard", tags=["Dashboard"]) api_v1_router.include_router(templates_router, tags=["Project Templates"]) # Mount the v1 router app.include_router(api_v1_router) # ============================================================================= # Legacy /api/ Routes (Deprecated - for backwards compatibility) # ============================================================================= # These routes will be removed in a future version. # All new integrations should use /api/v1/ prefix. app.include_router(auth_router.router, prefix="/api/auth", tags=["Authentication"]) app.include_router(users_router.router, prefix="/api/users", tags=["Users"]) app.include_router(departments_router.router, prefix="/api/departments", tags=["Departments"]) app.include_router(spaces_router) # Has /api/spaces prefix in router app.include_router(projects_router) # Has routes with /api prefix app.include_router(tasks_router) # Has routes with /api prefix app.include_router(workload_router, prefix="/api/workload", tags=["Workload"]) app.include_router(comments_router) app.include_router(notifications_router) app.include_router(blockers_router) app.include_router(websocket_router) app.include_router(audit_router) app.include_router(attachments_router) app.include_router(triggers_router) app.include_router(reports_router) app.include_router(health_router) # Has /api/projects/health prefix app.include_router(custom_fields_router) app.include_router(task_dependencies_router) app.include_router(admin_encryption_keys_router.router) app.include_router(dashboard_router, prefix="/api/dashboard", tags=["Dashboard"]) app.include_router(templates_router) # Has /api/templates prefix in router # ============================================================================= # Health Check Endpoints # ============================================================================= def check_database_health() -> dict: """Check database connectivity and return status.""" try: # Execute a simple query to verify connection with engine.connect() as conn: conn.execute(text("SELECT 1")) pool_status = get_pool_status() return { "status": "healthy", "connected": True, **pool_status, } except Exception as e: return { "status": "unhealthy", "connected": False, "error": str(e), } def check_redis_health() -> dict: """Check Redis connectivity and return status.""" try: # Ping Redis to verify connection redis_client.ping() redis_fallback = get_redis_fallback_status() return { "status": "healthy", "connected": True, **redis_fallback, } except Exception as e: redis_fallback = get_redis_fallback_status() return { "status": "unhealthy", "connected": False, "error": str(e), **redis_fallback, } def check_scheduler_health() -> dict: """Check scheduler status and return details.""" try: running = scheduler.running jobs = [] if running: for job in scheduler.get_jobs(): jobs.append({ "id": job.id, "name": job.name, "next_run": job.next_run_time.isoformat() if job.next_run_time else None, }) return { "status": "healthy" if running else "stopped", "running": running, "jobs": jobs, "job_count": len(jobs), } except Exception as e: return { "status": "unhealthy", "running": False, "error": str(e), } @app.get("/health") async def health_check(): """Basic health check endpoint for load balancers. Returns a simple healthy status if the application is running. For detailed status, use /health/detailed. """ return {"status": "healthy"} @app.get("/health/live") async def liveness_check(): """Kubernetes liveness probe endpoint. Returns healthy if the application process is running. Does not check external dependencies. """ return { "status": "healthy", "timestamp": datetime.utcnow().isoformat() + "Z", } @app.get("/health/ready") async def readiness_check(): """Kubernetes readiness probe endpoint. Returns healthy only if all critical dependencies are available. The application should not receive traffic until ready. """ db_health = check_database_health() redis_health = check_redis_health() # Application is ready only if database is healthy # Redis degradation is acceptable (we have fallback) is_ready = db_health["status"] == "healthy" return { "status": "ready" if is_ready else "not_ready", "timestamp": datetime.utcnow().isoformat() + "Z", "checks": { "database": db_health["status"], "redis": redis_health["status"], }, } @app.get("/health/detailed") async def detailed_health_check(): """Detailed health check endpoint. Returns comprehensive status of all system components: - database: Connection pool status and connectivity - redis: Connection status and fallback queue status - storage: File storage validation status - scheduler: Background job scheduler status """ db_health = check_database_health() redis_health = check_redis_health() scheduler_health = check_scheduler_health() storage_status = file_storage_service.get_storage_status() # Determine overall health is_healthy = ( db_health["status"] == "healthy" and storage_status.get("validated", False) ) # Degraded if Redis or scheduler has issues but DB is ok is_degraded = ( is_healthy and ( redis_health["status"] != "healthy" or scheduler_health["status"] != "healthy" ) ) overall_status = "unhealthy" if is_healthy: overall_status = "degraded" if is_degraded else "healthy" return { "status": overall_status, "timestamp": datetime.utcnow().isoformat() + "Z", "version": app.version, "components": { "database": db_health, "redis": redis_health, "scheduler": scheduler_health, "storage": { "status": "healthy" if storage_status.get("validated", False) else "unhealthy", **storage_status, }, }, }