# 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 1. **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 header `Retry-After` (seconds until midnight UTC). 2. **AC2: Pro sans limite** — User with tier="pro" has no daily limit (unlimited translations). 3. **AC3: Reset à minuit UTC** — daily_translation_count (or equivalent sliding-window counter) resets at midnight UTC. 4. **AC4: Redis + sliding window** — Rate limiting uses Redis with a sliding-window algorithm (per user, per day for free tier). 5. **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 - [x] **Task 1: Tier-aware rate limit service** (AC: 1, 2, 3, 4) - [x] 1.1 Introduce or reuse Redis client (connection from config/env). If Redis unavailable, fallback to in-memory with clear doc/comment. - [x] 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). - [x] 1.3 For free tier: limit 5 translations per day; for pro (and equivalent) tier: no daily cap. - [x] 1.4 Expose function to check quota (allowed, remaining, reset_at_utc) and to increment on successful translation. - [x] **Task 2: Middleware / dependency integration** (AC: 1, 5) - [x] 2.1 Apply tier-based quota check on translation endpoint(s): resolve current user (JWT or API key), get tier, then check/increment quota. - [x] 2.2 On quota exceeded: return 429, error code "QUOTA_EXCEEDED", message user-friendly, header Retry-After = seconds until next midnight UTC. - [x] 2.3 On success: include `meta.rate_limit_remaining` (and optionally `meta.rate_limit_reset_at` ISO8601) in response. - [x] **Task 3: daily_translation_count alignment** (AC: 3) - [x] 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). - [x] 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. - [x] **Task 4: Tests** (AC: 1–5) - [x] 4.1 Test: free user at 5 translations → next request returns 429 QUOTA_EXCEEDED and Retry-After. - [x] 4.2 Test: pro user can translate beyond 5 without 429. - [x] 4.3 Test: response payload includes meta.rate_limit_remaining for free user. - [x] 4.4 Test: after midnight UTC (or mocked reset), free user can translate again. - [x] 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.py` a déjà `tier` et `daily_translation_count`. Migration `002_add_tier_daily_count.py` existe. 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_user` ou équivalent, ou X-API-Key pour API). Voir `routes/auth_routes.py` et dépendances auth existantes. - **Endpoints concernés** : Tout endpoint qui déclenche une traduction (ex. POST upload/translate). Dans `main.py` la vérification actuelle est `rate_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] ```json { "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: ` (recommandé en secondes jusqu’à minuit UTC). - **Format succès avec meta** — [Source: architecture.md] ```json { "data": { ... }, "meta": { "rate_limit_remaining": 4, "rate_limit_reset_at": "2024-01-16T00:00:00Z" } } ``` Pour Pro, `rate_limit_remaining` peut être `null`, `-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 de `get_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"`, `message` en français, `details` optionnel (usage, limit, reset_at). Header `Retry-After` en secondes. Succès avec `meta.rate_limit_remaining` (et optionnellement `rate_limit_reset_at`). ### Architecture compliance - Conventions API : JSON snake_case, format erreur sans `data`, format succès avec `data` + `meta`. - NFR20 : Rate limiting par utilisateur, réponse 429 avec Retry-After. ### Library / framework - **Redis** : Client async recommandé (e.g. `redis.asyncio` ou `aioredis`) 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) ou `middleware/tier_quota.py` (nouveau) + possible `services/quota_service.py` si 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 avec `TOKEN_EXPIRED`, 400 avec `INVALID_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.py` et `main.py` ; modèle User avec `tier` et `daily_translation_count` dé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 `meta` dans 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 sous `routes/auth_routes.py` avec prefix `/api/v1/auth`. Route de traduction (upload) dans `main.py` ; vérification rate limit actuelle par IP avant traitement. - **Montage** : Middleware existant `RateLimitMiddleware` appliqué à 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 key `rate_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)