2026-10-21 13:17:44 +03:00
|
|
|
package user
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"errors"
|
|
|
|
|
"log/slog"
|
|
|
|
|
|
|
|
|
|
"golang.org/x/crypto/bcrypt"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const (
|
|
|
|
|
MaxImageWidth = 1024
|
|
|
|
|
MaxImageHeight = 1024
|
|
|
|
|
PasswordCost = 14
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type Service struct {
|
|
|
|
|
r *Repository
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func NewService(r *Repository) (*Service, error) {
|
|
|
|
|
return &Service{r: r}, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *Service) hashPassword(password string) (string, error) {
|
|
|
|
|
bytes, err := bcrypt.GenerateFromPassword([]byte(password), PasswordCost)
|
|
|
|
|
if err != nil {
|
|
|
|
|
slog.Error("hash password", slog.String("op", "service.hashPassword"), slog.Any("error", err))
|
|
|
|
|
return "", ErrInternal
|
|
|
|
|
}
|
|
|
|
|
return string(bytes), nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *Service) CreateUser(ctx context.Context, login, password string, isActive bool) (*User, error) {
|
|
|
|
|
hashed, err := s.hashPassword(password)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return s.r.CreateUser(ctx, login, hashed, isActive)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *Service) SetPassword(ctx context.Context, user *User, password string) (*User, error) {
|
|
|
|
|
hashed, err := s.hashPassword(password)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
values := *user
|
|
|
|
|
cloned := &values
|
|
|
|
|
cloned.Password = hashed
|
|
|
|
|
err = s.r.UpdateUser(ctx, cloned)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return cloned, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *Service) ValidatePassword(ctx context.Context, user *User, password string) error {
|
|
|
|
|
err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password))
|
|
|
|
|
if err != nil {
|
|
|
|
|
if errors.Is(err, bcrypt.ErrMismatchedHashAndPassword) {
|
|
|
|
|
return ErrInvalidPassword
|
|
|
|
|
}
|
|
|
|
|
slog.Error("compare password", slog.String("op", "service.ValidatePassword"), slog.Int64("user_id", user.Id), slog.Any("error", err))
|
|
|
|
|
return ErrInternal
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *Service) Users(ctx context.Context, includeInactive bool, limit, offset int64) ([]*User, error) {
|
|
|
|
|
return s.r.Users(ctx, includeInactive, limit, offset)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *Service) CountUsers(ctx context.Context, includeInactive bool) (int64, error) {
|
|
|
|
|
return s.r.CountUsers(ctx, includeInactive)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *Service) UserById(ctx context.Context, id int64) (*User, error) {
|
|
|
|
|
return s.r.UserById(ctx, id)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *Service) UserByLogin(ctx context.Context, login string) (*User, error) {
|
|
|
|
|
return s.r.UserByLogin(ctx, login)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *Service) UpdatePassword(ctx context.Context, user *User, oldPassword, newPassword string) (*User, error) {
|
|
|
|
|
err := s.ValidatePassword(ctx, user, oldPassword)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return s.SetPassword(ctx, user, newPassword)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *Service) UpdateProfile(ctx context.Context, user *User, fullname, email string) (*User, error) {
|
|
|
|
|
values := *user
|
|
|
|
|
cloned := &values
|
|
|
|
|
|
|
|
|
|
cloned.FullName = &fullname
|
|
|
|
|
cloned.Email = &email
|
|
|
|
|
err := s.r.UpdateUser(ctx, cloned)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return cloned, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *Service) CreateAdmin(ctx context.Context, login, password string) (*User, error) {
|
|
|
|
|
hashed, err := s.hashPassword(password)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
tx, err := s.r.Db.BeginTx(ctx, nil)
|
|
|
|
|
if err != nil {
|
|
|
|
|
slog.Error("begin transaction", slog.String("op", "service.CreateAdmin"), slog.Any("error", err))
|
|
|
|
|
return nil, ErrInternal
|
|
|
|
|
}
|
|
|
|
|
defer tx.Rollback()
|
|
|
|
|
txRepo := s.r.WithTx(tx)
|
|
|
|
|
user, err := txRepo.CreateUser(ctx, login, hashed, true)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
user.IsAdmin = true
|
|
|
|
|
err = txRepo.UpdateUser(ctx, user)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
tx.Commit()
|
|
|
|
|
return user, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *Service) UpdateAvatar(ctx context.Context, user *User, image []byte) error {
|
|
|
|
|
tx, err := s.r.Db.BeginTx(ctx, nil)
|
|
|
|
|
if err != nil {
|
|
|
|
|
slog.Error("begin transaction", slog.String("op", "service.UpdateAvatar"), slog.Int64("user_id", user.Id), slog.Any("error", err))
|
|
|
|
|
return ErrInternal
|
|
|
|
|
}
|
|
|
|
|
defer tx.Rollback()
|
|
|
|
|
txRepo := s.r.WithTx(tx)
|
|
|
|
|
|
|
|
|
|
err = txRepo.SetAvatar(ctx, user.Id, image)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
err = txRepo.UpdateUser(ctx, user)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
tx.Commit()
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *Service) Avatar(ctx context.Context, user *User) ([]byte, error) {
|
|
|
|
|
return s.r.Avatar(ctx, user.Id)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *Service) ActivateUser(ctx context.Context, user *User) (*User, error) {
|
|
|
|
|
tx, err := s.r.Db.BeginTx(ctx, nil)
|
|
|
|
|
if err != nil {
|
|
|
|
|
slog.Error("begin transaction", slog.String("op", "service.ActivateUser"), slog.Int64("user_id", user.Id), slog.Any("error", err))
|
|
|
|
|
return nil, ErrInternal
|
|
|
|
|
}
|
|
|
|
|
defer tx.Rollback()
|
|
|
|
|
txRepo := s.r.WithTx(tx)
|
|
|
|
|
|
|
|
|
|
err = txRepo.ActivateUser(ctx, user.Id)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cloned, err := txRepo.UserById(ctx, user.Id)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
tx.Commit()
|
|
|
|
|
return cloned, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *Service) DeleteUser(ctx context.Context, user *User) error {
|
|
|
|
|
return s.r.DeleteUser(ctx, user.Id)
|
|
|
|
|
}
|
2026-10-21 20:13:27 +03:00
|
|
|
|
|
|
|
|
// CleanupUsers deletes users that were created but never activated, e.g. when
|
|
|
|
|
// signup failed after the account row was inserted.
|
|
|
|
|
func (s *Service) CleanupUsers(ctx context.Context) error {
|
|
|
|
|
return s.r.CleanupUsers(ctx)
|
|
|
|
|
}
|