From b882dfa8f657c457616dea970fb391106d468508 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=E3=80=82?= Date: Mon, 1 Sep 2025 09:52:52 +0800 Subject: [PATCH 1/4] =?UTF-8?q?=E4=BF=AE=E5=A4=8D"=E8=BE=B9=E6=A0=8F"?= =?UTF-8?q?=E9=9A=90=E8=97=8F=E5=90=8E=E6=97=A0=E6=B3=95=E5=8D=B3=E6=97=B6?= =?UTF-8?q?=E7=94=9F=E6=95=88=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../personal/cards/NotificationSettings.jsx | 9 ++++++- web/src/hooks/common/useSidebar.js | 27 ++++++++++++++++--- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/web/src/components/settings/personal/cards/NotificationSettings.jsx b/web/src/components/settings/personal/cards/NotificationSettings.jsx index 0b097eaf..aad612d2 100644 --- a/web/src/components/settings/personal/cards/NotificationSettings.jsx +++ b/web/src/components/settings/personal/cards/NotificationSettings.jsx @@ -44,6 +44,7 @@ import CodeViewer from '../../../playground/CodeViewer'; import { StatusContext } from '../../../../context/Status'; import { UserContext } from '../../../../context/User'; import { useUserPermissions } from '../../../../hooks/common/useUserPermissions'; +import { useSidebar } from '../../../../hooks/common/useSidebar'; const NotificationSettings = ({ t, @@ -97,6 +98,9 @@ const NotificationSettings = ({ isSidebarModuleAllowed, } = useUserPermissions(); + // 使用useSidebar钩子获取刷新方法 + const { refreshUserConfig } = useSidebar(); + // 左侧边栏设置处理函数 const handleSectionChange = (sectionKey) => { return (checked) => { @@ -132,6 +136,9 @@ const NotificationSettings = ({ }); if (res.data.success) { showSuccess(t('侧边栏设置保存成功')); + + // 刷新useSidebar钩子中的用户配置,实现实时更新 + await refreshUserConfig(); } else { showError(res.data.message); } @@ -334,7 +341,7 @@ const NotificationSettings = ({ loading={sidebarLoading} className='!rounded-lg' > - {t('保存边栏设置')} + {t('保存设置')} ) : ( diff --git a/web/src/hooks/common/useSidebar.js b/web/src/hooks/common/useSidebar.js index 5dce44f9..e964855e 100644 --- a/web/src/hooks/common/useSidebar.js +++ b/web/src/hooks/common/useSidebar.js @@ -21,6 +21,10 @@ import { useState, useEffect, useMemo, useContext } from 'react'; import { StatusContext } from '../../context/Status'; import { API } from '../../helpers'; +// 创建一个全局事件系统来同步所有useSidebar实例 +const sidebarEventTarget = new EventTarget(); +const SIDEBAR_REFRESH_EVENT = 'sidebar-refresh'; + export const useSidebar = () => { const [statusState] = useContext(StatusContext); const [userConfig, setUserConfig] = useState(null); @@ -124,9 +128,11 @@ export const useSidebar = () => { // 刷新用户配置的方法(供外部调用) const refreshUserConfig = async () => { - if (Object.keys(adminConfig).length > 0) { - await loadUserConfig(); - } + // 移除adminConfig的条件限制,直接刷新用户配置 + await loadUserConfig(); + + // 触发全局刷新事件,通知所有useSidebar实例更新 + sidebarEventTarget.dispatchEvent(new CustomEvent(SIDEBAR_REFRESH_EVENT)); }; // 加载用户配置 @@ -137,6 +143,21 @@ export const useSidebar = () => { } }, [adminConfig]); + // 监听全局刷新事件 + useEffect(() => { + const handleRefresh = () => { + if (Object.keys(adminConfig).length > 0) { + loadUserConfig(); + } + }; + + sidebarEventTarget.addEventListener(SIDEBAR_REFRESH_EVENT, handleRefresh); + + return () => { + sidebarEventTarget.removeEventListener(SIDEBAR_REFRESH_EVENT, handleRefresh); + }; + }, [adminConfig]); + // 计算最终的显示配置 const finalConfig = useMemo(() => { const result = {}; From f23be16e981c5940de59243d0155d8a80a28227e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=E3=80=82?= Date: Mon, 1 Sep 2025 10:20:15 +0800 Subject: [PATCH 2/4] =?UTF-8?q?=E4=BF=AE=E5=A4=8D"=E8=BE=B9=E6=A0=8F"?= =?UTF-8?q?=E6=9D=83=E9=99=90=E6=8E=A7=E5=88=B6=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/src/hooks/common/useSidebar.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/web/src/hooks/common/useSidebar.js b/web/src/hooks/common/useSidebar.js index e964855e..13d76fd8 100644 --- a/web/src/hooks/common/useSidebar.js +++ b/web/src/hooks/common/useSidebar.js @@ -128,8 +128,9 @@ export const useSidebar = () => { // 刷新用户配置的方法(供外部调用) const refreshUserConfig = async () => { - // 移除adminConfig的条件限制,直接刷新用户配置 - await loadUserConfig(); + if (Object.keys(adminConfig).length > 0) { + await loadUserConfig(); + } // 触发全局刷新事件,通知所有useSidebar实例更新 sidebarEventTarget.dispatchEvent(new CustomEvent(SIDEBAR_REFRESH_EVENT)); From 1894ddc786cd0bde7a2affee3a88fb81e9567841 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=E3=80=82?= Date: Tue, 2 Sep 2025 18:10:08 +0800 Subject: [PATCH 3/4] =?UTF-8?q?=E6=94=B9=E8=BF=9B"=E4=BE=A7=E8=BE=B9?= =?UTF-8?q?=E6=A0=8F"=E6=9D=83=E9=99=90=E6=8E=A7=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/src/components/auth/ModuleRoute.jsx | 200 ++++++++++++++++++++++++ 1 file changed, 200 insertions(+) create mode 100644 web/src/components/auth/ModuleRoute.jsx diff --git a/web/src/components/auth/ModuleRoute.jsx b/web/src/components/auth/ModuleRoute.jsx new file mode 100644 index 00000000..3f208c7f --- /dev/null +++ b/web/src/components/auth/ModuleRoute.jsx @@ -0,0 +1,200 @@ +/* +Copyright (C) 2025 QuantumNous + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + +For commercial licensing, please contact support@quantumnous.com +*/ + +import React, { useState, useEffect, useContext } from 'react'; +import { Navigate } from 'react-router-dom'; +import { StatusContext } from '../../context/Status'; +import Loading from '../common/ui/Loading'; +import { API } from '../../helpers'; + +/** + * ModuleRoute - 基于功能模块权限的路由保护组件 + * + * @param {Object} props + * @param {React.ReactNode} props.children - 要保护的子组件 + * @param {string} props.modulePath - 模块权限路径,如 "admin.channel", "console.token" + * @param {React.ReactNode} props.fallback - 无权限时显示的组件,默认跳转到 /forbidden + * @returns {React.ReactNode} + */ +const ModuleRoute = ({ children, modulePath, fallback = }) => { + const [hasPermission, setHasPermission] = useState(null); + const [statusState] = useContext(StatusContext); + + useEffect(() => { + checkModulePermission(); + }, [modulePath, statusState?.status]); // 只在status数据变化时重新检查 + + const checkModulePermission = async () => { + try { + // 检查用户是否已登录 + const user = localStorage.getItem('user'); + if (!user) { + setHasPermission(false); + return; + } + + const userData = JSON.parse(user); + const userRole = userData.role; + + // 超级管理员始终有权限 + if (userRole >= 100) { + setHasPermission(true); + return; + } + + // 检查模块权限 + const permission = await checkModulePermissionAPI(modulePath); + + // 如果返回null,表示status数据还未加载完成,保持loading状态 + if (permission === null) { + setHasPermission(null); + return; + } + + setHasPermission(permission); + } catch (error) { + console.error('检查模块权限失败:', error); + // 出错时采用安全优先策略,拒绝访问 + setHasPermission(false); + } + }; + + const checkModulePermissionAPI = async (modulePath) => { + try { + // 数据看板始终允许访问,不受控制台区域开关影响 + if (modulePath === 'console.detail') { + return true; + } + + // 从StatusContext中获取配置信息 + // 如果status数据还未加载完成,返回null表示需要等待 + if (!statusState?.status) { + return null; + } + + const user = JSON.parse(localStorage.getItem('user')); + const userRole = user.role; + + // 解析模块路径 + const pathParts = modulePath.split('.'); + if (pathParts.length < 2) { + return false; + } + + // 普通用户权限检查 + if (userRole < 10) { + return await isUserModuleAllowed(modulePath); + } + + // 超级管理员权限检查 - 不受系统配置限制 + if (userRole >= 100) { + return true; + } + + // 管理员权限检查 - 受系统配置限制 + if (userRole >= 10 && userRole < 100) { + // 从/api/user/self获取系统权限配置 + try { + const userRes = await API.get('/api/user/self'); + if (userRes.data.success && userRes.data.data.sidebar_config) { + const sidebarConfigData = userRes.data.data.sidebar_config; + // 管理员权限检查基于系统配置,不受用户偏好影响 + const systemConfig = sidebarConfigData.system || sidebarConfigData; + return checkModulePermissionInConfig(systemConfig, modulePath); + } else { + // 没有配置时,除了系统设置外都允许访问 + return modulePath !== 'admin.setting'; + } + } catch (error) { + console.error('获取侧边栏配置失败:', error); + return false; + } + } + + return false; + } catch (error) { + console.error('API权限检查失败:', error); + return false; + } + }; + + const isUserModuleAllowed = async (modulePath) => { + // 数据看板始终允许访问,不受控制台区域开关影响 + if (modulePath === 'console.detail') { + return true; + } + + // 普通用户的权限基于最终计算的配置 + try { + const userRes = await API.get('/api/user/self'); + if (userRes.data.success && userRes.data.data.sidebar_config) { + const sidebarConfigData = userRes.data.data.sidebar_config; + // 使用最终计算的配置进行权限检查 + const finalConfig = sidebarConfigData.final || sidebarConfigData; + return checkModulePermissionInConfig(finalConfig, modulePath); + } + return false; + } catch (error) { + console.error('获取用户权限配置失败:', error); + return false; + } + }; + + // 检查新的sidebar_config结构中的模块权限 + const checkModulePermissionInConfig = (sidebarConfig, modulePath) => { + const parts = modulePath.split('.'); + if (parts.length !== 2) { + return false; + } + + const [sectionKey, moduleKey] = parts; + const section = sidebarConfig[sectionKey]; + + // 检查区域是否存在且启用 + if (!section || !section.enabled) { + return false; + } + + // 检查模块是否启用 + const moduleValue = section[moduleKey]; + // 处理布尔值和嵌套对象两种情况 + if (typeof moduleValue === 'boolean') { + return moduleValue === true; + } else if (typeof moduleValue === 'object' && moduleValue !== null) { + // 对于嵌套对象,检查其enabled状态 + return moduleValue.enabled === true; + } + return false; + }; + + // 权限检查中 + if (hasPermission === null) { + return ; + } + + // 无权限 + if (!hasPermission) { + return fallback; + } + + // 有权限,渲染子组件 + return children; +}; + +export default ModuleRoute; From 3a98ae3f70c3b131f39909de25f20139441ba6ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=E3=80=82?= Date: Tue, 2 Sep 2025 19:26:30 +0800 Subject: [PATCH 4/4] =?UTF-8?q?=E6=94=B9=E8=BF=9B"=E4=BE=A7=E8=BE=B9?= =?UTF-8?q?=E6=A0=8F"=E6=9D=83=E9=99=90=E6=8E=A7=E5=88=B6-1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit d798db5953906aa5ff76cf6f2b641eb204d279b0. --- web/src/components/auth/ModuleRoute.jsx | 200 ------------------------ 1 file changed, 200 deletions(-) delete mode 100644 web/src/components/auth/ModuleRoute.jsx diff --git a/web/src/components/auth/ModuleRoute.jsx b/web/src/components/auth/ModuleRoute.jsx deleted file mode 100644 index 3f208c7f..00000000 --- a/web/src/components/auth/ModuleRoute.jsx +++ /dev/null @@ -1,200 +0,0 @@ -/* -Copyright (C) 2025 QuantumNous - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as -published by the Free Software Foundation, either version 3 of the -License, or (at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . - -For commercial licensing, please contact support@quantumnous.com -*/ - -import React, { useState, useEffect, useContext } from 'react'; -import { Navigate } from 'react-router-dom'; -import { StatusContext } from '../../context/Status'; -import Loading from '../common/ui/Loading'; -import { API } from '../../helpers'; - -/** - * ModuleRoute - 基于功能模块权限的路由保护组件 - * - * @param {Object} props - * @param {React.ReactNode} props.children - 要保护的子组件 - * @param {string} props.modulePath - 模块权限路径,如 "admin.channel", "console.token" - * @param {React.ReactNode} props.fallback - 无权限时显示的组件,默认跳转到 /forbidden - * @returns {React.ReactNode} - */ -const ModuleRoute = ({ children, modulePath, fallback = }) => { - const [hasPermission, setHasPermission] = useState(null); - const [statusState] = useContext(StatusContext); - - useEffect(() => { - checkModulePermission(); - }, [modulePath, statusState?.status]); // 只在status数据变化时重新检查 - - const checkModulePermission = async () => { - try { - // 检查用户是否已登录 - const user = localStorage.getItem('user'); - if (!user) { - setHasPermission(false); - return; - } - - const userData = JSON.parse(user); - const userRole = userData.role; - - // 超级管理员始终有权限 - if (userRole >= 100) { - setHasPermission(true); - return; - } - - // 检查模块权限 - const permission = await checkModulePermissionAPI(modulePath); - - // 如果返回null,表示status数据还未加载完成,保持loading状态 - if (permission === null) { - setHasPermission(null); - return; - } - - setHasPermission(permission); - } catch (error) { - console.error('检查模块权限失败:', error); - // 出错时采用安全优先策略,拒绝访问 - setHasPermission(false); - } - }; - - const checkModulePermissionAPI = async (modulePath) => { - try { - // 数据看板始终允许访问,不受控制台区域开关影响 - if (modulePath === 'console.detail') { - return true; - } - - // 从StatusContext中获取配置信息 - // 如果status数据还未加载完成,返回null表示需要等待 - if (!statusState?.status) { - return null; - } - - const user = JSON.parse(localStorage.getItem('user')); - const userRole = user.role; - - // 解析模块路径 - const pathParts = modulePath.split('.'); - if (pathParts.length < 2) { - return false; - } - - // 普通用户权限检查 - if (userRole < 10) { - return await isUserModuleAllowed(modulePath); - } - - // 超级管理员权限检查 - 不受系统配置限制 - if (userRole >= 100) { - return true; - } - - // 管理员权限检查 - 受系统配置限制 - if (userRole >= 10 && userRole < 100) { - // 从/api/user/self获取系统权限配置 - try { - const userRes = await API.get('/api/user/self'); - if (userRes.data.success && userRes.data.data.sidebar_config) { - const sidebarConfigData = userRes.data.data.sidebar_config; - // 管理员权限检查基于系统配置,不受用户偏好影响 - const systemConfig = sidebarConfigData.system || sidebarConfigData; - return checkModulePermissionInConfig(systemConfig, modulePath); - } else { - // 没有配置时,除了系统设置外都允许访问 - return modulePath !== 'admin.setting'; - } - } catch (error) { - console.error('获取侧边栏配置失败:', error); - return false; - } - } - - return false; - } catch (error) { - console.error('API权限检查失败:', error); - return false; - } - }; - - const isUserModuleAllowed = async (modulePath) => { - // 数据看板始终允许访问,不受控制台区域开关影响 - if (modulePath === 'console.detail') { - return true; - } - - // 普通用户的权限基于最终计算的配置 - try { - const userRes = await API.get('/api/user/self'); - if (userRes.data.success && userRes.data.data.sidebar_config) { - const sidebarConfigData = userRes.data.data.sidebar_config; - // 使用最终计算的配置进行权限检查 - const finalConfig = sidebarConfigData.final || sidebarConfigData; - return checkModulePermissionInConfig(finalConfig, modulePath); - } - return false; - } catch (error) { - console.error('获取用户权限配置失败:', error); - return false; - } - }; - - // 检查新的sidebar_config结构中的模块权限 - const checkModulePermissionInConfig = (sidebarConfig, modulePath) => { - const parts = modulePath.split('.'); - if (parts.length !== 2) { - return false; - } - - const [sectionKey, moduleKey] = parts; - const section = sidebarConfig[sectionKey]; - - // 检查区域是否存在且启用 - if (!section || !section.enabled) { - return false; - } - - // 检查模块是否启用 - const moduleValue = section[moduleKey]; - // 处理布尔值和嵌套对象两种情况 - if (typeof moduleValue === 'boolean') { - return moduleValue === true; - } else if (typeof moduleValue === 'object' && moduleValue !== null) { - // 对于嵌套对象,检查其enabled状态 - return moduleValue.enabled === true; - } - return false; - }; - - // 权限检查中 - if (hasPermission === null) { - return ; - } - - // 无权限 - if (!hasPermission) { - return fallback; - } - - // 有权限,渲染子组件 - return children; -}; - -export default ModuleRoute;