]> git.digitality.be Git - pdw25-26/commitdiff
Alertes_Crud_socketIO_ok !
authorSteph Ponzo <ponzo.stephane2@gmail.com>
Mon, 23 Feb 2026 17:04:20 +0000 (18:04 +0100)
committerSteph Ponzo <ponzo.stephane2@gmail.com>
Mon, 23 Feb 2026 17:04:20 +0000 (18:04 +0100)
13 files changed:
server/modules/alerts/adapters/mysql.adapter.js
server/modules/alerts/alerts.controller.js
server/modules/alerts/alerts.repo.js
server/modules/alerts/alerts.router.js
server/modules/alerts/alerts.service.js
server/modules/alerts/index.js
server/modules/alerts/init-alerts.js [deleted file]
server/modules/alerts/socketManager.js
server/modules/init-alerts.js
server/package-lock.json
server/package.json
server/test-module-complet.js [new file with mode: 0644]
server/test-server.js [new file with mode: 0644]

index a6a1e148d8318463060eaf10d0dbbaaded44ccf0..7236df1b99ada81a7ff5f05fa7fbaaab6bc6a848 100644 (file)
@@ -171,10 +171,18 @@ export function createMySQLAdapter(dbConnection, options = {}) {
         },
 
         // =====================================================
-        // 5. CRÉER UNE NOUVELLE RÈGLE D'ALERTE
+        // 5. CRÉER UNE RÈGLE D'ALERTE (CRUD - Create)
         // =====================================================
-        async createAlertRule(ruleData) {
+        /**
+         * Crée une nouvelle règle d'alerte
+         * @param {Object} ruleData - Données de la règle
+         * @returns {Promise<Object>} Règle créée avec son ID
+         */
+        async createRule(ruleData) {
             try {
+                const ruleId = ruleData.ruleId || uuidv4();
+                const now = Date.now();
+
                 const sql = `
                     INSERT INTO ${tables.alerts} (
                         rule_id,
@@ -193,28 +201,141 @@ export function createMySQLAdapter(dbConnection, options = {}) {
                 `;
 
                 const values = [
-                    ruleData.rule_id || uuidv4(),
-                    ruleData.user_id,
-                    ruleData.pair_id || null,
+                    ruleId,
+                    ruleData.userId,
+                    ruleData.pairId || null,
                     ruleData.enabled !== undefined ? ruleData.enabled : 1,
-                    ruleData.rule_type,
+                    ruleData.ruleType || 'SIGNAL_THRESHOLD',
                     ruleData.severity || 'INFO',
-                    ruleData.min_confidence || 0.75,
-                    ruleData.channel || 'EMAIL',
+                    ruleData.minConfidence || 0.7,
+                    ruleData.channel || 'CONSOLE',
                     JSON.stringify(ruleData.params || {}),
-                    ruleData.cooldown_ms || 60000,
-                    Date.now(),
-                    Date.now()
+                    ruleData.cooldownMs || 60000,
+                    now,
+                    now
                 ];
 
                 await dbConnection.execute(sql, values);
-                console.log(`[MySQL] Règle d'alerte créée : ${ruleData.rule_type}`);
+                console.log(`[MySQL] Règle créée : ${ruleId}`);
+
+                return { ruleId, ...ruleData, createdAt: now };
+
+            } catch (error) {
+                console.error('[MySQL] Erreur dans createRule:', error);
+                throw error;
+            }
+        },
+
+        // =====================================================
+        // 6. MODIFIER UNE RÈGLE D'ALERTE (CRUD - Update)
+        // =====================================================
+        /**
+         * Met à jour une règle existante
+         * @param {string} ruleId - ID de la règle
+         * @param {Object} updates - Champs à mettre à jour
+         * @returns {Promise<boolean>} true si modifié
+         */
+        async updateRule(ruleId, updates) {
+            try {
+                // Construire la requête dynamiquement
+                const fields = [];
+                const values = [];
+
+                if (updates.minConfidence !== undefined) {
+                    fields.push('min_confidence = ?');
+                    values.push(updates.minConfidence);
+                }
+                if (updates.enabled !== undefined) {
+                    fields.push('enabled = ?');
+                    values.push(updates.enabled);
+                }
+                if (updates.channel !== undefined) {
+                    fields.push('channel = ?');
+                    values.push(updates.channel);
+                }
+                if (updates.severity !== undefined) {
+                    fields.push('severity = ?');
+                    values.push(updates.severity);
+                }
+                if (updates.cooldownMs !== undefined) {
+                    fields.push('cooldown_ms = ?');
+                    values.push(updates.cooldownMs);
+                }
+                if (updates.pairId !== undefined) {
+                    fields.push('pair_id = ?');
+                    values.push(updates.pairId);
+                }
+
+                // Toujours mettre à jour le timestamp
+                fields.push('updated_at_ms = ?');
+                values.push(Date.now());
+
+                // Ajouter le ruleId à la fin pour le WHERE
+                values.push(ruleId);
+
+                const sql = `
+                    UPDATE ${tables.alerts}
+                    SET ${fields.join(', ')}
+                    WHERE rule_id = ?
+                `;
+
+                const [result] = await dbConnection.execute(sql, values);
+                console.log(`[MySQL] Règle ${ruleId} mise à jour`);
+
+                return result.affectedRows > 0;
 
             } catch (error) {
-                console.error('[MySQL] Erreur dans createAlertRule:', error);
+                console.error('[MySQL] Erreur dans updateRule:', error);
                 throw error;
             }
-        }
+        },
+
+        // =====================================================
+        // 7. SUPPRIMER UNE RÈGLE D'ALERTE (CRUD - Delete)
+        // =====================================================
+        /**
+         * Supprime une règle
+         * @param {string} ruleId - ID de la règle
+         * @returns {Promise<boolean>} true si supprimé
+         */
+        async deleteRule(ruleId) {
+            try {
+                const sql = `DELETE FROM ${tables.alerts} WHERE rule_id = ?`;
+
+                const [result] = await dbConnection.execute(sql, [ruleId]);
+                console.log(`[MySQL] Règle ${ruleId} supprimée`);
+
+                return result.affectedRows > 0;
+
+            } catch (error) {
+                console.error('[MySQL] Erreur dans deleteRule:', error);
+                throw error;
+            }
+        },
+
+        // =====================================================
+        // 8. RÉCUPÉRER UNE RÈGLE PAR ID (CRUD - Read)
+        // =====================================================
+        /**
+         * Récupère une règle par son ID
+         * @param {string} ruleId - ID de la règle
+         * @returns {Promise<Object|null>} La règle ou null
+         */
+        async findRuleById(ruleId) {
+            try {
+                const sql = `
+                    SELECT * FROM ${tables.alerts}
+                    WHERE rule_id = ?
+                `;
+
+                const [rows] = await dbConnection.execute(sql, [ruleId]);
+                return rows.length > 0 ? rows[0] : null;
+
+            } catch (error) {
+                console.error('[MySQL] Erreur dans findRuleById:', error);
+                throw error;
+            }
+        },
     };
 }
 
index c2a495e156a9cc7b8b78ee0b6d285689743206bb..ea02247c55e43cff1d20da4300001e939371be69 100644 (file)
@@ -114,7 +114,7 @@ export function createAlertsController(alertsService, alertsRepo) {
                 // =================================================================================
                 // GESTION DES ERREURS
                 // =================================================================================
-                console.error(' Erreur dans processSignal controller:', error);
+                console.error(' Erreur dans processSignal controller:', error);
 
                 res.status(500).json({
                     success: false,
@@ -198,6 +198,169 @@ export function createAlertsController(alertsService, alertsRepo) {
                     error: 'Erreur serveur'
                 });
             }
+        },
+
+        // =========================================================================================
+        //                                         CRUD
+        //==========================================================================================
+
+
+        // =====================================================
+        // POST /api/alerts/rules - CRÉER UNE RÈGLE
+        // =====================================================
+        /**
+         * Crée une nouvelle règle d'alerte
+         * 
+         * Body attendu :
+         * {
+         *   "userId": "uuid",
+         *   "pairId": 1,
+         *   "channel": "EMAIL",
+         *   "minConfidence": 0.8,
+         *   "severity": "WARNING",
+         *   "cooldownMs": 60000
+         * }
+         */
+        async createRule(req, res) {
+            try {
+                const ruleData = req.body;
+
+                // Validation des champs obligatoires
+                if (!ruleData.userId || !ruleData.channel) {
+                    return res.status(400).json({
+                        success: false,
+                        error: 'Champs manquants (userId, channel requis)'
+                    });
+                }
+
+                // Appeler le repo (qui appelle l'adapter)
+                const newRule = await alertsRepo.createRule(ruleData);
+
+                res.status(201).json({
+                    success: true,
+                    rule: newRule,
+                    message: 'Règle créée avec succès'
+                });
+
+            } catch (error) {
+                console.error('❌ Erreur createRule:', error);
+                res.status(500).json({
+                    success: false,
+                    error: error.message
+                });
+            }
+        },
+
+        // =====================================================
+        // PUT /api/alerts/rules/:id - MODIFIER UNE RÈGLE
+        // =====================================================
+        /**
+         * Met à jour une règle existante
+         * 
+         * Body possible :
+         * {
+         *   "minConfidence": 0.9,
+         *   "enabled": true,
+         *   "channel": "CONSOLE",
+         *   "severity": "CRITICAL"
+         * }
+         */
+        async updateRule(req, res) {
+            try {
+                const { id } = req.params;
+                const updates = req.body;
+
+                // Vérifier qu'il y a quelque chose à mettre à jour
+                if (Object.keys(updates).length === 0) {
+                    return res.status(400).json({
+                        success: false,
+                        error: 'Aucune donnée à mettre à jour'
+                    });
+                }
+
+                // Appeler le repo (qui appelle l'adapter)
+                const updated = await alertsRepo.updateRule(id, updates);
+
+                if (!updated) {
+                    return res.status(404).json({
+                        success: false,
+                        error: 'Règle non trouvée'
+                    });
+                }
+
+                res.json({
+                    success: true,
+                    message: 'Règle mise à jour'
+                });
+
+            } catch (error) {
+                console.error('❌ Erreur updateRule:', error);
+                res.status(500).json({
+                    success: false,
+                    error: error.message
+                });
+            }
+        },
+
+        // =====================================================
+        // DELETE /api/alerts/rules/:id - SUPPRIMER UNE RÈGLE
+        // =====================================================
+        async deleteRule(req, res) {
+            try {
+                const { id } = req.params;
+
+                // Appeler le repo (qui appelle l'adapter)
+                const deleted = await alertsRepo.deleteRule(id);
+
+                if (!deleted) {
+                    return res.status(404).json({
+                        success: false,
+                        error: 'Règle non trouvée'
+                    });
+                }
+
+                res.json({
+                    success: true,
+                    message: 'Règle supprimée'
+                });
+
+            } catch (error) {
+                console.error('❌ Erreur deleteRule:', error);
+                res.status(500).json({
+                    success: false,
+                    error: error.message
+                });
+            }
+        },
+
+        // =====================================================
+        // GET /api/alerts/rules/detail/:id - DÉTAIL D'UNE RÈGLE
+        // =====================================================
+        async getRuleById(req, res) {
+            try {
+                const { id } = req.params;
+
+                const rule = await alertsRepo.findRuleById(id);
+
+                if (!rule) {
+                    return res.status(404).json({
+                        success: false,
+                        error: 'Règle non trouvée'
+                    });
+                }
+
+                res.json({
+                    success: true,
+                    rule
+                });
+
+            } catch (error) {
+                console.error('❌ Erreur getRuleById:', error);
+                res.status(500).json({
+                    success: false,
+                    error: error.message
+                });
+            }
         }
     };
 }
index bf26e2a67089bf495a886c38a12e4c0266f307dd..8c0e288ffec394878f04269ce6aa3017189c6602 100644 (file)
@@ -90,14 +90,52 @@ export function createAlertsRepo(adapter) {
         },
 
         // =====================================================
-        // 5. CRÉER UNE NOUVELLE RÈGLE D'ALERTE
+        // 5. CRÉER UNE RÈGLE D'ALERTE (CRUD - Create)
         // =====================================================
         /**
-         * Crée une règle d'alerte
+         * Crée une nouvelle règle d'alerte
          * @param {Object} ruleData - Données de la règle
+         * @returns {Promise<Object>} Règle créée
          */
-        async createAlertRule(ruleData) {
-            return adapter.createAlertRule(ruleData);
-        }
+        async createRule(ruleData) {
+            return adapter.createRule(ruleData);
+        },
+
+        // =====================================================
+        // 6. MODIFIER UNE RÈGLE D'ALERTE (CRUD - Update)
+        // =====================================================
+        /**
+         * Met à jour une règle existante
+         * @param {string} ruleId - ID de la règle
+         * @param {Object} updates - Champs à mettre à jour
+         * @returns {Promise<boolean>} true si modifié
+         */
+        async updateRule(ruleId, updates) {
+            return adapter.updateRule(ruleId, updates);
+        },
+
+        // =====================================================
+        // 7. SUPPRIMER UNE RÈGLE D'ALERTE (CRUD - Delete)
+        // =====================================================
+        /**
+         * Supprime une règle
+         * @param {string} ruleId - ID de la règle
+         * @returns {Promise<boolean>} true si supprimé
+         */
+        async deleteRule(ruleId) {
+            return adapter.deleteRule(ruleId);
+        },
+
+        // =====================================================
+        // 8. RÉCUPÉRER UNE RÈGLE PAR ID (CRUD - Read)
+        // =====================================================
+        /**
+         * Récupère une règle par son ID
+         * @param {string} ruleId - ID de la règle
+         * @returns {Promise<Object|null>} La règle ou null
+         */
+        async findRuleById(ruleId) {
+            return adapter.findRuleById(ruleId);
+        },
     };
 }
index 8bfb49bcf8ca054cf24fbb42f98ea4fde32b6470..8280d868765c2ea2302c0bf22c41767bb2c7c3a9 100644 (file)
@@ -26,10 +26,8 @@ export function createAlertsRouter(alertsController) {
 // =========================================================
     // DÉFINITION DES ROUTES
 // =========================================================
-
-// =============================================================================
     // POST /api/alerts/process-signal
-// =============================================================================
+// =========================================================
     /**
      * Traiter un signal crypto et envoyer les alertes
      *
@@ -69,6 +67,14 @@ export function createAlertsRouter(alertsController) {
      */
     router.get('/history/:userId', alertsController.getHistory);
 
+// =============================================================================
+//                                  CRUD
+// =============================================================================
+    router.post('/rules', alertsController.createRule);
+    router.get('/rules/detail/:id', alertsController.getRuleById);
+    router.put('/rules/:id', alertsController.updateRule);
+    router.delete('/rules/:id', alertsController.deleteRule);
+
     return router;
 }
 
index c8d541e11a06bf9088c9b9f707e61f22705cf0c8..4280614f35472362f3a4d3debeb45f1cf61d909c 100644 (file)
@@ -69,7 +69,7 @@ export function createAlertsService(alertsRepo, options = {}) {
      * @returns {boolean} true si l'alerte doit être envoyée
      */
     shouldSendAlert(rule, signal) {
-        console.log(`🔍 Vérification règle ${rule.rule_id}...`);
+        console.log(`Vérification règle ${rule.rule_id}...`);
 
         // ─────────────────────────────────────────────────
         // CRITÈRE 1 : Vérifier la confidence minimum
@@ -114,7 +114,7 @@ export function createAlertsService(alertsRepo, options = {}) {
         // ─────────────────────────────────────────────────
         // TOUS LES CRITÈRES SONT OK !
         // ─────────────────────────────────────────────────
-        console.log(`   ✅ Tous les critères sont remplis`);
+        console.log(`   Tous les critères sont remplis`);
         return true;
     },
 
@@ -134,7 +134,7 @@ export function createAlertsService(alertsRepo, options = {}) {
      * @returns {Promise<string>} 'SENT' ou 'FAILED'
      */
     async sendViaChannel(rule, signal) {
-        console.log(`📡 Envoi via ${rule.channel}...`);
+        console.log(`Envoi via ${rule.channel}...`);
 
         let status = 'SENT';
 
@@ -159,7 +159,7 @@ export function createAlertsService(alertsRepo, options = {}) {
                     const chatId = params.chatId;
 
                     if (!chatId) {
-                        console.error(' Pas de chatId dans params');
+                        console.error(' Pas de chatId dans params');
                         status = 'FAILED';
                         break;
                     }
@@ -173,7 +173,7 @@ export function createAlertsService(alertsRepo, options = {}) {
                     const webhookUrl = discordParams.webhookUrl;
 
                     if (!webhookUrl) {
-                        console.error(' Pas de webhookUrl dans params');
+                        console.error(' Pas de webhookUrl dans params');
                         status = 'FAILED';
                         break;
                     }
@@ -195,7 +195,7 @@ export function createAlertsService(alertsRepo, options = {}) {
 
                 // ── CANAL INCONNU ────────────────────────
                 default:
-                    console.warn(`⚠️ Canal inconnu: ${rule.channel}`);
+                    console.warn(` Canal inconnu: ${rule.channel}`);
                     status = 'FAILED';
             }
 
@@ -203,7 +203,7 @@ export function createAlertsService(alertsRepo, options = {}) {
             // ─────────────────────────────────────────────
             // GESTION DES ERREURS
             // ─────────────────────────────────────────────
-            console.error(` Erreur lors de l'envoi via ${rule.channel}:`);
+            console.error(` Erreur lors de l'envoi via ${rule.channel}:`);
             console.error(`   ${error.message}`);
             status = 'FAILED';
         }
@@ -235,7 +235,7 @@ export function createAlertsService(alertsRepo, options = {}) {
          */
         async processSignal(signal) {
             console.log('\n' + '═'.repeat(80));
-            console.log(`🔔 TRAITEMENT DU SIGNAL`);
+            console.log(` TRAITEMENT DU SIGNAL`);
             console.log(`   Action : ${signal.action}`);
             console.log(`   Paire  : ${signal.pair}`);
             console.log(`   User   : ${signal.userId}`);
@@ -252,12 +252,12 @@ export function createAlertsService(alertsRepo, options = {}) {
                 );
 
                 if (rules.length === 0) {
-                    console.log('⚠️ Aucune règle active trouvée pour ce signal');
+                    console.log(' Aucune règle active trouvée pour ce signal');
                     console.log('   → Vérifie que des règles sont configurées en DB\n');
                     return;
                 }
 
-                console.log(`📋 ${rules.length} règle(s) trouvée(s)\n`);
+                console.log(` ${rules.length} règle(s) trouvée(s)\n`);
 
                 // ─────────────────────────────────────────────
                 // ÉTAPE 2 : Traiter chaque règle
@@ -269,7 +269,7 @@ export function createAlertsService(alertsRepo, options = {}) {
 
                     // Vérifier si on doit envoyer
                     if (!this.shouldSendAlert(rule, signal)) {
-                        console.log(`└─  Alerte NON envoyée\n`);
+                        console.log(`└─  Alerte NON envoyée\n`);
                         continue;
                     }
 
@@ -298,9 +298,9 @@ export function createAlertsService(alertsRepo, options = {}) {
                     if (status === 'SENT') {
                         await alertsRepo.updateLastNotified(rule.rule_id, Date.now());
                         sentCount++;
-                        console.log(`└─  Alerte envoyée avec succès\n`);
+                        console.log(`└─  Alerte envoyée avec succès\n`);
                     } else {
-                        console.log(`└─  Échec de l'envoi\n`);
+                        console.log(`└─  Échec de l'envoi\n`);
                     }
                 }
 
@@ -308,13 +308,13 @@ export function createAlertsService(alertsRepo, options = {}) {
                 // RÉSUMÉ FINAL
                 // ─────────────────────────────────────────────
                 console.log('═'.repeat(80));
-                console.log(`📊 RÉSUMÉ`);
+                console.log(` RÉSUMÉ`);
                 console.log(`   Règles vérifiées : ${rules.length}`);
                 console.log(`   Alertes envoyées : ${sentCount}`);
                 console.log('═'.repeat(80) + '\n');
 
             } catch (error) {
-                console.error(' ERREUR CRITIQUE dans processSignal:');
+                console.error(' ERREUR CRITIQUE dans processSignal:');
                 console.error(error);
                 throw error; // Remonter l'erreur pour que le controller puisse la gérer
             }
index f9bc531b5832ff07141fdf27ec28b0b8d24a48a3..18f74caae50fd8cb90d044c6266cfa45929c63f8 100644 (file)
@@ -18,7 +18,7 @@ export {
     broadcastAlert,
     isUserConnected,
     getConnectedUsersCount,
-    getI
+    getIO
 } from './socketManager.js';
 
 // Export des canaux (utiles si on veut les tester séparément)
diff --git a/server/modules/alerts/init-alerts.js b/server/modules/alerts/init-alerts.js
deleted file mode 100644 (file)
index 42407ac..0000000
+++ /dev/null
@@ -1,159 +0,0 @@
-// =========================================================
-// INITIALISATION DU MODULE ALERTS - VERSION AVEC ADAPTER
-// =========================================================
-// Ce fichier montre comment initialiser et utiliser le module d'alertes dans Wall-e-tte
-// =========================================================
-
-import db from '../config/db.js';
-import { createMySQLAdapter } from './alerts/adapters/mysql.adapter.js';
-import {
-    createAlertsRepo,
-    createAlertsService,
-    createAlertsController,
-    createAlertsRouter
-} from './alerts/index.js';
-
-// =========================================================
-// FONCTION D'INITIALISATION
-// =========================================================
-/**
- * Initialise tout le module d'alertes
- *
- * @param {Object} adapter - Adapter de base de données (MySQL, MongoDB, etc.)
- * @param {Object} options - Options de configuration
- * @returns {Object} { adapter, repo, service, controller, router }
- *
- * @example
- * // Avec MySQL (défaut pour Wall-e-tte)
- * const alerts = initializeAlertsModule();
- *
- * @example
- * // Avec un adapter personnalisé (autre projet)
- * const mongoAdapter = createMongoAdapter(mongoClient);
- * const alerts = initializeAlertsModule(mongoAdapter);
- */
-export function initializeAlertsModule(adapter = null, options = {}) {
-
-    console.log('\n' + '═'.repeat(80));
-    console.log('INITIALISATION DU MODULE ALERTS (avec Adapter Pattern)');
-    console.log('═'.repeat(80) + '\n');
-
-    // ─────────────────────────────────────────────────────
-    // ÉTAPE 1 : Créer l'Adapter (si non fourni)
-    // ─────────────────────────────────────────────────────
-    // Par défaut, on utilise MySQL pour Wall-e-tte
-    // Mais un autre projet peut injecter son propre adapter
-    if (!adapter) {
-        console.log("Création de l'adapter MySQL par défaut...");
-        adapter = createMySQLAdapter(db, {
-            alertsTable: options.alertsTable || 'alert_rules',
-            usersTable: options.usersTable || 'users',
-            eventsTable: options.eventsTable || 'alert_events'
-        });
-    } else {
-        console.log('📦 Utilisation de l\'adapter fourni');
-    }
-
-    // ─────────────────────────────────────────────────────
-    // ÉTAPE 2 : Créer le Repository
-    // ─────────────────────────────────────────────────────
-    // Le repository utilise l'adapter
-    const alertsRepo = createAlertsRepo(adapter);
-
-    // ─────────────────────────────────────────────────────
-    // ÉTAPE 3 : Créer le Service
-    // ─────────────────────────────────────────────────────
-    // Le service contient la logique métier
-    const alertsService = createAlertsService(alertsRepo, {
-        defaultCooldown: options.defaultCooldown || 60000
-    });
-
-    // ─────────────────────────────────────────────────────
-    // ÉTAPE 4 : Créer le Controller
-    // ─────────────────────────────────────────────────────
-    // Le controller gère les requêtes HTTP
-    const alertsController = createAlertsController(alertsService, alertsRepo);
-
-    // ─────────────────────────────────────────────────────
-    // ÉTAPE 5 : Créer le Router
-    // ─────────────────────────────────────────────────────
-    // Le router définit les routes API
-    const alertsRouter = createAlertsRouter(alertsController);
-
-    console.log('✅ Module Alerts complètement initialisé !');
-    console.log('═'.repeat(80) + '\n');
-
-    // ─────────────────────────────────────────────────────
-    // RETOURNER TOUT
-    // ─────────────────────────────────────────────────────
-    return {
-        adapter: adapter,
-        repo: alertsRepo,
-        service: alertsService,
-        controller: alertsController,
-        router: alertsRouter
-    };
-}
-
-// =========================================================
-// EXPORT PAR DÉFAUT
-// =========================================================
-// Pour Wall-e-tte, initialiser avec la DB MySQL
-export default initializeAlertsModule();
-
-// =========================================================
-// EXEMPLE D'UTILISATION
-// =========================================================
-/*
-    ========================================================
-    UTILISATION DANS WALL-E-TTE
-    ========================================================
-
-    // Option 1 : Import par défaut (MySQL automatique)
-    import alerts from './modules/init-alerts.js';
-
-    app.use('/api/alerts', alerts.router);
-    await alerts.service.processSignal(signal);
-
-
-    // Option 2 : Configuration personnalisée
-    import { initializeAlertsModule } from './modules/init-alerts.js';
-
-    const alerts = initializeAlertsModule(null, {
-        alertsTable: 'mes_alertes',
-        defaultCooldown: 120000  // 2 minutes
-    });
-
-    =========================================================
-    UTILISATION DANS UN AUTRE PROJET (MySQL)
-    =========================================================
-
-    import theirDb from './their-db-config.js';
-    import { createMySQLAdapter } from '@wall-e-tte/alerts/adapters/mysql.adapter.js';
-    import { initializeAlertsModule } from '@wall-e-tte/alerts';
-
-    // Créer leur adapter avec leur DB
-    const adapter = createMySQLAdapter(theirDb, {
-        alertsTable: 'notifications',
-        usersTable: 'accounts'
-    });
-
-    // Initialiser avec leur adapter
-    const alerts = initializeAlertsModule(adapter);
-
-    app.use('/api/notifications', alerts.router);
-
-    =========================================================
-    UTILISATION AVEC MONGODB (autre projet)
-    =========================================================
-
-    import { MongoClient } from 'mongodb';
-    import { createMongoAdapter } from './adapters/mongo.adapter.js';  // À créer
-    import { initializeAlertsModule } from '@wall-e-tte/alerts';
-
-    const mongoClient = new MongoClient(uri);
-    const adapter = createMongoAdapter(mongoClient.db('alerts'));
-
-    const alerts = initializeAlertsModule(adapter);
-    // aucune modification du repo/service/controller/router
-*/
index 576feb37101d00d2b23f87ca27285249c9bdb9b0..ef6ff14333e9b162c3dc8d2839a756527abc6342 100644 (file)
@@ -4,14 +4,14 @@
 // RÔLE : Centraliser la gestion des connexions Socket.IO
 //        pour le module d'alertes
 // =========================================================
-// UTILISÉ PAR : web.js (canal de notification web)
+// UTILISÉ PAR : web.js
 // =========================================================
 
 import { Server } from 'socket.io';
 
-// ─────────────────────────────────────────────────────────
+// =========================================================
 // STOCKAGE DES CONNEXIONS
-// ─────────────────────────────────────────────────────────
+// =========================================================
 // Map : userId → socket
 // Permet d'envoyer une alerte à un utilisateur spécifique
 const userSockets = new Map();
@@ -19,26 +19,26 @@ const userSockets = new Map();
 // Instance du serveur Socket.IO
 let io = null;
 
-// ─────────────────────────────────────────────────────────
+// =========================================================
 // INITIALISATION DU SERVEUR SOCKET.IO
-// ─────────────────────────────────────────────────────────
+// =========================================================
 /**
  * Initialise Socket.IO sur un serveur HTTP existant
- * 
+ *
  * @param {Object} httpServer - Serveur HTTP (Express)
  * @param {Object} options - Options Socket.IO (optionnel)
  * @returns {Server} Instance Socket.IO
- * 
+ *
  * @example
- * // Dans le fichier principal du serveur (app.js ou index.js)
+ * // Dans le fichier principal du serveur (index.js)
  * import express from 'express';
  * import { createServer } from 'http';
  * import { initSocketIO } from './modules/alerts/socketManager.js';
- * 
+ *
  * const app = express();
  * const httpServer = createServer(app);
  * const io = initSocketIO(httpServer);
- * 
+ *
  * httpServer.listen(3000);
  */
 export function initSocketIO(httpServer, options = {}) {
@@ -56,7 +56,7 @@ export function initSocketIO(httpServer, options = {}) {
     // GESTION DES CONNEXIONS
     // ─────────────────────────────────────────────────────
     io.on('connection', (socket) => {
-        console.log(`🔌 [Socket.IO] Nouvelle connexion : ${socket.id}`);
+        console.log(` [Socket.IO] Nouvelle connexion : ${socket.id}`);
 
         // ─────────────────────────────────────────────────
         // EVENT : Authentification de l'utilisateur
@@ -64,7 +64,7 @@ export function initSocketIO(httpServer, options = {}) {
         // Le client doit envoyer son userId après connexion
         socket.on('auth', (userId) => {
             if (!userId) {
-                console.log(`⚠️  [Socket.IO] Auth sans userId (socket: ${socket.id})`);
+                console.log(`  [Socket.IO] Auth sans userId (socket: ${socket.id})`);
                 return;
             }
 
@@ -72,13 +72,13 @@ export function initSocketIO(httpServer, options = {}) {
             userSockets.set(userId, socket);
             socket.userId = userId; // Garder une référence sur le socket
 
-            console.log(` [Socket.IO] Utilisateur authentifié : ${userId}`);
+            console.log(` [Socket.IO] Utilisateur authentifié : ${userId}`);
             console.log(`   Utilisateurs connectés : ${userSockets.size}`);
 
             // Confirmer l'authentification au client
-            socket.emit('auth_success', { 
-                userId, 
-                message: 'Connecté au système d\'alertes' 
+            socket.emit('auth_success', {
+                userId,
+                message: 'Connecté au système d\'alertes'
             });
         });
 
@@ -97,24 +97,24 @@ export function initSocketIO(httpServer, options = {}) {
         // EVENT : Ping (test de connexion)
         // ─────────────────────────────────────────────────
         socket.on('ping_alerts', () => {
-            socket.emit('pong_alerts', { 
+            socket.emit('pong_alerts', {
                 timestamp: Date.now(),
-                status: 'ok' 
+                status: 'ok'
             });
         });
     });
 
-    console.log(' [Socket.IO] Serveur initialisé');
+    console.log(' [Socket.IO] Serveur initialisé');
     return io;
 }
 
-// ─────────────────────────────────────────────────────────
+// =========================================================
 // FONCTIONS D'ENVOI D'ALERTES
-// ─────────────────────────────────────────────────────────
+// =========================================================
 
 /**
  * Envoie une alerte à un utilisateur spécifique
- * 
+ *
  * @param {string} userId - ID de l'utilisateur cible
  * @param {Object} alertData - Données de l'alerte
  * @returns {boolean} true si envoyé, false si utilisateur non connecté
@@ -127,23 +127,23 @@ export function sendAlertToUser(userId, alertData) {
             ...alertData,
             timestamp: Date.now()
         });
-        console.log(`📤 [Socket.IO] Alerte envoyée à ${userId}`);
+        console.log(` [Socket.IO] Alerte envoyée à ${userId}`);
         return true;
     }
 
-    console.log(`⚠️  [Socket.IO] Utilisateur ${userId} non connecté`);
+    console.log(`  [Socket.IO] Utilisateur ${userId} non connecté`);
     return false;
 }
 
 /**
  * Envoie une alerte à tous les utilisateurs connectés
- * 
+ *
  * @param {Object} alertData - Données de l'alerte
  * @returns {number} Nombre d'utilisateurs notifiés
  */
 export function broadcastAlert(alertData) {
     if (!io) {
-        console.error(' [Socket.IO] Serveur non initialisé');
+        console.error(' [Socket.IO] Serveur non initialisé');
         return 0;
     }
 
@@ -152,17 +152,17 @@ export function broadcastAlert(alertData) {
         timestamp: Date.now()
     });
 
-    console.log(`📢 [Socket.IO] Alerte broadcast à ${userSockets.size} utilisateurs`);
+    console.log(` [Socket.IO] Alerte broadcast à ${userSockets.size} utilisateurs`);
     return userSockets.size;
 }
 
-// ─────────────────────────────────────────────────────────
+// =========================================================
 // FONCTIONS UTILITAIRES
-// ─────────────────────────────────────────────────────────
+// =========================================================
 
 /**
  * Vérifie si un utilisateur est connecté
- * @param {string} userId 
+ * @param {string} userId
  * @returns {boolean}
  */
 export function isUserConnected(userId) {
index 1956182d4fe9b8b15aa704eedfa4e33219704525..4f927b92a388c0f8fd74c3fa407f9db46e8562fd 100644 (file)
@@ -7,10 +7,11 @@
 
 import db from '../config/db.js';
 import {
+    createAlertsController,
     createAlertsRepo,
+    createAlertsRouter,
     createAlertsService,
-    createAlertsController,
-    createAlertsRouter
+    createMySQLAdapter
 } from './alerts/index.js';
 
 // =========================================================
@@ -30,17 +31,27 @@ export function initializeAlertsModule(dbConnection = db, options = {}) {
     console.log('═'.repeat(80) + '\n');
 
     // ─────────────────────────────────────────────────────
-    // ÉTAPE 1 : Créer le Repository
+    // ÉTAPE 1 : Créer l'Adapter MySQL
+    // ─────────────────────────────────────────────────────
+    // L'adapter contient les requêtes SQL spécifiques
+    const adapter = createMySQLAdapter(dbConnection, {
+        alertsTable: options.alertsTable || 'alert_rules',
+        usersTable: options.usersTable || 'users',
+        eventsTable: options.eventsTable || 'alert_events'
+    });
+
+    // ─────────────────────────────────────────────────────
+    // ÉTAPE 2 : Créer le Repository
     // ─────────────────────────────────────────────────────
     // Le repository gère les requêtes SQL
-    const alertsRepo = createAlertsRepo(dbConnection, {
+    const alertsRepo = createAlertsRepo(adapter, {
         alertsTable: options.alertsTable || 'alert_rules',
         usersTable: options.usersTable || 'users',
         eventsTable: options.eventsTable || 'alert_events'
     });
 
     // ─────────────────────────────────────────────────────
-    // ÉTAPE 2 : Créer le Service
+    // ÉTAPE 3 : Créer le Service
     // ─────────────────────────────────────────────────────
     // Le service contient la logique métier
     const alertsService = createAlertsService(alertsRepo, {
@@ -48,24 +59,26 @@ export function initializeAlertsModule(dbConnection = db, options = {}) {
     });
 
     // ─────────────────────────────────────────────────────
-    // ÉTAPE 3 : Créer le Controller
+    // ÉTAPE 4 : Créer le Controller
     // ─────────────────────────────────────────────────────
     // Le controller gère les requêtes HTTP
+    // (Plus besoin de db car le CRUD passe par le repo)
     const alertsController = createAlertsController(alertsService, alertsRepo);
 
     // ─────────────────────────────────────────────────────
-    // ÉTAPE 4 : Créer le Router
+    // ÉTAPE 5 : Créer le Router
     // ─────────────────────────────────────────────────────
     // Le router définit les routes API
     const alertsRouter = createAlertsRouter(alertsController);
 
-    console.log('✅ Module Alerts complètement initialisé !');
+    console.log('Module Alerts complètement initialisé !');
     console.log('═'.repeat(80) + '\n');
 
     // ─────────────────────────────────────────────────────
     // RETOURNER TOUT
     // ─────────────────────────────────────────────────────
     return {
+        adapter,
         repo: alertsRepo,
         service: alertsService,
         controller: alertsController,
index d7d67188656ede4441068a0edc6a6b571cafbf07..ba0a0dfcc12be750c61e607d13604195f48b836b 100644 (file)
         "express": "^4.18.2",
         "mysql2": "^3.6.5",
         "nodemailer": "^6.9.7",
+        "socket.io": "^4.8.3",
         "uuid": "^9.0.1"
       },
       "devDependencies": {}
     },
+    "node_modules/@socket.io/component-emitter": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
+      "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==",
+      "license": "MIT"
+    },
+    "node_modules/@types/cors": {
+      "version": "2.8.19",
+      "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz",
+      "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/node": "*"
+      }
+    },
+    "node_modules/@types/node": {
+      "version": "25.3.0",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.0.tgz",
+      "integrity": "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A==",
+      "license": "MIT",
+      "dependencies": {
+        "undici-types": "~7.18.0"
+      }
+    },
     "node_modules/accepts": {
       "version": "1.3.8",
       "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
         "node": ">= 6.0.0"
       }
     },
+    "node_modules/base64id": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz",
+      "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==",
+      "license": "MIT",
+      "engines": {
+        "node": "^4.5.0 || >= 5.9"
+      }
+    },
     "node_modules/body-parser": {
       "version": "1.20.4",
       "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz",
       "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
       "license": "MIT"
     },
+    "node_modules/cors": {
+      "version": "2.8.6",
+      "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz",
+      "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==",
+      "license": "MIT",
+      "dependencies": {
+        "object-assign": "^4",
+        "vary": "^1"
+      },
+      "engines": {
+        "node": ">= 0.10"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
     "node_modules/debug": {
       "version": "2.6.9",
       "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
         "node": ">= 0.8"
       }
     },
+    "node_modules/engine.io": {
+      "version": "6.6.5",
+      "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.5.tgz",
+      "integrity": "sha512-2RZdgEbXmp5+dVbRm0P7HQUImZpICccJy7rN7Tv+SFa55pH+lxnuw6/K1ZxxBfHoYpSkHLAO92oa8O4SwFXA2A==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/cors": "^2.8.12",
+        "@types/node": ">=10.0.0",
+        "accepts": "~1.3.4",
+        "base64id": "2.0.0",
+        "cookie": "~0.7.2",
+        "cors": "~2.8.5",
+        "debug": "~4.4.1",
+        "engine.io-parser": "~5.2.1",
+        "ws": "~8.18.3"
+      },
+      "engines": {
+        "node": ">=10.2.0"
+      }
+    },
+    "node_modules/engine.io-parser": {
+      "version": "5.2.3",
+      "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
+      "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=10.0.0"
+      }
+    },
+    "node_modules/engine.io/node_modules/debug": {
+      "version": "4.4.3",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+      "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+      "license": "MIT",
+      "dependencies": {
+        "ms": "^2.1.3"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/engine.io/node_modules/ms": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+      "license": "MIT"
+    },
     "node_modules/es-define-property": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
         "node": ">=6.0.0"
       }
     },
+    "node_modules/object-assign": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+      "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
     "node_modules/object-inspect": {
       "version": "1.13.4",
       "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/socket.io": {
+      "version": "4.8.3",
+      "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.3.tgz",
+      "integrity": "sha512-2Dd78bqzzjE6KPkD5fHZmDAKRNe3J15q+YHDrIsy9WEkqttc7GY+kT9OBLSMaPbQaEd0x1BjcmtMtXkfpc+T5A==",
+      "license": "MIT",
+      "dependencies": {
+        "accepts": "~1.3.4",
+        "base64id": "~2.0.0",
+        "cors": "~2.8.5",
+        "debug": "~4.4.1",
+        "engine.io": "~6.6.0",
+        "socket.io-adapter": "~2.5.2",
+        "socket.io-parser": "~4.2.4"
+      },
+      "engines": {
+        "node": ">=10.2.0"
+      }
+    },
+    "node_modules/socket.io-adapter": {
+      "version": "2.5.6",
+      "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.6.tgz",
+      "integrity": "sha512-DkkO/dz7MGln0dHn5bmN3pPy+JmywNICWrJqVWiVOyvXjWQFIv9c2h24JrQLLFJ2aQVQf/Cvl1vblnd4r2apLQ==",
+      "license": "MIT",
+      "dependencies": {
+        "debug": "~4.4.1",
+        "ws": "~8.18.3"
+      }
+    },
+    "node_modules/socket.io-adapter/node_modules/debug": {
+      "version": "4.4.3",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+      "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+      "license": "MIT",
+      "dependencies": {
+        "ms": "^2.1.3"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/socket.io-adapter/node_modules/ms": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+      "license": "MIT"
+    },
+    "node_modules/socket.io-parser": {
+      "version": "4.2.5",
+      "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.5.tgz",
+      "integrity": "sha512-bPMmpy/5WWKHea5Y/jYAP6k74A+hvmRCQaJuJB6I/ML5JZq/KfNieUVo/3Mh7SAqn7TyFdIo6wqYHInG1MU1bQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@socket.io/component-emitter": "~3.1.0",
+        "debug": "~4.4.1"
+      },
+      "engines": {
+        "node": ">=10.0.0"
+      }
+    },
+    "node_modules/socket.io-parser/node_modules/debug": {
+      "version": "4.4.3",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+      "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+      "license": "MIT",
+      "dependencies": {
+        "ms": "^2.1.3"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/socket.io-parser/node_modules/ms": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+      "license": "MIT"
+    },
+    "node_modules/socket.io/node_modules/debug": {
+      "version": "4.4.3",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+      "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+      "license": "MIT",
+      "dependencies": {
+        "ms": "^2.1.3"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/socket.io/node_modules/ms": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+      "license": "MIT"
+    },
     "node_modules/sql-escaper": {
       "version": "1.3.1",
       "resolved": "https://registry.npmjs.org/sql-escaper/-/sql-escaper-1.3.1.tgz",
         "node": ">= 0.6"
       }
     },
+    "node_modules/undici-types": {
+      "version": "7.18.2",
+      "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
+      "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
+      "license": "MIT"
+    },
     "node_modules/unpipe": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
       "engines": {
         "node": ">= 0.8"
       }
+    },
+    "node_modules/ws": {
+      "version": "8.18.3",
+      "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
+      "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=10.0.0"
+      },
+      "peerDependencies": {
+        "bufferutil": "^4.0.1",
+        "utf-8-validate": ">=5.0.2"
+      },
+      "peerDependenciesMeta": {
+        "bufferutil": {
+          "optional": true
+        },
+        "utf-8-validate": {
+          "optional": true
+        }
+      }
     }
   }
 }
index 2c76723218c53137a5d5e77460f10b9744d72322..59655e5513b64f407596c460d0444bea28276101 100644 (file)
@@ -7,7 +7,12 @@
   "scripts": {
     "test": "node test-alerts.js"
   },
-  "keywords": ["crypto", "alerts", "notifications", "wall-e-tte"],
+  "keywords": [
+    "crypto",
+    "alerts",
+    "notifications",
+    "wall-e-tte"
+  ],
   "author": "Stéphane",
   "license": "ISC",
   "dependencies": {
@@ -15,7 +20,7 @@
     "express": "^4.18.2",
     "mysql2": "^3.6.5",
     "nodemailer": "^6.9.7",
+    "socket.io": "^4.8.3",
     "uuid": "^9.0.1"
-  },
-  "devDependencies": {}
+  }
 }
diff --git a/server/test-module-complet.js b/server/test-module-complet.js
new file mode 100644 (file)
index 0000000..a17d027
--- /dev/null
@@ -0,0 +1,445 @@
+// =========================================================
+// TEST COMPLET DU MODULE ALERTS
+// =========================================================
+// Ce script teste :
+// 1. Connexion à la base de données
+// 2. CRUD des règles (Create, Read, Update, Delete)
+// 3. Canaux de notification
+// 4. Service processSignal
+// =========================================================
+// USAGE : node test-module-complet.js
+// =========================================================
+
+import dotenv from 'dotenv';
+import path from 'path';
+import { fileURLToPath } from 'url';
+
+// Charger les variables d'environnement
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+dotenv.config({ path: path.resolve(__dirname, '.env') });
+
+// =========================================================
+// IMPORTS DU MODULE
+// =========================================================
+import db from './config/db.js';
+import { createMySQLAdapter } from './modules/alerts/adapters/mysql.adapter.js';
+import { createAlertsRepo } from './modules/alerts/alerts.repo.js';
+import { createAlertsService } from './modules/alerts/alerts.service.js';
+
+// =========================================================
+// INITIALISATION
+// =========================================================
+const adapter = createMySQLAdapter(db);
+const repo = createAlertsRepo(adapter);
+const service = createAlertsService(repo);
+
+// Variable pour stocker l'ID de la règle créée
+let testRuleId = null;
+let testUserId = null;
+
+// =========================================================
+// FONCTIONS UTILITAIRES
+// =========================================================
+function printHeader(title) {
+    console.log('\n' + '─'.repeat(60));
+    console.log(`📋 ${title}`);
+    console.log('─'.repeat(60));
+}
+
+function printSuccess(message) {
+    console.log(`   ✅ ${message}`);
+}
+
+function printError(message) {
+    console.log(`   ❌ ${message}`);
+}
+
+function printInfo(message) {
+    console.log(`   ℹ️  ${message}`);
+}
+
+// =========================================================
+// TEST 1 : CONNEXION DB
+// =========================================================
+async function testConnection() {
+    printHeader('TEST 1 : Connexion à la base de données');
+
+    try {
+        const [rows] = await db.execute('SELECT 1 as test');
+        if (rows[0].test === 1) {
+            printSuccess('Connexion DB OK');
+            return true;
+        }
+    } catch (error) {
+        printError(`Connexion échouée : ${error.message}`);
+        return false;
+    }
+}
+
+// =========================================================
+// TEST 1.5 : PRÉPARER UN UTILISATEUR DE TEST
+// =========================================================
+async function prepareTestUser() {
+    printHeader('TEST 1.5 : Préparation utilisateur de test');
+
+    try {
+        // D'abord, chercher un utilisateur existant
+        const [existingUsers] = await db.execute('SELECT user_id FROM users LIMIT 1');
+
+        if (existingUsers.length > 0) {
+            testUserId = existingUsers[0].user_id;
+            printSuccess(`Utilisateur existant trouvé : ${testUserId}`);
+            return true;
+        }
+
+        // Sinon, créer un utilisateur de test
+        printInfo('Aucun utilisateur trouvé, création d\'un utilisateur de test...');
+
+        const { v4: uuidv4 } = await import('uuid');
+        testUserId = 'test-user-' + uuidv4().substring(0, 8);
+
+        await db.execute(`
+            INSERT INTO users (user_id, email, password_hash, display_name, created_at_ms, updated_at_ms)
+            VALUES (?, ?, ?, ?, ?, ?)
+        `, [testUserId, 'test@test.com', 'hash123', 'Test User', Date.now(), Date.now()]);
+
+        printSuccess(`Utilisateur de test créé : ${testUserId}`);
+        return true;
+
+    } catch (error) {
+        printError(`Erreur préparation utilisateur : ${error.message}`);
+        printInfo('Conseil : Exécute le seed.sql pour créer des données de test');
+        return false;
+    }
+}
+
+// =========================================================
+// TEST 2 : CREATE - Créer une règle
+// =========================================================
+async function testCreateRule() {
+    printHeader('TEST 2 : CRUD - Créer une règle (CREATE)');
+
+    if (!testUserId) {
+        printError('Pas d\'utilisateur de test disponible');
+        return false;
+    }
+
+    try {
+        const ruleData = {
+            userId: testUserId,  // Utiliser l'utilisateur existant/créé
+            pairId: 1,
+            channel: 'CONSOLE',
+            minConfidence: 0.75,
+            severity: 'WARNING',
+            ruleType: 'SIGNAL_THRESHOLD',
+            cooldownMs: 60000
+        };
+
+        printInfo(`Création d'une règle pour user: ${ruleData.userId}`);
+
+        const result = await repo.createRule(ruleData);
+
+        if (result && result.ruleId) {
+            testRuleId = result.ruleId;
+            printSuccess(`Règle créée avec ID: ${testRuleId}`);
+            return true;
+        } else {
+            printError('Règle créée mais pas d\'ID retourné');
+            return false;
+        }
+    } catch (error) {
+        printError(`Erreur création : ${error.message}`);
+        return false;
+    }
+}
+
+// =========================================================
+// TEST 3 : READ - Lire la règle créée
+// =========================================================
+async function testReadRule() {
+    printHeader('TEST 3 : CRUD - Lire la règle (READ)');
+
+    if (!testRuleId) {
+        printError('Pas de règle à lire (test CREATE a échoué)');
+        return false;
+    }
+
+    try {
+        printInfo(`Lecture de la règle ID: ${testRuleId}`);
+
+        const rule = await repo.findRuleById(testRuleId);
+
+        if (rule) {
+            printSuccess(`Règle trouvée :`);
+            console.log(`      - user_id: ${rule.user_id}`);
+            console.log(`      - channel: ${rule.channel}`);
+            console.log(`      - min_confidence: ${rule.min_confidence}`);
+            console.log(`      - enabled: ${rule.enabled}`);
+            return true;
+        } else {
+            printError('Règle non trouvée');
+            return false;
+        }
+    } catch (error) {
+        printError(`Erreur lecture : ${error.message}`);
+        return false;
+    }
+}
+
+// =========================================================
+// TEST 4 : UPDATE - Modifier la règle
+// =========================================================
+async function testUpdateRule() {
+    printHeader('TEST 4 : CRUD - Modifier la règle (UPDATE)');
+
+    if (!testRuleId) {
+        printError('Pas de règle à modifier (test CREATE a échoué)');
+        return false;
+    }
+
+    try {
+        const updates = {
+            minConfidence: 0.90,
+            channel: 'EMAIL',
+            severity: 'CRITICAL'
+        };
+
+        printInfo(`Modification de la règle ID: ${testRuleId}`);
+        printInfo(`Nouvelles valeurs: confidence=0.90, channel=EMAIL, severity=CRITICAL`);
+
+        const updated = await repo.updateRule(testRuleId, updates);
+
+        if (updated) {
+            // Vérifier la modification
+            const rule = await repo.findRuleById(testRuleId);
+
+            // Debug : afficher les valeurs réelles
+            console.log(`      [DEBUG] min_confidence: ${rule.min_confidence} (type: ${typeof rule.min_confidence})`);
+            console.log(`      [DEBUG] channel: ${rule.channel}`);
+            console.log(`      [DEBUG] severity: ${rule.severity}`);
+
+            // Comparaison tolérante (float peut avoir des imprécisions)
+            const confidenceOk = Math.abs(parseFloat(rule.min_confidence) - 0.90) < 0.01;
+            const channelOk = rule.channel === 'EMAIL';
+
+            if (confidenceOk && channelOk) {
+                printSuccess('Règle modifiée et vérifiée');
+                return true;
+            } else {
+                printError(`Modification non appliquée correctement`);
+                printError(`  → confidence OK: ${confidenceOk}, channel OK: ${channelOk}`);
+                return false;
+            }
+        } else {
+            printError('Modification a retourné false');
+            return false;
+        }
+    } catch (error) {
+        printError(`Erreur modification : ${error.message}`);
+        return false;
+    }
+}
+
+// =========================================================
+// TEST 5 : DELETE - Supprimer la règle
+// =========================================================
+async function testDeleteRule() {
+    printHeader('TEST 5 : CRUD - Supprimer la règle (DELETE)');
+
+    if (!testRuleId) {
+        printError('Pas de règle à supprimer (test CREATE a échoué)');
+        return false;
+    }
+
+    try {
+        printInfo(`Suppression de la règle ID: ${testRuleId}`);
+
+        const deleted = await repo.deleteRule(testRuleId);
+
+        if (deleted) {
+            // Vérifier la suppression
+            const rule = await repo.findRuleById(testRuleId);
+
+            if (!rule) {
+                printSuccess('Règle supprimée et vérifiée');
+                return true;
+            } else {
+                printError('Règle encore présente après suppression');
+                return false;
+            }
+        } else {
+            printError('Suppression a retourné false');
+            return false;
+        }
+    } catch (error) {
+        printError(`Erreur suppression : ${error.message}`);
+        return false;
+    }
+}
+
+// =========================================================
+// TEST 6 : Canal CONSOLE
+// =========================================================
+async function testConsoleChannel() {
+    printHeader('TEST 6 : Canal de notification CONSOLE');
+
+    try {
+        const { sendConsoleAlert } = await import('./modules/alerts/channels/console.js');
+
+        const testSignal = {
+            action: 'BUY',
+            pair: 'BTC/EUR',
+            confidence: 0.85,
+            criticality: 'WARNING',
+            reason: 'Test du canal console'
+        };
+
+        await sendConsoleAlert(testSignal);
+        printSuccess('Canal CONSOLE fonctionne');
+        return true;
+    } catch (error) {
+        printError(`Erreur canal CONSOLE : ${error.message}`);
+        return false;
+    }
+}
+
+// =========================================================
+// TEST 7 : Service processSignal (si règles existent)
+// =========================================================
+async function testProcessSignal() {
+    printHeader('TEST 7 : Service processSignal');
+
+    if (!testUserId) {
+        printError('Pas d\'utilisateur de test disponible');
+        return false;
+    }
+
+    try {
+        // D'abord créer une règle pour le test
+        const ruleData = {
+            userId: testUserId,  // Utiliser l'utilisateur existant
+            pairId: 1,
+            channel: 'CONSOLE',
+            minConfidence: 0.70,
+            severity: 'INFO'
+        };
+
+        printInfo('Création d\'une règle temporaire pour le test...');
+        const rule = await repo.createRule(ruleData);
+
+        // Simuler un signal
+        const testSignal = {
+            userId: testUserId,  // Utiliser l'utilisateur existant
+            pairId: 1,
+            pair: 'BTC/EUR',
+            action: 'BUY',
+            confidence: 0.85,
+            criticality: 'WARNING',
+            reason: 'Test du service processSignal'
+        };
+
+        printInfo('Envoi du signal au service...');
+        await service.processSignal(testSignal);
+
+        printSuccess('Service processSignal exécuté sans erreur');
+
+        // Nettoyer : supprimer la règle de test
+        await repo.deleteRule(rule.ruleId);
+        printInfo('Règle temporaire supprimée');
+
+        return true;
+    } catch (error) {
+        printError(`Erreur processSignal : ${error.message}`);
+        return false;
+    }
+}
+
+// =========================================================
+// EXÉCUTION DE TOUS LES TESTS
+// =========================================================
+async function runAllTests() {
+    console.log('\n' + '═'.repeat(60));
+    console.log('🧪 TESTS COMPLETS DU MODULE ALERTS');
+    console.log('═'.repeat(60));
+
+    const results = {
+        connection: false,
+        prepareUser: false,
+        create: false,
+        read: false,
+        update: false,
+        delete: false,
+        console: false,
+        processSignal: false
+    };
+
+    try {
+        // Tests dans l'ordre
+        results.connection = await testConnection();
+
+        if (results.connection) {
+            results.prepareUser = await prepareTestUser();
+
+            if (results.prepareUser) {
+                results.create = await testCreateRule();
+                results.read = await testReadRule();
+                results.update = await testUpdateRule();
+                results.delete = await testDeleteRule();
+                results.processSignal = await testProcessSignal();
+            }
+
+            results.console = await testConsoleChannel();
+        }
+
+    } catch (error) {
+        console.error('\n❌ ERREUR FATALE:', error);
+    }
+
+    // =========================================================
+    // RÉSUMÉ
+    // =========================================================
+    console.log('\n' + '═'.repeat(60));
+    console.log('📊 RÉSUMÉ DES TESTS');
+    console.log('═'.repeat(60));
+
+    const tests = [
+        { name: 'Connexion DB', result: results.connection },
+        { name: 'Préparation User', result: results.prepareUser },
+        { name: 'CRUD - Create', result: results.create },
+        { name: 'CRUD - Read', result: results.read },
+        { name: 'CRUD - Update', result: results.update },
+        { name: 'CRUD - Delete', result: results.delete },
+        { name: 'Canal Console', result: results.console },
+        { name: 'Service processSignal', result: results.processSignal }
+    ];
+
+    let passed = 0;
+    let failed = 0;
+
+    tests.forEach(test => {
+        const icon = test.result ? '✅' : '❌';
+        console.log(`   ${icon} ${test.name}`);
+        if (test.result) passed++;
+        else failed++;
+    });
+
+    console.log('\n' + '─'.repeat(60));
+    console.log(`   Total: ${passed}/${tests.length} tests réussis`);
+
+    if (failed === 0) {
+        console.log('\n   🎉 TOUS LES TESTS SONT PASSÉS !');
+    } else {
+        console.log(`\n   ⚠️  ${failed} test(s) échoué(s)`);
+    }
+
+    console.log('═'.repeat(60) + '\n');
+
+    // Quitter
+    process.exit(failed === 0 ? 0 : 1);
+}
+
+// =========================================================
+// LANCEMENT
+// =========================================================
+runAllTests();
diff --git a/server/test-server.js b/server/test-server.js
new file mode 100644 (file)
index 0000000..45ec6bc
--- /dev/null
@@ -0,0 +1,121 @@
+// =========================================================
+// MINI-SERVEUR DE TEST POUR LES ROUTES API
+// =========================================================
+// Ce script démarre un serveur Express pour tester les
+// routes du module Alerts via Postman, curl ou navigateur
+// =========================================================
+// USAGE : node test-server.js
+// =========================================================
+
+import express from 'express';
+import dotenv from 'dotenv';
+import path from 'path';
+import { fileURLToPath } from 'url';
+
+// Charger les variables d'environnement
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+dotenv.config({ path: path.resolve(__dirname, '.env') });
+
+// =========================================================
+// IMPORTS DU MODULE ALERTS
+// =========================================================
+import { initializeAlertsModule } from './modules/init-alerts.js';
+
+// =========================================================
+// CRÉER L'APPLICATION EXPRESS
+// =========================================================
+const app = express();
+app.use(express.json());
+
+// =========================================================
+// INITIALISER LE MODULE ALERTS
+// =========================================================
+console.log('\n🚀 Initialisation du module Alerts...\n');
+
+// Import dynamique pour éviter l'erreur de connexion DB
+import db from './config/db.js';
+import { createMySQLAdapter } from './modules/alerts/adapters/mysql.adapter.js';
+import { createAlertsRepo } from './modules/alerts/alerts.repo.js';
+import { createAlertsService } from './modules/alerts/alerts.service.js';
+import { createAlertsController } from './modules/alerts/alerts.controller.js';
+import { createAlertsRouter } from './modules/alerts/alerts.router.js';
+
+const adapter = createMySQLAdapter(db);
+const repo = createAlertsRepo(adapter);
+const service = createAlertsService(repo);
+const controller = createAlertsController(service, repo);
+const router = createAlertsRouter(controller);
+
+// =========================================================
+// MONTER LES ROUTES
+// =========================================================
+app.use('/api/alerts', router);
+
+// =========================================================
+// ROUTE DE TEST / HEALTH CHECK
+// =========================================================
+app.get('/', (req, res) => {
+    res.json({
+        status: 'ok',
+        message: 'Serveur de test Alerts',
+        timestamp: new Date().toISOString(),
+        routes: [
+            'GET  /api/alerts/rules/:userId     - Lister les règles',
+            'GET  /api/alerts/rules/detail/:id  - Détail d\'une règle',
+            'POST /api/alerts/rules             - Créer une règle',
+            'PUT  /api/alerts/rules/:id         - Modifier une règle',
+            'DELETE /api/alerts/rules/:id       - Supprimer une règle',
+            'GET  /api/alerts/history/:userId   - Historique des alertes',
+            'POST /api/alerts/process-signal    - Traiter un signal'
+        ]
+    });
+});
+
+// =========================================================
+// DÉMARRER LE SERVEUR
+// =========================================================
+const PORT = process.env.PORT || 3000;
+
+app.listen(PORT, () => {
+    console.log(`
+╔═══════════════════════════════════════════════════════════╗
+║           🧪 SERVEUR DE TEST - MODULE ALERTS              ║
+╠═══════════════════════════════════════════════════════════╣
+║                                                           ║
+║  Serveur démarré sur : http://localhost:${PORT}             ║
+║                                                           ║
+║  Routes disponibles :                                     ║
+║  ─────────────────────────────────────────────────────── ║
+║  GET    /                              → Health check     ║
+║  GET    /api/alerts/rules/:userId      → Lister règles    ║
+║  GET    /api/alerts/rules/detail/:id   → Détail règle     ║
+║  POST   /api/alerts/rules              → Créer règle      ║
+║  PUT    /api/alerts/rules/:id          → Modifier règle   ║
+║  DELETE /api/alerts/rules/:id          → Supprimer règle  ║
+║  GET    /api/alerts/history/:userId    → Historique       ║
+║  POST   /api/alerts/process-signal     → Traiter signal   ║
+║                                                           ║
+╚═══════════════════════════════════════════════════════════╝
+
+📝 Exemples de tests avec curl :
+
+1. Créer une règle :
+   curl -X POST http://localhost:${PORT}/api/alerts/rules \\
+     -H "Content-Type: application/json" \\
+     -d '{"userId":"test-user","pairId":1,"channel":"CONSOLE","minConfidence":0.8}'
+
+2. Lister les règles :
+   curl http://localhost:${PORT}/api/alerts/rules/test-user
+
+3. Modifier une règle :
+   curl -X PUT http://localhost:${PORT}/api/alerts/rules/RULE_ID \\
+     -H "Content-Type: application/json" \\
+     -d '{"minConfidence":0.9,"enabled":true}'
+
+4. Supprimer une règle :
+   curl -X DELETE http://localhost:${PORT}/api/alerts/rules/RULE_ID
+
+Appuie sur Ctrl+C pour arrêter le serveur.
+`);
+});