Merge pull request 'Add cursor rules and initial configuration for Tender Management Go Backend' (#1) from develop into main
Reviewed-on: https://repo.ravanertebat.com/TM/tm_back/pulls/1 Reviewed-by: Hadi Barzegar <barzagarhadi@gmail.com>
This commit is contained in:
File diff suppressed because it is too large
Load Diff
+838
@@ -0,0 +1,838 @@
|
||||
# Cursor Rules for Tender Management Go Backend
|
||||
|
||||
## Project Context
|
||||
This is a Go backend API for a tender management system following Clean Architecture principles with Domain-Driven Design (DDD) patterns, designed to be AI-friendly and easy to maintain.
|
||||
|
||||
## 🏗️ Architecture Guidelines
|
||||
|
||||
### Clean Architecture Layers
|
||||
- **Domain Layer** (`internal/{domain}/domain/`): Business entities, aggregates, interfaces, and core business rules
|
||||
- **Service Layer** (`internal/{domain}/service/`): Business logic and use cases
|
||||
- **Handler Layer** (`internal/{domain}/handler/`): HTTP controllers and request/response handling
|
||||
- **Repository Layer** (`internal/{domain}/domain/`): Data access implementations (embedded in domain)
|
||||
- **Infrastructure Layer** (`infra/`): External dependencies (DB, cache, queues, config)
|
||||
|
||||
### Domain Structure Pattern
|
||||
Each domain follows this structure:
|
||||
```
|
||||
internal/{domain}/
|
||||
├── domain/
|
||||
│ ├── entity/
|
||||
│ │ └── entity.go # Core domain entities
|
||||
│ ├── aggregate/
|
||||
│ │ └── aggregate.go # Aggregate roots and DTOs
|
||||
│ ├── {domain}.go # Repository implementation
|
||||
│ └── repository.go # Repository interface
|
||||
├── service/
|
||||
│ ├── {domain}.go # Main service implementation
|
||||
│ ├── profile.go # Profile-related operations
|
||||
│ └── form.go # Request/response forms
|
||||
└── handler/
|
||||
├── v1/
|
||||
│ └── handler.go # HTTP handlers
|
||||
└── middleware/ # Domain-specific middleware
|
||||
```
|
||||
|
||||
### Dependency Rules
|
||||
- Always depend on interfaces, not concrete implementations
|
||||
- Use dependency injection through constructors
|
||||
- Inner layers should never depend on outer layers
|
||||
- All dependencies flow inward toward the domain
|
||||
- Repository implementations are embedded in domain package
|
||||
|
||||
## 💻 Go Best Practices
|
||||
|
||||
### Code Structure
|
||||
- Use standard Go project layout with `internal/` for private code
|
||||
- Group related functionality in domain packages
|
||||
- Keep packages focused on single responsibility
|
||||
- Use meaningful package names (not generic like `utils` or `helpers`)
|
||||
|
||||
### Naming Conventions
|
||||
- Use Go naming conventions: camelCase for private, PascalCase for public
|
||||
- Interface names should describe what they do (e.g., `CustomerRepository`, `AuthService`)
|
||||
- Use descriptive variable names, avoid abbreviations
|
||||
- Constants should be in ALL_CAPS with underscores
|
||||
- Domain packages use singular form (e.g., `customer`, not `customers`)
|
||||
|
||||
### Error Handling
|
||||
- Always handle errors explicitly, never ignore them
|
||||
- Use structured error messages with context
|
||||
- Wrap errors with additional context using `fmt.Errorf("context: %w", err)`
|
||||
- Log errors at the point they occur with relevant fields
|
||||
- Return domain-specific error types when appropriate
|
||||
|
||||
### Function Design
|
||||
- Keep functions small and focused (max 20-30 lines)
|
||||
- Use early returns to reduce nesting
|
||||
- Validate inputs at function entry points
|
||||
- Use descriptive parameter names
|
||||
- Limit function parameters (max 3-4, use structs for more)
|
||||
|
||||
## 🔌 Interface Design
|
||||
|
||||
### Repository Interfaces
|
||||
- Always create interfaces in the domain layer (`repository.go`)
|
||||
- Use context.Context as first parameter for all methods
|
||||
- Return domain entities, not database models
|
||||
- Include common operations: Create, GetByID, Update, Delete, List
|
||||
- Use specific query methods (e.g., `GetByEmail`, `GetByCompanyID`)
|
||||
|
||||
### Service Interfaces
|
||||
- Define business operations clearly in service package
|
||||
- Use request/response forms for complex operations
|
||||
- Include validation in service methods
|
||||
- Handle business logic, not infrastructure concerns
|
||||
|
||||
## 📝 Logging Standards
|
||||
|
||||
### Using Custom Logger Interface
|
||||
- Always use structured logging with fields
|
||||
- Include relevant context (user_id, customer_id, request_id, etc.)
|
||||
- Use appropriate log levels:
|
||||
- `Debug`: Detailed debugging information
|
||||
- `Info`: General operational messages
|
||||
- `Warn`: Warning conditions that should be addressed
|
||||
- `Error`: Error conditions that need attention
|
||||
- `Fatal`: Critical errors that cause program termination
|
||||
|
||||
### Logging Examples
|
||||
```go
|
||||
// Good
|
||||
log.Info("Customer authenticated successfully", map[string]interface{}{
|
||||
"customer_id": customer.ID.String(),
|
||||
"email": customer.Email,
|
||||
"ip": clientIP,
|
||||
})
|
||||
|
||||
// Bad
|
||||
log.Info("Customer login")
|
||||
```
|
||||
|
||||
### Error Logging
|
||||
- Log errors with full context
|
||||
- Include error details and relevant fields
|
||||
- Don't log the same error multiple times in the call stack
|
||||
|
||||
## 🗄️ Database & Repository Patterns
|
||||
|
||||
### Repository Implementation
|
||||
- Implement repository interfaces in domain package (`{domain}.go`)
|
||||
- Use proper database transactions for related operations
|
||||
- Handle database errors gracefully
|
||||
- Use prepared statements or ORM query builders
|
||||
- Implement proper pagination with limit/offset
|
||||
- **Time Handling**: Always use Unix timestamps (int64) for database operations
|
||||
- **Timestamp Updates**: Use `time.Now().Unix()` for created_at and updated_at fields
|
||||
|
||||
### Data Models
|
||||
- Use domain entities directly with BSON tags for MongoDB
|
||||
- Include audit fields (created_at, updated_at)
|
||||
- Use proper database field tags (bson, json)
|
||||
- Never serialize sensitive fields (passwords)
|
||||
|
||||
### Time Handling Patterns
|
||||
- **Input**: Accept Unix timestamps (int64) for all time fields
|
||||
- **Database Storage**: Store as Unix timestamps (int64) in MongoDB
|
||||
- **Output**: Return Unix timestamps (int64) for all time fields in API responses
|
||||
- **Internal Processing**: Use Unix timestamps (int64) for all business logic
|
||||
- **Audit Fields**: Always use Unix timestamps for created_at, updated_at
|
||||
- **Consistency**: All time operations use Unix timestamps throughout the application
|
||||
|
||||
### MongoDB Patterns
|
||||
- Create indexes in repository constructor
|
||||
- Use proper BSON field names
|
||||
- Handle duplicate key errors appropriately
|
||||
- Use MongoDB-specific features (aggregations, etc.)
|
||||
- **Time Fields**: Store all time fields as Unix timestamps (int64) for consistency
|
||||
- **Queries**: Use Unix timestamps for time-based queries and filtering
|
||||
|
||||
## 🌐 HTTP Handler Guidelines
|
||||
|
||||
### Request Handling
|
||||
- Validate all incoming requests using govalidator
|
||||
- Use proper HTTP status codes
|
||||
- Return consistent API response format using `pkg/response`
|
||||
- Handle CORS appropriately
|
||||
- Include request timeout handling
|
||||
- **Time Input**: Accept Unix timestamps (int64) for all time fields in requests
|
||||
- **Time Validation**: Validate Unix timestamps are within reasonable ranges
|
||||
- **Time Consistency**: All time inputs must be Unix timestamps (int64)
|
||||
|
||||
### Govalidator Usage
|
||||
- Use govalidator for all request validation
|
||||
- Define validation tags in request structs
|
||||
- Use custom validation functions for complex rules
|
||||
- Validate at handler level before calling services
|
||||
- Return validation errors using `response.ValidationError()`
|
||||
- Use meaningful error messages for validation failures
|
||||
|
||||
### Response Format
|
||||
- Always use standardized response structure from `pkg/response`
|
||||
- Include appropriate metadata for paginated responses
|
||||
- Provide meaningful error messages
|
||||
- Never expose internal errors to clients
|
||||
- **Time Output**: Always return Unix timestamps (int64) for all time fields in API responses
|
||||
- **Time Consistency**: Ensure all time fields in responses use Unix timestamps consistently
|
||||
|
||||
### Security
|
||||
- Validate and sanitize all inputs
|
||||
- Use proper authentication middleware
|
||||
- Implement rate limiting
|
||||
- Never log sensitive information (passwords, tokens)
|
||||
- Use HTTPS in production
|
||||
|
||||
## 🧪 Testing Guidelines
|
||||
|
||||
### Test Structure
|
||||
- Write unit tests for all business logic
|
||||
- Use table-driven tests for multiple scenarios
|
||||
- Mock external dependencies using interfaces
|
||||
- Test error cases, not just happy paths
|
||||
- Use meaningful test names that describe the scenario
|
||||
|
||||
### Test Organization
|
||||
- Place tests in same package as code being tested
|
||||
- Use `_test.go` suffix for test files
|
||||
- Create test utilities in `testutil` package if needed
|
||||
- Use setup/teardown functions for common test data
|
||||
|
||||
## 📋 Configuration Management
|
||||
|
||||
### Configuration Structure
|
||||
- Use YAML for configuration files
|
||||
- Support environment variable overrides
|
||||
- Validate configuration on startup
|
||||
- Use default values for optional settings
|
||||
- Document all configuration options
|
||||
|
||||
### Configuration Types
|
||||
- Server configuration (host, port, timeouts)
|
||||
- Database configuration (MongoDB URI, pool settings)
|
||||
- Cache configuration (Redis settings)
|
||||
- Queue configuration (RabbitMQ settings)
|
||||
- Auth configuration (JWT settings)
|
||||
- Logging configuration (level, format, output)
|
||||
|
||||
### Environment Variables
|
||||
- Use consistent naming convention (e.g., `TM_DATABASE_URI`)
|
||||
- Never commit secrets in configuration files
|
||||
- Use different configs for different environments
|
||||
|
||||
## 🚀 Performance Guidelines
|
||||
|
||||
### Optimization
|
||||
- Use database indexes appropriately
|
||||
- Implement caching for frequently accessed data
|
||||
- Use connection pooling for databases
|
||||
- Avoid N+1 queries
|
||||
- Profile performance bottlenecks
|
||||
|
||||
### Memory Management
|
||||
- Close resources properly (database connections, files)
|
||||
- Use context for cancellation and timeouts
|
||||
- Avoid memory leaks in goroutines
|
||||
- Use sync.Pool for frequently allocated objects
|
||||
|
||||
## 📚 Documentation Standards
|
||||
|
||||
### Code Documentation
|
||||
- Write clear comments for complex business logic
|
||||
- Document all public interfaces and types
|
||||
- Use Go doc comments format
|
||||
- Include examples for complex functions
|
||||
- Keep comments up to date with code changes
|
||||
|
||||
### API Documentation
|
||||
- Use OpenAPI/Swagger annotations
|
||||
- Document all endpoints with examples
|
||||
- Include error response examples
|
||||
- Document authentication requirements
|
||||
|
||||
## 🔐 Security Best Practices
|
||||
|
||||
### Authentication & Authorization
|
||||
- Use JWT tokens with proper expiration
|
||||
- Implement refresh token rotation
|
||||
- Use bcrypt for password hashing
|
||||
- Validate token claims properly
|
||||
- Implement proper RBAC
|
||||
|
||||
### Input Validation
|
||||
- Validate all inputs at API boundaries
|
||||
- Use whitelist validation, not blacklist
|
||||
- Sanitize inputs to prevent injection attacks
|
||||
- Validate file uploads properly
|
||||
|
||||
## 📦 Dependency Management
|
||||
|
||||
### Go Modules
|
||||
- Keep go.mod clean and organized
|
||||
- Pin major versions for stability
|
||||
- Regular dependency updates with testing
|
||||
- Remove unused dependencies
|
||||
- Use vendor directory for critical dependencies
|
||||
|
||||
### External Libraries
|
||||
- Prefer standard library when possible
|
||||
- Choose well-maintained libraries
|
||||
- Avoid dependencies with security issues
|
||||
- Keep dependencies minimal
|
||||
|
||||
## 🔄 Code Review Guidelines
|
||||
|
||||
### Before Committing
|
||||
- Run `go fmt`, `go vet`, and linting tools
|
||||
- Ensure all tests pass
|
||||
- Check for proper error handling
|
||||
- Verify logging is appropriate
|
||||
- Confirm no sensitive data is exposed
|
||||
|
||||
### Code Quality
|
||||
- Follow single responsibility principle
|
||||
- Avoid code duplication
|
||||
- Use meaningful variable names
|
||||
- Keep functions and files reasonably sized
|
||||
- Maintain consistent code style
|
||||
|
||||
## 🚨 Error Patterns to Avoid
|
||||
|
||||
### Anti-patterns
|
||||
- Don't use panic for regular error handling
|
||||
- Avoid global variables for application state
|
||||
- Don't ignore context cancellation
|
||||
- Avoid deeply nested if statements
|
||||
- Don't use reflection unless absolutely necessary
|
||||
|
||||
### Common Mistakes
|
||||
- Not handling database connection errors
|
||||
- Forgetting to close resources
|
||||
- Not validating user inputs
|
||||
- Exposing internal errors to clients
|
||||
- Not using proper HTTP status codes
|
||||
|
||||
## 📋 Checklist for New Features
|
||||
|
||||
### Before Implementation
|
||||
- [ ] Define interfaces in domain layer
|
||||
- [ ] Create proper request/response forms with govalidator tags
|
||||
- [ ] Plan error handling strategy
|
||||
- [ ] Design database schema if needed
|
||||
- [ ] Consider caching requirements
|
||||
- [ ] Define custom validators if needed
|
||||
|
||||
### During Implementation
|
||||
- [ ] Follow clean architecture layers
|
||||
- [ ] Use dependency injection
|
||||
- [ ] Implement proper logging
|
||||
- [ ] Add govalidator validation tags to forms
|
||||
- [ ] Register custom validators in init() functions
|
||||
- [ ] Handle errors gracefully
|
||||
|
||||
### After Implementation
|
||||
- [ ] Write unit tests
|
||||
- [ ] Update API documentation
|
||||
- [ ] Test error scenarios
|
||||
- [ ] Verify security implications
|
||||
- [ ] Check performance impact
|
||||
- [ ] Test validation scenarios
|
||||
|
||||
## 🎯 Code Examples
|
||||
|
||||
### Time Handling Utilities
|
||||
```go
|
||||
// pkg/timeutil/timeutil.go
|
||||
package timeutil
|
||||
|
||||
import "time"
|
||||
|
||||
// NowUnix returns current Unix timestamp
|
||||
func NowUnix() int64 {
|
||||
return time.Now().Unix()
|
||||
}
|
||||
|
||||
// ValidateUnixTimestamp validates if Unix timestamp is reasonable
|
||||
func ValidateUnixTimestamp(unix int64) bool {
|
||||
// Check if timestamp is not too old (before 2020) or too future (after 2030)
|
||||
minTime := time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC).Unix()
|
||||
maxTime := time.Date(2030, 12, 31, 23, 59, 59, 0, time.UTC).Unix()
|
||||
return unix >= minTime && unix <= maxTime
|
||||
}
|
||||
|
||||
// ParseUnixTimestamp parses string to Unix timestamp
|
||||
func ParseUnixTimestamp(timestamp string) (int64, error) {
|
||||
// Handle various timestamp formats and convert to Unix
|
||||
// This can be extended based on your input requirements
|
||||
return 0, nil // Implementation depends on input format
|
||||
}
|
||||
|
||||
// FormatUnixTimestamp formats Unix timestamp for display (if needed)
|
||||
func FormatUnixTimestamp(unix int64, format string) string {
|
||||
return time.Unix(unix, 0).Format(format)
|
||||
}
|
||||
```
|
||||
|
||||
### Govalidator Request Forms
|
||||
```go
|
||||
// internal/customer/service/form.go
|
||||
package service
|
||||
|
||||
import (
|
||||
"github.com/asaskevich/govalidator"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// RegisterForm represents customer registration request
|
||||
type RegisterForm struct {
|
||||
FirstName string `json:"first_name" valid:"required,alpha,length(2|50)"`
|
||||
LastName string `json:"last_name" valid:"required,alpha,length(2|50)"`
|
||||
Email string `json:"email" valid:"required,email"`
|
||||
Password string `json:"password" valid:"required,length(8|128)"`
|
||||
Mobile string `json:"mobile" valid:"required,length(10|15)"`
|
||||
CompanyID string `json:"company_id" valid:"required,uuid"`
|
||||
}
|
||||
|
||||
// LoginForm represents customer login request
|
||||
type LoginForm struct {
|
||||
Email string `json:"email" valid:"required,email"`
|
||||
Password string `json:"password" valid:"required"`
|
||||
}
|
||||
|
||||
// UpdateProfileForm represents profile update request
|
||||
type UpdateProfileForm struct {
|
||||
FirstName string `json:"first_name" valid:"required,alpha,length(2|50)"`
|
||||
LastName string `json:"last_name" valid:"required,alpha,length(2|50)"`
|
||||
Mobile string `json:"mobile" valid:"required,length(10|15)"`
|
||||
LastLoginAt *int64 `json:"last_login_at,omitempty" valid:"optional,unix_timestamp"`
|
||||
}
|
||||
|
||||
// CreateCustomerForm represents customer creation request
|
||||
type CreateCustomerForm struct {
|
||||
FirstName string `json:"first_name" valid:"required,alpha,length(2|50)"`
|
||||
LastName string `json:"last_name" valid:"required,alpha,length(2|50)"`
|
||||
Email string `json:"email" valid:"required,email"`
|
||||
Password string `json:"password" valid:"required,length(8|128)"`
|
||||
Mobile string `json:"mobile" valid:"required,length(10|15)"`
|
||||
CompanyID string `json:"company_id" valid:"required,uuid"`
|
||||
CreatedAt int64 `json:"created_at,omitempty" valid:"optional,unix_timestamp"`
|
||||
UpdatedAt int64 `json:"updated_at,omitempty" valid:"optional,unix_timestamp"`
|
||||
LastLoginAt *int64 `json:"last_login_at,omitempty" valid:"optional,unix_timestamp"`
|
||||
}
|
||||
|
||||
// Custom validation functions
|
||||
func init() {
|
||||
// Register custom validators
|
||||
govalidator.CustomTypeTagMap.Set("unix_timestamp", govalidator.CustomTypeValidator(func(i interface{}, context interface{}) bool {
|
||||
if unix, ok := i.(int64); ok {
|
||||
return timeutil.ValidateUnixTimestamp(unix)
|
||||
}
|
||||
return false
|
||||
}))
|
||||
}
|
||||
```
|
||||
|
||||
### Domain Entity Pattern
|
||||
```go
|
||||
// internal/customer/domain/entity/entity.go
|
||||
type Customer struct {
|
||||
ID uuid.UUID `bson:"_id"`
|
||||
Email string `bson:"email"`
|
||||
Password string `bson:"password"` // Never serialize
|
||||
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"`
|
||||
LastLoginAt *int64 `bson:"last_login_at,omitempty"` // Unix timestamp
|
||||
CreatedAt int64 `bson:"created_at"` // Unix timestamp
|
||||
UpdatedAt int64 `bson:"updated_at"` // Unix timestamp
|
||||
}
|
||||
```
|
||||
|
||||
### Aggregate Pattern
|
||||
```go
|
||||
// internal/customer/domain/aggregate/aggregate.go
|
||||
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"`
|
||||
LastLoginAt *int64 `json:"last_login_at,omitempty"` // Unix timestamp
|
||||
CreatedAt int64 `json:"created_at"` // Unix timestamp
|
||||
UpdatedAt int64 `json:"updated_at"` // Unix timestamp
|
||||
}
|
||||
|
||||
func NewCustomer(c entity.Customer) Customer {
|
||||
customer := Customer{
|
||||
ID: c.ID,
|
||||
FirstName: c.FirstName,
|
||||
LastName: c.LastName,
|
||||
Mobile: c.Mobile,
|
||||
Email: c.Email,
|
||||
IsActive: c.IsActive,
|
||||
IsVerified: c.IsVerified,
|
||||
CreatedAt: c.CreatedAt,
|
||||
UpdatedAt: c.UpdatedAt,
|
||||
}
|
||||
|
||||
if c.LastLoginAt != nil {
|
||||
customer.LastLoginAt = c.LastLoginAt
|
||||
}
|
||||
|
||||
return customer
|
||||
}
|
||||
```
|
||||
|
||||
### Repository Implementation with Time Handling
|
||||
```go
|
||||
// internal/customer/domain/customer.go
|
||||
func (r *customerRepository) Create(ctx context.Context, customer *entity.Customer) error {
|
||||
// Set created/updated timestamps using Unix timestamps
|
||||
now := time.Now().Unix()
|
||||
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
|
||||
}
|
||||
|
||||
func (r *customerRepository) Update(ctx context.Context, customer *entity.Customer) error {
|
||||
// Set updated timestamp using Unix timestamp
|
||||
customer.UpdatedAt = time.Now().Unix()
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *customerRepository) UpdateLastLogin(ctx context.Context, id uuid.UUID) error {
|
||||
now := time.Now().Unix()
|
||||
|
||||
filter := bson.M{"_id": id}
|
||||
update := bson.M{
|
||||
"$set": bson.M{
|
||||
"last_login_at": now,
|
||||
"updated_at": now,
|
||||
},
|
||||
}
|
||||
|
||||
result, err := r.collection.UpdateOne(ctx, filter, update)
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to update last login", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"customer_id": id.String(),
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
if result.MatchedCount == 0 {
|
||||
return errors.New("customer not found")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
### Repository Interface
|
||||
```go
|
||||
// internal/customer/domain/repository.go
|
||||
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
|
||||
}
|
||||
```
|
||||
|
||||
### Service Implementation Template
|
||||
```go
|
||||
type CustomerService struct {
|
||||
customerRepo domain.Repository
|
||||
config *infra.AuthConfig
|
||||
logger logger.Logger
|
||||
}
|
||||
|
||||
func NewCustomerService(
|
||||
customerRepo domain.Repository,
|
||||
config *infra.AuthConfig,
|
||||
logger logger.Logger,
|
||||
) CustomerService {
|
||||
return CustomerService{
|
||||
customerRepo: customerRepo,
|
||||
config: config,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *CustomerService) Register(ctx context.Context, req RegisterForm) (*aggregate.AuthorizationResponse, error) {
|
||||
s.logger.Info("Processing customer registration", map[string]interface{}{
|
||||
"email": req.Email,
|
||||
})
|
||||
|
||||
// Convert form to entity
|
||||
customer := &entity.Customer{
|
||||
ID: uuid.New(),
|
||||
FirstName: req.FirstName,
|
||||
LastName: req.LastName,
|
||||
Email: req.Email,
|
||||
Password: req.Password, // Will be hashed in service
|
||||
Mobile: req.Mobile,
|
||||
CompanyID: uuid.MustParse(req.CompanyID),
|
||||
IsActive: true,
|
||||
IsVerified: false,
|
||||
}
|
||||
|
||||
// Set timestamps
|
||||
s.setTimestamps(customer)
|
||||
|
||||
// Create customer
|
||||
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, err
|
||||
}
|
||||
|
||||
// Generate tokens and return response
|
||||
// ... token generation logic
|
||||
|
||||
return &aggregate.AuthorizationResponse{
|
||||
Customer: aggregate.NewCustomer(*customer),
|
||||
// ... token fields
|
||||
}, nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// Time handling helper functions - all using Unix timestamps
|
||||
func (s *CustomerService) setTimestamps(customer *entity.Customer) {
|
||||
now := timeutil.NowUnix()
|
||||
if customer.CreatedAt == 0 {
|
||||
customer.CreatedAt = now
|
||||
}
|
||||
customer.UpdatedAt = now
|
||||
}
|
||||
|
||||
func (s *CustomerService) setLastLogin(customer *entity.Customer) {
|
||||
now := timeutil.NowUnix()
|
||||
customer.LastLoginAt = &now
|
||||
customer.UpdatedAt = now
|
||||
}
|
||||
|
||||
func (s *CustomerService) validateTimestamps(customer *entity.Customer) error {
|
||||
if customer.CreatedAt != 0 && !timeutil.ValidateUnixTimestamp(customer.CreatedAt) {
|
||||
return errors.New("invalid created_at timestamp")
|
||||
}
|
||||
if customer.UpdatedAt != 0 && !timeutil.ValidateUnixTimestamp(customer.UpdatedAt) {
|
||||
return errors.New("invalid updated_at timestamp")
|
||||
}
|
||||
if customer.LastLoginAt != nil && !timeutil.ValidateUnixTimestamp(*customer.LastLoginAt) {
|
||||
return errors.New("invalid last_login_at timestamp")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
### Handler Implementation Template
|
||||
```go
|
||||
type CustomerHandler struct {
|
||||
service service.CustomerService
|
||||
logger logger.Logger
|
||||
}
|
||||
|
||||
func NewHandler(
|
||||
customerService service.CustomerService,
|
||||
logger logger.Logger,
|
||||
) *CustomerHandler {
|
||||
return &CustomerHandler{
|
||||
service: customerService,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
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 using govalidator
|
||||
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")
|
||||
}
|
||||
|
||||
func (h *CustomerHandler) UpdateProfile(c echo.Context) error {
|
||||
var req service.UpdateProfileForm
|
||||
|
||||
// Bind request body
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return response.BadRequest(c, "Invalid request format", err.Error())
|
||||
}
|
||||
|
||||
// Validate request using govalidator
|
||||
if _, err := govalidator.ValidateStruct(&req); err != nil {
|
||||
return response.ValidationError(c, "Validation failed", err.Error())
|
||||
}
|
||||
|
||||
// Call service
|
||||
customer, err := h.service.UpdateProfile(c.Request().Context(), req)
|
||||
if err != nil {
|
||||
return response.InternalServerError(c, "Profile update failed")
|
||||
}
|
||||
|
||||
return response.Success(c, customer, "Profile updated successfully")
|
||||
}
|
||||
|
||||
func (h *CustomerHandler) CreateWithTimestamp(c echo.Context) error {
|
||||
var req service.CreateCustomerForm
|
||||
|
||||
// Bind request body
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return response.BadRequest(c, "Invalid request format", err.Error())
|
||||
}
|
||||
|
||||
// Validate request using govalidator
|
||||
if _, err := govalidator.ValidateStruct(&req); err != nil {
|
||||
return response.ValidationError(c, "Validation failed", err.Error())
|
||||
}
|
||||
|
||||
// Call service
|
||||
customer, err := h.service.CreateCustomer(c.Request().Context(), req)
|
||||
if err != nil {
|
||||
return response.InternalServerError(c, "Customer creation failed")
|
||||
}
|
||||
|
||||
return response.Created(c, customer, "Customer created successfully")
|
||||
}
|
||||
```
|
||||
|
||||
### Configuration Structure
|
||||
```go
|
||||
// infra/config.go
|
||||
type Config struct {
|
||||
Server ServerConfig `mapstructure:"server"`
|
||||
Database DatabaseConfig `mapstructure:"database"`
|
||||
Cache CacheConfig `mapstructure:"cache"`
|
||||
Queue QueueConfig `mapstructure:"queue"`
|
||||
Search SearchConfig `mapstructure:"search"`
|
||||
Auth AuthConfig `mapstructure:"auth"`
|
||||
AI AIConfig `mapstructure:"ai"`
|
||||
Scraping ScrapingConfig `mapstructure:"scraping"`
|
||||
Logging LoggingConfig `mapstructure:"logging"`
|
||||
RateLimit RateLimitConfig `mapstructure:"rate_limiting"`
|
||||
}
|
||||
```
|
||||
|
||||
### Application Bootstrap
|
||||
```go
|
||||
// cmd/web/main.go
|
||||
func main() {
|
||||
conf := intiConfig()
|
||||
|
||||
logger := initLogger(conf.Logging)
|
||||
defer logger.Sync()
|
||||
|
||||
logger.Info(
|
||||
"Starting Tender Management API Server",
|
||||
map[string]interface{}{
|
||||
"version": "1.0.0",
|
||||
"host": conf.Server.Host,
|
||||
"port": conf.Server.Port,
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## 🚀 Remember
|
||||
- Prioritize simplicity and readability over cleverness
|
||||
- Make code self-documenting through clear naming
|
||||
- Always consider the person who will maintain this code
|
||||
- Test your code thoroughly
|
||||
- Security and performance are not optional
|
||||
- Follow the established domain structure pattern
|
||||
- Use the custom logger interface consistently
|
||||
- Implement proper error handling and logging
|
||||
- Follow the repository pattern with embedded implementations
|
||||
- **Time Consistency**: Always use Unix timestamps (int64) for input, storage, and output
|
||||
- **Time Validation**: Validate all Unix timestamp inputs are within reasonable ranges
|
||||
- **Time Utilities**: Use centralized time utility functions for consistency across the application
|
||||
- **No Format Conversion**: Never convert between Unix timestamps and other time formats in the application
|
||||
- **Validation**: Always use govalidator for request validation with proper tags
|
||||
- **Form Design**: Define validation rules in request forms, not in handlers
|
||||
- **Custom Validators**: Register custom validators for complex validation rules
|
||||
-260
@@ -1,260 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/labstack/echo/v4/middleware"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
|
||||
"tm/internal/domain"
|
||||
"tm/internal/handler"
|
||||
"tm/internal/infrastructure"
|
||||
"tm/internal/repository"
|
||||
"tm/internal/service"
|
||||
"tm/pkg/logger"
|
||||
customMiddleware "tm/pkg/middleware"
|
||||
"tm/pkg/response"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Load configuration
|
||||
config, err := infrastructure.LoadConfig("./configs")
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("Failed to load config: %v", err))
|
||||
}
|
||||
|
||||
// Initialize logger
|
||||
logger.InitLogger(&config.Logging)
|
||||
log := logger.GetLogger()
|
||||
|
||||
log.Info("Starting Tender Management API Server", map[string]interface{}{
|
||||
"version": "1.0.0",
|
||||
"port": config.Server.Port,
|
||||
})
|
||||
|
||||
// Initialize MongoDB connection
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
mongoClient, err := mongo.Connect(ctx, options.Client().ApplyURI(config.Database.MongoDB.URI))
|
||||
if err != nil {
|
||||
log.Fatal("Failed to connect to MongoDB", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
// Ping MongoDB to verify connection
|
||||
if err = mongoClient.Ping(ctx, nil); err != nil {
|
||||
log.Fatal("Failed to ping MongoDB", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
log.Info("Connected to MongoDB successfully", map[string]interface{}{
|
||||
"database": config.Database.MongoDB.Name,
|
||||
})
|
||||
|
||||
// Get MongoDB database
|
||||
db := mongoClient.Database(config.Database.MongoDB.Name)
|
||||
|
||||
// Initialize validator
|
||||
validate := validator.New()
|
||||
|
||||
// Initialize repositories
|
||||
customerRepo := repository.NewCustomerRepository(db, log)
|
||||
userRepo := repository.NewUserRepository(db, log)
|
||||
companyRepo := repository.NewCompanyRepository(db, log)
|
||||
|
||||
// Initialize services
|
||||
customerAuthService := service.NewCustomerAuthService(customerRepo, companyRepo, &config.Auth, log)
|
||||
userAuthService := service.NewUserAuthService(userRepo, &config.Auth, log)
|
||||
// TODO: Initialize customer service when needed for admin customer management
|
||||
// customerService := service.NewCustomerService(customerRepo, log)
|
||||
|
||||
// Backward compatibility - create generic auth service
|
||||
authService := handler.NewAuthHandler(customerAuthService, userAuthService, validate, log)
|
||||
|
||||
// Initialize handlers
|
||||
customerAuthHandler := handler.NewCustomerAuthHandler(customerAuthService, validate, log)
|
||||
userAuthHandler := handler.NewUserAuthHandler(userAuthService, validate, log)
|
||||
|
||||
// Initialize Echo
|
||||
e := echo.New()
|
||||
|
||||
// Configure Echo
|
||||
e.HideBanner = true
|
||||
e.HidePort = true
|
||||
|
||||
// Add built-in middleware
|
||||
e.Use(middleware.Logger())
|
||||
e.Use(middleware.Recover())
|
||||
e.Use(middleware.CORS())
|
||||
e.Use(middleware.RequestID())
|
||||
|
||||
// Add custom middleware for logging
|
||||
e.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
start := time.Now()
|
||||
|
||||
err := next(c)
|
||||
|
||||
log.Info("HTTP Request", map[string]interface{}{
|
||||
"method": c.Request().Method,
|
||||
"path": c.Request().URL.Path,
|
||||
"status": c.Response().Status,
|
||||
"duration": time.Since(start).String(),
|
||||
"request_id": c.Response().Header().Get(echo.HeaderXRequestID),
|
||||
})
|
||||
|
||||
return err
|
||||
}
|
||||
})
|
||||
|
||||
// Add authentication middleware
|
||||
e.Use(customMiddleware.AuthMiddleware(customerAuthService, userAuthService, log))
|
||||
|
||||
// Setup routes
|
||||
setupRoutes(e, authService, customerAuthHandler, userAuthHandler)
|
||||
|
||||
// Health check endpoint
|
||||
e.GET("/health", func(c echo.Context) error {
|
||||
return response.Success(c, map[string]interface{}{
|
||||
"status": "healthy",
|
||||
"timestamp": time.Now().UTC(),
|
||||
"version": "1.0.0",
|
||||
}, "Service is healthy")
|
||||
})
|
||||
|
||||
// Start server in a goroutine
|
||||
go func() {
|
||||
address := fmt.Sprintf("%s:%d", config.Server.Host, config.Server.Port)
|
||||
log.Info("Server starting", map[string]interface{}{
|
||||
"address": address,
|
||||
})
|
||||
|
||||
if err := e.Start(address); err != nil && err != http.ErrServerClosed {
|
||||
log.Fatal("Failed to start server", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
}
|
||||
}()
|
||||
|
||||
// Wait for interrupt signal to gracefully shutdown the server
|
||||
quit := make(chan os.Signal, 1)
|
||||
signal.Notify(quit, os.Interrupt, syscall.SIGTERM)
|
||||
<-quit
|
||||
|
||||
log.Info("Server is shutting down...", nil)
|
||||
|
||||
// Create a deadline for graceful shutdown
|
||||
ctx, cancel = context.WithTimeout(context.Background(), config.Server.Timeout)
|
||||
defer cancel()
|
||||
|
||||
// Gracefully shutdown the server
|
||||
if err := e.Shutdown(ctx); err != nil {
|
||||
log.Fatal("Server forced to shutdown", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
// Close MongoDB connection
|
||||
if err := mongoClient.Disconnect(context.Background()); err != nil {
|
||||
log.Error("Failed to disconnect from MongoDB", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
log.Info("Server stopped gracefully", nil)
|
||||
|
||||
// Cleanup logger to flush any remaining logs
|
||||
logger.Cleanup()
|
||||
}
|
||||
|
||||
// setupRoutes configures all API routes
|
||||
func setupRoutes(
|
||||
e *echo.Echo,
|
||||
authHandler *handler.AuthHandler,
|
||||
customerAuthHandler *handler.CustomerAuthHandler,
|
||||
userAuthHandler *handler.UserAuthHandler,
|
||||
) {
|
||||
// Legacy/Generic authentication routes (backward compatibility)
|
||||
auth := e.Group("/auth")
|
||||
auth.POST("/register", authHandler.Register)
|
||||
auth.POST("/login", authHandler.Login)
|
||||
auth.POST("/refresh", authHandler.RefreshToken)
|
||||
auth.POST("/change-password", authHandler.ChangePassword)
|
||||
auth.GET("/profile", authHandler.GetProfile)
|
||||
auth.POST("/logout", authHandler.Logout)
|
||||
|
||||
// API version group
|
||||
api := e.Group("/api/v1")
|
||||
|
||||
// Mobile app routes (customers)
|
||||
mobile := api.Group("/mobile")
|
||||
mobileAuth := mobile.Group("/auth")
|
||||
mobileAuth.POST("/register", customerAuthHandler.Register)
|
||||
mobileAuth.POST("/login", customerAuthHandler.Login)
|
||||
mobileAuth.POST("/refresh", customerAuthHandler.RefreshToken)
|
||||
mobileAuth.POST("/verify-email", customerAuthHandler.VerifyEmail)
|
||||
mobileAuth.POST("/resend-verification", customerAuthHandler.ResendVerification)
|
||||
|
||||
// Protected mobile routes
|
||||
mobileAuth.POST("/change-password", customerAuthHandler.ChangePassword, customMiddleware.CustomerOnlyMiddleware())
|
||||
mobileAuth.GET("/profile", customerAuthHandler.GetProfile, customMiddleware.CustomerOnlyMiddleware())
|
||||
mobileAuth.POST("/logout", customerAuthHandler.Logout, customMiddleware.CustomerOnlyMiddleware())
|
||||
|
||||
// Mobile app tenders routes
|
||||
mobileTenders := mobile.Group("/tenders", customMiddleware.CustomerOnlyMiddleware())
|
||||
// TODO: Add tender routes for customers
|
||||
_ = mobileTenders
|
||||
|
||||
// Mobile app companies routes
|
||||
mobileCompanies := mobile.Group("/companies", customMiddleware.CustomerOnlyMiddleware())
|
||||
// TODO: Add company routes for customers
|
||||
_ = mobileCompanies
|
||||
|
||||
// Admin panel routes (panel users)
|
||||
admin := api.Group("/admin")
|
||||
adminAuth := admin.Group("/auth")
|
||||
adminAuth.POST("/login", userAuthHandler.Login)
|
||||
adminAuth.POST("/refresh", userAuthHandler.RefreshToken)
|
||||
|
||||
// Protected admin auth routes
|
||||
adminAuth.POST("/change-password", userAuthHandler.ChangePassword, customMiddleware.UserOnlyMiddleware())
|
||||
adminAuth.GET("/profile", userAuthHandler.GetProfile, customMiddleware.UserOnlyMiddleware())
|
||||
adminAuth.POST("/logout", userAuthHandler.Logout, customMiddleware.UserOnlyMiddleware())
|
||||
|
||||
// Admin user management routes (admin only)
|
||||
adminUsers := admin.Group("/users", customMiddleware.UserRoleMiddleware(domain.UserRoleAdmin))
|
||||
adminUsers.POST("", userAuthHandler.CreatePanelUser)
|
||||
adminUsers.PUT("/:user_id/permissions", userAuthHandler.UpdateUserPermissions)
|
||||
|
||||
// Admin customer management routes
|
||||
adminCustomers := admin.Group("/customers", customMiddleware.UserRoleMiddleware(domain.UserRoleAdmin, domain.UserRoleModerator))
|
||||
// TODO: Add customer management routes for admins
|
||||
_ = adminCustomers
|
||||
|
||||
// Admin tender management routes
|
||||
adminTenders := admin.Group("/tenders", customMiddleware.UserPermissionMiddleware("manage_tenders"))
|
||||
// TODO: Add tender management routes for admins
|
||||
_ = adminTenders
|
||||
|
||||
// Admin company management routes
|
||||
adminCompanies := admin.Group("/companies", customMiddleware.UserPermissionMiddleware("manage_companies"))
|
||||
// TODO: Add company management routes for admins
|
||||
_ = adminCompanies
|
||||
|
||||
// Admin reports routes
|
||||
adminReports := admin.Group("/reports", customMiddleware.UserPermissionMiddleware("view_reports"))
|
||||
// TODO: Add reporting routes for admins
|
||||
_ = adminReports
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"tm/infra"
|
||||
"tm/pkg/logger"
|
||||
)
|
||||
|
||||
// Init Application Configuration
|
||||
func intiConfig() infra.Config {
|
||||
config, err := infra.LoadConfig("./")
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("Failed to load config: %v", err))
|
||||
}
|
||||
|
||||
return *config
|
||||
}
|
||||
|
||||
// Init Logger Service
|
||||
func initLogger(conf infra.LoggingConfig) logger.Logger {
|
||||
return logger.NewLogger(&logger.Config{
|
||||
Level: conf.Level,
|
||||
Format: conf.Format,
|
||||
Output: conf.Output,
|
||||
File: logger.FileConfig{
|
||||
Path: conf.File.Path,
|
||||
MaxSize: conf.File.MaxSize,
|
||||
MaxBackups: conf.File.MaxBackups,
|
||||
MaxAge: conf.File.MaxAge,
|
||||
Compress: conf.File.Compress,
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package main
|
||||
|
||||
func main() {
|
||||
conf := intiConfig()
|
||||
|
||||
logger := initLogger(conf.Logging)
|
||||
defer logger.Sync()
|
||||
|
||||
logger.Info(
|
||||
"Starting Tender Management API Server",
|
||||
map[string]interface{}{
|
||||
"version": "1.0.0",
|
||||
"host": conf.Server.Host,
|
||||
"port": conf.Server.Port,
|
||||
})
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
server:
|
||||
host: "0.0.0.0"
|
||||
port: 8080
|
||||
timeout: 30s
|
||||
read_timeout: 10s
|
||||
write_timeout: 10s
|
||||
|
||||
database:
|
||||
mongodb:
|
||||
uri: "mongodb://localhost:27017"
|
||||
name: "tender_management"
|
||||
timeout: 10s
|
||||
max_pool_size: 100
|
||||
|
||||
cache:
|
||||
redis:
|
||||
host: "localhost"
|
||||
port: 6379
|
||||
password: ""
|
||||
db: 0
|
||||
pool_size: 10
|
||||
|
||||
queue:
|
||||
rabbitmq:
|
||||
url: "amqp://guest:guest@localhost:5672/"
|
||||
exchange: "tender_exchange"
|
||||
queues:
|
||||
scraping: "scraping_queue"
|
||||
processing: "processing_queue"
|
||||
notifications: "notifications_queue"
|
||||
|
||||
search:
|
||||
elasticsearch:
|
||||
urls: ["http://localhost:9200"]
|
||||
username: ""
|
||||
password: ""
|
||||
index_prefix: "tender_"
|
||||
|
||||
auth:
|
||||
jwt:
|
||||
secret: "your-super-secret-key-change-in-production"
|
||||
access_token_duration: "24h"
|
||||
refresh_token_duration: "168h" # 7 days
|
||||
|
||||
ai:
|
||||
openai:
|
||||
api_key: "your-openai-api-key"
|
||||
model: "gpt-3.5-turbo"
|
||||
max_tokens: 1000
|
||||
|
||||
scraping:
|
||||
user_agent: "TenderBot/1.0"
|
||||
timeout: 30s
|
||||
max_retries: 3
|
||||
delay_between_requests: 1s
|
||||
|
||||
logging:
|
||||
level: "info" # debug, info, warn, error, fatal
|
||||
format: "json" # json, console, text
|
||||
output: "file" # stdout, stderr, file
|
||||
file:
|
||||
path: "./logs/app.log" # Path to log file
|
||||
max_size: 100 # Max size in MB before rotation
|
||||
max_backups: 5 # Number of backup files to keep
|
||||
max_age: 30 # Max age in days to keep log files
|
||||
compress: true # Compress rotated files
|
||||
|
||||
rate_limiting:
|
||||
requests_per_minute: 100
|
||||
burst: 20
|
||||
@@ -5,11 +5,11 @@ go 1.23.0
|
||||
toolchain go1.24.4
|
||||
|
||||
require (
|
||||
github.com/go-playground/validator/v10 v10.27.0
|
||||
github.com/golang-jwt/jwt/v5 v5.2.3
|
||||
github.com/google/uuid v1.4.0
|
||||
github.com/labstack/echo/v4 v4.13.4
|
||||
github.com/spf13/viper v1.18.2
|
||||
github.com/stretchr/testify v1.10.0
|
||||
go.mongodb.org/mongo-driver v1.17.4
|
||||
go.uber.org/zap v1.26.0
|
||||
golang.org/x/crypto v0.38.0
|
||||
@@ -17,15 +17,17 @@ require (
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2
|
||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/klauspost/compress v1.17.0 // indirect
|
||||
github.com/labstack/gommon v0.4.2 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/magiconair/properties v1.8.7 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
@@ -51,7 +53,6 @@ require (
|
||||
golang.org/x/sync v0.14.0 // indirect
|
||||
golang.org/x/sys v0.33.0 // indirect
|
||||
golang.org/x/text v0.25.0 // indirect
|
||||
golang.org/x/time v0.11.0 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
@@ -6,16 +8,6 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
|
||||
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
|
||||
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.3 h1:kkGXqQOBSDDWRhWNXTFpqGSCMyh/PLnqUvMGJPDJDs0=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.3/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
|
||||
@@ -36,8 +28,6 @@ github.com/labstack/echo/v4 v4.13.4 h1:oTZZW+T3s9gAu5L8vmzihV7/lkXGZuITzTQkTEhcX
|
||||
github.com/labstack/echo/v4 v4.13.4/go.mod h1:g63b33BZ5vZzcIUF8AtRH40DrTlXnx4UMC8rBdndmjQ=
|
||||
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
|
||||
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
||||
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
@@ -132,8 +122,6 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
|
||||
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
|
||||
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
|
||||
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package infrastructure
|
||||
package infra
|
||||
|
||||
import (
|
||||
"time"
|
||||
@@ -100,17 +100,17 @@ type ScrapingConfig struct {
|
||||
}
|
||||
|
||||
type LoggingConfig struct {
|
||||
Level string `mapstructure:"level"`
|
||||
Format string `mapstructure:"format"`
|
||||
Output string `mapstructure:"output"`
|
||||
File LogFileConfig `mapstructure:"file"`
|
||||
Level string `mapstructure:"level"`
|
||||
Format string `mapstructure:"format"`
|
||||
Output string `mapstructure:"output"`
|
||||
File LogFileConfig `mapstructure:"file"`
|
||||
}
|
||||
|
||||
type LogFileConfig struct {
|
||||
Path string `mapstructure:"path"`
|
||||
MaxSize int `mapstructure:"max_size"` // MB
|
||||
MaxSize int `mapstructure:"max_size"` // MB
|
||||
MaxBackups int `mapstructure:"max_backups"`
|
||||
MaxAge int `mapstructure:"max_age"` // days
|
||||
MaxAge int `mapstructure:"max_age"` // days
|
||||
Compress bool `mapstructure:"compress"`
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -1,52 +1,51 @@
|
||||
package repository
|
||||
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"
|
||||
|
||||
"tm/internal/domain"
|
||||
"tm/pkg/logger"
|
||||
)
|
||||
|
||||
// CustomerRepositoryImpl implements the CustomerRepository interface
|
||||
type CustomerRepositoryImpl struct {
|
||||
// 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) domain.CustomerRepository {
|
||||
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)
|
||||
// Email index (unique)
|
||||
emailIndex := mongo.IndexModel{
|
||||
Keys: bson.D{{"email", 1}},
|
||||
Keys: bson.D{{Key: "email", Value: 1}},
|
||||
Options: options.Index().SetUnique(true),
|
||||
}
|
||||
|
||||
// Company ID index
|
||||
companyIndex := mongo.IndexModel{
|
||||
Keys: bson.D{{"company_id", 1}},
|
||||
Keys: bson.D{{Key: "company_id", Value: 1}},
|
||||
}
|
||||
|
||||
// Active status index
|
||||
activeIndex := mongo.IndexModel{
|
||||
Keys: bson.D{{"is_active", 1}},
|
||||
Keys: bson.D{{Key: "is_active", Value: 1}},
|
||||
}
|
||||
|
||||
// Created at index
|
||||
createdAtIndex := mongo.IndexModel{
|
||||
Keys: bson.D{{"created_at", -1}},
|
||||
Keys: bson.D{{Key: "created_at", Value: -1}},
|
||||
}
|
||||
|
||||
_, err := collection.Indexes().CreateMany(ctx, []mongo.IndexModel{
|
||||
@@ -62,14 +61,14 @@ func NewCustomerRepository(db *mongo.Database, logger logger.Logger) domain.Cust
|
||||
})
|
||||
}
|
||||
|
||||
return &CustomerRepositoryImpl{
|
||||
return &customerRepository{
|
||||
collection: collection,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// Create creates a new customer
|
||||
func (r *CustomerRepositoryImpl) Create(ctx context.Context, customer *domain.Customer) error {
|
||||
func (r *customerRepository) Create(ctx context.Context, customer *entity.Customer) error {
|
||||
// Set created/updated timestamps
|
||||
now := time.Now()
|
||||
customer.CreatedAt = now
|
||||
@@ -98,8 +97,8 @@ func (r *CustomerRepositoryImpl) Create(ctx context.Context, customer *domain.Cu
|
||||
}
|
||||
|
||||
// GetByID retrieves a customer by ID
|
||||
func (r *CustomerRepositoryImpl) GetByID(ctx context.Context, id uuid.UUID) (*domain.Customer, error) {
|
||||
var customer domain.Customer
|
||||
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)
|
||||
@@ -119,8 +118,8 @@ func (r *CustomerRepositoryImpl) GetByID(ctx context.Context, id uuid.UUID) (*do
|
||||
}
|
||||
|
||||
// GetByEmail retrieves a customer by email
|
||||
func (r *CustomerRepositoryImpl) GetByEmail(ctx context.Context, email string) (*domain.Customer, error) {
|
||||
var customer domain.Customer
|
||||
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)
|
||||
@@ -140,7 +139,7 @@ func (r *CustomerRepositoryImpl) GetByEmail(ctx context.Context, email string) (
|
||||
}
|
||||
|
||||
// Update updates a customer
|
||||
func (r *CustomerRepositoryImpl) Update(ctx context.Context, customer *domain.Customer) error {
|
||||
func (r *customerRepository) Update(ctx context.Context, customer *entity.Customer) error {
|
||||
// Set updated timestamp
|
||||
customer.UpdatedAt = time.Now()
|
||||
|
||||
@@ -169,7 +168,7 @@ func (r *CustomerRepositoryImpl) Update(ctx context.Context, customer *domain.Cu
|
||||
}
|
||||
|
||||
// Delete deletes a customer (soft delete by setting is_active to false)
|
||||
func (r *CustomerRepositoryImpl) Delete(ctx context.Context, id uuid.UUID) error {
|
||||
func (r *customerRepository) Delete(ctx context.Context, id uuid.UUID) error {
|
||||
filter := bson.M{"_id": id}
|
||||
update := bson.M{
|
||||
"$set": bson.M{
|
||||
@@ -199,14 +198,14 @@ func (r *CustomerRepositoryImpl) Delete(ctx context.Context, id uuid.UUID) error
|
||||
}
|
||||
|
||||
// List retrieves customers with pagination
|
||||
func (r *CustomerRepositoryImpl) List(ctx context.Context, limit, offset int) ([]*domain.Customer, error) {
|
||||
var customers []*domain.Customer
|
||||
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{{"created_at", -1}}) // Sort by created_at desc
|
||||
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}
|
||||
@@ -233,14 +232,14 @@ func (r *CustomerRepositoryImpl) List(ctx context.Context, limit, offset int) ([
|
||||
}
|
||||
|
||||
// GetByCompanyID retrieves customers by company ID with pagination
|
||||
func (r *CustomerRepositoryImpl) GetByCompanyID(ctx context.Context, companyID uuid.UUID, limit, offset int) ([]*domain.Customer, error) {
|
||||
var customers []*domain.Customer
|
||||
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{{"created_at", -1}}) // Sort by created_at desc
|
||||
opts.SetSort(bson.D{{Key: "created_at", Value: -1}}) // Sort by created_at desc
|
||||
|
||||
// Filter by company ID and active status
|
||||
filter := bson.M{
|
||||
@@ -271,38 +270,8 @@ func (r *CustomerRepositoryImpl) GetByCompanyID(ctx context.Context, companyID u
|
||||
return customers, nil
|
||||
}
|
||||
|
||||
// UpdatePreferences updates customer preferences
|
||||
func (r *CustomerRepositoryImpl) UpdatePreferences(ctx context.Context, id uuid.UUID, preferences *domain.CustomerPreferences) error {
|
||||
filter := bson.M{"_id": id}
|
||||
update := bson.M{
|
||||
"$set": bson.M{
|
||||
"preferences": preferences,
|
||||
"updated_at": time.Now(),
|
||||
},
|
||||
}
|
||||
|
||||
result, err := r.collection.UpdateOne(ctx, filter, update)
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to update customer preferences", 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 preferences updated successfully", map[string]interface{}{
|
||||
"customer_id": id.String(),
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddDeviceToken adds a device token for push notifications
|
||||
func (r *CustomerRepositoryImpl) AddDeviceToken(ctx context.Context, id uuid.UUID, token string) error {
|
||||
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
|
||||
@@ -332,7 +301,7 @@ func (r *CustomerRepositoryImpl) AddDeviceToken(ctx context.Context, id uuid.UUI
|
||||
}
|
||||
|
||||
// RemoveDeviceToken removes a device token
|
||||
func (r *CustomerRepositoryImpl) RemoveDeviceToken(ctx context.Context, id uuid.UUID, token string) error {
|
||||
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
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -1,34 +1,38 @@
|
||||
package service
|
||||
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"
|
||||
|
||||
"tm/internal/domain"
|
||||
"tm/pkg/logger"
|
||||
)
|
||||
|
||||
// CustomerServiceImpl implements the CustomerService interface
|
||||
type CustomerServiceImpl struct {
|
||||
customerRepo domain.CustomerRepository
|
||||
// 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.CustomerRepository,
|
||||
customerRepo domain.Repository,
|
||||
config *infrastructure.AuthConfig,
|
||||
logger logger.Logger,
|
||||
) domain.CustomerService {
|
||||
return &CustomerServiceImpl{
|
||||
) CustomerService {
|
||||
return CustomerService{
|
||||
customerRepo: customerRepo,
|
||||
config: config,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// GetCustomers retrieves customers with pagination (for panel users)
|
||||
func (s *CustomerServiceImpl) GetCustomers(ctx context.Context, limit, offset int) ([]*domain.Customer, error) {
|
||||
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,
|
||||
@@ -54,7 +58,7 @@ func (s *CustomerServiceImpl) GetCustomers(ctx context.Context, limit, offset in
|
||||
}
|
||||
|
||||
// GetCustomerByID retrieves a customer by ID (for panel users)
|
||||
func (s *CustomerServiceImpl) GetCustomerByID(ctx context.Context, id uuid.UUID) (*domain.Customer, error) {
|
||||
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(),
|
||||
})
|
||||
@@ -77,7 +81,7 @@ func (s *CustomerServiceImpl) GetCustomerByID(ctx context.Context, id uuid.UUID)
|
||||
}
|
||||
|
||||
// GetCustomersByCompany retrieves customers by company ID with pagination (for panel users)
|
||||
func (s *CustomerServiceImpl) GetCustomersByCompany(ctx context.Context, companyID uuid.UUID, limit, offset int) ([]*domain.Customer, error) {
|
||||
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,
|
||||
@@ -106,7 +110,7 @@ func (s *CustomerServiceImpl) GetCustomersByCompany(ctx context.Context, company
|
||||
}
|
||||
|
||||
// UpdateCustomerStatus updates customer active status (for panel users)
|
||||
func (s *CustomerServiceImpl) UpdateCustomerStatus(ctx context.Context, customerID uuid.UUID, isActive bool, updatedBy uuid.UUID) error {
|
||||
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,
|
||||
@@ -145,35 +149,3 @@ func (s *CustomerServiceImpl) UpdateCustomerStatus(ctx context.Context, customer
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateCustomerPreferences updates customer preferences (for panel users)
|
||||
func (s *CustomerServiceImpl) UpdateCustomerPreferences(ctx context.Context, customerID uuid.UUID, preferences *domain.CustomerPreferences) error {
|
||||
s.logger.Info("Updating customer preferences", map[string]interface{}{
|
||||
"customer_id": customerID.String(),
|
||||
})
|
||||
|
||||
// Verify customer exists
|
||||
_, err := s.customerRepo.GetByID(ctx, customerID)
|
||||
if err != nil {
|
||||
s.logger.Error("Customer not found for preferences update", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"customer_id": customerID.String(),
|
||||
})
|
||||
return errors.New("customer not found")
|
||||
}
|
||||
|
||||
// Update preferences
|
||||
if err := s.customerRepo.UpdatePreferences(ctx, customerID, preferences); err != nil {
|
||||
s.logger.Error("Failed to update customer preferences", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"customer_id": customerID.String(),
|
||||
})
|
||||
return errors.New("failed to update customer preferences")
|
||||
}
|
||||
|
||||
s.logger.Info("Customer preferences updated successfully", map[string]interface{}{
|
||||
"customer_id": customerID.String(),
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package customer
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -9,36 +9,12 @@ import (
|
||||
"github.com/google/uuid"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
"tm/internal/domain"
|
||||
"tm/internal/infrastructure"
|
||||
"tm/pkg/logger"
|
||||
"tm/internal/customer/domain/aggregate"
|
||||
"tm/internal/customer/domain/entity"
|
||||
)
|
||||
|
||||
// CustomerAuthServiceImpl implements the CustomerAuthService interface
|
||||
type CustomerAuthServiceImpl struct {
|
||||
customerRepo domain.CustomerRepository
|
||||
companyRepo domain.CompanyRepository
|
||||
config *infrastructure.AuthConfig
|
||||
logger logger.Logger
|
||||
}
|
||||
|
||||
// NewCustomerAuthService creates a new customer authentication service
|
||||
func NewCustomerAuthService(
|
||||
customerRepo domain.CustomerRepository,
|
||||
companyRepo domain.CompanyRepository,
|
||||
config *infrastructure.AuthConfig,
|
||||
logger logger.Logger,
|
||||
) domain.CustomerAuthService {
|
||||
return &CustomerAuthServiceImpl{
|
||||
customerRepo: customerRepo,
|
||||
companyRepo: companyRepo,
|
||||
config: config,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// Register creates a new customer account
|
||||
func (s *CustomerAuthServiceImpl) Register(ctx context.Context, req domain.CustomerRegisterRequest) (*domain.CustomerAuthResponse, error) {
|
||||
func (s *CustomerService) Register(ctx context.Context, req RegisterForm) (*aggregate.AuthorizationResponse, error) {
|
||||
s.logger.Info("Registering new customer", map[string]interface{}{
|
||||
"email": req.Email,
|
||||
})
|
||||
@@ -58,71 +34,19 @@ func (s *CustomerAuthServiceImpl) Register(ctx context.Context, req domain.Custo
|
||||
return nil, errors.New("failed to process password")
|
||||
}
|
||||
|
||||
// Handle company assignment
|
||||
var companyID uuid.UUID
|
||||
if req.CompanyID != "" {
|
||||
// Parse existing company ID
|
||||
companyID, err = uuid.Parse(req.CompanyID)
|
||||
if err != nil {
|
||||
return nil, errors.New("invalid company ID")
|
||||
}
|
||||
|
||||
// Verify company exists
|
||||
_, err = s.companyRepo.GetByID(ctx, companyID)
|
||||
if err != nil {
|
||||
return nil, errors.New("company not found")
|
||||
}
|
||||
} else {
|
||||
// Create new company for customer
|
||||
company := &domain.Company{
|
||||
ID: uuid.New(),
|
||||
Name: req.FirstName + " " + req.LastName + "'s Company",
|
||||
Description: "Personal company profile",
|
||||
Industry: "Not specified",
|
||||
Country: "Not specified",
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
if err := s.companyRepo.Create(ctx, company); err != nil {
|
||||
s.logger.Error("Failed to create company for customer", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"email": req.Email,
|
||||
})
|
||||
return nil, errors.New("failed to create company")
|
||||
}
|
||||
|
||||
companyID = company.ID
|
||||
}
|
||||
|
||||
// Create customer
|
||||
customer := &domain.Customer{
|
||||
customer := &entity.Customer{
|
||||
ID: uuid.New(),
|
||||
Email: req.Email,
|
||||
Password: hashedPassword,
|
||||
FirstName: req.FirstName,
|
||||
LastName: req.LastName,
|
||||
Phone: req.Phone,
|
||||
Role: domain.CustomerRoleUser,
|
||||
CompanyID: companyID,
|
||||
Mobile: req.Mobile,
|
||||
IsActive: true,
|
||||
IsVerified: false, // Require email verification
|
||||
DeviceTokens: make([]string, 0),
|
||||
Preferences: domain.CustomerPreferences{
|
||||
Language: "en",
|
||||
NotificationSettings: domain.NotificationSettings{
|
||||
NewTenders: true,
|
||||
DeadlineReminder: true,
|
||||
StatusUpdates: true,
|
||||
SystemAlerts: true,
|
||||
},
|
||||
TenderAlerts: true,
|
||||
EmailNotifications: true,
|
||||
PushNotifications: true,
|
||||
PreferredCategories: make([]string, 0),
|
||||
},
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
|
||||
if err := s.customerRepo.Create(ctx, customer); err != nil {
|
||||
@@ -161,8 +85,8 @@ func (s *CustomerAuthServiceImpl) Register(ctx context.Context, req domain.Custo
|
||||
// TODO: Send verification email
|
||||
s.sendVerificationEmail(ctx, customer)
|
||||
|
||||
return &domain.CustomerAuthResponse{
|
||||
Customer: customer,
|
||||
return &aggregate.AuthorizationResponse{
|
||||
Customer: aggregate.NewCustomer(*customer),
|
||||
AccessToken: accessToken,
|
||||
RefreshToken: refreshToken,
|
||||
ExpiresIn: int64(s.config.JWT.AccessTokenDuration.Seconds()),
|
||||
@@ -170,7 +94,7 @@ func (s *CustomerAuthServiceImpl) Register(ctx context.Context, req domain.Custo
|
||||
}
|
||||
|
||||
// Login authenticates a customer and returns tokens
|
||||
func (s *CustomerAuthServiceImpl) Login(ctx context.Context, req domain.LoginRequest) (*domain.CustomerAuthResponse, error) {
|
||||
func (s *CustomerService) Login(ctx context.Context, req LoginForm) (*aggregate.AuthorizationResponse, error) {
|
||||
s.logger.Info("Customer login attempt", map[string]interface{}{
|
||||
"email": req.Email,
|
||||
})
|
||||
@@ -240,8 +164,8 @@ func (s *CustomerAuthServiceImpl) Login(ctx context.Context, req domain.LoginReq
|
||||
"is_verified": customer.IsVerified,
|
||||
})
|
||||
|
||||
return &domain.CustomerAuthResponse{
|
||||
Customer: customer,
|
||||
return &aggregate.AuthorizationResponse{
|
||||
Customer: aggregate.NewCustomer(*customer),
|
||||
AccessToken: accessToken,
|
||||
RefreshToken: refreshToken,
|
||||
ExpiresIn: int64(s.config.JWT.AccessTokenDuration.Seconds()),
|
||||
@@ -249,7 +173,7 @@ func (s *CustomerAuthServiceImpl) Login(ctx context.Context, req domain.LoginReq
|
||||
}
|
||||
|
||||
// RefreshToken generates new tokens using refresh token
|
||||
func (s *CustomerAuthServiceImpl) RefreshToken(ctx context.Context, refreshToken string) (*domain.CustomerAuthResponse, error) {
|
||||
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 {
|
||||
@@ -310,8 +234,8 @@ func (s *CustomerAuthServiceImpl) RefreshToken(ctx context.Context, refreshToken
|
||||
return nil, errors.New("failed to generate refresh token")
|
||||
}
|
||||
|
||||
return &domain.CustomerAuthResponse{
|
||||
Customer: customer,
|
||||
return &aggregate.AuthorizationResponse{
|
||||
Customer: aggregate.NewCustomer(*customer),
|
||||
AccessToken: accessToken,
|
||||
RefreshToken: newRefreshToken,
|
||||
ExpiresIn: int64(s.config.JWT.AccessTokenDuration.Seconds()),
|
||||
@@ -319,7 +243,7 @@ func (s *CustomerAuthServiceImpl) RefreshToken(ctx context.Context, refreshToken
|
||||
}
|
||||
|
||||
// ValidateToken validates a JWT token and returns the customer
|
||||
func (s *CustomerAuthServiceImpl) ValidateToken(ctx context.Context, tokenString string) (*domain.Customer, error) {
|
||||
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")
|
||||
@@ -371,7 +295,7 @@ func (s *CustomerAuthServiceImpl) ValidateToken(ctx context.Context, tokenString
|
||||
}
|
||||
|
||||
// ChangePassword changes customer password
|
||||
func (s *CustomerAuthServiceImpl) ChangePassword(ctx context.Context, customerID uuid.UUID, oldPassword, newPassword string) error {
|
||||
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")
|
||||
@@ -408,7 +332,7 @@ func (s *CustomerAuthServiceImpl) ChangePassword(ctx context.Context, customerID
|
||||
}
|
||||
|
||||
// ResetPassword initiates password reset process
|
||||
func (s *CustomerAuthServiceImpl) ResetPassword(ctx context.Context, email string) error {
|
||||
func (s *CustomerService) ResetPassword(ctx context.Context, email string) error {
|
||||
// TODO: Implement password reset logic
|
||||
// This would typically involve:
|
||||
// 1. Generate reset token
|
||||
@@ -418,7 +342,7 @@ func (s *CustomerAuthServiceImpl) ResetPassword(ctx context.Context, email strin
|
||||
}
|
||||
|
||||
// VerifyEmail verifies customer's email address
|
||||
func (s *CustomerAuthServiceImpl) VerifyEmail(ctx context.Context, customerID uuid.UUID, token string) error {
|
||||
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
|
||||
@@ -427,8 +351,8 @@ func (s *CustomerAuthServiceImpl) VerifyEmail(ctx context.Context, customerID uu
|
||||
return errors.New("email verification not implemented")
|
||||
}
|
||||
|
||||
// ResendVerification resends verification email
|
||||
func (s *CustomerAuthServiceImpl) ResendVerification(ctx context.Context, email string) error {
|
||||
// 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")
|
||||
@@ -446,7 +370,7 @@ func (s *CustomerAuthServiceImpl) ResendVerification(ctx context.Context, email
|
||||
|
||||
// Private helper methods
|
||||
|
||||
func (s *CustomerAuthServiceImpl) hashPassword(password string) (string, error) {
|
||||
func (s *CustomerService) hashPassword(password string) (string, error) {
|
||||
hashedBytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -454,16 +378,15 @@ func (s *CustomerAuthServiceImpl) hashPassword(password string) (string, error)
|
||||
return string(hashedBytes), nil
|
||||
}
|
||||
|
||||
func (s *CustomerAuthServiceImpl) verifyPassword(password, hashedPassword string) bool {
|
||||
func (s *CustomerService) verifyPassword(password, hashedPassword string) bool {
|
||||
err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password))
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (s *CustomerAuthServiceImpl) generateAccessToken(customer *domain.Customer) (string, error) {
|
||||
func (s *CustomerService) generateAccessToken(customer *entity.Customer) (string, error) {
|
||||
claims := jwt.MapClaims{
|
||||
"customer_id": customer.ID.String(),
|
||||
"email": customer.Email,
|
||||
"role": customer.Role,
|
||||
"company_id": customer.CompanyID.String(),
|
||||
"user_type": "customer",
|
||||
"exp": time.Now().Add(s.config.JWT.AccessTokenDuration).Unix(),
|
||||
@@ -475,7 +398,7 @@ func (s *CustomerAuthServiceImpl) generateAccessToken(customer *domain.Customer)
|
||||
return token.SignedString([]byte(s.config.JWT.Secret))
|
||||
}
|
||||
|
||||
func (s *CustomerAuthServiceImpl) generateRefreshToken(customer *domain.Customer) (string, error) {
|
||||
func (s *CustomerService) generateRefreshToken(customer *entity.Customer) (string, error) {
|
||||
claims := jwt.MapClaims{
|
||||
"customer_id": customer.ID.String(),
|
||||
"user_type": "customer",
|
||||
@@ -488,8 +411,9 @@ func (s *CustomerAuthServiceImpl) generateRefreshToken(customer *domain.Customer
|
||||
return token.SignedString([]byte(s.config.JWT.Secret))
|
||||
}
|
||||
|
||||
func (s *CustomerAuthServiceImpl) sendVerificationEmail(ctx context.Context, customer *domain.Customer) {
|
||||
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,
|
||||
@@ -1,389 +0,0 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// Customer represents a mobile app user (customer)
|
||||
type Customer struct {
|
||||
ID uuid.UUID `json:"id" bson:"_id"`
|
||||
Email string `json:"email" bson:"email"`
|
||||
Password string `json:"-" bson:"password"` // Never serialize password
|
||||
FirstName string `json:"first_name" bson:"first_name"`
|
||||
LastName string `json:"last_name" bson:"last_name"`
|
||||
Phone string `json:"phone" bson:"phone"`
|
||||
Role CustomerRole `json:"role" bson:"role"`
|
||||
CompanyID uuid.UUID `json:"company_id" bson:"company_id"`
|
||||
IsActive bool `json:"is_active" bson:"is_active"`
|
||||
IsVerified bool `json:"is_verified" bson:"is_verified"`
|
||||
DeviceTokens []string `json:"device_tokens" bson:"device_tokens"` // For push notifications
|
||||
Preferences CustomerPreferences `json:"preferences" bson:"preferences"`
|
||||
LastLoginAt *time.Time `json:"last_login_at,omitempty" bson:"last_login_at,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at" bson:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at" bson:"updated_at"`
|
||||
}
|
||||
|
||||
// CustomerPreferences represents customer-specific preferences
|
||||
type CustomerPreferences struct {
|
||||
Language string `json:"language" bson:"language"`
|
||||
NotificationSettings NotificationSettings `json:"notification_settings" bson:"notification_settings"`
|
||||
TenderAlerts bool `json:"tender_alerts" bson:"tender_alerts"`
|
||||
EmailNotifications bool `json:"email_notifications" bson:"email_notifications"`
|
||||
PushNotifications bool `json:"push_notifications" bson:"push_notifications"`
|
||||
PreferredCategories []string `json:"preferred_categories" bson:"preferred_categories"`
|
||||
}
|
||||
|
||||
// NotificationSettings represents notification preferences
|
||||
type NotificationSettings struct {
|
||||
NewTenders bool `json:"new_tenders" bson:"new_tenders"`
|
||||
DeadlineReminder bool `json:"deadline_reminder" bson:"deadline_reminder"`
|
||||
StatusUpdates bool `json:"status_updates" bson:"status_updates"`
|
||||
SystemAlerts bool `json:"system_alerts" bson:"system_alerts"`
|
||||
}
|
||||
|
||||
// CustomerRole defines roles for mobile app users
|
||||
type CustomerRole string
|
||||
|
||||
const (
|
||||
CustomerRoleUser CustomerRole = "customer_user" // Regular customer
|
||||
CustomerRoleManager CustomerRole = "customer_manager" // Company manager
|
||||
)
|
||||
|
||||
// User represents a panel user (admin/staff)
|
||||
type User struct {
|
||||
ID uuid.UUID `json:"id" bson:"_id"`
|
||||
Email string `json:"email" bson:"email"`
|
||||
Password string `json:"-" bson:"password"` // Never serialize password
|
||||
FirstName string `json:"first_name" bson:"first_name"`
|
||||
LastName string `json:"last_name" bson:"last_name"`
|
||||
Role UserRole `json:"role" bson:"role"`
|
||||
Department string `json:"department" bson:"department"`
|
||||
IsActive bool `json:"is_active" bson:"is_active"`
|
||||
Permissions []string `json:"permissions" bson:"permissions"`
|
||||
LastLoginAt *time.Time `json:"last_login_at,omitempty" bson:"last_login_at,omitempty"`
|
||||
CreatedBy *uuid.UUID `json:"created_by,omitempty" bson:"created_by,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at" bson:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at" bson:"updated_at"`
|
||||
}
|
||||
|
||||
// UserRole defines roles for panel users
|
||||
type UserRole string
|
||||
|
||||
const (
|
||||
UserRoleAdmin UserRole = "admin" // Full system administrator
|
||||
UserRoleModerator UserRole = "moderator" // Moderator with limited admin access
|
||||
UserRoleSupport UserRole = "support" // Support user with read-only access
|
||||
UserRoleAnalyst UserRole = "analyst" // Data analyst with reporting access
|
||||
)
|
||||
|
||||
// Customer methods
|
||||
func (c *Customer) HasRole(role CustomerRole) bool {
|
||||
return c.Role == role
|
||||
}
|
||||
|
||||
func (c *Customer) IsManager() bool {
|
||||
return c.Role == CustomerRoleManager
|
||||
}
|
||||
|
||||
func (c *Customer) CanAccessCompanySettings() bool {
|
||||
return c.IsActive && c.IsVerified && c.IsManager()
|
||||
}
|
||||
|
||||
func (c *Customer) FullName() string {
|
||||
return c.FirstName + " " + c.LastName
|
||||
}
|
||||
|
||||
// User methods
|
||||
func (u *User) HasRole(role UserRole) bool {
|
||||
return u.Role == role
|
||||
}
|
||||
|
||||
func (u *User) HasAnyRole(roles ...UserRole) bool {
|
||||
for _, role := range roles {
|
||||
if u.Role == role {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (u *User) HasPermission(permission string) bool {
|
||||
for _, perm := range u.Permissions {
|
||||
if perm == permission {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (u *User) IsAdmin() bool {
|
||||
return u.Role == UserRoleAdmin
|
||||
}
|
||||
|
||||
func (u *User) CanManageUsers() bool {
|
||||
return u.IsActive && (u.Role == UserRoleAdmin || u.HasPermission("manage_users"))
|
||||
}
|
||||
|
||||
func (u *User) CanManageTenders() bool {
|
||||
return u.IsActive && (u.Role == UserRoleAdmin || u.Role == UserRoleModerator || u.HasPermission("manage_tenders"))
|
||||
}
|
||||
|
||||
func (u *User) CanViewReports() bool {
|
||||
return u.IsActive && (u.Role == UserRoleAdmin || u.Role == UserRoleAnalyst || u.HasPermission("view_reports"))
|
||||
}
|
||||
|
||||
func (u *User) FullName() string {
|
||||
return u.FirstName + " " + u.LastName
|
||||
}
|
||||
|
||||
// Company represents a company profile
|
||||
type Company struct {
|
||||
ID uuid.UUID `json:"id" bson:"_id"`
|
||||
Name string `json:"name" bson:"name"`
|
||||
Description string `json:"description" bson:"description"`
|
||||
Industry string `json:"industry" bson:"industry"`
|
||||
Country string `json:"country" bson:"country"`
|
||||
Website string `json:"website" bson:"website"`
|
||||
Keywords []string `json:"keywords" bson:"keywords"`
|
||||
Products []string `json:"products" bson:"products"`
|
||||
Services []string `json:"services" bson:"services"`
|
||||
Capabilities []string `json:"capabilities" bson:"capabilities"`
|
||||
Documents []Document `json:"documents" bson:"documents"`
|
||||
Interests InterestProfile `json:"interests" bson:"interests"`
|
||||
CreatedAt time.Time `json:"created_at" bson:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at" bson:"updated_at"`
|
||||
}
|
||||
|
||||
// InterestProfile represents company's tender interests
|
||||
type InterestProfile struct {
|
||||
Keywords []string `json:"keywords" bson:"keywords"`
|
||||
Industries []string `json:"industries" bson:"industries"`
|
||||
CPVCodes []string `json:"cpv_codes" bson:"cpv_codes"`
|
||||
MinBudget *float64 `json:"min_budget,omitempty" bson:"min_budget,omitempty"`
|
||||
MaxBudget *float64 `json:"max_budget,omitempty" bson:"max_budget,omitempty"`
|
||||
Regions []string `json:"regions" bson:"regions"`
|
||||
Embeddings []float32 `json:"-" bson:"embeddings"` // AI embeddings for matching
|
||||
UpdatedAt time.Time `json:"updated_at" bson:"updated_at"`
|
||||
}
|
||||
|
||||
// Tender represents a tender opportunity
|
||||
type Tender struct {
|
||||
ID uuid.UUID `json:"id" bson:"_id"`
|
||||
Title string `json:"title" bson:"title"`
|
||||
Description string `json:"description" bson:"description"`
|
||||
Summary string `json:"summary" bson:"summary"`
|
||||
Type TenderType `json:"type" bson:"type"`
|
||||
Status TenderStatus `json:"status" bson:"status"`
|
||||
PublicationDate time.Time `json:"publication_date" bson:"publication_date"`
|
||||
Deadline time.Time `json:"deadline" bson:"deadline"`
|
||||
Budget *float64 `json:"budget,omitempty" bson:"budget,omitempty"`
|
||||
Currency string `json:"currency" bson:"currency"`
|
||||
Region string `json:"region" bson:"region"`
|
||||
Country string `json:"country" bson:"country"`
|
||||
CPVCodes []string `json:"cpv_codes" bson:"cpv_codes"`
|
||||
Categories []string `json:"categories" bson:"categories"`
|
||||
Keywords []string `json:"keywords" bson:"keywords"`
|
||||
Tags []string `json:"tags" bson:"tags"`
|
||||
Language string `json:"language" bson:"language"`
|
||||
SourceURL string `json:"source_url" bson:"source_url"`
|
||||
SourceName string `json:"source_name" bson:"source_name"`
|
||||
Documents []TenderDocument `json:"documents" bson:"documents"`
|
||||
Requirements []Requirement `json:"requirements" bson:"requirements"`
|
||||
Embeddings []float32 `json:"-" bson:"embeddings"` // AI embeddings for matching
|
||||
ProcessedAt *time.Time `json:"processed_at,omitempty" bson:"processed_at,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at" bson:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at" bson:"updated_at"`
|
||||
}
|
||||
|
||||
// TenderType defines types of tenders
|
||||
type TenderType string
|
||||
|
||||
const (
|
||||
TenderTypeRFI TenderType = "RFI" // Request for Information
|
||||
TenderTypeRFP TenderType = "RFP" // Request for Proposal
|
||||
TenderTypeRFQ TenderType = "RFQ" // Request for Quote
|
||||
TenderTypeTender TenderType = "TENDER" // Public Tender
|
||||
)
|
||||
|
||||
// TenderStatus defines tender statuses
|
||||
type TenderStatus string
|
||||
|
||||
const (
|
||||
TenderStatusActive TenderStatus = "active"
|
||||
TenderStatusExpired TenderStatus = "expired"
|
||||
TenderStatusCancelled TenderStatus = "cancelled"
|
||||
TenderStatusAwarded TenderStatus = "awarded"
|
||||
)
|
||||
|
||||
// TenderDocument represents documents associated with a tender
|
||||
type TenderDocument struct {
|
||||
ID uuid.UUID `json:"id" bson:"id"`
|
||||
Name string `json:"name" bson:"name"`
|
||||
Type DocumentType `json:"type" bson:"type"`
|
||||
URL string `json:"url" bson:"url"`
|
||||
LocalPath string `json:"local_path" bson:"local_path"`
|
||||
Size int64 `json:"size" bson:"size"`
|
||||
ContentType string `json:"content_type" bson:"content_type"`
|
||||
Checksum string `json:"checksum" bson:"checksum"`
|
||||
Downloaded bool `json:"downloaded" bson:"downloaded"`
|
||||
DownloadedAt *time.Time `json:"downloaded_at,omitempty" bson:"downloaded_at,omitempty"`
|
||||
}
|
||||
|
||||
// Requirement represents tender requirements
|
||||
type Requirement struct {
|
||||
ID uuid.UUID `json:"id" bson:"id"`
|
||||
Type RequirementType `json:"type" bson:"type"`
|
||||
Description string `json:"description" bson:"description"`
|
||||
Mandatory bool `json:"mandatory" bson:"mandatory"`
|
||||
Category string `json:"category" bson:"category"`
|
||||
}
|
||||
|
||||
// RequirementType defines types of requirements
|
||||
type RequirementType string
|
||||
|
||||
const (
|
||||
RequirementTypeLegal RequirementType = "legal"
|
||||
RequirementTypeTechnical RequirementType = "technical"
|
||||
RequirementTypeFinancial RequirementType = "financial"
|
||||
RequirementTypeExperience RequirementType = "experience"
|
||||
)
|
||||
|
||||
// Document represents uploaded company documents
|
||||
type Document struct {
|
||||
ID uuid.UUID `json:"id" bson:"id"`
|
||||
Name string `json:"name" bson:"name"`
|
||||
Type DocumentType `json:"type" bson:"type"`
|
||||
Category string `json:"category" bson:"category"`
|
||||
Description string `json:"description" bson:"description"`
|
||||
FilePath string `json:"file_path" bson:"file_path"`
|
||||
Size int64 `json:"size" bson:"size"`
|
||||
ContentType string `json:"content_type" bson:"content_type"`
|
||||
Checksum string `json:"checksum" bson:"checksum"`
|
||||
ExpiryDate *time.Time `json:"expiry_date,omitempty" bson:"expiry_date,omitempty"`
|
||||
Keywords []string `json:"keywords" bson:"keywords"`
|
||||
UploadedBy uuid.UUID `json:"uploaded_by" bson:"uploaded_by"`
|
||||
CreatedAt time.Time `json:"created_at" bson:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at" bson:"updated_at"`
|
||||
}
|
||||
|
||||
// DocumentType defines types of documents
|
||||
type DocumentType string
|
||||
|
||||
const (
|
||||
DocumentTypePDF DocumentType = "pdf"
|
||||
DocumentTypeWord DocumentType = "word"
|
||||
DocumentTypeExcel DocumentType = "excel"
|
||||
DocumentTypeImage DocumentType = "image"
|
||||
DocumentTypeCertificate DocumentType = "certificate"
|
||||
DocumentTypeLicense DocumentType = "license"
|
||||
DocumentTypeContract DocumentType = "contract"
|
||||
DocumentTypeCatalog DocumentType = "catalog"
|
||||
)
|
||||
|
||||
// TenderSource represents tender source configuration
|
||||
type TenderSource struct {
|
||||
ID uuid.UUID `json:"id" bson:"_id"`
|
||||
Name string `json:"name" bson:"name"`
|
||||
BaseURL string `json:"base_url" bson:"base_url"`
|
||||
Type SourceType `json:"type" bson:"type"`
|
||||
AuthRequired bool `json:"auth_required" bson:"auth_required"`
|
||||
CredentialsID *uuid.UUID `json:"credentials_id,omitempty" bson:"credentials_id,omitempty"`
|
||||
CheckInterval time.Duration `json:"check_interval" bson:"check_interval"`
|
||||
CategoryTags []string `json:"category_tags" bson:"category_tags"`
|
||||
Region string `json:"region" bson:"region"`
|
||||
Language string `json:"language" bson:"language"`
|
||||
Status SourceStatus `json:"status" bson:"status"`
|
||||
LastCheckedAt *time.Time `json:"last_checked_at,omitempty" bson:"last_checked_at,omitempty"`
|
||||
Configuration SourceConfiguration `json:"configuration" bson:"configuration"`
|
||||
CreatedAt time.Time `json:"created_at" bson:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at" bson:"updated_at"`
|
||||
}
|
||||
|
||||
// SourceType defines types of tender sources
|
||||
type SourceType string
|
||||
|
||||
const (
|
||||
SourceTypeAPI SourceType = "api"
|
||||
SourceTypeHTML SourceType = "html"
|
||||
SourceTypeJS SourceType = "js"
|
||||
SourceTypePDF SourceType = "pdf"
|
||||
SourceTypeRSS SourceType = "rss"
|
||||
)
|
||||
|
||||
// SourceStatus defines tender source statuses
|
||||
type SourceStatus string
|
||||
|
||||
const (
|
||||
SourceStatusActive SourceStatus = "active"
|
||||
SourceStatusInactive SourceStatus = "inactive"
|
||||
SourceStatusError SourceStatus = "error"
|
||||
)
|
||||
|
||||
// SourceConfiguration holds source-specific configuration
|
||||
type SourceConfiguration struct {
|
||||
Headers map[string]string `json:"headers,omitempty" bson:"headers,omitempty"`
|
||||
QueryParams map[string]string `json:"query_params,omitempty" bson:"query_params,omitempty"`
|
||||
Selectors map[string]string `json:"selectors,omitempty" bson:"selectors,omitempty"`
|
||||
PaginationKey string `json:"pagination_key,omitempty" bson:"pagination_key,omitempty"`
|
||||
MaxPages int `json:"max_pages,omitempty" bson:"max_pages,omitempty"`
|
||||
RateLimitDelay time.Duration `json:"rate_limit_delay,omitempty" bson:"rate_limit_delay,omitempty"`
|
||||
}
|
||||
|
||||
// TenderMatch represents AI-powered tender recommendations
|
||||
type TenderMatch struct {
|
||||
ID uuid.UUID `json:"id" bson:"_id"`
|
||||
CompanyID uuid.UUID `json:"company_id" bson:"company_id"`
|
||||
TenderID uuid.UUID `json:"tender_id" bson:"tender_id"`
|
||||
Score float64 `json:"score" bson:"score"`
|
||||
Explanation string `json:"explanation" bson:"explanation"`
|
||||
Tags []string `json:"tags" bson:"tags"`
|
||||
Status MatchStatus `json:"status" bson:"status"`
|
||||
UserAction *UserAction `json:"user_action,omitempty" bson:"user_action,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at" bson:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at" bson:"updated_at"`
|
||||
}
|
||||
|
||||
// MatchStatus defines tender match statuses
|
||||
type MatchStatus string
|
||||
|
||||
const (
|
||||
MatchStatusPending MatchStatus = "pending"
|
||||
MatchStatusViewed MatchStatus = "viewed"
|
||||
MatchStatusLiked MatchStatus = "liked"
|
||||
MatchStatusDisliked MatchStatus = "disliked"
|
||||
MatchStatusApplied MatchStatus = "applied"
|
||||
)
|
||||
|
||||
// UserAction represents user interaction with tender matches
|
||||
type UserAction struct {
|
||||
Action MatchStatus `json:"action" bson:"action"`
|
||||
Timestamp time.Time `json:"timestamp" bson:"timestamp"`
|
||||
UserID uuid.UUID `json:"user_id" bson:"user_id"`
|
||||
}
|
||||
|
||||
// Notification represents system notifications
|
||||
type Notification struct {
|
||||
ID uuid.UUID `json:"id" bson:"_id"`
|
||||
UserID uuid.UUID `json:"user_id" bson:"user_id"`
|
||||
CompanyID uuid.UUID `json:"company_id" bson:"company_id"`
|
||||
Type NotificationType `json:"type" bson:"type"`
|
||||
Title string `json:"title" bson:"title"`
|
||||
Message string `json:"message" bson:"message"`
|
||||
Data map[string]any `json:"data,omitempty" bson:"data,omitempty"`
|
||||
Read bool `json:"read" bson:"read"`
|
||||
Sent bool `json:"sent" bson:"sent"`
|
||||
SentAt *time.Time `json:"sent_at,omitempty" bson:"sent_at,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at" bson:"created_at"`
|
||||
}
|
||||
|
||||
// NotificationType defines types of notifications
|
||||
type NotificationType string
|
||||
|
||||
const (
|
||||
NotificationTypeNewTender NotificationType = "new_tender"
|
||||
NotificationTypeDeadline NotificationType = "deadline_reminder"
|
||||
NotificationTypeStatusUpdate NotificationType = "status_update"
|
||||
NotificationTypeSystemAlert NotificationType = "system_alert"
|
||||
)
|
||||
@@ -1,321 +0,0 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// Repository interfaces define data access contracts
|
||||
|
||||
// CustomerRepository defines methods for customer (mobile user) data access
|
||||
type CustomerRepository interface {
|
||||
Create(ctx context.Context, customer *Customer) error
|
||||
GetByID(ctx context.Context, id uuid.UUID) (*Customer, error)
|
||||
GetByEmail(ctx context.Context, email string) (*Customer, error)
|
||||
Update(ctx context.Context, customer *Customer) error
|
||||
Delete(ctx context.Context, id uuid.UUID) error
|
||||
List(ctx context.Context, limit, offset int) ([]*Customer, error)
|
||||
GetByCompanyID(ctx context.Context, companyID uuid.UUID, limit, offset int) ([]*Customer, error)
|
||||
UpdatePreferences(ctx context.Context, id uuid.UUID, preferences *CustomerPreferences) error
|
||||
AddDeviceToken(ctx context.Context, id uuid.UUID, token string) error
|
||||
RemoveDeviceToken(ctx context.Context, id uuid.UUID, token string) error
|
||||
}
|
||||
|
||||
// UserRepository defines methods for panel user data access
|
||||
type UserRepository interface {
|
||||
Create(ctx context.Context, user *User) error
|
||||
GetByID(ctx context.Context, id uuid.UUID) (*User, error)
|
||||
GetByEmail(ctx context.Context, email string) (*User, error)
|
||||
Update(ctx context.Context, user *User) error
|
||||
Delete(ctx context.Context, id uuid.UUID) error
|
||||
List(ctx context.Context, limit, offset int) ([]*User, error)
|
||||
GetByRole(ctx context.Context, role UserRole, limit, offset int) ([]*User, error)
|
||||
UpdatePermissions(ctx context.Context, id uuid.UUID, permissions []string) error
|
||||
}
|
||||
|
||||
// CompanyRepository defines methods for company data access
|
||||
type CompanyRepository interface {
|
||||
Create(ctx context.Context, company *Company) error
|
||||
GetByID(ctx context.Context, id uuid.UUID) (*Company, error)
|
||||
Update(ctx context.Context, company *Company) error
|
||||
Delete(ctx context.Context, id uuid.UUID) error
|
||||
List(ctx context.Context, limit, offset int) ([]*Company, error)
|
||||
UpdateInterests(ctx context.Context, id uuid.UUID, interests *InterestProfile) error
|
||||
}
|
||||
|
||||
// TenderRepository defines methods for tender data access
|
||||
type TenderRepository interface {
|
||||
Create(ctx context.Context, tender *Tender) error
|
||||
GetByID(ctx context.Context, id uuid.UUID) (*Tender, error)
|
||||
Update(ctx context.Context, tender *Tender) error
|
||||
Delete(ctx context.Context, id uuid.UUID) error
|
||||
List(ctx context.Context, filters TenderFilters, limit, offset int) ([]*Tender, error)
|
||||
ListByStatus(ctx context.Context, status TenderStatus, limit, offset int) ([]*Tender, error)
|
||||
Search(ctx context.Context, query string, filters TenderFilters, limit, offset int) ([]*Tender, error)
|
||||
GetExpiringTenders(ctx context.Context, days int) ([]*Tender, error)
|
||||
BulkCreate(ctx context.Context, tenders []*Tender) error
|
||||
}
|
||||
|
||||
// TenderSourceRepository defines methods for tender source data access
|
||||
type TenderSourceRepository interface {
|
||||
Create(ctx context.Context, source *TenderSource) error
|
||||
GetByID(ctx context.Context, id uuid.UUID) (*TenderSource, error)
|
||||
Update(ctx context.Context, source *TenderSource) error
|
||||
Delete(ctx context.Context, id uuid.UUID) error
|
||||
List(ctx context.Context, limit, offset int) ([]*TenderSource, error)
|
||||
GetActiveources(ctx context.Context) ([]*TenderSource, error)
|
||||
UpdateLastChecked(ctx context.Context, id uuid.UUID) error
|
||||
}
|
||||
|
||||
// TenderMatchRepository defines methods for tender match data access
|
||||
type TenderMatchRepository interface {
|
||||
Create(ctx context.Context, match *TenderMatch) error
|
||||
GetByID(ctx context.Context, id uuid.UUID) (*TenderMatch, error)
|
||||
Update(ctx context.Context, match *TenderMatch) error
|
||||
Delete(ctx context.Context, id uuid.UUID) error
|
||||
GetByCompany(ctx context.Context, companyID uuid.UUID, limit, offset int) ([]*TenderMatch, error)
|
||||
GetByTender(ctx context.Context, tenderID uuid.UUID, limit, offset int) ([]*TenderMatch, error)
|
||||
UpdateUserAction(ctx context.Context, id uuid.UUID, action *UserAction) error
|
||||
BulkCreate(ctx context.Context, matches []*TenderMatch) error
|
||||
}
|
||||
|
||||
// DocumentRepository defines methods for document data access
|
||||
type DocumentRepository interface {
|
||||
Create(ctx context.Context, document *Document) error
|
||||
GetByID(ctx context.Context, id uuid.UUID) (*Document, error)
|
||||
Update(ctx context.Context, document *Document) error
|
||||
Delete(ctx context.Context, id uuid.UUID) error
|
||||
GetByCompany(ctx context.Context, companyID uuid.UUID) ([]*Document, error)
|
||||
GetByCategory(ctx context.Context, companyID uuid.UUID, category string) ([]*Document, error)
|
||||
}
|
||||
|
||||
// NotificationRepository defines methods for notification data access
|
||||
type NotificationRepository interface {
|
||||
Create(ctx context.Context, notification *Notification) error
|
||||
GetByID(ctx context.Context, id uuid.UUID) (*Notification, error)
|
||||
Update(ctx context.Context, notification *Notification) error
|
||||
Delete(ctx context.Context, id uuid.UUID) error
|
||||
GetByUser(ctx context.Context, userID uuid.UUID, limit, offset int) ([]*Notification, error)
|
||||
GetUnreadByUser(ctx context.Context, userID uuid.UUID) ([]*Notification, error)
|
||||
MarkAsRead(ctx context.Context, id uuid.UUID) error
|
||||
MarkAsSent(ctx context.Context, id uuid.UUID) error
|
||||
}
|
||||
|
||||
// Service interfaces define business logic contracts
|
||||
|
||||
// CustomerAuthService defines authentication methods for mobile customers
|
||||
type CustomerAuthService interface {
|
||||
Register(ctx context.Context, req CustomerRegisterRequest) (*CustomerAuthResponse, error)
|
||||
Login(ctx context.Context, req LoginRequest) (*CustomerAuthResponse, error)
|
||||
RefreshToken(ctx context.Context, refreshToken string) (*CustomerAuthResponse, error)
|
||||
ValidateToken(ctx context.Context, token string) (*Customer, error)
|
||||
ChangePassword(ctx context.Context, customerID uuid.UUID, oldPassword, newPassword string) error
|
||||
ResetPassword(ctx context.Context, email string) error
|
||||
VerifyEmail(ctx context.Context, customerID uuid.UUID, token string) error
|
||||
ResendVerification(ctx context.Context, email string) error
|
||||
}
|
||||
|
||||
// UserAuthService defines authentication methods for panel users
|
||||
type UserAuthService interface {
|
||||
Login(ctx context.Context, req LoginRequest) (*UserAuthResponse, error)
|
||||
RefreshToken(ctx context.Context, refreshToken string) (*UserAuthResponse, error)
|
||||
ValidateToken(ctx context.Context, token string) (*User, error)
|
||||
ChangePassword(ctx context.Context, userID uuid.UUID, oldPassword, newPassword string) error
|
||||
CreatePanelUser(ctx context.Context, req CreateUserRequest, createdBy uuid.UUID) (*User, error)
|
||||
UpdateUserPermissions(ctx context.Context, userID uuid.UUID, permissions []string, updatedBy uuid.UUID) error
|
||||
}
|
||||
|
||||
// TenderService defines tender management methods
|
||||
type TenderService interface {
|
||||
GetTenders(ctx context.Context, filters TenderFilters, limit, offset int) ([]*Tender, error)
|
||||
GetTenderByID(ctx context.Context, id uuid.UUID) (*Tender, error)
|
||||
SearchTenders(ctx context.Context, query string, filters TenderFilters, limit, offset int) ([]*Tender, error)
|
||||
GetTenderMatches(ctx context.Context, companyID uuid.UUID, limit, offset int) ([]*TenderMatch, error)
|
||||
ProcessTenderAction(ctx context.Context, matchID uuid.UUID, action MatchStatus, customerID uuid.UUID) error
|
||||
}
|
||||
|
||||
// CompanyService defines company management methods
|
||||
type CompanyService interface {
|
||||
CreateCompany(ctx context.Context, req CreateCompanyRequest) (*Company, error)
|
||||
GetCompany(ctx context.Context, id uuid.UUID) (*Company, error)
|
||||
UpdateCompany(ctx context.Context, id uuid.UUID, req UpdateCompanyRequest, updatedBy uuid.UUID) (*Company, error)
|
||||
UpdateInterests(ctx context.Context, companyID uuid.UUID, interests *InterestProfile) error
|
||||
UploadDocument(ctx context.Context, companyID uuid.UUID, req UploadDocumentRequest) (*Document, error)
|
||||
GetDocuments(ctx context.Context, companyID uuid.UUID) ([]*Document, error)
|
||||
}
|
||||
|
||||
// CustomerService defines customer management methods (for panel users)
|
||||
type CustomerService interface {
|
||||
GetCustomers(ctx context.Context, limit, offset int) ([]*Customer, error)
|
||||
GetCustomerByID(ctx context.Context, id uuid.UUID) (*Customer, error)
|
||||
GetCustomersByCompany(ctx context.Context, companyID uuid.UUID, limit, offset int) ([]*Customer, error)
|
||||
UpdateCustomerStatus(ctx context.Context, customerID uuid.UUID, isActive bool, updatedBy uuid.UUID) error
|
||||
UpdateCustomerPreferences(ctx context.Context, customerID uuid.UUID, preferences *CustomerPreferences) error
|
||||
}
|
||||
|
||||
// ScrapingService defines tender scraping methods
|
||||
type ScrapingService interface {
|
||||
ScrapeTenderSource(ctx context.Context, sourceID uuid.UUID) error
|
||||
ProcessTenderData(ctx context.Context, rawData []byte, sourceID uuid.UUID) ([]*Tender, error)
|
||||
DownloadTenderDocuments(ctx context.Context, tenderID uuid.UUID) error
|
||||
ScheduleScrapingJobs(ctx context.Context) error
|
||||
}
|
||||
|
||||
// AIService defines AI and machine learning methods
|
||||
type AIService interface {
|
||||
GenerateEmbeddings(ctx context.Context, text string) ([]float32, error)
|
||||
MatchTenders(ctx context.Context, companyID uuid.UUID) ([]*TenderMatch, error)
|
||||
ExtractTenderRequirements(ctx context.Context, tenderID uuid.UUID) ([]*Requirement, error)
|
||||
TranslateDocument(ctx context.Context, text, sourceLanguage, targetLanguage string) (string, error)
|
||||
ClassifyTender(ctx context.Context, tender *Tender) ([]string, error)
|
||||
ExtractKeywords(ctx context.Context, text string) ([]string, error)
|
||||
}
|
||||
|
||||
// NotificationService defines notification methods
|
||||
type NotificationService interface {
|
||||
SendNotification(ctx context.Context, notification *Notification) error
|
||||
SendBulkNotifications(ctx context.Context, notifications []*Notification) error
|
||||
GetUserNotifications(ctx context.Context, userID uuid.UUID, limit, offset int) ([]*Notification, error)
|
||||
MarkNotificationAsRead(ctx context.Context, id uuid.UUID) error
|
||||
CreateTenderNotification(ctx context.Context, tender *Tender, companyIDs []uuid.UUID) error
|
||||
}
|
||||
|
||||
// Cache interface defines caching methods
|
||||
type CacheService interface {
|
||||
Set(ctx context.Context, key string, value interface{}, ttl int) error
|
||||
Get(ctx context.Context, key string, dest interface{}) error
|
||||
Delete(ctx context.Context, key string) error
|
||||
Exists(ctx context.Context, key string) (bool, error)
|
||||
SetHash(ctx context.Context, key, field string, value interface{}) error
|
||||
GetHash(ctx context.Context, key, field string, dest interface{}) error
|
||||
}
|
||||
|
||||
// Queue interface defines message queue methods
|
||||
type QueueService interface {
|
||||
Publish(ctx context.Context, queue string, message interface{}) error
|
||||
Subscribe(ctx context.Context, queue string, handler func([]byte) error) error
|
||||
PublishDelayed(ctx context.Context, queue string, message interface{}, delay int) error
|
||||
}
|
||||
|
||||
// Search interface defines search engine methods
|
||||
type SearchService interface {
|
||||
IndexTender(ctx context.Context, tender *Tender) error
|
||||
SearchTenders(ctx context.Context, query string, filters map[string]interface{}, limit, offset int) ([]*Tender, error)
|
||||
DeleteTender(ctx context.Context, tenderID uuid.UUID) error
|
||||
BulkIndexTenders(ctx context.Context, tenders []*Tender) error
|
||||
}
|
||||
|
||||
// DTOs and Request/Response types
|
||||
|
||||
// TenderFilters defines filters for tender queries
|
||||
type TenderFilters struct {
|
||||
Type *TenderType `json:"type,omitempty"`
|
||||
Status *TenderStatus `json:"status,omitempty"`
|
||||
CPVCodes []string `json:"cpv_codes,omitempty"`
|
||||
Categories []string `json:"categories,omitempty"`
|
||||
Region *string `json:"region,omitempty"`
|
||||
Country *string `json:"country,omitempty"`
|
||||
MinBudget *float64 `json:"min_budget,omitempty"`
|
||||
MaxBudget *float64 `json:"max_budget,omitempty"`
|
||||
Keywords []string `json:"keywords,omitempty"`
|
||||
DateFrom *string `json:"date_from,omitempty"`
|
||||
DateTo *string `json:"date_to,omitempty"`
|
||||
}
|
||||
|
||||
// Customer Authentication DTOs
|
||||
type CustomerRegisterRequest struct {
|
||||
Email string `json:"email" validate:"required,email"`
|
||||
Password string `json:"password" validate:"required,min=8"`
|
||||
FirstName string `json:"first_name" validate:"required"`
|
||||
LastName string `json:"last_name" validate:"required"`
|
||||
Phone string `json:"phone"`
|
||||
CompanyID string `json:"company_id,omitempty"` // Optional, will create new company if not provided
|
||||
}
|
||||
|
||||
type CustomerAuthResponse struct {
|
||||
Customer *Customer `json:"customer"`
|
||||
AccessToken string `json:"access_token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
ExpiresIn int64 `json:"expires_in"`
|
||||
}
|
||||
|
||||
// Panel User Authentication DTOs
|
||||
type CreateUserRequest struct {
|
||||
Email string `json:"email" validate:"required,email"`
|
||||
Password string `json:"password" validate:"required,min=8"`
|
||||
FirstName string `json:"first_name" validate:"required"`
|
||||
LastName string `json:"last_name" validate:"required"`
|
||||
Role UserRole `json:"role" validate:"required"`
|
||||
Department string `json:"department"`
|
||||
Permissions []string `json:"permissions"`
|
||||
}
|
||||
|
||||
type UserAuthResponse struct {
|
||||
User *User `json:"user"`
|
||||
AccessToken string `json:"access_token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
ExpiresIn int64 `json:"expires_in"`
|
||||
}
|
||||
|
||||
// Shared Authentication DTOs
|
||||
type LoginRequest struct {
|
||||
Email string `json:"email" validate:"required,email"`
|
||||
Password string `json:"password" validate:"required"`
|
||||
}
|
||||
|
||||
// RegisterRequest defines user registration request (kept for backward compatibility)
|
||||
type RegisterRequest struct {
|
||||
Email string `json:"email" validate:"required,email"`
|
||||
Password string `json:"password" validate:"required,min=8"`
|
||||
FirstName string `json:"first_name" validate:"required"`
|
||||
LastName string `json:"last_name" validate:"required"`
|
||||
CompanyID string `json:"company_id,omitempty"`
|
||||
}
|
||||
|
||||
// AuthResponse defines authentication response (kept for backward compatibility)
|
||||
type AuthResponse struct {
|
||||
User *User `json:"user,omitempty"`
|
||||
Customer *Customer `json:"customer,omitempty"`
|
||||
AccessToken string `json:"access_token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
ExpiresIn int64 `json:"expires_in"`
|
||||
}
|
||||
|
||||
// CreateCompanyRequest defines company creation request
|
||||
type CreateCompanyRequest struct {
|
||||
Name string `json:"name" validate:"required"`
|
||||
Description string `json:"description"`
|
||||
Industry string `json:"industry" validate:"required"`
|
||||
Country string `json:"country" validate:"required"`
|
||||
Website string `json:"website"`
|
||||
Keywords []string `json:"keywords"`
|
||||
Products []string `json:"products"`
|
||||
Services []string `json:"services"`
|
||||
}
|
||||
|
||||
// UpdateCompanyRequest defines company update request
|
||||
type UpdateCompanyRequest struct {
|
||||
Name *string `json:"name,omitempty"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
Industry *string `json:"industry,omitempty"`
|
||||
Country *string `json:"country,omitempty"`
|
||||
Website *string `json:"website,omitempty"`
|
||||
Keywords []string `json:"keywords,omitempty"`
|
||||
Products []string `json:"products,omitempty"`
|
||||
Services []string `json:"services,omitempty"`
|
||||
Capabilities []string `json:"capabilities,omitempty"`
|
||||
}
|
||||
|
||||
// UploadDocumentRequest defines document upload request
|
||||
type UploadDocumentRequest struct {
|
||||
Name string `json:"name" validate:"required"`
|
||||
Type DocumentType `json:"type" validate:"required"`
|
||||
Category string `json:"category" validate:"required"`
|
||||
Description string `json:"description"`
|
||||
FilePath string `json:"file_path" validate:"required"`
|
||||
Size int64 `json:"size"`
|
||||
ContentType string `json:"content_type"`
|
||||
ExpiryDate *string `json:"expiry_date,omitempty"`
|
||||
Keywords []string `json:"keywords"`
|
||||
}
|
||||
@@ -1,358 +0,0 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/labstack/echo/v4"
|
||||
|
||||
"tm/internal/domain"
|
||||
"tm/pkg/logger"
|
||||
"tm/pkg/response"
|
||||
)
|
||||
|
||||
// AuthHandler handles authentication related HTTP requests (legacy/generic)
|
||||
// This handler provides backward compatibility and can handle both customer and user authentication
|
||||
type AuthHandler struct {
|
||||
customerAuthService domain.CustomerAuthService
|
||||
userAuthService domain.UserAuthService
|
||||
validator *validator.Validate
|
||||
logger logger.Logger
|
||||
}
|
||||
|
||||
// NewAuthHandler creates a new authentication handler
|
||||
func NewAuthHandler(
|
||||
customerAuthService domain.CustomerAuthService,
|
||||
userAuthService domain.UserAuthService,
|
||||
validator *validator.Validate,
|
||||
logger logger.Logger,
|
||||
) *AuthHandler {
|
||||
return &AuthHandler{
|
||||
customerAuthService: customerAuthService,
|
||||
userAuthService: userAuthService,
|
||||
validator: validator,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// Register handles user registration (defaults to customer registration)
|
||||
// @Summary Register new user
|
||||
// @Description Create a new user account (defaults to customer)
|
||||
// @Tags auth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body domain.RegisterRequest true "Registration request"
|
||||
// @Success 201 {object} response.APIResponse{data=domain.AuthResponse}
|
||||
// @Failure 400 {object} response.APIResponse
|
||||
// @Failure 409 {object} response.APIResponse
|
||||
// @Failure 500 {object} response.APIResponse
|
||||
// @Router /auth/register [post]
|
||||
func (h *AuthHandler) Register(c echo.Context) error {
|
||||
var req domain.RegisterRequest
|
||||
|
||||
// Bind request body
|
||||
if err := c.Bind(&req); err != nil {
|
||||
h.logger.Warn("Failed to bind register request", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
return response.BadRequest(c, "Invalid request format", err.Error())
|
||||
}
|
||||
|
||||
// Validate request
|
||||
if err := h.validator.Struct(&req); err != nil {
|
||||
h.logger.Warn("Register request validation failed", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"email": req.Email,
|
||||
})
|
||||
return response.ValidationError(c, "Validation failed", err.Error())
|
||||
}
|
||||
|
||||
// Convert to customer registration request
|
||||
customerReq := domain.CustomerRegisterRequest{
|
||||
Email: req.Email,
|
||||
Password: req.Password,
|
||||
FirstName: req.FirstName,
|
||||
LastName: req.LastName,
|
||||
CompanyID: req.CompanyID,
|
||||
}
|
||||
|
||||
// Call customer service (default registration is for customers)
|
||||
customerAuthResponse, err := h.customerAuthService.Register(c.Request().Context(), customerReq)
|
||||
if err != nil {
|
||||
h.logger.Error("Registration failed", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"email": req.Email,
|
||||
})
|
||||
|
||||
if err.Error() == "customer already exists" {
|
||||
return response.Conflict(c, "User already exists")
|
||||
}
|
||||
|
||||
return response.InternalServerError(c, "Registration failed")
|
||||
}
|
||||
|
||||
// Convert to generic auth response for backward compatibility
|
||||
authResponse := &domain.AuthResponse{
|
||||
Customer: customerAuthResponse.Customer,
|
||||
AccessToken: customerAuthResponse.AccessToken,
|
||||
RefreshToken: customerAuthResponse.RefreshToken,
|
||||
ExpiresIn: customerAuthResponse.ExpiresIn,
|
||||
}
|
||||
|
||||
return response.Created(c, authResponse, "User registered successfully")
|
||||
}
|
||||
|
||||
// Login handles user authentication (tries both customer and panel user)
|
||||
// @Summary User login
|
||||
// @Description Authenticate user and return tokens
|
||||
// @Tags auth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body domain.LoginRequest true "Login request"
|
||||
// @Success 200 {object} response.APIResponse{data=domain.AuthResponse}
|
||||
// @Failure 400 {object} response.APIResponse
|
||||
// @Failure 401 {object} response.APIResponse
|
||||
// @Failure 500 {object} response.APIResponse
|
||||
// @Router /auth/login [post]
|
||||
func (h *AuthHandler) Login(c echo.Context) error {
|
||||
var req domain.LoginRequest
|
||||
|
||||
// Bind request body
|
||||
if err := c.Bind(&req); err != nil {
|
||||
h.logger.Warn("Failed to bind login request", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
return response.BadRequest(c, "Invalid request format", err.Error())
|
||||
}
|
||||
|
||||
// Validate request
|
||||
if err := h.validator.Struct(&req); err != nil {
|
||||
h.logger.Warn("Login request validation failed", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"email": req.Email,
|
||||
})
|
||||
return response.ValidationError(c, "Validation failed", err.Error())
|
||||
}
|
||||
|
||||
// Try customer login first
|
||||
if customerAuthResponse, err := h.customerAuthService.Login(c.Request().Context(), req); err == nil {
|
||||
h.logger.Info("Customer logged in successfully", map[string]interface{}{
|
||||
"customer_id": customerAuthResponse.Customer.ID.String(),
|
||||
"email": customerAuthResponse.Customer.Email,
|
||||
})
|
||||
|
||||
// Convert to generic auth response
|
||||
authResponse := &domain.AuthResponse{
|
||||
Customer: customerAuthResponse.Customer,
|
||||
AccessToken: customerAuthResponse.AccessToken,
|
||||
RefreshToken: customerAuthResponse.RefreshToken,
|
||||
ExpiresIn: customerAuthResponse.ExpiresIn,
|
||||
}
|
||||
|
||||
return response.Success(c, authResponse, "Login successful")
|
||||
}
|
||||
|
||||
// Try panel user login
|
||||
if userAuthResponse, err := h.userAuthService.Login(c.Request().Context(), req); err == nil {
|
||||
h.logger.Info("Panel user logged in successfully", map[string]interface{}{
|
||||
"user_id": userAuthResponse.User.ID.String(),
|
||||
"email": userAuthResponse.User.Email,
|
||||
"role": userAuthResponse.User.Role,
|
||||
})
|
||||
|
||||
// Convert to generic auth response
|
||||
authResponse := &domain.AuthResponse{
|
||||
User: userAuthResponse.User,
|
||||
AccessToken: userAuthResponse.AccessToken,
|
||||
RefreshToken: userAuthResponse.RefreshToken,
|
||||
ExpiresIn: userAuthResponse.ExpiresIn,
|
||||
}
|
||||
|
||||
return response.Success(c, authResponse, "Login successful")
|
||||
}
|
||||
|
||||
h.logger.Warn("Login failed for both customer and user", map[string]interface{}{
|
||||
"email": req.Email,
|
||||
})
|
||||
|
||||
return response.Unauthorized(c, "Invalid email or password")
|
||||
}
|
||||
|
||||
// RefreshToken handles token refresh (tries both customer and panel user)
|
||||
// @Summary Refresh access token
|
||||
// @Description Generate new access token using refresh token
|
||||
// @Tags auth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body RefreshTokenRequest true "Refresh token request"
|
||||
// @Success 200 {object} response.APIResponse{data=domain.AuthResponse}
|
||||
// @Failure 400 {object} response.APIResponse
|
||||
// @Failure 401 {object} response.APIResponse
|
||||
// @Failure 500 {object} response.APIResponse
|
||||
// @Router /auth/refresh [post]
|
||||
func (h *AuthHandler) RefreshToken(c echo.Context) error {
|
||||
var req struct {
|
||||
RefreshToken string `json:"refresh_token" validate:"required"`
|
||||
}
|
||||
|
||||
// Bind request body
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return response.BadRequest(c, "Invalid request format", err.Error())
|
||||
}
|
||||
|
||||
// Validate request
|
||||
if err := h.validator.Struct(&req); err != nil {
|
||||
return response.ValidationError(c, "Validation failed", err.Error())
|
||||
}
|
||||
|
||||
// Try customer token refresh first
|
||||
if customerAuthResponse, err := h.customerAuthService.RefreshToken(c.Request().Context(), req.RefreshToken); err == nil {
|
||||
// Convert to generic auth response
|
||||
authResponse := &domain.AuthResponse{
|
||||
Customer: customerAuthResponse.Customer,
|
||||
AccessToken: customerAuthResponse.AccessToken,
|
||||
RefreshToken: customerAuthResponse.RefreshToken,
|
||||
ExpiresIn: customerAuthResponse.ExpiresIn,
|
||||
}
|
||||
|
||||
return response.Success(c, authResponse, "Token refreshed successfully")
|
||||
}
|
||||
|
||||
// Try panel user token refresh
|
||||
if userAuthResponse, err := h.userAuthService.RefreshToken(c.Request().Context(), req.RefreshToken); err == nil {
|
||||
// Convert to generic auth response
|
||||
authResponse := &domain.AuthResponse{
|
||||
User: userAuthResponse.User,
|
||||
AccessToken: userAuthResponse.AccessToken,
|
||||
RefreshToken: userAuthResponse.RefreshToken,
|
||||
ExpiresIn: userAuthResponse.ExpiresIn,
|
||||
}
|
||||
|
||||
return response.Success(c, authResponse, "Token refreshed successfully")
|
||||
}
|
||||
|
||||
h.logger.Warn("Token refresh failed for both customer and user", map[string]interface{}{})
|
||||
|
||||
return response.Unauthorized(c, "Invalid refresh token")
|
||||
}
|
||||
|
||||
// ChangePassword handles password change (determines user type from context)
|
||||
// @Summary Change user password
|
||||
// @Description Change authenticated user's password
|
||||
// @Tags 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 /auth/change-password [post]
|
||||
func (h *AuthHandler) ChangePassword(c echo.Context) error {
|
||||
var req struct {
|
||||
OldPassword string `json:"old_password" validate:"required"`
|
||||
NewPassword string `json:"new_password" validate:"required,min=8"`
|
||||
}
|
||||
|
||||
// Bind request body
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return response.BadRequest(c, "Invalid request format", err.Error())
|
||||
}
|
||||
|
||||
// Validate request
|
||||
if err := h.validator.Struct(&req); err != nil {
|
||||
return response.ValidationError(c, "Validation failed", err.Error())
|
||||
}
|
||||
|
||||
// Check if it's a customer
|
||||
if customer, ok := c.Get("customer").(*domain.Customer); ok {
|
||||
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")
|
||||
}
|
||||
|
||||
// Check if it's a panel user
|
||||
if user, ok := c.Get("user").(*domain.User); ok {
|
||||
err := h.userAuthService.ChangePassword(c.Request().Context(), user.ID, req.OldPassword, req.NewPassword)
|
||||
if err != nil {
|
||||
h.logger.Error("Panel user password change failed", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"user_id": user.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")
|
||||
}
|
||||
|
||||
return response.Unauthorized(c, "Authentication required")
|
||||
}
|
||||
|
||||
// GetProfile returns current user profile (determines user type from context)
|
||||
// @Summary Get user profile
|
||||
// @Description Get authenticated user's profile information
|
||||
// @Tags auth
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Success 200 {object} response.APIResponse{data=domain.User}
|
||||
// @Failure 401 {object} response.APIResponse
|
||||
// @Router /auth/profile [get]
|
||||
func (h *AuthHandler) GetProfile(c echo.Context) error {
|
||||
// Check if it's a customer
|
||||
if customer, ok := c.Get("customer").(*domain.Customer); ok {
|
||||
return response.Success(c, customer, "Profile retrieved successfully")
|
||||
}
|
||||
|
||||
// Check if it's a panel user
|
||||
if user, ok := c.Get("user").(*domain.User); ok {
|
||||
return response.Success(c, user, "Profile retrieved successfully")
|
||||
}
|
||||
|
||||
return response.Unauthorized(c, "Authentication required")
|
||||
}
|
||||
|
||||
// Logout handles user logout (determines user type from context)
|
||||
// @Summary User logout
|
||||
// @Description Logout user (client should remove tokens)
|
||||
// @Tags auth
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Success 200 {object} response.APIResponse
|
||||
// @Router /auth/logout [post]
|
||||
func (h *AuthHandler) Logout(c echo.Context) error {
|
||||
// Check if it's a customer
|
||||
if customer, ok := c.Get("customer").(*domain.Customer); 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")
|
||||
}
|
||||
|
||||
// Check if it's a panel user
|
||||
if user, ok := c.Get("user").(*domain.User); ok {
|
||||
h.logger.Info("Panel user logged out", map[string]interface{}{
|
||||
"user_id": user.ID.String(),
|
||||
"email": user.Email,
|
||||
})
|
||||
return response.Success(c, nil, "Logged out successfully")
|
||||
}
|
||||
|
||||
return response.Success(c, nil, "Logged out successfully")
|
||||
}
|
||||
@@ -1,378 +0,0 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/labstack/echo/v4"
|
||||
|
||||
"tm/internal/domain"
|
||||
"tm/pkg/logger"
|
||||
"tm/pkg/response"
|
||||
)
|
||||
|
||||
// CustomerAuthHandler handles authentication related HTTP requests for mobile customers
|
||||
type CustomerAuthHandler struct {
|
||||
customerAuthService domain.CustomerAuthService
|
||||
validator *validator.Validate
|
||||
logger logger.Logger
|
||||
}
|
||||
|
||||
// NewCustomerAuthHandler creates a new customer authentication handler
|
||||
func NewCustomerAuthHandler(
|
||||
customerAuthService domain.CustomerAuthService,
|
||||
validator *validator.Validate,
|
||||
logger logger.Logger,
|
||||
) *CustomerAuthHandler {
|
||||
return &CustomerAuthHandler{
|
||||
customerAuthService: customerAuthService,
|
||||
validator: validator,
|
||||
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 *CustomerAuthHandler) Register(c echo.Context) error {
|
||||
var req domain.CustomerRegisterRequest
|
||||
|
||||
// Bind request body
|
||||
if err := c.Bind(&req); err != nil {
|
||||
h.logger.Warn("Failed to bind customer register request", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
return response.BadRequest(c, "Invalid request format", err.Error())
|
||||
}
|
||||
|
||||
// Validate request
|
||||
if err := h.validator.Struct(&req); err != nil {
|
||||
h.logger.Warn("Customer register request validation failed", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"email": req.Email,
|
||||
})
|
||||
return response.ValidationError(c, "Validation failed", err.Error())
|
||||
}
|
||||
|
||||
// Call service
|
||||
authResponse, err := h.customerAuthService.Register(c.Request().Context(), req)
|
||||
if err != nil {
|
||||
h.logger.Error("Customer registration failed", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"email": req.Email,
|
||||
})
|
||||
|
||||
if err.Error() == "customer already exists" {
|
||||
return response.Conflict(c, "Customer already exists")
|
||||
}
|
||||
|
||||
return response.InternalServerError(c, "Registration failed")
|
||||
}
|
||||
|
||||
h.logger.Info("Customer registered successfully", map[string]interface{}{
|
||||
"customer_id": authResponse.Customer.ID.String(),
|
||||
"email": authResponse.Customer.Email,
|
||||
})
|
||||
|
||||
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 *CustomerAuthHandler) Login(c echo.Context) error {
|
||||
var req domain.LoginRequest
|
||||
|
||||
// Bind request body
|
||||
if err := c.Bind(&req); err != nil {
|
||||
h.logger.Warn("Failed to bind customer login request", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
return response.BadRequest(c, "Invalid request format", err.Error())
|
||||
}
|
||||
|
||||
// Validate request
|
||||
if err := h.validator.Struct(&req); err != nil {
|
||||
h.logger.Warn("Customer login request validation failed", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"email": req.Email,
|
||||
})
|
||||
return response.ValidationError(c, "Validation failed", err.Error())
|
||||
}
|
||||
|
||||
// Call service
|
||||
authResponse, err := h.customerAuthService.Login(c.Request().Context(), req)
|
||||
if err != nil {
|
||||
h.logger.Warn("Customer login failed", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"email": req.Email,
|
||||
})
|
||||
|
||||
if err.Error() == "invalid credentials" {
|
||||
return response.Unauthorized(c, "Invalid email or password")
|
||||
}
|
||||
|
||||
return response.InternalServerError(c, "Login failed")
|
||||
}
|
||||
|
||||
h.logger.Info("Customer logged in successfully", map[string]interface{}{
|
||||
"customer_id": authResponse.Customer.ID.String(),
|
||||
"email": authResponse.Customer.Email,
|
||||
})
|
||||
|
||||
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 *CustomerAuthHandler) RefreshToken(c echo.Context) error {
|
||||
var req struct {
|
||||
RefreshToken string `json:"refresh_token" validate:"required"`
|
||||
}
|
||||
|
||||
// Bind request body
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return response.BadRequest(c, "Invalid request format", err.Error())
|
||||
}
|
||||
|
||||
// Validate request
|
||||
if err := h.validator.Struct(&req); err != nil {
|
||||
return response.ValidationError(c, "Validation failed", err.Error())
|
||||
}
|
||||
|
||||
// Call service
|
||||
authResponse, err := h.customerAuthService.RefreshToken(c.Request().Context(), req.RefreshToken)
|
||||
if err != nil {
|
||||
h.logger.Warn("Customer token refresh failed", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
|
||||
if err.Error() == "invalid refresh token" {
|
||||
return response.Unauthorized(c, "Invalid refresh token")
|
||||
}
|
||||
|
||||
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 *CustomerAuthHandler) ChangePassword(c echo.Context) error {
|
||||
var req struct {
|
||||
OldPassword string `json:"old_password" validate:"required"`
|
||||
NewPassword string `json:"new_password" validate:"required,min=8"`
|
||||
}
|
||||
|
||||
// Bind request body
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return response.BadRequest(c, "Invalid request format", err.Error())
|
||||
}
|
||||
|
||||
// Validate request
|
||||
if err := h.validator.Struct(&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")
|
||||
}
|
||||
|
||||
h.logger.Info("Customer password changed successfully", map[string]interface{}{
|
||||
"customer_id": customer.ID.String(),
|
||||
})
|
||||
|
||||
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 *CustomerAuthHandler) 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, customer, "Profile retrieved successfully")
|
||||
}
|
||||
|
||||
// VerifyEmail handles email verification for customers
|
||||
// @Summary Verify customer email
|
||||
// @Description Verify customer's email address using token
|
||||
// @Tags customer-auth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body VerifyEmailRequest true "Email verification request"
|
||||
// @Success 200 {object} response.APIResponse
|
||||
// @Failure 400 {object} response.APIResponse
|
||||
// @Failure 500 {object} response.APIResponse
|
||||
// @Router /api/mobile/auth/verify-email [post]
|
||||
func (h *CustomerAuthHandler) VerifyEmail(c echo.Context) error {
|
||||
var req struct {
|
||||
Token string `json:"token" validate:"required"`
|
||||
}
|
||||
|
||||
// Bind request body
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return response.BadRequest(c, "Invalid request format", err.Error())
|
||||
}
|
||||
|
||||
// Validate request
|
||||
if err := h.validator.Struct(&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.VerifyEmail(c.Request().Context(), customer.ID, req.Token)
|
||||
if err != nil {
|
||||
h.logger.Error("Customer email verification failed", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"customer_id": customer.ID.String(),
|
||||
})
|
||||
|
||||
if err.Error() == "invalid verification token" {
|
||||
return response.BadRequest(c, "Invalid verification token", "")
|
||||
}
|
||||
|
||||
return response.InternalServerError(c, "Email verification failed")
|
||||
}
|
||||
|
||||
h.logger.Info("Customer email verified successfully", map[string]interface{}{
|
||||
"customer_id": customer.ID.String(),
|
||||
})
|
||||
|
||||
return response.Success(c, nil, "Email verified successfully")
|
||||
}
|
||||
|
||||
// ResendVerification handles resending verification email
|
||||
// @Summary Resend verification email
|
||||
// @Description Resend verification email to customer
|
||||
// @Tags customer-auth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body ResendVerificationRequest true "Resend verification request"
|
||||
// @Success 200 {object} response.APIResponse
|
||||
// @Failure 400 {object} response.APIResponse
|
||||
// @Failure 500 {object} response.APIResponse
|
||||
// @Router /api/mobile/auth/resend-verification [post]
|
||||
func (h *CustomerAuthHandler) ResendVerification(c echo.Context) error {
|
||||
var req struct {
|
||||
Email string `json:"email" validate:"required,email"`
|
||||
}
|
||||
|
||||
// Bind request body
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return response.BadRequest(c, "Invalid request format", err.Error())
|
||||
}
|
||||
|
||||
// Validate request
|
||||
if err := h.validator.Struct(&req); err != nil {
|
||||
return response.ValidationError(c, "Validation failed", err.Error())
|
||||
}
|
||||
|
||||
// Call service
|
||||
err := h.customerAuthService.ResendVerification(c.Request().Context(), req.Email)
|
||||
if err != nil {
|
||||
h.logger.Error("Resend verification failed", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"email": req.Email,
|
||||
})
|
||||
|
||||
return response.InternalServerError(c, "Failed to resend verification email")
|
||||
}
|
||||
|
||||
return response.Success(c, nil, "Verification email sent 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 *CustomerAuthHandler) 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")
|
||||
}
|
||||
@@ -1,380 +0,0 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/google/uuid"
|
||||
"github.com/labstack/echo/v4"
|
||||
|
||||
"tm/internal/domain"
|
||||
"tm/pkg/logger"
|
||||
"tm/pkg/response"
|
||||
)
|
||||
|
||||
// UserAuthHandler handles authentication related HTTP requests for panel users
|
||||
type UserAuthHandler struct {
|
||||
userAuthService domain.UserAuthService
|
||||
validator *validator.Validate
|
||||
logger logger.Logger
|
||||
}
|
||||
|
||||
// NewUserAuthHandler creates a new panel user authentication handler
|
||||
func NewUserAuthHandler(
|
||||
userAuthService domain.UserAuthService,
|
||||
validator *validator.Validate,
|
||||
logger logger.Logger,
|
||||
) *UserAuthHandler {
|
||||
return &UserAuthHandler{
|
||||
userAuthService: userAuthService,
|
||||
validator: validator,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// Login handles panel user authentication
|
||||
// @Summary Panel user login
|
||||
// @Description Authenticate panel user and return tokens
|
||||
// @Tags user-auth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body domain.LoginRequest true "Login request"
|
||||
// @Success 200 {object} response.APIResponse{data=domain.UserAuthResponse}
|
||||
// @Failure 400 {object} response.APIResponse
|
||||
// @Failure 401 {object} response.APIResponse
|
||||
// @Failure 500 {object} response.APIResponse
|
||||
// @Router /api/admin/auth/login [post]
|
||||
func (h *UserAuthHandler) Login(c echo.Context) error {
|
||||
var req domain.LoginRequest
|
||||
|
||||
// Bind request body
|
||||
if err := c.Bind(&req); err != nil {
|
||||
h.logger.Warn("Failed to bind panel user login request", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
return response.BadRequest(c, "Invalid request format", err.Error())
|
||||
}
|
||||
|
||||
// Validate request
|
||||
if err := h.validator.Struct(&req); err != nil {
|
||||
h.logger.Warn("Panel user login request validation failed", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"email": req.Email,
|
||||
})
|
||||
return response.ValidationError(c, "Validation failed", err.Error())
|
||||
}
|
||||
|
||||
// Call service
|
||||
authResponse, err := h.userAuthService.Login(c.Request().Context(), req)
|
||||
if err != nil {
|
||||
h.logger.Warn("Panel user login failed", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"email": req.Email,
|
||||
})
|
||||
|
||||
if err.Error() == "invalid credentials" {
|
||||
return response.Unauthorized(c, "Invalid email or password")
|
||||
}
|
||||
|
||||
return response.InternalServerError(c, "Login failed")
|
||||
}
|
||||
|
||||
h.logger.Info("Panel user logged in successfully", map[string]interface{}{
|
||||
"user_id": authResponse.User.ID.String(),
|
||||
"email": authResponse.User.Email,
|
||||
"role": authResponse.User.Role,
|
||||
})
|
||||
|
||||
return response.Success(c, authResponse, "Login successful")
|
||||
}
|
||||
|
||||
// RefreshToken handles token refresh for panel users
|
||||
// @Summary Refresh panel user access token
|
||||
// @Description Generate new access token using refresh token
|
||||
// @Tags user-auth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body RefreshTokenRequest true "Refresh token request"
|
||||
// @Success 200 {object} response.APIResponse{data=domain.UserAuthResponse}
|
||||
// @Failure 400 {object} response.APIResponse
|
||||
// @Failure 401 {object} response.APIResponse
|
||||
// @Failure 500 {object} response.APIResponse
|
||||
// @Router /api/admin/auth/refresh [post]
|
||||
func (h *UserAuthHandler) RefreshToken(c echo.Context) error {
|
||||
var req struct {
|
||||
RefreshToken string `json:"refresh_token" validate:"required"`
|
||||
}
|
||||
|
||||
// Bind request body
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return response.BadRequest(c, "Invalid request format", err.Error())
|
||||
}
|
||||
|
||||
// Validate request
|
||||
if err := h.validator.Struct(&req); err != nil {
|
||||
return response.ValidationError(c, "Validation failed", err.Error())
|
||||
}
|
||||
|
||||
// Call service
|
||||
authResponse, err := h.userAuthService.RefreshToken(c.Request().Context(), req.RefreshToken)
|
||||
if err != nil {
|
||||
h.logger.Warn("Panel user token refresh failed", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
|
||||
if err.Error() == "invalid refresh token" {
|
||||
return response.Unauthorized(c, "Invalid refresh token")
|
||||
}
|
||||
|
||||
return response.InternalServerError(c, "Token refresh failed")
|
||||
}
|
||||
|
||||
return response.Success(c, authResponse, "Token refreshed successfully")
|
||||
}
|
||||
|
||||
// ChangePassword handles password change for panel users
|
||||
// @Summary Change panel user password
|
||||
// @Description Change authenticated panel user's password
|
||||
// @Tags user-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/admin/auth/change-password [post]
|
||||
func (h *UserAuthHandler) ChangePassword(c echo.Context) error {
|
||||
var req struct {
|
||||
OldPassword string `json:"old_password" validate:"required"`
|
||||
NewPassword string `json:"new_password" validate:"required,min=8"`
|
||||
}
|
||||
|
||||
// Bind request body
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return response.BadRequest(c, "Invalid request format", err.Error())
|
||||
}
|
||||
|
||||
// Validate request
|
||||
if err := h.validator.Struct(&req); err != nil {
|
||||
return response.ValidationError(c, "Validation failed", err.Error())
|
||||
}
|
||||
|
||||
// Get user from context (set by auth middleware)
|
||||
user, ok := c.Get("user").(*domain.User)
|
||||
if !ok {
|
||||
return response.Unauthorized(c, "Authentication required")
|
||||
}
|
||||
|
||||
// Call service
|
||||
err := h.userAuthService.ChangePassword(c.Request().Context(), user.ID, req.OldPassword, req.NewPassword)
|
||||
if err != nil {
|
||||
h.logger.Error("Panel user password change failed", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"user_id": user.ID.String(),
|
||||
})
|
||||
|
||||
if err.Error() == "invalid old password" {
|
||||
return response.BadRequest(c, "Invalid old password", "")
|
||||
}
|
||||
|
||||
return response.InternalServerError(c, "Password change failed")
|
||||
}
|
||||
|
||||
h.logger.Info("Panel user password changed successfully", map[string]interface{}{
|
||||
"user_id": user.ID.String(),
|
||||
})
|
||||
|
||||
return response.Success(c, nil, "Password changed successfully")
|
||||
}
|
||||
|
||||
// GetProfile returns current panel user profile
|
||||
// @Summary Get panel user profile
|
||||
// @Description Get authenticated panel user's profile information
|
||||
// @Tags user-auth
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Success 200 {object} response.APIResponse{data=domain.User}
|
||||
// @Failure 401 {object} response.APIResponse
|
||||
// @Router /api/admin/auth/profile [get]
|
||||
func (h *UserAuthHandler) GetProfile(c echo.Context) error {
|
||||
// Get user from context (set by auth middleware)
|
||||
user, ok := c.Get("user").(*domain.User)
|
||||
if !ok {
|
||||
return response.Unauthorized(c, "Authentication required")
|
||||
}
|
||||
|
||||
return response.Success(c, user, "Profile retrieved successfully")
|
||||
}
|
||||
|
||||
// CreatePanelUser handles creating new panel users (admin only)
|
||||
// @Summary Create new panel user
|
||||
// @Description Create a new panel user (admin only)
|
||||
// @Tags user-auth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param request body domain.CreateUserRequest true "Create user request"
|
||||
// @Success 201 {object} response.APIResponse{data=domain.User}
|
||||
// @Failure 400 {object} response.APIResponse
|
||||
// @Failure 401 {object} response.APIResponse
|
||||
// @Failure 403 {object} response.APIResponse
|
||||
// @Failure 500 {object} response.APIResponse
|
||||
// @Router /api/admin/users [post]
|
||||
func (h *UserAuthHandler) CreatePanelUser(c echo.Context) error {
|
||||
var req domain.CreateUserRequest
|
||||
|
||||
// Bind request body
|
||||
if err := c.Bind(&req); err != nil {
|
||||
h.logger.Warn("Failed to bind create user request", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
return response.BadRequest(c, "Invalid request format", err.Error())
|
||||
}
|
||||
|
||||
// Validate request
|
||||
if err := h.validator.Struct(&req); err != nil {
|
||||
h.logger.Warn("Create user request validation failed", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"email": req.Email,
|
||||
})
|
||||
return response.ValidationError(c, "Validation failed", err.Error())
|
||||
}
|
||||
|
||||
// Get current user from context (set by auth middleware)
|
||||
currentUser, ok := c.Get("user").(*domain.User)
|
||||
if !ok {
|
||||
return response.Unauthorized(c, "Authentication required")
|
||||
}
|
||||
|
||||
// Check if current user can create users
|
||||
if !currentUser.CanManageUsers() {
|
||||
return response.Forbidden(c, "Insufficient permissions to create users")
|
||||
}
|
||||
|
||||
// Call service
|
||||
newUser, err := h.userAuthService.CreatePanelUser(c.Request().Context(), req, currentUser.ID)
|
||||
if err != nil {
|
||||
h.logger.Error("Panel user creation failed", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"email": req.Email,
|
||||
"created_by": currentUser.ID.String(),
|
||||
})
|
||||
|
||||
if err.Error() == "user already exists" {
|
||||
return response.Conflict(c, "User already exists")
|
||||
}
|
||||
|
||||
return response.InternalServerError(c, "User creation failed")
|
||||
}
|
||||
|
||||
h.logger.Info("Panel user created successfully", map[string]interface{}{
|
||||
"user_id": newUser.ID.String(),
|
||||
"email": newUser.Email,
|
||||
"role": newUser.Role,
|
||||
"created_by": currentUser.ID.String(),
|
||||
})
|
||||
|
||||
return response.Created(c, newUser, "User created successfully")
|
||||
}
|
||||
|
||||
// UpdateUserPermissions handles updating user permissions (admin only)
|
||||
// @Summary Update user permissions
|
||||
// @Description Update panel user permissions (admin only)
|
||||
// @Tags user-auth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param user_id path string true "User ID"
|
||||
// @Param request body UpdatePermissionsRequest true "Update permissions request"
|
||||
// @Success 200 {object} response.APIResponse
|
||||
// @Failure 400 {object} response.APIResponse
|
||||
// @Failure 401 {object} response.APIResponse
|
||||
// @Failure 403 {object} response.APIResponse
|
||||
// @Failure 404 {object} response.APIResponse
|
||||
// @Failure 500 {object} response.APIResponse
|
||||
// @Router /api/admin/users/{user_id}/permissions [put]
|
||||
func (h *UserAuthHandler) UpdateUserPermissions(c echo.Context) error {
|
||||
var req struct {
|
||||
Permissions []string `json:"permissions" validate:"required"`
|
||||
}
|
||||
|
||||
// Bind request body
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return response.BadRequest(c, "Invalid request format", err.Error())
|
||||
}
|
||||
|
||||
// Validate request
|
||||
if err := h.validator.Struct(&req); err != nil {
|
||||
return response.ValidationError(c, "Validation failed", err.Error())
|
||||
}
|
||||
|
||||
// Get user ID from path
|
||||
userIDStr := c.Param("user_id")
|
||||
if userIDStr == "" {
|
||||
return response.BadRequest(c, "User ID is required", "")
|
||||
}
|
||||
|
||||
userID, err := uuid.Parse(userIDStr)
|
||||
if err != nil {
|
||||
return response.BadRequest(c, "Invalid user ID format", "")
|
||||
}
|
||||
|
||||
// Get current user from context (set by auth middleware)
|
||||
currentUser, ok := c.Get("user").(*domain.User)
|
||||
if !ok {
|
||||
return response.Unauthorized(c, "Authentication required")
|
||||
}
|
||||
|
||||
// Check if current user can manage users
|
||||
if !currentUser.CanManageUsers() {
|
||||
return response.Forbidden(c, "Insufficient permissions to update user permissions")
|
||||
}
|
||||
|
||||
// Call service
|
||||
err = h.userAuthService.UpdateUserPermissions(c.Request().Context(), userID, req.Permissions, currentUser.ID)
|
||||
if err != nil {
|
||||
h.logger.Error("User permissions update failed", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"user_id": userID.String(),
|
||||
"updated_by": currentUser.ID.String(),
|
||||
})
|
||||
|
||||
if err.Error() == "user not found" {
|
||||
return response.NotFound(c, "User not found")
|
||||
}
|
||||
|
||||
return response.InternalServerError(c, "Permissions update failed")
|
||||
}
|
||||
|
||||
h.logger.Info("User permissions updated successfully", map[string]interface{}{
|
||||
"user_id": userID.String(),
|
||||
"permissions": req.Permissions,
|
||||
"updated_by": currentUser.ID.String(),
|
||||
})
|
||||
|
||||
return response.Success(c, nil, "Permissions updated successfully")
|
||||
}
|
||||
|
||||
// Logout handles panel user logout
|
||||
// @Summary Panel user logout
|
||||
// @Description Logout panel user (client should remove tokens)
|
||||
// @Tags user-auth
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Success 200 {object} response.APIResponse
|
||||
// @Router /api/admin/auth/logout [post]
|
||||
func (h *UserAuthHandler) 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.
|
||||
|
||||
user, ok := c.Get("user").(*domain.User)
|
||||
if ok {
|
||||
h.logger.Info("Panel user logged out", map[string]interface{}{
|
||||
"user_id": user.ID.String(),
|
||||
"email": user.Email,
|
||||
})
|
||||
}
|
||||
|
||||
return response.Success(c, nil, "Logged out successfully")
|
||||
}
|
||||
@@ -1,219 +0,0 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
|
||||
"tm/internal/domain"
|
||||
"tm/pkg/logger"
|
||||
)
|
||||
|
||||
// CompanyRepositoryImpl implements the CompanyRepository interface
|
||||
type CompanyRepositoryImpl struct {
|
||||
collection *mongo.Collection
|
||||
logger logger.Logger
|
||||
}
|
||||
|
||||
// NewCompanyRepository creates a new company repository
|
||||
func NewCompanyRepository(db *mongo.Database, logger logger.Logger) domain.CompanyRepository {
|
||||
collection := db.Collection("companies")
|
||||
|
||||
// Create indexes
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Name index
|
||||
nameIndex := mongo.IndexModel{
|
||||
Keys: bson.D{{"name", 1}},
|
||||
}
|
||||
|
||||
// Created at index
|
||||
createdAtIndex := mongo.IndexModel{
|
||||
Keys: bson.D{{"created_at", -1}},
|
||||
}
|
||||
|
||||
_, err := collection.Indexes().CreateMany(ctx, []mongo.IndexModel{
|
||||
nameIndex,
|
||||
createdAtIndex,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
logger.Warn("Failed to create company indexes", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
return &CompanyRepositoryImpl{
|
||||
collection: collection,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// Create creates a new company
|
||||
func (r *CompanyRepositoryImpl) Create(ctx context.Context, company *domain.Company) error {
|
||||
// Set created/updated timestamps
|
||||
now := time.Now()
|
||||
company.CreatedAt = now
|
||||
company.UpdatedAt = now
|
||||
|
||||
// Insert company
|
||||
_, err := r.collection.InsertOne(ctx, company)
|
||||
if err != nil {
|
||||
if mongo.IsDuplicateKeyError(err) {
|
||||
return errors.New("company already exists")
|
||||
}
|
||||
r.logger.Error("Failed to create company", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"company_id": company.ID.String(),
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
r.logger.Info("Company created successfully", map[string]interface{}{
|
||||
"company_id": company.ID.String(),
|
||||
"name": company.Name,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetByID retrieves a company by ID
|
||||
func (r *CompanyRepositoryImpl) GetByID(ctx context.Context, id uuid.UUID) (*domain.Company, error) {
|
||||
var company domain.Company
|
||||
|
||||
filter := bson.M{"_id": id}
|
||||
err := r.collection.FindOne(ctx, filter).Decode(&company)
|
||||
|
||||
if err != nil {
|
||||
if err == mongo.ErrNoDocuments {
|
||||
return nil, errors.New("company not found")
|
||||
}
|
||||
r.logger.Error("Failed to get company by ID", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"company_id": id.String(),
|
||||
})
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &company, nil
|
||||
}
|
||||
|
||||
// Update updates a company
|
||||
func (r *CompanyRepositoryImpl) Update(ctx context.Context, company *domain.Company) error {
|
||||
// Set updated timestamp
|
||||
company.UpdatedAt = time.Now()
|
||||
|
||||
filter := bson.M{"_id": company.ID}
|
||||
update := bson.M{"$set": company}
|
||||
|
||||
result, err := r.collection.UpdateOne(ctx, filter, update)
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to update company", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"company_id": company.ID.String(),
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
if result.MatchedCount == 0 {
|
||||
return errors.New("company not found")
|
||||
}
|
||||
|
||||
r.logger.Info("Company updated successfully", map[string]interface{}{
|
||||
"company_id": company.ID.String(),
|
||||
"name": company.Name,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete deletes a company
|
||||
func (r *CompanyRepositoryImpl) Delete(ctx context.Context, id uuid.UUID) error {
|
||||
filter := bson.M{"_id": id}
|
||||
|
||||
result, err := r.collection.DeleteOne(ctx, filter)
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to delete company", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"company_id": id.String(),
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
if result.DeletedCount == 0 {
|
||||
return errors.New("company not found")
|
||||
}
|
||||
|
||||
r.logger.Info("Company deleted successfully", map[string]interface{}{
|
||||
"company_id": id.String(),
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// List retrieves companies with pagination
|
||||
func (r *CompanyRepositoryImpl) List(ctx context.Context, limit, offset int) ([]*domain.Company, error) {
|
||||
var companies []*domain.Company
|
||||
|
||||
// Build options
|
||||
opts := options.Find()
|
||||
opts.SetLimit(int64(limit))
|
||||
opts.SetSkip(int64(offset))
|
||||
opts.SetSort(bson.D{{"created_at", -1}}) // Sort by created_at desc
|
||||
|
||||
cursor, err := r.collection.Find(ctx, bson.M{}, opts)
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to list companies", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"limit": limit,
|
||||
"offset": offset,
|
||||
})
|
||||
return nil, err
|
||||
}
|
||||
defer cursor.Close(ctx)
|
||||
|
||||
if err = cursor.All(ctx, &companies); err != nil {
|
||||
r.logger.Error("Failed to decode companies", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return companies, nil
|
||||
}
|
||||
|
||||
// UpdateInterests updates company interests
|
||||
func (r *CompanyRepositoryImpl) UpdateInterests(ctx context.Context, id uuid.UUID, interests *domain.InterestProfile) error {
|
||||
filter := bson.M{"_id": id}
|
||||
update := bson.M{
|
||||
"$set": bson.M{
|
||||
"interests": interests,
|
||||
"updated_at": time.Now(),
|
||||
},
|
||||
}
|
||||
|
||||
result, err := r.collection.UpdateOne(ctx, filter, update)
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to update company interests", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"company_id": id.String(),
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
if result.MatchedCount == 0 {
|
||||
return errors.New("company not found")
|
||||
}
|
||||
|
||||
r.logger.Info("Company interests updated successfully", map[string]interface{}{
|
||||
"company_id": id.String(),
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,311 +0,0 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
|
||||
"tm/internal/domain"
|
||||
"tm/pkg/logger"
|
||||
)
|
||||
|
||||
// UserRepositoryImpl implements the UserRepository interface
|
||||
type UserRepositoryImpl struct {
|
||||
collection *mongo.Collection
|
||||
logger logger.Logger
|
||||
}
|
||||
|
||||
// NewUserRepository creates a new panel user repository
|
||||
func NewUserRepository(db *mongo.Database, logger logger.Logger) domain.UserRepository {
|
||||
collection := db.Collection("users")
|
||||
|
||||
// Create indexes
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Email index (unique)
|
||||
emailIndex := mongo.IndexModel{
|
||||
Keys: bson.D{{"email", 1}},
|
||||
Options: options.Index().SetUnique(true),
|
||||
}
|
||||
|
||||
// Role index
|
||||
roleIndex := mongo.IndexModel{
|
||||
Keys: bson.D{{"role", 1}},
|
||||
}
|
||||
|
||||
// Active status index
|
||||
activeIndex := mongo.IndexModel{
|
||||
Keys: bson.D{{"is_active", 1}},
|
||||
}
|
||||
|
||||
// Department index
|
||||
departmentIndex := mongo.IndexModel{
|
||||
Keys: bson.D{{"department", 1}},
|
||||
}
|
||||
|
||||
// Created at index
|
||||
createdAtIndex := mongo.IndexModel{
|
||||
Keys: bson.D{{"created_at", -1}},
|
||||
}
|
||||
|
||||
_, err := collection.Indexes().CreateMany(ctx, []mongo.IndexModel{
|
||||
emailIndex,
|
||||
roleIndex,
|
||||
activeIndex,
|
||||
departmentIndex,
|
||||
createdAtIndex,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
logger.Warn("Failed to create user indexes", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
return &UserRepositoryImpl{
|
||||
collection: collection,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// Create creates a new panel user
|
||||
func (r *UserRepositoryImpl) Create(ctx context.Context, user *domain.User) error {
|
||||
// Set created/updated timestamps
|
||||
now := time.Now()
|
||||
user.CreatedAt = now
|
||||
user.UpdatedAt = now
|
||||
|
||||
// Insert user
|
||||
_, err := r.collection.InsertOne(ctx, user)
|
||||
if err != nil {
|
||||
if mongo.IsDuplicateKeyError(err) {
|
||||
return errors.New("user already exists")
|
||||
}
|
||||
r.logger.Error("Failed to create user", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"email": user.Email,
|
||||
"user_id": user.ID.String(),
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
r.logger.Info("Panel user created successfully", map[string]interface{}{
|
||||
"user_id": user.ID.String(),
|
||||
"email": user.Email,
|
||||
"role": user.Role,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetByID retrieves a panel user by ID
|
||||
func (r *UserRepositoryImpl) GetByID(ctx context.Context, id uuid.UUID) (*domain.User, error) {
|
||||
var user domain.User
|
||||
|
||||
filter := bson.M{"_id": id}
|
||||
err := r.collection.FindOne(ctx, filter).Decode(&user)
|
||||
|
||||
if err != nil {
|
||||
if err == mongo.ErrNoDocuments {
|
||||
return nil, errors.New("user not found")
|
||||
}
|
||||
r.logger.Error("Failed to get user by ID", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"user_id": id.String(),
|
||||
})
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// GetByEmail retrieves a panel user by email
|
||||
func (r *UserRepositoryImpl) GetByEmail(ctx context.Context, email string) (*domain.User, error) {
|
||||
var user domain.User
|
||||
|
||||
filter := bson.M{"email": email}
|
||||
err := r.collection.FindOne(ctx, filter).Decode(&user)
|
||||
|
||||
if err != nil {
|
||||
if err == mongo.ErrNoDocuments {
|
||||
return nil, errors.New("user not found")
|
||||
}
|
||||
r.logger.Error("Failed to get user by email", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"email": email,
|
||||
})
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// Update updates a panel user
|
||||
func (r *UserRepositoryImpl) Update(ctx context.Context, user *domain.User) error {
|
||||
// Set updated timestamp
|
||||
user.UpdatedAt = time.Now()
|
||||
|
||||
filter := bson.M{"_id": user.ID}
|
||||
update := bson.M{"$set": user}
|
||||
|
||||
result, err := r.collection.UpdateOne(ctx, filter, update)
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to update user", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"user_id": user.ID.String(),
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
if result.MatchedCount == 0 {
|
||||
return errors.New("user not found")
|
||||
}
|
||||
|
||||
r.logger.Info("Panel user updated successfully", map[string]interface{}{
|
||||
"user_id": user.ID.String(),
|
||||
"email": user.Email,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete deletes a panel user (soft delete by setting is_active to false)
|
||||
func (r *UserRepositoryImpl) 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 user", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"user_id": id.String(),
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
if result.MatchedCount == 0 {
|
||||
return errors.New("user not found")
|
||||
}
|
||||
|
||||
r.logger.Info("Panel user deleted successfully", map[string]interface{}{
|
||||
"user_id": id.String(),
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// List retrieves panel users with pagination
|
||||
func (r *UserRepositoryImpl) List(ctx context.Context, limit, offset int) ([]*domain.User, error) {
|
||||
var users []*domain.User
|
||||
|
||||
// Build options
|
||||
opts := options.Find()
|
||||
opts.SetLimit(int64(limit))
|
||||
opts.SetSkip(int64(offset))
|
||||
opts.SetSort(bson.D{{"created_at", -1}}) // Sort by created_at desc
|
||||
|
||||
// Only active users 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 users", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"limit": limit,
|
||||
"offset": offset,
|
||||
})
|
||||
return nil, err
|
||||
}
|
||||
defer cursor.Close(ctx)
|
||||
|
||||
if err = cursor.All(ctx, &users); err != nil {
|
||||
r.logger.Error("Failed to decode users", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return users, nil
|
||||
}
|
||||
|
||||
// GetByRole retrieves panel users by role with pagination
|
||||
func (r *UserRepositoryImpl) GetByRole(ctx context.Context, role domain.UserRole, limit, offset int) ([]*domain.User, error) {
|
||||
var users []*domain.User
|
||||
|
||||
// Build options
|
||||
opts := options.Find()
|
||||
opts.SetLimit(int64(limit))
|
||||
opts.SetSkip(int64(offset))
|
||||
opts.SetSort(bson.D{{"created_at", -1}}) // Sort by created_at desc
|
||||
|
||||
// Filter by role and active status
|
||||
filter := bson.M{
|
||||
"role": role,
|
||||
"is_active": true,
|
||||
}
|
||||
|
||||
cursor, err := r.collection.Find(ctx, filter, opts)
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to get users by role", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"role": role,
|
||||
"limit": limit,
|
||||
"offset": offset,
|
||||
})
|
||||
return nil, err
|
||||
}
|
||||
defer cursor.Close(ctx)
|
||||
|
||||
if err = cursor.All(ctx, &users); err != nil {
|
||||
r.logger.Error("Failed to decode users by role", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"role": role,
|
||||
})
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return users, nil
|
||||
}
|
||||
|
||||
// UpdatePermissions updates panel user permissions
|
||||
func (r *UserRepositoryImpl) UpdatePermissions(ctx context.Context, id uuid.UUID, permissions []string) error {
|
||||
filter := bson.M{"_id": id}
|
||||
update := bson.M{
|
||||
"$set": bson.M{
|
||||
"permissions": permissions,
|
||||
"updated_at": time.Now(),
|
||||
},
|
||||
}
|
||||
|
||||
result, err := r.collection.UpdateOne(ctx, filter, update)
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to update user permissions", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"user_id": id.String(),
|
||||
"permissions": permissions,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
if result.MatchedCount == 0 {
|
||||
return errors.New("user not found")
|
||||
}
|
||||
|
||||
r.logger.Info("Panel user permissions updated successfully", map[string]interface{}{
|
||||
"user_id": id.String(),
|
||||
"permissions": permissions,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,517 +0,0 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/google/uuid"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
"tm/internal/domain"
|
||||
"tm/internal/infrastructure"
|
||||
"tm/pkg/logger"
|
||||
)
|
||||
|
||||
// UserAuthServiceImpl implements the UserAuthService interface
|
||||
type UserAuthServiceImpl struct {
|
||||
userRepo domain.UserRepository
|
||||
config *infrastructure.AuthConfig
|
||||
logger logger.Logger
|
||||
}
|
||||
|
||||
// NewUserAuthService creates a new panel user authentication service
|
||||
func NewUserAuthService(
|
||||
userRepo domain.UserRepository,
|
||||
config *infrastructure.AuthConfig,
|
||||
logger logger.Logger,
|
||||
) domain.UserAuthService {
|
||||
return &UserAuthServiceImpl{
|
||||
userRepo: userRepo,
|
||||
config: config,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// Login authenticates a panel user and returns tokens
|
||||
func (s *UserAuthServiceImpl) Login(ctx context.Context, req domain.LoginRequest) (*domain.UserAuthResponse, error) {
|
||||
s.logger.Info("Panel user login attempt", map[string]interface{}{
|
||||
"email": req.Email,
|
||||
})
|
||||
|
||||
// Get user by email
|
||||
user, err := s.userRepo.GetByEmail(ctx, req.Email)
|
||||
if err != nil {
|
||||
s.logger.Warn("Panel user login failed - user not found", map[string]interface{}{
|
||||
"email": req.Email,
|
||||
})
|
||||
return nil, errors.New("invalid credentials")
|
||||
}
|
||||
|
||||
// Check if user is active
|
||||
if !user.IsActive {
|
||||
s.logger.Warn("Panel user login failed - account inactive", map[string]interface{}{
|
||||
"email": req.Email,
|
||||
"user_id": user.ID.String(),
|
||||
})
|
||||
return nil, errors.New("account is inactive")
|
||||
}
|
||||
|
||||
// Verify password
|
||||
if !s.verifyPassword(req.Password, user.Password) {
|
||||
s.logger.Warn("Panel user login failed - invalid password", map[string]interface{}{
|
||||
"email": req.Email,
|
||||
"user_id": user.ID.String(),
|
||||
})
|
||||
return nil, errors.New("invalid credentials")
|
||||
}
|
||||
|
||||
// Update last login time
|
||||
user.LastLoginAt = &time.Time{}
|
||||
*user.LastLoginAt = time.Now()
|
||||
|
||||
if err := s.userRepo.Update(ctx, user); err != nil {
|
||||
s.logger.Warn("Failed to update user last login", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"user_id": user.ID.String(),
|
||||
})
|
||||
// Don't fail login for this
|
||||
}
|
||||
|
||||
// Generate tokens
|
||||
accessToken, err := s.generateAccessToken(user)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to generate access token", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"user_id": user.ID.String(),
|
||||
})
|
||||
return nil, errors.New("failed to generate access token")
|
||||
}
|
||||
|
||||
refreshToken, err := s.generateRefreshToken(user)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to generate refresh token", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"user_id": user.ID.String(),
|
||||
})
|
||||
return nil, errors.New("failed to generate refresh token")
|
||||
}
|
||||
|
||||
s.logger.Info("Panel user logged in successfully", map[string]interface{}{
|
||||
"user_id": user.ID.String(),
|
||||
"email": user.Email,
|
||||
"role": user.Role,
|
||||
"department": user.Department,
|
||||
})
|
||||
|
||||
return &domain.UserAuthResponse{
|
||||
User: user,
|
||||
AccessToken: accessToken,
|
||||
RefreshToken: refreshToken,
|
||||
ExpiresIn: int64(s.config.JWT.AccessTokenDuration.Seconds()),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// RefreshToken generates new tokens using refresh token
|
||||
func (s *UserAuthServiceImpl) RefreshToken(ctx context.Context, refreshToken string) (*domain.UserAuthResponse, 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 != "user" {
|
||||
return nil, errors.New("invalid user type")
|
||||
}
|
||||
|
||||
userIDStr, ok := claims["user_id"].(string)
|
||||
if !ok {
|
||||
return nil, errors.New("invalid user ID in token")
|
||||
}
|
||||
|
||||
userID, err := uuid.Parse(userIDStr)
|
||||
if err != nil {
|
||||
return nil, errors.New("invalid user ID format")
|
||||
}
|
||||
|
||||
// Get user from database
|
||||
user, err := s.userRepo.GetByID(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, errors.New("user not found")
|
||||
}
|
||||
|
||||
if !user.IsActive {
|
||||
return nil, errors.New("account is inactive")
|
||||
}
|
||||
|
||||
// Generate new tokens
|
||||
accessToken, err := s.generateAccessToken(user)
|
||||
if err != nil {
|
||||
return nil, errors.New("failed to generate access token")
|
||||
}
|
||||
|
||||
newRefreshToken, err := s.generateRefreshToken(user)
|
||||
if err != nil {
|
||||
return nil, errors.New("failed to generate refresh token")
|
||||
}
|
||||
|
||||
return &domain.UserAuthResponse{
|
||||
User: user,
|
||||
AccessToken: accessToken,
|
||||
RefreshToken: newRefreshToken,
|
||||
ExpiresIn: int64(s.config.JWT.AccessTokenDuration.Seconds()),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ValidateToken validates a JWT token and returns the user
|
||||
func (s *UserAuthServiceImpl) ValidateToken(ctx context.Context, tokenString string) (*domain.User, 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 != "user" {
|
||||
return nil, errors.New("invalid user type")
|
||||
}
|
||||
|
||||
userIDStr, ok := claims["user_id"].(string)
|
||||
if !ok {
|
||||
return nil, errors.New("invalid user ID in token")
|
||||
}
|
||||
|
||||
userID, err := uuid.Parse(userIDStr)
|
||||
if err != nil {
|
||||
return nil, errors.New("invalid user ID format")
|
||||
}
|
||||
|
||||
user, err := s.userRepo.GetByID(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, errors.New("user not found")
|
||||
}
|
||||
|
||||
if !user.IsActive {
|
||||
return nil, errors.New("account is inactive")
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// ChangePassword changes panel user password
|
||||
func (s *UserAuthServiceImpl) ChangePassword(ctx context.Context, userID uuid.UUID, oldPassword, newPassword string) error {
|
||||
user, err := s.userRepo.GetByID(ctx, userID)
|
||||
if err != nil {
|
||||
return errors.New("user not found")
|
||||
}
|
||||
|
||||
// Verify old password
|
||||
if !s.verifyPassword(oldPassword, user.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
|
||||
user.Password = hashedPassword
|
||||
|
||||
if err := s.userRepo.Update(ctx, user); err != nil {
|
||||
s.logger.Error("Failed to update user password", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"user_id": userID.String(),
|
||||
})
|
||||
return errors.New("failed to update password")
|
||||
}
|
||||
|
||||
s.logger.Info("Panel user password changed successfully", map[string]interface{}{
|
||||
"user_id": userID.String(),
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreatePanelUser creates a new panel user (admin only)
|
||||
func (s *UserAuthServiceImpl) CreatePanelUser(ctx context.Context, req domain.CreateUserRequest, createdBy uuid.UUID) (*domain.User, error) {
|
||||
s.logger.Info("Creating new panel user", map[string]interface{}{
|
||||
"email": req.Email,
|
||||
"role": req.Role,
|
||||
"created_by": createdBy.String(),
|
||||
})
|
||||
|
||||
// Check if user already exists
|
||||
existingUser, err := s.userRepo.GetByEmail(ctx, req.Email)
|
||||
if err == nil && existingUser != nil {
|
||||
return nil, errors.New("user already exists")
|
||||
}
|
||||
|
||||
// Validate role
|
||||
if !s.isValidRole(req.Role) {
|
||||
return nil, errors.New("invalid role")
|
||||
}
|
||||
|
||||
// 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 user
|
||||
user := &domain.User{
|
||||
ID: uuid.New(),
|
||||
Email: req.Email,
|
||||
Password: hashedPassword,
|
||||
FirstName: req.FirstName,
|
||||
LastName: req.LastName,
|
||||
Role: req.Role,
|
||||
Department: req.Department,
|
||||
IsActive: true,
|
||||
Permissions: s.getDefaultPermissions(req.Role),
|
||||
CreatedBy: &createdBy,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
// Add custom permissions if provided
|
||||
if len(req.Permissions) > 0 {
|
||||
user.Permissions = append(user.Permissions, req.Permissions...)
|
||||
user.Permissions = s.removeDuplicatePermissions(user.Permissions)
|
||||
}
|
||||
|
||||
if err := s.userRepo.Create(ctx, user); err != nil {
|
||||
s.logger.Error("Failed to create panel user", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"email": req.Email,
|
||||
"created_by": createdBy.String(),
|
||||
})
|
||||
return nil, errors.New("failed to create user")
|
||||
}
|
||||
|
||||
s.logger.Info("Panel user created successfully", map[string]interface{}{
|
||||
"user_id": user.ID.String(),
|
||||
"email": user.Email,
|
||||
"role": user.Role,
|
||||
"created_by": createdBy.String(),
|
||||
})
|
||||
|
||||
// Remove password from response
|
||||
user.Password = ""
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// UpdateUserPermissions updates panel user permissions (admin only)
|
||||
func (s *UserAuthServiceImpl) UpdateUserPermissions(ctx context.Context, userID uuid.UUID, permissions []string, updatedBy uuid.UUID) error {
|
||||
// Check if user exists
|
||||
_, err := s.userRepo.GetByID(ctx, userID)
|
||||
if err != nil {
|
||||
return errors.New("user not found")
|
||||
}
|
||||
|
||||
// Validate permissions
|
||||
for _, permission := range permissions {
|
||||
if !s.isValidPermission(permission) {
|
||||
return errors.New("invalid permission: " + permission)
|
||||
}
|
||||
}
|
||||
|
||||
// Update permissions
|
||||
if err := s.userRepo.UpdatePermissions(ctx, userID, permissions); err != nil {
|
||||
s.logger.Error("Failed to update user permissions", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"user_id": userID.String(),
|
||||
"updated_by": updatedBy.String(),
|
||||
})
|
||||
return errors.New("failed to update permissions")
|
||||
}
|
||||
|
||||
s.logger.Info("Panel user permissions updated successfully", map[string]interface{}{
|
||||
"user_id": userID.String(),
|
||||
"permissions": permissions,
|
||||
"updated_by": updatedBy.String(),
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Private helper methods
|
||||
|
||||
func (s *UserAuthServiceImpl) hashPassword(password string) (string, error) {
|
||||
hashedBytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(hashedBytes), nil
|
||||
}
|
||||
|
||||
func (s *UserAuthServiceImpl) verifyPassword(password, hashedPassword string) bool {
|
||||
err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password))
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (s *UserAuthServiceImpl) generateAccessToken(user *domain.User) (string, error) {
|
||||
claims := jwt.MapClaims{
|
||||
"user_id": user.ID.String(),
|
||||
"email": user.Email,
|
||||
"role": user.Role,
|
||||
"department": user.Department,
|
||||
"permissions": user.Permissions,
|
||||
"user_type": "user",
|
||||
"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 *UserAuthServiceImpl) generateRefreshToken(user *domain.User) (string, error) {
|
||||
claims := jwt.MapClaims{
|
||||
"user_id": user.ID.String(),
|
||||
"user_type": "user",
|
||||
"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 *UserAuthServiceImpl) isValidRole(role domain.UserRole) bool {
|
||||
validRoles := []domain.UserRole{
|
||||
domain.UserRoleAdmin,
|
||||
domain.UserRoleModerator,
|
||||
domain.UserRoleSupport,
|
||||
domain.UserRoleAnalyst,
|
||||
}
|
||||
|
||||
for _, validRole := range validRoles {
|
||||
if role == validRole {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *UserAuthServiceImpl) getDefaultPermissions(role domain.UserRole) []string {
|
||||
switch role {
|
||||
case domain.UserRoleAdmin:
|
||||
return []string{
|
||||
"manage_users",
|
||||
"manage_tenders",
|
||||
"manage_companies",
|
||||
"manage_customers",
|
||||
"view_reports",
|
||||
"manage_system",
|
||||
"manage_sources",
|
||||
}
|
||||
case domain.UserRoleModerator:
|
||||
return []string{
|
||||
"manage_tenders",
|
||||
"manage_companies",
|
||||
"view_customers",
|
||||
"view_reports",
|
||||
"manage_sources",
|
||||
}
|
||||
case domain.UserRoleSupport:
|
||||
return []string{
|
||||
"view_customers",
|
||||
"view_tenders",
|
||||
"view_companies",
|
||||
"create_tickets",
|
||||
}
|
||||
case domain.UserRoleAnalyst:
|
||||
return []string{
|
||||
"view_reports",
|
||||
"view_tenders",
|
||||
"view_customers",
|
||||
"view_companies",
|
||||
"export_data",
|
||||
}
|
||||
default:
|
||||
return []string{}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *UserAuthServiceImpl) isValidPermission(permission string) bool {
|
||||
validPermissions := []string{
|
||||
"manage_users",
|
||||
"manage_tenders",
|
||||
"manage_companies",
|
||||
"manage_customers",
|
||||
"view_customers",
|
||||
"view_tenders",
|
||||
"view_companies",
|
||||
"view_reports",
|
||||
"manage_system",
|
||||
"manage_sources",
|
||||
"create_tickets",
|
||||
"export_data",
|
||||
}
|
||||
|
||||
for _, validPermission := range validPermissions {
|
||||
if permission == validPermission {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *UserAuthServiceImpl) removeDuplicatePermissions(permissions []string) []string {
|
||||
seen := make(map[string]bool)
|
||||
result := []string{}
|
||||
|
||||
for _, permission := range permissions {
|
||||
if !seen[permission] {
|
||||
seen[permission] = true
|
||||
result = append(result, permission)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -1,336 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
"tm/internal/domain"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultMongoURI = "mongodb://localhost:27017"
|
||||
DefaultDBName = "tender_management"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Get database connection details from environment or use defaults
|
||||
mongoURI := DefaultMongoURI
|
||||
if uri := getEnv("MONGO_URI"); uri != "" {
|
||||
mongoURI = uri
|
||||
}
|
||||
|
||||
dbName := DefaultDBName
|
||||
if name := getEnv("DB_NAME"); name != "" {
|
||||
dbName = name
|
||||
}
|
||||
|
||||
fmt.Println("🚀 Starting database migration for dual user system...")
|
||||
fmt.Printf("📊 Database: %s\n", dbName)
|
||||
fmt.Printf("🔗 URI: %s\n", mongoURI)
|
||||
|
||||
// Connect to MongoDB
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
client, err := mongo.Connect(ctx, options.Client().ApplyURI(mongoURI))
|
||||
if err != nil {
|
||||
log.Fatal("❌ Failed to connect to MongoDB:", err)
|
||||
}
|
||||
defer client.Disconnect(context.Background())
|
||||
|
||||
// Ping to verify connection
|
||||
if err = client.Ping(ctx, nil); err != nil {
|
||||
log.Fatal("❌ Failed to ping MongoDB:", err)
|
||||
}
|
||||
|
||||
fmt.Println("✅ Connected to MongoDB successfully")
|
||||
|
||||
db := client.Database(dbName)
|
||||
|
||||
// Run migrations
|
||||
if err := createCustomersCollection(db); err != nil {
|
||||
log.Fatal("❌ Failed to create customers collection:", err)
|
||||
}
|
||||
|
||||
if err := createUsersCollection(db); err != nil {
|
||||
log.Fatal("❌ Failed to create users collection:", err)
|
||||
}
|
||||
|
||||
if err := createSampleData(db); err != nil {
|
||||
log.Fatal("❌ Failed to create sample data:", err)
|
||||
}
|
||||
|
||||
fmt.Println("🎉 Database migration completed successfully!")
|
||||
}
|
||||
|
||||
// createCustomersCollection creates the customers collection with indexes
|
||||
func createCustomersCollection(db *mongo.Database) error {
|
||||
fmt.Println("📝 Creating customers collection...")
|
||||
|
||||
collection := db.Collection("customers")
|
||||
|
||||
// Create indexes
|
||||
indexes := []mongo.IndexModel{
|
||||
{
|
||||
Keys: bson.D{{"email", 1}},
|
||||
Options: options.Index().SetUnique(true).SetName("email_unique"),
|
||||
},
|
||||
{
|
||||
Keys: bson.D{{"company_id", 1}},
|
||||
Options: options.Index().SetName("company_id_idx"),
|
||||
},
|
||||
{
|
||||
Keys: bson.D{{"is_active", 1}},
|
||||
Options: options.Index().SetName("is_active_idx"),
|
||||
},
|
||||
{
|
||||
Keys: bson.D{{"created_at", -1}},
|
||||
Options: options.Index().SetName("created_at_desc_idx"),
|
||||
},
|
||||
{
|
||||
Keys: bson.D{{"role", 1}},
|
||||
Options: options.Index().SetName("role_idx"),
|
||||
},
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
_, err := collection.Indexes().CreateMany(ctx, indexes)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create customer indexes: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println("✅ Customers collection created with indexes")
|
||||
return nil
|
||||
}
|
||||
|
||||
// createUsersCollection creates the panel users collection with indexes
|
||||
func createUsersCollection(db *mongo.Database) error {
|
||||
fmt.Println("📝 Creating users collection...")
|
||||
|
||||
collection := db.Collection("users")
|
||||
|
||||
// Create indexes
|
||||
indexes := []mongo.IndexModel{
|
||||
{
|
||||
Keys: bson.D{{"email", 1}},
|
||||
Options: options.Index().SetUnique(true).SetName("email_unique"),
|
||||
},
|
||||
{
|
||||
Keys: bson.D{{"role", 1}},
|
||||
Options: options.Index().SetName("role_idx"),
|
||||
},
|
||||
{
|
||||
Keys: bson.D{{"is_active", 1}},
|
||||
Options: options.Index().SetName("is_active_idx"),
|
||||
},
|
||||
{
|
||||
Keys: bson.D{{"department", 1}},
|
||||
Options: options.Index().SetName("department_idx"),
|
||||
},
|
||||
{
|
||||
Keys: bson.D{{"created_at", -1}},
|
||||
Options: options.Index().SetName("created_at_desc_idx"),
|
||||
},
|
||||
{
|
||||
Keys: bson.D{{"created_by", 1}},
|
||||
Options: options.Index().SetName("created_by_idx"),
|
||||
},
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
_, err := collection.Indexes().CreateMany(ctx, indexes)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create user indexes: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println("✅ Users collection created with indexes")
|
||||
return nil
|
||||
}
|
||||
|
||||
// createSampleData creates sample admin user and test customer
|
||||
func createSampleData(db *mongo.Database) error {
|
||||
fmt.Println("📝 Creating sample data...")
|
||||
|
||||
// Create sample admin user
|
||||
if err := createSampleAdmin(db); err != nil {
|
||||
return fmt.Errorf("failed to create sample admin: %w", err)
|
||||
}
|
||||
|
||||
// Create sample customer
|
||||
if err := createSampleCustomer(db); err != nil {
|
||||
return fmt.Errorf("failed to create sample customer: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println("✅ Sample data created successfully")
|
||||
return nil
|
||||
}
|
||||
|
||||
// createSampleAdmin creates a sample admin user
|
||||
func createSampleAdmin(db *mongo.Database) error {
|
||||
collection := db.Collection("users")
|
||||
|
||||
// Check if admin already exists
|
||||
ctx := context.Background()
|
||||
count, err := collection.CountDocuments(ctx, bson.M{"email": "admin@tendermanagement.com"})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if count > 0 {
|
||||
fmt.Println("ℹ️ Sample admin user already exists, skipping...")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Hash password for admin
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte("admin123!"), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
adminID := uuid.New()
|
||||
admin := bson.M{
|
||||
"_id": adminID,
|
||||
"email": "admin@tendermanagement.com",
|
||||
"password": string(hashedPassword),
|
||||
"first_name": "System",
|
||||
"last_name": "Administrator",
|
||||
"role": domain.UserRoleAdmin,
|
||||
"department": "IT",
|
||||
"is_active": true,
|
||||
"permissions": []string{
|
||||
"manage_users",
|
||||
"manage_tenders",
|
||||
"manage_companies",
|
||||
"manage_customers",
|
||||
"view_reports",
|
||||
"manage_system",
|
||||
"manage_sources",
|
||||
},
|
||||
"created_at": now,
|
||||
"updated_at": now,
|
||||
}
|
||||
|
||||
_, err = collection.InsertOne(ctx, admin)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println("👤 Sample admin user created:")
|
||||
fmt.Println(" 📧 Email: admin@tendermanagement.com")
|
||||
fmt.Println(" 🔑 Password: admin123!")
|
||||
fmt.Println(" 🛡️ Role: admin")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// createSampleCustomer creates a sample customer
|
||||
func createSampleCustomer(db *mongo.Database) error {
|
||||
collection := db.Collection("customers")
|
||||
|
||||
// Check if customer already exists
|
||||
ctx := context.Background()
|
||||
count, err := collection.CountDocuments(ctx, bson.M{"email": "customer@example.com"})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if count > 0 {
|
||||
fmt.Println("ℹ️ Sample customer already exists, skipping...")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Hash password for customer
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte("customer123!"), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
companyID := uuid.New()
|
||||
customerID := uuid.New()
|
||||
|
||||
// Create sample company first
|
||||
companyCollection := db.Collection("companies")
|
||||
company := bson.M{
|
||||
"_id": companyID,
|
||||
"name": "Sample Company Ltd",
|
||||
"description": "A sample company for testing",
|
||||
"industry": "Technology",
|
||||
"country": "United States",
|
||||
"website": "https://example.com",
|
||||
"keywords": []string{"technology", "software", "consulting"},
|
||||
"products": []string{"software development", "consulting"},
|
||||
"services": []string{"web development", "mobile apps"},
|
||||
"created_at": now,
|
||||
"updated_at": now,
|
||||
}
|
||||
|
||||
_, err = companyCollection.InsertOne(ctx, company)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create customer
|
||||
customer := bson.M{
|
||||
"_id": customerID,
|
||||
"email": "customer@example.com",
|
||||
"password": string(hashedPassword),
|
||||
"first_name": "John",
|
||||
"last_name": "Doe",
|
||||
"phone": "+1234567890",
|
||||
"role": domain.CustomerRoleUser,
|
||||
"company_id": companyID,
|
||||
"is_active": true,
|
||||
"is_verified": true,
|
||||
"device_tokens": []string{},
|
||||
"preferences": bson.M{
|
||||
"language": "en",
|
||||
"notification_settings": bson.M{
|
||||
"new_tenders": true,
|
||||
"deadline_reminder": true,
|
||||
"status_updates": true,
|
||||
"system_alerts": true,
|
||||
},
|
||||
"tender_alerts": true,
|
||||
"email_notifications": true,
|
||||
"push_notifications": true,
|
||||
"preferred_categories": []string{},
|
||||
},
|
||||
"created_at": now,
|
||||
"updated_at": now,
|
||||
}
|
||||
|
||||
_, err = collection.InsertOne(ctx, customer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println("👤 Sample customer created:")
|
||||
fmt.Println(" 📧 Email: customer@example.com")
|
||||
fmt.Println(" 🔑 Password: customer123!")
|
||||
fmt.Println(" 🏢 Company: Sample Company Ltd")
|
||||
fmt.Println(" 📱 Role: customer_user")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
|
||||
func getEnv(key string) string {
|
||||
// Simple environment variable getter
|
||||
// In a real implementation, you might use os.Getenv()
|
||||
return ""
|
||||
}
|
||||
+18
-70
@@ -8,8 +8,6 @@ import (
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
"gopkg.in/natefinch/lumberjack.v2"
|
||||
|
||||
"tm/internal/infrastructure"
|
||||
)
|
||||
|
||||
// Logger interface defines logging methods
|
||||
@@ -30,8 +28,23 @@ type ZapLogger struct {
|
||||
fields map[string]interface{}
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Level string `mapstructure:"level"`
|
||||
Format string `mapstructure:"format"`
|
||||
Output string `mapstructure:"output"`
|
||||
File FileConfig `mapstructure:"file"`
|
||||
}
|
||||
|
||||
type FileConfig struct {
|
||||
Path string `mapstructure:"path"`
|
||||
MaxSize int `mapstructure:"max_size"` // MB
|
||||
MaxBackups int `mapstructure:"max_backups"`
|
||||
MaxAge int `mapstructure:"max_age"` // days
|
||||
Compress bool `mapstructure:"compress"`
|
||||
}
|
||||
|
||||
// NewLogger creates a new logger instance with Zap and Lumberjack
|
||||
func NewLogger(config *infrastructure.LoggingConfig) Logger {
|
||||
func NewLogger(config *Config) Logger {
|
||||
// Parse log level
|
||||
level := parseLogLevel(config.Level)
|
||||
|
||||
@@ -62,7 +75,7 @@ func NewLogger(config *infrastructure.LoggingConfig) Logger {
|
||||
}
|
||||
|
||||
// createWriter creates the appropriate writer based on configuration
|
||||
func createWriter(config *infrastructure.LoggingConfig) zapcore.WriteSyncer {
|
||||
func createWriter(config *Config) zapcore.WriteSyncer {
|
||||
switch config.Output {
|
||||
case "stdout":
|
||||
return zapcore.AddSync(os.Stdout)
|
||||
@@ -77,7 +90,7 @@ func createWriter(config *infrastructure.LoggingConfig) zapcore.WriteSyncer {
|
||||
}
|
||||
|
||||
// createFileWriter creates a file writer with rotation using lumberjack
|
||||
func createFileWriter(fileConfig infrastructure.LogFileConfig) zapcore.WriteSyncer {
|
||||
func createFileWriter(fileConfig FileConfig) zapcore.WriteSyncer {
|
||||
// Ensure log directory exists
|
||||
logDir := filepath.Dir(fileConfig.Path)
|
||||
if err := os.MkdirAll(logDir, 0755); err != nil {
|
||||
@@ -237,68 +250,3 @@ func (l *ZapLogger) mergeFields(fields map[string]interface{}) map[string]interf
|
||||
|
||||
return merged
|
||||
}
|
||||
|
||||
// Global logger instance
|
||||
var globalLogger Logger
|
||||
|
||||
// InitLogger initializes the global logger
|
||||
func InitLogger(config *infrastructure.LoggingConfig) {
|
||||
globalLogger = NewLogger(config)
|
||||
}
|
||||
|
||||
// GetLogger returns the global logger instance
|
||||
func GetLogger() Logger {
|
||||
if globalLogger == nil {
|
||||
// Fallback configuration
|
||||
config := &infrastructure.LoggingConfig{
|
||||
Level: "info",
|
||||
Format: "json",
|
||||
Output: "stdout",
|
||||
File: infrastructure.LogFileConfig{
|
||||
Path: "./logs/app.log",
|
||||
MaxSize: 100,
|
||||
MaxBackups: 5,
|
||||
MaxAge: 30,
|
||||
Compress: true,
|
||||
},
|
||||
}
|
||||
globalLogger = NewLogger(config)
|
||||
}
|
||||
return globalLogger
|
||||
}
|
||||
|
||||
// Cleanup flushes and closes the logger
|
||||
func Cleanup() {
|
||||
if globalLogger != nil {
|
||||
globalLogger.Sync()
|
||||
}
|
||||
}
|
||||
|
||||
// Helper functions for convenience
|
||||
func Debug(msg string, fields map[string]interface{}) {
|
||||
GetLogger().Debug(msg, fields)
|
||||
}
|
||||
|
||||
func Info(msg string, fields map[string]interface{}) {
|
||||
GetLogger().Info(msg, fields)
|
||||
}
|
||||
|
||||
func Warn(msg string, fields map[string]interface{}) {
|
||||
GetLogger().Warn(msg, fields)
|
||||
}
|
||||
|
||||
func Error(msg string, fields map[string]interface{}) {
|
||||
GetLogger().Error(msg, fields)
|
||||
}
|
||||
|
||||
func Fatal(msg string, fields map[string]interface{}) {
|
||||
GetLogger().Fatal(msg, fields)
|
||||
}
|
||||
|
||||
func WithFields(fields map[string]interface{}) Logger {
|
||||
return GetLogger().WithFields(fields)
|
||||
}
|
||||
|
||||
func Sync() error {
|
||||
return GetLogger().Sync()
|
||||
}
|
||||
|
||||
@@ -1,270 +0,0 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
|
||||
"tm/internal/domain"
|
||||
"tm/pkg/logger"
|
||||
"tm/pkg/response"
|
||||
)
|
||||
|
||||
// AuthMiddleware creates JWT authentication middleware for both customers and users
|
||||
func AuthMiddleware(customerAuthService domain.CustomerAuthService, userAuthService domain.UserAuthService, logger logger.Logger) echo.MiddlewareFunc {
|
||||
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
// Skip authentication for public routes
|
||||
if isPublicRoute(c.Path(), c.Request().Method) {
|
||||
return next(c)
|
||||
}
|
||||
|
||||
// Get Authorization header
|
||||
authHeader := c.Request().Header.Get("Authorization")
|
||||
if authHeader == "" {
|
||||
logger.Warn("Missing authorization header", map[string]interface{}{
|
||||
"path": c.Path(),
|
||||
"method": c.Request().Method,
|
||||
})
|
||||
return response.Unauthorized(c, "Authorization header required")
|
||||
}
|
||||
|
||||
// Check Bearer format
|
||||
const bearerPrefix = "Bearer "
|
||||
if !strings.HasPrefix(authHeader, bearerPrefix) {
|
||||
logger.Warn("Invalid authorization header format", map[string]interface{}{
|
||||
"path": c.Path(),
|
||||
"method": c.Request().Method,
|
||||
})
|
||||
return response.Unauthorized(c, "Invalid authorization header format")
|
||||
}
|
||||
|
||||
// Extract token
|
||||
token := strings.TrimPrefix(authHeader, bearerPrefix)
|
||||
if token == "" {
|
||||
return response.Unauthorized(c, "Token required")
|
||||
}
|
||||
|
||||
// Try to validate as customer first (for mobile routes)
|
||||
if strings.HasPrefix(c.Path(), "/api/mobile/") || strings.HasPrefix(c.Path(), "/mobile/") {
|
||||
customer, err := customerAuthService.ValidateToken(c.Request().Context(), token)
|
||||
if err == nil && customer != nil {
|
||||
// Store customer in context
|
||||
c.Set("customer", customer)
|
||||
c.Set("customer_id", customer.ID.String())
|
||||
c.Set("company_id", customer.CompanyID.String())
|
||||
c.Set("user_type", "customer")
|
||||
return next(c)
|
||||
}
|
||||
}
|
||||
|
||||
// Try to validate as panel user (for admin routes)
|
||||
if strings.HasPrefix(c.Path(), "/api/admin/") || strings.HasPrefix(c.Path(), "/admin/") {
|
||||
user, err := userAuthService.ValidateToken(c.Request().Context(), token)
|
||||
if err == nil && user != nil {
|
||||
// Store user in context
|
||||
c.Set("user", user)
|
||||
c.Set("user_id", user.ID.String())
|
||||
c.Set("user_type", "user")
|
||||
return next(c)
|
||||
}
|
||||
}
|
||||
|
||||
// For other routes, try both types
|
||||
if customer, err := customerAuthService.ValidateToken(c.Request().Context(), token); err == nil && customer != nil {
|
||||
c.Set("customer", customer)
|
||||
c.Set("customer_id", customer.ID.String())
|
||||
c.Set("company_id", customer.CompanyID.String())
|
||||
c.Set("user_type", "customer")
|
||||
return next(c)
|
||||
}
|
||||
|
||||
if user, err := userAuthService.ValidateToken(c.Request().Context(), token); err == nil && user != nil {
|
||||
c.Set("user", user)
|
||||
c.Set("user_id", user.ID.String())
|
||||
c.Set("user_type", "user")
|
||||
return next(c)
|
||||
}
|
||||
|
||||
logger.Warn("Token validation failed", map[string]interface{}{
|
||||
"path": c.Path(),
|
||||
})
|
||||
return response.Unauthorized(c, "Invalid or expired token")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CustomerOnlyMiddleware ensures only customers can access the route
|
||||
func CustomerOnlyMiddleware() echo.MiddlewareFunc {
|
||||
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
customer, ok := c.Get("customer").(*domain.Customer)
|
||||
if !ok || customer == nil {
|
||||
return response.Unauthorized(c, "Customer authentication required")
|
||||
}
|
||||
|
||||
if !customer.IsActive {
|
||||
return response.Forbidden(c, "Account is inactive")
|
||||
}
|
||||
|
||||
return next(c)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// UserOnlyMiddleware ensures only panel users can access the route
|
||||
func UserOnlyMiddleware() echo.MiddlewareFunc {
|
||||
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
user, ok := c.Get("user").(*domain.User)
|
||||
if !ok || user == nil {
|
||||
return response.Unauthorized(c, "Panel user authentication required")
|
||||
}
|
||||
|
||||
if !user.IsActive {
|
||||
return response.Forbidden(c, "Account is inactive")
|
||||
}
|
||||
|
||||
return next(c)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CustomerRoleMiddleware creates role-based authorization middleware for customers
|
||||
func CustomerRoleMiddleware(allowedRoles ...domain.CustomerRole) echo.MiddlewareFunc {
|
||||
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
customer, ok := c.Get("customer").(*domain.Customer)
|
||||
if !ok {
|
||||
return response.Unauthorized(c, "Customer authentication required")
|
||||
}
|
||||
|
||||
// Check if customer has required role
|
||||
for _, role := range allowedRoles {
|
||||
if customer.Role == role {
|
||||
return next(c)
|
||||
}
|
||||
}
|
||||
|
||||
return response.Forbidden(c, "Insufficient permissions")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// UserRoleMiddleware creates role-based authorization middleware for panel users
|
||||
func UserRoleMiddleware(allowedRoles ...domain.UserRole) echo.MiddlewareFunc {
|
||||
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
user, ok := c.Get("user").(*domain.User)
|
||||
if !ok {
|
||||
return response.Unauthorized(c, "Panel user authentication required")
|
||||
}
|
||||
|
||||
// Check if user has required role
|
||||
for _, role := range allowedRoles {
|
||||
if user.Role == role {
|
||||
return next(c)
|
||||
}
|
||||
}
|
||||
|
||||
return response.Forbidden(c, "Insufficient permissions")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// UserPermissionMiddleware creates permission-based authorization middleware for panel users
|
||||
func UserPermissionMiddleware(requiredPermissions ...string) echo.MiddlewareFunc {
|
||||
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
user, ok := c.Get("user").(*domain.User)
|
||||
if !ok {
|
||||
return response.Unauthorized(c, "Panel user authentication required")
|
||||
}
|
||||
|
||||
// Admin has all permissions
|
||||
if user.IsAdmin() {
|
||||
return next(c)
|
||||
}
|
||||
|
||||
// Check if user has all required permissions
|
||||
for _, permission := range requiredPermissions {
|
||||
if !user.HasPermission(permission) {
|
||||
return response.Forbidden(c, "Insufficient permissions")
|
||||
}
|
||||
}
|
||||
|
||||
return next(c)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// isPublicRoute checks if a route should be accessible without authentication
|
||||
func isPublicRoute(path, method string) bool {
|
||||
// Debug logging
|
||||
fmt.Printf("Checking public route: %s %s\n", method, path)
|
||||
|
||||
// Also log to stderr to ensure we see it
|
||||
fmt.Fprintf(os.Stderr, "DEBUG: Checking public route: %s %s\n", method, path)
|
||||
|
||||
publicRoutes := map[string][]string{
|
||||
// Mobile customer auth routes
|
||||
"/api/v1/mobile/auth/register": {"POST"},
|
||||
"/api/v1/mobile/auth/login": {"POST"},
|
||||
"/api/v1/mobile/auth/refresh": {"POST"},
|
||||
"/api/v1/mobile/auth/resend-verification": {"POST"},
|
||||
"/api/mobile/auth/register": {"POST"},
|
||||
"/api/mobile/auth/login": {"POST"},
|
||||
"/api/mobile/auth/refresh": {"POST"},
|
||||
"/api/mobile/auth/resend-verification": {"POST"},
|
||||
"/mobile/auth/register": {"POST"},
|
||||
"/mobile/auth/login": {"POST"},
|
||||
"/mobile/auth/refresh": {"POST"},
|
||||
"/mobile/auth/resend-verification": {"POST"},
|
||||
|
||||
// Panel user auth routes
|
||||
"/api/v1/admin/auth/login": {"POST"},
|
||||
"/api/v1/admin/auth/refresh": {"POST"},
|
||||
"/api/admin/auth/login": {"POST"},
|
||||
"/api/admin/auth/refresh": {"POST"},
|
||||
"/admin/auth/login": {"POST"},
|
||||
"/admin/auth/refresh": {"POST"},
|
||||
|
||||
// Legacy auth routes (for backward compatibility)
|
||||
"/auth/register": {"POST"},
|
||||
"/auth/login": {"POST"},
|
||||
"/auth/refresh": {"POST"},
|
||||
|
||||
// Health and documentation routes
|
||||
"/health": {"GET"},
|
||||
"/metrics": {"GET"},
|
||||
"/api/docs": {"GET"},
|
||||
"/api/docs/*": {"GET"},
|
||||
"/swagger/*": {"GET"},
|
||||
}
|
||||
|
||||
if methods, exists := publicRoutes[path]; exists {
|
||||
for _, allowedMethod := range methods {
|
||||
if method == allowedMethod {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for wildcard routes
|
||||
for route, methods := range publicRoutes {
|
||||
if strings.HasSuffix(route, "/*") {
|
||||
prefix := strings.TrimSuffix(route, "/*")
|
||||
if strings.HasPrefix(path, prefix) {
|
||||
for _, allowedMethod := range methods {
|
||||
if method == allowedMethod {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,479 @@
|
||||
# MongoDB ORM for Go
|
||||
|
||||
A production-grade, high-performance generic ORM for MongoDB in Go, using the official MongoDB driver. This ORM provides a clean, type-safe interface for MongoDB operations with support for pagination, transactions, and advanced querying.
|
||||
|
||||
## Features
|
||||
|
||||
✅ **Generic Repository Pattern** - Type-safe operations for any struct
|
||||
✅ **Dual Pagination Support** - Both offset-based and cursor-based pagination
|
||||
✅ **Comprehensive CRUD Operations** - Create, Read, Update, Delete with bulk operations
|
||||
✅ **Advanced Querying** - Fluent query builder with complex conditions
|
||||
✅ **Aggregation Support** - Full MongoDB aggregation pipeline support
|
||||
✅ **Connection Management** - Connection pooling and lifecycle management
|
||||
✅ **Index Management** - Easy index creation and management
|
||||
✅ **Transaction Support** - ACID transactions for multi-document operations
|
||||
✅ **Structured Logging** - Comprehensive logging with structured fields
|
||||
✅ **Performance Optimized** - Efficient cursor-based pagination and connection pooling
|
||||
✅ **Type Safety** - Full Go generics support for compile-time type safety
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Define Your Model
|
||||
|
||||
```go
|
||||
type User struct {
|
||||
Model
|
||||
Email string `bson:"email" json:"email"`
|
||||
FirstName string `bson:"first_name" json:"first_name"`
|
||||
LastName string `bson:"last_name" json:"last_name"`
|
||||
Age int `bson:"age" json:"age"`
|
||||
IsActive bool `bson:"is_active" json:"is_active"`
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Set Up Connection
|
||||
|
||||
```go
|
||||
// Create logger
|
||||
logger := &YourLogger{}
|
||||
|
||||
// Configure connection
|
||||
config := mongo.ConnectionConfig{
|
||||
URI: "mongodb://localhost:27017",
|
||||
Database: "your_database",
|
||||
}
|
||||
|
||||
// Create connection manager
|
||||
connManager, err := mongo.NewConnectionManager(config, logger)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer connManager.Close(context.Background())
|
||||
|
||||
// Create repository
|
||||
userRepo := mongo.NewRepository[User](connManager.GetCollection("users"), logger)
|
||||
```
|
||||
|
||||
### 3. Basic Operations
|
||||
|
||||
```go
|
||||
// Create
|
||||
user := &User{
|
||||
Email: "john@example.com",
|
||||
FirstName: "John",
|
||||
LastName: "Doe",
|
||||
Age: 30,
|
||||
IsActive: true,
|
||||
}
|
||||
err := userRepo.Create(context.Background(), user)
|
||||
|
||||
// Find by ID
|
||||
foundUser, err := userRepo.FindByID(context.Background(), user.ID)
|
||||
|
||||
// Update
|
||||
user.FirstName = "Jane"
|
||||
err = userRepo.Update(context.Background(), user)
|
||||
|
||||
// Delete
|
||||
err = userRepo.Delete(context.Background(), user.ID)
|
||||
```
|
||||
|
||||
## Pagination
|
||||
|
||||
### Offset-based Pagination
|
||||
|
||||
```go
|
||||
pagination := mongo.NewPaginationBuilder().
|
||||
Limit(10).
|
||||
Skip(20).
|
||||
SortDesc("created_at").
|
||||
Build()
|
||||
|
||||
results, err := userRepo.FindAll(context.Background(), bson.M{}, pagination)
|
||||
```
|
||||
|
||||
### Cursor-based Pagination (Recommended for Performance)
|
||||
|
||||
```go
|
||||
// First page
|
||||
pagination := mongo.NewPaginationBuilder().
|
||||
Limit(10).
|
||||
SortDesc("created_at").
|
||||
Build()
|
||||
|
||||
firstPage, err := userRepo.FindAll(context.Background(), bson.M{}, pagination)
|
||||
|
||||
// Next page using cursor
|
||||
if firstPage.NextCursor != "" {
|
||||
nextPagination := mongo.NewPaginationBuilder().
|
||||
Limit(10).
|
||||
SortDesc("created_at").
|
||||
Cursor(firstPage.NextCursor).
|
||||
Build()
|
||||
|
||||
nextPage, err := userRepo.FindAll(context.Background(), bson.M{}, nextPagination)
|
||||
}
|
||||
```
|
||||
|
||||
## Advanced Querying
|
||||
|
||||
### Using QueryBuilder
|
||||
|
||||
```go
|
||||
query := mongo.NewQueryBuilder().
|
||||
Where("is_active", true).
|
||||
WhereGreaterThan("age", 18).
|
||||
WhereIn("category", []interface{}{"admin", "user"}).
|
||||
WhereRegex("email", ".*@example\\.com", "i").
|
||||
Build()
|
||||
|
||||
results, err := userRepo.FindAll(context.Background(), query, pagination)
|
||||
```
|
||||
|
||||
### Complex Queries
|
||||
|
||||
```go
|
||||
// AND/OR conditions
|
||||
query := mongo.NewQueryBuilder().
|
||||
WhereAnd(
|
||||
bson.M{"is_active": true},
|
||||
bson.M{"age": bson.M{"$gte": 18}},
|
||||
).
|
||||
WhereOr(
|
||||
bson.M{"role": "admin"},
|
||||
bson.M{"role": "moderator"},
|
||||
).
|
||||
Build()
|
||||
```
|
||||
|
||||
## Aggregation
|
||||
|
||||
```go
|
||||
pipeline := mongo.Pipeline{
|
||||
{{Key: "$match", Value: bson.M{"is_active": true}}},
|
||||
{{Key: "$group", Value: bson.M{
|
||||
"_id": "$role",
|
||||
"count": bson.M{"$sum": 1},
|
||||
"avgAge": bson.M{"$avg": "$age"},
|
||||
}}},
|
||||
{{Key: "$sort", Value: bson.M{"count": -1}}},
|
||||
}
|
||||
|
||||
results, err := userRepo.Aggregate(context.Background(), pipeline)
|
||||
```
|
||||
|
||||
## Bulk Operations
|
||||
|
||||
```go
|
||||
// Bulk create
|
||||
users := []User{
|
||||
{Email: "user1@example.com", FirstName: "User1"},
|
||||
{Email: "user2@example.com", FirstName: "User2"},
|
||||
}
|
||||
err := userRepo.BulkCreate(context.Background(), users)
|
||||
|
||||
// Bulk update
|
||||
filter := bson.M{"is_active": false}
|
||||
update := bson.M{"$set": bson.M{"status": "inactive"}}
|
||||
err = userRepo.BulkUpdate(context.Background(), filter, update)
|
||||
|
||||
// Bulk delete
|
||||
filter := bson.M{"created_at": bson.M{"$lt": time.Now().AddDate(0, -1, 0).Unix()}}
|
||||
err = userRepo.BulkDelete(context.Background(), filter)
|
||||
```
|
||||
|
||||
## Index Management
|
||||
|
||||
### Create Indexes
|
||||
|
||||
```go
|
||||
// Create common indexes
|
||||
indexes := mongo.UserIndexes()
|
||||
err := connManager.CreateIndexes("users", indexes)
|
||||
|
||||
// Create custom indexes
|
||||
customIndexes := []mongo.Index{
|
||||
*mongo.CreateUniqueIndex("email_idx", bson.D{{Key: "email", Value: 1}}),
|
||||
*mongo.CreateTTLIndex("expires_at_idx", "expires_at", 3600),
|
||||
*mongo.CreateTextIndex("search_idx", "title", "description"),
|
||||
}
|
||||
|
||||
err = connManager.CreateIndexes("documents", customIndexes)
|
||||
```
|
||||
|
||||
### Index Types
|
||||
|
||||
```go
|
||||
// Unique index
|
||||
uniqueIdx := mongo.CreateUniqueIndex("email_idx", bson.D{{Key: "email", Value: 1}})
|
||||
|
||||
// TTL index
|
||||
ttlIdx := mongo.CreateTTLIndex("expires_at_idx", "expires_at", 3600)
|
||||
|
||||
// Text index
|
||||
textIdx := mongo.CreateTextIndex("search_idx", "title", "description")
|
||||
|
||||
// Compound index
|
||||
compoundIdx := mongo.CreateCompoundIndex("user_status_idx", map[string]int{
|
||||
"user_id": 1,
|
||||
"status": 1,
|
||||
})
|
||||
|
||||
// Geospatial index
|
||||
geoIdx := mongo.CreateGeospatialIndex("location_idx", "location")
|
||||
|
||||
// Hashed index
|
||||
hashIdx := mongo.CreateHashedIndex("user_id_hash_idx", "user_id")
|
||||
```
|
||||
|
||||
## Transactions
|
||||
|
||||
```go
|
||||
// Start session
|
||||
session, err := connManager.client.StartSession()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer session.EndSession(context.Background())
|
||||
|
||||
// Execute transaction
|
||||
_, err = session.WithTransaction(context.Background(), func(sessCtx mongo.SessionContext) (interface{}, error) {
|
||||
// Create user
|
||||
user := &User{Email: "transaction@example.com"}
|
||||
if err := userRepo.Create(sessCtx, user); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Create related document
|
||||
profile := &Profile{UserID: user.ID}
|
||||
if err := profileRepo.Create(sessCtx, profile); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
})
|
||||
```
|
||||
|
||||
## Connection Configuration
|
||||
|
||||
```go
|
||||
config := mongo.ConnectionConfig{
|
||||
URI: "mongodb://localhost:27017",
|
||||
Database: "your_database",
|
||||
MaxPoolSize: 100,
|
||||
MinPoolSize: 5,
|
||||
MaxConnIdleTime: 30 * time.Minute,
|
||||
ConnectTimeout: 10 * time.Second,
|
||||
ServerSelectionTimeout: 5 * time.Second,
|
||||
}
|
||||
```
|
||||
|
||||
## Logger Interface
|
||||
|
||||
Implement the Logger interface for structured logging:
|
||||
|
||||
```go
|
||||
type YourLogger struct{}
|
||||
|
||||
func (l *YourLogger) Info(message string, fields map[string]interface{}) {
|
||||
log.Printf("[INFO] %s: %+v", message, fields)
|
||||
}
|
||||
|
||||
func (l *YourLogger) Error(message string, fields map[string]interface{}) {
|
||||
log.Printf("[ERROR] %s: %+v", message, fields)
|
||||
}
|
||||
|
||||
func (l *YourLogger) Debug(message string, fields map[string]interface{}) {
|
||||
log.Printf("[DEBUG] %s: %+v", message, fields)
|
||||
}
|
||||
|
||||
func (l *YourLogger) Warn(message string, fields map[string]interface{}) {
|
||||
log.Printf("[WARN] %s: %+v", message, fields)
|
||||
}
|
||||
```
|
||||
|
||||
## Model Interfaces
|
||||
|
||||
### Timestampable Interface
|
||||
|
||||
Implement for automatic timestamp management:
|
||||
|
||||
```go
|
||||
type YourModel struct {
|
||||
Model
|
||||
// Your fields...
|
||||
}
|
||||
|
||||
// Model already implements Timestampable
|
||||
// Timestamps are automatically set on Create/Update operations
|
||||
```
|
||||
|
||||
### Custom ID Management
|
||||
|
||||
```go
|
||||
type CustomModel struct {
|
||||
Model
|
||||
// Your fields...
|
||||
}
|
||||
|
||||
// Model already implements IDGetter and IDSetter
|
||||
// ID is automatically set after Create operations
|
||||
```
|
||||
|
||||
## Performance Best Practices
|
||||
|
||||
### 1. Use Cursor-based Pagination
|
||||
|
||||
Cursor-based pagination is more efficient than offset-based pagination for large datasets:
|
||||
|
||||
```go
|
||||
// Good - Cursor-based
|
||||
pagination := mongo.NewPaginationBuilder().
|
||||
Limit(10).
|
||||
SortDesc("created_at").
|
||||
Build()
|
||||
|
||||
// Avoid - Offset-based for large datasets
|
||||
pagination := mongo.NewPaginationBuilder().
|
||||
Limit(10).
|
||||
Skip(10000). // Expensive for large datasets
|
||||
Build()
|
||||
```
|
||||
|
||||
### 2. Create Appropriate Indexes
|
||||
|
||||
```go
|
||||
// Create indexes for frequently queried fields
|
||||
indexes := []mongo.Index{
|
||||
*mongo.CreateUniqueIndex("email_idx", bson.D{{Key: "email", Value: 1}}),
|
||||
*mongo.NewIndex("status_created_idx", bson.D{
|
||||
{Key: "status", Value: 1},
|
||||
{Key: "created_at", Value: -1},
|
||||
}),
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Use Projection for Large Documents
|
||||
|
||||
```go
|
||||
// Use FindMany with limit for large result sets
|
||||
users, err := userRepo.FindMany(context.Background(), filter, 1000)
|
||||
```
|
||||
|
||||
### 4. Connection Pooling
|
||||
|
||||
Configure appropriate connection pool settings:
|
||||
|
||||
```go
|
||||
config := mongo.ConnectionConfig{
|
||||
MaxPoolSize: 100,
|
||||
MinPoolSize: 5,
|
||||
MaxConnIdleTime: 30 * time.Minute,
|
||||
}
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
The ORM provides specific error types:
|
||||
|
||||
```go
|
||||
var (
|
||||
ErrDocumentNotFound = errors.New("document not found")
|
||||
ErrInvalidCursor = errors.New("invalid cursor")
|
||||
ErrInvalidPagination = errors.New("invalid pagination parameters")
|
||||
)
|
||||
|
||||
// Handle specific errors
|
||||
user, err := userRepo.FindByID(ctx, id)
|
||||
if err != nil {
|
||||
if errors.Is(err, mongo.ErrDocumentNotFound) {
|
||||
// Handle not found
|
||||
return
|
||||
}
|
||||
// Handle other errors
|
||||
}
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
### Unit Testing with Mocks
|
||||
|
||||
```go
|
||||
// Create mock repository for testing
|
||||
type MockUserRepository struct {
|
||||
users map[string]*User
|
||||
}
|
||||
|
||||
func (m *MockUserRepository) FindByID(ctx context.Context, id string) (*User, error) {
|
||||
if user, exists := m.users[id]; exists {
|
||||
return user, nil
|
||||
}
|
||||
return nil, mongo.ErrDocumentNotFound
|
||||
}
|
||||
|
||||
// Use in tests
|
||||
mockRepo := &MockUserRepository{users: make(map[string]*User)}
|
||||
```
|
||||
|
||||
### Integration Testing
|
||||
|
||||
```go
|
||||
func TestUserRepository(t *testing.T) {
|
||||
// Set up test database
|
||||
config := mongo.ConnectionConfig{
|
||||
URI: "mongodb://localhost:27017",
|
||||
Database: "test_database",
|
||||
}
|
||||
|
||||
connManager, err := mongo.NewConnectionManager(config, logger)
|
||||
require.NoError(t, err)
|
||||
defer connManager.Close(context.Background())
|
||||
|
||||
userRepo := mongo.NewRepository[User](connManager.GetCollection("users"), logger)
|
||||
|
||||
// Run tests...
|
||||
}
|
||||
```
|
||||
|
||||
## Migration from Existing Code
|
||||
|
||||
### From Manual MongoDB Operations
|
||||
|
||||
```go
|
||||
// Before
|
||||
collection := client.Database("db").Collection("users")
|
||||
filter := bson.M{"email": email}
|
||||
var user User
|
||||
err := collection.FindOne(ctx, filter).Decode(&user)
|
||||
|
||||
// After
|
||||
userRepo := mongo.NewRepository[User](collection, logger)
|
||||
user, err := userRepo.FindOne(ctx, bson.M{"email": email})
|
||||
```
|
||||
|
||||
### From Other ORMs
|
||||
|
||||
The generic repository pattern makes migration straightforward:
|
||||
|
||||
```go
|
||||
// Replace existing repository interface
|
||||
type UserRepository interface {
|
||||
FindByID(ctx context.Context, id string) (*User, error)
|
||||
Create(ctx context.Context, user *User) error
|
||||
// ... other methods
|
||||
}
|
||||
|
||||
// Implementation remains the same, just use mongo.NewRepository
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
1. Fork the repository
|
||||
2. Create a feature branch
|
||||
3. Add tests for new functionality
|
||||
4. Ensure all tests pass
|
||||
5. Submit a pull request
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the MIT License - see the LICENSE file for details.
|
||||
@@ -0,0 +1,264 @@
|
||||
package mongo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
"go.mongodb.org/mongo-driver/mongo/readpref"
|
||||
)
|
||||
|
||||
// ConnectionConfig holds MongoDB connection configuration
|
||||
type ConnectionConfig struct {
|
||||
URI string `json:"uri"`
|
||||
Database string `json:"database"`
|
||||
MaxPoolSize uint64 `json:"maxPoolSize"`
|
||||
MinPoolSize uint64 `json:"minPoolSize"`
|
||||
MaxConnIdleTime time.Duration `json:"maxConnIdleTime"`
|
||||
ConnectTimeout time.Duration `json:"connectTimeout"`
|
||||
ServerSelectionTimeout time.Duration `json:"serverSelectionTimeout"`
|
||||
}
|
||||
|
||||
// ConnectionManager manages MongoDB connections and collections
|
||||
type ConnectionManager struct {
|
||||
client *mongo.Client
|
||||
database *mongo.Database
|
||||
logger Logger
|
||||
config ConnectionConfig
|
||||
}
|
||||
|
||||
// NewConnectionManager creates a new MongoDB connection manager
|
||||
func NewConnectionManager(config ConnectionConfig, logger Logger) (*ConnectionManager, error) {
|
||||
// Set default values
|
||||
if config.MaxPoolSize == 0 {
|
||||
config.MaxPoolSize = 100
|
||||
}
|
||||
if config.MinPoolSize == 0 {
|
||||
config.MinPoolSize = 5
|
||||
}
|
||||
if config.MaxConnIdleTime == 0 {
|
||||
config.MaxConnIdleTime = 30 * time.Minute
|
||||
}
|
||||
if config.ConnectTimeout == 0 {
|
||||
config.ConnectTimeout = 10 * time.Second
|
||||
}
|
||||
if config.ServerSelectionTimeout == 0 {
|
||||
config.ServerSelectionTimeout = 5 * time.Second
|
||||
}
|
||||
|
||||
// Create client options
|
||||
clientOptions := options.Client().
|
||||
ApplyURI(config.URI).
|
||||
SetMaxPoolSize(config.MaxPoolSize).
|
||||
SetMinPoolSize(config.MinPoolSize).
|
||||
SetMaxConnIdleTime(config.MaxConnIdleTime).
|
||||
SetConnectTimeout(config.ConnectTimeout).
|
||||
SetServerSelectionTimeout(config.ServerSelectionTimeout)
|
||||
|
||||
// Connect to MongoDB
|
||||
ctx, cancel := context.WithTimeout(context.Background(), config.ConnectTimeout)
|
||||
defer cancel()
|
||||
|
||||
client, err := mongo.Connect(ctx, clientOptions)
|
||||
if err != nil {
|
||||
logger.Error("Failed to connect to MongoDB", map[string]interface{}{
|
||||
"uri": config.URI,
|
||||
"error": err.Error(),
|
||||
})
|
||||
return nil, fmt.Errorf("failed to connect to MongoDB: %w", err)
|
||||
}
|
||||
|
||||
// Ping the database
|
||||
if err = client.Ping(ctx, readpref.Primary()); err != nil {
|
||||
logger.Error("Failed to ping MongoDB", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
return nil, fmt.Errorf("failed to ping MongoDB: %w", err)
|
||||
}
|
||||
|
||||
database := client.Database(config.Database)
|
||||
|
||||
logger.Info("Successfully connected to MongoDB", map[string]interface{}{
|
||||
"database": config.Database,
|
||||
"uri": config.URI,
|
||||
})
|
||||
|
||||
return &ConnectionManager{
|
||||
client: client,
|
||||
database: database,
|
||||
logger: logger,
|
||||
config: config,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetCollection returns a MongoDB collection
|
||||
func (cm *ConnectionManager) GetCollection(name string) *mongo.Collection {
|
||||
return cm.database.Collection(name)
|
||||
}
|
||||
|
||||
// CreateRepository creates a new repository for a given collection
|
||||
func (cm *ConnectionManager) CreateRepository(collectionName string) *mongo.Collection {
|
||||
return cm.GetCollection(collectionName)
|
||||
}
|
||||
|
||||
// CreateIndexes creates indexes for a collection
|
||||
func (cm *ConnectionManager) CreateIndexes(collectionName string, indexes []Index) error {
|
||||
collection := cm.GetCollection(collectionName)
|
||||
|
||||
for _, index := range indexes {
|
||||
indexModel := mongo.IndexModel{
|
||||
Keys: index.Keys,
|
||||
Options: index.Options,
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
_, err := collection.Indexes().CreateOne(ctx, indexModel)
|
||||
if err != nil {
|
||||
cm.logger.Error("Failed to create index", map[string]interface{}{
|
||||
"collection": collectionName,
|
||||
"index": index.Name,
|
||||
"error": err.Error(),
|
||||
})
|
||||
return fmt.Errorf("failed to create index %s: %w", index.Name, err)
|
||||
}
|
||||
|
||||
cm.logger.Info("Index created successfully", map[string]interface{}{
|
||||
"collection": collectionName,
|
||||
"index": index.Name,
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DropIndexes drops indexes from a collection
|
||||
func (cm *ConnectionManager) DropIndexes(collectionName string, indexNames []string) error {
|
||||
collection := cm.GetCollection(collectionName)
|
||||
|
||||
for _, indexName := range indexNames {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
_, err := collection.Indexes().DropOne(ctx, indexName)
|
||||
if err != nil {
|
||||
cm.logger.Error("Failed to drop index", map[string]interface{}{
|
||||
"collection": collectionName,
|
||||
"index": indexName,
|
||||
"error": err.Error(),
|
||||
})
|
||||
return fmt.Errorf("failed to drop index %s: %w", indexName, err)
|
||||
}
|
||||
|
||||
cm.logger.Info("Index dropped successfully", map[string]interface{}{
|
||||
"collection": collectionName,
|
||||
"index": indexName,
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListIndexes lists all indexes for a collection
|
||||
func (cm *ConnectionManager) ListIndexes(collectionName string) ([]bson.M, error) {
|
||||
collection := cm.GetCollection(collectionName)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
cursor, err := collection.Indexes().List(ctx)
|
||||
if err != nil {
|
||||
cm.logger.Error("Failed to list indexes", map[string]interface{}{
|
||||
"collection": collectionName,
|
||||
"error": err.Error(),
|
||||
})
|
||||
return nil, fmt.Errorf("failed to list indexes: %w", err)
|
||||
}
|
||||
defer cursor.Close(ctx)
|
||||
|
||||
var indexes []bson.M
|
||||
if err = cursor.All(ctx, &indexes); err != nil {
|
||||
cm.logger.Error("Failed to decode indexes", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
return nil, fmt.Errorf("failed to decode indexes: %w", err)
|
||||
}
|
||||
|
||||
return indexes, nil
|
||||
}
|
||||
|
||||
// Close closes the MongoDB connection
|
||||
func (cm *ConnectionManager) Close(ctx context.Context) error {
|
||||
if err := cm.client.Disconnect(ctx); err != nil {
|
||||
cm.logger.Error("Failed to disconnect from MongoDB", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
return fmt.Errorf("failed to disconnect from MongoDB: %w", err)
|
||||
}
|
||||
|
||||
cm.logger.Info("Successfully disconnected from MongoDB", nil)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Ping pings the MongoDB server
|
||||
func (cm *ConnectionManager) Ping(ctx context.Context) error {
|
||||
if err := cm.client.Ping(ctx, readpref.Primary()); err != nil {
|
||||
cm.logger.Error("Failed to ping MongoDB", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
return fmt.Errorf("failed to ping MongoDB: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetStats returns database statistics
|
||||
func (cm *ConnectionManager) GetStats(ctx context.Context) (bson.M, error) {
|
||||
stats := cm.database.RunCommand(ctx, bson.M{"dbStats": 1})
|
||||
if stats.Err() != nil {
|
||||
cm.logger.Error("Failed to get database stats", map[string]interface{}{
|
||||
"error": stats.Err().Error(),
|
||||
})
|
||||
return nil, fmt.Errorf("failed to get database stats: %w", stats.Err())
|
||||
}
|
||||
|
||||
var result bson.M
|
||||
if err := stats.Decode(&result); err != nil {
|
||||
cm.logger.Error("Failed to decode database stats", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
return nil, fmt.Errorf("failed to decode database stats: %w", err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GetCollectionStats returns collection statistics
|
||||
func (cm *ConnectionManager) GetCollectionStats(ctx context.Context, collectionName string) (bson.M, error) {
|
||||
collection := cm.GetCollection(collectionName)
|
||||
|
||||
stats := collection.Database().RunCommand(ctx, bson.M{
|
||||
"collStats": collectionName,
|
||||
})
|
||||
if stats.Err() != nil {
|
||||
cm.logger.Error("Failed to get collection stats", map[string]interface{}{
|
||||
"collection": collectionName,
|
||||
"error": stats.Err().Error(),
|
||||
})
|
||||
return nil, fmt.Errorf("failed to get collection stats: %w", stats.Err())
|
||||
}
|
||||
|
||||
var result bson.M
|
||||
if err := stats.Decode(&result); err != nil {
|
||||
cm.logger.Error("Failed to decode collection stats", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
return nil, fmt.Errorf("failed to decode collection stats: %w", err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package mongo
|
||||
|
||||
// Logger defines the interface for logging operations
|
||||
type Logger interface {
|
||||
Info(message string, fields map[string]interface{})
|
||||
Error(message string, fields map[string]interface{})
|
||||
Debug(message string, fields map[string]interface{})
|
||||
Warn(message string, fields map[string]interface{})
|
||||
}
|
||||
|
||||
// Timestampable interface for models that support timestamps
|
||||
type Timestampable interface {
|
||||
SetCreatedAt(timestamp int64)
|
||||
SetUpdatedAt(timestamp int64)
|
||||
GetCreatedAt() int64
|
||||
GetUpdatedAt() int64
|
||||
}
|
||||
|
||||
// IDGetter interface for models that can provide their ID
|
||||
type IDGetter interface {
|
||||
GetID() string
|
||||
}
|
||||
|
||||
// IDSetter interface for models that can set their ID
|
||||
type IDSetter interface {
|
||||
SetID(id string)
|
||||
}
|
||||
|
||||
// Model provides a common base for MongoDB documents
|
||||
type Model struct {
|
||||
ID string `bson:"_id,omitempty" json:"id,omitempty"`
|
||||
CreatedAt int64 `bson:"created_at" json:"created_at"`
|
||||
UpdatedAt int64 `bson:"updated_at" json:"updated_at"`
|
||||
}
|
||||
|
||||
// GetID returns the document ID
|
||||
func (b *Model) GetID() string {
|
||||
return b.ID
|
||||
}
|
||||
|
||||
// SetID sets the document ID
|
||||
func (b *Model) SetID(id string) {
|
||||
b.ID = id
|
||||
}
|
||||
|
||||
// SetCreatedAt sets the creation timestamp
|
||||
func (b *Model) SetCreatedAt(timestamp int64) {
|
||||
b.CreatedAt = timestamp
|
||||
}
|
||||
|
||||
// SetUpdatedAt sets the update timestamp
|
||||
func (b *Model) SetUpdatedAt(timestamp int64) {
|
||||
b.UpdatedAt = timestamp
|
||||
}
|
||||
|
||||
// GetCreatedAt returns the creation timestamp
|
||||
func (b *Model) GetCreatedAt() int64 {
|
||||
return b.CreatedAt
|
||||
}
|
||||
|
||||
// GetUpdatedAt returns the update timestamp
|
||||
func (b *Model) GetUpdatedAt() int64 {
|
||||
return b.UpdatedAt
|
||||
}
|
||||
@@ -0,0 +1,593 @@
|
||||
package mongo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
// Common errors
|
||||
var (
|
||||
ErrDocumentNotFound = errors.New("document not found")
|
||||
ErrInvalidCursor = errors.New("invalid cursor")
|
||||
ErrInvalidPagination = errors.New("invalid pagination parameters")
|
||||
)
|
||||
|
||||
// Pagination represents pagination parameters for queries
|
||||
type Pagination struct {
|
||||
Limit int `json:"limit"` // Number of documents to return
|
||||
Skip int `json:"skip"` // Number of documents to skip (for offset-based)
|
||||
Cursor string `json:"cursor"` // Base64 encoded cursor for cursor-based pagination
|
||||
SortField string `json:"sortField"` // Field to sort by (default: "_id")
|
||||
SortOrder int `json:"sortOrder"` // Sort order: 1 for ascending, -1 for descending
|
||||
}
|
||||
|
||||
// PaginatedResult represents a paginated result set
|
||||
type PaginatedResult[T any] struct {
|
||||
Items []T `json:"items"` // The actual documents
|
||||
TotalCount int64 `json:"totalCount"` // Total number of documents matching the filter
|
||||
NextCursor string `json:"nextCursor"` // Base64 encoded cursor for next page
|
||||
HasMore bool `json:"hasMore"` // Whether there are more documents
|
||||
}
|
||||
|
||||
// Repository defines the interface for MongoDB operations
|
||||
type Repository[T any] interface {
|
||||
// FindByID retrieves a document by its ID
|
||||
FindByID(ctx context.Context, id string) (*T, error)
|
||||
|
||||
// FindAll retrieves documents with optional filtering and pagination
|
||||
FindAll(ctx context.Context, filter bson.M, pagination Pagination) (*PaginatedResult[T], error)
|
||||
|
||||
// Create inserts a new document
|
||||
Create(ctx context.Context, model *T) error
|
||||
|
||||
// Update updates an existing document
|
||||
Update(ctx context.Context, model *T) error
|
||||
|
||||
// Delete removes a document by ID
|
||||
Delete(ctx context.Context, id string) error
|
||||
|
||||
// Count returns the number of documents matching the filter
|
||||
Count(ctx context.Context, filter bson.M) (int64, error)
|
||||
|
||||
// Aggregate executes an aggregation pipeline
|
||||
Aggregate(ctx context.Context, pipeline mongo.Pipeline) ([]bson.M, error)
|
||||
|
||||
// FindOne retrieves a single document matching the filter
|
||||
FindOne(ctx context.Context, filter bson.M) (*T, error)
|
||||
|
||||
// FindMany retrieves multiple documents without pagination
|
||||
FindMany(ctx context.Context, filter bson.M, limit int) ([]T, error)
|
||||
|
||||
// BulkCreate inserts multiple documents
|
||||
BulkCreate(ctx context.Context, models []T) error
|
||||
|
||||
// BulkUpdate updates multiple documents
|
||||
BulkUpdate(ctx context.Context, filter bson.M, update bson.M) error
|
||||
|
||||
// BulkDelete removes multiple documents
|
||||
BulkDelete(ctx context.Context, filter bson.M) error
|
||||
}
|
||||
|
||||
// repository implements the Repository interface
|
||||
type repository[T any] struct {
|
||||
collection *mongo.Collection
|
||||
logger Logger
|
||||
}
|
||||
|
||||
// NewRepository creates a new repository instance
|
||||
func NewRepository[T any](collection *mongo.Collection, logger Logger) Repository[T] {
|
||||
return &repository[T]{
|
||||
collection: collection,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// FindByID retrieves a document by its ID
|
||||
func (r *repository[T]) FindByID(ctx context.Context, id string) (*T, error) {
|
||||
objectID, err := primitive.ObjectIDFromHex(id)
|
||||
if err != nil {
|
||||
r.logger.Error("Invalid ObjectID format", map[string]interface{}{
|
||||
"id": id,
|
||||
"error": err.Error(),
|
||||
})
|
||||
return nil, fmt.Errorf("invalid id format: %w", err)
|
||||
}
|
||||
|
||||
filter := bson.M{"_id": objectID}
|
||||
var result T
|
||||
|
||||
err = r.collection.FindOne(ctx, filter).Decode(&result)
|
||||
if err != nil {
|
||||
if errors.Is(err, mongo.ErrNoDocuments) {
|
||||
return nil, ErrDocumentNotFound
|
||||
}
|
||||
r.logger.Error("Failed to find document by ID", map[string]interface{}{
|
||||
"id": id,
|
||||
"error": err.Error(),
|
||||
})
|
||||
return nil, fmt.Errorf("failed to find document: %w", err)
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// FindAll retrieves documents with optional filtering and pagination
|
||||
func (r *repository[T]) FindAll(ctx context.Context, filter bson.M, pagination Pagination) (*PaginatedResult[T], error) {
|
||||
// Validate pagination parameters
|
||||
if err := r.validatePagination(pagination); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Set default values
|
||||
if pagination.Limit <= 0 {
|
||||
pagination.Limit = 10
|
||||
}
|
||||
if pagination.SortField == "" {
|
||||
pagination.SortField = "_id"
|
||||
}
|
||||
if pagination.SortOrder == 0 {
|
||||
pagination.SortOrder = -1 // Default to descending
|
||||
}
|
||||
|
||||
// Build sort options
|
||||
sort := bson.D{{Key: pagination.SortField, Value: pagination.SortOrder}}
|
||||
|
||||
// Build find options
|
||||
findOptions := options.Find().
|
||||
SetSort(sort).
|
||||
SetLimit(int64(pagination.Limit + 1)) // +1 to check if there are more documents
|
||||
|
||||
// Handle cursor-based pagination
|
||||
if pagination.Cursor != "" {
|
||||
cursorFilter, err := r.buildCursorFilter(pagination.Cursor, pagination.SortField, pagination.SortOrder)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Merge cursor filter with existing filter
|
||||
for key, value := range cursorFilter {
|
||||
filter[key] = value
|
||||
}
|
||||
} else if pagination.Skip > 0 {
|
||||
// Use offset-based pagination
|
||||
findOptions.SetSkip(int64(pagination.Skip))
|
||||
}
|
||||
|
||||
// Execute query
|
||||
cursor, err := r.collection.Find(ctx, filter, findOptions)
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to execute find query", map[string]interface{}{
|
||||
"filter": filter,
|
||||
"pagination": pagination,
|
||||
"error": err.Error(),
|
||||
})
|
||||
return nil, fmt.Errorf("failed to execute query: %w", err)
|
||||
}
|
||||
defer cursor.Close(ctx)
|
||||
|
||||
// Decode results
|
||||
var items []T
|
||||
if err = cursor.All(ctx, &items); err != nil {
|
||||
r.logger.Error("Failed to decode query results", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
return nil, fmt.Errorf("failed to decode results: %w", err)
|
||||
}
|
||||
|
||||
// Check if there are more documents
|
||||
hasMore := false
|
||||
if len(items) > pagination.Limit {
|
||||
hasMore = true
|
||||
items = items[:pagination.Limit] // Remove the extra item
|
||||
}
|
||||
|
||||
// Get total count
|
||||
totalCount, err := r.Count(ctx, filter)
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to get total count", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
// Don't fail the entire query, just set count to 0
|
||||
totalCount = 0
|
||||
}
|
||||
|
||||
// Generate next cursor
|
||||
var nextCursor string
|
||||
if hasMore && len(items) > 0 {
|
||||
nextCursor, err = r.generateCursor(items[len(items)-1], pagination.SortField)
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to generate next cursor", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
// Don't fail the entire query, just leave cursor empty
|
||||
}
|
||||
}
|
||||
|
||||
return &PaginatedResult[T]{
|
||||
Items: items,
|
||||
TotalCount: totalCount,
|
||||
NextCursor: nextCursor,
|
||||
HasMore: hasMore,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Create inserts a new document
|
||||
func (r *repository[T]) Create(ctx context.Context, model *T) error {
|
||||
// Set timestamps if the model implements Timestampable
|
||||
if timestampable, ok := any(model).(Timestampable); ok {
|
||||
now := time.Now().Unix()
|
||||
timestampable.SetCreatedAt(now)
|
||||
timestampable.SetUpdatedAt(now)
|
||||
}
|
||||
|
||||
result, err := r.collection.InsertOne(ctx, model)
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to create document", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
return fmt.Errorf("failed to create document: %w", err)
|
||||
}
|
||||
|
||||
// Set the ID if the model implements IDSetter
|
||||
if idSetter, ok := any(model).(IDSetter); ok {
|
||||
if oid, ok := result.InsertedID.(primitive.ObjectID); ok {
|
||||
idSetter.SetID(oid.Hex())
|
||||
}
|
||||
}
|
||||
|
||||
r.logger.Info("Document created successfully", map[string]interface{}{
|
||||
"id": result.InsertedID,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Update updates an existing document
|
||||
func (r *repository[T]) Update(ctx context.Context, model *T) error {
|
||||
// Set updated timestamp if the model implements Timestampable
|
||||
if timestampable, ok := any(model).(Timestampable); ok {
|
||||
timestampable.SetUpdatedAt(time.Now().Unix())
|
||||
}
|
||||
|
||||
// Get the ID from the model
|
||||
id, err := r.getModelID(model)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
objectID, err := primitive.ObjectIDFromHex(id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid id format: %w", err)
|
||||
}
|
||||
|
||||
filter := bson.M{"_id": objectID}
|
||||
update := bson.M{"$set": model}
|
||||
|
||||
result, err := r.collection.UpdateOne(ctx, filter, update)
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to update document", map[string]interface{}{
|
||||
"id": id,
|
||||
"error": err.Error(),
|
||||
})
|
||||
return fmt.Errorf("failed to update document: %w", err)
|
||||
}
|
||||
|
||||
if result.MatchedCount == 0 {
|
||||
return ErrDocumentNotFound
|
||||
}
|
||||
|
||||
r.logger.Info("Document updated successfully", map[string]interface{}{
|
||||
"id": id,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete removes a document by ID
|
||||
func (r *repository[T]) Delete(ctx context.Context, id string) error {
|
||||
objectID, err := primitive.ObjectIDFromHex(id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid id format: %w", err)
|
||||
}
|
||||
|
||||
filter := bson.M{"_id": objectID}
|
||||
result, err := r.collection.DeleteOne(ctx, filter)
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to delete document", map[string]interface{}{
|
||||
"id": id,
|
||||
"error": err.Error(),
|
||||
})
|
||||
return fmt.Errorf("failed to delete document: %w", err)
|
||||
}
|
||||
|
||||
if result.DeletedCount == 0 {
|
||||
return ErrDocumentNotFound
|
||||
}
|
||||
|
||||
r.logger.Info("Document deleted successfully", map[string]interface{}{
|
||||
"id": id,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Count returns the number of documents matching the filter
|
||||
func (r *repository[T]) Count(ctx context.Context, filter bson.M) (int64, error) {
|
||||
count, err := r.collection.CountDocuments(ctx, filter)
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to count documents", map[string]interface{}{
|
||||
"filter": filter,
|
||||
"error": err.Error(),
|
||||
})
|
||||
return 0, fmt.Errorf("failed to count documents: %w", err)
|
||||
}
|
||||
|
||||
return count, nil
|
||||
}
|
||||
|
||||
// Aggregate executes an aggregation pipeline
|
||||
func (r *repository[T]) Aggregate(ctx context.Context, pipeline mongo.Pipeline) ([]bson.M, error) {
|
||||
cursor, err := r.collection.Aggregate(ctx, pipeline)
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to execute aggregation", map[string]interface{}{
|
||||
"pipeline": pipeline,
|
||||
"error": err.Error(),
|
||||
})
|
||||
return nil, fmt.Errorf("failed to execute aggregation: %w", err)
|
||||
}
|
||||
defer cursor.Close(ctx)
|
||||
|
||||
var results []bson.M
|
||||
if err = cursor.All(ctx, &results); err != nil {
|
||||
r.logger.Error("Failed to decode aggregation results", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
return nil, fmt.Errorf("failed to decode aggregation results: %w", err)
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// FindOne retrieves a single document matching the filter
|
||||
func (r *repository[T]) FindOne(ctx context.Context, filter bson.M) (*T, error) {
|
||||
var result T
|
||||
|
||||
err := r.collection.FindOne(ctx, filter).Decode(&result)
|
||||
if err != nil {
|
||||
if errors.Is(err, mongo.ErrNoDocuments) {
|
||||
return nil, ErrDocumentNotFound
|
||||
}
|
||||
r.logger.Error("Failed to find document", map[string]interface{}{
|
||||
"filter": filter,
|
||||
"error": err.Error(),
|
||||
})
|
||||
return nil, fmt.Errorf("failed to find document: %w", err)
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// FindMany retrieves multiple documents without pagination
|
||||
func (r *repository[T]) FindMany(ctx context.Context, filter bson.M, limit int) ([]T, error) {
|
||||
findOptions := options.Find()
|
||||
if limit > 0 {
|
||||
findOptions.SetLimit(int64(limit))
|
||||
}
|
||||
|
||||
cursor, err := r.collection.Find(ctx, filter, findOptions)
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to execute find query", map[string]interface{}{
|
||||
"filter": filter,
|
||||
"limit": limit,
|
||||
"error": err.Error(),
|
||||
})
|
||||
return nil, fmt.Errorf("failed to execute query: %w", err)
|
||||
}
|
||||
defer cursor.Close(ctx)
|
||||
|
||||
var results []T
|
||||
if err = cursor.All(ctx, &results); err != nil {
|
||||
r.logger.Error("Failed to decode query results", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
return nil, fmt.Errorf("failed to decode results: %w", err)
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// BulkCreate inserts multiple documents
|
||||
func (r *repository[T]) BulkCreate(ctx context.Context, models []T) error {
|
||||
if len(models) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Set timestamps for all models
|
||||
now := time.Now().Unix()
|
||||
for i := range models {
|
||||
if timestampable, ok := any(&models[i]).(Timestampable); ok {
|
||||
timestampable.SetCreatedAt(now)
|
||||
timestampable.SetUpdatedAt(now)
|
||||
}
|
||||
}
|
||||
|
||||
// Convert to interface slice for bulk insert
|
||||
documents := make([]interface{}, len(models))
|
||||
for i, model := range models {
|
||||
documents[i] = model
|
||||
}
|
||||
|
||||
result, err := r.collection.InsertMany(ctx, documents)
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to bulk create documents", map[string]interface{}{
|
||||
"count": len(models),
|
||||
"error": err.Error(),
|
||||
})
|
||||
return fmt.Errorf("failed to bulk create documents: %w", err)
|
||||
}
|
||||
|
||||
r.logger.Info("Documents bulk created successfully", map[string]interface{}{
|
||||
"count": len(result.InsertedIDs),
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// BulkUpdate updates multiple documents
|
||||
func (r *repository[T]) BulkUpdate(ctx context.Context, filter bson.M, update bson.M) error {
|
||||
result, err := r.collection.UpdateMany(ctx, filter, update)
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to bulk update documents", map[string]interface{}{
|
||||
"filter": filter,
|
||||
"update": update,
|
||||
"error": err.Error(),
|
||||
})
|
||||
return fmt.Errorf("failed to bulk update documents: %w", err)
|
||||
}
|
||||
|
||||
r.logger.Info("Documents bulk updated successfully", map[string]interface{}{
|
||||
"matched": result.MatchedCount,
|
||||
"modified": result.ModifiedCount,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// BulkDelete removes multiple documents
|
||||
func (r *repository[T]) BulkDelete(ctx context.Context, filter bson.M) error {
|
||||
result, err := r.collection.DeleteMany(ctx, filter)
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to bulk delete documents", map[string]interface{}{
|
||||
"filter": filter,
|
||||
"error": err.Error(),
|
||||
})
|
||||
return fmt.Errorf("failed to bulk delete documents: %w", err)
|
||||
}
|
||||
|
||||
r.logger.Info("Documents bulk deleted successfully", map[string]interface{}{
|
||||
"deleted": result.DeletedCount,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Helper methods
|
||||
|
||||
// validatePagination validates pagination parameters
|
||||
func (r *repository[T]) validatePagination(pagination Pagination) error {
|
||||
if pagination.Limit < 0 {
|
||||
return fmt.Errorf("%w: limit cannot be negative", ErrInvalidPagination)
|
||||
}
|
||||
if pagination.Skip < 0 {
|
||||
return fmt.Errorf("%w: skip cannot be negative", ErrInvalidPagination)
|
||||
}
|
||||
if pagination.SortOrder != 0 && pagination.SortOrder != 1 && pagination.SortOrder != -1 {
|
||||
return fmt.Errorf("%w: sort order must be 1, -1, or 0", ErrInvalidPagination)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// buildCursorFilter builds a filter for cursor-based pagination
|
||||
func (r *repository[T]) buildCursorFilter(cursor, sortField string, sortOrder int) (bson.M, error) {
|
||||
// Decode cursor
|
||||
cursorBytes, err := base64.StdEncoding.DecodeString(cursor)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: invalid cursor encoding", ErrInvalidCursor)
|
||||
}
|
||||
|
||||
var cursorValue interface{}
|
||||
|
||||
// Try to parse as ObjectID first
|
||||
if sortField == "_id" {
|
||||
if objectID, err := primitive.ObjectIDFromHex(string(cursorBytes)); err == nil {
|
||||
cursorValue = objectID
|
||||
} else {
|
||||
return nil, fmt.Errorf("%w: invalid ObjectID in cursor", ErrInvalidCursor)
|
||||
}
|
||||
} else {
|
||||
// For other fields, try to parse as appropriate type
|
||||
// This is a simplified implementation - you might want to make this more sophisticated
|
||||
cursorValue = string(cursorBytes)
|
||||
}
|
||||
|
||||
// Build comparison operator based on sort order
|
||||
var operator string
|
||||
if sortOrder == 1 {
|
||||
operator = "$gt"
|
||||
} else {
|
||||
operator = "$lt"
|
||||
}
|
||||
|
||||
return bson.M{sortField: bson.M{operator: cursorValue}}, nil
|
||||
}
|
||||
|
||||
// generateCursor generates a cursor from a document
|
||||
func (r *repository[T]) generateCursor(doc T, sortField string) (string, error) {
|
||||
// Convert document to BSON
|
||||
docBytes, err := bson.Marshal(doc)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to marshal document: %w", err)
|
||||
}
|
||||
|
||||
var docMap bson.M
|
||||
if err = bson.Unmarshal(docBytes, &docMap); err != nil {
|
||||
return "", fmt.Errorf("failed to unmarshal document: %w", err)
|
||||
}
|
||||
|
||||
// Get the sort field value
|
||||
sortValue, exists := docMap[sortField]
|
||||
if !exists {
|
||||
return "", fmt.Errorf("sort field %s not found in document", sortField)
|
||||
}
|
||||
|
||||
// Convert to string for encoding
|
||||
var cursorStr string
|
||||
switch v := sortValue.(type) {
|
||||
case primitive.ObjectID:
|
||||
cursorStr = v.Hex()
|
||||
case string:
|
||||
cursorStr = v
|
||||
case int64:
|
||||
cursorStr = fmt.Sprintf("%d", v)
|
||||
case float64:
|
||||
cursorStr = fmt.Sprintf("%f", v)
|
||||
default:
|
||||
cursorStr = fmt.Sprintf("%v", v)
|
||||
}
|
||||
|
||||
// Encode as base64
|
||||
return base64.StdEncoding.EncodeToString([]byte(cursorStr)), nil
|
||||
}
|
||||
|
||||
// getModelID extracts the ID from a model
|
||||
func (r *repository[T]) getModelID(model *T) (string, error) {
|
||||
if idGetter, ok := any(model).(IDGetter); ok {
|
||||
return idGetter.GetID(), nil
|
||||
}
|
||||
|
||||
// Fallback: try to get ID from BSON
|
||||
docBytes, err := bson.Marshal(model)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to marshal model: %w", err)
|
||||
}
|
||||
|
||||
var docMap bson.M
|
||||
if err = bson.Unmarshal(docBytes, &docMap); err != nil {
|
||||
return "", fmt.Errorf("failed to unmarshal model: %w", err)
|
||||
}
|
||||
|
||||
if id, exists := docMap["_id"]; exists {
|
||||
if oid, ok := id.(primitive.ObjectID); ok {
|
||||
return oid.Hex(), nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", errors.New("model does not have an ID field")
|
||||
}
|
||||
@@ -0,0 +1,538 @@
|
||||
package mongo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
)
|
||||
|
||||
// TestUser represents a test user model
|
||||
type TestUser struct {
|
||||
Model
|
||||
Email string `bson:"email" json:"email"`
|
||||
FirstName string `bson:"first_name" json:"first_name"`
|
||||
LastName string `bson:"last_name" json:"last_name"`
|
||||
Age int `bson:"age" json:"age"`
|
||||
IsActive bool `bson:"is_active" json:"is_active"`
|
||||
}
|
||||
|
||||
// TestLogger implements Logger for testing
|
||||
type TestLogger struct{}
|
||||
|
||||
func (l *TestLogger) Info(message string, fields map[string]interface{}) {}
|
||||
func (l *TestLogger) Error(message string, fields map[string]interface{}) {}
|
||||
func (l *TestLogger) Debug(message string, fields map[string]interface{}) {}
|
||||
func (l *TestLogger) Warn(message string, fields map[string]interface{}) {}
|
||||
|
||||
// setupTestConnection creates a test connection manager
|
||||
func setupTestConnection(t *testing.T) *ConnectionManager {
|
||||
config := ConnectionConfig{
|
||||
URI: "mongodb://localhost:27017",
|
||||
Database: "test_database",
|
||||
}
|
||||
|
||||
logger := &TestLogger{}
|
||||
connManager, err := NewConnectionManager(config, logger)
|
||||
require.NoError(t, err)
|
||||
|
||||
return connManager
|
||||
}
|
||||
|
||||
// cleanupTestData cleans up test data
|
||||
func cleanupTestData(t *testing.T, connManager *ConnectionManager, collectionName string) {
|
||||
collection := connManager.GetCollection(collectionName)
|
||||
_, err := collection.DeleteMany(context.Background(), bson.M{})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestRepository_Create(t *testing.T) {
|
||||
connManager := setupTestConnection(t)
|
||||
defer connManager.Close(context.Background())
|
||||
defer cleanupTestData(t, connManager, "test_users")
|
||||
|
||||
repo := NewRepository[TestUser](connManager.GetCollection("test_users"), &TestLogger{})
|
||||
|
||||
user := &TestUser{
|
||||
Email: "test@example.com",
|
||||
FirstName: "Test",
|
||||
LastName: "User",
|
||||
Age: 25,
|
||||
IsActive: true,
|
||||
}
|
||||
|
||||
err := repo.Create(context.Background(), user)
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, user.ID)
|
||||
assert.NotZero(t, user.CreatedAt)
|
||||
assert.NotZero(t, user.UpdatedAt)
|
||||
}
|
||||
|
||||
func TestRepository_FindByID(t *testing.T) {
|
||||
connManager := setupTestConnection(t)
|
||||
defer connManager.Close(context.Background())
|
||||
defer cleanupTestData(t, connManager, "test_users")
|
||||
|
||||
repo := NewRepository[TestUser](connManager.GetCollection("test_users"), &TestLogger{})
|
||||
|
||||
// Create a user
|
||||
user := &TestUser{
|
||||
Email: "test@example.com",
|
||||
FirstName: "Test",
|
||||
LastName: "User",
|
||||
Age: 25,
|
||||
IsActive: true,
|
||||
}
|
||||
|
||||
err := repo.Create(context.Background(), user)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Find by ID
|
||||
foundUser, err := repo.FindByID(context.Background(), user.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, user.Email, foundUser.Email)
|
||||
assert.Equal(t, user.FirstName, foundUser.FirstName)
|
||||
|
||||
// Test not found
|
||||
_, err = repo.FindByID(context.Background(), "507f1f77bcf86cd799439011")
|
||||
assert.ErrorIs(t, err, ErrDocumentNotFound)
|
||||
}
|
||||
|
||||
func TestRepository_FindOne(t *testing.T) {
|
||||
connManager := setupTestConnection(t)
|
||||
defer connManager.Close(context.Background())
|
||||
defer cleanupTestData(t, connManager, "test_users")
|
||||
|
||||
repo := NewRepository[TestUser](connManager.GetCollection("test_users"), &TestLogger{})
|
||||
|
||||
// Create a user
|
||||
user := &TestUser{
|
||||
Email: "test@example.com",
|
||||
FirstName: "Test",
|
||||
LastName: "User",
|
||||
Age: 25,
|
||||
IsActive: true,
|
||||
}
|
||||
|
||||
err := repo.Create(context.Background(), user)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Find by email
|
||||
foundUser, err := repo.FindOne(context.Background(), bson.M{"email": "test@example.com"})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, user.Email, foundUser.Email)
|
||||
|
||||
// Test not found
|
||||
_, err = repo.FindOne(context.Background(), bson.M{"email": "nonexistent@example.com"})
|
||||
assert.ErrorIs(t, err, ErrDocumentNotFound)
|
||||
}
|
||||
|
||||
func TestRepository_Update(t *testing.T) {
|
||||
connManager := setupTestConnection(t)
|
||||
defer connManager.Close(context.Background())
|
||||
defer cleanupTestData(t, connManager, "test_users")
|
||||
|
||||
repo := NewRepository[TestUser](connManager.GetCollection("test_users"), &TestLogger{})
|
||||
|
||||
// Create a user
|
||||
user := &TestUser{
|
||||
Email: "test@example.com",
|
||||
FirstName: "Test",
|
||||
LastName: "User",
|
||||
Age: 25,
|
||||
IsActive: true,
|
||||
}
|
||||
|
||||
err := repo.Create(context.Background(), user)
|
||||
require.NoError(t, err)
|
||||
|
||||
originalUpdatedAt := user.UpdatedAt
|
||||
|
||||
// Update user
|
||||
user.FirstName = "Updated"
|
||||
user.Age = 30
|
||||
time.Sleep(1 * time.Second) // Ensure timestamp difference
|
||||
|
||||
err = repo.Update(context.Background(), user)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify update
|
||||
foundUser, err := repo.FindByID(context.Background(), user.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "Updated", foundUser.FirstName)
|
||||
assert.Equal(t, 30, foundUser.Age)
|
||||
assert.Greater(t, foundUser.UpdatedAt, originalUpdatedAt)
|
||||
}
|
||||
|
||||
func TestRepository_Delete(t *testing.T) {
|
||||
connManager := setupTestConnection(t)
|
||||
defer connManager.Close(context.Background())
|
||||
defer cleanupTestData(t, connManager, "test_users")
|
||||
|
||||
repo := NewRepository[TestUser](connManager.GetCollection("test_users"), &TestLogger{})
|
||||
|
||||
// Create a user
|
||||
user := &TestUser{
|
||||
Email: "test@example.com",
|
||||
FirstName: "Test",
|
||||
LastName: "User",
|
||||
Age: 25,
|
||||
IsActive: true,
|
||||
}
|
||||
|
||||
err := repo.Create(context.Background(), user)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Delete user
|
||||
err = repo.Delete(context.Background(), user.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify deletion
|
||||
_, err = repo.FindByID(context.Background(), user.ID)
|
||||
assert.ErrorIs(t, err, ErrDocumentNotFound)
|
||||
}
|
||||
|
||||
func TestRepository_FindAll_OffsetPagination(t *testing.T) {
|
||||
connManager := setupTestConnection(t)
|
||||
defer connManager.Close(context.Background())
|
||||
defer cleanupTestData(t, connManager, "test_users")
|
||||
|
||||
repo := NewRepository[TestUser](connManager.GetCollection("test_users"), &TestLogger{})
|
||||
|
||||
// Create multiple users
|
||||
users := []TestUser{
|
||||
{Email: "user1@example.com", FirstName: "User1", Age: 25, IsActive: true},
|
||||
{Email: "user2@example.com", FirstName: "User2", Age: 30, IsActive: true},
|
||||
{Email: "user3@example.com", FirstName: "User3", Age: 35, IsActive: false},
|
||||
{Email: "user4@example.com", FirstName: "User4", Age: 40, IsActive: true},
|
||||
{Email: "user5@example.com", FirstName: "User5", Age: 45, IsActive: true},
|
||||
}
|
||||
|
||||
err := repo.BulkCreate(context.Background(), users)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Test offset-based pagination
|
||||
pagination := NewPaginationBuilder().
|
||||
Limit(2).
|
||||
Skip(1).
|
||||
SortAsc("created_at").
|
||||
Build()
|
||||
|
||||
results, err := repo.FindAll(context.Background(), bson.M{}, pagination)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Len(t, results.Items, 2)
|
||||
assert.Equal(t, int64(5), results.TotalCount)
|
||||
assert.False(t, results.HasMore) // Should be false since we're using offset
|
||||
}
|
||||
|
||||
func TestRepository_FindAll_CursorPagination(t *testing.T) {
|
||||
connManager := setupTestConnection(t)
|
||||
defer connManager.Close(context.Background())
|
||||
defer cleanupTestData(t, connManager, "test_users")
|
||||
|
||||
repo := NewRepository[TestUser](connManager.GetCollection("test_users"), &TestLogger{})
|
||||
|
||||
// Create multiple users
|
||||
users := []TestUser{
|
||||
{Email: "user1@example.com", FirstName: "User1", Age: 25, IsActive: true},
|
||||
{Email: "user2@example.com", FirstName: "User2", Age: 30, IsActive: true},
|
||||
{Email: "user3@example.com", FirstName: "User3", Age: 35, IsActive: false},
|
||||
{Email: "user4@example.com", FirstName: "User4", Age: 40, IsActive: true},
|
||||
{Email: "user5@example.com", FirstName: "User5", Age: 45, IsActive: true},
|
||||
}
|
||||
|
||||
err := repo.BulkCreate(context.Background(), users)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Test cursor-based pagination
|
||||
pagination := NewPaginationBuilder().
|
||||
Limit(2).
|
||||
SortDesc("created_at").
|
||||
Build()
|
||||
|
||||
firstPage, err := repo.FindAll(context.Background(), bson.M{}, pagination)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Len(t, firstPage.Items, 2)
|
||||
assert.Equal(t, int64(5), firstPage.TotalCount)
|
||||
assert.True(t, firstPage.HasMore)
|
||||
assert.NotEmpty(t, firstPage.NextCursor)
|
||||
|
||||
// Get second page
|
||||
secondPagePagination := NewPaginationBuilder().
|
||||
Limit(2).
|
||||
SortDesc("created_at").
|
||||
Cursor(firstPage.NextCursor).
|
||||
Build()
|
||||
|
||||
secondPage, err := repo.FindAll(context.Background(), bson.M{}, secondPagePagination)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Len(t, secondPage.Items, 2)
|
||||
assert.Equal(t, int64(5), secondPage.TotalCount)
|
||||
assert.True(t, secondPage.HasMore)
|
||||
assert.NotEmpty(t, secondPage.NextCursor)
|
||||
}
|
||||
|
||||
func TestRepository_Count(t *testing.T) {
|
||||
connManager := setupTestConnection(t)
|
||||
defer connManager.Close(context.Background())
|
||||
defer cleanupTestData(t, connManager, "test_users")
|
||||
|
||||
repo := NewRepository[TestUser](connManager.GetCollection("test_users"), &TestLogger{})
|
||||
|
||||
// Create users
|
||||
users := []TestUser{
|
||||
{Email: "user1@example.com", FirstName: "User1", Age: 25, IsActive: true},
|
||||
{Email: "user2@example.com", FirstName: "User2", Age: 30, IsActive: true},
|
||||
{Email: "user3@example.com", FirstName: "User3", Age: 35, IsActive: false},
|
||||
}
|
||||
|
||||
err := repo.BulkCreate(context.Background(), users)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Count all users
|
||||
totalCount, err := repo.Count(context.Background(), bson.M{})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(3), totalCount)
|
||||
|
||||
// Count active users
|
||||
activeCount, err := repo.Count(context.Background(), bson.M{"is_active": true})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(2), activeCount)
|
||||
}
|
||||
|
||||
func TestRepository_BulkOperations(t *testing.T) {
|
||||
connManager := setupTestConnection(t)
|
||||
defer connManager.Close(context.Background())
|
||||
defer cleanupTestData(t, connManager, "test_users")
|
||||
|
||||
repo := NewRepository[TestUser](connManager.GetCollection("test_users"), &TestLogger{})
|
||||
|
||||
// Test bulk create
|
||||
users := []TestUser{
|
||||
{Email: "user1@example.com", FirstName: "User1", Age: 25, IsActive: true},
|
||||
{Email: "user2@example.com", FirstName: "User2", Age: 30, IsActive: true},
|
||||
{Email: "user3@example.com", FirstName: "User3", Age: 35, IsActive: false},
|
||||
}
|
||||
|
||||
err := repo.BulkCreate(context.Background(), users)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify bulk create
|
||||
count, err := repo.Count(context.Background(), bson.M{})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(3), count)
|
||||
|
||||
// Test bulk update
|
||||
updateFilter := bson.M{"is_active": false}
|
||||
update := bson.M{"$set": bson.M{"age": 50}}
|
||||
|
||||
err = repo.BulkUpdate(context.Background(), updateFilter, update)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify bulk update
|
||||
updatedUser, err := repo.FindOne(context.Background(), bson.M{"email": "user3@example.com"})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 50, updatedUser.Age)
|
||||
|
||||
// Test bulk delete
|
||||
deleteFilter := bson.M{"is_active": false}
|
||||
err = repo.BulkDelete(context.Background(), deleteFilter)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify bulk delete
|
||||
count, err = repo.Count(context.Background(), bson.M{})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(2), count)
|
||||
}
|
||||
|
||||
func TestRepository_Aggregate(t *testing.T) {
|
||||
connManager := setupTestConnection(t)
|
||||
defer connManager.Close(context.Background())
|
||||
defer cleanupTestData(t, connManager, "test_users")
|
||||
|
||||
repo := NewRepository[TestUser](connManager.GetCollection("test_users"), &TestLogger{})
|
||||
|
||||
// Create users
|
||||
users := []TestUser{
|
||||
{Email: "user1@example.com", FirstName: "User1", Age: 25, IsActive: true},
|
||||
{Email: "user2@example.com", FirstName: "User2", Age: 30, IsActive: true},
|
||||
{Email: "user3@example.com", FirstName: "User3", Age: 35, IsActive: false},
|
||||
}
|
||||
|
||||
err := repo.BulkCreate(context.Background(), users)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Test aggregation
|
||||
pipeline := mongo.Pipeline{
|
||||
{{Key: "$match", Value: bson.M{"is_active": true}}},
|
||||
{{Key: "$group", Value: bson.M{
|
||||
"_id": nil,
|
||||
"count": bson.M{"$sum": 1},
|
||||
"avgAge": bson.M{"$avg": "$age"},
|
||||
}}},
|
||||
}
|
||||
|
||||
results, err := repo.Aggregate(context.Background(), pipeline)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, results, 1)
|
||||
|
||||
result := results[0]
|
||||
assert.Equal(t, int64(2), result["count"])
|
||||
assert.Equal(t, 27.5, result["avgAge"])
|
||||
}
|
||||
|
||||
func TestRepository_QueryBuilder(t *testing.T) {
|
||||
connManager := setupTestConnection(t)
|
||||
defer connManager.Close(context.Background())
|
||||
defer cleanupTestData(t, connManager, "test_users")
|
||||
|
||||
repo := NewRepository[TestUser](connManager.GetCollection("test_users"), &TestLogger{})
|
||||
|
||||
// Create users
|
||||
users := []TestUser{
|
||||
{Email: "user1@example.com", FirstName: "User1", Age: 25, IsActive: true},
|
||||
{Email: "user2@example.com", FirstName: "User2", Age: 30, IsActive: true},
|
||||
{Email: "user3@example.com", FirstName: "User3", Age: 35, IsActive: false},
|
||||
}
|
||||
|
||||
err := repo.BulkCreate(context.Background(), users)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Test QueryBuilder
|
||||
query := NewQueryBuilder().
|
||||
Where("is_active", true).
|
||||
WhereGreaterThan("age", 20).
|
||||
WhereLessThan("age", 35).
|
||||
Build()
|
||||
|
||||
results, err := repo.FindMany(context.Background(), query, 10)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, results, 2)
|
||||
|
||||
// Test complex query with AND/OR
|
||||
complexQuery := NewQueryBuilder().
|
||||
WhereAnd(
|
||||
bson.M{"is_active": true},
|
||||
bson.M{"age": bson.M{"$gte": 25}},
|
||||
).
|
||||
WhereOr(
|
||||
bson.M{"email": "user1@example.com"},
|
||||
bson.M{"email": "user2@example.com"},
|
||||
).
|
||||
Build()
|
||||
|
||||
complexResults, err := repo.FindMany(context.Background(), complexQuery, 10)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, complexResults, 2)
|
||||
}
|
||||
|
||||
func TestRepository_Validation(t *testing.T) {
|
||||
connManager := setupTestConnection(t)
|
||||
defer connManager.Close(context.Background())
|
||||
defer cleanupTestData(t, connManager, "test_users")
|
||||
|
||||
repo := NewRepository[TestUser](connManager.GetCollection("test_users"), &TestLogger{})
|
||||
|
||||
// Test invalid pagination
|
||||
invalidPagination := Pagination{
|
||||
Limit: -1, // Invalid
|
||||
SortOrder: 2, // Invalid
|
||||
}
|
||||
|
||||
_, err := repo.FindAll(context.Background(), bson.M{}, invalidPagination)
|
||||
assert.Error(t, err)
|
||||
assert.ErrorIs(t, err, ErrInvalidPagination)
|
||||
|
||||
// Test invalid cursor
|
||||
invalidCursorPagination := NewPaginationBuilder().
|
||||
Limit(10).
|
||||
Cursor("invalid-base64").
|
||||
Build()
|
||||
|
||||
_, err = repo.FindAll(context.Background(), bson.M{}, invalidCursorPagination)
|
||||
assert.Error(t, err)
|
||||
assert.ErrorIs(t, err, ErrInvalidCursor)
|
||||
}
|
||||
|
||||
func TestRepository_IndexManagement(t *testing.T) {
|
||||
connManager := setupTestConnection(t)
|
||||
defer connManager.Close(context.Background())
|
||||
|
||||
// Test creating indexes
|
||||
indexes := []Index{
|
||||
*CreateUniqueIndex("email_idx", bson.D{{Key: "email", Value: 1}}),
|
||||
*NewIndex("age_idx", bson.D{{Key: "age", Value: 1}}),
|
||||
}
|
||||
|
||||
err := connManager.CreateIndexes("test_users", indexes)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Test listing indexes
|
||||
listIndexes, err := connManager.ListIndexes("test_users")
|
||||
require.NoError(t, err)
|
||||
assert.GreaterOrEqual(t, len(listIndexes), 3) // _id + created_at + our indexes
|
||||
|
||||
// Test dropping indexes
|
||||
err = connManager.DropIndexes("test_users", []string{"age_idx"})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestRepository_ConnectionManagement(t *testing.T) {
|
||||
config := ConnectionConfig{
|
||||
URI: "mongodb://localhost:27017",
|
||||
Database: "test_database",
|
||||
}
|
||||
|
||||
logger := &TestLogger{}
|
||||
connManager, err := NewConnectionManager(config, logger)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Test ping
|
||||
err = connManager.Ping(context.Background())
|
||||
require.NoError(t, err)
|
||||
|
||||
// Test get stats
|
||||
stats, err := connManager.GetStats(context.Background())
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, stats)
|
||||
|
||||
// Test collection stats
|
||||
collectionStats, err := connManager.GetCollectionStats(context.Background(), "test_users")
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, collectionStats)
|
||||
|
||||
// Test close
|
||||
err = connManager.Close(context.Background())
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestRepository_ErrorHandling(t *testing.T) {
|
||||
connManager := setupTestConnection(t)
|
||||
defer connManager.Close(context.Background())
|
||||
defer cleanupTestData(t, connManager, "test_users")
|
||||
|
||||
repo := NewRepository[TestUser](connManager.GetCollection("test_users"), &TestLogger{})
|
||||
|
||||
// Test invalid ObjectID
|
||||
_, err := repo.FindByID(context.Background(), "invalid-id")
|
||||
assert.Error(t, err)
|
||||
|
||||
// Test update non-existent document
|
||||
nonExistentUser := &TestUser{
|
||||
Model: Model{ID: "507f1f77bcf86cd799439011"},
|
||||
Email: "nonexistent@example.com",
|
||||
}
|
||||
err = repo.Update(context.Background(), nonExistentUser)
|
||||
assert.ErrorIs(t, err, ErrDocumentNotFound)
|
||||
|
||||
// Test delete non-existent document
|
||||
err = repo.Delete(context.Background(), "507f1f77bcf86cd799439011")
|
||||
assert.ErrorIs(t, err, ErrDocumentNotFound)
|
||||
}
|
||||
@@ -0,0 +1,301 @@
|
||||
package mongo
|
||||
|
||||
import (
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
// Index represents a MongoDB index
|
||||
type Index struct {
|
||||
Name string `json:"name"`
|
||||
Keys bson.D `json:"keys"`
|
||||
Options *options.IndexOptions `json:"options"`
|
||||
}
|
||||
|
||||
// NewIndex creates a new index with default options
|
||||
func NewIndex(name string, keys bson.D) *Index {
|
||||
return &Index{
|
||||
Name: name,
|
||||
Keys: keys,
|
||||
Options: options.Index(),
|
||||
}
|
||||
}
|
||||
|
||||
// WithUnique sets the unique option for the index
|
||||
func (i *Index) WithUnique(unique bool) *Index {
|
||||
i.Options.SetUnique(unique)
|
||||
return i
|
||||
}
|
||||
|
||||
// WithSparse sets the sparse option for the index
|
||||
func (i *Index) WithSparse(sparse bool) *Index {
|
||||
i.Options.SetSparse(sparse)
|
||||
return i
|
||||
}
|
||||
|
||||
// WithBackground sets the background option for the index
|
||||
func (i *Index) WithBackground(background bool) *Index {
|
||||
i.Options.SetBackground(background)
|
||||
return i
|
||||
}
|
||||
|
||||
// WithExpireAfterSeconds sets the TTL for the index
|
||||
func (i *Index) WithExpireAfterSeconds(seconds int32) *Index {
|
||||
i.Options.SetExpireAfterSeconds(seconds)
|
||||
return i
|
||||
}
|
||||
|
||||
// WithPartialFilterExpression sets the partial filter expression for the index
|
||||
func (i *Index) WithPartialFilterExpression(filter bson.M) *Index {
|
||||
i.Options.SetPartialFilterExpression(filter)
|
||||
return i
|
||||
}
|
||||
|
||||
// WithCollation sets the collation for the index
|
||||
func (i *Index) WithCollation(collation *options.Collation) *Index {
|
||||
i.Options.SetCollation(collation)
|
||||
return i
|
||||
}
|
||||
|
||||
// Common index builders
|
||||
|
||||
// CreateUniqueIndex creates a unique index
|
||||
func CreateUniqueIndex(name string, keys bson.D) *Index {
|
||||
return NewIndex(name, keys).WithUnique(true)
|
||||
}
|
||||
|
||||
// CreateTTLIndex creates a TTL index
|
||||
func CreateTTLIndex(name string, field string, expireAfterSeconds int32) *Index {
|
||||
return NewIndex(name, bson.D{{Key: field, Value: 1}}).WithExpireAfterSeconds(expireAfterSeconds)
|
||||
}
|
||||
|
||||
// CreateTextIndex creates a text index
|
||||
func CreateTextIndex(name string, fields ...string) *Index {
|
||||
keys := bson.D{}
|
||||
for _, field := range fields {
|
||||
keys = append(keys, bson.E{Key: field, Value: "text"})
|
||||
}
|
||||
return NewIndex(name, keys)
|
||||
}
|
||||
|
||||
// CreateCompoundIndex creates a compound index
|
||||
func CreateCompoundIndex(name string, fields map[string]int) *Index {
|
||||
keys := bson.D{}
|
||||
for field, direction := range fields {
|
||||
keys = append(keys, bson.E{Key: field, Value: direction})
|
||||
}
|
||||
return NewIndex(name, keys)
|
||||
}
|
||||
|
||||
// CreateGeospatialIndex creates a 2dsphere geospatial index
|
||||
func CreateGeospatialIndex(name string, field string) *Index {
|
||||
return NewIndex(name, bson.D{{Key: field, Value: "2dsphere"}})
|
||||
}
|
||||
|
||||
// CreateHashedIndex creates a hashed index
|
||||
func CreateHashedIndex(name string, field string) *Index {
|
||||
return NewIndex(name, bson.D{{Key: field, Value: "hashed"}})
|
||||
}
|
||||
|
||||
// Common index patterns
|
||||
|
||||
// DefaultIndexes returns common default indexes for most collections
|
||||
func DefaultIndexes() []Index {
|
||||
return []Index{
|
||||
*CreateUniqueIndex("_id_", bson.D{{Key: "_id", Value: 1}}),
|
||||
*NewIndex("created_at_idx", bson.D{{Key: "created_at", Value: -1}}),
|
||||
*NewIndex("updated_at_idx", bson.D{{Key: "updated_at", Value: -1}}),
|
||||
}
|
||||
}
|
||||
|
||||
// UserIndexes returns common indexes for user collections
|
||||
func UserIndexes() []Index {
|
||||
return []Index{
|
||||
*CreateUniqueIndex("_id_", bson.D{{Key: "_id", Value: 1}}),
|
||||
*CreateUniqueIndex("email_idx", bson.D{{Key: "email", Value: 1}}),
|
||||
*NewIndex("created_at_idx", bson.D{{Key: "created_at", Value: -1}}),
|
||||
*NewIndex("updated_at_idx", bson.D{{Key: "updated_at", Value: -1}}),
|
||||
*NewIndex("is_active_idx", bson.D{{Key: "is_active", Value: 1}}),
|
||||
}
|
||||
}
|
||||
|
||||
// TenderIndexes returns common indexes for tender collections
|
||||
func TenderIndexes() []Index {
|
||||
return []Index{
|
||||
*CreateUniqueIndex("_id_", bson.D{{Key: "_id", Value: 1}}),
|
||||
*NewIndex("created_at_idx", bson.D{{Key: "created_at", Value: -1}}),
|
||||
*NewIndex("updated_at_idx", bson.D{{Key: "updated_at", Value: -1}}),
|
||||
*NewIndex("status_idx", bson.D{{Key: "status", Value: 1}}),
|
||||
*NewIndex("deadline_idx", bson.D{{Key: "deadline", Value: 1}}),
|
||||
*NewIndex("category_idx", bson.D{{Key: "category", Value: 1}}),
|
||||
*NewIndex("location_idx", bson.D{{Key: "location", Value: 1}}),
|
||||
}
|
||||
}
|
||||
|
||||
// CompanyIndexes returns common indexes for company collections
|
||||
func CompanyIndexes() []Index {
|
||||
return []Index{
|
||||
*CreateUniqueIndex("_id_", bson.D{{Key: "_id", Value: 1}}),
|
||||
*CreateUniqueIndex("name_idx", bson.D{{Key: "name", Value: 1}}),
|
||||
*NewIndex("created_at_idx", bson.D{{Key: "created_at", Value: -1}}),
|
||||
*NewIndex("updated_at_idx", bson.D{{Key: "updated_at", Value: -1}}),
|
||||
*NewIndex("is_active_idx", bson.D{{Key: "is_active", Value: 1}}),
|
||||
}
|
||||
}
|
||||
|
||||
// QueryBuilder provides a fluent interface for building MongoDB queries
|
||||
type QueryBuilder struct {
|
||||
filter bson.M
|
||||
}
|
||||
|
||||
// NewQueryBuilder creates a new query builder
|
||||
func NewQueryBuilder() *QueryBuilder {
|
||||
return &QueryBuilder{
|
||||
filter: bson.M{},
|
||||
}
|
||||
}
|
||||
|
||||
// Where adds a simple equality condition
|
||||
func (qb *QueryBuilder) Where(field string, value interface{}) *QueryBuilder {
|
||||
qb.filter[field] = value
|
||||
return qb
|
||||
}
|
||||
|
||||
// WhereIn adds an $in condition
|
||||
func (qb *QueryBuilder) WhereIn(field string, values []interface{}) *QueryBuilder {
|
||||
qb.filter[field] = bson.M{"$in": values}
|
||||
return qb
|
||||
}
|
||||
|
||||
// WhereNotIn adds a $nin condition
|
||||
func (qb *QueryBuilder) WhereNotIn(field string, values []interface{}) *QueryBuilder {
|
||||
qb.filter[field] = bson.M{"$nin": values}
|
||||
return qb
|
||||
}
|
||||
|
||||
// WhereGreaterThan adds a $gt condition
|
||||
func (qb *QueryBuilder) WhereGreaterThan(field string, value interface{}) *QueryBuilder {
|
||||
qb.filter[field] = bson.M{"$gt": value}
|
||||
return qb
|
||||
}
|
||||
|
||||
// WhereGreaterThanOrEqual adds a $gte condition
|
||||
func (qb *QueryBuilder) WhereGreaterThanOrEqual(field string, value interface{}) *QueryBuilder {
|
||||
qb.filter[field] = bson.M{"$gte": value}
|
||||
return qb
|
||||
}
|
||||
|
||||
// WhereLessThan adds a $lt condition
|
||||
func (qb *QueryBuilder) WhereLessThan(field string, value interface{}) *QueryBuilder {
|
||||
qb.filter[field] = bson.M{"$lt": value}
|
||||
return qb
|
||||
}
|
||||
|
||||
// WhereLessThanOrEqual adds a $lte condition
|
||||
func (qb *QueryBuilder) WhereLessThanOrEqual(field string, value interface{}) *QueryBuilder {
|
||||
qb.filter[field] = bson.M{"$lte": value}
|
||||
return qb
|
||||
}
|
||||
|
||||
// WhereNotEqual adds a $ne condition
|
||||
func (qb *QueryBuilder) WhereNotEqual(field string, value interface{}) *QueryBuilder {
|
||||
qb.filter[field] = bson.M{"$ne": value}
|
||||
return qb
|
||||
}
|
||||
|
||||
// WhereExists adds an $exists condition
|
||||
func (qb *QueryBuilder) WhereExists(field string, exists bool) *QueryBuilder {
|
||||
qb.filter[field] = bson.M{"$exists": exists}
|
||||
return qb
|
||||
}
|
||||
|
||||
// WhereRegex adds a $regex condition
|
||||
func (qb *QueryBuilder) WhereRegex(field string, pattern string, options string) *QueryBuilder {
|
||||
qb.filter[field] = bson.M{"$regex": pattern, "$options": options}
|
||||
return qb
|
||||
}
|
||||
|
||||
// WhereText adds a $text condition
|
||||
func (qb *QueryBuilder) WhereText(search string) *QueryBuilder {
|
||||
qb.filter["$text"] = bson.M{"$search": search}
|
||||
return qb
|
||||
}
|
||||
|
||||
// WhereAnd adds an $and condition
|
||||
func (qb *QueryBuilder) WhereAnd(conditions ...bson.M) *QueryBuilder {
|
||||
qb.filter["$and"] = conditions
|
||||
return qb
|
||||
}
|
||||
|
||||
// WhereOr adds an $or condition
|
||||
func (qb *QueryBuilder) WhereOr(conditions ...bson.M) *QueryBuilder {
|
||||
qb.filter["$or"] = conditions
|
||||
return qb
|
||||
}
|
||||
|
||||
// WhereNor adds a $nor condition
|
||||
func (qb *QueryBuilder) WhereNor(conditions ...bson.M) *QueryBuilder {
|
||||
qb.filter["$nor"] = conditions
|
||||
return qb
|
||||
}
|
||||
|
||||
// Build returns the final filter
|
||||
func (qb *QueryBuilder) Build() bson.M {
|
||||
return qb.filter
|
||||
}
|
||||
|
||||
// PaginationBuilder provides a fluent interface for building pagination options
|
||||
type PaginationBuilder struct {
|
||||
pagination Pagination
|
||||
}
|
||||
|
||||
// NewPaginationBuilder creates a new pagination builder
|
||||
func NewPaginationBuilder() *PaginationBuilder {
|
||||
return &PaginationBuilder{
|
||||
pagination: Pagination{
|
||||
Limit: 10,
|
||||
SortField: "_id",
|
||||
SortOrder: -1,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Limit sets the limit
|
||||
func (pb *PaginationBuilder) Limit(limit int) *PaginationBuilder {
|
||||
pb.pagination.Limit = limit
|
||||
return pb
|
||||
}
|
||||
|
||||
// Skip sets the skip value for offset-based pagination
|
||||
func (pb *PaginationBuilder) Skip(skip int) *PaginationBuilder {
|
||||
pb.pagination.Skip = skip
|
||||
return pb
|
||||
}
|
||||
|
||||
// Cursor sets the cursor for cursor-based pagination
|
||||
func (pb *PaginationBuilder) Cursor(cursor string) *PaginationBuilder {
|
||||
pb.pagination.Cursor = cursor
|
||||
return pb
|
||||
}
|
||||
|
||||
// SortBy sets the sort field and order
|
||||
func (pb *PaginationBuilder) SortBy(field string, order int) *PaginationBuilder {
|
||||
pb.pagination.SortField = field
|
||||
pb.pagination.SortOrder = order
|
||||
return pb
|
||||
}
|
||||
|
||||
// SortAsc sorts in ascending order
|
||||
func (pb *PaginationBuilder) SortAsc(field string) *PaginationBuilder {
|
||||
return pb.SortBy(field, 1)
|
||||
}
|
||||
|
||||
// SortDesc sorts in descending order
|
||||
func (pb *PaginationBuilder) SortDesc(field string) *PaginationBuilder {
|
||||
return pb.SortBy(field, -1)
|
||||
}
|
||||
|
||||
// Build returns the final pagination options
|
||||
func (pb *PaginationBuilder) Build() Pagination {
|
||||
return pb.pagination
|
||||
}
|
||||
Reference in New Issue
Block a user