Email d'invitation HTML généré depuis l'administration des rencontres

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
This commit is contained in:
Claude committed 2026-07-14 07:11:34 +00:00
1 parent 9cfd4e4e93
commit 02cfa8f8bc
5 files changed
+235 -4

No files matched your search

+17
View File
@@ -18,6 +18,7 @@ import { imageUpload, saveImage, deleteImage } from '../uploads.js';
import { ensureMemberAccess } from '../memberAccess.js';
import { createStaffUser, resetStaffAccess } from '../userAccess.js';
import { AccessError } from '../errors.js';
import { buildInvitationEmail } from '../emailTemplate.js';
export const adminRouter = Router();
@@ -227,6 +228,22 @@ adminRouter.post('/rencontres/:id/image', validate(idParam, 'params'), (req, res
});
});
// Ready-to-send HTML invitation email for a rencontre (admin + moderator).
// The registration link opens the public site with the inscription modal
// pre-opened (/?inscription=<id>). URLs are absolute, built from the
// request's host so they match however the site is reached (direct port
// or reverse proxy).
adminRouter.get('/rencontres/:id/email', validate(idParam, 'params'), async (req, res, next) => {
try {
const result = await query(`${RENC_SQL} WHERE r.id = $1 GROUP BY r.id`, [req.params.id]);
if (result.rowCount === 0) return res.status(404).json({ error: 'Rencontre introuvable' });
const baseUrl = `${req.protocol}://${req.get('host')}`;
res.json(buildInvitationEmail({ rencontre: result.rows[0], baseUrl }));
} catch (err) {
next(err);
}
});
adminRouter.get('/rencontres/:id/inscriptions', validate(idParam, 'params'), async (req, res, next) => {
try {
const result = await query(`${INSCR_SQL} WHERE i.rencontre_id = $1 ORDER BY i.nom`, [req.params.id]);