mirror of
https://github.com/R0m1k3/LogiFlow.git
synced 2026-10-11 17:27:31 +02:00
Nouvel onglet Paramètres > API externe (admin) : état de l'API, adresse, création d'une clé nommée par outil (affichée une seule fois), liste avec dernière utilisation, révocation immédiate. - table external_api_keys (empreinte SHA-256 uniquement), créée par migrations.production.ts et init.sql - l'API accepte les clés de Paramètres et toujours celles de EXTERNAL_API_KEYS ; 503 seulement si aucune clé active - routes /api/external-api/keys (session + admin) - documentation et .env.example mis à jour Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JrRFddV2BaqDCxGY1j52UJ
201 lines
7.1 KiB
TypeScript
201 lines
7.1 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);
|
|
`
|
|
},
|
|
{
|
|
filename: '20261010000000_create_external_api_keys.sql',
|
|
content: `
|
|
CREATE TABLE IF NOT EXISTS external_api_keys (
|
|
id SERIAL PRIMARY KEY,
|
|
name VARCHAR(100) NOT NULL,
|
|
key_prefix VARCHAR(16) NOT NULL,
|
|
key_hash VARCHAR(64) NOT NULL UNIQUE,
|
|
created_by VARCHAR,
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
last_used_at TIMESTAMP,
|
|
revoked_at TIMESTAMP
|
|
);
|
|
`
|
|
}
|
|
];
|
|
|
|
// 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;
|
|
} |