Files
tm_back/internal/customer/service/profile.go
T

422 lines
12 KiB
Go

package customer
import (
"context"
"errors"
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/google/uuid"
"golang.org/x/crypto/bcrypt"
"tm/internal/customer/domain/aggregate"
"tm/internal/customer/domain/entity"
)
// Register creates a new customer account
func (s *CustomerService) Register(ctx context.Context, req RegisterForm) (*aggregate.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 := &entity.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(),
CreatedAt: 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 &aggregate.AuthorizationResponse{
Customer: aggregate.NewCustomer(*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) (*aggregate.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
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 &aggregate.AuthorizationResponse{
Customer: aggregate.NewCustomer(*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) (*aggregate.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 &aggregate.AuthorizationResponse{
Customer: aggregate.NewCustomer(*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) (*entity.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()
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 *entity.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 *entity.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 *entity.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,
})
}