
This commit introduces several improvements to the application configuration and logging mechanisms, including: - Added a new `REDIS_URL` configuration option in the production environment template for easier Redis setup. - Implemented a soft delete method in the `UserManager` class to anonymize user data while maintaining referential integrity. - Enhanced session secret management to ensure a secure fallback in non-production environments. - Introduced a `PiiRedactionFilter` to loggers for redacting sensitive information from logs. - Added rate limiting middleware to control API request rates and prevent abuse. These changes aim to improve security, maintainability, and user data protection within the application.
129 lines
4.2 KiB
Python
129 lines
4.2 KiB
Python
import logging
|
|
from fastapi import FastAPI
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
from starlette.middleware.sessions import SessionMiddleware
|
|
import sentry_sdk
|
|
from sentry_sdk.integrations.fastapi import FastApiIntegration
|
|
import os
|
|
import sys
|
|
from app.api.api_router import api_router
|
|
from app.config import settings
|
|
from app.core.api_config import API_METADATA, API_TAGS
|
|
from app.auth import fastapi_users, auth_backend
|
|
from app.schemas.user import UserPublic, UserCreate, UserUpdate
|
|
from app.core.scheduler import init_scheduler, shutdown_scheduler
|
|
from app.core.middleware import RequestContextMiddleware
|
|
from app.core.logging_utils import PiiRedactionFilter
|
|
from app.core.error_handlers import sqlalchemy_exception_handler, generic_exception_handler
|
|
from app.core.rate_limiter import RateLimitMiddleware
|
|
|
|
if settings.SENTRY_DSN:
|
|
sentry_sdk.init(
|
|
dsn=settings.SENTRY_DSN,
|
|
integrations=[
|
|
FastApiIntegration(),
|
|
],
|
|
traces_sample_rate=0.1 if settings.is_production else 1.0,
|
|
environment=settings.ENVIRONMENT,
|
|
send_default_pii=not settings.is_production
|
|
)
|
|
|
|
logging.basicConfig(
|
|
level=getattr(logging, settings.LOG_LEVEL),
|
|
format=settings.LOG_FORMAT
|
|
)
|
|
|
|
# Attach PII redaction filter to root logger
|
|
root_logger = logging.getLogger()
|
|
root_logger.addFilter(PiiRedactionFilter())
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
api_metadata = {
|
|
**API_METADATA,
|
|
"docs_url": settings.docs_url,
|
|
"redoc_url": settings.redoc_url,
|
|
"openapi_url": settings.openapi_url,
|
|
}
|
|
|
|
app = FastAPI(
|
|
**api_metadata,
|
|
openapi_tags=API_TAGS
|
|
)
|
|
|
|
app.add_middleware(
|
|
SessionMiddleware,
|
|
secret_key=settings.SESSION_SECRET_KEY
|
|
)
|
|
|
|
# Structured logging & request tracing
|
|
app.add_middleware(RequestContextMiddleware)
|
|
app.add_middleware(RateLimitMiddleware)
|
|
|
|
app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins=(settings.cors_origins_list if not settings.is_production else [settings.FRONTEND_URL]),
|
|
# Credentials (cookies) are not required because we use JWTs in Authorization headers.
|
|
allow_credentials=False,
|
|
allow_methods=["*"],
|
|
allow_headers=["*"],
|
|
expose_headers=["*"]
|
|
)
|
|
|
|
# Register exception handlers BEFORE adding middleware/router
|
|
app.add_exception_handler(Exception, generic_exception_handler)
|
|
from sqlalchemy.exc import SQLAlchemyError
|
|
app.add_exception_handler(SQLAlchemyError, sqlalchemy_exception_handler)
|
|
|
|
app.include_router(api_router, prefix=settings.API_PREFIX)
|
|
|
|
@app.get("/health", tags=["Health"])
|
|
async def health_check():
|
|
"""Minimal health check endpoint that avoids leaking build metadata."""
|
|
return {"status": settings.HEALTH_STATUS_OK}
|
|
|
|
@app.get("/", tags=["Root"])
|
|
async def read_root():
|
|
"""Public root endpoint with minimal information."""
|
|
logger.info("Root endpoint '/' accessed.")
|
|
return {"message": settings.ROOT_MESSAGE}
|
|
|
|
async def run_migrations():
|
|
"""Run database migrations."""
|
|
try:
|
|
logger.info("Running database migrations...")
|
|
base_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
alembic_path = os.path.join(base_path, 'alembic')
|
|
|
|
if alembic_path not in sys.path:
|
|
sys.path.insert(0, alembic_path)
|
|
|
|
from migrations import run_migrations as run_db_migrations
|
|
await run_db_migrations()
|
|
|
|
logger.info("Database migrations completed successfully.")
|
|
except Exception as e:
|
|
logger.error(f"Error running migrations: {e}")
|
|
raise
|
|
|
|
@app.on_event("startup")
|
|
async def startup_event():
|
|
"""Initialize services on startup."""
|
|
logger.info(f"Application startup in {settings.ENVIRONMENT} environment...")
|
|
# await run_migrations()
|
|
init_scheduler()
|
|
logger.info("Application startup complete.")
|
|
|
|
@app.on_event("shutdown")
|
|
async def shutdown_event():
|
|
"""Cleanup services on shutdown."""
|
|
logger.info("Application shutdown: Disconnecting from database...")
|
|
shutdown_scheduler()
|
|
# Close Redis connection pool to avoid leaking file descriptors.
|
|
try:
|
|
from app.core.redis import redis_pool
|
|
await redis_pool.aclose()
|
|
logger.info("Redis pool closed.")
|
|
except Exception as e:
|
|
logger.warning(f"Error closing Redis pool: {e}")
|
|
logger.info("Application shutdown complete.") |