Commit 5fc35e28 by CaIon

fix(user): harden account email and password handling

- normalize emails (trim + lowercase) and enforce uniqueness across
  registration, OAuth auto-registration, and email binding
- serialize concurrent writers on the same normalized email within a
  transaction to avoid duplicate accounts
- resolve password reset to a single matching account and reject
  ambiguous or absent matches
- require an existing password before self-service password change and
  reject login for accounts without a usable password
parent 1ae75747
...@@ -2,12 +2,14 @@ package controller ...@@ -2,12 +2,14 @@ package controller
import ( import (
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"net/http" "net/http"
"strings" "strings"
"github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/constant" "github.com/QuantumNous/new-api/constant"
"github.com/QuantumNous/new-api/i18n"
"github.com/QuantumNous/new-api/logger" "github.com/QuantumNous/new-api/logger"
"github.com/QuantumNous/new-api/middleware" "github.com/QuantumNous/new-api/middleware"
"github.com/QuantumNous/new-api/model" "github.com/QuantumNous/new-api/model"
...@@ -232,12 +234,9 @@ func GetHomePageContent(c *gin.Context) { ...@@ -232,12 +234,9 @@ func GetHomePageContent(c *gin.Context) {
} }
func SendEmailVerification(c *gin.Context) { func SendEmailVerification(c *gin.Context) {
email := c.Query("email") email := model.NormalizeEmail(c.Query("email"))
if err := common.Validate.Var(email, "required,email"); err != nil { if err := common.Validate.Var(email, "required,email"); err != nil {
c.JSON(http.StatusOK, gin.H{ common.ApiErrorI18n(c, i18n.MsgInvalidParams)
"success": false,
"message": "无效的参数",
})
return return
} }
parts := strings.Split(email, "@") parts := strings.Split(email, "@")
...@@ -278,10 +277,7 @@ func SendEmailVerification(c *gin.Context) { ...@@ -278,10 +277,7 @@ func SendEmailVerification(c *gin.Context) {
} }
if model.IsEmailAlreadyTaken(email) { if model.IsEmailAlreadyTaken(email) {
c.JSON(http.StatusOK, gin.H{ common.ApiErrorI18n(c, i18n.MsgUserEmailAlreadyTaken)
"success": false,
"message": "邮箱地址已被占用",
})
return return
} }
code := common.GenerateVerificationCode(6) code := common.GenerateVerificationCode(6)
...@@ -303,15 +299,12 @@ func SendEmailVerification(c *gin.Context) { ...@@ -303,15 +299,12 @@ func SendEmailVerification(c *gin.Context) {
} }
func SendPasswordResetEmail(c *gin.Context) { func SendPasswordResetEmail(c *gin.Context) {
email := c.Query("email") email := model.NormalizeEmail(c.Query("email"))
if err := common.Validate.Var(email, "required,email"); err != nil { if err := common.Validate.Var(email, "required,email"); err != nil {
c.JSON(http.StatusOK, gin.H{ common.ApiErrorI18n(c, i18n.MsgInvalidParams)
"success": false,
"message": "无效的参数",
})
return return
} }
if model.IsEmailAlreadyTaken(email) { if _, err := model.GetUniqueUserByEmail(email); err == nil {
code := common.GenerateVerificationCode(0) code := common.GenerateVerificationCode(0)
common.RegisterVerificationCodeWithKey(email, code, common.PasswordResetPurpose) common.RegisterVerificationCodeWithKey(email, code, common.PasswordResetPurpose)
link := fmt.Sprintf("%s/user/reset?email=%s&token=%s", system_setting.ServerAddress, email, code) link := fmt.Sprintf("%s/user/reset?email=%s&token=%s", system_setting.ServerAddress, email, code)
...@@ -324,6 +317,8 @@ func SendPasswordResetEmail(c *gin.Context) { ...@@ -324,6 +317,8 @@ func SendPasswordResetEmail(c *gin.Context) {
if err != nil { if err != nil {
logger.LogError(c.Request.Context(), fmt.Sprintf("failed to send password reset email to %s: %s", email, err.Error())) logger.LogError(c.Request.Context(), fmt.Sprintf("failed to send password reset email to %s: %s", email, err.Error()))
} }
} else if err != nil && !errors.Is(err, model.ErrEmailNotFound) {
logger.LogWarn(c.Request.Context(), fmt.Sprintf("skip password reset email for %s: %s", email, err.Error()))
} }
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"success": true, "success": true,
...@@ -339,23 +334,26 @@ type PasswordResetRequest struct { ...@@ -339,23 +334,26 @@ type PasswordResetRequest struct {
func ResetPassword(c *gin.Context) { func ResetPassword(c *gin.Context) {
var req PasswordResetRequest var req PasswordResetRequest
err := json.NewDecoder(c.Request.Body).Decode(&req) err := json.NewDecoder(c.Request.Body).Decode(&req)
if err != nil {
common.ApiError(c, err)
return
}
req.Email = model.NormalizeEmail(req.Email)
if req.Email == "" || req.Token == "" { if req.Email == "" || req.Token == "" {
c.JSON(http.StatusOK, gin.H{ common.ApiErrorI18n(c, i18n.MsgInvalidParams)
"success": false,
"message": "无效的参数",
})
return return
} }
if !common.VerifyCodeWithKey(req.Email, req.Token, common.PasswordResetPurpose) { if !common.VerifyCodeWithKey(req.Email, req.Token, common.PasswordResetPurpose) {
c.JSON(http.StatusOK, gin.H{ common.ApiErrorI18n(c, i18n.MsgUserPasswordResetLinkInvalid)
"success": false,
"message": "重置链接非法或已过期",
})
return return
} }
password := common.GenerateVerificationCode(12) password := common.GenerateVerificationCode(12)
err = model.ResetUserPasswordByEmail(req.Email, password) err = model.ResetUserPasswordByEmail(req.Email, password)
if err != nil { if err != nil {
if errors.Is(err, model.ErrEmailNotFound) || errors.Is(err, model.ErrEmailAmbiguous) {
common.ApiErrorI18n(c, i18n.MsgUserPasswordResetLinkInvalid)
return
}
common.ApiError(c, err) common.ApiError(c, err)
return return
} }
......
package controller package controller
import ( import (
"errors"
"fmt" "fmt"
"net/http" "net/http"
"strconv" "strconv"
...@@ -106,11 +107,17 @@ func HandleOAuth(c *gin.Context) { ...@@ -106,11 +107,17 @@ func HandleOAuth(c *gin.Context) {
// 7. Find or create user // 7. Find or create user
user, err := findOrCreateOAuthUser(c, provider, oauthUser, session) user, err := findOrCreateOAuthUser(c, provider, oauthUser, session)
if err != nil { if err != nil {
if errors.Is(err, model.ErrEmailAlreadyTaken) {
common.ApiErrorI18n(c, i18n.MsgUserEmailAlreadyTaken)
return
}
switch err.(type) { switch err.(type) {
case *OAuthUserDeletedError: case *OAuthUserDeletedError:
common.ApiErrorI18n(c, i18n.MsgOAuthUserDeleted) common.ApiErrorI18n(c, i18n.MsgOAuthUserDeleted)
case *OAuthRegistrationDisabledError: case *OAuthRegistrationDisabledError:
common.ApiErrorI18n(c, i18n.MsgUserRegisterDisabled) common.ApiErrorI18n(c, i18n.MsgUserRegisterDisabled)
case *OAuthEmailAlreadyTakenError:
common.ApiErrorI18n(c, i18n.MsgUserEmailAlreadyTaken)
default: default:
common.ApiError(c, err) common.ApiError(c, err)
} }
...@@ -257,7 +264,13 @@ func findOrCreateOAuthUser(c *gin.Context, provider oauth.Provider, oauthUser *o ...@@ -257,7 +264,13 @@ func findOrCreateOAuthUser(c *gin.Context, provider oauth.Provider, oauthUser *o
user.DisplayName = provider.GetName() + " User" user.DisplayName = provider.GetName() + " User"
} }
if oauthUser.Email != "" { if oauthUser.Email != "" {
user.Email = oauthUser.Email user.Email = model.NormalizeEmail(oauthUser.Email)
if err := model.EnsureEmailAvailable(user.Email, 0); err != nil {
if errors.Is(err, model.ErrEmailAlreadyTaken) {
return nil, &OAuthEmailAlreadyTakenError{}
}
return nil, err
}
} }
user.Role = common.RoleCommonUser user.Role = common.RoleCommonUser
user.Status = common.UserStatusEnabled user.Status = common.UserStatusEnabled
...@@ -343,6 +356,12 @@ func (e *OAuthRegistrationDisabledError) Error() string { ...@@ -343,6 +356,12 @@ func (e *OAuthRegistrationDisabledError) Error() string {
return "registration is disabled" return "registration is disabled"
} }
type OAuthEmailAlreadyTakenError struct{}
func (e *OAuthEmailAlreadyTakenError) Error() string {
return "email is already in use"
}
// handleOAuthError handles OAuth errors and returns translated message // handleOAuthError handles OAuth errors and returns translated message
func handleOAuthError(c *gin.Context, err error) { func handleOAuthError(c *gin.Context, err error) {
switch e := err.(type) { switch e := err.(type) {
......
...@@ -32,6 +32,11 @@ type LoginRequest struct { ...@@ -32,6 +32,11 @@ type LoginRequest struct {
Password string `json:"password"` 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) { func Login(c *gin.Context) {
if !common.PasswordLoginEnabled { if !common.PasswordLoginEnabled {
common.ApiErrorI18n(c, i18n.MsgUserPasswordLoginDisabled) common.ApiErrorI18n(c, i18n.MsgUserPasswordLoginDisabled)
...@@ -191,6 +196,7 @@ func Register(c *gin.Context) { ...@@ -191,6 +196,7 @@ func Register(c *gin.Context) {
return return
} }
user.Username = strings.TrimSpace(user.Username) user.Username = strings.TrimSpace(user.Username)
user.Email = model.NormalizeEmail(user.Email)
if user.Username == "" { if user.Username == "" {
common.ApiErrorI18n(c, i18n.MsgInvalidParams) common.ApiErrorI18n(c, i18n.MsgInvalidParams)
return return
...@@ -208,8 +214,20 @@ func Register(c *gin.Context) { ...@@ -208,8 +214,20 @@ func Register(c *gin.Context) {
common.ApiErrorI18n(c, i18n.MsgUserVerificationCodeError) common.ApiErrorI18n(c, i18n.MsgUserVerificationCodeError)
return 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, user.Email) exist, err := model.CheckUserExistOrDeleted(user.Username, emailForExistCheck)
if err != nil { if err != nil {
common.ApiErrorI18n(c, i18n.MsgDatabaseError) common.ApiErrorI18n(c, i18n.MsgDatabaseError)
common.SysLog(fmt.Sprintf("CheckUserExistOrDeleted error: %v", err)) common.SysLog(fmt.Sprintf("CheckUserExistOrDeleted error: %v", err))
...@@ -232,6 +250,10 @@ func Register(c *gin.Context) { ...@@ -232,6 +250,10 @@ func Register(c *gin.Context) {
cleanUser.Email = user.Email cleanUser.Email = user.Email
} }
if err := cleanUser.Insert(inviterId); err != nil { if err := cleanUser.Insert(inviterId); err != nil {
if errors.Is(err, model.ErrEmailAlreadyTaken) {
common.ApiErrorI18n(c, i18n.MsgUserEmailAlreadyTaken)
return
}
common.ApiError(c, err) common.ApiError(c, err)
return return
} }
...@@ -831,6 +853,14 @@ func UpdateSelf(c *gin.Context) { ...@@ -831,6 +853,14 @@ func UpdateSelf(c *gin.Context) {
} }
updatePassword, err := checkUpdatePassword(user.OriginalPassword, user.Password, cleanUser.Id) updatePassword, err := checkUpdatePassword(user.OriginalPassword, user.Password, cleanUser.Id)
if err != nil { 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) common.ApiError(c, err)
return return
} }
...@@ -847,6 +877,9 @@ func UpdateSelf(c *gin.Context) { ...@@ -847,6 +877,9 @@ func UpdateSelf(c *gin.Context) {
} }
func checkUpdatePassword(originalPassword string, newPassword string, userId int) (updatePassword bool, err error) { func checkUpdatePassword(originalPassword string, newPassword string, userId int) (updatePassword bool, err error) {
if newPassword == "" {
return
}
var currentUser *model.User var currentUser *model.User
currentUser, err = model.GetUserById(userId, true) currentUser, err = model.GetUserById(userId, true)
if err != nil { if err != nil {
...@@ -854,12 +887,12 @@ func checkUpdatePassword(originalPassword string, newPassword string, userId int ...@@ -854,12 +887,12 @@ func checkUpdatePassword(originalPassword string, newPassword string, userId int
} }
// 密码不为空,需要验证原密码 // 密码不为空,需要验证原密码
// 支持第一次账号绑定时原密码为空的情况 if currentUser.Password == "" {
if !common.ValidatePasswordAndHash(originalPassword, currentUser.Password) && currentUser.Password != "" { err = errUserPasswordUnset
err = fmt.Errorf("原密码错误")
return return
} }
if newPassword == "" { if !common.ValidatePasswordAndHash(originalPassword, currentUser.Password) {
err = errOriginalPasswordFail
return return
} }
updatePassword = true updatePassword = true
...@@ -1181,6 +1214,7 @@ func EmailBind(c *gin.Context) { ...@@ -1181,6 +1214,7 @@ func EmailBind(c *gin.Context) {
return return
} }
email := req.Email email := req.Email
email = model.NormalizeEmail(email)
code := req.Code code := req.Code
if !common.VerifyCodeWithKey(email, code, common.EmailVerificationPurpose) { if !common.VerifyCodeWithKey(email, code, common.EmailVerificationPurpose) {
common.ApiErrorI18n(c, i18n.MsgUserVerificationCodeError) common.ApiErrorI18n(c, i18n.MsgUserVerificationCodeError)
...@@ -1196,10 +1230,11 @@ func EmailBind(c *gin.Context) { ...@@ -1196,10 +1230,11 @@ func EmailBind(c *gin.Context) {
common.ApiError(c, err) common.ApiError(c, err)
return return
} }
user.Email = email if err := model.BindEmailToUser(&user, email); err != nil {
// no need to check if this email already taken, because we have used verification code to check it if errors.Is(err, model.ErrEmailAlreadyTaken) {
err = user.Update(false) common.ApiErrorI18n(c, i18n.MsgUserEmailAlreadyTaken)
if err != nil { return
}
common.ApiError(c, err) common.ApiError(c, err)
return return
} }
......
...@@ -86,6 +86,9 @@ const ( ...@@ -86,6 +86,9 @@ const (
MsgUserRequire2FA = "user.require_2fa" MsgUserRequire2FA = "user.require_2fa"
MsgUserEmailVerificationRequired = "user.email_verification_required" MsgUserEmailVerificationRequired = "user.email_verification_required"
MsgUserVerificationCodeError = "user.verification_code_error" MsgUserVerificationCodeError = "user.verification_code_error"
MsgUserEmailAlreadyTaken = "user.email_already_taken"
MsgUserPasswordUnset = "user.password_unset"
MsgUserPasswordResetLinkInvalid = "user.password_reset_link_invalid"
MsgUserInputInvalid = "user.input_invalid" MsgUserInputInvalid = "user.input_invalid"
MsgUserNoPermissionSameLevel = "user.no_permission_same_level" MsgUserNoPermissionSameLevel = "user.no_permission_same_level"
MsgUserNoPermissionHigherLevel = "user.no_permission_higher_level" MsgUserNoPermissionHigherLevel = "user.no_permission_higher_level"
......
...@@ -74,6 +74,9 @@ user.session_save_failed: "Failed to save session, please try again" ...@@ -74,6 +74,9 @@ user.session_save_failed: "Failed to save session, please try again"
user.require_2fa: "Please enter two-factor authentication code" user.require_2fa: "Please enter two-factor authentication code"
user.email_verification_required: "Email verification is enabled, please enter email address and verification code" user.email_verification_required: "Email verification is enabled, please enter email address and verification code"
user.verification_code_error: "Verification code is incorrect or has expired" user.verification_code_error: "Verification code is incorrect or has expired"
user.email_already_taken: "Email address is already in use"
user.password_unset: "This account has no password set. Please use password reset or contact an administrator to reset it."
user.password_reset_link_invalid: "Password reset link is invalid or has expired"
user.input_invalid: "Invalid input {{.Error}}" user.input_invalid: "Invalid input {{.Error}}"
user.no_permission_same_level: "No permission to access users of same or higher level" user.no_permission_same_level: "No permission to access users of same or higher level"
user.no_permission_higher_level: "No permission to update users of same or higher permission level" user.no_permission_higher_level: "No permission to update users of same or higher permission level"
......
...@@ -75,6 +75,9 @@ user.session_save_failed: "无法保存会话信息,请重试" ...@@ -75,6 +75,9 @@ user.session_save_failed: "无法保存会话信息,请重试"
user.require_2fa: "请输入两步验证码" user.require_2fa: "请输入两步验证码"
user.email_verification_required: "管理员开启了邮箱验证,请输入邮箱地址和验证码" user.email_verification_required: "管理员开启了邮箱验证,请输入邮箱地址和验证码"
user.verification_code_error: "验证码错误或已过期" user.verification_code_error: "验证码错误或已过期"
user.email_already_taken: "邮箱地址已被占用"
user.password_unset: "当前账号未设置密码,请使用密码重置或联系管理员重置密码"
user.password_reset_link_invalid: "重置链接非法或已过期"
user.input_invalid: "输入不合法 {{.Error}}" user.input_invalid: "输入不合法 {{.Error}}"
user.no_permission_same_level: "无权获取同级或更高等级用户的信息" user.no_permission_same_level: "无权获取同级或更高等级用户的信息"
user.no_permission_higher_level: "无权更新同权限等级或更高权限等级的用户信息" user.no_permission_higher_level: "无权更新同权限等级或更高权限等级的用户信息"
......
...@@ -75,6 +75,9 @@ user.session_save_failed: "無法保存對話,請重試" ...@@ -75,6 +75,9 @@ user.session_save_failed: "無法保存對話,請重試"
user.require_2fa: "請輸入雙重驗證碼" user.require_2fa: "請輸入雙重驗證碼"
user.email_verification_required: "管理員開啟了信箱驗證,請輸入信箱位址和驗證碼" user.email_verification_required: "管理員開啟了信箱驗證,請輸入信箱位址和驗證碼"
user.verification_code_error: "驗證碼錯誤或已過期" user.verification_code_error: "驗證碼錯誤或已過期"
user.email_already_taken: "信箱位址已被占用"
user.password_unset: "目前帳號未設定密碼,請使用密碼重置或聯繫管理員重置密碼"
user.password_reset_link_invalid: "重置連結非法或已過期"
user.input_invalid: "輸入不合法 {{.Error}}" user.input_invalid: "輸入不合法 {{.Error}}"
user.no_permission_same_level: "無權獲取同級或更高等級使用者的資訊" user.no_permission_same_level: "無權獲取同級或更高等級使用者的資訊"
user.no_permission_higher_level: "無權更新同權限等級或更高權限等級的使用者資訊" user.no_permission_higher_level: "無權更新同權限等級或更高權限等級的使用者資訊"
......
...@@ -11,6 +11,9 @@ var ( ...@@ -11,6 +11,9 @@ var (
var ( var (
ErrInvalidCredentials = errors.New("invalid credentials") ErrInvalidCredentials = errors.New("invalid credentials")
ErrUserEmptyCredentials = errors.New("empty credentials") ErrUserEmptyCredentials = errors.New("empty credentials")
ErrEmailAlreadyTaken = errors.New("email already taken")
ErrEmailNotFound = errors.New("email not found")
ErrEmailAmbiguous = errors.New("email matches multiple users")
) )
// Token auth errors // Token auth errors
......
package model package model
import ( import (
"errors"
"testing" "testing"
"github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/common"
...@@ -90,3 +91,130 @@ func TestUpdateUserSettingOnlyUpdatesSetting(t *testing.T) { ...@@ -90,3 +91,130 @@ func TestUpdateUserSettingOnlyUpdatesSetting(t *testing.T) {
assert.Equal(t, 4, got.RequestCount) assert.Equal(t, 4, got.RequestCount)
assert.Equal(t, "zh", got.GetSetting().Language) assert.Equal(t, "zh", got.GetSetting().Language)
} }
func TestEnsureEmailAvailableRejectsExistingEmailCaseInsensitive(t *testing.T) {
setupUserUpdateTestState(t)
require.NoError(t, DB.Create(&User{
Username: "existing",
Password: "old-password",
Email: "Taken@Example.com",
Status: common.UserStatusEnabled,
}).Error)
err := EnsureEmailAvailable(" taken@example.COM ", 0)
require.ErrorIs(t, err, ErrEmailAlreadyTaken)
user, err := GetUniqueUserByEmail("TAKEN@example.com")
require.NoError(t, err)
assert.Equal(t, "existing", user.Username)
require.NoError(t, EnsureEmailAvailable("taken@example.com", user.Id))
}
func TestInsertRejectsDuplicateEmailWithoutUniqueIndex(t *testing.T) {
setupUserUpdateTestState(t)
require.NoError(t, DB.Create(&User{
Username: "existing",
Password: "old-password",
Email: "taken@example.com",
Status: common.UserStatusEnabled,
}).Error)
user := &User{
Username: "oauth-user",
Email: "TAKEN@example.com",
Role: common.RoleCommonUser,
Status: common.UserStatusEnabled,
}
err := user.Insert(0)
require.ErrorIs(t, err, ErrEmailAlreadyTaken)
var count int64
require.NoError(t, DB.Model(&User{}).Where("username = ?", "oauth-user").Count(&count).Error)
assert.Zero(t, count)
}
func TestInsertKeepsBlankPasswordForPasswordlessUser(t *testing.T) {
setupUserUpdateTestState(t)
user := &User{
Username: "passwordless-user",
Role: common.RoleCommonUser,
Status: common.UserStatusEnabled,
}
require.NoError(t, user.Insert(0))
var stored User
require.NoError(t, DB.Where("username = ?", user.Username).First(&stored).Error)
assert.Empty(t, stored.Password)
}
func TestValidateAndFillRejectsPasswordlessUser(t *testing.T) {
setupUserUpdateTestState(t)
require.NoError(t, DB.Create(&User{
Username: "passwordless-user",
Password: "",
Status: common.UserStatusEnabled,
}).Error)
loginUser := User{
Username: "passwordless-user",
Password: "NewPassword123",
}
err := loginUser.ValidateAndFill()
require.ErrorIs(t, err, ErrInvalidCredentials)
var stored User
require.NoError(t, DB.Where("username = ?", "passwordless-user").First(&stored).Error)
assert.Empty(t, stored.Password)
}
func TestResetUserPasswordByEmailRequiresSingleActiveMatch(t *testing.T) {
setupUserUpdateTestState(t)
require.NoError(t, DB.Create(&User{
Username: "duplicate-1",
Password: "old-1",
Email: "legacy@example.com",
AffCode: "dupe1",
Status: common.UserStatusEnabled,
}).Error)
require.NoError(t, DB.Create(&User{
Username: "duplicate-2",
Password: "old-2",
Email: "LEGACY@example.com",
AffCode: "dupe2",
Status: common.UserStatusEnabled,
}).Error)
err := ResetUserPasswordByEmail("legacy@example.com", "NewPassword123")
require.ErrorIs(t, err, ErrEmailAmbiguous)
var duplicates []User
require.NoError(t, DB.Where("LOWER(email) = ?", "legacy@example.com").Order("username asc").Find(&duplicates).Error)
require.Len(t, duplicates, 2)
assert.Equal(t, "old-1", duplicates[0].Password)
assert.Equal(t, "old-2", duplicates[1].Password)
require.NoError(t, DB.Create(&User{
Username: "unique",
Password: "old",
Email: "unique@example.com",
AffCode: "unique",
Status: common.UserStatusEnabled,
}).Error)
require.NoError(t, ResetUserPasswordByEmail("UNIQUE@example.com", "NewPassword123"))
var unique User
require.NoError(t, DB.Where("username = ?", "unique").First(&unique).Error)
assert.True(t, common.ValidatePasswordAndHash("NewPassword123", unique.Password))
err = ResetUserPasswordByEmail("missing@example.com", "NewPassword123")
require.True(t, errors.Is(err, ErrEmailNotFound))
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment