package service import ( "context" "errors" "time" "github.com/golang-jwt/jwt/v5" "github.com/google/uuid" "golang.org/x/crypto/bcrypt" "tm/internal/domain" "tm/internal/infrastructure" "tm/pkg/logger" ) // CustomerAuthServiceImpl implements the CustomerAuthService interface type CustomerAuthServiceImpl struct { customerRepo domain.CustomerRepository companyRepo domain.CompanyRepository config *infrastructure.AuthConfig logger logger.Logger } // NewCustomerAuthService creates a new customer authentication service func NewCustomerAuthService( customerRepo domain.CustomerRepository, companyRepo domain.CompanyRepository, config *infrastructure.AuthConfig, logger logger.Logger, ) domain.CustomerAuthService { return &CustomerAuthServiceImpl{ customerRepo: customerRepo, companyRepo: companyRepo, config: config, logger: logger, } } // Register creates a new customer account func (s *CustomerAuthServiceImpl) Register(ctx context.Context, req domain.CustomerRegisterRequest) (*domain.CustomerAuthResponse, error) { s.logger.Info("Registering new customer", map[string]interface{}{ "email": req.Email, }) // Check if customer already exists existingCustomer, err := s.customerRepo.GetByEmail(ctx, req.Email) if err == nil && existingCustomer != nil { return nil, errors.New("customer already exists") } // Hash password hashedPassword, err := s.hashPassword(req.Password) if err != nil { s.logger.Error("Failed to hash password", map[string]interface{}{ "error": err.Error(), }) return nil, errors.New("failed to process password") } // Handle company assignment var companyID uuid.UUID if req.CompanyID != "" { // Parse existing company ID companyID, err = uuid.Parse(req.CompanyID) if err != nil { return nil, errors.New("invalid company ID") } // Verify company exists _, err = s.companyRepo.GetByID(ctx, companyID) if err != nil { return nil, errors.New("company not found") } } else { // Create new company for customer company := &domain.Company{ ID: uuid.New(), Name: req.FirstName + " " + req.LastName + "'s Company", Description: "Personal company profile", Industry: "Not specified", Country: "Not specified", CreatedAt: time.Now(), UpdatedAt: time.Now(), } if err := s.companyRepo.Create(ctx, company); err != nil { s.logger.Error("Failed to create company for customer", map[string]interface{}{ "error": err.Error(), "email": req.Email, }) return nil, errors.New("failed to create company") } companyID = company.ID } // Create customer customer := &domain.Customer{ ID: uuid.New(), Email: req.Email, Password: hashedPassword, FirstName: req.FirstName, LastName: req.LastName, Phone: req.Phone, Role: domain.CustomerRoleUser, CompanyID: companyID, IsActive: true, IsVerified: false, // Require email verification DeviceTokens: make([]string, 0), Preferences: domain.CustomerPreferences{ Language: "en", NotificationSettings: domain.NotificationSettings{ NewTenders: true, DeadlineReminder: true, StatusUpdates: true, SystemAlerts: true, }, TenderAlerts: true, EmailNotifications: true, PushNotifications: true, PreferredCategories: make([]string, 0), }, CreatedAt: time.Now(), UpdatedAt: time.Now(), } if err := s.customerRepo.Create(ctx, customer); err != nil { s.logger.Error("Failed to create customer", map[string]interface{}{ "error": err.Error(), "email": req.Email, }) return nil, errors.New("failed to create customer") } // Generate tokens accessToken, err := s.generateAccessToken(customer) if err != nil { s.logger.Error("Failed to generate access token", map[string]interface{}{ "error": err.Error(), "customer_id": customer.ID.String(), }) return nil, errors.New("failed to generate access token") } refreshToken, err := s.generateRefreshToken(customer) if err != nil { s.logger.Error("Failed to generate refresh token", map[string]interface{}{ "error": err.Error(), "customer_id": customer.ID.String(), }) return nil, errors.New("failed to generate refresh token") } s.logger.Info("Customer registered successfully", map[string]interface{}{ "customer_id": customer.ID.String(), "email": customer.Email, "company_id": customer.CompanyID.String(), }) // TODO: Send verification email s.sendVerificationEmail(ctx, customer) return &domain.CustomerAuthResponse{ Customer: customer, AccessToken: accessToken, RefreshToken: refreshToken, ExpiresIn: int64(s.config.JWT.AccessTokenDuration.Seconds()), }, nil } // Login authenticates a customer and returns tokens func (s *CustomerAuthServiceImpl) Login(ctx context.Context, req domain.LoginRequest) (*domain.CustomerAuthResponse, error) { s.logger.Info("Customer login attempt", map[string]interface{}{ "email": req.Email, }) // Get customer by email customer, err := s.customerRepo.GetByEmail(ctx, req.Email) if err != nil { s.logger.Warn("Customer login failed - customer not found", map[string]interface{}{ "email": req.Email, }) return nil, errors.New("invalid credentials") } // Check if customer is active if !customer.IsActive { s.logger.Warn("Customer login failed - account inactive", map[string]interface{}{ "email": req.Email, "customer_id": customer.ID.String(), }) return nil, errors.New("account is inactive") } // Verify password if !s.verifyPassword(req.Password, customer.Password) { s.logger.Warn("Customer login failed - invalid password", map[string]interface{}{ "email": req.Email, "customer_id": customer.ID.String(), }) return nil, errors.New("invalid credentials") } // Update last login time customer.LastLoginAt = &time.Time{} *customer.LastLoginAt = time.Now() customer.UpdatedAt = time.Now() if err := s.customerRepo.Update(ctx, customer); err != nil { s.logger.Warn("Failed to update customer last login", map[string]interface{}{ "error": err.Error(), "customer_id": customer.ID.String(), }) // Don't fail login for this } // Generate tokens accessToken, err := s.generateAccessToken(customer) if err != nil { s.logger.Error("Failed to generate access token", map[string]interface{}{ "error": err.Error(), "customer_id": customer.ID.String(), }) return nil, errors.New("failed to generate access token") } refreshToken, err := s.generateRefreshToken(customer) if err != nil { s.logger.Error("Failed to generate refresh token", map[string]interface{}{ "error": err.Error(), "customer_id": customer.ID.String(), }) return nil, errors.New("failed to generate refresh token") } s.logger.Info("Customer logged in successfully", map[string]interface{}{ "customer_id": customer.ID.String(), "email": customer.Email, "is_verified": customer.IsVerified, }) return &domain.CustomerAuthResponse{ Customer: customer, AccessToken: accessToken, RefreshToken: refreshToken, ExpiresIn: int64(s.config.JWT.AccessTokenDuration.Seconds()), }, nil } // RefreshToken generates new tokens using refresh token func (s *CustomerAuthServiceImpl) RefreshToken(ctx context.Context, refreshToken string) (*domain.CustomerAuthResponse, error) { // Parse and validate refresh token token, err := jwt.Parse(refreshToken, func(token *jwt.Token) (interface{}, error) { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, errors.New("invalid signing method") } return []byte(s.config.JWT.Secret), nil }) if err != nil || !token.Valid { return nil, errors.New("invalid refresh token") } claims, ok := token.Claims.(jwt.MapClaims) if !ok { return nil, errors.New("invalid token claims") } // Verify token type tokenType, ok := claims["type"].(string) if !ok || tokenType != "refresh" { return nil, errors.New("invalid token type") } // Verify user type userType, ok := claims["user_type"].(string) if !ok || userType != "customer" { return nil, errors.New("invalid user type") } customerIDStr, ok := claims["customer_id"].(string) if !ok { return nil, errors.New("invalid customer ID in token") } customerID, err := uuid.Parse(customerIDStr) if err != nil { return nil, errors.New("invalid customer ID format") } // Get customer from database customer, err := s.customerRepo.GetByID(ctx, customerID) if err != nil { return nil, errors.New("customer not found") } if !customer.IsActive { return nil, errors.New("account is inactive") } // Generate new tokens accessToken, err := s.generateAccessToken(customer) if err != nil { return nil, errors.New("failed to generate access token") } newRefreshToken, err := s.generateRefreshToken(customer) if err != nil { return nil, errors.New("failed to generate refresh token") } return &domain.CustomerAuthResponse{ Customer: customer, AccessToken: accessToken, RefreshToken: newRefreshToken, ExpiresIn: int64(s.config.JWT.AccessTokenDuration.Seconds()), }, nil } // ValidateToken validates a JWT token and returns the customer func (s *CustomerAuthServiceImpl) ValidateToken(ctx context.Context, tokenString string) (*domain.Customer, error) { token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, errors.New("invalid signing method") } return []byte(s.config.JWT.Secret), nil }) if err != nil || !token.Valid { return nil, errors.New("invalid token") } claims, ok := token.Claims.(jwt.MapClaims) if !ok { return nil, errors.New("invalid token claims") } // Verify token type tokenType, ok := claims["type"].(string) if !ok || tokenType != "access" { return nil, errors.New("invalid token type") } // Verify user type userType, ok := claims["user_type"].(string) if !ok || userType != "customer" { return nil, errors.New("invalid user type") } customerIDStr, ok := claims["customer_id"].(string) if !ok { return nil, errors.New("invalid customer ID in token") } customerID, err := uuid.Parse(customerIDStr) if err != nil { return nil, errors.New("invalid customer ID format") } customer, err := s.customerRepo.GetByID(ctx, customerID) if err != nil { return nil, errors.New("customer not found") } if !customer.IsActive { return nil, errors.New("account is inactive") } return customer, nil } // ChangePassword changes customer password func (s *CustomerAuthServiceImpl) ChangePassword(ctx context.Context, customerID uuid.UUID, oldPassword, newPassword string) error { customer, err := s.customerRepo.GetByID(ctx, customerID) if err != nil { return errors.New("customer not found") } // Verify old password if !s.verifyPassword(oldPassword, customer.Password) { return errors.New("invalid old password") } // Hash new password hashedPassword, err := s.hashPassword(newPassword) if err != nil { return errors.New("failed to process new password") } // Update password customer.Password = hashedPassword customer.UpdatedAt = time.Now() if err := s.customerRepo.Update(ctx, customer); err != nil { s.logger.Error("Failed to update customer password", map[string]interface{}{ "error": err.Error(), "customer_id": customerID.String(), }) return errors.New("failed to update password") } s.logger.Info("Customer password changed successfully", map[string]interface{}{ "customer_id": customerID.String(), }) return nil } // ResetPassword initiates password reset process func (s *CustomerAuthServiceImpl) ResetPassword(ctx context.Context, email string) error { // TODO: Implement password reset logic // This would typically involve: // 1. Generate reset token // 2. Store token with expiration // 3. Send reset email return errors.New("password reset not implemented") } // VerifyEmail verifies customer's email address func (s *CustomerAuthServiceImpl) VerifyEmail(ctx context.Context, customerID uuid.UUID, token string) error { // TODO: Implement email verification logic // This would typically involve: // 1. Validate verification token // 2. Update customer verification status // 3. Send welcome email return errors.New("email verification not implemented") } // ResendVerification resends verification email func (s *CustomerAuthServiceImpl) ResendVerification(ctx context.Context, email string) error { customer, err := s.customerRepo.GetByEmail(ctx, email) if err != nil { return errors.New("customer not found") } if customer.IsVerified { return errors.New("email already verified") } // TODO: Send verification email s.sendVerificationEmail(ctx, customer) return nil } // Private helper methods func (s *CustomerAuthServiceImpl) hashPassword(password string) (string, error) { hashedBytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) if err != nil { return "", err } return string(hashedBytes), nil } func (s *CustomerAuthServiceImpl) verifyPassword(password, hashedPassword string) bool { err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password)) return err == nil } func (s *CustomerAuthServiceImpl) generateAccessToken(customer *domain.Customer) (string, error) { claims := jwt.MapClaims{ "customer_id": customer.ID.String(), "email": customer.Email, "role": customer.Role, "company_id": customer.CompanyID.String(), "user_type": "customer", "exp": time.Now().Add(s.config.JWT.AccessTokenDuration).Unix(), "iat": time.Now().Unix(), "type": "access", } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) return token.SignedString([]byte(s.config.JWT.Secret)) } func (s *CustomerAuthServiceImpl) generateRefreshToken(customer *domain.Customer) (string, error) { claims := jwt.MapClaims{ "customer_id": customer.ID.String(), "user_type": "customer", "exp": time.Now().Add(s.config.JWT.RefreshTokenDuration).Unix(), "iat": time.Now().Unix(), "type": "refresh", } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) return token.SignedString([]byte(s.config.JWT.Secret)) } func (s *CustomerAuthServiceImpl) sendVerificationEmail(ctx context.Context, customer *domain.Customer) { // TODO: Implement email sending logic s.logger.Info("Verification email would be sent", map[string]interface{}{ "customer_id": customer.ID.String(), "email": customer.Email, }) }