diff --git a/CHANGELOG.md b/CHANGELOG.md index 59b2471f..780d3781 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +- **Fixed:** Remote node proxy forwarding the browser's `sencho_token` cookie to the remote Sencho instance — the remote's `authMiddleware` evaluates `cookieToken || bearerToken` and the cookie (signed with the local JWT secret) was validated before the valid Bearer token, causing 401 on all proxied API calls. Fixed by stripping the `cookie` header in `proxyReq` so only the Bearer token is used for remote authentication. +- **Fixed:** `nodeContextMiddleware` blocking `/api/nodes` when `x-node-id` references a deleted/non-existent node — the nodes list endpoint must always succeed so the frontend can re-sync a stale node ID in localStorage; exempted alongside `/api/auth/`. - **Fixed:** Remote node proxy stripping the `/api` path prefix — `remoteNodeProxy` is mounted at `app.use('/api/', ...)` so Express strips that prefix from `req.url` before `http-proxy-middleware` sees it; added `pathRewrite: (path) => '/api' + path` to restore the full path when forwarding to the remote Sencho instance (e.g. `/stats` → `/api/stats`). This was the root cause of all remote API calls returning the remote's SPA HTML instead of JSON. - **Fixed:** Dashboard cards (Active Containers, Host CPU, Host RAM, Docker Network) showing stale local-node data after switching to a remote node — `HomeDashboard` polling effects now depend on `activeNode?.id` and clear state immediately on node change. - **Fixed:** `refreshStacks` crashing with `SyntaxError` or `TypeError` when the remote proxy returns a non-JSON response (e.g., connection refused to unreachable remote node) — now checks `res.ok` before calling `res.json()` and iterates a typed `fileList` instead of the raw parsed value. diff --git a/backend/src/index.ts b/backend/src/index.ts index a2ac912d..f82d503d 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -82,8 +82,15 @@ const nodeContextMiddleware = (req: Request, res: Response, next: NextFunction) req.nodeId = NodeRegistry.getInstance().getDefaultNodeId(); } - // Intercept requests to deleted nodes to prevent downstream errors - if (req.path.startsWith('/api/') && !req.path.startsWith('/api/auth/')) { + // Intercept requests to deleted nodes to prevent downstream errors. + // /api/nodes is intentionally exempt: it must always be reachable so the + // frontend can re-sync after a node is deleted (otherwise a stale x-node-id + // in localStorage causes an unrecoverable 404 loop). + if ( + req.path.startsWith('/api/') && + !req.path.startsWith('/api/auth/') && + !req.path.startsWith('/api/nodes') + ) { const node = DatabaseService.getInstance().getNode(req.nodeId); if (!node) { res.status(404).json({ error: `Node with id ${req.nodeId} not found or was deleted.` }); @@ -338,8 +345,13 @@ const remoteNodeProxy = createProxyMiddleware({ on: { proxyReq: (proxyReq, req) => { const node = NodeRegistry.getInstance().getNode(req.nodeId); - // Remote Sencho sees itself as local - strip node context and inject bearer auth + // Strip headers that must not reach the remote instance: + // - x-node-id: remote Sencho treats all requests as local + // - cookie: the browser's sencho_token is signed with THIS instance's JWT secret; + // the remote would try to verify it with its own secret and return 401. + // Authentication is handled exclusively via the Bearer token below. proxyReq.removeHeader('x-node-id'); + proxyReq.removeHeader('cookie'); if (node?.api_token) { proxyReq.setHeader('Authorization', `Bearer ${node.api_token}`); }