diff --git a/i18n/keys.go b/i18n/keys.go index 67ddbb9a..5123fa9d 100644 --- a/i18n/keys.go +++ b/i18n/keys.go @@ -28,6 +28,18 @@ const ( MsgBatchTooMany = "common.batch_too_many" ) +// Auth middleware messages +const ( + MsgAuthNotLoggedIn = "auth.not_logged_in" + MsgAuthAccessTokenInvalid = "auth.access_token_invalid" + MsgAuthUserInfoInvalid = "auth.user_info_invalid" + MsgAuthUserIdNotProvided = "auth.user_id_not_provided" + MsgAuthUserIdFormatError = "auth.user_id_format_error" + MsgAuthUserIdMismatch = "auth.user_id_mismatch" + MsgAuthUserBanned = "auth.user_banned" + MsgAuthInsufficientPrivilege = "auth.insufficient_privilege" +) + // Token related messages const ( MsgTokenNameTooLong = "token.name_too_long" diff --git a/i18n/locales/en.yaml b/i18n/locales/en.yaml index 5618a782..e9fc80f2 100644 --- a/i18n/locales/en.yaml +++ b/i18n/locales/en.yaml @@ -23,6 +23,16 @@ common.already_exists: "Already exists" common.name_cannot_be_empty: "Name cannot be empty" common.batch_too_many: "Too many items in batch request, maximum is {{.Max}}" +# Auth middleware messages +auth.not_logged_in: "Unauthorized, not logged in and no access token provided" +auth.access_token_invalid: "Unauthorized, invalid access token" +auth.user_info_invalid: "Unauthorized, invalid user info" +auth.user_id_not_provided: "Unauthorized, New-Api-User header not provided" +auth.user_id_format_error: "Unauthorized, New-Api-User header format error" +auth.user_id_mismatch: "Unauthorized, New-Api-User does not match logged in user" +auth.user_banned: "User has been banned" +auth.insufficient_privilege: "Unauthorized, insufficient privileges" + # Token messages token.name_too_long: "Token name is too long" token.quota_negative: "Quota value cannot be negative" diff --git a/i18n/locales/zh-CN.yaml b/i18n/locales/zh-CN.yaml index e4f1fac1..58ba5007 100644 --- a/i18n/locales/zh-CN.yaml +++ b/i18n/locales/zh-CN.yaml @@ -24,6 +24,16 @@ common.already_exists: "已存在" common.name_cannot_be_empty: "名称不能为空" common.batch_too_many: "批量请求数量过多,最多 {{.Max}} 条" +# Auth middleware messages +auth.not_logged_in: "无权进行此操作,未登录且未提供 access token" +auth.access_token_invalid: "无权进行此操作,access token 无效" +auth.user_info_invalid: "无权进行此操作,用户信息无效" +auth.user_id_not_provided: "无权进行此操作,未提供 New-Api-User" +auth.user_id_format_error: "无权进行此操作,New-Api-User 格式错误" +auth.user_id_mismatch: "无权进行此操作,New-Api-User 与登录用户不匹配" +auth.user_banned: "用户已被封禁" +auth.insufficient_privilege: "无权进行此操作,权限不足" + # Token messages token.name_too_long: "令牌名称过长" token.quota_negative: "额度值不能为负数" diff --git a/i18n/locales/zh-TW.yaml b/i18n/locales/zh-TW.yaml index 854cd111..5a24bff7 100644 --- a/i18n/locales/zh-TW.yaml +++ b/i18n/locales/zh-TW.yaml @@ -24,6 +24,16 @@ common.already_exists: "已存在" common.name_cannot_be_empty: "名稱不能為空" common.batch_too_many: "批次請求數量過多,最多 {{.Max}} 條" +# Auth middleware messages +auth.not_logged_in: "無權進行此操作,未登入且未提供 access token" +auth.access_token_invalid: "無權進行此操作,access token 無效" +auth.user_info_invalid: "無權進行此操作,使用者資訊無效" +auth.user_id_not_provided: "無權進行此操作,未提供 New-Api-User" +auth.user_id_format_error: "無權進行此操作,New-Api-User 格式錯誤" +auth.user_id_mismatch: "無權進行此操作,New-Api-User 與登入使用者不匹配" +auth.user_banned: "使用者已被封禁" +auth.insufficient_privilege: "無權進行此操作,權限不足" + # Token messages token.name_too_long: "令牌名稱過長" token.quota_negative: "額度值不能為負數" diff --git a/middleware/auth.go b/middleware/auth.go index 342e7f49..23d933fb 100644 --- a/middleware/auth.go +++ b/middleware/auth.go @@ -1,6 +1,7 @@ package middleware import ( + "errors" "fmt" "net" "net/http" @@ -9,6 +10,7 @@ import ( "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/constant" + "github.com/QuantumNous/new-api/i18n" "github.com/QuantumNous/new-api/logger" "github.com/QuantumNous/new-api/model" "github.com/QuantumNous/new-api/service" @@ -17,6 +19,7 @@ import ( "github.com/gin-contrib/sessions" "github.com/gin-gonic/gin" + "gorm.io/gorm" ) func validUserInfo(username string, role int) bool { @@ -43,17 +46,33 @@ func authHelper(c *gin.Context, minRole int) { if accessToken == "" { c.JSON(http.StatusUnauthorized, gin.H{ "success": false, - "message": "无权进行此操作,未登录且未提供 access token", + "message": common.TranslateMessage(c, i18n.MsgAuthNotLoggedIn), }) c.Abort() return } - user := model.ValidateAccessToken(accessToken) + user, authErr := model.ValidateAccessToken(accessToken) + if authErr != nil { + if errors.Is(authErr, model.ErrDatabase) { + common.SysLog("ValidateAccessToken database error: " + authErr.Error()) + c.JSON(http.StatusInternalServerError, gin.H{ + "success": false, + "message": common.TranslateMessage(c, i18n.MsgDatabaseError), + }) + } else { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": common.TranslateMessage(c, i18n.MsgAuthAccessTokenInvalid), + }) + } + c.Abort() + return + } if user != nil && user.Username != "" { if !validUserInfo(user.Username, user.Role) { c.JSON(http.StatusOK, gin.H{ "success": false, - "message": "无权进行此操作,用户信息无效", + "message": common.TranslateMessage(c, i18n.MsgAuthUserInfoInvalid), }) c.Abort() return @@ -67,7 +86,7 @@ func authHelper(c *gin.Context, minRole int) { } else { c.JSON(http.StatusOK, gin.H{ "success": false, - "message": "无权进行此操作,access token 无效", + "message": common.TranslateMessage(c, i18n.MsgAuthAccessTokenInvalid), }) c.Abort() return @@ -78,7 +97,7 @@ func authHelper(c *gin.Context, minRole int) { if apiUserIdStr == "" { c.JSON(http.StatusUnauthorized, gin.H{ "success": false, - "message": "无权进行此操作,未提供 New-Api-User", + "message": common.TranslateMessage(c, i18n.MsgAuthUserIdNotProvided), }) c.Abort() return @@ -87,7 +106,7 @@ func authHelper(c *gin.Context, minRole int) { if err != nil { c.JSON(http.StatusUnauthorized, gin.H{ "success": false, - "message": "无权进行此操作,New-Api-User 格式错误", + "message": common.TranslateMessage(c, i18n.MsgAuthUserIdFormatError), }) c.Abort() return @@ -96,7 +115,7 @@ func authHelper(c *gin.Context, minRole int) { if id != apiUserId { c.JSON(http.StatusUnauthorized, gin.H{ "success": false, - "message": "无权进行此操作,New-Api-User 与登录用户不匹配", + "message": common.TranslateMessage(c, i18n.MsgAuthUserIdMismatch), }) c.Abort() return @@ -104,7 +123,7 @@ func authHelper(c *gin.Context, minRole int) { if status.(int) == common.UserStatusDisabled { c.JSON(http.StatusOK, gin.H{ "success": false, - "message": "用户已被封禁", + "message": common.TranslateMessage(c, i18n.MsgAuthUserBanned), }) c.Abort() return @@ -112,7 +131,7 @@ func authHelper(c *gin.Context, minRole int) { if role.(int) < minRole { c.JSON(http.StatusOK, gin.H{ "success": false, - "message": "无权进行此操作,权限不足", + "message": common.TranslateMessage(c, i18n.MsgAuthInsufficientPrivilege), }) c.Abort() return @@ -120,7 +139,7 @@ func authHelper(c *gin.Context, minRole int) { if !validUserInfo(username.(string), role.(int)) { c.JSON(http.StatusOK, gin.H{ "success": false, - "message": "无权进行此操作,用户信息无效", + "message": common.TranslateMessage(c, i18n.MsgAuthUserInfoInvalid), }) c.Abort() return @@ -198,7 +217,7 @@ func TokenAuthReadOnly() func(c *gin.Context) { if key == "" { c.JSON(http.StatusUnauthorized, gin.H{ "success": false, - "message": "未提供 Authorization 请求头", + "message": common.TranslateMessage(c, i18n.MsgTokenNotProvided), }) c.Abort() return @@ -212,19 +231,28 @@ func TokenAuthReadOnly() func(c *gin.Context) { token, err := model.GetTokenByKey(key, false) if err != nil { - c.JSON(http.StatusUnauthorized, gin.H{ - "success": false, - "message": "无效的令牌", - }) + if errors.Is(err, gorm.ErrRecordNotFound) { + c.JSON(http.StatusUnauthorized, gin.H{ + "success": false, + "message": common.TranslateMessage(c, i18n.MsgTokenInvalid), + }) + } else { + common.SysLog("TokenAuthReadOnly GetTokenByKey database error: " + err.Error()) + c.JSON(http.StatusInternalServerError, gin.H{ + "success": false, + "message": common.TranslateMessage(c, i18n.MsgDatabaseError), + }) + } c.Abort() return } userCache, err := model.GetUserCache(token.UserId) if err != nil { + common.SysLog(fmt.Sprintf("TokenAuthReadOnly GetUserCache error for user %d: %v", token.UserId, err)) c.JSON(http.StatusInternalServerError, gin.H{ "success": false, - "message": err.Error(), + "message": common.TranslateMessage(c, i18n.MsgDatabaseError), }) c.Abort() return @@ -232,7 +260,7 @@ func TokenAuthReadOnly() func(c *gin.Context) { if userCache.Status != common.UserStatusEnabled { c.JSON(http.StatusForbidden, gin.H{ "success": false, - "message": "用户已被封禁", + "message": common.TranslateMessage(c, i18n.MsgAuthUserBanned), }) c.Abort() return @@ -309,7 +337,14 @@ func TokenAuth() func(c *gin.Context) { } } if err != nil { - abortWithOpenAiMessage(c, http.StatusUnauthorized, err.Error()) + if errors.Is(err, model.ErrDatabase) { + common.SysLog("TokenAuth ValidateUserToken database error: " + err.Error()) + abortWithOpenAiMessage(c, http.StatusInternalServerError, + common.TranslateMessage(c, i18n.MsgDatabaseError)) + } else { + abortWithOpenAiMessage(c, http.StatusUnauthorized, + common.TranslateMessage(c, i18n.MsgTokenInvalid)) + } return } @@ -331,12 +366,14 @@ func TokenAuth() func(c *gin.Context) { userCache, err := model.GetUserCache(token.UserId) if err != nil { - abortWithOpenAiMessage(c, http.StatusInternalServerError, err.Error()) + common.SysLog(fmt.Sprintf("TokenAuth GetUserCache error for user %d: %v", token.UserId, err)) + abortWithOpenAiMessage(c, http.StatusInternalServerError, + common.TranslateMessage(c, i18n.MsgDatabaseError)) return } userEnabled := userCache.Status == common.UserStatusEnabled if !userEnabled { - abortWithOpenAiMessage(c, http.StatusForbidden, "用户已被封禁") + abortWithOpenAiMessage(c, http.StatusForbidden, common.TranslateMessage(c, i18n.MsgAuthUserBanned)) return } diff --git a/model/errors.go b/model/errors.go new file mode 100644 index 00000000..a942a5bc --- /dev/null +++ b/model/errors.go @@ -0,0 +1,26 @@ +package model + +import "errors" + +// Common errors +var ( + ErrDatabase = errors.New("database error") +) + +// User auth errors +var ( + ErrInvalidCredentials = errors.New("invalid credentials") + ErrUserEmptyCredentials = errors.New("empty credentials") +) + +// Token auth errors +var ( + ErrTokenNotProvided = errors.New("token not provided") + ErrTokenInvalid = errors.New("token invalid") +) + +// Redemption errors +var ErrRedeemFailed = errors.New("redeem.failed") + +// 2FA errors +var ErrTwoFANotEnabled = errors.New("2fa not enabled") diff --git a/model/redemption.go b/model/redemption.go index 378976a3..b0ccb5df 100644 --- a/model/redemption.go +++ b/model/redemption.go @@ -11,9 +11,6 @@ import ( "gorm.io/gorm" ) -// ErrRedeemFailed is returned when redemption fails due to database error -var ErrRedeemFailed = errors.New("redeem.failed") - type Redemption struct { Id int `json:"id"` UserId int `json:"user_id"` diff --git a/model/token.go b/model/token.go index b7989ad1..8cfcd618 100644 --- a/model/token.go +++ b/model/token.go @@ -187,19 +187,14 @@ func SearchUserTokens(userId int, keyword string, token string, offset int, limi func ValidateUserToken(key string) (token *Token, err error) { if key == "" { - return nil, errors.New("未提供令牌") + return nil, ErrTokenNotProvided } token, err = GetTokenByKey(key, false) if err == nil { - if token.Status == common.TokenStatusExhausted { - keyPrefix := key[:3] - keySuffix := key[len(key)-3:] - return token, errors.New("该令牌额度已用尽 TokenStatusExhausted[sk-" + keyPrefix + "***" + keySuffix + "]") - } else if token.Status == common.TokenStatusExpired { - return token, errors.New("该令牌已过期") - } - if token.Status != common.TokenStatusEnabled { - return token, errors.New("该令牌状态不可用") + if token.Status == common.TokenStatusExhausted || + token.Status == common.TokenStatusExpired || + token.Status != common.TokenStatusEnabled { + return token, ErrTokenInvalid } if token.ExpiredTime != -1 && token.ExpiredTime < common.GetTimestamp() { if !common.RedisEnabled { @@ -209,29 +204,25 @@ func ValidateUserToken(key string) (token *Token, err error) { common.SysLog("failed to update token status" + err.Error()) } } - return token, errors.New("该令牌已过期") + return token, ErrTokenInvalid } if !token.UnlimitedQuota && token.RemainQuota <= 0 { if !common.RedisEnabled { - // in this case, we can make sure the token is exhausted token.Status = common.TokenStatusExhausted err := token.SelectUpdate() if err != nil { common.SysLog("failed to update token status" + err.Error()) } } - keyPrefix := key[:3] - keySuffix := key[len(key)-3:] - return token, fmt.Errorf("[sk-%s***%s] 该令牌额度已用尽 !token.UnlimitedQuota && token.RemainQuota = %d", keyPrefix, keySuffix, token.RemainQuota) + return token, ErrTokenInvalid } return token, nil } common.SysLog("ValidateUserToken: failed to get token: " + err.Error()) if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, errors.New("无效的令牌") - } else { - return nil, errors.New("无效的令牌,数据库查询出错,请联系管理员") + return nil, ErrTokenInvalid } + return nil, fmt.Errorf("%w: %v", ErrDatabase, err) } func GetTokenByIds(id int, userId int) (*Token, error) { diff --git a/model/twofa.go b/model/twofa.go index e63c6662..a2d0c7e1 100644 --- a/model/twofa.go +++ b/model/twofa.go @@ -10,8 +10,6 @@ import ( "gorm.io/gorm" ) -var ErrTwoFANotEnabled = errors.New("用户未启用2FA") - // TwoFA 用户2FA设置表 type TwoFA struct { Id int `json:"id" gorm:"primaryKey"` diff --git a/model/user.go b/model/user.go index 9ff2a15f..79e63e8f 100644 --- a/model/user.go +++ b/model/user.go @@ -18,12 +18,6 @@ import ( const UserNameMaxLength = 20 -var ( - ErrDatabase = errors.New("database error") - ErrInvalidCredentials = errors.New("invalid credentials") - ErrUserEmptyCredentials = errors.New("empty credentials") -) - // User if you add sensitive fields, don't forget to clean them in setupLogin function. // Otherwise, the sensitive information will be saved on local storage in plain text! type User struct { @@ -766,16 +760,20 @@ func IsAdmin(userId int) bool { // return user.Status == common.UserStatusEnabled, nil //} -func ValidateAccessToken(token string) (user *User) { +func ValidateAccessToken(token string) (*User, error) { if token == "" { - return nil + return nil, nil } token = strings.Replace(token, "Bearer ", "", 1) - user = &User{} - if DB.Where("access_token = ?", token).First(user).RowsAffected == 1 { - return user + user := &User{} + err := DB.Where("access_token = ?", token).First(user).Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, nil + } + return nil, fmt.Errorf("%w: %v", ErrDatabase, err) } - return nil + return user, nil } // GetUserQuota gets quota from Redis first, falls back to DB if needed