OncoLit: a multi-tenant oncology literature search, feed, and collaboration platform. Built with FastAPI + Vue 3 + PostgreSQL. Includes PubMed pipeline, drug approvals, AI summaries, and systematic review tools.
107 lines
3.8 KiB
Python
107 lines
3.8 KiB
Python
from contextlib import asynccontextmanager
|
|
|
|
from fastapi import FastAPI, HTTPException
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
|
|
from app.api.v1.router import api_router
|
|
from app.config import settings
|
|
from app.core.csrf import CSRFProtectionMiddleware
|
|
from app.core.error_handlers import SecurityHeadersMiddleware, global_exception_handler, http_exception_handler
|
|
from app.core.logging_config import setup_logging
|
|
from app.core.middleware import TraceMiddleware
|
|
from app.core.monitoring import PerformanceMiddleware, RequestLoggingMiddleware, get_metrics
|
|
from app.core.monitoring import init_sentry
|
|
from app.core.rate_limiter import RateLimitMiddleware
|
|
from app.db import engine
|
|
from app.core.logging_config import get_logger
|
|
|
|
setup_logging()
|
|
init_sentry(settings.SENTRY_DSN or None)
|
|
logger = get_logger("startup")
|
|
|
|
|
|
@asynccontextmanager
|
|
async def lifespan(app: FastAPI):
|
|
# 启动检查:非 DEBUG 模式不能使用默认 JWT_SECRET
|
|
if not settings.DEBUG and settings.JWT_SECRET == "dev-secret-change-in-production":
|
|
import sys
|
|
print("\n" + "=" * 60, file=sys.stderr)
|
|
print(" FATAL: JWT_SECRET is set to the default dev value!", file=sys.stderr)
|
|
print(" Set a secure JWT_SECRET in .env or environment variables.", file=sys.stderr)
|
|
print("=" * 60 + "\n", file=sys.stderr)
|
|
sys.exit(1)
|
|
# SMTP 配置状态检查
|
|
if settings.SMTP_HOST:
|
|
logger.info("SMTP configured: %s:%s → %s", settings.SMTP_HOST, settings.SMTP_PORT, settings.SMTP_FROM)
|
|
else:
|
|
logger.warning("SMTP not configured — emails will be logged, not sent")
|
|
yield
|
|
await engine.dispose()
|
|
|
|
|
|
app = FastAPI(
|
|
title="SciLit Oncology",
|
|
description="肿瘤科文献管理平台",
|
|
version="0.1.0",
|
|
lifespan=lifespan,
|
|
docs_url="/docs" if settings.DEBUG else None,
|
|
redoc_url="/redoc" if settings.DEBUG else None,
|
|
openapi_url="/openapi.json" if settings.DEBUG else None,
|
|
)
|
|
|
|
# 全局异常处理
|
|
app.add_exception_handler(Exception, global_exception_handler)
|
|
app.add_exception_handler(HTTPException, http_exception_handler)
|
|
|
|
# 中间件(从内到外)
|
|
# CORS: allow_credentials 不能与 allow_origins="*" 同时用,必须明确列出允许的源
|
|
app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins=settings.CORS_ORIGINS,
|
|
allow_credentials=True,
|
|
allow_methods=["GET", "POST", "PUT", "DELETE", "PATCH"],
|
|
allow_headers=["Authorization", "Content-Type", "X-CSRF-Token", "X-Tenant-Id"],
|
|
expose_headers=["X-Response-Time-Ms", "X-RateLimit-Remaining", "X-RateLimit-Limit"],
|
|
)
|
|
app.add_middleware(RateLimitMiddleware)
|
|
app.add_middleware(PerformanceMiddleware)
|
|
app.add_middleware(SecurityHeadersMiddleware)
|
|
app.add_middleware(CSRFProtectionMiddleware)
|
|
app.add_middleware(RequestLoggingMiddleware)
|
|
app.add_middleware(TraceMiddleware)
|
|
|
|
app.include_router(api_router, prefix=settings.API_V1_PREFIX)
|
|
|
|
|
|
@app.get("/health")
|
|
async def health():
|
|
db_ok = False
|
|
try:
|
|
from sqlalchemy import text
|
|
async with engine.begin() as conn:
|
|
await conn.execute(text("SELECT 1"))
|
|
db_ok = True
|
|
except Exception:
|
|
import logging
|
|
logging.getLogger(__name__).warning("Health check DB connection failed")
|
|
return {"status": "ok" if db_ok else "degraded", "specialty": settings.SPECIALTY, "db": "ok" if db_ok else "error", "version": "0.1.0"}
|
|
|
|
|
|
@app.get("/health/metrics")
|
|
async def health_metrics():
|
|
"""监控指标端点"""
|
|
db_ok = False
|
|
try:
|
|
from sqlalchemy import text
|
|
async with engine.begin() as conn:
|
|
await conn.execute(text("SELECT 1"))
|
|
db_ok = True
|
|
except Exception:
|
|
import logging
|
|
logging.getLogger(__name__).warning("Health metrics DB connection failed")
|
|
return {
|
|
"db": "ok" if db_ok else "error",
|
|
"alerts": get_metrics(),
|
|
"ws": {"online_users": 0, "online_connections": 0},
|
|
}
|