All files / src/middleware planLimits.js

0% Statements 0/59
0% Branches 0/32
0% Functions 0/9
0% Lines 0/56

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                                                                                                                                                                                                                                                                                                                                                                               
/**
 * Middleware Plan Limits — Verifie les quotas et features selon le plan utilisateur.
 *
 * Trois middlewares :
 *   - requireFeature(feature) — bloque si la feature n'est pas dans le plan
 *   - checkQuota(resource, table, opts) — bloque si le quota est atteint
 *   - checkDailyQuota(resource, table, opts) — bloque si le quota journalier est atteint
 *   - checkPlanValue(resource) — injecte req.planLimit[resource] pour validation custom
 *
 * Les limites sont chargees depuis la DB (table plans) avec cache memoire.
 * Fallback sur les valeurs par defaut si la DB est inaccessible.
 *
 * @module middleware/planLimits
 */
 
const { pool: db } = require('../db');
const { getPlanLimits, requiredPlanFor } = require('../config/plans');
 
/**
 * Retourne le plan effectif : celui du proprietaire du groupe si en mode groupe,
 * sinon celui de l'utilisateur connecte.
 */
async function getEffectivePlan(req) {
  if (req.access?.ownerUserId && req.access.ownerUserId !== req.user?.id) {
    const { rows } = await db.query('SELECT plan FROM users WHERE id = $1', [req.access.ownerUserId]);
    return rows[0]?.plan || 'free';
  }
  return req.user?.plan || 'free';
}
 
/**
 * Verifie qu'une feature booleenne est disponible pour le plan de l'utilisateur.
 *
 * @param {string} feature — Cle booleenne dans les limites (ex: 'terminal_ssh')
 * @returns {Function} Express middleware
 */
function requireFeature(feature) {
  return async (req, res, next) => {
    try {
      const plan = await getEffectivePlan(req);
      const limits = await getPlanLimits(plan);
 
      if (!limits[feature]) {
        const required = requiredPlanFor(feature);
        return res.status(403).json({
          error: 'Cette fonctionnalite necessite un plan superieur.',
          code: 'PLAN_FEATURE_REQUIRED',
          feature,
          current_plan: plan,
          required_plan: required,
          upgrade_url: '/settings#billing',
        });
      }
      next();
    } catch (err) {
      next(err);
    }
  };
}
 
/**
 * Verifie qu'un quota numerique n'est pas depasse.
 *
 * @param {string} resource — Cle numerique dans les limites (ex: 'servers')
 * @param {string} table — Nom de la table SQL pour compter les ressources
 * @param {Object} [opts]
 * @param {string} [opts.userColumn='user_id'] — Colonne pour filtrer par utilisateur
 * @param {string} [opts.joinClause] — JOIN optionnel pour compter via une table liee
 * @returns {Function} Express middleware
 */
function checkQuota(resource, table, opts = {}) {
  const { userColumn = 'user_id', joinClause } = opts;
 
  return async (req, res, next) => {
    try {
      const plan = await getEffectivePlan(req);
      const limits = await getPlanLimits(plan);
      const limit = limits[resource];
 
      // Pas de limite = Infinity
      if (limit === Infinity || limit === undefined) return next();
 
      let query;
      if (joinClause) {
        query = `SELECT COUNT(*) FROM ${table} ${joinClause} WHERE ${userColumn} = $1`;
      } else {
        query = `SELECT COUNT(*) FROM ${table} WHERE ${userColumn} = $1`;
      }
 
      const { rows } = await db.query(query, [req.user.id]);
      const count = Number.parseInt(rows[0].count);
 
      if (count >= limit) {
        return res.status(403).json({
          error: `Limite atteinte : ${count}/${limit} ${resource.replaceAll('_', ' ')} (plan ${plan}).`,
          code: 'PLAN_QUOTA_EXCEEDED',
          resource,
          current: count,
          limit,
          current_plan: plan,
          upgrade_url: '/settings#billing',
        });
      }
 
      next();
    } catch (err) {
      next(err);
    }
  };
}
 
/**
 * Verifie qu'un quota journalier n'est pas depasse.
 *
 * @param {string} resource — Cle dans les limites (ex: 'chat_messages_per_day')
 * @param {string} table — Table SQL
 * @param {Object} [opts]
 * @param {string} [opts.userColumn='user_id'] — Colonne pour filtrer
 * @param {string} [opts.joinClause] — JOIN optionnel
 * @returns {Function} Express middleware
 */
function checkDailyQuota(resource, table, opts = {}) {
  const { userColumn = 'user_id', joinClause } = opts;
 
  return async (req, res, next) => {
    try {
      const plan = await getEffectivePlan(req);
      const limits = await getPlanLimits(plan);
      const limit = limits[resource];
 
      if (limit === Infinity || limit === undefined) return next();
 
      let query;
      if (joinClause) {
        query = `SELECT COUNT(*) FROM ${table} ${joinClause} WHERE ${userColumn} = $1 AND ${table}.created_at > NOW() - INTERVAL '24 hours'`;
      } else {
        query = `SELECT COUNT(*) FROM ${table} WHERE ${userColumn} = $1 AND created_at > NOW() - INTERVAL '24 hours'`;
      }
 
      const { rows } = await db.query(query, [req.user.id]);
      const count = Number.parseInt(rows[0].count);
 
      if (count >= limit) {
        return res.status(429).json({
          error: `Limite journaliere atteinte : ${count}/${limit} (plan ${plan}).`,
          code: 'PLAN_DAILY_QUOTA_EXCEEDED',
          resource,
          current: count,
          limit,
          current_plan: plan,
          upgrade_url: '/settings#billing',
        });
      }
 
      next();
    } catch (err) {
      next(err);
    }
  };
}
 
/**
 * Injecte la valeur d'une limite du plan dans req.planLimit pour validation custom.
 * Utile pour les contraintes comme uptime_interval_seconds ou backup_retention_days.
 *
 * @param {string} resource — Cle dans les limites (ex: 'uptime_interval_seconds')
 * @returns {Function} Express middleware
 */
function checkPlanValue(resource) {
  return async (req, res, next) => {
    try {
      const plan = await getEffectivePlan(req);
      const limits = await getPlanLimits(plan);
      if (!req.planLimit) req.planLimit = {};
      req.planLimit[resource] = limits[resource];
      next();
    } catch (err) {
      next(err);
    }
  };
}
 
module.exports = { requireFeature, checkQuota, checkDailyQuota, checkPlanValue };