All files / src/routes audit.js

0% Statements 0/68
0% Branches 0/29
0% Functions 0/6
0% Lines 0/67

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                                                                                                                                                                                                                                                                                                                                                             
/**
 * Routes Audit — Consultation et export des logs d'audit.
 *
 * Endpoints :
 *   GET    /audit/logs          — Lister les logs (pagination + filtres)
 *   GET    /audit/logs/export   — Exporter en CSV
 *   GET    /audit/stats         — Statistiques par categorie
 *   DELETE /audit/logs          — Purger les logs anciens
 *
 * @module routes/audit
 */
 
const express = require('express');
const { pool: db } = require('../db');
const { requireAuth } = require('../middleware/auth');
const { requireFeature } = require('../middleware/planLimits');
const { requireGroupContext, requireGroupPermission } = require('../middleware/groupContext');
 
const router = express.Router();
 
router.use(requireAuth);
router.use(requireGroupContext());
 
// Categories valides pour le filtre
const VALID_CATEGORIES = new Set([
  'auth', 'server', 'docker', 'ssh', 'ssh_key', 'terminal', 'chat', 'settings',
  'backup', 'alert', 'file', 'cron', 'webhook', 'ssl',
  'uptime', 'status_page', 'export', 'general',
]);
 
// ─── GET /audit/logs ────────────────────────────────────────────────────────
 
router.get('/logs', requireGroupPermission('audit', 'read'), async (req, res, next) => {
  try {
    const page = Math.max(1, Number.parseInt(req.query.page) || 1);
    const limit = Math.min(100, Math.max(1, Number.parseInt(req.query.limit) || 50));
    const offset = (page - 1) * limit;
    const category = req.query.category;
    const search = req.query.search;
    const range = req.query.range || '7d';
 
    const intervals = { '24h': '24 hours', '7d': '7 days', '30d': '30 days', '90d': '90 days' };
    const interval = intervals[range] || '7 days';
 
    let whereClause = `WHERE al.user_id = $1 AND al.created_at > NOW() - INTERVAL '${interval}'`;
    const params = [req.user.id];
    let paramIndex = 2;
 
    // En mode groupe, filtrer les logs d'audit par group_id
    if (req.access) {
      whereClause += ` AND al.group_id = $${paramIndex}`;
      params.push(req.access.groupId);
      paramIndex++;
    }
 
    if (category && VALID_CATEGORIES.has(category)) {
      whereClause += ` AND al.category = $${paramIndex}`;
      params.push(category);
      paramIndex++;
    }
 
    if (search && typeof search === 'string' && search.length <= 100) {
      whereClause += ` AND (al.action ILIKE $${paramIndex} OR al.target ILIKE $${paramIndex} OR al.ip ILIKE $${paramIndex})`;
      params.push(`%${search}%`);
      paramIndex++;
    }
 
    // Count total
    const countResult = await db.query(
      `SELECT COUNT(*) FROM audit_logs al ${whereClause}`,
      params,
    );
    const total = Number.parseInt(countResult.rows[0].count);
 
    // Fetch logs
    const { rows } = await db.query(
      `SELECT al.id, al.action, al.category, al.target, al.details, al.ip, al.created_at
       FROM audit_logs al
       ${whereClause}
       ORDER BY al.created_at DESC
       LIMIT $${paramIndex} OFFSET $${paramIndex + 1}`,
      [...params, limit, offset],
    );
 
    res.json({
      logs: rows,
      pagination: { page, limit, total, total_pages: Math.ceil(total / limit) },
    });
  } catch (err) {
    next(err);
  }
});
 
// ─── GET /audit/logs/export — Export CSV ────────────────────────────────────
 
router.get('/logs/export', requireGroupPermission('audit', 'read'), requireFeature('export_csv_audit'), async (req, res, next) => {
  try {
    const range = req.query.range || '30d';
    const intervals = { '7d': '7 days', '30d': '30 days', '90d': '90 days' };
    const interval = intervals[range] || '30 days';
 
    const { rows } = await db.query(
      `SELECT al.action, al.category, al.target, al.ip, al.created_at,
              al.details->>'method' AS method, al.details->>'path' AS path,
              al.details->>'status' AS status
       FROM audit_logs al
       WHERE al.user_id = $1 AND al.created_at > NOW() - INTERVAL '${interval}'
       ORDER BY al.created_at DESC
       LIMIT 10000`,
      [req.user.id],
    );
 
    // Generer le CSV
    const header = 'Date,Action,Categorie,Cible,Methode,Chemin,Status,IP\n';
    const csvRows = rows.map((r) => {
      const date = new Date(r.created_at).toISOString();
      // Echapper les guillemets dans les champs CSV
      const escape = (v) => `"${(v || '').replaceAll('"', '""')}"`;
      return [date, escape(r.action), escape(r.category), escape(r.target),
        escape(r.method), escape(r.path), r.status || '', escape(r.ip)].join(',');
    });
 
    res.setHeader('Content-Type', 'text/csv; charset=utf-8');
    res.setHeader('Content-Disposition', `attachment; filename="audit-logs-${range}.csv"`);
    res.send(header + csvRows.join('\n'));
  } catch (err) {
    next(err);
  }
});
 
// ─── GET /audit/stats — Statistiques ────────────────────────────────────────
 
router.get('/stats', requireGroupPermission('audit', 'read'), async (req, res, next) => {
  try {
    const { rows } = await db.query(
      `SELECT category, COUNT(*) AS count
       FROM audit_logs
       WHERE user_id = $1 AND created_at > NOW() - INTERVAL '30 days'
       GROUP BY category
       ORDER BY count DESC`,
      [req.user.id],
    );
 
    const { rows: recent } = await db.query(
      `SELECT COUNT(*) AS count FROM audit_logs
       WHERE user_id = $1 AND created_at > NOW() - INTERVAL '24 hours'`,
      [req.user.id],
    );
 
    res.json({
      by_category: rows,
      last_24h: Number.parseInt(recent[0].count),
    });
  } catch (err) {
    next(err);
  }
});
 
// ─── DELETE /audit/logs — Purger les anciens logs ───────────────────────────
 
router.delete('/logs', requireGroupPermission('audit', 'read'), async (req, res, next) => {
  try {
    const days = Math.max(7, Math.min(Number.parseInt(req.query.days) || 90, 365));
    const result = await db.query(
      `DELETE FROM audit_logs WHERE user_id = $1 AND created_at < NOW() - INTERVAL '${days} days'`,
      [req.user.id],
    );
    res.json({ message: `${result.rowCount} logs supprimes (> ${days} jours).` });
  } catch (err) {
    next(err);
  }
});
 
module.exports = router;