22 KiB
Tickets correctifs — Projet Inventory
Liste de tâches issues de la review du 2026-06-11 (
REVIEW.md). Chaque ticket est autonome : contexte, ce qu'il faut faire, fichiers concernés. Commence par les P0, puis P1, etc. Convention de commit :fix(T-XXX) : description courte.
P0 — Urgents (sécurité)
T-001 — Révoquer et retirer les credentials de .mcp.json
Pourquoi : le fichier .mcp.json est dans git et contient le mot de passe admin MCP de production (A123) et un token Lesstime valide. Toute personne avec accès au dépôt (ou à son historique) peut se connecter aux deux systèmes. Supprimer le fichier ne suffit pas : git garde l'historique — il faut changer les secrets.
À faire :
- Changer le mot de passe du profil
admin-default-profilesurinventory.malio-dev.fr(et choisir un vrai mot de passe, pasA123). - Régénérer le bearer token Lesstime côté Lesstime.
- Sortir le fichier de git sans le supprimer du disque :
git rm --cached .mcp.json echo ".mcp.json" >> .gitignore - Créer
.mcp.json.exampleavec des placeholders :{ "mcpServers": { "inventory": { "type": "http", "url": "https://inventory.malio-dev.fr/_mcp", "headers": { "X-Profile-Id": "<PROFILE_ID>", "X-Profile-Password": "<PASSWORD>" } } } } - Remettre les nouveaux secrets dans ton
.mcp.jsonlocal (désormais ignoré). Fichiers :.mcp.json,.gitignore,.mcp.json.example(nouveau)
T-002 — Supprimer create_test_user.php et vérifier la prod
Pourquoi : ce script de debug, commité à la racine, crée un compte ROLE_ADMIN avec admin@admin.com / admin123 — un mot de passe devinable en quelques essais. S'il a déjà tourné en production, un compte admin faible existe peut-être en ce moment.
À faire :
- Vérifier en prod qu'aucun profil
admin@admin.comn'est actif :S'il existe : le désactiver ou changer son mot de passe immédiatement.SELECT id, email, is_active, roles FROM profiles WHERE email = 'admin@admin.com'; - Supprimer le script :
git rm create_test_user.php - (Optionnel) Si le besoin « créer un admin de dev » existe encore, créer une commande Symfony
app:create-adminqui prend le mot de passe en argument — ne jamais le hardcoder. Fichiers :create_test_user.php
T-003 — Changer le mot de passe PG prod et archiver les scripts *-prod-*.php
Pourquoi : 9 scripts dans scripts/ contiennent le mot de passe de la base de production en clair (fermerecette). Ce sont des scripts de réparation one-shot qui ont déjà servi : ils n'ont plus de raison d'être dans le dépôt avec des secrets dedans.
À faire :
- Changer le mot de passe de l'utilisateur PG concerné sur le serveur de prod.
- Archiver les scripts (le dossier
_archives/est déjà dans le.gitignore) :mkdir -p _archives/scripts-prod git rm scripts/check-prod-values.php scripts/check-prod-audit-dates.php \ scripts/check-prod-missing-piece-cfs.php scripts/check-prod-orphaned-detail.php \ scripts/fix-prod-all.php scripts/fix-prod-recreate-and-migrate.php \ scripts/migrate-orphaned-custom-fields.php scripts/restore-custom-field-values.php \ scripts/verify-prod-health.php # (les copies locales peuvent aller dans _archives/scripts-prod si tu veux les garder) - Si l'un d'eux doit rester utilisable : remplacer les credentials en dur par
getenv('DATABASE_URL'). Fichiers :scripts/*-prod-*.php,scripts/migrate-orphaned-custom-fields.php,scripts/restore-custom-field-values.php
P1 — Importants
T-004 — Ajouter une CI qui lance tests et lint
Pourquoi : aujourd'hui, rien ne lance les tests automatiquement : la CI Gitea ne fait que tagger et builder l'image Docker, et le hook pre-commit est contourné avec --no-verify (trop lent). On peut donc livrer en prod avec des tests rouges sans aucune alerte.
À faire :
- Créer
.gitea/workflows/ci.yml:name: CI on: pull_request: push: branches: [develop] jobs: backend: runs-on: ubuntu-latest services: postgres: image: postgres:16-alpine env: POSTGRES_USER: root POSTGRES_PASSWORD: root POSTGRES_DB: inventory_test ports: ["5432:5432"] steps: - uses: actions/checkout@v4 - uses: shivammathur/setup-php@v2 with: { php-version: '8.4', extensions: 'pdo_pgsql, intl' } - run: composer install --no-interaction --prefer-dist - run: vendor/bin/php-cs-fixer fix --dry-run --diff --allow-risky=yes - run: php bin/console doctrine:schema:create --env=test env: { DATABASE_URL: "postgresql://root:root@localhost:5432/inventory_test" } - run: vendor/bin/phpunit env: { DATABASE_URL: "postgresql://root:root@localhost:5432/inventory_test" } frontend: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: { node-version: 22 } - run: npm ci working-directory: frontend - run: npx eslint . working-directory: frontend - run: npx nuxi typecheck working-directory: frontend - run: npm run build working-directory: frontendAdapter les détails (version PHP exacte, env de test) au premier run — l'important est que les 4 vérifications (cs-fixer, PHPUnit, ESLint, typecheck) tournent.
- Dans Gitea, marquer ce workflow comme requis pour merger une PR vers
develop. Fichiers :.gitea/workflows/ci.yml(nouveau)
T-005 — Définir une limite de taille d'upload à tous les niveaux
Pourquoi : aucune limite n'est choisie nulle part. Conséquence double : en prod, nginx applique son défaut de 1 Mo (les gros PDF sont sans doute rejetés avec une erreur brute), et côté application rien n'empêcherait de remplir le disque si les limites infra étaient relevées. Décision à prendre : 50 Mo max (à ajuster au métier). À faire :
- Check applicatif dans
DocumentUploadProcessor::handleMultipartUpload()(après la validation MIME, ligne ~79) :private const MAX_UPLOAD_BYTES = 50 * 1024 * 1024; // 50 Mo if ($file->getSize() > self::MAX_UPLOAD_BYTES) { throw new BadRequestHttpException('Fichier trop volumineux (max 50 Mo).'); } - Même check dans
CommentController::create()dans la boucleforeach ($files as $file)(ligne ~106) — renvoyer$this->json(['message' => 'Fichier trop volumineux (max 50 Mo).'], 400). infra/dev/php.ini— ajouter :Et vérifier que l'image prod (infra/prod/Dockerfile) reçoit la même config.upload_max_filesize = 50M post_max_size = 55Minfra/prod/nginx.conf— dans le blocserver:(idem dansclient_max_body_size 55m;nginx-proxy.confsi le proxy frontal est aussi versionné ici).- Tester : upload d'un PDF de ~5 Mo en local, et vérifier le message d'erreur propre au-delà de 50 Mo.
Fichiers :
src/State/DocumentUploadProcessor.php,src/Controller/CommentController.php,infra/dev/php.ini,infra/prod/nginx.conf,infra/prod/Dockerfile
T-006 — Corriger le fallback d'URL API du frontend
Pourquoi : si la variable NUXT_PUBLIC_API_BASE_URL est vide en prod, tous les appels API partent vers http://localhost:3000 — c'est-à-dire vers la machine de l'utilisateur. L'app casse silencieusement. Un fallback vide = « même origine », ce qui est le comportement correct.
À faire :
frontend/app/composables/useApi.tsligne 18 :// AVANT const API_BASE_URL = (publicConfig.apiBaseUrl as string) || 'http://localhost:3000' // APRÈS (chaîne vide = même origine ; useApi ajoute déjà /api) const API_BASE_URL = (publicConfig.apiBaseUrl as string) || ''frontend/nuxt.config.tsligne ~49 : remplacer le fallback'http://localhost/api'par''(valeur SSR jamais utilisée, SSR off — autant ne pas mentir).- Vérifier en dev que tout fonctionne encore (la variable est définie en dev, donc rien ne doit changer), puis
npx nuxi typecheck. Fichiers :frontend/app/composables/useApi.ts,frontend/nuxt.config.ts
T-007 — Purger la doc « submodule » (le frontend est dans le monorepo)
Pourquoi : le frontend a été rapatrié dans le repo principal, mais README, DEPLOY, RELEASE et le README frontend décrivent encore le clonage --recurse-submodules et le workflow de commit en deux temps. Un nouveau dev (ou toi en incident de prod) suit des étapes qui n'existent plus.
À faire :
README.md: lignes 49-50 →git clone <url-du-repo>; section « workflow » lignes ~291-296 → décrire « un seul commit depuis la racine couvre backend + frontend ».DEPLOY.md: supprimer lesgit submodule update --init --recursive(lignes 58, 62, 208).RELEASE.md: supprimer les étapes submodule (lignes 49, 117).frontend/README.md: remplacer la section « submodule » (lignes 150-154) par « ce dossier fait partie du monorepo Inventory ». Fichiers :README.md,DEPLOY.md,RELEASE.md,frontend/README.md
T-008 — Corriger l'utilisateur PG dans DEPLOY.md
Pourquoi : les commandes de DEPLOY.md utilisent ferme_user / fermerecette — copié-collé du projet Ferme. Le vrai utilisateur est inventory_user. En situation d'incident, suivre la doc ferait taper des commandes qui échouent ou visent la mauvaise base.
À faire :
- Remplacer les 6 occurrences de
ferme_userparinventory_user. - Remplacer le mot de passe en clair par un placeholder
<mot-de-passe-prod>(le mot de passe n'a rien à faire dans la doc). Fichiers :DEPLOY.md
T-009 — Durcir le garde anti path-traversal de DocumentStorageService
Pourquoi : le contrôle realpath() (qui vérifie que le chemin final est bien dans le dossier de stockage) est sauté quand le fichier n'existe pas, car realpath() renvoie false dans ce cas. Le risque actuel est faible (le chemin vient de la base, pas de l'utilisateur), mais le contrôle se veut une protection — autant qu'il protège vraiment.
À faire : dans getAbsolutePath() (src/Service/DocumentStorageService.php:28-42) :
// AVANT
$absolutePath = $this->storageDir.'/'.$relativePath;
$realPath = realpath($absolutePath);
if (false !== $realPath && !str_starts_with($realPath, realpath($this->storageDir))) {
throw new RuntimeException(sprintf('Path traversal detected: "%s"', $relativePath));
}
// APRÈS — valider sur le dossier parent, qui existe toujours pour un fichier servi
$absolutePath = $this->storageDir.'/'.$relativePath;
$realParent = realpath(dirname($absolutePath));
$realStorage = realpath($this->storageDir);
if (false === $realStorage || false === $realParent
|| !str_starts_with($realParent.'/', $realStorage.'/')) {
throw new RuntimeException(sprintf('Path traversal detected: "%s"', $relativePath));
}
Garder le check str_contains($relativePath, '..') existant en première ligne. Ajouter un test unitaire avec un chemin contenant ../ et un chemin absolu.
Fichiers : src/Service/DocumentStorageService.php, tests/ (nouveau test)
P2 — Consolidation
T-010 — Hardening CSRF : header X-Requested-With obligatoire sur les écritures
Pourquoi : l'auth est par cookie, et la seule protection contre le CSRF (un site malveillant qui fait faire des requêtes à ton navigateur connecté) est l'attribut SameSite=Lax du cookie. Une deuxième barrière peu coûteuse : exiger un header custom, qu'un formulaire HTML cross-site ne peut pas envoyer.
À faire :
- Côté front, dans
useApi.ts, ajouter'X-Requested-With': 'XMLHttpRequest'aux headers de toutes les requêtes. - Côté back, créer un listener
kernel.requestqui renvoie 403 si la méthode n'est pas GET/HEAD/OPTIONS, que le chemin matche^/api(hors/api/session/profilepour le login) et que le header est absent. - Supprimer la clé morte
csrfTokendenuxt.config.ts(elle laisse croire qu'une protection CSRF existe). - Adapter les tests API (
AbstractApiTestCase) pour envoyer le header. Fichiers :frontend/app/composables/useApi.ts,src/EventListener/(nouveau),frontend/nuxt.config.ts,tests/AbstractApiTestCase.php
T-011 — Test fonctionnel : l'ID de session change au login
Pourquoi : la protection automatique contre la fixation de session est désactivée (session_fixation_strategy: none, choix documenté pour la SPA) et compensée par un $session->migrate(true) manuel au login. Si quelqu'un supprime ce migrate un jour, plus rien ne protège — un test doit le verrouiller.
À faire : écrire un test API qui : récupère le cookie de session avant login → se loggue → vérifie que l'ID de session a changé. Ajouter un commentaire dans SessionProfileController::activateProfile() pointant vers security.yaml:5.
Fichiers : tests/Api/SessionProfileTest.php (ou équivalent), src/Controller/SessionProfileController.php
T-012 — MCP : remplacer le mot de passe par un token Bearer + HTTPS
Pourquoi : le mot de passe du profil circule en clair dans les headers de chaque requête MCP (et l'URL configurée est en http://). Les headers finissent dans les logs des proxys. Un token dédié révocable, transmis en Authorization: Bearer, est le pattern déjà utilisé par Lesstime.
À faire : ajouter un champ mcpToken (hashé) sur Profile ou une table api_tokens, générer via une commande console, adapter McpHeaderAuthenticator pour valider le Bearer (garder le rate-limiting), mettre à jour .mcp.json.example, et servir /_mcp en HTTPS uniquement.
Fichiers : src/Mcp/Security/McpHeaderAuthenticator.php, src/Entity/Profile.php ou nouvelle entité, migration, .mcp.json.example
T-013 — Maintenance : faire respecter le flag côté backend
Pourquoi : le toggle admin écrit var/maintenance, mais seul le frontend (middleware) le vérifie. Quelqu'un qui appelle l'API directement (curl, MCP, script) passe à travers. Et le second mécanisme (maintenance.on lu par nginx, posé par deploy.sh) n'est documenté nulle part.
À faire :
- Listener
kernel.requestbackend : sivar/maintenanceexiste et que l'utilisateur n'est pas admin → 503 JSON (sauf/api/maintenance/checket/api/session/*). - Documenter les deux niveaux (applicatif vs nginx/deploy) dans
DEPLOY.md. Fichiers :src/EventListener/(nouveau),DEPLOY.md
T-014 — Résoudre l'incohérence provider/email nullable
Pourquoi : le user provider Symfony charge les profils par email, mais l'email est nullable (profils « kiosque »). Ça marche par chance (l'authenticator charge par ID), mais tout futur usage du provider standard cassera sur ces profils.
À faire : décision à prendre — option A : email obligatoire + email technique généré pour les kiosques ; option B : provider custom qui charge par id ou email. Documenter le choix dans docs/BACKEND.md.
Fichiers : config/packages/security.yaml, src/Entity/Profile.php, éventuellement src/Security/
T-015 — Supprimer @nuxtjs/tailwindcss
Pourquoi : dépendance non utilisée (le projet utilise @tailwindcss/vite, Tailwind 4) qui installe en plus un Tailwind 3 parallèle — bloat et risque de conflit de résolution.
À faire :
cd frontend && npm uninstall @nuxtjs/tailwindcss && npm run build && npx nuxi typecheck
Fichiers : frontend/package.json, frontend/package-lock.json
T-016 — Mettre à jour la doc de versioning (VERSION → config/version.yaml)
Pourquoi : RELEASE.md (lignes 17, 50, 80, 82) et l'arbre projet de CLAUDE.md référencent un fichier VERSION qui n'existe plus — la version vit dans config/version.yaml.
À faire : remplacer toutes les références ; vérifier au passage que la description du footer frontend (« lit VERSION au build ») correspond au mécanisme réel.
Fichiers : RELEASE.md, CLAUDE.md
T-017 — Sortir les données client de la racine + gitignore défensif
Pourquoi : customer.json, Company (1).json, Ensemble simple rotor.pdf (données client réelles → enjeu RGPD) traînent à la racine, non protégés par le .gitignore : un git add . distrait les commiterait.
À faire :
- Déplacer ces fichiers hors du dépôt (ex.
~/imports/inventory/). Supprimer aussiinventory_prod (2).sql.gzet lenode_modules/orphelin de la racine. - Ajouter au
.gitignoreracine :(les/node_modules/ /*.json /*.pdf.jsonlégitimes du projet sont dans des sous-dossiers ou explicitement trackés —composer.jsonetc. restent suivis car déjà trackés ; pour les nouveaux,git add -freste possible). Fichiers :.gitignore, racine du projet
T-018 — Uniformiser la gestion d'erreur frontend (état d'erreur au lieu de console.error)
Pourquoi : en cas d'échec de chargement, le pattern actuel est console.error(...) puis la page s'affiche à moitié vide, sans message. L'utilisateur ne sait pas que quelque chose a raté ni comment réessayer.
À faire : règle : useApi est le seul à toaster ; les composables exposent un error: Ref<string | null> que la page affiche (bandeau avec bouton Réessayer). Commencer par les 3 pages principales : détail machine (useMachineDetailData.ts:372,385), détail composant, détail pièce. Étendre ensuite au reste.
Fichiers : frontend/app/composables/useMachineDetailData.ts, useComponentEdit.ts, usePieceEdit.ts, pages correspondantes
T-019 — Cacher le résultat de /maintenance/check (TTL)
Pourquoi : chaque navigation d'un non-admin déclenche un appel API pour vérifier la maintenance — de la latence sur toutes les transitions de page pour un état qui ne change presque jamais.
À faire : dans profile.global.ts, stocker le résultat dans un useState avec timestamp et ne re-fetcher que si > 60 s.
Fichiers : frontend/app/middleware/profile.global.ts
T-020 — Détracker infra/dev/.env.docker.local + fournir un .example
Pourquoi : le fichier est dans le .gitignore mais a été commité avant l'ajout de la règle — git continue donc de le suivre. Chaque dev qui le modifie crée du diff, et ses secrets (même de dev) sont versionnés.
À faire :
cp infra/dev/.env.docker.local infra/dev/.env.docker.local.example
# Dans le .example : remplacer les valeurs par des placeholders <CHANGE_ME>
# et supprimer les variables JWT_* (inutilisées, cf. T-023)
git rm --cached infra/dev/.env.docker.local
git add infra/dev/.env.docker.local.example
Mettre à jour le README (section installation) pour mentionner la copie du .example.
Fichiers : infra/dev/.env.docker.local, infra/dev/.env.docker.local.example (nouveau), README.md
P3 — Nice to have
T-021 — Décision métier : commentaires (et pièces jointes) en ROLE_VIEWER ?
Pourquoi : seule écriture accessible aux lecteurs. Si c'est voulu (« tout le monde commente »), le documenter dans docs/BACKEND.md ; sinon passer à ROLE_GESTIONNAIRE (CommentController.php:33).
Fichiers : src/Controller/CommentController.php, docs/BACKEND.md
T-022 — Revoir les pagination max (2000/1000/500)
Pourquoi : Constructeur (2000), ConstructeurCategorie (1000), Document (500) — vérifier le besoin réel du front et redescendre, ou commenter pourquoi.
Fichiers : src/Entity/Constructeur.php, src/Entity/ConstructeurCategorie.php, src/Entity/Document.php
T-023 — Nettoyer les variables JWT et les secrets changeme
Pourquoi : JWT_SECRET_KEY/JWT_PUBLIC_KEY/JWT_PASSPHRASE ne servent à rien (auth session). APP_SECRET=changeme_… mérite une vraie valeur aléatoire locale. (Fusionne naturellement avec T-020.)
Fichiers : infra/dev/.env.docker.local
T-024 — Supprimer la config morte de nuxt.config.ts
Pourquoi : csrfToken, requestTimeout, enableDebug, enableAnalytics, logLevel ne sont consommés nulle part — de la config qui ment. (csrfToken est déjà traité par T-010.)
Fichiers : frontend/nuxt.config.ts:56-59
T-025 — Nettoyer le Dockerfile dev (blocs Oracle/IMAP/MySQL commentés)
Pourquoi : restes d'un template générique sans rapport avec un projet PostgreSQL.
Fichiers : infra/dev/Dockerfile:48-53,82-100
T-026 — Renommer les 3 utils .js en .ts
Pourquoi : documentPreview.js, fileIcons.js, printTemplates/machineReport.js sont importés depuis du TypeScript sans types. Tâche mécanique (bon candidat pour Codex).
Fichiers : frontend/app/utils/documentPreview.js, frontend/app/utils/fileIcons.js, frontend/app/utils/printTemplates/machineReport.js
T-027 — Ajouter nosniff + CSP: sandbox à download()
Pourquoi : serve() envoie ces deux headers de protection, download() non — asymétrie gratuite.
À faire : copier les deux headers->set(...) de serve() dans download() (DocumentServeController.php:110-116).
Fichiers : src/Controller/DocumentServeController.php
T-028 — Smoke test de l'image Docker avant push
Pourquoi : build-docker.yml pousse latest sans vérifier que l'image démarre.
À faire : entre build et push : docker run --rm gitea.malio.fr/malio-dev/inventory:${{ gitea.ref_name }} php bin/console about.
Fichiers : .gitea/workflows/build-docker.yml
Hors tickets : la dette d'architecture (smartMatch dupliqué, double flush,
pendingStructure, God controller à 1121 lignes,any×179…) a déjà son plan d'action chiffré dansdocs/REVIEW_ARCHITECTURE.md. Recommandation forte : exécuter sa Phase 1 (4 corrections effort S, sans impact d'interface) avant qu'elle ne prenne encore 3 mois — voirREVIEW.md§4.
Résumé
| Priorité | Tickets | Estimation |
|---|---|---|
| P0 | T-001 à T-003 | ~2h (+ rotations de secrets) |
| P1 | T-004 à T-009 | ~1,5 j |
| P2 | T-010 à T-020 | ~2,5 j |
| P3 | T-021 à T-028 | ~0,5 j |
| Total | 28 tickets | ~5 j |
Commence par T-001 — tant que les secrets ne sont pas révoqués, tout le reste est secondaire. Pour chaque ticket, fais un commit dédié avec le numéro dans le message (ex.
fix(T-001) : retirer .mcp.json du dépôt).