All files / src/services pushNotifier.js

0% Statements 0/65
0% Branches 0/35
0% Functions 0/5
0% Lines 0/62

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191                                                                                                                                                                                                                                                                                                                                                                                             
/**
 * Service de notifications push — Envoie des alertes via Web Push ET Webhooks.
 *
 * Verifie les seuils configures apres chaque collecte de metriques.
 * Envoie une alerte si un seuil est depasse.
 * Envoie une recovery quand la metrique revient sous le seuil.
 *
 * Anti-spam : 15 min entre deux alertes identiques.
 *
 * @module services/pushNotifier
 */
 
const webPush = require('web-push');
const { pool: db } = require('../db');
const { notify } = require('./webhookNotifier');
 
// Configurer web-push avec les cles VAPID
if (process.env.VAPID_PUBLIC_KEY && process.env.VAPID_PRIVATE_KEY) {
  webPush.setVapidDetails(
    `mailto:${process.env.VAPID_EMAIL || 'admin@iliacloud.app'}`,
    process.env.VAPID_PUBLIC_KEY,
    process.env.VAPID_PRIVATE_KEY,
  );
  console.log('[push] Web Push configure avec les cles VAPID');
} else {
  console.warn('[push] VAPID_PUBLIC_KEY/VAPID_PRIVATE_KEY manquantes — notifications desactivees');
}
 
// Cache cooldown (clé = "userId:serverId:metric", valeur = timestamp)
const lastAlertSent = new Map();
const ALERT_COOLDOWN_MS = 15 * 60 * 1000;
 
// Etat precedent par metrique (pour detecter les transitions)
const previousState = new Map(); // clé → 'alert' | 'ok'
 
const METRIC_LABELS = { cpu: 'CPU', mem: 'RAM', disk: 'Disque', load: 'Load' };
 
/**
 * Verifie les seuils et envoie des notifications si necessaire.
 */
async function checkAndNotify(serverId, metrics) {
  try {
    const rulesResult = await db.query(
      `SELECT ar.*, s.name AS server_name
       FROM alert_rules ar
       JOIN servers s ON s.id = ar.server_id
       WHERE ar.server_id = $1 AND ar.enabled = TRUE`,
      [serverId],
    );
 
    for (const rule of rulesResult.rows) {
      const value = getMetricValue(metrics, rule.metric);
      if (value === null) continue;
 
      const stateKey = `${rule.user_id}:${rule.server_id}:${rule.metric}`;
      const wasInAlert = previousState.get(stateKey) === 'alert';
      const isInAlert = value >= rule.threshold;
 
      if (isInAlert) {
        // En alerte (nouvelle ou rappel periodique)
        previousState.set(stateKey, 'alert');
        await triggerAlert(rule, value);
      } else if (wasInAlert) {
        // Transition ALERTE → OK (recovery)
        previousState.set(stateKey, 'ok');
        await triggerRecovery(rule, value);
      }
    }
  } catch (err) {
    console.error('[push] Erreur checkAndNotify :', err.message);
  }
}
 
function getMetricValue(metrics, metric) {
  switch (metric) {
    case 'cpu': return metrics.cpu;
    case 'mem': return metrics.mem;
    case 'disk': return metrics.disk;
    case 'load': return metrics.load == null ? null : Math.round(metrics.load * 100);
    default: return null;
  }
}
 
/**
 * Declenche une alerte : historique + push + webhook.
 */
async function triggerAlert(rule, value) {
  const cooldownKey = `${rule.user_id}:${rule.server_id}:${rule.metric}`;
  const lastSent = lastAlertSent.get(cooldownKey);
  if (lastSent && Date.now() - lastSent < ALERT_COOLDOWN_MS) return;
 
  const label = METRIC_LABELS[rule.metric] || rule.metric;
  const message = `${label} a ${value}% sur ${rule.server_name} (seuil : ${rule.threshold}%)`;
 
  // Historique
  await db.query(
    `INSERT INTO alert_history (alert_rule_id, server_id, user_id, metric, value, threshold, message)
     VALUES ($1, $2, $3, $4, $5, $6, $7)`,
    [rule.id, rule.server_id, rule.user_id, rule.metric, value, rule.threshold, message],
  );
 
  // Push notifications
  await sendPush(rule.user_id, `Alerte ${label}`, message, { serverId: rule.server_id, metric: rule.metric });
 
  // Webhook
  await notify(rule.user_id, 'server_alert', {
    title: `${label} critique — ${rule.server_name}`,
    message,
    fields: [
      { name: 'Serveur', value: rule.server_name, inline: true },
      { name: label, value: `${value}%`, inline: true },
      { name: 'Seuil', value: `${rule.threshold}%`, inline: true },
    ],
  });
 
  lastAlertSent.set(cooldownKey, Date.now());
  console.log(`[push] Alerte envoyee : ${message}`);
}
 
/**
 * Envoie une notification de recovery : la metrique est revenue sous le seuil.
 */
async function triggerRecovery(rule, value) {
  const cooldownKey = `${rule.user_id}:${rule.server_id}:${rule.metric}:recovery`;
  const lastSent = lastAlertSent.get(cooldownKey);
  if (lastSent && Date.now() - lastSent < ALERT_COOLDOWN_MS) return;
 
  const label = METRIC_LABELS[rule.metric] || rule.metric;
  const message = `${label} revenu a ${value}% sur ${rule.server_name} (seuil : ${rule.threshold}%)`;
 
  // Historique
  await db.query(
    `INSERT INTO alert_history (alert_rule_id, server_id, user_id, metric, value, threshold, message)
     VALUES ($1, $2, $3, $4, $5, $6, $7)`,
    [rule.id, rule.server_id, rule.user_id, rule.metric, value, rule.threshold, `[OK] ${message}`],
  );
 
  // Push
  await sendPush(rule.user_id, `${label} OK`, message, { serverId: rule.server_id, metric: rule.metric });
 
  // Webhook
  await notify(rule.user_id, 'server_recovery', {
    title: `${label} revenu a la normale — ${rule.server_name}`,
    message,
    fields: [
      { name: 'Serveur', value: rule.server_name, inline: true },
      { name: label, value: `${value}%`, inline: true },
      { name: 'Seuil', value: `${rule.threshold}%`, inline: true },
    ],
  });
 
  lastAlertSent.set(cooldownKey, Date.now());
  console.log(`[push] Recovery : ${message}`);
}
 
/**
 * Envoie les notifications push a tous les appareils d'un utilisateur.
 */
async function sendPush(userId, title, body, data) {
  try {
    const { rows: subs } = await db.query(
      'SELECT id, endpoint, keys_p256dh, keys_auth FROM push_subscriptions WHERE user_id = $1',
      [userId],
    );
 
    const payload = JSON.stringify({
      title, body,
      icon: '/pwa-192x192.png',
      badge: '/pwa-192x192.png',
      data,
    });
 
    for (const sub of subs) {
      try {
        await webPush.sendNotification(
          { endpoint: sub.endpoint, keys: { p256dh: sub.keys_p256dh, auth: sub.keys_auth } },
          payload,
        );
      } catch (err) {
        if (err.statusCode === 410 || err.statusCode === 404) {
          await db.query('DELETE FROM push_subscriptions WHERE id = $1', [sub.id]);
        }
      }
    }
  } catch (err) {
    console.error('[push] Erreur sendPush :', err.message);
  }
}
 
module.exports = { checkAndNotify };