Ctrl K
ComunidadGuías…

server/api/DataApiRoute.js

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
const rateLimit = require("express-rate-limit");

const DataApiController = require("../controllers/DataApiController");
const verifyDataApiKey = require("../modules/verifyDataApiKey");
const { getDataApiLimits } = require("../modules/dataApiLimits");
const { incrementDataApiMetric } = require("../modules/dataApiMetrics");
const {
  DataApiError,
  acceptsJson,
  ensureRequestId,
  sendDataApiError,
  setDataHeaders,
} = require("../modules/dataApiResponse");
const { failRun, startRun } = require("../modules/updateAudit");

function isDataApiEnabled() {
  return process.env.CB_DATA_API_ENABLED !== "false";
}

function requestId(req, _res, next) {
  ensureRequestId(req);
  return next();
}

function requireEnabled(req, res, next) {
  if (!isDataApiEnabled()) return sendDataApiError(req, res, new DataApiError("RESOURCE_NOT_FOUND"));
  return next();
}

function validateHttpRequest(req, res, next) {
  if (!acceptsJson(req)) return sendDataApiError(req, res, new DataApiError("NOT_ACCEPTABLE"));
  if (req.method === "GET" && Object.keys(req.query || {}).length > 0) {
    return sendDataApiError(req, res, new DataApiError("INVALID_REQUEST"));
  }
  if (req.method === "POST") {
    if (!req.is("application/json")) {
      return sendDataApiError(req, res, new DataApiError("UNSUPPORTED_MEDIA_TYPE"));
    }
    const rawBytes = Buffer.byteLength(req.rawBody || JSON.stringify(req.body || {}), "utf8");
    if (rawBytes > getDataApiLimits().maxRequestBytes) {
      incrementDataApiMetric("size_rejections", { resourceType: "request", type: "REQUEST_TOO_LARGE" });
      return sendDataApiError(req, res, new DataApiError("REQUEST_TOO_LARGE"));
    }
  }
  return next();
}

function createAuthLimiter() {
  return rateLimit({
    legacyHeaders: false,
    limit: () => getDataApiLimits().authRateLimitMax,
    standardHeaders: true,
    windowMs: 60 * 1000,
    requestWasSuccessful: (req) => Boolean(req.apiKeyAccess),
    skipSuccessfulRequests: true,
    handler: (req, res) => {
      incrementDataApiMetric("authorization_failures", { code: "RATE_LIMITED" });
      return sendDataApiError(req, res, new DataApiError("RATE_LIMITED"));
    },
  });
}

function positiveAuditId(value) {
  if (!/^[1-9]\d*$/.test(`${value || ""}`)) return null;
  const parsed = Number(value);
  return Number.isSafeInteger(parsed) ? parsed : null;
}

async function auditAuthenticatedRateLimit(req) {
  const isChart = Boolean(req.params.chart_id);
  const traceContext = await startRun({
    triggerType: "data_api",
    entityType: isChart ? "chart_data" : "dataset_data",
    status: "running",
    apiKeyId: req.apiKeyAccess.apiKeyId,
    teamId: req.apiKeyAccess.teamId,
    projectId: positiveAuditId(req.params.project_id),
    chartId: positiveAuditId(req.params.chart_id),
    datasetId: positiveAuditId(req.params.dataset_id),
    summary: {
      apiVersion: "v1",
      method: req.method,
      requestId: req.id,
    },
  });
  await failRun(traceContext, new DataApiError("RATE_LIMITED"), {
    stage: "data_api",
    summary: {
      apiVersion: "v1",
      errorCode: "RATE_LIMITED",
      method: req.method,
      requestId: req.id,
      responseBytes: 0,
      statusCode: 429,
    },
  });
}

function createKeyLimiter() {
  return rateLimit({
    keyGenerator: (req) => req.apiKeyAccess.apiKeyId,
    legacyHeaders: false,
    limit: () => getDataApiLimits().rateLimitMax,
    standardHeaders: true,
    windowMs: 60 * 1000,
    handler: async (req, res) => {
      incrementDataApiMetric("requests", { resourceType: "rate_limit", statusCode: 429 });
      await auditAuthenticatedRateLimit(req);
      return sendDataApiError(req, res, new DataApiError("RATE_LIMITED"));
    },
  });
}

function sendResult(res, result) {
  setDataHeaders(res, result);
  if (result.notModified) return res.status(304).end();
  res.status(200).set("Content-Type", "application/json; charset=utf-8");
  return res.end(result.serialized);
}

function routeHandler(controllerMethod) {
  return async (req, res) => {
    try {
      return sendResult(res, await controllerMethod(req));
    } catch (error) {
      return sendDataApiError(req, res, error);
    }
  };
}

module.exports = (app) => {
  const controller = new DataApiController();
  const authLimiter = createAuthLimiter();
  const keyLimiter = createKeyLimiter();
  const access = [requestId, requireEnabled, validateHttpRequest, authLimiter, verifyDataApiKey(), keyLimiter];

  app.get(
    "/api/v1/teams/:team_id/datasets/:dataset_id/data",
    ...access,
    routeHandler(controller.datasetData.bind(controller))
  );
  app.post(
    "/api/v1/teams/:team_id/datasets/:dataset_id/data",
    ...access,
    routeHandler(controller.datasetData.bind(controller))
  );
  app.get(
    "/api/v1/projects/:project_id/charts/:chart_id/data",
    ...access,
    routeHandler(controller.chartData.bind(controller))
  );
  app.post(
    "/api/v1/projects/:project_id/charts/:chart_id/data",
    ...access,
    routeHandler(controller.chartData.bind(controller))
  );

  return (req, res, next) => next();
};