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:
4 files changed
+182
-64
No files matched your search
+14
-10
@@ -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);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user