mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-08 18:05:10 +00:00
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:
@@ -0,0 +1,44 @@
|
||||
import type { Request, Response, NextFunction, RequestHandler } from 'express';
|
||||
import { isDebugEnabled } from '../utils/debug';
|
||||
import type { ApiTokenScope } from '../services/DatabaseService';
|
||||
|
||||
// Scope enforcement for API tokens: restricts which endpoints a token can reach.
|
||||
const DEPLOY_ALLOWED_PATTERNS: RegExp[] = [
|
||||
/^\/api\/stacks\/[^/]+\/deploy$/,
|
||||
/^\/api\/stacks\/[^/]+\/down$/,
|
||||
/^\/api\/stacks\/[^/]+\/restart$/,
|
||||
/^\/api\/stacks\/[^/]+\/stop$/,
|
||||
/^\/api\/stacks\/[^/]+\/start$/,
|
||||
/^\/api\/stacks\/[^/]+\/update$/,
|
||||
];
|
||||
|
||||
const deny = (res: Response, req: Request, error: string, scope: ApiTokenScope | 'unknown'): void => {
|
||||
if (isDebugEnabled()) console.log('[ApiTokenScope:diag] Denied:', req.method, req.path, 'scope:', scope);
|
||||
res.status(403).json({ error, code: 'SCOPE_DENIED' });
|
||||
};
|
||||
|
||||
export const enforceApiTokenScope: RequestHandler = (req: Request, res: Response, next: NextFunction): void => {
|
||||
const scope = req.apiTokenScope;
|
||||
if (!scope) { next(); return; } // Not an API token request
|
||||
if (isDebugEnabled()) console.log('[ApiTokenScope:diag]', req.method, req.path, 'scope:', scope);
|
||||
if (scope === 'full-admin') { next(); return; }
|
||||
|
||||
if (scope === 'read-only') {
|
||||
if (req.method === 'GET') { next(); return; }
|
||||
deny(res, req, 'API token scope "read-only" only allows GET requests.', scope);
|
||||
return;
|
||||
}
|
||||
|
||||
if (scope === 'deploy-only') {
|
||||
if (req.method === 'GET') { next(); return; }
|
||||
const fullPath = `/api${req.path}`;
|
||||
if (req.method === 'POST' && DEPLOY_ALLOWED_PATTERNS.some(p => p.test(fullPath))) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
deny(res, req, 'API token scope "deploy-only" does not allow this action.', scope);
|
||||
return;
|
||||
}
|
||||
|
||||
deny(res, req, 'Unknown API token scope.', 'unknown');
|
||||
};
|
||||
Reference in New Issue
Block a user