Phase 2 coffre: bouton Importer l'existant (import puis purge confirmee)

This commit is contained in:
2026-10-27 23:45:10 -04:00
parent 5492f99f06
commit 3acadf6ad7
+88
View File
@@ -18,6 +18,11 @@
* _copieManuelle) passe par un consommateur temporaire créé puis révoqué * _copieManuelle) passe par un consommateur temporaire créé puis révoqué
* immédiatement, avec distribution auditée ; il n'existe pas encore * immédiatement, avec distribution auditée ; il n'existe pas encore
* d'endpoint de distribution par session utilisateur côté backend. * 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 * - La vue est rendue directement dans #content-area : elle ne passe ni
* par TabManager.openNav (navigation de fichiers), ni par l'indexeur, * par TabManager.openNav (navigation de fichiers), ni par l'indexeur,
* ni par la recherche, ni par le graphe. * 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) { function openKekModal(mode) {
if (!isAdmin()) return; if (!isAdmin()) return;
if (mode === 'generer') { if (mode === 'generer') {
@@ -1002,6 +1084,12 @@ export async function openCoffresView(slug, options = {}) {
}); });
toolbar.appendChild(search); toolbar.appendChild(search);
toolbar.appendChild(modalBtn(t('coffres.new_secret'), 'primary', () => openCreateSecretModal(coffre.slug))); 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 () => { 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 }); const ok = await showConfirm({ title: t('coffres.dek_rotate'), message: t('coffres.confirm_dek_rotate'), danger: true });
if (!ok) return; if (!ok) return;