feat: homelab deployment - NPM + IONOS DNS + monitoring + NAS backup
- Restructured docker-compose for Nginx Proxy Manager (no custom nginx) - Added domain wordly.art configuration - Added Prometheus + Grafana monitoring stack with pre-configured dashboards - Added PostgreSQL backup script to NAS (daily/weekly/monthly rotation) - Added alert rules for backend, system, and Docker metrics - Updated deployment guide for NPM + IONOS DNS homelab setup - Added marketing plan document - PDF translator and watermark support - Enhanced middleware, routes, and translator modules Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -21,9 +21,9 @@ logger = logging.getLogger(__name__)
|
||||
class RateLimitConfig:
|
||||
"""Configuration for rate limiting"""
|
||||
# Requests per window
|
||||
requests_per_minute: int = 30
|
||||
requests_per_hour: int = 200
|
||||
requests_per_day: int = 1000
|
||||
requests_per_minute: int = 120
|
||||
requests_per_hour: int = 1000
|
||||
requests_per_day: int = 5000
|
||||
|
||||
# Translation-specific limits
|
||||
translations_per_minute: int = 10
|
||||
@@ -35,7 +35,7 @@ class RateLimitConfig:
|
||||
max_total_size_per_hour_mb: int = 500
|
||||
|
||||
# Burst protection
|
||||
burst_limit: int = 10 # Max requests in 1 second
|
||||
burst_limit: int = 30 # Max requests in 1 second
|
||||
|
||||
# Whitelist IPs (no rate limiting)
|
||||
whitelist_ips: list = field(default_factory=lambda: ["127.0.0.1", "::1"])
|
||||
@@ -270,6 +270,23 @@ class RateLimitManager:
|
||||
async def check_request(self, request: Request) -> tuple[bool, str, str]:
|
||||
"""Check if request is allowed, return (allowed, reason, client_id)"""
|
||||
client_id = self.get_client_id(request)
|
||||
|
||||
# Prefer user ID for authenticated users (avoids shared limits behind proxy)
|
||||
# Try to extract from already-set state (auth middleware ran first)
|
||||
user_id = None
|
||||
if hasattr(request, "state"):
|
||||
user_id = getattr(request.state, "client_id", None)
|
||||
if not user_id:
|
||||
# Try to get user from auth header for better per-user limiting
|
||||
auth_header = request.headers.get("Authorization", "")
|
||||
if auth_header.startswith("Bearer "):
|
||||
# Use a hash of the token as user identifier (no decoding needed)
|
||||
import hashlib
|
||||
token = auth_header[7:]
|
||||
user_id = f"tok:{hashlib.sha256(token.encode()).hexdigest()[:16]}"
|
||||
if user_id:
|
||||
client_id = user_id
|
||||
|
||||
self._total_requests += 1
|
||||
|
||||
if self.is_whitelisted(client_id):
|
||||
@@ -387,6 +404,19 @@ class RateLimitMiddleware(BaseHTTPMiddleware):
|
||||
if request.url.path.startswith("/static"):
|
||||
return await call_next(request)
|
||||
|
||||
# Skip rate limiting for lightweight GET endpoints (read-only, cacheable)
|
||||
# These are config/fetch endpoints that don't consume resources
|
||||
if request.method == "GET":
|
||||
skip_paths = (
|
||||
"/api/v1/languages",
|
||||
"/api/v1/providers",
|
||||
"/api/v1/auth/me",
|
||||
"/api/v1/auth/usage",
|
||||
"/api/v1/translations/", # status polling (uses job_id suffix)
|
||||
)
|
||||
if any(request.url.path.startswith(p) for p in skip_paths):
|
||||
return await call_next(request)
|
||||
|
||||
# Check rate limit
|
||||
allowed, reason, client_id = await self.manager.check_request(request)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user