Corrige le logo cassé dans l'email d'invitation derrière un reverse proxy
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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Fkg15RCxNgUys4ru73He2o
This commit is contained in:
2 files changed
+20
-5
No files matched your search
@@ -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).
|
// Ready-to-send HTML invitation email for a rencontre (admin + moderator).
|
||||||
// The registration link opens the public site with the inscription modal
|
// The registration link opens the public site with the inscription modal
|
||||||
// pre-opened (/?inscription=<id>). URLs are absolute, built from the
|
// pre-opened (/?inscription=<id>). URLs must be absolute for email
|
||||||
// request's host so they match however the site is reached (direct port
|
// clients: the frontend passes its own window.location.origin (?base=),
|
||||||
// or reverse proxy).
|
// 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) => {
|
adminRouter.get('/rencontres/:id/email', validate(idParam, 'params'), async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const result = await query(`${RENC_SQL} WHERE r.id = $1 GROUP BY r.id`, [req.params.id]);
|
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' });
|
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 }));
|
res.json(buildInvitationEmail({ rencontre: result.rows[0], baseUrl }));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
next(err);
|
next(err);
|
||||||
|
|||||||
@@ -326,7 +326,12 @@ function EmailModal({ renc, onClose }) {
|
|||||||
const [copied, setCopied] = useState('');
|
const [copied, setCopied] = useState('');
|
||||||
|
|
||||||
useEffect(() => {
|
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]);
|
}, [renc.id]);
|
||||||
|
|
||||||
const flash = (what) => {
|
const flash = (what) => {
|
||||||
|
|||||||
Reference in new issue
Block a user