feat: support replacing bound primary email

This commit is contained in:
IanShaw027
2026-04-21 13:47:15 +08:00
parent 12f1e19d68
commit 65efef1eee
8 changed files with 313 additions and 17 deletions

View File

@@ -13,7 +13,8 @@ import (
infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
)
// BindEmailIdentity verifies and binds a local email/password identity to the current user.
// BindEmailIdentity verifies and binds a local email/password identity to the
// current user, or replaces the existing bound primary email.
func (s *AuthService) BindEmailIdentity(
ctx context.Context,
userID int64,
@@ -43,6 +44,13 @@ func (s *AuthService) BindEmailIdentity(
if err != nil {
return nil, err
}
firstRealEmailBind := !hasBindableEmailIdentitySubject(currentUser.Email)
if firstRealEmailBind && len(password) < 6 {
return nil, infraerrors.BadRequest("PASSWORD_TOO_SHORT", "password must be at least 6 characters")
}
if !firstRealEmailBind && !s.CheckPassword(password, currentUser.PasswordHash) {
return nil, ErrPasswordIncorrect
}
existingUser, err := s.userRepo.GetByEmail(ctx, normalizedEmail)
switch {
@@ -57,9 +65,8 @@ func (s *AuthService) BindEmailIdentity(
return nil, fmt.Errorf("hash password: %w", err)
}
firstRealEmailBind := !hasBindableEmailIdentitySubject(currentUser.Email)
if firstRealEmailBind && s.entClient != nil {
if err := s.bindEmailIdentityWithDefaultsTx(ctx, currentUser, normalizedEmail, hashedPassword); err != nil {
if s.entClient != nil {
if err := s.updateBoundEmailIdentityTx(ctx, currentUser, normalizedEmail, hashedPassword, firstRealEmailBind); err != nil {
return nil, err
}
return currentUser, nil
@@ -137,14 +144,15 @@ func hasBindableEmailIdentitySubject(email string) bool {
return normalized != "" && !isReservedEmail(normalized)
}
func (s *AuthService) bindEmailIdentityWithDefaultsTx(
func (s *AuthService) updateBoundEmailIdentityTx(
ctx context.Context,
currentUser *User,
email string,
hashedPassword string,
applyFirstBindDefaults bool,
) error {
if tx := dbent.TxFromContext(ctx); tx != nil {
return s.bindEmailIdentityWithDefaults(ctx, tx.Client(), currentUser, email, hashedPassword)
return s.updateBoundEmailIdentityWithClient(ctx, tx.Client(), currentUser, email, hashedPassword, applyFirstBindDefaults)
}
tx, err := s.entClient.Tx(ctx)
@@ -154,7 +162,7 @@ func (s *AuthService) bindEmailIdentityWithDefaultsTx(
defer func() { _ = tx.Rollback() }()
txCtx := dbent.NewTxContext(ctx, tx)
if err := s.bindEmailIdentityWithDefaults(txCtx, tx.Client(), currentUser, email, hashedPassword); err != nil {
if err := s.updateBoundEmailIdentityWithClient(txCtx, tx.Client(), currentUser, email, hashedPassword, applyFirstBindDefaults); err != nil {
return err
}
if err := tx.Commit(); err != nil {
@@ -163,12 +171,13 @@ func (s *AuthService) bindEmailIdentityWithDefaultsTx(
return nil
}
func (s *AuthService) bindEmailIdentityWithDefaults(
func (s *AuthService) updateBoundEmailIdentityWithClient(
ctx context.Context,
client *dbent.Client,
currentUser *User,
email string,
hashedPassword string,
applyFirstBindDefaults bool,
) error {
if client == nil || currentUser == nil || currentUser.ID <= 0 {
return ErrServiceUnavailable
@@ -192,8 +201,10 @@ func (s *AuthService) bindEmailIdentityWithDefaults(
return ErrServiceUnavailable
}
if err := s.ApplyProviderDefaultSettingsOnFirstBind(ctx, currentUser.ID, "email"); err != nil {
return fmt.Errorf("apply email first bind defaults: %w", err)
if applyFirstBindDefaults {
if err := s.ApplyProviderDefaultSettingsOnFirstBind(ctx, currentUser.ID, "email"); err != nil {
return fmt.Errorf("apply email first bind defaults: %w", err)
}
}
updatedUser, err := client.User.Get(ctx, currentUser.ID)