Add cursor rules and initial configuration for Tender Management Go Backend

This commit is contained in:
n.nakhostin
2025-08-02 10:43:47 +03:30
parent ad9db7bcce
commit 5a1ceb0bd6
35 changed files with 4123 additions and 4550 deletions
@@ -0,0 +1,37 @@
package aggregate
import (
"tm/internal/customer/domain/entity"
"github.com/google/uuid"
)
type Customer struct {
ID uuid.UUID `json:"_id"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Mobile string `json:"mobile"`
Email string `json:"email"`
IsActive bool `json:"is_active"`
IsVerified bool `json:"is_verified"`
}
func NewCustomer(c entity.Customer) Customer {
return Customer{
ID: c.ID,
FirstName: c.FirstName,
LastName: c.LastName,
Mobile: c.Mobile,
Email: c.Email,
IsActive: c.IsActive,
IsVerified: c.IsVerified,
}
}
// AuthResponse defines authentication response (kept for backward compatibility)
type AuthorizationResponse struct {
Customer Customer `json:"customer"`
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
ExpiresIn int64 `json:"expires_in"`
}
+331
View File
@@ -0,0 +1,331 @@
package customer
import (
"context"
"errors"
"time"
"tm/internal/customer/domain/entity"
"tm/pkg/logger"
"github.com/google/uuid"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
// customerRepository implements the Repository interface
type customerRepository struct {
collection *mongo.Collection
logger logger.Logger
}
// NewCustomerRepository creates a new customer repository
func NewCustomerRepository(db *mongo.Database, logger logger.Logger) Repository {
collection := db.Collection("customers")
// Create indexes
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
// Email index (unique)
emailIndex := mongo.IndexModel{
Keys: bson.D{{Key: "email", Value: 1}},
Options: options.Index().SetUnique(true),
}
// Company ID index
companyIndex := mongo.IndexModel{
Keys: bson.D{{Key: "company_id", Value: 1}},
}
// Active status index
activeIndex := mongo.IndexModel{
Keys: bson.D{{Key: "is_active", Value: 1}},
}
// Created at index
createdAtIndex := mongo.IndexModel{
Keys: bson.D{{Key: "created_at", Value: -1}},
}
_, err := collection.Indexes().CreateMany(ctx, []mongo.IndexModel{
emailIndex,
companyIndex,
activeIndex,
createdAtIndex,
})
if err != nil {
logger.Warn("Failed to create customer indexes", map[string]interface{}{
"error": err.Error(),
})
}
return &customerRepository{
collection: collection,
logger: logger,
}
}
// Create creates a new customer
func (r *customerRepository) Create(ctx context.Context, customer *entity.Customer) error {
// Set created/updated timestamps
now := time.Now()
customer.CreatedAt = now
customer.UpdatedAt = now
// Insert customer
_, err := r.collection.InsertOne(ctx, customer)
if err != nil {
if mongo.IsDuplicateKeyError(err) {
return errors.New("customer already exists")
}
r.logger.Error("Failed to create customer", map[string]interface{}{
"error": err.Error(),
"email": customer.Email,
"customer_id": customer.ID.String(),
})
return err
}
r.logger.Info("Customer created successfully", map[string]interface{}{
"customer_id": customer.ID.String(),
"email": customer.Email,
})
return nil
}
// GetByID retrieves a customer by ID
func (r *customerRepository) GetByID(ctx context.Context, id uuid.UUID) (*entity.Customer, error) {
var customer entity.Customer
filter := bson.M{"_id": id}
err := r.collection.FindOne(ctx, filter).Decode(&customer)
if err != nil {
if err == mongo.ErrNoDocuments {
return nil, errors.New("customer not found")
}
r.logger.Error("Failed to get customer by ID", map[string]interface{}{
"error": err.Error(),
"customer_id": id.String(),
})
return nil, err
}
return &customer, nil
}
// GetByEmail retrieves a customer by email
func (r *customerRepository) GetByEmail(ctx context.Context, email string) (*entity.Customer, error) {
var customer entity.Customer
filter := bson.M{"email": email}
err := r.collection.FindOne(ctx, filter).Decode(&customer)
if err != nil {
if err == mongo.ErrNoDocuments {
return nil, errors.New("customer not found")
}
r.logger.Error("Failed to get customer by email", map[string]interface{}{
"error": err.Error(),
"email": email,
})
return nil, err
}
return &customer, nil
}
// Update updates a customer
func (r *customerRepository) Update(ctx context.Context, customer *entity.Customer) error {
// Set updated timestamp
customer.UpdatedAt = time.Now()
filter := bson.M{"_id": customer.ID}
update := bson.M{"$set": customer}
result, err := r.collection.UpdateOne(ctx, filter, update)
if err != nil {
r.logger.Error("Failed to update customer", map[string]interface{}{
"error": err.Error(),
"customer_id": customer.ID.String(),
})
return err
}
if result.MatchedCount == 0 {
return errors.New("customer not found")
}
r.logger.Info("Customer updated successfully", map[string]interface{}{
"customer_id": customer.ID.String(),
"email": customer.Email,
})
return nil
}
// Delete deletes a customer (soft delete by setting is_active to false)
func (r *customerRepository) Delete(ctx context.Context, id uuid.UUID) error {
filter := bson.M{"_id": id}
update := bson.M{
"$set": bson.M{
"is_active": false,
"updated_at": time.Now(),
},
}
result, err := r.collection.UpdateOne(ctx, filter, update)
if err != nil {
r.logger.Error("Failed to delete customer", map[string]interface{}{
"error": err.Error(),
"customer_id": id.String(),
})
return err
}
if result.MatchedCount == 0 {
return errors.New("customer not found")
}
r.logger.Info("Customer deleted successfully", map[string]interface{}{
"customer_id": id.String(),
})
return nil
}
// List retrieves customers with pagination
func (r *customerRepository) List(ctx context.Context, limit, offset int) ([]*entity.Customer, error) {
var customers []*entity.Customer
// Build options
opts := options.Find()
opts.SetLimit(int64(limit))
opts.SetSkip(int64(offset))
opts.SetSort(bson.D{{Key: "created_at", Value: -1}}) // Sort by created_at desc
// Only active customers by default
filter := bson.M{"is_active": true}
cursor, err := r.collection.Find(ctx, filter, opts)
if err != nil {
r.logger.Error("Failed to list customers", map[string]interface{}{
"error": err.Error(),
"limit": limit,
"offset": offset,
})
return nil, err
}
defer cursor.Close(ctx)
if err = cursor.All(ctx, &customers); err != nil {
r.logger.Error("Failed to decode customers", map[string]interface{}{
"error": err.Error(),
})
return nil, err
}
return customers, nil
}
// GetByCompanyID retrieves customers by company ID with pagination
func (r *customerRepository) GetByCompanyID(ctx context.Context, companyID uuid.UUID, limit, offset int) ([]*entity.Customer, error) {
var customers []*entity.Customer
// Build options
opts := options.Find()
opts.SetLimit(int64(limit))
opts.SetSkip(int64(offset))
opts.SetSort(bson.D{{Key: "created_at", Value: -1}}) // Sort by created_at desc
// Filter by company ID and active status
filter := bson.M{
"company_id": companyID,
"is_active": true,
}
cursor, err := r.collection.Find(ctx, filter, opts)
if err != nil {
r.logger.Error("Failed to get customers by company ID", map[string]interface{}{
"error": err.Error(),
"company_id": companyID.String(),
"limit": limit,
"offset": offset,
})
return nil, err
}
defer cursor.Close(ctx)
if err = cursor.All(ctx, &customers); err != nil {
r.logger.Error("Failed to decode customers by company", map[string]interface{}{
"error": err.Error(),
"company_id": companyID.String(),
})
return nil, err
}
return customers, nil
}
// AddDeviceToken adds a device token for push notifications
func (r *customerRepository) AddDeviceToken(ctx context.Context, id uuid.UUID, token string) error {
filter := bson.M{"_id": id}
update := bson.M{
"$addToSet": bson.M{"device_tokens": token}, // Use $addToSet to avoid duplicates
"$set": bson.M{"updated_at": time.Now()},
}
result, err := r.collection.UpdateOne(ctx, filter, update)
if err != nil {
r.logger.Error("Failed to add device token", map[string]interface{}{
"error": err.Error(),
"customer_id": id.String(),
"token": token,
})
return err
}
if result.MatchedCount == 0 {
return errors.New("customer not found")
}
r.logger.Info("Device token added successfully", map[string]interface{}{
"customer_id": id.String(),
"token": token,
})
return nil
}
// RemoveDeviceToken removes a device token
func (r *customerRepository) RemoveDeviceToken(ctx context.Context, id uuid.UUID, token string) error {
filter := bson.M{"_id": id}
update := bson.M{
"$pull": bson.M{"device_tokens": token}, // Use $pull to remove the token
"$set": bson.M{"updated_at": time.Now()},
}
result, err := r.collection.UpdateOne(ctx, filter, update)
if err != nil {
r.logger.Error("Failed to remove device token", map[string]interface{}{
"error": err.Error(),
"customer_id": id.String(),
"token": token,
})
return err
}
if result.MatchedCount == 0 {
return errors.New("customer not found")
}
r.logger.Info("Device token removed successfully", map[string]interface{}{
"customer_id": id.String(),
"token": token,
})
return nil
}
+24
View File
@@ -0,0 +1,24 @@
package entity
import (
"time"
"github.com/google/uuid"
)
// Customer represents a mobile app user (customer)
type Customer struct {
ID uuid.UUID `bson:"_id"`
Email string `bson:"email"`
Password string `bson:"password"` // Never serialize password
FirstName string `bson:"first_name"`
LastName string `bson:"last_name"`
Mobile string `bson:"mobile"`
CompanyID uuid.UUID `bson:"company_id"`
IsActive bool `bson:"is_active"`
IsVerified bool `bson:"is_verified"`
DeviceTokens []string `bson:"device_tokens"` // For push notifications
LastLoginAt *time.Time `bson:"last_login_at,omitempty"`
CreatedAt time.Time `bson:"created_at"`
UpdatedAt time.Time `bson:"updated_at"`
}
+21
View File
@@ -0,0 +1,21 @@
package customer
import (
"context"
"tm/internal/customer/domain/entity"
"github.com/google/uuid"
)
// CustomerRepository defines methods for customer (mobile user) data access
type Repository interface {
Create(ctx context.Context, customer *entity.Customer) error
GetByID(ctx context.Context, id uuid.UUID) (*entity.Customer, error)
GetByEmail(ctx context.Context, email string) (*entity.Customer, error)
Update(ctx context.Context, customer *entity.Customer) error
Delete(ctx context.Context, id uuid.UUID) error
List(ctx context.Context, limit, offset int) ([]*entity.Customer, error)
GetByCompanyID(ctx context.Context, companyID uuid.UUID, limit, offset int) ([]*entity.Customer, error)
AddDeviceToken(ctx context.Context, id uuid.UUID, token string) error
RemoveDeviceToken(ctx context.Context, id uuid.UUID, token string) error
}
+222
View File
@@ -0,0 +1,222 @@
package v1
import (
service "tm/internal/customer/service"
"tm/pkg/logger"
"tm/pkg/response"
"github.com/asaskevich/govalidator"
"github.com/labstack/echo/v4"
)
// CustomerHandler handles authentication related HTTP requests for mobile customers
type CustomerHandler struct {
service service.CustomerService
logger logger.Logger
}
// NewCustomerHandler creates a new customer authentication handler
func NewHandler(
customerAuthService service.CustomerService,
logger logger.Logger,
) *CustomerHandler {
return &CustomerHandler{
service: customerAuthService,
logger: logger,
}
}
// Register handles customer registration
// @Summary Register new customer
// @Description Create a new customer account for mobile app
// @Tags customer-auth
// @Accept json
// @Produce json
// @Param request body domain.CustomerRegisterRequest true "Customer registration request"
// @Success 201 {object} response.APIResponse{data=domain.CustomerAuthResponse}
// @Failure 400 {object} response.APIResponse
// @Failure 409 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /api/mobile/auth/register [post]
func (h *CustomerHandler) Register(c echo.Context) error {
var req service.RegisterForm
// Bind request body
if err := c.Bind(&req); err != nil {
return response.BadRequest(c, "Invalid request format", err.Error())
}
// Validate request
if _, err := govalidator.ValidateStruct(&req); err != nil {
return response.ValidationError(c, "Validation failed", err.Error())
}
// Call service
authResponse, err := h.service.Register(c.Request().Context(), req)
if err != nil {
return response.InternalServerError(c, "Registration failed")
}
return response.Created(c, authResponse, "Customer registered successfully")
}
// Login handles customer authentication
// @Summary Customer login
// @Description Authenticate customer and return tokens
// @Tags customer-auth
// @Accept json
// @Produce json
// @Param request body domain.LoginRequest true "Login request"
// @Success 200 {object} response.APIResponse{data=domain.CustomerAuthResponse}
// @Failure 400 {object} response.APIResponse
// @Failure 401 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /api/mobile/auth/login [post]
func (h *CustomerHandler) Login(c echo.Context) error {
var req service.LoginForm
// Bind request body
if err := c.Bind(&req); err != nil {
return response.BadRequest(c, "Invalid request format", err.Error())
}
// Validate request
if _, err := govalidator.ValidateStruct(&req); err != nil {
return response.ValidationError(c, "Validation failed", err.Error())
}
// Call service
authResponse, err := h.service.Login(c.Request().Context(), req)
if err != nil {
return response.InternalServerError(c, "Login failed")
}
return response.Success(c, authResponse, "Login successful")
}
// RefreshToken handles token refresh for customers
// @Summary Refresh customer access token
// @Description Generate new access token using refresh token
// @Tags customer-auth
// @Accept json
// @Produce json
// @Param request body RefreshTokenRequest true "Refresh token request"
// @Success 200 {object} response.APIResponse{data=domain.CustomerAuthResponse}
// @Failure 400 {object} response.APIResponse
// @Failure 401 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /api/mobile/auth/refresh [post]
func (h *CustomerHandler) RefreshToken(c echo.Context) error {
var req service.RefreshTokenForm
// Bind request body
if err := c.Bind(&req); err != nil {
return response.BadRequest(c, "Invalid request format", err.Error())
}
// Validate request
if _, err := govalidator.ValidateStruct(&req); err != nil {
return response.ValidationError(c, "Validation failed", err.Error())
}
// Call service
authResponse, err := h.service.RefreshToken(c.Request().Context(), req.RefreshToken)
if err != nil {
return response.InternalServerError(c, "Token refresh failed")
}
return response.Success(c, authResponse, "Token refreshed successfully")
}
// ChangePassword handles password change for customers
// @Summary Change customer password
// @Description Change authenticated customer's password
// @Tags customer-auth
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param request body ChangePasswordRequest true "Change password request"
// @Success 200 {object} response.APIResponse
// @Failure 400 {object} response.APIResponse
// @Failure 401 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /api/mobile/auth/change-password [post]
func (h *CustomerHandler) ChangePassword(c echo.Context) error {
var req service.ChangePasswordForm
// Bind request body
if err := c.Bind(&req); err != nil {
return response.BadRequest(c, "Invalid request format", err.Error())
}
// Validate request
if _, err := govalidator.ValidateStruct(&req); err != nil {
return response.ValidationError(c, "Validation failed", err.Error())
}
// Get customer from context (set by auth middleware)
// customer, ok := c.Get("customer").(*domain.Customer)
// if !ok {
// return response.Unauthorized(c, "Authentication required")
// }
// Call service
// err := h.customerAuthService.ChangePassword(c.Request().Context(), customer.ID, req.OldPassword, req.NewPassword)
// if err != nil {
// h.logger.Error("Customer password change failed", map[string]interface{}{
// "error": err.Error(),
// "customer_id": customer.ID.String(),
// })
// if err.Error() == "invalid old password" {
// return response.BadRequest(c, "Invalid old password", "")
// }
// return response.InternalServerError(c, "Password change failed")
// }
return response.Success(c, nil, "Password changed successfully")
}
// GetProfile returns current customer profile
// @Summary Get customer profile
// @Description Get authenticated customer's profile information
// @Tags customer-auth
// @Produce json
// @Security ApiKeyAuth
// @Success 200 {object} response.APIResponse{data=domain.Customer}
// @Failure 401 {object} response.APIResponse
// @Router /api/mobile/auth/profile [get]
func (h *CustomerHandler) GetProfile(c echo.Context) error {
// Get customer from context (set by auth middleware)
// customer, ok := c.Get("customer").(*domain.Customer)
// if !ok {
// return response.Unauthorized(c, "Authentication required")
// }
return response.Success(c, nil, "Profile retrieved successfully")
}
// Logout handles customer logout
// @Summary Customer logout
// @Description Logout customer (client should remove tokens)
// @Tags customer-auth
// @Produce json
// @Security ApiKeyAuth
// @Success 200 {object} response.APIResponse
// @Router /api/mobile/auth/logout [post]
func (h *CustomerHandler) Logout(c echo.Context) error {
// In a stateless JWT implementation, logout is typically handled client-side
// by removing the tokens from storage. However, you could implement token
// blacklisting here if needed.
// customer, ok := c.Get("customer").(*domain.Customer)
// if ok {
// h.logger.Info("Customer logged out", map[string]interface{}{
// "customer_id": customer.ID.String(),
// "email": customer.Email,
// })
// }
return response.Success(c, nil, "Logged out successfully")
}
+151
View File
@@ -0,0 +1,151 @@
package customer
import (
"context"
"errors"
infrastructure "tm/infra"
domain "tm/internal/customer/domain"
"tm/internal/customer/domain/entity"
"tm/pkg/logger"
"github.com/google/uuid"
)
// CustomerService implements the CustomerService interface
type CustomerService struct {
customerRepo domain.Repository
config *infrastructure.AuthConfig
logger logger.Logger
}
// NewCustomerService creates a new customer service
func NewCustomerService(
customerRepo domain.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) ([]*entity.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) (*entity.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) ([]*entity.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
}
+24
View File
@@ -0,0 +1,24 @@
package customer
// Customer Authentication DTOs
type RegisterForm struct {
FirstName string `json:"first_name" validate:"required"`
LastName string `json:"last_name" validate:"required"`
Email string `json:"email" validate:"required,email"`
Mobile string `json:"mobile,omitempty"`
Password string `json:"password" validate:"required,min=8"`
}
type LoginForm struct {
Email string `json:"email" validate:"required,email"`
Password string `json:"password" validate:"required"`
}
type RefreshTokenForm struct {
RefreshToken string `json:"refresh_token" validate:"required"`
}
type ChangePasswordForm struct {
OldPassword string `json:"old_password" validate:"required"`
NewPassword string `json:"new_password" validate:"required,min=8"`
}
+421
View File
@@ -0,0 +1,421 @@
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,
})
}