-
Notifications
You must be signed in to change notification settings - Fork 11.1k
新增"顶栏"、"侧边栏"管理功能 #1701
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
新增"顶栏"、"侧边栏"管理功能 #1701
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
|
|
@@ -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) | ||
|
|
@@ -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 { | ||
|
|
@@ -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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
+ }
🤖 Prompt for AI Agents |
||
|
|
||
| // 原有的用户信息更新逻辑 | ||
| 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 :) | ||
| } | ||
|
|
@@ -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) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
|
|
@@ -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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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))
+ }
+ }
🤖 Prompt for AI Agents |
||
|
|
||
| if common.QuotaForNewUser > 0 { | ||
| RecordLog(user.Id, LogTypeSystem, fmt.Sprintf("新用户注册赠送 %s", logger.LogQuota(common.QuotaForNewUser))) | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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:
Then, after data is constructed:
🤖 Prompt for AI Agents