Files
personal-ai-assistant/backend/app/main.py
dolgolyov.alexei 4cbce89129 Phase 7: Hardening — logging, security, Docker, production readiness
Backend:
- Structured JSON logging (python-json-logger) with request ID correlation
- RequestIDMiddleware (server-generated UUID, no client trust)
- Global exception handlers: AppException, RequestValidationError, generic 500
  — all return consistent {"error": {code, message, request_id}} format
- Async rate limiting with lock + stale key eviction on auth endpoints
- Health endpoint checks DB connectivity, returns version + status
- Custom exception classes (NotFoundException, ForbiddenException, etc.)
- OpenAPI docs with tag descriptions, conditional URL (disabled in production)
- LOG_LEVEL, DOCS_ENABLED, RATE_LIMIT_* settings added

Docker:
- Backend: multi-stage build (builder + runtime), non-root user, HEALTHCHECK
- Frontend: removed dead user, HEALTHCHECK directive
- docker-compose: restart policies, healthchecks, Redis service, named volumes
  for uploads/PDFs, rate limit env vars forwarded
- Alembic migrations run only in Dockerfile CMD (removed from lifespan)

Nginx:
- server_tokens off
- CSP, Referrer-Policy, Permissions-Policy headers
- HSTS ready (commented, enable with TLS)

Config & Docs:
- .env.production.example with production-ready settings
- CLAUDE.md project conventions (structure, workflow, naming, how-to)
- .env.example updated with new variables

Review fixes applied:
- Rate limiter: async lock prevents race condition, stale key eviction
- Request ID: always server-generated (no log injection)
- Removed duplicate alembic migration from lifespan
- Removed dead app user from frontend Dockerfile
- Health check logs DB errors
- Rate limit env vars forwarded in docker-compose

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 14:52:21 +03:00

114 lines
3.8 KiB
Python

import logging
from contextlib import asynccontextmanager
from fastapi import FastAPI, Request
from fastapi.exceptions import RequestValidationError
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from app.config import settings
from app.core.exceptions import AppException
from app.core.logging import setup_logging
from app.core.middleware import RequestIDMiddleware, get_request_id
logger = logging.getLogger(__name__)
@asynccontextmanager
async def lifespan(app: FastAPI):
setup_logging()
logger.info("Starting AI Assistant API")
# Note: Alembic migrations run via Dockerfile CMD before uvicorn starts
from app.services.scheduler_service import start_scheduler, shutdown_scheduler
start_scheduler()
yield
shutdown_scheduler()
logger.info("Shutting down AI Assistant API")
def create_app() -> FastAPI:
app = FastAPI(
title="AI Assistant API",
description="Personal AI health assistant with document management, chat, and notifications.",
version="0.1.0",
lifespan=lifespan,
docs_url="/api/docs" if settings.DOCS_ENABLED else None,
redoc_url="/api/redoc" if settings.DOCS_ENABLED else None,
openapi_url="/api/openapi.json" if settings.DOCS_ENABLED else None,
openapi_tags=[
{"name": "auth", "description": "Authentication and registration"},
{"name": "chats", "description": "AI chat conversations"},
{"name": "documents", "description": "Health document management"},
{"name": "memory", "description": "Health memory entries"},
{"name": "skills", "description": "AI specialist skills"},
{"name": "notifications", "description": "User notifications"},
{"name": "pdf", "description": "PDF report generation"},
{"name": "admin", "description": "Admin management"},
{"name": "users", "description": "User profile and context"},
{"name": "websocket", "description": "WebSocket endpoints"},
],
)
# Middleware (order matters: outermost first)
app.add_middleware(RequestIDMiddleware)
app.add_middleware(
CORSMiddleware,
allow_origins=settings.BACKEND_CORS_ORIGINS,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Exception handlers
@app.exception_handler(AppException)
async def app_exception_handler(request: Request, exc: AppException):
return JSONResponse(
status_code=exc.status_code,
content={
"error": {
"code": exc.code,
"message": exc.detail,
"request_id": get_request_id(),
}
},
)
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
return JSONResponse(
status_code=422,
content={
"error": {
"code": "VALIDATION_ERROR",
"message": "Request validation failed",
"details": exc.errors(),
"request_id": get_request_id(),
}
},
)
@app.exception_handler(Exception)
async def generic_exception_handler(request: Request, exc: Exception):
logger.exception("Unhandled exception", extra={"request_id": get_request_id()})
return JSONResponse(
status_code=500,
content={
"error": {
"code": "INTERNAL_ERROR",
"message": "An internal error occurred",
"request_id": get_request_id(),
}
},
)
from app.api.v1.router import api_v1_router
app.include_router(api_v1_router)
return app
app = create_app()