feat: implement 8 OpenSpec proposals for security, reliability, and UX improvements

## Security Enhancements (P0)
- Add input validation with max_length and numeric range constraints
- Implement WebSocket token authentication via first message
- Add path traversal prevention in file storage service

## Permission Enhancements (P0)
- Add project member management for cross-department access
- Implement is_department_manager flag for workload visibility

## Cycle Detection (P0)
- Add DFS-based cycle detection for task dependencies
- Add formula field circular reference detection
- Display user-friendly cycle path visualization

## Concurrency & Reliability (P1)
- Implement optimistic locking with version field (409 Conflict on mismatch)
- Add trigger retry mechanism with exponential backoff (1s, 2s, 4s)
- Implement cascade restore for soft-deleted tasks

## Rate Limiting (P1)
- Add tiered rate limits: standard (60/min), sensitive (20/min), heavy (5/min)
- Apply rate limits to tasks, reports, attachments, and comments

## Frontend Improvements (P1)
- Add responsive sidebar with hamburger menu for mobile
- Improve touch-friendly UI with proper tap target sizes
- Complete i18n translations for all components

## Backend Reliability (P2)
- Configure database connection pool (size=10, overflow=20)
- Add Redis fallback mechanism with message queue
- Add blocker check before task deletion

## API Enhancements (P3)
- Add standardized response wrapper utility
- Add /health/ready and /health/live endpoints
- Implement project templates with status/field copying

## Tests Added
- test_input_validation.py - Schema and path traversal tests
- test_concurrency_reliability.py - Optimistic locking and retry tests
- test_backend_reliability.py - Connection pool and Redis tests
- test_api_enhancements.py - Health check and template tests

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
beabigegg
2026-01-10 22:13:43 +08:00
parent 96210c7ad4
commit 3bdc6ff1c9
106 changed files with 9704 additions and 429 deletions

View File

@@ -1,13 +1,16 @@
from contextlib import asynccontextmanager
from fastapi import FastAPI, Request
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
from app.core.scheduler import start_scheduler, shutdown_scheduler, scheduler
from app.core.rate_limiter import limiter
from app.core.deprecation import DeprecationMiddleware
@asynccontextmanager
@@ -18,6 +21,8 @@ async def lifespan(app: FastAPI):
yield
# Shutdown
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
@@ -38,12 +43,17 @@ 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="0.1.0",
version="1.0.0",
lifespan=lifespan,
)
@@ -63,13 +73,37 @@ app.add_middleware(
# Audit middleware - extracts request metadata for audit logging
app.add_middleware(AuditMiddleware)
# Include routers
# 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)
app.include_router(projects_router)
app.include_router(tasks_router)
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)
@@ -79,13 +113,176 @@ 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)
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,
},
},
}