Files
aggios.app/backend/internal/service/auth_service.go
2025-12-08 21:47:38 -03:00

171 lines
4.1 KiB
Go

package service
import (
"errors"
"time"
"aggios-app/backend/internal/config"
"aggios-app/backend/internal/domain"
"aggios-app/backend/internal/repository"
"github.com/golang-jwt/jwt/v5"
"github.com/google/uuid"
"golang.org/x/crypto/bcrypt"
)
var (
ErrEmailAlreadyExists = errors.New("email already registered")
ErrInvalidCredentials = errors.New("invalid email or password")
ErrWeakPassword = errors.New("password too weak")
ErrSubdomainTaken = errors.New("subdomain already taken")
ErrUnauthorized = errors.New("unauthorized access")
)
// AuthService handles authentication business logic
type AuthService struct {
userRepo *repository.UserRepository
tenantRepo *repository.TenantRepository
cfg *config.Config
}
// NewAuthService creates a new auth service
func NewAuthService(userRepo *repository.UserRepository, tenantRepo *repository.TenantRepository, cfg *config.Config) *AuthService {
return &AuthService{
userRepo: userRepo,
tenantRepo: tenantRepo,
cfg: cfg,
}
}
// Register creates a new user account
func (s *AuthService) Register(req domain.CreateUserRequest) (*domain.User, error) {
// Validate password strength
if len(req.Password) < s.cfg.Security.PasswordMinLength {
return nil, ErrWeakPassword
}
// Check if email already exists
exists, err := s.userRepo.EmailExists(req.Email)
if err != nil {
return nil, err
}
if exists {
return nil, ErrEmailAlreadyExists
}
// Hash password
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
if err != nil {
return nil, err
}
// Create user
user := &domain.User{
Email: req.Email,
Password: string(hashedPassword),
Name: req.Name,
}
if err := s.userRepo.Create(user); err != nil {
return nil, err
}
return user, nil
}
// Login authenticates a user and returns a JWT token
func (s *AuthService) Login(req domain.LoginRequest) (*domain.LoginResponse, error) {
// Find user by email
user, err := s.userRepo.FindByEmail(req.Email)
if err != nil {
return nil, err
}
if user == nil {
return nil, ErrInvalidCredentials
}
// Verify password
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.Password)); err != nil {
return nil, ErrInvalidCredentials
}
// Generate JWT token
token, err := s.generateToken(user)
if err != nil {
return nil, err
}
response := &domain.LoginResponse{
Token: token,
User: *user,
}
// If user has a tenant, get the subdomain
if user.TenantID != nil {
tenant, err := s.tenantRepo.FindByID(*user.TenantID)
if err == nil && tenant != nil {
response.Subdomain = &tenant.Subdomain
}
}
return response, nil
}
func (s *AuthService) generateToken(user *domain.User) (string, error) {
claims := jwt.MapClaims{
"user_id": user.ID.String(),
"email": user.Email,
"role": user.Role,
"tenant_id": nil,
"exp": time.Now().Add(time.Hour * 24 * 7).Unix(), // 7 days
}
if user.TenantID != nil {
claims["tenant_id"] = user.TenantID.String()
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte(s.cfg.JWT.Secret))
}
// ChangePassword changes a user's password
func (s *AuthService) ChangePassword(userID string, currentPassword, newPassword string) error {
// Validate new password strength
if len(newPassword) < s.cfg.Security.PasswordMinLength {
return ErrWeakPassword
}
// Parse userID
uid, err := parseUUID(userID)
if err != nil {
return ErrInvalidCredentials
}
// Find user
user, err := s.userRepo.FindByID(uid)
if err != nil {
return err
}
if user == nil {
return ErrInvalidCredentials
}
// Verify current password
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(currentPassword)); err != nil {
return ErrInvalidCredentials
}
// Hash new password
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost)
if err != nil {
return err
}
// Update password
return s.userRepo.UpdatePassword(userID, string(hashedPassword))
}
func parseUUID(s string) (uuid.UUID, error) {
return uuid.Parse(s)
}