# Audit de la production — résultats (06/08/2026)

Réalisé en lecture seule via accès SSH par clé, sur `57.129.1.122`.
**Aucune écriture n'a été faite.**

---

## Ce qu'il faut retenir en trois lignes

1. **`APP_DEBUG=true` en production** : une simple erreur applicative expose les secrets. À corriger aujourd'hui.
2. **Le déploiement ne passe pas par git**, et 23 migrations ont été appliquées à la main : lancer `php artisan migrate` sur la production **casserait la base**. Bloquant pour toute mise en production, y compris nos deux PR.
3. **MySQL 8.0.46** : bonne nouvelle, les requêtes récursives sont disponibles pour « Mes équipes » (EA-048).

---

## 1. Sécurité — à traiter en priorité

| Constat | Gravité | Détail |
|---|---|---|
| `APP_ENV=local` et `APP_DEBUG=true` | **Critique** | En mode debug, une erreur non gérée affiche la trace complète : chemins, requêtes SQL, **et le contenu des variables d'environnement** — donc les clés Stripe, Pusher, SMTP, AWS et le mot de passe de la base. N'importe quel visiteur déclenchant une erreur peut les lire. |
| `DB_USERNAME=root` | Élevée | L'application se connecte à MySQL en super-utilisateur. Une injection SQL ou une compromission applicative donne le contrôle de **toutes** les bases du serveur, dont celles de Formind et d'OPDO. |
| Compte `ubuntu` partagé, membre du groupe `sudo` | Élevée | Un seul jeu d'identifiants donne les pleins pouvoirs sur les trois projets hébergés. Mot de passe transmis en clair le 06/08 → à renouveler. |
| 426 jours sans redémarrage | Moyenne | Les correctifs de sécurité du noyau ne sont pas appliqués tant que la machine n'a pas redémarré. |

**Correctif immédiat pour le point 1** (deux lignes dans le `.env` de production) :

```
APP_ENV=production
APP_DEBUG=false
```

Puis `php artisan config:clear`. Effet immédiat, aucun risque fonctionnel : cela ne change que
l'affichage des erreurs.

## 2. Le déploiement — le vrai obstacle à la mise en production

**Le code n'est pas déployé par git.** Le dossier de production contient bien un dépôt git, mais
il pointe vers `https://github.com/Rigictech/e-learning-api.git` et son dernier commit date du
**1ᵉʳ août 2025**. Or **226 fichiers diffèrent** de ce commit, et le fichier applicatif le plus
récent date du **1ᵉʳ juillet 2026**. Les fichiers sont donc déposés directement sur le serveur
(vraisemblablement par SFTP), et les métadonnées git sont un vestige trompeur.

Conséquences :

- **Aucune traçabilité** : impossible de savoir quelle version tourne réellement.
- **Aucun retour arrière** possible en cas de problème.
### Le dépôt git est-il à jour ? Oui — c'est la production qui est en retard

Vérification faite le 06/08 par comparaison des fichiers, et le résultat est rassurant :

| Comparaison | Résultat |
|---|---|
| Migrations : prod vs `development` | 62 en prod, **toutes présentes** dans git ; git en a **8 de plus**, non déployées |
| Fichiers `app/` : prod vs `development` | 147 en prod, **aucun absent** de git ; 109 identiques, 38 plus récents dans git ; 2 fichiers nouveaux dans git |
| Versions de production retrouvées dans l'historique git | Oui — testé sur `SanitizeInput.php` (commit du 18/03), `QuizController.php` (08/05), `DashBoardController.php` (25/03) |

**Conclusion : aucun code n'existe uniquement sur le serveur.** La production correspond à un
état **plus ancien** de la même lignée que `development` — figée aux alentours de mai-juillet 2026.
Le dépôt `AAZTEKDEV` est donc bien la source de vérité du code, et l'environnement local monté
depuis `development` est **plus récent** que la production, pas divergent.

Le dépôt `Rigictech/e-learning-api` référencé par le `.git` du serveur est vraisemblablement le
dépôt d'origine de l'équipe externe, dont `AAZTEKDEV` est une copie. Point à confirmer auprès
d'eux, mais sans conséquence sur la suite : ce qui tourne en production existe intégralement
dans `AAZTEKDEV`.

**Ce qui reste vrai malgré tout** : le déploiement n'étant pas fait par git, rien ne garantit
qu'il en aille toujours ainsi. Un correctif appliqué directement sur le serveur un jour d'urgence
disparaîtrait au déploiement suivant, sans trace. C'est le processus qu'il faut corriger, pas
l'état actuel du code.

### Le point bloquant : 23 migrations appliquées hors du système

La table `migrations` enregistre **39** entrées, alors que **62** fichiers de migration existent
sur le disque. Les 23 manquantes correspondent pourtant à des objets **bien présents en base** —
vérifié un par un :

- 15 tables : `settings`, `notifications`, `point_master`, `user_follows`, `badges`,
  `user_badges`, `user_points`, `learning_paths`, `learning_path_courses`, `message_likes`,
  `message_follows`, `course_files`, `submanager_profiles`, `manager_general_profiles`,
  `director_profiles` ;
- 5 colonnes : `users.parent_id`, `quizzes.passing_percentage`, `sections.order_no`,
  `categories.order_no`, `courses.resource_file`.

Autrement dit, le schéma a été appliqué **manuellement en SQL**, sans passer par Laravel.

> ⚠️ **Conséquence opérationnelle majeure** : si quelqu'un lance `php artisan migrate` sur la
> production, Laravel tentera de rejouer ces 23 migrations et échouera sur des tables déjà
> existantes — en laissant potentiellement la base dans un état intermédiaire. C'est exactement
> le scénario rencontré en montant l'environnement local.

**Remède** : « recaler » l'état des migrations en insérant les 23 noms dans la table `migrations`
**sans les exécuter**, après vérification (déjà faite) que chaque objet existe. C'est une
opération d'écriture sur la production : à faire sous supervision, snapshot préalable, et
**avant** tout déploiement de nos PR — celles-ci ajoutent trois nouvelles migrations.

## 3. Configuration d'exécution

| Élément | Valeur | Commentaire |
|---|---|---|
| MySQL | **8.0.46** | ✅ Requêtes récursives disponibles → « Mes équipes » (EA-048) peut s'écrire naturellement |
| PHP | 8.2.30 | Cohérent avec l'environnement local |
| Laravel | ^12.0 | — |
| `QUEUE_CONNECTION` | **`sync`** | ⚠️ Voir ci-dessous |
| Worker de file d'attente | **aucun** | Cohérent avec `sync` |
| Tâches planifiées (`cron`) | **aucune** | Le planificateur Laravel ne tourne pas |
| `BROADCAST_CONNECTION` | `pusher` | Le temps réel du chat est actif en production |
| `SESSION_DRIVER` / `CACHE_STORE` | `database` | — |

### Correction à apporter à notre propre travail (EA-083)

Dans la PR #95, j'ai remplacé l'envoi direct des emails de quiz autonome par une mise en file
(`Mail::queue()`), en indiquant que cela réglait le risque d'expiration de la requête.

**C'est inexact en l'état** : avec `QUEUE_CONNECTION=sync`, Laravel exécute la file
immédiatement, dans le cycle de la requête. Le comportement reste donc synchrone et le risque
d'expiration demeure.

Le correctif reste juste et utile, mais il n'a d'effet **qu'accompagné** de deux changements
d'exploitation : passer `QUEUE_CONNECTION` à `database` et faire tourner un worker en service
permanent (`systemd`). À intégrer au lot L1, qui prévoit déjà des écritures asynchrones pour le
tracking — cette infrastructure de file d'attente lui est de toute façon nécessaire.

## 4. Données réelles

| Table | Volume |
|---|---|
| Utilisateurs | 301 |
| Inscriptions | 2 128 |
| Leçons validées | 330 |
| Réponses de quiz | 1 184 |
| Complétions | 134 |
| Cours | 54 |

**Répartition des rôles** — cela répond à la question ouverte de la fiche F7 :

| Rôle | Utilisateurs |
|---|---|
| `learner` | 262 |
| `sub_manager` | **16** |
| `director` | 7 |
| `manager_general` | 7 |
| `manager` | 6 |
| `teacher` | 3 |
| `admin` | 2 |

`sub_manager` est bien le rôle managérial le plus utilisé : c'est le « Manager » du document
Features UX. Les rôles `director` et `manager_general` sont réellement employés (7 comptes
chacun) alors qu'ils ne sont pas créés par le seeder — ils ont été insérés à la main, comme le
reste du schéma.

**Bonne nouvelle pour la PR #95** : **aucun doublon** dans `completed_courses`. La migration de
déduplication ne supprimera donc rien.

### Colonnes hors migrations, confirmées en base

`users.is_active`, `lessons.order_no`, `quizzes.order_no`, `sections.start_date`,
`sections.frequency`, `sections.start_after_enroll` existent bien.
En revanche **`enrollments.is_complete` n'existe pas** — ce qui confirme que l'écriture faite par
`EnrollmentController` sur cette colonne est silencieusement perdue.

## 5. Le serveur est mutualisé

`/var/www` héberge **trois projets** : Efektiv Académie, **Formind** (ton propre SaaS) et OPDO,
chacun avec production, staging et dev. Un seul compte `ubuntu` avec `sudo` gouverne l'ensemble.
Point à intégrer au plan de reprise : reprendre ce serveur, c'est aussi hériter d'OPDO.

---

---

## Interventions réalisées le 06/08/2026

| Action | État | Détail |
|---|---|---|
| `APP_ENV=production`, `APP_DEBUG=false` | ✅ Fait | `.env` sauvegardé au préalable (`.env.backup-avant-debug-20260806-102004`). API vérifiée : HTTP 200, les erreurs n'exposent plus de trace |
| Export complet de la base | ✅ Fait | `~/backups/e-learning-prod-complet-20260806-105442.sql` (1,1 Mo, dump intègre) |
| Export ciblé de `migrations` | ✅ Fait | `~/backups/migrations-avant-recalage-20260806-105442.sql` (rollback précis) |
| Recalage des 23 migrations | ✅ Fait | Insérées en **batch 5**, sans exécution. Table passée de 39 à **62** lignes |

**Vérifications après recalage :**

- `php artisan migrate:status` → **0 migration en attente**
- `php artisan migrate --pretend --force` → **« Nothing to migrate »**
- Données métier inchangées : 301 utilisateurs, 2 128 inscriptions, 134 complétions, 54 cours
- API en production : HTTP 200

Les 24 objets de schéma correspondant aux 23 migrations avaient été vérifiés un par un
au préalable — tous présents. (Une alerte sur `messages.parent_id` s'est révélée être une
erreur de ma part : la colonne s'appelle `parent_message_id` et existe bien.)

**Rollback si nécessaire** : `DELETE FROM migrations WHERE batch = 5;` — ou restauration du
fichier `migrations-avant-recalage-*.sql`.

**Conséquence** : `php artisan migrate` est désormais sûr sur la production. Le déploiement des
PR #11 et #95, qui ajoutent trois migrations, n'est plus bloqué de ce côté.

⚠️ Les sauvegardes sont sur le serveur lui-même : suffisant comme filet pour cette opération,
insuffisant comme sauvegarde au sens propre. Une copie hors-serveur reste à organiser.

---

## Actions par ordre d'urgence

| # | Action | Qui | Quand |
|---|---|---|---|
| 1 | `APP_DEBUG=false` et `APP_ENV=production` | Nous, sur accord | Aujourd'hui |
| 2 | Renouveler le mot de passe du compte `ubuntu` | Enguerran | Aujourd'hui |
| 3 | Snapshot OVH avant toute écriture | Enguerran | Avant l'action 4 |
| 4 | Recaler la table `migrations` (23 entrées) | Nous, sur accord | Avant tout déploiement |
| 5 | Créer un utilisateur MySQL dédié (fin du `root`) | Nous | Lot L1 |
| 6 | File d'attente : `database` + worker `systemd` | Nous | Lot L1 |
| 7 | Établir une procédure de déploiement traçable | À définir | Avant L5 |
| 8 | Clarifier le lien entre `Rigictech/e-learning-api` et `AAZTEKDEV` | Enguerran | Cette semaine |
