This commit is contained in:
2026-10-03 13:22:11 +07:00
parent 05090722a0
commit 2caa2c1a88
74 changed files with 44 additions and 676 deletions
+2 -33
View File
@@ -22,19 +22,12 @@ import (
"golang.org/x/crypto/bcrypt"
)
// Версии документов, акцепт которых фиксируется. Должны совпадать с
// редакциями, опубликованными по постоянным URL (/api/terms и /api/privacy,
// rpc GetTerms/GetPrivacy, текст встраивается из пакета docs): при изменении
// текста документа версия ОБЯЗАНА увеличиваться, иначе повторный акцепт
// не будет зафиксирован как согласие с новой редакцией
// (UNIQUE (user_id, agreement_type, version)).
const (
termsVersion = "1.0"
privacyVersion = "1.0"
)
// usersRepository — граница доступа к данным пользователей, реализуемая
// *users_repo.UsersRepo; интерфейс позволяет тестировать сервис без БД.
type usersRepository interface {
AddUserWithAgreements(
ctx context.Context,
@@ -60,9 +53,7 @@ type UsersService struct {
emailSender email_sender.IEmailSender
processorJWT processor_jwt.IProcessorJWT
refreshTokensRepo *refresh_tokens_repo.RefreshTokensRepo
// appBaseURL — публичный адрес фронтенда для ссылки «Войти в игру» в
// письмах; пустое значение — ссылка не добавляется.
appBaseURL string
appBaseURL string
}
// NewUsersService creates a users service.
@@ -85,7 +76,6 @@ func NewUsersService(
}
var (
// ErrTermsNotAccepted — регистрация невозможна без акцепта соглашений.
ErrTermsNotAccepted = errors.New("Необходимо принять Пользовательское соглашение и Политику конфиденциальности")
// ErrInvalidEmail indicates that an email address is invalid.
ErrInvalidEmail = errors.New("Некорректный email")
@@ -126,8 +116,6 @@ func (s *UsersService) AddUser(
if err != nil {
return err
}
// Создание пользователя и фиксация акцептов — в одной транзакции:
// без записей о согласиях пользователь не существует.
id, err := s.usersRepo.AddUserWithAgreements(
ctx,
username,
@@ -153,12 +141,6 @@ func (s *UsersService) AddUser(
return err
}
// Консистентность ответа и состояния БД: если письмо с паролем не
// ушло, пользователь не должен оставаться в системе «полусозданным»
// (клиент получит ошибку и повторит регистрацию). Компенсация —
// удаление созданного пользователя. Выполняется вне отменённого
// контекста: письмо могло упасть именно из-за истечения таймаута ctx,
// и тогда тот же ctx не дал бы выполнить удаление.
err = s.emailSender.Send(ctx, email_sender.Message{
To: email,
Subject: welcomeSubject,
@@ -175,9 +157,6 @@ func (s *UsersService) AddUser(
return nil
}
// DeleteAccount удаляет учётную запись и все персональные данные пользователя
// (ст. 21 152-ФЗ). Требует подтверждения паролем: обладатель украденного
// access-токена не должен мочь удалить чужой аккаунт.
func (s *UsersService) DeleteAccount(
ctx context.Context,
userID int,
@@ -210,8 +189,6 @@ func (s *UsersService) RefreshPassword(
return err
}
// Анти-enumeration: ответ одинаков для существующего и несуществующего
// адреса, письмо отправляется только реальному пользователю.
user, err := s.usersRepo.GetUserByEmail(ctx, email)
if err != nil {
if errors.Is(err, users_repo.ErrUserNotFound) {
@@ -229,9 +206,6 @@ func (s *UsersService) RefreshPassword(
return err
}
// Письмо отправляется ДО смены пароля: при сбое отправки пароль
// остаётся прежним и аккаунт не блокируется (иначе старый пароль уже
// не работает, а новый пользователь так и не узнал).
err = s.emailSender.Send(ctx, email_sender.Message{
To: user.Email,
Subject: resetSubject,
@@ -245,7 +219,6 @@ func (s *UsersService) RefreshPassword(
return s.usersRepo.UpdateUserPassword(ctx, user.Email, string(hashedPassword))
}
// validateEmail проверяет, что строка является одиночным email-адресом.
func validateEmail(email string) error {
if email == "" {
return ErrInvalidEmail
@@ -254,7 +227,6 @@ func validateEmail(email string) error {
if err != nil {
return ErrInvalidEmail
}
// Отклоняем форму "Имя <a@b.c>" и всё, что не является чистым адресом.
if !strings.EqualFold(addr.Address, email) {
return ErrInvalidEmail
}
@@ -267,7 +239,6 @@ func (s *UsersService) Login(
email string,
password string,
) (string, string, error) {
// Сохраняем регистр email, как при регистрации; убираем только внешние пробелы.
user, err := s.usersRepo.GetUserByEmail(
ctx,
strings.TrimSpace(email),
@@ -418,11 +389,9 @@ var (
ErrPasswordChangeForbidden = errors.New("Изменять чужой пароль может только администратор")
// ErrInvalidPasswordChange indicates that the password change request is invalid.
ErrInvalidPasswordChange = errors.New("Укажите чужой положительный ID и пароль от 8 до 16 символов")
// ErrUserNotFound — целевой пользователь не найден.
ErrUserNotFound = errors.New("Пользователь не найден")
ErrUserNotFound = errors.New("Пользователь не найден")
)
// ChangeUserPassword сохраняет только хеш; действующие сеансы не изменяются.
// ChangeUserPassword changes a user's password subject to authorization checks.
func (s *UsersService) ChangeUserPassword(ctx context.Context, actorID, userID int, password string) error {
actor, err := s.usersRepo.GetUserByID(ctx, actorID)