dfcdef60d6
- 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.
1125 lines
34 KiB
Go
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
|
|
}
|