feat(preferences): éditeur de templates email HTML avec variables et pièces jointes
- 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 <noreply@anthropic.com>
This commit is contained in:
parent
18751d75c8
commit
805f0fe00b
2 changed files with 662 additions and 36 deletions
697
preferences.html
697
preferences.html
|
|
@ -366,6 +366,26 @@
|
|||
#hoverCard .hc-rack{position:relative;width:120px;border:1px solid var(--grid);border-radius:4px;background:#0d0e12;flex-shrink:0}
|
||||
#hoverCard .hc-slot{position:absolute;left:1px;right:1px;border-radius:2px}
|
||||
#hoverCard .hc-uax{display:flex;flex-direction:column;justify-content:space-between;font-size:8px;color:var(--muted);font-variant-numeric:tabular-nums}
|
||||
|
||||
/* ---------- Préférences : catégories dépliantes ---------- */
|
||||
.settings-cat{margin-bottom:14px;border:1px solid var(--border);border-radius:var(--radius);background:var(--surface);overflow:hidden}
|
||||
.cat-head{
|
||||
display:flex;align-items:center;gap:12px;width:100%;background:none;border:none;cursor:pointer;
|
||||
padding:16px 20px;font-family:inherit;color:var(--ink);text-align:left;transition:background .12s;
|
||||
}
|
||||
.cat-head:hover{background:var(--surface-2)}
|
||||
.cat-ico{width:32px;height:32px;border-radius:9px;background:var(--accent-soft);color:var(--accent);
|
||||
display:flex;align-items:center;justify-content:center;font-size:16px;flex-shrink:0}
|
||||
.cat-txt{flex:1;min-width:0}
|
||||
.cat-txt b{display:block;font-size:15px;font-weight:700;letter-spacing:-.2px}
|
||||
.cat-txt small{display:block;font-size:12px;color:var(--muted);margin-top:2px}
|
||||
.cat-chev{width:18px;height:18px;flex-shrink:0;color:var(--muted);transition:transform .18s ease}
|
||||
.settings-cat.collapsed .cat-chev{transform:rotate(-90deg)}
|
||||
.cat-body{padding:0 16px 6px;border-top:1px solid var(--border)}
|
||||
.settings-cat.collapsed .cat-body{display:none}
|
||||
/* dans une catégorie, les cartes perdent leur fond/bordure (la catégorie les porte) */
|
||||
.cat-body > .card{background:var(--surface-2);border-color:var(--border);margin:14px 0}
|
||||
.cat-body > .card:first-child{margin-top:16px}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
|
@ -380,6 +400,7 @@
|
|||
<a href="cmdb.html"><div class="psw-ico">◧</div><div class="psw-txt"><b>CMDB</b><small>Machines, réseaux, salles</small></div></a>
|
||||
<a href="../cert-generator/dashboard.html"><div class="psw-ico">🔐</div><div class="psw-txt"><b>Générateur certificats</b><small>Clés RSA, CSR, SSL</small></div></a>
|
||||
<a href="../switch-config/switch_vlan_mapper.html"><div class="psw-ico">🔌</div><div class="psw-txt"><b>Configuration switch</b><small>VLANs, export config</small></div></a>
|
||||
<a href="../password-generator/password_generator.html"><div class="psw-ico">🔑</div><div class="psw-txt"><b>Mots de passe</b><small>Générateur & envoi</small></div></a>
|
||||
</div>
|
||||
<div class="psw" style="margin-top:auto;padding-top:14px;border-top:1px solid var(--border)">
|
||||
<a class="active" href="preferences.html"><div class="psw-ico">⚙</div><div class="psw-txt"><b>Préférences</b><small>Types, tickets, réglages</small></div></a>
|
||||
|
|
@ -462,6 +483,15 @@
|
|||
<div><h2>Préférences</h2><p>Types de machine, catalogue OS et réglages de base</p></div>
|
||||
</div>
|
||||
|
||||
<!-- ===== Catégorie : Inventaire & matériel ===== -->
|
||||
<div class="settings-cat" data-cat="inventaire">
|
||||
<button class="cat-head" onclick="toggleCat('inventaire')">
|
||||
<span class="cat-ico">🗄️</span>
|
||||
<span class="cat-txt"><b>Inventaire & matériel</b><small>Baies, types & statuts de machine, catalogues OS / matériel</small></span>
|
||||
<svg class="cat-chev" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="m6 9 6 6 6-6"/></svg>
|
||||
</button>
|
||||
<div class="cat-body">
|
||||
|
||||
<div class="card">
|
||||
<h3>Général</h3>
|
||||
<div class="field-row" style="max-width:420px">
|
||||
|
|
@ -470,6 +500,65 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:14px">
|
||||
<h3 style="margin:0">Types de machine</h3>
|
||||
<button class="btn primary small" onclick="openTypeModal()">+ Nouveau type</button>
|
||||
</div>
|
||||
<div class="table-wrap" id="typesContainer"></div>
|
||||
<div class="hint" style="margin-top:10px">« Virtuel » = hébergé sur une autre machine. « Rackable » = peut occuper des U dans une baie. « User » = permet d'assigner un utilisateur (laptop, optiplex…).</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:14px">
|
||||
<h3 style="margin:0">Statuts de machine</h3>
|
||||
<button class="btn primary small" onclick="openStatusModal()">+ Nouveau statut</button>
|
||||
</div>
|
||||
<div class="table-wrap" id="statusesContainer"></div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:14px">
|
||||
<h3 style="margin:0">Catalogue OS</h3>
|
||||
<button class="btn primary small" onclick="openOsModal()">+ Nouvel OS</button>
|
||||
</div>
|
||||
<div id="osContainer"></div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>Modèles de serveur</h3>
|
||||
<div class="toolbar" style="margin-bottom:12px">
|
||||
<input id="newBrand" placeholder="Marque (Dell, HP, Lenovo…)" style="width:200px" onkeydown="if(event.key==='Enter')addModel()">
|
||||
<input id="newModel" placeholder="Modèle (PowerEdge R740…)" style="width:230px" onkeydown="if(event.key==='Enter')addModel()">
|
||||
<button class="btn primary small" onclick="addModel()">+ Ajouter</button>
|
||||
</div>
|
||||
<div class="os-chips" id="modelsList"></div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>CPU</h3>
|
||||
<div class="toolbar" style="margin-bottom:12px">
|
||||
<input id="newCpu" placeholder="Intel Xeon Silver 4210, AMD EPYC 7302…" style="width:280px" onkeydown="if(event.key==='Enter')addCpu()">
|
||||
<input id="newCpuCores" type="number" min="1" placeholder="Cores / CPU" style="width:110px" onkeydown="if(event.key==='Enter')addCpu()">
|
||||
<input id="newCpuMhz" type="number" min="1" placeholder="MHz" style="width:90px" onkeydown="if(event.key==='Enter')addCpu()">
|
||||
<button class="btn primary small" onclick="addCpu()">+ Ajouter</button>
|
||||
</div>
|
||||
<div class="os-chips" id="cpusList"></div>
|
||||
<div class="hint" style="margin-top:8px">Sur un serveur : choisir le CPU + nombre de sockets → total de cores calculé automatiquement.</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===== Catégorie : Sauvegardes & données ===== -->
|
||||
<div class="settings-cat" data-cat="donnees">
|
||||
<button class="cat-head" onclick="toggleCat('donnees')">
|
||||
<span class="cat-ico">💾</span>
|
||||
<span class="cat-txt"><b>Sauvegardes & données</b><small>Snapshots de la base, import / export CSV</small></span>
|
||||
<svg class="cat-chev" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="m6 9 6 6 6-6"/></svg>
|
||||
</button>
|
||||
<div class="cat-body">
|
||||
|
||||
<div class="card" id="snapCard">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:14px">
|
||||
<div><h3 style="margin:0">Sauvegardes (snapshots)</h3></div>
|
||||
|
|
@ -497,22 +586,17 @@
|
|||
migrer les données hors de la base.</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:14px">
|
||||
<h3 style="margin:0">Types de machine</h3>
|
||||
<button class="btn primary small" onclick="openTypeModal()">+ Nouveau type</button>
|
||||
</div>
|
||||
<div class="table-wrap" id="typesContainer"></div>
|
||||
<div class="hint" style="margin-top:10px">« Virtuel » = hébergé sur une autre machine. « Rackable » = peut occuper des U dans une baie. « User » = permet d'assigner un utilisateur (laptop, optiplex…).</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:14px">
|
||||
<h3 style="margin:0">Statuts de machine</h3>
|
||||
<button class="btn primary small" onclick="openStatusModal()">+ Nouveau statut</button>
|
||||
</div>
|
||||
<div class="table-wrap" id="statusesContainer"></div>
|
||||
</div>
|
||||
<!-- ===== Catégorie : Intégrations ===== -->
|
||||
<div class="settings-cat" data-cat="integrations">
|
||||
<button class="cat-head" onclick="toggleCat('integrations')">
|
||||
<span class="cat-ico">🔗</span>
|
||||
<span class="cat-txt"><b>Intégrations</b><small>Active Directory, liens vers tickets</small></span>
|
||||
<svg class="cat-chev" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="m6 9 6 6 6-6"/></svg>
|
||||
</button>
|
||||
<div class="cat-body">
|
||||
|
||||
<div class="card">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:14px">
|
||||
|
|
@ -523,14 +607,6 @@
|
|||
<div class="hint" style="margin-top:10px">Lien + modèle de description à copier pour créer un ticket (ex : changement de configuration switch). <code>{name}</code> est remplacé par le nom de la machine.</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:14px">
|
||||
<h3 style="margin:0">Catalogue OS</h3>
|
||||
<button class="btn primary small" onclick="openOsModal()">+ Nouvel OS</button>
|
||||
</div>
|
||||
<div id="osContainer"></div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>Active Directory (recherche d'utilisateurs)</h3>
|
||||
<div class="field" style="max-width:420px"><label>Source</label>
|
||||
|
|
@ -590,26 +666,84 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>Modèles de serveur</h3>
|
||||
<div class="toolbar" style="margin-bottom:12px">
|
||||
<input id="newBrand" placeholder="Marque (Dell, HP, Lenovo…)" style="width:200px" onkeydown="if(event.key==='Enter')addModel()">
|
||||
<input id="newModel" placeholder="Modèle (PowerEdge R740…)" style="width:230px" onkeydown="if(event.key==='Enter')addModel()">
|
||||
<button class="btn primary small" onclick="addModel()">+ Ajouter</button>
|
||||
</div>
|
||||
<div class="os-chips" id="modelsList"></div>
|
||||
</div>
|
||||
|
||||
<!-- ===== Catégorie : Générateur de mots de passe ===== -->
|
||||
<div class="settings-cat" data-cat="pwgen" id="pwgen">
|
||||
<button class="cat-head" onclick="toggleCat('pwgen')">
|
||||
<span class="cat-ico">🔑</span>
|
||||
<span class="cat-txt"><b>Générateur de mots de passe</b><small>Longueurs par défaut, préfixe, templates de génération & email, SMTP</small></span>
|
||||
<svg class="cat-chev" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="m6 9 6 6 6-6"/></svg>
|
||||
</button>
|
||||
<div class="cat-body">
|
||||
|
||||
<div class="card">
|
||||
<h3>Général</h3>
|
||||
<div class="field-row" style="max-width:640px">
|
||||
<div class="field"><label>Longueur par défaut (mot de passe)</label>
|
||||
<input type="number" id="pg_pw_len" min="4" max="128" onchange="savePwgenGeneral()"></div>
|
||||
<div class="field"><label>Longueur par défaut (temporaire)</label>
|
||||
<input type="number" id="pg_tmp_len" min="4" max="64" onchange="savePwgenGeneral()"></div>
|
||||
<div class="field"><label>Préfixe par défaut (temporaire)</label>
|
||||
<input id="pg_tmp_prefix" placeholder="ex : toto" onchange="savePwgenGeneral()"></div>
|
||||
</div>
|
||||
<div class="hint">Le préfixe est ajouté devant la partie aléatoire du mot de passe temporaire (ex : <code>toto</code> → <code>totoXk4fq2Pm</code>).</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>CPU</h3>
|
||||
<div class="toolbar" style="margin-bottom:12px">
|
||||
<input id="newCpu" placeholder="Intel Xeon Silver 4210, AMD EPYC 7302…" style="width:280px" onkeydown="if(event.key==='Enter')addCpu()">
|
||||
<input id="newCpuCores" type="number" min="1" placeholder="Cores / CPU" style="width:110px" onkeydown="if(event.key==='Enter')addCpu()">
|
||||
<input id="newCpuMhz" type="number" min="1" placeholder="MHz" style="width:90px" onkeydown="if(event.key==='Enter')addCpu()">
|
||||
<button class="btn primary small" onclick="addCpu()">+ Ajouter</button>
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:14px">
|
||||
<h3 style="margin:0">Templates de génération</h3>
|
||||
<button class="btn primary small" onclick="openPgGenTplModal()">+ Nouveau template</button>
|
||||
</div>
|
||||
<div id="pgGenTplTable"></div>
|
||||
<div class="hint" style="margin-top:10px">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.</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:14px">
|
||||
<h3 style="margin:0">Templates email — mot de passe temporaire</h3>
|
||||
<button class="btn primary small" onclick="openPgEmailTplModal()">+ Nouveau template</button>
|
||||
</div>
|
||||
<div id="pgEmailTplTable"></div>
|
||||
<div class="hint" style="margin-top:10px">
|
||||
Placeholders : <code>{{ prenom }}</code>, <code>{{ nom }}</code>, <code>{{ displayName }}</code>,
|
||||
<code>{{ username }}</code>, <code>{{ mail }}</code>, <code>{{ tmp_password }}</code>.
|
||||
Deux formats : <b>Texte brut</b> ou <b>HTML enrichi</b> (bandeau, image, listes, bloc de code, liens)
|
||||
avec aperçu en direct. Voir le template « Exemple enrichi (HTML) ».
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>Serveur SMTP (envoi des mails)</h3>
|
||||
<div class="field-row">
|
||||
<div class="field"><label>Hôte</label><input id="pg_smtp_host" placeholder="smtp.exemple.com" onchange="savePwgenSmtp()"></div>
|
||||
<div class="field" style="max-width:100px"><label>Port</label><input type="number" id="pg_smtp_port" placeholder="587" onchange="savePwgenSmtp()"></div>
|
||||
<div class="field" style="max-width:160px"><label>Sécurité</label>
|
||||
<select id="pg_smtp_security" onchange="savePwgenSmtp()">
|
||||
<option value="none">Aucune</option>
|
||||
<option value="starttls">STARTTLS</option>
|
||||
<option value="ssl">SSL/TLS</option>
|
||||
</select></div>
|
||||
</div>
|
||||
<div class="field-row">
|
||||
<div class="field"><label>Utilisateur (optionnel)</label><input id="pg_smtp_user" autocomplete="off" onchange="savePwgenSmtp()"></div>
|
||||
<div class="field"><label>Mot de passe (optionnel)</label><input type="password" id="pg_smtp_pass" autocomplete="off" onchange="savePwgenSmtp()"></div>
|
||||
</div>
|
||||
<div class="field-row">
|
||||
<div class="field"><label>Adresse expéditeur</label><input id="pg_smtp_from" placeholder="it@exemple.com" onchange="savePwgenSmtp()"></div>
|
||||
<div class="field"><label>Nom expéditeur (optionnel)</label><input id="pg_smtp_from_name" placeholder="Support IT" onchange="savePwgenSmtp()"></div>
|
||||
</div>
|
||||
<div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap;margin-top:6px">
|
||||
<input id="pg_smtp_test_to" placeholder="destinataire@exemple.com" style="width:260px">
|
||||
<button class="btn small" onclick="sendPwgenTestMail()">Envoyer un mail de test</button>
|
||||
<span id="pgSmtpTestResult" style="font-size:12px"></span>
|
||||
</div>
|
||||
<div class="hint" style="margin-top:10px">Réglages partagés avec le <a href="../password-generator/password_generator.html" style="color:var(--accent)">générateur de mots de passe</a>, stockés dans la base. L'envoi passe par le serveur du portail.</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="os-chips" id="cpusList"></div>
|
||||
<div class="hint" style="margin-top:8px">Sur un serveur : choisir le CPU + nombre de sockets → total de cores calculé automatiquement.</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
|
@ -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 : #<catégorie> 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 }} 👋',
|
||||
':::',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'## 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'
|
||||
? '<strong><code style="font-family:ui-monospace,Consolas,monospace;background:#f2f2f2;color:#222;padding:2px 6px;border-radius:4px;font-size:14px">' + v + '</code></strong>'
|
||||
: v;
|
||||
});
|
||||
}
|
||||
function renderEmailHtml(src, ctx){
|
||||
const lines = emailEsc(src).split('\n');
|
||||
const inline = t => t
|
||||
.replace(/`([^`]+)`/g, '<code style="font-family:ui-monospace,Consolas,monospace;background:#f2f2f2;color:#222;padding:1px 5px;border-radius:4px">$1</code>')
|
||||
.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
|
||||
.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, '<a href="$2" style="color:#2f6fd8;text-decoration:underline">$1</a>');
|
||||
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 += '<div style="background:' + color + ';color:#fff;padding:22px 24px;border-radius:8px;font-size:20px;font-weight:700;line-height:1.35;margin:0 0 18px">' + inline(buf.join('<br>')) + '</div>';
|
||||
continue;
|
||||
}
|
||||
if(/^```/.test(line)){
|
||||
const buf = [];
|
||||
for(i++; i < lines.length && !/^```/.test(lines[i]); i++) buf.push(lines[i]);
|
||||
i++;
|
||||
html += '<pre style="background:#1d2026;color:#e6e6e6;padding:14px 16px;border-radius:8px;overflow:auto;font-family:ui-monospace,Consolas,monospace;font-size:13px;line-height:1.55;margin:0 0 16px"><code>' + buf.join('\n') + '</code></pre>';
|
||||
continue;
|
||||
}
|
||||
if(m = line.match(/^!\[([^\]]*)\]\(([^)]+)\)\s*$/)){
|
||||
html += '<div style="margin:0 0 16px"><img src="' + m[2] + '" alt="' + m[1] + '" style="max-width:100%;height:auto;border-radius:8px;display:block"></div>';
|
||||
i++; continue;
|
||||
}
|
||||
if(/^---+\s*$/.test(line)){ html += '<hr style="border:0;border-top:1px solid #e2e2e2;margin:20px 0">'; i++; continue; }
|
||||
if(m = line.match(/^(#{1,3})\s+(.*)$/)){
|
||||
const lvl = m[1].length, sz = lvl === 1 ? '22px' : lvl === 2 ? '18px' : '15px';
|
||||
html += '<h' + lvl + ' style="font-size:' + sz + ';font-weight:700;color:#1a1a1a;margin:16px 0 12px">' + inline(m[2]) + '</h' + lvl + '>';
|
||||
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 += '<ul style="margin:0 0 16px;padding-left:22px;color:#333">' + items.map(it => '<li style="margin:0 0 6px;line-height:1.5">' + it + '</li>').join('') + '</ul>';
|
||||
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 += '<ol style="margin:0 0 16px;padding-left:22px;color:#333">' + items.map(it => '<li style="margin:0 0 6px;line-height:1.5">' + it + '</li>').join('') + '</ol>';
|
||||
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 += '<p style="margin:0 0 16px;line-height:1.6;color:#333;font-size:14px">' + inline(buf.join('<br>')) + '</p>';
|
||||
}
|
||||
// conteneur neutre, sans fond : le mail adopte le fond par défaut du client
|
||||
return emailFillVars('<div style="max-width:600px;margin:0 auto;padding:8px 4px;font-family:-apple-system,Segoe UI,Roboto,Arial,sans-serif;font-size:14px;color:#333;line-height:1.6">' + html + '</div>', 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 = '<div class="hint">Aucun template.</div>'; return; }
|
||||
el.innerHTML = '<div class="table-wrap"><table><thead><tr><th>Nom</th><th>Réglages</th><th style="width:110px"></th></tr></thead><tbody>' +
|
||||
list.map((t,i) => '<tr><td class="name">'+esc(t.label)+'</td><td>'+esc(pgPresetDesc(t))+'</td>'+
|
||||
'<td style="text-align:right;white-space:nowrap"><button class="btn ghost small" onclick="openPgGenTplModal('+i+')">✎</button>'+
|
||||
'<button class="btn ghost small danger" onclick="deletePgGenTpl('+i+')">🗑</button></td></tr>').join('') +
|
||||
'</tbody></table></div>';
|
||||
}
|
||||
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(
|
||||
'<h3>'+(idx!==undefined?'Modifier le template de génération':'Nouveau template de génération')+'</h3>'+
|
||||
'<div class="field"><label>Nom</label><input id="pgt_label" value="'+esc(t.label)+'" placeholder="ex : Simple Password"></div>'+
|
||||
'<div class="field-row">'+
|
||||
'<div class="field"><label>Caractères aléatoires</label><input type="number" id="pgt_len" min="1" max="128" value="'+(parseInt(t.len)||8)+'"></div>'+
|
||||
'<div class="field"><label>Préfixe (optionnel)</label><input id="pgt_prefix" value="'+esc(t.prefix)+'" placeholder="ex : toto"></div></div>'+
|
||||
'<div class="field"><label>Classes de caractères</label>'+
|
||||
'<div style="display:flex;gap:20px;flex-wrap:wrap;margin-top:4px">'+
|
||||
'<label style="display:flex;align-items:center;gap:8px;font-size:13px;color:var(--ink-2);text-transform:none;letter-spacing:0"><input type="checkbox" id="pgt_lower"'+chk(t.lower)+'> Minuscules</label>'+
|
||||
'<label style="display:flex;align-items:center;gap:8px;font-size:13px;color:var(--ink-2);text-transform:none;letter-spacing:0"><input type="checkbox" id="pgt_upper"'+chk(t.upper)+'> Majuscules</label>'+
|
||||
'<label style="display:flex;align-items:center;gap:8px;font-size:13px;color:var(--ink-2);text-transform:none;letter-spacing:0"><input type="checkbox" id="pgt_digits"'+chk(t.digits)+'> Chiffres</label>'+
|
||||
'<label style="display:flex;align-items:center;gap:8px;font-size:13px;color:var(--ink-2);text-transform:none;letter-spacing:0"><input type="checkbox" id="pgt_symbols"'+chk(t.symbols)+'> Symboles</label></div></div>'+
|
||||
'<div class="modal-actions"><button class="btn" onclick="closeModal()">Annuler</button>'+
|
||||
'<button class="btn primary" onclick="savePgGenTpl('+(idx!==undefined?idx:-1)+')">Enregistrer</button></div>');
|
||||
}
|
||||
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 = '<div class="hint">Aucun template.</div>'; return; }
|
||||
const fmtBadge = f => f === 'html'
|
||||
? '<span class="badge" style="background:#5b9cf522;color:#5b9cf5;border:1px solid #5b9cf544">HTML</span>'
|
||||
: '<span class="badge" style="background:#7c818922;color:#7c8189;border:1px solid #7c818944">Texte</span>';
|
||||
el.innerHTML = '<div class="table-wrap"><table><thead><tr><th>Nom</th><th>Format</th><th>Objet</th><th style="width:110px"></th></tr></thead><tbody>' +
|
||||
list.map((t,i) => '<tr><td class="name">'+esc(t.label)+'</td><td>'+fmtBadge(t.format)+'</td><td>'+esc(t.subject)+'</td>'+
|
||||
'<td style="text-align:right;white-space:nowrap"><button class="btn ghost small" onclick="openPgEmailTplModal('+i+')">✎</button>'+
|
||||
'<button class="btn ghost small danger" onclick="deletePgEmailTpl('+i+')">🗑</button></td></tr>').join('') +
|
||||
'</tbody></table></div>';
|
||||
}
|
||||
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) => '<button type="button" class="btn small" onclick="pgInsertSnippet(\''+type+'\')">'+txt+'</button>';
|
||||
openModal(
|
||||
'<h3>'+(idx!==undefined?'Modifier le template email':'Nouveau template email')+'</h3>'+
|
||||
'<div class="field-row">'+
|
||||
'<div class="field" style="flex:2"><label>Nom</label><input id="pge_label" value="'+esc(t.label)+'" placeholder="ex : Compte stagiaire"></div>'+
|
||||
'<div class="field" style="max-width:150px"><label>Format</label>'+
|
||||
'<select id="pge_format" onchange="pgEmailPreview()">'+
|
||||
'<option value="text"'+(isHtml?'':' selected')+'>Texte brut</option>'+
|
||||
'<option value="html"'+(isHtml?' selected':'')+'>HTML enrichi</option>'+
|
||||
'</select></div>'+
|
||||
'</div>'+
|
||||
'<div class="field"><label>Objet</label><input id="pge_subject" value="'+esc(t.subject)+'" placeholder="Votre mot de passe temporaire" oninput="pgEmailPreview()"></div>'+
|
||||
'<div class="field"><label>Corps du message</label>'+
|
||||
'<div style="display:flex;gap:6px;flex-wrap:wrap;margin-bottom:8px">'+
|
||||
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')+
|
||||
'<button type="button" class="btn small" onclick="document.getElementById(\'pge_img\').click()">🖼 Image…</button>'+
|
||||
'<input type="file" id="pge_img" accept="image/*" style="display:none" onchange="pgImportImage(this)">'+
|
||||
'</div>'+
|
||||
'<textarea id="pge_body" style="min-height:170px;font-family:ui-monospace,Consolas,monospace;font-size:12.5px" oninput="pgEmailPreview()" placeholder="Bonjour {{ prenom }}, voici ton mot de passe temporaire : {{ tmp_password }}">'+esc(t.body)+'</textarea>'+
|
||||
'<div class="hint">Placeholders : {{ prenom }}, {{ nom }}, {{ displayName }}, {{ username }}, {{ mail }}, {{ tmp_password }}. '+
|
||||
'Syntaxe HTML : <code>:::banner #couleur … :::</code>, <code></code>, <code># titre</code>, <code>- liste</code>, <code>```code```</code>, <code>**gras**</code>, <code>[lien](url)</code>.</div></div>'+
|
||||
'<div class="field"><label>Pièces jointes</label>'+
|
||||
'<div id="pge_attach"></div>'+
|
||||
'<button type="button" class="btn small" style="margin-top:8px" onclick="document.getElementById(\'pge_file\').click()">📎 Ajouter un fichier…</button>'+
|
||||
'<input type="file" id="pge_file" style="display:none" onchange="pgImportFile(this)">'+
|
||||
'<div class="hint">Fichiers joints au mail (ex : un <code>.rdp</code> de connexion). Max 10 Mo par fichier.</div></div>'+
|
||||
'<div class="field"><label>Aperçu du mail</label>'+
|
||||
'<div id="pge_preview" style="background:#ffffff;border:1px solid var(--border);border-radius:10px;padding:12px;max-height:320px;overflow:auto"></div></div>'+
|
||||
'<div class="modal-actions"><button class="btn" onclick="closeModal()">Annuler</button>'+
|
||||
'<button class="btn primary" onclick="savePgEmailTpl('+(idx!==undefined?idx:-1)+')">Enregistrer</button></div>');
|
||||
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 = '<div class="hint" style="margin:0">Aucune pièce jointe.</div>'; return; }
|
||||
el.innerHTML = '<div style="display:flex;flex-direction:column;gap:6px">' + PG_EDIT_ATTACH.map((a,i) =>
|
||||
'<div style="display:flex;align-items:center;gap:8px;background:var(--surface-3);border:1px solid var(--border);border-radius:8px;padding:6px 10px">'+
|
||||
'<span>📎</span><span style="flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">'+esc(a.name)+'</span>'+
|
||||
'<button type="button" class="btn ghost small danger" onclick="pgRemoveAttach('+i+')" title="Retirer">✕</button></div>').join('') + '</div>';
|
||||
}
|
||||
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 = '<div style="font-family:-apple-system,Segoe UI,Arial,sans-serif;font-size:13px;color:#555;border-bottom:1px solid #e2e2e2;padding-bottom:8px;margin-bottom:8px"><b>Objet :</b> '+
|
||||
emailFillVars(emailEsc(subject), PG_SAMPLE_CTX)+'</div>';
|
||||
const rendered = fmt === 'html'
|
||||
? renderEmailHtml(body, PG_SAMPLE_CTX)
|
||||
: '<div style="font-family:Arial,sans-serif;font-size:14px;color:#333;white-space:pre-wrap">'+emailFillVars(emailEsc(body), PG_SAMPLE_CTX)+'</div>';
|
||||
const attachLine = PG_EDIT_ATTACH.length
|
||||
? '<div style="font-family:-apple-system,Segoe UI,Arial,sans-serif;font-size:12.5px;color:#555;border-top:1px solid #e2e2e2;margin-top:10px;padding-top:8px">📎 '+
|
||||
PG_EDIT_ATTACH.length+' pièce(s) jointe(s) : '+PG_EDIT_ATTACH.map(a=>emailEsc(a.name)).join(', ')+'</div>'
|
||||
: '';
|
||||
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\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) ---------------- */
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue