Redis:
- Singleton ConnectionPool (redis.asyncio), 50 connections — не создаём
новое TCP-соединение на каждый HTTP-запрос
Rate limiter:
- Полностью переписан на async/await
- Lua-скрипт _LUA_CHECK_AND_INCR — атомарная проверка+инкремент без race condition
- Lua-скрипт _LUA_ACQUIRE_CONCURRENT — атомарный захват слота задачи
- Старый паттерн INCR→check→DECR удалён (race condition при конкурентных запросах)
Security:
- get_current_user кэширует пользователя в Redis на 5 минут (TTL)
Раньше: SELECT users на каждый HTTP-запрос
Теперь: Redis GET (кэш) → SELECT users (только при промахе)
- hashed_password НЕ кладётся в кэш
- invalidate_user_cache() для сброса при смене тарифа/пароля
- get_ws_user() для WebSocket через ?token=JWT (браузеры не могут
передавать Authorization header при WS-handshake)
WebSocket:
- Добавлена аутентификация (Depends(get_ws_user))
- Проверка ownership задачи ДО accept() соединения
- Чужой task_id → закрытие с кодом 4004
URL obfuscation:
- Task.public_id = secrets.token_urlsafe(16) = 22 случайных base64url символа
- Клиент работает только с public_id, внутренний UUID не раскрывается
- Все роутеры переключены на public_id в WHERE условиях
- TaskResponse больше не возвращает input_data (там minio_key и т.д.)
- Миграция 002_add_task_public_id.py
MinIO:
- Singleton клиент (не создаём новый на каждый upload)
- ensure_bucket() вызывается один раз при старте (lifespan), не на каждый запрос
- Путь uploads/{doc_uuid}{ext} — user_id убран из пути
CORS:
- Убраны wildcard allow_methods/allow_headers (несовместимы с credentials=True)
- Явный список: methods=[GET,POST,DELETE,OPTIONS], headers=[Authorization,Content-Type,Accept]
- Swagger/OpenAPI доступны только в ENVIRONMENT=development
Documents:
- Content-Length проверяется ДО чтения тела (ранняя отбивка больших файлов)
- Повторная проверка реального размера после чтения (защита от поддельного заголовка)
- Используем get_current_verified_user вместо get_current_user (требуем подтверждённый email)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
37 lines
1.2 KiB
Python
37 lines
1.2 KiB
Python
"""Singleton пул Redis-соединений.
|
||
|
||
Один ConnectionPool на весь процесс — не создаём новое соединение на каждый запрос.
|
||
Используем redis.asyncio для совместимости с FastAPI async endpoints.
|
||
"""
|
||
|
||
import redis.asyncio as aioredis
|
||
|
||
from app.config import settings
|
||
|
||
_pool: aioredis.ConnectionPool | None = None
|
||
|
||
|
||
def get_pool() -> aioredis.ConnectionPool:
|
||
"""Инициализировать пул при первом вызове, затем возвращать тот же."""
|
||
global _pool
|
||
if _pool is None:
|
||
_pool = aioredis.ConnectionPool.from_url(
|
||
settings.REDIS_URL,
|
||
max_connections=50,
|
||
decode_responses=True,
|
||
)
|
||
return _pool
|
||
|
||
|
||
def get_redis() -> aioredis.Redis:
|
||
"""Получить Redis клиент из пула (не создаёт новое TCP-соединение)."""
|
||
return aioredis.Redis(connection_pool=get_pool())
|
||
|
||
|
||
async def close_pool() -> None:
|
||
"""Закрыть пул при остановке приложения."""
|
||
global _pool
|
||
if _pool:
|
||
await _pool.aclose()
|
||
_pool = None
|