Files
sencho/backend/src/middleware/nodeContext.ts
T
Anso 856a260a11 refactor(backend): extract rate limiters, body parsing, and request gates (phase 1) (#731)
Phase 1 of the index.ts monolith refactor. Extracts all non-auth middleware
into focused modules. Routes and authMiddleware itself stay in index.ts for
now; Phase 2 introduces createApp() and extracts authMiddleware.

New modules:
- middleware/rateLimiters.ts: globalApiLimiter, pollingLimiter,
  webhookTriggerLimiter, authRateLimiter, ssoRateLimiter, trivyInstallLimiter
  plus the hybrid rateLimitKeyGenerator and isNodeProxyRequest helper
- middleware/jsonParser.ts: conditionalJsonParser that preserves the raw
  stream for remote-proxy forwarding (via helpers/proxyExemptPaths)
- middleware/nodeContext.ts: nodeContextMiddleware
- middleware/apiTokenScope.ts: enforceApiTokenScope + DEPLOY_ALLOWED_PATTERNS
- middleware/authGate.ts: createAuthGate(authMiddleware) factory + auditLog.
  Factory takes authMiddleware as a dependency to avoid a circular import
  until Phase 2 extracts the auth module.
- middleware/errorHandler.ts: central error handler that preserves
  err.status / err.expose from body-parser and other HTTP errors
- helpers/routePatterns.ts: WEBHOOK_TRIGGER_RE shared by rateLimiters and
  authGate

index.ts drops ~290 lines. Middleware registration order is unchanged.
All 1278 tests pass.

Code review fixes: typed ApiTokenScope in apiTokenScope.ts; replaced 2
em dashes with colons (Directive 18); added local CachedProxyFlagReq
type alias for the node_proxy memoization cast; extracted deny() helper.
2026-04-23 18:22:26 -04:00

36 lines
1.4 KiB
TypeScript

import type { Request, Response, NextFunction, RequestHandler } from 'express';
import { NodeRegistry } from '../services/NodeRegistry';
import { DatabaseService } from '../services/DatabaseService';
import { isProxyExemptPath } from '../helpers/proxyExemptPaths';
/**
* Resolve `req.nodeId` from the `x-node-id` header, `?nodeId=` query param,
* or the default node. Returns 404 for requests targeting a deleted node so
* downstream handlers don't fail with obscure errors.
*
* `/api/nodes` is intentionally exempt so the frontend can re-sync after a
* node is deleted (otherwise a stale `x-node-id` in localStorage triggers an
* unrecoverable 404 loop).
*/
export const nodeContextMiddleware: RequestHandler = (req: Request, res: Response, next: NextFunction) => {
const nodeIdHeader = req.headers['x-node-id'] as string;
const nodeIdQuery = req.query.nodeId as string;
if (nodeIdHeader) {
req.nodeId = parseInt(nodeIdHeader, 10);
} else if (nodeIdQuery) {
req.nodeId = parseInt(nodeIdQuery, 10);
} else {
req.nodeId = NodeRegistry.getInstance().getDefaultNodeId();
}
if (req.path.startsWith('/api/') && !isProxyExemptPath(req.path)) {
const node = DatabaseService.getInstance().getNode(req.nodeId);
if (!node) {
res.status(404).json({ error: `Node with id ${req.nodeId} not found or was deleted.` });
return;
}
}
next();
};