package usecase import ( "context" "errors" "strings" "time" "github.com/golang-jwt/jwt/v5" "github.com/gofrs/uuid/v5" "golang.org/x/crypto/bcrypt" "github.com/saveinmed/backend-go/internal/domain" ) // Login validates credentials via username or email and returns a signed JWT. func (s *Service) Login(ctx context.Context, identifier, password string) (string, time.Time, error) { if strings.TrimSpace(identifier) == "" { return "", time.Time{}, errors.New("identifier is required") } user, err := s.repo.GetUserByUsername(ctx, identifier) if err != nil { user, err = s.repo.GetUserByEmail(ctx, identifier) if err != nil { return "", time.Time{}, errors.New("invalid credentials") } } if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(s.pepperPassword(password))); err != nil { return "", time.Time{}, errors.New("invalid credentials") } return s.issueAccessToken(user) } // Authenticate is an alias for Login kept for backward compatibility. func (s *Service) Authenticate(ctx context.Context, identifier, password string) (string, time.Time, error) { user, err := s.repo.GetUserByUsername(ctx, identifier) if err != nil { user, err = s.repo.GetUserByEmail(ctx, identifier) if err != nil { return "", time.Time{}, errors.New("invalid credentials") } } if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(s.pepperPassword(password))); err != nil { return "", time.Time{}, errors.New("invalid credentials") } if user.CompanyID != uuid.Nil { if _, err := s.repo.GetCompany(ctx, user.CompanyID); err != nil { return "", time.Time{}, errors.New("associated company not found") } } return s.issueAccessToken(user) } // RegisterAccount creates the company (if provided) and the user bound to it. func (s *Service) RegisterAccount(ctx context.Context, company *domain.Company, user *domain.User, password string) error { if company != nil { if company.ID == uuid.Nil { company.ID = uuid.Must(uuid.NewV7()) company.IsVerified = false if err := s.repo.CreateCompany(ctx, company); err != nil { return err } } else { if _, err := s.repo.GetCompany(ctx, company.ID); err != nil { return err } } user.CompanyID = company.ID } return s.CreateUser(ctx, user, password) } // RefreshToken validates the provided JWT and issues a new access token. func (s *Service) RefreshToken(ctx context.Context, tokenStr string) (string, time.Time, error) { claims, err := s.parseToken(tokenStr) if err != nil { return "", time.Time{}, err } if scope, ok := claims["scope"].(string); ok && scope != "" { return "", time.Time{}, errors.New("invalid token scope") } sub, ok := claims["sub"].(string) if !ok || sub == "" { return "", time.Time{}, errors.New("invalid token subject") } userID, err := uuid.FromString(sub) if err != nil { return "", time.Time{}, errors.New("invalid token subject") } user, err := s.repo.GetUser(ctx, userID) if err != nil { return "", time.Time{}, err } return s.issueAccessToken(user) } // CreatePasswordResetToken generates a short-lived JWT for password reset. func (s *Service) CreatePasswordResetToken(ctx context.Context, email string) (string, time.Time, error) { user, err := s.repo.GetUserByEmail(ctx, email) if err != nil { return "", time.Time{}, err } expiresAt := time.Now().Add(passwordResetTTL) claims := jwt.MapClaims{ "sub": user.ID.String(), "scope": "password_reset", } signed, err := s.signToken(claims, expiresAt) if err != nil { return "", time.Time{}, err } return signed, expiresAt, nil } // ResetPassword validates the reset token and updates the user password. func (s *Service) ResetPassword(ctx context.Context, tokenStr, newPassword string) error { claims, err := s.parseToken(tokenStr) if err != nil { return err } scope, _ := claims["scope"].(string) if scope != "password_reset" { return errors.New("invalid token scope") } sub, ok := claims["sub"].(string) if !ok || sub == "" { return errors.New("invalid token subject") } userID, err := uuid.FromString(sub) if err != nil { return errors.New("invalid token subject") } user, err := s.repo.GetUser(ctx, userID) if err != nil { return err } return s.UpdateUser(ctx, user, newPassword) } // VerifyEmail marks the user email as verified using a scoped JWT. func (s *Service) VerifyEmail(ctx context.Context, tokenStr string) (*domain.User, error) { claims, err := s.parseToken(tokenStr) if err != nil { return nil, err } if scope, ok := claims["scope"].(string); ok && scope != "" && scope != "email_verify" { return nil, errors.New("invalid token scope") } sub, ok := claims["sub"].(string) if !ok || sub == "" { return nil, errors.New("invalid token subject") } userID, err := uuid.FromString(sub) if err != nil { return nil, errors.New("invalid token subject") } user, err := s.repo.GetUser(ctx, userID) if err != nil { return nil, err } if !user.EmailVerified { user.EmailVerified = true if err := s.repo.UpdateUser(ctx, user); err != nil { return nil, err } } return user, nil } // CreateAdminIfMissing idempotently creates the platform admin account. func (s *Service) CreateAdminIfMissing(ctx context.Context, email, username, password string) error { if _, err := s.repo.GetUserByEmail(ctx, email); err == nil { return nil } return s.CreateUser(ctx, &domain.User{ Role: domain.RoleAdmin, Name: "Administrador", Username: username, Email: email, EmailVerified: true, }, password) } // CreateLojistaIfMissing idempotently creates an owner seed account. func (s *Service) CreateLojistaIfMissing(ctx context.Context, email, username, password, cnpj, corporateName string) error { if _, err := s.repo.GetUserByEmail(ctx, email); err == nil { return nil } company := &domain.Company{ ID: uuid.Must(uuid.NewV7()), CNPJ: cnpj, CorporateName: corporateName, Category: "distribuidora", IsVerified: true, } user := &domain.User{ Role: domain.RoleOwner, Name: corporateName + " Admin", Username: username, Email: email, EmailVerified: true, } return s.RegisterAccount(ctx, company, user, password) } // issueAccessToken signs a JWT with user identity claims. func (s *Service) issueAccessToken(user *domain.User) (string, time.Time, error) { expiresAt := time.Now().Add(s.tokenTTL) claims := jwt.MapClaims{ "sub": user.ID.String(), "role": user.Role, "company_id": user.CompanyID.String(), } signed, err := s.signToken(claims, expiresAt) if err != nil { return "", time.Time{}, err } return signed, expiresAt, nil } // signToken appends expiry and produces a signed HS256 JWT. func (s *Service) signToken(claims jwt.MapClaims, expiresAt time.Time) (string, error) { claims["exp"] = expiresAt.Unix() token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) return token.SignedString(s.jwtSecret) } // parseToken validates and decodes a JWT signed by this service. func (s *Service) parseToken(tokenStr string) (jwt.MapClaims, error) { if strings.TrimSpace(tokenStr) == "" { return nil, errors.New("token is required") } claims := jwt.MapClaims{} parser := jwt.NewParser(jwt.WithValidMethods([]string{jwt.SigningMethodHS256.Alg()})) token, err := parser.ParseWithClaims(tokenStr, claims, func(token *jwt.Token) (any, error) { return s.jwtSecret, nil }) if err != nil || !token.Valid { return nil, errors.New("invalid token") } return claims, nil } // pepperPassword appends the configured pepper to a raw password before hashing. func (s *Service) pepperPassword(password string) string { if s.passwordPepper == "" { return password } return password + s.passwordPepper }