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

202 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Story 1.6: Middleware Rate Limiting par Tier
Status: done
<!-- Note: Validation is optional. Run validate-create-story for quality check before dev-story. -->
## 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: 15)
- [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 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]
```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: <seconds>` (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 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)