From 89913c0d26fe99a00b443eb08c6db5db9001b58c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=E3=80=82?= Date: Sun, 31 Aug 2025 21:01:54 +0800 Subject: [PATCH 01/18] =?UTF-8?q?=E6=96=B0=E5=A2=9E"=E9=82=80=E8=AF=B7?= =?UTF-8?q?=E5=8A=9F=E8=83=BD"=E5=BC=80=E5=85=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1.新增"邀请功能"开关 2.i18n本地化 3.修复侧边栏权限同步问题 --- controller/misc.go | 1 + controller/user.go | 79 ++++++++--- model/main.go | 2 + model/option.go | 2 + model/user.go | 25 ++-- setting/operation_setting/general_setting.go | 2 + .../personal/cards/NotificationSettings.jsx | 2 +- web/src/components/topup/index.jsx | 125 ++++++++++++++---- web/src/helpers/utils.jsx | 21 +-- web/src/hooks/common/useUserPermissions.js | 6 +- web/src/i18n/locales/en.json | 4 +- .../Setting/Operation/SettingsGeneral.jsx | 31 ++++- 12 files changed, 230 insertions(+), 70 deletions(-) diff --git a/controller/misc.go b/controller/misc.go index 875142ffbf04..90a10373d95c 100644 --- a/controller/misc.go +++ b/controller/misc.go @@ -63,6 +63,7 @@ func GetStatus(c *gin.Context) { "turnstile_site_key": common.TurnstileSiteKey, "top_up_link": common.TopUpLink, "docs_link": operation_setting.GetGeneralSetting().DocsLink, + "invitation_enabled": operation_setting.GetGeneralSetting().InvitationEnabled, "quota_per_unit": common.QuotaPerUnit, "display_in_currency": common.DisplayInCurrencyEnabled, "enable_batch_update": common.BatchUpdateEnabled, diff --git a/controller/user.go b/controller/user.go index 982329cec0de..03208b19404f 100644 --- a/controller/user.go +++ b/controller/user.go @@ -10,6 +10,7 @@ import ( "one-api/logger" "one-api/model" "one-api/setting" + "one-api/setting/operation_setting" "strconv" "strings" "sync" @@ -375,6 +376,16 @@ type TransferAffQuotaRequest struct { } func TransferAffQuota(c *gin.Context) { + // 检查邀请功能是否启用 + generalSetting := operation_setting.GetGeneralSetting() + if !generalSetting.InvitationEnabled { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": "邀请功能已被管理员禁用", + }) + return + } + id := c.GetInt("id") user, err := model.GetUserById(id, true) if err != nil { @@ -401,6 +412,16 @@ func TransferAffQuota(c *gin.Context) { } func GetAffCode(c *gin.Context) { + // 检查邀请功能是否启用 + generalSetting := operation_setting.GetGeneralSetting() + if !generalSetting.InvitationEnabled { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": "邀请功能已被管理员禁用", + }) + return + } + id := c.GetInt("id") user, err := model.GetUserById(id, true) if err != nil { @@ -444,26 +465,26 @@ func GetSelf(c *gin.Context) { // 构建响应数据,包含用户信息和权限 responseData := map[string]interface{}{ - "id": user.Id, - "username": user.Username, - "display_name": user.DisplayName, - "role": user.Role, - "status": user.Status, - "email": user.Email, - "group": user.Group, - "quota": user.Quota, - "used_quota": user.UsedQuota, - "request_count": user.RequestCount, - "aff_code": user.AffCode, - "aff_count": user.AffCount, - "aff_quota": user.AffQuota, + "id": user.Id, + "username": user.Username, + "display_name": user.DisplayName, + "role": user.Role, + "status": user.Status, + "email": user.Email, + "group": user.Group, + "quota": user.Quota, + "used_quota": user.UsedQuota, + "request_count": user.RequestCount, + "aff_code": user.AffCode, + "aff_count": user.AffCount, + "aff_quota": user.AffQuota, "aff_history_quota": user.AffHistoryQuota, - "inviter_id": user.InviterId, - "linux_do_id": user.LinuxDOId, - "setting": user.Setting, - "stripe_customer": user.StripeCustomer, - "sidebar_modules": userSetting.SidebarModules, // 正确提取sidebar_modules字段 - "permissions": permissions, // 新增权限字段 + "inviter_id": user.InviterId, + "linux_do_id": user.LinuxDOId, + "setting": user.Setting, + "stripe_customer": user.StripeCustomer, + "sidebar_modules": userSetting.SidebarModules, // 正确提取sidebar_modules字段 + "permissions": permissions, // 新增权限字段 } c.JSON(http.StatusOK, gin.H{ @@ -564,6 +585,8 @@ func generateDefaultSidebarConfig(userRole int) string { return string(configBytes) } + + func GetUserModels(c *gin.Context) { id, err := strconv.Atoi(c.Param("id")) if err != nil { @@ -949,6 +972,15 @@ func ManageUser(c *gin.Context) { return } user.Role = common.RoleAdminUser + + // 同步更新用户的sidebar_modules配置 + currentSetting := user.GetSetting() + newSidebarConfig := model.GenerateDefaultSidebarConfigForRole(user.Role) + if newSidebarConfig != "" { + currentSetting.SidebarModules = newSidebarConfig + user.SetSetting(currentSetting) + common.SysLog(fmt.Sprintf("用户 %s 提升为管理员,已同步更新边栏配置", user.Username)) + } case "demote": if user.Role == common.RoleRootUser { c.JSON(http.StatusOK, gin.H{ @@ -965,6 +997,15 @@ func ManageUser(c *gin.Context) { return } user.Role = common.RoleCommonUser + + // 同步更新用户的sidebar_modules配置 + currentSetting := user.GetSetting() + newSidebarConfig := model.GenerateDefaultSidebarConfigForRole(user.Role) + if newSidebarConfig != "" { + currentSetting.SidebarModules = newSidebarConfig + user.SetSetting(currentSetting) + common.SysLog(fmt.Sprintf("用户 %s 降级为普通用户,已同步更新边栏配置", user.Username)) + } } if err := user.Update(false); err != nil { diff --git a/model/main.go b/model/main.go index 1a38d371b847..6900a0451958 100644 --- a/model/main.go +++ b/model/main.go @@ -114,6 +114,8 @@ func CheckSetup() { } } + + func chooseDB(envName string, isLog bool) (*gorm.DB, error) { defer func() { initCol() diff --git a/model/option.go b/model/option.go index ceecff658f5e..9f44ae828d6a 100644 --- a/model/option.go +++ b/model/option.go @@ -159,6 +159,8 @@ func loadOptionsFromDatabase() { } } + + func SyncOptions(frequency int) { for { time.Sleep(time.Duration(frequency) * time.Second) diff --git a/model/user.go b/model/user.go index ea0584c5a05a..d1efb15be798 100644 --- a/model/user.go +++ b/model/user.go @@ -7,6 +7,7 @@ import ( "one-api/common" "one-api/dto" "one-api/logger" + "one-api/setting/operation_setting" "strconv" "strings" @@ -92,7 +93,7 @@ func (user *User) SetSetting(setting dto.UserSetting) { } // 根据用户角色生成默认的边栏配置 -func generateDefaultSidebarConfigForRole(userRole int) string { +func GenerateDefaultSidebarConfigForRole(userRole int) string { defaultConfig := map[string]interface{}{} // 聊天区域 - 所有用户都可以访问 @@ -400,7 +401,7 @@ func (user *User) Insert(inviterId int) error { var createdUser User if err := DB.Where("username = ?", user.Username).First(&createdUser).Error; err == nil { // 生成基于角色的默认边栏配置 - defaultSidebarConfig := generateDefaultSidebarConfigForRole(createdUser.Role) + defaultSidebarConfig := GenerateDefaultSidebarConfigForRole(createdUser.Role) if defaultSidebarConfig != "" { currentSetting := createdUser.GetSetting() currentSetting.SidebarModules = defaultSidebarConfig @@ -414,14 +415,18 @@ func (user *User) Insert(inviterId int) error { RecordLog(user.Id, LogTypeSystem, fmt.Sprintf("新用户注册赠送 %s", logger.LogQuota(common.QuotaForNewUser))) } if inviterId != 0 { - if common.QuotaForInvitee > 0 { - _ = IncreaseUserQuota(user.Id, common.QuotaForInvitee, true) - RecordLog(user.Id, LogTypeSystem, fmt.Sprintf("使用邀请码赠送 %s", logger.LogQuota(common.QuotaForInvitee))) - } - if common.QuotaForInviter > 0 { - //_ = IncreaseUserQuota(inviterId, common.QuotaForInviter) - RecordLog(inviterId, LogTypeSystem, fmt.Sprintf("邀请用户赠送 %s", logger.LogQuota(common.QuotaForInviter))) - _ = inviteUser(inviterId) + // 检查邀请功能是否启用 + generalSetting := operation_setting.GetGeneralSetting() + if generalSetting.InvitationEnabled { + if common.QuotaForInvitee > 0 { + _ = IncreaseUserQuota(user.Id, common.QuotaForInvitee, true) + RecordLog(user.Id, LogTypeSystem, fmt.Sprintf("使用邀请码赠送 %s", logger.LogQuota(common.QuotaForInvitee))) + } + if common.QuotaForInviter > 0 { + //_ = IncreaseUserQuota(inviterId, common.QuotaForInviter) + RecordLog(inviterId, LogTypeSystem, fmt.Sprintf("邀请用户赠送 %s", logger.LogQuota(common.QuotaForInviter))) + _ = inviteUser(inviterId) + } } } return nil diff --git a/setting/operation_setting/general_setting.go b/setting/operation_setting/general_setting.go index ae0c436ecefd..fc9190f4d1cf 100644 --- a/setting/operation_setting/general_setting.go +++ b/setting/operation_setting/general_setting.go @@ -6,6 +6,7 @@ type GeneralSetting struct { DocsLink string `json:"docs_link"` PingIntervalEnabled bool `json:"ping_interval_enabled"` PingIntervalSeconds int `json:"ping_interval_seconds"` + InvitationEnabled bool `json:"invitation_enabled"` } // 默认配置 @@ -13,6 +14,7 @@ var generalSetting = GeneralSetting{ DocsLink: "https://docs.newapi.pro", PingIntervalEnabled: false, PingIntervalSeconds: 60, + InvitationEnabled: true, } func init() { diff --git a/web/src/components/settings/personal/cards/NotificationSettings.jsx b/web/src/components/settings/personal/cards/NotificationSettings.jsx index 0b097eaff232..68ddab51a3b4 100644 --- a/web/src/components/settings/personal/cards/NotificationSettings.jsx +++ b/web/src/components/settings/personal/cards/NotificationSettings.jsx @@ -664,7 +664,7 @@ const NotificationSettings = ({ color: 'var(--semi-color-text-2)', }} > - {t('您可以个性化设置侧边栏的要显示功能')} + {t('您可以个性化设置侧边栏要显示的功能')} {/* 边栏设置功能区域容器 */} diff --git a/web/src/components/topup/index.jsx b/web/src/components/topup/index.jsx index 929a47e39b04..32d7dfae4bf1 100644 --- a/web/src/components/topup/index.jsx +++ b/web/src/components/topup/index.jsx @@ -60,6 +60,8 @@ const TopUp = () => { const [enableStripeTopUp, setEnableStripeTopUp] = useState( statusState?.status?.enable_stripe_topup || false, ); + const [invitationEnabled, setInvitationEnabled] = useState(false); // 初始为false,避免闪烁 + const [invitationConfigLoaded, setInvitationConfigLoaded] = useState(false); // 添加配置加载状态 const [statusLoading, setStatusLoading] = useState(true); const [isSubmitting, setIsSubmitting] = useState(false); @@ -359,6 +361,44 @@ const TopUp = () => { } }; + // 获取邀请功能配置状态 + const getInvitationConfig = async () => { + try { + const res = await API.get('/api/status'); + const { success, data } = res.data; + if (success) { + //console.log('从status接口获取到的邀请功能配置:', data.invitation_enabled); + const enabled = data.invitation_enabled === true; + //console.log('邀请功能状态:', enabled); + setInvitationEnabled(enabled); + setInvitationConfigLoaded(true); + // 只有在邀请功能启用时才获取邀请链接 + if (enabled && !affFetchedRef.current) { + affFetchedRef.current = true; + getAffLink(); + } + } else { + // API调用失败,使用后端默认值(true) + //console.log('status接口调用失败,使用默认值true'); + setInvitationEnabled(true); + setInvitationConfigLoaded(true); + if (!affFetchedRef.current) { + affFetchedRef.current = true; + getAffLink(); + } + } + } catch (error) { + //console.error('获取邀请功能配置失败:', error); + // 出错时使用后端默认值(true) + setInvitationEnabled(true); + setInvitationConfigLoaded(true); + if (!affFetchedRef.current) { + affFetchedRef.current = true; + getAffLink(); + } + } + }; + // 划转邀请额度 const transfer = async () => { if (transferAmount < getQuotaPerUnit()) { @@ -389,18 +429,55 @@ const TopUp = () => { getUserQuota().then(); } setTransferAmount(getQuotaPerUnit()); - }, []); + getInvitationConfig().then(); - useEffect(() => { - if (affFetchedRef.current) return; - affFetchedRef.current = true; - getAffLink().then(); - }, []); + let payMethods = localStorage.getItem('pay_methods'); + try { + payMethods = JSON.parse(payMethods); + if (payMethods && payMethods.length > 0) { + // 检查name和type是否为空 + payMethods = payMethods.filter((method) => { + return method.name && method.type; + }); + // 如果没有color,则设置默认颜色 + payMethods = payMethods.map((method) => { + if (!method.color) { + if (method.type === 'alipay') { + method.color = 'rgba(var(--semi-blue-5), 1)'; + } else if (method.type === 'wxpay') { + method.color = 'rgba(var(--semi-green-5), 1)'; + } else if (method.type === 'stripe') { + method.color = 'rgba(var(--semi-purple-5), 1)'; + } else { + method.color = 'rgba(var(--semi-primary-5), 1)'; + } + } + return method; + }); + } else { + payMethods = []; + } - // 在 statusState 可用时获取充值信息 - useEffect(() => { - getTopupInfo().then(); - }, []); + // 如果启用了 Stripe 支付,添加到支付方法列表 + if (statusState?.status?.enable_stripe_topup) { + const hasStripe = payMethods.some((method) => method.type === 'stripe'); + if (!hasStripe) { + payMethods.push({ + name: 'Stripe', + type: 'stripe', + color: 'rgba(var(--semi-purple-5), 1)', + }); + } + } + + setPayMethods(payMethods); + } catch (e) { + console.log(e); + showError(t('支付方式配置错误, 请联系管理员')); + } + }, [statusState?.status?.enable_stripe_topup]); + + // 移除独立的getAffLink调用,现在由getInvitationConfig统一处理 useEffect(() => { if (statusState?.status) { @@ -537,9 +614,9 @@ const TopUp = () => { {/* 用户信息头部 */}
-
+
{/* 左侧充值区域 */} -
+
{ />
- {/* 右侧信息区域 */} -
- -
+ {/* 右侧信息区域 - 仅在配置加载完成且邀请功能启用时显示 */} + {invitationConfigLoaded && invitationEnabled && ( +
+ +
+ )}
diff --git a/web/src/helpers/utils.jsx b/web/src/helpers/utils.jsx index e446ea69d370..e7c4a126d1e2 100644 --- a/web/src/helpers/utils.jsx +++ b/web/src/helpers/utils.jsx @@ -294,16 +294,17 @@ export function setPromptShown(id) { export function compareObjects(oldObject, newObject) { const changedProperties = []; - // 比较两个对象的属性 - for (const key in oldObject) { - if (oldObject.hasOwnProperty(key) && newObject.hasOwnProperty(key)) { - if (oldObject[key] !== newObject[key]) { - changedProperties.push({ - key: key, - oldValue: oldObject[key], - newValue: newObject[key], - }); - } + // 获取两个对象的所有键 + const allKeys = new Set([...Object.keys(oldObject), ...Object.keys(newObject)]); + + // 比较所有键的值 + for (const key of allKeys) { + if (oldObject[key] !== newObject[key]) { + changedProperties.push({ + key: key, + oldValue: oldObject[key], + newValue: newObject[key], + }); } } diff --git a/web/src/hooks/common/useUserPermissions.js b/web/src/hooks/common/useUserPermissions.js index 8d57f972ef49..1428fcc2da5c 100644 --- a/web/src/hooks/common/useUserPermissions.js +++ b/web/src/hooks/common/useUserPermissions.js @@ -37,14 +37,14 @@ export const useUserPermissions = () => { if (res.data.success) { const userPermissions = res.data.data.permissions; setPermissions(userPermissions); - console.log('用户权限加载成功:', userPermissions); + //console.log('用户权限加载成功:', userPermissions); } else { setError(res.data.message || '获取权限失败'); - console.error('获取权限失败:', res.data.message); + //console.error('获取权限失败:', res.data.message); } } catch (error) { setError('网络错误,请重试'); - console.error('加载用户权限异常:', error); + //console.error('加载用户权限异常:', error); } finally { setLoading(false); } diff --git a/web/src/i18n/locales/en.json b/web/src/i18n/locales/en.json index c86fb0e7f608..8d520b72bb3d 100644 --- a/web/src/i18n/locales/en.json +++ b/web/src/i18n/locales/en.json @@ -2011,7 +2011,7 @@ "为一个 JSON 文本,键为模型名称,值为倍率,例如:{\"gpt-4o-realtime\": 2}": "A JSON text with model name as key and ratio as value, e.g.: {\"gpt-4o-realtime\": 2}", "顶栏管理": "Header Management", "控制顶栏模块显示状态,全局生效": "Control header module display status, global effect", - "用户主页,展示系统信息": "User homepage, displaying system information", + "系统主页": "System homepage", "用户控制面板,管理账户": "User control panel for account management", "模型广场": "Model Marketplace", "模型定价,需要登录访问": "Model pricing, requires login to access", @@ -2062,7 +2062,7 @@ "系统设置": "System Settings", "系统参数配置": "System parameter configuration", "边栏设置": "Sidebar Settings", - "您可以个性化设置侧边栏的要显示功能": "You can customize the sidebar functions to display", + "您可以个性化设置侧边栏要显示的功能": "You can customize the sidebar functions to display", "保存边栏设置": "Save Sidebar Settings", "侧边栏设置保存成功": "Sidebar settings saved successfully", "需要登录访问": "Require Login", diff --git a/web/src/pages/Setting/Operation/SettingsGeneral.jsx b/web/src/pages/Setting/Operation/SettingsGeneral.jsx index 5af750ec39f0..29e9b7348845 100644 --- a/web/src/pages/Setting/Operation/SettingsGeneral.jsx +++ b/web/src/pages/Setting/Operation/SettingsGeneral.jsx @@ -43,6 +43,7 @@ export default function GeneralSettings(props) { DefaultCollapseSidebar: false, DemoSiteEnabled: false, SelfUseModeEnabled: false, + 'general_setting.invitation_enabled': true, }); const refForm = useRef(); const [inputsRow, setInputsRow] = useState(inputs); @@ -89,12 +90,27 @@ export default function GeneralSettings(props) { } useEffect(() => { - const currentInputs = {}; + // 从初始 inputs 开始,确保保留所有默认值 + const currentInputs = { ...inputs }; + + // 用 props.options 中的值覆盖对应的键 for (let key in props.options) { if (Object.keys(inputs).includes(key)) { - currentInputs[key] = props.options[key]; + let value = props.options[key]; + // 对于布尔类型的字段,需要进行字符串到布尔值的转换 + if (typeof inputs[key] === 'boolean') { + if (typeof value === 'string') { + value = value === 'true'; + } else if (typeof value === 'boolean') { + value = value; + } else { + value = inputs[key]; // 保持默认值 + } + } + currentInputs[key] = value; } } + setInputs(currentInputs); setInputsRow(structuredClone(currentInputs)); refForm.current.setValues(currentInputs); @@ -218,6 +234,17 @@ export default function GeneralSettings(props) { onChange={handleFieldChange('SelfUseModeEnabled')} /> + + + -
+ <> +
+ + {canShowGroupManagement() && ( + + )} +
+ + {canShowGroupManagement() && ( + setShowGroupManagement(false)} + onGroupUpdated={onRefreshUsers} + /> + )} + ); }; diff --git a/web/src/components/table/users/index.jsx b/web/src/components/table/users/index.jsx index 59e12a4e5fc0..0f8fc08a9752 100644 --- a/web/src/components/table/users/index.jsx +++ b/web/src/components/table/users/index.jsx @@ -88,7 +88,11 @@ const UsersPage = () => { } actionsArea={
- + . For commercial licensing, please contact support@quantumnous.com */ -import React, { useState, useRef } from 'react'; +import React, { useState, useRef, useEffect } from 'react'; import { API, showError, showSuccess } from '../../../../helpers'; import { useIsMobile } from '../../../../hooks/common/useIsMobile'; import { @@ -42,15 +42,34 @@ const AddUserModal = (props) => { const { t } = useTranslation(); const formApiRef = useRef(null); const [loading, setLoading] = useState(false); + const [groupOptions, setGroupOptions] = useState([]); const isMobile = useIsMobile(); const getInitValues = () => ({ username: '', display_name: '', password: '', + group: 'default', // 默认分组 remark: '', }); + // 获取分组列表 + const fetchGroups = async () => { + try { + const res = await API.get('/api/group/'); + if (res.data.success) { + setGroupOptions( + res.data.data.map((group) => ({ + label: group, + value: group, + })) + ); + } + } catch (error) { + showError(t('获取分组列表失败')); + } + }; + const submit = async (values) => { setLoading(true); const res = await API.post(`/api/user/`, values); @@ -70,6 +89,13 @@ const AddUserModal = (props) => { props.handleClose(); }; + // 组件加载时获取分组列表 + useEffect(() => { + if (props.visible) { + fetchGroups(); + } + }, [props.visible]); + return ( <> { showClear /> + + + { + const { t } = useTranslation(); + const formApiRef = useRef(null); + const [loading, setLoading] = useState(false); + + const isEdit = editingGroup && editingGroup.id; + const isSystemGroup = editingGroup && ( + editingGroup.name === 'default' || + editingGroup.name === 'vip' || + editingGroup.name === 'svip' + ); + + // 获取分组显示名称 + const getGroupDisplayName = (groupName) => { + if (groupName === 'default') { + return t('默认'); + } + return groupName; + }; + + const getInitValues = () => ({ + name: editingGroup?.name || '', + description: editingGroup?.description || '', + ratio: editingGroup?.ratio || 1.0, + }); + + const submit = async (values) => { + setLoading(true); + try { + const data = { + ...values, + ratio: parseFloat(values.ratio) || 1.0, + }; + + if (isEdit) { + data.id = editingGroup.id; + } + + const url = isEdit ? '/api/user_group' : '/api/user_group'; + const method = isEdit ? 'PUT' : 'POST'; + + const res = await API[method.toLowerCase()](url, data); + const { success, message } = res.data; + + if (success) { + showSuccess(isEdit ? t('分组更新成功!') : t('分组创建成功!')); + onSuccess(); + } else { + showError(message); + } + } catch (error) { + showError(isEdit ? t('分组更新失败') : t('分组创建失败')); + } + setLoading(false); + }; + + const handleCancel = () => { + onClose(); + }; + + // 重置表单当编辑分组改变时 + useEffect(() => { + if (visible && formApiRef.current) { + formApiRef.current.setValues(getInitValues()); + } + }, [visible, editingGroup]); + + return ( + + + + + + {isEdit ? t('编辑分组') : t('新建分组')} + + {isEdit && ( + + {getGroupDisplayName(editingGroup.name)} + + )} + + } + visible={visible} + onCancel={handleCancel} + width={500} + footer={ +
+ + + + +
+ } + closeIcon={null} + > + +
(formApiRef.current = api)} + onSubmit={submit} + onSubmitFail={(errs) => { + const first = Object.values(errs)[0]; + if (first) showError(Array.isArray(first) ? first[0] : first); + formApiRef.current?.scrollToError(); + }} + > +
+ + + + + + + {isSystemGroup && ( +
+ + {t('提示:')} + {t('这是系统默认分组,只能修改描述和倍率,不能修改名称或删除。')} + +
+ )} +
+
+
+
+ ); +}; + +export default EditUserGroupModal; diff --git a/web/src/components/table/users/modals/UserGroupManagement.jsx b/web/src/components/table/users/modals/UserGroupManagement.jsx new file mode 100644 index 000000000000..429fbf996ed2 --- /dev/null +++ b/web/src/components/table/users/modals/UserGroupManagement.jsx @@ -0,0 +1,298 @@ +/* +Copyright 2024 Quantumnous Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +For commercial licensing, please contact support@quantumnous.com +*/ + +import React, { useState, useEffect } from 'react'; +import { + SideSheet, + Button, + Space, + Card, + Avatar, + Typography, + Spin, + Empty, + Popconfirm, + Tag, +} from '@douyinfe/semi-ui'; +import { + IconUserGroup, + IconPlus, +} from '@douyinfe/semi-icons'; +import { + IllustrationNoResult, + IllustrationNoResultDark, +} from '@douyinfe/semi-illustrations'; +import { useTranslation } from 'react-i18next'; +import { useIsMobile } from '../../../../hooks/common/useIsMobile'; +import { API, showError, showSuccess } from '../../../../helpers'; +import CardTable from '../../../common/ui/CardTable'; +import EditUserGroupModal from './EditUserGroupModal'; + +const UserGroupManagement = ({ visible, onClose, onGroupUpdated }) => { + const { t } = useTranslation(); + const isMobile = useIsMobile(); + const [loading, setLoading] = useState(false); + const [groups, setGroups] = useState([]); + const [showEdit, setShowEdit] = useState(false); + const [editingGroup, setEditingGroup] = useState({ id: undefined }); + + // 加载分组列表 + const loadGroups = async () => { + setLoading(true); + try { + const res = await API.get('/api/user_group'); + if (res.data.success) { + setGroups(res.data.data || []); + } else { + showError(res.data.message || t('获取分组列表失败')); + } + } catch (error) { + showError(t('获取分组列表失败')); + } + setLoading(false); + }; + + // 删除分组 + const deleteGroup = async (id) => { + try { + const res = await API.delete(`/api/user_group/${id}`); + if (res.data.success) { + showSuccess(t('删除成功')); + loadGroups(); + } else { + showError(res.data.message || t('删除失败')); + } + } catch (error) { + showError(t('删除失败')); + } + }; + + // 编辑分组 + const handleEdit = (group = {}) => { + setEditingGroup(group); + setShowEdit(true); + }; + + // 关闭编辑 + const closeEdit = () => { + setShowEdit(false); + setTimeout(() => { + setEditingGroup({ id: undefined }); + }, 300); + }; + + // 编辑成功回调 + const handleEditSuccess = () => { + closeEdit(); + loadGroups(); + // 通知父组件刷新用户数据 + if (onGroupUpdated) { + onGroupUpdated(); + } + }; + + // 获取分组显示名称 + const getGroupDisplayName = (groupName) => { + if (groupName === 'default') { + return t('默认'); + } + return groupName; + }; + + // 获取分组描述的翻译 + const getGroupDescription = (groupName, originalDescription) => { + // 对于系统默认分组,使用翻译 + if (groupName === 'default' && originalDescription === '默认分组') { + return t('默认分组'); + } + if (groupName === 'vip' && originalDescription === 'VIP分组') { + return t('VIP分组'); + } + if (groupName === 'svip' && originalDescription === 'SVIP分组') { + return t('SVIP分组'); + } + // 对于用户自定义分组,使用原始描述 + return originalDescription; + }; + + // 表格列定义 + const columns = [ + { + title: 'ID', + dataIndex: 'id', + width: 80, + }, + { + title: t('分组名称'), + dataIndex: 'name', + render: (text, record) => ( +
+ + {getGroupDisplayName(text)} + + {(record.name === 'default' || record.name === 'vip' || record.name === 'svip') && ( + + {t('系统默认')} + + )} +
+ ), + }, + { + title: t('分组描述'), + dataIndex: 'description', + render: (text, record) => { + const translatedDescription = getGroupDescription(record.name, text); + return translatedDescription || {t('无描述')}; + }, + }, + { + title: t('分组倍率'), + dataIndex: 'ratio', + width: 100, + render: (text) => ( + + {text} + + ), + }, + { + title: t('创建时间'), + dataIndex: 'created_time', + width: 150, + render: (text) => new Date(text * 1000).toLocaleString(), + }, + { + title: '', + key: 'action', + fixed: 'right', + width: 140, + render: (_, record) => ( + + + {record.name !== 'default' && record.name !== 'vip' && record.name !== 'svip' && ( + deleteGroup(record.id)} + > + + + )} + + ), + }, + ]; + + useEffect(() => { + if (visible) { + loadGroups(); + } + }, [visible]); + + return ( + <> + + + + + {t('用户分组管理')} + + } + visible={visible} + onCancel={onClose} + width={isMobile ? '100%' : 1000} + bodyStyle={{ padding: '0' }} + closeIcon={null} + > + +
+ +
+ + + +
+ {t('分组列表')} +
+ {t('管理用户分组,设置分组倍率')} +
+
+
+
+ +
+ {groups.length > 0 ? ( + + ) : ( + + } + darkModeImage={ + + } + description={t('暂无用户分组')} + style={{ padding: 30 }} + /> + )} +
+
+
+
+ + {/* 编辑组件 */} + + + ); +}; + +export default UserGroupManagement; diff --git a/web/src/helpers/api.js b/web/src/helpers/api.js index b7092fe775e2..84ab6c4e02ac 100644 --- a/web/src/helpers/api.js +++ b/web/src/helpers/api.js @@ -25,6 +25,7 @@ import { } from './utils'; import axios from 'axios'; import { MESSAGE_ROLES } from '../constants/playground.constants'; +import i18next from 'i18next'; export let API = axios.create({ baseURL: import.meta.env.VITE_REACT_APP_SERVER_URL @@ -185,15 +186,34 @@ export const processModelsData = (data, currentModel) => { return { modelOptions, selectedModel }; }; +// 获取分组描述的翻译 +const getGroupDescription = (groupName, originalDescription) => { + // 对于系统默认分组,使用翻译 + if (groupName === 'default' && originalDescription === '默认分组') { + return i18next.t('默认分组'); + } + if (groupName === 'vip' && originalDescription === 'VIP分组') { + return i18next.t('VIP分组'); + } + if (groupName === 'svip' && originalDescription === 'SVIP分组') { + return i18next.t('SVIP分组'); + } + // 对于用户自定义分组,使用原始描述 + return originalDescription; +}; + // 处理分组数据 export const processGroupsData = (data, userGroup) => { - let groupOptions = Object.entries(data).map(([group, info]) => ({ - label: - info.desc.length > 20 ? info.desc.substring(0, 20) + '...' : info.desc, - value: group, - ratio: info.ratio, - fullLabel: info.desc, - })); + let groupOptions = Object.entries(data).map(([group, info]) => { + const translatedDesc = getGroupDescription(group, info.desc); + return { + label: + translatedDesc.length > 20 ? translatedDesc.substring(0, 20) + '...' : translatedDesc, + value: group, + ratio: info.ratio, + fullLabel: translatedDesc, + }; + }); if (groupOptions.length === 0) { groupOptions = [ diff --git a/web/src/helpers/render.jsx b/web/src/helpers/render.jsx index 65332701bbb6..8de251036853 100644 --- a/web/src/helpers/render.jsx +++ b/web/src/helpers/render.jsx @@ -629,6 +629,14 @@ export function renderGroup(group) { premium: 'red', }; + // 获取分组显示名称 + const getGroupDisplayName = (groupName) => { + if (groupName === 'default') { + return i18next.t('默认'); + } + return groupName; + }; + const groups = group.split(',').sort(); return ( @@ -650,7 +658,7 @@ export function renderGroup(group) { } }} > - {group} + {getGroupDisplayName(group)} ))} diff --git a/web/src/i18n/locales/en.json b/web/src/i18n/locales/en.json index 8d520b72bb3d..1bc94ecd6585 100644 --- a/web/src/i18n/locales/en.json +++ b/web/src/i18n/locales/en.json @@ -2091,8 +2091,52 @@ "模型社区需要大家的共同维护,如发现数据有误或想贡献新的模型数据,请访问:": "The model community needs everyone's contribution. If you find incorrect data or want to contribute new models, please visit:", "是": "Yes", "否": "No", - "原价": "Original price", - "优惠": "Discount", - "折": "% off", - "节省": "Save" + "邀请功能": "Invitation Feature", + "关闭后:不在启用邀请奖励功能": "When disabled: invitation reward feature will not be enabled", + "分组管理": "Group Management", + "用户分组管理": "User Group Management", + "分组列表": "Group List", + "管理用户分组,设置分组倍率": "Manage user groups and set group ratios", + "新建分组": "Create Group", + "编辑分组": "Edit Group", + "分组名称": "Group Name", + "请输入分组名称": "Please enter group name", + "分组名称不能为空": "Group name cannot be empty", + "分组名称不能超过64个字符": "Group name cannot exceed 64 characters", + "分组名称只能包含字母、数字、下划线和连字符": "Group name can only contain letters, numbers, underscores and hyphens", + "系统默认分组,名称不可修改": "System default group, name cannot be modified", + "分组描述": "Group Description", + "请输入分组描述(可选)": "Please enter group description (optional)", + "分组的详细描述,用于说明分组用途": "Detailed description of the group, used to explain the group purpose", + "分组倍率": "Group Ratio", + "请输入分组倍率": "Please enter group ratio", + "分组倍率不能为空": "Group ratio cannot be empty", + "分组倍率不能小于0": "Group ratio cannot be less than 0", + "分组的计费倍率,影响该分组用户的费用计算": "Billing ratio of the group, affects cost calculation for users in this group", + "倍": "x", + "系统默认": "System Default", + "无描述": "No Description", + "创建时间": "Created Time", + "确定删除此分组?": "Are you sure to delete this group?", + "删除后无法恢复,请确认该分组未被用户使用": "Cannot be recovered after deletion, please confirm that this group is not being used by users", + "删除成功": "Deleted successfully", + "删除失败": "Delete failed", + "分组名称已存在": "Group name already exists", + "分组倍率不能小于0": "Group ratio cannot be less than 0", + "缺少分组 ID": "Missing group ID", + "该分组正在被用户使用,无法删除": "This group is being used by users and cannot be deleted", + "不能删除系统默认分组": "Cannot delete system default groups", + "分组创建成功!": "Group created successfully!", + "分组更新成功!": "Group updated successfully!", + "分组创建失败": "Group creation failed", + "分组更新失败": "Group update failed", + "获取分组列表失败": "Failed to get group list", + "暂无用户分组": "No user groups", + "提示:": "Note: ", + "这是系统默认分组,只能修改描述和倍率,不能修改名称或删除。": "This is a system default group. You can only modify the description and ratio, but cannot modify the name or delete it.", + "默认": "default", + "控制管理员是否可以访问分组管理功能": "Control whether administrators can access group management features", + "默认分组": "Default Group", + "VIP分组": "VIP Group", + "SVIP分组": "SVIP Group" } diff --git a/web/src/pages/Home/index.jsx b/web/src/pages/Home/index.jsx index 19681639ab6b..ffe2cf1abaad 100644 --- a/web/src/pages/Home/index.jsx +++ b/web/src/pages/Home/index.jsx @@ -158,7 +158,7 @@ const Home = () => { {homePageContentLoaded && homePageContent === '' ? (
{/* Banner 部分 */} -
+
{/* 背景模糊晕染球 */}
diff --git a/web/src/pages/Setting/Operation/SettingsSidebarModulesAdmin.jsx b/web/src/pages/Setting/Operation/SettingsSidebarModulesAdmin.jsx index a46893b81d4b..8472181bcb93 100644 --- a/web/src/pages/Setting/Operation/SettingsSidebarModulesAdmin.jsx +++ b/web/src/pages/Setting/Operation/SettingsSidebarModulesAdmin.jsx @@ -63,9 +63,12 @@ export default function SettingsSidebarModulesAdmin(props) { channel: true, models: true, redemption: true, - user: true, - setting: true, - }, + user: { + enabled: true, + groupManagement: false // 默认关闭分组管理 + }, + setting: true + } }); // 处理区域级别开关变更 @@ -89,13 +92,28 @@ export default function SettingsSidebarModulesAdmin(props) { ...sidebarModulesAdmin, [sectionKey]: { ...sidebarModulesAdmin[sectionKey], - [moduleKey]: checked, - }, + [moduleKey]: checked + } }; setSidebarModulesAdmin(newModules); }; } + // 处理用户管理分组管理子开关变更 + function handleUserGroupManagementChange(checked) { + const newModules = { + ...sidebarModulesAdmin, + admin: { + ...sidebarModulesAdmin.admin, + user: { + ...sidebarModulesAdmin.admin.user, + groupManagement: checked + } + } + }; + setSidebarModulesAdmin(newModules); + } + // 重置为默认配置 function resetSidebarModules() { const defaultModules = { @@ -122,9 +140,12 @@ export default function SettingsSidebarModulesAdmin(props) { channel: true, models: true, redemption: true, - user: true, - setting: true, - }, + user: { + enabled: true, + groupManagement: false // 默认关闭分组管理 + }, + setting: true + } }; setSidebarModulesAdmin(defaultModules); showSuccess(t('已重置为默认配置')); @@ -189,9 +210,12 @@ export default function SettingsSidebarModulesAdmin(props) { channel: true, models: true, redemption: true, - user: true, - setting: true, - }, + user: { + enabled: true, + groupManagement: false // 默认关闭分组管理 + }, + setting: true + } }; setSidebarModulesAdmin(defaultModules); } @@ -366,14 +390,86 @@ export default function SettingsSidebarModulesAdmin(props) {
{ + const newModules = { + ...sidebarModulesAdmin, + [section.key]: { + ...sidebarModulesAdmin[section.key], + user: { + ...sidebarModulesAdmin[section.key].user, + enabled: checked + } + } + }; + setSidebarModulesAdmin(newModules); + } + : handleModuleChange(section.key, module.key) } - onChange={handleModuleChange(section.key, module.key)} - size='default' + size="default" disabled={!sidebarModulesAdmin[section.key]?.enabled} />
+ + {/* 为用户管理添加分组管理子开关 */} + {module.key === 'user' && ( + module.key === 'user' + ? sidebarModulesAdmin[section.key]?.user?.enabled + : sidebarModulesAdmin[section.key]?.[module.key] + ) && ( +
+
+
+
+ {t('分组管理')} +
+ + {t('控制管理员是否可以访问分组管理功能')} + +
+
+ +
+
+
+ )} ))} diff --git a/web/src/pages/Setting/Personal/SettingsSidebarModulesUser.jsx b/web/src/pages/Setting/Personal/SettingsSidebarModulesUser.jsx index 939e76cfc920..ff63b73a5fc7 100644 --- a/web/src/pages/Setting/Personal/SettingsSidebarModulesUser.jsx +++ b/web/src/pages/Setting/Personal/SettingsSidebarModulesUser.jsx @@ -141,8 +141,8 @@ export default function SettingsSidebarModulesUser() { ...sidebarModulesUser, [sectionKey]: { ...sidebarModulesUser[sectionKey], - [moduleKey]: checked, - }, + [moduleKey]: checked + } }; setSidebarModulesUser(newModules); console.log( From 001f7320055ded23a4e8a34e2e47ce839d19fe8c 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 03/18] =?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 68ddab51a3b4..e54a64ab63f0 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 5dce44f9ec2e..e964855e382f 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 3410ec14aa952f77c80a6db16260ab4731925246 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 04/18] =?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 e964855e382f..13d76fd86281 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 228bd86be0e224ddc52a8a590a3a174c65f5ef05 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 05/18] =?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 000000000000..3f208c7faf29 --- /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 7512e4a48d2499bc8355b1abb0a537777b54434b 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 06/18] =?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 3f208c7faf29..000000000000 --- 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; From d451c0d01246d1a347e5ca36ac1af35af56a4b48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=E3=80=82?= Date: Tue, 2 Sep 2025 19:55:45 +0800 Subject: [PATCH 07/18] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E9=A1=B6=E6=A0=8F?= =?UTF-8?q?=E8=BE=B9=E6=A0=8F=E6=9D=83=E9=99=90=E5=8F=8A=E5=85=B6=E4=BB=96?= =?UTF-8?q?=E4=B8=80=E4=BA=9B=E5=B0=8F=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- controller/misc.go | 275 +++++++- controller/user.go | 608 +++++++++++++++++- middleware/auth.go | 161 +++++ model/user.go | 14 +- model/user_group.go | 4 +- router/api-router.go | 32 +- web/index.html | 23 + web/src/App.jsx | 97 ++- web/src/components/auth/ModuleRoute.jsx | 181 ++++++ web/src/components/layout/PageLayout.jsx | 2 + web/src/components/settings/OtherSetting.jsx | 5 + .../personal/cards/NotificationSettings.jsx | 80 ++- .../components/table/users/UsersActions.jsx | 38 +- .../table/users/modals/EditUserGroupModal.jsx | 59 +- .../users/modals/UserGroupManagement.jsx | 75 ++- web/src/helpers/data.js | 7 + web/src/hooks/common/useHeaderBar.js | 2 +- web/src/hooks/common/useSidebar.js | 201 ++---- web/src/pages/Home/index.jsx | 12 +- .../Operation/SettingsHeaderNavModules.jsx | 4 +- .../Operation/SettingsSidebarModulesAdmin.jsx | 21 +- .../Personal/SettingsSidebarModulesUser.jsx | 390 ++++++----- 22 files changed, 1855 insertions(+), 436 deletions(-) create mode 100644 web/src/components/auth/ModuleRoute.jsx diff --git a/controller/misc.go b/controller/misc.go index 90a10373d95c..9e0892c64fbe 100644 --- a/controller/misc.go +++ b/controller/misc.go @@ -42,6 +42,12 @@ func GetStatus(c *gin.Context) { common.OptionMapRWMutex.RLock() defer common.OptionMapRWMutex.RUnlock() + // 获取用户角色信息(如果已登录) + var userRole int = -1 + if role := c.GetInt("role"); role > 0 { + userRole = role + } + data := gin.H{ "version": common.Version, "start_time": common.StartTime, @@ -88,9 +94,8 @@ func GetStatus(c *gin.Context) { "announcements_enabled": cs.AnnouncementsEnabled, "faq_enabled": cs.FAQEnabled, - // 模块管理配置 - "HeaderNavModules": common.OptionMap["HeaderNavModules"], - "SidebarModulesAdmin": common.OptionMap["SidebarModulesAdmin"], + // 模块管理配置 - 根据用户权限过滤 + "header_nav_modules": filterHeaderNavModulesForUser(common.OptionMap["HeaderNavModules"], userRole), "oidc_enabled": system_setting.GetOIDCSettings().Enabled, "oidc_client_id": system_setting.GetOIDCSettings().ClientId, @@ -304,3 +309,267 @@ func ResetPassword(c *gin.Context) { }) return } + +// filterHeaderNavModulesForUser 根据用户权限过滤顶栏模块配置 +func filterHeaderNavModulesForUser(headerNavModulesRaw interface{}, userRole int) interface{} { + // 如果配置为空,返回原配置 + if headerNavModulesRaw == nil { + return headerNavModulesRaw + } + + // 如果用户未登录,返回原配置(顶栏模块对所有用户都是公开的) + if userRole == -1 { + return headerNavModulesRaw + } + + headerNavModulesStr, ok := headerNavModulesRaw.(string) + if !ok || headerNavModulesStr == "" { + return headerNavModulesRaw + } + + // 解析配置 + var config map[string]interface{} + if err := json.Unmarshal([]byte(headerNavModulesStr), &config); err != nil { + // 解析失败时返回空配置,采用安全优先策略 + return "{}" + } + + // 对于所有用户,移除被禁用的模块 + filteredConfig := make(map[string]interface{}) + for key, value := range config { + // 首先检查模块是否启用 + if isHeaderNavModuleEnabled(key, value) { + // 超级管理员可以看到所有启用的模块 + if userRole >= common.RoleRootUser { + filteredConfig[key] = value + } else { + // 对于管理员和普通用户,进行额外的权限检查 + if hasHeaderNavModulePermission(key, value, userRole) { + filteredConfig[key] = value + } + } + } + // 被禁用的模块(isHeaderNavModuleEnabled返回false)不会被添加到filteredConfig中 + } + + // 转换回JSON字符串 + filteredBytes, err := json.Marshal(filteredConfig) + if err != nil { + return "{}" + } + + return string(filteredBytes) +} + +// FilterSidebarModulesAdminForUser 根据用户权限过滤侧边栏管理配置 +func FilterSidebarModulesAdminForUser(sidebarModulesRaw interface{}, userRole int) interface{} { + // 如果用户未登录,返回空配置以保护敏感信息 + if userRole == -1 { + return "{}" + } + + if sidebarModulesRaw == nil { + return sidebarModulesRaw + } + + sidebarModulesStr, ok := sidebarModulesRaw.(string) + if !ok || sidebarModulesStr == "" { + return sidebarModulesRaw + } + + // 解析配置 + var config map[string]interface{} + if err := json.Unmarshal([]byte(sidebarModulesStr), &config); err != nil { + // 解析失败时返回空配置,采用安全优先策略 + return "{}" + } + + + + // 对于所有用户,移除被禁用的模块 + filteredConfig := make(map[string]interface{}) + for sectionKey, sectionValue := range config { + if sectionObj, ok := sectionValue.(map[string]interface{}); ok { + // 检查区域是否启用 + sectionEnabledByConfig := true + if enabled, hasEnabled := sectionObj["enabled"]; hasEnabled { + if enabledBool, ok := enabled.(bool); ok && !enabledBool { + sectionEnabledByConfig = false + } + } + + // 如果区域被配置为禁用,跳过整个区域(但console区域需要特殊处理,因为数据看板始终可访问) + if !sectionEnabledByConfig && sectionKey != "console" { + continue + } + + filteredSection := make(map[string]interface{}) + hasValidModules := false // 标记区域是否有有效的模块 + + // 复制区域配置 + for moduleKey, moduleValue := range sectionObj { + // 检查用户是否有权限访问此模块 + modulePath := sectionKey + "." + moduleKey + + // 数据看板始终允许访问,强制设置为启用 + if modulePath == "console.detail" { + filteredSection[moduleKey] = true + hasValidModules = true + } else if moduleKey == "enabled" { + // 只有当区域启用时才复制enabled字段 + if sectionEnabledByConfig { + filteredSection[moduleKey] = moduleValue + } + } else if sectionEnabledByConfig && hasModulePermissionForUser(userRole, modulePath, moduleValue) { + // 处理嵌套权限(如admin.user.groupManagement) + if moduleObj, ok := moduleValue.(map[string]interface{}); ok { + filteredModule := make(map[string]interface{}) + + // 首先检查模块本身是否启用 + if enabled, hasEnabled := moduleObj["enabled"]; hasEnabled { + if enabledBool, ok := enabled.(bool); ok && !enabledBool { + continue // 跳过被禁用的模块 + } + filteredModule["enabled"] = enabled + } + + // 过滤子模块 + for subKey, subValue := range moduleObj { + if subKey == "enabled" { + continue // enabled字段已经处理过了 + } + + subModulePath := modulePath + "." + subKey + + // 检查子模块是否启用 + if subValueBool, ok := subValue.(bool); ok && !subValueBool { + continue // 跳过被禁用的子模块 + } + + if hasModulePermissionForUser(userRole, subModulePath, subValue) { + filteredModule[subKey] = subValue + } + } + + // 只有当过滤后的模块不为空时才添加 + if len(filteredModule) > 0 { + filteredSection[moduleKey] = filteredModule + hasValidModules = true + } + } else { + // 检查简单模块值是否启用 + if moduleValueBool, ok := moduleValue.(bool); ok && !moduleValueBool { + continue // 跳过被禁用的简单模块 + } + filteredSection[moduleKey] = moduleValue + hasValidModules = true + } + } + } + + // 只有当区域有有效模块时才添加到结果中 + if hasValidModules && len(filteredSection) > 0 { + filteredConfig[sectionKey] = filteredSection + } + } + } + + // 转换回JSON字符串 + filteredBytes, err := json.Marshal(filteredConfig) + if err != nil { + return "{}" + } + + return string(filteredBytes) +} + +// isHeaderNavModuleEnabled 检查顶栏模块是否启用 +func isHeaderNavModuleEnabled(moduleKey string, moduleValue interface{}) bool { + switch v := moduleValue.(type) { + case bool: + return v + case map[string]interface{}: + if enabled, hasEnabled := v["enabled"]; hasEnabled { + if enabledBool, ok := enabled.(bool); ok { + return enabledBool + } + } + return true // 如果没有enabled字段,默认启用 + default: + return true + } +} + +// hasHeaderNavModulePermission 检查用户是否有权限访问顶栏模块 +func hasHeaderNavModulePermission(moduleKey string, moduleValue interface{}, userRole int) bool { + // 普通用户只能访问基础模块 + if userRole < common.RoleAdminUser { + allowedModules := map[string]bool{ + "home": true, + "console": true, + } + return allowedModules[moduleKey] + } + + // 管理员可以访问更多模块 + return true +} + +// hasModulePermissionForUser 检查用户是否有权限访问指定模块 +func hasModulePermissionForUser(userRole int, modulePath string, moduleValue interface{}) bool { + // 数据看板始终允许访问,不受控制台区域开关影响 + if modulePath == "console.detail" { + return true + } + + // 普通用户只能访问基础功能 + if userRole < common.RoleAdminUser { + return isUserModuleAllowedInFilter(modulePath) + } + + // 管理员需要检查模块是否启用 + if userRole >= common.RoleAdminUser && userRole < common.RoleRootUser { + // 检查模块值是否为启用状态 + switch v := moduleValue.(type) { + case bool: + return v + case map[string]interface{}: + if enabled, hasEnabled := v["enabled"]; hasEnabled { + if enabledBool, ok := enabled.(bool); ok { + return enabledBool + } + } + return true // 如果没有enabled字段,默认启用 + default: + return true + } + } + + return true +} + +// isUserModuleAllowedInFilter 检查普通用户是否允许访问指定模块(用于过滤) +func isUserModuleAllowedInFilter(modulePath string) bool { + // 数据看板始终允许访问,不受控制台区域开关影响 + if modulePath == "console.detail" { + return true + } + + // 普通用户允许访问的模块列表 + allowedModules := map[string]bool{ + "console.enabled": true, + "console.detail": true, + "console.token": true, + "console.log": true, + "console.midjourney": true, + "console.task": true, + "personal.enabled": true, + "personal.topup": true, + "personal.personal": true, + "chat.enabled": true, + "chat.playground": true, + "chat.chat": true, + } + + return allowedModules[modulePath] +} diff --git a/controller/user.go b/controller/user.go index 8e43fe0e39a2..79e908693400 100644 --- a/controller/user.go +++ b/controller/user.go @@ -463,28 +463,57 @@ func GetSelf(c *gin.Context) { // 获取用户设置并提取sidebar_modules userSetting := user.GetSetting() - // 构建响应数据,包含用户信息和权限 + // 计算系统允许的最大权限范围 + systemSidebarConfig := calculateFinalSidebarConfig(userRole, userSetting) + + // 提取并过滤用户的侧边栏偏好设置,确保与系统权限一致 + var userSidebarModules interface{} + if userSetting.SidebarModules != "" { + var userSidebarModulesMap map[string]interface{} + if err := json.Unmarshal([]byte(userSetting.SidebarModules), &userSidebarModulesMap); err == nil { + // 基于系统权限过滤用户偏好 + filteredUserModules := filterUserModulesBySystemConfig(userSidebarModulesMap, systemSidebarConfig) + userSidebarModules = filteredUserModules + } else { + userSidebarModules = userSetting.SidebarModules + } + } else { + userSidebarModules = map[string]interface{}{} + } + + // 清理用户设置中的sidebar_modules,确保与最终配置一致 + cleanedSetting := cleanUserSettingForResponse(user.Setting, systemSidebarConfig) + + // 计算最终的显示配置(系统权限 ∩ 用户偏好) + finalSidebarConfig := calculateFinalDisplayConfig(systemSidebarConfig, userSidebarModules) + + // 精简权限信息,只保留必要的权限标识 + simplifiedPermissions := map[string]interface{}{ + "sidebar_settings": permissions["sidebar_settings"], // 是否有侧边栏设置权限 + } + + // 构建响应数据,包含用户信息和精简的配置 responseData := map[string]interface{}{ - "id": user.Id, - "username": user.Username, - "display_name": user.DisplayName, - "role": user.Role, - "status": user.Status, - "email": user.Email, - "group": user.Group, - "quota": user.Quota, - "used_quota": user.UsedQuota, - "request_count": user.RequestCount, - "aff_code": user.AffCode, - "aff_count": user.AffCount, - "aff_quota": user.AffQuota, + "id": user.Id, + "username": user.Username, + "display_name": user.DisplayName, + "role": user.Role, + "status": user.Status, + "email": user.Email, + "group": user.Group, + "quota": user.Quota, + "used_quota": user.UsedQuota, + "request_count": user.RequestCount, + "aff_code": user.AffCode, + "aff_count": user.AffCount, + "aff_quota": user.AffQuota, "aff_history_quota": user.AffHistoryQuota, - "inviter_id": user.InviterId, - "linux_do_id": user.LinuxDOId, - "setting": user.Setting, - "stripe_customer": user.StripeCustomer, - "sidebar_modules": userSetting.SidebarModules, // 正确提取sidebar_modules字段 - "permissions": permissions, // 新增权限字段 + "inviter_id": user.InviterId, + "linux_do_id": user.LinuxDOId, + "setting": cleanedSetting, // 完整用户设置(保持兼容性) + "stripe_customer": user.StripeCustomer, + "sidebar_config": finalSidebarConfig, // 最终的侧边栏配置 + "permissions": simplifiedPermissions, // 精简的权限信息 } c.JSON(http.StatusOK, gin.H{ @@ -523,6 +552,531 @@ func calculateUserPermissions(userRole int) map[string]interface{} { return permissions } +// 计算最终的侧边栏配置(系统配置 + 权限过滤,不包含用户偏好) +func calculateFinalSidebarConfig(userRole int, userSetting dto.UserSetting) map[string]interface{} { + // 1. 获取系统的侧边栏管理配置 + common.OptionMapRWMutex.RLock() + sidebarAdminConfigRaw := common.OptionMap["SidebarModulesAdmin"] + common.OptionMapRWMutex.RUnlock() + + // 2. 解析系统配置 + var systemConfig map[string]interface{} + if sidebarAdminConfigRaw != "" { + if err := json.Unmarshal([]byte(sidebarAdminConfigRaw), &systemConfig); err != nil { + // 解析失败时使用默认配置 + systemConfig = getDefaultSystemConfig() + } + } else { + systemConfig = getDefaultSystemConfig() + } + + // 3. 不再考虑用户个人偏好,sidebar_config只反映系统允许的最大权限范围 + + // 4. 计算最终配置 + finalConfig := map[string]interface{}{} + + // 遍历系统配置的所有区域 + for sectionKey, sectionValue := range systemConfig { + sectionObj, ok := sectionValue.(map[string]interface{}) + if !ok { + continue + } + + // 检查用户是否有权限访问这个区域 + if !hasUserPermissionForSection(userRole, sectionKey) { + continue + } + + // 检查系统是否启用了这个区域 + sectionEnabled := true + if enabled, hasEnabled := sectionObj["enabled"]; hasEnabled { + if enabledBool, ok := enabled.(bool); ok { + sectionEnabled = enabledBool + } + } + + if !sectionEnabled { + continue + } + + // 计算区域的最终配置(只考虑系统配置和用户权限,不考虑用户偏好) + sectionConfig := map[string]interface{}{} + + // 区域级别的enabled状态:只要系统启用就为true + sectionConfig["enabled"] = sectionEnabled + + // 处理区域内的各个模块 + for moduleKey, moduleValue := range sectionObj { + if moduleKey == "enabled" { + continue + } + + // 检查用户是否有权限访问这个模块 + modulePath := sectionKey + "." + moduleKey + if !hasUserPermissionForModule(userRole, modulePath) { + sectionConfig[moduleKey] = false + continue + } + + // 处理嵌套的模块配置(如 admin.user) + switch v := moduleValue.(type) { + case bool: + // 简单的布尔值模块 + systemModuleEnabled := v + finalModuleEnabled := systemModuleEnabled && sectionConfig["enabled"].(bool) + sectionConfig[moduleKey] = finalModuleEnabled + case map[string]interface{}: + // 嵌套的对象模块(如 admin.user 包含 enabled 和 groupManagement) + nestedModuleConfig := map[string]interface{}{} + + // 检查嵌套模块的enabled状态 + nestedEnabled := true + if enabled, hasEnabled := v["enabled"]; hasEnabled { + if enabledBool, ok := enabled.(bool); ok { + nestedEnabled = enabledBool + } + } + + // 最终的enabled状态 + finalNestedEnabled := nestedEnabled && sectionConfig["enabled"].(bool) + nestedModuleConfig["enabled"] = finalNestedEnabled + + // 处理嵌套模块的子功能 + for subModuleKey, subModuleValue := range v { + if subModuleKey == "enabled" { + continue + } + + // 检查用户是否有权限访问这个子功能 + subModulePath := sectionKey + "." + moduleKey + "." + subModuleKey + if !hasUserPermissionForModule(userRole, subModulePath) { + nestedModuleConfig[subModuleKey] = false + continue + } + + // 检查系统是否启用了这个子功能 + subModuleEnabled := true + if subModuleBool, ok := subModuleValue.(bool); ok { + subModuleEnabled = subModuleBool + } + + // 最终状态:系统启用 && 用户权限允许 && 父模块启用 + finalSubModuleEnabled := subModuleEnabled && finalNestedEnabled + nestedModuleConfig[subModuleKey] = finalSubModuleEnabled + } + + sectionConfig[moduleKey] = nestedModuleConfig + default: + // 其他类型,直接设置为false + sectionConfig[moduleKey] = false + } + } + + finalConfig[sectionKey] = sectionConfig + } + + return finalConfig +} + +// 获取默认的系统配置 +func getDefaultSystemConfig() map[string]interface{} { + return map[string]interface{}{ + "chat": map[string]interface{}{ + "enabled": true, + "playground": true, + "chat": true, + }, + "console": map[string]interface{}{ + "enabled": true, + "detail": true, + "token": true, + "log": true, + "midjourney": true, + "task": true, + }, + "personal": map[string]interface{}{ + "enabled": true, + "topup": true, + "personal": true, + }, + "admin": map[string]interface{}{ + "enabled": true, + "channel": true, + "models": true, + "redemption": true, + "user": map[string]interface{}{ + "enabled": true, + "groupManagement": true, // 默认启用分组管理 + }, + "setting": true, + }, + } +} + +// 检查用户是否有权限访问指定区域 +func hasUserPermissionForSection(userRole int, sectionKey string) bool { + // 普通用户不能访问管理员区域 + if userRole < common.RoleAdminUser && sectionKey == "admin" { + return false + } + return true +} + +// 检查用户是否有权限访问指定模块 +func hasUserPermissionForModule(userRole int, modulePath string) bool { + // 数据看板始终允许访问 + if modulePath == "console.detail" { + return true + } + + // 管理员不能访问系统设置 + if userRole == common.RoleAdminUser && modulePath == "admin.setting" { + return false + } + + // 处理嵌套的模块路径(如 admin.user.groupManagement) + pathParts := strings.Split(modulePath, ".") + if len(pathParts) >= 2 { + sectionKey := pathParts[0] + + // 普通用户不能访问管理员区域的任何模块 + if userRole < common.RoleAdminUser && sectionKey == "admin" { + return false + } + + // 对于三层路径(如 admin.user.groupManagement),检查特殊权限 + if len(pathParts) == 3 && sectionKey == "admin" && pathParts[1] == "user" && pathParts[2] == "groupManagement" { + // 分组管理功能:管理员和超级管理员都可以访问 + return userRole >= common.RoleAdminUser + } + } + + return true +} + +// 清理用户设置,添加系统权限信息供个人设置页面使用 +func cleanUserSettingForResponse(originalSetting string, systemSidebarConfig map[string]interface{}) string { + if originalSetting == "" { + return "" + } + + // 解析原始设置 + var userSetting dto.UserSetting + if err := json.Unmarshal([]byte(originalSetting), &userSetting); err != nil { + // 解析失败,返回原始设置 + return originalSetting + } + + // 如果没有sidebar_modules配置,直接返回 + if userSetting.SidebarModules == "" { + return originalSetting + } + + // 解析用户的sidebar_modules配置 + var userSidebarModules map[string]interface{} + if err := json.Unmarshal([]byte(userSetting.SidebarModules), &userSidebarModules); err != nil { + // 解析失败,返回原始设置 + return originalSetting + } + + // 基于系统配置过滤用户的sidebar_modules,同时保留系统权限信息 + filteredSidebarModules := map[string]interface{}{} + for sectionKey, sectionValue := range systemSidebarConfig { + sectionObj, ok := sectionValue.(map[string]interface{}) + if !ok { + continue + } + + // 检查系统是否允许这个区域 + systemSectionEnabled, hasEnabled := sectionObj["enabled"] + if !hasEnabled || systemSectionEnabled != true { + continue + } + + // 获取用户对这个区域的配置 + userSection := map[string]interface{}{} + if userSidebarModules[sectionKey] != nil { + if userSectionObj, ok := userSidebarModules[sectionKey].(map[string]interface{}); ok { + userSection = userSectionObj + } + } + + // 构建过滤后的区域配置 + filteredSection := map[string]interface{}{ + "enabled": userSection["enabled"], // 保持用户的enabled偏好 + } + + // 只保留最终配置中存在的模块(支持布尔与嵌套对象) + for moduleKey, moduleValue := range sectionObj { + if moduleKey == "enabled" { + continue + } + + // 判断系统是否允许该模块 + systemAllows := false + switch v := moduleValue.(type) { + case bool: + systemAllows = v + case map[string]interface{}: + // 嵌套对象,检查其enabled状态,缺省视为true + if enabled, hasEnabled := v["enabled"]; hasEnabled { + if enabledBool, ok := enabled.(bool); ok { + systemAllows = enabledBool + } else { + systemAllows = true + } + } else { + systemAllows = true + } + default: + systemAllows = false + } + + if systemAllows { + // 保持用户对这个模块的偏好(布尔或对象),若未设置则默认启用 + if userModuleValue, exists := userSection[moduleKey]; exists { + filteredSection[moduleKey] = userModuleValue + } else { + filteredSection[moduleKey] = true // 默认启用 + } + } + } + + filteredSidebarModules[sectionKey] = filteredSection + } + + // 更新用户设置中的sidebar_modules + filteredSidebarModulesJSON, err := json.Marshal(filteredSidebarModules) + if err != nil { + // 序列化失败,返回原始设置 + return originalSetting + } + + userSetting.SidebarModules = string(filteredSidebarModulesJSON) + + // 添加系统权限信息供个人设置页面使用 + systemConfigJSON, err := json.Marshal(systemSidebarConfig) + if err == nil { + // 创建一个扩展的用户设置结构 + extendedSetting := map[string]interface{}{ + "sidebar_modules": userSetting.SidebarModules, + "sidebar_system_config": string(systemConfigJSON), // 系统权限信息 + } + + // 添加其他用户设置字段(如果有的话) + var originalSettingMap map[string]interface{} + if err := json.Unmarshal([]byte(originalSetting), &originalSettingMap); err == nil { + for key, value := range originalSettingMap { + if key != "sidebar_modules" && key != "sidebar_system_config" { + extendedSetting[key] = value + } + } + } + + // 序列化扩展的设置 + if extendedSettingJSON, err := json.Marshal(extendedSetting); err == nil { + return string(extendedSettingJSON) + } + } + + // 如果添加系统配置失败,使用原有逻辑 + cleanedSettingJSON, err := json.Marshal(userSetting) + if err != nil { + return originalSetting + } + + return string(cleanedSettingJSON) +} + +// 基于系统权限过滤用户偏好设置 +func filterUserModulesBySystemConfig(userModules map[string]interface{}, systemConfig map[string]interface{}) map[string]interface{} { + filteredModules := map[string]interface{}{} + + // 只保留系统允许的区域和模块 + for sectionKey, sectionValue := range systemConfig { + systemSection, ok := sectionValue.(map[string]interface{}) + if !ok || systemSection["enabled"] != true { + continue + } + + // 获取用户对这个区域的偏好 + userSection := map[string]interface{}{} + if userModules[sectionKey] != nil { + if userSectionObj, ok := userModules[sectionKey].(map[string]interface{}); ok { + userSection = userSectionObj + } + } + + // 构建过滤后的区域配置 + filteredSection := map[string]interface{}{ + "enabled": userSection["enabled"], // 保持用户的enabled偏好 + } + + // 只保留系统允许的模块(同时支持布尔模块与嵌套对象模块) + for moduleKey, moduleValue := range systemSection { + if moduleKey == "enabled" { + continue + } + + // 判断系统是否允许该模块 + systemAllows := false + switch v := moduleValue.(type) { + case bool: + systemAllows = v + case map[string]interface{}: + // 嵌套对象,检查其enabled状态,缺省视为true + if enabled, hasEnabled := v["enabled"]; hasEnabled { + if enabledBool, ok := enabled.(bool); ok { + systemAllows = enabledBool + } else { + systemAllows = true + } + } else { + systemAllows = true + } + default: + systemAllows = false + } + + if systemAllows { + // 保持用户对这个模块的偏好(支持布尔或对象),若未设置则默认启用 + if userModuleValue, exists := userSection[moduleKey]; exists { + filteredSection[moduleKey] = userModuleValue + } else { + filteredSection[moduleKey] = true // 默认启用 + } + } + } + + filteredModules[sectionKey] = filteredSection + } + + return filteredModules +} + +// 计算最终的显示配置(系统权限 ∩ 用户偏好) +func calculateFinalDisplayConfig(systemConfig map[string]interface{}, userModules interface{}) map[string]interface{} { + finalConfig := map[string]interface{}{} + + // 解析用户偏好设置 + var userPreferences map[string]interface{} + switch v := userModules.(type) { + case map[string]interface{}: + userPreferences = v + case string: + if err := json.Unmarshal([]byte(v), &userPreferences); err != nil { + userPreferences = map[string]interface{}{} + } + default: + userPreferences = map[string]interface{}{} + } + + // 遍历系统允许的所有区域 + for sectionKey, sectionValue := range systemConfig { + systemSection, ok := sectionValue.(map[string]interface{}) + if !ok || systemSection["enabled"] != true { + continue + } + + // 获取用户对这个区域的偏好 + userSection := map[string]interface{}{} + if userPreferences[sectionKey] != nil { + if userSectionObj, ok := userPreferences[sectionKey].(map[string]interface{}); ok { + userSection = userSectionObj + } + } + + // 计算区域的最终配置 + sectionConfig := map[string]interface{}{} + + // 区域级别:用户可以关闭系统允许的区域 + userSectionEnabled := userSection["enabled"] != false + sectionConfig["enabled"] = userSectionEnabled + + // 处理区域内的模块 + for moduleKey, moduleValue := range systemSection { + if moduleKey == "enabled" { + continue + } + + // 检查系统是否允许这个模块 + var systemModuleEnabled bool + switch v := moduleValue.(type) { + case bool: + systemModuleEnabled = v + case map[string]interface{}: + // 对于嵌套对象,检查其enabled状态 + if enabled, hasEnabled := v["enabled"]; hasEnabled { + if enabledBool, ok := enabled.(bool); ok { + systemModuleEnabled = enabledBool + } else { + systemModuleEnabled = true // 默认启用 + } + } else { + systemModuleEnabled = true // 没有enabled字段时默认启用 + } + default: + systemModuleEnabled = false + } + + if !systemModuleEnabled { + sectionConfig[moduleKey] = false + continue + } + + // 对于嵌套对象,需要合并系统配置和用户偏好 + if nestedObj, isNested := moduleValue.(map[string]interface{}); isNested { + // 获取用户对这个嵌套对象的偏好 + userNestedObj := map[string]interface{}{} + if userSection[moduleKey] != nil { + if userNestedMap, ok := userSection[moduleKey].(map[string]interface{}); ok { + userNestedObj = userNestedMap + } + } + + // 计算有效的enabled:支持用户以布尔值直接覆盖嵌套对象(个人设置场景) + var effectiveEnabled interface{} + if userBool, ok := userSection[moduleKey].(bool); ok { + effectiveEnabled = userBool + } else if ue, exists := userNestedObj["enabled"]; exists { + effectiveEnabled = ue + } else if sysEnabled, has := nestedObj["enabled"]; has { + effectiveEnabled = sysEnabled + } else { + effectiveEnabled = true + } + + // 合并系统配置和用户偏好 + finalNestedObj := make(map[string]interface{}) + for k, v := range nestedObj { + if k == "enabled" { + finalNestedObj[k] = effectiveEnabled + } else { + // 其他字段保持系统配置 + finalNestedObj[k] = v + } + } + + // 如果区域被禁用,强制将嵌套对象的enabled设置为false + if !userSectionEnabled { + finalNestedObj["enabled"] = false + } + + sectionConfig[moduleKey] = finalNestedObj + } else { + // 简单布尔值模块,用户可以关闭系统允许的模块 + userModuleEnabled := userSection[moduleKey] != false + // 最终状态:系统允许 && 用户偏好 && 区域启用 + sectionConfig[moduleKey] = systemModuleEnabled && userModuleEnabled && userSectionEnabled + } + } + + finalConfig[sectionKey] = sectionConfig + } + + return finalConfig +} + // 根据用户角色生成默认的边栏配置 func generateDefaultSidebarConfig(userRole int) string { defaultConfig := map[string]interface{}{} @@ -559,8 +1113,11 @@ func generateDefaultSidebarConfig(userRole int) string { "channel": true, "models": true, "redemption": true, - "user": true, - "setting": false, // 管理员不能访问系统设置 + "user": map[string]interface{}{ + "enabled": true, + "groupManagement": true, // 管理员默认可以访问分组管理 + }, + "setting": false, // 管理员不能访问系统设置 } } else if userRole == common.RoleRootUser { // 超级管理员可以访问所有功能 @@ -569,8 +1126,11 @@ func generateDefaultSidebarConfig(userRole int) string { "channel": true, "models": true, "redemption": true, - "user": true, - "setting": true, + "user": map[string]interface{}{ + "enabled": true, + "groupManagement": true, // 超级管理员默认可以访问分组管理 + }, + "setting": true, } } // 普通用户不包含admin区域 diff --git a/middleware/auth.go b/middleware/auth.go index 25caf50d9be0..b1f0099abc9f 100644 --- a/middleware/auth.go +++ b/middleware/auth.go @@ -1,6 +1,7 @@ package middleware import ( + "encoding/json" "fmt" "net/http" "one-api/common" @@ -175,6 +176,166 @@ func WssAuth(c *gin.Context) { } +// ModuleAuth 检查用户是否有权限访问特定功能模块 +func ModuleAuth(modulePath string) gin.HandlerFunc { + return func(c *gin.Context) { + session := sessions.Default(c) + role := session.Get("role") + id := session.Get("id") + + // 如果用户未登录,先进行基础认证 + if role == nil || id == nil { + c.JSON(http.StatusUnauthorized, gin.H{ + "success": false, + "message": "未登录,无权访问", + }) + c.Abort() + return + } + + userRole := role.(int) + userId := id.(int) + + // 超级管理员始终允许访问所有功能 + if userRole >= common.RoleRootUser { + c.Next() + return + } + + // 检查用户是否有权限访问指定模块 + if !hasModulePermission(userRole, userId, modulePath) { + c.JSON(http.StatusForbidden, gin.H{ + "success": false, + "message": "无权访问此功能模块", + }) + c.Abort() + return + } + + c.Next() + } +} + +// hasModulePermission 检查用户是否有权限访问指定模块 +func hasModulePermission(userRole int, userId int, modulePath string) bool { + // 普通用户只能访问基础功能 + if userRole < common.RoleAdminUser { + return isUserModuleAllowed(modulePath) + } + + // 管理员需要检查侧边栏管理配置 + if userRole >= common.RoleAdminUser && userRole < common.RoleRootUser { + return isAdminModuleAllowed(modulePath) + } + + return true +} + +// isUserModuleAllowed 检查普通用户是否允许访问指定模块 +func isUserModuleAllowed(modulePath string) bool { + // 数据看板始终允许访问,不受控制台区域开关影响 + if modulePath == "console.detail" { + return true + } + + // 普通用户允许访问的模块列表 + allowedModules := map[string]bool{ + "console.detail": true, + "console.token": true, + "console.log": true, + "console.midjourney": true, + "console.task": true, + "personal.topup": true, + "personal.personal": true, + "chat.playground": true, + "chat.chat": true, + } + + return allowedModules[modulePath] +} + +// isAdminModuleAllowed 检查管理员是否允许访问指定模块 +func isAdminModuleAllowed(modulePath string) bool { + // 数据看板始终允许访问,不受控制台区域开关影响 + if modulePath == "console.detail" { + return true + } + + // 获取侧边栏管理配置 + common.OptionMapRWMutex.RLock() + sidebarConfig, exists := common.OptionMap["SidebarModulesAdmin"] + common.OptionMapRWMutex.RUnlock() + + if !exists || sidebarConfig == "" { + // 如果没有配置,默认允许管理员访问所有功能(除了系统设置) + if modulePath == "admin.setting" { + return false + } + return true + } + + // 解析配置 + var config map[string]interface{} + if err := json.Unmarshal([]byte(sidebarConfig), &config); err != nil { + // 解析失败时采用安全优先策略,拒绝访问 + common.SysLog("解析侧边栏配置失败: " + err.Error()) + return false + } + + // 检查嵌套权限 + return checkNestedPermission(config, modulePath) +} + +// checkNestedPermission 检查嵌套权限路径 +func checkNestedPermission(config map[string]interface{}, modulePath string) bool { + parts := strings.Split(modulePath, ".") + current := config + + for i, part := range parts { + if current == nil { + return false + } + + value, exists := current[part] + if !exists { + return false + } + + // 如果是最后一个部分,检查布尔值 + if i == len(parts)-1 { + if boolVal, ok := value.(bool); ok { + return boolVal + } + // 如果是对象且有enabled字段,检查enabled + if objVal, ok := value.(map[string]interface{}); ok { + if enabled, hasEnabled := objVal["enabled"]; hasEnabled { + if enabledBool, ok := enabled.(bool); ok { + return enabledBool + } + } + // 如果没有enabled字段,默认为true + return true + } + return false + } + + // 中间路径必须是对象 + if objVal, ok := value.(map[string]interface{}); ok { + // 检查区域是否启用 + if enabled, hasEnabled := objVal["enabled"]; hasEnabled { + if enabledBool, ok := enabled.(bool); ok && !enabledBool { + return false + } + } + current = objVal + } else { + return false + } + } + + return false +} + func TokenAuth() func(c *gin.Context) { return func(c *gin.Context) { // 先检测是否为ws diff --git a/model/user.go b/model/user.go index 16dd64d519a5..a53e6eb32032 100644 --- a/model/user.go +++ b/model/user.go @@ -128,8 +128,11 @@ func GenerateDefaultSidebarConfigForRole(userRole int) string { "channel": true, "models": true, "redemption": true, - "user": true, - "setting": false, // 管理员不能访问系统设置 + "user": map[string]interface{}{ + "enabled": true, + "groupManagement": true, // 管理员默认可以访问分组管理 + }, + "setting": false, // 管理员不能访问系统设置 } } else if userRole == common.RoleRootUser { // 超级管理员可以访问所有功能 @@ -138,8 +141,11 @@ func GenerateDefaultSidebarConfigForRole(userRole int) string { "channel": true, "models": true, "redemption": true, - "user": true, - "setting": true, + "user": map[string]interface{}{ + "enabled": true, + "groupManagement": true, // 超级管理员默认可以访问分组管理 + }, + "setting": true, } } // 普通用户不包含admin区域 diff --git a/model/user_group.go b/model/user_group.go index b63ae5e7459c..9a5ce7e9c29e 100644 --- a/model/user_group.go +++ b/model/user_group.go @@ -29,9 +29,9 @@ func (g *UserGroup) Update() error { return DB.Model(g).Updates(g).Error } -// Delete 软删除用户分组 +// Delete 硬删除用户分组 func (g *UserGroup) Delete() error { - return DB.Delete(g).Error + return DB.Unscoped().Delete(g).Error } // GetAllUserGroups 获取所有用户分组 diff --git a/router/api-router.go b/router/api-router.go index 9b10f7f58e9a..9ceb96fcdb8c 100644 --- a/router/api-router.go +++ b/router/api-router.go @@ -79,6 +79,7 @@ func SetApiRouter(router *gin.Engine) { adminRoute := userRoute.Group("/") adminRoute.Use(middleware.AdminAuth()) + adminRoute.Use(middleware.ModuleAuth("admin.user")) { adminRoute.GET("/", controller.GetAllUsers) adminRoute.GET("/search", controller.SearchUsers) @@ -109,6 +110,7 @@ func SetApiRouter(router *gin.Engine) { } channelRoute := apiRouter.Group("/channel") channelRoute.Use(middleware.AdminAuth()) + channelRoute.Use(middleware.ModuleAuth("admin.channel")) { channelRoute.GET("/", controller.GetAllChannels) channelRoute.GET("/search", controller.SearchChannels) @@ -138,6 +140,7 @@ func SetApiRouter(router *gin.Engine) { } tokenRoute := apiRouter.Group("/token") tokenRoute.Use(middleware.UserAuth()) + tokenRoute.Use(middleware.ModuleAuth("console.token")) { tokenRoute.GET("/", controller.GetAllTokens) tokenRoute.GET("/search", controller.SearchTokens) @@ -160,6 +163,7 @@ func SetApiRouter(router *gin.Engine) { redemptionRoute := apiRouter.Group("/redemption") redemptionRoute.Use(middleware.AdminAuth()) + redemptionRoute.Use(middleware.ModuleAuth("admin.redemption")) { redemptionRoute.GET("/", controller.GetAllRedemptions) redemptionRoute.GET("/search", controller.SearchRedemptions) @@ -170,17 +174,17 @@ func SetApiRouter(router *gin.Engine) { redemptionRoute.DELETE("/:id", controller.DeleteRedemption) } logRoute := apiRouter.Group("/log") - logRoute.GET("/", middleware.AdminAuth(), controller.GetAllLogs) - logRoute.DELETE("/", middleware.AdminAuth(), controller.DeleteHistoryLogs) - logRoute.GET("/stat", middleware.AdminAuth(), controller.GetLogsStat) - logRoute.GET("/self/stat", middleware.UserAuth(), controller.GetLogsSelfStat) - logRoute.GET("/search", middleware.AdminAuth(), controller.SearchAllLogs) - logRoute.GET("/self", middleware.UserAuth(), controller.GetUserLogs) - logRoute.GET("/self/search", middleware.UserAuth(), controller.SearchUserLogs) + logRoute.GET("/", middleware.AdminAuth(), middleware.ModuleAuth("console.log"), controller.GetAllLogs) + logRoute.DELETE("/", middleware.AdminAuth(), middleware.ModuleAuth("console.log"), controller.DeleteHistoryLogs) + logRoute.GET("/stat", middleware.AdminAuth(), middleware.ModuleAuth("console.log"), controller.GetLogsStat) + logRoute.GET("/self/stat", middleware.UserAuth(), middleware.ModuleAuth("console.log"), controller.GetLogsSelfStat) + logRoute.GET("/search", middleware.AdminAuth(), middleware.ModuleAuth("console.log"), controller.SearchAllLogs) + logRoute.GET("/self", middleware.UserAuth(), middleware.ModuleAuth("console.log"), controller.GetUserLogs) + logRoute.GET("/self/search", middleware.UserAuth(), middleware.ModuleAuth("console.log"), controller.SearchUserLogs) dataRoute := apiRouter.Group("/data") - dataRoute.GET("/", middleware.AdminAuth(), controller.GetAllQuotaDates) - dataRoute.GET("/self", middleware.UserAuth(), controller.GetUserQuotaDates) + dataRoute.GET("/", middleware.AdminAuth(), middleware.ModuleAuth("console.detail"), controller.GetAllQuotaDates) + dataRoute.GET("/self", middleware.UserAuth(), middleware.ModuleAuth("console.detail"), controller.GetUserQuotaDates) logRoute.Use(middleware.CORS()) { @@ -203,6 +207,7 @@ func SetApiRouter(router *gin.Engine) { userGroupRoute := apiRouter.Group("/user_group") userGroupRoute.Use(middleware.AdminAuth()) + userGroupRoute.Use(middleware.ModuleAuth("admin.user.groupManagement")) { userGroupRoute.GET("/", controller.GetAllUserGroups) userGroupRoute.POST("/", controller.CreateUserGroup) @@ -211,13 +216,13 @@ func SetApiRouter(router *gin.Engine) { } mjRoute := apiRouter.Group("/mj") - mjRoute.GET("/self", middleware.UserAuth(), controller.GetUserMidjourney) - mjRoute.GET("/", middleware.AdminAuth(), controller.GetAllMidjourney) + mjRoute.GET("/self", middleware.UserAuth(), middleware.ModuleAuth("console.midjourney"), controller.GetUserMidjourney) + mjRoute.GET("/", middleware.AdminAuth(), middleware.ModuleAuth("console.midjourney"), controller.GetAllMidjourney) taskRoute := apiRouter.Group("/task") { - taskRoute.GET("/self", middleware.UserAuth(), controller.GetUserTask) - taskRoute.GET("/", middleware.AdminAuth(), controller.GetAllTask) + taskRoute.GET("/self", middleware.UserAuth(), middleware.ModuleAuth("console.task"), controller.GetUserTask) + taskRoute.GET("/", middleware.AdminAuth(), middleware.ModuleAuth("console.task"), controller.GetAllTask) } vendorRoute := apiRouter.Group("/vendors") @@ -233,6 +238,7 @@ func SetApiRouter(router *gin.Engine) { modelsRoute := apiRouter.Group("/models") modelsRoute.Use(middleware.AdminAuth()) + modelsRoute.Use(middleware.ModuleAuth("admin.models")) { modelsRoute.GET("/sync_upstream/preview", controller.SyncUpstreamPreview) modelsRoute.POST("/sync_upstream", controller.SyncUpstreamModels) diff --git a/web/index.html b/web/index.html index 09d87ae1a890..a9ecbe0153e4 100644 --- a/web/index.html +++ b/web/index.html @@ -10,6 +10,29 @@ content="OpenAI 接口聚合管理,支持多种渠道包括 Azure,可用于二次分发管理 key,仅单可执行文件,已打包好 Docker 镜像,一键部署,开箱即用" /> New API + diff --git a/web/src/App.jsx b/web/src/App.jsx index 635742f9161e..ed2261ad569b 100644 --- a/web/src/App.jsx +++ b/web/src/App.jsx @@ -22,6 +22,7 @@ import { Route, Routes, useLocation } from 'react-router-dom'; import Loading from './components/common/ui/Loading'; import User from './pages/User'; import { AuthRedirect, PrivateRoute, AdminRoute } from './helpers'; +import ModuleRoute from './components/auth/ModuleRoute'; import RegisterForm from './components/auth/RegisterForm'; import LoginForm from './components/auth/LoginForm'; import NotFound from './pages/NotFound'; @@ -58,7 +59,7 @@ function App() { // 获取模型广场权限配置 const pricingRequireAuth = useMemo(() => { - const headerNavModulesConfig = statusState?.status?.HeaderNavModules; + const headerNavModulesConfig = statusState?.status?.header_nav_modules; if (headerNavModulesConfig) { try { const modules = JSON.parse(headerNavModulesConfig); @@ -76,7 +77,7 @@ function App() { } } return false; // 默认不需要登录 - }, [statusState?.status?.HeaderNavModules]); + }, [statusState?.status?.header_nav_modules]); return ( @@ -102,7 +103,9 @@ function App() { path='/console/models' element={ - + + + } /> @@ -110,7 +113,9 @@ function App() { path='/console/channel' element={ - + + + } /> @@ -118,7 +123,9 @@ function App() { path='/console/token' element={ - + + + } /> @@ -126,7 +133,9 @@ function App() { path='/console/playground' element={ - + + + } /> @@ -134,7 +143,9 @@ function App() { path='/console/redemption' element={ - + + + } /> @@ -142,7 +153,9 @@ function App() { path='/console/user' element={ - + + + } /> @@ -210,9 +223,11 @@ function App() { path='/console/setting' element={ - } key={location.pathname}> - - + + } key={location.pathname}> + + + } /> @@ -220,9 +235,11 @@ function App() { path='/console/personal' element={ - } key={location.pathname}> - - + + } key={location.pathname}> + + + } /> @@ -230,9 +247,11 @@ function App() { path='/console/topup' element={ - } key={location.pathname}> - - + + } key={location.pathname}> + + + } /> @@ -240,7 +259,9 @@ function App() { path='/console/log' element={ - + + + } /> @@ -248,9 +269,11 @@ function App() { path='/console' element={ - } key={location.pathname}> - - + + } key={location.pathname}> + + + } /> @@ -258,9 +281,11 @@ function App() { path='/console/midjourney' element={ - } key={location.pathname}> - - + + } key={location.pathname}> + + + } /> @@ -268,9 +293,11 @@ function App() { path='/console/task' element={ - } key={location.pathname}> - - + + } key={location.pathname}> + + + } /> @@ -304,9 +331,11 @@ function App() { } key={location.pathname}> - - + + } key={location.pathname}> + + + } /> {/* 方便使用chat2link直接跳转聊天... */} @@ -314,9 +343,11 @@ function App() { path='/chat2link' element={ - } key={location.pathname}> - - + + } key={location.pathname}> + + + } /> diff --git a/web/src/components/auth/ModuleRoute.jsx b/web/src/components/auth/ModuleRoute.jsx new file mode 100644 index 000000000000..93f7b438dd3a --- /dev/null +++ b/web/src/components/auth/ModuleRoute.jsx @@ -0,0 +1,181 @@ +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; \ No newline at end of file diff --git a/web/src/components/layout/PageLayout.jsx b/web/src/components/layout/PageLayout.jsx index f8cdfb0cb8a1..f11fa358df7b 100644 --- a/web/src/components/layout/PageLayout.jsx +++ b/web/src/components/layout/PageLayout.jsx @@ -139,6 +139,8 @@ const PageLayout = () => { overflow: isMobile ? 'visible' : 'auto', display: 'flex', flexDirection: 'column', + height: '100%', + flex: '1 1 auto', }} > {showSider && ( diff --git a/web/src/components/settings/OtherSetting.jsx b/web/src/components/settings/OtherSetting.jsx index 18119d2427db..46d22b696a26 100644 --- a/web/src/components/settings/OtherSetting.jsx +++ b/web/src/components/settings/OtherSetting.jsx @@ -106,6 +106,11 @@ const OtherSetting = () => { SystemName: true, })); await updateOption('SystemName', inputs.SystemName); + // 更新localStorage并触发title更新事件 + localStorage.setItem('system_name', inputs.SystemName); + window.dispatchEvent(new CustomEvent('systemNameUpdated', { + detail: { systemName: inputs.SystemName } + })); showSuccess(t('系统名称已更新')); } catch (error) { console.error(t('系统名称更新失败'), error); diff --git a/web/src/components/settings/personal/cards/NotificationSettings.jsx b/web/src/components/settings/personal/cards/NotificationSettings.jsx index e54a64ab63f0..61e4341cce8f 100644 --- a/web/src/components/settings/personal/cards/NotificationSettings.jsx +++ b/web/src/components/settings/personal/cards/NotificationSettings.jsx @@ -53,8 +53,6 @@ const NotificationSettings = ({ saveNotificationSettings, }) => { const formApiRef = useRef(null); - const [statusState] = useContext(StatusContext); - const [userState] = useContext(UserContext); // 左侧边栏设置相关状态 const [sidebarLoading, setSidebarLoading] = useState(false); @@ -172,29 +170,85 @@ const NotificationSettings = ({ setSidebarModulesUser(defaultConfig); }; + // 获取默认系统配置 + const getDefaultSystemConfig = () => { + return { + chat: { + enabled: true, + playground: true, + chat: true + }, + console: { + enabled: true, + detail: true, + token: true, + log: true, + midjourney: true, + task: true + }, + personal: { + enabled: true, + topup: true, + personal: true + }, + admin: { + enabled: true, + channel: true, + models: true, + redemption: true, + user: true, + setting: true + } + }; + }; + // 加载左侧边栏配置 useEffect(() => { const loadSidebarConfigs = async () => { try { - // 获取管理员全局配置 - if (statusState?.status?.SidebarModulesAdmin) { - const adminConf = JSON.parse(statusState.status.SidebarModulesAdmin); - setAdminConfig(adminConf); - } - - // 获取用户个人配置 + // 获取侧边栏配置 const userRes = await API.get('/api/user/self'); - if (userRes.data.success && userRes.data.data.sidebar_modules) { - const userConf = JSON.parse(userRes.data.data.sidebar_modules); - setSidebarModulesUser(userConf); + if (userRes.data.success) { + // 从setting字段中获取系统配置和用户偏好设置 + if (userRes.data.data.setting) { + try { + const setting = JSON.parse(userRes.data.data.setting); + + // 获取系统配置(用于权限检查) + const systemConfig = setting.sidebar_system_config ? + JSON.parse(setting.sidebar_system_config) : + getDefaultSystemConfig(); + setAdminConfig(systemConfig); + + // 获取用户偏好设置(用于显示当前状态) + if (setting.sidebar_modules) { + let userConf; + if (typeof setting.sidebar_modules === 'string') { + userConf = JSON.parse(setting.sidebar_modules); + } else { + userConf = setting.sidebar_modules; + } + setSidebarModulesUser(userConf); + } + } catch (error) { + console.error('解析用户设置失败:', error); + // 出错时使用默认配置 + setAdminConfig(getDefaultSystemConfig()); + } + } else { + // 没有setting时使用默认配置 + setAdminConfig(getDefaultSystemConfig()); + } } } catch (error) { console.error('加载边栏配置失败:', error); + // 出错时使用默认配置 + setAdminConfig(getDefaultSystemConfig()); } }; loadSidebarConfigs(); - }, [statusState]); + }, []); // 初始化表单值 useEffect(() => { diff --git a/web/src/components/table/users/UsersActions.jsx b/web/src/components/table/users/UsersActions.jsx index 0cc57207e0a1..2241ea8d06b2 100644 --- a/web/src/components/table/users/UsersActions.jsx +++ b/web/src/components/table/users/UsersActions.jsx @@ -17,14 +17,14 @@ along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ -import { useState, useContext } from 'react'; +import { useState } from 'react'; import { Button } from '@douyinfe/semi-ui'; import UserGroupManagement from './modals/UserGroupManagement'; -import { StatusContext } from '../../../context/Status'; +import { useSidebar } from '../../../hooks/common/useSidebar'; const UsersActions = ({ setShowAddUser, onRefreshUsers, t }) => { const [showGroupManagement, setShowGroupManagement] = useState(false); - const [statusState] = useContext(StatusContext); + const { finalConfig, loading: sidebarLoading } = useSidebar(); // 检查用户权限 const getUserRole = () => { @@ -37,6 +37,11 @@ const UsersActions = ({ setShowAddUser, onRefreshUsers, t }) => { // 检查分组管理功能是否可见 const canShowGroupManagement = () => { + // 如果侧边栏配置还在加载中,暂时不显示按钮 + if (sidebarLoading) { + return false; + } + // 超级管理员始终可以看到分组管理按钮 if (isRoot()) { return true; @@ -44,25 +49,16 @@ const UsersActions = ({ setShowAddUser, onRefreshUsers, t }) => { // 管理员需要检查系统设置中的分组管理开关 if (isAdmin()) { - // 从StatusContext中获取侧边栏管理员配置 - if (statusState?.status?.SidebarModulesAdmin) { - try { - const config = JSON.parse(statusState.status.SidebarModulesAdmin); - const userModuleConfig = config?.admin?.user; - - // 检查用户管理模块是否启用 - if (!userModuleConfig || !userModuleConfig.enabled) { - return false; - } - - // 检查分组管理子功能是否启用 - return userModuleConfig.groupManagement === true; - } catch (error) { - console.error('解析侧边栏配置失败:', error); - return true; // 解析失败时默认允许访问 - } + // 从useSidebar钩子获取最终的权限配置 + const userSection = finalConfig?.admin?.user; + + // 检查用户管理模块是否启用 + if (!userSection || userSection.enabled === false) { + return false; } - return true; // 没有配置时默认允许访问 + + // 检查分组管理子功能是否启用 + return userSection.groupManagement === true; } // 普通用户无权访问 diff --git a/web/src/components/table/users/modals/EditUserGroupModal.jsx b/web/src/components/table/users/modals/EditUserGroupModal.jsx index c0bb1fc7148c..572f5ecd166e 100644 --- a/web/src/components/table/users/modals/EditUserGroupModal.jsx +++ b/web/src/components/table/users/modals/EditUserGroupModal.jsx @@ -34,11 +34,52 @@ import { } from '@douyinfe/semi-icons'; import { useTranslation } from 'react-i18next'; import { API, showError, showSuccess } from '../../../../helpers'; +import { useSidebar } from '../../../../hooks/common/useSidebar'; const EditUserGroupModal = ({ visible, onClose, editingGroup, onSuccess }) => { const { t } = useTranslation(); const formApiRef = useRef(null); const [loading, setLoading] = useState(false); + const { finalConfig, loading: sidebarLoading } = useSidebar(); + + // 检查用户权限 + const getUserRole = () => { + const user = JSON.parse(localStorage.getItem('user') || '{}'); + return user?.role || 0; + }; + + const isRoot = () => getUserRole() >= 100; + const isAdmin = () => getUserRole() >= 10; + + // 检查是否有分组管理权限 + const hasGroupManagementPermission = () => { + // 如果侧边栏配置还在加载中,暂时拒绝访问 + if (sidebarLoading) { + return false; + } + + // 超级管理员始终有权限 + if (isRoot()) { + return true; + } + + // 管理员需要检查权限配置 + if (isAdmin()) { + // 从useSidebar钩子获取最终的权限配置 + const userSection = finalConfig?.admin?.user; + + // 检查用户管理模块是否启用 + if (!userSection || userSection.enabled === false) { + return false; + } + + // 检查分组管理子功能是否启用 + return userSection.groupManagement === true; + } + + // 普通用户无权访问 + return false; + }; const isEdit = editingGroup && editingGroup.id; const isSystemGroup = editingGroup && ( @@ -62,6 +103,13 @@ const EditUserGroupModal = ({ visible, onClose, editingGroup, onSuccess }) => { }); const submit = async (values) => { + // 检查权限 + if (!hasGroupManagementPermission()) { + showError(t('无权访问分组管理功能')); + onClose(); + return; + } + setLoading(true); try { const data = { @@ -75,10 +123,10 @@ const EditUserGroupModal = ({ visible, onClose, editingGroup, onSuccess }) => { const url = isEdit ? '/api/user_group' : '/api/user_group'; const method = isEdit ? 'PUT' : 'POST'; - + const res = await API[method.toLowerCase()](url, data); const { success, message } = res.data; - + if (success) { showSuccess(isEdit ? t('分组更新成功!') : t('分组创建成功!')); onSuccess(); @@ -86,7 +134,12 @@ const EditUserGroupModal = ({ visible, onClose, editingGroup, onSuccess }) => { showError(message); } } catch (error) { - showError(isEdit ? t('分组更新失败') : t('分组创建失败')); + if (error.response?.status === 403) { + showError(t('无权访问分组管理功能')); + onClose(); + } else { + showError(isEdit ? t('分组更新失败') : t('分组创建失败')); + } } setLoading(false); }; diff --git a/web/src/components/table/users/modals/UserGroupManagement.jsx b/web/src/components/table/users/modals/UserGroupManagement.jsx index 429fbf996ed2..98b6bbb675f2 100644 --- a/web/src/components/table/users/modals/UserGroupManagement.jsx +++ b/web/src/components/table/users/modals/UserGroupManagement.jsx @@ -42,6 +42,7 @@ import { useIsMobile } from '../../../../hooks/common/useIsMobile'; import { API, showError, showSuccess } from '../../../../helpers'; import CardTable from '../../../common/ui/CardTable'; import EditUserGroupModal from './EditUserGroupModal'; +import { useSidebar } from '../../../../hooks/common/useSidebar'; const UserGroupManagement = ({ visible, onClose, onGroupUpdated }) => { const { t } = useTranslation(); @@ -50,9 +51,56 @@ const UserGroupManagement = ({ visible, onClose, onGroupUpdated }) => { const [groups, setGroups] = useState([]); const [showEdit, setShowEdit] = useState(false); const [editingGroup, setEditingGroup] = useState({ id: undefined }); + const { finalConfig, loading: sidebarLoading } = useSidebar(); + + // 检查用户权限 + const getUserRole = () => { + const user = JSON.parse(localStorage.getItem('user') || '{}'); + return user?.role || 0; + }; + + const isRoot = () => getUserRole() >= 100; + const isAdmin = () => getUserRole() >= 10; + + // 检查是否有分组管理权限 + const hasGroupManagementPermission = () => { + // 如果侧边栏配置还在加载中,暂时拒绝访问 + if (sidebarLoading) { + return false; + } + + // 超级管理员始终有权限 + if (isRoot()) { + return true; + } + + // 管理员需要检查权限配置 + if (isAdmin()) { + // 从useSidebar钩子获取最终的权限配置 + const userSection = finalConfig?.admin?.user; + + // 检查用户管理模块是否启用 + if (!userSection || userSection.enabled === false) { + return false; + } + + // 检查分组管理子功能是否启用 + return userSection.groupManagement === true; + } + + // 普通用户无权访问 + return false; + }; // 加载分组列表 const loadGroups = async () => { + // 检查权限 + if (!hasGroupManagementPermission()) { + showError(t('无权访问分组管理功能')); + onClose(); + return; + } + setLoading(true); try { const res = await API.get('/api/user_group'); @@ -60,15 +108,29 @@ const UserGroupManagement = ({ visible, onClose, onGroupUpdated }) => { setGroups(res.data.data || []); } else { showError(res.data.message || t('获取分组列表失败')); + // 如果是权限错误,关闭模态框 + if (res.status === 403) { + onClose(); + } } } catch (error) { showError(t('获取分组列表失败')); + // 如果是权限错误,关闭模态框 + if (error.response?.status === 403) { + onClose(); + } } setLoading(false); }; // 删除分组 const deleteGroup = async (id) => { + // 检查权限 + if (!hasGroupManagementPermission()) { + showError(t('无权访问分组管理功能')); + return; + } + try { const res = await API.delete(`/api/user_group/${id}`); if (res.data.success) { @@ -78,12 +140,23 @@ const UserGroupManagement = ({ visible, onClose, onGroupUpdated }) => { showError(res.data.message || t('删除失败')); } } catch (error) { - showError(t('删除失败')); + if (error.response?.status === 403) { + showError(t('无权访问分组管理功能')); + onClose(); + } else { + showError(t('删除失败')); + } } }; // 编辑分组 const handleEdit = (group = {}) => { + // 检查权限 + if (!hasGroupManagementPermission()) { + showError(t('无权访问分组管理功能')); + return; + } + setEditingGroup(group); setShowEdit(true); }; diff --git a/web/src/helpers/data.js b/web/src/helpers/data.js index b894a953c318..6404e9e8dbd0 100644 --- a/web/src/helpers/data.js +++ b/web/src/helpers/data.js @@ -21,6 +21,13 @@ export function setStatusData(data) { localStorage.setItem('status', JSON.stringify(data)); localStorage.setItem('system_name', data.system_name); localStorage.setItem('logo', data.logo); + + // 触发自定义事件来立即更新title + if (data.system_name) { + window.dispatchEvent(new CustomEvent('systemNameUpdated', { + detail: { systemName: data.system_name } + })); + } localStorage.setItem('footer_html', data.footer_html); localStorage.setItem('quota_per_unit', data.quota_per_unit); localStorage.setItem('display_in_currency', data.display_in_currency); diff --git a/web/src/hooks/common/useHeaderBar.js b/web/src/hooks/common/useHeaderBar.js index 3458a1d163da..375e57dd355d 100644 --- a/web/src/hooks/common/useHeaderBar.js +++ b/web/src/hooks/common/useHeaderBar.js @@ -52,7 +52,7 @@ export const useHeaderBar = ({ onMobileMenuToggle, drawerOpen }) => { const isDemoSiteMode = statusState?.status?.demo_site_enabled || false; // 获取顶栏模块配置 - const headerNavModulesConfig = statusState?.status?.HeaderNavModules; + const headerNavModulesConfig = statusState?.status?.header_nav_modules; // 使用useMemo确保headerNavModules正确响应statusState变化 const headerNavModules = useMemo(() => { diff --git a/web/src/hooks/common/useSidebar.js b/web/src/hooks/common/useSidebar.js index 13d76fd86281..8c8fd7f5f502 100644 --- a/web/src/hooks/common/useSidebar.js +++ b/web/src/hooks/common/useSidebar.js @@ -17,21 +17,22 @@ along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ -import { useState, useEffect, useMemo, useContext } from 'react'; -import { StatusContext } from '../../context/Status'; +import { useState, useEffect } from 'react'; import { API } from '../../helpers'; // 创建一个全局事件系统来同步所有useSidebar实例 -const sidebarEventTarget = new EventTarget(); +if (!window.sidebarEventTarget) { + window.sidebarEventTarget = new EventTarget(); +} +const sidebarEventTarget = window.sidebarEventTarget; const SIDEBAR_REFRESH_EVENT = 'sidebar-refresh'; export const useSidebar = () => { - const [statusState] = useContext(StatusContext); - const [userConfig, setUserConfig] = useState(null); + const [sidebarConfig, setSidebarConfig] = useState(null); const [loading, setLoading] = useState(true); // 默认配置 - const defaultAdminConfig = { + const defaultSidebarConfig = { chat: { enabled: true, playground: true, @@ -55,101 +56,49 @@ export const useSidebar = () => { channel: true, models: true, redemption: true, - user: true, - setting: true, - }, - }; - - // 获取管理员配置 - const adminConfig = useMemo(() => { - if (statusState?.status?.SidebarModulesAdmin) { - try { - const config = JSON.parse(statusState.status.SidebarModulesAdmin); - return config; - } catch (error) { - return defaultAdminConfig; - } + user: { + enabled: true, + groupManagement: true // 默认启用分组管理 + }, + setting: true } - return defaultAdminConfig; - }, [statusState?.status?.SidebarModulesAdmin]); + }; - // 加载用户配置的通用方法 - const loadUserConfig = async () => { + // 加载侧边栏配置的方法 + const loadSidebarConfig = async () => { try { setLoading(true); const res = await API.get('/api/user/self'); - if (res.data.success && res.data.data.sidebar_modules) { - let config; - // 检查sidebar_modules是字符串还是对象 - if (typeof res.data.data.sidebar_modules === 'string') { - config = JSON.parse(res.data.data.sidebar_modules); - } else { - config = res.data.data.sidebar_modules; - } - setUserConfig(config); + if (res.data.success && res.data.data.sidebar_config) { + setSidebarConfig(res.data.data.sidebar_config); } else { - // 当用户没有配置时,生成一个基于管理员配置的默认用户配置 - // 这样可以确保权限控制正确生效 - const defaultUserConfig = {}; - Object.keys(adminConfig).forEach((sectionKey) => { - if (adminConfig[sectionKey]?.enabled) { - defaultUserConfig[sectionKey] = { enabled: true }; - // 为每个管理员允许的模块设置默认值为true - Object.keys(adminConfig[sectionKey]).forEach((moduleKey) => { - if ( - moduleKey !== 'enabled' && - adminConfig[sectionKey][moduleKey] - ) { - defaultUserConfig[sectionKey][moduleKey] = true; - } - }); - } - }); - setUserConfig(defaultUserConfig); + // 使用默认配置 + setSidebarConfig(defaultSidebarConfig); } } catch (error) { - // 出错时也生成默认配置,而不是设置为空对象 - const defaultUserConfig = {}; - Object.keys(adminConfig).forEach((sectionKey) => { - if (adminConfig[sectionKey]?.enabled) { - defaultUserConfig[sectionKey] = { enabled: true }; - Object.keys(adminConfig[sectionKey]).forEach((moduleKey) => { - if (moduleKey !== 'enabled' && adminConfig[sectionKey][moduleKey]) { - defaultUserConfig[sectionKey][moduleKey] = true; - } - }); - } - }); - setUserConfig(defaultUserConfig); + // 出错时使用默认配置 + setSidebarConfig(defaultSidebarConfig); } finally { setLoading(false); } }; - // 刷新用户配置的方法(供外部调用) + // 刷新侧边栏配置的方法(供外部调用) const refreshUserConfig = async () => { - if (Object.keys(adminConfig).length > 0) { - await loadUserConfig(); - } - + await loadSidebarConfig(); // 触发全局刷新事件,通知所有useSidebar实例更新 sidebarEventTarget.dispatchEvent(new CustomEvent(SIDEBAR_REFRESH_EVENT)); }; - // 加载用户配置 + // 初始加载配置 useEffect(() => { - // 只有当管理员配置加载完成后才加载用户配置 - if (Object.keys(adminConfig).length > 0) { - loadUserConfig(); - } - }, [adminConfig]); + loadSidebarConfig(); + }, []); // 监听全局刷新事件 useEffect(() => { const handleRefresh = () => { - if (Object.keys(adminConfig).length > 0) { - loadUserConfig(); - } + loadSidebarConfig(); }; sidebarEventTarget.addEventListener(SIDEBAR_REFRESH_EVENT, handleRefresh); @@ -157,60 +106,23 @@ export const useSidebar = () => { return () => { sidebarEventTarget.removeEventListener(SIDEBAR_REFRESH_EVENT, handleRefresh); }; - }, [adminConfig]); - - // 计算最终的显示配置 - const finalConfig = useMemo(() => { - const result = {}; + }, []); - // 确保adminConfig已加载 - if (!adminConfig || Object.keys(adminConfig).length === 0) { - return result; - } - - // 如果userConfig未加载,等待加载完成 - if (!userConfig) { - return result; - } - - // 遍历所有区域 - Object.keys(adminConfig).forEach((sectionKey) => { - const adminSection = adminConfig[sectionKey]; - const userSection = userConfig[sectionKey]; - - // 如果管理员禁用了整个区域,则该区域不显示 - if (!adminSection?.enabled) { - result[sectionKey] = { enabled: false }; - return; - } - - // 区域级别:用户可以选择隐藏管理员允许的区域 - // 当userSection存在时检查enabled状态,否则默认为true - const sectionEnabled = userSection ? userSection.enabled !== false : true; - result[sectionKey] = { enabled: sectionEnabled }; - - // 功能级别:只有管理员和用户都允许的功能才显示 - Object.keys(adminSection).forEach((moduleKey) => { - if (moduleKey === 'enabled') return; - - const adminAllowed = adminSection[moduleKey]; - // 当userSection存在时检查模块状态,否则默认为true - const userAllowed = userSection - ? userSection[moduleKey] !== false - : true; - - result[sectionKey][moduleKey] = - adminAllowed && userAllowed && sectionEnabled; - }); - }); - - return result; - }, [adminConfig, userConfig]); + // 直接使用后端计算好的最终配置 + const finalConfig = sidebarConfig || {}; // 检查特定功能是否应该显示 const isModuleVisible = (sectionKey, moduleKey = null) => { if (moduleKey) { - return finalConfig[sectionKey]?.[moduleKey] === true; + const moduleValue = finalConfig[sectionKey]?.[moduleKey]; + // 处理布尔值和嵌套对象两种情况 + if (typeof moduleValue === 'boolean') { + return moduleValue === true; + } else if (typeof moduleValue === 'object' && moduleValue !== null) { + // 对于嵌套对象,检查其enabled状态 + return moduleValue.enabled === true; + } + return false; } else { return finalConfig[sectionKey]?.enabled === true; } @@ -221,9 +133,19 @@ export const useSidebar = () => { const section = finalConfig[sectionKey]; if (!section?.enabled) return false; - return Object.keys(section).some( - (key) => key !== 'enabled' && section[key] === true, - ); + return Object.keys(section).some(key => { + if (key === 'enabled') return false; + + const moduleValue = section[key]; + // 处理布尔值和嵌套对象两种情况 + if (typeof moduleValue === 'boolean') { + return moduleValue === true; + } else if (typeof moduleValue === 'object' && moduleValue !== null) { + // 对于嵌套对象,检查其enabled状态 + return moduleValue.enabled === true; + } + return false; + }); }; // 获取区域的可见功能列表 @@ -231,15 +153,24 @@ export const useSidebar = () => { const section = finalConfig[sectionKey]; if (!section?.enabled) return []; - return Object.keys(section).filter( - (key) => key !== 'enabled' && section[key] === true, - ); + return Object.keys(section).filter(key => { + if (key === 'enabled') return false; + + const moduleValue = section[key]; + // 处理布尔值和嵌套对象两种情况 + if (typeof moduleValue === 'boolean') { + return moduleValue === true; + } else if (typeof moduleValue === 'object' && moduleValue !== null) { + // 对于嵌套对象,检查其enabled状态 + return moduleValue.enabled === true; + } + return false; + }); }; return { loading, - adminConfig, - userConfig, + sidebarConfig, finalConfig, isModuleVisible, hasSectionVisibleModules, diff --git a/web/src/pages/Home/index.jsx b/web/src/pages/Home/index.jsx index ffe2cf1abaad..c556ec27832e 100644 --- a/web/src/pages/Home/index.jsx +++ b/web/src/pages/Home/index.jsx @@ -149,20 +149,20 @@ const Home = () => { }, [endpointItems.length]); return ( -
+
setNoticeVisible(false)} isMobile={isMobile} /> {homePageContentLoaded && homePageContent === '' ? ( -
+
{/* Banner 部分 */}
{/* 背景模糊晕染球 */}
-
+
{/* 居中内容区 */}
@@ -343,15 +343,15 @@ const Home = () => {
) : ( -
+
{homePageContent.startsWith('https://') ? (