Files
sencho/backend/src/middleware/hubOnlyGuard.ts
T
Anso ccad5c925b feat(nodes): hide hub-only views when active node is remote (#1007)
* feat(nodes): hide hub-only views when active node is remote

Fleet, Schedules, Audit, Logs, and Auto-Update operate on hub-owned state
(node registry, fleet schedules, centralized audit, fleet-wide log
aggregation, fleet-wide update preview). When the active node is remote,
proxying those surfaces would show that remote's own disconnected state
instead of the hub's. Hide them from the nav strip and force-redirect to
Home if one was open during the node switch.

Backend hubOnlyGuard middleware sits between nodeContextMiddleware and the
remote proxy and rejects /api/scheduled-tasks, /api/audit-log, and
/api/notification-routes with 403 + HUB_ONLY_ENDPOINT when nodeId resolves
to a remote, closing the script-bypass path the UI gating cannot reach.

Settings sub-sections were already gated via the hiddenOnRemote registry;
this extends the same model to top-level views.

* docs(nodes): note hub-only visibility on Fleet, Schedules, Audit, Logs, Auto-Update

Each of the five hub-only feature pages now points readers to the
canonical "What top-level views show when a remote node is active"
section in multi-node.mdx, so users landing directly on a feature page
understand why the nav item disappears when they switch to a remote node.
2026-05-08 22:54:58 -04:00

39 lines
1.6 KiB
TypeScript

import type { Request, Response, NextFunction, RequestHandler } from 'express';
import { NodeRegistry } from '../services/NodeRegistry';
import { isHubOnlyPath } from '../helpers/proxyExemptPaths';
/**
* Reject hub-only API requests whose `req.nodeId` resolves to a remote node.
*
* The hub-level views in the frontend (Schedules, Audit, Notification
* Routing config, etc.) are hidden from the nav strip when the active node
* is remote, so a normal user never reaches these endpoints with a remote
* nodeId. A scripted client could still craft `x-node-id: <remote>` against
* one of these paths; without this guard, the request would be forwarded
* by `remoteNodeProxy` and processed on the remote as if it were local,
* silently crossing a node-authority boundary that the UI promised would
* not happen.
*
* Mounted at `/api` between `nodeContextMiddleware` (which sets req.nodeId)
* and `createRemoteProxyMiddleware` (which would otherwise forward the
* request). Rejects with 403 — the endpoint exists but the request cannot
* be served as routed.
*
* Returns 403 only for hub-only paths; non-hub paths fall through.
*/
export const hubOnlyGuard: RequestHandler = (req: Request, res: Response, next: NextFunction) => {
if (!isHubOnlyPath(`/api${req.path}`)) {
next();
return;
}
const node = NodeRegistry.getInstance().getNode(req.nodeId);
if (node?.type === 'remote') {
res.status(403).json({
error: 'This endpoint is hub-only and cannot be proxied to a remote node. Switch the active node back to your local hub.',
code: 'HUB_ONLY_ENDPOINT',
});
return;
}
next();
};