Files
office_translator/_bmad-output/implementation-artifacts/1-6-middleware-rate-limiting-par-tier.md
Sepehr Ramezani 26bd096a06 feat: production deployment - full update with providers, admin, glossaries, pricing, tests
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>
2026-04-25 15:01:47 +02:00

13 KiB
Raw Blame History

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

  • 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 optionally meta.rate_limit_reset_at ISO8601) 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: 15)
    • 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.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 lutilisateur 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]
    {
      "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]
    {
      "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 AC1AC5.
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 levent 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 dinté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 sappuyer sur lutilisateur 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 sinté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 dun 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 lendpoint 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 lapp ; 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 AC1AC5. 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)