Files
PROJECT-CONTORL/backend/app/main.py
beabigegg 35c90fe76b feat: implement 5 QA-driven security and quality proposals
Implemented proposals from comprehensive QA review:

1. extend-csrf-protection
   - Add POST to CSRF protected methods in frontend
   - Global CSRF middleware for all state-changing operations
   - Update tests with CSRF token fixtures

2. tighten-cors-websocket-security
   - Replace wildcard CORS with explicit method/header lists
   - Disable query parameter auth in production (code 4002)
   - Add per-user WebSocket connection limit (max 5, code 4005)

3. shorten-jwt-expiry
   - Reduce JWT expiry from 7 days to 60 minutes
   - Add refresh token support with 7-day expiry
   - Implement token rotation on refresh
   - Frontend auto-refresh when token near expiry (<5 min)

4. fix-frontend-quality
   - Add React.lazy() code splitting for all pages
   - Fix useCallback dependency arrays (Dashboard, Comments)
   - Add localStorage data validation in AuthContext
   - Complete i18n for AttachmentUpload component

5. enhance-backend-validation
   - Add SecurityAuditMiddleware for access denied logging
   - Add ErrorSanitizerMiddleware for production error messages
   - Protect /health/detailed with admin authentication
   - Add input length validation (comment 5000, desc 10000)

All 521 backend tests passing. Frontend builds successfully.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 23:19:05 +08:00

322 lines
12 KiB
Python

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",
version="1.0.0",
lifespan=lifespan,
)
# 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,
},
},
}