From 9ff894988f6c6e14b62dfc74d1228d2307b7def5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BB=D0=B5=D0=BA=D1=81=D0=B5=D0=B9=20=D0=A1=D0=B0?= =?UTF-8?q?=D0=B1=D0=B8=D0=BB=D0=B8=D0=BD?= Date: Mon, 13 Jul 2026 17:14:51 +0300 Subject: [PATCH] =?UTF-8?q?fix(ws):=20=D1=84=D0=B8=D0=BB=D1=8C=D1=82=D1=80?= =?UTF-8?q?=D0=B0=D1=86=D0=B8=D1=8F=20=D1=83=D0=B2=D0=B5=D0=B4=D0=BE=D0=BC?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=B8=20=D0=BF=D0=BE=D0=B4?= =?UTF-8?q?=D0=BF=D0=B8=D1=81=D0=BE=D0=BA=20=D0=BF=D0=BE=20=D1=80=D0=BE?= =?UTF-8?q?=D0=BB=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refs EventHub/EventHubFrontAdmin#8 Co-authored-by: Cursor --- src/config/adminAccess.ts | 13 ++++++++++++ src/hooks/useAdminWebSocket.ts | 37 ++++++++++++++++++++++++++-------- src/layouts/AdminLayout.tsx | 10 +++++++-- 3 files changed, 50 insertions(+), 10 deletions(-) diff --git a/src/config/adminAccess.ts b/src/config/adminAccess.ts index 33498c6..cdaad47 100644 --- a/src/config/adminAccess.ts +++ b/src/config/adminAccess.ts @@ -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); +} diff --git a/src/hooks/useAdminWebSocket.ts b/src/hooks/useAdminWebSocket.ts index 5f0e860..c11ec7e 100644 --- a/src/hooks/useAdminWebSocket.ts +++ b/src/hooks/useAdminWebSocket.ts @@ -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]); }; diff --git a/src/layouts/AdminLayout.tsx b/src/layouts/AdminLayout.tsx index fc95d8d..ab149ea 100644 --- a/src/layouts/AdminLayout.tsx +++ b/src/layouts/AdminLayout.tsx @@ -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');