From 3acadf6ad7133f93cb839e86a1f452c346d4f178 Mon Sep 17 00:00:00 2001 From: bruno Date: Tue, 27 Oct 2026 23:45:10 -0400 Subject: [PATCH] Phase 2 coffre: bouton Importer l'existant (import puis purge confirmee) --- frontend/js/coffres.js | 88 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/frontend/js/coffres.js b/frontend/js/coffres.js index 8d57b89..1334d33 100644 --- a/frontend/js/coffres.js +++ b/frontend/js/coffres.js @@ -18,6 +18,11 @@ * _copieManuelle) passe par un consommateur temporaire créé puis révoqué * immédiatement, avec distribution auditée ; il n'existe pas encore * d'endpoint de distribution par session utilisateur côté backend. + * - Phase 2 (issue #20) : « Importer l'existant » migre les clés de + * data/api_keys.json vers le coffre d'instance « ObsiGate interne », + * en deux temps (import, puis purge confirmée). Aucune valeur ne + * transite jamais par cette vue : l'aperçu et les résultats ne + * contiennent que des noms et des comptes (has_value). * - La vue est rendue directement dans #content-area : elle ne passe ni * par TabManager.openNav (navigation de fichiers), ni par l'indexeur, * ni par la recherche, ni par le graphe. @@ -865,6 +870,83 @@ async function copieManuelle(s) { } } +/** Phase 2 — « Importer l'existant » (admin seulement, deux temps). + * 1. Aperçu (métadonnées seules, jamais de valeur) puis import vers le + * coffre d'instance. L'import ne supprime rien dans api_keys.json. + * 2. Purge du clair, uniquement après confirmation explicite, et + * uniquement pour les noms effectivement migrés. L'avertissement est + * affiché avant l'import : les lecteurs actuels (IA, outils) lisent + * encore api_keys.json, la purge les prive donc de la clé tant que + * leur lecteur n'est pas branché sur le coffre (voir la documentation + * de sauvegarde/restauration). */ +async function importerExistant() { + if (!isAdmin()) return; + let apercu; + try { + apercu = await api('/api/coffres/migration/api-keys'); + } catch (e) { + showToast(e.message || t('common.error'), 'error'); + return; + } + const candidats = apercu.candidats || []; + const lignes = candidats.map((c) => { + const etat = c.deja_existant + ? t('coffres.import_already_exists') + : (c.has_value ? t('coffres.import_has_value') : t('coffres.import_no_value')); + return `${c.nom} (${c.service}) — ${etat}`; + }); + const body = el('div', {}, [ + text('p', t('coffres.import_warning'), 'modal-hint'), + ...lignes.map((ligne) => text('p', ligne, '')), + ]); + const m = openModal(t('coffres.import_title'), body, [ + modalBtn(t('common.cancel'), '', () => m.close()), + modalBtn(t('coffres.import_existing'), 'primary', async () => { + let resultat; + try { + resultat = await api('/api/coffres/migration/api-keys/import', { method: 'POST', body: '{}' }); + } catch (e) { + showToast(e.message || t('common.error'), 'error'); + return; + } + m.close(); + const migres = resultat.migres || []; + const resume = [ + `${t('coffres.import_done')} — ${t('coffres.import_migrated')}: ${migres.length}, ${t('coffres.import_already_exists')}: ${(resultat.deja_existants || []).length}, ${t('coffres.import_no_value')}: ${(resultat.sans_valeur || []).length}`, + ...migres.map((nom) => `${nom} — ${t('coffres.import_migrated')}`), + ]; + const rb = el('div', {}, resume.map((ligne) => text('p', ligne, ''))); + const boutons = [modalBtn(t('common.close'), 'primary', async () => { + rm.close(); + await openCoffresView('obsigate-interne'); + })]; + if (migres.length) { + boutons.unshift(modalBtn(t('coffres.import_purge'), 'danger', async () => { + const ok = await showConfirm({ + title: t('coffres.import_purge'), + message: `${t('coffres.import_purge_confirm')} ${migres.join(', ')}`, + danger: true, + }); + if (!ok) return; + try { + const purge = await api('/api/coffres/migration/api-keys/purger', { + method: 'POST', + body: JSON.stringify({ confirmer: true, noms: migres }), + }); + showToast(`${t('coffres.import_purged')} (${(purge.supprimes || []).length})`, 'success'); + } catch (e) { + showToast(e.message || t('common.error'), 'error'); + } + rm.close(); + await openCoffresView('obsigate-interne'); + })); + } + const rm = openModal(t('coffres.import_title'), rb, boutons); + void rm; + }), + ]); +} + function openKekModal(mode) { if (!isAdmin()) return; if (mode === 'generer') { @@ -1002,6 +1084,12 @@ export async function openCoffresView(slug, options = {}) { }); toolbar.appendChild(search); toolbar.appendChild(modalBtn(t('coffres.new_secret'), 'primary', () => openCreateSecretModal(coffre.slug))); + // Phase 2 : migration api_keys.json -> coffre d'instance (admin seulement). + // Le bouton est visible quel que soit le coffre affiché : la cible est + // toujours le coffre d'instance « ObsiGate interne ». + if (isAdmin()) { + toolbar.appendChild(modalBtn(t('coffres.import_existing'), '', () => importerExistant())); + } toolbar.appendChild(modalBtn(t('coffres.dek_rotate'), '', async () => { const ok = await showConfirm({ title: t('coffres.dek_rotate'), message: t('coffres.confirm_dek_rotate'), danger: true }); if (!ok) return;