Files
user-system/internal/service/auth_contact_binding.go

300 lines
7.3 KiB
Go

package service
import (
"context"
"errors"
"strings"
"github.com/user-management-system/internal/domain"
)
func (s *AuthService) SendEmailBindCode(ctx context.Context, userID int64, email string) error {
if s == nil || s.userRepo == nil || s.emailCodeSvc == nil {
return errors.New("email binding is not configured")
}
user, err := s.userRepo.GetByID(ctx, userID)
if err != nil {
return err
}
if err := s.ensureUserActive(user); err != nil {
return err
}
normalizedEmail := strings.TrimSpace(email)
if normalizedEmail == "" {
return errors.New("email is required")
}
if strings.EqualFold(strings.TrimSpace(domain.DerefStr(user.Email)), normalizedEmail) {
return errors.New("email is already bound to the current account")
}
exists, err := s.userRepo.ExistsByEmail(ctx, normalizedEmail)
if err != nil {
return err
}
if exists {
return errors.New("email already in use")
}
return s.emailCodeSvc.SendEmailCode(ctx, normalizedEmail, "bind")
}
func (s *AuthService) BindEmail(
ctx context.Context,
userID int64,
email string,
code string,
currentPassword string,
totpCode string,
) error {
if s == nil || s.userRepo == nil || s.emailCodeSvc == nil {
return errors.New("email binding is not configured")
}
user, err := s.userRepo.GetByID(ctx, userID)
if err != nil {
return err
}
if err := s.ensureUserActive(user); err != nil {
return err
}
normalizedEmail := strings.TrimSpace(email)
if normalizedEmail == "" {
return errors.New("email is required")
}
if strings.EqualFold(strings.TrimSpace(domain.DerefStr(user.Email)), normalizedEmail) {
return errors.New("email is already bound to the current account")
}
exists, err := s.userRepo.ExistsByEmail(ctx, normalizedEmail)
if err != nil {
return err
}
if exists {
return errors.New("email already in use")
}
if err := s.verifySensitiveAction(ctx, user, currentPassword, totpCode); err != nil {
return err
}
if err := s.emailCodeSvc.VerifyEmailCode(ctx, normalizedEmail, "bind", strings.TrimSpace(code)); err != nil {
return err
}
user.Email = domain.StrPtr(normalizedEmail)
if err := s.userRepo.Update(ctx, user); err != nil {
return err
}
s.cacheUserInfo(ctx, user)
s.publishEvent(ctx, domain.EventUserUpdated, map[string]interface{}{
"user_id": user.ID,
"email": normalizedEmail,
"action": "bind_email",
})
return nil
}
func (s *AuthService) UnbindEmail(ctx context.Context, userID int64, currentPassword, totpCode string) error {
if s == nil || s.userRepo == nil {
return errors.New("email binding is not configured")
}
user, err := s.userRepo.GetByID(ctx, userID)
if err != nil {
return err
}
if err := s.ensureUserActive(user); err != nil {
return err
}
if strings.TrimSpace(domain.DerefStr(user.Email)) == "" {
return errors.New("email is not bound")
}
if err := s.verifySensitiveAction(ctx, user, currentPassword, totpCode); err != nil {
return err
}
accounts, err := s.GetSocialAccounts(ctx, userID)
if err != nil {
return err
}
if s.availableLoginMethodCountAfterContactRemoval(user, accounts, true, false) == 0 {
return errors.New("at least one login method must remain after unbinding")
}
user.Email = nil
if err := s.userRepo.Update(ctx, user); err != nil {
return err
}
s.cacheUserInfo(ctx, user)
s.publishEvent(ctx, domain.EventUserUpdated, map[string]interface{}{
"user_id": user.ID,
"action": "unbind_email",
})
return nil
}
func (s *AuthService) SendPhoneBindCode(ctx context.Context, userID int64, phone string) (*SendCodeResponse, error) {
if s == nil || s.userRepo == nil || s.smsCodeSvc == nil {
return nil, errors.New("phone binding is not configured")
}
user, err := s.userRepo.GetByID(ctx, userID)
if err != nil {
return nil, err
}
if err := s.ensureUserActive(user); err != nil {
return nil, err
}
normalizedPhone := strings.TrimSpace(phone)
if normalizedPhone == "" {
return nil, errors.New("phone is required")
}
if strings.TrimSpace(domain.DerefStr(user.Phone)) == normalizedPhone {
return nil, errors.New("phone is already bound to the current account")
}
exists, err := s.userRepo.ExistsByPhone(ctx, normalizedPhone)
if err != nil {
return nil, err
}
if exists {
return nil, errors.New("phone already in use")
}
return s.smsCodeSvc.SendCode(ctx, &SendCodeRequest{
Phone: normalizedPhone,
Purpose: "bind",
})
}
func (s *AuthService) BindPhone(
ctx context.Context,
userID int64,
phone string,
code string,
currentPassword string,
totpCode string,
) error {
if s == nil || s.userRepo == nil || s.smsCodeSvc == nil {
return errors.New("phone binding is not configured")
}
user, err := s.userRepo.GetByID(ctx, userID)
if err != nil {
return err
}
if err := s.ensureUserActive(user); err != nil {
return err
}
normalizedPhone := strings.TrimSpace(phone)
if normalizedPhone == "" {
return errors.New("phone is required")
}
if strings.TrimSpace(domain.DerefStr(user.Phone)) == normalizedPhone {
return errors.New("phone is already bound to the current account")
}
exists, err := s.userRepo.ExistsByPhone(ctx, normalizedPhone)
if err != nil {
return err
}
if exists {
return errors.New("phone already in use")
}
if err := s.verifySensitiveAction(ctx, user, currentPassword, totpCode); err != nil {
return err
}
if err := s.smsCodeSvc.VerifyCode(ctx, normalizedPhone, "bind", strings.TrimSpace(code)); err != nil {
return err
}
user.Phone = domain.StrPtr(normalizedPhone)
if err := s.userRepo.Update(ctx, user); err != nil {
return err
}
s.cacheUserInfo(ctx, user)
s.publishEvent(ctx, domain.EventUserUpdated, map[string]interface{}{
"user_id": user.ID,
"phone": normalizedPhone,
"action": "bind_phone",
})
return nil
}
func (s *AuthService) UnbindPhone(ctx context.Context, userID int64, currentPassword, totpCode string) error {
if s == nil || s.userRepo == nil {
return errors.New("phone binding is not configured")
}
user, err := s.userRepo.GetByID(ctx, userID)
if err != nil {
return err
}
if err := s.ensureUserActive(user); err != nil {
return err
}
if strings.TrimSpace(domain.DerefStr(user.Phone)) == "" {
return errors.New("phone is not bound")
}
if err := s.verifySensitiveAction(ctx, user, currentPassword, totpCode); err != nil {
return err
}
accounts, err := s.GetSocialAccounts(ctx, userID)
if err != nil {
return err
}
if s.availableLoginMethodCountAfterContactRemoval(user, accounts, false, true) == 0 {
return errors.New("at least one login method must remain after unbinding")
}
user.Phone = nil
if err := s.userRepo.Update(ctx, user); err != nil {
return err
}
s.cacheUserInfo(ctx, user)
s.publishEvent(ctx, domain.EventUserUpdated, map[string]interface{}{
"user_id": user.ID,
"action": "unbind_phone",
})
return nil
}
func (s *AuthService) availableLoginMethodCountAfterContactRemoval(
user *domain.User,
accounts []*domain.SocialAccount,
removeEmail bool,
removePhone bool,
) int {
if user == nil {
return 0
}
count := 0
if strings.TrimSpace(user.Password) != "" {
count++
}
if !removeEmail && s.emailCodeSvc != nil && strings.TrimSpace(domain.DerefStr(user.Email)) != "" {
count++
}
if !removePhone && s.smsCodeSvc != nil && strings.TrimSpace(domain.DerefStr(user.Phone)) != "" {
count++
}
for _, account := range accounts {
if account == nil || account.Status != domain.SocialAccountStatusActive {
continue
}
count++
}
return count
}