Add cursor rules and initial configuration for Tender Management Go Backend
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
|
toolchain go1.24.4
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/go-playground/validator/v10 v10.27.0
|
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.3
|
github.com/golang-jwt/jwt/v5 v5.2.3
|
||||||
github.com/google/uuid v1.4.0
|
github.com/google/uuid v1.4.0
|
||||||
github.com/labstack/echo/v4 v4.13.4
|
github.com/labstack/echo/v4 v4.13.4
|
||||||
github.com/spf13/viper v1.18.2
|
github.com/spf13/viper v1.18.2
|
||||||
|
github.com/stretchr/testify v1.10.0
|
||||||
go.mongodb.org/mongo-driver v1.17.4
|
go.mongodb.org/mongo-driver v1.17.4
|
||||||
go.uber.org/zap v1.26.0
|
go.uber.org/zap v1.26.0
|
||||||
golang.org/x/crypto v0.38.0
|
golang.org/x/crypto v0.38.0
|
||||||
@@ -17,15 +17,17 @@ require (
|
|||||||
)
|
)
|
||||||
|
|
||||||
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/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/golang/snappy v0.0.4 // indirect
|
||||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||||
github.com/klauspost/compress v1.17.0 // indirect
|
github.com/klauspost/compress v1.17.0 // indirect
|
||||||
github.com/labstack/gommon v0.4.2 // 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/magiconair/properties v1.8.7 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // 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/sync v0.14.0 // indirect
|
||||||
golang.org/x/sys v0.33.0 // indirect
|
golang.org/x/sys v0.33.0 // indirect
|
||||||
golang.org/x/text v0.25.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/ini.v1 v1.67.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // 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.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.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
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/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 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
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 h1:kkGXqQOBSDDWRhWNXTFpqGSCMyh/PLnqUvMGJPDJDs0=
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.3/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
github.com/golang-jwt/jwt/v5 v5.2.3/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||||
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
|
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/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 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
|
||||||
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
|
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 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
||||||
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
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.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||||
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
|
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/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-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.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package infrastructure
|
package infra
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"time"
|
"time"
|
||||||
@@ -100,17 +100,17 @@ type ScrapingConfig struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type LoggingConfig struct {
|
type LoggingConfig struct {
|
||||||
Level string `mapstructure:"level"`
|
Level string `mapstructure:"level"`
|
||||||
Format string `mapstructure:"format"`
|
Format string `mapstructure:"format"`
|
||||||
Output string `mapstructure:"output"`
|
Output string `mapstructure:"output"`
|
||||||
File LogFileConfig `mapstructure:"file"`
|
File LogFileConfig `mapstructure:"file"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type LogFileConfig struct {
|
type LogFileConfig struct {
|
||||||
Path string `mapstructure:"path"`
|
Path string `mapstructure:"path"`
|
||||||
MaxSize int `mapstructure:"max_size"` // MB
|
MaxSize int `mapstructure:"max_size"` // MB
|
||||||
MaxBackups int `mapstructure:"max_backups"`
|
MaxBackups int `mapstructure:"max_backups"`
|
||||||
MaxAge int `mapstructure:"max_age"` // days
|
MaxAge int `mapstructure:"max_age"` // days
|
||||||
Compress bool `mapstructure:"compress"`
|
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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"time"
|
"time"
|
||||||
|
"tm/internal/customer/domain/entity"
|
||||||
|
"tm/pkg/logger"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"go.mongodb.org/mongo-driver/bson"
|
"go.mongodb.org/mongo-driver/bson"
|
||||||
"go.mongodb.org/mongo-driver/mongo"
|
"go.mongodb.org/mongo-driver/mongo"
|
||||||
"go.mongodb.org/mongo-driver/mongo/options"
|
"go.mongodb.org/mongo-driver/mongo/options"
|
||||||
|
|
||||||
"tm/internal/domain"
|
|
||||||
"tm/pkg/logger"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// CustomerRepositoryImpl implements the CustomerRepository interface
|
// customerRepository implements the Repository interface
|
||||||
type CustomerRepositoryImpl struct {
|
type customerRepository struct {
|
||||||
collection *mongo.Collection
|
collection *mongo.Collection
|
||||||
logger logger.Logger
|
logger logger.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewCustomerRepository creates a new customer repository
|
// 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")
|
collection := db.Collection("customers")
|
||||||
|
|
||||||
// Create indexes
|
// Create indexes
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
// Email index (unique)
|
// Email index (unique)
|
||||||
emailIndex := mongo.IndexModel{
|
emailIndex := mongo.IndexModel{
|
||||||
Keys: bson.D{{"email", 1}},
|
Keys: bson.D{{Key: "email", Value: 1}},
|
||||||
Options: options.Index().SetUnique(true),
|
Options: options.Index().SetUnique(true),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Company ID index
|
// Company ID index
|
||||||
companyIndex := mongo.IndexModel{
|
companyIndex := mongo.IndexModel{
|
||||||
Keys: bson.D{{"company_id", 1}},
|
Keys: bson.D{{Key: "company_id", Value: 1}},
|
||||||
}
|
}
|
||||||
|
|
||||||
// Active status index
|
// Active status index
|
||||||
activeIndex := mongo.IndexModel{
|
activeIndex := mongo.IndexModel{
|
||||||
Keys: bson.D{{"is_active", 1}},
|
Keys: bson.D{{Key: "is_active", Value: 1}},
|
||||||
}
|
}
|
||||||
|
|
||||||
// Created at index
|
// Created at index
|
||||||
createdAtIndex := mongo.IndexModel{
|
createdAtIndex := mongo.IndexModel{
|
||||||
Keys: bson.D{{"created_at", -1}},
|
Keys: bson.D{{Key: "created_at", Value: -1}},
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := collection.Indexes().CreateMany(ctx, []mongo.IndexModel{
|
_, 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,
|
collection: collection,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create creates a new customer
|
// 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
|
// Set created/updated timestamps
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
customer.CreatedAt = now
|
customer.CreatedAt = now
|
||||||
@@ -98,8 +97,8 @@ func (r *CustomerRepositoryImpl) Create(ctx context.Context, customer *domain.Cu
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetByID retrieves a customer by ID
|
// GetByID retrieves a customer by ID
|
||||||
func (r *CustomerRepositoryImpl) GetByID(ctx context.Context, id uuid.UUID) (*domain.Customer, error) {
|
func (r *customerRepository) GetByID(ctx context.Context, id uuid.UUID) (*entity.Customer, error) {
|
||||||
var customer domain.Customer
|
var customer entity.Customer
|
||||||
|
|
||||||
filter := bson.M{"_id": id}
|
filter := bson.M{"_id": id}
|
||||||
err := r.collection.FindOne(ctx, filter).Decode(&customer)
|
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
|
// GetByEmail retrieves a customer by email
|
||||||
func (r *CustomerRepositoryImpl) GetByEmail(ctx context.Context, email string) (*domain.Customer, error) {
|
func (r *customerRepository) GetByEmail(ctx context.Context, email string) (*entity.Customer, error) {
|
||||||
var customer domain.Customer
|
var customer entity.Customer
|
||||||
|
|
||||||
filter := bson.M{"email": email}
|
filter := bson.M{"email": email}
|
||||||
err := r.collection.FindOne(ctx, filter).Decode(&customer)
|
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
|
// 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
|
// Set updated timestamp
|
||||||
customer.UpdatedAt = time.Now()
|
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)
|
// 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}
|
filter := bson.M{"_id": id}
|
||||||
update := bson.M{
|
update := bson.M{
|
||||||
"$set": 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
|
// List retrieves customers with pagination
|
||||||
func (r *CustomerRepositoryImpl) List(ctx context.Context, limit, offset int) ([]*domain.Customer, error) {
|
func (r *customerRepository) List(ctx context.Context, limit, offset int) ([]*entity.Customer, error) {
|
||||||
var customers []*domain.Customer
|
var customers []*entity.Customer
|
||||||
|
|
||||||
// Build options
|
// Build options
|
||||||
opts := options.Find()
|
opts := options.Find()
|
||||||
opts.SetLimit(int64(limit))
|
opts.SetLimit(int64(limit))
|
||||||
opts.SetSkip(int64(offset))
|
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
|
// Only active customers by default
|
||||||
filter := bson.M{"is_active": true}
|
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
|
// GetByCompanyID retrieves customers by company ID with pagination
|
||||||
func (r *CustomerRepositoryImpl) GetByCompanyID(ctx context.Context, companyID uuid.UUID, limit, offset int) ([]*domain.Customer, error) {
|
func (r *customerRepository) GetByCompanyID(ctx context.Context, companyID uuid.UUID, limit, offset int) ([]*entity.Customer, error) {
|
||||||
var customers []*domain.Customer
|
var customers []*entity.Customer
|
||||||
|
|
||||||
// Build options
|
// Build options
|
||||||
opts := options.Find()
|
opts := options.Find()
|
||||||
opts.SetLimit(int64(limit))
|
opts.SetLimit(int64(limit))
|
||||||
opts.SetSkip(int64(offset))
|
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 by company ID and active status
|
||||||
filter := bson.M{
|
filter := bson.M{
|
||||||
@@ -271,38 +270,8 @@ func (r *CustomerRepositoryImpl) GetByCompanyID(ctx context.Context, companyID u
|
|||||||
return customers, nil
|
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
|
// 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}
|
filter := bson.M{"_id": id}
|
||||||
update := bson.M{
|
update := bson.M{
|
||||||
"$addToSet": bson.M{"device_tokens": token}, // Use $addToSet to avoid duplicates
|
"$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
|
// 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}
|
filter := bson.M{"_id": id}
|
||||||
update := bson.M{
|
update := bson.M{
|
||||||
"$pull": bson.M{"device_tokens": token}, // Use $pull to remove the token
|
"$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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
infrastructure "tm/infra"
|
||||||
|
domain "tm/internal/customer/domain"
|
||||||
|
"tm/internal/customer/domain/entity"
|
||||||
|
"tm/pkg/logger"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
|
||||||
"tm/internal/domain"
|
|
||||||
"tm/pkg/logger"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// CustomerServiceImpl implements the CustomerService interface
|
// CustomerService implements the CustomerService interface
|
||||||
type CustomerServiceImpl struct {
|
type CustomerService struct {
|
||||||
customerRepo domain.CustomerRepository
|
customerRepo domain.Repository
|
||||||
|
config *infrastructure.AuthConfig
|
||||||
logger logger.Logger
|
logger logger.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewCustomerService creates a new customer service
|
// NewCustomerService creates a new customer service
|
||||||
func NewCustomerService(
|
func NewCustomerService(
|
||||||
customerRepo domain.CustomerRepository,
|
customerRepo domain.Repository,
|
||||||
|
config *infrastructure.AuthConfig,
|
||||||
logger logger.Logger,
|
logger logger.Logger,
|
||||||
) domain.CustomerService {
|
) CustomerService {
|
||||||
return &CustomerServiceImpl{
|
return CustomerService{
|
||||||
customerRepo: customerRepo,
|
customerRepo: customerRepo,
|
||||||
|
config: config,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetCustomers retrieves customers with pagination (for panel users)
|
// 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{}{
|
s.logger.Info("Retrieving customers", map[string]interface{}{
|
||||||
"limit": limit,
|
"limit": limit,
|
||||||
"offset": offset,
|
"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)
|
// 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{}{
|
s.logger.Info("Retrieving customer by ID", map[string]interface{}{
|
||||||
"customer_id": id.String(),
|
"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)
|
// 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{}{
|
s.logger.Info("Retrieving customers by company", map[string]interface{}{
|
||||||
"company_id": companyID.String(),
|
"company_id": companyID.String(),
|
||||||
"limit": limit,
|
"limit": limit,
|
||||||
@@ -106,7 +110,7 @@ func (s *CustomerServiceImpl) GetCustomersByCompany(ctx context.Context, company
|
|||||||
}
|
}
|
||||||
|
|
||||||
// UpdateCustomerStatus updates customer active status (for panel users)
|
// 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{}{
|
s.logger.Info("Updating customer status", map[string]interface{}{
|
||||||
"customer_id": customerID.String(),
|
"customer_id": customerID.String(),
|
||||||
"is_active": isActive,
|
"is_active": isActive,
|
||||||
@@ -145,35 +149,3 @@ func (s *CustomerServiceImpl) UpdateCustomerStatus(ctx context.Context, customer
|
|||||||
|
|
||||||
return nil
|
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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@@ -9,36 +9,12 @@ import (
|
|||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
|
||||||
"tm/internal/domain"
|
"tm/internal/customer/domain/aggregate"
|
||||||
"tm/internal/infrastructure"
|
"tm/internal/customer/domain/entity"
|
||||||
"tm/pkg/logger"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// 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
|
// 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{}{
|
s.logger.Info("Registering new customer", map[string]interface{}{
|
||||||
"email": req.Email,
|
"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")
|
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
|
// Create customer
|
||||||
customer := &domain.Customer{
|
customer := &entity.Customer{
|
||||||
ID: uuid.New(),
|
ID: uuid.New(),
|
||||||
Email: req.Email,
|
Email: req.Email,
|
||||||
Password: hashedPassword,
|
Password: hashedPassword,
|
||||||
FirstName: req.FirstName,
|
FirstName: req.FirstName,
|
||||||
LastName: req.LastName,
|
LastName: req.LastName,
|
||||||
Phone: req.Phone,
|
Mobile: req.Mobile,
|
||||||
Role: domain.CustomerRoleUser,
|
|
||||||
CompanyID: companyID,
|
|
||||||
IsActive: true,
|
IsActive: true,
|
||||||
IsVerified: false, // Require email verification
|
IsVerified: false, // Require email verification
|
||||||
DeviceTokens: make([]string, 0),
|
DeviceTokens: make([]string, 0),
|
||||||
Preferences: domain.CustomerPreferences{
|
UpdatedAt: time.Now(),
|
||||||
Language: "en",
|
CreatedAt: time.Now(),
|
||||||
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(),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.customerRepo.Create(ctx, customer); err != nil {
|
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
|
// TODO: Send verification email
|
||||||
s.sendVerificationEmail(ctx, customer)
|
s.sendVerificationEmail(ctx, customer)
|
||||||
|
|
||||||
return &domain.CustomerAuthResponse{
|
return &aggregate.AuthorizationResponse{
|
||||||
Customer: customer,
|
Customer: aggregate.NewCustomer(*customer),
|
||||||
AccessToken: accessToken,
|
AccessToken: accessToken,
|
||||||
RefreshToken: refreshToken,
|
RefreshToken: refreshToken,
|
||||||
ExpiresIn: int64(s.config.JWT.AccessTokenDuration.Seconds()),
|
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
|
// 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{}{
|
s.logger.Info("Customer login attempt", map[string]interface{}{
|
||||||
"email": req.Email,
|
"email": req.Email,
|
||||||
})
|
})
|
||||||
@@ -240,8 +164,8 @@ func (s *CustomerAuthServiceImpl) Login(ctx context.Context, req domain.LoginReq
|
|||||||
"is_verified": customer.IsVerified,
|
"is_verified": customer.IsVerified,
|
||||||
})
|
})
|
||||||
|
|
||||||
return &domain.CustomerAuthResponse{
|
return &aggregate.AuthorizationResponse{
|
||||||
Customer: customer,
|
Customer: aggregate.NewCustomer(*customer),
|
||||||
AccessToken: accessToken,
|
AccessToken: accessToken,
|
||||||
RefreshToken: refreshToken,
|
RefreshToken: refreshToken,
|
||||||
ExpiresIn: int64(s.config.JWT.AccessTokenDuration.Seconds()),
|
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
|
// 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
|
// Parse and validate refresh token
|
||||||
token, err := jwt.Parse(refreshToken, func(token *jwt.Token) (interface{}, error) {
|
token, err := jwt.Parse(refreshToken, func(token *jwt.Token) (interface{}, error) {
|
||||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
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 nil, errors.New("failed to generate refresh token")
|
||||||
}
|
}
|
||||||
|
|
||||||
return &domain.CustomerAuthResponse{
|
return &aggregate.AuthorizationResponse{
|
||||||
Customer: customer,
|
Customer: aggregate.NewCustomer(*customer),
|
||||||
AccessToken: accessToken,
|
AccessToken: accessToken,
|
||||||
RefreshToken: newRefreshToken,
|
RefreshToken: newRefreshToken,
|
||||||
ExpiresIn: int64(s.config.JWT.AccessTokenDuration.Seconds()),
|
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
|
// 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) {
|
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
|
||||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||||
return nil, errors.New("invalid signing method")
|
return nil, errors.New("invalid signing method")
|
||||||
@@ -371,7 +295,7 @@ func (s *CustomerAuthServiceImpl) ValidateToken(ctx context.Context, tokenString
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ChangePassword changes customer password
|
// 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)
|
customer, err := s.customerRepo.GetByID(ctx, customerID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.New("customer not found")
|
return errors.New("customer not found")
|
||||||
@@ -408,7 +332,7 @@ func (s *CustomerAuthServiceImpl) ChangePassword(ctx context.Context, customerID
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ResetPassword initiates password reset process
|
// 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
|
// TODO: Implement password reset logic
|
||||||
// This would typically involve:
|
// This would typically involve:
|
||||||
// 1. Generate reset token
|
// 1. Generate reset token
|
||||||
@@ -418,7 +342,7 @@ func (s *CustomerAuthServiceImpl) ResetPassword(ctx context.Context, email strin
|
|||||||
}
|
}
|
||||||
|
|
||||||
// VerifyEmail verifies customer's email address
|
// 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
|
// TODO: Implement email verification logic
|
||||||
// This would typically involve:
|
// This would typically involve:
|
||||||
// 1. Validate verification token
|
// 1. Validate verification token
|
||||||
@@ -427,8 +351,8 @@ func (s *CustomerAuthServiceImpl) VerifyEmail(ctx context.Context, customerID uu
|
|||||||
return errors.New("email verification not implemented")
|
return errors.New("email verification not implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResendVerification resends verification email
|
// ResendVerification resend verification email
|
||||||
func (s *CustomerAuthServiceImpl) ResendVerification(ctx context.Context, email string) error {
|
func (s *CustomerService) ResendVerification(ctx context.Context, email string) error {
|
||||||
customer, err := s.customerRepo.GetByEmail(ctx, email)
|
customer, err := s.customerRepo.GetByEmail(ctx, email)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.New("customer not found")
|
return errors.New("customer not found")
|
||||||
@@ -446,7 +370,7 @@ func (s *CustomerAuthServiceImpl) ResendVerification(ctx context.Context, email
|
|||||||
|
|
||||||
// Private helper methods
|
// 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)
|
hashedBytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
@@ -454,16 +378,15 @@ func (s *CustomerAuthServiceImpl) hashPassword(password string) (string, error)
|
|||||||
return string(hashedBytes), nil
|
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))
|
err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password))
|
||||||
return err == nil
|
return err == nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *CustomerAuthServiceImpl) generateAccessToken(customer *domain.Customer) (string, error) {
|
func (s *CustomerService) generateAccessToken(customer *entity.Customer) (string, error) {
|
||||||
claims := jwt.MapClaims{
|
claims := jwt.MapClaims{
|
||||||
"customer_id": customer.ID.String(),
|
"customer_id": customer.ID.String(),
|
||||||
"email": customer.Email,
|
"email": customer.Email,
|
||||||
"role": customer.Role,
|
|
||||||
"company_id": customer.CompanyID.String(),
|
"company_id": customer.CompanyID.String(),
|
||||||
"user_type": "customer",
|
"user_type": "customer",
|
||||||
"exp": time.Now().Add(s.config.JWT.AccessTokenDuration).Unix(),
|
"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))
|
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{
|
claims := jwt.MapClaims{
|
||||||
"customer_id": customer.ID.String(),
|
"customer_id": customer.ID.String(),
|
||||||
"user_type": "customer",
|
"user_type": "customer",
|
||||||
@@ -488,8 +411,9 @@ func (s *CustomerAuthServiceImpl) generateRefreshToken(customer *domain.Customer
|
|||||||
return token.SignedString([]byte(s.config.JWT.Secret))
|
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
|
// TODO: Implement email sending logic
|
||||||
|
_ = ctx
|
||||||
s.logger.Info("Verification email would be sent", map[string]interface{}{
|
s.logger.Info("Verification email would be sent", map[string]interface{}{
|
||||||
"customer_id": customer.ID.String(),
|
"customer_id": customer.ID.String(),
|
||||||
"email": customer.Email,
|
"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"
|
||||||
"go.uber.org/zap/zapcore"
|
"go.uber.org/zap/zapcore"
|
||||||
"gopkg.in/natefinch/lumberjack.v2"
|
"gopkg.in/natefinch/lumberjack.v2"
|
||||||
|
|
||||||
"tm/internal/infrastructure"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Logger interface defines logging methods
|
// Logger interface defines logging methods
|
||||||
@@ -30,8 +28,23 @@ type ZapLogger struct {
|
|||||||
fields map[string]interface{}
|
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
|
// NewLogger creates a new logger instance with Zap and Lumberjack
|
||||||
func NewLogger(config *infrastructure.LoggingConfig) Logger {
|
func NewLogger(config *Config) Logger {
|
||||||
// Parse log level
|
// Parse log level
|
||||||
level := parseLogLevel(config.Level)
|
level := parseLogLevel(config.Level)
|
||||||
|
|
||||||
@@ -62,7 +75,7 @@ func NewLogger(config *infrastructure.LoggingConfig) Logger {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// createWriter creates the appropriate writer based on configuration
|
// createWriter creates the appropriate writer based on configuration
|
||||||
func createWriter(config *infrastructure.LoggingConfig) zapcore.WriteSyncer {
|
func createWriter(config *Config) zapcore.WriteSyncer {
|
||||||
switch config.Output {
|
switch config.Output {
|
||||||
case "stdout":
|
case "stdout":
|
||||||
return zapcore.AddSync(os.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
|
// 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
|
// Ensure log directory exists
|
||||||
logDir := filepath.Dir(fileConfig.Path)
|
logDir := filepath.Dir(fileConfig.Path)
|
||||||
if err := os.MkdirAll(logDir, 0755); err != nil {
|
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
|
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