# Documentation Technique API RESTful PHP 8.3 ## Table des matières 1. [Structure du projet](#structure-du-projet) 2. [Configuration du serveur](#configuration-du-serveur) 3. [Flux d'une requête](#flux-dune-requête) 4. [Architecture des composants](#architecture-des-composants) 5. [Base de données](#base-de-données) 6. [Sécurité](#sécurité) 7. [Gestion des mots de passe (NIST SP 800-63B)](#gestion-des-mots-de-passe-nist-sp-800-63b) 8. [Endpoints API](#endpoints-api) 9. [Changements récents](#changements-récents) ## Structure du projet ```plaintext /api/ ├── docs/ │ └── TECHBOOK.md ├── src/ │ ├── Controllers/ │ │ └── UserController.php │ ├── Core/ │ │ ├── Router.php │ │ ├── Request.php │ │ ├── Response.php │ │ ├── Session.php │ │ └── Database.php │ └── Config/ │ └── config.php ├── index.php └── .htaccess ``` ## Configuration du serveur ### Prérequis - Debian 12 - NGINX - PHP 8.3-FPM - MariaDB 10.11 ### Configuration NGINX Le serveur NGINX est configuré pour rediriger toutes les requêtes vers le point d'entrée `index.php` de l'API. ### Configuration PHP-FPM PHP-FPM est configuré pour gérer les processus PHP avec des paramètres optimisés pour une API. ## Flux d'une requête Exemple détaillé du parcours d'une requête POST /api/users : 1. **Entrée de la requête** - La requête arrive sur le serveur NGINX - NGINX redirige vers PHP-FPM via le socket unix - Le fichier .htaccess redirige vers index.php 2. **Initialisation (index.php)** - Chargement des dépendances - Initialisation de la configuration - Démarrage de la session - Configuration des headers CORS - Initialisation du routeur 3. **Routage** - Le Router analyse la méthode HTTP (POST) - Analyse de l'URI (/api/users) - Correspondance avec les routes enregistrées - Instanciation du Controller approprié 4. **Traitement (UserController)** - Vérification de l'authentification - Récupération des données JSON - Validation des données reçues - Traitement métier - Interaction avec la base de données - Préparation de la réponse 5. **Réponse** - Formatage de la réponse en JSON - Configuration des headers de réponse - Envoi au client ## Architecture des composants ### Core Components #### Router - Gère le routage des requêtes - Associe les URLs aux Controllers - Gère les paramètres d'URL - Dispatch vers les méthodes appropriées #### Request - Parse les données entrantes - Gère les différents types de contenu - Nettoie et valide les entrées - Fournit une interface unifiée pour accéder aux données #### Response - Formate les réponses en JSON - Gère les codes HTTP - Configure les headers de réponse - Assure la cohérence des réponses #### Session - Gère l'état des sessions - Stocke les données d'authentification - Vérifie les permissions - Sécurise les données de session #### Database - Gère la connexion à MariaDB - Fournit une interface PDO - Gère le pool de connexions - Assure la sécurité des requêtes ## Base de données ### Structure des tables principales #### Table `users` - `encrypted_user_name` : Identifiant de connexion chiffré (unique) - `encrypted_email` : Email chiffré (unique) - `user_pass_hash` : Hash du mot de passe - `encrypted_name`, `encrypted_phone`, `encrypted_mobile` : Données personnelles chiffrées - Autres champs : `first_name`, `sect_name`, `fk_role`, `fk_entite`, etc. #### Table `entites` (Amicales) - `chk_mdp_manuel` (DEFAULT 0) : Gestion manuelle des mots de passe - `chk_username_manuel` (DEFAULT 0) : Gestion manuelle des identifiants - `chk_stripe` : Activation des paiements Stripe - Données chiffrées : `encrypted_name`, `encrypted_email`, `encrypted_phone`, etc. #### Table `medias` - `support` : Type de support (entite, user, operation, passage) - `support_id` : ID de l'élément associé - `file_category` : Catégorie (logo, export, carte, etc.) - `file_path` : Chemin complet du fichier - `processed_width/height` : Dimensions après traitement - Utilisée pour stocker les logos des entités ### Chiffrement des données Toutes les données sensibles sont chiffrées avec AES-256-CBC : - Emails, noms, téléphones - Identifiants de connexion - Informations bancaires (IBAN, BIC) ### Migration de base de données Script SQL pour ajouter les nouveaux champs : ```sql -- Ajout de la gestion manuelle des usernames ALTER TABLE `entites` ADD COLUMN `chk_username_manuel` tinyint(1) unsigned NOT NULL DEFAULT 0 COMMENT 'Gestion des usernames manuelle (1) ou automatique (0)' AFTER `chk_mdp_manuel`; -- Index pour optimiser la vérification d'unicité ALTER TABLE `users` ADD INDEX `idx_encrypted_user_name` (`encrypted_user_name`); ``` ## Sécurité ### Mesures implémentées - Validation stricte des entrées - Protection contre les injections SQL (PDO) - Hachage sécurisé des mots de passe - Headers de sécurité HTTP - Gestion des CORS - Session sécurisée - Authentification requise - Chiffrement AES-256 des données sensibles - Envoi séparé des identifiants par email ## Gestion des mots de passe (NIST SP 800-63B) ### Vue d'ensemble L'API implémente un système de gestion des mots de passe conforme aux recommandations NIST SP 800-63B, avec quelques adaptations spécifiques demandées par le client. ### Service PasswordSecurityService Le service `PasswordSecurityService` (`src/Services/PasswordSecurityService.php`) gère : - Validation des mots de passe selon NIST - Vérification contre les bases de données de mots de passe compromis (HIBP) - Génération de mots de passe sécurisés - Estimation de la force des mots de passe ### Conformités NIST respectées | Recommandation NIST | Notre Implémentation | Status | |-------------------|---------------------|--------| | **Longueur minimale : 8 caractères** | ✅ MIN = 8 caractères | ✅ CONFORME | | **Longueur maximale : 64 caractères minimum** | ✅ MAX = 64 caractères | ✅ CONFORME | | **Accepter TOUS les caractères ASCII imprimables** | ✅ Aucune restriction sur les caractères | ✅ CONFORME | | **Accepter les espaces** | ✅ Espaces acceptés (début, milieu, fin) | ✅ CONFORME | | **Accepter Unicode (émojis, accents, etc.)** | ✅ Support UTF-8 avec `mb_strlen()` | ✅ CONFORME | | **Vérifier contre les mots de passe compromis** | ✅ API Have I Been Pwned avec k-anonymity | ✅ CONFORME | | **Pas d'obligation de composition** | ✅ Pas d'erreur si manque majuscules/chiffres/spéciaux | ✅ CONFORME | | **Pas de changement périodique forcé** | ✅ Aucune expiration automatique | ✅ CONFORME | | **Permettre les phrases de passe** | ✅ "Mon chat Félix a 3 ans!" accepté | ✅ CONFORME | ### Déviations par choix du client | Recommandation NIST | Notre Implémentation | Raison | |-------------------|---------------------|--------| | **Email unique par compte** | ❌ Plusieurs comptes par email autorisés | Demande client | | **Mot de passe ≠ identifiant** | ❌ Mot de passe = identifiant autorisé | Demande client | | **Vérifier contexte utilisateur** | ❌ Pas de vérification nom/email dans mdp | Demande client | ### Vérification contre les mots de passe compromis #### Have I Been Pwned (HIBP) API L'implémentation utilise l'API HIBP avec la technique **k-anonymity** pour préserver la confidentialité : 1. **Hash SHA-1** du mot de passe 2. **Envoi des 5 premiers caractères** du hash à l'API 3. **Comparaison locale** avec les suffixes retournés 4. **Aucun mot de passe en clair** n'est transmis #### Mode "Fail Open" En cas d'erreur de l'API HIBP : - Le système laisse passer le mot de passe - Un avertissement est enregistré dans les logs - L'utilisateur n'est pas bloqué ### Exemples de mots de passe #### Acceptés (conformes NIST) - `monmotdepasse` → Accepté (≥8 caractères, pas compromis) - `12345678` → Accepté SI pas dans HIBP - `Mon chat s'appelle Félix!` → Accepté (phrase de passe) - ` ` → Accepté si ≥8 espaces - `😀🎉🎈🎁🎂🍰🎊🎀` → Accepté (8 émojis) - `jean.dupont` → Accepté même si = username #### Refusés - `pass123` → Refusé (< 8 caractères) - `password` → Refusé (compromis dans HIBP) - `123456789` → Refusé (compromis dans HIBP) - Mot de passe > 64 caractères → Refusé ### Force des mots de passe Le système privilégie la **LONGUEUR** sur la complexité (conforme NIST) : | Longueur | Force | Score | |----------|-------|-------| | < 8 car. | Trop court | 0-10 | | 8-11 car. | Acceptable | 20-40 | | 12-15 car. | Bon | 40-60 | | 16-19 car. | Fort | 60-80 | | ≥20 car. | Très fort | 80-100 | | Compromis | Compromis | ≤10 | ### Génération automatique Pour la génération automatique, le système reste **strict** pour garantir des mots de passe forts : - Longueur : 12-16 caractères - Contient : majuscules + minuscules + chiffres + spéciaux - Vérifié contre HIBP (10 tentatives max) - Exemple : `Xk9#mP2$nL5!` ### Gestion des comptes multiples par email Depuis janvier 2025, le système permet plusieurs comptes avec le même email : #### Fonction `lostPassword` adaptée - Recherche **TOUS** les comptes avec l'email fourni - Génère **UN SEUL** mot de passe pour tous ces comptes - Met à jour **TOUS** les comptes en une requête - Envoie **UN SEUL** email avec la liste des usernames concernés #### Exemple de comportement Si 3 comptes partagent l'email `contact@amicale.fr` : - `jean.dupont` - `marie.martin` - `paul.durand` L'email contiendra : ``` Bonjour, Voici votre nouveau mot de passe pour les comptes : jean.dupont, marie.martin, paul.durand Mot de passe : XyZ123!@# ``` ### Endpoints API dédiés aux mots de passe #### Vérification de force (public) ```http POST /api/password/check Content-Type: application/json { "password": "monmotdepasse", "check_compromised": true } ``` **Réponse :** ```json { "status": "success", "valid": false, "errors": [ "Ce mot de passe a été trouvé 23 547 fois dans des fuites de données." ], "warnings": [ "Suggestion : Évitez les séquences communes pour plus de sécurité" ], "strength": { "score": 20, "strength": "Faible", "feedback": ["Ce mot de passe a été compromis"], "length": 13, "diversity": 1 }, "compromised": { "compromised": true, "occurrences": 23547, "message": "Ce mot de passe a été trouvé 23 547 fois dans des fuites de données" } } ``` #### Vérification de compromission uniquement (public) ```http POST /api/password/compromised Content-Type: application/json { "password": "monmotdepasse" } ``` #### Génération automatique (authentifié) ```http GET /api/password/generate?length=14 Authorization: Bearer {session_id} ``` **Réponse :** ```json { "status": "success", "password": "Xk9#mP2$nL5!qR", "length": 14, "strength": { "score": 85, "strength": "Très fort", "feedback": [] } } ``` ### Configuration et sécurité #### Paramètres de sécurité - **Timeout API HIBP** : 5 secondes - **Cache** : 15 minutes pour les vérifications répétées - **Logging** : Aucun mot de passe en clair dans les logs - **K-anonymity** : Seuls 5 caractères du hash SHA-1 envoyés #### Points d'intégration - `LoginController::register` : Validation lors de l'inscription - `LoginController::lostPassword` : Génération sécurisée - `UserController::createUser` : Validation si mot de passe manuel - `UserController::updateUser` : Validation lors du changement - `ApiService::generateSecurePassword` : Génération avec vérification HIBP ### Résumé ✅ **100% CONFORME NIST** pour les aspects techniques ✅ **Adapté aux besoins du client** (emails multiples, mdp=username) ✅ **Sécurité maximale** avec vérification HIBP ✅ **Expérience utilisateur optimale** (souple mais sécurisé) ## Endpoints API ### Routes Publiques vs Privées L'API distingue deux types de routes : #### Routes Publiques - POST /api/login - POST /api/register - GET /api/health #### Routes Privées (Nécessitent une session authentifiée) - Toutes les autres routes ### Authentification L'authentification utilise le système de session PHP natif. #### Login ```http POST /api/login Content-Type: application/json { "email": "user@example.com", "password": "SecurePassword123" } ``` **Réponse réussie :** ```json { "message": "Connecté avec succès", "user": { "id": 123, "email": "user@example.com" } } ``` **Notes importantes :** - Un cookie de session PHP sécurisé est automatiquement créé - Le cookie est httpOnly, secure et SameSite=Strict - L'ID de session est régénéré à chaque login réussi #### Logout ```http POST /api/logout ``` **Réponse réussie :** ```json { "message": "Déconnecté avec succès" } ``` ### Sécurité des Sessions La configuration des sessions inclut : - Sessions PHP natives sécurisées - Protection contre la fixation de session - Cookies httpOnly (protection XSS) - Mode strict pour les cookies - Validation côté serveur à chaque requête - use_strict_mode = 1 - cookie_httponly = 1 - cookie_secure = 1 - cookie_samesite = Strict - Régénération de l'ID de session après login - Destruction complète de la session au logout ### Users #### Création d'utilisateur La création d'utilisateur s'adapte aux paramètres de l'entité (amicale) : ```http POST /api/users Content-Type: application/json Authorization: Bearer {session_id} { "email": "john@example.com", "name": "John Doe", "first_name": "John", "role": 1, "fk_entite": 5, "username": "j.doe38", // Requis si chk_username_manuel=1 pour l'entité "password": "SecurePass123", // Requis si chk_mdp_manuel=1 pour l'entité "phone": "0476123456", "mobile": "0612345678", "sect_name": "Secteur A", "date_naissance": "1990-01-15", "date_embauche": "2020-03-01" } ``` **Comportement selon les paramètres de l'entité :** | chk_username_manuel | chk_mdp_manuel | Comportement | |---------------------|----------------|--------------| | 0 | 0 | Username et password générés automatiquement | | 0 | 1 | Username généré, password requis dans le payload | | 1 | 0 | Username requis dans le payload, password généré | | 1 | 1 | Username et password requis dans le payload | **Validation du username (si manuel) :** - Format : 10-30 caractères - Commence par une lettre - Caractères autorisés : a-z, 0-9, ., -, _ - Doit être unique dans toute la base **Réponse réussie :** ```json { "status": "success", "message": "Utilisateur créé avec succès", "id": 123, "username": "j.doe38", // Toujours retourné "password": "xY7#mK9@pL2" // Retourné seulement si généré automatiquement } ``` **Envoi d'emails :** - **Email 1** : Identifiant de connexion (toujours envoyé) - **Email 2** : Mot de passe (toujours envoyé, 1 seconde après le premier) **Codes de statut :** - 201: Création réussie - 400: Données invalides ou username/password manquant si requis - 401: Non authentifié - 403: Accès non autorisé (rôle insuffisant) - 409: Email ou username déjà utilisé - 500: Erreur serveur #### Vérification de disponibilité du username ```http POST /api/users/check-username Content-Type: application/json Authorization: Bearer {session_id} { "username": "j.doe38" } ``` **Réponse si disponible :** ```json { "status": "success", "available": true, "message": "Nom d'utilisateur disponible", "username": "j.doe38" } ``` **Réponse si déjà pris :** ```json { "status": "success", "available": false, "message": "Ce nom d'utilisateur est déjà utilisé", "suggestions": ["j.doe38_42", "j.doe381234", "j.doe3825"] } ``` #### Autres endpoints - GET /api/users - GET /api/users/{id} - PUT /api/users/{id} - DELETE /api/users/{id} - POST /api/users/{id}/reset-password ### Entités (Amicales) #### Upload du logo d'une entité ```http POST /api/entites/{id}/logo Content-Type: multipart/form-data Authorization: Bearer {session_id} Body: logo: File (image/png, image/jpeg, image/jpg) ``` **Restrictions :** - Réservé aux administrateurs d'amicale (fk_role == 2) - L'admin ne peut uploader que le logo de sa propre amicale - Un seul logo actif par entité (le nouveau remplace l'ancien) **Traitement de l'image :** - Formats acceptés : PNG, JPG, JPEG - Redimensionnement automatique : 250x250px maximum (ratio conservé) - Résolution : 72 DPI (standard web) - Préservation de la transparence pour les PNG **Stockage :** - Chemin : `/uploads/entites/{id}/logo/logo_{id}_{timestamp}.{ext}` - Enregistrement dans la table `medias` - Suppression automatique de l'ancien logo **Réponse réussie :** ```json { "status": "success", "message": "Logo uploadé avec succès", "media_id": 42, "file_name": "logo_5_1234567890.jpg", "file_path": "/entites/5/logo/logo_5_1234567890.jpg", "dimensions": { "width": 250, "height": 180 } } ``` #### Récupération du logo d'une entité ```http GET /api/entites/{id}/logo Authorization: Bearer {session_id} ``` **Réponse :** ```json { "status": "success", "logo": { "id": 42, "data_url": "data:image/png;base64,iVBORw0KGgoAAAANS...", "file_name": "logo_5_1234567890.png", "mime_type": "image/png", "width": 250, "height": 180, "size": 15234 } } ``` **Note :** Le logo est également inclus automatiquement dans la réponse du login si disponible. #### Mise à jour d'une entité ```http PUT /api/entites/{id} Content-Type: application/json Authorization: Bearer {session_id} { "name": "Amicale de Grenoble", "adresse1": "123 rue de la Caserne", "adresse2": "", "code_postal": "38000", "ville": "Grenoble", "phone": "0476123456", "mobile": "0612345678", "email": "contact@amicale38.fr", "chk_stripe": true, // Activation paiement Stripe "chk_mdp_manuel": false, // Génération auto des mots de passe "chk_username_manuel": false, // Génération auto des usernames "chk_copie_mail_recu": true, "chk_accept_sms": false } ``` **Paramètres de gestion des membres :** | Paramètre | Type | Description | |-----------|------|-------------| | chk_mdp_manuel | boolean | `true`: L'admin saisit les mots de passe
`false`: Génération automatique | | chk_username_manuel | boolean | `true`: L'admin saisit les identifiants
`false`: Génération automatique | | chk_stripe | boolean | Active/désactive les paiements Stripe | **Note :** Ces paramètres sont modifiables uniquement par les administrateurs (fk_role > 1). #### Réponse du login avec paramètres entité Lors du login, les paramètres de l'entité sont retournés dans le groupe `amicale` : ```json { "status": "success", "session_id": "abc123...", "session_expiry": "2025-01-09T15:30:00+00:00", "user": { "id": 9999980, "fk_entite": 5, "fk_role": 2, "fk_titre": null, "first_name": "Pierre", "sect_name": "", "date_naissance": "1990-01-15", // Maintenant correctement récupéré "date_embauche": "2020-03-01", // Maintenant correctement récupéré "username": "pv_admin", "name": "VALERY ADM", "phone": "0476123456", // Maintenant correctement récupéré "mobile": "0612345678", // Maintenant correctement récupéré "email": "contact@resalice.com" }, "amicale": { "id": 5, "name": "Amicale de Grenoble", "chk_mdp_manuel": 0, "chk_username_manuel": 0, "chk_stripe": 1, "logo": { // Logo de l'entité (si disponible) "id": 42, "data_url": "data:image/png;base64,iVBORw0KGgoAAAANS...", "file_name": "logo_5_1234567890.png", "mime_type": "image/png", "width": 250, "height": 180 } // ... autres champs } } ``` Ces paramètres permettent à l'application Flutter d'adapter dynamiquement le formulaire de création de membre. ## Intégration Frontend ### Configuration des Requêtes Toutes les requêtes API depuis le frontend doivent inclure : ```javascript fetch('/api/endpoint', { credentials: 'include', // Important pour les cookies de session // ... autres options }); ``` ### Gestion des Sessions - Les cookies de session sont automatiquement gérés par le navigateur - Pas besoin de stocker ou gérer des tokens manuellement - Redirection vers /login si session expirée (401) ## Maintenance et Déploiement ### Logs - Logs d'accès NGINX : /var/log/nginx/api-access.log - Logs d'erreur NGINX : /var/log/nginx/api-error.log - Logs PHP : /var/log/php/php-error.log ### Déploiement 1. Pull du repository 2. Vérification des permissions 3. Configuration de l'environnement 4. Tests des endpoints 5. Redémarrage des services ### Surveillance - Monitoring des processus PHP-FPM - Surveillance de la base de données - Monitoring des performances - Alertes sur erreurs critiques ## Changements récents ### Version 3.0.7 (Août 2025) #### 1. Implémentation complète de la norme NIST SP 800-63B pour les mots de passe - **Nouveau service :** `PasswordSecurityService` pour la gestion sécurisée des mots de passe - **Vérification HIBP :** Intégration de l'API Have I Been Pwned avec k-anonymity - **Validation souple :** Suppression des obligations de composition (majuscules, chiffres, spéciaux) - **Support Unicode :** Acceptation de tous les caractères, incluant émojis et espaces - **Nouveaux endpoints :** `/api/password/check`, `/api/password/compromised`, `/api/password/generate` #### 2. Autorisation des emails multiples - **Suppression de l'unicité :** Un même email peut être utilisé pour plusieurs comptes - **Adaptation de `lostPassword` :** Mise à jour de tous les comptes partageant l'email - **Un seul mot de passe :** Tous les comptes avec le même email reçoivent le même nouveau mot de passe #### 3. Autorisation mot de passe = identifiant - **Choix client :** Permet d'avoir un mot de passe identique au nom d'utilisateur - **Pas de vérification contextuelle :** Aucune vérification nom/email dans le mot de passe ### Version 3.0.6 (Août 2025) #### 1. Correction des rôles administrateurs - **Avant :** Les administrateurs d'amicale devaient avoir `fk_role > 2` - **Après :** Les administrateurs d'amicale ont `fk_role > 1` (donc rôle 2 et plus) - **Impact :** Les champs `chk_stripe`, `chk_mdp_manuel`, `chk_username_manuel` sont maintenant modifiables par les admins d'amicale (rôle 2) #### 2. Envoi systématique des deux emails lors de la création d'utilisateur - **Avant :** Le 2ème email (mot de passe) n'était envoyé que si le mot de passe était généré automatiquement - **Après :** Les deux emails sont toujours envoyés lors de la création d'un membre - Email 1 : Identifiant (username) - Email 2 : Mot de passe (1 seconde après) - **Raison :** Le nouveau membre a toujours besoin des deux informations pour se connecter #### 3. Ajout des champs manquants dans la réponse du login - **Champs ajoutés dans la requête SQL :** - `fk_titre` - `date_naissance` - `date_embauche` - `encrypted_phone` - `encrypted_mobile` - **Impact :** Ces données sont maintenant correctement retournées dans l'objet `user` lors du login #### 4. Système de gestion des logos d'entité - **Nouvelle fonctionnalité :** Upload et gestion des logos pour les amicales - **Routes ajoutées :** - `POST /api/entites/{id}/logo` : Upload d'un nouveau logo - `GET /api/entites/{id}/logo` : Récupération du logo - **Caractéristiques :** - Réservé aux administrateurs d'amicale (fk_role == 2) - Un seul logo actif par entité - Redimensionnement automatique (250x250px max) - Format base64 dans les réponses JSON (compatible Flutter) - Logo inclus automatiquement dans la réponse du login #### 5. Amélioration de l'intégration Flutter - **Format d'envoi des images :** Base64 data URL pour compatibilité multiplateforme - **Structure de réponse enrichie :** Le logo est inclus dans l'objet `amicale` lors du login - **Optimisation :** Pas de requête HTTP supplémentaire nécessaire pour afficher le logo ### Version 3.0.8 (Janvier 2025) #### 1. Système de génération automatique de reçus fiscaux pour les dons - **Nouveau service :** `ReceiptService` pour la génération automatique de reçus PDF - **Déclencheurs automatiques :** - Création d'un passage avec `fk_type=1` (don) et email valide - Mise à jour d'un passage en don si `nom_recu` est vide/null - **Caractéristiques techniques :** - PDF ultra-légers (< 5KB) générés en format natif sans librairie externe - Support des caractères accentués avec conversion automatique - Stockage structuré : `/uploads/entites/{entite_id}/recus/{operation_id}/` - Enregistrement dans la table `medias` avec catégorie `recu` - **Queue d'envoi email :** - Envoi automatique par email avec pièce jointe PDF - Format MIME multipart pour compatibilité maximale - Gestion dans la table `email_queue` avec statut de suivi - **Nouvelle route API :** - `GET /api/passages/{id}/receipt` : Récupération du PDF d'un reçu - Retourne le PDF en base64 ou téléchargement direct selon Accept header - **Champs base de données utilisés :** - `nom_recu` : Nom du fichier PDF généré - `date_creat_recu` : Date de génération du reçu - `date_sent_recu` : Date d'envoi par email - `chk_email_sent` : Indicateur d'envoi réussi