---
suivi: 1806
date: 2026-10-07
sujet: Diagnostic monprojet côté Hectarion
chantier: monprojet
type: diagnostic
statut: poussé
hash: bf990cf6
fichiers:
  - docs/suivi/SUIVI_1806_diag_monprojet_cote_hectarion.md
---

# SUIVI #1806 — PHASE 1 diagnostic : monprojet côté Hectarion

Diagnostic seul. Aucune écriture en base, aucun appel Graph, aucun code, aucune migration.

## PROMPT ENVOYÉ

Page publique « Décrire mon projet » (monprojet.hectare.fr), hors ERP. En fin de conversation, e-mail signé vers une boîte dédiée. Hectarion relève cette boîte via Graph, vérifie la signature, crée une fiche projet prospect, range la transcription, alimente un tableau de bord. Constater en brut la lecture d’e-mails, le modèle prospect, les cookies de session, les graphiques déjà là. Architecture en 10 lignes, non codée.

## Fiches lues

- `#1063`, `#1226` : `OutlookAttachmentService` est un glisser-déposer délégué (`Mail.Read`, `/me`), pas une relève de boîte. Le lot ne réutilise pas ce service comme poller.
- `#1346`, `#1355` : `graph.microsoft.com` et `login.microsoftonline.com` sont joignables en CLI ; `*.powerplatform.com` ne l’est pas. La relève Graph peut viser cet hôte. Une notification Teams, elle, reste en PHP-FPM.
- `#1356` : le commentaire SIWI dit encore le jeton applicatif indisponible en artisan. L’endpoint web reste le déclencheur sûr.
- `#1368` : l’entrée Meta est un webhook HMAC en PHP-FPM, avec identifiant unique. Modèle de vérification, pas de transport (ici l’entrée est un e-mail).
- `#1502` : les leads Meta ont un commercial de repli en config. Utile plus tard pour un assigné, pas pour le schéma Meta lui-même.

## SYNTHÈSE

### 1. Lecture d’e-mails par Graph

La boîte de dépôt des factures ne lit pas une boîte en tâche de fond. Elle télécharge une pièce jointe Outlook quand un utilisateur authentifié la dépose.

Service : `app/Services/OutlookAttachmentService.php`.

- Jeton : `GraphApiService::getAccessToken()` (délégué, utilisateur connecté), ligne 78. Pas le jeton applicatif.
- Droits exigés dans le JWT : `Mail.Read` ou `Mail.ReadWrite`, lignes 85-93. Consentement à la connexion : `app/Http/Controllers/Auth/MicrosoftAuthController.php` lignes 49-50 (`Mail.Send` et `Mail.Read` délégués).
- Appels Graph, tous sur `/me` :
  - `POST https://graph.microsoft.com/v1.0/me/translateExchangeIds` (ewsId → restId), ligne 209.
  - `GET https://graph.microsoft.com/v1.0/me/messages/{id}?$select=id,subject,hasAttachments`, lignes 257-259. Contrôle, non bloquant.
  - `GET …/me/messages/{id}/attachments/{id}/$value`, lignes 276-277.
- `mailboxSmtpAddress` est journalisé (ligne 160), jamais utilisé comme boîte cible. Aucun `GET /users/{boîte}/messages` dans `app/`.

Déclencheur : `POST` depuis l’écran, pas un cron.

- `DepotFactureController::storeFromOutlook`, lignes 285-366. PDF seulement, 20 Mo max (`OutlookAttachmentService` ligne 20 ; validation `size` max 20480000, ligne 299).
- Front : `resources/js/Pages/DepotFactures/Index.vue` `handleOutlookAttachmentDrop` vers la ligne 428. Le même geste existe sur le widget dashboard et l’explorateur GED.
- Page de test réservée `GS_DSI` : `routes/web.php` lignes 1715-1717, `GraphMailTestController`.

Fréquence : à chaque dépôt. `routes/console.php` lignes 23-44 ne planifie aucune lecture de courrier.

Marquage « traité » : absent. Aucun `PATCH` `isRead`. L’e-mail reste dans Outlook. Seul le PDF devient un dépôt.

Pièces jointes : binaire Graph, enregistré sur le disque `factures`. Le corps du message n’est pas lu.

Jeton applicatif existant, autre usage : `GraphApiService::getAppAccessToken` lignes 23-52 (`client_credentials`, scope `https://graph.microsoft.com/.default`). Envoi : `POST /users/{expéditeur}/sendMail`, lignes 375-413 (`Mail.Send` application). Ce jeton ne liste aucune boîte.

Réseau OVH :

- CLI : `graph.microsoft.com` et `login.microsoftonline.com` sont le chemin prévu par le diagnostic `#1346` (`tools/diag/diag_sharepoint_403_nommage_1346.php` ligne 287 : « Si echec CLI, retenter en FPM n est pas requis pour ce host »). `#1355` : HTTPS CLI sauf `*.powerplatform.com`.
- Web seulement : `*.powerplatform.com`, `logic.azure.com`, `webhook.office.com` (Teams). Digest tâches déclenché en PHP-FPM (`#1206`). Intacct et l’API Meta sont aussi traités depuis le web (`#1121`, `#1368`).
- Tension : `SiwiDocumentDepotService.php` lignes 19-20 refuse encore `getAppAccessToken` en artisan (« indisponible en CLI OVH »). Pour une relève sans utilisateur connecté, le modèle déjà utilisé est un endpoint web PHP-FPM appelé par cron `curl`, pas une commande qui parle à Power Automate.

Réutilisable pour `projet@` ? Le client HTTP Graph et le jeton applicatif, oui. Le service Outlook, non : il lit `/me`, une pièce, sur un geste humain, sans corps de message ni marquage lu. Il faut un droit applicatif `Mail.Read` (absent du code actuel) et un traitement neuf.

### 2. Modèle prospect existant

Trois objets proches, aucun n’est une fiche « projet » terrain + maison.

| Objet | Table / écran | Ce que c’est |
|---|---|---|
| Lead Meta | `leads_marketing` (`database/migrations/2026_05_22_200100_create_leads_marketing_table.php` lignes 11-46). Écran `/admin/leads-marketing` (`routes/web.php` ligne 2113). | Formulaire Meta. `meta_lead_id` unique obligatoire. Statuts nouveau → perdu. Copie SharePoint (`CreateLeadSharePointItem`). |
| Contact / prospect commercial | `contacts` (`database/migrations/2026_05_13_140200_create_contacts_table.php`). `Contact::PROFIL_PROSPECT` (`app/Models/Contact.php` lignes 25-29). Onglet PROSPECTS de la commercialisation (`resources/js/Pages/Programmes/Commercialisation.vue` ligne 37 ; données = contacts du programme, `CommercialisationController.php` lignes 38-46). | Personne rattachée à un programme, puis réservation. |
| Acquéreur | `AcquereurController::show` sur une réservation en cours ou actée (`app/Http/Controllers/AcquereurController.php` lignes 27-34). | Aval d’une vente, pas un prospect entrant. |
| Prospection foncière | `/foncier/prospection` | Parcelles et propriétaires. Autre métier. |

`leads_marketing` impose un identifiant Meta et un payload publicitaire. Y loger un e-mail monprojet dupliquerait le pipeline et casserait l’unicité. `contacts` est l’annuaire d’un programme (financement, co-acquéreur, documents, RGPD manuel). Une fiche sans programme n’y a pas sa place. Le bon insertion : tables neuves. Un `contact_id` nullable plus tard, quand un commercial rattache le projet à un programme.

Droits actuels (`config/feature_permissions.php`) :

- `leads_marketing.*` et `marketing.*` : `GS_COMMUNICATION` (lignes 235-244). `GS_ENVOL` a l’écriture leads (lignes 281-282). `GS_DIRECTION_GENERALE` a `marketing.lecture` et `leads_marketing.assigner_commercial` (lignes 224-232).
- `acquereurs.*` / `commercialisation.*` : `GS_DIRECTION_GENERALE` (lignes 209-210), `GS_ENVOL` (lignes 274-275). La route commercialisation n’a pas de `feature.permission` ; le menu programme l’affiche dès qu’il y a un programme (`AppShell.vue` lignes 240-245).
- `GS_HECTARE` : seulement `foncier.lecture`, `foncier.gestion`, `foncier.prix` (lignes 299-303). Ce groupe ne voit ni les leads ni la commercialisation.
- `GS_DSI` et `GS_ADMIN_ERP` : `*`.
- Piège : `GS_DIRECTION_GENERALE` et `GS_ENVOL` ont `*.lecture` (lignes 191 et 271). Une nouvelle clé `quelquechose.lecture` leur serait donnée sans mapping explicite. `GS_HECTARE` et `GS_COMMUNICATION` ne l’auraient pas, sauf ligne ajoutée.

### 3. Sécurité

Cookies de session. Constat sur la configuration chargée par cette copie (valeur d’environnement non recopiée). Le `.env` du serveur OVH n’a pas été lu. Le modèle commité (`.env.example` ligne 49) est `SESSION_DOMAIN=null`.

- Domaine : `config/session.php` ligne 159, `env('SESSION_DOMAIN')` sans défaut. Classe résolue : vide = cookie limité à l’hôte exact. Ce n’est ni `envol.hectare.fr` en dur, ni `.hectare.fr`. `monprojet.hectare.fr` ne reçoit pas ce cookie.
- Secure : `SESSION_SECURE_COOKIE` non défini (`config/session.php` ligne 172 → `null`). Symfony n’ajoute `Secure` que si la requête est déjà HTTPS (`vendor/symfony/http-foundation/Response.php` lignes 300-303, `Cookie::isSecure` lignes 353-355).
- SameSite : `lax` (`config/session.php` ligne 202, défaut). HttpOnly : vrai (ligne 185). Partitioned : faux.

Clé de signature. Emplacement aligné sur Meta : bloc dans `config/services.php` (le secret Meta est `services.meta.app_secret`, ligne 125) et variable d’environnement, jamais dans le dépôt. Vérification déjà en place pour Meta (`MetaWebhookController.php` lignes 41-45) : `hash_hmac('sha256', $rawBody, $secret)` comparé avec `hash_equals`. À étendre pour l’e-mail : HMAC du corps JSON canonique, horodatage dans le message signé, rejet au-delà de quelques minutes, identifiant unique en colonne unique (l’analogue est `leads_marketing.meta_lead_id`, migration ligne 13).

E-mails non signés ou invalides. Le webhook Meta journalise un warning et répond 403 sans créer de lead (lignes 42-45). Même règle : journal + ligne de quarantaine, aucune fiche, aucun marquage « traité » qui ferait disparaître le message avant revue.

### 4. Tableau de bord

Graphiques : `vue3-apexcharts`, composant global `apexchart` (`resources/js/app.js` lignes 14 et 44).

- Le plus proche : `resources/js/Pages/Marketing/Meta/Dashboard.vue` — cartes (lignes 19-37), courbe `type="area"` (lignes 175-185), `DataTable` des derniers leads.
- Barres factures : `resources/js/Pages/Factures/TableauBord.vue` ligne 306.
- Courbe commercialisation : `resources/js/Components/Commercialisation/SynthesePanel.vue` ligne 256, plus un `DataTable`.
- Trésorerie programme : `resources/js/Pages/Programmes/Tresorerie/Index.vue` ligne 279.

L’accueil (`config/dashboard_widgets.php`) est une grille de listes, pas de graphiques. Les clés `leads_marketing_recents`, `meta_health`, `synthese_commerciale` sont obsolètes (lignes 120-131). Le tableau de campagne se cale sur la page Meta, pas sur un widget d’accueil.

### 5. Conclusion

Relève par endpoint web (cron vers PHP-FPM) : jeton applicatif, `users/{boîte}/messages`, droit `Mail.Read` application à créer. Pas le glisser-déposer `/me`.
Vérification HMAC-SHA256 du JSON canonique plus horodatage, rejet au-delà de quelques minutes, anti-rejeu par identifiant unique (`hash_equals`, comme Meta).
Signature absente ou fausse : journal et quarantaine, aucune fiche, message laissé non lu.
Tables neuves (fiche, transcription, consentement horodaté, trace de bannière). Hors leads Meta, hors contacts, hors prospection foncière. `contact_id` seulement au rattachement ultérieur à un programme.
Écrans liste et fiche. Feature dédiée. `GS_HECTARE` et `GS_COMMUNICATION` en mapping explicite. `*.lecture` donnerait déjà la lecture à la direction et à ENVOL : trancher ENVOL dans le lot droits.
Tableau de bord : `apexchart` et `DataTable` du dashboard Meta.
Conservation : 3 ans avec e-mail ou téléphone, 12 mois sans. Purge planifiée sur le modèle `api-logs:purger` (`routes/console.php` ligne 41, rétention 365 j). L’anonymisation manuelle des contacts (`AnnuaireController.php` lignes 211-237) ne convient pas. Transcription en base, pas dans SharePoint (les leads Meta y sont copiés, ce qui gêne une purge datée).
Cookie ERP : hôte exact, SameSite lax, Secure si HTTPS. Pas de session partagée avec monprojet.
Clé dans `config/services.php` et l’environnement, hors dépôt.
Ordre des lots : (1) clé, vérif, quarantaine ; (2) tables et création ; (3) relève Graph ; (4) écrans ; (5) tableau de bord ; (6) droits et menu ; (7) purge.

## DÉPLOIEMENT-TEST

Fiche seule. Pas de migration, pas de `journal:sync`, pas de build.

git pull origin main

## LEÇON

La boîte de dépôt n’est pas une boîte mail relevée. C’est le jeton de la personne qui dépose un PDF. Une seconde boîte (`projet@`) a besoin d’un droit applicatif et d’un endpoint web. Le cookie de session n’est pas posé sur `.hectare.fr` : la page publique et l’ERP ne partagent pas la session, ce qui est le comportement voulu tant que le pont reste l’e-mail signé.
