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.
This commit is contained in:
Anso
2026-04-23 18:22:26 -04:00
committed by GitHub
parent 929e2fa6b1
commit 856a260a11
8 changed files with 396 additions and 313 deletions
+66
View File
@@ -0,0 +1,66 @@
import type { Request, Response, NextFunction, RequestHandler } from 'express';
import { DatabaseService } from '../services/DatabaseService';
import { isDebugEnabled } from '../utils/debug';
import { getAuditSummary } from '../utils/audit-summaries';
import { WEBHOOK_TRIGGER_RE } from '../helpers/routePatterns';
/**
* Build the `/api/*` auth gate. Mounted at `/api`, so paths it sees are
* already stripped of the `/api` prefix. Exempts `/auth/*` (setup, login,
* SSO: handled by their own routes) and webhook triggers (authenticated
* via HMAC, not session).
*
* Takes `authMiddleware` as a dependency instead of importing it so this
* file does not pin the monolith's auth lifecycle. Phase 2 extracts
* `authMiddleware` into its own module.
*/
export function createAuthGate(authMiddleware: RequestHandler): RequestHandler {
return (req: Request, res: Response, next: NextFunction): void => {
if (req.path.startsWith('/auth/') || WEBHOOK_TRIGGER_RE.test(req.path)) {
next();
return;
}
authMiddleware(req, res, next);
};
}
/**
* Audit-logging middleware. Records every mutating `/api/*` action for
* Admiral accountability. Mounted at `/api`. Uses `res.on('finish')` to
* capture the final status code.
*/
export const auditLog: RequestHandler = (req: Request, res: Response, next: NextFunction): void => {
if (!['POST', 'PUT', 'DELETE', 'PATCH'].includes(req.method)) {
next();
return;
}
const username = req.user?.username || 'unknown';
const nodeId = req.nodeId ?? null;
const forwarded = req.headers['x-forwarded-for'];
const xff = typeof forwarded === 'string' ? forwarded.split(',')[0].trim() : '';
const ip = req.ip || xff || '';
const apiPath = req.path;
res.on('finish', () => {
try {
if (isDebugEnabled()) {
console.log(`[Audit:diag] ${req.method} /api${apiPath} by=${username} status=${res.statusCode} node=${nodeId ?? 'local'} ip=${ip}`);
}
DatabaseService.getInstance().insertAuditLog({
timestamp: Date.now(),
username,
method: req.method,
path: `/api${apiPath}`,
status_code: res.statusCode,
node_id: nodeId,
ip_address: ip,
summary: getAuditSummary(req.method, apiPath),
});
} catch (err) {
console.error('[Audit] Failed to write audit log:', err);
}
});
next();
};