Files
LogiFlow/server/migrations.ts
T
Claude 9de717387f feat(securite): encrypt SMTP passwords and NocoDB tokens at rest, log supplier mails
Encryption (server/crypto.ts):
- AES-256-GCM with a key derived from ENCRYPTION_KEY, falling back to
  SESSION_SECRET so existing deployments need no new configuration
- stored format enc:v1:<iv>:<tag>:<data>; encryption is idempotent and
  decryption passes legacy plaintext through unchanged, so nothing breaks
  mid-migration
- tampered data or a changed key raises an explicit error instead of
  returning garbage
- encrypt/decrypt is confined to the storage layer: group writes encrypt
  smtpPassword (decrypted only in emailService at connection time, never sent
  to the client), NocoDB config writes encrypt apiToken and reads decrypt it
  so the invoice verification and the admin page behave as before
- startup migration sweep encrypts secrets already stored in plaintext,
  idempotently; the active-config log line no longer prints the token

Mail history (supplier_mail_logs):
- every send attempt is recorded: delivery, store, supplier, recipient,
  subject, status sent/failed with error, message id, user id and name;
  a logging failure never fails the send itself
- GET /api/supplier-mail-logs restricted to the user's stores (admin may
  filter by store)
- on the reconciliation page the mail icon turns green once a request has
  been sent, with the date and sender in the tooltip; clicking again resends
- table created in init.sql, versioned migrations and the production
  startup migration, with delivery/group indexes

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FHdE9nEh8zHhQNGpCgrtYe
2026-08-14 16:02:55 +00:00

186 lines
6.6 KiB
TypeScript

import { sql } from 'drizzle-orm';
import { db } from './db.js';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Table pour suivre les migrations appliquées
const createMigrationsTable = async () => {
await db.execute(sql`
CREATE TABLE IF NOT EXISTS migrations (
id SERIAL PRIMARY KEY,
filename VARCHAR(255) UNIQUE NOT NULL,
executed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`);
};
interface MigrationFile {
filename: string;
sql: string;
}
export async function runMigrations() {
console.log('🔄 Vérification des migrations...');
try {
// Créer la table des migrations si elle n'existe pas
await createMigrationsTable();
// Récupérer les migrations déjà appliquées
const appliedMigrations = await db.execute(sql`SELECT filename FROM migrations`);
const appliedFilenames = new Set(appliedMigrations.rows.map((row: any) => row.filename));
// Ajouter les migrations hardcodées directement dans le code pour éviter les problèmes de chemins
const hardcodedMigrations = [
{
filename: '20260402000002_add_notified_fields_to_customer_orders.sql',
content: `
ALTER TABLE customer_orders ADD COLUMN IF NOT EXISTS notified_at TIMESTAMP;
ALTER TABLE customer_orders ADD COLUMN IF NOT EXISTS notified_comment TEXT;
`
},
{
filename: '20260402000000_add_codefou_to_suppliers.sql',
content: `ALTER TABLE suppliers ADD COLUMN IF NOT EXISTS codefou VARCHAR;`
},
{
filename: '20250903141000_create_webhook_bap_config.sql',
content: `CREATE TABLE IF NOT EXISTS webhook_bap_config (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL DEFAULT 'Configuration BAP',
webhook_url TEXT NOT NULL,
description TEXT,
is_active BOOLEAN NOT NULL DEFAULT true,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
INSERT INTO webhook_bap_config (name, webhook_url, description, is_active)
SELECT
'Configuration BAP',
'https://workflow.ffnancy.fr/webhook/a3d03176-b72f-412d-8fb9-f920b9fbab4d',
'Configuration par défaut pour envoi des fichiers BAP vers n8n',
true
WHERE NOT EXISTS (SELECT 1 FROM webhook_bap_config);`
},
{
filename: '20260814000000_add_store_contact_and_smtp_to_groups.sql',
content: `
-- Coordonnées du magasin (signature des mails fournisseurs)
ALTER TABLE groups ADD COLUMN IF NOT EXISTS address TEXT;
ALTER TABLE groups ADD COLUMN IF NOT EXISTS phone VARCHAR(50);
ALTER TABLE groups ADD COLUMN IF NOT EXISTS logo TEXT;
-- Configuration SMTP propre à chaque magasin
ALTER TABLE groups ADD COLUMN IF NOT EXISTS smtp_enabled BOOLEAN DEFAULT false;
ALTER TABLE groups ADD COLUMN IF NOT EXISTS smtp_host VARCHAR(255);
ALTER TABLE groups ADD COLUMN IF NOT EXISTS smtp_port INTEGER;
ALTER TABLE groups ADD COLUMN IF NOT EXISTS smtp_secure BOOLEAN DEFAULT false;
ALTER TABLE groups ADD COLUMN IF NOT EXISTS smtp_user VARCHAR(255);
ALTER TABLE groups ADD COLUMN IF NOT EXISTS smtp_password VARCHAR(255);
ALTER TABLE groups ADD COLUMN IF NOT EXISTS smtp_sender_email VARCHAR(255);
ALTER TABLE groups ADD COLUMN IF NOT EXISTS smtp_sender_name VARCHAR(255);
`
},
{
filename: '20260814000001_create_supplier_mail_logs.sql',
content: `
CREATE TABLE IF NOT EXISTS supplier_mail_logs (
id SERIAL PRIMARY KEY,
delivery_id INTEGER NOT NULL,
group_id INTEGER NOT NULL,
supplier_id INTEGER,
supplier_name VARCHAR(255),
sent_to VARCHAR(255) NOT NULL,
subject TEXT,
status VARCHAR(20) NOT NULL,
error_message TEXT,
message_id VARCHAR(255),
sent_by VARCHAR NOT NULL,
sent_by_name VARCHAR(255),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_supplier_mail_logs_delivery ON supplier_mail_logs(delivery_id);
CREATE INDEX IF NOT EXISTS idx_supplier_mail_logs_group ON supplier_mail_logs(group_id);
`
}
];
// Ignorer les fichiers de migrations pour éviter les erreurs SQL
// Utiliser uniquement les migrations hardcodées
const allMigrations = hardcodedMigrations;
if (allMigrations.length === 0) {
console.log('✅ Aucune migration à exécuter');
return;
}
let executedCount = 0;
for (const migration of allMigrations) {
if (appliedFilenames.has(migration.filename)) {
console.log(`⏭️ Migration déjà appliquée: ${migration.filename}`);
continue;
}
console.log(`🔄 Exécution migration: ${migration.filename}`);
try {
// Exécuter la migration dans une transaction
await db.transaction(async (tx: any) => {
// Exécuter le SQL de migration
await tx.execute(sql.raw(migration.content));
// Marquer comme appliquée
await tx.execute(sql`
INSERT INTO migrations (filename)
VALUES (${migration.filename})
`);
});
console.log(`✅ Migration appliquée avec succès: ${migration.filename}`);
executedCount++;
} catch (error) {
console.error(`❌ Erreur lors de la migration ${migration.filename}:`, error);
throw error;
}
}
if (executedCount > 0) {
console.log(`✅ ${executedCount} migration(s) appliquée(s) avec succès`);
} else {
console.log('✅ Toutes les migrations sont déjà appliquées');
}
} catch (error) {
console.error('❌ Erreur lors de l\'exécution des migrations:', error);
throw error;
}
}
// Fonction pour créer une nouvelle migration
export function createMigration(name: string): string {
const timestamp = new Date().toISOString().replace(/[^\d]/g, '').slice(0, 14);
const filename = `${timestamp}_${name.toLowerCase().replace(/\s+/g, '_')}.sql`;
const migrationsDir = path.join(__dirname, '..', 'migrations');
if (!fs.existsSync(migrationsDir)) {
fs.mkdirSync(migrationsDir, { recursive: true });
}
const filePath = path.join(migrationsDir, filename);
const template = `-- Migration: ${name}
-- Created: ${new Date().toISOString()}
-- Add your SQL here
`;
fs.writeFileSync(filePath, template);
console.log(`📝 Migration créée: ${filePath}`);
return filePath;
}