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>
12 KiB
Revue de code adverse — Stories 1-1 à 1-8 (Epic 1)
Workflow: code-review
Date: 2026-02-21
Revu par: Agent (adversarial senior developer)
Stories: 1-1, 1-2, 1-3, 1-4, 1-5, 1-6, 1-7, 1-8
Contexte: Comparaison Git vs File List des stories, validation des AC et des tâches [x], qualité de code, sécurité, perf, conformité architecture.
Discrepancies Git vs Story File List
- Fichiers modifiés (git) :
.env.example,alembic/env.py,database/*,main.py,models/subscription.py,requirements.txt,routes/auth_routes.py,services/auth_service.py - Fichiers non suivis (??) :
alembic/versions/002_*,database/utils.py,middleware/tier_quota.py,tests/*,pytest.ini - Constat : Les fichiers créés (migrations, utils, middleware, tests) ne sont pas commités — MEDIUM (transparence / traçabilité). Les File List des stories sont globalement alignées avec le code touché.
Story 1-1 : Setup base de données & modèle User
Statut story : in-progress
Sprint : in-progress
Problèmes trouvés (5)
| Sévérité | Id | Description | Fichier:ligne / preuve |
|---|---|---|---|
| HIGH | 1-1-H1 | UserRepository.create() ne reçoit pas subscription_status — auth_service.create_user() appelle repo.create(..., subscription_status=SubscriptionStatus.ACTIVE.value) alors que la signature de UserRepository.create() n’a que email, name, password_hash, plan. → TypeError à l’inscription avec DB. |
services/auth_service.py:285-291 vs database/repositories.py:41-56 |
| MEDIUM | 1-1-M1 | datetime.utcnow() déprécié — L’architecture impose datetime.now(timezone.utc). Utilisé dans auth_service (create_access_token, create_refresh_token, _db_user_to_model, check_usage_limits, update_user JSON path) et dans repositories (increment_usage, update_status). |
services/auth_service.py (multiples), database/repositories.py:99, 241 |
| MEDIUM | 1-1-M2 | UserRepository.create(plan=PlanType.FREE.value) — On passe une str alors que la signature attend PlanType. Fonctionne avec l’Enum SQLAlchemy mais incohérent pour le typage et la maintenance. |
services/auth_service.py:289 |
| LOW | 1-1-L1 | Task 1.5 (UUID PK) reportée — La story le documente comme DEFERRED ; pas de preuve dans le code que la migration coordonnée est planifiée (lien vers une story/ticket). | Story 1-1, Task 1.5 |
| LOW | 1-1-L2 | Modèle User — to_dict() n’expose pas tier ni daily_translation_count alors qu’ils sont centraux pour l’API (rate limit, billing). Risque d’incohérence si d’autres chemins s’appuient sur to_dict(). |
database/models.py:121-138 |
Story 1-2 : Inscription utilisateur
Statut story : done
Sprint : done
Problèmes trouvés (4)
| Sévérité | Id | Description | Fichier:ligne / preuve |
|---|---|---|---|
| CRITICAL | 1-2-C1 | Même bug que 1-1-H1 — Inscription avec DB échoue à cause de subscription_status passé à repo.create(). Inscription en mode JSON fonctionne ; avec PostgreSQL/SQLite → crash. |
services/auth_service.py:285-291 |
| MEDIUM | 1-2-M1 | register_v1 retourne tier: user.plan.value — La story et l’architecture parlent de tier (free/pro). Si user vient de la DB, _db_user_to_model mappe plan ; le champ tier du modèle SQLAlchemy n’est pas exposé directement sur le modèle Pydantic User (subscription). Risque de divergence si plan et tier ne sont pas toujours synchronisés. |
routes/auth_routes.py:382-388 |
| LOW | 1-2-L1 | Réponse 201 — AC respectée (201 + user data). Pas de meta.rate_limit_remaining sur register ; acceptable car pas encore de traduction. |
— |
| LOW | 1-2-L2 | File List story 1-2 — Liste des fichiers modifiés (dont requirements, database, alembic) reflète des changements “hors story” ; utile pour traçabilité mais à garder cohérent avec les stories 1-1 / 1-2. | Story 1-2, File List |
Story 1-3 : Login utilisateur (JWT)
Statut story : done
Problèmes trouvés (4)
| Sévérité | Id | Description | Fichier:ligne / preuve |
|---|---|---|---|
| MEDIUM | 1-3-M1 | login_v1 utilise user.password_hash — Propriété dépréciée (alias de hashed_password). Préférable d’utiliser user.hashed_password si le modèle subscription l’expose, sinon documenter la dépendance au legacy. |
routes/auth_routes.py:419 (modèle subscription User) |
| MEDIUM | 1-3-M2 | Tokens JWT — create_access_token / create_refresh_token utilisent datetime.utcnow() au lieu de datetime.now(timezone.utc) (dépréciation Python 3.12+ et bonnes pratiques timezone). |
services/auth_service.py:141-144, 165-168 |
| LOW | 1-3-L1 | AC2 (JWT signing) — SECRET_KEY depuis l’env (JWT_SECRET / JWT_SECRET_KEY) ; OK. Pas de fallback en production à documenter. | — |
| LOW | 1-3-L2 | AC3/AC4 (INVALID_CREDENTIALS) — Même message pour “user not found” et “wrong password” ; conforme à la story (anti-énumération). | — |
Story 1-4 : Logout utilisateur
Statut story : done
Problèmes trouvés (3)
| Sévérité | Id | Description | Fichier:ligne / preuve |
|---|---|---|---|
| MEDIUM | 1-4-M1 | Blocklist JTI en mémoire — Documenté comme non thread-safe et perdu au redémarrage. Pour un déploiement multi-worker, la révocation ne sera pas partagée. Story 1.6 introduit Redis ; pas de migration de la blocklist vers Redis dans la codebase. | services/auth_service.py:76-94, Story 1-4 Dev Notes |
| LOW | 1-4-L1 | Corps logout optionnel — request.json() en cas d’exception → pass ; correct. Pas de validation stricte du body si présent. |
routes/auth_routes.py:415-423 |
| LOW | 1-4-L2 | Backward compat tokens sans JTI — Gérée (payload.get("jti") → pas de révocation). OK. | — |
Story 1-5 : Refresh token
Statut story : done
Problèmes trouvés (3)
| Sévérité | Id | Description | Fichier:ligne / preuve |
|---|---|---|---|
| MEDIUM | 1-5-M1 | refresh_v1 — Utilise user.plan.value pour create_access_token(..., tier=user.plan.value). Cohérent avec login_v1 ; même remarque que 1-2-M1 si un jour tier et plan divergent. |
routes/auth_routes.py:431-432 |
| LOW | 1-5-L1 | Guard isinstance(body, dict) — Présent ; évite 500 sur body non-objet. OK. |
routes/auth_routes.py:401-409 |
| LOW | 1-5-L2 | Rotation refresh token — Nouveau refresh_token émis à chaque refresh ; ancien non révoqué explicitement (pas demandé par la story). À considérer pour une future “rotation” sécurisée. | — |
Story 1-6 : Middleware rate limiting par tier
Statut story : done
Problèmes trouvés (5)
| Sévérité | Id | Description | Fichier:ligne / preuve |
|---|---|---|---|
| MEDIUM | 1-6-M1 | AC5 (meta.rate_limit_remaining) — L’architecture prévoit meta.rate_limit_remaining dans le body JSON. L’implémentation renvoie FileResponse pour /translate et met l’info dans les headers (X-Rate-Limit-Remaining, X-Rate-Limit-Reset-At). Pour un endpoint qui retourne du JSON, il faudrait meta dans le body ; pour un fichier, les headers sont une alternative raisonnable mais à documenter par rapport à l’AC. |
main.py:621-678 ; Story 1-6 Completion Notes |
| MEDIUM | 1-6-M2 | Tier pour quota — _tier_for_quota(current_user.plan) utilise plan (PlanType). tier_quota accepte une string ("free"/"pro" etc.) ; cohérent avec tier en DB. Vérifier que tous les chemins (API key, admin) passent bien un tier aligné. |
main.py:496, 612 ; middleware/tier_quota.py:129 |
| LOW | 1-6-L1 | Sliding window — AC4 demande “sliding window”. Implémentation par clé Redis rate_limit:daily:{user_id}:{YYYY-MM-DD} = fenêtre par jour UTC, pas fenêtre glissante 24h. C’est un “fixed window” par jour. À aligner avec le libellé AC ou documenter le choix. |
middleware/tier_quota.py, AC4 |
| LOW | 1-6-L2 | Fallback in-memory — Par process ; documenté. OK pour MVP. | — |
| LOW | 1-6-L3 | auth_update_user en to_thread — Utilisation de asyncio.to_thread pour ne pas bloquer l’event loop ; OK. |
main.py:632-636 |
Story 1-7 : Admin changement de tier manuel
Statut story : done
Problèmes trouvés (4)
| Sévérité | Id | Description | Fichier:ligne / preuve |
|---|---|---|---|
| MEDIUM | 1-7-M1 | Préfixe endpoint — L’epic indique PATCH /api/v1/admin/users/{user_id}. Si l’app expose PATCH /admin/users/{user_id} (sans /api/v1), c’est une déviation par rapport à la convention du reste de l’API v1. À documenter ou aligner. |
Story 1-7, main.py (route admin) |
| LOW | 1-7-L1 | Réponses 404/400 — Format { error, message, details? } sans data ; conforme à la revue précédente. |
— |
| LOW | 1-7-L2 | Audit log — event admin_tier_change avec target_user_id, new_tier, timestamp ; pas de contenu document. OK. | — |
| LOW | 1-7-L3 | Literal plan — Schéma Pydantic avec Literal pour les plans ; 422 sur valeur invalide. OK. | — |
Story 1-8 : Tracking usage pour billing
Statut story : review
Problèmes trouvés (5)
| Sévérité | Id | Description | Fichier:ligne / preuve |
|---|---|---|---|
| MEDIUM | 1-8-M1 | create_completed dans to_thread — La création du log de traduction est dans un asyncio.to_thread(_create_translation_log). Si la session sync est utilisée ailleurs dans le même flux, attention aux risques de concurrence / fermeture de session. Le context manager get_sync_session() est bien utilisé dans la fonction passée à to_thread. |
main.py:641-658 |
| MEDIUM | 1-8-M2 | update_status(..., status="completed") — Utilise datetime.utcnow() pour completed_at au lieu de datetime.now(timezone.utc). Incohérent avec create_completed qui utilise timezone.utc. |
database/repositories.py:241 |
| LOW | 1-8-L1 | Champs translation_log — AC2 demande user_id, file_name, file_size, timestamp, status, provider_used. Le modèle Translation a original_filename, file_size_bytes, created_at, completed_at, status, provider. completed_at est renseigné dans create_completed ; OK. |
database/models.py:141-184 |
| LOW | 1-8-L2 | Pas de contenu fichier — Aucun champ ne stocke le contenu ; NFR11/NFR16 respectés. | — |
| LOW | 1-8-L3 | Skip si pas DB — Si USE_DATABASE ou DATABASE_AVAILABLE est False, pas d’entrée Translation ; documenté. OK. | main.py:643-644 |
Synthèse
| Sévérité | Nombre |
|---|---|
| CRITICAL | 1 (1-2-C1 = 1-1-H1, inscription DB) |
| HIGH | 1 (1-1-H1) |
| MEDIUM | 12+ |
| LOW | 15+ |
Problème bloquant commun aux stories 1-1 et 1-2 :
UserRepository.create() ne prend pas subscription_status ; l’appel dans auth_service.create_user() avec DB lève TypeError et casse l’inscription.
Recommandations prioritaires :
- Corriger l’appel à
UserRepository.create(): soit retirer l’argumentsubscription_statusde l’appel dansauth_service.create_user()(le repo met déjàSubscriptionStatus.ACTIVEdans le modèle), soit ajouter le paramètre au repository et le faire passer jusqu’au modèle. Recommandation : retirer l’argument dansauth_service. - Remplacer
datetime.utcnow()pardatetime.now(timezone.utc)dansauth_serviceetrepositories(migrations, completed_at, usage_reset, etc.). - Clarifier AC5 story 1-6 : documenter que pour
POST /translate(FileResponse), le rate limit est exposé en headers et non dans un bodymeta.
Suite à donner
Conformément au workflow (étape 4) :
- Corriger automatiquement — Appliquer les correctifs (au moins CRITICAL + HIGH + MEDIUM listés ci-dessus) dans le code et les tests.
- Créer des action items — Ajouter une sous-section « Review Follow-ups (AI) » dans les tâches des stories concernées avec des items
- [ ] [AI-Review][Severity] Description [file:line]. - Détails — Approfondir un point précis (indiquer le numéro d’issue ou la story).
Réponse attendue : [1], [2] ou [3] (ou combinaison, ex. « 1 pour C1/H1, 2 pour le reste »).