From d4923e9dfc230c644661cccdf9c49ff57ef563b2 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 14 Jul 2026 07:18:14 +0000 Subject: [PATCH] =?UTF-8?q?Corrige=20le=20logo=20cass=C3=A9=20dans=20l'ema?= =?UTF-8?q?il=20d'invitation=20derri=C3=A8re=20un=20reverse=20proxy?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Les URL absolues de l'email (logo, lien d'inscription) étaient construites côté serveur à partir de req.protocol : derrière un reverse proxy HTTPS (sans TRUST_PROXY), le serveur voit « http » et produit des URL en contenu mixte que le navigateur bloque — logo cassé dans l'aperçu et dans les webmails. Le frontend transmet désormais window.location.origin (?base=), c'est-à-dire l'adresse publique exacte vue par l'admin ; le serveur la valide (http/https uniquement) et ne garde l'hôte de la requête qu'en secours. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01Fkg15RCxNgUys4ru73He2o --- server/src/routes/admin.js | 18 ++++++++++++++---- web/src/components/admin/AdminTabs.jsx | 7 ++++++- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/server/src/routes/admin.js b/server/src/routes/admin.js index 944d82c..2e0d43c 100644 --- a/server/src/routes/admin.js +++ b/server/src/routes/admin.js @@ -230,14 +230,24 @@ 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=). URLs are absolute, built from the -// request's host so they match however the site is reached (direct port -// or reverse proxy). +// pre-opened (/?inscription=). 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) => { 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')}`; + let baseUrl = `${req.protocol}://${req.get('host')}`; + if (typeof req.query.base === 'string') { + try { + const u = new URL(req.query.base); + if (u.protocol === 'http:' || u.protocol === 'https:') baseUrl = u.origin; + } catch { + // invalid ?base= — keep the fallback + } + } res.json(buildInvitationEmail({ rencontre: result.rows[0], baseUrl })); } catch (err) { next(err); diff --git a/web/src/components/admin/AdminTabs.jsx b/web/src/components/admin/AdminTabs.jsx index baafb1c..4f04ef2 100644 --- a/web/src/components/admin/AdminTabs.jsx +++ b/web/src/components/admin/AdminTabs.jsx @@ -326,7 +326,12 @@ function EmailModal({ renc, onClose }) { const [copied, setCopied] = useState(''); useEffect(() => { - api.get(`/api/admin/rencontres/${renc.id}/email`).then(setData).catch((e) => setError(e.message)); + // The browser knows the site's real public origin (scheme included) — + // the server can't always tell behind a reverse proxy. + api + .get(`/api/admin/rencontres/${renc.id}/email?base=${encodeURIComponent(window.location.origin)}`) + .then(setData) + .catch((e) => setError(e.message)); }, [renc.id]); const flash = (what) => {