slack-auth-1770277926/services/auth-api/internal/service/user.go
rdev-worker fd9bf961bb
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
build: /implement-feature auth-system --requirements 'User model with email/...
2026-02-05 07:59:55 +00:00

144 lines
3.7 KiB
Go

package service
import (
"context"
"time"
"github.com/google/uuid"
"git.threesix.ai/jordan/slack-auth-1770277926/pkg/auth"
"git.threesix.ai/jordan/slack-auth-1770277926/pkg/logging"
"git.threesix.ai/jordan/slack-auth-1770277926/services/auth-api/internal/domain"
"git.threesix.ai/jordan/slack-auth-1770277926/services/auth-api/internal/port"
)
// UserService handles user-related business logic.
type UserService struct {
repo port.UserRepository
jwtSecret []byte
jwtIssuer string
logger *logging.Logger
}
// NewUserService creates a new user service.
func NewUserService(repo port.UserRepository, jwtSecret []byte, jwtIssuer string, logger *logging.Logger) *UserService {
return &UserService{
repo: repo,
jwtSecret: jwtSecret,
jwtIssuer: jwtIssuer,
logger: logger.WithService("UserService"),
}
}
// RegisterInput contains the data needed to register a user.
type RegisterInput struct {
Email string
Password string
}
// RegisterOutput contains the result of user registration.
type RegisterOutput struct {
User *domain.User
Token string
}
// Register creates a new user with duplicate detection.
// Returns domain.ErrDuplicateUser if email already exists.
// Returns domain.ErrInvalidEmail if email is invalid.
// Returns domain.ErrInvalidPassword if password is invalid.
func (s *UserService) Register(ctx context.Context, input RegisterInput) (*RegisterOutput, error) {
// Check for duplicates
exists, err := s.repo.ExistsByEmail(ctx, input.Email)
if err != nil {
return nil, err
}
if exists {
return nil, domain.ErrDuplicateUser
}
// Generate new ID
id := domain.UserID(uuid.New().String())
// Create domain entity (validates email/password and hashes password)
user, err := domain.NewUser(id, input.Email, input.Password)
if err != nil {
return nil, err
}
// Persist
if err := s.repo.Create(ctx, user); err != nil {
return nil, err
}
// Generate JWT token
token, err := s.generateToken(user)
if err != nil {
s.logger.Error("failed to generate token after registration", "error", err, "user_id", id)
return nil, err
}
s.logger.Info("user registered", "id", id, "email", input.Email)
return &RegisterOutput{
User: user,
Token: token,
}, nil
}
// LoginInput contains the data needed to authenticate a user.
type LoginInput struct {
Email string
Password string
}
// LoginOutput contains the result of user authentication.
type LoginOutput struct {
User *domain.User
Token string
}
// Login authenticates a user and returns a JWT token.
// Returns domain.ErrInvalidCredentials if credentials are incorrect.
func (s *UserService) Login(ctx context.Context, input LoginInput) (*LoginOutput, error) {
// Find user by email
user, err := s.repo.GetByEmail(ctx, input.Email)
if err != nil {
if err == domain.ErrUserNotFound {
return nil, domain.ErrInvalidCredentials
}
return nil, err
}
// Verify password
if !user.CheckPassword(input.Password) {
return nil, domain.ErrInvalidCredentials
}
// Generate JWT token
token, err := s.generateToken(user)
if err != nil {
s.logger.Error("failed to generate token", "error", err, "user_id", user.ID)
return nil, err
}
s.logger.Info("user logged in", "id", user.ID, "email", input.Email)
return &LoginOutput{
User: user,
Token: token,
}, nil
}
// GetByID returns a user by ID.
// Returns domain.ErrUserNotFound if not found.
func (s *UserService) GetByID(ctx context.Context, id domain.UserID) (*domain.User, error) {
return s.repo.Get(ctx, id)
}
// generateToken creates a JWT token for the user.
func (s *UserService) generateToken(user *domain.User) (string, error) {
authUser := &auth.User{
ID: user.ID.String(),
Email: user.Email,
}
return auth.GenerateTokenWithIssuer(s.jwtSecret, authUser, 24*time.Hour, s.jwtIssuer, s.jwtIssuer)
}