/* ============================================================================
CléPass v3 — popup « Modifier » (portage de CléPass Éditeur.dc.html).
Composant importé par l'app principale (dc-import → ).
Elle édite la FICHE RÉELLEMENT SÉLECTIONNÉE : les blocs et les champs sont
remplis depuis `item` (la fiche déchiffrée), et « Enregistrer » renvoie la
saisie à l'application via onSave() — qui la range dans le coffre. Les
valeurs de démonstration d'origine (« Gd7$k2!pQ9xL4m », « DCAF-5G »,
« MAIRIE JULLIÉ »…) ont disparu : elles masquaient le contenu du coffre et
pouvaient être enregistrées par erreur.
Props :
item fiche à éditer : { name, sub, urls, blocks, user, password, … }
— même structure que l'éditeur intégré (app.jsx / mkBlock)
onSave (données) → true si l'enregistrement a été accepté
onClose fermeture sans enregistrer
customCats types personnalisés proposés dans « Ajouter un élément »
============================================================================ */
(function () {
const { sx, Hv, DCLogic } = window.CP;
class CPEditeur extends DCLogic {
state = {
order: null,
data: null,
open: {},
reveal: {}, // mots de passe affichés en clair, par bloc
seq: 1,
addOpen: false,
dragId: null,
overId: null,
info: null,
errNom: false,
};
/* Tic-tac TOTP : re-rend la popup chaque seconde tant qu'un bloc 2FA a une
clé valide, pour faire tourner le code (fenêtre de 30 s). */
componentDidMount() {
this._otpTick = setInterval(() => {
const d = this.state.data || {};
for (const k in d) if (d[k].type === 'otp' && window.CP.totp(d[k].secret || '')) { this.setState({ otpNow: Date.now() }); return; }
}, 1000);
}
componentWillUnmount() { if (this._otpTick) clearInterval(this._otpTick); }
/* Types de blocs : ceux que le coffre sait réellement stocker (mêmes clés
que app.jsx / mkBlock). L'ancien type « QR code » a été retiré : il n'avait
aucun équivalent dans la fiche, sa saisie n'aurait donc pas pu être
enregistrée. Le QR du Wi-Fi, lui, est bien généré depuis le SSID et la clé
(voir la fiche → section Wi-Fi). */
meta() {
return {
pw: { bg: '#fdeede', fg: '#b87e2e', label: 'Mot de passe simple', desc: 'Identifiant + mot de passe' },
otp: { bg: '#def3ec', fg: '#2e8f72', label: 'Code 2FA', desc: 'Authenticator (TOTP)' },
wifi: { bg: '#dfeafc', fg: '#3a6fb8', label: 'Réseau Wi-Fi', desc: 'SSID + clé' },
card: { bg: '#fbe2ea', fg: '#bb4f78', label: 'Carte bancaire', desc: 'Numéro + expiration' },
notes: { bg: '#efe7fc', fg: '#6b4fb8', label: 'Note sécurisée', desc: 'Texte libre chiffré' },
};
}
blankFields(type) {
if (type === 'pw') return { user: '', password: '' };
if (type === 'otp') return { issuer: '', secret: '' };
if (type === 'wifi') return { ssid: '', wifiPass: '', security: 'WPA2' };
if (type === 'card') return { number: '', exp: '', cvv: '' };
if (type === 'notes') return { text: '' };
return {};
}
/* Champs d'un bloc de la fiche → champs de la popup. */
champsDepuisBloc(b) {
if (b.type === 'pw' || b.type === 'o365') return { user: b.user || '', password: b.password || '' };
if (b.type === 'otp') return { issuer: b.issuer || '', secret: b.secret || '' };
if (b.type === 'wifi') return { ssid: b.ssid || '', wifiPass: b.wifiPass || '', security: b.security || 'WPA2' };
if (b.type === 'card') return { number: b.number || '', exp: b.exp || '', cvv: b.cvv || '' };
if (b.type === 'notes') return { text: b.text || '' };
return {};
}
genPassword() {
const cs = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz23456789!@#$%&*?';
let s = '';
for (let i = 0; i < 16; i++) s += cs[Math.floor(Math.random() * cs.length)];
return s;
}
/* Jauge de force du mot de passe saisi (l'ancienne barre était figée sur
« FORT » quel que soit le contenu). */
forceMdp(mdp) {
const p = String(mdp || '');
if (!p) return { pct: 0, label: '', color: '#e2d8e6' };
let n = 0;
if (p.length >= 8) n++;
if (p.length >= 12) n++;
if (p.length >= 16) n++;
if (/[a-z]/.test(p) && /[A-Z]/.test(p)) n++;
if (/\d/.test(p)) n++;
if (/[^A-Za-z0-9]/.test(p)) n++;
if (n <= 2) return { pct: 30, label: 'FAIBLE', color: '#c0392b' };
if (n <= 4) return { pct: 65, label: 'MOYEN', color: '#d98a2b' };
return { pct: 100, label: 'FORT', color: '#2e8f72' };
}
iconEl(type) {
const svg = (kids) => React.createElement('svg', { viewBox: '0 0 24 24', width: 16, height: 16, fill: 'none', stroke: 'currentColor', strokeWidth: 1.9 }, kids);
if (type === 'otp') return svg([React.createElement('circle', { key: 1, cx: 12, cy: 12, r: 8 }), React.createElement('path', { key: 2, d: 'M12 8v4l3 2' })]);
if (type === 'wifi') return svg([React.createElement('path', { key: 1, d: 'M4.5 11a11 11 0 0 1 15 0M7.5 14a7 7 0 0 1 9 0' }), React.createElement('circle', { key: 2, cx: 12, cy: 17.5, r: 1, fill: 'currentColor', stroke: 'none' })]);
if (type === 'notes') return svg([React.createElement('path', { key: 1, d: 'M7 3h7l4 4v12a1.5 1.5 0 0 1-1.5 1.5h-9.5A1.5 1.5 0 0 1 5.5 19V4.5A1.5 1.5 0 0 1 7 3z' }), React.createElement('path', { key: 2, d: 'M14 3v4h4' })]);
if (type === 'card') return svg([React.createElement('rect', { key: 1, x: 3, y: 6, width: 18, height: 12, rx: 2 }), React.createElement('path', { key: 2, d: 'M3 10h18' })]);
return svg([React.createElement('circle', { key: 1, cx: 7, cy: 16, r: 3 }), React.createElement('path', { key: 2, d: 'M9.3 13.7l8-8M15 5l3 3' })]);
}
reorder(from, to) {
if (!from || from === to) return;
this.setState((s) => {
const fromIdx = s.order.indexOf(from);
const toIdx = s.order.indexOf(to);
if (fromIdx < 0 || toIdx < 0) return {};
const order = s.order.slice();
order.splice(fromIdx, 1);
const nt = order.indexOf(to);
const at = fromIdx < toIdx ? nt + 1 : nt;
order.splice(at, 0, from);
return { order };
});
}
addType(type) {
this.setState((s) => {
const id = 'e' + s.seq;
const m = this.meta()[type];
return {
order: s.order.concat(id),
data: Object.assign({}, s.data, { [id]: Object.assign({ type, name: m.label }, this.blankFields(type)) }),
open: Object.assign({}, s.open, { [id]: true }),
seq: s.seq + 1,
addOpen: false,
};
});
}
/* Type personnalisé (Office 365…) : bloc identifiants portant le nom et
les couleurs du type. */
addCustomType(c) {
this.setState((s) => {
const id = 'e' + s.seq;
return {
order: s.order.concat(id),
data: Object.assign({}, s.data, { [id]: Object.assign({ type: 'pw', name: c.label, bg: c.bg, fg: c.fg }, this.blankFields('pw')) }),
open: Object.assign({}, s.open, { [id]: true }),
seq: s.seq + 1,
addOpen: false,
};
});
}
/* Blocs de la fiche réelle. Si elle n'a pas encore de blocs enregistrés
(fiche jamais éditée), ils sont déduits de ses drapeaux de type — même
règle que l'éditeur intégré, pour que les deux voies montrent la même
chose. Les blocs de type personnalisé (cat:…) sont ignorés ici : la popup
ne sait pas les éditer, l'application les conserve à l'enregistrement. */
blocsDeLaFiche() {
const it = this.props.item || {};
if (it.blocks && it.blocks.length) return it.blocks.filter((b) => String(b.type).indexOf('cat:') !== 0);
const t = [];
if (it.o365) t.push('o365'); else if (it.pw) t.push('pw');
if (it.twofa) t.push('otp');
if (it.wifi) t.push('wifi');
if (it.card) t.push('card');
if (it.notes) t.push('notes');
if (!t.length) t.push('pw');
return t.map((type) => {
if (type === 'pw' || type === 'o365') return { type: type, user: (it.user && it.user !== '—') ? it.user : '', password: it.password || '' };
if (type === 'otp') return { type: 'otp', issuer: it.name || '', secret: it.secret || '' };
if (type === 'wifi') return { type: 'wifi', ssid: it.ssid || '', wifiPass: it.wifiPass || '', security: it.security || 'WPA2' };
if (type === 'card') return { type: 'card', number: it.cardNumber || '', exp: it.cardExp || '', cvv: it.cardCvv || '' };
return { type: 'notes', text: it.noteText || '' };
});
}
buildFromItem() {
const M = this.meta();
const it = this.props.item || {};
const order = []; const data = {}; const open = {};
this.blocsDeLaFiche().forEach((b, i) => {
const id = 'p' + i;
/* Un bloc « o365 » est un bloc identifiants portant le nom du type. */
const tt = b.type === 'o365' ? 'pw' : (M[b.type] ? b.type : 'pw');
const nom = b.type === 'o365' ? 'Office 365' : M[tt].label;
data[id] = Object.assign({ type: tt, name: nom, bg: '', fg: '' }, this.champsDepuisBloc(b));
order.push(id); open[id] = (i === 0);
});
if (!order.length) { order.push('p0'); data.p0 = Object.assign({ type: 'pw', name: M.pw.label }, this.blankFields('pw')); open.p0 = true; }
const sub = (it.sub && it.sub !== '—' && it.sub !== 'Aucun domaine') ? it.sub : '';
return {
order, data, open, seq: order.length + 1,
info: { name: it.name || '', sub: sub },
urls: (it.urls || []).slice(),
};
}
/* « Enregistrer » : renvoie la saisie à l'application. Tant que le nom est
vide, la popup reste ouverte avec le champ en erreur — enregistrer une
fiche sans nom la rendrait introuvable dans la liste. */
enregistrer() {
const st = this.state;
const info = st.info || {};
if (!(info.name || '').trim()) { this.setState({ errNom: true }); return; }
const elements = st.order.map((id) => Object.assign({}, st.data[id]));
const ok = this.props.onSave
&& this.props.onSave({ info: { name: info.name, sub: info.sub || '' }, urls: (st.urls || []).slice(), elements: elements });
if (ok === false) { this.setState({ errNom: true }); return; } // refus côté application
if (this.props.onClose) this.props.onClose();
}
setField(id, key, val) { this.setState((s) => ({ data: Object.assign({}, s.data, { [id]: Object.assign({}, s.data[id], { [key]: val }) }) })); }
setInfo(key, val) { this.setState((s) => ({ info: Object.assign({}, s.info, { [key]: val }) })); }
renderVals() {
const st = this.state;
if (!this._init) {
this._init = true;
const b = this.buildFromItem();
st.order = b.order; st.data = b.data; st.open = b.open; st.seq = b.seq;
st.info = b.info; st.urls = b.urls; st.urlDraft = '';
}
const info = st.info || { name: '', sub: '' };
const M = this.meta();
const elements = st.order.map((id) => {
const e = st.data[id] || { type: 'pw', name: '' };
const open = !!st.open[id];
const c = M[e.type] || M.pw;
const tbg = e.bg || c.bg; const tfg = e.fg || c.fg;
const dragging = st.dragId === id;
const anyDrag = !!st.dragId;
const bodyOpen = open && !anyDrag;
const myIdx = st.order.indexOf(id);
const dIdx = st.dragId ? st.order.indexOf(st.dragId) : -1;
const over = (!!st.dragId && st.dragId !== id && st.overId === id);
/* Code 2FA « tournant » (TOTP 30 s) calculé depuis la clé du bloc. */
const lt = e.type === 'otp' ? window.CP.totp(e.secret || '') : null;
const ltPct = lt ? Math.round((lt.remaining / lt.period) * 1000) / 10 : 0;
return {
id, name: e.name, open, bodyOpen, drag: true, showBefore: (over && dIdx > myIdx), showAfter: (over && dIdx < myIdx),
isPw: e.type === 'pw', isOtp: e.type === 'otp', isWifi: e.type === 'wifi', isCard: e.type === 'card', isNotes: e.type === 'notes',
user: e.user || '', password: e.password || '', showPw: !!st.reveal[id],
pwType: st.reveal[id] ? 'text' : 'password',
pwEyeStyle: 'width:42px;border-radius:11px;background:#f7f3fb;border:1.5px solid #ece5f0;display:grid;place-items:center;flex:none;cursor:pointer;color:' + (st.reveal[id] ? '#6b4fb8' : '#8a7f90'),
force: this.forceMdp(e.password || ''),
issuer: e.issuer || '', secret: e.secret || '', text: e.text || '',
otpLiveCode: lt ? (lt.code.length === 6 ? lt.code.slice(0, 3) + ' ' + lt.code.slice(3) : lt.code) : '— — —',
otpRemaining: lt ? String(lt.remaining) : '',
otpRingStyle: `width:34px;height:34px;border-radius:50%;background:conic-gradient(#6b4fb8 0 ${ltPct}%,#e8e0ee ${ltPct}% 100%);display:grid;place-items:center`,
otpHoleStyle: `width:26px;height:26px;border-radius:50%;background:#fff;display:grid;place-items:center;font:600 10px 'IBM Plex Mono';color:#6b4fb8`,
ssid: e.ssid || '', wifiPass: e.wifiPass || '', security: e.security || '',
cardNumber: e.number || '', cardExp: e.exp || '', cardCvv: e.cvv || '',
onUser: (ev) => this.setField(id, 'user', ev.target.value),
onPassword: (ev) => this.setField(id, 'password', ev.target.value),
onGen: (ev) => { if (ev && ev.stopPropagation) ev.stopPropagation(); this.setField(id, 'password', this.genPassword()); },
onReveal: (ev) => { if (ev && ev.stopPropagation) ev.stopPropagation(); this.setState((s) => ({ reveal: Object.assign({}, s.reveal, { [id]: !s.reveal[id] }) })); },
onIssuer: (ev) => this.setField(id, 'issuer', ev.target.value),
onSecret: (ev) => this.setField(id, 'secret', ev.target.value),
onText: (ev) => this.setField(id, 'text', ev.target.value),
onSsid: (ev) => this.setField(id, 'ssid', ev.target.value),
onWifiPass: (ev) => this.setField(id, 'wifiPass', ev.target.value),
onSecurity: (ev) => this.setField(id, 'security', ev.target.value),
onCardNumber: (ev) => this.setField(id, 'number', ev.target.value),
onCardExp: (ev) => this.setField(id, 'exp', ev.target.value),
onCardCvv: (ev) => this.setField(id, 'cvv', ev.target.value),
cardStyle: `background:#faf7fd;border:1px solid #f0eaf2;border-radius:15px;padding:14px;display:flex;flex-direction:column;gap:11px;transition:opacity .15s,box-shadow .15s;${dragging ? 'opacity:.5;box-shadow:0 12px 30px -12px rgba(80,50,110,.4);' : ''}`,
tileStyle: `width:30px;height:30px;border-radius:8px;background:${tbg};color:${tfg};display:grid;place-items:center;flex:none`,
chevStyle: `display:inline-flex;align-items:center;justify-content:center;width:24px;height:24px;border-radius:7px;transition:transform .2s;transform:rotate(${bodyOpen ? 0 : -90}deg);color:#a98bde;flex:none`,
onToggle: () => this.setState((s) => ({ open: Object.assign({}, s.open, { [id]: !s.open[id] }) })),
onRemove: (e2) => { if (e2 && e2.stopPropagation) e2.stopPropagation(); this.setState((s) => { const order = s.order.filter((x) => x !== id); const data = Object.assign({}, s.data); delete data[id]; return { order, data }; }); },
onDragStart: (e2) => { if (e2 && e2.dataTransfer) { e2.dataTransfer.effectAllowed = 'move'; try { e2.dataTransfer.setData('text/plain', id); } catch (x) {} } setTimeout(() => this.setState({ dragId: id }), 0); },
onDragEnd: () => this.setState({ dragId: null, overId: null }),
onDragOver: (e2) => { if (e2 && e2.preventDefault) e2.preventDefault(); if (this.state.dragId && this.state.dragId !== id && this.state.overId !== id) this.setState({ overId: id }); },
onDrop: (e2) => { if (e2 && e2.preventDefault) e2.preventDefault(); this.reorder(this.state.dragId, id); this.setState({ dragId: null, overId: null }); },
};
});
const n = st.order.length;
const addOptions = ['pw', 'otp', 'wifi', 'card', 'notes'].map((t) => {
const m = M[t];
return { label: m.label, desc: m.desc, icon: this.iconEl(t), tileStyle: `width:32px;height:32px;border-radius:9px;background:${m.bg};color:${m.fg};display:grid;place-items:center;flex:none`, onClick: () => this.addType(t) };
});
/* Types personnalisés fournis par l'app (customCats ← st.catList). */
const addCustomOptions = (this.props.customCats || []).map((c) => ({
label: c.label, desc: 'Fiche · type personnalisé', icon: c.icon || this.iconEl('pw'),
tileStyle: `width:32px;height:32px;border-radius:9px;background:${c.bg};color:${c.fg};display:grid;place-items:center;font-size:15px;flex:none`,
onClick: () => this.addCustomType(c),
}));
const urlChips = (st.urls || []).map((u, i) => ({
label: u,
style: 'display:inline-flex;align-items:center;gap:5px;font-size:11px;font-weight:600;padding:4px 9px;border-radius:999px;background:#dfeafc;color:#3a6fb8;cursor:grab;' + ((st.urlOver === i && st.urlDrag !== null && st.urlDrag !== undefined && st.urlDrag !== i) ? 'box-shadow:inset 0 0 0 2px #7c5cff;' : '') + (st.urlDrag === i ? 'opacity:.45;' : ''),
onDragStart: (ev) => { if (ev && ev.dataTransfer) { ev.dataTransfer.effectAllowed = 'move'; } this.setState({ urlDrag: i }); },
onDragEnd: () => this.setState({ urlDrag: null, urlOver: null }),
onOver: (ev) => { if (ev && ev.preventDefault) ev.preventDefault(); if (this.state.urlDrag !== null && this.state.urlDrag !== undefined && this.state.urlDrag !== i && this.state.urlOver !== i) this.setState({ urlOver: i }); },
onDrop: (ev) => { if (ev && ev.preventDefault) ev.preventDefault(); const from = this.state.urlDrag; if (from !== null && from !== undefined && from !== i) this.setState((s) => { const a = (s.urls || []).slice(); const m = a.splice(from, 1)[0]; a.splice(i, 0, m); return { urls: a, urlDrag: null, urlOver: null }; }); else this.setState({ urlDrag: null, urlOver: null }); },
onRemove: (ev) => { if (ev && ev.stopPropagation) ev.stopPropagation(); this.setState((s) => ({ urls: (s.urls || []).filter((x, j) => j !== i) })); },
}));
return {
elements,
countLabel: n + (n > 1 ? ' éléments' : ' élément') + ' · glissez l’en-tête pour réordonner',
addMenuOpen: st.addOpen,
addCustomOptions,
addBtnStyle: `display:flex;align-items:center;justify-content:center;gap:9px;border:1.5px dashed ${st.addOpen ? '#a98bde' : '#d9cde6'};border-radius:13px;padding:13px;color:#6b4fb8;font-size:13px;font-weight:600;cursor:pointer;background:${st.addOpen ? '#f7f3fb' : 'transparent'}`,
addOptions,
onAddToggle: () => this.setState((s) => ({ addOpen: !s.addOpen })),
onAddClose: () => this.setState({ addOpen: false }),
onEditorClose: () => { if (this.props.onClose) this.props.onClose(); },
onSave: () => this.enregistrer(),
infoName: info.name, infoSub: info.sub || '',
/* Avatar de la fiche réelle (initiale + couleur), à la place du « g »
lavande figé du design d'origine. */
avLettre: ((this.props.item || {}).ini || info.name || '?').charAt(0).toUpperCase() || '?',
avStyle: "width:68px;height:68px;border-radius:18px;background:" + (((this.props.item || {}).avBg) || '#a98bde') + ";display:grid;place-items:center;font:800 28px 'Sora';color:" + (((this.props.item || {}).avFg) || '#ffffff'),
nameStyle: 'width:100%;box-sizing:border-box;background:#fff;border:1.5px solid ' + (st.errNom ? '#c0392b' : '#6b4fb8') + ';border-radius:10px;padding:9px 11px;font-size:12.5px;color:#3a3340;font-family:inherit;outline:none',
errNom: !!st.errNom,
urlChips, urlDraft: st.urlDraft || '',
onUrlDraft: (ev) => this.setState({ urlDraft: ev.target.value }),
onUrlKey: (ev) => { if (ev.key === 'Enter') { if (ev.preventDefault) ev.preventDefault(); const v = (this.state.urlDraft || '').trim(); if (v) this.setState((s) => ({ urls: (s.urls || []).concat(v), urlDraft: '' })); } },
onInfoName: (ev) => { this.setInfo('name', ev.target.value); if (st.errNom) this.setState({ errNom: false }); },
onInfoSub: (ev) => this.setInfo('sub', ev.target.value),
onMenuStop: (e2) => { if (e2 && e2.stopPropagation) e2.stopPropagation(); },
showEnd: (!!st.dragId && st.overId === '__end__'),
onEndOver: (e2) => { if (e2 && e2.preventDefault) e2.preventDefault(); if (this.state.dragId && this.state.overId !== '__end__') this.setState({ overId: '__end__' }); },
onEndDrop: (e2) => { if (e2 && e2.preventDefault) e2.preventDefault(); const from = this.state.dragId; if (from) this.setState((s) => { const order = s.order.filter((x) => x !== from); order.push(from); return { order, dragId: null, overId: null }; }); else this.setState({ dragId: null, overId: null }); },
};
}
render() {
const v = this.renderVals();
const F = React.Fragment;
return (
INFOS
NOM
{v.errNom &&
Le nom est obligatoire.
}
SITES WEB
{v.urlChips.map((u, i) => (
{u.label}✕))}
Compte complet
{v.countLabel}
✕
{v.elements.map((el) => (
{el.showBefore &&
}
{el.isPw && }
{el.isOtp && }
{el.isWifi && }
{el.isNotes && }
{el.isCard && }
{el.name}
✕
{el.bodyOpen &&
{el.isPw &&
}
{el.isOtp &&
}
{el.isWifi &&
}
{el.isNotes &&
}
{el.isCard &&
}
}
{el.showAfter &&
}
))}
{v.showEnd &&
}
+ Ajouter un élément
{v.addMenuOpen &&
CHOISIR UN TYPE
{v.addOptions.map((opt, i) => (
{opt.icon}
))}
{v.addCustomOptions.length > 0 &&
CHOISIR UN TYPE PERSONNALISÉ
{v.addCustomOptions.map((opt, i) => (
{opt.icon}
))}
}
}
);
}
}
window.CPEditeur = CPEditeur;
})();