package controller import ( "errors" "fmt" "net/http" "net/url" "strconv" "strings" "sync" "time" "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/i18n" "github.com/QuantumNous/new-api/logger" "github.com/QuantumNous/new-api/middleware" "github.com/QuantumNous/new-api/model" "github.com/QuantumNous/new-api/relaykit/dto" "github.com/QuantumNous/new-api/service" "github.com/QuantumNous/new-api/service/authz" "github.com/QuantumNous/new-api/setting" "github.com/QuantumNous/new-api/setting/operation_setting" "github.com/QuantumNous/new-api/constant" "github.com/gin-gonic/gin" "gorm.io/gorm" ) type LoginRequest struct { Username string `json:"username"` Password string `json:"password"` } var ( errUserPasswordUnset = errors.New("user password is not set") errOriginalPasswordFail = errors.New("original password is incorrect") ) func Login(c *gin.Context) { if !common.PasswordLoginEnabled { common.ApiErrorI18n(c, i18n.MsgUserPasswordLoginDisabled) return } var loginRequest LoginRequest err := common.DecodeJson(c.Request.Body, &loginRequest) if err != nil { common.ApiErrorI18n(c, i18n.MsgInvalidParams) return } username := loginRequest.Username password := loginRequest.Password if username == "" || password == "" { common.ApiErrorI18n(c, i18n.MsgInvalidParams) return } user := model.User{ Username: username, Password: password, } err = user.ValidateAndFill() if err != nil { switch { case errors.Is(err, model.ErrDatabase): common.SysLog(fmt.Sprintf("Login database error for user %s: %v", username, err)) common.ApiErrorI18n(c, i18n.MsgDatabaseError) case errors.Is(err, model.ErrUserEmptyCredentials): common.ApiErrorI18n(c, i18n.MsgInvalidParams) default: common.ApiErrorI18n(c, i18n.MsgUserUsernameOrPasswordError) } return } // 检查是否启用2FA twoFAEnabled, err := model.IsTwoFAEnabled(user.Id) if err != nil { common.SysLog(fmt.Sprintf("Login failed to load 2FA status for user %d: %v", user.Id, err)) common.ApiErrorI18n(c, i18n.MsgDatabaseError) return } if twoFAEnabled { expiresAt := time.Now().Add(5 * time.Minute) payload, err := common.Marshal(twoFALoginFlowPayload{AuthVersion: user.AuthVersion}) if err != nil { common.ApiError(c, err) return } flowToken, _, err := model.CreateAuthFlow(model.AuthFlowCreate{ Purpose: model.AuthFlowPurposeTwoFALogin, UserId: user.Id, Payload: string(payload), ExpiresAt: expiresAt, }) if err != nil { common.ApiError(c, err) return } c.JSON(http.StatusOK, gin.H{ "message": i18n.T(c, i18n.MsgUserRequire2FA), "success": true, "data": map[string]interface{}{ "require_2fa": true, "flow_token": flowToken, "expires_at": expiresAt.Unix(), }, }) return } setupLogin(&user, c) } // loginMethodFromContext 根据请求路径推导登录方式,用于登录审计日志。 func loginMethodFromContext(c *gin.Context) string { switch c.FullPath() { case "/api/user/login": return "password" case "/api/user/login/2fa": return "2fa" case "/api/user/passkey/login/finish": return "passkey" case "/api/oauth/wechat": return "wechat" case "/api/oauth/telegram/login": return "telegram" case "/api/oauth/:provider": if provider := c.Param("provider"); provider != "" { return "oauth:" + provider } return "oauth" default: return "unknown" } } // recordLoginAudit 记录登录成功审计日志(对所有用户启用,仅记录成功,不记录失败)。 func recordLoginAudit(user *model.User, c *gin.Context) { method := loginMethodFromContext(c) ip := c.ClientIP() extra := map[string]interface{}{ "login_method": method, "user_agent": c.Request.UserAgent(), } content := fmt.Sprintf("Logged in successfully via %s", method) model.RecordLoginLog(user.Id, user.Username, content, ip, "login", map[string]interface{}{ "method": method, }, extra) } // setupLogin creates a server-controlled login Session and returns the shared // authentication bundle used by every login method. func setupLogin(user *model.User, c *gin.Context) { setupLoginAtAuthVersion(user, 0, c) } func setupLoginAtAuthVersion(user *model.User, expectedAuthVersion int64, c *gin.Context) { if user == nil || user.Id <= 0 || user.Status != common.UserStatusEnabled { common.ApiErrorI18n(c, i18n.MsgAuthUserBanned) return } currentUser, err := model.GetUserById(user.Id, false) if err != nil { common.ApiError(c, err) return } var bundle *service.AuthBundle if expectedAuthVersion > 0 { bundle, err = service.CreateLoginSessionAtAuthVersion( user.Id, expectedAuthVersion, loginMethodFromContext(c), c.ClientIP(), c.Request.UserAgent(), ) } else { bundle, err = service.CreateLoginSession( user.Id, loginMethodFromContext(c), c.ClientIP(), c.Request.UserAgent(), ) } if err != nil { writeAuthSessionError(c, err) return } model.UpdateUserLastLoginAt(user.Id) service.WriteRefreshCookie(c, bundle.RefreshToken) setAuthNoStore(c) recordLoginAudit(user, c) c.JSON(http.StatusOK, gin.H{ "message": "", "success": true, "data": gin.H{ "access_token": bundle.AccessToken, "token_type": bundle.TokenType, "access_expires_at": bundle.AccessExpiresAt, "session": bundle.Session, "user": buildSelfUserData(currentUser), }, }) } func Register(c *gin.Context) { if !common.RegisterEnabled { common.ApiErrorI18n(c, i18n.MsgUserRegisterDisabled) return } if !common.PasswordRegisterEnabled { common.ApiErrorI18n(c, i18n.MsgUserPasswordRegisterDisabled) return } var user model.User err := common.DecodeJson(c.Request.Body, &user) if err != nil { common.ApiErrorI18n(c, i18n.MsgInvalidParams) return } user.Username = strings.TrimSpace(user.Username) user.Email = model.NormalizeEmail(user.Email) if user.Username == "" { common.ApiErrorI18n(c, i18n.MsgInvalidParams) return } if err := common.Validate.Struct(&user); err != nil { common.ApiErrorI18n(c, i18n.MsgUserInputInvalid, map[string]any{"Error": err.Error()}) return } if common.EmailVerificationEnabled { if user.Email == "" || user.VerificationCode == "" { common.ApiErrorI18n(c, i18n.MsgUserEmailVerificationRequired) return } if !common.VerifyCodeWithKey(user.Email, user.VerificationCode, common.EmailVerificationPurpose) { common.ApiErrorI18n(c, i18n.MsgUserVerificationCodeError) return } if err := model.EnsureEmailAvailable(user.Email, 0); err != nil { if errors.Is(err, model.ErrEmailAlreadyTaken) { common.ApiErrorI18n(c, i18n.MsgUserEmailAlreadyTaken) return } common.ApiErrorI18n(c, i18n.MsgDatabaseError) return } } emailForExistCheck := "" if common.EmailVerificationEnabled { emailForExistCheck = user.Email } exist, err := model.CheckUserExistOrDeleted(user.Username, emailForExistCheck) if err != nil { common.ApiErrorI18n(c, i18n.MsgDatabaseError) common.SysLog(fmt.Sprintf("CheckUserExistOrDeleted error: %v", err)) return } if exist { common.ApiErrorI18n(c, i18n.MsgUserExists) return } affCode := user.AffCode // this code is the inviter's code, not the user's own code inviterId, _ := model.GetUserIdByAffCode(affCode) cleanUser := model.User{ Username: user.Username, Password: user.Password, DisplayName: user.Username, InviterId: inviterId, Role: common.RoleCommonUser, // 明确设置角色为普通用户 } if common.EmailVerificationEnabled { cleanUser.Email = user.Email } if err := cleanUser.Insert(inviterId); err != nil { if errors.Is(err, model.ErrEmailAlreadyTaken) { common.ApiErrorI18n(c, i18n.MsgUserEmailAlreadyTaken) return } common.ApiError(c, err) return } // 获取插入后的用户ID var insertedUser model.User if err := model.DB.Where("username = ?", cleanUser.Username).First(&insertedUser).Error; err != nil { common.ApiErrorI18n(c, i18n.MsgUserRegisterFailed) return } // 生成默认令牌 if constant.GenerateDefaultToken { key, err := common.GenerateKey() if err != nil { common.ApiErrorI18n(c, i18n.MsgUserDefaultTokenFailed) common.SysLog("failed to generate token key: " + err.Error()) return } // 生成默认令牌 token := model.Token{ UserId: insertedUser.Id, // 使用插入后的用户ID Name: cleanUser.Username + "的初始令牌", Key: key, CreatedTime: common.GetTimestamp(), AccessedTime: common.GetTimestamp(), ExpiredTime: -1, // 永不过期 RemainQuota: 500000, // 示例额度 UnlimitedQuota: true, ModelLimitsEnabled: false, } if setting.DefaultUseAutoGroup { token.Group = "auto" } if err := token.Insert(); err != nil { common.ApiErrorI18n(c, i18n.MsgCreateDefaultTokenErr) return } } c.JSON(http.StatusOK, gin.H{ "success": true, "message": "", }) return } func GetAllUsers(c *gin.Context) { pageInfo := common.GetPageQuery(c) sortOptions := model.NewUserSortOptions(c.Query("sort_by"), c.Query("sort_order")) users, total, err := model.GetAllUsers(pageInfo, sortOptions) if err != nil { common.ApiError(c, err) return } pageInfo.SetTotal(int(total)) pageInfo.SetItems(users) common.ApiSuccess(c, pageInfo) return } func SearchUsers(c *gin.Context) { keyword := c.Query("keyword") group := c.Query("group") var role *int if roleStr := c.Query("role"); roleStr != "" { if parsed, err := strconv.Atoi(roleStr); err == nil { role = &parsed } } var status *int if statusStr := c.Query("status"); statusStr != "" { if parsed, err := strconv.Atoi(statusStr); err == nil { status = &parsed } } pageInfo := common.GetPageQuery(c) sortOptions := model.NewUserSortOptions(c.Query("sort_by"), c.Query("sort_order")) users, total, err := model.SearchUsers(keyword, group, role, status, pageInfo.GetStartIdx(), pageInfo.GetPageSize(), sortOptions) if err != nil { common.ApiError(c, err) return } pageInfo.SetTotal(int(total)) pageInfo.SetItems(users) common.ApiSuccess(c, pageInfo) return } func canManageTargetRole(myRole int, targetRole int) bool { return myRole == common.RoleRootUser || myRole > targetRole } func GetUser(c *gin.Context) { id, err := strconv.Atoi(c.Param("id")) if err != nil { common.ApiError(c, err) return } user, err := model.GetUserById(id, false) if err != nil { common.ApiError(c, err) return } myRole := c.GetInt("role") if !canManageTargetRole(myRole, user.Role) { common.ApiErrorI18n(c, i18n.MsgUserNoPermissionSameLevel) return } user.AdminPermissions = authz.Capabilities(user.Id, user.Role) c.JSON(http.StatusOK, gin.H{ "success": true, "message": "", "data": user, }) return } func GenerateAccessToken(c *gin.Context) { id := c.GetInt("id") user, err := model.GetUserById(id, true) if err != nil { common.ApiError(c, err) return } // get rand int 28-32 randI := common.GetRandomInt(4) key, err := common.GenerateRandomKey(29 + randI) if err != nil { common.ApiErrorI18n(c, i18n.MsgGenerateFailed) common.SysLog("failed to generate key: " + err.Error()) return } user.SetAccessToken(key) if model.DB.Where("access_token = ?", user.AccessToken).First(user).RowsAffected != 0 { common.ApiErrorI18n(c, i18n.MsgUuidDuplicate) return } if err := user.Update(false); err != nil { common.ApiError(c, err) return } c.JSON(http.StatusOK, gin.H{ "success": true, "message": "", "data": user.AccessToken, }) return } type TransferAffQuotaRequest struct { Quota int `json:"quota" binding:"required"` } func TransferAffQuota(c *gin.Context) { if !requirePaymentCompliance(c) { return } id := c.GetInt("id") user, err := model.GetUserById(id, true) if err != nil { common.ApiError(c, err) return } tran := TransferAffQuotaRequest{} if err := c.ShouldBindJSON(&tran); err != nil { common.ApiError(c, err) return } err = user.TransferAffQuotaToQuota(tran.Quota) if err != nil { common.ApiErrorI18n(c, i18n.MsgUserTransferFailed, map[string]any{"Error": err.Error()}) return } common.ApiSuccessI18n(c, i18n.MsgUserTransferSuccess, nil) } func GetAffCode(c *gin.Context) { id := c.GetInt("id") user, err := model.GetUserById(id, true) if err != nil { common.ApiError(c, err) return } if user.AffCode == "" { user.AffCode = common.GetRandomString(4) 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": "", "data": user.AffCode, }) return } 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) return } responseData := buildSelfUserData(user) // The authenticated role is loaded from GetUserCache. It should equal the // row role, but use it for capabilities so GetSelf and login/refresh remain // consistent with the authorization decision made for this request. permissions := calculateUserPermissions(userRole) permissions["admin_permissions"] = authz.Capabilities(id, userRole) responseData["permissions"] = permissions c.JSON(http.StatusOK, gin.H{ "success": true, "message": "", "data": responseData, }) return } // buildSelfUserData is the single safe dashboard-user DTO used by GetSelf, // login and refresh. It intentionally excludes password, management PAT and // administrator-only remarks. func buildSelfUserData(user *model.User) map[string]interface{} { userSetting := user.GetSetting() permissions := calculateUserPermissions(user.Role) permissions["admin_permissions"] = authz.Capabilities(user.Id, user.Role) return map[string]interface{}{ "id": user.Id, "username": user.Username, "display_name": user.DisplayName, "role": user.Role, "status": user.Status, "email": user.Email, "github_id": user.GitHubId, "discord_id": user.DiscordId, "oidc_id": user.OidcId, "wechat_id": user.WeChatId, "telegram_id": user.TelegramId, "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, } } // 计算用户权限的辅助函数 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 := common.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 { id = c.GetInt("id") } user, err := model.GetUserCache(id) if err != nil { common.ApiError(c, err) return } groups := service.GetUserUsableGroups(user.Group) group := c.Query("group") var groupsToQuery []string switch { case group == "": for g := range groups { groupsToQuery = append(groupsToQuery, g) } case group == "auto": if _, ok := groups[group]; ok { groupsToQuery = service.GetUserAutoGroup(user.Group) } default: if _, ok := groups[group]; ok { groupsToQuery = []string{group} } } c.JSON(http.StatusOK, gin.H{ "success": true, "message": "", "data": service.GetGroupsEnabledModels(groupsToQuery), }) } func UpdateUser(c *gin.Context) { var updatedUser model.User err := common.DecodeJson(c.Request.Body, &updatedUser) if err != nil || updatedUser.Id == 0 { common.ApiErrorI18n(c, i18n.MsgInvalidParams) return } updatedUser.Username = strings.TrimSpace(updatedUser.Username) if updatedUser.Username == "" { common.ApiErrorI18n(c, i18n.MsgInvalidParams) return } if updatedUser.Password == "" { updatedUser.Password = "$I_LOVE_U" // make Validator happy :) } if err := common.Validate.Struct(&updatedUser); err != nil { common.ApiErrorI18n(c, i18n.MsgUserInputInvalid, map[string]any{"Error": err.Error()}) return } originUser, err := model.GetUserById(updatedUser.Id, false) if err != nil { common.ApiError(c, err) return } if updatedUser.Role != common.RoleGuestUser && updatedUser.Role != originUser.Role { common.ApiErrorI18n(c, i18n.MsgInvalidParams) return } updatedUser.Role = originUser.Role myRole := c.GetInt("role") if !canManageTargetRole(myRole, originUser.Role) { common.ApiErrorI18n(c, i18n.MsgUserNoPermissionHigherLevel) return } if updatedUser.Password == "$I_LOVE_U" { updatedUser.Password = "" // rollback to what it should be } updatePassword := updatedUser.Password != "" authzTouched := false if err := model.DB.Transaction(func(tx *gorm.DB) error { if err := updatedUser.EditWithTx(tx, updatePassword); err != nil { return err } touched, err := updateAdminPermissionsForUserInTx(c, tx, updatedUser.Id, originUser.Role, updatedUser.AdminPermissions) authzTouched = touched return err }); err != nil { common.ApiError(c, err) return } if authzTouched { if err := authz.ReloadPolicy(); err != nil { common.ApiError(c, err) return } } if updatedUser.AuthVersion > originUser.AuthVersion { if _, err := model.RevokeAllUserSessions(updatedUser.Id, "admin_user_update"); err != nil { common.ApiError(c, err) return } } if err := model.PublishUserAuthCache(updatedUser.Id); err != nil { common.ApiError(c, err) return } recordManageAuditFor(c, updatedUser.Id, "user.update", map[string]interface{}{ "username": originUser.Username, "id": updatedUser.Id, }) c.JSON(http.StatusOK, gin.H{ "success": true, "message": "", }) return } func AdminClearUserBinding(c *gin.Context) { id, err := strconv.Atoi(c.Param("id")) if err != nil { common.ApiErrorI18n(c, i18n.MsgInvalidParams) return } bindingType := strings.ToLower(strings.TrimSpace(c.Param("binding_type"))) if bindingType == "" { common.ApiErrorI18n(c, i18n.MsgInvalidParams) return } user, err := model.GetUserById(id, false) if err != nil { common.ApiError(c, err) return } myRole := c.GetInt("role") if !canManageTargetRole(myRole, user.Role) { common.ApiErrorI18n(c, i18n.MsgUserNoPermissionSameLevel) return } if err := user.ClearBinding(bindingType); err != nil { common.ApiError(c, err) return } recordManageAuditFor(c, user.Id, "user.binding_clear", map[string]interface{}{ "bindingType": bindingType, "username": user.Username, }) c.JSON(http.StatusOK, gin.H{ "success": true, "message": "success", }) } func UpdateSelf(c *gin.Context) { var requestData map[string]interface{} if err := common.DecodeJson(c.Request.Body, &requestData); err != nil { common.ApiErrorI18n(c, i18n.MsgInvalidParams) return } // 检查是否是用户设置更新请求 (sidebar_modules 或 language) if sidebarModules, sidebarExists := requestData["sidebar_modules"]; sidebarExists { 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 } if err := model.UpdateUserSetting(user.Id, currentSetting); err != nil { common.ApiErrorI18n(c, i18n.MsgUpdateFailed) return } common.ApiSuccessI18n(c, i18n.MsgUpdateSuccess, nil) return } // 检查是否是语言偏好更新请求 if language, langExists := requestData["language"]; langExists { userId := c.GetInt("id") user, err := model.GetUserById(userId, false) if err != nil { common.ApiError(c, err) return } // 获取当前用户设置 currentSetting := user.GetSetting() // 更新language字段 if langStr, ok := language.(string); ok { currentSetting.Language = langStr } if err := model.UpdateUserSetting(user.Id, currentSetting); err != nil { common.ApiErrorI18n(c, i18n.MsgUpdateFailed) return } common.ApiSuccessI18n(c, i18n.MsgUpdateSuccess, nil) return } // 原有的用户信息更新逻辑 var user model.User requestDataBytes, err := common.Marshal(requestData) if err != nil { common.ApiErrorI18n(c, i18n.MsgInvalidParams) return } if err = common.Unmarshal(requestDataBytes, &user); err != nil { common.ApiErrorI18n(c, i18n.MsgInvalidParams) return } if user.Password == "" { user.Password = "$I_LOVE_U" // make Validator happy :) } if err := common.Validate.Struct(&user); err != nil { common.ApiErrorI18n(c, i18n.MsgInvalidInput) return } cleanUser := model.User{ Id: c.GetInt("id"), Username: user.Username, Password: user.Password, DisplayName: user.DisplayName, } if user.Password == "$I_LOVE_U" { user.Password = "" // rollback to what it should be cleanUser.Password = "" } updatePassword, err := checkUpdatePassword(user.OriginalPassword, user.Password, cleanUser.Id) if err != nil { if errors.Is(err, errUserPasswordUnset) { common.ApiErrorI18n(c, i18n.MsgUserPasswordUnset) return } if errors.Is(err, errOriginalPasswordFail) { common.ApiErrorI18n(c, i18n.MsgUserOriginalPasswordError) return } common.ApiError(c, err) return } if updatePassword { identity, ok := middleware.GetSessionAuthIdentity(c) if !ok { common.ApiError(c, errors.New("当前认证方式不支持安全验证")) return } if err := model.DB.Transaction(func(tx *gorm.DB) error { return cleanUser.UpdateWithTx(tx, true) }); err != nil { common.ApiError(c, err) return } if err := model.PublishUserAuthCache(cleanUser.Id); err != nil { common.ApiError(c, err) return } bundle, err := service.AdvanceCurrentSessionToUserVersion(identity, "password_changed") if err != nil { common.ApiError(c, err) return } c.JSON(http.StatusOK, gin.H{ "success": true, "message": "", "data": gin.H{ "access_token": bundle.AccessToken, "token_type": bundle.TokenType, "access_expires_at": bundle.AccessExpiresAt, "session": bundle.Session, }, }) return } if err := cleanUser.Update(false); err != nil { common.ApiError(c, err) return } c.JSON(http.StatusOK, gin.H{"success": true, "message": ""}) return } func checkUpdatePassword(originalPassword string, newPassword string, userId int) (updatePassword bool, err error) { if newPassword == "" { return } var currentUser *model.User currentUser, err = model.GetUserById(userId, true) if err != nil { return } // 密码不为空,需要验证原密码 if currentUser.Password == "" { err = errUserPasswordUnset return } if !common.ValidatePasswordAndHash(originalPassword, currentUser.Password) { err = errOriginalPasswordFail return } updatePassword = true return } func DeleteUser(c *gin.Context) { id, err := strconv.Atoi(c.Param("id")) if err != nil { common.ApiError(c, err) return } originUser, err := model.GetUserById(id, false) if err != nil { common.ApiError(c, err) return } myRole := c.GetInt("role") if myRole <= originUser.Role { common.ApiErrorI18n(c, i18n.MsgUserNoPermissionHigherLevel) return } err = model.HardDeleteUserById(id) if err != nil { common.ApiError(c, err) return } recordManageAuditFor(c, originUser.Id, "user.delete", map[string]interface{}{ "username": originUser.Username, "id": originUser.Id, }) c.JSON(http.StatusOK, gin.H{ "success": true, "message": "", }) return } func DeleteSelf(c *gin.Context) { id := c.GetInt("id") user, _ := model.GetUserById(id, false) if user.Role == common.RoleRootUser { common.ApiErrorI18n(c, i18n.MsgUserCannotDeleteRootUser) return } err := model.DeleteUserById(id) if err != nil { common.ApiError(c, err) return } c.JSON(http.StatusOK, gin.H{ "success": true, "message": "", }) return } func CreateUser(c *gin.Context) { var user model.User err := common.DecodeJson(c.Request.Body, &user) user.Username = strings.TrimSpace(user.Username) if err != nil || user.Username == "" || user.Password == "" { common.ApiErrorI18n(c, i18n.MsgInvalidParams) return } if err := common.Validate.Struct(&user); err != nil { common.ApiErrorI18n(c, i18n.MsgUserInputInvalid, map[string]any{"Error": err.Error()}) return } if user.DisplayName == "" { user.DisplayName = user.Username } myRole := c.GetInt("role") if user.Role >= myRole { common.ApiErrorI18n(c, i18n.MsgUserCannotCreateHigherLevel) return } // Even for admin users, we cannot fully trust them! cleanUser := model.User{ Username: user.Username, Password: user.Password, DisplayName: user.DisplayName, Role: user.Role, // 保持管理员设置的角色 } authzTouched := false if err := model.DB.Transaction(func(tx *gorm.DB) error { if err := cleanUser.InsertWithTx(tx, 0); err != nil { return err } touched, err := updateAdminPermissionsForUserInTx(c, tx, cleanUser.Id, cleanUser.Role, user.AdminPermissions) authzTouched = touched return err }); err != nil { common.ApiError(c, err) return } if authzTouched { if err := authz.ReloadPolicy(); err != nil { common.ApiError(c, err) return } } cleanUser.FinishInsert(0) recordManageAuditFor(c, cleanUser.Id, "user.create", map[string]interface{}{ "username": cleanUser.Username, "role": cleanUser.Role, }) c.JSON(http.StatusOK, gin.H{ "success": true, "message": "", }) return } func updateAdminPermissionsForUserInTx(c *gin.Context, tx *gorm.DB, userID int, userRole int, permissions map[string]map[string]bool) (bool, error) { if permissions == nil { if userRole < common.RoleAdminUser && c.GetInt("role") == common.RoleRootUser { return true, authz.ClearUserAuthorizationInTx(tx, userID) } return false, nil } if c.GetInt("role") != common.RoleRootUser { return false, fmt.Errorf("only root can update admin permissions") } if userRole < common.RoleAdminUser { return true, authz.ClearUserAuthorizationInTx(tx, userID) } return true, authz.SetUserPermissionsInTx(tx, userID, permissions) } type ManageRequest struct { Id int `json:"id"` Action string `json:"action"` Value int `json:"value"` Mode string `json:"mode"` } // ManageUser Only admin user can do this func ManageUser(c *gin.Context) { var req ManageRequest err := common.DecodeJson(c.Request.Body, &req) if err != nil { common.ApiErrorI18n(c, i18n.MsgInvalidParams) return } user := model.User{ Id: req.Id, } // Fill attributes model.DB.Unscoped().Where(&user).First(&user) if user.Id == 0 { common.ApiErrorI18n(c, i18n.MsgUserNotExists) return } myRole := c.GetInt("role") if !canManageTargetRole(myRole, user.Role) { common.ApiErrorI18n(c, i18n.MsgUserNoPermissionHigherLevel) return } switch req.Action { case "disable": user.Status = common.UserStatusDisabled if user.Role == common.RoleRootUser { common.ApiErrorI18n(c, i18n.MsgUserCannotDisableRootUser) return } case "enable": user.Status = common.UserStatusEnabled case "delete": if user.Role == common.RoleRootUser { common.ApiErrorI18n(c, i18n.MsgUserCannotDeleteRootUser) return } if err := user.Delete(); err != nil { c.JSON(http.StatusOK, gin.H{ "success": false, "message": err.Error(), }) return } // 删除用户后,强制清理 Redis 中所有该用户令牌的缓存, // 避免已缓存的令牌在 TTL 过期前仍能通过 TokenAuth 校验。 if err := model.InvalidateUserTokensCache(user.Id); err != nil { common.SysLog(fmt.Sprintf("failed to invalidate tokens cache for user %d: %s", user.Id, err.Error())) } recordManageAuditFor(c, user.Id, "user.manage", map[string]interface{}{ "action": req.Action, "username": user.Username, "id": user.Id, }) c.JSON(http.StatusOK, gin.H{ "success": true, "message": "", }) return case "promote": if myRole != common.RoleRootUser { common.ApiErrorI18n(c, i18n.MsgUserAdminCannotPromote) return } if user.Role >= common.RoleAdminUser { common.ApiErrorI18n(c, i18n.MsgUserAlreadyAdmin) return } user.Role = common.RoleAdminUser case "demote": if user.Role == common.RoleRootUser { common.ApiErrorI18n(c, i18n.MsgUserCannotDemoteRootUser) return } if user.Role == common.RoleCommonUser { common.ApiErrorI18n(c, i18n.MsgUserAlreadyCommon) return } user.Role = common.RoleCommonUser case "add_quota": switch req.Mode { case "add": if req.Value <= 0 { common.ApiErrorI18n(c, i18n.MsgUserQuotaChangeZero) return } if err := model.IncreaseUserQuota(user.Id, req.Value, true); err != nil { common.ApiError(c, err) return } recordManageAuditFor(c, user.Id, "user.quota_add", map[string]interface{}{ "quota": logger.LogQuota(req.Value), }) case "subtract": if req.Value <= 0 { common.ApiErrorI18n(c, i18n.MsgUserQuotaChangeZero) return } if err := model.DecreaseUserQuota(user.Id, req.Value, true); err != nil { common.ApiError(c, err) return } recordManageAuditFor(c, user.Id, "user.quota_subtract", map[string]interface{}{ "quota": logger.LogQuota(req.Value), }) case "override": oldQuota := user.Quota if err := model.DB.Model(&model.User{}).Where("id = ?", user.Id).Update("quota", req.Value).Error; err != nil { common.ApiError(c, err) return } recordManageAuditFor(c, user.Id, "user.quota_override", map[string]interface{}{ "from": logger.LogQuota(oldQuota), "to": logger.LogQuota(req.Value), }) default: common.ApiErrorI18n(c, i18n.MsgInvalidParams) return } c.JSON(http.StatusOK, gin.H{ "success": true, "message": "", }) return default: common.ApiErrorI18n(c, i18n.MsgInvalidParams) return } if req.Action == "demote" { if err := model.DB.Transaction(func(tx *gorm.DB) error { if err := user.UpdateWithTx(tx, false); err != nil { return err } return authz.ClearUserAuthorizationInTx(tx, user.Id) }); err != nil { common.ApiError(c, err) return } if err := authz.ReloadPolicy(); err != nil { common.ApiError(c, err) return } if err := model.PublishUserAuthCache(user.Id); err != nil { common.ApiError(c, err) return } if _, err := model.RevokeAllUserSessions(user.Id, "admin_demote"); err != nil { common.ApiError(c, err) return } } else { if err := user.Update(false); err != nil { common.ApiError(c, err) return } } // Update/UpdateWithTx has already published the new user hash and revoked // browser sessions exactly once. Only PAT/relay token caches still need an // explicit invalidation; deleting the user hash here would discard the // freshly published auth-version floor. if err := model.InvalidateUserTokensCache(user.Id); err != nil { common.SysLog(fmt.Sprintf("failed to invalidate tokens cache for user %d: %s", user.Id, err.Error())) } recordManageAuditFor(c, user.Id, "user.manage", map[string]interface{}{ "action": req.Action, "username": user.Username, "id": user.Id, }) clearUser := model.User{ Role: user.Role, Status: user.Status, } c.JSON(http.StatusOK, gin.H{ "success": true, "message": "", "data": clearUser, }) return } type emailBindRequest struct { Email string `json:"email"` Code string `json:"code"` } func EmailBind(c *gin.Context) { var req emailBindRequest if err := common.DecodeJson(c.Request.Body, &req); err != nil { common.ApiError(c, errors.New("invalid request body")) return } email := req.Email email = model.NormalizeEmail(email) code := req.Code if !common.VerifyCodeWithKey(email, code, common.EmailVerificationPurpose) { common.ApiErrorI18n(c, i18n.MsgUserVerificationCodeError) return } user := model.User{ Id: c.GetInt("id"), } if user.Id == 0 { c.JSON(http.StatusUnauthorized, gin.H{"success": false, "message": "not authenticated"}) return } err := user.FillUserById() if err != nil { common.ApiError(c, err) return } if err := model.BindEmailToUser(&user, email); err != nil { if errors.Is(err, model.ErrEmailAlreadyTaken) { common.ApiErrorI18n(c, i18n.MsgUserEmailAlreadyTaken) return } common.ApiError(c, err) return } c.JSON(http.StatusOK, gin.H{ "success": true, "message": "", }) return } type topUpRequest struct { Key string `json:"key"` } var topUpLocks sync.Map var topUpCreateLock sync.Mutex type topUpTryLock struct { ch chan struct{} } func newTopUpTryLock() *topUpTryLock { return &topUpTryLock{ch: make(chan struct{}, 1)} } func (l *topUpTryLock) TryLock() bool { select { case l.ch <- struct{}{}: return true default: return false } } func (l *topUpTryLock) Unlock() { select { case <-l.ch: default: } } func getTopUpLock(userID int) *topUpTryLock { if v, ok := topUpLocks.Load(userID); ok { return v.(*topUpTryLock) } topUpCreateLock.Lock() defer topUpCreateLock.Unlock() if v, ok := topUpLocks.Load(userID); ok { return v.(*topUpTryLock) } l := newTopUpTryLock() topUpLocks.Store(userID, l) return l } func TopUp(c *gin.Context) { if !operation_setting.IsPaymentComplianceConfirmed() { common.ApiErrorI18n(c, i18n.MsgPaymentComplianceRequired) return } id := c.GetInt("id") lock := getTopUpLock(id) if !lock.TryLock() { common.ApiErrorI18n(c, i18n.MsgUserTopUpProcessing) return } defer lock.Unlock() req := topUpRequest{} err := c.ShouldBindJSON(&req) if err != nil { common.ApiError(c, err) return } quota, err := model.Redeem(req.Key, id) if err != nil { // 不向用户暴露兑换失败的细分原因,避免攻击者根据错误类型判断兑换码状态。 common.ApiErrorI18n(c, i18n.MsgRedeemFailed) logger.LogError(c, fmt.Sprintf("failed to redeem key %s for user %d: %s", req.Key, id, err.Error())) return } c.JSON(http.StatusOK, gin.H{ "success": true, "message": "", "data": quota, }) } type UpdateUserSettingRequest struct { QuotaWarningType string `json:"notify_type"` QuotaWarningThreshold float64 `json:"quota_warning_threshold"` WebhookUrl string `json:"webhook_url,omitempty"` WebhookSecret string `json:"webhook_secret,omitempty"` NotificationEmail string `json:"notification_email,omitempty"` BarkUrl string `json:"bark_url,omitempty"` GotifyUrl string `json:"gotify_url,omitempty"` GotifyToken string `json:"gotify_token,omitempty"` GotifyPriority int `json:"gotify_priority,omitempty"` UpstreamModelUpdateNotifyEnabled *bool `json:"upstream_model_update_notify_enabled,omitempty"` AcceptUnsetModelRatioModel bool `json:"accept_unset_model_ratio_model"` RecordIpLog bool `json:"record_ip_log"` } func UpdateUserSetting(c *gin.Context) { var req UpdateUserSettingRequest if err := c.ShouldBindJSON(&req); err != nil { common.ApiErrorI18n(c, i18n.MsgInvalidParams) return } // 验证预警类型 if req.QuotaWarningType != dto.NotifyTypeEmail && req.QuotaWarningType != dto.NotifyTypeWebhook && req.QuotaWarningType != dto.NotifyTypeBark && req.QuotaWarningType != dto.NotifyTypeGotify { common.ApiErrorI18n(c, i18n.MsgSettingInvalidType) return } // 验证预警阈值 if req.QuotaWarningThreshold <= 0 { common.ApiErrorI18n(c, i18n.MsgQuotaThresholdGtZero) return } // 如果是webhook类型,验证webhook地址 if req.QuotaWarningType == dto.NotifyTypeWebhook { if req.WebhookUrl == "" { common.ApiErrorI18n(c, i18n.MsgSettingWebhookEmpty) return } // 验证URL格式 if _, err := url.ParseRequestURI(req.WebhookUrl); err != nil { common.ApiErrorI18n(c, i18n.MsgSettingWebhookInvalid) return } } // 如果是邮件类型,验证邮箱地址 if req.QuotaWarningType == dto.NotifyTypeEmail && req.NotificationEmail != "" { // 验证邮箱格式 if !strings.Contains(req.NotificationEmail, "@") { common.ApiErrorI18n(c, i18n.MsgSettingEmailInvalid) return } } // 如果是Bark类型,验证Bark URL if req.QuotaWarningType == dto.NotifyTypeBark { if req.BarkUrl == "" { common.ApiErrorI18n(c, i18n.MsgSettingBarkUrlEmpty) return } // 验证URL格式 if _, err := url.ParseRequestURI(req.BarkUrl); err != nil { common.ApiErrorI18n(c, i18n.MsgSettingBarkUrlInvalid) return } // 检查是否是HTTP或HTTPS if !strings.HasPrefix(req.BarkUrl, "https://") && !strings.HasPrefix(req.BarkUrl, "http://") { common.ApiErrorI18n(c, i18n.MsgSettingUrlMustHttp) return } } // 如果是Gotify类型,验证Gotify URL和Token if req.QuotaWarningType == dto.NotifyTypeGotify { if req.GotifyUrl == "" { common.ApiErrorI18n(c, i18n.MsgSettingGotifyUrlEmpty) return } if req.GotifyToken == "" { common.ApiErrorI18n(c, i18n.MsgSettingGotifyTokenEmpty) return } // 验证URL格式 if _, err := url.ParseRequestURI(req.GotifyUrl); err != nil { common.ApiErrorI18n(c, i18n.MsgSettingGotifyUrlInvalid) return } // 检查是否是HTTP或HTTPS if !strings.HasPrefix(req.GotifyUrl, "https://") && !strings.HasPrefix(req.GotifyUrl, "http://") { common.ApiErrorI18n(c, i18n.MsgSettingUrlMustHttp) return } } userId := c.GetInt("id") user, err := model.GetUserById(userId, true) if err != nil { common.ApiError(c, err) return } existingSettings := user.GetSetting() upstreamModelUpdateNotifyEnabled := existingSettings.UpstreamModelUpdateNotifyEnabled if user.Role >= common.RoleAdminUser && req.UpstreamModelUpdateNotifyEnabled != nil { upstreamModelUpdateNotifyEnabled = *req.UpstreamModelUpdateNotifyEnabled } // 构建设置 settings := dto.UserSetting{ NotifyType: req.QuotaWarningType, QuotaWarningThreshold: req.QuotaWarningThreshold, UpstreamModelUpdateNotifyEnabled: upstreamModelUpdateNotifyEnabled, AcceptUnsetRatioModel: req.AcceptUnsetModelRatioModel, RecordIpLog: req.RecordIpLog, } // 如果是webhook类型,添加webhook相关设置 if req.QuotaWarningType == dto.NotifyTypeWebhook { settings.WebhookUrl = req.WebhookUrl if req.WebhookSecret != "" { settings.WebhookSecret = req.WebhookSecret } } // 如果提供了通知邮箱,添加到设置中 if req.QuotaWarningType == dto.NotifyTypeEmail && req.NotificationEmail != "" { settings.NotificationEmail = req.NotificationEmail } // 如果是Bark类型,添加Bark URL到设置中 if req.QuotaWarningType == dto.NotifyTypeBark { settings.BarkUrl = req.BarkUrl } // 如果是Gotify类型,添加Gotify配置到设置中 if req.QuotaWarningType == dto.NotifyTypeGotify { settings.GotifyUrl = req.GotifyUrl settings.GotifyToken = req.GotifyToken // Gotify优先级范围0-10,超出范围则使用默认值5 if req.GotifyPriority < 0 || req.GotifyPriority > 10 { settings.GotifyPriority = 5 } else { settings.GotifyPriority = req.GotifyPriority } } // 更新用户设置 if err := model.UpdateUserSetting(user.Id, settings); err != nil { common.ApiErrorI18n(c, i18n.MsgUpdateFailed) return } common.ApiSuccessI18n(c, i18n.MsgSettingSaved, nil) }