Harden auth and persist report schedules

This commit is contained in:
dwindown
2026-06-06 19:40:32 +07:00
parent aaf64264f7
commit fd7989f673
18 changed files with 748 additions and 105 deletions

View File

@@ -4,14 +4,21 @@ Lightweight in-process rate limiting helpers.
from __future__ import annotations
import logging
import threading
import time
from collections import defaultdict, deque
from fastapi import HTTPException, Request, status
from redis.asyncio import Redis
from app.core.config import get_settings
_lock = threading.Lock()
_hits: dict[str, deque[float]] = defaultdict(deque)
_redis_client: Redis | None = None
_redis_unavailable = False
logger = logging.getLogger(__name__)
def _client_ip(request: Request) -> str:
@@ -20,16 +27,26 @@ def _client_ip(request: Request) -> str:
return "unknown"
def enforce_rate_limit(
request: Request,
def _get_redis_client() -> Redis | None:
global _redis_client
if _redis_unavailable:
return None
if _redis_client is None:
settings = get_settings()
if not settings.REDIS_URL:
return None
_redis_client = Redis.from_url(settings.REDIS_URL, decode_responses=True)
return _redis_client
def _enforce_in_memory_rate_limit(
*,
key: str,
scope: str,
max_requests: int,
window_seconds: int,
) -> None:
now = time.time()
ip = _client_ip(request)
key = f"{scope}:{ip}"
cutoff = now - window_seconds
with _lock:
@@ -43,3 +60,62 @@ def enforce_rate_limit(
)
dq.append(now)
async def enforce_rate_limit(
request: Request,
*,
scope: str,
max_requests: int,
window_seconds: int,
) -> None:
global _redis_unavailable
ip = _client_ip(request)
key = f"{scope}:{ip}"
redis = _get_redis_client()
if redis is not None:
try:
current = await redis.incr(key)
if current == 1:
await redis.expire(key, window_seconds)
if current > max_requests:
ttl = await redis.ttl(key)
retry_after = ttl if ttl and ttl > 0 else window_seconds
raise HTTPException(
status_code=status.HTTP_429_TOO_MANY_REQUESTS,
detail=f"Too many requests for {scope}. Please try again later.",
headers={"Retry-After": str(retry_after)},
)
return
except HTTPException:
raise
except Exception as exc:
_redis_unavailable = True
logger.warning("Redis rate limiter unavailable; falling back to memory: %s", exc)
_enforce_in_memory_rate_limit(
key=key,
scope=scope,
max_requests=max_requests,
window_seconds=window_seconds,
)
async def close_rate_limit() -> None:
global _redis_client
if _redis_client is None:
return
try:
await _redis_client.aclose()
finally:
_redis_client = None
def reset_rate_limit_state() -> None:
"""Reset local limiter state for tests."""
global _redis_client, _redis_unavailable
_redis_client = None
_redis_unavailable = False
with _lock:
_hits.clear()