mirror of
https://github.com/R0m1k3/LogiFlow.git
synced 2026-10-11 17:27:31 +02:00
Replaces the mailto: link with a server-side send, so the message carries the store's signature and logo instead of depending on each workstation's Outlook. Store record (groups): - address, phone and logo (data URI, 200 KB cap) feed the mail signature - per-store SMTP settings: host, port, SSL/STARTTLS, credentials, sender address and display name, with an enable switch - "test connection" button verifies the server without sending anything - the empty-form literal, previously repeated five times, becomes one constant Sending: - nodemailer transport built per store from its own settings - multipart mail: plain-text alternative plus HTML whose signature embeds the logo as an inline CID attachment, which Outlook renders without the remote image blocking that a data: URI would hit - delivery details are HTML-escaped - Reply-To set to the store address; the row shows a spinner and refuses a second click while a send is in flight Credentials: - the SMTP password is never returned to the client; a response-layer sanitizer strips it from every /api payload and replaces it with a smtpPasswordSet flag, covering the ten-plus queries that join full group rows into deliveries, orders and user relations - an empty password field on save keeps the stored one rather than clearing it Verified end-to-end against a local SMTP server: transport, auth, From/Reply-To, multipart structure and the inline logo attachment. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FHdE9nEh8zHhQNGpCgrtYe
44 lines
1.4 KiB
TypeScript
44 lines
1.4 KiB
TypeScript
/**
|
|
* Nettoyage des données sensibles avant envoi au client.
|
|
*/
|
|
|
|
/**
|
|
* Retire récursivement le mot de passe SMTP des réponses API et le remplace par
|
|
* un indicateur de présence (smtpPasswordSet). Traverse les objets et tableaux
|
|
* imbriqués car les magasins apparaissent sous plusieurs formes : liste de
|
|
* groupes, champ "group" d'une livraison ou d'une commande, relations
|
|
* utilisateur... Filtrer chaque requête serait fragile, on nettoie donc une
|
|
* seule fois à la sortie.
|
|
*/
|
|
export function stripSmtpPassword(value: any, depth = 0, seen = new WeakSet()): any {
|
|
if (depth > 8 || value === null || typeof value !== 'object') {
|
|
return value;
|
|
}
|
|
|
|
// Les structures cycliques sont renvoyées telles quelles plutôt que de
|
|
// faire boucler la récursion
|
|
if (seen.has(value)) {
|
|
return value;
|
|
}
|
|
seen.add(value);
|
|
|
|
if (Array.isArray(value)) {
|
|
return value.map(item => stripSmtpPassword(item, depth + 1, seen));
|
|
}
|
|
|
|
// Ne pas dénaturer les types non sérialisables en objets simples
|
|
if (value instanceof Date || Buffer.isBuffer(value)) {
|
|
return value;
|
|
}
|
|
|
|
const result: Record<string, any> = {};
|
|
for (const [key, entry] of Object.entries(value)) {
|
|
if (key === 'smtpPassword' || key === 'smtp_password') {
|
|
result.smtpPasswordSet = Boolean(entry);
|
|
continue;
|
|
}
|
|
result[key] = stripSmtpPassword(entry, depth + 1, seen);
|
|
}
|
|
return result;
|
|
}
|