Files
tm_back/internal/customer/service.go
T
n.nakhostin dfcdef60d6 Add Device Token Support for Customer and User Entities
- Introduced a new DeviceToken field in both Customer and User entities to store device tokens for authentication and notifications.
- Updated LoginForm structures for both Customer and User to include an optional DeviceToken field, allowing clients to send device tokens during login.
- Enhanced the Login methods in the customer and user services to append the device token to the respective entities if it does not already exist, improving user session management and notification capabilities.
- Ensured proper validation and structured responses for the new DeviceToken field in the forms, maintaining compliance with best practices for API design.
2025-09-20 10:11:36 +03:30

1125 lines
34 KiB
Go

package customer
import (
"context"
"crypto/rand"
"encoding/json"
"errors"
"fmt"
"math/big"
"slices"
"strings"
"time"
"tm/internal/company"
"tm/pkg/logger"
"tm/pkg/notification"
"tm/pkg/redis"
"tm/pkg/response"
"tm/pkg/authorization"
"go.mongodb.org/mongo-driver/v2/bson"
"golang.org/x/crypto/bcrypt"
)
// Service defines business logic for customer operations
type Service interface {
// Core customer operations
Register(ctx context.Context, form *CreateCustomerForm) (*CustomerResponse, error)
GetByID(ctx context.Context, id string) (*CustomerResponse, error)
Search(ctx context.Context, form *SearchCustomersForm, pagination *response.Pagination) (*CustomerListResponse, error)
Update(ctx context.Context, id string, form *UpdateCustomerForm) (*CustomerResponse, error)
Delete(ctx context.Context, id string) error
UpdateStatus(ctx context.Context, id string, form *UpdateStatusForm) error
// Authentication operations
Login(ctx context.Context, form *LoginForm) (*AuthResponse, error)
RefreshToken(ctx context.Context, refreshToken string) (*AuthResponse, error)
GetProfileWithCompanies(ctx context.Context, id string) (*CustomerResponse, error)
Logout(ctx context.Context, id string, accessToken string) error
// Forgot password operations
RequestResetPassword(ctx context.Context, form *RequestResetPasswordForm) (*RequestResetPasswordResponse, error)
VerifyOTP(ctx context.Context, form *VerifyOTPForm) (*VerifyOTPResponse, error)
ResetPassword(ctx context.Context, form *ResetPasswordForm) (*ResetPasswordResponse, error)
// Role assignment operations
AssignRole(ctx context.Context, customerID string, form *AssignRoleForm) (*AssignRoleResponse, error)
}
// customerService implements the Service interface
type customerService struct {
repository Repository
logger logger.Logger
authService authorization.AuthorizationService
companyService company.Service
redisClient redis.Client
notification notification.SDK
}
// New creates a new customer service
func New(repository Repository, logger logger.Logger, authService authorization.AuthorizationService, companyService company.Service, redisClient redis.Client, notificationSDK notification.SDK) Service {
return &customerService{
repository: repository,
logger: logger,
authService: authService,
companyService: companyService,
redisClient: redisClient,
notification: notificationSDK,
}
}
// Register creates a new customer
func (s *customerService) Register(ctx context.Context, form *CreateCustomerForm) (*CustomerResponse, error) {
// Check if email already exists
existingCustomer, _ := s.repository.GetByEmail(ctx, form.Email)
if existingCustomer != nil {
return nil, errors.New("customer with this email already exists")
}
// Verify that all company IDs exist
var companyIDs []string
if len(form.CompanyIDs) > 0 {
for _, companyID := range form.CompanyIDs {
_, err := s.companyService.GetByID(ctx, companyID)
if err != nil {
s.logger.Error("Invalid company ID provided", map[string]interface{}{
"error": err.Error(),
"company_id": companyID,
})
return nil, errors.New("invalid company ID: " + companyID)
}
}
companyIDs = form.CompanyIDs
}
// Hash password
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(form.Password), bcrypt.DefaultCost)
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{
Type: CustomerType(form.Type),
Role: CustomerRole(form.Role),
Status: CustomerStatusActive,
CompanyIDs: companyIDs,
FullName: form.FullName,
Username: form.Username,
Email: form.Email,
Password: string(hashedPassword),
Phone: form.Phone,
}
// Save to database
err = s.repository.Register(ctx, customer)
if err != nil {
s.logger.Error("Failed to create customer", map[string]interface{}{
"error": err.Error(),
"email": form.Email,
"username": form.Username,
})
return nil, err
}
s.logger.Info("Customer created successfully", map[string]interface{}{
"customer_id": customer.ID,
"email": customer.Email,
"type": customer.Type,
"company_ids": companyIDs,
})
go s.notification.SendEmail(ctx, customer.Email, fmt.Sprintf("Welcome to Tender Management\nUsername: %s\nPassword: %s", customer.Username, form.Password), customer.ID.Hex())
return customer.ToResponse(nil), nil
}
// GetByID retrieves a customer by ID
func (s *customerService) GetByID(ctx context.Context, id string) (*CustomerResponse, error) {
customer, err := s.repository.GetByID(ctx, id)
if err != nil {
s.logger.Error("Failed to get customer by ID", map[string]interface{}{
"error": err.Error(),
"customer_id": id,
})
return nil, err
}
var companies []*CompanySummary
if len(customer.CompanyIDs) > 0 {
companies, err = s.loadCompaniesForCustomer(ctx, customer)
if err != nil {
s.logger.Error("Failed to load companies for customer", map[string]interface{}{
"error": err.Error(),
"customer_id": customer.ID,
})
}
}
s.logger.Info("Customer retrieved successfully", map[string]interface{}{
"customer_id": customer.ID,
"email": customer.Email,
})
return customer.ToResponse(companies), nil
}
// Search searches for customers
func (s *customerService) Search(ctx context.Context, form *SearchCustomersForm, pagination *response.Pagination) (*CustomerListResponse, error) {
customers, total, err := s.repository.Search(ctx, form, pagination)
if err != nil {
s.logger.Error("Failed to search customers", map[string]interface{}{
"error": err.Error(),
})
return nil, errors.New("failed to search customers")
}
var customerResponses []*CustomerResponse
for _, customer := range customers {
companies, err := s.loadCompaniesForCustomer(ctx, &customer)
if err != nil {
s.logger.Error("Failed to load companies for customer", map[string]interface{}{
"error": err.Error(),
"customer_id": customer.ID,
})
}
customerResponses = append(customerResponses, customer.ToResponse(companies))
}
return &CustomerListResponse{
Customers: customerResponses,
Meta: pagination.Response(total),
}, nil
}
// Update updates a customer
func (s *customerService) Update(ctx context.Context, id string, form *UpdateCustomerForm) (*CustomerResponse, error) {
// Get existing customer
customer, err := s.repository.GetByID(ctx, id)
if err != nil {
s.logger.Error("Failed to get customer for update", map[string]interface{}{
"error": err.Error(),
"customer_id": id,
})
return nil, errors.New("customer not found")
}
// Check if email already exists (if changing email)
if form.Email != "" && form.Email != customer.Email {
existingCustomer, _ := s.repository.GetByEmail(ctx, form.Email)
if existingCustomer != nil {
return nil, errors.New("customer with this email already exists")
}
customer.Email = form.Email
}
// Check if username already exists (if changing username)
if form.Username != "" && form.Username != customer.Username {
existingCustomer, _ := s.repository.GetByUsername(ctx, form.Username)
if existingCustomer != nil {
return nil, errors.New("customer with this username already exists")
}
customer.Username = form.Username
}
// Handle company assignments
if len(form.CompanyIDs) > 0 {
// Verify that all company IDs exist
var validCompanyIDs []string
for _, companyID := range form.CompanyIDs {
_, err := s.companyService.GetByID(ctx, companyID)
if err != nil {
s.logger.Error("Invalid company ID provided in update", map[string]interface{}{
"error": err.Error(),
"company_id": companyID,
})
return nil, errors.New("invalid company ID: " + companyID)
}
validCompanyIDs = append(validCompanyIDs, companyID)
}
customer.CompanyIDs = validCompanyIDs
}
if form.FullName != nil {
customer.FullName = form.FullName
}
if form.Phone != nil {
customer.Phone = form.Phone
}
// Update role if provided
if form.Role != "" {
customer.Role = CustomerRole(form.Role)
}
// Update in database
err = s.repository.Update(ctx, customer)
if err != nil {
s.logger.Error("Failed to update customer", map[string]interface{}{
"error": err.Error(),
"customer_id": id,
})
return nil, errors.New("failed to update customer")
}
s.logger.Info("Customer updated successfully", map[string]interface{}{
"customer_id": customer.ID,
"email": customer.Email,
})
return customer.ToResponse(nil), nil
}
// Delete deletes a customer (soft delete)
func (s *customerService) Delete(ctx context.Context, id string) error {
// Get customer to check if exists
_, err := s.repository.GetByID(ctx, id)
if err != nil {
s.logger.Error("Failed to get customer for delete", map[string]interface{}{
"error": err.Error(),
"customer_id": id,
})
return errors.New("customer not found")
}
// Delete customer (soft delete)
err = s.repository.Delete(ctx, id)
if err != nil {
s.logger.Error("Failed to delete customer", map[string]interface{}{
"error": err.Error(),
"customer_id": id,
})
return errors.New("failed to delete customer")
}
s.logger.Info("Customer deleted successfully", map[string]interface{}{
"customer_id": id,
})
return nil
}
// UpdateStatus updates customer status
func (s *customerService) UpdateStatus(ctx context.Context, id string, form *UpdateStatusForm) error {
// Get customer to check if exists
customer, err := s.repository.GetByID(ctx, id)
if err != nil {
return errors.New("customer not found")
}
// Update status
status := CustomerStatus(form.Status)
err = s.repository.UpdateStatus(ctx, id, status)
if err != nil {
s.logger.Error("Failed to update customer status", map[string]interface{}{
"error": err.Error(),
"customer_id": id,
"status": form.Status,
})
return errors.New("failed to update customer status")
}
s.logger.Info("Customer status updated successfully", map[string]interface{}{
"customer_id": id,
"status": form.Status,
})
go s.notification.SendEmail(ctx, customer.Email, fmt.Sprintf("Your account has been %s", strings.ToUpper(form.Status)), customer.ID.Hex())
return nil
}
// AssignCompanies assigns companies to a customer
func (s *customerService) AssignCompanies(ctx context.Context, customerID string, companyIDs []string) error {
// Get customer to check if exists
customer, err := s.repository.GetByID(ctx, customerID)
if err != nil {
return errors.New("customer not found")
}
// Verify companies exist and update customer's company IDs
var validCompanyIDs []string
for _, companyID := range companyIDs {
_, err = s.companyService.GetByID(ctx, companyID)
if err != nil {
s.logger.Error("Company not found during assignment", map[string]interface{}{
"error": err.Error(),
"customer_id": customerID,
"company_id": companyID,
})
continue // Skip invalid company IDs
}
validCompanyIDs = append(validCompanyIDs, companyID)
}
// Update customer's company IDs
customer.CompanyIDs = append(customer.CompanyIDs, validCompanyIDs...)
customer.UpdatedAt = time.Now().Unix()
err = s.repository.Update(ctx, customer)
if err != nil {
s.logger.Error("Failed to update customer with company assignments", map[string]interface{}{
"error": err.Error(),
"customer_id": customerID,
})
return errors.New("failed to assign companies to customer")
}
s.logger.Info("Companies assigned to customer successfully", map[string]interface{}{
"customer_id": customerID,
"company_ids": validCompanyIDs,
})
return nil
}
// RemoveCompaniesFromCustomer removes companies from a customer
func (s *customerService) RemoveCompanies(ctx context.Context, customerID string, companyIDs []string) error {
// Get customer to check if exists
customer, err := s.repository.GetByID(ctx, customerID)
if err != nil {
return errors.New("customer not found")
}
// Remove company IDs from customer's list
var updatedCompanyIDs []string
for _, existingID := range customer.CompanyIDs {
shouldRemove := false
for _, removeID := range companyIDs {
if existingID == removeID {
shouldRemove = true
break
}
}
if !shouldRemove {
updatedCompanyIDs = append(updatedCompanyIDs, existingID)
}
}
// Update customer's company IDs
customer.CompanyIDs = updatedCompanyIDs
customer.UpdatedAt = time.Now().Unix()
err = s.repository.Update(ctx, customer)
if err != nil {
s.logger.Error("Failed to update customer after removing company assignments", map[string]interface{}{
"error": err.Error(),
"customer_id": customerID,
})
return errors.New("failed to remove companies from customer")
}
s.logger.Info("Companies removed from customer successfully", map[string]interface{}{
"customer_id": customerID,
"company_ids": companyIDs,
})
return nil
}
// Suspend suspends a customer
func (s *customerService) Suspend(ctx context.Context, id string, reason string) error {
// Get customer to check if exists
_, err := s.repository.GetByID(ctx, id)
if err != nil {
return errors.New("customer not found")
}
// Update status to suspended
err = s.repository.UpdateStatus(ctx, id, CustomerStatusSuspended)
if err != nil {
s.logger.Error("Failed to suspend customer", map[string]interface{}{
"error": err.Error(),
"customer_id": id,
})
return errors.New("failed to suspend customer")
}
s.logger.Info("Customer suspended successfully", map[string]interface{}{
"customer_id": id,
"reason": reason,
})
return nil
}
// Activate activates a customer
func (s *customerService) Activate(ctx context.Context, id string) error {
// Get customer to check if exists
_, err := s.repository.GetByID(ctx, id)
if err != nil {
return errors.New("customer not found")
}
// Update status to active
err = s.repository.UpdateStatus(ctx, id, CustomerStatusActive)
if err != nil {
s.logger.Error("Failed to activate customer", map[string]interface{}{
"error": err.Error(),
"customer_id": id,
})
return errors.New("failed to activate customer")
}
s.logger.Info("Customer activated successfully", map[string]interface{}{
"customer_id": id,
})
return nil
}
// Login handles customer authentication
func (s *customerService) Login(ctx context.Context, form *LoginForm) (*AuthResponse, error) {
s.logger.Info("Customer login attempt", map[string]interface{}{
"username": form.Username,
})
// Get customer by username
customer, err := s.repository.GetByUsername(ctx, form.Username)
if err != nil {
s.logger.Warn("Customer login failed - username not found", map[string]interface{}{
"username": form.Username,
})
return nil, errors.New("invalid credentials")
}
// Check if customer is active
if customer.Status != CustomerStatusActive {
s.logger.Warn("Customer login failed - account not active", map[string]interface{}{
"username": form.Username,
"customer_id": customer.ID,
"status": string(customer.Status),
})
return nil, errors.New("account is not active")
}
// Verify password
err = bcrypt.CompareHashAndPassword([]byte(customer.Password), []byte(form.Password))
if err != nil {
s.logger.Warn("Customer login failed - wrong password", map[string]interface{}{
"customer_id": customer.ID,
"email": customer.Email,
})
return nil, errors.New("invalid credentials")
}
// Generate JWT tokens using authorization service
companyID := ""
if len(customer.CompanyIDs) > 0 {
companyID = customer.CompanyIDs[0] // Use first company ID for JWT token
}
tokenPair, err := s.authService.GenerateTokenPair(
customer.ID.Hex(),
customer.Email,
string(customer.Role), // Use customer's actual role
companyID,
)
if err != nil {
s.logger.Error("Failed to generate JWT tokens", map[string]interface{}{
"error": err.Error(),
"customer_id": customer.ID,
})
return nil, errors.New("failed to generate authentication tokens")
}
// append device token to customer if not exists
if !slices.Contains(customer.DeviceToken, form.DeviceToken) {
customer.DeviceToken = append(customer.DeviceToken, form.DeviceToken)
}
err = s.repository.Login(ctx, customer.ID.Hex())
if err != nil {
s.logger.Error("Failed to update customer last login", map[string]interface{}{
"error": err.Error(),
"customer_id": customer.ID,
})
}
companies, err := s.loadCompaniesForCustomer(ctx, customer)
if err != nil {
s.logger.Error("Failed to load companies for customer", map[string]interface{}{
"error": err.Error(),
"customer_id": customer.ID,
})
}
s.logger.Info("Customer login successful", map[string]interface{}{
"username": form.Username,
"customer_id": customer.ID,
"customer_type": string(customer.Type),
})
return &AuthResponse{
Customer: customer.ToResponse(companies),
AccessToken: tokenPair.AccessToken,
RefreshToken: tokenPair.RefreshToken,
ExpiresAt: time.Now().Add(15 * time.Minute).Unix(), // 15 minutes from now
}, nil
}
// RefreshToken handles token refresh for customers
func (s *customerService) RefreshToken(ctx context.Context, refreshToken string) (*AuthResponse, error) {
s.logger.Info("Customer token refresh attempt", map[string]interface{}{
"refresh_token": refreshToken[:10] + "...", // Log only first 10 chars for security
})
// Validate refresh token and generate new access token
newAccessToken, err := s.authService.RefreshAccessToken(refreshToken)
if err != nil {
s.logger.Warn("Failed to refresh access token", map[string]interface{}{
"error": err.Error(),
})
return nil, errors.New("invalid or expired refresh token")
}
// Parse the new access token to get customer information
validationResult, err := s.authService.ValidateAccessToken(newAccessToken)
if err != nil {
s.logger.Error("Failed to validate new access token", map[string]interface{}{
"error": err.Error(),
})
return nil, errors.New("failed to generate valid access token")
}
if !validationResult.Valid {
s.logger.Error("New access token validation failed", map[string]interface{}{
"error": validationResult.Error,
})
return nil, errors.New("failed to generate valid access token")
}
// Get customer information
customerID, err := bson.ObjectIDFromHex(validationResult.Claims.UserID)
if err != nil {
s.logger.Error("Failed to parse customer ID from token", map[string]interface{}{
"error": err.Error(),
})
return nil, errors.New("invalid token format")
}
customer, err := s.repository.GetByID(ctx, customerID.Hex())
if err != nil {
s.logger.Error("Failed to get customer for token refresh", map[string]interface{}{
"error": err.Error(),
"customer_id": customerID.Hex(),
})
return nil, errors.New("customer not found")
}
// Check if customer is still active
if customer.Status != CustomerStatusActive {
return nil, errors.New("customer account is inactive")
}
companies, err := s.loadCompaniesForCustomer(ctx, customer)
if err != nil {
s.logger.Error("Failed to load companies for customer", map[string]interface{}{
"error": err.Error(),
"customer_id": customer.ID,
})
}
s.logger.Info("Access token refreshed successfully", map[string]interface{}{
"customer_id": customer.ID,
"customer_type": string(customer.Type),
})
return &AuthResponse{
Customer: customer.ToResponse(companies),
AccessToken: newAccessToken,
RefreshToken: refreshToken, // Return the same refresh token
ExpiresAt: time.Now().Add(15 * time.Minute).Unix(), // 15 minutes from now
}, nil
}
// GetProfile retrieves customer profile information
func (s *customerService) GetProfile(ctx context.Context, customerID string) (*Customer, error) {
s.logger.Info("Getting customer profile", map[string]interface{}{
"customer_id": customerID,
})
customer, err := s.repository.GetByID(ctx, customerID)
if err != nil {
s.logger.Error("Failed to get customer profile", map[string]interface{}{
"error": err.Error(),
"customer_id": customerID,
})
return nil, errors.New("customer not found")
}
s.logger.Info("Customer profile retrieved successfully", map[string]interface{}{
"customer_id": customerID,
"customer_type": string(customer.Type),
})
return customer, nil
}
// GetProfileWithCompanies retrieves customer profile information including companies
func (s *customerService) GetProfileWithCompanies(ctx context.Context, customerID string) (*CustomerResponse, error) {
s.logger.Info("Getting customer profile with companies", map[string]interface{}{
"customer_id": customerID,
})
customer, err := s.repository.GetByID(ctx, customerID)
if err != nil {
s.logger.Error("Failed to get customer profile with companies", map[string]interface{}{
"error": err.Error(),
"customer_id": customerID,
})
return nil, errors.New("customer not found")
}
// Load companies for the customer
companies, err := s.loadCompaniesForCustomer(ctx, customer)
if err != nil {
s.logger.Error("Failed to load companies for customer profile", map[string]interface{}{
"error": err.Error(),
"customer_id": customer.ID,
})
// Continue without companies if loading fails
companies = []*CompanySummary{}
}
customerResponse := customer.ToResponse(companies)
s.logger.Info("Customer profile retrieved with companies successfully", map[string]interface{}{
"customer_id": customerID,
"customer_type": string(customer.Type),
})
return customerResponse, nil
}
// Logout handles customer logout
func (s *customerService) Logout(ctx context.Context, customerID string, accessToken string) error {
s.logger.Info("Customer logout", map[string]interface{}{
"customer_id": customerID,
"access_token": accessToken[:10] + "...", // Log only first 10 chars for security
})
// Invalidate the access token
err := s.authService.InvalidateToken(accessToken)
if err != nil {
s.logger.Error("Failed to invalidate access token", map[string]interface{}{
"error": err.Error(),
"customer_id": customerID,
})
// Don't return error here as the customer should still be logged out
}
s.logger.Info("Customer logout successful", map[string]interface{}{
"customer_id": customerID,
})
return nil
}
// RequestResetPassword handles password reset request
func (s *customerService) RequestResetPassword(ctx context.Context, form *RequestResetPasswordForm) (*RequestResetPasswordResponse, error) {
s.logger.Info("Password reset request", map[string]interface{}{
"email": form.Email,
})
// Check if customer exists
customer, err := s.repository.GetByEmail(ctx, form.Email)
if err != nil {
s.logger.Warn("Password reset request for non-existent email", map[string]interface{}{
"email": form.Email,
})
// Return success even if email doesn't exist for security
return &RequestResetPasswordResponse{
Message: "If the email exists, a password reset code has been sent",
Success: true,
}, nil
}
// Check if customer is active
if customer.Status != CustomerStatusActive {
s.logger.Warn("Password reset request for inactive customer", map[string]interface{}{
"email": form.Email,
"customer_id": customer.ID,
"status": string(customer.Status),
})
return &RequestResetPasswordResponse{
Message: "If the email exists, a password reset code has been sent",
Success: true,
}, nil
}
// Generate 6-digit OTP
otpCode, err := s.generateOTP()
if err != nil {
s.logger.Error("Failed to generate OTP", map[string]interface{}{
"error": err.Error(),
"email": form.Email,
})
return nil, errors.New("failed to generate verification code")
}
// Store OTP in Redis with 10 minutes expiry
otpKey := fmt.Sprintf("otp:%s", form.Email)
otpData := OTP{
Code: otpCode,
Email: form.Email,
ExpiresAt: time.Now().Add(10 * time.Minute).Unix(),
CreatedAt: time.Now().Unix(),
}
otpDataJSON, err := json.Marshal(otpData)
if err != nil {
s.logger.Error("Failed to marshal OTP data", map[string]interface{}{
"error": err.Error(),
"email": form.Email,
})
return nil, errors.New("failed to process reset request")
}
err = s.redisClient.Set(ctx, otpKey, string(otpDataJSON), 10*time.Minute)
if err != nil {
s.logger.Error("Failed to store OTP in Redis", map[string]interface{}{
"error": err.Error(),
"email": form.Email,
})
return nil, errors.New("failed to process reset request")
}
// Send OTP via email
go s.notification.SendEmail(ctx, form.Email, fmt.Sprintf("Your password reset code is: %s", otpCode), customer.ID.Hex())
s.logger.Info("Password reset OTP sent successfully", map[string]interface{}{
"email": form.Email,
"customer_id": customer.ID,
})
return &RequestResetPasswordResponse{
Message: "Password reset code sent to your email",
Success: true,
}, nil
}
// VerifyOTP handles OTP verification
func (s *customerService) VerifyOTP(ctx context.Context, form *VerifyOTPForm) (*VerifyOTPResponse, error) {
s.logger.Info("OTP verification attempt", map[string]interface{}{
"email": form.Email,
})
// Get OTP from Redis
otpKey := fmt.Sprintf("otp:%s", form.Email)
otpDataStr, err := s.redisClient.Get(ctx, otpKey)
if err != nil {
s.logger.Warn("OTP verification failed - OTP not found or expired", map[string]interface{}{
"email": form.Email,
})
return nil, errors.New("invalid or expired verification code")
}
var otpData OTP
err = json.Unmarshal([]byte(otpDataStr), &otpData)
if err != nil {
s.logger.Error("Failed to unmarshal OTP data", map[string]interface{}{
"error": err.Error(),
"email": form.Email,
})
return nil, errors.New("invalid verification code format")
}
if otpData.Code != form.Code {
s.logger.Warn("OTP verification failed - wrong code", map[string]interface{}{
"email": form.Email,
})
return nil, errors.New("invalid verification code")
}
// Generate reset token
resetToken, err := s.generateResetToken()
if err != nil {
s.logger.Error("Failed to generate reset token", map[string]interface{}{
"error": err.Error(),
"email": form.Email,
})
return nil, errors.New("failed to generate reset token")
}
// Store reset token in Redis with 30 minutes expiry
tokenKey := fmt.Sprintf("reset_token:%s", resetToken)
tokenData := ResetToken{
Token: resetToken,
Email: form.Email,
ExpiresAt: time.Now().Add(30 * time.Minute).Unix(),
CreatedAt: time.Now().Unix(),
}
tokenDataJSON, err := json.Marshal(tokenData)
if err != nil {
s.logger.Error("Failed to marshal reset token data", map[string]interface{}{
"error": err.Error(),
"email": form.Email,
})
return nil, errors.New("failed to process verification")
}
err = s.redisClient.Set(ctx, tokenKey, string(tokenDataJSON), 30*time.Minute)
if err != nil {
s.logger.Error("Failed to store reset token in Redis", map[string]interface{}{
"error": err.Error(),
"email": form.Email,
})
return nil, errors.New("failed to process verification")
}
// Delete OTP from Redis after successful verification
err = s.redisClient.Del(ctx, otpKey)
if err != nil {
s.logger.Warn("Failed to delete OTP after verification", map[string]interface{}{
"error": err.Error(),
"email": form.Email,
})
}
s.logger.Info("OTP verified successfully", map[string]interface{}{
"email": form.Email,
})
return &VerifyOTPResponse{
Message: "OTP verified successfully",
Token: resetToken,
Success: true,
}, nil
}
// ResetPassword handles password reset
func (s *customerService) ResetPassword(ctx context.Context, form *ResetPasswordForm) (*ResetPasswordResponse, error) {
s.logger.Info("Password reset attempt", map[string]interface{}{
"token": form.Token,
})
// Validate password strength
if !s.isValidPassword(form.NewPassword) {
return nil, errors.New("password must be at least 5 characters with uppercase, lowercase, and special character")
}
// Get reset token from Redis
tokenKey := fmt.Sprintf("reset_token:%s", form.Token)
tokenDataStr, err := s.redisClient.Get(ctx, tokenKey)
if err != nil {
s.logger.Warn("Password reset failed - invalid or expired token", map[string]interface{}{
"token": form.Token,
})
return nil, errors.New("invalid or expired reset token")
}
var tokenData ResetToken
err = json.Unmarshal([]byte(tokenDataStr), &tokenData)
if err != nil {
s.logger.Error("Failed to unmarshal reset token data", map[string]interface{}{
"error": err.Error(),
"token": form.Token,
})
return nil, errors.New("invalid reset token format")
}
if tokenData.Token != form.Token {
s.logger.Warn("Password reset failed - wrong token", map[string]interface{}{
"token": form.Token,
})
return nil, errors.New("invalid reset token")
}
// Get customer
customer, err := s.repository.GetByEmail(ctx, tokenData.Email)
if err != nil {
s.logger.Error("Password reset failed - customer not found", map[string]interface{}{
"error": err.Error(),
"email": tokenData.Email,
})
return nil, errors.New("customer not found")
}
// Hash new password
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(form.NewPassword), bcrypt.DefaultCost)
if err != nil {
s.logger.Error("Failed to hash new password", map[string]interface{}{
"error": err.Error(),
"email": tokenData.Email,
})
return nil, errors.New("failed to process new password")
}
// Update customer password
customer.Password = string(hashedPassword)
customer.UpdatedAt = time.Now().Unix()
err = s.repository.Update(ctx, customer)
if err != nil {
s.logger.Error("Failed to update customer password", map[string]interface{}{
"error": err.Error(),
"email": tokenData.Email,
"customer_id": customer.ID,
})
return nil, errors.New("failed to reset password")
}
// Delete reset token from Redis
err = s.redisClient.Del(ctx, tokenKey)
if err != nil {
s.logger.Warn("Failed to delete reset token after password reset", map[string]interface{}{
"error": err.Error(),
"email": tokenData.Email,
})
}
s.logger.Info("Password reset successfully", map[string]interface{}{
"email": tokenData.Email,
"customer_id": customer.ID,
})
go s.notification.SendEmail(ctx, tokenData.Email, "Your password has been reset successfully", customer.ID.Hex())
return &ResetPasswordResponse{
Message: "Password reset successfully",
Success: true,
}, nil
}
// generateOTP generates a 6-digit OTP
func (s *customerService) generateOTP() (string, error) {
// Generate 6 random digits
otp := ""
for i := 0; i < 6; i++ {
num, err := rand.Int(rand.Reader, big.NewInt(10))
if err != nil {
return "", err
}
otp += num.String()
}
return otp, nil
}
// generateResetToken generates a random reset token
func (s *customerService) generateResetToken() (string, error) {
// Generate 32 random bytes
tokenBytes := make([]byte, 32)
_, err := rand.Read(tokenBytes)
if err != nil {
return "", err
}
return fmt.Sprintf("%x", tokenBytes), nil
}
// isValidPassword validates password strength
func (s *customerService) isValidPassword(password string) bool {
if len(password) < 5 {
return false
}
hasUpper := false
hasLower := false
hasSpecial := false
for _, char := range password {
if char >= 'A' && char <= 'Z' {
hasUpper = true
} else if char >= 'a' && char <= 'z' {
hasLower = true
} else if char == '!' || char == '@' || char == '#' || char == '$' || char == '%' || char == '^' || char == '&' || char == '*' {
hasSpecial = true
}
}
return hasUpper && hasLower && hasSpecial
}
// loadCompaniesForCustomer loads company summaries for a customer
func (s *customerService) loadCompaniesForCustomer(ctx context.Context, customer *Customer) ([]*CompanySummary, error) {
if len(customer.CompanyIDs) == 0 {
return []*CompanySummary{}, nil
}
// Load companies in batch using the new GetCompaniesByIDs method
companies, err := s.companyService.GetByIDs(ctx, customer.CompanyIDs)
if err != nil {
s.logger.Error("Failed to load companies for customer", map[string]interface{}{
"error": err.Error(),
"customer_id": customer.ID,
"company_ids": customer.CompanyIDs,
})
return []*CompanySummary{}, err
}
// Convert to CompanySummary format
var companySummaries []*CompanySummary
for _, company := range companies {
companySummaries = append(companySummaries, &CompanySummary{
ID: company.ID,
Name: company.Name,
})
}
s.logger.Info("Companies loaded for customer", map[string]interface{}{
"customer_id": customer.ID,
"requested_count": len(customer.CompanyIDs),
"loaded_count": len(companySummaries),
})
return companySummaries, nil
}
// AssignRole assigns a role to a customer
func (s *customerService) AssignRole(ctx context.Context, customerID string, form *AssignRoleForm) (*AssignRoleResponse, error) {
s.logger.Info("Assigning role to customer", map[string]interface{}{
"customer_id": customerID,
"role": form.Role,
})
// Get customer to check if exists
customer, err := s.repository.GetByID(ctx, customerID)
if err != nil {
s.logger.Error("Failed to get customer for role assignment", map[string]interface{}{
"error": err.Error(),
"customer_id": customerID,
})
return nil, errors.New("customer not found")
}
// Validate role
role := CustomerRole(form.Role)
if role != CustomerRoleAdmin && role != CustomerRoleAnalyst {
s.logger.Error("Invalid role provided", map[string]interface{}{
"customer_id": customerID,
"role": form.Role,
})
return nil, errors.New("invalid role. Must be 'admin' or 'analyst'")
}
// Update customer role
customer.Role = role
customer.UpdatedAt = time.Now().Unix()
err = s.repository.Update(ctx, customer)
if err != nil {
s.logger.Error("Failed to update customer role", map[string]interface{}{
"error": err.Error(),
"customer_id": customerID,
"role": form.Role,
})
return nil, errors.New("failed to assign role to customer")
}
s.logger.Info("Role assigned to customer successfully", map[string]interface{}{
"customer_id": customerID,
"role": form.Role,
})
return &AssignRoleResponse{
Message: "Role assigned successfully",
Success: true,
}, nil
}