Chaque gestionnaire de discord_bot.py enveloppait tout son corps dans un except Exception qui journalisait et poursuivait. Un refus de la base, une boite de reception fermee et une faute de frappe dans ce module produisaient la meme ligne, et la personne qui avait clique n'apprenait rien dans aucun des trois cas. Le defaut que l'audit citait en exemple : dans handle_attendance_confirm, le commit et le message "Your attendance has been confirmed!" etaient dans le meme bloc protege. Si le commit levait, rien n'etait envoye et rien n'etait signale. La reaction devenait indiscernable d'un bot arrete. Trois familles, trois reponses. La base refuse : rollback, l'entree pending est conservee pour que la reaction reste reessayable, et la personne est prevenue que rien n'a ete enregistre. Discord est injoignable : apres un commit c'est du meilleur effort, un DM qui rebondit ne defait pas une decision prise. Tout le reste est un defaut et remonte, jusqu'a on_error, qui est ajoute parce que discord.py journalise sur le logger 'discord' que configure_logging ne collecte pas. Trouve en appliquant : deux reactions sur le meme message passent toutes deux le test d'appartenance puis s'attendent sur deux await, et la perdante levait un KeyError qui se lisait comme une erreur sans consequence ; un fetch_channel en echec renvoyait sans un mot, donc un clic sans effet et sans trace ; un start_scheduler en echec supprime tous les rappels a jamais et laissait trois cles de /health au vert, d'ou reminders_scheduled. L'ordre des etapes apres le commit est desormais fixe : oublier l'entree pending avant les messages, sinon un DM rebondi laisse une demande deja approuvee reactivable une seconde fois. Les tests ont ete verifies par mutation du code de production. La deuxieme mutation a trouve une faiblesse dans le test lui-meme, qui ne regardait que le premier message emis. QUA-004 (roadmap) / ARCH-008 (constats). Co-Authored-By: Claude Opus 5 <[email protected]>
Plateforme centralisée de tryouts
Application interne du club e-sport de l'UdeS : inscriptions aux sélections, évaluations, gestion des équipes, disponibilités, contrats, et notifications Discord.
Le site est servi en français, l'anglais reste accessible par le sélecteur
de la barre latérale (voir docs/translations.md).
Démarrer
python -m venv .venv
.venv/Scripts/pip install -r requirements.txt -r requirements-dev.txt
cp app/.env.example .env # puis remplir SECRET_KEY et DATABASE_URL
.venv/Scripts/python run.py # développement, http://127.0.0.1:5000
SECRET_KEY et DATABASE_URL sont obligatoires : create_app() refuse
de démarrer sans eux. DATABASE_URL doit pointer sur PostgreSQL ; le pilote
psycopg 3 est nommé automatiquement si l'URL n'en nomme pas.
Production : python wsgi.py (Waitress derrière nginx). Voir
docs/deployment.md.
Ce sont les deux seuls points d'entrée.
Vérifier
.venv/Scripts/python -m pytest # suite complète
.venv/Scripts/python -m ruff check . # lint
.venv/Scripts/python -m ruff format --check .
Les trois tournent en CI et y sont bloquants.
Ce que fait l'application
- Comptes et rôles — cinq rôles : président (
admin), gérant (manager), coach, joueur (player), recruteur (scout). Le président attribue les rôles. - Sélections — organisation des tryouts, trois formats de match (équipe contre équipe, joueur contre joueur, scrim), évaluation des joueurs sur dix critères.
- Équipes — effectifs de la saison, matchs et entraînements. Le formulaire d'entraînement affiche les disponibilités des joueurs.
- Disponibilités — créneaux hebdomadaires des joueurs, créneaux réservables des coachs.
- Notes — un coach écrit des notes d'équipe (visibles par l'équipe) et des notes nominatives (visibles par le joueur concerné).
- Un-à-un — un joueur demande une séance à son coach ; le coach répond depuis le site ou par une réaction sur le message privé Discord.
- Contrats — dépôt d'un contrat par le staff, signature par le joueur.
Comment c'est construit
Backend Python 3.12 / Flask, rendu serveur en Jinja2, CSS et JavaScript
maison, sans framework front. Base PostgreSQL via SQLAlchemy. Bot Discord
(discord.py) dans un fil du même processus que le serveur web.
docs/architecture.md contient les diagrammes (classes, paquets, flux
d'une requête).
Sécurité
En place et vérifié par des tests :
- Limitation de débit sur la connexion (10 requêtes/minute par IP).
- Cookies de session
HttpOnly,SameSite=Lax,Secure, avec expiration effective. - CSRF sur tous les formulaires, y compris la déconnexion (en POST).
- HTTPS forcé en production, HSTS.
- CSP sans
unsafe-inlinesurscript-src: aucun gestionnaire d'événement en ligne, chaque bloc<script>porte un nonce par requête. - Redirections validées (rien ne sort du site).
- Validation par schémas marshmallow sur les formulaires de compte, avec politique de mot de passe.
- Autorisation centralisée dans
app/permissions.py. - Journal d'authentification (
logs/auth.log) : connexions, échecs, changements de rôle, suppressions de compte. - Téléversements vérifiés par extension et par signature de fichier.
Ce qui n'est pas fait, pour que personne ne s'y fie :
- Aucune migration de schéma.
db.create_all()crée les tables manquantes et ne modifie jamais une table existante : une colonne ajoutée à 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='*'reste le défaut : l'en-têteX-Forwarded-Forest accepté de n'importe quelle source, donc la limitation par IP est contournable. C'est désormais la variableTRUSTED_PROXYplutôt qu'une constante —docs/deployment.mddonne 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
Messages privés au coach lors d'une demande d'un-à-un, aux joueurs à la création d'un match ou d'un entraînement les concernant, et rappel 24 h avant un match. Les réponses se font par réaction sur le message ou depuis le site.
L'état des messages en attente de réponse est dans discord_pending.json,
non versionné : c'est de l'état d'exécution, propre à chaque serveur.
Mise en place
Le bot du club existe déjà ; ce qui suit ne concerne qu'une nouvelle installation.
- Créer une application sur le portail développeur Discord, puis un bot.
- Copier le jeton dans
DISCORD_BOT_TOKEN. - Activer Message Content Intent dans les Privileged Gateway Intents. C'est le seul intent privilégié demandé : il sert à lire le motif d'un refus écrit en réponse au message.
- Chaque personne doit partager un serveur avec le bot (ou l'avoir en ami) pour recevoir un message privé, et renseigner son identifiant Discord dans son profil (Discord → Paramètres → Avancés → Mode développeur, puis clic droit sur son profil → Copier l'identifiant).
/health indique si le bot tourne et s'il est connecté.