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>
202 lines
13 KiB
Markdown
202 lines
13 KiB
Markdown
# 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: 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: <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 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)
|