Email d'invitation complet avec textes personnalisables

L'email généré était réduit au bloc d'invitation. Il devient une vraie
communication du Club : salutation, message d'introduction, carte de la
rencontre (titre, description, date/heure/lieu/places), bouton
d'inscription, message de conclusion, signature et pied de page contact.

La modale d'administration devient un composeur : les quatre zones de
texte (salutation, introduction, conclusion, signature) sont pré-remplies
avec une rédaction par défaut et modifiables, avec régénération de
l'aperçu pendant la saisie ; vider un champ retire le bloc de l'email.
La route passe en POST avec validation zod des textes (longueurs max).

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:55:08 +00:00
1 parent d4923e9dfc
commit 03ec1b7a30
4 files changed
+182 -64

No files matched your search

+14 -10
View File
@@ -12,6 +12,7 @@ import {
roleChangeSchema,
rencontrePasseeSchema,
pastPhotoParam,
emailComposeSchema,
idParam,
} from '../schemas.js';
import { imageUpload, saveImage, deleteImage } from '../uploads.js';
@@ -230,25 +231,28 @@ 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 must be absolute for email
// clients: the frontend passes its own window.location.origin (?base=),
// which is exactly the public address the admin is browsing — reliable
// even behind a TLS-terminating reverse proxy where req.protocol would
// say "http". The request's host is only a fallback.
adminRouter.get('/rencontres/:id/email', validate(idParam, 'params'), async (req, res, next) => {
// pre-opened (/?inscription=<id>). The body carries the editable text
// zones (greeting/intro/outro/signature — omitted = default wording,
// empty = block hidden) plus `base`: URLs must be absolute for email
// clients, and the frontend's window.location.origin is exactly the
// public address the admin is browsing — reliable even behind a
// TLS-terminating reverse proxy where req.protocol would say "http".
// The request's host is only a fallback.
adminRouter.post('/rencontres/:id/email', validate(idParam, 'params'), validate(emailComposeSchema), 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 { base, ...texts } = req.data;
let baseUrl = `${req.protocol}://${req.get('host')}`;
if (typeof req.query.base === 'string') {
if (base) {
try {
const u = new URL(req.query.base);
const u = new URL(base);
if (u.protocol === 'http:' || u.protocol === 'https:') baseUrl = u.origin;
} catch {
// invalid ?base= — keep the fallback
// invalid base — keep the fallback
}
}
res.json(buildInvitationEmail({ rencontre: result.rows[0], baseUrl }));
res.json(buildInvitationEmail({ rencontre: result.rows[0], baseUrl, texts }));
} catch (err) {
next(err);
}