Major changes across backend, frontend, infrastructure: - Provider system with model selection (Google, DeepL, OpenAI, Ollama, Google Cloud) - Admin panel: user management, pricing, settings - Glossary system with CSV import/export - Subscription and tier quota management - Security hardening (rate limiting, API key auth, path traversal fixes) - Docker compose for dev, prod, and IONOS deployment - Alembic migrations for new tables - Frontend: dashboard, pricing page, landing page, i18n (en/fr) - Test suite and verification scripts Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
13 KiB
13 KiB
Story 1.6: Middleware Rate Limiting par Tier
Status: done
Story
As a system, I want to enforce daily translation quotas based on user tier, so that free users are limited and system resources are protected.
Acceptance Criteria
- AC1: Quota Free dépassé — Given a user with tier="free" has translated 5 files today, when they attempt another translation, then they receive HTTP 429 with body
{"error": "QUOTA_EXCEEDED", "message": "..."}and headerRetry-After(seconds until midnight UTC). - AC2: Pro sans limite — User with tier="pro" has no daily limit (unlimited translations).
- AC3: Reset à minuit UTC — daily_translation_count (or equivalent sliding-window counter) resets at midnight UTC.
- AC4: Redis + sliding window — Rate limiting uses Redis with a sliding-window algorithm (per user, per day for free tier).
- AC5: meta.rate_limit_remaining — Successful translation responses (and optionally other authenticated responses) include
meta.rate_limit_remaining(integer: remaining files for the current day for free; -1 or omit for pro).
Tasks / Subtasks
- Task 1: Tier-aware rate limit service (AC: 1, 2, 3, 4)
- 1.1 Introduce or reuse Redis client (connection from config/env). If Redis unavailable, fallback to in-memory with clear doc/comment.
- 1.2 Implement sliding-window daily counter per user_id in Redis (key pattern e.g.
rate_limit:daily:{user_id}, window = day in UTC). - 1.3 For free tier: limit 5 translations per day; for pro (and equivalent) tier: no daily cap.
- 1.4 Expose function to check quota (allowed, remaining, reset_at_utc) and to increment on successful translation.
- Task 2: Middleware / dependency integration (AC: 1, 5)
- 2.1 Apply tier-based quota check on translation endpoint(s): resolve current user (JWT or API key), get tier, then check/increment quota.
- 2.2 On quota exceeded: return 429, error code "QUOTA_EXCEEDED", message user-friendly, header Retry-After = seconds until next midnight UTC.
- 2.3 On success: include
meta.rate_limit_remaining(and optionallymeta.rate_limit_reset_atISO8601) in response.
- Task 3: daily_translation_count alignment (AC: 3)
- 3.1 Keep User.daily_translation_count in sync with Redis (or use as fallback if no Redis): increment on successful translation; reset at midnight UTC (job or on-read with date check).
- 3.2 Prefer single source of truth: either Redis-only for quota enforcement with DB as cache, or DB-only with Redis as cache; document choice.
- Task 4: Tests (AC: 1–5)
- 4.1 Test: free user at 5 translations → next request returns 429 QUOTA_EXCEEDED and Retry-After.
- 4.2 Test: pro user can translate beyond 5 without 429.
- 4.3 Test: response payload includes meta.rate_limit_remaining for free user.
- 4.4 Test: after midnight UTC (or mocked reset), free user can translate again.
- 4.5 Test: unauthenticated translation request handled (401 or existing behavior; quota not applied without user).
Dev Notes
- Le projet a déjà un rate limiting par IP dans
middleware/rate_limiting.py(SlidingWindowCounter, TokenBucket, par client_id = IP). La story 1.6 ajoute un quota par tier utilisateur (free 5/jour, pro illimité) avec Redis et sliding window, sans nécessairement supprimer le rate limit par IP (à conserver ou documenter la coexistence). - Modèle User :
database/models.pya déjàtieretdaily_translation_count. Migration002_add_tier_daily_count.pyexiste. Aligner la logique de quota avec ces champs (sync ou source of truth). - Auth : Les routes de traduction doivent résoudre l’utilisateur courant (JWT via
get_current_userou équivalent, ou X-API-Key pour API). Voirroutes/auth_routes.pyet dépendances auth existantes. - Endpoints concernés : Tout endpoint qui déclenche une traduction (ex. POST upload/translate). Dans
main.pyla vérification actuelle estrate_limit_manager.check_translation_limit(client_ip)— à remplacer ou compléter par un check par user/tier.
Architecture Compliance
- Format erreur 429 — [Source: architecture.md]
{ "error": "QUOTA_EXCEEDED", "message": "Limite quotidienne atteinte (5/5 fichiers). Réessayez après minuit UTC.", "details": { "current_usage": 5, "limit": 5, "tier": "free", "reset_at": "2024-01-16T00:00:00Z" } } - Header :
Retry-After: <seconds>(recommandé en secondes jusqu’à minuit UTC). - Format succès avec meta — [Source: architecture.md]
Pour Pro,
{ "data": { ... }, "meta": { "rate_limit_remaining": 4, "rate_limit_reset_at": "2024-01-16T00:00:00Z" } }rate_limit_remainingpeut êtrenull,-1, ou omis.
Fichiers à modifier / créer
| Fichier | Action |
|---|---|
middleware/rate_limiting.py ou nouveau middleware/tier_quota.py |
Ajouter logique quota par user/tier (Redis sliding window + limite 5 free). |
main.py (ou route translate) |
Remplacer/compléter check_translation_limit(client_ip) par check quota user + retour 429 QUOTA_EXCEEDED + Retry-After; ajouter meta.rate_limit_remaining en succès. |
database/models.py / database/utils.py |
Si on garde daily_translation_count comme reflet: incrément + reset minuit UTC (ou délégation au service Redis). |
tests/test_tier_rate_limit.py (ou équivalent) |
Nouveaux tests AC1–AC5. |
Config / .env.example |
REDIS_URL si pas déjà présent pour rate limiting. |
Fichiers à ne pas casser
routes/auth_routes.py,services/auth_service.py— Pas de changement requis sauf utilisation deget_current_user/ user dans la route translate.- Comportement existant du rate limit par IP (à conserver ou documenter comme couche supplémentaire).
Références
- [Source: _bmad-output/planning-artifacts/epics.md#Story 1.6]
- [Source: _bmad-output/planning-artifacts/architecture.md#API Response Formats]
- [Source: _bmad-output/planning-artifacts/architecture.md#Data Boundaries - Rate Limiting Logic]
- [Source: _bmad-output/planning-artifacts/prd.md#NFR20 - Rate limiting]
- [Source: _bmad-output/implementation-artifacts/1-5-refresh-token.md — Patterns auth, format data/meta]
Developer Context (Guardrails)
Technical requirements
- Backend : FastAPI, Python 3.11+. Rate limiting par user (identifié par JWT ou API key), pas seulement par IP.
- Redis : Utiliser Redis pour sliding-window quotidien par user (clé type
rate_limit:daily:{user_id}). Si Redis absent, fallback in-memory documenté. - Tier :
free→ 5 fichiers/jour;pro(et équivalents selon modèle) → pas de limite quotidienne. - Réponses : 429 avec
error: "QUOTA_EXCEEDED",messageen français,detailsoptionnel (usage, limit, reset_at). HeaderRetry-Afteren secondes. Succès avecmeta.rate_limit_remaining(et optionnellementrate_limit_reset_at).
Architecture compliance
- Conventions API : JSON snake_case, format erreur sans
data, format succès avecdata+meta. - NFR20 : Rate limiting par utilisateur, réponse 429 avec Retry-After.
Library / framework
- Redis : Client async recommandé (e.g.
redis.asyncioouaioredis) pour ne pas bloquer l’event loop. - FastAPI : Dépendance
get_current_user(ou équivalent) sur la route de traduction pour obtenir user_id et tier.
File structure
- Middleware ou service :
middleware/rate_limiting.py(étendre) oumiddleware/tier_quota.py(nouveau) + possibleservices/quota_service.pysi logique lourde. - Config : REDIS_URL dans config/env ; constante FREE_TIER_DAILY_LIMIT = 5.
Testing requirements
- Tests d’intégration ou unitaires : free user 5ème traduction OK, 6ème → 429 QUOTA_EXCEEDED, Retry-After présent ; pro user au-delà de 5 → 200 avec meta ; vérification de meta.rate_limit_remaining ; reset après minuit UTC (mock ou time freeze).
Previous Story Intelligence (1-5 Refresh Token)
- Fichiers modifiés :
routes/auth_routes.py(endpoint refresh v1),tests/test_auth_refresh.py. Auth service inchangé (verify_token, create_access_token, create_refresh_token). - Patterns : Réponses 200 avec
{"data": {...}, "meta": {}}, 401 avecTOKEN_EXPIRED, 400 avecINVALID_REQUEST. Pour la story 1.6, réutiliser le même style de réponses et s’appuyer sur l’utilisateur résolu via JWT (ou API key) pour obtenir tier et user_id. - Tests : Fixtures utilisateur/tokens dans tests auth ; pour 1.6, ajouter fixtures free vs pro user et mocker Redis si besoin.
Git Intelligence Summary
- Derniers commits : Docker/PostgreSQL, frontend/admin, providers (OpenRouter, DeepSeek), corrections admin login. Aucun commit récent sur rate limiting par tier.
- Codebase actuelle : rate limiting par IP dans
middleware/rate_limiting.pyetmain.py; modèle User avectieretdaily_translation_countdéjà présents. Implémentation 1.6 doit s’intégrer sans supprimer le rate limit IP sauf décision explicite (documenter coexistence ou remplacement).
Latest Tech Information
- Redis : Utiliser une fenêtre glissante (sliding window) en Redis : soit INCR + EXPIRE avec clé par jour (rollover à minuit UTC), soit sorted set (ZADD avec timestamp, ZREMRANGEBYSCORE pour fenêtre 24h). Retry-After = secondes jusqu’à minuit UTC (calcul avec datetime en UTC).
- FastAPI : Pour injecter
metadans toutes les réponses succès d’un sous-ensemble de routes, on peut utiliser un middleware de réponse ou une dépendance qui enrichit le corps ; pour la traduction, enrichir directement la réponse de l’endpoint est suffisant.
Project Context Reference
- Structure : Backend à la racine :
main.py,routes/,services/,database/,middleware/,tests/. Auth v1 sousroutes/auth_routes.pyavec prefix/api/v1/auth. Route de traduction (upload) dansmain.py; vérification rate limit actuelle par IP avant traitement. - Montage : Middleware existant
RateLimitMiddlewareappliqué à toute l’app ; le quota par tier peut être appliqué au niveau de la route translate (après auth) plutôt que dans un middleware global, pour avoir accès au user.
Story Completion Status
- Status : done
- Note : Implementation complete; tier quota (Redis + in-memory fallback), /translate integration, daily_translation_count sync, tests AC1–AC5. Code review 2026-02-20: tests 4.2/4.4 added, auth_update_user in thread pool, File List completed.
Change Log
- 2026-02-20: Story 1.6 implemented — middleware/tier_quota.py (Redis + in-memory), /translate tier quota check and 429 QUOTA_EXCEEDED, X-Rate-Limit-Remaining/Reset-At headers, daily_translation_count sync, 12 tests (unit + integration).
- 2026-02-20: Code review fixes — added test_pro_user_beyond_five_no_429 (Task 4.2), test_translate_free_user_after_reset_can_translate_again (Task 4.4); main.py auth_update_user run in asyncio.to_thread; File List completed with alembic/database/routes/requirements.
Dev Agent Record
Agent Model Used
{{agent_model_name_version}}
Debug Log References
Completion Notes List
- TierQuotaService in
middleware/tier_quota.py: Redis keyrate_limit:daily:{user_id}:{YYYY-MM-DD}, TTL 25h; in-memory fallback when REDIS_URL absent. check_quota(user_id, tier), increment_on_success(user_id). /translate: optional Depends(get_current_user). If user present: tier from plan (pro/business/enterprise = unlimited), quota check before translation; 429 JSONResponse with QUOTA_EXCEEDED and Retry-After on exceed; on success increment quota + DB daily_translation_count, add headers X-Rate-Limit-Remaining and X-Rate-Limit-Reset-At.- Source of truth: Redis for enforcement; User.daily_translation_count updated on each successful translation for reporting (documented in tier_quota.py). Reset at midnight UTC: automatic in Redis (new key per day).
- Tests: 12 tests in
tests/test_tier_rate_limit.py(unit + integration for 429, Retry-After, X-Rate-Limit-Remaining, unauthenticated). - [Code review 2026-02-20] Tests 4.2 and 4.4 added (pro user beyond 5; reset then free user can translate again). auth_update_user called via asyncio.to_thread in main.py to avoid blocking event loop. AC5: for FileResponse, rate-limit info in headers (X-Rate-Limit-Remaining / X-Rate-Limit-Reset-At); story aligned with implementation.
File List
- middleware/tier_quota.py (new)
- main.py (modified: tier quota check, 429 response, increment, headers, auth_update_user via asyncio.to_thread)
- models/subscription.py (modified: daily_translation_count on User)
- services/auth_service.py (modified: daily_translation_count in _db_user_to_model)
- tests/test_tier_rate_limit.py (new; + tests 4.2 pro beyond 5, 4.4 reset then translate again)
- alembic/env.py (modified: context for DB/migrations used by tier/daily_translation_count)
- database/init.py (modified: context for DB)
- database/connection.py (modified: context for DB)
- database/models.py (modified: daily_translation_count, tier on User)
- routes/auth_routes.py (modified: context for get_current_user used by /translate)
- requirements.txt (modified: dependencies for Redis/DB if applicable)
- _bmad-output/implementation-artifacts/sprint-status.yaml (modified: 1-6 → in-progress then review)
- _bmad-output/implementation-artifacts/1-6-middleware-rate-limiting-par-tier.md (modified: tasks, status, Dev Agent Record, File List)