Commit d2dcd8be by creamlike1024

Merge branch 'alpha' of github.com:x-Ai/new-api into x-Ai-alpha

parents ed71c9fc d0d6168e
...@@ -89,6 +89,10 @@ func GetStatus(c *gin.Context) { ...@@ -89,6 +89,10 @@ func GetStatus(c *gin.Context) {
"announcements_enabled": cs.AnnouncementsEnabled, "announcements_enabled": cs.AnnouncementsEnabled,
"faq_enabled": cs.FAQEnabled, "faq_enabled": cs.FAQEnabled,
// 模块管理配置
"HeaderNavModules": common.OptionMap["HeaderNavModules"],
"SidebarModulesAdmin": common.OptionMap["SidebarModulesAdmin"],
"oidc_enabled": system_setting.GetOIDCSettings().Enabled, "oidc_enabled": system_setting.GetOIDCSettings().Enabled,
"oidc_client_id": system_setting.GetOIDCSettings().ClientId, "oidc_client_id": system_setting.GetOIDCSettings().ClientId,
"oidc_authorization_endpoint": system_setting.GetOIDCSettings().AuthorizationEndpoint, "oidc_authorization_endpoint": system_setting.GetOIDCSettings().AuthorizationEndpoint,
......
...@@ -210,6 +210,7 @@ func Register(c *gin.Context) { ...@@ -210,6 +210,7 @@ func Register(c *gin.Context) {
Password: user.Password, Password: user.Password,
DisplayName: user.Username, DisplayName: user.Username,
InviterId: inviterId, InviterId: inviterId,
Role: common.RoleCommonUser, // 明确设置角色为普通用户
} }
if common.EmailVerificationEnabled { if common.EmailVerificationEnabled {
cleanUser.Email = user.Email cleanUser.Email = user.Email
...@@ -426,6 +427,7 @@ func GetAffCode(c *gin.Context) { ...@@ -426,6 +427,7 @@ func GetAffCode(c *gin.Context) {
func GetSelf(c *gin.Context) { func GetSelf(c *gin.Context) {
id := c.GetInt("id") id := c.GetInt("id")
userRole := c.GetInt("role")
user, err := model.GetUserById(id, false) user, err := model.GetUserById(id, false)
if err != nil { if err != nil {
common.ApiError(c, err) common.ApiError(c, err)
...@@ -434,14 +436,136 @@ func GetSelf(c *gin.Context) { ...@@ -434,14 +436,136 @@ func GetSelf(c *gin.Context) {
// Hide admin remarks: set to empty to trigger omitempty tag, ensuring the remark field is not included in JSON returned to regular users // Hide admin remarks: set to empty to trigger omitempty tag, ensuring the remark field is not included in JSON returned to regular users
user.Remark = "" user.Remark = ""
// 计算用户权限信息
permissions := calculateUserPermissions(userRole)
// 获取用户设置并提取sidebar_modules
userSetting := user.GetSetting()
// 构建响应数据,包含用户信息和权限
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,
"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, // 新增权限字段
}
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"success": true, "success": true,
"message": "", "message": "",
"data": user, "data": responseData,
}) })
return return
} }
// 计算用户权限的辅助函数
func calculateUserPermissions(userRole int) map[string]interface{} {
permissions := map[string]interface{}{}
// 根据用户角色计算权限
if userRole == common.RoleRootUser {
// 超级管理员不需要边栏设置功能
permissions["sidebar_settings"] = false
permissions["sidebar_modules"] = map[string]interface{}{}
} else if userRole == common.RoleAdminUser {
// 管理员可以设置边栏,但不包含系统设置功能
permissions["sidebar_settings"] = true
permissions["sidebar_modules"] = map[string]interface{}{
"admin": map[string]interface{}{
"setting": false, // 管理员不能访问系统设置
},
}
} else {
// 普通用户只能设置个人功能,不包含管理员区域
permissions["sidebar_settings"] = true
permissions["sidebar_modules"] = map[string]interface{}{
"admin": false, // 普通用户不能访问管理员区域
}
}
return permissions
}
// 根据用户角色生成默认的边栏配置
func generateDefaultSidebarConfig(userRole int) string {
defaultConfig := map[string]interface{}{}
// 聊天区域 - 所有用户都可以访问
defaultConfig["chat"] = map[string]interface{}{
"enabled": true,
"playground": true,
"chat": true,
}
// 控制台区域 - 所有用户都可以访问
defaultConfig["console"] = map[string]interface{}{
"enabled": true,
"detail": true,
"token": true,
"log": true,
"midjourney": true,
"task": true,
}
// 个人中心区域 - 所有用户都可以访问
defaultConfig["personal"] = map[string]interface{}{
"enabled": true,
"topup": true,
"personal": true,
}
// 管理员区域 - 根据角色决定
if userRole == common.RoleAdminUser {
// 管理员可以访问管理员区域,但不能访问系统设置
defaultConfig["admin"] = map[string]interface{}{
"enabled": true,
"channel": true,
"models": true,
"redemption": true,
"user": true,
"setting": false, // 管理员不能访问系统设置
}
} else if userRole == common.RoleRootUser {
// 超级管理员可以访问所有功能
defaultConfig["admin"] = map[string]interface{}{
"enabled": true,
"channel": true,
"models": true,
"redemption": true,
"user": true,
"setting": true,
}
}
// 普通用户不包含admin区域
// 转换为JSON字符串
configBytes, err := json.Marshal(defaultConfig)
if err != nil {
common.SysLog("生成默认边栏配置失败: " + err.Error())
return ""
}
return string(configBytes)
}
func GetUserModels(c *gin.Context) { func GetUserModels(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id")) id, err := strconv.Atoi(c.Param("id"))
if err != nil { if err != nil {
...@@ -528,8 +652,61 @@ func UpdateUser(c *gin.Context) { ...@@ -528,8 +652,61 @@ func UpdateUser(c *gin.Context) {
} }
func UpdateSelf(c *gin.Context) { func UpdateSelf(c *gin.Context) {
var requestData map[string]interface{}
err := json.NewDecoder(c.Request.Body).Decode(&requestData)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "无效的参数",
})
return
}
// 检查是否是sidebar_modules更新请求
if sidebarModules, exists := requestData["sidebar_modules"]; exists {
userId := c.GetInt("id")
user, err := model.GetUserById(userId, false)
if err != nil {
common.ApiError(c, err)
return
}
// 获取当前用户设置
currentSetting := user.GetSetting()
// 更新sidebar_modules字段
if sidebarModulesStr, ok := sidebarModules.(string); ok {
currentSetting.SidebarModules = sidebarModulesStr
}
// 保存更新后的设置
user.SetSetting(currentSetting)
if err := user.Update(false); err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "更新设置失败: " + err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "设置更新成功",
})
return
}
// 原有的用户信息更新逻辑
var user model.User var user model.User
err := json.NewDecoder(c.Request.Body).Decode(&user) requestDataBytes, err := json.Marshal(requestData)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "无效的参数",
})
return
}
err = json.Unmarshal(requestDataBytes, &user)
if err != nil { if err != nil {
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"success": false, "success": false,
...@@ -537,6 +714,7 @@ func UpdateSelf(c *gin.Context) { ...@@ -537,6 +714,7 @@ func UpdateSelf(c *gin.Context) {
}) })
return return
} }
if user.Password == "" { if user.Password == "" {
user.Password = "$I_LOVE_U" // make Validator happy :) user.Password = "$I_LOVE_U" // make Validator happy :)
} }
...@@ -679,6 +857,7 @@ func CreateUser(c *gin.Context) { ...@@ -679,6 +857,7 @@ func CreateUser(c *gin.Context) {
Username: user.Username, Username: user.Username,
Password: user.Password, Password: user.Password,
DisplayName: user.DisplayName, DisplayName: user.DisplayName,
Role: user.Role, // 保持管理员设置的角色
} }
if err := cleanUser.Insert(0); err != nil { if err := cleanUser.Insert(0); err != nil {
common.ApiError(c, err) common.ApiError(c, err)
......
...@@ -8,6 +8,7 @@ type UserSetting struct { ...@@ -8,6 +8,7 @@ type UserSetting struct {
NotificationEmail string `json:"notification_email,omitempty"` // NotificationEmail 通知邮箱地址 NotificationEmail string `json:"notification_email,omitempty"` // NotificationEmail 通知邮箱地址
AcceptUnsetRatioModel bool `json:"accept_unset_model_ratio_model,omitempty"` // AcceptUnsetRatioModel 是否接受未设置价格的模型 AcceptUnsetRatioModel bool `json:"accept_unset_model_ratio_model,omitempty"` // AcceptUnsetRatioModel 是否接受未设置价格的模型
RecordIpLog bool `json:"record_ip_log,omitempty"` // 是否记录请求和错误日志IP RecordIpLog bool `json:"record_ip_log,omitempty"` // 是否记录请求和错误日志IP
SidebarModules string `json:"sidebar_modules,omitempty"` // SidebarModules 左侧边栏模块配置
} }
var ( var (
......
...@@ -91,6 +91,68 @@ func (user *User) SetSetting(setting dto.UserSetting) { ...@@ -91,6 +91,68 @@ func (user *User) SetSetting(setting dto.UserSetting) {
user.Setting = string(settingBytes) user.Setting = string(settingBytes)
} }
// 根据用户角色生成默认的边栏配置
func generateDefaultSidebarConfigForRole(userRole int) string {
defaultConfig := map[string]interface{}{}
// 聊天区域 - 所有用户都可以访问
defaultConfig["chat"] = map[string]interface{}{
"enabled": true,
"playground": true,
"chat": true,
}
// 控制台区域 - 所有用户都可以访问
defaultConfig["console"] = map[string]interface{}{
"enabled": true,
"detail": true,
"token": true,
"log": true,
"midjourney": true,
"task": true,
}
// 个人中心区域 - 所有用户都可以访问
defaultConfig["personal"] = map[string]interface{}{
"enabled": true,
"topup": true,
"personal": true,
}
// 管理员区域 - 根据角色决定
if userRole == common.RoleAdminUser {
// 管理员可以访问管理员区域,但不能访问系统设置
defaultConfig["admin"] = map[string]interface{}{
"enabled": true,
"channel": true,
"models": true,
"redemption": true,
"user": true,
"setting": false, // 管理员不能访问系统设置
}
} else if userRole == common.RoleRootUser {
// 超级管理员可以访问所有功能
defaultConfig["admin"] = map[string]interface{}{
"enabled": true,
"channel": true,
"models": true,
"redemption": true,
"user": true,
"setting": true,
}
}
// 普通用户不包含admin区域
// 转换为JSON字符串
configBytes, err := json.Marshal(defaultConfig)
if err != nil {
common.SysLog("生成默认边栏配置失败: " + err.Error())
return ""
}
return string(configBytes)
}
// CheckUserExistOrDeleted check if user exist or deleted, if not exist, return false, nil, if deleted or exist, return true, nil // CheckUserExistOrDeleted check if user exist or deleted, if not exist, return false, nil, if deleted or exist, return true, nil
func CheckUserExistOrDeleted(username string, email string) (bool, error) { func CheckUserExistOrDeleted(username string, email string) (bool, error) {
var user User var user User
...@@ -320,10 +382,34 @@ func (user *User) Insert(inviterId int) error { ...@@ -320,10 +382,34 @@ func (user *User) Insert(inviterId int) error {
user.Quota = common.QuotaForNewUser user.Quota = common.QuotaForNewUser
//user.SetAccessToken(common.GetUUID()) //user.SetAccessToken(common.GetUUID())
user.AffCode = common.GetRandomString(4) user.AffCode = common.GetRandomString(4)
// 初始化用户设置,包括默认的边栏配置
if user.Setting == "" {
defaultSetting := dto.UserSetting{}
// 这里暂时不设置SidebarModules,因为需要在用户创建后根据角色设置
user.SetSetting(defaultSetting)
}
result := DB.Create(user) result := DB.Create(user)
if result.Error != nil { if result.Error != nil {
return result.Error return result.Error
} }
// 用户创建成功后,根据角色初始化边栏配置
// 需要重新获取用户以确保有正确的ID和Role
var createdUser User
if err := DB.Where("username = ?", user.Username).First(&createdUser).Error; err == nil {
// 生成基于角色的默认边栏配置
defaultSidebarConfig := generateDefaultSidebarConfigForRole(createdUser.Role)
if defaultSidebarConfig != "" {
currentSetting := createdUser.GetSetting()
currentSetting.SidebarModules = defaultSidebarConfig
createdUser.SetSetting(currentSetting)
createdUser.Update(false)
common.SysLog(fmt.Sprintf("为新用户 %s (角色: %d) 初始化边栏配置", createdUser.Username, createdUser.Role))
}
}
if common.QuotaForNewUser > 0 { if common.QuotaForNewUser > 0 {
RecordLog(user.Id, LogTypeSystem, fmt.Sprintf("新用户注册赠送 %s", logger.LogQuota(common.QuotaForNewUser))) RecordLog(user.Id, LogTypeSystem, fmt.Sprintf("新用户注册赠送 %s", logger.LogQuota(common.QuotaForNewUser)))
} }
......
...@@ -17,7 +17,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. ...@@ -17,7 +17,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com For commercial licensing, please contact support@quantumnous.com
*/ */
import React, { lazy, Suspense } from 'react'; import React, { lazy, Suspense, useContext, useMemo } from 'react';
import { Route, Routes, useLocation } from 'react-router-dom'; import { Route, Routes, useLocation } from 'react-router-dom';
import Loading from './components/common/ui/Loading'; import Loading from './components/common/ui/Loading';
import User from './pages/User'; import User from './pages/User';
...@@ -27,6 +27,7 @@ import LoginForm from './components/auth/LoginForm'; ...@@ -27,6 +27,7 @@ import LoginForm from './components/auth/LoginForm';
import NotFound from './pages/NotFound'; import NotFound from './pages/NotFound';
import Forbidden from './pages/Forbidden'; import Forbidden from './pages/Forbidden';
import Setting from './pages/Setting'; import Setting from './pages/Setting';
import { StatusContext } from './context/Status';
import PasswordResetForm from './components/auth/PasswordResetForm'; import PasswordResetForm from './components/auth/PasswordResetForm';
import PasswordResetConfirm from './components/auth/PasswordResetConfirm'; import PasswordResetConfirm from './components/auth/PasswordResetConfirm';
...@@ -53,6 +54,29 @@ const About = lazy(() => import('./pages/About')); ...@@ -53,6 +54,29 @@ const About = lazy(() => import('./pages/About'));
function App() { function App() {
const location = useLocation(); const location = useLocation();
const [statusState] = useContext(StatusContext);
// 获取模型广场权限配置
const pricingRequireAuth = useMemo(() => {
const headerNavModulesConfig = statusState?.status?.HeaderNavModules;
if (headerNavModulesConfig) {
try {
const modules = JSON.parse(headerNavModulesConfig);
// 处理向后兼容性:如果pricing是boolean,默认不需要登录
if (typeof modules.pricing === 'boolean') {
return false; // 默认不需要登录鉴权
}
// 如果是对象格式,使用requireAuth配置
return modules.pricing?.requireAuth === true;
} catch (error) {
console.error('解析顶栏模块配置失败:', error);
return false; // 默认不需要登录
}
}
return false; // 默认不需要登录
}, [statusState?.status?.HeaderNavModules]);
return ( return (
<SetupCheck> <SetupCheck>
...@@ -253,9 +277,17 @@ function App() { ...@@ -253,9 +277,17 @@ function App() {
<Route <Route
path='/pricing' path='/pricing'
element={ element={
pricingRequireAuth ? (
<PrivateRoute>
<Suspense fallback={<Loading></Loading>} key={location.pathname}>
<Pricing />
</Suspense>
</PrivateRoute>
) : (
<Suspense fallback={<Loading></Loading>} key={location.pathname}> <Suspense fallback={<Loading></Loading>} key={location.pathname}>
<Pricing /> <Pricing />
</Suspense> </Suspense>
)
} }
/> />
<Route <Route
......
...@@ -21,7 +21,7 @@ import React from 'react'; ...@@ -21,7 +21,7 @@ import React from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import SkeletonWrapper from './SkeletonWrapper'; import SkeletonWrapper from './SkeletonWrapper';
const Navigation = ({ mainNavLinks, isMobile, isLoading, userState }) => { const Navigation = ({ mainNavLinks, isMobile, isLoading, userState, pricingRequireAuth }) => {
const renderNavLinks = () => { const renderNavLinks = () => {
const baseClasses = const baseClasses =
'flex-shrink-0 flex items-center gap-1 font-semibold rounded-md transition-all duration-200 ease-in-out'; 'flex-shrink-0 flex items-center gap-1 font-semibold rounded-md transition-all duration-200 ease-in-out';
...@@ -51,6 +51,9 @@ const Navigation = ({ mainNavLinks, isMobile, isLoading, userState }) => { ...@@ -51,6 +51,9 @@ const Navigation = ({ mainNavLinks, isMobile, isLoading, userState }) => {
if (link.itemKey === 'console' && !userState.user) { if (link.itemKey === 'console' && !userState.user) {
targetPath = '/login'; targetPath = '/login';
} }
if (link.itemKey === 'pricing' && pricingRequireAuth && !userState.user) {
targetPath = '/login';
}
return ( return (
<Link key={link.itemKey} to={targetPath} className={commonLinkClasses}> <Link key={link.itemKey} to={targetPath} className={commonLinkClasses}>
......
...@@ -44,6 +44,8 @@ const HeaderBar = ({ onMobileMenuToggle, drawerOpen }) => { ...@@ -44,6 +44,8 @@ const HeaderBar = ({ onMobileMenuToggle, drawerOpen }) => {
isDemoSiteMode, isDemoSiteMode,
isConsoleRoute, isConsoleRoute,
theme, theme,
headerNavModules,
pricingRequireAuth,
logout, logout,
handleLanguageChange, handleLanguageChange,
handleThemeToggle, handleThemeToggle,
...@@ -60,7 +62,7 @@ const HeaderBar = ({ onMobileMenuToggle, drawerOpen }) => { ...@@ -60,7 +62,7 @@ const HeaderBar = ({ onMobileMenuToggle, drawerOpen }) => {
getUnreadKeys, getUnreadKeys,
} = useNotifications(statusState); } = useNotifications(statusState);
const { mainNavLinks } = useNavigation(t, docsLink); const { mainNavLinks } = useNavigation(t, docsLink, headerNavModules);
return ( return (
<header className='text-semi-color-text-0 sticky top-0 z-50 transition-colors duration-300 bg-white/75 dark:bg-zinc-900/75 backdrop-blur-lg'> <header className='text-semi-color-text-0 sticky top-0 z-50 transition-colors duration-300 bg-white/75 dark:bg-zinc-900/75 backdrop-blur-lg'>
...@@ -102,6 +104,7 @@ const HeaderBar = ({ onMobileMenuToggle, drawerOpen }) => { ...@@ -102,6 +104,7 @@ const HeaderBar = ({ onMobileMenuToggle, drawerOpen }) => {
isMobile={isMobile} isMobile={isMobile}
isLoading={isLoading} isLoading={isLoading}
userState={userState} userState={userState}
pricingRequireAuth={pricingRequireAuth}
/> />
<ActionButtons <ActionButtons
......
...@@ -23,6 +23,7 @@ import { useTranslation } from 'react-i18next'; ...@@ -23,6 +23,7 @@ import { useTranslation } from 'react-i18next';
import { getLucideIcon } from '../../helpers/render'; import { getLucideIcon } from '../../helpers/render';
import { ChevronLeft } from 'lucide-react'; import { ChevronLeft } from 'lucide-react';
import { useSidebarCollapsed } from '../../hooks/common/useSidebarCollapsed'; import { useSidebarCollapsed } from '../../hooks/common/useSidebarCollapsed';
import { useSidebar } from '../../hooks/common/useSidebar';
import { isAdmin, isRoot, showError } from '../../helpers'; import { isAdmin, isRoot, showError } from '../../helpers';
import { Nav, Divider, Button } from '@douyinfe/semi-ui'; import { Nav, Divider, Button } from '@douyinfe/semi-ui';
...@@ -49,6 +50,7 @@ const routerMap = { ...@@ -49,6 +50,7 @@ const routerMap = {
const SiderBar = ({ onNavigate = () => {} }) => { const SiderBar = ({ onNavigate = () => {} }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const [collapsed, toggleCollapsed] = useSidebarCollapsed(); const [collapsed, toggleCollapsed] = useSidebarCollapsed();
const { isModuleVisible, hasSectionVisibleModules, loading: sidebarLoading } = useSidebar();
const [selectedKeys, setSelectedKeys] = useState(['home']); const [selectedKeys, setSelectedKeys] = useState(['home']);
const [chatItems, setChatItems] = useState([]); const [chatItems, setChatItems] = useState([]);
...@@ -57,7 +59,8 @@ const SiderBar = ({ onNavigate = () => {} }) => { ...@@ -57,7 +59,8 @@ const SiderBar = ({ onNavigate = () => {} }) => {
const [routerMapState, setRouterMapState] = useState(routerMap); const [routerMapState, setRouterMapState] = useState(routerMap);
const workspaceItems = useMemo( const workspaceItems = useMemo(
() => [ () => {
const items = [
{ {
text: t('数据看板'), text: t('数据看板'),
itemKey: 'detail', itemKey: 'detail',
...@@ -93,17 +96,28 @@ const SiderBar = ({ onNavigate = () => {} }) => { ...@@ -93,17 +96,28 @@ const SiderBar = ({ onNavigate = () => {} }) => {
className: className:
localStorage.getItem('enable_task') === 'true' ? '' : 'tableHiddle', localStorage.getItem('enable_task') === 'true' ? '' : 'tableHiddle',
}, },
], ];
// 根据配置过滤项目
const filteredItems = items.filter(item => {
const configVisible = isModuleVisible('console', item.itemKey);
return configVisible;
});
return filteredItems;
},
[ [
localStorage.getItem('enable_data_export'), localStorage.getItem('enable_data_export'),
localStorage.getItem('enable_drawing'), localStorage.getItem('enable_drawing'),
localStorage.getItem('enable_task'), localStorage.getItem('enable_task'),
t, t,
isModuleVisible,
], ],
); );
const financeItems = useMemo( const financeItems = useMemo(
() => [ () => {
const items = [
{ {
text: t('钱包管理'), text: t('钱包管理'),
itemKey: 'topup', itemKey: 'topup',
...@@ -114,12 +128,22 @@ const SiderBar = ({ onNavigate = () => {} }) => { ...@@ -114,12 +128,22 @@ const SiderBar = ({ onNavigate = () => {} }) => {
itemKey: 'personal', itemKey: 'personal',
to: '/personal', to: '/personal',
}, },
], ];
[t],
// 根据配置过滤项目
const filteredItems = items.filter(item => {
const configVisible = isModuleVisible('personal', item.itemKey);
return configVisible;
});
return filteredItems;
},
[t, isModuleVisible],
); );
const adminItems = useMemo( const adminItems = useMemo(
() => [ () => {
const items = [
{ {
text: t('渠道管理'), text: t('渠道管理'),
itemKey: 'channel', itemKey: 'channel',
...@@ -150,12 +174,22 @@ const SiderBar = ({ onNavigate = () => {} }) => { ...@@ -150,12 +174,22 @@ const SiderBar = ({ onNavigate = () => {} }) => {
to: '/setting', to: '/setting',
className: isRoot() ? '' : 'tableHiddle', className: isRoot() ? '' : 'tableHiddle',
}, },
], ];
[isAdmin(), isRoot(), t],
// 根据配置过滤项目
const filteredItems = items.filter(item => {
const configVisible = isModuleVisible('admin', item.itemKey);
return configVisible;
});
return filteredItems;
},
[isAdmin(), isRoot(), t, isModuleVisible],
); );
const chatMenuItems = useMemo( const chatMenuItems = useMemo(
() => [ () => {
const items = [
{ {
text: t('操练场'), text: t('操练场'),
itemKey: 'playground', itemKey: 'playground',
...@@ -166,8 +200,17 @@ const SiderBar = ({ onNavigate = () => {} }) => { ...@@ -166,8 +200,17 @@ const SiderBar = ({ onNavigate = () => {} }) => {
itemKey: 'chat', itemKey: 'chat',
items: chatItems, items: chatItems,
}, },
], ];
[chatItems, t],
// 根据配置过滤项目
const filteredItems = items.filter(item => {
const configVisible = isModuleVisible('chat', item.itemKey);
return configVisible;
});
return filteredItems;
},
[chatItems, t, isModuleVisible],
); );
// 更新路由映射,添加聊天路由 // 更新路由映射,添加聊天路由
...@@ -213,7 +256,6 @@ const SiderBar = ({ onNavigate = () => {} }) => { ...@@ -213,7 +256,6 @@ const SiderBar = ({ onNavigate = () => {} }) => {
updateRouterMapWithChats(chats); updateRouterMapWithChats(chats);
} }
} catch (e) { } catch (e) {
console.error(e);
showError('聊天数据解析失败'); showError('聊天数据解析失败');
} }
} }
...@@ -382,12 +424,16 @@ const SiderBar = ({ onNavigate = () => {} }) => { ...@@ -382,12 +424,16 @@ const SiderBar = ({ onNavigate = () => {} }) => {
}} }}
> >
{/* 聊天区域 */} {/* 聊天区域 */}
{hasSectionVisibleModules('chat') && (
<div className='sidebar-section'> <div className='sidebar-section'>
{!collapsed && <div className='sidebar-group-label'>{t('聊天')}</div>} {!collapsed && <div className='sidebar-group-label'>{t('聊天')}</div>}
{chatMenuItems.map((item) => renderSubItem(item))} {chatMenuItems.map((item) => renderSubItem(item))}
</div> </div>
)}
{/* 控制台区域 */} {/* 控制台区域 */}
{hasSectionVisibleModules('console') && (
<>
<Divider className='sidebar-divider' /> <Divider className='sidebar-divider' />
<div> <div>
{!collapsed && ( {!collapsed && (
...@@ -395,8 +441,12 @@ const SiderBar = ({ onNavigate = () => {} }) => { ...@@ -395,8 +441,12 @@ const SiderBar = ({ onNavigate = () => {} }) => {
)} )}
{workspaceItems.map((item) => renderNavItem(item))} {workspaceItems.map((item) => renderNavItem(item))}
</div> </div>
</>
)}
{/* 个人中心区域 */} {/* 个人中心区域 */}
{hasSectionVisibleModules('personal') && (
<>
<Divider className='sidebar-divider' /> <Divider className='sidebar-divider' />
<div> <div>
{!collapsed && ( {!collapsed && (
...@@ -404,9 +454,11 @@ const SiderBar = ({ onNavigate = () => {} }) => { ...@@ -404,9 +454,11 @@ const SiderBar = ({ onNavigate = () => {} }) => {
)} )}
{financeItems.map((item) => renderNavItem(item))} {financeItems.map((item) => renderNavItem(item))}
</div> </div>
</>
)}
{/* 管理员区域 - 只在管理员时显示 */} {/* 管理员区域 - 只在管理员时显示且配置允许时显示 */}
{isAdmin() && ( {isAdmin() && hasSectionVisibleModules('admin') && (
<> <>
<Divider className='sidebar-divider' /> <Divider className='sidebar-divider' />
<div> <div>
......
...@@ -20,6 +20,8 @@ For commercial licensing, please contact support@quantumnous.com ...@@ -20,6 +20,8 @@ For commercial licensing, please contact support@quantumnous.com
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Card, Spin } from '@douyinfe/semi-ui'; import { Card, Spin } from '@douyinfe/semi-ui';
import SettingsGeneral from '../../pages/Setting/Operation/SettingsGeneral'; import SettingsGeneral from '../../pages/Setting/Operation/SettingsGeneral';
import SettingsHeaderNavModules from '../../pages/Setting/Operation/SettingsHeaderNavModules';
import SettingsSidebarModulesAdmin from '../../pages/Setting/Operation/SettingsSidebarModulesAdmin';
import SettingsSensitiveWords from '../../pages/Setting/Operation/SettingsSensitiveWords'; import SettingsSensitiveWords from '../../pages/Setting/Operation/SettingsSensitiveWords';
import SettingsLog from '../../pages/Setting/Operation/SettingsLog'; import SettingsLog from '../../pages/Setting/Operation/SettingsLog';
import SettingsMonitoring from '../../pages/Setting/Operation/SettingsMonitoring'; import SettingsMonitoring from '../../pages/Setting/Operation/SettingsMonitoring';
...@@ -46,6 +48,12 @@ const OperationSetting = () => { ...@@ -46,6 +48,12 @@ const OperationSetting = () => {
DemoSiteEnabled: false, DemoSiteEnabled: false,
SelfUseModeEnabled: false, SelfUseModeEnabled: false,
/* 顶栏模块管理 */
HeaderNavModules: '',
/* 左侧边栏模块管理(管理员) */
SidebarModulesAdmin: '',
/* 敏感词设置 */ /* 敏感词设置 */
CheckSensitiveEnabled: false, CheckSensitiveEnabled: false,
CheckSensitiveOnPromptEnabled: false, CheckSensitiveOnPromptEnabled: false,
...@@ -108,6 +116,14 @@ const OperationSetting = () => { ...@@ -108,6 +116,14 @@ const OperationSetting = () => {
<Card style={{ marginTop: '10px' }}> <Card style={{ marginTop: '10px' }}>
<SettingsGeneral options={inputs} refresh={onRefresh} /> <SettingsGeneral options={inputs} refresh={onRefresh} />
</Card> </Card>
{/* 顶栏模块管理 */}
<div style={{ marginTop: '10px' }}>
<SettingsHeaderNavModules options={inputs} refresh={onRefresh} />
</div>
{/* 左侧边栏模块管理(管理员) */}
<div style={{ marginTop: '10px' }}>
<SettingsSidebarModulesAdmin options={inputs} refresh={onRefresh} />
</div>
{/* 屏蔽词过滤设置 */} {/* 屏蔽词过滤设置 */}
<Card style={{ marginTop: '10px' }}> <Card style={{ marginTop: '10px' }}>
<SettingsSensitiveWords options={inputs} refresh={onRefresh} /> <SettingsSensitiveWords options={inputs} refresh={onRefresh} />
......
...@@ -38,6 +38,8 @@ const PersonalSetting = () => { ...@@ -38,6 +38,8 @@ const PersonalSetting = () => {
let navigate = useNavigate(); let navigate = useNavigate();
const { t } = useTranslation(); const { t } = useTranslation();
const [inputs, setInputs] = useState({ const [inputs, setInputs] = useState({
wechat_verification_code: '', wechat_verification_code: '',
email_verification_code: '', email_verification_code: '',
...@@ -330,6 +332,8 @@ const PersonalSetting = () => { ...@@ -330,6 +332,8 @@ const PersonalSetting = () => {
saveNotificationSettings={saveNotificationSettings} saveNotificationSettings={saveNotificationSettings}
/> />
</div> </div>
</div> </div>
</div> </div>
......
...@@ -17,7 +17,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. ...@@ -17,7 +17,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com For commercial licensing, please contact support@quantumnous.com
*/ */
import { useState, useEffect, useContext, useCallback } from 'react'; import { useState, useEffect, useContext, useCallback, useMemo } from 'react';
import { useNavigate, useLocation } from 'react-router-dom'; import { useNavigate, useLocation } from 'react-router-dom';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { UserContext } from '../../context/User'; import { UserContext } from '../../context/User';
...@@ -51,6 +51,42 @@ export const useHeaderBar = ({ onMobileMenuToggle, drawerOpen }) => { ...@@ -51,6 +51,42 @@ export const useHeaderBar = ({ onMobileMenuToggle, drawerOpen }) => {
const docsLink = statusState?.status?.docs_link || ''; const docsLink = statusState?.status?.docs_link || '';
const isDemoSiteMode = statusState?.status?.demo_site_enabled || false; const isDemoSiteMode = statusState?.status?.demo_site_enabled || false;
// 获取顶栏模块配置
const headerNavModulesConfig = statusState?.status?.HeaderNavModules;
// 使用useMemo确保headerNavModules正确响应statusState变化
const headerNavModules = useMemo(() => {
if (headerNavModulesConfig) {
try {
const modules = JSON.parse(headerNavModulesConfig);
// 处理向后兼容性:如果pricing是boolean,转换为对象格式
if (typeof modules.pricing === 'boolean') {
modules.pricing = {
enabled: modules.pricing,
requireAuth: false // 默认不需要登录鉴权
};
}
return modules;
} catch (error) {
console.error('解析顶栏模块配置失败:', error);
return null;
}
}
return null;
}, [headerNavModulesConfig]);
// 获取模型广场权限配置
const pricingRequireAuth = useMemo(() => {
if (headerNavModules?.pricing) {
return typeof headerNavModules.pricing === 'object'
? headerNavModules.pricing.requireAuth
: false; // 默认不需要登录
}
return false; // 默认不需要登录
}, [headerNavModules]);
const isConsoleRoute = location.pathname.startsWith('/console'); const isConsoleRoute = location.pathname.startsWith('/console');
const theme = useTheme(); const theme = useTheme();
...@@ -156,6 +192,8 @@ export const useHeaderBar = ({ onMobileMenuToggle, drawerOpen }) => { ...@@ -156,6 +192,8 @@ export const useHeaderBar = ({ onMobileMenuToggle, drawerOpen }) => {
isConsoleRoute, isConsoleRoute,
theme, theme,
drawerOpen, drawerOpen,
headerNavModules,
pricingRequireAuth,
// Actions // Actions
logout, logout,
......
...@@ -19,9 +19,22 @@ For commercial licensing, please contact support@quantumnous.com ...@@ -19,9 +19,22 @@ For commercial licensing, please contact support@quantumnous.com
import { useMemo } from 'react'; import { useMemo } from 'react';
export const useNavigation = (t, docsLink) => { export const useNavigation = (t, docsLink, headerNavModules) => {
const mainNavLinks = useMemo( const mainNavLinks = useMemo(
() => [ () => {
// 默认配置,如果没有传入配置则显示所有模块
const defaultModules = {
home: true,
console: true,
pricing: true,
docs: true,
about: true,
};
// 使用传入的配置或默认配置
const modules = headerNavModules || defaultModules;
const allLinks = [
{ {
text: t('首页'), text: t('首页'),
itemKey: 'home', itemKey: 'home',
...@@ -52,8 +65,21 @@ export const useNavigation = (t, docsLink) => { ...@@ -52,8 +65,21 @@ export const useNavigation = (t, docsLink) => {
itemKey: 'about', itemKey: 'about',
to: '/about', to: '/about',
}, },
], ];
[t, docsLink],
// 根据配置过滤导航链接
return allLinks.filter(link => {
if (link.itemKey === 'docs') {
return docsLink && modules.docs;
}
if (link.itemKey === 'pricing') {
// 支持新的pricing配置格式
return typeof modules.pricing === 'object' ? modules.pricing.enabled : modules.pricing;
}
return modules[link.itemKey] === true;
});
},
[t, docsLink, headerNavModules],
); );
return { return {
......
/*
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 <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import { useState, useEffect, useMemo, useContext } from 'react';
import { StatusContext } from '../../context/Status';
import { API } from '../../helpers';
export const useSidebar = () => {
const [statusState] = useContext(StatusContext);
const [userConfig, setUserConfig] = useState(null);
const [loading, setLoading] = useState(true);
// 默认配置
const defaultAdminConfig = {
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
}
};
// 获取管理员配置
const adminConfig = useMemo(() => {
if (statusState?.status?.SidebarModulesAdmin) {
try {
const config = JSON.parse(statusState.status.SidebarModulesAdmin);
return config;
} catch (error) {
return defaultAdminConfig;
}
}
return defaultAdminConfig;
}, [statusState?.status?.SidebarModulesAdmin]);
// 加载用户配置的通用方法
const loadUserConfig = 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);
} 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);
}
} 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);
} finally {
setLoading(false);
}
};
// 刷新用户配置的方法(供外部调用)
const refreshUserConfig = async () => {
if (Object.keys(adminConfig).length > 0) {
await loadUserConfig();
}
};
// 加载用户配置
useEffect(() => {
// 只有当管理员配置加载完成后才加载用户配置
if (Object.keys(adminConfig).length > 0) {
loadUserConfig();
}
}, [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 isModuleVisible = (sectionKey, moduleKey = null) => {
if (moduleKey) {
return finalConfig[sectionKey]?.[moduleKey] === true;
} else {
return finalConfig[sectionKey]?.enabled === true;
}
};
// 检查区域是否有任何可见的功能
const hasSectionVisibleModules = (sectionKey) => {
const section = finalConfig[sectionKey];
if (!section?.enabled) return false;
return Object.keys(section).some(key =>
key !== 'enabled' && section[key] === true
);
};
// 获取区域的可见功能列表
const getVisibleModules = (sectionKey) => {
const section = finalConfig[sectionKey];
if (!section?.enabled) return [];
return Object.keys(section)
.filter(key => key !== 'enabled' && section[key] === true);
};
return {
loading,
adminConfig,
userConfig,
finalConfig,
isModuleVisible,
hasSectionVisibleModules,
getVisibleModules,
refreshUserConfig
};
};
import { useState, useEffect } from 'react';
import { API } from '../../helpers';
/**
* 用户权限钩子 - 从后端获取用户权限,替代前端角色判断
* 确保权限控制的安全性,防止前端绕过
*/
export const useUserPermissions = () => {
const [permissions, setPermissions] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
// 加载用户权限(从用户信息接口获取)
const loadPermissions = async () => {
try {
setLoading(true);
setError(null);
const res = await API.get('/api/user/self');
if (res.data.success) {
const userPermissions = res.data.data.permissions;
setPermissions(userPermissions);
console.log('用户权限加载成功:', userPermissions);
} else {
setError(res.data.message || '获取权限失败');
console.error('获取权限失败:', res.data.message);
}
} catch (error) {
setError('网络错误,请重试');
console.error('加载用户权限异常:', error);
} finally {
setLoading(false);
}
};
useEffect(() => {
loadPermissions();
}, []);
// 检查是否有边栏设置权限
const hasSidebarSettingsPermission = () => {
return permissions?.sidebar_settings === true;
};
// 检查是否允许访问特定的边栏区域
const isSidebarSectionAllowed = (sectionKey) => {
if (!permissions?.sidebar_modules) return true;
const sectionPerms = permissions.sidebar_modules[sectionKey];
return sectionPerms !== false;
};
// 检查是否允许访问特定的边栏模块
const isSidebarModuleAllowed = (sectionKey, moduleKey) => {
if (!permissions?.sidebar_modules) return true;
const sectionPerms = permissions.sidebar_modules[sectionKey];
// 如果整个区域被禁用
if (sectionPerms === false) return false;
// 如果区域存在但模块被禁用
if (sectionPerms && sectionPerms[moduleKey] === false) return false;
return true;
};
// 获取允许的边栏区域列表
const getAllowedSidebarSections = () => {
if (!permissions?.sidebar_modules) return [];
return Object.keys(permissions.sidebar_modules).filter(sectionKey =>
isSidebarSectionAllowed(sectionKey)
);
};
// 获取特定区域允许的模块列表
const getAllowedSidebarModules = (sectionKey) => {
if (!permissions?.sidebar_modules) return [];
const sectionPerms = permissions.sidebar_modules[sectionKey];
if (sectionPerms === false) return [];
if (!sectionPerms || typeof sectionPerms !== 'object') return [];
return Object.keys(sectionPerms).filter(moduleKey =>
moduleKey !== 'enabled' && sectionPerms[moduleKey] === true
);
};
return {
permissions,
loading,
error,
loadPermissions,
hasSidebarSettingsPermission,
isSidebarSectionAllowed,
isSidebarModuleAllowed,
getAllowedSidebarSections,
getAllowedSidebarModules,
};
};
export default useUserPermissions;
...@@ -2017,5 +2017,63 @@ ...@@ -2017,5 +2017,63 @@
"查看密钥": "View key", "查看密钥": "View key",
"查看渠道密钥": "View channel key", "查看渠道密钥": "View channel key",
"渠道密钥信息": "Channel key information", "渠道密钥信息": "Channel key information",
"密钥获取成功": "Key acquisition successful" "密钥获取成功": "Key acquisition successful",
"顶栏管理": "Header Management",
"控制顶栏模块显示状态,全局生效": "Control header module display status, global effect",
"用户主页,展示系统信息": "User homepage, displaying system information",
"用户控制面板,管理账户": "User control panel for account management",
"模型广场": "Model Marketplace",
"模型定价,需要登录访问": "Model pricing, requires login to access",
"文档": "Documentation",
"系统文档和帮助信息": "System documentation and help information",
"关于系统的详细信息": "Detailed information about the system",
"重置为默认": "Reset to Default",
"保存设置": "Save Settings",
"已重置为默认配置": "Reset to default configuration",
"保存成功": "Saved successfully",
"保存失败,请重试": "Save failed, please try again",
"侧边栏管理(全局控制)": "Sidebar Management (Global Control)",
"全局控制侧边栏区域和功能显示,管理员隐藏的功能用户无法启用": "Global control of sidebar areas and functions, users cannot enable functions hidden by administrators",
"聊天区域": "Chat Area",
"操练场和聊天功能": "Playground and chat functions",
"操练场": "Playground",
"AI模型测试环境": "AI model testing environment",
"聊天": "Chat",
"聊天会话管理": "Chat session management",
"控制台区域": "Console Area",
"数据管理和日志查看": "Data management and log viewing",
"数据看板": "Dashboard",
"系统数据统计": "System data statistics",
"令牌管理": "Token Management",
"API令牌管理": "API token management",
"使用日志": "Usage Logs",
"API使用记录": "API usage records",
"绘图日志": "Drawing Logs",
"绘图任务记录": "Drawing task records",
"任务日志": "Task Logs",
"系统任务记录": "System task records",
"个人中心区域": "Personal Center Area",
"用户个人功能": "User personal functions",
"钱包管理": "Wallet Management",
"余额充值管理": "Balance recharge management",
"个人设置": "Personal Settings",
"个人信息设置": "Personal information settings",
"管理员区域": "Administrator Area",
"系统管理功能": "System management functions",
"渠道管理": "Channel Management",
"API渠道配置": "API channel configuration",
"模型管理": "Model Management",
"AI模型配置": "AI model configuration",
"兑换码管理": "Redemption Code Management",
"兑换码生成管理": "Redemption code generation management",
"用户管理": "User Management",
"用户账户管理": "User account management",
"系统设置": "System Settings",
"系统参数配置": "System parameter configuration",
"边栏设置": "Sidebar Settings",
"您可以个性化设置侧边栏的要显示功能": "You can customize the sidebar functions to display",
"保存边栏设置": "Save Sidebar Settings",
"侧边栏设置保存成功": "Sidebar settings saved successfully",
"需要登录访问": "Require Login",
"开启后未登录用户无法访问模型广场": "When enabled, unauthenticated users cannot access the model marketplace"
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment