fix(web): restore SSE streams via single-use ticket flow
The V3.1.1 backend change moved SSE auth off ?token=<JWT> onto a single-use
?ticket=, but the dashboard was never updated, so every live stream 401'd
('Could not validate credentials'). Add mintSseTicket() (POST /auth/sse-ticket
with the Bearer JWT, returns an opaque 60s single-use ticket) and refactor all
stream consumers to mint a fresh ticket at the top of each connect() — initial
and every reconnect — then open EventSource with ?ticket=. A reused single-use
ticket would 401-loop, so re-mint-per-connect is required.
Covers Dashboard /stream, LiveLogs, and the attacker/identity/campaign/
orchestrator/topology hooks. connect() is now async with an unmount guard
(cancelled flag checked after the await, before opening the stream); on a mint
401 the connect is skipped and the axios logout interceptor takes over.
This commit is contained in:
@@ -10,6 +10,7 @@
|
||||
* pending topologies don't open a useless channel.
|
||||
*/
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { mintSseTicket } from '../../utils/sseTicket';
|
||||
|
||||
export type TopologyStreamEventName =
|
||||
| 'snapshot'
|
||||
@@ -69,11 +70,25 @@ export function useTopologyStream({
|
||||
useEffect(() => {
|
||||
if (!enabled || !topologyId) return;
|
||||
|
||||
const connect = () => {
|
||||
let cancelled = false;
|
||||
|
||||
const connect = async () => {
|
||||
if (esRef.current) esRef.current.close();
|
||||
const token = localStorage.getItem('token') ?? '';
|
||||
|
||||
let ticket: string;
|
||||
try {
|
||||
ticket = await mintSseTicket();
|
||||
} catch {
|
||||
onErrorRef.current?.();
|
||||
if (!cancelled) {
|
||||
reconnectRef.current = setTimeout(connect, 3000);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (cancelled) return;
|
||||
|
||||
const baseUrl = import.meta.env.VITE_API_URL || 'http://localhost:8000/api/v1';
|
||||
const url = `${baseUrl}/topologies/${topologyId}/events?token=${encodeURIComponent(token)}`;
|
||||
const url = `${baseUrl}/topologies/${topologyId}/events?ticket=${encodeURIComponent(ticket)}`;
|
||||
|
||||
const es = new EventSource(url);
|
||||
esRef.current = es;
|
||||
@@ -101,13 +116,16 @@ export function useTopologyStream({
|
||||
es.close();
|
||||
esRef.current = null;
|
||||
onErrorRef.current?.();
|
||||
reconnectRef.current = setTimeout(connect, 3000);
|
||||
if (!cancelled) {
|
||||
reconnectRef.current = setTimeout(connect, 3000);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
connect();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
if (reconnectRef.current) clearTimeout(reconnectRef.current);
|
||||
if (esRef.current) esRef.current.close();
|
||||
esRef.current = null;
|
||||
|
||||
Reference in New Issue
Block a user