import os from contextlib import asynccontextmanager from datetime import datetime from fastapi import FastAPI, Request, APIRouter, Depends 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.middleware.csrf import CSRFMiddleware from app.middleware.security_audit import SecurityAuditMiddleware from app.middleware.error_sanitizer import ErrorSanitizerMiddleware 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 from app.middleware.auth import require_system_admin from app.models import User app = FastAPI( title="Project Control API", description=""" ## Cross-departmental Project Management System API ### Features - **Task Management**: CRUD operations for tasks with custom fields - **Project & Space**: Hierarchical project organization - **Workload**: Resource allocation and capacity planning - **Collaboration**: Comments, mentions, and real-time sync - **Audit Trail**: Complete change history and compliance logging - **Automation**: Triggers, schedules, and automated reports ### Authentication All endpoints except `/health` and `/api/auth/login` require JWT authentication. Include the token in the `Authorization` header: `Bearer ` ### CSRF Protection State-changing requests (POST, PUT, PATCH, DELETE) require `X-CSRF-Token` header. """, version="1.0.0", lifespan=lifespan, docs_url="/docs", redoc_url="/redoc", openapi_tags=[ {"name": "auth", "description": "Authentication and authorization"}, {"name": "tasks", "description": "Task management operations"}, {"name": "projects", "description": "Project and space management"}, {"name": "workload", "description": "Resource workload and capacity"}, {"name": "audit", "description": "Audit trail and compliance"}, {"name": "health", "description": "System health checks"}, ], ) # Initialize rate limiter app.state.limiter = limiter app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler) # CORS middleware - Explicit methods and headers for security app.add_middleware( CORSMiddleware, allow_origins=settings.CORS_ORIGINS, allow_credentials=True, allow_methods=["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"], allow_headers=["Content-Type", "Authorization", "X-CSRF-Token", "X-Request-ID"], ) # Error sanitizer middleware - sanitizes error messages in production # Must be first in the chain to intercept all error responses app.add_middleware(ErrorSanitizerMiddleware) # Audit middleware - extracts request metadata for audit logging app.add_middleware(AuditMiddleware) # Security audit middleware - logs 401/403 responses to audit trail app.add_middleware(SecurityAuditMiddleware) # CSRF middleware - validates CSRF tokens for state-changing requests app.add_middleware(CSRFMiddleware) # 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( current_user: User = Depends(require_system_admin), ): """Detailed health check endpoint (requires system admin). 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 Note: This endpoint requires system admin authentication because it exposes sensitive infrastructure details including connection pool statistics and internal service states. """ 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, }, }, }