3e9877574c
- Introduced a flat structure for the customer domain, consolidating entities, aggregates, repositories, services, forms, and handlers into a single package. - Added core entities: Customer and CustomerAggregate. - Implemented request/response forms for customer authentication (Register, Login, Refresh Token, Change Password). - Created CustomerService to handle business logic and data access through the Repository interface. - Established CustomerHandler for HTTP request handling related to customer authentication. - Removed previous domain structure artifacts to streamline the codebase.
560 lines
16 KiB
Go
560 lines
16 KiB
Go
package customer
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"time"
|
|
infrastructure "tm/infra"
|
|
"tm/pkg/logger"
|
|
|
|
"github.com/golang-jwt/jwt/v5"
|
|
"github.com/google/uuid"
|
|
"golang.org/x/crypto/bcrypt"
|
|
)
|
|
|
|
// CustomerService implements the CustomerService interface
|
|
type CustomerService struct {
|
|
customerRepo Repository
|
|
config *infrastructure.AuthConfig
|
|
logger logger.Logger
|
|
}
|
|
|
|
// NewCustomerService creates a new customer service
|
|
func NewCustomerService(
|
|
customerRepo Repository,
|
|
config *infrastructure.AuthConfig,
|
|
logger logger.Logger,
|
|
) CustomerService {
|
|
return CustomerService{
|
|
customerRepo: customerRepo,
|
|
config: config,
|
|
logger: logger,
|
|
}
|
|
}
|
|
|
|
// GetCustomers retrieves customers with pagination (for panel users)
|
|
func (s *CustomerService) GetCustomers(ctx context.Context, limit, offset int) ([]*Customer, error) {
|
|
s.logger.Info("Retrieving customers", map[string]interface{}{
|
|
"limit": limit,
|
|
"offset": offset,
|
|
})
|
|
|
|
customers, err := s.customerRepo.List(ctx, limit, offset)
|
|
if err != nil {
|
|
s.logger.Error("Failed to retrieve customers", map[string]interface{}{
|
|
"error": err.Error(),
|
|
"limit": limit,
|
|
"offset": offset,
|
|
})
|
|
return nil, errors.New("failed to retrieve customers")
|
|
}
|
|
|
|
s.logger.Info("Customers retrieved successfully", map[string]interface{}{
|
|
"count": len(customers),
|
|
"limit": limit,
|
|
"offset": offset,
|
|
})
|
|
|
|
return customers, nil
|
|
}
|
|
|
|
// GetCustomerByID retrieves a customer by ID (for panel users)
|
|
func (s *CustomerService) GetCustomerByID(ctx context.Context, id uuid.UUID) (*Customer, error) {
|
|
s.logger.Info("Retrieving customer by ID", map[string]interface{}{
|
|
"customer_id": id.String(),
|
|
})
|
|
|
|
customer, err := s.customerRepo.GetByID(ctx, id)
|
|
if err != nil {
|
|
s.logger.Error("Failed to retrieve customer", map[string]interface{}{
|
|
"error": err.Error(),
|
|
"customer_id": id.String(),
|
|
})
|
|
return nil, errors.New("customer not found")
|
|
}
|
|
|
|
s.logger.Info("Customer retrieved successfully", map[string]interface{}{
|
|
"customer_id": customer.ID.String(),
|
|
"email": customer.Email,
|
|
})
|
|
|
|
return customer, nil
|
|
}
|
|
|
|
// GetCustomersByCompany retrieves customers by company ID with pagination (for panel users)
|
|
func (s *CustomerService) GetCustomersByCompany(ctx context.Context, companyID uuid.UUID, limit, offset int) ([]*Customer, error) {
|
|
s.logger.Info("Retrieving customers by company", map[string]interface{}{
|
|
"company_id": companyID.String(),
|
|
"limit": limit,
|
|
"offset": offset,
|
|
})
|
|
|
|
customers, err := s.customerRepo.GetByCompanyID(ctx, companyID, limit, offset)
|
|
if err != nil {
|
|
s.logger.Error("Failed to retrieve customers by company", map[string]interface{}{
|
|
"error": err.Error(),
|
|
"company_id": companyID.String(),
|
|
"limit": limit,
|
|
"offset": offset,
|
|
})
|
|
return nil, errors.New("failed to retrieve customers")
|
|
}
|
|
|
|
s.logger.Info("Customers by company retrieved successfully", map[string]interface{}{
|
|
"count": len(customers),
|
|
"company_id": companyID.String(),
|
|
"limit": limit,
|
|
"offset": offset,
|
|
})
|
|
|
|
return customers, nil
|
|
}
|
|
|
|
// UpdateCustomerStatus updates customer active status (for panel users)
|
|
func (s *CustomerService) UpdateCustomerStatus(ctx context.Context, customerID uuid.UUID, isActive bool, updatedBy uuid.UUID) error {
|
|
s.logger.Info("Updating customer status", map[string]interface{}{
|
|
"customer_id": customerID.String(),
|
|
"is_active": isActive,
|
|
"updated_by": updatedBy.String(),
|
|
})
|
|
|
|
// Get customer
|
|
customer, err := s.customerRepo.GetByID(ctx, customerID)
|
|
if err != nil {
|
|
s.logger.Error("Customer not found for status update", map[string]interface{}{
|
|
"error": err.Error(),
|
|
"customer_id": customerID.String(),
|
|
})
|
|
return errors.New("customer not found")
|
|
}
|
|
|
|
// Update status
|
|
customer.IsActive = isActive
|
|
|
|
// Save changes
|
|
if err := s.customerRepo.Update(ctx, customer); err != nil {
|
|
s.logger.Error("Failed to update customer status", map[string]interface{}{
|
|
"error": err.Error(),
|
|
"customer_id": customerID.String(),
|
|
"is_active": isActive,
|
|
"updated_by": updatedBy.String(),
|
|
})
|
|
return errors.New("failed to update customer status")
|
|
}
|
|
|
|
s.logger.Info("Customer status updated successfully", map[string]interface{}{
|
|
"customer_id": customerID.String(),
|
|
"is_active": isActive,
|
|
"updated_by": updatedBy.String(),
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
// Register creates a new customer account
|
|
func (s *CustomerService) Register(ctx context.Context, req RegisterForm) (*AuthorizationResponse, 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")
|
|
}
|
|
|
|
// Create customer
|
|
customer := &Customer{
|
|
ID: uuid.New(),
|
|
Email: req.Email,
|
|
Password: hashedPassword,
|
|
FirstName: req.FirstName,
|
|
LastName: req.LastName,
|
|
Mobile: req.Mobile,
|
|
IsActive: true,
|
|
IsVerified: false, // Require email verification
|
|
DeviceTokens: make([]string, 0),
|
|
UpdatedAt: time.Now().Unix(),
|
|
CreatedAt: time.Now().Unix(),
|
|
}
|
|
|
|
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 &AuthorizationResponse{
|
|
Customer: NewCustomerAggregate(*customer),
|
|
AccessToken: accessToken,
|
|
RefreshToken: refreshToken,
|
|
ExpiresIn: int64(s.config.JWT.AccessTokenDuration.Seconds()),
|
|
}, nil
|
|
}
|
|
|
|
// Login authenticates a customer and returns tokens
|
|
func (s *CustomerService) Login(ctx context.Context, req LoginForm) (*AuthorizationResponse, 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
|
|
now := time.Now().Unix()
|
|
customer.LastLoginAt = &now
|
|
customer.UpdatedAt = 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 &AuthorizationResponse{
|
|
Customer: NewCustomerAggregate(*customer),
|
|
AccessToken: accessToken,
|
|
RefreshToken: refreshToken,
|
|
ExpiresIn: int64(s.config.JWT.AccessTokenDuration.Seconds()),
|
|
}, nil
|
|
}
|
|
|
|
// RefreshToken generates new tokens using refresh token
|
|
func (s *CustomerService) RefreshToken(ctx context.Context, refreshToken string) (*AuthorizationResponse, 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 &AuthorizationResponse{
|
|
Customer: NewCustomerAggregate(*customer),
|
|
AccessToken: accessToken,
|
|
RefreshToken: newRefreshToken,
|
|
ExpiresIn: int64(s.config.JWT.AccessTokenDuration.Seconds()),
|
|
}, nil
|
|
}
|
|
|
|
// ValidateToken validates a JWT token and returns the customer
|
|
func (s *CustomerService) ValidateToken(ctx context.Context, tokenString string) (*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 *CustomerService) 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().Unix()
|
|
|
|
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 *CustomerService) 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 *CustomerService) 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 resend verification email
|
|
func (s *CustomerService) 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 *CustomerService) hashPassword(password string) (string, error) {
|
|
hashedBytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return string(hashedBytes), nil
|
|
}
|
|
|
|
func (s *CustomerService) verifyPassword(password, hashedPassword string) bool {
|
|
err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password))
|
|
return err == nil
|
|
}
|
|
|
|
func (s *CustomerService) generateAccessToken(customer *Customer) (string, error) {
|
|
claims := jwt.MapClaims{
|
|
"customer_id": customer.ID.String(),
|
|
"email": customer.Email,
|
|
"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 *CustomerService) generateRefreshToken(customer *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 *CustomerService) sendVerificationEmail(ctx context.Context, customer *Customer) {
|
|
// TODO: Implement email sending logic
|
|
_ = ctx
|
|
s.logger.Info("Verification email would be sent", map[string]interface{}{
|
|
"customer_id": customer.ID.String(),
|
|
"email": customer.Email,
|
|
})
|
|
}
|