ValidateAndFill now checks the DB query result and returns sentinel errors (ErrDatabase, ErrInvalidCredentials, ErrUserEmptyCredentials) instead of hardcoded Chinese strings. The controller maps each sentinel to the appropriate i18n message, so users see "please contact admin" on DB errors instead of a misleading "wrong password" message. Non-DB errors still return a unified vague response to avoid leaking user existence.
1245 lines
32 KiB
Go
1245 lines
32 KiB
Go
package controller
|
||
|
||
import (
|
||
"encoding/json"
|
||
"errors"
|
||
"fmt"
|
||
"net/http"
|
||
"net/url"
|
||
"strconv"
|
||
"strings"
|
||
"sync"
|
||
|
||
"github.com/QuantumNous/new-api/common"
|
||
"github.com/QuantumNous/new-api/dto"
|
||
"github.com/QuantumNous/new-api/i18n"
|
||
"github.com/QuantumNous/new-api/logger"
|
||
"github.com/QuantumNous/new-api/model"
|
||
"github.com/QuantumNous/new-api/service"
|
||
"github.com/QuantumNous/new-api/setting"
|
||
|
||
"github.com/QuantumNous/new-api/constant"
|
||
|
||
"github.com/gin-contrib/sessions"
|
||
"github.com/gin-gonic/gin"
|
||
)
|
||
|
||
type LoginRequest struct {
|
||
Username string `json:"username"`
|
||
Password string `json:"password"`
|
||
}
|
||
|
||
func Login(c *gin.Context) {
|
||
if !common.PasswordLoginEnabled {
|
||
common.ApiErrorI18n(c, i18n.MsgUserPasswordLoginDisabled)
|
||
return
|
||
}
|
||
var loginRequest LoginRequest
|
||
err := json.NewDecoder(c.Request.Body).Decode(&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
|
||
if model.IsTwoFAEnabled(user.Id) {
|
||
// 设置pending session,等待2FA验证
|
||
session := sessions.Default(c)
|
||
session.Set("pending_username", user.Username)
|
||
session.Set("pending_user_id", user.Id)
|
||
err := session.Save()
|
||
if err != nil {
|
||
common.ApiErrorI18n(c, i18n.MsgUserSessionSaveFailed)
|
||
return
|
||
}
|
||
|
||
c.JSON(http.StatusOK, gin.H{
|
||
"message": i18n.T(c, i18n.MsgUserRequire2FA),
|
||
"success": true,
|
||
"data": map[string]interface{}{
|
||
"require_2fa": true,
|
||
},
|
||
})
|
||
return
|
||
}
|
||
|
||
setupLogin(&user, c)
|
||
}
|
||
|
||
// setup session & cookies and then return user info
|
||
func setupLogin(user *model.User, c *gin.Context) {
|
||
session := sessions.Default(c)
|
||
session.Set("id", user.Id)
|
||
session.Set("username", user.Username)
|
||
session.Set("role", user.Role)
|
||
session.Set("status", user.Status)
|
||
session.Set("group", user.Group)
|
||
err := session.Save()
|
||
if err != nil {
|
||
common.ApiErrorI18n(c, i18n.MsgUserSessionSaveFailed)
|
||
return
|
||
}
|
||
c.JSON(http.StatusOK, gin.H{
|
||
"message": "",
|
||
"success": true,
|
||
"data": map[string]any{
|
||
"id": user.Id,
|
||
"username": user.Username,
|
||
"display_name": user.DisplayName,
|
||
"role": user.Role,
|
||
"status": user.Status,
|
||
"group": user.Group,
|
||
},
|
||
})
|
||
}
|
||
|
||
func Logout(c *gin.Context) {
|
||
session := sessions.Default(c)
|
||
session.Clear()
|
||
err := session.Save()
|
||
if err != nil {
|
||
c.JSON(http.StatusOK, gin.H{
|
||
"message": err.Error(),
|
||
"success": false,
|
||
})
|
||
return
|
||
}
|
||
c.JSON(http.StatusOK, gin.H{
|
||
"message": "",
|
||
"success": true,
|
||
})
|
||
}
|
||
|
||
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 := json.NewDecoder(c.Request.Body).Decode(&user)
|
||
if err != nil {
|
||
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
|
||
}
|
||
}
|
||
exist, err := model.CheckUserExistOrDeleted(user.Username, user.Email)
|
||
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 {
|
||
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)
|
||
users, total, err := model.GetAllUsers(pageInfo)
|
||
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")
|
||
pageInfo := common.GetPageQuery(c)
|
||
users, total, err := model.SearchUsers(keyword, group, pageInfo.GetStartIdx(), pageInfo.GetPageSize())
|
||
if err != nil {
|
||
common.ApiError(c, err)
|
||
return
|
||
}
|
||
|
||
pageInfo.SetTotal(int(total))
|
||
pageInfo.SetItems(users)
|
||
common.ApiSuccess(c, pageInfo)
|
||
return
|
||
}
|
||
|
||
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 myRole <= user.Role && myRole != common.RoleRootUser {
|
||
common.ApiErrorI18n(c, i18n.MsgUserNoPermissionSameLevel)
|
||
return
|
||
}
|
||
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) {
|
||
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
|
||
}
|
||
// 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,
|
||
"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, // 新增权限字段
|
||
}
|
||
|
||
c.JSON(http.StatusOK, gin.H{
|
||
"success": true,
|
||
"message": "",
|
||
"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 {
|
||
id = c.GetInt("id")
|
||
}
|
||
user, err := model.GetUserCache(id)
|
||
if err != nil {
|
||
common.ApiError(c, err)
|
||
return
|
||
}
|
||
groups := service.GetUserUsableGroups(user.Group)
|
||
var models []string
|
||
for group := range groups {
|
||
for _, g := range model.GetGroupEnabledModels(group) {
|
||
if !common.StringsContains(models, g) {
|
||
models = append(models, g)
|
||
}
|
||
}
|
||
}
|
||
c.JSON(http.StatusOK, gin.H{
|
||
"success": true,
|
||
"message": "",
|
||
"data": models,
|
||
})
|
||
return
|
||
}
|
||
|
||
func UpdateUser(c *gin.Context) {
|
||
var updatedUser model.User
|
||
err := json.NewDecoder(c.Request.Body).Decode(&updatedUser)
|
||
if err != nil || updatedUser.Id == 0 {
|
||
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
|
||
}
|
||
myRole := c.GetInt("role")
|
||
if myRole <= originUser.Role && myRole != common.RoleRootUser {
|
||
common.ApiErrorI18n(c, i18n.MsgUserNoPermissionHigherLevel)
|
||
return
|
||
}
|
||
if myRole <= updatedUser.Role && myRole != common.RoleRootUser {
|
||
common.ApiErrorI18n(c, i18n.MsgUserCannotCreateHigherLevel)
|
||
return
|
||
}
|
||
if updatedUser.Password == "$I_LOVE_U" {
|
||
updatedUser.Password = "" // rollback to what it should be
|
||
}
|
||
updatePassword := updatedUser.Password != ""
|
||
if err := updatedUser.Edit(updatePassword); err != nil {
|
||
common.ApiError(c, err)
|
||
return
|
||
}
|
||
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 myRole <= user.Role && myRole != common.RoleRootUser {
|
||
common.ApiErrorI18n(c, i18n.MsgUserNoPermissionSameLevel)
|
||
return
|
||
}
|
||
|
||
if err := user.ClearBinding(bindingType); err != nil {
|
||
common.ApiError(c, err)
|
||
return
|
||
}
|
||
|
||
model.RecordLog(user.Id, model.LogTypeManage, fmt.Sprintf("admin cleared %s binding for user %s", bindingType, user.Username))
|
||
|
||
c.JSON(http.StatusOK, gin.H{
|
||
"success": true,
|
||
"message": "success",
|
||
})
|
||
}
|
||
|
||
func UpdateSelf(c *gin.Context) {
|
||
var requestData map[string]interface{}
|
||
err := json.NewDecoder(c.Request.Body).Decode(&requestData)
|
||
if 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
|
||
}
|
||
|
||
// 保存更新后的设置
|
||
user.SetSetting(currentSetting)
|
||
if err := user.Update(false); 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
|
||
}
|
||
|
||
// 保存更新后的设置
|
||
user.SetSetting(currentSetting)
|
||
if err := user.Update(false); err != nil {
|
||
common.ApiErrorI18n(c, i18n.MsgUpdateFailed)
|
||
return
|
||
}
|
||
|
||
common.ApiSuccessI18n(c, i18n.MsgUpdateSuccess, nil)
|
||
return
|
||
}
|
||
|
||
// 原有的用户信息更新逻辑
|
||
var user model.User
|
||
requestDataBytes, err := json.Marshal(requestData)
|
||
if err != nil {
|
||
common.ApiErrorI18n(c, i18n.MsgInvalidParams)
|
||
return
|
||
}
|
||
err = json.Unmarshal(requestDataBytes, &user)
|
||
if 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 {
|
||
common.ApiError(c, err)
|
||
return
|
||
}
|
||
if err := cleanUser.Update(updatePassword); 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) {
|
||
var currentUser *model.User
|
||
currentUser, err = model.GetUserById(userId, true)
|
||
if err != nil {
|
||
return
|
||
}
|
||
|
||
// 密码不为空,需要验证原密码
|
||
// 支持第一次账号绑定时原密码为空的情况
|
||
if !common.ValidatePasswordAndHash(originalPassword, currentUser.Password) && currentUser.Password != "" {
|
||
err = fmt.Errorf("原密码错误")
|
||
return
|
||
}
|
||
if newPassword == "" {
|
||
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 {
|
||
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 := json.NewDecoder(c.Request.Body).Decode(&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, // 保持管理员设置的角色
|
||
}
|
||
if err := cleanUser.Insert(0); err != nil {
|
||
common.ApiError(c, err)
|
||
return
|
||
}
|
||
|
||
c.JSON(http.StatusOK, gin.H{
|
||
"success": true,
|
||
"message": "",
|
||
})
|
||
return
|
||
}
|
||
|
||
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 := json.NewDecoder(c.Request.Body).Decode(&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 myRole <= user.Role && myRole != common.RoleRootUser {
|
||
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
|
||
}
|
||
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
|
||
}
|
||
model.RecordLog(user.Id, model.LogTypeManage,
|
||
fmt.Sprintf("管理员增加用户额度 %s", 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
|
||
}
|
||
model.RecordLog(user.Id, model.LogTypeManage,
|
||
fmt.Sprintf("管理员减少用户额度 %s", 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
|
||
}
|
||
model.RecordLog(user.Id, model.LogTypeManage,
|
||
fmt.Sprintf("管理员覆盖用户额度从 %s 为 %s", logger.LogQuota(oldQuota), logger.LogQuota(req.Value)))
|
||
default:
|
||
common.ApiErrorI18n(c, i18n.MsgInvalidParams)
|
||
return
|
||
}
|
||
c.JSON(http.StatusOK, gin.H{
|
||
"success": true,
|
||
"message": "",
|
||
})
|
||
return
|
||
}
|
||
|
||
if err := user.Update(false); err != nil {
|
||
common.ApiError(c, err)
|
||
return
|
||
}
|
||
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
|
||
code := req.Code
|
||
if !common.VerifyCodeWithKey(email, code, common.EmailVerificationPurpose) {
|
||
common.ApiErrorI18n(c, i18n.MsgUserVerificationCodeError)
|
||
return
|
||
}
|
||
session := sessions.Default(c)
|
||
id := session.Get("id")
|
||
user := model.User{
|
||
Id: id.(int),
|
||
}
|
||
err := user.FillUserById()
|
||
if err != nil {
|
||
common.ApiError(c, err)
|
||
return
|
||
}
|
||
user.Email = email
|
||
// no need to check if this email already taken, because we have used verification code to check it
|
||
err = user.Update(false)
|
||
if err != nil {
|
||
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) {
|
||
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 {
|
||
if errors.Is(err, model.ErrRedeemFailed) {
|
||
common.ApiErrorI18n(c, i18n.MsgRedeemFailed)
|
||
return
|
||
}
|
||
common.ApiError(c, err)
|
||
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
|
||
}
|
||
}
|
||
|
||
// 更新用户设置
|
||
user.SetSetting(settings)
|
||
if err := user.Update(false); err != nil {
|
||
common.ApiErrorI18n(c, i18n.MsgUpdateFailed)
|
||
return
|
||
}
|
||
|
||
common.ApiSuccessI18n(c, i18n.MsgSettingSaved, nil)
|
||
}
|