diff --git a/CHANGELOG.md b/CHANGELOG.md index a3a18323..59e91560 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 authentication failures by updating middleware to support Bearer tokens in WebSocket upgrade handler (node-to-node WS proxy now authenticates correctly on the receiving instance). +- **Fixed:** Node connection testing logic updated to normalize `api_url` trailing slashes before constructing the authenticated HTTP ping URL. - **Fixed:** Memory leak in `GlobalObservabilityView` SSE mode — log array now capped at 10,000 entries (`.slice(-10000)`) to prevent unbounded accumulation across long sessions. - **Fixed:** Infinite re-fetch loop in `NodeContext` — `refreshNodes` useCallback no longer depends on `activeNode` state; replaced with a `useRef` to read current node inside the callback without being a reactive dependency. - **Fixed:** Infinite page reload loop — `apiFetch` was calling `window.location.href = '/'` on every 401, causing a full browser reload before auth could complete. Replaced with a `sencho-unauthorized` custom event that `AuthContext` handles by setting `appStatus` to `notAuthenticated`. diff --git a/backend/src/index.ts b/backend/src/index.ts index 4ef72ed1..8b48e903 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -123,7 +123,8 @@ const authMiddleware = async (req: Request, res: Response, next: NextFunction): // Accept both user sessions and node proxy tokens req.user = { username: decoded.username || 'node-proxy' }; next(); - } catch { + } catch (err) { + console.error('[Auth] Token validation failed:', (err as Error).message); res.status(401).json({ error: 'Invalid or expired token' }); return; } @@ -327,7 +328,7 @@ app.use('/api/', (req: Request, res: Response, next: NextFunction): void => { } createProxyMiddleware({ - target: node.api_url, + target: node.api_url.replace(/\/$/, ''), changeOrigin: true, on: { proxyReq: (proxyReq) => { @@ -362,7 +363,11 @@ server.on('upgrade', async (req, socket, head) => { cookieHeader.split(';').map(c => c.trim().split('=')).filter(([k, v]) => k && v) ); - const token = cookies[COOKIE_NAME]; + // Accept either cookie auth (browser sessions) or Bearer token auth (node-to-node WS proxy) + const cookieToken = cookies[COOKIE_NAME]; + const authHeader = req.headers['authorization'] as string | undefined; + const bearerToken = authHeader?.startsWith('Bearer ') ? authHeader.slice(7) : null; + const token = cookieToken || bearerToken; if (!token) { socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n'); @@ -387,7 +392,7 @@ server.on('upgrade', async (req, socket, head) => { // Remote Node WebSocket Proxy — forward the entire WS connection to the remote Sencho instance if (node && node.type === 'remote' && node.api_url && node.api_token) { - const wsTarget = node.api_url.replace(/^https?/, (m) => m === 'https' ? 'wss' : 'ws'); + const wsTarget = node.api_url.replace(/\/$/, '').replace(/^https?/, (m) => m === 'https' ? 'wss' : 'ws'); req.headers['authorization'] = `Bearer ${node.api_token}`; delete req.headers['x-node-id']; wsProxyServer.ws(req, socket, head, { target: wsTarget }); diff --git a/backend/src/services/NodeRegistry.ts b/backend/src/services/NodeRegistry.ts index 00c65b0a..85a22d6c 100644 --- a/backend/src/services/NodeRegistry.ts +++ b/backend/src/services/NodeRegistry.ts @@ -154,7 +154,8 @@ export class NodeRegistry { } try { - const response = await axios.get(`${node.api_url}/api/auth/check`, { + const baseUrl = node.api_url.replace(/\/$/, ''); + const response = await axios.get(`${baseUrl}/api/auth/check`, { headers: { Authorization: `Bearer ${node.api_token}` }, timeout: 8000, });