Chaque rencontre dispose d'un bouton « ✉ Email » dans l'administration
(admins et modérateurs) qui génère un email HTML prêt à envoyer, aux
couleurs du site avec le logo du club : titre, description, encadré
date/heure/lieu/places restantes et bouton d'inscription.
- Nouvelle route GET /api/admin/rencontres/:id/email : construit l'email
(mise en page en tables + styles inline, compatible clients mail) avec
des URL absolues dérivées de l'hôte de la requête.
- Le bouton de l'email pointe vers /?inscription=<id> : la page d'accueil
ouvre alors automatiquement la modale d'inscription de la rencontre.
- Modale d'aperçu dans l'admin : copie de l'email (HTML enrichi presse-
papiers pour coller directement dans un logiciel de messagerie), copie
de l'objet et du lien d'inscription, téléchargement du fichier .html.
- Les images publiques (logo, uploads) sont maintenant servies avec
Cross-Origin-Resource-Policy: cross-origin pour que le logo se charge
dans les webmails et l'aperçu.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fkg15RCxNgUys4ru73He2o
Photos :
- Nouvelle table rencontre_passee_photos (photos illimitées par rencontre
passée, avec position). Migration idempotente au démarrage des anciennes
colonnes image_path/_2/_3 vers cette table (puis colonnes nullées)
- Le nombre de photos affiché est désormais celui réellement uploadé
(calculé), le champ manuel nb_photos n'est plus utilisé
- Gestion admin : ajout illimité, choix de la photo « principale » (mise
en avant), suppression réservée aux admins (les modérateurs ajoutent et
réordonnent mais ne suppriment pas)
Participants auto :
- rencontres_passees.rencontre_id (FK, ON DELETE SET NULL) ; quand une
rencontre passée est liée à une rencontre, le nombre de participants est
calculé depuis ses inscriptions ; sinon valeur manuelle de repli. Le
formulaire admin propose la liaison et affiche le compte auto
Public :
- Nouvelle page /rencontres-passees (menu « Rencontres passées ») listant
toutes les rencontres passées
- Carrousel plein écran (composant Carousel) montrant TOUTES les photos
(flèches, clavier ←/→/Échap, compteur, miniatures) ; ouvert au clic sur
une carte. La grille d'accueil montre 3 aperçus avec badge « +N » et un
lien « Voir toutes les rencontres → »
- Composant PastEventCard partagé entre l'accueil et la page dédiée
Vérifié : 34 + 21 + 26 tests existants au vert, 19 nouveaux tests
(participants auto liés/déliés, upload illimité, nb_photos calculé,
principale, restrictions modérateur sur suppression de photo, migration
legacy des colonnes inline), parcours navigateur complet (galerie admin,
page publique, carrousel).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fkg15RCxNgUys4ru73He2o
Rencontres passées (section « Ils y étaient » de l'accueil) :
- Nouvel onglet « Rencontres passées » (admin + modérateur) : création,
édition, jusqu'à 3 photos par rencontre (1 grande + 2 petites, upload
après enregistrement — même schéma que la photo des rencontres à venir)
- Nouvelles colonnes rencontres_passees.image_path/_2/_3 (migration
ALTER TABLE IF NOT EXISTS)
- Home.jsx affiche désormais les vraies photos uploadées (repli sur le
motif placeholder si absentes) ; ordre passé de ORDER BY id à
ORDER BY id DESC (les plus récentes en premier, seed réordonné en
conséquence pour préserver l'affichage actuel)
- Suppression réservée aux administrateurs
Modérateurs : plus aucune action destructrice possible, imposé côté
serveur (adminOnly) et masqué côté interface :
- Validation/suspension d'un membre (toggle-valide)
- Suppression d'une rencontre, d'une rencontre passée, d'une inscription
Vérifié : 34 + 21 + 26 tests existants toujours au vert, 21 nouveaux
tests (CRUD rencontres passées, upload des 3 emplacements photo, ordre
d'affichage, restrictions modérateur sur chaque action désormais admin-
only), parcours navigateur complet (upload des 3 photos, rendu réel sur
la page d'accueil).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fkg15RCxNgUys4ru73He2o
- Nouveau rôle users.role='moderator' (contrainte CHECK migrée via
DROP+ADD CONSTRAINT, idempotent) : accès à Membres, Rencontres,
Inscriptions uniquement. Dashboard, Catégories (écriture), Contenu du
site, Demandes d'adhésion et gestion des comptes restent admin-only,
imposé côté serveur (requireAuth accepte désormais un tableau de rôles)
- Nouvel onglet « Administrateurs » (admin-only) : créer un compte admin
ou modérateur (mot de passe temporaire généré, même mécanique que pour
les membres — visible tant que non changé, changement forcé à la
première connexion), réinitialiser l'accès, promouvoir/rétrograder,
supprimer. Garde-fous : impossible de se supprimer ou de se rétrograder
soi-même, impossible de supprimer le dernier administrateur
- users.full_name (colonne migrée) pour l'affichage des comptes staff
- AccessCell/CredentialsModal extraits dans AccessControls.jsx, partagés
entre la gestion des membres et celle des comptes admin/modérateur
- AdminShell filtre ses onglets par rôle ; Espace.jsx route admin et
modérateur vers le back-office
Corrigé en cours de route : GET /api/admin/categories doit rester lisible
par les modérateurs (nécessaire au formulaire membre) même si sa gestion
en écriture reste admin-only — sans quoi le Promise.all du frontend
échouait silencieusement et vidait la liste des membres.
Vérifié : 34 + 21 tests existants toujours au vert, 26 nouveaux tests de
permissions par rôle, parcours navigateur complet (création modérateur,
connexion, changement forcé, menu restreint, accès aux membres).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fkg15RCxNgUys4ru73He2o
- Nouveau membre créé avec email : un mot de passe temporaire aléatoire
(~69 bits, sans caractères ambigus) est généré et haché ; il est retourné
à l'admin et reste lisible en clair (users.temp_password) tant que le
membre ne l'a pas changé
- Colonnes users.temp_password / must_change_password (migration
ALTER TABLE IF NOT EXISTS, appliquée au démarrage par le bootstrap)
- POST /api/admin/members/:id/reset-access : régénère l'accès (création
s'il n'existait pas, ou réinitialisation si le membre a perdu son mot
de passe) ; login/me exposent mustChangePassword ; change-password
efface systématiquement le mot de passe temporaire et lève le blocage
- Frontend : colonne « Accès » dans la liste des membres (mot de passe
visible + copier + réinitialiser, ou « Défini », ou « Créer l'accès »),
modal de confirmation après création/réinitialisation, écran de
changement de mot de passe obligatoire avant tout accès au portail ou
au back-office
Vérifié : cycle complet (création → mot de passe visible côté admin →
connexion → changement forcé → effacement automatique → réinitialisation
admin en cas de perte) via 21 tests e2e dédiés + parcours navigateur ;
34 tests existants toujours au vert.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fkg15RCxNgUys4ru73He2o
Résout définitivement « Role sbc_app does not exist » : l'initialisation
ne dépend plus de docker-entrypoint-initdb.d ni de bind mounts du dépôt
(fragiles selon le mode de déploiement Portainer).
- Nouveau bootstrap idempotent exécuté par l'app à chaque démarrage avec
le superuser (PG_SUPERUSER_PASSWORD) : création de la base et du rôle
sbc_app si absents, réalignement du mot de passe, schéma en
CREATE ... IF NOT EXISTS, données de démo si la base est vide, grants
- Schéma et seed embarqués dans l'image (server/src/sql), seed par
recherche de noms (indépendant des séquences)
- Compose réduit à deux services (db + app), suppression de db-sync et
du montage ./db/init ; répertoire db/ retiré
Vérifié : volume totalement vierge auto-initialisé, redémarrage sans
re-seed, changement d'APP_DB_PASSWORD auto-réparé, 34 tests e2e verts.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fkg15RCxNgUys4ru73He2o
Affiche host/port/db/user à la connexion et un message explicite en cas
d'échec d'authentification persistant, pour identifier immédiatement une
stack déployée avec une ancienne version du compose.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fkg15RCxNgUys4ru73He2o
Sur le réseau partagé nginx_default, le nom « db » peut résoudre vers le
PostgreSQL d'une autre stack : l'application se connectait au mauvais
serveur (password authentication failed pour sbc_app alors que db-sync,
lui, visait le bon). Reproduit et vérifié avec un conteneur leurre.
- La base obtient l'alias réseau unique « sbc-db » ; l'app et db-sync
s'y connectent via ce nom, jamais via « db »
- DATABASE_URL remplacé par PGHOST/PGPORT/PGDATABASE/PGUSER/PGPASSWORD :
insensible aux caractères spéciaux du mot de passe et aux ambiguïtés
de résolution DNS
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fkg15RCxNgUys4ru73He2o
Plus aucune référence au port 3000 : l'application écoute sur 8321 dans
le conteneur (PORT, EXPOSE, healthcheck, mapping 8321:8321, upstream
nginx sbc-app:8321, proxy de dev Vite, README).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fkg15RCxNgUys4ru73He2o
- docker-compose : valeurs par défaut pour POSTGRES_PASSWORD,
APP_DB_PASSWORD et les mots de passe initiaux (admin SlucAdmin2026!,
membres SlucMembre2026!) — surchargeables par variables d'environnement
- JWT_SECRET devient optionnel : l'application génère un secret aléatoire
au démarrage s'il est absent (les sessions expirent alors au redémarrage
du conteneur), aucun secret de signature n'est committé
- .env.example et README mis à jour (démarrage clé en main, consignes de
surcharge pour la production)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fkg15RCxNgUys4ru73He2o
- Un seul conteneur applicatif : Express sert l'API, le frontend React
compilé (fallback SPA, cache immutable sur /assets) et les images
/uploads (nosniff + CSP default-src 'none')
- En-têtes de sécurité (CSP stricte, X-Frame-Options DENY, etc.) portés
de nginx vers helmet, compression gzip ajoutée
- Application exposée sur le port 8321 (APP_PORT), PostgreSQL sur
127.0.0.1:56432 (DB_PORT, loopback uniquement pour l'admin locale)
- TRUST_PROXY pilote la confiance aux en-têtes X-Forwarded-* (désactivé
par défaut hors reverse-proxy)
- Dockerfile multi-étages unique à la racine (build React → deps → image
finale non-root, fs en lecture seule)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fkg15RCxNgUys4ru73He2o