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) } // 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) }