- Бэкенд: FastAPI + SQLAlchemy async + PostgreSQL, JWT-аутентификация, Qdrant multi-tenant векторное хранилище, Celery воркер - Фронтенд: React + Vite + TypeScript SPA (логин, регистрация, дашборд, поиск по коллекциям, настройки) - Инфраструктура: docker-compose, Helm-чарт, GitHub Actions CI, Prometheus/Grafana мониторинг - Embedding через OpenRouter (nvidia/llama-nemotron-embed-vl-1b-v2:free) - LLM: google/gemma-4-31b-it:free через OpenRouter
29 lines
1.1 KiB
Python
29 lines
1.1 KiB
Python
import time
|
|
|
|
from fastapi import Request
|
|
from starlette.middleware.base import BaseHTTPMiddleware
|
|
from starlette.responses import JSONResponse
|
|
from collections import defaultdict
|
|
|
|
|
|
class RateLimitMiddleware(BaseHTTPMiddleware):
|
|
def __init__(self, app, requests_per_minute: int = 10):
|
|
super().__init__(app)
|
|
self.requests_per_minute = requests_per_minute
|
|
self.requests: dict = defaultdict(list)
|
|
|
|
async def dispatch(self, request: Request, call_next):
|
|
if request.url.path.startswith("/api/health"):
|
|
return await call_next(request)
|
|
|
|
user_key = request.headers.get("X-Forwarded-For", request.client.host if request.client else "unknown")
|
|
now = time.time()
|
|
window = now - 60
|
|
|
|
self.requests[user_key] = [t for t in self.requests[user_key] if t > window]
|
|
self.requests[user_key].append(now)
|
|
|
|
if len(self.requests[user_key]) > self.requests_per_minute:
|
|
return JSONResponse(status_code=429, content={"detail": "Rate limit exceeded"})
|
|
|
|
return await call_next(request)
|