Files
office_translator/_bmad-output/implementation-artifacts/code-review-1-1-to-1-8.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

12 KiB
Raw Blame History

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_statusauth_service.create_user() appelle repo.create(..., subscription_status=SubscriptionStatus.ACTIVE.value) alors que la signature de UserRepository.create() na que email, name, password_hash, plan. → TypeError à linscription avec DB. services/auth_service.py:285-291 vs database/repositories.py:41-56
MEDIUM 1-1-M1 datetime.utcnow() déprécié — Larchitecture 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 lEnum 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 Userto_dict() nexpose pas tier ni daily_translation_count alors quils sont centraux pour lAPI (rate limit, billing). Risque dincohérence si dautres chemins sappuient 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 larchitecture parlent de tier (free/pro). Si user vient de la DB, _db_user_to_model mappe plan ; le champ tier du modèle SQLAlchemy nest 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 dutiliser user.hashed_password si le modèle subscription lexpose, sinon documenter la dépendance au legacy. routes/auth_routes.py:419 (modèle subscription User)
MEDIUM 1-3-M2 Tokens JWTcreate_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 lenv (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 optionnelrequest.json() en cas dexception → 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) — Larchitecture prévoit meta.rate_limit_remaining dans le body JSON. Limplémentation renvoie FileResponse pour /translate et met linfo 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 à lAC. 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. Cest 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 levent 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 — Lepic indique PATCH /api/v1/admin/users/{user_id}. Si lapp expose PATCH /admin/users/{user_id} (sans /api/v1), cest une déviation par rapport à la convention du reste de lAPI 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 dentré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 ; lappel dans auth_service.create_user() avec DB lève TypeError et casse linscription.

Recommandations prioritaires :

  1. Corriger lappel à UserRepository.create() : soit retirer largument subscription_status de lappel dans auth_service.create_user() (le repo met déjà SubscriptionStatus.ACTIVE dans le modèle), soit ajouter le paramètre au repository et le faire passer jusquau modèle. Recommandation : retirer largument dans auth_service.
  2. Remplacer datetime.utcnow() par datetime.now(timezone.utc) dans auth_service et repositories (migrations, completed_at, usage_reset, etc.).
  3. Clarifier AC5 story 1-6 : documenter que pour POST /translate (FileResponse), le rate limit est exposé en headers et non dans un body meta.

Suite à donner

Conformément au workflow (étape 4) :

  1. Corriger automatiquement — Appliquer les correctifs (au moins CRITICAL + HIGH + MEDIUM listés ci-dessus) dans le code et les tests.
  2. 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].
  3. Détails — Approfondir un point précis (indiquer le numéro dissue ou la story).

Réponse attendue : [1], [2] ou [3] (ou combinaison, ex. « 1 pour C1/H1, 2 pour le reste »).