feat: Fix CrowdSec re-enrollment and live log viewer WebSocket
- Add logging when console enrollment is silently skipped - Add DELETE /admin/crowdsec/console/enrollment endpoint - Add enhanced re-enrollment UI with CrowdSec Console link - Fix WebSocket authentication by passing token in query params - Change Live Log Viewer default mode to security logs - Add error message display for failed WebSocket connections Fixes silent enrollment idempotency bug and WebSocket authentication issue causing disconnected log viewer.
This commit is contained in:
@@ -128,6 +128,12 @@ export const connectLiveLogs = (
|
||||
if (filters.level) params.append('level', filters.level);
|
||||
if (filters.source) params.append('source', filters.source);
|
||||
|
||||
// Get auth token from localStorage
|
||||
const token = localStorage.getItem('token');
|
||||
if (token) {
|
||||
params.append('token', token);
|
||||
}
|
||||
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const wsUrl = `${protocol}//${window.location.host}/api/v1/logs/live?${params.toString()}`;
|
||||
|
||||
@@ -190,6 +196,12 @@ export const connectSecurityLogs = (
|
||||
if (filters.host) params.append('host', filters.host);
|
||||
if (filters.blocked_only) params.append('blocked_only', 'true');
|
||||
|
||||
// Get auth token from localStorage
|
||||
const token = localStorage.getItem('token');
|
||||
if (token) {
|
||||
params.append('token', token);
|
||||
}
|
||||
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const wsUrl = `${protocol}//${window.location.host}/api/v1/cerberus/logs/ws?${params.toString()}`;
|
||||
|
||||
|
||||
@@ -137,13 +137,14 @@ const getLevelColor = (level: string): string => {
|
||||
export function LiveLogViewer({
|
||||
filters = {},
|
||||
securityFilters = {},
|
||||
mode = 'application',
|
||||
mode = 'security',
|
||||
maxLogs = 500,
|
||||
className = '',
|
||||
}: LiveLogViewerProps) {
|
||||
const [logs, setLogs] = useState<DisplayLogEntry[]>([]);
|
||||
const [isPaused, setIsPaused] = useState(false);
|
||||
const [isConnected, setIsConnected] = useState(false);
|
||||
const [connectionError, setConnectionError] = useState<string | null>(null);
|
||||
const [currentMode, setCurrentMode] = useState<LogMode>(mode);
|
||||
const [textFilter, setTextFilter] = useState('');
|
||||
const [levelFilter, setLevelFilter] = useState('');
|
||||
@@ -180,11 +181,13 @@ export function LiveLogViewer({
|
||||
const handleOpen = () => {
|
||||
console.log(`${currentMode} log viewer connected`);
|
||||
setIsConnected(true);
|
||||
setConnectionError(null);
|
||||
};
|
||||
|
||||
const handleError = (error: Event) => {
|
||||
console.error('WebSocket error:', error);
|
||||
console.error(`${currentMode} log viewer error:`, error);
|
||||
setIsConnected(false);
|
||||
setConnectionError('Failed to connect to log stream. Check your authentication or try refreshing.');
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
@@ -318,6 +321,11 @@ export function LiveLogViewer({
|
||||
>
|
||||
{isConnected ? 'Connected' : 'Disconnected'}
|
||||
</span>
|
||||
{connectionError && (
|
||||
<div className="text-xs text-red-400 bg-red-900/20 px-2 py-1 rounded">
|
||||
{connectionError}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Mode toggle */}
|
||||
|
||||
@@ -54,7 +54,8 @@ describe('LiveLogViewer', () => {
|
||||
it('renders the component with initial state', async () => {
|
||||
render(<LiveLogViewer />);
|
||||
|
||||
expect(screen.getByText('Live Security Logs')).toBeTruthy();
|
||||
// Default mode is now 'security'
|
||||
expect(screen.getByText('Security Access Logs')).toBeTruthy();
|
||||
// Initially disconnected until WebSocket opens
|
||||
expect(screen.getByText('Disconnected')).toBeTruthy();
|
||||
|
||||
@@ -67,7 +68,8 @@ describe('LiveLogViewer', () => {
|
||||
});
|
||||
|
||||
it('displays incoming log messages', async () => {
|
||||
render(<LiveLogViewer />);
|
||||
// Explicitly use application mode for this test
|
||||
render(<LiveLogViewer mode="application" />);
|
||||
|
||||
// Simulate receiving a log
|
||||
const logEntry: logsApi.LiveLogEntry = {
|
||||
@@ -90,7 +92,8 @@ describe('LiveLogViewer', () => {
|
||||
|
||||
it('filters logs by text', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<LiveLogViewer />);
|
||||
// Explicitly use application mode for this test
|
||||
render(<LiveLogViewer mode="application" />);
|
||||
|
||||
// Add multiple logs
|
||||
if (mockOnMessage) {
|
||||
@@ -115,7 +118,8 @@ describe('LiveLogViewer', () => {
|
||||
|
||||
it('filters logs by level', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<LiveLogViewer />);
|
||||
// Explicitly use application mode for this test
|
||||
render(<LiveLogViewer mode="application" />);
|
||||
|
||||
// Add multiple logs
|
||||
if (mockOnMessage) {
|
||||
@@ -140,7 +144,8 @@ describe('LiveLogViewer', () => {
|
||||
|
||||
it('pauses and resumes log streaming', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<LiveLogViewer />);
|
||||
// Explicitly use application mode for this test
|
||||
render(<LiveLogViewer mode="application" />);
|
||||
|
||||
// Add initial log
|
||||
if (mockOnMessage) {
|
||||
@@ -184,7 +189,8 @@ describe('LiveLogViewer', () => {
|
||||
|
||||
it('clears all logs', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<LiveLogViewer />);
|
||||
// Explicitly use application mode for this test
|
||||
render(<LiveLogViewer mode="application" />);
|
||||
|
||||
// Add logs
|
||||
if (mockOnMessage) {
|
||||
@@ -209,7 +215,8 @@ describe('LiveLogViewer', () => {
|
||||
});
|
||||
|
||||
it('limits the number of stored logs', async () => {
|
||||
render(<LiveLogViewer maxLogs={2} />);
|
||||
// Explicitly use application mode for this test
|
||||
render(<LiveLogViewer maxLogs={2} mode="application" />);
|
||||
|
||||
// Add 3 logs (exceeding maxLogs)
|
||||
if (mockOnMessage) {
|
||||
@@ -227,7 +234,8 @@ describe('LiveLogViewer', () => {
|
||||
});
|
||||
|
||||
it('displays log data when available', async () => {
|
||||
render(<LiveLogViewer />);
|
||||
// Explicitly use application mode for this test
|
||||
render(<LiveLogViewer mode="application" />);
|
||||
|
||||
const logWithData: logsApi.LiveLogEntry = {
|
||||
level: 'error',
|
||||
@@ -250,7 +258,8 @@ describe('LiveLogViewer', () => {
|
||||
it('closes WebSocket connection on unmount', () => {
|
||||
const { unmount } = render(<LiveLogViewer />);
|
||||
|
||||
expect(logsApi.connectLiveLogs).toHaveBeenCalled();
|
||||
// Default mode is security
|
||||
expect(logsApi.connectSecurityLogs).toHaveBeenCalled();
|
||||
|
||||
unmount();
|
||||
|
||||
@@ -268,7 +277,8 @@ describe('LiveLogViewer', () => {
|
||||
let mockOnOpen: (() => void) | undefined;
|
||||
let mockOnError: ((error: Event) => void) | undefined;
|
||||
|
||||
vi.mocked(logsApi.connectLiveLogs).mockImplementation((_filters, _onMessage, onOpen, onError) => {
|
||||
// Use security logs mock since default mode is security
|
||||
vi.mocked(logsApi.connectSecurityLogs).mockImplementation((_filters, _onMessage, onOpen, onError) => {
|
||||
mockOnOpen = onOpen;
|
||||
mockOnError = onError;
|
||||
return mockCloseConnection as () => void;
|
||||
@@ -295,12 +305,15 @@ describe('LiveLogViewer', () => {
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Disconnected')).toBeTruthy();
|
||||
// Should show error message
|
||||
expect(screen.getByText('Failed to connect to log stream. Check your authentication or try refreshing.')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it('shows no-match message when filters exclude all logs', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<LiveLogViewer />);
|
||||
// Explicitly use application mode for this test
|
||||
render(<LiveLogViewer mode="application" />);
|
||||
|
||||
if (mockOnMessage) {
|
||||
mockOnMessage({ level: 'info', timestamp: '2025-12-09T10:30:00Z', message: 'Visible' });
|
||||
|
||||
Reference in New Issue
Block a user