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:
CaIon
2026-07-05 13:15:41 +08:00
parent 1ae757475f
commit 5fc35e28a2
10 changed files with 416 additions and 102 deletions
+20 -1
View File
@@ -1,6 +1,7 @@
package controller
import (
"errors"
"fmt"
"net/http"
"strconv"
@@ -106,11 +107,17 @@ func HandleOAuth(c *gin.Context) {
// 7. Find or create user
user, err := findOrCreateOAuthUser(c, provider, oauthUser, session)
if err != nil {
if errors.Is(err, model.ErrEmailAlreadyTaken) {
common.ApiErrorI18n(c, i18n.MsgUserEmailAlreadyTaken)
return
}
switch err.(type) {
case *OAuthUserDeletedError:
common.ApiErrorI18n(c, i18n.MsgOAuthUserDeleted)
case *OAuthRegistrationDisabledError:
common.ApiErrorI18n(c, i18n.MsgUserRegisterDisabled)
case *OAuthEmailAlreadyTakenError:
common.ApiErrorI18n(c, i18n.MsgUserEmailAlreadyTaken)
default:
common.ApiError(c, err)
}
@@ -257,7 +264,13 @@ func findOrCreateOAuthUser(c *gin.Context, provider oauth.Provider, oauthUser *o
user.DisplayName = provider.GetName() + " User"
}
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.Status = common.UserStatusEnabled
@@ -343,6 +356,12 @@ func (e *OAuthRegistrationDisabledError) Error() string {
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
func handleOAuthError(c *gin.Context, err error) {
switch e := err.(type) {