mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-09-03 06:07:58 +00:00
Merge pull request #31 from AnsoCode/fix/distributed-api-auth
fix: Distributed API Auth
This commit is contained in:
@@ -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).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
## [Unreleased]
|
## [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:** 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 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`.
|
- **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`.
|
||||||
|
|||||||
@@ -123,7 +123,8 @@ const authMiddleware = async (req: Request, res: Response, next: NextFunction):
|
|||||||
// Accept both user sessions and node proxy tokens
|
// Accept both user sessions and node proxy tokens
|
||||||
req.user = { username: decoded.username || 'node-proxy' };
|
req.user = { username: decoded.username || 'node-proxy' };
|
||||||
next();
|
next();
|
||||||
} catch {
|
} catch (err) {
|
||||||
|
console.error('[Auth] Token validation failed:', (err as Error).message);
|
||||||
res.status(401).json({ error: 'Invalid or expired token' });
|
res.status(401).json({ error: 'Invalid or expired token' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -327,7 +328,7 @@ app.use('/api/', (req: Request, res: Response, next: NextFunction): void => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
createProxyMiddleware<Request, Response>({
|
createProxyMiddleware<Request, Response>({
|
||||||
target: node.api_url,
|
target: node.api_url.replace(/\/$/, ''),
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
on: {
|
on: {
|
||||||
proxyReq: (proxyReq) => {
|
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)
|
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) {
|
if (!token) {
|
||||||
socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
|
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
|
// 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) {
|
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}`;
|
req.headers['authorization'] = `Bearer ${node.api_token}`;
|
||||||
delete req.headers['x-node-id'];
|
delete req.headers['x-node-id'];
|
||||||
wsProxyServer.ws(req, socket, head, { target: wsTarget });
|
wsProxyServer.ws(req, socket, head, { target: wsTarget });
|
||||||
|
|||||||
@@ -154,7 +154,8 @@ export class NodeRegistry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
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}` },
|
headers: { Authorization: `Bearer ${node.api_token}` },
|
||||||
timeout: 8000,
|
timeout: 8000,
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user