},
// =====================================================
- // 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,
`;
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;
+ }
+ },
};
}
// =================================================================================
// GESTION DES ERREURS
// =================================================================================
- console.error('❌ Erreur dans processSignal controller:', error);
+ console.error(' Erreur dans processSignal controller:', error);
res.status(500).json({
success: false,
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
+ });
+ }
}
};
}
},
// =====================================================
- // 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);
+ },
};
}
// =========================================================
// DÉFINITION DES ROUTES
// =========================================================
-
-// =============================================================================
// POST /api/alerts/process-signal
-// =============================================================================
+// =========================================================
/**
* Traiter un signal crypto et envoyer les alertes
*
*/
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;
}
* @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
// ─────────────────────────────────────────────────
// TOUS LES CRITÈRES SONT OK !
// ─────────────────────────────────────────────────
- console.log(` ✅ Tous les critères sont remplis`);
+ console.log(` Tous les critères sont remplis`);
return true;
},
* @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';
const chatId = params.chatId;
if (!chatId) {
- console.error('❌ Pas de chatId dans params');
+ console.error(' Pas de chatId dans params');
status = 'FAILED';
break;
}
const webhookUrl = discordParams.webhookUrl;
if (!webhookUrl) {
- console.error('❌ Pas de webhookUrl dans params');
+ console.error(' Pas de webhookUrl dans params');
status = 'FAILED';
break;
}
// ── CANAL INCONNU ────────────────────────
default:
- console.warn(`⚠️ Canal inconnu: ${rule.channel}`);
+ console.warn(` Canal inconnu: ${rule.channel}`);
status = 'FAILED';
}
// ─────────────────────────────────────────────
// 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';
}
*/
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}`);
);
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
// 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;
}
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`);
}
}
// 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
}
broadcastAlert,
isUserConnected,
getConnectedUsersCount,
- getI
+ getIO
} from './socketManager.js';
// Export des canaux (utiles si on veut les tester séparément)
+++ /dev/null
-// =========================================================
-// 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
-*/
// 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();
// 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 = {}) {
// 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
// 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;
}
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'
});
});
// 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é
...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;
}
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) {
import db from '../config/db.js';
import {
+ createAlertsController,
createAlertsRepo,
+ createAlertsRouter,
createAlertsService,
- createAlertsController,
- createAlertsRouter
+ createMySQLAdapter
} from './alerts/index.js';
// =========================================================
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, {
});
// ─────────────────────────────────────────────────────
- // É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,
"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
+ }
+ }
}
}
}
"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": {
"express": "^4.18.2",
"mysql2": "^3.6.5",
"nodemailer": "^6.9.7",
+ "socket.io": "^4.8.3",
"uuid": "^9.0.1"
- },
- "devDependencies": {}
+ }
}
--- /dev/null
+// =========================================================
+// 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();
--- /dev/null
+// =========================================================
+// 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.
+`);
+});