package services import ( "context" "errors" "fmt" "time" "github.com/golang-jwt/jwt/v5" "github.com/google/uuid" "golang.org/x/crypto/bcrypt" "github.com/ren/econ/backend/internal/config" "github.com/ren/econ/backend/internal/models" "github.com/ren/econ/backend/internal/repository" ) var ( ErrInvalidCredentials = errors.New("credenciales inválidas") ErrUserNotFound = errors.New("usuario no encontrado") ErrUserInactive = errors.New("usuario inactivo") ErrInvalidToken = errors.New("token inválido") ErrTokenExpired = errors.New("token expirado") ) type AuthService struct { config *config.Config userRepo *repository.UserRepository } func NewAuthService(cfg *config.Config, userRepo *repository.UserRepository) *AuthService { return &AuthService{ config: cfg, userRepo: userRepo, } } type Claims struct { UserID uuid.UUID `json:"user_id"` Email string `json:"email"` Rol string `json:"rol"` jwt.RegisteredClaims } func (s *AuthService) Login(ctx context.Context, req *models.LoginRequest) (*models.LoginResponse, error) { var user *models.Usuario var err error // Buscar por email o username if req.Username != "" { user, err = s.userRepo.GetByUsername(ctx, req.Username) } else if req.Email != "" { user, err = s.userRepo.GetByEmail(ctx, req.Email) } else { return nil, ErrInvalidCredentials } if err != nil { return nil, ErrInvalidCredentials } if !user.Activo { return nil, ErrUserInactive } if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(req.Password)); err != nil { return nil, ErrInvalidCredentials } // Update last login s.userRepo.UpdateLastLogin(ctx, user.ID) // Generate tokens accessToken, err := s.generateAccessToken(user) if err != nil { return nil, fmt.Errorf("error generando access token: %w", err) } refreshToken, err := s.generateRefreshToken(user) if err != nil { return nil, fmt.Errorf("error generando refresh token: %w", err) } return &models.LoginResponse{ AccessToken: accessToken, RefreshToken: refreshToken, ExpiresIn: s.config.JWTExpirationHours * 60, User: user, }, nil } func (s *AuthService) RefreshToken(ctx context.Context, refreshToken string) (*models.LoginResponse, error) { claims, err := s.validateToken(refreshToken) if err != nil { return nil, ErrInvalidToken } user, err := s.userRepo.GetByID(ctx, claims.UserID) if err != nil { return nil, ErrUserNotFound } if !user.Activo { return nil, ErrUserInactive } accessToken, err := s.generateAccessToken(user) if err != nil { return nil, fmt.Errorf("error generando access token: %w", err) } newRefreshToken, err := s.generateRefreshToken(user) if err != nil { return nil, fmt.Errorf("error generando refresh token: %w", err) } return &models.LoginResponse{ AccessToken: accessToken, RefreshToken: newRefreshToken, ExpiresIn: s.config.JWTExpirationHours * 60, User: user, }, nil } func (s *AuthService) Logout(ctx context.Context, userID uuid.UUID) error { // In a more complete implementation, we would blacklist the tokens // For now, just return success return nil } func (s *AuthService) HashPassword(password string) (string, error) { hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) return string(hash), err } func (s *AuthService) generateAccessToken(user *models.Usuario) (string, error) { claims := Claims{ UserID: user.ID, Email: user.Email, Rol: user.Rol, RegisteredClaims: jwt.RegisteredClaims{ ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Duration(s.config.JWTExpirationHours) * time.Minute)), IssuedAt: jwt.NewNumericDate(time.Now()), NotBefore: jwt.NewNumericDate(time.Now()), Subject: user.ID.String(), }, } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) return token.SignedString([]byte(s.config.JWTSecret)) } func (s *AuthService) generateRefreshToken(user *models.Usuario) (string, error) { claims := Claims{ UserID: user.ID, Email: user.Email, Rol: user.Rol, RegisteredClaims: jwt.RegisteredClaims{ ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Duration(s.config.RefreshExpDays) * 24 * time.Hour)), IssuedAt: jwt.NewNumericDate(time.Now()), NotBefore: jwt.NewNumericDate(time.Now()), Subject: user.ID.String(), }, } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) return token.SignedString([]byte(s.config.JWTSecret)) } func (s *AuthService) validateToken(tokenString string) (*Claims, error) { token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) } return []byte(s.config.JWTSecret), nil }) if err != nil { if errors.Is(err, jwt.ErrTokenExpired) { return nil, ErrTokenExpired } return nil, ErrInvalidToken } claims, ok := token.Claims.(*Claims) if !ok || !token.Valid { return nil, ErrInvalidToken } return claims, nil } func (s *AuthService) ValidateToken(tokenString string) (*Claims, error) { return s.validateToken(tokenString) }