api-key-auth.js 2.38 KB
/**
 * API Key Authentication Middleware
 *
 * 用于外部 Open API (/api/open/*) 的认证。
 * 从 Authorization: Bearer <key> 或 X-API-Key: <key> 头提取 API Key,
 * 校验是否在 OPEN_API_KEYS 列表中。
 */

const config = require('../config');

/**
 * 提取 API Key
 * @param {import('express').Request} req
 * @returns {string|null}
 */
function extractApiKey(req) {
  // 优先 Authorization: Bearer <key>
  const authHeader = req.headers['authorization'];
  if (authHeader && authHeader.startsWith('Bearer ')) {
    return authHeader.slice(7).trim();
  }

  // 备选 X-API-Key
  const xApiKey = req.headers['x-api-key'];
  if (xApiKey) return String(xApiKey).trim();

  // Query param fallback (方便测试)
  if (req.query.api_key) return String(req.query.api_key).trim();

  return null;
}

/**
 * Express 中间件:校验 API Key
 */
function requireApiKey(req, res, next) {
  const key = extractApiKey(req);

  if (!key) {
    return res.status(401).json({
      error: 'Unauthorized',
      message: 'API Key is required. Provide via Authorization: Bearer <key> or X-API-Key header.',
    });
  }

  let validUser = null;

  try {
    const userDb = require('../services/user-db');
    const row = userDb.findByApiKey(key);
    // 过 toPublicUser 再挂上去:findByApiKey 返回的是**原始数据库行**,带 password_hash。
    // JWT 那条路(jwt-auth.extractUser)一直是脱敏后才挂的,这里以前不是 ——
    // 两条鉴权路径行为不一致,而 req.user 是 handler 唯一的身份来源:
    // 将来任何一个 res.json({ user: req.user }) 都会把密码哈希发给外部 API 调用方。
    // 目前所有 handler 只读 req.user.id / .email,还没真漏,但这个口子不该留着。
    validUser = row ? userDb.toPublicUser(row) : null;
  } catch (err) {
    console.error('[OpenAPI] DB check failed for API key:', err);
  }

  const validKeys = config.openApi?.apiKeys || [];

  if (!validUser && !validKeys.includes(key)) {
    // 都不匹配时拦截
    return res.status(401).json({
      error: 'Unauthorized',
      message: 'Invalid API Key. Please provide a valid user key or system key.',
    });
  }

  // 将 key 和(如有)的 user 附加到 req 上(后续用于任务归属和下载跟踪)
  req.apiKey = key;
  if (validUser) {
    req.user = validUser;
  }
  next();
}

module.exports = { requireApiKey, extractApiKey };