Files
PleinR/src/app/api/informations/[id]/image/route.ts
T
Claude 877b0ece78 Diffuser une information par e-mail, à la publication
La case « Envoyer aussi par e-mail » du bouton Publier met une ligne par
adhérent dans la file : jamais de copie partagée, aucune adresse visible
des autres. `informations.email_sent_at` fait office de garde
anti-double-diffusion, comme `publishPromoShares` pour les réseaux — une
information dépubliée puis republiée ne repart pas.

La diffusion est réservée à `manageEmails` : un modérateur publie, il
n'écrit pas à tous les adhérents. Sans adresse publique enregistrée, rien
ne part et le bandeau le dit, plutôt que d'expédier un message dont le
logo et tous les liens seraient cassés.

L'image de couverture passe par `/api/informations/<id>/image` et non en
data-URI : Gmail et Outlook suppriment les `<img src="data:">`, et
l'incorporer gonflerait chaque ligne de la file à plusieurs méga-octets.
La route ne sert que les informations publiées.

Deux défauts corrigés en chemin, trouvés en jouant le parcours complet :

- `db.execute` rend les colonnes brutes, sans les rebaptiser comme le fait
  le constructeur de requêtes : `to_address` n'arrivait pas dans
  `toAddress`, et nodemailer refusait tous les messages faute de
  destinataire.
- `PROMO_SCHEDULER=off` empêchait le chargement du module de fond, donc
  coupait aussi l'envoi des e-mails. Chaque boucle a maintenant son propre
  interrupteur.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014QsLjRAnuLwivqxCbM4WeP
2026-09-14 16:15:11 +00:00

43 lines
1.7 KiB
TypeScript

import { NextResponse } from "next/server";
import { getInformation } from "@/lib/informations";
export const dynamic = "force-dynamic";
/**
* Sert l'image de couverture d'une information **publiée**, en octets.
*
* Elle est stockée en data-URI, ce qui convient au site mais pas aux e-mails :
* Gmail et Outlook suppriment les `<img src="data:">`. Cette route donne au
* gabarit une URL ordinaire, et évite au passage de recopier plusieurs
* méga-octets dans chaque ligne de la file d'attente.
*
* Seules les informations publiées répondent : un brouillon ne doit pas fuiter
* par une URL devinable.
*/
const DATA_URI = /^data:(image\/(?:png|jpeg|jpg|webp|gif));base64,([A-Za-z0-9+/=\s]+)$/;
export async function GET(_request: Request, { params }: { params: Promise<{ id: string }> }) {
const { id: raw } = await params;
const id = Number(raw);
if (!Number.isInteger(id) || id <= 0) return new NextResponse("Introuvable", { status: 404 });
const information = await getInformation(id);
if (!information || information.status !== "published" || !information.imageUrl) {
return new NextResponse("Introuvable", { status: 404 });
}
const match = DATA_URI.exec(information.imageUrl);
if (!match) return new NextResponse("Introuvable", { status: 404 });
const bytes = Buffer.from(match[2].replace(/\s+/g, ""), "base64");
return new NextResponse(bytes, {
headers: {
"Content-Type": match[1],
"Content-Length": String(bytes.length),
// L'image d'une information ne change pas : les clients mail peuvent la
// garder, et un message relu six mois plus tard l'affiche encore.
"Cache-Control": "public, max-age=86400, stale-while-revalidate=604800",
},
});
}