abinazebinoy's picture
feat(polish): frontend hardening, rate limit consolidation, v7.0.0
9d42189
from fastapi import FastAPI, Request
from fastapi.middleware.gzip import GZipMiddleware
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse
from contextlib import asynccontextmanager
from datetime import datetime
from slowapi import Limiter, _rate_limit_exceeded_handler
from slowapi.util import get_remote_address
from slowapi.errors import RateLimitExceeded
from slowapi.middleware import SlowAPIMiddleware
import os
from backend.core.config import settings
from backend.core.logger import setup_logger
from backend.api.routes import upload, analyze, cases, keys
logger = setup_logger(__name__)
# Shared rate limiter — imported by all routes
limiter = Limiter(key_func=get_remote_address)
shared_limiter = limiter # alias for explicit import
@asynccontextmanager
async def lifespan(app: FastAPI):
logger.info("VeriFile-X API starting up")
logger.info(f"Debug mode: {settings.DEBUG}")
logger.info(f"Max file size: {settings.MAX_FILE_SIZE_MB}MB")
yield
logger.info("VeriFile-X API shutting down")
app = FastAPI(
title=settings.PROJECT_NAME,
version=settings.VERSION,
debug=settings.DEBUG,
lifespan=lifespan,
)
app.state.limiter = limiter
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
app.add_middleware(SlowAPIMiddleware)
app.add_middleware(GZipMiddleware, minimum_size=1000)
app.add_middleware(
CORSMiddleware,
allow_origins=settings.cors_origins_list,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@app.middleware("http")
async def add_security_headers(request: Request, call_next):
"""Add security headers to every response."""
response = await call_next(request)
response.headers["X-Content-Type-Options"] = "nosniff"
response.headers["X-Frame-Options"] = "DENY"
response.headers["X-XSS-Protection"] = "1; mode=block"
response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"
response.headers["Cache-Control"] = "no-store" if "/api/" in str(request.url.path) else "public, max-age=3600"
return response
app.include_router(upload.router)
app.include_router(analyze.router)
app.include_router(cases.router)
app.include_router(keys.router)
@app.get("/")
async def root():
frontend_path = os.path.join(os.path.dirname(__file__), "..", "frontend", "index.html")
if os.path.exists(frontend_path):
return FileResponse(frontend_path)
return {
"name": settings.PROJECT_NAME,
"version": settings.VERSION,
"status": "operational",
"docs": "/docs",
}
@app.get("/api/v1/metrics", tags=["Observability"], summary="System metrics")
@limiter.limit("30/minute")
async def get_metrics(request: Request):
"""Return real-time system metrics: request rates, score distributions, latency."""
from backend.services.metrics_collector import get_metrics
return get_metrics()
@app.post("/api/v1/metrics/reset", tags=["Observability"], summary="Reset metrics (admin)")
@limiter.limit("5/minute")
async def reset_metrics(request: Request):
"""Reset all metrics counters."""
from backend.services.metrics_collector import reset_metrics
reset_metrics()
return {"message": "Metrics reset successfully."}
@app.get("/health")
@limiter.limit("60/minute")
async def health_check(request: Request):
return {
"status": "healthy",
"debug_mode": settings.DEBUG,
"timestamp": datetime.now().isoformat(),
}