Améliorations

This commit is contained in:
ocroguennec committed 2026-09-05 17:05:54 +02:00
1 parent 8d104f2058
commit bab7a79b63
4 files changed
+147 -5

No files matched your search

+85
View File
@@ -14,6 +14,39 @@ fs.mkdirSync(path.dirname(DB_PATH), { recursive: true });
// ── Pending restore (déclenché par POST /api/admin/exports/:filename/restore) ─
// Si un fichier .pending-restore existe, on l'applique avant d'ouvrir la DB.
const PENDING_RESTORE = DB_PATH + '.pending-restore';
// Tables locales à CET environnement, à préserver à travers une restauration
// (cf. feedback_restore_preserve_env_local.md) : une restauration remplace le
// fichier DB dans son intégralité, y compris ces tables — sans ce mécanisme,
// restaurer un export prod dans dev invalide silencieusement les clés API
// générées côté dev (api_keys, y compris les clés internes auto-provisionnées
// pour l'assistant IA), écrase la configuration des fournisseurs IA propres
// à dev (ai_providers/ai_config) par celle de la source restaurée, et
// écraserait de la même façon l'étiquette d'environnement de cette instance
// (parametres_environnement — Admin > Général, cf. 05/09/26) par celle de la
// source restaurée. Ordre important : ai_config référence ai_providers (FK
// provider_defaut_id).
const ENV_LOCAL_TABLES = ['api_keys', 'ai_providers', 'ai_config', 'parametres_environnement'];
let pendingRestorePreserve = null;
if (fs.existsSync(PENDING_RESTORE) && fs.existsSync(DB_PATH)) {
try {
const oldDb = new Database(DB_PATH, { readonly: true });
const existingTables = new Set(
oldDb.prepare("SELECT name FROM sqlite_master WHERE type='table'").all().map(r => r.name)
);
pendingRestorePreserve = {};
for (const table of ENV_LOCAL_TABLES) {
if (existingTables.has(table)) {
pendingRestorePreserve[table] = oldDb.prepare(`SELECT * FROM ${table}`).all();
}
}
oldDb.close();
} catch (e) {
console.error('[restore] Échec de la sauvegarde des tables locales (api_keys/ai_providers/ai_config/parametres_environnement) avant restauration :', e.message);
pendingRestorePreserve = null;
}
}
if (fs.existsSync(PENDING_RESTORE)) {
try {
fs.copyFileSync(PENDING_RESTORE, DB_PATH);
@@ -1794,6 +1827,26 @@ db.exec(`
)
`);
// ── Migration : identification de l'environnement (Admin > Général) ─────────
// Demande Olivier 05/09/26 : chaque instance (prod / qualification / dev)
// affiche et enregistre son propre environnement, choisi manuellement par un
// admin — aucune inférence automatique possible ici (contrairement à
// appUrl/mcpUrl en dev via import.meta.env.DEV, cf. devOverrides.js : une
// instance de qualification tourne sur un build comme la prod, le mode DEV de
// Vite ne permet donc pas de la distinguer). Table dédiée (plutôt qu'une
// colonne de plus dans smtp_config) et ajoutée à ENV_LOCAL_TABLES ci-dessus :
// une restauration de base copiée depuis un autre environnement ne doit
// jamais réécrire l'étiquette de CET environnement-ci.
db.exec(`
CREATE TABLE IF NOT EXISTS parametres_environnement (
id INTEGER PRIMARY KEY CHECK (id = 1),
environnement TEXT NOT NULL DEFAULT 'developpement'
CHECK (environnement IN ('production', 'qualification', 'developpement')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
)
`);
db.exec(`INSERT OR IGNORE INTO parametres_environnement (id) VALUES (1)`);
// ── Migration : email_verified sur users ─────────────────────────────────────
{
const userCols = db.prepare('PRAGMA table_info(users)').all().map(c => c.name);
@@ -2803,6 +2856,38 @@ db.exec('CREATE INDEX IF NOT EXISTS idx_ai_usage_date ON ai_usage_log(created_at
}
}
// ── Réapplication des tables locales préservées avant une restauration ──
// (cf. feedback_restore_preserve_env_local.md). Placé ici, après toutes les
// migrations api_keys/ai_providers/ai_config ci-dessus, pour que le schéma
// de la base fraîchement restaurée soit à jour avant qu'on y réinjecte le
// contenu propre à CET environnement (sinon une colonne migrée seulement en
// aval de ce point serait silencieusement perdue lors de la réinsertion).
if (pendingRestorePreserve) {
try {
const restoreEnvLocal = db.transaction(() => {
for (const table of ENV_LOCAL_TABLES) {
const rows = pendingRestorePreserve[table];
if (!rows) continue; // table absente de l'environnement d'avant restauration
db.exec(`DELETE FROM ${table}`);
if (!rows.length) continue;
const currentCols = new Set(db.prepare(`PRAGMA table_info(${table})`).all().map(c => c.name));
// Ne garder que les colonnes existant réellement dans le schéma courant :
// un export source plus ancien ou plus récent peut avoir un jeu de
// colonnes légèrement différent.
const usableCols = Object.keys(rows[0]).filter(c => currentCols.has(c));
const placeholders = usableCols.map(() => '?').join(',');
const insert = db.prepare(`INSERT INTO ${table} (${usableCols.join(',')}) VALUES (${placeholders})`);
for (const row of rows) insert.run(...usableCols.map(c => row[c]));
}
});
restoreEnvLocal();
console.log('[restore] Tables locales (api_keys/ai_providers/ai_config/parametres_environnement) préservées et réappliquées après restauration.');
} catch (e) {
console.error('[restore] Échec de la réapplication des tables locales préservées :', e.message);
}
pendingRestorePreserve = null;
}
// Conversations de l'assistant IA — plusieurs fils nommés par couple
// user/investisseur (même granularité de scope que le reste de l'appli, cf.
// requireInvestisseur), consultables et supprimables individuellement
+10
View File
@@ -27,6 +27,9 @@ function ensureRow() {
// colonne applique automatiquement son DEFAULT (5 Go / 500 documents,
// cf. migration dans db/index.js) quand elle est omise d'un INSERT.
}
// parametres_environnement : normalement déjà créée avec sa ligne par défaut
// par la migration dans db/index.js (INSERT OR IGNORE) — filet de sécurité ici.
db.prepare('INSERT OR IGNORE INTO parametres_environnement (id) VALUES (1)').run();
}
// 1 Go = 1024^3 octets (convention déjà utilisée par fmtOctets côté frontend).
@@ -36,9 +39,11 @@ router.get('/', (_req, res, next) => {
try {
ensureRow();
const row = db.prepare('SELECT app_name, app_url, mcp_url, allow_registration, min_password_length, documents_quota_bytes, documents_quota_count FROM smtp_config WHERE id = 1').get();
const envRow = db.prepare('SELECT environnement FROM parametres_environnement WHERE id = 1').get();
res.json({
appName: row.app_name || 'Crowdlending Tracker',
appUrl: row.app_url || '',
environnement: envRow?.environnement || 'developpement',
mcpUrl: row.mcp_url || '',
allowRegistration: row.allow_registration !== 0,
minPasswordLength: row.min_password_length || 8,
@@ -51,6 +56,7 @@ router.get('/', (_req, res, next) => {
const PatchSchema = z.object({
appName: z.string().min(1).max(100).optional(),
appUrl: z.string().max(500).optional(),
environnement: z.enum(['production', 'qualification', 'developpement']).optional(),
mcpUrl: z.string().max(500).optional(),
allowRegistration: z.boolean().optional(),
minPasswordLength: z.number().int().min(6).max(64).optional(),
@@ -84,6 +90,10 @@ router.patch('/', (req, res, next) => {
body.documentsQuotaCount !== undefined ? body.documentsQuotaCount : (row.documents_quota_count || 500),
);
if (body.environnement !== undefined) {
db.prepare('UPDATE parametres_environnement SET environnement = ? WHERE id = 1').run(body.environnement);
}
res.json({ ok: true });
} catch (e) { next(e); }
});