From e9cae1de16318c3d271c4ef0d81bc61990c1392f Mon Sep 17 00:00:00 2001 From: Steph Ponzo Date: Tue, 24 Feb 2026 15:48:27 +0100 Subject: [PATCH] Alignement module alertes avec contrat API + format uniforme { ok, data/error } --- .../modules/alerts/adapters/mysql.adapter.js | 7 +- .../modules/alerts/alerts.controller.js | 448 ++++++++---------- .../server/modules/alerts/alerts.router.js | 110 ++--- 3 files changed, 252 insertions(+), 313 deletions(-) diff --git a/Wallette/server/modules/alerts/adapters/mysql.adapter.js b/Wallette/server/modules/alerts/adapters/mysql.adapter.js index 830efbd..2f73ec2 100644 --- a/Wallette/server/modules/alerts/adapters/mysql.adapter.js +++ b/Wallette/server/modules/alerts/adapters/mysql.adapter.js @@ -149,6 +149,9 @@ export function createMySQLAdapter(dbConnection, options = {}) { // ===================================================== async getAlertHistory(userId, limit = 50) { try { + // Sécuriser le limit (doit être un entier) + const safeLimit = parseInt(limit) || 50; + const sql = ` SELECT ae.*, @@ -158,10 +161,10 @@ export function createMySQLAdapter(dbConnection, options = {}) { JOIN ${tables.alerts} ar ON ae.rule_id = ar.rule_id WHERE ar.user_id = ? ORDER BY ae.timestamp_ms DESC - LIMIT ? + LIMIT ${safeLimit} `; - const [rows] = await dbConnection.execute(sql, [userId, limit]); + const [rows] = await dbConnection.execute(sql, [userId]); return rows; } catch (error) { diff --git a/Wallette/server/modules/alerts/alerts.controller.js b/Wallette/server/modules/alerts/alerts.controller.js index a153787..5aee41d 100644 --- a/Wallette/server/modules/alerts/alerts.controller.js +++ b/Wallette/server/modules/alerts/alerts.controller.js @@ -1,367 +1,309 @@ // ========================================================= -// ALERTS CONTROLLER - VERSION RÉUTILISABLE +// ALERTS CONTROLLER - CONTRAT API + CRUD // ========================================================= -// RÔLE : Gestion des requêtes et réponses HTTP +// Format de réponse uniforme : +// Succès : { ok: true, data: { ... } } +// Erreur : { ok: false, error: { code, message } } // ========================================================= - /** - * Crée un controller d'alertes réutilisable - * - * @param {Object} alertsService - Service d'alertes (créé par createAlertsService) - * @param {Object} alertsRepo - Repository d'alertes (pour certaines méthodes) - * + * Crée un controller d'alertes selon le contrat API + * @param {Object} alertsService - Service d'alertes + * @param {Object} alertsRepo - Repository d'alertes * @returns {Object} Controller avec toutes les méthodes */ export function createAlertsController(alertsService, alertsRepo) { console.log("Controller d'alertes initialisé"); + // ========================================================= + // HELPERS - Format de réponse uniforme + // ========================================================= + + const sendSuccess = (res, data, status = 200) => { + res.status(status).json({ ok: true, data }); + }; + + const sendError = (res, code, message, status = 400) => { + res.status(status).json({ ok: false, error: { code, message } }); + }; + return { // ===================================================== - // POST /api/alerts/process-signal + // CONTRAT API // ===================================================== + + // GET /api/alerts?userId=... /** - * Endpoint pour traiter un signal crypto - * - * Appelé par le module Strategy (Sacha) quand il - * génère un signal BUY/SELL/HOLD - * - * Body attendu : - * { - * "userId": "uuid", - * "pairId": 1, - * "pair": "BTC/EUR", - * "action": "BUY", - * "confidence": 0.87, - * "criticality": "WARNING", - * "reason": "Indicators show...", - * "priceAtSignal": 45000.50 - * } + * Liste les règles d'alerte d'un utilisateur + * Query params : userId (requis) */ - async processSignal(req, res) { + async getAlerts(req, res) { try { - // ================================================================================== - // 1. RÉCUPÉRER LES DONNÉES DU BODY - // ================================================================================== - const signal = req.body; + const { userId } = req.query; - // ================================================================================== - // 2. VALIDATION DES DONNÉES - // ================================================================================== - // Vérifier que tous les champs obligatoires sont présents - const requiredFields = ['userId', 'pairId', 'pair', 'action', 'confidence', 'criticality']; - /** - * on remplit le tableau des champs manquant ou faux du contenu de signal - */ - const missingFields = requiredFields.filter(field => !signal[field]); - // dans ce cas on retourne une erreur et le noms des champs en cause - if (missingFields.length > 0) { - return res.status(400).json({ - success: false, - error: 'Champs manquants', - missingFields: missingFields - }); + if (!userId) { + return sendError(res, 'MISSING_USER_ID', 'Le paramètre userId est requis', 400); } - // Valider le format de action - const validActions = ['BUY', 'SELL', 'HOLD', 'STOP_LOSS']; - if (!validActions.includes(signal.action)) { - return res.status(400).json({ - success: false, - error: `Action invalide. Doit être: ${validActions.join(', ')}` - }); - } + const alerts = await alertsRepo.findActiveRulesForSignal(userId, null); - // Valider le format de criticality - const validCriticalities = ['CRITICAL', 'WARNING', 'INFO']; - if (!validCriticalities.includes(signal.criticality)) { - return res.status(400).json({ - success: false, - error: `Criticality invalide. Doit être: ${validCriticalities.join(', ')}` - }); - } + sendSuccess(res, { + alerts, + count: alerts.length + }); - // Valider la confidence (doit être entre 0 et 1) - if (signal.confidence < 0 || signal.confidence > 1) { - return res.status(400).json({ - success: false, - error: 'Confidence doit être entre 0 et 1' - }); + } catch (error) { + console.error('Erreur getAlerts:', error); + sendError(res, 'SERVER_ERROR', error.message, 500); + } + }, + + // POST /api/alerts + /** + * Crée une nouvelle règle d'alerte + * Body : { userId, pairId, channel, minConfidence, severity, ... } + */ + async createAlert(req, res) { + try { + const ruleData = req.body; + + // Validation + if (!ruleData.userId) { + return sendError(res, 'MISSING_USER_ID', 'userId est requis', 400); + } + if (!ruleData.channel) { + return sendError(res, 'MISSING_CHANNEL', 'channel est requis', 400); } - // ================================================================================= - // 3. APPELER LE SERVICE (injecté) - // ================================================================================= - // Le service contient toute la logique - await alertsService.processSignal(signal); + const newAlert = await alertsRepo.createRule(ruleData); - // ================================================================================= - // 4. RÉPONDRE AU CLIENT - // ================================================================================= - res.status(200).json({ - success: true, - message: 'Signal traité avec succès', - signal: { - action: signal.action, - pair: signal.pair, - confidence: signal.confidence - } - }); + sendSuccess(res, { alert: newAlert }, 201); } catch (error) { - // ================================================================================= - // GESTION DES ERREURS - // ================================================================================= - console.error(' Erreur dans processSignal controller:', error); - - res.status(500).json({ - success: false, - error: 'Erreur serveur lors du traitement du signal', - details: process.env.NODE_ENV === 'development' ? error.message : undefined - }); + console.error('Erreur createAlert:', error); + sendError(res, 'SERVER_ERROR', error.message, 500); } }, - // ===================================================== - // GET /api/alerts/rules/:userId - // ===================================================== + // POST /api/alerts/:id/toggle /** - * Récupère toutes les règles d'alerte d'un utilisateur - * - * Utile pour afficher la configuration dans l'interface + * Active ou désactive une règle d'alerte + * Params : id (requis) */ - async getRules(req, res) { + async toggleAlert(req, res) { try { - const { userId } = req.params; + const { id } = req.params; - // Validation - if (!userId) { - return res.status(400).json({ - success: false, - error: 'userId manquant' - }); + // Récupérer la règle actuelle + const alert = await alertsRepo.findRuleById(id); + + if (!alert) { + return sendError(res, 'ALERT_NOT_FOUND', 'Règle d\'alerte non trouvée', 404); } - // Récupérer les règles (via repo injecté) - const rules = await alertsRepo.findActiveRulesForSignal(userId, null); + // Inverser l'état enabled + const newEnabledState = !alert.enabled; + await alertsRepo.updateRule(id, { enabled: newEnabledState }); + + // Récupérer la règle mise à jour + const updatedAlert = await alertsRepo.findRuleById(id); - res.status(200).json({ - success: true, - count: rules.length, - rules: rules + sendSuccess(res, { + alert: updatedAlert, + enabled: newEnabledState }); } catch (error) { - console.error('Erreur dans getRules controller:', error); - - res.status(500).json({ - success: false, - error: 'Erreur serveur' - }); + console.error('Erreur toggleAlert:', error); + sendError(res, 'SERVER_ERROR', error.message, 500); } }, - // ===================================================== - // GET /api/alerts/history/:userId - // ===================================================== + // GET /api/alerts/events?userId=...&limit=... /** - * Récupère l'historique des alertes d'un utilisateur + * Récupère l'historique des alertes envoyées + * Query params : userId (requis), limit (optionnel, défaut 50) */ - async getHistory(req, res) { + async getEvents(req, res) { try { - const { userId } = req.params; - const limit = parseInt(req.query.limit) || 50; + const { userId, limit } = req.query; if (!userId) { - return res.status(400).json({ - success: false, - error: 'userId manquant' - }); + return sendError(res, 'MISSING_USER_ID', 'Le paramètre userId est requis', 400); } - // Récupérer l'historique (via repo injecté) - const history = await alertsRepo.getAlertHistory(userId, limit); + const parsedLimit = parseInt(limit) || 50; + const events = await alertsRepo.getAlertHistory(userId, parsedLimit); - res.status(200).json({ - success: true, - count: history.length, - history: history + sendSuccess(res, { + events, + count: events.length, + limit: parsedLimit }); } catch (error) { - console.error('Erreur dans getHistory controller:', error); - - res.status(500).json({ - success: false, - error: 'Erreur serveur' - }); + console.error('Erreur getEvents:', error); + sendError(res, 'SERVER_ERROR', error.message, 500); } }, - // ========================================================================================= - // CRUD - //========================================================================================== - - // ===================================================== - // POST /api/alerts/rules - CRÉER UNE RÈGLE + // CRUD COMPLET // ===================================================== + + // GET /api/alerts/:id /** - * Crée une nouvelle règle d'alerte - * - * Body attendu : - * { - * "userId": "uuid", - * "pairId": 1, - * "channel": "EMAIL", - * "minConfidence": 0.8, - * "severity": "WARNING", - * "cooldownMs": 60000 - * } + * Récupère le détail d'une règle */ - async createRule(req, res) { + async getAlertById(req, res) { try { - const ruleData = req.body; + const { id } = req.params; - // Validation des champs obligatoires - if (!ruleData.userId || !ruleData.channel) { - return res.status(400).json({ - success: false, - error: 'Champs manquants (userId, channel requis)' - }); - } + const alert = await alertsRepo.findRuleById(id); - // Appeler le repo (qui appelle l'adapter) - const newRule = await alertsRepo.createRule(ruleData); + if (!alert) { + return sendError(res, 'ALERT_NOT_FOUND', 'Règle d\'alerte non trouvée', 404); + } - res.status(201).json({ - success: true, - rule: newRule, - message: 'Règle créée avec succès' - }); + sendSuccess(res, { alert }); } catch (error) { - console.error('❌ Erreur createRule:', error); - res.status(500).json({ - success: false, - error: error.message - }); + console.error('❌ Erreur getAlertById:', error); + sendError(res, 'SERVER_ERROR', error.message, 500); } }, - // ===================================================== - // PUT /api/alerts/rules/:id - MODIFIER UNE RÈGLE - // ===================================================== + // PUT /api/alerts/:id /** - * Met à jour une règle existante - * - * Body possible : - * { - * "minConfidence": 0.9, - * "enabled": true, - * "channel": "CONSOLE", - * "severity": "CRITICAL" - * } + * Modifie une règle d'alerte + * Body : { minConfidence, enabled, channel, severity, ... } */ - async updateRule(req, res) { + async updateAlert(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' - }); + return sendError(res, 'NO_DATA', 'Aucune donnée à mettre à jour', 400); } - // 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' - }); + // Vérifier que la règle existe + const existingAlert = await alertsRepo.findRuleById(id); + if (!existingAlert) { + return sendError(res, 'ALERT_NOT_FOUND', 'Règle d\'alerte non trouvée', 404); } - res.json({ - success: true, - message: 'Règle mise à jour' - }); + await alertsRepo.updateRule(id, updates); + + // Récupérer la règle mise à jour + const updatedAlert = await alertsRepo.findRuleById(id); + + sendSuccess(res, { alert: updatedAlert }); } catch (error) { - console.error('❌ Erreur updateRule:', error); - res.status(500).json({ - success: false, - error: error.message - }); + console.error('Erreur updateAlert:', error); + sendError(res, 'SERVER_ERROR', error.message, 500); } }, - // ===================================================== - // DELETE /api/alerts/rules/:id - SUPPRIMER UNE RÈGLE - // ===================================================== - async deleteRule(req, res) { + // DELETE /api/alerts/:id + /** + * Supprime une règle d'alerte + */ + async deleteAlert(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' - }); + return sendError(res, 'ALERT_NOT_FOUND', 'Règle d\'alerte non trouvée', 404); } - res.json({ - success: true, - message: 'Règle supprimée' - }); + sendSuccess(res, { deleted: true, id }); } catch (error) { - console.error('❌ Erreur deleteRule:', error); - res.status(500).json({ - success: false, - error: error.message - }); + console.error('❌ Erreur deleteAlert:', error); + sendError(res, 'SERVER_ERROR', error.message, 500); } }, // ===================================================== - // GET /api/alerts/rules/detail/:id - DÉTAIL D'UNE RÈGLE + // ROUTE INTERNE (pour module Strategy) // ===================================================== - async getRuleById(req, res) { + + // POST /api/alerts/process-signal + /** + * Traite un signal du module Strategy + * Body : { userId, pairId, pair, action, confidence, criticality, reason } + */ + async processSignal(req, res) { try { - const { id } = req.params; + const signal = req.body; + + // Validation + const requiredFields = ['userId', 'pairId', 'pair', 'action', 'confidence', 'criticality']; + const missingFields = requiredFields.filter(field => signal[field] === undefined); + + if (missingFields.length > 0) { + return sendError( + res, + 'MISSING_FIELDS', + `Champs manquants: ${missingFields.join(', ')}`, + 400 + ); + } + + // Valider action + const validActions = ['BUY', 'SELL', 'HOLD', 'STOP_LOSS']; + if (!validActions.includes(signal.action)) { + return sendError( + res, + 'INVALID_ACTION', + `Action invalide. Valeurs acceptées: ${validActions.join(', ')}`, + 400 + ); + } - const rule = await alertsRepo.findRuleById(id); + // Valider criticality + const validCriticalities = ['CRITICAL', 'WARNING', 'INFO']; + if (!validCriticalities.includes(signal.criticality)) { + return sendError( + res, + 'INVALID_CRITICALITY', + `Criticality invalide. Valeurs acceptées: ${validCriticalities.join(', ')}`, + 400 + ); + } - if (!rule) { - return res.status(404).json({ - success: false, - error: 'Règle non trouvée' - }); + // Valider confidence + if (signal.confidence < 0 || signal.confidence > 1) { + return sendError( + res, + 'INVALID_CONFIDENCE', + 'Confidence doit être entre 0 et 1', + 400 + ); } - res.json({ - success: true, - rule + // Traiter le signal + await alertsService.processSignal(signal); + + sendSuccess(res, { + processed: true, + signal: { + action: signal.action, + pair: signal.pair, + confidence: signal.confidence + } }); } catch (error) { - console.error('❌ Erreur getRuleById:', error); - res.status(500).json({ - success: false, - error: error.message - }); + console.error('Erreur processSignal:', error); + sendError(res, 'SERVER_ERROR', error.message, 500); } } }; } - diff --git a/Wallette/server/modules/alerts/alerts.router.js b/Wallette/server/modules/alerts/alerts.router.js index bc7ac78..5ec1af5 100644 --- a/Wallette/server/modules/alerts/alerts.router.js +++ b/Wallette/server/modules/alerts/alerts.router.js @@ -1,80 +1,74 @@ // ========================================================= -// ALERTS ROUTER - VERSION RÉUTILISABLE +// ALERTS ROUTER - CONTRAT API + CRUD // ========================================================= -// RÔLE : Définir les routes (endpoints) de l'API +// Routes du contrat API : +// GET /api/alerts?userId=... → Liste des alertes +// POST /api/alerts → Créer une alerte +// POST /api/alerts/:id/toggle → Activer/désactiver +// GET /api/alerts/events?userId=... → Historique +// +// Routes supplémentaires (CRUD complet) : +// GET /api/alerts/:id → Détail d'une alerte +// PUT /api/alerts/:id → Modifier une alerte +// DELETE /api/alerts/:id → Supprimer une alerte +// +// Route interne (pour module Strategy) : +// POST /api/alerts/process-signal → Traiter un signal // ========================================================= - import express from 'express'; -// ========================================================= -// FACTORY FUNCTION : Créer un router -// ========================================================= /** - * Crée un router d'alertes réutilisable - * - * @param {Object} alertsController - Controller d'alertes (créé par createAlertsController) - * + * Crée un router d'alertes selon le contrat API + * @param {Object} alertsController - Controller d'alertes * @returns {express.Router} Router Express configuré */ export function createAlertsRouter(alertsController) { const router = express.Router(); - console.log("Router d'alertes initialisé"); -// ========================================================= - // DÉFINITION DES ROUTES -// ========================================================= + // ========================================================= + // CONTRAT API + // ========================================================= + + // GET /api/alerts?userId=... + // Liste les règles d'alerte d'un utilisateur + router.get('/', alertsController.getAlerts); + + // POST /api/alerts + // Crée une nouvelle règle d'alerte + router.post('/', alertsController.createAlert); + + // GET /api/alerts/events?userId=...&limit=... + // Récupère l'historique des alertes envoyées + // ⚠️ DOIT être AVANT /:id sinon "events" sera pris comme un id + router.get('/events', alertsController.getEvents); + // POST /api/alerts/process-signal -// ========================================================= - /** - * Traiter un signal crypto et envoyer les alertes - * - * Exemple d'appel : - * POST http://localhost:3000/api/alerts/process-signal - * Body : { - * "userId": "user-123", - * "pairId": 1, - * "pair": "BTC/EUR", - * "action": "BUY", - * "confidence": 0.85, - * "criticality": "WARNING", - * "reason": "RSI oversold + MACD crossover" - * } - */ + // Traite un signal et envoie les notifications + // ⚠️ DOIT être AVANT /:id/toggle sinon "process-signal" sera pris comme un id router.post('/process-signal', alertsController.processSignal); -// ============================================================================= - // GET /api/alerts/rules/:userId -// ============================================================================= - /** - * Récupérer les règles d'alerte d'un utilisateur - * - * Exemple d'appel : - * GET http://localhost:3000/api/alerts/rules/user-123 - */ - router.get('/rules/:userId', alertsController.getRules); + // POST /api/alerts/:id/toggle + // Active ou désactive une règle d'alerte + router.post('/:id/toggle', alertsController.toggleAlert); -// ============================================================================= - // GET /api/alerts/history/:userId -// ============================================================================= - /** - * Récupérer l'historique des alertes d'un utilisateur - * - * Exemple d'appel : - * GET http://localhost:3000/api/alerts/history/user-123?limit=20 - */ - router.get('/history/:userId', alertsController.getHistory); + // ========================================================= + // CRUD COMPLET (en plus du contrat) + // ========================================================= -// ============================================================================= -// 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); + // GET /api/alerts/:id + // Détail d'une règle + router.get('/:id', alertsController.getAlertById); + + // PUT /api/alerts/:id + // Modifier une règle + router.put('/:id', alertsController.updateAlert); + + // DELETE /api/alerts/:id + // Supprimer une règle + router.delete('/:id', alertsController.deleteAlert); return router; } - -- 2.50.1