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
This commit is contained in:
+44
-9
@@ -32,6 +32,11 @@ type LoginRequest struct {
|
||||
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)
|
||||
@@ -191,6 +196,7 @@ func Register(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
user.Username = strings.TrimSpace(user.Username)
|
||||
user.Email = model.NormalizeEmail(user.Email)
|
||||
if user.Username == "" {
|
||||
common.ApiErrorI18n(c, i18n.MsgInvalidParams)
|
||||
return
|
||||
@@ -208,8 +214,20 @@ func Register(c *gin.Context) {
|
||||
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
|
||||
}
|
||||
}
|
||||
exist, err := model.CheckUserExistOrDeleted(user.Username, user.Email)
|
||||
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))
|
||||
@@ -232,6 +250,10 @@ func Register(c *gin.Context) {
|
||||
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
|
||||
}
|
||||
@@ -831,6 +853,14 @@ func UpdateSelf(c *gin.Context) {
|
||||
}
|
||||
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
|
||||
}
|
||||
@@ -847,6 +877,9 @@ func UpdateSelf(c *gin.Context) {
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -854,12 +887,12 @@ func checkUpdatePassword(originalPassword string, newPassword string, userId int
|
||||
}
|
||||
|
||||
// 密码不为空,需要验证原密码
|
||||
// 支持第一次账号绑定时原密码为空的情况
|
||||
if !common.ValidatePasswordAndHash(originalPassword, currentUser.Password) && currentUser.Password != "" {
|
||||
err = fmt.Errorf("原密码错误")
|
||||
if currentUser.Password == "" {
|
||||
err = errUserPasswordUnset
|
||||
return
|
||||
}
|
||||
if newPassword == "" {
|
||||
if !common.ValidatePasswordAndHash(originalPassword, currentUser.Password) {
|
||||
err = errOriginalPasswordFail
|
||||
return
|
||||
}
|
||||
updatePassword = true
|
||||
@@ -1181,6 +1214,7 @@ func EmailBind(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
email := req.Email
|
||||
email = model.NormalizeEmail(email)
|
||||
code := req.Code
|
||||
if !common.VerifyCodeWithKey(email, code, common.EmailVerificationPurpose) {
|
||||
common.ApiErrorI18n(c, i18n.MsgUserVerificationCodeError)
|
||||
@@ -1196,10 +1230,11 @@ func EmailBind(c *gin.Context) {
|
||||
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 {
|
||||
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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user