Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions controller/misc.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,10 @@ func GetStatus(c *gin.Context) {
"announcements_enabled": cs.AnnouncementsEnabled,
"faq_enabled": cs.FAQEnabled,

// 模块管理配置
"HeaderNavModules": common.OptionMap["HeaderNavModules"],
"SidebarModulesAdmin": common.OptionMap["SidebarModulesAdmin"],

Comment on lines +92 to +95

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Data race on common.OptionMap reads in GetStatus.

OptionMap is a map guarded elsewhere with OptionMapRWMutex; reading it here without the lock can race with writers.

Apply:

-        // 模块管理配置
-        "HeaderNavModules":     common.OptionMap["HeaderNavModules"],
-        "SidebarModulesAdmin":  common.OptionMap["SidebarModulesAdmin"],
+        // 模块管理配置(下方加锁后设置)

Then, after data is constructed:

   }
 
+  // 模块管理配置(加读锁)
+  common.OptionMapRWMutex.RLock()
+  data["HeaderNavModules"] = common.OptionMap["HeaderNavModules"]
+  data["SidebarModulesAdmin"] = common.OptionMap["SidebarModulesAdmin"]
+  common.OptionMapRWMutex.RUnlock()
+
   // 根据启用状态注入可选内容
🤖 Prompt for AI Agents
In controller/misc.go around lines 92-95, reads of common.OptionMap
(HeaderNavModules and SidebarModulesAdmin) are unprotected and can race with
writers; wrap the reads with common.OptionMapRWMutex.RLock() before accessing
OptionMap and RUnlock() afterwards, either by copying the needed values into
local variables under the read-lock or by populating the response map entries
while the lock is held, then release the lock once those values have been
captured/inserted.

"oidc_enabled": system_setting.GetOIDCSettings().Enabled,
"oidc_client_id": system_setting.GetOIDCSettings().ClientId,
"oidc_authorization_endpoint": system_setting.GetOIDCSettings().AuthorizationEndpoint,
Expand Down
183 changes: 181 additions & 2 deletions controller/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,7 @@ func Register(c *gin.Context) {
Password: user.Password,
DisplayName: user.Username,
InviterId: inviterId,
Role: common.RoleCommonUser, // 明确设置角色为普通用户
}
if common.EmailVerificationEnabled {
cleanUser.Email = user.Email
Expand Down Expand Up @@ -426,6 +427,7 @@ func GetAffCode(c *gin.Context) {

func GetSelf(c *gin.Context) {
id := c.GetInt("id")
userRole := c.GetInt("role")
user, err := model.GetUserById(id, false)
if err != nil {
common.ApiError(c, err)
Expand All @@ -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
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{
"success": true,
"message": "",
"data": user,
"data": responseData,
})
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) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
Expand Down Expand Up @@ -528,15 +652,69 @@ func UpdateUser(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
}
Comment on lines +655 to +697

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

UpdateSelf lacks server-side authorization and input validation for sidebar_modules

Anyone can POST sidebar_modules regardless of role; also raw strings are accepted without JSON validation. Enforce RBAC and validate JSON; accept object input too.

 func UpdateSelf(c *gin.Context) {
   var requestData map[string]interface{}
   err := json.NewDecoder(c.Request.Body).Decode(&requestData)
@@
   // 检查是否是sidebar_modules更新请求
   if sidebarModules, exists := requestData["sidebar_modules"]; exists {
-    userId := c.GetInt("id")
+    userId := c.GetInt("id")
+    userRole := c.GetInt("role")
+    // 基于后端权限控制,避免绕过前端限制
+    perms := calculateUserPermissions(userRole)
+    if allow, ok := perms["sidebar_settings"].(bool); !ok || !allow {
+      c.JSON(http.StatusOK, gin.H{
+        "success": false,
+        "message": "无权更新边栏设置",
+      })
+      return
+    }
     user, err := model.GetUserById(userId, false)
@@
-    // 更新sidebar_modules字段
-    if sidebarModulesStr, ok := sidebarModules.(string); ok {
-      currentSetting.SidebarModules = sidebarModulesStr
-    }
+    // 更新sidebar_modules字段(支持字符串或对象)
+    switch v := sidebarModules.(type) {
+    case string:
+      // 验证JSON格式
+      var tmp map[string]interface{}
+      if err := json.Unmarshal([]byte(v), &tmp); err != nil {
+        c.JSON(http.StatusOK, gin.H{"success": false, "message": "无效的边栏配置"})
+        return
+      }
+      currentSetting.SidebarModules = v
+    case map[string]interface{}:
+      // 统一存为字符串
+      b, _ := json.Marshal(v)
+      currentSetting.SidebarModules = string(b)
+    default:
+      c.JSON(http.StatusOK, gin.H{"success": false, "message": "无效的边栏配置类型"})
+      return
+    }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In controller/user.go around lines 655-697, enforce server-side RBAC and
stricter JSON validation for sidebar_modules: before loading the target user,
verify the requester is authorized (e.g., requester ID equals target ID OR
requester has an admin role/permission via c.GetInt("id") vs target id and
c.GetString("role") or model permission check) and return 403 if not allowed;
then accept sidebar_modules as either a JSON string or an object/array — if it's
a string attempt to json.Unmarshal to a concrete structure (e.g., []string or
map[string]bool) to validate contents, if it's an object/array validate its
shape and items, sanitize/whitelist allowed module keys, enforce size limits,
then json.Marshal the validated structure to store as the setting string; on any
validation error return 400 with an explanatory message and do not update,
otherwise set the new setting and persist as before.


// 原有的用户信息更新逻辑
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 {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "无效的参数",
})
return
}

if user.Password == "" {
user.Password = "$I_LOVE_U" // make Validator happy :)
}
Expand Down Expand Up @@ -679,6 +857,7 @@ func CreateUser(c *gin.Context) {
Username: user.Username,
Password: user.Password,
DisplayName: user.DisplayName,
Role: user.Role, // 保持管理员设置的角色
}
if err := cleanUser.Insert(0); err != nil {
common.ApiError(c, err)
Expand Down
1 change: 1 addition & 0 deletions dto/user_settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ type UserSetting struct {
NotificationEmail string `json:"notification_email,omitempty"` // NotificationEmail 通知邮箱地址
AcceptUnsetRatioModel bool `json:"accept_unset_model_ratio_model,omitempty"` // AcceptUnsetRatioModel 是否接受未设置价格的模型
RecordIpLog bool `json:"record_ip_log,omitempty"` // 是否记录请求和错误日志IP
SidebarModules string `json:"sidebar_modules,omitempty"` // SidebarModules 左侧边栏模块配置
}

var (
Expand Down
86 changes: 86 additions & 0 deletions model/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,68 @@ func (user *User) SetSetting(setting dto.UserSetting) {
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
func CheckUserExistOrDeleted(username string, email string) (bool, error) {
var user User
Expand Down Expand Up @@ -320,10 +382,34 @@ func (user *User) Insert(inviterId int) error {
user.Quota = common.QuotaForNewUser
//user.SetAccessToken(common.GetUUID())
user.AffCode = common.GetRandomString(4)

// 初始化用户设置,包括默认的边栏配置
if user.Setting == "" {
defaultSetting := dto.UserSetting{}
// 这里暂时不设置SidebarModules,因为需要在用户创建后根据角色设置
user.SetSetting(defaultSetting)
}

result := DB.Create(user)
if result.Error != nil {
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))
}
}
Comment on lines +398 to +411

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Avoid re-query by username; update by ID and handle errors

Re-fetching by username is unnecessary and error-prone. Also, errors from Update are ignored.

-  // 用户创建成功后,根据角色初始化边栏配置
-  // 需要重新获取用户以确保有正确的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))
-    }
-  }
+  // 用户创建成功后,根据角色初始化边栏配置(使用已获取的 user.Id / user.Role)
+  if cfg := generateDefaultSidebarConfigForRole(user.Role); cfg != nil {
+    current := user.GetSetting()
+    current.SidebarModules = cfg // json.RawMessage if adopted; else keep string
+    user.SetSetting(current)
+    if err := DB.Model(&User{}).Where("id = ?", user.Id).Update("setting", user.Setting).Error; err != nil {
+      common.SysLog("初始化边栏配置失败: " + err.Error())
+    } else {
+      common.SysLog(fmt.Sprintf("为新用户 %s (角色: %d) 初始化边栏配置", user.Username, user.Role))
+    }
+  }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In model/user.go around lines 398 to 411, remove the re-query by username and
instead load/update the record by the new user's ID (e.g.,
DB.First(&createdUser, user.ID) or use the created user instance that has the
ID), set the role-based SidebarModules on that record, call Update and check its
returned error (handle/log/return it instead of ignoring), and ensure DB
lookup/update use the primary key to avoid ambiguity and race conditions.


if common.QuotaForNewUser > 0 {
RecordLog(user.Id, LogTypeSystem, fmt.Sprintf("新用户注册赠送 %s", logger.LogQuota(common.QuotaForNewUser)))
}
Expand Down
Loading