All files / src/middleware apiAuth.js

0% Statements 0/28
0% Branches 0/18
0% Functions 0/5
0% Lines 0/28

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                                                                                                                                                                                                                       
/**
 * Middleware d'authentification API — Verifie la cle API et injecte req.user + req.apiKey.
 *
 * Accepte le header : Authorization: Bearer iliacloud_xxxxx
 *
 * Fonctionnement :
 *   1. Extrait la cle du header Authorization
 *   2. Hash la cle avec SHA-256
 *   3. Cherche le hash dans user_api_keys
 *   4. Verifie que la cle n'est pas expiree
 *   5. Met a jour last_used_at
 *   6. Charge l'utilisateur et son plan
 *   7. Injecte req.user et req.apiKey (avec scopes)
 *
 * @module middleware/apiAuth
 */
 
const crypto = require('node:crypto');
const { pool: db } = require('../db');
 
/**
 * Hash une cle API avec SHA-256.
 * @param {string} key
 * @returns {string}
 */
function hashKey(key) {
  return crypto.createHash('sha256').update(key).digest('hex');
}
 
/**
 * Middleware Express — verifie la cle API et injecte req.user + req.apiKey.
 */
async function requireApiAuth(req, res, next) {
  try {
    const authHeader = req.headers['authorization'];
    if (!authHeader?.startsWith('Bearer ')) {
      return res.status(401).json({
        error: { code: 'MISSING_API_KEY', message: 'Header Authorization: Bearer <api_key> requis.' },
      });
    }
 
    const key = authHeader.slice(7);
    if (!key?.startsWith('iliacloud_')) {
      return res.status(401).json({
        error: { code: 'INVALID_API_KEY', message: 'Format de cle API invalide.' },
      });
    }
 
    const keyHash = hashKey(key);
 
    const { rows } = await db.query(
      `SELECT ak.id, ak.user_id, ak.scopes, ak.expires_at, ak.label,
              u.email, u.plan
       FROM user_api_keys ak
       JOIN users u ON u.id = ak.user_id
       WHERE ak.key_hash = $1`,
      [keyHash],
    );
 
    if (rows.length === 0) {
      return res.status(401).json({
        error: { code: 'INVALID_API_KEY', message: 'Cle API invalide ou revoquee.' },
      });
    }
 
    const apiKey = rows[0];
 
    // Verifier l'expiration
    if (apiKey.expires_at && new Date(apiKey.expires_at) < new Date()) {
      return res.status(401).json({
        error: { code: 'EXPIRED_API_KEY', message: 'Cle API expiree.' },
      });
    }
 
    // Mettre a jour last_used_at (fire and forget)
    db.query('UPDATE user_api_keys SET last_used_at = NOW() WHERE id = $1', [apiKey.id]).catch(() => {});
 
    // Injecter req.user et req.apiKey
    req.user = { id: apiKey.user_id, email: apiKey.email, plan: apiKey.plan };
    req.apiKey = { id: apiKey.id, scopes: apiKey.scopes || [], label: apiKey.label };
 
    next();
  } catch (err) {
    next(err);
  }
}
 
/**
 * Middleware de verification de scope.
 * Bloque si la cle API n'a pas le scope requis.
 *
 * @param {string} scope — 'read', 'write' ou 'admin'
 * @returns {Function} Express middleware
 */
function requireScope(scope) {
  return (req, res, next) => {
    const scopes = req.apiKey?.scopes || [];
    if (scopes.includes('admin') || scopes.includes(scope)) {
      return next();
    }
    return res.status(403).json({
      error: { code: 'INSUFFICIENT_SCOPE', message: `Scope '${scope}' requis. Scopes actuels : [${scopes.join(', ')}].` },
    });
  };
}
 
module.exports = { requireApiAuth, requireScope, hashKey };