Files
evening_detective_server/internal/services/users_service/service.go
T
2026-08-19 23:31:37 +07:00

292 lines
8.5 KiB
Go

package users_service
import (
"context"
"errors"
"evening_detective_server/internal/modules/email_sender"
"evening_detective_server/internal/modules/password_generator"
"evening_detective_server/internal/modules/processor_jwt"
"evening_detective_server/internal/modules/roles"
"evening_detective_server/internal/repos"
"evening_detective_server/internal/repos/refresh_tokens_repo"
"evening_detective_server/internal/repos/user_agreements_repo"
"evening_detective_server/internal/repos/users_repo"
"fmt"
"slices"
"time"
"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"
)
type UsersService struct {
usersRepo *users_repo.UsersRepo
passwordGenerator password_generator.IPasswordGenerator
emailSender email_sender.IEmailSender
processorJWT processor_jwt.IProcessorJWT
refreshTokensRepo *refresh_tokens_repo.RefreshTokensRepo
}
func NewUsersService(
usersRepo *users_repo.UsersRepo,
passwordGenerator password_generator.IPasswordGenerator,
emailSender email_sender.IEmailSender,
processorJWT processor_jwt.IProcessorJWT,
refreshTokensRepo *refresh_tokens_repo.RefreshTokensRepo,
) *UsersService {
return &UsersService{
usersRepo: usersRepo,
passwordGenerator: passwordGenerator,
emailSender: emailSender,
processorJWT: processorJWT,
refreshTokensRepo: refreshTokensRepo,
}
}
// ErrTermsNotAccepted — регистрация невозможна без акцепта соглашений.
var ErrTermsNotAccepted = errors.New("Необходимо принять Пользовательское соглашение и Политику конфиденциальности")
func (s *UsersService) AddUser(
ctx context.Context,
username string,
email string,
acceptTerms bool,
acceptPrivacy bool,
ip string,
userAgent string,
) error {
if !acceptTerms || !acceptPrivacy {
return ErrTermsNotAccepted
}
password, err := s.passwordGenerator.Generate()
if err != nil {
return err
}
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return err
}
// Создание пользователя и фиксация акцептов — в одной транзакции:
// без записей о согласиях пользователь не существует.
_, err = s.usersRepo.AddUserWithAgreements(
ctx,
username,
email,
string(hashedPassword),
[]string{"user"},
[]user_agreements_repo.Agreement{
{
AgreementType: user_agreements_repo.AgreementTypeTerms,
Version: termsVersion,
IP: ip,
UserAgent: userAgent,
},
{
AgreementType: user_agreements_repo.AgreementTypePrivacy,
Version: privacyVersion,
IP: ip,
UserAgent: userAgent,
},
},
)
if err != nil {
return err
}
err = s.emailSender.Send(ctx, email_sender.Message{
To: email,
Subject: "Приветствую тебя, детектив!",
Body: fmt.Sprintf("Вот твой пароль для входа в систему: %s", password),
})
return err
}
// DeleteAccount удаляет учётную запись и все персональные данные пользователя
// (ст. 21 152-ФЗ). Требует подтверждения паролем: обладатель украденного
// access-токена не должен мочь удалить чужой аккаунт.
func (s *UsersService) DeleteAccount(
ctx context.Context,
userId int,
password string,
) error {
user, err := s.usersRepo.GetUserByID(ctx, userId)
if err != nil {
return err
}
if err = bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(password)); err != nil {
return errors.New("Неверный пароль")
}
return s.usersRepo.DeleteUser(ctx, userId)
}
func (s *UsersService) RefreshPassword(
ctx context.Context,
email string,
) error {
password, err := s.passwordGenerator.Generate()
if err != nil {
return err
}
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
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: "Приветствую тебя, детектив!",
Body: fmt.Sprintf("Вот твой новый пароль для входа в систему, не теряй: %s", password),
})
return err
}
func (s *UsersService) Login(
ctx context.Context,
email string,
password string,
) (string, string, error) {
user, err := s.usersRepo.GetUserByEmail(
ctx,
email,
)
if err != nil {
return "", "", err
}
if err = bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(password)); err != nil {
return "", "", errors.New("Не верен email или пароль")
}
accessToken, err := s.processorJWT.GenerateAccessToken(user.ID, user.Email, user.Roles)
if err != nil {
return "", "", errors.New("Не получилось получить доступ")
}
refreshToken, err := s.processorJWT.GenerateRefreshToken(user.ID)
if err != nil {
return "", "", errors.New("Не получилось получить доступ")
}
refreshTokenClaims, err := s.processorJWT.GetClaims(refreshToken)
if err != nil {
return "", "", errors.New("Не получилось получить доступ")
}
if err := s.refreshTokensRepo.AddRefreshToken(ctx, user.ID, refreshTokenClaims.ExpiresAt.Time, refreshTokenClaims.ID); err != nil {
return "", "", err
}
return accessToken, refreshToken, nil
}
func (s *UsersService) Refresh(
ctx context.Context,
token string,
) (string, string, error) {
claims, err := s.processorJWT.GetClaims(token)
if err != nil {
return "", "", errors.New("Пользователь не найден")
}
now := time.Now()
ok, err := s.refreshTokensRepo.ExistsRefreshToken(ctx, claims.UserID, claims.ID, now)
if err != nil {
return "", "", err
}
if !ok {
return "", "", errors.New("Пользователь не найден")
}
if err := s.refreshTokensRepo.DeleteRefreshTokenByJti(ctx, claims.ID); err != nil {
return "", "", err
}
user, err := s.usersRepo.GetUserByID(ctx, claims.UserID)
if err != nil {
return "", "", err
}
accessToken, err := s.processorJWT.GenerateAccessToken(user.ID, user.Email, user.Roles)
if err != nil {
return "", "", errors.New("Не получилось получить доступ")
}
refreshToken, err := s.processorJWT.GenerateRefreshToken(user.ID)
if err != nil {
return "", "", errors.New("Не получилось получить доступ")
}
refreshTokenClaims, err := s.processorJWT.GetClaims(refreshToken)
if err != nil {
return "", "", errors.New("Не получилось получить доступ")
}
if err := s.refreshTokensRepo.AddRefreshToken(ctx, user.ID, refreshTokenClaims.ExpiresAt.Time, refreshTokenClaims.ID); err != nil {
return "", "", err
}
return accessToken, refreshToken, nil
}
func (s *UsersService) GetUsers(
ctx context.Context,
) ([]*repos.User, error) {
users, err := s.usersRepo.GetUsers(ctx)
if err != nil {
return nil, err
}
return users, nil
}
func (s *UsersService) GetUserByID(
ctx context.Context,
userId int,
) (*repos.User, error) {
user, err := s.usersRepo.GetUserByID(ctx, userId)
if err != nil {
return nil, err
}
return user, nil
}
func (s *UsersService) AddUserRole(
ctx context.Context,
userId int,
role string,
) error {
if !slices.Contains(roles.AllRoles, role) {
return fmt.Errorf("role %s not support", role)
}
return s.usersRepo.AddUserRole(ctx, userId, role)
}
func (s *UsersService) DeleteUserRole(
ctx context.Context,
userId int,
role string,
) error {
if !slices.Contains(roles.AllRoles, role) {
return fmt.Errorf("role %s not support", role)
}
return s.usersRepo.DeleteUserRole(ctx, userId, role)
}