Files
Inventory/REVIEW.md
T

30 KiB
Raw Blame History

Review complète — Projet Inventory

Audit éducatif du projet. Chaque point explique le problème, pourquoi c'est un problème, et comment le corriger. Document généré le 2026-06-11 (branche develop, v1.9.47). Complément de docs/REVIEW_ARCHITECTURE.md (2026-03-23) — les findings d'architecture n'y sont pas dupliqués, seul leur statut est mis à jour ici (§4).


Table des matières

  1. Sécurité
  2. Bugs fonctionnels
  3. Code mort et violations des règles projet
  4. Dette d'architecture — suivi REVIEW_ARCHITECTURE.md
  5. Documentation et configuration
  6. Frontend et UX
  7. CI/CD et dépendances
  8. Hygiène git
  9. Bonnes pratiques à retenir

1. Sécurité

1.1 CRITIQUE — Credentials de production commités dans .mcp.json

Fichier : .mcp.json (tracké dans git, pas dans .gitignore)

Le fichier contient le mot de passe du profil admin MCP de production (X-Profile-Password: A123 pour inventory.malio-dev.fr) et un bearer token Lesstime valide (project.malio-dev.fr). Toute personne ayant accès au dépôt (ou à son historique, même après suppression du fichier) peut s'authentifier sur les deux systèmes de production.

Pourquoi c'est grave : un secret commité reste dans l'historique git pour toujours. Le token Lesstime donne accès à tout le système de gestion de projet (tâches, temps, absences). Le mot de passe A123 est en plus trivial.

Correction :

  1. Révoquer/changer les deux secrets (mot de passe du profil MCP + régénérer le token Lesstime) — c'est l'étape la plus importante, supprimer le fichier ne suffit pas.
  2. git rm --cached .mcp.json + ajouter .mcp.json au .gitignore.
  3. Conserver un .mcp.json.example avec des placeholders.

1.2 CRITIQUE — create_test_user.php tracké à la racine avec credentials admin en clair

Fichier : create_test_user.php:16,59-61 (tracké dans git)

Script de debug qui crée un compte ROLE_ADMIN avec admin@admin.com / admin123 (hardcodé et affiché en clair sur stdout), connexion PDO brute root/root. Posé à la racine du projet, il est embarquable dans une image de prod et exécutable partout où vendor/ existe.

Pourquoi c'est grave : si ce script tourne (ou a tourné) en production, il existe un compte admin avec un mot de passe devinable en 3 essais. Les credentials sont aussi dans l'historique git.

Correction : supprimer le fichier du dépôt (git rm), vérifier en prod qu'aucun profil admin@admin.com actif n'existe. Le besoin légitime (seed d'un admin de dev) est déjà couvert par fixtures/ ou peut devenir une commande Symfony app:create-admin qui demande le mot de passe en argument.

1.3 IMPORTANT — Mot de passe de la base de prod hardcodé dans 9 scripts trackés

Fichiers : scripts/check-prod-values.php, scripts/fix-prod-all.php, scripts/restore-custom-field-values.php, scripts/migrate-orphaned-custom-fields.php, scripts/check-prod-audit-dates.php, scripts/check-prod-missing-piece-cfs.php, scripts/check-prod-orphaned-detail.php, scripts/fix-prod-recreate-and-migrate.php, scripts/verify-prod-health.php

Neuf scripts de réparation one-shot contiennent le couple ferme_user/fermerecette en dur — des credentials de base de données de production, dans le dépôt.

Pourquoi c'est grave : même problème que 1.1 — secret en clair dans l'historique. En plus ces scripts contournent Doctrine et l'audit : les exécuter par erreur modifie la prod sans trace.

Correction : changer le mot de passe PG concerné, puis archiver/supprimer ces scripts (ils ont déjà servi). S'ils doivent rester, lire les credentials depuis l'environnement (getenv('DATABASE_URL')) et les déplacer dans _archives/ (déjà gitignoré).

1.4 IMPORTANT — Aucune limite de taille d'upload (ni applicative, ni infra)

Fichiers : src/State/DocumentUploadProcessor.php:55-116, src/Controller/CommentController.php:104-137, infra/dev/php.ini, infra/prod/nginx.conf

Aucun des deux chemins d'upload ne vérifie $file->getSize(). Côté infra : php.ini ne définit ni upload_max_filesize ni post_max_size (défauts PHP : 2 Mo / 8 Mo), et nginx.conf prod n'a pas de client_max_body_size (défaut nginx : 1 Mo).

Pourquoi c'est un problème (double) :

  • Fonctionnel : en prod, tout upload > 1 Mo est probablement rejeté par nginx avec une erreur 413 brute (non gérée par le front) — alors que l'app est censée stocker des PDF techniques.
  • Sécurité : aucune limite choisie n'existe ; le jour où quelqu'un monte les limites infra "pour faire passer un gros PDF", plus rien ne protège le disque (un ROLE_VIEWER peut uploader via les commentaires, cf. 1.9).

Correction : décider d'une limite métier (ex. 50 Mo), puis l'appliquer aux 3 niveaux :

  1. Check applicatif if ($file->getSize() > self::MAX_UPLOAD_BYTES) dans les deux chemins (erreur 400 propre).
  2. upload_max_filesize = 50M / post_max_size = 55M dans infra/dev/php.ini et l'image prod.
  3. client_max_body_size 55m; dans infra/prod/nginx.conf.

1.5 MOYEN — Garde anti path-traversal incomplet dans DocumentStorageService

Fichier : src/Service/DocumentStorageService.php:28-42

getAbsolutePath() vérifie str_contains($relativePath, '..') puis compare realpath() au répertoire de stockage — mais realpath() renvoie false pour un fichier inexistant, donc le second contrôle est sauté dans ce cas. Un chemin absolu (/etc/passwd) passe le premier contrôle (pas de ..) : $this->storageDir.'/'.'/etc/passwd'… ne résout pas vers /etc/passwd, mais un chemin via symlink dans le storage le pourrait. L'exploitation exige d'écrire document.path en base (pas d'input direct utilisateur), donc le risque actuel est faible — c'est du hardening.

Correction : valider sur le répertoire parent, qui existe toujours :

$absolutePath = $this->storageDir.'/'.$relativePath;
$realParent   = realpath(dirname($absolutePath));
if (false === $realParent || !str_starts_with($realParent.'/', realpath($this->storageDir).'/')) {
    throw new RuntimeException(sprintf('Path traversal detected: "%s"', $relativePath));
}

1.6 MOYEN — Pas de protection CSRF : SameSite=Lax est la seule barrière

Fichiers : config/packages/framework.yaml:8, config/packages/nelmio_cors.yaml

L'auth est par cookie de session, et aucun endpoint d'écriture ne vérifie de token CSRF ni de header custom. La protection repose à 100 % sur cookie_samesite: lax (et le CORS pour les lectures cross-origin). C'est la posture courante pour une SPA en 2026, mais c'est une défense à un seul étage : un navigateur ancien/exotique ou une config future en sous-domaine partagé la ferait tomber. À noter : csrfToken existe dans nuxt.config.ts:59 mais n'est branché nulle part (config morte, cf. 3.1).

Correction (peu coûteuse) : exiger un header X-Requested-With: XMLHttpRequest sur les méthodes non-GET du firewall api (un listener de 15 lignes) — un formulaire HTML cross-site ne peut pas envoyer ce header. L'ajouter dans useApi.ts côté front. Supprimer le csrfToken mort.

1.7 MOYEN — session_fixation_strategy: none désactive la protection globalement

Fichier : config/packages/security.yaml:5, mitigé par src/Controller/SessionProfileController.php:96

Le choix est documenté (le migrate par défaut casse les requêtes concurrentes de la SPA) et le login appelle bien $session->migrate(true) manuellement — le flux actuel est correct. Le risque est futur : tout nouveau chemin d'authentification (reset de mot de passe, impersonation…) n'aura pas la régénération d'ID de session, silencieusement.

Correction : garder le réglage, mais l'encadrer : un test fonctionnel qui vérifie que l'ID de session change au login (échouera si quelqu'un retire le migrate(true)), et un commentaire dans SessionProfileController pointant vers security.yaml.

1.8 MOYEN — MCP HTTP : mot de passe en clair dans les headers à chaque requête

Fichier : src/Mcp/Security/McpHeaderAuthenticator.php:43-44, infra/prod/nginx-proxy.conf

Chaque requête MCP porte X-Profile-Password en clair. Les headers transitent par le proxy nginx et peuvent finir dans des logs (proxy, APM, outils de debug). Le rate-limiting et le hash côté serveur sont bien faits, mais le secret circule en permanence — et l'URL configurée dans .mcp.json est en http:// (pas de TLS).

Correction : passer à un token d'API dédié (longue chaîne aléatoire, stockée hashée, comparée via le hasher existant), transmis en Authorization: Bearer — comme le fait déjà Lesstime. Et servir /_mcp uniquement en HTTPS.

1.9 MINEUR — Création de commentaires + upload de fichiers ouverte à ROLE_VIEWER

Fichier : src/Controller/CommentController.php:33

Convention du projet : lecture = ROLE_VIEWER, écriture = ROLE_GESTIONNAIRE. La création de commentaire (avec pièces jointes !) est la seule écriture accessible aux viewers. Si c'est un choix métier (« tout le monde peut commenter »), OK — mais combiné à 1.4, un compte en lecture seule peut remplir le disque.

Correction : confirmer le choix métier et le documenter dans docs/BACKEND.md ; appliquer la limite de taille de 1.4 dans tous les cas.

1.10 MINEUR — download() sans les headers de protection de serve()

Fichier : src/Controller/DocumentServeController.php:105-116

serve() envoie X-Content-Type-Options: nosniff + Content-Security-Policy: sandbox (très bien) ; download() n'envoie ni l'un ni l'autre pour les fichiers disque. La disposition attachment protège déjà beaucoup, mais l'asymétrie est gratuite.

Correction : copier les deux headers dans download().

1.11 MINEUR — Pagination max très élevée sur certaines ressources

Fichiers : src/Entity/Constructeur.php:47 (2000), src/Entity/ConstructeurCategorie.php:43 (1000), src/Entity/Document.php:55 (500)

Un ?itemsPerPage=2000 charge 2000 entités + sérialisation en une requête. Pour des catalogues internes c'est sans doute volontaire (dropdowns sans pagination), mais c'est aussi un vecteur de charge facile.

Correction : vérifier ce que le front demande réellement et redescendre au besoin réel (ou documenter pourquoi 2000).

1.12 MINEUR — infra/dev/.env.docker.local tracké malgré le .gitignore

Fichier : infra/dev/.env.docker.local:27,34

Le fichier est dans .gitignore (ligne 23) mais déjà tracké (ajouté avant la règle — gitignore n'agit pas sur les fichiers déjà suivis). Secrets de dev faibles (changeme_…) : sans gravité en soi, mais le fichier est censé être local et chaque dev qui le modifie crée du diff.

Correction : git rm --cached infra/dev/.env.docker.local, fournir infra/dev/.env.docker.local.example (référencé par le README au passage).


2. Bugs fonctionnels

2.1 IMPORTANT — Fallback http://localhost:3000 dans useApi.ts

Fichier : frontend/app/composables/useApi.ts:18

const API_BASE_URL = (publicConfig.apiBaseUrl as string) || 'http://localhost:3000'

Si NUXT_PUBLIC_API_BASE_URL est vide au build/runtime de prod, tous les appels API partent silencieusement vers localhost:3000 (le port du dev server, qui ne sert même pas l'API). Échec garanti mais difficile à diagnostiquer.

Correction : fallback relatif '/api'… attention, useApi préfixe déjà /api lui-même — le bon fallback est donc '' (origine courante) :

const API_BASE_URL = (publicConfig.apiBaseUrl as string) || ''

Même nettoyage pour nuxt.config.ts:49 (http://localhost/api, valeur SSR jamais utilisée puisque ssr: false).

2.2 IMPORTANT — Uploads de prod probablement plafonnés à 1 Mo par nginx

Voir 1.4 — c'est le versant fonctionnel : sans client_max_body_size, nginx rejette en 413 tout body > 1 Mo, et le front n'affiche pas d'erreur claire (le toast générique de useApi au mieux). À tester en prod avec un PDF de 5 Mo ; si les uploads passent, c'est qu'une config existe ailleurs et il faut l'aligner dans le dépôt.

2.3 MOYEN — Deux mécanismes de maintenance déconnectés

Fichiers : src/Controller/MaintenanceController.php:56 (flag var/maintenance), infra/prod/nginx.conf:6 (flag maintenance.on), infra/prod/deploy.sh:47

Le toggle admin (PUT /api/admin/maintenance) écrit var/maintenance, lu par le middleware front — maintenance applicative. Le déploiement crée maintenance.on, lu par nginx — maintenance infra. Les deux coexistent volontairement mais rien ne le documente : un admin qui active la maintenance via l'UI ne bloque pas les appels API directs (le flag n'est vérifié que par le middleware front), et inversement.

Correction : documenter les deux niveaux dans DEPLOY.md ; idéalement faire vérifier le flag applicatif côté backend (listener kernel.request qui renvoie 503 pour les non-admins) plutôt que de ne compter que sur le middleware front (contournable).

2.4 MOYEN — Provider Symfony par email alors que email est nullable

Fichiers : config/packages/security.yaml:18, src/Entity/Profile.php:59-61, src/Controller/AdminProfileController.php:66

On peut créer un profil sans email (profils « kiosque »), mais le user provider charge par property: email. Ça marche aujourd'hui parce que SessionProfileAuthenticator charge par ID — le provider n'est jamais utilisé pour ces profils. Incohérence latente : tout futur usage du provider standard (remember-me, impersonation, commande console) cassera sur ces profils.

Correction : soit rendre l'email obligatoire et générer un email technique pour les kiosques, soit écrire un UserProviderInterface custom qui charge par id ou email, et le déclarer dans security.yaml.


3. Code mort et violations des règles projet

3.1 MINEUR — Config morte dans nuxt.config.ts

Fichier : frontend/nuxt.config.ts:56-59

csrfToken, requestTimeout, enableDebug, enableAnalytics, logLevel : définis, jamais consommés (seul apiTimeout est lu par useApi.ts). Du code mort en config laisse croire que des fonctionnalités existent (un lecteur pense que le CSRF est géré — il ne l'est pas, cf. 1.6).

Correction : supprimer ces 5 clés.

3.2 MINEUR — Variables JWT dans un projet 100 % session

Fichier : infra/dev/.env.docker.local:28-30

JWT_SECRET_KEY, JWT_PUBLIC_KEY, JWT_PASSPHRASE — copiées d'un autre projet (Lesstime/Starseed utilisent JWT, pas Inventory). Le CLAUDE.md martèle « pas JWT » ; ces variables sèment le doute.

Correction : supprimer les 3 lignes.

3.3 MOYEN — @nuxtjs/tailwindcss : dépendance inutilisée et conflictuelle

Fichier : frontend/package.json:21

Le projet utilise Tailwind 4 via @tailwindcss/vite (nuxt.config.ts:1,64). @nuxtjs/tailwindcss n'est référencé nulle part et installe son propre Tailwind v3 dans node_modules — bloat + risque de résolution ambiguë.

Correction : npm uninstall @nuxtjs/tailwindcss puis vérifier npm run build.

3.4 MINEUR — Dockerfile dev pollué par un template générique

Fichier : infra/dev/Dockerfile:48-53,82-100

Blocs commentés Oracle OCI8, IMAP/Kerberos, PDO MySQL/SQLite — aucun rapport avec un projet PostgreSQL. Bruit pur.

Correction : supprimer les blocs commentés.

3.5 MINEUR — node_modules/ orphelin à la racine backend

Pas de package.json à la racine, mais un node_modules/ (untracked) y traîne, et node_modules/ n'est pas dans le .gitignore racine — seul le hasard l'empêche d'être commité un jour.

Correction : rm -rf node_modules/ à la racine + ajouter /node_modules/ au .gitignore.

3.6 MINEUR — 3 fichiers utils restés en .js non typés

Fichiers : frontend/app/utils/documentPreview.js, frontend/app/utils/fileIcons.js, frontend/app/utils/printTemplates/machineReport.js

Importés depuis du .ts sans aucune sécurité de type — incohérent avec la règle « TypeScript, 0 erreur typecheck ».

Correction : renommer en .ts et typer les signatures (mécanique, bon candidat Codex).


4. Dette d'architecture — suivi REVIEW_ARCHITECTURE.md

État des 10 chantiers identifiés le 2026-03-23, vérifié ce jour : 1 corrigé sur 10, et le God controller a grossi.

# Source de complexité Statut 2026-06-11
1 smartMatch dupliqué dans les Sync Strategies Toujours dupliqué (ComposantSyncStrategy.php:380, PieceSyncStrategy.php:244)
2 Custom Fields : 4-6 FK nullable (polymorphisme pauvre) Inchangé, pas de contrainte CHECK
3 Composables géants ⚠️ Partiel : useComponentEdit.ts 539 LOC, usePieceEdit.ts 404, useComponentCreate.ts 366
4 Triple duplication utils custom fields Corrigé — fusionné dans shared/utils/customFields.ts
5 pendingStructure canal caché Toujours sans try/finally (ModelTypeProcessor.php)
6 PieceProductSyncSubscriber legacy Inchangé (recomputeSingleEntityChangeSet toujours là)
7 Double flush dans les processors Inchangé (ComposantProcessor.php:45,132)
8 MachineStructureController God controller Aggravé : 300+ → 1121 lignes
9 Dépendance circulaire useMachineDetailData Proxy ref toujours en place (:133)
10 Typage any systématique 179 occurrences dans 26 composables

Le point clé : la Phase 1 « quick wins » du plan (items 1, 5, 6, 7 — effort S chacun, sans impact d'interface) n'a pas été entamée en presque 3 mois, alors que ce sont les corrections au meilleur ratio risque/bénéfice du projet. Le MachineStructureController qui grossit de +150 lignes confirme la trajectoire : sans extraction de services, chaque feature l'alourdit.


5. Documentation et configuration

5.1 IMPORTANT — Toute la doc décrit encore un submodule qui n'existe plus

Fichiers : README.md:49-50,291-296, DEPLOY.md:58,62,208, RELEASE.md:49,117, frontend/README.md:150-154

Le frontend a été intégré au monorepo (cf. CLAUDE.md « plus de submodule »), mais le README explique git clone --recurse-submodules, un workflow de commit en deux temps, et DEPLOY/RELEASE font des git submodule update. Un nouveau dev qui suit le README perd du temps sur des commandes sans effet ; un déploiement scripté depuis DEPLOY.md exécute des étapes mortes.

Correction : purger toute mention de submodule des 4 fichiers, décrire le workflow monorepo (1 commit racine).

5.2 IMPORTANT — DEPLOY.md utilise l'utilisateur PG d'un autre projet

Fichier : DEPLOY.md (6 occurrences de ferme_user)

Les commandes psql/backup de DEPLOY.md utilisent ferme_user/fermerecette — copié-collé du projet Ferme. Le vrai user prod est inventory_user (cf. infra/prod/.env.example). Quelqu'un qui suit la doc en incident de prod tape des commandes qui échouent (ou pire, sur la mauvaise base si les deux co-habitent sur l'instance partagée).

Correction : remplacer les 6 occurrences par inventory_user et des placeholders de mot de passe.

5.3 MOYEN — RELEASE.md et CLAUDE.md référencent un fichier VERSION inexistant

Fichiers : RELEASE.md:17,50,80,82, CLAUDE.md (arbre projet)

La version vit dans config/version.yaml (1.9.47) — le fichier VERSION n'existe plus. scripts/release.sh et la CI (auto-tag-develop.yml) sont à jour, mais la doc décrit l'ancien système, y compris « frontend/nuxt.config.ts lit VERSION au build ».

Correction : mettre à jour RELEASE.md et la ligne d'arbre dans CLAUDE.md vers config/version.yaml.

5.4 MINEUR — Pas de .env.example backend ni de .env.docker.local.example

Un nouveau dev n'a aucun modèle des variables attendues côté backend (le README pointe vers infra/dev/.env.docker.local… qui est censé être local/ignoré, cf. 1.12).

Correction : créer infra/dev/.env.docker.local.example avec placeholders.

5.5 MINEUR — Fichiers de données métier à la racine du projet

Company (1).json, customer.json, customer.original.json, Ensemble simple rotor.pdf, inventory_prod (2).sql.gz traînent à la racine (untracked — le .sql.gz est protégé par le gitignore, pas les .json ni le .pdf : un git add . distrait les commiterait, et customer.json ressemble à des données client réelles → RGPD).

Correction : déplacer hors du dépôt (ex. ~/imports/), et ajouter une règle défensive au .gitignore (/*.json racine, /*.pdf).


6. Frontend et UX

6.1 MOYEN — Erreurs avalées : console.error sans feedback utilisateur

Fichiers : 57+ occurrences ; ex. useMachineDetailData.ts:372,385, useProfileSession.ts:27

Le pattern dominant en cas d'échec API est console.error(...) puis on continue avec null/[]. useApi toaste déjà les erreurs HTTP, donc une partie est couverte — mais les erreurs réseau/parsing et les branches qui catchent avant le toast laissent l'utilisateur devant une page partiellement vide sans explication, et créent du double-reporting ailleurs.

Correction : définir une règle unique : useApi est le seul à toaster ; les composables ne re-loggent pas, mais positionnent un état d'erreur (error.value = ...) que la page affiche (bandeau « Impossible de charger X — Réessayer »).

6.2 MOYEN — Appel /maintenance/check à chaque navigation

Fichier : frontend/app/middleware/profile.global.ts:34-39

Chaque changement de route d'un non-admin déclenche un aller-retour API. Latence ajoutée à toutes les navigations pour un état qui change une fois par an.

Correction : cacher le résultat dans un useState avec TTL (ex. 60 s) ; le 503 éventuel d'un appel API normal peut aussi servir de signal.

6.3 Rappels (déjà au §4)

any ×179, composables géants, dépendance circulaire — voir tableau §4 et docs/REVIEW_ARCHITECTURE.md pour les solutions détaillées.


7. CI/CD et dépendances

7.1 IMPORTANT — Aucun garde-fou automatisé : la CI ne lance ni tests ni lint

Fichiers : .gitea/workflows/auto-tag-develop.yml, .gitea/workflows/build-docker.yml, workflow de commit (CLAUDE.md : « committer avec --no-verify »)

La chaîne actuelle : push sur develop → auto-tag → build Docker → image prod. Aucune étape ne lance PHPUnit, php-cs-fixer, ESLint ou nuxi typecheck. Et comme le hook pre-commit (qui devait jouer ce rôle) est trop lent, la convention projet est de le contourner avec --no-verify. Résultat : il est possible de tagger et construire une image de prod avec des tests rouges sans qu'aucun système ne le signale.

Pourquoi c'est le finding process le plus important de cette review : la suite de tests est bonne (48 fichiers, DAMA, factories) — mais une suite de tests qui ne tourne pas en CI ne protège rien.

Correction : ajouter un workflow ci.yml déclenché sur PR + push develop :

  1. composer install + php-cs-fixer --dry-run + PHPUnit (avec un service PG 16).
  2. npm ci + eslint + npx nuxi typecheck + npm run build dans frontend/.
  3. Faire dépendre auto-tag-develop du succès de la CI (ou au minimum bloquer les PR). Le hook pre-commit lent peut alors être assumé comme optionnel.

7.2 MOYEN — Le build Docker de prod n'est pas testé avant le push

build-docker.yml build + push latest dès qu'un tag est posé — sans health-check de l'image (la CI de 7.1 règle l'essentiel ; un docker run --rm image php bin/console about est un bon smoke test bon marché).

7.3 Dépendances

  • Backend : propre, à jour (Symfony 8.0.*, AP ^4.2, ORM ^3.6). symfony/twig-bundle à vérifier : aucun template Twig dans templates/ — si seul le MCP bundle le requiert, le laisser en dépendance transitive.
  • Frontend : @nuxtjs/tailwindcss à supprimer (cf. 3.3). Le reste est à jour et utilisé.

8. Hygiène git

Synthèse des fichiers trackés à tort (détails en §1/§5) :

Fichier Problème Action
.mcp.json Credentials prod git rm --cached + gitignore + rotation
create_test_user.php Script debug + credentials git rm
scripts/{check,fix,restore,migrate,verify}-prod-*.php (9) Mot de passe PG prod rotation + archive/suppression
infra/dev/.env.docker.local Censé être local, déjà tracké git rm --cached + .example
.claude/settings.json Inoffensif (config plugins) OK, peut rester

Gaps .gitignore racine : /node_modules/, .mcp.json, /*.json (défensif), /*.pdf.

Note : contrairement au premier rapport d'agent, .claude/settings.local.json n'est pas tracké — pas d'action nécessaire.


9. Bonnes pratiques à retenir

Ce qui est bien fait dans le projet

  • declare(strict_types=1) partout, attributs PHP 8 modernes, code backend homogène et lisible.
  • Sécurité API systématique : chaque opération API Platform porte son security:, chaque controller custom ouvre par denyAccessUnlessGranted() — aucun endpoint accidentellement public trouvé.
  • Rate limiting sur le login et sur l'auth MCP (souvent oublié ailleurs).
  • IDs CUID : pas d'énumération séquentielle possible.
  • Upload : validation MIME par contenu (finfo), nom de fichier régénéré (CUID), stockage hors de public/ — et SVG exclu de l'allowlist (XSS évité).
  • serve() documents : nosniff + CSP: sandbox — au-dessus du standard.
  • Système d'audit propre (AbstractAuditSubscriber en template method, flag skipAudit réfléchi).
  • Tests solides : 48 fichiers, AbstractApiTestCase avec factories, DAMA rollback, coûts de hash réduits en test.
  • Le refacto customFields.ts a été fait — la dette de REVIEW_ARCHITECTURE n'est pas ignorée, juste lente.
  • Docker prod multi-stage propre, page maintenance nginx, volumes nommés pour le storage.
  • Zéro console.log, zéro TODO/FIXME oublié dans src/ et frontend/app/.

Les règles à graver

  1. Un secret commité est un secret grillé : la suppression du fichier ne suffit jamais, il faut révoquer (cf. .mcp.json, scripts prod).
  2. .gitignore n'agit pas sur les fichiers déjà trackésgit rm --cached d'abord.
  3. Une suite de tests qui ne tourne pas en CI ne protège rien : si le hook est trop lent pour être vécu, le garde-fou doit vivre en CI.
  4. Toute limite (taille, pagination, timeout) doit être choisie, pas héritée d'un défaut — sinon c'est le défaut le plus bas de la chaîne qui décide (nginx 1 Mo).
  5. Un fallback doit échouer bruyamment ou être correct|| 'http://localhost:3000' en prod est le pire des deux mondes.
  6. La doc copiée d'un autre projet est pire que pas de doc (ferme_user dans DEPLOY.md).
  7. Config morte = mensonge : un csrfToken non branché fait croire qu'une protection existe.
  8. Les scripts one-shot ont une date de péremption : après usage → _archives/ ou suppression.
  9. Quand un God controller existe, chaque feature le fait grossir : extraire tôt (1121 lignes et ça monte).
  10. Les quick wins planifiés perdent leur valeur s'ils ne sont jamais faits : la Phase 1 de REVIEW_ARCHITECTURE (4×effort S) attend depuis 3 mois.

Résumé par priorité

Priorité # Problème Fichier
P0 1.1 Credentials prod + token Lesstime commités (rotation requise) .mcp.json
P0 1.2 Script admin avec mot de passe en clair tracké create_test_user.php
P0 1.3 Mot de passe PG prod dans 9 scripts trackés scripts/*-prod-*.php
P1 7.1 CI sans tests ni lint + hook contourné .gitea/workflows/
P1 1.4/2.2 Aucune limite d'upload choisie (et défaut nginx 1 Mo) DocumentUploadProcessor.php, CommentController.php, infra/
P1 2.1 Fallback API localhost:3000 en prod frontend/app/composables/useApi.ts:18
P1 5.1 Doc submodule obsolète (4 fichiers) README.md, DEPLOY.md, RELEASE.md, frontend/README.md
P1 5.2 ferme_user dans DEPLOY.md DEPLOY.md
P1 1.5 Garde path-traversal incomplet DocumentStorageService.php:28-42
P2 1.6 CSRF : une seule barrière (SameSite) framework.yaml, useApi.ts
P2 1.7 Session fixation : protection désactivée globalement security.yaml:5
P2 1.8 Mot de passe MCP dans headers + HTTP sans TLS McpHeaderAuthenticator.php
P2 2.3 Double mécanisme maintenance non documenté MaintenanceController.php, nginx.conf
P2 2.4 Provider email vs email nullable security.yaml:18, Profile.php
P2 3.3 @nuxtjs/tailwindcss inutilisé (TW3 vs TW4) frontend/package.json:21
P2 5.3 Doc VERSION obsolète RELEASE.md, CLAUDE.md
P2 5.5 Données client à la racine (customer.json…) racine projet
P2 6.1 Erreurs avalées sans feedback UI composables (57+)
P2 6.2 Maintenance check à chaque navigation profile.global.ts:34-39
P2 1.12 .env.docker.local tracké malgré gitignore infra/dev/.env.docker.local
P3 1.9 Commentaires+upload en ROLE_VIEWER (à confirmer métier) CommentController.php:33
P3 1.10 Pagination max 2000/1000/500 Constructeur.php, Document.php
P3 1.11/3.2 Secrets dev faibles + vars JWT mortes infra/dev/.env.docker.local
P3 3.1 Config morte nuxt.config (5 clés) frontend/nuxt.config.ts:56-59
P3 3.4 Dockerfile dev : blocs commentés Oracle/MySQL infra/dev/Dockerfile
P3 3.5 node_modules/ orphelin racine + gitignore racine projet
P3 3.6 3 fichiers .js non typés frontend/app/utils/
P3 1.10 download() sans nosniff/CSP DocumentServeController.php:105-116
P3 5.4 Pas de .env.docker.local.example infra/dev/
P3 7.2 Image Docker poussée sans smoke test build-docker.yml

La dette d'architecture (§4) a son propre plan dans docs/REVIEW_ARCHITECTURE.md — recommandation : exécuter enfin sa Phase 1 (4 corrections effort S, sans impact d'interface).