fix(ws): фильтрация уведомлений и подписок по роли
Refs EventHub/EventHubFrontAdmin#8 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -67,3 +67,16 @@ export function getMenuSelectedKey(pathname: string): string {
|
||||
}
|
||||
return pathname;
|
||||
}
|
||||
|
||||
const WS_NOTIFICATION_ROUTES: Record<'report_created' | 'ticket_created', string> = {
|
||||
report_created: '/reports',
|
||||
ticket_created: '/tickets',
|
||||
};
|
||||
|
||||
/** Доступ к WS-уведомлению по роли (согласовано с ROUTE_ACCESS). */
|
||||
export function canReceiveWsNotification(
|
||||
type: 'report_created' | 'ticket_created',
|
||||
role: string | undefined
|
||||
): boolean {
|
||||
return canAccessRoute(WS_NOTIFICATION_ROUTES[type], role);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useEffect, useRef } from 'react';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { useAuthStore } from '../store/authStore';
|
||||
import { useMetricsStore } from '../store/metricsStore';
|
||||
import { canAccessRoute, canReceiveWsNotification } from '../config/adminAccess';
|
||||
|
||||
type WsMessage = {
|
||||
type: 'report_created' | 'ticket_created' | 'node_metric';
|
||||
@@ -11,10 +12,17 @@ type WsMessage = {
|
||||
timestamp?: number;
|
||||
};
|
||||
|
||||
const wsLog = (...args: unknown[]) => {
|
||||
if (import.meta.env.DEV) {
|
||||
console.log(...args);
|
||||
}
|
||||
};
|
||||
|
||||
export const useAdminWebSocket = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const accessToken = useAuthStore((s) => s.accessToken);
|
||||
const isAuthenticated = useAuthStore((s) => s.isAuthenticated);
|
||||
const role = useAuthStore((s) => s.user?.role);
|
||||
const setCurrentMetric = useMetricsStore((s) => s.setCurrent);
|
||||
const addToAllHistory = useMetricsStore((s) => s.addToAllHistory);
|
||||
|
||||
@@ -74,9 +82,13 @@ export const useAdminWebSocket = () => {
|
||||
|
||||
ws.onopen = () => {
|
||||
if (!isMounted || !shouldReconnectRef.current) return;
|
||||
console.log('[WS] Connected');
|
||||
ws.send(JSON.stringify({ action: 'subscribe', channel: 'reports' }));
|
||||
ws.send(JSON.stringify({ action: 'subscribe', channel: 'tickets' }));
|
||||
wsLog('[WS] Connected');
|
||||
if (canAccessRoute('/reports', role)) {
|
||||
ws.send(JSON.stringify({ action: 'subscribe', channel: 'reports' }));
|
||||
}
|
||||
if (canAccessRoute('/tickets', role)) {
|
||||
ws.send(JSON.stringify({ action: 'subscribe', channel: 'tickets' }));
|
||||
}
|
||||
|
||||
pingIntervalRef.current = setInterval(() => {
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
@@ -89,7 +101,7 @@ export const useAdminWebSocket = () => {
|
||||
if (!isMounted) return;
|
||||
try {
|
||||
const msg: WsMessage = JSON.parse(event.data);
|
||||
console.log('[WS] Message:', msg);
|
||||
wsLog('[WS] Message:', msg);
|
||||
|
||||
if (msg.timestamp && msg.timestamp === lastMessageTimestamp.current) {
|
||||
return;
|
||||
@@ -99,9 +111,14 @@ export const useAdminWebSocket = () => {
|
||||
}
|
||||
|
||||
if (msg.type === 'report_created' || msg.type === 'ticket_created') {
|
||||
if (!canReceiveWsNotification(msg.type, role)) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('admin-ws-message', { detail: msg })
|
||||
);
|
||||
|
||||
if (msg.type === 'report_created') {
|
||||
queryClient.invalidateQueries({ queryKey: ['reports'] });
|
||||
} else if (msg.type === 'ticket_created') {
|
||||
@@ -113,13 +130,15 @@ export const useAdminWebSocket = () => {
|
||||
addToAllHistory(msg.data);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[WS] Failed to parse message:', e);
|
||||
if (import.meta.env.DEV) {
|
||||
console.warn('[WS] Failed to parse message:', e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
ws.onclose = (event) => {
|
||||
if (!isMounted || !shouldReconnectRef.current) return;
|
||||
console.log('[WS] Disconnected:', event.reason);
|
||||
wsLog('[WS] Disconnected:', event.reason);
|
||||
clearTimers();
|
||||
wsRef.current = null;
|
||||
reconnectTimeoutRef.current = setTimeout(connect, 5000);
|
||||
@@ -127,7 +146,9 @@ export const useAdminWebSocket = () => {
|
||||
|
||||
ws.onerror = (error) => {
|
||||
if (!isMounted) return;
|
||||
console.error('[WS] Error:', error);
|
||||
if (import.meta.env.DEV) {
|
||||
console.error('[WS] Error:', error);
|
||||
}
|
||||
ws.close();
|
||||
};
|
||||
};
|
||||
@@ -139,5 +160,5 @@ export const useAdminWebSocket = () => {
|
||||
shouldReconnectRef.current = false;
|
||||
closeSocket();
|
||||
};
|
||||
}, [isAuthenticated, accessToken, queryClient, setCurrentMetric, addToAllHistory]);
|
||||
}, [isAuthenticated, accessToken, role, queryClient, setCurrentMetric, addToAllHistory]);
|
||||
};
|
||||
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
CloudServerOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import dayjs from 'dayjs';
|
||||
import { MENU_ITEMS, filterMenuByRole, getMenuSelectedKey } from '../config/adminAccess';
|
||||
import { MENU_ITEMS, filterMenuByRole, getMenuSelectedKey, canReceiveWsNotification } from '../config/adminAccess';
|
||||
|
||||
const { Header, Sider, Content } = Layout;
|
||||
const { Text } = Typography;
|
||||
@@ -45,6 +45,9 @@ const AdminLayout: React.FC = () => {
|
||||
const handler = (event: CustomEvent) => {
|
||||
const msg = event.detail;
|
||||
if (msg.type === 'report_created') {
|
||||
if (!canReceiveWsNotification('report_created', user?.role)) {
|
||||
return;
|
||||
}
|
||||
const { report_id, target_type, target_id, reason } = msg.data || {};
|
||||
notification.info({
|
||||
title: 'Новая жалоба',
|
||||
@@ -68,6 +71,9 @@ const AdminLayout: React.FC = () => {
|
||||
placement: 'topRight',
|
||||
});
|
||||
} else if (msg.type === 'ticket_created') {
|
||||
if (!canReceiveWsNotification('ticket_created', user?.role)) {
|
||||
return;
|
||||
}
|
||||
const { ticket_id } = msg.data || {};
|
||||
notification.info({
|
||||
title: 'Новый тикет',
|
||||
@@ -88,7 +94,7 @@ const AdminLayout: React.FC = () => {
|
||||
};
|
||||
window.addEventListener('admin-ws-message', handler as EventListener);
|
||||
return () => window.removeEventListener('admin-ws-message', handler as EventListener);
|
||||
}, [navigate]);
|
||||
}, [navigate, user?.role]);
|
||||
|
||||
const onlineNodes = React.useMemo(() => {
|
||||
const cutoff = dayjs().subtract(NODE_STATS_THRESHOLD, 'minute');
|
||||
|
||||
Reference in New Issue
Block a user