Multi-tenancy : espaces de travail + fix auth reload (rate limiter OPTIONS)
- Modèles Organization + OrgMember, migration Alembic (SQLite compatible) - organization_id nullable sur Document, Decision, Mandate, VotingProtocol - Service, schéma, router /organizations + dependency get_active_org_id - Seed : Duniter G1 + Axiom Team ; tout le contenu seed attaché à Duniter G1 - Backend : list/create filtrés par header X-Organization - Frontend : store organizations, WorkspaceSelector réel, useApi injecte l'org - Fix critique : rate_limiter exclut les requêtes OPTIONS (CORS preflight) → résout le bug "Failed to fetch /auth/me" au reload (429 sur preflight) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -64,14 +64,6 @@ class RateLimiterMiddleware(BaseHTTPMiddleware):
|
||||
self._last_cleanup: float = time.time()
|
||||
self._lock = asyncio.Lock()
|
||||
|
||||
def _get_limit_for_path(self, path: str) -> int:
|
||||
"""Return the rate limit applicable to the given request path."""
|
||||
if "/auth" in path:
|
||||
return self.rate_limit_auth
|
||||
if "/vote" in path:
|
||||
return self.rate_limit_vote
|
||||
return self.rate_limit_default
|
||||
|
||||
def _get_client_ip(self, request: Request) -> str:
|
||||
"""Extract the client IP from the request, respecting X-Forwarded-For."""
|
||||
forwarded = request.headers.get("x-forwarded-for")
|
||||
@@ -101,6 +93,22 @@ class RateLimiterMiddleware(BaseHTTPMiddleware):
|
||||
if ips_to_delete:
|
||||
logger.debug("Nettoyage rate limiter: %d IPs supprimees", len(ips_to_delete))
|
||||
|
||||
def _get_limit_for_request(self, request: Request) -> int:
|
||||
"""Return the rate limit applicable to the given request.
|
||||
|
||||
CORS preflight (OPTIONS) requests are never rate-limited — blocking them
|
||||
breaks authenticated cross-origin requests in browsers.
|
||||
Strict auth limit applies only to POST (login flows), not to GET /auth/me.
|
||||
"""
|
||||
if request.method == "OPTIONS":
|
||||
return 10_000 # effectively unlimited for preflights
|
||||
path = request.url.path
|
||||
if request.method == "POST" and "/auth" in path:
|
||||
return self.rate_limit_auth
|
||||
if "/vote" in path:
|
||||
return self.rate_limit_vote
|
||||
return self.rate_limit_default
|
||||
|
||||
async def dispatch(self, request: Request, call_next) -> Response:
|
||||
"""Check rate limit and either allow the request or return 429."""
|
||||
# Skip rate limiting for WebSocket upgrades
|
||||
@@ -111,8 +119,7 @@ class RateLimiterMiddleware(BaseHTTPMiddleware):
|
||||
await self._cleanup_old_entries()
|
||||
|
||||
client_ip = self._get_client_ip(request)
|
||||
path = request.url.path
|
||||
limit = self._get_limit_for_path(path)
|
||||
limit = self._get_limit_for_request(request)
|
||||
now = time.time()
|
||||
window_start = now - 60
|
||||
|
||||
@@ -133,7 +140,7 @@ class RateLimiterMiddleware(BaseHTTPMiddleware):
|
||||
|
||||
logger.warning(
|
||||
"Rate limit depasse pour %s sur %s (%d/%d)",
|
||||
client_ip, path, request_count, limit,
|
||||
client_ip, request.url.path, request_count, limit,
|
||||
)
|
||||
|
||||
return JSONResponse(
|
||||
|
||||
Reference in New Issue
Block a user