/* ============================================================================ 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
{v.avLettre}
NOM
{v.errNom &&
Le nom est obligatoire.
}
SOUS-TITRE
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 &&
IDENTIFIANT
MOT DE PASSE
{el.force.label}
} {el.isOtp &&
ÉMETTEUR
CLÉ / OTPAUTH
{el.otpRemaining}
{el.otpLiveCode}
} {el.isWifi &&
SSID
MOT DE PASSE
SÉCURITÉ
} {el.isNotes &&
NOTE
} {el.isCard &&
NUMÉRO DE CARTE
EXPIRATION
CVV
}
}
{el.showAfter &&
}
))}
{v.showEnd &&
}
+ Ajouter un élément
Annuler
Enregistrer
{v.addMenuOpen &&
CHOISIR UN TYPE
{v.addOptions.map((opt, i) => ( {opt.icon}
{opt.label}
{opt.desc}
))} {v.addCustomOptions.length > 0 &&
CHOISIR UN TYPE PERSONNALISÉ
{v.addCustomOptions.map((opt, i) => ( {opt.icon}
{opt.label}
{opt.desc}
))}
}
}
); } } window.CPEditeur = CPEditeur; })();