Files
PROJECT-CONTORL/backend/app/main.py
2026-01-11 08:37:21 +08:00

295 lines
10 KiB
Python

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
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,
},
},
}