--- /dev/null
+DB_HOST=127.0.0.1
+DB_PORT=33020
+DB_USER=root
+DB_PASSWORD=password
+DB_NAME=wallette
+PORT=3000
\ No newline at end of file
--- /dev/null
+node_modules/
+.env
--- /dev/null
+# 📡 Guide Socket.IO - Alertes Temps Réel
+
+## Pour Océane (Web) et Thibaud (Mobile)
+
+Ce document explique comment recevoir les alertes en temps réel depuis le serveur.
+
+---
+
+## 🔧 Installation
+
+### Web (JavaScript/React)
+```bash
+npm install socket.io-client
+```
+
+### Mobile (React Native / Expo)
+```bash
+npm install socket.io-client
+```
+
+---
+
+## Connexion au serveur
+
+### Code TypeScript/JavaScript
+
+```typescript
+import { io, Socket } from 'socket.io-client';
+
+// URL du serveur (à adapter selon l'environnement)
+const SERVER_URL = 'http://localhost:3000';
+
+// Créer la connexion
+const socket: Socket = io(SERVER_URL);
+
+// Événement : connexion établie
+socket.on('connect', () => {
+ console.log('✅ Connecté au serveur Socket.IO');
+
+ // IMPORTANT : S'authentifier avec l'userId
+ const userId = 'user-123'; // Récupérer depuis votre auth
+ socket.emit('auth', userId);
+});
+
+// Événement : authentification confirmée
+socket.on('auth_success', (data) => {
+ console.log('✅ Authentifié:', data.message);
+});
+
+// Événement : déconnexion
+socket.on('disconnect', (reason) => {
+ console.log('❌ Déconnecté:', reason);
+});
+```
+
+---
+
+## Recevoir les alertes
+
+```typescript
+// Structure d'une alerte reçue
+interface Alert {
+ action: 'BUY' | 'SELL' | 'HOLD';
+ pair: string; // ex: 'BTC/EUR'
+ confidence: number; // 0 à 1
+ reason: string; // Explication du signal
+ alertLevel: 'INFO' | 'WARNING' | 'CRITICAL';
+ timestamp: number; // Unix timestamp ms
+ price?: number; // Prix au moment du signal
+}
+
+// Écouter les alertes
+socket.on('alert', (alert: Alert) => {
+ console.log('🔔 Nouvelle alerte:', alert);
+
+ // Exemple de traitement
+ if (alert.alertLevel === 'CRITICAL') {
+ // Notification urgente
+ showUrgentNotification(alert);
+ } else {
+ // Notification normale
+ showNotification(alert);
+ }
+});
+```
+
+---
+
+## Tester la connexion
+
+```typescript
+// Envoyer un ping de test
+socket.emit('ping_alerts');
+
+// Recevoir
+socket.on('pong_alerts', (data) => {
+ console.log('Connexion OK, latence:', Date.now() - data.timestamp, 'ms');
+});
+```
+
+---
+
+## Exemple complet React Native (Thibaud)
+
+```typescript
+// services/socketService.ts
+import { io, Socket } from 'socket.io-client';
+import { Alert } from '../types/DashboardSummary';
+
+class SocketService {
+ private socket: Socket | null = null;
+ private listeners: ((alert: Alert) => void)[] = [];
+
+ connect(serverUrl: string, userId: string) {
+ this.socket = io(serverUrl);
+
+ this.socket.on('connect', () => {
+ console.log('Socket connecté');
+ this.socket?.emit('auth', userId);
+ });
+
+ this.socket.on('alert', (alert: Alert) => {
+ this.listeners.forEach(cb => cb(alert));
+ });
+ }
+
+ onAlert(callback: (alert: Alert) => void) {
+ this.listeners.push(callback);
+ }
+
+ disconnect() {
+ this.socket?.disconnect();
+ this.socket = null;
+ }
+}
+
+export const socketService = new SocketService();
+```
+
+```typescript
+// Dans un composant React Native
+import { useEffect, useState } from 'react';
+import { socketService } from '../services/socketService';
+
+export function useLiveAlerts(userId: string) {
+ const [alerts, setAlerts] = useState<Alert[]>([]);
+
+ useEffect(() => {
+ socketService.connect('http://YOUR_SERVER:3000', userId);
+
+ socketService.onAlert((alert) => {
+ setAlerts(prev => [alert, ...prev]);
+ });
+
+ return () => socketService.disconnect();
+ }, [userId]);
+
+ return alerts;
+}
+```
+
+---
+
+## Exemple complet React Web (Océane)
+
+```typescript
+// hooks/useSocket.ts
+import { useEffect, useState } from 'react';
+import { io, Socket } from 'socket.io-client';
+
+interface Alert {
+ action: 'BUY' | 'SELL' | 'HOLD';
+ pair: string;
+ confidence: number;
+ reason: string;
+ alertLevel: 'INFO' | 'WARNING' | 'CRITICAL';
+ timestamp: number;
+ price?: number;
+}
+
+export function useSocket(serverUrl: string, userId: string) {
+ const [socket, setSocket] = useState<Socket | null>(null);
+ const [connected, setConnected] = useState(false);
+ const [alerts, setAlerts] = useState<Alert[]>([]);
+
+ useEffect(() => {
+ const newSocket = io(serverUrl);
+
+ newSocket.on('connect', () => {
+ setConnected(true);
+ newSocket.emit('auth', userId);
+ });
+
+ newSocket.on('disconnect', () => {
+ setConnected(false);
+ });
+
+ newSocket.on('alert', (alert: Alert) => {
+ setAlerts(prev => [alert, ...prev].slice(0, 50)); // Garder 50 max
+ });
+
+ setSocket(newSocket);
+
+ return () => {
+ newSocket.disconnect();
+ };
+ }, [serverUrl, userId]);
+
+ return { socket, connected, alerts };
+}
+```
+
+---
+
+## Récapitulatif des événements
+
+| Event | Direction | Description |
+|-------|-----------|-------------|
+| `auth` | Client → Serveur | Envoyer l'userId pour s'authentifier |
+| `auth_success` | Serveur → Client | Confirmation d'authentification |
+| `alert` | Serveur → Client | Réception d'une alerte |
+| `ping_alerts` | Client → Serveur | Test de connexion |
+| `pong_alerts` | Serveur → Client | Réponse au ping |
+
+---
+
+
--- /dev/null
+// =========================================================
+// EXEMPLE : Utilisation du module dans UN AUTRE PROJET
+// =========================================================
+// Ce fichier montre comment un projet DIFFÉRENT de Wall-e-tte
+// peut utiliser le module d'alertes
+// =========================================================
+
+/*
+ CONTEXTE :
+ ton module d'alertes.
+
+ Leur projet a :
+ - Une DB avec des tables différentes
+ - Une structure différente
+ - Mais ils veulent ton système d'alertes !
+*/
+
+// =========================================================
+// 1. LEUR CONFIGURATION DB (différente de Wall-e-tte)
+// =========================================================
+import mysql from 'mysql2/promise';
+
+// Leur connexion DB (avec leurs credentials)
+const theirDb = mysql.createPool({
+ host: 'their-server.com',
+ user: 'their-user',
+ password: 'their-password',
+ database: 'crypto_tracker_db'
+});
+
+// =========================================================
+// 2. IMPORT DU MODULE ALERTS
+// =========================================================
+// Ils peuvent installer via npm ou copier les fichiers
+// import { createAlertsRepo, createAlertsService } from '@wall-e-tte/alerts-module';
+
+// Ou si les fichiers sont copiés dans leur projet :
+import { createAlertsRepo } from './vendors/wall-e-tte-alerts/alerts.repo.js';
+import { createAlertsService } from './vendors/wall-e-tte-alerts/alerts.service.js';
+
+// =========================================================
+// 3. INITIALISATION AVEC LEURS TABLES
+// =========================================================
+const alertsRepo = createAlertsRepo(theirDb, {
+ alertsTable: 'notification_rules', // ← Leur nom
+ usersTable: 'accounts', // ← Leur nom
+ eventsTable: 'notification_history' // ← Leur nom
+});
+
+const alertsService = createAlertsService(alertsRepo);
+
+// =========================================================
+// 4. UTILISATION - EXACTEMENT PAREIL !
+// =========================================================
+async function handleCryptoSignal() {
+
+ // Leur signal (même structure que Wall-e-tte)
+ const signal = {
+ userId: 'account-789', // Leur ID format
+ pairId: 42, // Leur ID format
+ pair: 'ETH/USD',
+ action: 'SELL',
+ confidence: 0.92,
+ criticality: 'CRITICAL',
+ reason: 'Major resistance level reached, volume increasing'
+ };
+
+ //Le module utilise LEURS tables
+ await alertsService.processSignal(signal);
+
+ console.log('Signal traité avec succès dans CryptoTracker !');
+}
+
+// =========================================================
+// 5. STRUCTURE DE TABLE (différente de Wall-e-tte)
+// =========================================================
+/*
+ CREATE TABLE notification_rules (
+ rule_id VARCHAR(36) PRIMARY KEY,
+ user_id VARCHAR(36), -- Pointent vers 'accounts'
+ enabled TINYINT(1),
+ severity ENUM('CRITICAL','WARNING','INFO'),
+ min_confidence DECIMAL(5,4),
+ channel ENUM('WEB','CONSOLE','EMAIL','DISCORD','TELEGRAM'),
+ cooldown_ms BIGINT,
+ last_notified_at_ms BIGINT,
+ -- ... autres colonnes
+ FOREIGN KEY (user_id) REFERENCES accounts(user_id)
+ );
+
+ CREATE TABLE accounts (
+ user_id VARCHAR(36) PRIMARY KEY,
+ email VARCHAR(255),
+ -- ... autres colonnes
+ );
+
+ CREATE TABLE notification_history (
+ alert_event_id VARCHAR(36) PRIMARY KEY,
+ rule_id VARCHAR(36),
+ timestamp_ms BIGINT,
+ severity ENUM('CRITICAL','WARNING','INFO'),
+ channel VARCHAR(20),
+ send_status ENUM('PENDING','SENT','FAILED'),
+ -- ... autres colonnes
+ );
+*/
+
+// =========================================================
+// RÉSULTAT :
+// =========================================================
+/*
+ Le module marche dans d'autres projets
+ pas de mondif du code
+ Il y aura juste à configurr les noms de tables
+*/
+
+// =========================================================
+// EXPORT POUR UTILISATION DANS LEUR APP
+// =========================================================
+export { alertsRepo, alertsService };
+export default alertsService;
+
+// Dans leur app :
+// import alertsService from './alerts-integration.js';
+// await alertsService.processSignal(signal);
--- /dev/null
+// =========================================================
+// WALL-E-TTE - SERVEUR PRINCIPAL
+// =========================================================
+// Point d'entrée de l'application
+// Intègre tous les modules : Alerts, (Stratégie, Prix, etc.)
+// =========================================================
+// USAGE : node app.js
+// =========================================================
+
+import cors from 'cors';
+import dotenv from 'dotenv';
+import express from 'express';
+import { createServer } from 'http';
+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 DES MODULES
+// =========================================================
+
+// Module Alerts (Stéphane)
+import db from './config/db.js';
+import { createMySQLAdapter } from './modules/alerts/adapters/mysql.adapter.js';
+import { createAlertsController } from './modules/alerts/alerts.controller.js';
+import { createAlertsRepo } from './modules/alerts/alerts.repo.js';
+import { createAlertsRouter } from './modules/alerts/alerts.router.js';
+import { createAlertsService } from './modules/alerts/alerts.service.js';
+import { broadcastAlert, getConnectedUsersCount, initSocketIO } from './modules/alerts/socketManager.js';
+
+// ─────────────────────────────────────────────────────────
+// TODO: Ajouter les autres modules ici
+// ─────────────────────────────────────────────────────────
+// Module Stratégie (Sacha)
+// import { initializeStrategyModule } from './modules/strategy/index.js';
+
+// Module Prix (Valentin)
+// import { initializePriceModule } from './modules/price/index.js';
+
+// Module Auth (si existant)
+// import { initializeAuthModule } from './modules/auth/index.js';
+
+// =========================================================
+// CRÉER L'APPLICATION EXPRESS
+// =========================================================
+const app = express();
+
+// Middlewares
+app.use(cors()); // Autoriser les requêtes cross-origin
+app.use(express.json()); // Parser le JSON
+app.use(express.urlencoded({ extended: true }));
+
+// =========================================================
+// CRÉER LE SERVEUR HTTP (nécessaire pour Socket.IO)
+// =========================================================
+const httpServer = createServer(app);
+
+// =========================================================
+// INITIALISER SOCKET.IO
+// =========================================================
+const io = initSocketIO(httpServer, {
+ cors: {
+ origin: "*", // En prod, restreindre aux domaines autorisés
+ methods: ["GET", "POST"]
+ }
+});
+
+console.log('✅ Socket.IO initialisé');
+
+// =========================================================
+// INITIALISER LE MODULE ALERTS (Stéphane)
+// =========================================================
+const alertsAdapter = createMySQLAdapter(db);
+const alertsRepo = createAlertsRepo(alertsAdapter);
+const alertsService = createAlertsService(alertsRepo);
+const alertsController = createAlertsController(alertsService, alertsRepo);
+const alertsRouter = createAlertsRouter(alertsController);
+
+console.log('✅ Module Alerts initialisé');
+
+// =========================================================
+// MONTER LES ROUTES API
+// =========================================================
+
+// Routes Alerts
+app.use('/api/alerts', alertsRouter);
+
+// ─────────────────────────────────────────────────────────
+// TODO: Monter les autres routes ici
+// ─────────────────────────────────────────────────────────
+// app.use('/api/strategy', strategyRouter);
+// app.use('/api/price', priceRouter);
+// app.use('/api/auth', authRouter);
+
+// =========================================================
+// ROUTE HEALTH CHECK
+// =========================================================
+app.get('/', (req, res) => {
+ res.json({
+ name: 'Wall-e-tte API',
+ status: 'running',
+ version: '1.0.0',
+ socketConnections: getConnectedUsersCount(),
+ timestamp: new Date().toISOString(),
+ endpoints: {
+ alerts: '/api/alerts',
+ // strategy: '/api/strategy',
+ // price: '/api/price',
+ }
+ });
+});
+
+// =========================================================
+// PAGE DE TEST SOCKET.IO
+// =========================================================
+app.get('/test', (req, res) => {
+ res.send(`<!DOCTYPE html>
+<html lang="fr">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>Wall-e-tte - Test Socket.IO</title>
+ <script src="/socket.io/socket.io.js"></script>
+ <style>
+ * { box-sizing: border-box; }
+ body {
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+ background: #1a1a2e;
+ color: #eee;
+ padding: 20px;
+ max-width: 800px;
+ margin: 0 auto;
+ }
+ h1 { color: #00d4ff; }
+ .status {
+ padding: 10px 20px;
+ border-radius: 8px;
+ margin: 10px 0;
+ font-weight: bold;
+ }
+ .connected { background: #00c853; color: #000; }
+ .disconnected { background: #ff5252; }
+ .authenticated { background: #00bcd4; color: #000; }
+
+ #alerts {
+ background: #16213e;
+ border-radius: 8px;
+ padding: 15px;
+ max-height: 400px;
+ overflow-y: auto;
+ }
+ .alert-item {
+ background: #0f3460;
+ border-left: 4px solid #00d4ff;
+ padding: 10px 15px;
+ margin: 10px 0;
+ border-radius: 0 8px 8px 0;
+ }
+ .alert-item.WARNING { border-left-color: #ffc107; }
+ .alert-item.CRITICAL { border-left-color: #ff5252; }
+ .alert-item.INFO { border-left-color: #00d4ff; }
+
+ .alert-action {
+ font-size: 1.5em;
+ font-weight: bold;
+ }
+ .alert-action.BUY { color: #00c853; }
+ .alert-action.SELL { color: #ff5252; }
+ .alert-action.HOLD { color: #ffc107; }
+
+ button {
+ background: #00d4ff;
+ color: #000;
+ border: none;
+ padding: 10px 20px;
+ border-radius: 8px;
+ cursor: pointer;
+ font-size: 1em;
+ margin: 5px;
+ }
+ button:hover { background: #00a0c0; }
+
+ input {
+ background: #16213e;
+ border: 1px solid #00d4ff;
+ color: #fff;
+ padding: 10px;
+ border-radius: 8px;
+ margin: 5px;
+ }
+
+ #log {
+ background: #000;
+ color: #0f0;
+ font-family: monospace;
+ padding: 15px;
+ border-radius: 8px;
+ max-height: 200px;
+ overflow-y: auto;
+ font-size: 12px;
+ }
+ </style>
+</head>
+<body>
+ <h1> Wall-e-tte - Test Socket.IO</h1>
+
+ <div id="connection-status" class="status disconnected">
+ Déconnecté
+ </div>
+
+ <div id="auth-status" class="status" style="display: none;">
+ </div>
+
+ <div style="margin: 20px 0;">
+ <input type="text" id="userId" placeholder="User ID" value="test-user">
+ <button onclick="authenticate()">S'authentifier</button>
+ <button onclick="sendTestAlert()">Envoyer alerte test</button>
+ </div>
+
+ <h2> Alertes reçues</h2>
+ <div id="alerts">
+ <em>En attente d'alertes...</em>
+ </div>
+
+ <h2> Log</h2>
+ <div id="log"></div>
+
+ <script>
+ const statusEl = document.getElementById('connection-status');
+ const authStatusEl = document.getElementById('auth-status');
+ const alertsEl = document.getElementById('alerts');
+ const logEl = document.getElementById('log');
+
+ let alertCount = 0;
+
+ function log(message) {
+ const time = new Date().toLocaleTimeString();
+ logEl.innerHTML = '[' + time + '] ' + message + '<br>' + logEl.innerHTML;
+ console.log(message);
+ }
+
+ // Connexion Socket.IO
+ const socket = io();
+
+ socket.on('connect', () => {
+ log(' Connecté au serveur Socket.IO');
+ statusEl.className = 'status connected';
+ statusEl.textContent = ' Connecté (ID: ' + socket.id + ')';
+ });
+
+ socket.on('disconnect', (reason) => {
+ log(' Déconnecté: ' + reason);
+ statusEl.className = 'status disconnected';
+ statusEl.textContent = '❌ Déconnecté';
+ authStatusEl.style.display = 'none';
+ });
+
+ socket.on('auth_success', (data) => {
+ log(' Authentifié: ' + data.message);
+ authStatusEl.style.display = 'block';
+ authStatusEl.className = 'status authenticated';
+ authStatusEl.textContent = ' Authentifié: ' + data.userId;
+ });
+
+ socket.on('alert', (alert) => {
+ alertCount++;
+ log(' Alerte reçue: ' + alert.action + ' ' + alert.pair);
+
+ if (alertCount === 1) {
+ alertsEl.innerHTML = '';
+ }
+
+ const alertHtml =
+ '<div class="alert-item ' + alert.alertLevel + '">' +
+ '<div class="alert-action ' + alert.action + '">' + alert.action + '</div>' +
+ '<div><strong>' + alert.pair + '</strong></div>' +
+ '<div>Confiance: ' + Math.round(alert.confidence * 100) + '%</div>' +
+ '<div>Niveau: ' + alert.alertLevel + '</div>' +
+ '<div>' + alert.reason + '</div>' +
+ '<div><small>' + new Date(alert.timestamp).toLocaleString() + '</small></div>' +
+ '</div>';
+
+ alertsEl.innerHTML = alertHtml + alertsEl.innerHTML;
+ });
+
+ function authenticate() {
+ const userId = document.getElementById('userId').value;
+ if (userId) {
+ socket.emit('auth', userId);
+ log(' Envoi auth pour: ' + userId);
+ }
+ }
+
+ async function sendTestAlert() {
+ try {
+ const response = await fetch('/test/broadcast-alert', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({})
+ });
+ const result = await response.json();
+ log(' Alerte broadcast: ' + result.message);
+ } catch (error) {
+ log(' Erreur: ' + error.message);
+ }
+ }
+
+ // Auto-auth au chargement
+ socket.on('connect', () => {
+ setTimeout(authenticate, 500);
+ });
+ </script>
+</body>
+</html>`);
+});
+
+// =========================================================
+// ROUTE DE TEST : SIMULER UNE ALERTE
+// =========================================================
+app.post('/test/broadcast-alert', (req, res) => {
+ const testAlert = req.body.alert || {
+ action: 'BUY',
+ pair: 'BTC/EUR',
+ confidence: 0.87,
+ reason: 'Signal de test',
+ alertLevel: 'WARNING',
+ price: 42150.23
+ };
+
+ const count = broadcastAlert(testAlert);
+
+ res.json({
+ success: true,
+ message: `Alerte envoyée à ${count} utilisateur(s) connecté(s)`,
+ alert: testAlert
+ });
+});
+
+// =========================================================
+// GESTION DES ERREURS 404
+// =========================================================
+app.use((req, res) => {
+ res.status(404).json({
+ error: 'Route non trouvée',
+ path: req.path
+ });
+});
+
+// =========================================================
+// GESTION DES ERREURS GLOBALES
+// =========================================================
+app.use((err, req, res, next) => {
+ console.error(' Erreur serveur:', err);
+ res.status(500).json({
+ error: 'Erreur serveur',
+ message: process.env.NODE_ENV === 'development' ? err.message : undefined
+ });
+});
+
+// =========================================================
+// DÉMARRER LE SERVEUR
+// =========================================================
+const PORT = process.env.PORT || 3000;
+
+httpServer.listen(PORT, () => {
+ console.log(`
+╔═══════════════════════════════════════════════════════════╗
+║ WALL-E-TTE SERVER ║
+╠═══════════════════════════════════════════════════════════╣
+║ ║
+║ HTTP : http://localhost:${PORT} ║
+║ Socket : ws://localhost:${PORT} ║
+║ ║
+╠═══════════════════════════════════════════════════════════╣
+║ MODULES ACTIFS : ║
+║ Alerts (Stéphane) → /api/alerts ║
+║ Strategy (Sacha) → À intégrer ║
+║ Price (Valentin) → À intégrer ║
+║ ║
+╠═══════════════════════════════════════════════════════════╣
+║ SOCKET.IO EVENTS : ║
+║ → Client envoie 'auth' avec userId ║
+║ → Client reçoit 'alert' quand signal détecté ║
+║ ║
+╚═══════════════════════════════════════════════════════════╝
+`);
+});
+
+// =========================================================
+// EXPORT POUR LES AUTRES MODULES
+// =========================================================
+// Les autres modules peuvent importer le service alerts
+// pour envoyer des notifications
+export { alertsRepo, alertsService, io };
+
--- /dev/null
+// =========================================================
+// CONFIGURATION DE LA BASE DE DONNÉES
+// =========================================================
+// Crée une connexion réutilisable à MySQL
+// via un pool de connexions (mysql2/promise)
+// =========================================================
+
+import dotenv from "dotenv";
+import mysql from "mysql2/promise";
+import path from "path";
+import { fileURLToPath } from "url";
+
+// =========================================================
+// CONFIGURATION DU CHEMIN
+// =========================================================
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+
+// =========================================================
+// CHARGEMENT DES VARIABLES D'ENVIRONNEMENT
+// =========================================================
+// .env doit être dans server/.env
+dotenv.config({ path: path.resolve(__dirname, "../.env") });
+
+// =========================================================
+// VALIDATION MINIMALE (évite les connexions "vides")
+// =========================================================
+const DB_HOST = process.env.DB_HOST || "127.0.0.1";
+const DB_PORT = Number(process.env.DB_PORT || 3306);
+const DB_USER = process.env.DB_USER || "root";
+const DB_PASSWORD = process.env.DB_PASSWORD || ""; // ✅ bon nom
+const DB_NAME = process.env.DB_NAME || "wallette"; // ✅ bon nom / pas de typo
+
+// =========================================================
+// CRÉATION DU POOL DE CONNEXIONS
+// =========================================================
+const db = mysql.createPool({
+ host: DB_HOST,
+ port: DB_PORT,
+ user: DB_USER,
+ password: DB_PASSWORD,
+ database: DB_NAME,
+
+ waitForConnections: true,
+ connectionLimit: 10,
+ queueLimit: 0,
+});
+
+// =========================================================
+// TEST DE CONNEXION
+// =========================================================
+db.getConnection()
+ .then((connection) => {
+ console.log("✅ Connexion à la base de données réussie");
+ connection.release();
+ })
+ .catch((err) => {
+ console.error("❌ Erreur de connexion à la base de données:", err.message);
+ console.error("👉 Vérifie ton fichier server/.env (host/port/user/password/db)");
+ });
+
+export default db;
\ No newline at end of file
--- /dev/null
+// =========================================================
+// MYSQL ADAPTER - Couche d'accès MySQL
+// =========================================================
+// RÔLE : Contient TOUT le SQL pour MySQL/MariaDB
+// =========================================================
+// Ce fichier isole le SQL du reste du module.
+// Pour utiliser MongoDB, créer un mongo.adapter.js
+// qui implémente les mêmes méthodes.
+// =========================================================
+
+import { v4 as uuidv4 } from 'uuid';
+
+// =========================================================
+// FACTORY FUNCTION : Créer un adapter MySQL
+// =========================================================
+/**
+ * Crée un adapter MySQL pour le module d'alertes
+ *
+ * @param {Object} dbConnection - Connexion MySQL (mysql2/promise)
+ * @param {Object} options - Configuration des noms de tables
+ * @param {string} options.alertsTable - Nom de la table des règles (défaut: 'alert_rules')
+ * @param {string} options.usersTable - Nom de la table users (défaut: 'users')
+ * @param {string} options.eventsTable - Nom de la table events (défaut: 'alert_events')
+ *
+ * @returns {Object} Adapter avec toutes les méthodes d'accès DB
+ *
+ * @example
+ * import db from './config/db.js';
+ * import { createMySQLAdapter } from './adapters/mysql.adapter.js';
+ *
+ * const adapter = createMySQLAdapter(db);
+ * const rules = await adapter.findActiveRulesForSignal('user-123', 1);
+ */
+export function createMySQLAdapter(dbConnection, options = {}) {
+
+ // =========================================================
+ // CONFIGURATION DES NOMS DE TABLES
+ // =========================================================
+ const tables = {
+ alerts: options.alertsTable || 'alert_rules',
+ users: options.usersTable || 'users',
+ events: options.eventsTable || 'alert_events'
+ };
+
+ console.log(`🔌 Adapter MySQL configuré avec tables: ${JSON.stringify(tables)}`);
+
+ // =========================================================
+ // RETOURNER L'OBJET ADAPTER
+ // =========================================================
+ return {
+
+ // =====================================================
+ // 1. RÉCUPÉRER LES RÈGLES D'ALERTE ACTIVES
+ // =====================================================
+ async findActiveRulesForSignal(userId, pairId) {
+ try {
+ const sql = `
+ SELECT
+ ar.*,
+ u.email
+ FROM ${tables.alerts} ar
+ JOIN ${tables.users} u ON ar.user_id = u.user_id
+ WHERE ar.enabled = 1
+ AND ar.user_id = ?
+ AND (ar.pair_id = ? OR ar.pair_id IS NULL)
+ ORDER BY ar.severity DESC
+ `;
+
+ const [rows] = await dbConnection.execute(sql, [userId, pairId]);
+
+ console.log(`[MySQL] Règles trouvées pour user ${userId} : ${rows.length}`);
+ return rows;
+
+ } catch (error) {
+ console.error('[MySQL] Erreur dans findActiveRulesForSignal:', error);
+ throw error;
+ }
+ },
+
+ // =====================================================
+ // 2. SAUVEGARDER UN ÉVÉNEMENT D'ALERTE
+ // =====================================================
+ async saveAlertEvent(eventData) {
+ try {
+ const sql = `
+ INSERT INTO ${tables.events} (
+ alert_event_id,
+ rule_id,
+ timestamp_ms,
+ severity,
+ channel,
+ send_status,
+ title,
+ message,
+ signal_action,
+ confidence,
+ correlation_id,
+ created_at_ms
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+ `;
+
+ const values = [
+ eventData.alert_event_id || uuidv4(),
+ eventData.rule_id,
+ eventData.timestamp_ms || Date.now(),
+ eventData.severity,
+ eventData.channel,
+ eventData.send_status,
+ eventData.title,
+ eventData.message,
+ eventData.signal_action,
+ eventData.confidence,
+ eventData.correlation_id || null,
+ Date.now()
+ ];
+
+ await dbConnection.execute(sql, values);
+ console.log(`[MySQL] Événement d'alerte sauvegardé : ${eventData.title}`);
+
+ } catch (error) {
+ console.error('[MySQL] Erreur dans saveAlertEvent:', error);
+ throw error;
+ }
+ },
+
+ // =====================================================
+ // 3. METTRE À JOUR LA DERNIÈRE NOTIFICATION
+ // =====================================================
+ async updateLastNotified(ruleId, timestamp) {
+ try {
+ const sql = `
+ UPDATE ${tables.alerts}
+ SET last_notified_at_ms = ?,
+ updated_at_ms = ?
+ WHERE rule_id = ?
+ `;
+
+ await dbConnection.execute(sql, [timestamp, Date.now(), ruleId]);
+ console.log(`[MySQL] Timestamp mis à jour pour rule ${ruleId}`);
+
+ } catch (error) {
+ console.error('[MySQL] Erreur dans updateLastNotified:', error);
+ throw error;
+ }
+ },
+
+ // =====================================================
+ // 4. RÉCUPÉRER L'HISTORIQUE DES ALERTES
+ // =====================================================
+ async getAlertHistory(userId, limit = 50) {
+ try {
+ const sql = `
+ SELECT
+ ae.*,
+ ar.channel,
+ ar.rule_type
+ FROM ${tables.events} ae
+ JOIN ${tables.alerts} ar ON ae.rule_id = ar.rule_id
+ WHERE ar.user_id = ?
+ ORDER BY ae.timestamp_ms DESC
+ LIMIT ?
+ `;
+
+ const [rows] = await dbConnection.execute(sql, [userId, limit]);
+ return rows;
+
+ } catch (error) {
+ console.error('[MySQL] Erreur dans getAlertHistory:', error);
+ throw error;
+ }
+ },
+
+ // =====================================================
+ // 5. CRÉER UNE RÈGLE D'ALERTE (CRUD - Create)
+ // =====================================================
+ /**
+ * 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,
+ user_id,
+ pair_id,
+ enabled,
+ rule_type,
+ severity,
+ min_confidence,
+ channel,
+ params,
+ cooldown_ms,
+ created_at_ms,
+ updated_at_ms
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+ `;
+
+ const values = [
+ ruleId,
+ ruleData.userId,
+ ruleData.pairId || null,
+ ruleData.enabled !== undefined ? ruleData.enabled : 1,
+ ruleData.ruleType || 'SIGNAL_THRESHOLD',
+ ruleData.severity || 'INFO',
+ ruleData.minConfidence || 0.7,
+ ruleData.channel || 'CONSOLE',
+ JSON.stringify(ruleData.params || {}),
+ ruleData.cooldownMs || 60000,
+ now,
+ now
+ ];
+
+ await dbConnection.execute(sql, values);
+ 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 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;
+ }
+ },
+ };
+}
+
--- /dev/null
+// =========================================================
+// ALERTS CONTROLLER - VERSION RÉUTILISABLE
+// =========================================================
+// RÔLE : Gestion des requêtes et réponses HTTP
+// =========================================================
+
+
+/**
+ * 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)
+ *
+ * @returns {Object} Controller avec toutes les méthodes
+ */
+export function createAlertsController(alertsService, alertsRepo) {
+
+ console.log("Controller d'alertes initialisé");
+
+ return {
+
+ // =====================================================
+ // POST /api/alerts/process-signal
+ // =====================================================
+ /**
+ * 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
+ * }
+ */
+ async processSignal(req, res) {
+ try {
+ // ==================================================================================
+ // 1. RÉCUPÉRER LES DONNÉES DU BODY
+ // ==================================================================================
+ const signal = req.body;
+
+ // ==================================================================================
+ // 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
+ });
+ }
+
+ // 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(', ')}`
+ });
+ }
+
+ // 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(', ')}`
+ });
+ }
+
+ // 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'
+ });
+ }
+
+ // =================================================================================
+ // 3. APPELER LE SERVICE (injecté)
+ // =================================================================================
+ // Le service contient toute la logique
+ await alertsService.processSignal(signal);
+
+ // =================================================================================
+ // 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
+ }
+ });
+
+ } 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
+ });
+ }
+ },
+
+ // =====================================================
+ // GET /api/alerts/rules/:userId
+ // =====================================================
+ /**
+ * Récupère toutes les règles d'alerte d'un utilisateur
+ *
+ * Utile pour afficher la configuration dans l'interface
+ */
+ async getRules(req, res) {
+ try {
+ const { userId } = req.params;
+
+ // Validation
+ if (!userId) {
+ return res.status(400).json({
+ success: false,
+ error: 'userId manquant'
+ });
+ }
+
+ // Récupérer les règles (via repo injecté)
+ const rules = await alertsRepo.findActiveRulesForSignal(userId, null);
+
+ res.status(200).json({
+ success: true,
+ count: rules.length,
+ rules: rules
+ });
+
+ } catch (error) {
+ console.error('Erreur dans getRules controller:', error);
+
+ res.status(500).json({
+ success: false,
+ error: 'Erreur serveur'
+ });
+ }
+ },
+
+ // =====================================================
+ // GET /api/alerts/history/:userId
+ // =====================================================
+ /**
+ * Récupère l'historique des alertes d'un utilisateur
+ */
+ async getHistory(req, res) {
+ try {
+ const { userId } = req.params;
+ const limit = parseInt(req.query.limit) || 50;
+
+ if (!userId) {
+ return res.status(400).json({
+ success: false,
+ error: 'userId manquant'
+ });
+ }
+
+ // Récupérer l'historique (via repo injecté)
+ const history = await alertsRepo.getAlertHistory(userId, limit);
+
+ res.status(200).json({
+ success: true,
+ count: history.length,
+ history: history
+ });
+
+ } catch (error) {
+ console.error('Erreur dans getHistory 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
+ });
+ }
+ }
+ };
+}
+
--- /dev/null
+// =========================================================
+// ALERTS REPOSITORY - VERSION AVEC ADAPTER
+// =========================================================
+// RÔLE : Interface entre le Service et l'Adapter
+// =========================================================
+// Le repo ne fait PLUS de SQL directement.
+// Il délègue à l'adapter injecté.
+// =========================================================
+
+// =========================================================
+// FACTORY FUNCTION : Créer un repository
+// =========================================================
+/**
+ * Crée un repository d'alertes réutilisable
+ *
+ * @param {Object} adapter - Adapter de base de données (MySQL, MongoDB, etc.)
+ * @returns {Object} Repository avec toutes les méthodes
+ *
+ * @example
+ * // Avec MySQL
+ * import { createMySQLAdapter } from './adapters/mysql.adapter.js';
+ * import { createAlertsRepo } from './alerts.repo.js';
+ *
+ * const adapter = createMySQLAdapter(db);
+ * const repo = createAlertsRepo(adapter);
+ *
+ * @example
+ * // Avec MongoDB (adapter à créer)
+ * import { createMongoAdapter } from './adapters/mongo.adapter.js';
+ *
+ * const adapter = createMongoAdapter(mongoClient);
+ * const repo = createAlertsRepo(adapter);
+ */
+export function createAlertsRepo(adapter) {
+
+ console.log(`Repository d'alertes initialisé`);
+
+// =========================================================
+// RETOURNER L'OBJET REPOSITORY
+// =========================================================
+ return {
+
+ // =====================================================
+ // 1. RÉCUPÉRER LES RÈGLES D'ALERTE ACTIVES
+ // =====================================================
+ /**
+ * Récupère les règles d'alerte pour un signal donné
+ * @param {string} userId - ID de l'utilisateur
+ * @param {number} pairId - ID de la paire crypto
+ * @returns {Promise<Array>} Liste des règles actives
+ */
+ async findActiveRulesForSignal(userId, pairId) {
+ return adapter.findActiveRulesForSignal(userId, pairId);
+ },
+
+ // =====================================================
+ // 2. SAUVEGARDER UN ÉVÉNEMENT D'ALERTE
+ // =====================================================
+ /**
+ * Enregistre qu'une alerte a été envoyée
+ * @param {Object} eventData - Données de l'événement
+ */
+ async saveAlertEvent(eventData) {
+ return adapter.saveAlertEvent(eventData);
+ },
+
+ // =====================================================
+ // 3. METTRE À JOUR LA DERNIÈRE NOTIFICATION
+ // =====================================================
+ /**
+ * Met à jour le timestamp pour le cooldown (anti-spam)
+ * @param {string} ruleId - ID de la règle
+ * @param {number} timestamp - Timestamp en millisecondes
+ */
+ async updateLastNotified(ruleId, timestamp) {
+ return adapter.updateLastNotified(ruleId, timestamp);
+ },
+
+ // =====================================================
+ // 4. RÉCUPÉRER L'HISTORIQUE DES ALERTES
+ // =====================================================
+ /**
+ * Récupère les dernières alertes d'un utilisateur
+ * @param {string} userId - ID de l'utilisateur
+ * @param {number} limit - Nombre max de résultats
+ * @returns {Promise<Array>} Historique des alertes
+ */
+ async getAlertHistory(userId, limit = 50) {
+ return adapter.getAlertHistory(userId, limit);
+ },
+
+ // =====================================================
+ // 5. CRÉER UNE RÈGLE D'ALERTE (CRUD - Create)
+ // =====================================================
+ /**
+ * 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 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);
+ },
+ };
+}
--- /dev/null
+// =========================================================
+// ALERTS ROUTER - VERSION RÉUTILISABLE
+// =========================================================
+// RÔLE : Définir les routes (endpoints) de l'API
+// =========================================================
+
+
+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)
+ *
+ * @returns {express.Router} Router Express configuré
+ */
+export function createAlertsRouter(alertsController) {
+
+ const router = express.Router();
+
+ console.log("Router d'alertes initialisé");
+
+// =========================================================
+ // DÉFINITION DES ROUTES
+// =========================================================
+ // 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"
+ * }
+ */
+ 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);
+
+// =============================================================================
+ // 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
+// =============================================================================
+ 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;
+}
+
--- /dev/null
+// =========================================================
+// ALERTS SERVICE - VERSION RÉUTILISABLE
+// =========================================================
+// RÔLE : Logique métier du système d'alertes
+// =========================================================
+// PRINCIPE : INJECTION DE DÉPENDANCES
+// Au lieu d'importer le Repository directement, on le reçoit en paramètre
+// =========================================================
+
+import { v4 as uuidv4 } from 'uuid';
+
+// Import des canaux
+import { sendAlertEmail } from './channels/mailer.js';
+import { sendConsoleAlert } from './channels/console.js';
+import { sendTelegramAlert } from './channels/telegram.js';
+import { sendDiscordAlert } from './channels/discord.js';
+import { sendWebAlert } from './channels/web.js';
+
+// =========================================================
+// FACTORY FUNCTION : Créer un service
+// =========================================================
+/**
+ * Crée un service d'alertes réutilisable
+ *
+ * @param {Object} alertsRepo - Repository d'alertes (créé par createAlertsRepo)
+ * @param {Object} options - Configuration optionnelle
+ * @param {number} options.defaultCooldown - Cooldown par défaut en ms (60000 = 1min)
+ *
+ * @returns {Object} Service avec toutes les méthodes
+ *
+ * @example
+ * // Dans Wall-e-tte
+ * import db from './config/db.js';
+ * import { createAlertsRepo } from './modules/alerts/alerts.repo.js';
+ * import { createAlertsService } from './modules/alerts/alerts.service.js';
+ *
+ * const repo = createAlertsRepo(db);
+ * const service = createAlertsService(repo);
+ *
+ * await service.processSignal(signal);
+ *
+ * @example
+ * // Dans un autre projet
+ * const repo = createAlertsRepo(strangersDb, { ... });
+ * const service = createAlertsService(repo);
+ * await service.processSignal(signal); // Marche pareil !
+ */
+export function createAlertsService(alertsRepo, options = {}) {
+
+ console.log('🔧 Service d\'alertes initialisé');
+
+ // ─────────────────────────────────────────────────────
+ // RETOURNER L'OBJET SERVICE
+ // ─────────────────────────────────────────────────────
+ return {
+
+ // =====================================================
+ // 1. VÉRIFIER SI UNE ALERTE DOIT ÊTRE ENVOYÉE
+ // =====================================================
+ /**
+ * Détermine si une alerte doit être envoyée selon la règle
+ *
+ * Cette fonction applique les RÈGLES MÉTIER :
+ * - Vérification de la confidence minimum
+ * - Vérification du cooldown (anti-spam)
+ *
+ * @param {Object} rule - Règle d'alerte
+ * @param {Object} signal - Signal reçu
+ * @returns {boolean} true si l'alerte doit être envoyée
+ */
+ shouldSendAlert(rule, signal) {
+ console.log(`Vérification règle ${rule.rule_id}...`);
+
+ // ─────────────────────────────────────────────────
+ // CRITÈRE 1 : Vérifier la confidence minimum
+ // ─────────────────────────────────────────────────
+ // Si le signal a une confidence trop basse, on n'envoie pas
+ // Exemple : rule.min_confidence = 0.80 (80%)
+ // signal.confidence = 0.75 (75%)
+ // → Ne pas envoyer (75% < 80%)
+
+ if (signal.confidence < rule.min_confidence) {
+ console.log(`Confidence trop basse`);
+ console.log(`Signal : ${(signal.confidence * 100).toFixed(2)}%`);
+ console.log(`Minimum: ${(rule.min_confidence * 100).toFixed(2)}%`);
+ return false;
+ }
+
+ // ─────────────────────────────────────────────────
+ // CRITÈRE 2 : Vérifier le cooldown (anti-spam)
+ // ─────────────────────────────────────────────────
+ // Le cooldown évite d'envoyer 100 emails en 1 minute
+ // Exemple : cooldown_ms = 60000 (1 minute)
+ // Dernière alerte envoyée il y a 30 secondes
+ // → Trop tôt, ne pas envoyer
+
+ const now = Date.now();
+
+ if (rule.last_notified_at_ms) {
+ // Calculer le temps écoulé depuis la dernière alerte
+ const timeSinceLastAlert = now - rule.last_notified_at_ms;
+
+ // Si pas assez de temps écoulé, ne pas envoyer
+ if (timeSinceLastAlert < rule.cooldown_ms) {
+ const remainingMs = rule.cooldown_ms - timeSinceLastAlert;
+ const remainingSec = Math.ceil(remainingMs / 1000);
+
+ console.log(` ⏳ Cooldown actif`);
+ console.log(` Attendre encore ${remainingSec}s`);
+ return false;
+ }
+ }
+
+ // ─────────────────────────────────────────────────
+ // TOUS LES CRITÈRES SONT OK !
+ // ─────────────────────────────────────────────────
+ console.log(` Tous les critères sont remplis`);
+ return true;
+ },
+
+ // =====================================================
+ // 2. ENVOYER UNE ALERTE VIA LE BON CANAL
+ // =====================================================
+ /**
+ * Envoie une alerte via le canal spécifié dans la règle
+ *
+ * Cette fonction :
+ * - Choisit la bonne fonction d'envoi (email, telegram, etc.)
+ * - Prépare les paramètres spécifiques au canal
+ * - Gère les erreurs d'envoi
+ *
+ * @param {Object} rule - Règle d'alerte avec le canal
+ * @param {Object} signal - Signal à envoyer
+ * @returns {Promise<string>} 'SENT' ou 'FAILED'
+ */
+ async sendViaChannel(rule, signal) {
+ console.log(`Envoi via ${rule.channel}...`);
+
+ let status = 'SENT';
+
+ try {
+ // ─────────────────────────────────────────────
+ // SWITCH selon le canal configuré
+ // ─────────────────────────────────────────────
+ switch (rule.channel) {
+
+ // ── EMAIL ────────────────────────────────
+ case 'EMAIL':
+ // Le destinataire est l'email de l'utilisateur
+ // On l'a récupéré via le JOIN dans la requête SQL
+ await sendAlertEmail(rule.email, signal);
+ break;
+
+ // ── TELEGRAM ─────────────────────────────
+ case 'TELEGRAM':
+ // Le chatId est stocké dans rule.params (JSON)
+ // Exemple de params : {"chatId": "123456789"}
+ const params = JSON.parse(rule.params);
+ const chatId = params.chatId;
+
+ if (!chatId) {
+ console.error(' Pas de chatId dans params');
+ status = 'FAILED';
+ break;
+ }
+
+ await sendTelegramAlert(chatId, signal);
+ break;
+
+ // ── DISCORD ──────────────────────────────
+ case 'DISCORD':
+ const discordParams = JSON.parse(rule.params);
+ const webhookUrl = discordParams.webhookUrl;
+
+ if (!webhookUrl) {
+ console.error(' Pas de webhookUrl dans params');
+ status = 'FAILED';
+ break;
+ }
+
+ await sendDiscordAlert(webhookUrl, signal);
+ break;
+
+ // ── CONSOLE ──────────────────────────────
+ case 'CONSOLE':
+ // Pas de paramètres nécessaires
+ await sendConsoleAlert(signal);
+ break;
+
+ // ── WEB ──────────────────────────────────
+ case 'WEB':
+ // Envoyer à l'interface web de l'utilisateur
+ await sendWebAlert(rule.user_id, signal);
+ break;
+
+ // ── CANAL INCONNU ────────────────────────
+ default:
+ console.warn(` Canal inconnu: ${rule.channel}`);
+ status = 'FAILED';
+ }
+
+ } catch (error) {
+ // ─────────────────────────────────────────────
+ // GESTION DES ERREURS
+ // ─────────────────────────────────────────────
+ console.error(` Erreur lors de l'envoi via ${rule.channel}:`);
+ console.error(` ${error.message}`);
+ status = 'FAILED';
+ }
+
+ return status;
+ },
+
+ // =====================================================
+ // 3. TRAITER UN SIGNAL ET ENVOYER LES ALERTES
+ // =====================================================
+ /**
+ * Fonction principale qui traite un signal crypto
+ *
+ * WORKFLOW :
+ * 1. Récupérer les règles actives pour ce signal
+ * 2. Pour chaque règle :
+ * a. Vérifier si alerte doit être envoyée
+ * b. Envoyer via le bon canal
+ * c. Enregistrer dans l'historique
+ * d. Mettre à jour le timestamp
+ *
+ * @param {Object} signal - Signal crypto reçu
+ * @param {string} signal.userId - ID utilisateur
+ * @param {number} signal.pairId - ID de la paire
+ * @param {string} signal.action - BUY, SELL, HOLD, STOP_LOSS
+ * @param {number} signal.confidence - 0-1
+ * @param {string} signal.criticality - CRITICAL, WARNING, INFO
+ * @param {string} signal.reason - Explication
+ */
+ async processSignal(signal) {
+ console.log('\n' + '═'.repeat(80));
+ console.log(` TRAITEMENT DU SIGNAL`);
+ console.log(` Action : ${signal.action}`);
+ console.log(` Paire : ${signal.pair}`);
+ console.log(` User : ${signal.userId}`);
+ console.log('═'.repeat(80) + '\n');
+
+ try {
+ // ─────────────────────────────────────────────
+ // ÉTAPE 1 : Récupérer les règles actives
+ // ─────────────────────────────────────────────
+ // Utilise le repository injecté (pas d'import hardcodé)
+ const rules = await alertsRepo.findActiveRulesForSignal(
+ signal.userId,
+ signal.pairId
+ );
+
+ if (rules.length === 0) {
+ 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`);
+
+ // ─────────────────────────────────────────────
+ // ÉTAPE 2 : Traiter chaque règle
+ // ─────────────────────────────────────────────
+ let sentCount = 0;
+
+ for (const rule of rules) {
+ console.log(`┌─ Règle: ${rule.rule_type} (${rule.channel})`);
+
+ // Vérifier si on doit envoyer
+ if (!this.shouldSendAlert(rule, signal)) {
+ console.log(`└─ Alerte NON envoyée\n`);
+ continue;
+ }
+
+ // Envoyer via le canal
+ const status = await this.sendViaChannel(rule, signal);
+
+ // Préparer les données de l'événement
+ const eventData = {
+ alert_event_id: uuidv4(),
+ rule_id: rule.rule_id,
+ timestamp_ms: Date.now(),
+ severity: signal.criticality,
+ channel: rule.channel,
+ send_status: status,
+ title: `${signal.action} ${signal.pair}`,
+ message: signal.reason,
+ signal_action: signal.action,
+ confidence: signal.confidence,
+ correlation_id: signal.correlationId || null
+ };
+
+ // Sauvegarder dans alert_events (via repo injecté)
+ await alertsRepo.saveAlertEvent(eventData);
+
+ // Mettre à jour last_notified_at si envoi réussi (via repo injecté)
+ if (status === 'SENT') {
+ await alertsRepo.updateLastNotified(rule.rule_id, Date.now());
+ sentCount++;
+ console.log(`└─ Alerte envoyée avec succès\n`);
+ } else {
+ console.log(`└─ Échec de l'envoi\n`);
+ }
+ }
+
+ // ─────────────────────────────────────────────
+ // RÉSUMÉ FINAL
+ // ─────────────────────────────────────────────
+ console.log('═'.repeat(80));
+ 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(error);
+ throw error; // Remonter l'erreur pour que le controller puisse la gérer
+ }
+ }
+ };
+}
--- /dev/null
+// =========================================================
+// CANAL CONSOLE - Notifications dans la console
+// =========================================================
+// RÔLE : Afficher les alertes dans le terminal
+// =========================================================
+// UTILITÉ :
+// - Développement et debugging
+// - Tests rapides sans configurer email/telegram
+// - Logs pour surveillance serveur
+// =========================================================
+
+// =========================================================
+// CODES COULEUR ANSI POUR LE TERMINAL (merci GPT)
+// =========================================================
+// Ces codes permettent de colorer le texte dans la console
+const COLORS = {
+ RESET: '\x1b[0m',
+
+ // Couleurs de texte
+ RED: '\x1b[31m',
+ GREEN: '\x1b[32m',
+ YELLOW: '\x1b[33m',
+ BLUE: '\x1b[34m',
+ MAGENTA: '\x1b[35m',
+ CYAN: '\x1b[36m',
+ WHITE: '\x1b[37m',
+
+ // Styles
+ BOLD: '\x1b[1m',
+ DIM: '\x1b[2m',
+
+ // Fond
+ BG_RED: '\x1b[41m',
+ BG_YELLOW: '\x1b[43m',
+ BG_BLUE: '\x1b[44m'
+};
+
+// =========================================================
+// FONCTION PRINCIPALE : Afficher une alerte dans la console
+// =========================================================
+/**
+ * Affiche une alerte formatée dans la console
+ *
+ * @param {Object} signal - Objet contenant les infos du signal
+ * @param {string} signal.action - BUY, SELL, HOLD, STOP_LOSS
+ * @param {string} signal.pair - Paire crypto (ex: BTC/EUR)
+ * @param {number} signal.confidence - Niveau de confiance (0-1)
+ * @param {string} signal.reason - Raison de l'alerte
+ * @param {string} signal.criticality - CRITICAL, WARNING, INFO
+ *
+ * @returns {Promise<string>} Toujours 'SENT' (console ne peut pas échouer)
+ */
+export async function sendConsoleAlert(signal) {
+
+ // =====================================================
+ // 1. DÉTERMINER LA COULEUR SELON LA CRITICITÉ
+ // =====================================================
+ let color;
+ let bgColor;
+ let icon;
+
+ switch (signal.criticality) {
+ case 'CRITICAL':
+ color = COLORS.RED;
+ bgColor = COLORS.BG_RED;
+ icon = '🚨';
+ break;
+ case 'WARNING':
+ color = COLORS.YELLOW;
+ bgColor = COLORS.BG_YELLOW;
+ icon = '⚠️';
+ break;
+ case 'INFO':
+ default:
+ color = COLORS.BLUE;
+ bgColor = COLORS.BG_BLUE;
+ icon = 'ℹ️';
+ break;
+ }
+
+ // =====================================================
+ // 2. FORMATER LA CONFIDENCE EN POURCENTAGE
+ // =====================================================
+ const confidencePercent = (parseFloat(signal.confidence) * 100).toFixed(2);
+
+ // =====================================================
+ // 3. CRÉER LE TIMESTAMP
+ // =====================================================
+ const timestamp = new Date().toLocaleString('fr-FR', {
+ day: '2-digit',
+ month: '2-digit',
+ year: 'numeric',
+ hour: '2-digit',
+ minute: '2-digit',
+ second: '2-digit'
+ });
+
+ // =====================================================
+ // 4. AFFICHER L'ALERTE AVEC MISE EN FORME
+ // =====================================================
+ console.log('\n'); // Ligne vide avant
+ console.log('═'.repeat(80)); // Ligne de séparation
+
+ // En-tête avec couleur de fond
+ console.log(
+ `${bgColor}${COLORS.WHITE}${COLORS.BOLD} ` +
+ `${icon} ALERTE ${signal.criticality} `.padEnd(80) +
+ `${COLORS.RESET}`
+ );
+
+ // Ligne de séparation
+ console.log('─'.repeat(80));
+
+ // Action recommandée (en couleur et gras)
+ console.log(
+ `${COLORS.BOLD}${color}ACTION :${COLORS.RESET} ` +
+ `${getActionEmoji(signal.action)} ${signal.action}`
+ );
+
+ // Paire crypto
+ console.log(
+ `${COLORS.BOLD}${COLORS.CYAN}PAIRE :${COLORS.RESET} ` +
+ `${signal.pair}`
+ );
+
+ // Niveau de confiance avec barre visuelle
+ const confidenceBar = createProgressBar(signal.confidence);
+ console.log(
+ `${COLORS.BOLD}${COLORS.MAGENTA}CONFIANCE :${COLORS.RESET} ` +
+ `${confidenceBar} ${confidencePercent}%`
+ );
+
+ // Ligne de séparation
+ console.log('─'.repeat(80));
+
+ // Raison (analyse)
+ console.log(`${COLORS.BOLD}ANALYSE :${COLORS.RESET}`);
+ console.log(wrapText(signal.reason, 76)); // Wrap à 76 caractères
+
+ // Ligne de séparation
+ console.log('─'.repeat(80));
+
+ // Timestamp en grisé
+ console.log(`${COLORS.DIM}📅 ${timestamp}${COLORS.RESET}`);
+
+ // Ligne de fin
+ console.log('═'.repeat(80));
+ console.log('\n'); // Ligne vide après
+
+ // =====================================================
+ // 5. RETOURNER LE STATUT
+ // =====================================================
+ // La console ne peut jamais échouer, donc toujours SENT
+ return 'SENT';
+}
+
+// =========================================================
+// FONCTIONS UTILITAIRES
+// =========================================================
+
+/**
+ * Retourne l'emoji correspondant à l'action
+ */
+function getActionEmoji(action) {
+ const emojis = {
+ 'BUY': '🟢',
+ 'SELL': '🔴',
+ 'HOLD': '🟡',
+ 'STOP_LOSS': '🛑'
+ };
+ return emojis[action] || '📊';
+}
+
+/**
+ * Crée une barre de progression visuelle pour la confidence
+ * Exemple : ████████░░ (80%)
+ */
+function createProgressBar(confidence, length = 20) {
+ const filled = Math.round(confidence * length);
+ const empty = length - filled;
+
+ const bar =
+ COLORS.GREEN + '█'.repeat(filled) +
+ COLORS.DIM + '░'.repeat(empty) +
+ COLORS.RESET;
+
+ return `[${bar}]`;
+}
+
+/**
+ * Coupe le texte en plusieurs lignes si trop long
+ * Utile pour que la raison ne dépasse pas la largeur du terminal
+ */
+function wrapText(text, maxWidth) {
+ const words = text.split(' ');
+ const lines = [];
+ let currentLine = ' '; // Indentation
+
+ words.forEach(word => {
+ if ((currentLine + word).length > maxWidth) {
+ lines.push(currentLine);
+ currentLine = ' ' + word + ' ';
+ } else {
+ currentLine += word + ' ';
+ }
+ });
+
+ if (currentLine.trim()) {
+ lines.push(currentLine);
+ }
+
+ return lines.join('\n');
+}
+
+// =========================================================
+// EXPORT D'UNE FONCTION DE TEST
+// =========================================================
+/**
+ * Fonction pour tester rapidement le canal console
+ * Usage : node test-console.js
+ */
+export function testConsoleAlert() {
+ const fakeSignal = {
+ action: 'BUY',
+ pair: 'BTC/EUR',
+ confidence: 0.87,
+ criticality: 'WARNING',
+ reason: 'Les indicateurs techniques montrent une tendance haussière forte. Le RSI est à 45 (zone neutre), le MACD vient de croiser la ligne de signal vers le haut, et le volume des échanges augmente significativement.'
+ };
+
+ sendConsoleAlert(fakeSignal);
+}
+
--- /dev/null
+// !!! A FAIRE !!!
+
+// =========================================================
+// CANAL DISCORD - Notifications via Discord Webhook
+// =========================================================
+// RÔLE : Envoyer des alertes dans un channel Discord
+// =========================================================
+
+/**
+ * Envoie une alerte via Discord Webhook
+ *
+ * @param {string} webhookUrl - URL du webhook Discord
+ * @param {Object} signal - Signal crypto
+ * @returns {Promise<string>} 'SENT' ou 'FAILED'
+ */
+export async function sendDiscordAlert(webhookUrl, signal) {
+ // TODO: À implémenter si nécessaire
+
+ console.log('💬 Canal Discord pas encore implémenté');
+
+ return 'FAILED';
+
+}
--- /dev/null
+// =========================================================
+// CANAL EMAIL - Notifications par email
+// =========================================================
+// RÔLE : Envoyer des alertes par email avec Nodemailer
+// =========================================================
+
+import nodemailer from 'nodemailer';
+
+// =========================================================
+// FONCTION PRINCIPALE : Envoyer une alerte par email
+// =========================================================
+/**
+ * Envoie une alerte par email
+ *
+ * @param {string} to - Adresse email du destinataire
+ * @param {Object} signal - Objet contenant les infos du signal
+ * @param {string} signal.action - BUY, SELL, HOLD, STOP_LOSS
+ * @param {string} signal.pair - Paire crypto (ex: BTC/EUR)
+ * @param {number} signal.confidence - Niveau de confiance (0-1)
+ * @param {string} signal.reason - Raison de l'alerte
+ * @param {string} signal.criticality - CRITICAL, WARNING, INFO
+ *
+ * @returns {Promise<string>} 'SENT' ou 'FAILED'
+ */
+export async function sendAlertEmail(to, signal) {
+
+ // =====================================================
+ // 1. CRÉER LE TRANSPORTEUR SMTP
+ // =====================================================
+ // Le transporteur est l'objet qui envoie les emails
+ // Il se connecte au serveur SMTP (ex: Gmail, Mailtrap, etc.)
+ const transporter = nodemailer.createTransport({
+ host: process.env.MAIL_HOST, // ex: smtp.gmail.com
+ port: parseInt(process.env.MAIL_PORT), // ex: 587
+ secure: process.env.MAIL_PORT === '465', // true pour le port 465, false pour les autres
+ auth: {
+ user: process.env.MAIL_USER, // Ton adresse email
+ pass: process.env.MAIL_PASS // Ton mot de passe ou App Password
+ }
+ });
+
+ try {
+ // =====================================================
+ // 2. FORMATER LES DONNÉES
+ // =====================================================
+ // Convertir la confidence en pourcentage
+ const confidencePercent = (parseFloat(signal.confidence) * 100).toFixed(2);
+
+ // Choisir l'emoji selon l'action
+ const emoji = getEmojiForAction(signal.action);
+
+ // Choisir la couleur selon la criticité
+ const color = getColorForCriticality(signal.criticality);
+
+ // =====================================================
+ // 3. CRÉER LE CONTENU DE L'EMAIL
+ // =====================================================
+ const mailOptions = {
+ from: `"Wall-e-tte Bot 🤖" <${process.env.MAIL_USER}>`,
+ to: to,
+ subject: `${emoji} Alerte ${signal.criticality} : ${signal.action} sur ${signal.pair}`,
+
+ // Version texte (pour clients email sans HTML)
+ text: `
+🚨 ALERTE CRYPTO 🚨
+
+Action recommandée : ${signal.action}
+Paire : ${signal.pair}
+Confiance : ${confidencePercent}%
+Criticité : ${signal.criticality}
+
+Raison :
+${signal.reason}
+
+---
+Wall-e-tte - Votre conseiller crypto
+ `.trim(),
+
+ // Version HTML (plus jolie)
+ html: `
+ <!DOCTYPE html>
+ <html>
+ <head>
+ <style>
+ body {
+ font-family: Arial, sans-serif;
+ background-color: #f4f4f4;
+ margin: 0;
+ padding: 0;
+ }
+ .container {
+ max-width: 600px;
+ margin: 20px auto;
+ background-color: white;
+ border-radius: 10px;
+ overflow: hidden;
+ box-shadow: 0 4px 6px rgba(0,0,0,0.1);
+ }
+ .header {
+ background-color: ${color};
+ color: white;
+ padding: 20px;
+ text-align: center;
+ }
+ .content {
+ padding: 30px;
+ }
+ .alert-box {
+ background-color: #f9f9f9;
+ border-left: 4px solid ${color};
+ padding: 15px;
+ margin: 20px 0;
+ }
+ .footer {
+ background-color: #333;
+ color: white;
+ text-align: center;
+ padding: 15px;
+ font-size: 12px;
+ }
+ .stat {
+ display: inline-block;
+ margin: 10px 20px;
+ }
+ .stat-label {
+ color: #666;
+ font-size: 12px;
+ text-transform: uppercase;
+ }
+ .stat-value {
+ font-size: 24px;
+ font-weight: bold;
+ color: ${color};
+ }
+ </style>
+ </head>
+ <body>
+ <div class="container">
+ <div class="header">
+ <h1>${emoji} ALERTE ${signal.criticality}</h1>
+ </div>
+ <div class="content">
+ <h2>Recommandation : ${signal.action}</h2>
+ <p><strong>Paire :</strong> ${signal.pair}</p>
+
+ <div class="stat">
+ <div class="stat-label">Confiance</div>
+ <div class="stat-value">${confidencePercent}%</div>
+ </div>
+
+ <div class="alert-box">
+ <h3>Analyse :</h3>
+ <p>${signal.reason}</p>
+ </div>
+
+ <p style="color: #666; font-size: 12px;">
+ Timestamp : ${new Date().toLocaleString('fr-FR')}
+ </p>
+ </div>
+ <div class="footer">
+ Wall-e-tte - Votre conseiller crypto automatisé
+ </div>
+ </div>
+ </body>
+ </html>
+ `
+ };
+
+ // =====================================================
+ // 4. ENVOYER L'EMAIL
+ // =====================================================
+ const info = await transporter.sendMail(mailOptions);
+
+ console.log(`Email envoyé avec succès !`);
+ console.log(` → Destinataire : ${to}`);
+ console.log(` → Message ID : ${info.messageId}`);
+
+ return 'SENT';
+
+ } catch (error) {
+ // =====================================================
+ // 5. GESTION DES ERREURS
+ // =====================================================
+ console.error('Erreur lors de l\'envoi de l\'email :');
+ console.error(` → ${error.message}`);
+
+ // Erreurs communes et solutions
+ if (error.message.includes('authentication')) {
+ console.error(' Vérifie MAIL_USER et MAIL_PASS dans .env');
+ } else if (error.message.includes('ECONNREFUSED')) {
+ console.error(' Vérifie MAIL_HOST et MAIL_PORT dans .env');
+ }
+
+ return 'FAILED';
+ }
+}
+
+// =========================================================
+// FONCTIONS UTILITAIRES
+// =========================================================
+
+/**
+ * Retourne un emoji selon l'action recommandée
+ */
+function getEmojiForAction(action) {
+ const emojis = {
+ 'BUY': '🟢',
+ 'SELL': '🔴',
+ 'HOLD': '🟡',
+ 'STOP_LOSS': '🛑'
+ };
+ return emojis[action] || '📊';
+}
+
+/**
+ * Retourne une couleur selon le niveau de criticité
+ */
+function getColorForCriticality(criticality) {
+ const colors = {
+ 'CRITICAL': '#dc3545', // Rouge
+ 'WARNING': '#ffc107', // Orange/Jaune
+ 'INFO': '#17a2b8' // Bleu
+ };
+ return colors[criticality] || '#6c757d';
+}
+
--- /dev/null
+// !!! À FAIRE !!!
+
+
+// =========================================================
+// CANAL TELEGRAM - Notifications via Telegram Bot
+// =========================================================
+// RÔLE : Envoyer des alertes via un bot Telegram
+// =========================================================
+
+// =========================================================
+// TODO : CONFIGURATION
+// =========================================================
+/*
+ Pour utiliser Telegram, il faut :
+
+ 1. Créer un bot Telegram :
+ - Parler à @BotFather sur Telegram
+ - Utiliser /newbot
+ - Récupérer le TOKEN
+
+ 2. Récupérer ton CHAT_ID :
+ - Parler à ton bot
+ - Aller sur : https://api.telegram.org/bot<TOKEN>/getUpdates
+ - Récupérer ton chat_id dans le JSON
+
+ 3. Ajouter dans .env :
+ TELEGRAM_BOT_TOKEN=123456:ABC-DEF...
+ TELEGRAM_CHAT_ID=123456789
+
+ 4. Tester l'envoi :
+ - Utiliser fetch() ou axios
+ - POST vers https://api.telegram.org/bot<TOKEN>/sendMessage
+*/
+
+// =========================================================
+// FONCTION À IMPLÉMENTER
+// =========================================================
+/**
+ * Envoie une alerte via Telegram
+ *
+ * @param {string} chatId - ID du chat Telegram
+ * @param {Object} signal - Signal crypto
+ * @returns {Promise<string>} 'SENT' ou 'FAILED'
+ */
+export async function sendTelegramAlert(chatId, signal) {
+ // TODO: À implémenter ensemble
+
+ console.log('📱 Canal Telegram pas encore implémenté');
+ console.log(` → chatId: ${chatId}`);
+ console.log(` → signal: ${signal.action} sur ${signal.pair}`);
+
+ // Pour l'instant, on retourne FAILED
+ return 'FAILED';
+}
+
--- /dev/null
+// =========================================================
+// CANAL WEB - Notifications temps réel via Socket.IO
+// =========================================================
+// RÔLE : Envoyer des alertes en temps réel aux clients
+// web (Océane) et mobile (Thibaud)
+// =========================================================
+// DÉPENDANCE : socketManager.js doit être initialisé
+// =========================================================
+
+import { sendAlertToUser, isUserConnected } from '../socketManager.js';
+
+/**
+ * Envoie une alerte via WebSocket à un utilisateur
+ *
+ * @param {string} userId - ID de l'utilisateur
+ * @param {Object} signal - Signal crypto à envoyer
+ * @param {string} signal.action - 'BUY', 'SELL', ou 'HOLD'
+ * @param {string} signal.pair - Paire crypto (ex: 'BTC/EUR')
+ * @param {number} signal.confidence - Niveau de confiance (0-1)
+ * @param {string} signal.reason - Raison du signal
+ * @param {string} [signal.criticality] - 'INFO', 'WARNING', 'CRITICAL'
+ * @returns {Promise<string>} 'SENT' ou 'FAILED'
+ *
+ * @example
+ * const status = await sendWebAlert('user-123', {
+ * action: 'BUY',
+ * pair: 'BTC/EUR',
+ * confidence: 0.85,
+ * reason: 'RSI indique une survente',
+ * criticality: 'WARNING'
+ * });
+ */
+export async function sendWebAlert(userId, signal) {
+ // Vérifier si l'utilisateur est connecté
+ if (!isUserConnected(userId)) {
+ console.log(`⚠️ [Web Channel] User ${userId} non connecté - alerte non envoyée`);
+ return 'FAILED';
+ }
+
+ // Formater l'alerte pour les clients
+ const alertPayload = {
+ // Données du signal
+ action: signal.action,
+ pair: signal.pair,
+ confidence: signal.confidence,
+ reason: signal.reason,
+
+ // Métadonnées
+ alertLevel: signal.criticality || 'INFO',
+ timestamp: Date.now(),
+
+ // Prix si disponible
+ ...(signal.priceAtSignal && { price: signal.priceAtSignal })
+ };
+
+ // Envoyer via Socket.IO
+ const sent = sendAlertToUser(userId, alertPayload);
+
+ if (sent) {
+ console.log(`✅ [Web Channel] Alerte envoyée à ${userId} : ${signal.action} ${signal.pair}`);
+ return 'SENT';
+ }
+
+ return 'FAILED';
+}
+
+/**
+ * Vérifie si le canal web est disponible pour un utilisateur
+ * (utile pour le service avant de tenter l'envoi)
+ *
+ * @param {string} userId - ID de l'utilisateur
+ * @returns {boolean} true si l'utilisateur peut recevoir des alertes web
+ */
+export function isWebChannelAvailable(userId) {
+ return isUserConnected(userId);
+}
--- /dev/null
+// =========================================================
+// MODULE ALERTS - Point d'entrée
+// =========================================================
+
+// Export des factory functions principales
+export { createAlertsRepo } from './alerts.repo.js';
+export { createAlertsService } from './alerts.service.js';
+export { createAlertsController } from './alerts.controller.js';
+export { createAlertsRouter } from './alerts.router.js';
+
+// Export des adapters (pour différentes bases de données)
+export { createMySQLAdapter } from './adapters/mysql.adapter.js';
+
+// Export du gestionnaire Socket.IO
+export {
+ initSocketIO,
+ sendAlertToUser,
+ broadcastAlert,
+ isUserConnected,
+ getConnectedUsersCount,
+ getIO
+} from './socketManager.js';
+
+// Export des canaux (utiles si on veut les tester séparément)
+export { sendAlertEmail } from './channels/mailer.js';
+export { sendConsoleAlert } from './channels/console.js';
+export { sendTelegramAlert } from './channels/telegram.js';
+export { sendDiscordAlert } from './channels/discord.js';
+export { sendWebAlert } from './channels/web.js';
+
+// =========================================================
+// EXEMPLE D'UTILISATION RAPIDE
+// =========================================================
+/*
+ DANS WALL-E-TTE :
+
+ // 1. Import du module
+ import db from './config/db.js';
+ import {
+ createAlertsRepo,
+ createAlertsService,
+ createAlertsController,
+ createAlertsRouter
+ } from './modules/alerts/index.js';
+
+ // 2. Initialiser le module (dans l'ordre !)
+ const alertsRepo = createAlertsRepo(db);
+ const alertsService = createAlertsService(alertsRepo);
+ const alertsController = createAlertsController(alertsService, alertsRepo);
+ const alertsRouter = createAlertsRouter(alertsController);
+
+ // 3. Utiliser dans Express
+ app.use('/api/alerts', alertsRouter);
+
+ // 4. Ou utiliser directement le service
+ await alertsService.processSignal(mySignal);
+
+ ═══════════════════════════════════════════════════════
+
+ DANS UN AUTRE PROJET :
+
+ // Exactement pareil !
+ import myDb from './my-db-config.js';
+ import {
+ createAlertsRepo,
+ createAlertsService
+ } from '@wall-e-tte/alerts-module';
+
+ const repo = createAlertsRepo(myDb, {
+ alertsTable: 'my_notifications',
+ usersTable: 'my_users'
+ });
+ const service = createAlertsService(repo);
+
+ await service.processSignal(signal); // Ça marche !
+*/
--- /dev/null
+// =========================================================
+// SOCKET MANAGER - Gestion des connexions WebSocket
+// =========================================================
+// RÔLE : Centraliser la gestion des connexions Socket.IO
+// pour le module d'alertes
+// =========================================================
+// 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 (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 = {}) {
+ // Configuration par défaut + options personnalisées
+ const defaultOptions = {
+ cors: {
+ origin: "*", // En prod, restreindre aux domaines autorisés
+ methods: ["GET", "POST"]
+ }
+ };
+
+ io = new Server(httpServer, { ...defaultOptions, ...options });
+
+ // ─────────────────────────────────────────────────────
+ // GESTION DES CONNEXIONS
+ // ─────────────────────────────────────────────────────
+ io.on('connection', (socket) => {
+ 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})`);
+ return;
+ }
+
+ // Stocker la correspondance userId → socket
+ userSockets.set(userId, socket);
+ socket.userId = userId; // Garder une référence sur le socket
+
+ 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'
+ });
+ });
+
+ // ─────────────────────────────────────────────────
+ // EVENT : Déconnexion
+ // ─────────────────────────────────────────────────
+ socket.on('disconnect', (reason) => {
+ if (socket.userId) {
+ userSockets.delete(socket.userId);
+ console.log(`🔌 [Socket.IO] Déconnexion : ${socket.userId} (${reason})`);
+ console.log(` Utilisateurs connectés : ${userSockets.size}`);
+ }
+ });
+
+ // ─────────────────────────────────────────────────
+ // EVENT : Ping (test de connexion)
+ // ─────────────────────────────────────────────────
+ socket.on('ping_alerts', () => {
+ socket.emit('pong_alerts', {
+ timestamp: Date.now(),
+ status: 'ok'
+ });
+ });
+ });
+
+ 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é
+ */
+export function sendAlertToUser(userId, alertData) {
+ const socket = userSockets.get(userId);
+
+ if (socket) {
+ socket.emit('alert', {
+ ...alertData,
+ timestamp: Date.now()
+ });
+ console.log(` [Socket.IO] Alerte envoyée à ${userId}`);
+ return true;
+ }
+
+ 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é');
+ return 0;
+ }
+
+ io.emit('alert', {
+ ...alertData,
+ timestamp: Date.now()
+ });
+
+ console.log(` [Socket.IO] Alerte broadcast à ${userSockets.size} utilisateurs`);
+ return userSockets.size;
+}
+
+// =========================================================
+// FONCTIONS UTILITAIRES
+// =========================================================
+
+/**
+ * Vérifie si un utilisateur est connecté
+ * @param {string} userId
+ * @returns {boolean}
+ */
+export function isUserConnected(userId) {
+ return userSockets.has(userId);
+}
+
+/**
+ * Retourne le nombre d'utilisateurs connectés
+ * @returns {number}
+ */
+export function getConnectedUsersCount() {
+ return userSockets.size;
+}
+
+/**
+ * Retourne la liste des userIds connectés
+ * @returns {string[]}
+ */
+export function getConnectedUserIds() {
+ return Array.from(userSockets.keys());
+}
+
+/**
+ * Retourne l'instance Socket.IO (pour usage avancé)
+ * @returns {Server|null}
+ */
+export function getIO() {
+ return io;
+}
--- /dev/null
+// =========================================================
+// SCRIPT DE TEST - Module Alerts RÉUTILISABLE
+// =========================================================
+// Ce script teste ton module d'alertes avec la nouvelle
+// architecture réutilisable (injection de dépendances)
+// =========================================================
+// USAGE : node test-alerts.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') });
+
+// =========================================================
+// INITIALISATION 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';
+
+// 1. Créer l'adapter MySQL avec la connexion DB
+const mysqlAdapter = createMySQLAdapter(db);
+
+// 2. Créer le repository avec l'adapter
+const alertsRepo = createAlertsRepo(mysqlAdapter);
+
+// 3. Créer le service avec le repository
+const alertsService = createAlertsService(alertsRepo);
+
+// =========================================================
+// FONCTION DE TEST PRINCIPALE
+// =========================================================
+async function runTest() {
+ console.log('\n' + '═'.repeat(80));
+ console.log('TEST DU MODULE ALERTS');
+ console.log('═'.repeat(80) + '\n');
+
+ try {
+ // =========================================================
+ // ÉTAPE 1 : Vérifier la connexion DB
+ // =========================================================
+ console.log('Test de connexion à la base de données\n');
+
+ // Utilise le repository créé avec injection de dépendances
+ const rules = await alertsRepo.findActiveRulesForSignal('test', 1);
+ console.log(`Connexion DB OK`);
+ console.log(` Règles trouvées : ${rules.length}\n`);
+
+ if (rules.length === 0) {
+ console.log('ATTENTION : Aucune règle trouvée en DB');
+ }
+
+ // =========================================================
+ // ÉTAPE 2 : Test du canal CONSOLE
+ // =========================================================
+ console.log('─'.repeat(80));
+ console.log('Test du canal CONSOLE\n');
+
+ const { sendConsoleAlert } = await import('./modules/alerts/channels/console.js');
+
+ const testSignalConsole = {
+ action: 'BUY',
+ pair: 'BTC/EUR',
+ confidence: 0.87,
+ criticality: 'WARNING',
+ reason: 'Test du canal console : Les indicateurs techniques montrent une tendance haussière. Le RSI est à 45, le MACD vient de croiser.'
+ };
+
+ await sendConsoleAlert(testSignalConsole);
+ console.log('Test CONSOLE réussi\n');
+
+ // =========================================================
+ // ÉTAPE 3 : Test du canal EMAIL
+ // =========================================================
+ console.log('─'.repeat(80));
+ console.log('Test du canal EMAIL\n');
+
+ if (!process.env.MAIL_USER || !process.env.MAIL_PASS) {
+ console.log('Email non configuré (MAIL_USER et MAIL_PASS manquants dans .env)');
+ console.log(' Pour tester configurez vos variables dans .env\n');
+ } else {
+ console.log('Configuration email détectée');
+ console.log(` Host : ${process.env.MAIL_HOST}`);
+ console.log(` User : ${process.env.MAIL_USER}`);
+ console.log(' Envoi d\'un email de test...\n');
+
+ const { sendAlertEmail } = await import('./modules/alerts/channels/mailer.js');
+
+ const testSignalEmail = {
+ action: 'SELL',
+ pair: 'ETH/EUR',
+ confidence: 0.92,
+ criticality: 'CRITICAL',
+ reason: 'Test du canal email : Signal de vente critique détecté. Forte baisse imminente selon les indicateurs.'
+ };
+
+ const status = await sendAlertEmail(process.env.MAIL_USER, testSignalEmail);
+
+ if (status === 'SENT') {
+ console.log('Test EMAIL réussi ! Vérifiez votre boîte mail.\n');
+ } else {
+ console.log('Test EMAIL échoué. Vérifiez votre configuration.\n');
+ }
+ }
+
+ // =========================================================
+ // ÉTAPE 4 : Test du service complet
+ // =========================================================
+ if (rules.length > 0) {
+ console.log('─'.repeat(80));
+ console.log('Test du service complet\n');
+
+ const testSignalService = {
+ userId: rules[0].user_id,
+ pairId: rules[0].pair_id || 1,
+ pair: 'BTC/EUR',
+ action: 'BUY',
+ confidence: 0.85,
+ criticality: 'INFO',
+ reason: 'Test complet du service : Signal généré automatiquement pour tester le workflow complet.',
+ priceAtSignal: 45000.50
+ };
+
+ // Utilise le service créé avec injection de dépendances
+ await alertsService.processSignal(testSignalService);
+ console.log('Test SERVICE complet réussi !\n');
+ }
+
+ // =========================================================
+ // RÉSUMÉ FINAL
+ // =========================================================
+ console.log('═'.repeat(80));
+ console.log('TOUS LES TESTS SONT TERMINÉS');
+ console.log('═'.repeat(80));
+ console.log('\n RÉSUMÉ :');
+ console.log(' Connexion DB');
+ console.log(' Canal CONSOLE');
+ console.log(` ${process.env.MAIL_USER ? 'OK : ' : 'FAIL : '} Canal EMAIL ${process.env.MAIL_USER ? '' : '(non configuré)'}`);
+ console.log(` ${rules.length > 0 ? 'OK : ' : 'FAIL : '} Service complet ${rules.length > 0 ? '' : '(pas de règles en DB)'}`);
+
+ } catch (error) {
+ console.error('\n ERREUR LORS DU TEST :');
+ console.error(error);
+ console.error('\n VÉRIFICATIONS :');
+ console.error(' - Le fichier .env existe et contient les bonnes valeurs ?');
+ console.error(' - La base de données est démarrée ?');
+ console.error(' - Les tables sont créées (schema.sql) ?');
+ console.error('\n');
+ } finally {
+ // Quitter proprement
+ process.exit();
+ }
+}
+
+// =========================================================
+// LANCEMENT DU TEST
+// =========================================================
+runTest();
--- /dev/null
+// =========================================================
+// INITIALISATION DU MODULE ALERTS
+// =========================================================
+// Ce fichier montre comment initialiser et utiliser
+// le module d'alertes dans Wall-e-tte
+// =========================================================
+
+import db from '../config/db.js';
+import {
+ createAlertsController,
+ createAlertsRepo,
+ createAlertsRouter,
+ createAlertsService,
+ createMySQLAdapter
+} from './alerts/index.js';
+
+// =========================================================
+// FONCTION D'INITIALISATION
+// =========================================================
+/**
+ * Initialise tout le module d'alertes
+ *
+ * @param {Object} dbConnection - Connexion à la base de données
+ * @param {Object} options - Options de configuration
+ * @returns {Object} { repo, service, controller, router }
+ */
+export function initializeAlertsModule(dbConnection = db, options = {}) {
+
+ console.log('\n' + '═'.repeat(80)); // Pour creer une ligne de separation horizontale | /n pour saut de ligne
+ console.log('INITIALISATION DU MODULE ALERTS');
+ console.log('═'.repeat(80) + '\n');
+
+ // ─────────────────────────────────────────────────────
+ // É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(adapter, {
+ alertsTable: options.alertsTable || 'alert_rules',
+ usersTable: options.usersTable || 'users',
+ eventsTable: options.eventsTable || 'alert_events'
+ });
+
+ // ─────────────────────────────────────────────────────
+ // É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
+ // (Plus besoin de db car le CRUD passe par le repo)
+ 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,
+ repo: alertsRepo,
+ service: alertsService,
+ controller: alertsController,
+ router: alertsRouter
+ };
+}
+
+// =========================================================
+// EXPORT PAR DÉFAUT
+// =========================================================
+// Pour Wall-e-tte, initialiser avec la DB par défaut
+export default initializeAlertsModule();
+
+// =========================================================
+// EXEMPLE D'UTILISATION
+// =========================================================
+/*
+ UTILISATION SIMPLE (dans Wall-e-tte) :
+
+ // Option 1 : Import par défaut (utilise la DB par défaut)
+ import alerts from './modules/init-alerts.js';
+
+ app.use('/api/alerts', alerts.router);
+ await alerts.service.processSignal(signal);
+
+================================================================================
+
+ // Option 2 : Import avec configuration personnalisée
+ import { initializeAlertsModule } from './modules/init-alerts.js';
+ import myDb from './my-db.js';
+
+ const alerts = initializeAlertsModule(myDb, {
+ alertsTable: 'mes_alertes',
+ defaultCooldown: 120000 // 2 minutes
+ });
+
+ app.use('/api/alerts', alerts.router);
+
+================================================================================
+
+ UTILISATION DANS UN AUTRE PROJET :
+
+ // Copier ce fichier et changer juste l'import de 'db'
+ import strangerDb from './stranger-db-config.js';
+ import {
+ createAlertsRepo,
+ createAlertsService
+ } from '@wall-e-tte/alerts';
+
+ const repo = createAlertsRepo(strangerDb);
+ const service = createAlertsService(repo);
+
+*/
--- /dev/null
+{
+ "name": "wall-e-tte-server",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "wall-e-tte-server",
+ "version": "1.0.0",
+ "license": "ISC",
+ "dependencies": {
+ "cors": "^2.8.5",
+ "dotenv": "^16.4.5",
+ "express": "^4.18.2",
+ "mysql2": "^3.6.5",
+ "nodemailer": "^8.0.1",
+ "socket.io": "^4.8.3",
+ "uuid": "^9.0.1"
+ }
+ },
+ "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",
+ "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-types": "~2.1.34",
+ "negotiator": "0.6.3"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/array-flatten": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
+ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
+ "license": "MIT"
+ },
+ "node_modules/aws-ssl-profiles": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz",
+ "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==",
+ "license": "MIT",
+ "engines": {
+ "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-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "~3.1.2",
+ "content-type": "~1.0.5",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "~1.2.0",
+ "http-errors": "~2.0.1",
+ "iconv-lite": "~0.4.24",
+ "on-finished": "~2.4.1",
+ "qs": "~6.14.0",
+ "raw-body": "~2.5.3",
+ "type-is": "~1.6.18",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
+ "node_modules/bytes": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/call-bound": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "get-intrinsic": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/content-disposition": {
+ "version": "0.5.4",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
+ "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "5.2.1"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/content-type": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
+ "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie-signature": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.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",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/denque": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
+ "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/depd": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/destroy": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
+ "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
+ "node_modules/dotenv": {
+ "version": "16.6.1",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
+ "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://dotenvx.com"
+ }
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/ee-first": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
+ "license": "MIT"
+ },
+ "node_modules/encodeurl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
+ "license": "MIT",
+ "engines": {
+ "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",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/escape-html": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
+ "license": "MIT"
+ },
+ "node_modules/etag": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/express": {
+ "version": "4.22.1",
+ "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
+ "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==",
+ "license": "MIT",
+ "dependencies": {
+ "accepts": "~1.3.8",
+ "array-flatten": "1.1.1",
+ "body-parser": "~1.20.3",
+ "content-disposition": "~0.5.4",
+ "content-type": "~1.0.4",
+ "cookie": "~0.7.1",
+ "cookie-signature": "~1.0.6",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "finalhandler": "~1.3.1",
+ "fresh": "~0.5.2",
+ "http-errors": "~2.0.0",
+ "merge-descriptors": "1.0.3",
+ "methods": "~1.1.2",
+ "on-finished": "~2.4.1",
+ "parseurl": "~1.3.3",
+ "path-to-regexp": "~0.1.12",
+ "proxy-addr": "~2.0.7",
+ "qs": "~6.14.0",
+ "range-parser": "~1.2.1",
+ "safe-buffer": "5.2.1",
+ "send": "~0.19.0",
+ "serve-static": "~1.16.2",
+ "setprototypeof": "1.2.0",
+ "statuses": "~2.0.1",
+ "type-is": "~1.6.18",
+ "utils-merge": "1.0.1",
+ "vary": "~1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.10.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/finalhandler": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
+ "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "2.6.9",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "on-finished": "~2.4.1",
+ "parseurl": "~1.3.3",
+ "statuses": "~2.0.2",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/forwarded": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/fresh": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
+ "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/generate-function": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz",
+ "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==",
+ "license": "MIT",
+ "dependencies": {
+ "is-property": "^1.0.2"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/http-errors": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
+ "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
+ "license": "MIT",
+ "dependencies": {
+ "depd": "~2.0.0",
+ "inherits": "~2.0.4",
+ "setprototypeof": "~1.2.0",
+ "statuses": "~2.0.2",
+ "toidentifier": "~1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.4.24",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "license": "ISC"
+ },
+ "node_modules/ipaddr.js": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/is-property": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz",
+ "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==",
+ "license": "MIT"
+ },
+ "node_modules/long": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
+ "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/lru.min": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.4.tgz",
+ "integrity": "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==",
+ "license": "MIT",
+ "engines": {
+ "bun": ">=1.0.0",
+ "deno": ">=1.30.0",
+ "node": ">=8.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wellwelwel"
+ }
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/media-typer": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+ "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/merge-descriptors": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
+ "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/methods": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
+ "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
+ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
+ "license": "MIT",
+ "bin": {
+ "mime": "cli.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "license": "MIT"
+ },
+ "node_modules/mysql2": {
+ "version": "3.17.0",
+ "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.17.0.tgz",
+ "integrity": "sha512-qF1KYPuytBGqAnMzaQ5/rW90iIqcjnrnnS7bvcJcdarJzlUTAiD9ZC0T7mwndacECseSQ6LcRbRvryXLp25m+g==",
+ "license": "MIT",
+ "dependencies": {
+ "aws-ssl-profiles": "^1.1.2",
+ "denque": "^2.1.0",
+ "generate-function": "^2.3.1",
+ "iconv-lite": "^0.7.2",
+ "long": "^5.3.2",
+ "lru.min": "^1.1.3",
+ "named-placeholders": "^1.1.6",
+ "seq-queue": "^0.0.5",
+ "sql-escaper": "^1.3.1"
+ },
+ "engines": {
+ "node": ">= 8.0"
+ }
+ },
+ "node_modules/mysql2/node_modules/iconv-lite": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
+ "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==",
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/named-placeholders": {
+ "version": "1.1.6",
+ "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.6.tgz",
+ "integrity": "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==",
+ "license": "MIT",
+ "dependencies": {
+ "lru.min": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/negotiator": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
+ "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/nodemailer": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.1.tgz",
+ "integrity": "sha512-5kcldIXmaEjZcHR6F28IKGSgpmZHaF1IXLWFTG+Xh3S+Cce4MiakLtWY+PlBU69fLbRa8HlaGIrC/QolUpHkhg==",
+ "license": "MIT-0",
+ "engines": {
+ "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",
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/on-finished": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+ "license": "MIT",
+ "dependencies": {
+ "ee-first": "1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/parseurl": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/path-to-regexp": {
+ "version": "0.1.12",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
+ "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
+ "license": "MIT"
+ },
+ "node_modules/proxy-addr": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
+ "license": "MIT",
+ "dependencies": {
+ "forwarded": "0.2.0",
+ "ipaddr.js": "1.9.1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/qs": {
+ "version": "6.14.2",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz",
+ "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "side-channel": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/range-parser": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/raw-body": {
+ "version": "2.5.3",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz",
+ "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "~3.1.2",
+ "http-errors": "~2.0.1",
+ "iconv-lite": "~0.4.24",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "license": "MIT"
+ },
+ "node_modules/send": {
+ "version": "0.19.2",
+ "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz",
+ "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "1.2.0",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "fresh": "~0.5.2",
+ "http-errors": "~2.0.1",
+ "mime": "1.6.0",
+ "ms": "2.1.3",
+ "on-finished": "~2.4.1",
+ "range-parser": "~1.2.1",
+ "statuses": "~2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/send/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/seq-queue": {
+ "version": "0.0.5",
+ "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz",
+ "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q=="
+ },
+ "node_modules/serve-static": {
+ "version": "1.16.3",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz",
+ "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==",
+ "license": "MIT",
+ "dependencies": {
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "parseurl": "~1.3.3",
+ "send": "~0.19.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/setprototypeof": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
+ "license": "ISC"
+ },
+ "node_modules/side-channel": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
+ "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3",
+ "side-channel-list": "^1.0.0",
+ "side-channel-map": "^1.0.1",
+ "side-channel-weakmap": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-list": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
+ "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-map": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-weakmap": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3",
+ "side-channel-map": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "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",
+ "integrity": "sha512-GLMJGWKzrr7BS5E5+8Prix6RGfBd4UokKMxkPSg313X0TvUyjdJU3Xg7FAhhcba4dHnLy81t4YeHETKLGVsDow==",
+ "license": "MIT",
+ "engines": {
+ "bun": ">=1.0.0",
+ "deno": ">=2.0.0",
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/mysqljs/sql-escaper?sponsor=1"
+ }
+ },
+ "node_modules/statuses": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
+ "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/toidentifier": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
+ "node_modules/type-is": {
+ "version": "1.6.18",
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
+ "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
+ "license": "MIT",
+ "dependencies": {
+ "media-typer": "0.3.0",
+ "mime-types": "~2.1.24"
+ },
+ "engines": {
+ "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",
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/utils-merge": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
+ "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
+ "node_modules/uuid": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
+ "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
+ "funding": [
+ "https://github.com/sponsors/broofa",
+ "https://github.com/sponsors/ctavan"
+ ],
+ "license": "MIT",
+ "bin": {
+ "uuid": "dist/bin/uuid"
+ }
+ },
+ "node_modules/vary": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
+ "license": "MIT",
+ "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
+ }
+ }
+ }
+ }
+}
--- /dev/null
+{
+ "name": "wall-e-tte-server",
+ "version": "1.0.0",
+ "description": "Serveur Wall-e-tte - Trading Crypto Advisor",
+ "type": "module",
+ "main": "app.js",
+ "scripts": {
+ "start": "node app.js",
+ "dev": "node --watch app.js",
+ "test": "node test-module-complet.js",
+ "test:server": "node test-server-socket.js"
+ },
+ "keywords": [
+ "crypto",
+ "trading",
+ "alerts",
+ "wall-e-tte"
+ ],
+ "author": "Équipe Wall-e-tte",
+ "license": "ISC",
+ "dependencies": {
+ "cors": "^2.8.5",
+ "dotenv": "^16.4.5",
+ "express": "^4.18.2",
+ "mysql2": "^3.6.5",
+ "nodemailer": "^8.0.1",
+ "socket.io": "^4.8.3",
+ "uuid": "^9.0.1"
+ }
+}
--- /dev/null
+// =========================================================
+// SCRIPT DE TEST - Module Alerts RÉUTILISABLE
+// =========================================================
+// Ce script teste ton module d'alertes avec la nouvelle
+// architecture réutilisable (injection de dépendances)
+// =========================================================
+// USAGE : node test-alerts.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') });
+
+// =========================================================
+// INITIALISATION DU MODULE (nouvelle architecture !)
+// =========================================================
+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';
+
+// 1. Créer l'adapter MySQL avec la connexion DB
+const mysqlAdapter = createMySQLAdapter(db);
+
+// 2. Créer le repository avec l'adapter (plus de SQL dans le repo !)
+const alertsRepo = createAlertsRepo(mysqlAdapter);
+
+// 3. Créer le service avec le repository
+const alertsService = createAlertsService(alertsRepo);
+
+// =========================================================
+// FONCTION DE TEST PRINCIPALE
+// =========================================================
+async function runTest() {
+ console.log('\n' + '═'.repeat(80));
+ console.log('🧪 TEST DU MODULE ALERTS');
+ console.log('═'.repeat(80) + '\n');
+
+ try {
+ // ─────────────────────────────────────────────────
+ // ÉTAPE 1 : Vérifier la connexion DB
+ // ─────────────────────────────────────────────────
+ console.log('📊 ÉTAPE 1 : Test de connexion à la base de données\n');
+
+ // Utilise le repository créé avec injection de dépendances
+ const rules = await alertsRepo.findActiveRulesForSignal('test', 1);
+ console.log(`✅ Connexion DB OK`);
+ console.log(` Règles trouvées : ${rules.length}\n`);
+
+ if (rules.length === 0) {
+ console.log('⚠️ ATTENTION : Aucune règle trouvée en DB');
+ console.log(' Pour tester complètement:');
+ console.log(' 1. Créer un utilisateur dans la table users');
+ console.log(' 2. Créer une règle dans la table alert_rules\n');
+ console.log(' Exemple de SQL :');
+ console.log(`
+ INSERT INTO users (user_id, email, password_hash, display_name,
+ notify_on_hold, min_confidence_notify,
+ created_at_ms, updated_at_ms)
+ VALUES ('test-user', 'test@example.com', 'hash', 'Test User',
+ 0, 0.75,
+ UNIX_TIMESTAMP()*1000, UNIX_TIMESTAMP()*1000);
+
+ INSERT INTO alert_rules (rule_id, user_id, pair_id, enabled,
+ rule_type, severity, min_confidence,
+ channel, params, cooldown_ms,
+ created_at_ms, updated_at_ms)
+ VALUES ('test-rule', 'test-user', 1, 1,
+ 'SIGNAL_THRESHOLD', 'WARNING', 0.70,
+ 'CONSOLE', '{}', 60000,
+ UNIX_TIMESTAMP()*1000, UNIX_TIMESTAMP()*1000);
+ `);
+
+ console.log('\n tester les canaux !\n');
+ }
+
+ // ─────────────────────────────────────────────────
+ // ÉTAPE 2 : Test du canal CONSOLE
+ // ─────────────────────────────────────────────────
+ console.log('─'.repeat(80));
+ console.log('📺 ÉTAPE 2 : Test du canal CONSOLE\n');
+
+ const { sendConsoleAlert } = await import('./modules/alerts/channels/console.js');
+
+ const testSignalConsole = {
+ action: 'BUY',
+ pair: 'BTC/EUR',
+ confidence: 0.87,
+ criticality: 'WARNING',
+ reason: 'Test du canal console : Les indicateurs techniques montrent une tendance haussière. Le RSI est à 45, le MACD vient de croiser.'
+ };
+
+ await sendConsoleAlert(testSignalConsole);
+ console.log('✅ Test CONSOLE réussi\n');
+
+ // ─────────────────────────────────────────────────
+ // ÉTAPE 3 : Test du canal EMAIL (si configuré)
+ // ─────────────────────────────────────────────────
+ console.log('─'.repeat(80));
+ console.log('📧 ÉTAPE 3 : Test du canal EMAIL\n');
+
+ if (!process.env.MAIL_USER || !process.env.MAIL_PASS) {
+ console.log('⚠️ Email non configuré (MAIL_USER et MAIL_PASS manquants dans .env)');
+ console.log(' Pour tester l\'email, configure tes variables dans .env\n');
+ } else {
+ console.log('📬 Configuration email détectée');
+ console.log(` Host : ${process.env.MAIL_HOST}`);
+ console.log(` User : ${process.env.MAIL_USER}`);
+ console.log(' Envoi d\'un email de test...\n');
+
+ const { sendAlertEmail } = await import('./modules/alerts/channels/mailer.js');
+
+ const testSignalEmail = {
+ action: 'SELL',
+ pair: 'ETH/EUR',
+ confidence: 0.92,
+ criticality: 'CRITICAL',
+ reason: 'Test du canal email : Signal de vente critique détecté. Forte baisse imminente selon les indicateurs.'
+ };
+
+ const status = await sendAlertEmail(process.env.MAIL_TO || 'wallette@outlook.be', testSignalEmail);
+
+ if (status === 'SENT') {
+ console.log('✅ Test EMAIL réussi ! Vérifiez la boîte mail.\n');
+ } else {
+ console.log('❌ Test EMAIL échoué. Vérifiez la configuration.\n');
+ }
+ }
+
+ // ─────────────────────────────────────────────────
+ // ÉTAPE 4 : Test du service complet (si règles en DB)
+ // ─────────────────────────────────────────────────
+ if (rules.length > 0) {
+ console.log('─'.repeat(80));
+ console.log('🔄 ÉTAPE 4 : Test du service complet\n');
+
+ const testSignalService = {
+ userId: rules[0].user_id,
+ pairId: rules[0].pair_id || 1,
+ pair: 'BTC/EUR',
+ action: 'BUY',
+ confidence: 0.85,
+ criticality: 'INFO',
+ reason: 'Test complet du service : Signal généré automatiquement pour tester le workflow complet.',
+ priceAtSignal: 45000.50
+ };
+
+ // Utilise le service créé avec injection de dépendances
+ await alertsService.processSignal(testSignalService);
+ console.log('✅ Test SERVICE complet réussi !\n');
+ }
+
+ // ─────────────────────────────────────────────────
+ // RÉSUMÉ FINAL
+ // ─────────────────────────────────────────────────
+ console.log('═'.repeat(80));
+ console.log('✅ TOUS LES TESTS SONT TERMINÉS');
+ console.log('═'.repeat(80));
+ console.log('\n📋 RÉSUMÉ :');
+ console.log(' ✅ Connexion DB');
+ console.log(' ✅ Canal CONSOLE');
+ console.log(` ${process.env.MAIL_USER ? '✅' : '⚠️ '} Canal EMAIL ${process.env.MAIL_USER ? '' : '(non configuré)'}`);
+ console.log(` ${rules.length > 0 ? '✅' : '⚠️ '} Service complet ${rules.length > 0 ? '' : '(pas de règles en DB)'}`);
+ } catch (error) {
+ console.error('\n❌ ERREUR LORS DU TEST :');
+ console.error(error);
+ console.error('\n💡 VÉRIFICATIONS :');
+ console.error(' - Le fichier .env existe et contient les bonnes valeurs ?');
+ console.error(' - La base de données est démarrée ?');
+ console.error(' - Les tables sont créées (schema.sql) ?');
+ console.error('\n');
+ } finally {
+ // Quitter proprement
+ process.exit();
+ }
+}
+// =========================================================
+// LANCEMENT DU TEST
+// =========================================================
+runTest();
--- /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
+// =========================================================
+// SERVEUR DE TEST AVEC SOCKET.IO
+// =========================================================
+// Lance ce serveur pour tester les alertes temps réel
+// avec Thibault (mobile) ou Océane (web)
+// =========================================================
+// USAGE : node test-server-socket.js
+// =========================================================
+
+import express from 'express';
+import { createServer } from 'http';
+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 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';
+import { initSocketIO, sendAlertToUser, broadcastAlert, getConnectedUsersCount } from './modules/alerts/socketManager.js';
+
+// =========================================================
+// CRÉER L'APPLICATION EXPRESS
+// =========================================================
+const app = express();
+app.use(express.json());
+
+// =========================================================
+// CRÉER LE SERVEUR HTTP (nécessaire pour Socket.IO)
+// =========================================================
+const httpServer = createServer(app);
+
+// =========================================================
+// INITIALISER SOCKET.IO
+// =========================================================
+const io = initSocketIO(httpServer, {
+ cors: {
+ origin: "*", // Accepter toutes les origines pour le test
+ methods: ["GET", "POST"]
+ }
+});
+
+// =========================================================
+// INITIALISER LE MODULE ALERTS
+// =========================================================
+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 Wall-e-tte avec Socket.IO',
+ socketConnections: getConnectedUsersCount(),
+ timestamp: new Date().toISOString()
+ });
+});
+
+// =========================================================
+// ROUTE POUR SIMULER UNE ALERTE (test)
+// =========================================================
+app.post('/test/send-alert', (req, res) => {
+ const { userId, broadcast } = req.body;
+
+ const testAlert = {
+ action: 'BUY',
+ pair: 'BTC/EUR',
+ confidence: 0.87,
+ reason: 'Test manuel depuis le serveur',
+ alertLevel: 'WARNING',
+ price: 42150.23
+ };
+
+ if (broadcast) {
+ // Envoyer à tous
+ const count = broadcastAlert(testAlert);
+ res.json({
+ success: true,
+ message: `Alerte envoyée à ${count} utilisateur(s)`
+ });
+ } else if (userId) {
+ // Envoyer à un utilisateur spécifique
+ const sent = sendAlertToUser(userId, testAlert);
+ res.json({
+ success: sent,
+ message: sent ? 'Alerte envoyée' : 'Utilisateur non connecté'
+ });
+ } else {
+ res.status(400).json({
+ error: 'userId ou broadcast requis'
+ });
+ }
+});
+
+// =========================================================
+// ROUTE POUR VOIR LES CONNEXIONS ACTIVES
+// =========================================================
+app.get('/test/connections', (req, res) => {
+ const { getConnectedUserIds } = require('./modules/alerts/socketManager.js');
+ res.json({
+ count: getConnectedUsersCount(),
+ users: getConnectedUserIds ? getConnectedUserIds() : 'N/A'
+ });
+});
+
+// =========================================================
+// DÉMARRER LE SERVEUR
+// =========================================================
+const PORT = process.env.PORT || 3000;
+
+httpServer.listen(PORT, () => {
+ console.log(`
+ ╔═══════════════════════════════════════════════════════════╗
+ ║ SERVEUR WALL-E-TTE AVEC SOCKET.IO ║
+ ╠═══════════════════════════════════════════════════════════╣
+ ║ ║
+ ║ HTTP Server : http://localhost:${PORT} ║
+ ║ Socket.IO : ws://localhost:${PORT} ║
+ ║ ║
+ ╠═══════════════════════════════════════════════════════════╣
+ ║ ROUTES API : ║
+ ║ ─────────────────────────────────────────────────────── ║
+ ║ GET / → Health check ║
+ ║ GET /test/connections → Voir qui est connecté ║
+ ║ POST /test/send-alert → Simuler une alerte ║
+ ║ POST /api/alerts/rules → Créer une règle ║
+ ║ GET /api/alerts/rules/:id → Lister les règles ║
+ ║ ║
+ ╠═══════════════════════════════════════════════════════════╣
+ ║ SOCKET.IO EVENTS : ║
+ ║ ─────────────────────────────────────────────────────── ║
+ ║ Client → Serveur : ║
+ ║ • 'auth' (userId) → S'authentifier ║
+ ║ • 'ping_alerts' → Tester la connexion ║
+ ║ ║
+ ║ Serveur → Client : ║
+ ║ • 'auth_success' → Authentification OK ║
+ ║ • 'alert' → Réception d'une alerte ║
+ ║ • 'pong_alerts' → Réponse au ping ║
+ ║ ║
+ ╚═══════════════════════════════════════════════════════════╝
+
+
+ Pour tester manuellement :
+ curl -X POST http://localhost:${PORT}/test/send-alert -H "Content-Type: application/json" -d '{"broadcast": true}'
+
+ Ctrl+C pour arrêter.
+`);
+});
--- /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.
+`);
+});
--- /dev/null
+node_modules/
+.expo/
\ No newline at end of file
--- /dev/null
+# system_notification
--- /dev/null
+import mysql from 'mysql2/promise';
+import dotenv from 'dotenv';
+import path from 'path';
+import { fileURLToPath } from 'url';
+
+// On recrée __dirname qui n'existe pas en mode "module"
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+
+// On charge le .env
+dotenv.config({ path: path.resolve(__dirname, '../.env') });
+
+const db = mysql.createPool({
+ host: process.env.DB_HOST,
+ port: process.env.DB_PORT,
+ user: process.env.DB_USER,
+ password: process.env.DB_PASS,
+ database: process.env.DB_NAME,
+ waitForConnections: true,
+ connectionLimit: 10,
+ queueLimit: 0
+});
+
+export default db;
--- /dev/null
+{
+ "name": "system_notification",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "system_notification",
+ "version": "1.0.0",
+ "license": "ISC",
+ "dependencies": {
+ "dotenv": "^17.2.4",
+ "mysql2": "^3.16.3",
+ "nodemailer": "^8.0.1"
+ }
+ },
+ "node_modules/aws-ssl-profiles": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz",
+ "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6.0.0"
+ }
+ },
+ "node_modules/denque": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
+ "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/dotenv": {
+ "version": "17.2.4",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.4.tgz",
+ "integrity": "sha512-mudtfb4zRB4bVvdj0xRo+e6duH1csJRM8IukBqfTRvHotn9+LBXB8ynAidP9zHqoRC/fsllXgk4kCKlR21fIhw==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://dotenvx.com"
+ }
+ },
+ "node_modules/generate-function": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz",
+ "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==",
+ "license": "MIT",
+ "dependencies": {
+ "is-property": "^1.0.2"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
+ "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==",
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/is-property": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz",
+ "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==",
+ "license": "MIT"
+ },
+ "node_modules/long": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
+ "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/lru.min": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.4.tgz",
+ "integrity": "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==",
+ "license": "MIT",
+ "engines": {
+ "bun": ">=1.0.0",
+ "deno": ">=1.30.0",
+ "node": ">=8.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wellwelwel"
+ }
+ },
+ "node_modules/mysql2": {
+ "version": "3.16.3",
+ "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.16.3.tgz",
+ "integrity": "sha512-+3XhQEt4FEFuvGV0JjIDj4eP2OT/oIj/54dYvqhblnSzlfcxVOuj+cd15Xz6hsG4HU1a+A5+BA9gm0618C4z7A==",
+ "license": "MIT",
+ "dependencies": {
+ "aws-ssl-profiles": "^1.1.2",
+ "denque": "^2.1.0",
+ "generate-function": "^2.3.1",
+ "iconv-lite": "^0.7.2",
+ "long": "^5.3.2",
+ "lru.min": "^1.1.3",
+ "named-placeholders": "^1.1.6",
+ "seq-queue": "^0.0.5",
+ "sqlstring": "^2.3.3"
+ },
+ "engines": {
+ "node": ">= 8.0"
+ }
+ },
+ "node_modules/named-placeholders": {
+ "version": "1.1.6",
+ "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.6.tgz",
+ "integrity": "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==",
+ "license": "MIT",
+ "dependencies": {
+ "lru.min": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/nodemailer": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.1.tgz",
+ "integrity": "sha512-5kcldIXmaEjZcHR6F28IKGSgpmZHaF1IXLWFTG+Xh3S+Cce4MiakLtWY+PlBU69fLbRa8HlaGIrC/QolUpHkhg==",
+ "license": "MIT-0",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "license": "MIT"
+ },
+ "node_modules/seq-queue": {
+ "version": "0.0.5",
+ "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz",
+ "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q=="
+ },
+ "node_modules/sqlstring": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz",
+ "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ }
+ }
+}
--- /dev/null
+{
+ "name": "system_notification",
+ "version": "1.0.0",
+ "type": "module",
+ "description": "",
+ "main": "test-connection.js",
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "keywords": [],
+ "author": "",
+ "license": "ISC",
+ "dependencies": {
+ "dotenv": "^17.2.4",
+ "mysql2": "^3.16.3",
+ "nodemailer": "^8.0.1"
+ }
+}
--- /dev/null
+// 1. On importe la connexion à la base de données
+import db from '../config/db.js';
+
+const MySqlAlertRepository = {
+
+ /**
+ * Récupère toutes les règles d'alerte actives avec l'email de l'utilisateur
+ */
+ async findActiveRules() {
+ try {
+ // On prépare la requête SQL avec une jointure (JOIN)
+ // On récupère les colonnes de alert_rules (ar) et l'email de users (u)
+ const sql = `
+ SELECT ar.*, u.email
+ FROM alert_rules ar
+ JOIN users u ON ar.user_id = u.user_id
+ WHERE ar.enabled = 1
+ `;
+
+ // On exécute la requête
+ const [rows] = await db.execute(sql);
+
+ // On retourne les données
+ return rows;
+ } catch (error) {
+ console.error("Erreur dans findActiveRules:", error);
+ throw error; // On laisse le service gérer l'erreur si besoin
+ }
+ }
+
+
+};
+
+// 3. On exporte le repository
+export default MySqlAlertRepository;
--- /dev/null
+import nodemailer from 'nodemailer';
+
+
+export async function sendAlertEmail(to, signal) {
+ const transporter = nodemailer.createTransport({
+ host: process.env.MAIL_HOST,
+ port: parseInt(process.env.MAIL_PORT),
+ auth: {
+ user: process.env.MAIL_USER,
+ pass: process.env.MAIL_PASS
+ }
+ });
+
+ try {
+ const confidencePercent = (parseFloat(signal.confidence) * 100).toFixed(2);
+ const mailOptions = {
+ from: `"Wall-e-tte" <${process.env.MAIL_USER}>`,
+ to: to,
+ subject: `🚨 Alerte : ${signal.action} sur ${signal.pair}`,
+ text: `Action : ${signal.action}\nConfiance : ${confidencePercent}%`,
+ };
+
+ const info = await transporter.sendMail(mailOptions);
+ console.log("✅ Email envoyé ! ID:", info.messageId);
+ return 'SENT';
+ } catch (error) {
+ console.error("❌ Erreur d'envoi mail :", error.message);
+ return 'FAILED';
+ }
+}
+
+
--- /dev/null
+import repository from './repositories/MySqlAlertRepository.js'; // Note le .js obligatoire !
+
+async function test() {
+ console.log("Tentative de connexion à MariaDB...");
+
+ try {
+ const rules = await repository.findActiveRules();
+ console.log("✅ Connexion réussie !");
+ console.log(`📊 Nombre de règles actives trouvées : ${rules.length}`);
+
+ if (rules.length > 0) {
+ console.log("Détail de la première règle :", rules[0]);
+ } else {
+ console.log("La connexion marche, mais la table 'alert_rules' est vide.");
+ }
+
+ } catch (error) {
+ console.error("❌ ÉCHEC du test :");
+ console.error("Détail de l'erreur :", error.message);
+ } finally {
+ process.exit();
+ }
+}
+
+test();
--- /dev/null
+import dotenv from 'dotenv';
+import path from 'path';
+import { fileURLToPath } from 'url';
+import repository from './repositories/MySqlAlertRepository.js';
+import { sendAlertEmail } from './services/channels/mailer.js';
+
+// Configuration nécessaire pour retrouver __dirname en mode "import"
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+
+// Chargement des variables d'environnement
+dotenv.config({ path: path.resolve(__dirname, '.env') });
+
+async function runTest() {
+ console.log("🔍 Recherche d'une règle active dans la DB...");
+
+ try {
+ const rules = await repository.findActiveRules();
+
+ if (rules.length === 0) {
+ console.log("⚠️ Aucune règle trouvée. Vérifie tes tables SQL.");
+ return;
+ }
+
+ const rule = rules[0];
+ console.log(`✅ Règle trouvée pour : ${rule.email}`);
+
+ // On simule un signal de test
+ const fakeSignal = {
+ action: 'BUY',
+ pair: 'BTC/EUR',
+ confidence: 0.85,
+ severity: 'WARNING'
+ };
+
+ console.log("✉️ Tentative d'envoi du mail...");
+
+ // Appel direct de la fonction importée (plus de require ici !)
+ const status = await sendAlertEmail(rule.email, fakeSignal);
+
+ console.log(`📊 Résultat de l'envoi : ${status}`);
+
+ } catch (error) {
+ console.error("❌ Erreur pendant le test :", error);
+ } finally {
+ process.exit();
+ }
+}
+
+runTest();