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