package service import ( "context" "errors" "log/slog" "gitea.michaelthomson.dev/mthomson/habits/internal/auth" "gitea.michaelthomson.dev/mthomson/habits/internal/user/repository" "github.com/gofrs/uuid/v5" ) var ( ErrNotFound error = errors.New("user cannot be found") ErrUserExists error = errors.New("user already exists") ) type User struct { Id uuid.UUID Email string } func UserFromUserRow(userRow repository.UserRow) User { return User{Id: userRow.Id, Email: userRow.Email} } func (t User) Equal(user User) bool { return t.Id == user.Id && t.Email == user.Email } type UserRepository interface { Create(ctx context.Context, user repository.UserRow) (repository.UserRow, error) GetById(ctx context.Context, id uuid.UUID) (repository.UserRow, error) GetByEmail(ctx context.Context, email string) (repository.UserRow, error) Update(ctx context.Context, user repository.UserRow) error Delete(ctx context.Context, id uuid.UUID) error } type UserService struct { logger *slog.Logger repo UserRepository argon2IdHash *auth.Argon2IdHash } func NewUserService(logger *slog.Logger, userRepo UserRepository, argon2IdHash *auth.Argon2IdHash) *UserService { return &UserService{ logger: logger, repo: userRepo, argon2IdHash: argon2IdHash, } } func (s *UserService) GetUser(ctx context.Context, id uuid.UUID) (User, error) { user, err := s.repo.GetById(ctx, id) if err != nil { if err == repository.ErrNotFound { return User{}, ErrNotFound } s.logger.ErrorContext(ctx, err.Error()) return User{}, err } return UserFromUserRow(user), err } func (s *UserService) Register(ctx context.Context, email string, password string) (uuid.UUID, error) { uuid, err := uuid.NewV4() if err != nil { s.logger.ErrorContext(ctx, err.Error()) return uuid, err } _, err = s.repo.GetByEmail(ctx, email) if err != repository.ErrNotFound { return uuid, ErrUserExists } hashSalt, err := s.argon2IdHash.GenerateHash([]byte(password), nil) if err != nil { s.logger.ErrorContext(ctx, err.Error()) return uuid, err } userRow := repository.UserRow{Id: uuid, Email: email, HashedPassword: hashSalt.Hash, Salt: hashSalt.Salt} _, err = s.repo.Create(ctx, userRow) if err != nil { s.logger.ErrorContext(ctx, err.Error()) return uuid, err } return uuid, err } func (s *UserService) DeleteUser(ctx context.Context, id uuid.UUID) error { err := s.repo.Delete(ctx, id) if err == repository.ErrNotFound { return ErrNotFound } if err != nil { s.logger.ErrorContext(ctx, err.Error()) } return err } func (s *UserService) UpdateUserEmail(ctx context.Context, id uuid.UUID, email string) error { user, err := s.repo.GetById(ctx, id) if err == repository.ErrNotFound { return ErrNotFound } if err != nil { s.logger.ErrorContext(ctx, err.Error()) return err } _, err = s.repo.GetByEmail(ctx, email) switch err { case repository.ErrNotFound: user.Email = email err = s.repo.Update(ctx, user) if err == repository.ErrNotFound { return ErrNotFound } if err != nil { s.logger.ErrorContext(ctx, err.Error()) } return err case nil: return ErrUserExists default: s.logger.ErrorContext(ctx, err.Error()) return err } } func (s *UserService) UpdateUserPassword(ctx context.Context, id uuid.UUID, password string) error { user, err := s.repo.GetById(ctx, id) if err == repository.ErrNotFound { return ErrNotFound } if err != nil { s.logger.ErrorContext(ctx, err.Error()) return err } hashSalt, err := s.argon2IdHash.GenerateHash([]byte(password), nil) if err != nil { s.logger.ErrorContext(ctx, err.Error()) return err } user.HashedPassword = hashSalt.Hash user.Salt = hashSalt.Salt err = s.repo.Update(ctx, user) if err == repository.ErrNotFound { return ErrNotFound } if err != nil { s.logger.ErrorContext(ctx, err.Error()) } return err }