Files
LogiFlow/server/migrations.ts
T
Claude 012024a293 feat(mails): send supplier document requests over each store's own SMTP
Replaces the mailto: link with a server-side send, so the message carries the
store's signature and logo instead of depending on each workstation's Outlook.

Store record (groups):
- address, phone and logo (data URI, 200 KB cap) feed the mail signature
- per-store SMTP settings: host, port, SSL/STARTTLS, credentials, sender
  address and display name, with an enable switch
- "test connection" button verifies the server without sending anything
- the empty-form literal, previously repeated five times, becomes one constant

Sending:
- nodemailer transport built per store from its own settings
- multipart mail: plain-text alternative plus HTML whose signature embeds the
  logo as an inline CID attachment, which Outlook renders without the remote
  image blocking that a data: URI would hit
- delivery details are HTML-escaped
- Reply-To set to the store address; the row shows a spinner and refuses a
  second click while a send is in flight

Credentials:
- the SMTP password is never returned to the client; a response-layer
  sanitizer strips it from every /api payload and replaces it with a
  smtpPasswordSet flag, covering the ten-plus queries that join full group
  rows into deliveries, orders and user relations
- an empty password field on save keeps the stored one rather than clearing it

Verified end-to-end against a local SMTP server: transport, auth, From/Reply-To,
multipart structure and the inline logo attachment.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FHdE9nEh8zHhQNGpCgrtYe
2026-08-14 15:25:00 +00:00

164 lines
5.8 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);
`
}
];
// 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;
}