From 805f0fe00b1314970a95c717c2945102130a0fd4 Mon Sep 17 00:00:00 2001 From: Guillaume DENIS Date: Wed, 8 Jul 2026 07:38:42 +0200 Subject: [PATCH] =?UTF-8?q?feat(preferences):=20=C3=A9diteur=20de=20templa?= =?UTF-8?q?tes=20email=20HTML=20avec=20variables=20et=20pi=C3=A8ces=20join?= =?UTF-8?q?tes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rendu HTML email-safe (banner, titres, listes, code, liens) via renderEmailHtml - Import d'images (stockées sur disque, inlinées en data URI à l'envoi) - Variables {{ }} éditables, pré-remplies via AD - Pièces jointes : ajout de fichiers joints au mail (ex : connexion.rdp) - Template d'exemple enrichi semé une seule fois dans la base partagée - Lien vers le générateur de mots de passe dans la navigation Co-Authored-By: Claude Opus 4.8 --- cmdb.html | 1 + preferences.html | 697 ++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 662 insertions(+), 36 deletions(-) diff --git a/cmdb.html b/cmdb.html index d2e8c06..fcde914 100644 --- a/cmdb.html +++ b/cmdb.html @@ -405,6 +405,7 @@
CMDBMachines, réseaux, salles
🔐
Générateur certificatsClés RSA, CSR, SSL
🔌
Configuration switchVLANs, export config
+
🔑
Mots de passeGénérateur & envoi
+
+

Général

@@ -470,6 +500,65 @@
+
+
+

Types de machine

+ +
+
+
« Virtuel » = hébergé sur une autre machine. « Rackable » = peut occuper des U dans une baie. « User » = permet d'assigner un utilisateur (laptop, optiplex…).
+
+ +
+
+

Statuts de machine

+ +
+
+
+ +
+
+

Catalogue OS

+ +
+
+
+ +
+

Modèles de serveur

+
+ + + +
+
+
+ +
+

CPU

+
+ + + + +
+
+
Sur un serveur : choisir le CPU + nombre de sockets → total de cores calculé automatiquement.
+
+ +
+ + + +
+ +
+

Sauvegardes (snapshots)

@@ -497,22 +586,17 @@ migrer les données hors de la base.
-
-
-

Types de machine

-
-
-
« Virtuel » = hébergé sur une autre machine. « Rackable » = peut occuper des U dans une baie. « User » = permet d'assigner un utilisateur (laptop, optiplex…).
-
-
-

Statuts de machine

- -
-
-
+ +
+ +
@@ -523,14 +607,6 @@
Lien + modèle de description à copier pour créer un ticket (ex : changement de configuration switch). {name} est remplacé par le nom de la machine.
-
-
-

Catalogue OS

- -
-
-
-

Active Directory (recherche d'utilisateurs)

@@ -590,26 +666,84 @@
-
-

Modèles de serveur

-
- - -
-
+
+ + +
+ +
+ +
+

Général

+
+
+
+
+
+
+
+
+
Le préfixe est ajouté devant la partie aléatoire du mot de passe temporaire (ex : totototoXk4fq2Pm).
-

CPU

-
- - - - +
+

Templates de génération

+ +
+
+
Préréglages de génération : classes de caractères, longueur et préfixe. + Exemple : « Simple Password » = minuscules + chiffres, pas de majuscules ni de caractères spéciaux, 6 caractères.
+
+ +
+
+

Templates email — mot de passe temporaire

+ +
+
+
+ Placeholders : {{ prenom }}, {{ nom }}, {{ displayName }}, + {{ username }}, {{ mail }}, {{ tmp_password }}. + Deux formats : Texte brut ou HTML enrichi (bandeau, image, listes, bloc de code, liens) + avec aperçu en direct. Voir le template « Exemple enrichi (HTML) ». +
+
+ +
+

Serveur SMTP (envoi des mails)

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + + +
+
Réglages partagés avec le générateur de mots de passe, stockés dans la base. L'envoi passe par le serveur du portail.
+
+
-
-
Sur un serveur : choisir le CPU + nombre de sockets → total de cores calculé automatiquement.
@@ -1073,9 +1207,11 @@ async function boot(){ seedOs(); seedTypes(); seedSwitchType(); loadTypes(); seedStatuses(); loadStatuses(); seedTickets(); syncTicketsToLocalStorage(); // serveur : ne pousser que si base vide (init) — sinon on garderait la version if(!SERVER_MODE || fresh) await persist(); + pgSeedExampleOnce(); // ajoute l'exemple de template HTML dans la base partagée (une fois) renderAll(); updateStatus(); nav('settings'); // page dédiée Préférences : n'affiche que les réglages + focusHashCat(); // deep-link #pwgen (ou autre) : déplie + défile vers la catégorie if(SERVER_MODE) startSync(); } @@ -1310,6 +1446,495 @@ function renderSettings(){ renderHwCatalogs(); renderAdSettings(); renderSnapshots(); + renderPwgenSettings(); + applyCatState(); +} + +/* ---------------- Catégories dépliantes (état mémorisé) ---------------- */ +function catState(){ try{ return JSON.parse(localStorage.getItem('pref_cats')||'{}'); }catch(e){ return {}; } } +function toggleCat(id){ + const cat = document.querySelector('.settings-cat[data-cat="'+id+'"]'); + if(!cat) return; + const collapsed = cat.classList.toggle('collapsed'); + const st = catState(); st[id] = collapsed; + try{ localStorage.setItem('pref_cats', JSON.stringify(st)); }catch(e){} +} +function applyCatState(){ + const st = catState(); + document.querySelectorAll('.settings-cat').forEach(cat => { + // repliées par défaut ; l'état mémorisé (ouvert/fermé) prime s'il existe + const collapsed = (cat.dataset.cat in st) ? !!st[cat.dataset.cat] : true; + cat.classList.toggle('collapsed', collapsed); + }); +} +/* deep-link : # déplie la catégorie ciblée et défile jusqu'à elle */ +function focusHashCat(){ + const id = (location.hash || '').replace('#',''); + if(!id) return; + const cat = document.querySelector('.settings-cat[data-cat="'+id+'"]'); + if(!cat) return; + cat.classList.remove('collapsed'); + setTimeout(() => cat.scrollIntoView({ behavior:'smooth', block:'start' }), 120); +} +window.addEventListener('hashchange', focusHashCat); + +/* ========================================================================== + Réglages du générateur de mots de passe (stockés dans settings['pwgen_prefs']). + Édités ici (préférences globales), lus par la page password-generator. + ========================================================================== */ +const PWGEN_KEY = 'pwgen_prefs'; +const PG_DEF_EMAIL_TPL = { + id: 'default', format: 'text', label: 'Mot de passe temporaire', + subject: 'Votre mot de passe temporaire', + body: 'Bonjour {{ prenom }},\n\nVoici ton mot de passe temporaire : {{ tmp_password }}\n\nMerci de le changer dès ta première connexion.\n\nLe support IT', +}; +/* Exemple de template HTML enrichi : banner, image (data URI), listes, bloc de code… */ +const PG_EXAMPLE_HTML_TPL = { + id: 'example_html', format: 'html', label: 'Exemple enrichi (HTML)', + subject: 'Bienvenue {{ prenom }} — vos accès', + body: [ + ':::banner #5b9cf5', + 'Bienvenue chez ACME, {{ prenom }} 👋', + ':::', + '', + '![Logo ACME](data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxODAiIGhlaWdodD0iNDgiPjxyZWN0IHdpZHRoPSIxODAiIGhlaWdodD0iNDgiIHJ4PSI4IiBmaWxsPSIjNWI5Y2Y1Ii8+PHRleHQgeD0iOTAiIHk9IjMxIiBmb250LWZhbWlseT0iQXJpYWwsc2Fucy1zZXJpZiIgZm9udC1zaXplPSIyMCIgZmlsbD0iI2ZmZmZmZiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC13ZWlnaHQ9ImJvbGQiPkFDTUUgwrcgSVQ8L3RleHQ+PC9zdmc+)', + '', + '## Vos identifiants', + '', + 'Bonjour **{{ displayName }}**, votre compte a bien été créé. Voici vos informations de connexion :', + '', + '- Nom d’utilisateur : `{{ username }}`', + '- Adresse email : `{{ mail }}`', + '- Mot de passe temporaire : {{ tmp_password }}', + '', + '## Première connexion', + '', + '1. Rendez-vous sur [le portail](https://portail.exemple.com)', + '2. Connectez-vous avec les identifiants ci-dessus', + '3. Changez votre mot de passe immédiatement', + '', + '### Exemple en ligne de commande', + '', + '```', + 'ssh {{ username }}@serveur.exemple.com', + 'passwd # changez votre mot de passe', + '```', + '', + '## Accès au bureau à distance', + '', + 'Le fichier de connexion RDP est joint à ce mail : ouvrez-le pour lancer directement la session.', + '', + 'Voir aussi [la documentation d’accès distant](https://portail.exemple.com/vpn).', + '', + '---', + '', + 'Besoin d’aide ? Contactez le **support IT**.', + ].join('\n'), + attachments: [{ name: 'connexion.rdp', url: '/password-generator/files/connexion.rdp' }], +}; + +/* ---- Rendu email HTML enrichi (markdown léger → HTML « email-safe »). + Fonction partagée à l'identique avec la page password-generator. Les + {{ placeholders }} sont substitués en dernier (valeurs échappées). ---- */ +function emailEsc(s){ return String(s == null ? '' : s).replace(/[&<>]/g, c => ({'&':'&','<':'<','>':'>'}[c])); } +function emailFillVars(html, ctx){ + return html.replace(/\{\{\s*(\w+)\s*\}\}/g, (m, k) => { + if(!(k in ctx)) return m; + const v = emailEsc(ctx[k]); + return k === 'tmp_password' + ? '' + v + '' + : v; + }); +} +function renderEmailHtml(src, ctx){ + const lines = emailEsc(src).split('\n'); + const inline = t => t + .replace(/`([^`]+)`/g, '$1') + .replace(/\*\*([^*]+)\*\*/g, '$1') + .replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, '$1'); + let html = '', i = 0, m; + while(i < lines.length){ + const line = lines[i]; + if(m = line.match(/^:::\s*banner\s*(#[0-9a-fA-F]{3,8})?\s*$/)){ + const color = m[1] || '#5b9cf5', buf = []; + for(i++; i < lines.length && !/^:::\s*$/.test(lines[i]); i++) buf.push(lines[i]); + i++; + html += '
' + inline(buf.join('
')) + '
'; + continue; + } + if(/^```/.test(line)){ + const buf = []; + for(i++; i < lines.length && !/^```/.test(lines[i]); i++) buf.push(lines[i]); + i++; + html += '
' + buf.join('\n') + '
'; + continue; + } + if(m = line.match(/^!\[([^\]]*)\]\(([^)]+)\)\s*$/)){ + html += '
' + m[1] + '
'; + i++; continue; + } + if(/^---+\s*$/.test(line)){ html += '
'; i++; continue; } + if(m = line.match(/^(#{1,3})\s+(.*)$/)){ + const lvl = m[1].length, sz = lvl === 1 ? '22px' : lvl === 2 ? '18px' : '15px'; + html += '' + inline(m[2]) + ''; + i++; continue; + } + if(/^[-*]\s+/.test(line)){ + const items = []; + for(; i < lines.length && /^[-*]\s+/.test(lines[i]); i++) items.push(inline(lines[i].replace(/^[-*]\s+/, ''))); + html += '
    ' + items.map(it => '
  • ' + it + '
  • ').join('') + '
'; + continue; + } + if(/^\d+\.\s+/.test(line)){ + const items = []; + for(; i < lines.length && /^\d+\.\s+/.test(lines[i]); i++) items.push(inline(lines[i].replace(/^\d+\.\s+/, ''))); + html += '
    ' + items.map(it => '
  1. ' + it + '
  2. ').join('') + '
'; + continue; + } + if(/^\s*$/.test(line)){ i++; continue; } + const buf = [lines[i]]; i++; // consomme au moins la ligne courante (jamais de boucle infinie) + for(; i < lines.length && !/^\s*$/.test(lines[i]) && !/^(#{1,3}\s|[-*]\s|\d+\.\s|```|:::|---+\s*$|!\[)/.test(lines[i]); i++) buf.push(lines[i]); + html += '

' + inline(buf.join('
')) + '

'; + } + // conteneur neutre, sans fond : le mail adopte le fond par défaut du client + return emailFillVars('
' + html + '
', ctx); +} +const PG_DEF_GEN_PRESET = { id:'simple', label:'Simple Password', len:6, prefix:'', lower:true, upper:false, digits:true, symbols:false }; +function pgDefProfile(o){ return { len:16, prefix:'', lower:true, upper:true, digits:true, symbols:true, preset:'', ...o }; } +function pgNormalize(p){ + p = p || {}; + p.smtp = { host:'', port:'', security:'starttls', user:'', pass:'', from:'', from_name:'', ...(p.smtp||{}) }; + if(!Array.isArray(p.templates) || !p.templates.length) p.templates = [ {...PG_DEF_EMAIL_TPL} ]; + p.templates.forEach(t => { if(!t.format) t.format = 'text'; if(!Array.isArray(t.attachments)) t.attachments = []; }); // schéma + // ajoute l'exemple HTML une seule fois (marqueur persistant : la suppression tient) + if(!p._seeded_html_example){ p.templates.push({...PG_EXAMPLE_HTML_TPL}); p._seeded_html_example = true; } + if(!Array.isArray(p.gen_presets)) p.gen_presets = [ {...PG_DEF_GEN_PRESET} ]; + if(!p.profiles || typeof p.profiles !== 'object') + p.profiles = { password: pgDefProfile({len:16}), temp: pgDefProfile({len:8, symbols:false}) }; + p.profiles.password = pgDefProfile(p.profiles.password); + p.profiles.temp = pgDefProfile(p.profiles.temp); + if(p.pp_words == null) p.pp_words = 4; + return p; +} +function pgPrefs(){ try{ return pgNormalize(JSON.parse(getSetting(PWGEN_KEY,'{}'))); }catch(e){ return pgNormalize({}); } } +function pgSave(p){ setSetting(PWGEN_KEY, JSON.stringify(p)); } // run() → persist() (base partagée) +/* Persiste l'exemple HTML une seule fois dans la base partagée : le générateur + (qui lit la même base) le voit alors sans avoir à normaliser lui-même. */ +function pgSeedExampleOnce(){ + let raw; try{ raw = JSON.parse(getSetting(PWGEN_KEY, '{}')); }catch(e){ raw = {}; } + if(raw && raw._seeded_html_example) return; // déjà fait (ou exemple supprimé) → on ne touche pas + pgSave(pgNormalize(raw)); // pgNormalize ajoute l'exemple + le marqueur +} + +function renderPwgenSettings(){ + const p = pgPrefs(); + const set = (id,v)=>{ const e=document.getElementById(id); if(e) e.value = v; }; + set('pg_pw_len', p.profiles.password.len); + set('pg_tmp_len', p.profiles.temp.len); + set('pg_tmp_prefix', p.profiles.temp.prefix); + set('pg_smtp_host', p.smtp.host); set('pg_smtp_port', p.smtp.port); + set('pg_smtp_security', p.smtp.security); set('pg_smtp_user', p.smtp.user); + set('pg_smtp_pass', p.smtp.pass); set('pg_smtp_from', p.smtp.from); set('pg_smtp_from_name', p.smtp.from_name); + renderPgGenTplTable(); renderPgEmailTplTable(); +} +function savePwgenGeneral(){ + const p = pgPrefs(); + p.profiles.password.len = Math.max(4, Math.min(128, parseInt(document.getElementById('pg_pw_len').value) || 16)); + p.profiles.password.preset = ''; + p.profiles.temp.len = Math.max(4, Math.min(64, parseInt(document.getElementById('pg_tmp_len').value) || 8)); + p.profiles.temp.prefix = document.getElementById('pg_tmp_prefix').value.trim(); + p.profiles.temp.preset = ''; + pgSave(p); + showToast('Réglages du générateur enregistrés', 'info', 2500); +} +function savePwgenSmtp(){ + const p = pgPrefs(); + p.smtp = { + host: document.getElementById('pg_smtp_host').value.trim(), + port: document.getElementById('pg_smtp_port').value.trim(), + security: document.getElementById('pg_smtp_security').value, + user: document.getElementById('pg_smtp_user').value.trim(), + pass: document.getElementById('pg_smtp_pass').value, + from: document.getElementById('pg_smtp_from').value.trim(), + from_name: document.getElementById('pg_smtp_from_name').value.trim(), + }; + pgSave(p); +} +async function sendPwgenTestMail(){ + const to = document.getElementById('pg_smtp_test_to').value.trim(); + const out = document.getElementById('pgSmtpTestResult'); + if(!to || !to.includes('@')){ out.textContent = 'destinataire invalide'; out.style.color = 'var(--danger)'; return; } + savePwgenSmtp(); + out.textContent = '… envoi'; out.style.color = 'var(--muted)'; + try{ + const res = await fetch('/mail/send', { + method:'POST', headers:{'Content-Type':'application/json'}, + body: JSON.stringify({ smtp: pgPrefs().smtp, to, + subject:'Test SMTP — portail Infra', + body:'Mail de test envoyé depuis les préférences du portail Infra.' }), + }); + const data = await res.json(); + if(!data.ok) throw new Error(data.error || 'échec'); + out.textContent = '✓ envoyé'; out.style.color = 'var(--ok, #4ecfa5)'; + }catch(e){ out.textContent = '✗ ' + String(e.message || e); out.style.color = 'var(--danger)'; } +} + +/* ----- templates de génération (CRUD) ----- */ +function pgPresetDesc(t){ + const parts = []; + if(t.lower) parts.push('minuscules'); + if(t.upper) parts.push('majuscules'); + if(t.digits) parts.push('chiffres'); + if(t.symbols) parts.push('symboles'); + let s = (parts.join(' + ') || 'minuscules') + ' · ' + t.len + ' car.'; + if(t.prefix) s += ' · préfixe « ' + t.prefix + ' »'; + return s; +} +function renderPgGenTplTable(){ + const el = document.getElementById('pgGenTplTable'); if(!el) return; + const list = pgPrefs().gen_presets; + if(!list.length){ el.innerHTML = '
Aucun template.
'; return; } + el.innerHTML = '
' + + list.map((t,i) => ''+ + '').join('') + + '
NomRéglages
'+esc(t.label)+''+esc(pgPresetDesc(t))+''+ + '
'; +} +function openPgGenTplModal(idx){ + const list = pgPrefs().gen_presets; + const t = idx !== undefined ? list[idx] : { label:'', len:8, prefix:'', lower:true, upper:true, digits:true, symbols:false }; + const chk = v => v ? ' checked' : ''; + openModal( + '

'+(idx!==undefined?'Modifier le template de génération':'Nouveau template de génération')+'

'+ + '
'+ + '
'+ + '
'+ + '
'+ + '
'+ + '
'+ + ''+ + ''+ + ''+ + '
'+ + ''); +} +function savePgGenTpl(idx){ + const label = document.getElementById('pgt_label').value.trim(); + if(!label){ showToast('Nom du template requis','warn'); return; } + const data = { + label, + len: Math.max(1, Math.min(128, parseInt(document.getElementById('pgt_len').value)||8)), + prefix: document.getElementById('pgt_prefix').value.trim(), + lower: document.getElementById('pgt_lower').checked, + upper: document.getElementById('pgt_upper').checked, + digits: document.getElementById('pgt_digits').checked, + symbols: document.getElementById('pgt_symbols').checked, + }; + if(!data.lower && !data.upper && !data.digits && !data.symbols){ showToast('Cochez au moins une classe','warn'); return; } + const p = pgPrefs(); + if(idx >= 0) Object.assign(p.gen_presets[idx], data); + else p.gen_presets.push({ id:'gen_'+Date.now().toString(36), ...data }); + pgSave(p); closeModal(); renderPgGenTplTable(); +} +function deletePgGenTpl(idx){ + const p = pgPrefs(); const t = p.gen_presets[idx]; + if(!t || !confirm('Supprimer le template « '+t.label+' » ?')) return; + for(const k of ['password','temp']) if(p.profiles[k].preset === t.id) p.profiles[k].preset = ''; + p.gen_presets.splice(idx,1); + pgSave(p); renderPgGenTplTable(); +} + +/* ----- templates email (CRUD) ----- */ +/* contexte d'exemple pour l'aperçu en direct de l'éditeur */ +const PG_SAMPLE_CTX = { prenom:'Marie', nom:'Dupont', displayName:'Marie Dupont', + username:'mdupont', mail:'marie.dupont@exemple.com', tmp_password:'Xk4-fq2Pm!' }; +/* fragments insérables depuis la barre d'outils de l'éditeur HTML */ +const PG_SNIPPETS = { + banner: ':::banner #5b9cf5\nTitre du bandeau\n:::\n', + heading: '## Titre de section\n', + list: '- premier élément\n- deuxième élément\n- troisième élément\n', + ordered: '1. première étape\n2. deuxième étape\n3. troisième étape\n', + code: '```\ncommande --exemple\n```\n', + link: '[texte du lien](https://exemple.com)', + rule: '\n---\n', +}; + +function renderPgEmailTplTable(){ + const el = document.getElementById('pgEmailTplTable'); if(!el) return; + const list = pgPrefs().templates; + if(!list.length){ el.innerHTML = '
Aucun template.
'; return; } + const fmtBadge = f => f === 'html' + ? 'HTML' + : 'Texte'; + el.innerHTML = '
' + + list.map((t,i) => ''+ + '').join('') + + '
NomFormatObjet
'+esc(t.label)+''+fmtBadge(t.format)+''+esc(t.subject)+''+ + '
'; +} +let PG_EDIT_ATTACH = []; // pièces jointes du template en cours d'édition [{name, url}] +function openPgEmailTplModal(idx){ + const list = pgPrefs().templates; + const t = idx !== undefined ? list[idx] : { label:'', subject:'', body:'', format:'html' }; + PG_EDIT_ATTACH = Array.isArray(t.attachments) ? t.attachments.slice() : []; + const isHtml = (t.format || 'text') === 'html'; + const tbBtn = (type, txt) => ''; + openModal( + '

'+(idx!==undefined?'Modifier le template email':'Nouveau template email')+'

'+ + '
'+ + '
'+ + '
'+ + '
'+ + '
'+ + '
'+ + '
'+ + '
'+ + tbBtn('banner','▬ Bandeau')+tbBtn('heading','H Titre')+tbBtn('list','• Liste')+ + tbBtn('ordered','1. Numérotée')+tbBtn('code','</> Code')+tbBtn('link','🔗 Lien')+tbBtn('rule','— Séparateur')+ + ''+ + ''+ + '
'+ + ''+ + '
Placeholders : {{ prenom }}, {{ nom }}, {{ displayName }}, {{ username }}, {{ mail }}, {{ tmp_password }}. '+ + 'Syntaxe HTML : :::banner #couleur … :::, ![alt](url), # titre, - liste, ```code```, **gras**, [lien](url).
'+ + '
'+ + '
'+ + ''+ + ''+ + '
Fichiers joints au mail (ex : un .rdp de connexion). Max 10 Mo par fichier.
'+ + '
'+ + '
'+ + ''); + pgRenderAttach(); + pgEmailPreview(); +} +/* liste des pièces jointes du template en édition (chips + suppression) */ +function pgRenderAttach(){ + const el = document.getElementById('pge_attach'); if(!el) return; + if(!PG_EDIT_ATTACH.length){ el.innerHTML = '
Aucune pièce jointe.
'; return; } + el.innerHTML = '
' + PG_EDIT_ATTACH.map((a,i) => + '
'+ + '📎'+esc(a.name)+''+ + '
').join('') + '
'; +} +function pgRemoveAttach(i){ PG_EDIT_ATTACH.splice(i,1); pgRenderAttach(); pgEmailPreview(); } +/* aperçu en direct : rend le corps selon le format choisi, avec un contexte d'exemple */ +function pgEmailPreview(){ + const box = document.getElementById('pge_preview'); if(!box) return; + const fmt = document.getElementById('pge_format').value; + const body = document.getElementById('pge_body').value; + const subject = document.getElementById('pge_subject').value; + const subLine = '
Objet : '+ + emailFillVars(emailEsc(subject), PG_SAMPLE_CTX)+'
'; + const rendered = fmt === 'html' + ? renderEmailHtml(body, PG_SAMPLE_CTX) + : '
'+emailFillVars(emailEsc(body), PG_SAMPLE_CTX)+'
'; + const attachLine = PG_EDIT_ATTACH.length + ? '
📎 '+ + PG_EDIT_ATTACH.length+' pièce(s) jointe(s) : '+PG_EDIT_ATTACH.map(a=>emailEsc(a.name)).join(', ')+'
' + : ''; + box.innerHTML = subLine + rendered + attachLine; +} +/* insère un fragment de syntaxe à la position du curseur dans le textarea */ +function pgInsertSnippet(type){ + const ta = document.getElementById('pge_body'); + const snip = PG_SNIPPETS[type] || ''; + const s = ta.selectionStart, e = ta.selectionEnd; + ta.value = ta.value.slice(0, s) + snip + ta.value.slice(e); + const pos = s + snip.length; + ta.focus(); ta.setSelectionRange(pos, pos); + document.getElementById('pge_format').value = 'html'; // un fragment implique le format HTML + pgEmailPreview(); +} +/* insère la référence markdown de l'image dans le corps, au curseur */ +function pgInsertImageMd(url, alt){ + const ta = document.getElementById('pge_body'); + const md = '\n![' + alt + '](' + url + ')\n'; + const s = ta.selectionStart; + ta.value = ta.value.slice(0, s) + md + ta.value.slice(ta.selectionEnd); + document.getElementById('pge_format').value = 'html'; + pgEmailPreview(); +} +/* importe une image : envoyée au serveur qui la stocke dans password-generator/images/ + (le template ne référence qu'une URL, pas de gros data URI dans la base). En mode + fichier (pas de serveur) on retombe sur une intégration data URI. */ +async function pgImportImage(input){ + const f = input.files && input.files[0]; if(!f) return; + input.value = ''; + if(f.size > 2 * 1024 * 1024){ showToast('Image trop lourde (max 2 Mo)', 'warn'); return; } + const alt = f.name.replace(/\.[^.]+$/, ''); + const b64 = await new Promise((res, rej) => { + const r = new FileReader(); + r.onload = () => res(String(r.result).split(',')[1] || ''); + r.onerror = rej; + r.readAsDataURL(f); + }); + try{ + const res = await fetch('/pwgen/upload-image', { + method:'POST', headers:{'Content-Type':'application/json'}, + body: JSON.stringify({ name:f.name, b64 }), + }); + const data = await res.json(); + if(!data.ok) throw new Error(data.error || 'échec'); + pgInsertImageMd(data.url, alt); + showToast('Image importée : ' + data.name, 'info', 2500); + }catch(e){ + // pas de serveur (file://) → repli data URI intégré + pgInsertImageMd('data:' + (f.type || 'image/png') + ';base64,' + b64, alt); + showToast('Image intégrée localement (serveur indisponible)', 'info', 3000); + } +} +/* importe un fichier (rdp, pdf…) : stocké côté serveur dans password-generator/files/ + et ajouté à la liste des pièces jointes du template (rien n'est inséré dans le + corps). À l'envoi, le serveur joint ces fichiers au mail. Nécessite le serveur. */ +async function pgImportFile(input){ + const f = input.files && input.files[0]; if(!f) return; + input.value = ''; + if(f.size > 10 * 1024 * 1024){ showToast('Fichier trop lourd (max 10 Mo)', 'warn'); return; } + const b64 = await new Promise((res, rej) => { + const r = new FileReader(); + r.onload = () => res(String(r.result).split(',')[1] || ''); + r.onerror = rej; + r.readAsDataURL(f); + }); + try{ + const res = await fetch('/pwgen/upload-file', { + method:'POST', headers:{'Content-Type':'application/json'}, + body: JSON.stringify({ name:f.name, b64 }), + }); + const data = await res.json(); + if(!data.ok) throw new Error(data.error || 'échec'); + PG_EDIT_ATTACH.push({ name: f.name, url: data.url }); + pgRenderAttach(); + pgEmailPreview(); + showToast('Pièce jointe ajoutée : ' + f.name, 'info', 2500); + }catch(e){ + showToast('Pièce jointe impossible : serveur requis (envoi indisponible en mode fichier)', 'warn'); + } +} +function savePgEmailTpl(idx){ + const label = document.getElementById('pge_label').value.trim(); + if(!label){ showToast('Nom du template requis','warn'); return; } + const subject = document.getElementById('pge_subject').value.trim(); + const body = document.getElementById('pge_body').value; + const format = document.getElementById('pge_format').value === 'html' ? 'html' : 'text'; + const attachments = PG_EDIT_ATTACH.slice(); + const p = pgPrefs(); + if(idx >= 0) Object.assign(p.templates[idx], { label, subject, body, format, attachments }); + else p.templates.push({ id:'tpl_'+Date.now().toString(36), label, subject, body, format, attachments }); + pgSave(p); closeModal(); renderPgEmailTplTable(); +} +function deletePgEmailTpl(idx){ + const p = pgPrefs(); const t = p.templates[idx]; + if(!t || !confirm('Supprimer le template « '+t.label+' » ?')) return; + p.templates.splice(idx,1); + if(!p.templates.length) p.templates = [ {...PG_DEF_EMAIL_TPL} ]; + pgSave(p); renderPgEmailTplTable(); } /* ---------------- Snapshots de la base (serveur) ---------------- */