generated from VLADIMIR/template
updates email
This commit is contained in:
@@ -12,7 +12,10 @@ import (
|
||||
"evening_detective_server/internal/repos/user_agreements_repo"
|
||||
"evening_detective_server/internal/repos/users_repo"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/mail"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
@@ -29,8 +32,28 @@ const (
|
||||
privacyVersion = "1.0"
|
||||
)
|
||||
|
||||
// usersRepository — граница доступа к данным пользователей, реализуемая
|
||||
// *users_repo.UsersRepo; интерфейс позволяет тестировать сервис без БД.
|
||||
type usersRepository interface {
|
||||
AddUserWithAgreements(
|
||||
ctx context.Context,
|
||||
username string,
|
||||
email string,
|
||||
passwordHash string,
|
||||
roles []string,
|
||||
agreements []user_agreements_repo.Agreement,
|
||||
) (int, error)
|
||||
UpdateUserPassword(ctx context.Context, email string, passwordHash string) error
|
||||
GetUserByEmail(ctx context.Context, email string) (*repos.User, error)
|
||||
GetUserByID(ctx context.Context, id int) (*repos.User, error)
|
||||
GetUsers(ctx context.Context) ([]*repos.User, error)
|
||||
AddUserRole(ctx context.Context, userId int, role string) error
|
||||
DeleteUserRole(ctx context.Context, userId int, role string) error
|
||||
DeleteUser(ctx context.Context, userId int) error
|
||||
}
|
||||
|
||||
type UsersService struct {
|
||||
usersRepo *users_repo.UsersRepo
|
||||
usersRepo usersRepository
|
||||
passwordGenerator password_generator.IPasswordGenerator
|
||||
emailSender email_sender.IEmailSender
|
||||
processorJWT processor_jwt.IProcessorJWT
|
||||
@@ -38,7 +61,7 @@ type UsersService struct {
|
||||
}
|
||||
|
||||
func NewUsersService(
|
||||
usersRepo *users_repo.UsersRepo,
|
||||
usersRepo usersRepository,
|
||||
passwordGenerator password_generator.IPasswordGenerator,
|
||||
emailSender email_sender.IEmailSender,
|
||||
processorJWT processor_jwt.IProcessorJWT,
|
||||
@@ -69,6 +92,11 @@ func (s *UsersService) AddUser(
|
||||
return ErrTermsNotAccepted
|
||||
}
|
||||
|
||||
email = normalizeEmail(email)
|
||||
if err := validateEmail(email); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
password, err := s.passwordGenerator.Generate()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -79,7 +107,7 @@ func (s *UsersService) AddUser(
|
||||
}
|
||||
// Создание пользователя и фиксация акцептов — в одной транзакции:
|
||||
// без записей о согласиях пользователь не существует.
|
||||
_, err = s.usersRepo.AddUserWithAgreements(
|
||||
id, err := s.usersRepo.AddUserWithAgreements(
|
||||
ctx,
|
||||
username,
|
||||
email,
|
||||
@@ -104,13 +132,25 @@ func (s *UsersService) AddUser(
|
||||
return err
|
||||
}
|
||||
|
||||
// Консистентность ответа и состояния БД: если письмо с паролем не
|
||||
// ушло, пользователь не должен оставаться в системе «полусозданным»
|
||||
// (клиент получит ошибку и повторит регистрацию). Компенсация —
|
||||
// удаление созданного пользователя. Выполняется вне отменённого
|
||||
// контекста: письмо могло упасть именно из-за истечения таймаута ctx,
|
||||
// и тогда тот же ctx не дал бы выполнить удаление.
|
||||
err = s.emailSender.Send(ctx, email_sender.Message{
|
||||
To: email,
|
||||
Subject: "Приветствую тебя, детектив!",
|
||||
Body: fmt.Sprintf("Вот твой пароль для входа в систему: %s", password),
|
||||
})
|
||||
if err != nil {
|
||||
if delErr := s.usersRepo.DeleteUser(context.WithoutCancel(ctx), id); delErr != nil {
|
||||
log.Printf("email: отправка не удалась (%v) и компенсация не выполнена: %v", err, delErr)
|
||||
}
|
||||
return fmt.Errorf("email send: %w", err)
|
||||
}
|
||||
|
||||
return err
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteAccount удаляет учётную запись и все персональные данные пользователя
|
||||
@@ -135,6 +175,21 @@ func (s *UsersService) RefreshPassword(
|
||||
ctx context.Context,
|
||||
email string,
|
||||
) error {
|
||||
email = normalizeEmail(email)
|
||||
if err := validateEmail(email); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Анти-enumeration: ответ одинаков для существующего и несуществующего
|
||||
// адреса, письмо отправляется только реальному пользователю.
|
||||
user, err := s.usersRepo.GetUserByEmail(ctx, email)
|
||||
if err != nil {
|
||||
if errors.Is(err, users_repo.ErrUserNotFound) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
password, err := s.passwordGenerator.Generate()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -144,22 +199,41 @@ func (s *UsersService) RefreshPassword(
|
||||
return err
|
||||
}
|
||||
|
||||
err = s.usersRepo.UpdateUserPassword(
|
||||
ctx,
|
||||
email,
|
||||
string(hashedPassword),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Письмо отправляется ДО смены пароля: при сбое отправки пароль
|
||||
// остаётся прежним и аккаунт не блокируется (иначе старый пароль уже
|
||||
// не работает, а новый пользователь так и не узнал).
|
||||
err = s.emailSender.Send(ctx, email_sender.Message{
|
||||
To: email,
|
||||
Subject: "Приветствую тебя, детектив!",
|
||||
To: user.Email,
|
||||
Subject: "Сброс пароля",
|
||||
Body: fmt.Sprintf("Вот твой новый пароль для входа в систему, не теряй: %s", password),
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("email send: %w", err)
|
||||
}
|
||||
|
||||
return err
|
||||
return s.usersRepo.UpdateUserPassword(ctx, user.Email, string(hashedPassword))
|
||||
}
|
||||
|
||||
// normalizeEmail приводит адрес к каноническому виду (без пробелов, нижний
|
||||
// регистр): "User@Example.COM " и "user@example.com" — один аккаунт.
|
||||
func normalizeEmail(email string) string {
|
||||
return strings.ToLower(strings.TrimSpace(email))
|
||||
}
|
||||
|
||||
// validateEmail проверяет, что строка является одиночным email-адресом.
|
||||
func validateEmail(email string) error {
|
||||
if email == "" {
|
||||
return errors.New("Email не указан")
|
||||
}
|
||||
addr, err := mail.ParseAddress(email)
|
||||
if err != nil {
|
||||
return errors.New("Некорректный email")
|
||||
}
|
||||
// Отклоняем форму "Имя <a@b.c>" и всё, что не является чистым адресом.
|
||||
if !strings.EqualFold(addr.Address, email) {
|
||||
return errors.New("Некорректный email")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *UsersService) Login(
|
||||
@@ -167,9 +241,11 @@ func (s *UsersService) Login(
|
||||
email string,
|
||||
password string,
|
||||
) (string, string, error) {
|
||||
// Email нормализуется так же, как при регистрации: иначе пользователь,
|
||||
// зарегистрировавшийся как user@example.com, не войдёт с User@Example.COM.
|
||||
user, err := s.usersRepo.GetUserByEmail(
|
||||
ctx,
|
||||
email,
|
||||
normalizeEmail(email),
|
||||
)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
|
||||
Reference in New Issue
Block a user