fix(ws): фильтрация уведомлений и подписок по роли

Refs EventHub/EventHubFrontAdmin#8

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-13 17:14:51 +03:00
parent 9deaa34a6e
commit 9ff894988f
3 changed files with 50 additions and 10 deletions
+13
View File
@@ -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);
}
+25 -4
View File
@@ -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');
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) {
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;
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]);
};
+8 -2
View File
@@ -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');