diff --git a/README.md b/README.md index 1c512a2..6fff8c0 100644 --- a/README.md +++ b/README.md @@ -94,14 +94,31 @@ Ce qui **n'est pas** fait, pour que personne ne s'y fie : à un modèle est absente de la production. - **Les secrets de l'historique git ne sont pas révoqués** — jeton du bot, mot de passe PostgreSQL, `SECRET_KEY`. -- **`trusted_proxy='*'`** dans `wsgi.py` : l'en-tête `X-Forwarded-For` est +- **`trusted_proxy='*'`** reste le défaut : l'en-tête `X-Forwarded-For` est accepté de n'importe quelle source, donc la limitation par IP est - contournable. À régler avec la topologie réelle du déploiement. + contournable. C'est désormais la variable `TRUSTED_PROXY` plutôt qu'une + constante — `docs/deployment.md` donne la valeur pour chaque topologie. +- **Les comptes créés par le formulaire d'inscription sont actifs + immédiatement** : il n'y a pas d'étape de validation par le staff. Décision + de produit en attente, voir `docs/roles-and-permissions.md`. - **L'identité Discord** transite encore par un champ caché du formulaire d'inscription : elle n'est pas prouvée par le passage OAuth2. `docs/security-checklist.md` détaille la liste avant mise en production. +### Documentation + +| Document | Pour | +|---|---| +| `docs/deployment.md` | Installer, déployer, revenir en arrière | +| `docs/roles-and-permissions.md` | Qui peut faire quoi, et où c'est décidé | +| `docs/database-restore.md` | Sauvegarder et restaurer | +| `docs/database-schema.md` | Sortir de `create_all()` : relevé, Alembic, migrations | +| `docs/incident-runbook.md` | Quand quelque chose ne va pas | +| `docs/architecture.md` | Diagrammes | +| `docs/translations.md` | Ajouter ou corriger une traduction | +| `docs/security-checklist.md` | Avant une mise en production | + --- ## Intégration Discord diff --git a/docs/incident-runbook.md b/docs/incident-runbook.md new file mode 100644 index 0000000..b692dee --- /dev/null +++ b/docs/incident-runbook.md @@ -0,0 +1,204 @@ +# Manuel d'incident + +À lire quand quelque chose ne va pas. Chaque section : le symptôme, où +regarder, quoi faire. + +> **Le réflexe préalable** — demander la **référence** affichée sur la page +> d'erreur (`Référence à indiquer si vous signalez ce problème`). C'est +> l'identifiant de requête ; il apparaît dans chaque ligne de journal produite +> par cette requête, entre crochets. +> +> ```bash +> grep '\[a1b2c3d4e5f60718\]' logs/*.log +> ``` + +## Où sont les journaux + +| Fichier | Contenu | +|---|---| +| `logs/app.log` | Tout, à partir de `LOG_LEVEL` (défaut `INFO`) | +| `logs/errors.log` | `ERROR` et au-dessus, avec les traces | +| `logs/auth.log` | Connexions, échecs, verrous, changements de rôle, suppressions de compte, refus d'inscription | +| Console Pterodactyl | Les mêmes lignes que `app.log` — le handler console est actif en production, délibérément | +| `C:\nginx\logs\error.log` | Ce qui n'a jamais atteint l'application | + +Format : `[date] NIVEAU [module:ligne] [id-de-requête] message`. Un `-` à la +place de l'identifiant signifie « hors requête » : démarrage, bot Discord, +planificateur. + +### Les événements de `auth.log` + +Tout est en `clé=valeur`, dans l'ordre, donc greppable sans dépendance JSON. + +| Événement | Quand | +|---|---| +| `login.success` | Connexion réussie | +| `login.failure` | Mot de passe faux sur un compte existant | +| `login.failure.unknown_user` | Identifiant inconnu | +| `login.rejected.deactivated` | Bons identifiants, compte désactivé | +| `account.throttled` | Délai d'attente déclenché après des échecs répétés | +| `logout` | Déconnexion | +| `account.registered` | Inscription réussie | +| `account.registration_refused` | Inscription filtrée — `reason=honeypot`, `too-fast` ou `no-form-issued` | +| `account.created_by_admin` | Compte créé depuis l'administration | +| `account.updated` | Fiche modifiée | +| `account.role_changed` | Changement de rôle, avec l'ancien et le nouveau | +| `account.password_changed` | Changement par la personne elle-même | +| `account.password_reset_by_admin` | Réinitialisation par un président | +| `account.deleted` | Suppression, avec le nombre de fichiers de contrat retirés | + +Le champ `ip=` vient de `request.remote_addr`, donc de `X-Forwarded-For`. +Tant que `TRUSTED_PROXY=*`, **c'est une indication et pas une preuve**. + +## Le site ne répond pas + +1. **`/health` répond-il ?** + ```bash + curl -i https:///health + ``` + - Pas de réponse du tout → nginx est tombé, ou l'application n'écoute plus. + Vérifier la console Pterodactyl. + - `503` → l'application tourne mais la base ne répond pas. Voir plus bas. + - `200` mais le site est inutilisable → le problème est dans nginx ou dans + le DNS, pas dans l'application. + +2. **L'application est-elle démarrée ?** Console Pterodactyl. Au démarrage elle + imprime `Starting Waitress server on :`. + +3. **Un déploiement vient-il d'avoir lieu ?** C'est la cause la plus fréquente. + Voir « Revenir en arrière » dans `docs/deployment.md`. + +## `/health` renvoie 503 + +La base ne répond pas. Le corps de la réponse le dit : + +```json +{"status": "unhealthy", "database": "error"} +``` + +Le corps ne contient **jamais** l'erreur du pilote : elle porte l'hôte, le nom +de la base et l'utilisateur de la chaîne de connexion, et `/health` n'est pas +authentifié. La trace complète est dans `logs/errors.log`. + +- La base est hébergée sur Render : vérifier son état côté Render en premier. +- `DATABASE_URL` a-t-elle changé ? Un mot de passe tourné et pas reporté + produit exactement ça. +- **Piège connu** : `DATABASE_URL=postgresql://…` seul ne démarre pas — + SQLAlchemy y cherche psycopg **2**, le projet épingle psycopg **3**. + `normalise_database_url()` nomme le pilote, donc les deux formes marchent ; + si vous voyez `ModuleNotFoundError: psycopg2`, c'est que le code qui + normalise n'a pas été déployé. + +## Plus aucune notification Discord + +Le bot tourne dans un **fil du même processus** que le site. Quand il meurt, +les pages continuent d'être servies et toutes les notifications s'arrêtent. +C'est précisément ce qui est resté invisible longtemps. + +1. `/health` le rapporte : + ```json + {"discord_bot": {"running": false, ...}} + ``` + Rapporté et **non fatal** : un club sans rappels Discord est dégradé, pas + hors service, et un 503 le sortirait du répartiteur de charge pour ça. + +2. Chercher les livraisons manquées : + ```bash + grep 'not delivered' logs/app.log + ``` + Trois causes, qui ne se traitent pas pareil : + - `their direct messages are closed` → **définitif**. La personne doit + autoriser les messages privés des membres du serveur. Réessayer ne sert à + rien. + - `does not exist` → l'identifiant Discord sur le compte est faux ou le + compte a été supprimé. À corriger dans la fiche de la personne. + - autre chose → passager, côté Discord. + +3. Le bilan du lot quotidien : + ```bash + grep 'Daily reminders' logs/app.log + ``` + `17 of 20 delivered, 3 failed` — les trois sont nommées juste au-dessus. + +4. Redémarrer le processus relance le bot. Les réactions en attente survivent + au redémarrage (`discord_pending.json`), sauf si ce fichier a été mis de + côté pour corruption — auquel cas le journal le dit et donne le nom du + fichier de quarantaine. + +## Quelqu'un est verrouillé dehors + +Cinq échecs consécutifs déclenchent un délai qui double ensuite. Le compte +n'est pas bloqué définitivement. + +```bash +grep -E 'login\.failure|account\.throttled' logs/auth.log | grep '' +``` + +- **Le verrou se lève tout seul.** Le délai est dans le message. +- Un président peut réinitialiser le mot de passe depuis la fiche du compte, + ce qui remet le compteur à zéro. +- **Beaucoup de verrous sur des comptes différents en même temps** → quelqu'un + essaie des mots de passe. Le champ `ip=` est une indication et **pas une + preuve** tant que `TRUSTED_PROXY=*` (voir `OPS-002` dans + `docs/deployment.md`) : il est falsifiable à chaque requête. + +## Une page renvoie 500 + +1. Récupérer la référence auprès de la personne, ou la dernière trace : + ```bash + tail -50 logs/errors.log + ``` +2. La trace complète y est. Elle n'est jamais montrée à l'utilisateur. +3. Si c'est arrivé juste après un déploiement, revenir en arrière d'abord et + diagnostiquer ensuite. + +## Le style ou le JavaScript ne se chargent plus + +Les statiques sont servis par nginx avec un cache de 30 jours, ce qui n'est +sûr que parce que leurs URL portent une estampille (`?v=`). + +- **Après un déploiement, la page a l'ancien style** → l'estampille n'a pas + changé, donc le fichier n'a pas été déployé. Vérifier : + ```bash + curl -s https:///auth/login | grep -o 'style.css?v=[0-9]*' + ``` +- **404 sur `/static/…`** → le chemin `alias` du bloc `location /static/` de + `nginx.conf` ne pointe pas au bon endroit. +- **Une page est cassée et la console du navigateur parle de CSP** → un bloc + `