Merge branch 'hotfix/improvment-struct'

This commit is contained in:
n.nakhostin
2025-08-10 19:09:24 +03:30
15 changed files with 713 additions and 5781 deletions
+1 -7
View File
@@ -116,13 +116,7 @@ func initHTTPServer(conf infra.Config, log logger.Logger) *echo.Echo {
})
// Add health check endpoint
e.GET("/health", func(c echo.Context) error {
return c.JSON(http.StatusOK, map[string]interface{}{
"status": "healthy",
"time": time.Now().Unix(),
"version": "1.0.0",
})
})
e.GET("/admin/v1/health", healthHandler)
// Add Swagger documentation endpoint
e.GET("/swagger/*", echoSwagger.WrapHandler)
+194 -1186
View File
File diff suppressed because it is too large Load Diff
+194 -1186
View File
File diff suppressed because it is too large Load Diff
+191 -811
View File
File diff suppressed because it is too large Load Diff
+30
View File
@@ -0,0 +1,30 @@
package main
import (
"net/http"
"time"
"github.com/labstack/echo/v4"
)
// @Summary Health check
// @Description Get server health status
// @Tags Health
// @Accept json
// @Produce json
// @Success 200 {object} HealthResponse
// @Router /health [get]
func healthHandler(c echo.Context) error {
return c.JSON(http.StatusOK, HealthResponse{
Status: "healthy",
Time: time.Now().Unix(),
Version: "1.0.0",
})
}
// HealthResponse represents the health check response
type HealthResponse struct {
Status string `json:"status"`
Time int64 `json:"time"`
Version string `json:"version"`
}
+7 -33
View File
@@ -1,22 +1,3 @@
// Package main Tender Management API
//
// This is the API documentation for the Tender Management System.
//
// Schemes: http, https
// Host: localhost:8081
// BasePath: /api/v1
// Version: 1.0.0
//
// Consumes:
// - application/json
//
// Produces:
// - application/json
//
// Security:
// - bearer
//
// swagger:meta
package main
// @title Tender Management API
@@ -32,31 +13,27 @@ package main
// @license.url http://www.apache.org/licenses/LICENSE-2.0.html
// @host localhost:8081
// @BasePath /api/v1
// @BasePath /admin/v1
// @securityDefinitions.apikey BearerAuth
// @in header
// @name Authorization
// @description Type "Bearer" followed by a space and JWT token.
// @tag.name customers
// @tag.description Customer management operations
// @tag.name Users
// @tag.description User management operations including authentication, profile management, and admin operations
// @tag.name users
// @tag.description User management operations
// @tag.name Authorization
// @tag.description Authentication operations including login, logout, and token management
// @tag.name health
// @tag.name Health
// @tag.description Health check operations
// @tag.name auth
// @tag.description Authentication operations
import (
"context"
"fmt"
"net/http"
"time"
"tm/internal/customer"
"tm/internal/user"
_ "tm/cmd/web/docs" // This is generated by swag
@@ -92,8 +69,8 @@ func main() {
// Initialize authorization service
authService := initAuthorizationService(conf.Auth.JWT, redisClient, logger)
// Initialize repositories with MongoDB connection manager
customerRepository := customer.NewCustomerRepository(mongoManager, logger)
userRepository := user.NewUserRepository(mongoManager, logger)
logger.Info("Repositories initialized successfully", map[string]interface{}{
@@ -104,7 +81,6 @@ func main() {
userValidator := user.NewValidationService()
// Initialize services with repositories
customerService := customer.NewCustomerService(customerRepository, logger)
userService := user.NewUserService(userRepository, logger, authService, userValidator)
logger.Info("Services initialized successfully", map[string]interface{}{
@@ -112,7 +88,6 @@ func main() {
})
// Initialize handlers with services
customerHandler := customer.NewHandler(customerService)
userHandler := user.NewUserHandler(userService, logger, userValidator, authService)
logger.Info("Handlers initialized successfully", map[string]interface{}{
@@ -123,7 +98,6 @@ func main() {
e := initHTTPServer(conf, logger)
// Register routes
customerHandler.RegisterRoutes(e)
userHandler.RegisterRoutes(e)
// Start server
-554
View File
@@ -1,554 +0,0 @@
# Customer Management Module
This module implements a complete User Management system for the Tender Management System, allowing administrators to register users who can later access the mobile application.
## Features
### 1. User Registration (by Admin)
- **Fields**: Full name, username, email, mobile number, password (hashed), national ID (optional), gender (enum), birthdate (optional), profile image (optional), user status (active/inactive), registration date
- **Uniqueness**: Ensures uniqueness for email, mobile, and username
- **Validation**: Comprehensive input validation using govalidator
### 2. Authentication for Mobile Users
- **Login**: Secure login via email or mobile number + password
- **JWT Tokens**: Access and refresh token generation
- **Password Security**: bcrypt hashing for secure password storage
### 3. Device Token Management
- **Storage**: Saves device_token and device_type (Android/iOS) when user logs in
- **Updates**: Updates token if user logs in from new device or re-installs app
- **Cleanup**: Endpoint to delete token on logout
### 4. User Profile Management
- **View/Edit**: Personal info (name, email, mobile, etc.)
- **Password Change**: Secure password change endpoint
- **Profile Picture**: Upload/update profile picture support
### 5. Push Notification Ready
- **Multiple Tokens**: Stores multiple device tokens per user
- **User Association**: Tokens tied to correct user ID
- **Admin Access**: API to get all active device tokens for admin push campaigns
### 6. Admin Panel APIs
- **List/Search/Filter**: Users by name, mobile, email, registration date, and status
- **Status Management**: Activate/deactivate users
- **Role Support**: Framework for role assignment (user, moderator, etc.)
### 7. Security & Best Practices
- **Input Validation**: Comprehensive validation and sanitization
- **Rate Limiting**: Framework for authentication endpoint rate limiting
- **Password Security**: bcrypt hashing with configurable cost
- **HTTP Status Codes**: Proper status codes and error messages
- **Activity Logging**: User activities (login, logout, profile update)
## Architecture
### Clean Architecture Implementation
- **Domain Layer**: Business entities, aggregates, interfaces, and core business rules
- **Service Layer**: Business logic and use cases
- **Handler Layer**: HTTP controllers and request/response handling
- **Repository Layer**: Data access implementations
- **Infrastructure Layer**: External dependencies (DB, cache, queues, config)
### File Structure
```
internal/customer/
├── entity.go # Core domain entities and types
├── form.go # Request/response forms with validation
├── repository.go # Repository interface and implementation
├── service.go # Business logic and use cases
├── handler.go # HTTP controllers and request/response handling
├── validation.go # Custom validation rules
└── README.md # This documentation
```
## Database Schema
### Customer Collection
```javascript
{
"_id": "uuid",
"full_name": "string",
"username": "string (unique)",
"email": "string (unique)",
"mobile": "string (unique)",
"password": "string (hashed)",
"national_id": "string (optional)",
"gender": "enum (male|female|other) (optional)",
"birthdate": "int64 (unix timestamp) (optional)",
"profile_image": "string (url) (optional)",
"status": "enum (active|inactive)",
"company_id": "uuid (optional)",
"is_verified": "boolean",
"device_tokens": [
{
"token": "string",
"device_type": "enum (android|ios)",
"created_at": "int64 (unix timestamp)",
"updated_at": "int64 (unix timestamp)"
}
],
"last_login_at": "int64 (unix timestamp) (optional)",
"created_at": "int64 (unix timestamp)",
"updated_at": "int64 (unix timestamp)"
}
```
### Indexes
- `email` (unique)
- `username` (unique)
- `mobile` (unique)
- `company_id`
- `status`
- `gender`
- `created_at` (descending)
- Text index on `full_name`, `email`, `mobile`, `username`
## API Endpoints
### Public Endpoints (No Authentication Required)
#### 1. Register Customer
```http
POST /api/customers/register
Content-Type: application/json
{
"full_name": "John Doe",
"username": "johndoe",
"email": "john@example.com",
"mobile": "+1234567890",
"password": "securepassword123",
"national_id": "123456789",
"gender": "male",
"birthdate": 631152000,
"profile_image": "https://example.com/avatar.jpg",
"company_id": "uuid-optional"
}
```
**Response:**
```json
{
"success": true,
"message": "Customer registered successfully",
"data": {
"id": "uuid",
"full_name": "John Doe",
"username": "johndoe",
"email": "john@example.com",
"mobile": "+1234567890",
"national_id": "123456789",
"gender": "male",
"birthdate": 631152000,
"profile_image": "https://example.com/avatar.jpg",
"status": "active",
"company_id": "uuid",
"is_verified": false,
"device_tokens": [],
"created_at": 1703123456,
"updated_at": 1703123456
}
}
```
#### 2. Login
```http
POST /api/customers/login
Content-Type: application/json
{
"email_or_mobile": "john@example.com",
"password": "securepassword123"
}
```
**Response:**
```json
{
"success": true,
"message": "Login successful",
"data": {
"customer": {
"id": "uuid",
"full_name": "John Doe",
"username": "johndoe",
"email": "john@example.com",
"mobile": "+1234567890",
"status": "active",
"is_verified": false,
"device_tokens": [],
"last_login_at": 1703123456,
"created_at": 1703123456,
"updated_at": 1703123456
},
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refresh_token": "refresh_token_here",
"expires_at": 1703127056
}
}
```
#### 3. Refresh Token
```http
POST /api/customers/refresh-token
Content-Type: application/json
{
"refresh_token": "refresh_token_here"
}
```
### Protected Endpoints (Authentication Required)
#### 4. Get Profile
```http
GET /api/customers/profile
Authorization: Bearer <access_token>
X-Customer-ID: <customer_uuid>
```
#### 5. Update Profile
```http
PUT /api/customers/profile
Authorization: Bearer <access_token>
X-Customer-ID: <customer_uuid>
Content-Type: application/json
{
"full_name": "John Updated Doe",
"email": "john.updated@example.com",
"mobile": "+1234567891",
"gender": "male",
"birthdate": 631152000,
"profile_image": "https://example.com/new-avatar.jpg"
}
```
#### 6. Change Password
```http
PUT /api/customers/change-password
Authorization: Bearer <access_token>
X-Customer-ID: <customer_uuid>
Content-Type: application/json
{
"old_password": "currentpassword",
"new_password": "newsecurepassword123"
}
```
#### 7. Add Device Token
```http
POST /api/customers/device-token
Authorization: Bearer <access_token>
X-Customer-ID: <customer_uuid>
Content-Type: application/json
{
"device_token": "fcm_token_here",
"device_type": "android"
}
```
#### 8. Remove Device Token
```http
DELETE /api/customers/device-token
Authorization: Bearer <access_token>
X-Customer-ID: <customer_uuid>
Content-Type: application/json
{
"device_token": "fcm_token_here"
}
```
#### 9. Logout
```http
POST /api/customers/logout
Authorization: Bearer <access_token>
X-Customer-ID: <customer_uuid>
Content-Type: application/json
{
"device_token": "fcm_token_here"
}
```
### Admin Endpoints (Admin Authentication Required)
#### 10. List Customers
```http
GET /api/admin/customers?search=john&status=active&gender=male&limit=20&offset=0&sort_by=created_at&sort_order=desc
Authorization: Bearer <admin_access_token>
```
**Response:**
```json
{
"success": true,
"message": "Customers retrieved successfully",
"data": [
{
"id": "uuid",
"full_name": "John Doe",
"username": "johndoe",
"email": "john@example.com",
"mobile": "+1234567890",
"status": "active",
"is_verified": false,
"device_tokens": [],
"last_login_at": 1703123456,
"created_at": 1703123456,
"updated_at": 1703123456
}
],
"meta": {
"total": 100,
"limit": 20,
"offset": 0,
"page": 1,
"pages": 5
}
}
```
#### 11. Get Customer by ID
```http
GET /api/admin/customers/{customer_id}
Authorization: Bearer <admin_access_token>
```
#### 12. Update Customer Status
```http
PUT /api/admin/customers/{customer_id}/status
Authorization: Bearer <admin_access_token>
Content-Type: application/json
{
"status": "inactive"
}
```
#### 13. Get All Device Tokens
```http
GET /api/admin/customers/device-tokens
Authorization: Bearer <admin_access_token>
```
**Response:**
```json
{
"success": true,
"message": "Device tokens retrieved successfully",
"data": [
{
"token": "fcm_token_1",
"device_type": "android",
"created_at": 1703123456,
"updated_at": 1703123456
},
{
"token": "fcm_token_2",
"device_type": "ios",
"created_at": 1703123457,
"updated_at": 1703123457
}
]
}
```
## Validation Rules
### Registration Form
- `full_name`: Required, 2-100 characters
- `username`: Required, alphanumeric, 3-30 characters, unique
- `email`: Required, valid email format, unique
- `mobile`: Required, 10-15 characters, unique
- `password`: Required, 8-128 characters
- `national_id`: Optional, 5-20 characters
- `gender`: Optional, must be "male", "female", or "other"
- `birthdate`: Optional, valid Unix timestamp
- `profile_image`: Optional, valid URL
- `company_id`: Optional, valid UUID
### Login Form
- `email_or_mobile`: Required
- `password`: Required
### Update Form
- All fields optional with same validation rules as registration
- Uniqueness checks only if field is being updated
### Device Token Form
- `device_token`: Required, 32-255 characters
- `device_type`: Required, must be "android" or "ios"
### List Customers Form
- `search`: Optional, text search
- `status`: Optional, must be "active" or "inactive"
- `gender`: Optional, must be "male", "female", or "other"
- `company_id`: Optional, valid UUID
- `limit`: Optional, 1-100
- `offset`: Optional, minimum 0
- `sort_by`: Optional, must be "full_name", "email", "mobile", "created_at", or "last_login_at"
- `sort_order`: Optional, must be "asc" or "desc"
## Error Handling
### Standard Error Response Format
```json
{
"success": false,
"message": "Error message",
"error": {
"code": "ERROR_CODE",
"message": "Detailed error message",
"details": "Additional error details"
}
}
```
### Common Error Codes
- `BAD_REQUEST`: Invalid request format or parameters
- `VALIDATION_ERROR`: Input validation failed
- `UNAUTHORIZED`: Authentication required or failed
- `FORBIDDEN`: Insufficient permissions
- `NOT_FOUND`: Resource not found
- `CONFLICT`: Resource already exists
- `INTERNAL_SERVER_ERROR`: Server error
### HTTP Status Codes
- `200 OK`: Success
- `201 Created`: Resource created successfully
- `400 Bad Request`: Invalid request
- `401 Unauthorized`: Authentication required
- `403 Forbidden`: Insufficient permissions
- `404 Not Found`: Resource not found
- `409 Conflict`: Resource conflict
- `422 Unprocessable Entity`: Validation error
- `500 Internal Server Error`: Server error
## Security Features
### Password Security
- bcrypt hashing with configurable cost
- Minimum 8 characters required
- Maximum 128 characters allowed
### Input Validation
- Comprehensive validation using govalidator
- Custom validators for Unix timestamps
- SQL injection prevention through parameterized queries
- XSS prevention through input sanitization
### Authentication
- JWT-based token system
- Access and refresh token separation
- Token expiration handling
- Secure token generation
### Data Protection
- Passwords never serialized in responses
- Sensitive data logging prevention
- Input sanitization and validation
## Usage Examples
### Register a New Customer (Admin)
```bash
curl -X POST http://localhost:8081/api/customers/register \
-H "Content-Type: application/json" \
-d '{
"full_name": "John Doe",
"username": "johndoe",
"email": "john@example.com",
"mobile": "+1234567890",
"password": "securepassword123",
"gender": "male",
"birthdate": 631152000
}'
```
### Login as Customer
```bash
curl -X POST http://localhost:8081/api/customers/login \
-H "Content-Type: application/json" \
-d '{
"email_or_mobile": "john@example.com",
"password": "securepassword123"
}'
```
### Update Profile
```bash
curl -X PUT http://localhost:8081/api/customers/profile \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <access_token>" \
-H "X-Customer-ID: <customer_uuid>" \
-d '{
"full_name": "John Updated Doe",
"email": "john.updated@example.com"
}'
```
### List Customers (Admin)
```bash
curl -X GET "http://localhost:8081/api/admin/customers?search=john&status=active&limit=10" \
-H "Authorization: Bearer <admin_access_token>"
```
## Future Enhancements
### Planned Features
1. **JWT Implementation**: Proper JWT token generation and validation
2. **Email Verification**: Email verification system
3. **Password Reset**: Forgot password functionality
4. **Role-Based Access Control**: User roles and permissions
5. **Rate Limiting**: API rate limiting implementation
6. **Audit Logging**: Comprehensive audit trail
7. **Bulk Operations**: Bulk user import/export
8. **Advanced Search**: Full-text search with filters
9. **File Upload**: Profile picture upload handling
10. **Two-Factor Authentication**: 2FA support
### Technical Improvements
1. **Caching**: Redis caching for frequently accessed data
2. **Background Jobs**: Async processing for heavy operations
3. **Monitoring**: Health checks and metrics
4. **Testing**: Comprehensive unit and integration tests
5. **Documentation**: OpenAPI/Swagger documentation
6. **Performance**: Query optimization and indexing
7. **Security**: Additional security measures
8. **Scalability**: Horizontal scaling support
## Dependencies
### Required Packages
- `github.com/google/uuid`: UUID generation
- `github.com/labstack/echo/v4`: HTTP framework
- `github.com/asaskevich/govalidator`: Input validation
- `golang.org/x/crypto/bcrypt`: Password hashing
- `go.mongodb.org/mongo-driver`: MongoDB driver
### Configuration
- MongoDB connection string
- JWT secret key
- Password hashing cost
- Token expiration times
- Rate limiting settings
## Contributing
When contributing to this module:
1. Follow the existing code structure and patterns
2. Add comprehensive validation for new fields
3. Include proper error handling and logging
4. Write unit tests for new functionality
5. Update documentation for new endpoints
6. Follow security best practices
7. Use Unix timestamps for all time fields
8. Implement proper pagination for list endpoints
9. Add appropriate indexes for new fields
10. Follow the flat domain structure pattern
-45
View File
@@ -1,45 +0,0 @@
package customer
import (
"github.com/google/uuid"
)
type CustomerAggregate struct {
ID uuid.UUID `json:"_id"`
FullName string `json:"full_name"`
Username string `json:"username"`
Mobile string `json:"mobile"`
Email string `json:"email"`
Status string `json:"status"`
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 NewCustomerAggregate(c Customer) CustomerAggregate {
customer := CustomerAggregate{
ID: c.ID,
FullName: c.FullName,
Username: c.Username,
Email: c.Email,
Mobile: c.Mobile,
Status: string(c.Status),
IsVerified: c.IsVerified,
CreatedAt: c.CreatedAt,
UpdatedAt: c.UpdatedAt,
}
if c.LastLoginAt != nil {
customer.LastLoginAt = c.LastLoginAt
}
return customer
}
// AuthResponse defines authentication response (kept for backward compatibility)
type AuthorizationResponse struct {
Customer CustomerAggregate `json:"customer"`
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
ExpiresIn int64 `json:"expires_in"`
}
-59
View File
@@ -1,59 +0,0 @@
package customer
import (
"github.com/google/uuid"
)
// Gender represents user gender
type Gender string
const (
GenderMale Gender = "male"
GenderFemale Gender = "female"
GenderOther Gender = "other"
)
// CustomerStatus represents customer account status
type CustomerStatus string
const (
CustomerStatusActive CustomerStatus = "active"
CustomerStatusInactive CustomerStatus = "inactive"
)
// DeviceType represents the type of device
type DeviceType string
const (
DeviceTypeAndroid DeviceType = "android"
DeviceTypeIOS DeviceType = "ios"
)
// DeviceToken represents a device token for push notifications
type DeviceToken struct {
Token string `bson:"token"`
DeviceType DeviceType `bson:"device_type"`
CreatedAt int64 `bson:"created_at"` // Unix timestamp
UpdatedAt int64 `bson:"updated_at"` // Unix timestamp
}
// Customer represents a mobile app user (customer)
type Customer struct {
ID uuid.UUID `bson:"_id"`
FullName string `bson:"full_name"`
Username string `bson:"username"`
Email string `bson:"email"`
Mobile string `bson:"mobile"`
Password string `bson:"password"` // Never serialize password
NationalID *string `bson:"national_id,omitempty"`
Gender *Gender `bson:"gender,omitempty"`
Birthdate *int64 `bson:"birthdate,omitempty"` // Unix timestamp
ProfileImage *string `bson:"profile_image,omitempty"`
Status CustomerStatus `bson:"status"`
CompanyID *uuid.UUID `bson:"company_id,omitempty"`
IsVerified bool `bson:"is_verified"`
DeviceTokens []DeviceToken `bson:"device_tokens"` // For push notifications
LastLoginAt *int64 `bson:"last_login_at,omitempty"` // Unix timestamp
CreatedAt int64 `bson:"created_at"` // Unix timestamp
UpdatedAt int64 `bson:"updated_at"` // Unix timestamp
}
-134
View File
@@ -1,134 +0,0 @@
package customer
// Customer Registration DTOs
type RegisterCustomerForm struct {
FullName string `json:"full_name" valid:"required,length(2|100)"`
Username string `json:"username" valid:"required,alphanum,length(3|30)"`
Email string `json:"email" valid:"required,email"`
Mobile string `json:"mobile" valid:"required,length(10|15)"`
Password string `json:"password" valid:"required,length(8|128)"`
NationalID *string `json:"national_id,omitempty" valid:"optional,length(5|20)"`
Gender *string `json:"gender,omitempty" valid:"optional,in(male|female|other)"`
Birthdate *int64 `json:"birthdate,omitempty" valid:"optional,unix_timestamp"`
ProfileImage *string `json:"profile_image,omitempty" valid:"optional,url"`
CompanyID *string `json:"company_id,omitempty" valid:"optional,uuid"`
}
type UpdateCustomerForm struct {
FullName *string `json:"full_name,omitempty" valid:"optional,length(2|100)"`
Username *string `json:"username,omitempty" valid:"optional,alphanum,length(3|30)"`
Email *string `json:"email,omitempty" valid:"optional,email"`
Mobile *string `json:"mobile,omitempty" valid:"optional,length(10|15)"`
NationalID *string `json:"national_id,omitempty" valid:"optional,length(5|20)"`
Gender *string `json:"gender,omitempty" valid:"optional,in(male|female|other)"`
Birthdate *int64 `json:"birthdate,omitempty" valid:"optional,unix_timestamp"`
ProfileImage *string `json:"profile_image,omitempty" valid:"optional,url"`
Status *string `json:"status,omitempty" valid:"optional,in(active|inactive)"`
}
// Customer Authentication DTOs
type LoginForm struct {
EmailOrMobile string `json:"email_or_mobile" valid:"required"`
Password string `json:"password" valid:"required"`
}
type RefreshTokenForm struct {
RefreshToken string `json:"refresh_token" valid:"required"`
}
type ChangePasswordForm struct {
OldPassword string `json:"old_password" valid:"required"`
NewPassword string `json:"new_password" valid:"required,length(8|128)"`
}
// Device Token DTOs
type AddDeviceTokenForm struct {
DeviceToken string `json:"device_token" valid:"required,length(32|255)"`
DeviceType string `json:"device_type" valid:"required,in(android|ios)"`
}
type RemoveDeviceTokenForm struct {
DeviceToken string `json:"device_token" valid:"required"`
}
// Admin DTOs
type ListCustomersForm struct {
Search *string `query:"search" valid:"optional"`
Status *string `query:"status" valid:"optional,in(active|inactive)"`
Gender *string `query:"gender" valid:"optional,in(male|female|other)"`
CompanyID *string `query:"company_id" valid:"optional,uuid"`
Limit *int `query:"limit" valid:"optional,range(1|100)"`
Offset *int `query:"offset" valid:"optional,min(0)"`
SortBy *string `query:"sort_by" valid:"optional,in(full_name|email|mobile|created_at|last_login_at)"`
SortOrder *string `query:"sort_order" valid:"optional,in(asc|desc)"`
}
type UpdateCustomerStatusForm struct {
Status string `json:"status" valid:"required,in(active|inactive)"`
}
// Response DTOs
type CustomerResponse struct {
ID string `json:"id"`
FullName string `json:"full_name"`
Username string `json:"username"`
Email string `json:"email"`
Mobile string `json:"mobile"`
NationalID *string `json:"national_id,omitempty"`
Gender *string `json:"gender,omitempty"`
Birthdate *int64 `json:"birthdate,omitempty"`
ProfileImage *string `json:"profile_image,omitempty"`
Status string `json:"status"`
CompanyID *string `json:"company_id,omitempty"`
IsVerified bool `json:"is_verified"`
DeviceTokens []DeviceToken `json:"device_tokens"`
LastLoginAt *int64 `json:"last_login_at,omitempty"`
CreatedAt int64 `json:"created_at"`
UpdatedAt int64 `json:"updated_at"`
}
type AuthResponse struct {
Customer *CustomerResponse `json:"customer"`
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
ExpiresAt int64 `json:"expires_at"`
}
type DeviceTokenResponse struct {
Token string `json:"token"`
DeviceType string `json:"device_type"`
CreatedAt int64 `json:"created_at"`
UpdatedAt int64 `json:"updated_at"`
}
// Helper function to convert Customer to CustomerResponse
func (c *Customer) ToResponse() *CustomerResponse {
gender := ""
if c.Gender != nil {
gender = string(*c.Gender)
}
companyID := ""
if c.CompanyID != nil {
companyID = c.CompanyID.String()
}
return &CustomerResponse{
ID: c.ID.String(),
FullName: c.FullName,
Username: c.Username,
Email: c.Email,
Mobile: c.Mobile,
NationalID: c.NationalID,
Gender: &gender,
Birthdate: c.Birthdate,
ProfileImage: c.ProfileImage,
Status: string(c.Status),
CompanyID: &companyID,
IsVerified: c.IsVerified,
DeviceTokens: c.DeviceTokens,
LastLoginAt: c.LastLoginAt,
CreatedAt: c.CreatedAt,
UpdatedAt: c.UpdatedAt,
}
}
-571
View File
@@ -1,571 +0,0 @@
package customer
import (
"net/http"
"tm/pkg/response"
"github.com/asaskevich/govalidator"
"github.com/google/uuid"
"github.com/labstack/echo/v4"
)
// Handler handles HTTP requests for customer operations
type Handler struct {
service Service
}
// NewHandler creates a new customer handler
func NewHandler(service Service) *Handler {
return &Handler{
service: service,
}
}
// RegisterRoutes registers all customer routes
func (h *Handler) RegisterRoutes(e *echo.Echo) {
v1 := e.Group("/api/v1")
v1.POST("/customers/register", h.RegisterCustomer)
// Public routes (no authentication required)
v1.POST("/customers/login", h.Login)
v1.POST("/customers/refresh-token", h.RefreshToken)
// Protected routes (authentication required)
customers := v1.Group("/customers")
customers.Use(h.authMiddleware) // TODO: Implement auth middleware
customers.GET("/profile", h.GetProfile)
customers.PUT("/profile", h.UpdateProfile)
customers.PUT("/change-password", h.ChangePassword)
customers.POST("/device-token", h.AddDeviceToken)
customers.DELETE("/device-token", h.RemoveDeviceToken)
customers.POST("/logout", h.Logout)
// Admin routes (admin authentication required)
admin := v1.Group("/admin/customers")
admin.Use(h.adminAuthMiddleware) // TODO: Implement admin auth middleware
admin.POST("/register", h.RegisterCustomer)
admin.GET("", h.ListCustomers)
admin.GET("/:id", h.GetCustomerByID)
admin.PUT("/:id/status", h.UpdateCustomerStatus)
admin.GET("/device-tokens", h.GetAllDeviceTokens)
}
// RegisterCustomer handles customer registration
// @Summary Register a new customer
// @Description Register a new customer with the provided information
// @Tags customers
// @Accept json
// @Produce json
// @Param customer body RegisterCustomerForm true "Customer registration information"
// @Success 201 {object} response.APIResponse{data=customer.CustomerResponse} "Customer registered successfully"
// @Failure 400 {object} response.APIResponse "Bad request"
// @Failure 409 {object} response.APIResponse "Customer already exists"
// @Failure 500 {object} response.APIResponse "Internal server error"
// @Router /admin/customers/register [post]
func (h *Handler) RegisterCustomer(c echo.Context) error {
var form RegisterCustomerForm
if err := c.Bind(&form); err != nil {
return response.BadRequest(c, "Invalid request body", err.Error())
}
// Validate form
if _, err := govalidator.ValidateStruct(form); err != nil {
return response.ValidationError(c, "Validation failed", err.Error())
}
// Register customer
customer, err := h.service.RegisterCustomer(c.Request().Context(), &form)
if err != nil {
return response.Conflict(c, err.Error())
}
return response.Created(c, customer.ToResponse(), "Customer registered successfully")
}
// Login handles customer authentication
// @Summary Customer login
// @Description Authenticate customer with email and password
// @Tags customers
// @Accept json
// @Produce json
// @Param credentials body LoginForm true "Login credentials"
// @Success 200 {object} response.APIResponse{data=customer.AuthResponse} "Login successful"
// @Failure 400 {object} response.APIResponse "Bad request"
// @Failure 401 {object} response.APIResponse "Invalid credentials"
// @Failure 500 {object} response.APIResponse "Internal server error"
// @Router /customers/login [post]
func (h *Handler) Login(c echo.Context) error {
var form LoginForm
if err := c.Bind(&form); err != nil {
return response.BadRequest(c, "Invalid request body", err.Error())
}
// Validate form
if _, err := govalidator.ValidateStruct(form); err != nil {
return response.ValidationError(c, "Validation failed", err.Error())
}
// Authenticate customer
authResponse, err := h.service.Login(c.Request().Context(), &form)
if err != nil {
return response.Unauthorized(c, err.Error())
}
return response.Success(c, authResponse, "Login successful")
}
// RefreshToken handles token refresh
// @Summary Refresh access token
// @Description Refresh the access token using a refresh token
// @Tags customers
// @Accept json
// @Produce json
// @Param token body RefreshTokenForm true "Refresh token information"
// @Success 200 {object} response.APIResponse{data=customer.AuthResponse} "Token refreshed successfully"
// @Failure 400 {object} response.APIResponse "Bad request"
// @Failure 401 {object} response.APIResponse "Invalid refresh token"
// @Failure 500 {object} response.APIResponse "Internal server error"
// @Router /customers/refresh-token [post]
func (h *Handler) RefreshToken(c echo.Context) error {
var form RefreshTokenForm
if err := c.Bind(&form); err != nil {
return response.BadRequest(c, "Invalid request body", err.Error())
}
// Validate form
if _, err := govalidator.ValidateStruct(form); err != nil {
return response.ValidationError(c, "Validation failed", err.Error())
}
// Refresh token
authResponse, err := h.service.RefreshToken(c.Request().Context(), form.RefreshToken)
if err != nil {
return response.Unauthorized(c, err.Error())
}
return response.Success(c, authResponse, "Token refreshed successfully")
}
// GetProfile retrieves customer profile
// @Summary Get customer profile
// @Description Retrieve the authenticated customer's profile information
// @Tags customers
// @Accept json
// @Produce json
// @Security BearerAuth
// @Success 200 {object} response.APIResponse{data=customer.CustomerResponse} "Profile retrieved successfully"
// @Failure 401 {object} response.APIResponse "Unauthorized"
// @Failure 404 {object} response.APIResponse "Customer not found"
// @Failure 500 {object} response.APIResponse "Internal server error"
// @Router /customers/profile [get]
func (h *Handler) GetProfile(c echo.Context) error {
// Get customer ID from context (set by auth middleware)
customerID, err := h.getCustomerIDFromContext(c)
if err != nil {
return response.Unauthorized(c, "Invalid authentication")
}
// Get customer
customer, err := h.service.GetCustomerByID(c.Request().Context(), customerID)
if err != nil {
return response.NotFound(c, "Customer not found")
}
return response.Success(c, customer.ToResponse(), "Profile retrieved successfully")
}
// UpdateProfile updates customer profile
// @Summary Update customer profile
// @Description Update the authenticated customer's profile information
// @Tags customers
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param profile body UpdateCustomerForm true "Profile update information"
// @Success 200 {object} response.APIResponse{data=customer.CustomerResponse} "Profile updated successfully"
// @Failure 400 {object} response.APIResponse "Bad request"
// @Failure 401 {object} response.APIResponse "Unauthorized"
// @Failure 500 {object} response.APIResponse "Internal server error"
// @Router /customers/profile [put]
func (h *Handler) UpdateProfile(c echo.Context) error {
// Get customer ID from context
customerID, err := h.getCustomerIDFromContext(c)
if err != nil {
return response.Unauthorized(c, "Invalid authentication")
}
var form UpdateCustomerForm
if err := c.Bind(&form); err != nil {
return response.BadRequest(c, "Invalid request body", err.Error())
}
// Validate form
if _, err := govalidator.ValidateStruct(form); err != nil {
return response.ValidationError(c, "Validation failed", err.Error())
}
// Update customer
customer, err := h.service.UpdateCustomer(c.Request().Context(), customerID, &form)
if err != nil {
return response.BadRequest(c, err.Error(), "")
}
return response.Success(c, customer.ToResponse(), "Profile updated successfully")
}
// ChangePassword changes customer password
// @Summary Change customer password
// @Description Change the authenticated customer's password
// @Tags customers
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param password body ChangePasswordForm true "Password change information"
// @Success 200 {object} response.APIResponse "Password changed successfully"
// @Failure 400 {object} response.APIResponse "Bad request"
// @Failure 401 {object} response.APIResponse "Unauthorized"
// @Failure 500 {object} response.APIResponse "Internal server error"
// @Router /customers/change-password [put]
func (h *Handler) ChangePassword(c echo.Context) error {
// Get customer ID from context
customerID, err := h.getCustomerIDFromContext(c)
if err != nil {
return response.Unauthorized(c, "Invalid authentication")
}
var form ChangePasswordForm
if err := c.Bind(&form); err != nil {
return response.BadRequest(c, "Invalid request body", err.Error())
}
// Validate form
if _, err := govalidator.ValidateStruct(form); err != nil {
return response.ValidationError(c, "Validation failed", err.Error())
}
// Change password
err = h.service.ChangePassword(c.Request().Context(), customerID, &form)
if err != nil {
return response.BadRequest(c, err.Error(), "")
}
return response.Success(c, nil, "Password changed successfully")
}
// AddDeviceToken adds a device token for push notifications
// @Summary Add device token
// @Description Add a device token for push notifications
// @Tags customers
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param device_token body AddDeviceTokenForm true "Device token information"
// @Success 200 {object} response.APIResponse "Device token added successfully"
// @Failure 400 {object} response.APIResponse "Bad request"
// @Failure 401 {object} response.APIResponse "Unauthorized"
// @Failure 500 {object} response.APIResponse "Internal server error"
// @Router /customers/device-token [post]
func (h *Handler) AddDeviceToken(c echo.Context) error {
// Get customer ID from context
customerID, err := h.getCustomerIDFromContext(c)
if err != nil {
return response.Unauthorized(c, "Invalid authentication")
}
var form AddDeviceTokenForm
if err := c.Bind(&form); err != nil {
return response.BadRequest(c, "Invalid request body", err.Error())
}
// Validate form
if _, err := govalidator.ValidateStruct(form); err != nil {
return response.ValidationError(c, "Validation failed", err.Error())
}
// Add device token
err = h.service.AddDeviceToken(c.Request().Context(), customerID, &form)
if err != nil {
return response.BadRequest(c, err.Error(), "")
}
return response.Success(c, nil, "Device token added successfully")
}
// RemoveDeviceToken removes a device token
// @Summary Remove device token
// @Description Remove a device token for push notifications
// @Tags customers
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param device_token body RemoveDeviceTokenForm true "Device token information"
// @Success 200 {object} response.APIResponse "Device token removed successfully"
// @Failure 400 {object} response.APIResponse "Bad request"
// @Failure 401 {object} response.APIResponse "Unauthorized"
// @Failure 500 {object} response.APIResponse "Internal server error"
// @Router /customers/device-token [delete]
func (h *Handler) RemoveDeviceToken(c echo.Context) error {
// Get customer ID from context
customerID, err := h.getCustomerIDFromContext(c)
if err != nil {
return response.Unauthorized(c, "Invalid authentication")
}
var form RemoveDeviceTokenForm
if err := c.Bind(&form); err != nil {
return response.BadRequest(c, "Invalid request body", err.Error())
}
// Validate form
if _, err := govalidator.ValidateStruct(form); err != nil {
return response.ValidationError(c, "Validation failed", err.Error())
}
// Remove device token
err = h.service.RemoveDeviceToken(c.Request().Context(), customerID, &form)
if err != nil {
return response.BadRequest(c, err.Error(), "")
}
return response.Success(c, nil, "Device token removed successfully")
}
// Logout handles customer logout
// @Summary Customer logout
// @Description Logout customer and invalidate device token
// @Tags customers
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param logout body object true "Logout request with device token"
// @Success 200 {object} response.APIResponse "Logged out successfully"
// @Failure 400 {object} response.APIResponse "Bad request"
// @Failure 401 {object} response.APIResponse "Unauthorized"
// @Failure 500 {object} response.APIResponse "Internal server error"
// @Router /customers/logout [post]
func (h *Handler) Logout(c echo.Context) error {
// Get customer ID from context
customerID, err := h.getCustomerIDFromContext(c)
if err != nil {
return response.Unauthorized(c, "Invalid authentication")
}
// Get device token from request body
var req struct {
DeviceToken string `json:"device_token" valid:"required"`
}
if err := c.Bind(&req); err != nil {
return response.BadRequest(c, "Invalid request body", err.Error())
}
// Validate request
if _, err := govalidator.ValidateStruct(req); err != nil {
return response.ValidationError(c, "Validation failed", err.Error())
}
// Logout
err = h.service.Logout(c.Request().Context(), customerID, req.DeviceToken)
if err != nil {
return response.BadRequest(c, err.Error(), "")
}
return response.Success(c, nil, "Logged out successfully")
}
// ListCustomers lists customers (admin only)
// @Summary List customers
// @Description Retrieve a paginated list of customers (admin only)
// @Tags customers
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param page query int false "Page number" default(1)
// @Param limit query int false "Number of items per page" default(20)
// @Param search query string false "Search term"
// @Success 200 {object} response.APIResponse{data=[]customer.CustomerResponse} "Customers retrieved successfully"
// @Failure 400 {object} response.APIResponse "Bad request"
// @Failure 401 {object} response.APIResponse "Unauthorized"
// @Failure 500 {object} response.APIResponse "Internal server error"
// @Router /admin/customers [get]
func (h *Handler) ListCustomers(c echo.Context) error {
var form ListCustomersForm
// Bind query parameters
if err := c.Bind(&form); err != nil {
return response.BadRequest(c, "Invalid query parameters", err.Error())
}
// Validate form
if _, err := govalidator.ValidateStruct(form); err != nil {
return response.ValidationError(c, "Validation failed", err.Error())
}
// List customers
customers, total, err := h.service.ListCustomers(c.Request().Context(), &form)
if err != nil {
return response.InternalServerError(c, err.Error())
}
// Convert to responses
var responses []*CustomerResponse
for _, customer := range customers {
responses = append(responses, customer.ToResponse())
}
// Create metadata
meta := &response.Meta{
Total: total,
Limit: 20, // Default limit
Offset: 0, // Default offset
}
if form.Limit != nil {
meta.Limit = *form.Limit
}
if form.Offset != nil {
meta.Offset = *form.Offset
}
return response.SuccessWithMeta(c, responses, meta, "Customers retrieved successfully")
}
// GetCustomerByID retrieves a customer by ID (admin only)
// @Summary Get customer by ID
// @Description Retrieve customer information by ID (admin only)
// @Tags customers
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path string true "Customer ID"
// @Success 200 {object} response.APIResponse{data=customer.CustomerResponse} "Customer retrieved successfully"
// @Failure 400 {object} response.APIResponse "Bad request"
// @Failure 401 {object} response.APIResponse "Unauthorized"
// @Failure 404 {object} response.APIResponse "Customer not found"
// @Failure 500 {object} response.APIResponse "Internal server error"
// @Router /admin/customers/{id} [get]
func (h *Handler) GetCustomerByID(c echo.Context) error {
// Parse customer ID
customerIDStr := c.Param("id")
customerID, err := uuid.Parse(customerIDStr)
if err != nil {
return response.BadRequest(c, "Invalid customer ID", err.Error())
}
// Get customer
customer, err := h.service.GetCustomerByID(c.Request().Context(), customerID)
if err != nil {
return response.NotFound(c, "Customer not found")
}
return response.Success(c, customer.ToResponse(), "Customer retrieved successfully")
}
// UpdateCustomerStatus updates customer status (admin only)
// @Summary Update customer status
// @Description Update customer status (admin only)
// @Tags customers
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path string true "Customer ID"
// @Param status body UpdateCustomerStatusForm true "Status update information"
// @Success 200 {object} response.APIResponse "Customer status updated successfully"
// @Failure 400 {object} response.APIResponse "Bad request"
// @Failure 401 {object} response.APIResponse "Unauthorized"
// @Failure 404 {object} response.APIResponse "Customer not found"
// @Failure 500 {object} response.APIResponse "Internal server error"
// @Router /admin/customers/{id}/status [put]
func (h *Handler) UpdateCustomerStatus(c echo.Context) error {
// Parse customer ID
customerIDStr := c.Param("id")
customerID, err := uuid.Parse(customerIDStr)
if err != nil {
return response.BadRequest(c, "Invalid customer ID", err.Error())
}
var form UpdateCustomerStatusForm
if err := c.Bind(&form); err != nil {
return response.BadRequest(c, "Invalid request body", err.Error())
}
// Validate form
if _, err := govalidator.ValidateStruct(form); err != nil {
return response.ValidationError(c, "Validation failed", err.Error())
}
// Update status
err = h.service.UpdateCustomerStatus(c.Request().Context(), customerID, &form)
if err != nil {
return response.BadRequest(c, err.Error(), "")
}
return response.Success(c, nil, "Customer status updated successfully")
}
// GetAllDeviceTokens retrieves all device tokens (admin only)
// @Summary Get all device tokens
// @Description Retrieve all device tokens (admin only)
// @Tags customers
// @Accept json
// @Produce json
// @Security BearerAuth
// @Success 200 {object} response.APIResponse{data=[]string} "Device tokens retrieved successfully"
// @Failure 401 {object} response.APIResponse "Unauthorized"
// @Failure 500 {object} response.APIResponse "Internal server error"
// @Router /admin/customers/device-tokens [get]
func (h *Handler) GetAllDeviceTokens(c echo.Context) error {
// Get all device tokens
tokens, err := h.service.GetAllDeviceTokens(c.Request().Context())
if err != nil {
return response.InternalServerError(c, err.Error())
}
return response.Success(c, tokens, "Device tokens retrieved successfully")
}
// Helper methods
// getCustomerIDFromContext extracts customer ID from context
func (h *Handler) getCustomerIDFromContext(c echo.Context) (uuid.UUID, error) {
// TODO: Implement proper JWT token validation and extraction
// For now, we'll use a header-based approach
customerIDStr := c.Request().Header.Get("X-Customer-ID")
if customerIDStr == "" {
return uuid.Nil, echo.NewHTTPError(http.StatusUnauthorized, "Missing customer ID")
}
customerID, err := uuid.Parse(customerIDStr)
if err != nil {
return uuid.Nil, echo.NewHTTPError(http.StatusUnauthorized, "Invalid customer ID")
}
return customerID, nil
}
// authMiddleware is a placeholder for authentication middleware
func (h *Handler) authMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
// TODO: Implement proper JWT token validation
// For now, we'll just pass through
return next(c)
}
}
// adminAuthMiddleware is a placeholder for admin authentication middleware
func (h *Handler) adminAuthMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
// TODO: Implement proper admin authentication
// For now, we'll just pass through
return next(c)
}
}
-554
View File
@@ -1,554 +0,0 @@
package customer
import (
"context"
"errors"
"time"
"tm/pkg/logger"
mongopkg "tm/pkg/mongo"
"github.com/google/uuid"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
// Repository defines methods for customer (mobile user) data access
type Repository 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)
GetByUsername(ctx context.Context, username string) (*Customer, error)
GetByMobile(ctx context.Context, mobile 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)
Search(ctx context.Context, search string, status *string, gender *string, companyID *uuid.UUID, limit, offset int, sortBy, sortOrder string) ([]*Customer, error)
GetByCompanyID(ctx context.Context, companyID uuid.UUID, limit, offset int) ([]*Customer, error)
AddDeviceToken(ctx context.Context, id uuid.UUID, deviceToken DeviceToken) error
RemoveDeviceToken(ctx context.Context, id uuid.UUID, token string) error
UpdateLastLogin(ctx context.Context, id uuid.UUID) error
GetAllDeviceTokens(ctx context.Context) ([]DeviceToken, error)
}
// customerRepository implements the Repository interface
type customerRepository struct {
collection *mongo.Collection
logger logger.Logger
}
// NewCustomerRepository creates a new customer repository
func NewCustomerRepository(mongoManager *mongopkg.ConnectionManager, logger logger.Logger) Repository {
collection := mongoManager.GetCollection("customers")
// Create indexes
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
// Email index (unique)
emailIndex := mongo.IndexModel{
Keys: bson.D{{Key: "email", Value: 1}},
Options: options.Index().SetUnique(true),
}
// Username index (unique)
usernameIndex := mongo.IndexModel{
Keys: bson.D{{Key: "username", Value: 1}},
Options: options.Index().SetUnique(true),
}
// Mobile index (unique)
mobileIndex := mongo.IndexModel{
Keys: bson.D{{Key: "mobile", Value: 1}},
Options: options.Index().SetUnique(true),
}
// Company ID index
companyIndex := mongo.IndexModel{
Keys: bson.D{{Key: "company_id", Value: 1}},
}
// Status index
statusIndex := mongo.IndexModel{
Keys: bson.D{{Key: "status", Value: 1}},
}
// Gender index
genderIndex := mongo.IndexModel{
Keys: bson.D{{Key: "gender", Value: 1}},
}
// Created at index
createdAtIndex := mongo.IndexModel{
Keys: bson.D{{Key: "created_at", Value: -1}},
}
// Full text search index
textIndex := mongo.IndexModel{
Keys: bson.D{
{Key: "full_name", Value: "text"},
{Key: "email", Value: "text"},
{Key: "mobile", Value: "text"},
{Key: "username", Value: "text"},
},
}
_, err := collection.Indexes().CreateMany(ctx, []mongo.IndexModel{
emailIndex,
usernameIndex,
mobileIndex,
companyIndex,
statusIndex,
genderIndex,
createdAtIndex,
textIndex,
})
if err != nil {
logger.Warn("Failed to create customer indexes", map[string]interface{}{
"error": err.Error(),
})
}
return &customerRepository{
collection: collection,
logger: logger,
}
}
// Create creates a new customer
func (r *customerRepository) Create(ctx context.Context, customer *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
}
// GetByID retrieves a customer by ID
func (r *customerRepository) GetByID(ctx context.Context, id uuid.UUID) (*Customer, error) {
var customer Customer
filter := bson.M{"_id": id}
err := r.collection.FindOne(ctx, filter).Decode(&customer)
if err != nil {
if err == mongo.ErrNoDocuments {
return nil, errors.New("customer not found")
}
r.logger.Error("Failed to get customer by ID", map[string]interface{}{
"error": err.Error(),
"customer_id": id.String(),
})
return nil, err
}
return &customer, nil
}
// GetByEmail retrieves a customer by email
func (r *customerRepository) GetByEmail(ctx context.Context, email string) (*Customer, error) {
var customer Customer
filter := bson.M{"email": email}
err := r.collection.FindOne(ctx, filter).Decode(&customer)
if err != nil {
if err == mongo.ErrNoDocuments {
return nil, errors.New("customer not found")
}
r.logger.Error("Failed to get customer by email", map[string]interface{}{
"error": err.Error(),
"email": email,
})
return nil, err
}
return &customer, nil
}
// GetByUsername retrieves a customer by username
func (r *customerRepository) GetByUsername(ctx context.Context, username string) (*Customer, error) {
var customer Customer
filter := bson.M{"username": username}
err := r.collection.FindOne(ctx, filter).Decode(&customer)
if err != nil {
if err == mongo.ErrNoDocuments {
return nil, errors.New("customer not found")
}
r.logger.Error("Failed to get customer by username", map[string]interface{}{
"error": err.Error(),
"username": username,
})
return nil, err
}
return &customer, nil
}
// GetByMobile retrieves a customer by mobile
func (r *customerRepository) GetByMobile(ctx context.Context, mobile string) (*Customer, error) {
var customer Customer
filter := bson.M{"mobile": mobile}
err := r.collection.FindOne(ctx, filter).Decode(&customer)
if err != nil {
if err == mongo.ErrNoDocuments {
return nil, errors.New("customer not found")
}
r.logger.Error("Failed to get customer by mobile", map[string]interface{}{
"error": err.Error(),
"mobile": mobile,
})
return nil, err
}
return &customer, nil
}
// Update updates a customer
func (r *customerRepository) Update(ctx context.Context, customer *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")
}
r.logger.Info("Customer updated successfully", map[string]interface{}{
"customer_id": customer.ID.String(),
"email": customer.Email,
})
return nil
}
// Delete deletes a customer (soft delete by setting is_active to false)
func (r *customerRepository) Delete(ctx context.Context, id uuid.UUID) error {
filter := bson.M{"_id": id}
update := bson.M{
"$set": bson.M{
"is_active": false,
"updated_at": time.Now().Unix(),
},
}
result, err := r.collection.UpdateOne(ctx, filter, update)
if err != nil {
r.logger.Error("Failed to delete customer", map[string]interface{}{
"error": err.Error(),
"customer_id": id.String(),
})
return err
}
if result.MatchedCount == 0 {
return errors.New("customer not found")
}
r.logger.Info("Customer deleted successfully", map[string]interface{}{
"customer_id": id.String(),
})
return nil
}
// List retrieves customers with pagination
func (r *customerRepository) List(ctx context.Context, limit, offset int) ([]*Customer, error) {
var customers []*Customer
// Build options
opts := options.Find()
opts.SetLimit(int64(limit))
opts.SetSkip(int64(offset))
opts.SetSort(bson.D{{Key: "created_at", Value: -1}}) // Sort by created_at desc
// Only active customers by default
filter := bson.M{"status": "active"}
cursor, err := r.collection.Find(ctx, filter, opts)
if err != nil {
r.logger.Error("Failed to list customers", map[string]interface{}{
"error": err.Error(),
"limit": limit,
"offset": offset,
})
return nil, err
}
defer cursor.Close(ctx)
if err = cursor.All(ctx, &customers); err != nil {
r.logger.Error("Failed to decode customers", map[string]interface{}{
"error": err.Error(),
})
return nil, err
}
return customers, nil
}
// Search retrieves customers with search and filters
func (r *customerRepository) Search(ctx context.Context, search string, status *string, gender *string, companyID *uuid.UUID, limit, offset int, sortBy, sortOrder string) ([]*Customer, error) {
var customers []*Customer
// Build filter
filter := bson.M{}
if search != "" {
filter["$text"] = bson.M{"$search": search}
}
if status != nil {
filter["status"] = *status
}
if gender != nil {
filter["gender"] = *gender
}
if companyID != nil {
filter["company_id"] = *companyID
}
// Build options
opts := options.Find()
opts.SetLimit(int64(limit))
opts.SetSkip(int64(offset))
// Set sort
if sortBy != "" {
sortValue := 1
if sortOrder == "desc" {
sortValue = -1
}
opts.SetSort(bson.D{{Key: sortBy, Value: sortValue}})
} else {
opts.SetSort(bson.D{{Key: "created_at", Value: -1}}) // Default sort
}
cursor, err := r.collection.Find(ctx, filter, opts)
if err != nil {
r.logger.Error("Failed to search customers", map[string]interface{}{
"error": err.Error(),
"search": search,
})
return nil, err
}
defer cursor.Close(ctx)
if err = cursor.All(ctx, &customers); err != nil {
r.logger.Error("Failed to decode customers from search", map[string]interface{}{
"error": err.Error(),
})
return nil, err
}
return customers, nil
}
// GetByCompanyID retrieves customers by company ID with pagination
func (r *customerRepository) GetByCompanyID(ctx context.Context, companyID uuid.UUID, limit, offset int) ([]*Customer, error) {
var customers []*Customer
// Build options
opts := options.Find()
opts.SetLimit(int64(limit))
opts.SetSkip(int64(offset))
opts.SetSort(bson.D{{Key: "created_at", Value: -1}}) // Sort by created_at desc
// Filter by company ID and active status
filter := bson.M{
"company_id": companyID,
"status": "active",
}
cursor, err := r.collection.Find(ctx, filter, opts)
if err != nil {
r.logger.Error("Failed to get customers by company ID", map[string]interface{}{
"error": err.Error(),
"company_id": companyID.String(),
"limit": limit,
"offset": offset,
})
return nil, err
}
defer cursor.Close(ctx)
if err = cursor.All(ctx, &customers); err != nil {
r.logger.Error("Failed to decode customers by company", map[string]interface{}{
"error": err.Error(),
"company_id": companyID.String(),
})
return nil, err
}
return customers, nil
}
// AddDeviceToken adds a device token for push notifications
func (r *customerRepository) AddDeviceToken(ctx context.Context, id uuid.UUID, deviceToken DeviceToken) error {
// Set timestamps
now := time.Now().Unix()
deviceToken.CreatedAt = now
deviceToken.UpdatedAt = now
filter := bson.M{"_id": id}
update := bson.M{
"$push": bson.M{"device_tokens": deviceToken}, // Use $push to add new token
"$set": bson.M{"updated_at": now},
}
result, err := r.collection.UpdateOne(ctx, filter, update)
if err != nil {
r.logger.Error("Failed to add device token", map[string]interface{}{
"error": err.Error(),
"customer_id": id.String(),
"token": deviceToken.Token,
})
return err
}
if result.MatchedCount == 0 {
return errors.New("customer not found")
}
r.logger.Info("Device token added successfully", map[string]interface{}{
"customer_id": id.String(),
"token": deviceToken.Token,
"device_type": deviceToken.DeviceType,
})
return nil
}
// RemoveDeviceToken removes a device token
func (r *customerRepository) RemoveDeviceToken(ctx context.Context, id uuid.UUID, token string) error {
filter := bson.M{"_id": id}
update := bson.M{
"$pull": bson.M{"device_tokens": bson.M{"token": token}}, // Use $pull to remove the token
"$set": bson.M{"updated_at": time.Now().Unix()},
}
result, err := r.collection.UpdateOne(ctx, filter, update)
if err != nil {
r.logger.Error("Failed to remove device token", map[string]interface{}{
"error": err.Error(),
"customer_id": id.String(),
"token": token,
})
return err
}
if result.MatchedCount == 0 {
return errors.New("customer not found")
}
r.logger.Info("Device token removed successfully", map[string]interface{}{
"customer_id": id.String(),
"token": token,
})
return nil
}
// UpdateLastLogin updates the last login timestamp
func (r *customerRepository) UpdateLastLogin(ctx context.Context, id uuid.UUID) error {
filter := bson.M{"_id": id}
update := bson.M{
"$set": bson.M{
"last_login_at": time.Now().Unix(),
"updated_at": time.Now().Unix(),
},
}
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")
}
r.logger.Info("Last login updated successfully", map[string]interface{}{
"customer_id": id.String(),
})
return nil
}
// GetAllDeviceTokens retrieves all device tokens for push notifications
func (r *customerRepository) GetAllDeviceTokens(ctx context.Context) ([]DeviceToken, error) {
var customers []*Customer
// Get all active customers
filter := bson.M{"status": "active"}
opts := options.Find().SetProjection(bson.M{"device_tokens": 1})
cursor, err := r.collection.Find(ctx, filter, opts)
if err != nil {
r.logger.Error("Failed to get all device tokens", map[string]interface{}{
"error": err.Error(),
})
return nil, err
}
defer cursor.Close(ctx)
if err = cursor.All(ctx, &customers); err != nil {
r.logger.Error("Failed to decode customers for device tokens", map[string]interface{}{
"error": err.Error(),
})
return nil, err
}
// Collect all device tokens
var allTokens []DeviceToken
for _, customer := range customers {
allTokens = append(allTokens, customer.DeviceTokens...)
}
r.logger.Info("Retrieved all device tokens", map[string]interface{}{
"total_tokens": len(allTokens),
})
return allTokens, nil
}
-523
View File
@@ -1,523 +0,0 @@
package customer
import (
"context"
"crypto/rand"
"encoding/hex"
"errors"
"strings"
"time"
"tm/pkg/logger"
"github.com/google/uuid"
"golang.org/x/crypto/bcrypt"
)
// Service defines business logic for customer operations
type Service interface {
RegisterCustomer(ctx context.Context, form *RegisterCustomerForm) (*Customer, error)
Login(ctx context.Context, form *LoginForm) (*AuthResponse, error)
RefreshToken(ctx context.Context, refreshToken string) (*AuthResponse, error)
ChangePassword(ctx context.Context, customerID uuid.UUID, form *ChangePasswordForm) error
GetCustomerByID(ctx context.Context, id uuid.UUID) (*Customer, error)
UpdateCustomer(ctx context.Context, id uuid.UUID, form *UpdateCustomerForm) (*Customer, error)
AddDeviceToken(ctx context.Context, customerID uuid.UUID, form *AddDeviceTokenForm) error
RemoveDeviceToken(ctx context.Context, customerID uuid.UUID, form *RemoveDeviceTokenForm) error
ListCustomers(ctx context.Context, form *ListCustomersForm) ([]*Customer, int, error)
UpdateCustomerStatus(ctx context.Context, id uuid.UUID, form *UpdateCustomerStatusForm) error
GetAllDeviceTokens(ctx context.Context) ([]DeviceToken, error)
Logout(ctx context.Context, customerID uuid.UUID, deviceToken string) error
}
// customerService implements the Service interface
type customerService struct {
repository Repository
logger logger.Logger
}
// NewCustomerService creates a new customer service
func NewCustomerService(repository Repository, logger logger.Logger) Service {
return &customerService{
repository: repository,
logger: logger,
}
}
// RegisterCustomer registers a new customer
func (s *customerService) RegisterCustomer(ctx context.Context, form *RegisterCustomerForm) (*Customer, error) {
// Check if email already exists
existingCustomer, _ := s.repository.GetByEmail(ctx, form.Email)
if existingCustomer != nil {
return nil, errors.New("email already exists")
}
// Check if username already exists
existingCustomer, _ = s.repository.GetByUsername(ctx, form.Username)
if existingCustomer != nil {
return nil, errors.New("username already exists")
}
// Check if mobile already exists
existingCustomer, _ = s.repository.GetByMobile(ctx, form.Mobile)
if existingCustomer != nil {
return nil, errors.New("mobile number already exists")
}
// Hash password
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(form.Password), bcrypt.DefaultCost)
if err != nil {
s.logger.Error("Failed to hash password", map[string]interface{}{
"error": err.Error(),
})
return nil, errors.New("failed to process password")
}
// Parse optional fields
var companyID *uuid.UUID
if form.CompanyID != nil {
parsedID, err := uuid.Parse(*form.CompanyID)
if err != nil {
return nil, errors.New("invalid company ID")
}
companyID = &parsedID
}
var gender *Gender
if form.Gender != nil {
g := Gender(*form.Gender)
gender = &g
}
// Create customer
customer := &Customer{
ID: uuid.New(),
FullName: form.FullName,
Username: form.Username,
Email: form.Email,
Mobile: form.Mobile,
Password: string(hashedPassword),
NationalID: form.NationalID,
Gender: gender,
Birthdate: form.Birthdate,
ProfileImage: form.ProfileImage,
Status: CustomerStatusActive,
CompanyID: companyID,
IsVerified: false,
DeviceTokens: []DeviceToken{},
}
// Save to database
err = s.repository.Create(ctx, customer)
if err != nil {
s.logger.Error("Failed to create customer", map[string]interface{}{
"error": err.Error(),
"email": form.Email,
"username": form.Username,
})
return nil, err
}
s.logger.Info("Customer registered successfully", map[string]interface{}{
"customer_id": customer.ID.String(),
"email": customer.Email,
"username": customer.Username,
})
return customer, nil
}
// Login authenticates a customer
func (s *customerService) Login(ctx context.Context, form *LoginForm) (*AuthResponse, error) {
// Find customer by email or mobile
var customer *Customer
var err error
if strings.Contains(form.EmailOrMobile, "@") {
customer, err = s.repository.GetByEmail(ctx, form.EmailOrMobile)
} else {
customer, err = s.repository.GetByMobile(ctx, form.EmailOrMobile)
}
if err != nil {
s.logger.Warn("Login attempt with invalid credentials", map[string]interface{}{
"email_or_mobile": form.EmailOrMobile,
"error": err.Error(),
})
return nil, errors.New("invalid credentials")
}
// Check if customer is active
if customer.Status != CustomerStatusActive {
return nil, errors.New("account is inactive")
}
// Verify password
err = bcrypt.CompareHashAndPassword([]byte(customer.Password), []byte(form.Password))
if err != nil {
s.logger.Warn("Login attempt with wrong password", map[string]interface{}{
"customer_id": customer.ID.String(),
"email": customer.Email,
})
return nil, errors.New("invalid credentials")
}
// Update last login
err = s.repository.UpdateLastLogin(ctx, customer.ID)
if err != nil {
s.logger.Error("Failed to update last login", map[string]interface{}{
"error": err.Error(),
"customer_id": customer.ID.String(),
})
}
// Generate tokens
accessToken, refreshToken, expiresAt, err := s.generateTokens(customer.ID)
if err != nil {
s.logger.Error("Failed to generate tokens", map[string]interface{}{
"error": err.Error(),
"customer_id": customer.ID.String(),
})
return nil, errors.New("failed to generate authentication tokens")
}
s.logger.Info("Customer logged in successfully", map[string]interface{}{
"customer_id": customer.ID.String(),
"email": customer.Email,
})
return &AuthResponse{
Customer: customer.ToResponse(),
AccessToken: accessToken,
RefreshToken: refreshToken,
ExpiresAt: expiresAt,
}, nil
}
// RefreshToken refreshes the access token
func (s *customerService) RefreshToken(ctx context.Context, refreshToken string) (*AuthResponse, error) {
// TODO: Implement JWT refresh token validation
// For now, we'll return an error
return nil, errors.New("refresh token functionality not implemented")
}
// ChangePassword changes customer password
func (s *customerService) ChangePassword(ctx context.Context, customerID uuid.UUID, form *ChangePasswordForm) error {
// Get customer
customer, err := s.repository.GetByID(ctx, customerID)
if err != nil {
return errors.New("customer not found")
}
// Verify old password
err = bcrypt.CompareHashAndPassword([]byte(customer.Password), []byte(form.OldPassword))
if err != nil {
return errors.New("invalid old password")
}
// Hash new password
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(form.NewPassword), bcrypt.DefaultCost)
if err != nil {
s.logger.Error("Failed to hash new password", map[string]interface{}{
"error": err.Error(),
"customer_id": customerID.String(),
})
return errors.New("failed to process new password")
}
// Update password
customer.Password = string(hashedPassword)
err = s.repository.Update(ctx, customer)
if err != nil {
s.logger.Error("Failed to update password", map[string]interface{}{
"error": err.Error(),
"customer_id": customerID.String(),
})
return errors.New("failed to update password")
}
s.logger.Info("Password changed successfully", map[string]interface{}{
"customer_id": customerID.String(),
})
return nil
}
// GetCustomerByID retrieves a customer by ID
func (s *customerService) GetCustomerByID(ctx context.Context, id uuid.UUID) (*Customer, error) {
customer, err := s.repository.GetByID(ctx, id)
if err != nil {
return nil, err
}
return customer, nil
}
// UpdateCustomer updates customer information
func (s *customerService) UpdateCustomer(ctx context.Context, id uuid.UUID, form *UpdateCustomerForm) (*Customer, error) {
// Get customer
customer, err := s.repository.GetByID(ctx, id)
if err != nil {
return nil, errors.New("customer not found")
}
// Update fields if provided
if form.FullName != nil {
customer.FullName = *form.FullName
}
if form.Username != nil {
// Check if username already exists
existingCustomer, _ := s.repository.GetByUsername(ctx, *form.Username)
if existingCustomer != nil && existingCustomer.ID != id {
return nil, errors.New("username already exists")
}
customer.Username = *form.Username
}
if form.Email != nil {
// Check if email already exists
existingCustomer, _ := s.repository.GetByEmail(ctx, *form.Email)
if existingCustomer != nil && existingCustomer.ID != id {
return nil, errors.New("email already exists")
}
customer.Email = *form.Email
}
if form.Mobile != nil {
// Check if mobile already exists
existingCustomer, _ := s.repository.GetByMobile(ctx, *form.Mobile)
if existingCustomer != nil && existingCustomer.ID != id {
return nil, errors.New("mobile number already exists")
}
customer.Mobile = *form.Mobile
}
if form.NationalID != nil {
customer.NationalID = form.NationalID
}
if form.Gender != nil {
g := Gender(*form.Gender)
customer.Gender = &g
}
if form.Birthdate != nil {
customer.Birthdate = form.Birthdate
}
if form.ProfileImage != nil {
customer.ProfileImage = form.ProfileImage
}
if form.Status != nil {
customer.Status = CustomerStatus(*form.Status)
}
// Update in database
err = s.repository.Update(ctx, customer)
if err != nil {
s.logger.Error("Failed to update customer", map[string]interface{}{
"error": err.Error(),
"customer_id": id.String(),
})
return nil, errors.New("failed to update customer")
}
s.logger.Info("Customer updated successfully", map[string]interface{}{
"customer_id": id.String(),
})
return customer, nil
}
// AddDeviceToken adds a device token for push notifications
func (s *customerService) AddDeviceToken(ctx context.Context, customerID uuid.UUID, form *AddDeviceTokenForm) error {
// Verify customer exists
_, err := s.repository.GetByID(ctx, customerID)
if err != nil {
return errors.New("customer not found")
}
// Create device token
deviceToken := DeviceToken{
Token: form.DeviceToken,
DeviceType: DeviceType(form.DeviceType),
}
// Add to database
err = s.repository.AddDeviceToken(ctx, customerID, deviceToken)
if err != nil {
s.logger.Error("Failed to add device token", map[string]interface{}{
"error": err.Error(),
"customer_id": customerID.String(),
"token": form.DeviceToken,
})
return errors.New("failed to add device token")
}
s.logger.Info("Device token added successfully", map[string]interface{}{
"customer_id": customerID.String(),
"token": form.DeviceToken,
"device_type": form.DeviceType,
})
return nil
}
// RemoveDeviceToken removes a device token
func (s *customerService) RemoveDeviceToken(ctx context.Context, customerID uuid.UUID, form *RemoveDeviceTokenForm) error {
// Remove from database
err := s.repository.RemoveDeviceToken(ctx, customerID, form.DeviceToken)
if err != nil {
s.logger.Error("Failed to remove device token", map[string]interface{}{
"error": err.Error(),
"customer_id": customerID.String(),
"token": form.DeviceToken,
})
return errors.New("failed to remove device token")
}
s.logger.Info("Device token removed successfully", map[string]interface{}{
"customer_id": customerID.String(),
"token": form.DeviceToken,
})
return nil
}
// ListCustomers lists customers with search and filters
func (s *customerService) ListCustomers(ctx context.Context, form *ListCustomersForm) ([]*Customer, int, error) {
// Set defaults
limit := 20
if form.Limit != nil {
limit = *form.Limit
}
offset := 0
if form.Offset != nil {
offset = *form.Offset
}
search := ""
if form.Search != nil {
search = *form.Search
}
sortBy := "created_at"
if form.SortBy != nil {
sortBy = *form.SortBy
}
sortOrder := "desc"
if form.SortOrder != nil {
sortOrder = *form.SortOrder
}
// Get customers
customers, err := s.repository.Search(ctx, search, form.Status, form.Gender, nil, limit, offset, sortBy, sortOrder)
if err != nil {
s.logger.Error("Failed to list customers", map[string]interface{}{
"error": err.Error(),
})
return nil, 0, errors.New("failed to list customers")
}
// TODO: Implement count for pagination metadata
total := len(customers) // This should be a separate count query
return customers, total, nil
}
// UpdateCustomerStatus updates customer status
func (s *customerService) UpdateCustomerStatus(ctx context.Context, id uuid.UUID, form *UpdateCustomerStatusForm) error {
// Get customer
customer, err := s.repository.GetByID(ctx, id)
if err != nil {
return errors.New("customer not found")
}
// Update status
customer.Status = CustomerStatus(form.Status)
// Update in database
err = s.repository.Update(ctx, customer)
if err != nil {
s.logger.Error("Failed to update customer status", map[string]interface{}{
"error": err.Error(),
"customer_id": id.String(),
"status": form.Status,
})
return errors.New("failed to update customer status")
}
s.logger.Info("Customer status updated successfully", map[string]interface{}{
"customer_id": id.String(),
"status": form.Status,
})
return nil
}
// GetAllDeviceTokens retrieves all device tokens for push notifications
func (s *customerService) GetAllDeviceTokens(ctx context.Context) ([]DeviceToken, error) {
tokens, err := s.repository.GetAllDeviceTokens(ctx)
if err != nil {
s.logger.Error("Failed to get all device tokens", map[string]interface{}{
"error": err.Error(),
})
return nil, errors.New("failed to get device tokens")
}
return tokens, nil
}
// Logout removes device token and logs the action
func (s *customerService) Logout(ctx context.Context, customerID uuid.UUID, deviceToken string) error {
// Remove device token
err := s.repository.RemoveDeviceToken(ctx, customerID, deviceToken)
if err != nil {
s.logger.Error("Failed to remove device token on logout", map[string]interface{}{
"error": err.Error(),
"customer_id": customerID.String(),
"token": deviceToken,
})
return errors.New("failed to logout")
}
s.logger.Info("Customer logged out successfully", map[string]interface{}{
"customer_id": customerID.String(),
"token": deviceToken,
})
return nil
}
// generateTokens generates access and refresh tokens
func (s *customerService) generateTokens(customerID uuid.UUID) (string, string, int64, error) {
// Generate access token (simple implementation for now)
accessToken, err := s.generateRandomToken(32)
if err != nil {
return "", "", 0, err
}
// Generate refresh token
refreshToken, err := s.generateRandomToken(64)
if err != nil {
return "", "", 0, err
}
// Set expiration (1 hour from now)
expiresAt := time.Now().Add(1 * time.Hour).Unix()
return accessToken, refreshToken, expiresAt, nil
}
// generateRandomToken generates a random token
func (s *customerService) generateRandomToken(length int) (string, error) {
bytes := make([]byte, length)
_, err := rand.Read(bytes)
if err != nil {
return "", err
}
return hex.EncodeToString(bytes), nil
}
-32
View File
@@ -1,32 +0,0 @@
package customer
import (
"time"
"github.com/asaskevich/govalidator"
)
// init registers custom validators
func init() {
// Register custom validators
govalidator.CustomTypeTagMap.Set("unix_timestamp", govalidator.CustomTypeValidator(func(i interface{}, context interface{}) bool {
switch v := i.(type) {
case int64:
// Check if timestamp is reasonable (not too old, not in the future)
now := time.Now().Unix()
minTimestamp := now - (100 * 365 * 24 * 60 * 60) // 100 years ago
maxTimestamp := now + (10 * 365 * 24 * 60 * 60) // 10 years in future
return v >= minTimestamp && v <= maxTimestamp
case *int64:
if v == nil {
return true // nil is valid for optional fields
}
now := time.Now().Unix()
minTimestamp := now - (100 * 365 * 24 * 60 * 60) // 100 years ago
maxTimestamp := now + (10 * 365 * 24 * 60 * 60) // 10 years in future
return *v >= minTimestamp && *v <= maxTimestamp
default:
return false
}
}))
}
+78 -68
View File
@@ -31,28 +31,25 @@ func NewUserHandler(service Service, logger logger.Logger, validator ValidationS
// RegisterRoutes registers user routes
func (h *Handler) RegisterRoutes(e *echo.Echo) {
v1 := e.Group("/api/v1")
v1 := e.Group("/admin/v1")
userGroup := v1.Group("/users")
{
// Public routes
userGroup.POST("/login", h.Login)
userGroup.POST("/refresh-token", h.RefreshToken)
userGroup.POST("/reset-password", h.ResetPassword)
// Public routes (no authentication required)
authorizationGP := v1.Group("")
authorizationGP.POST("/login", h.Login)
authorizationGP.POST("/refresh-token", h.RefreshToken)
authorizationGP.POST("/reset-password", h.ResetPassword)
// Protected routes (require authentication)
authGroup := userGroup.Group("")
// authGroup.Use(h.AuthMiddleware())
{
authGroup.GET("/profile", h.GetProfile)
authGroup.PUT("/profile", h.UpdateProfile)
authGroup.PUT("/change-password", h.ChangePassword)
authGroup.POST("/logout", h.Logout)
profileGP := v1.Group("")
profileGP.Use(h.AuthMiddleware())
profileGP.GET("/profile", h.GetProfile)
profileGP.PUT("/profile", h.UpdateProfile)
profileGP.PUT("/change-password", h.ChangePassword)
profileGP.DELETE("/logout", h.Logout)
// Admin routes
adminGroup := authGroup.Group("/admin")
// adminGroup.Use(h.AdminMiddleware())
{
// Admin routes (require admin role)
adminGroup := v1.Group("/users")
adminGroup.Use(h.AuthMiddleware(), h.AdminMiddleware())
adminGroup.POST("", h.CreateUser)
adminGroup.GET("", h.ListUsers)
adminGroup.GET("/:id", h.GetUserByID)
@@ -62,15 +59,12 @@ func (h *Handler) RegisterRoutes(e *echo.Echo) {
adminGroup.PUT("/:id/role", h.UpdateUserRole)
adminGroup.GET("/company/:company_id", h.GetUsersByCompanyID)
adminGroup.GET("/role/:role", h.GetUsersByRole)
}
}
}
}
// Login handles user login
// @Summary Login user
// @Description Authenticate user with email/username and password
// @Tags users
// @Description Authenticate user with username and password
// @Tags Authorization
// @Accept json
// @Produce json
// @Param login body LoginForm true "Login credentials"
@@ -78,7 +72,7 @@ func (h *Handler) RegisterRoutes(e *echo.Echo) {
// @Failure 400 {object} response.APIResponse
// @Failure 401 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /users/login [post]
// @Router /login [post]
func (h *Handler) Login(c echo.Context) error {
form, err := response.Parse[LoginForm](c)
if err != nil {
@@ -97,7 +91,7 @@ func (h *Handler) Login(c echo.Context) error {
// RefreshToken handles token refresh
// @Summary Refresh access token
// @Description Refresh access token using refresh token
// @Tags users
// @Tags Authorization
// @Accept json
// @Produce json
// @Param refresh body RefreshTokenForm true "Refresh token"
@@ -105,7 +99,7 @@ func (h *Handler) Login(c echo.Context) error {
// @Failure 400 {object} response.APIResponse
// @Failure 401 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /users/refresh-token [post]
// @Router /refresh-token [post]
func (h *Handler) RefreshToken(c echo.Context) error {
form, err := response.Parse[RefreshTokenForm](c)
if err != nil {
@@ -123,14 +117,14 @@ func (h *Handler) RefreshToken(c echo.Context) error {
// ResetPassword handles password reset request
// @Summary Reset password
// @Description Send password reset email to user
// @Tags users
// @Tags Authorization
// @Accept json
// @Produce json
// @Param reset body ResetPasswordForm true "Password reset request"
// @Success 200 {object} response.APIResponse
// @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /users/reset-password [post]
// @Router /reset-password [post]
func (h *Handler) ResetPassword(c echo.Context) error {
form, err := response.Parse[ResetPasswordForm](c)
if err != nil {
@@ -150,7 +144,7 @@ func (h *Handler) ResetPassword(c echo.Context) error {
// GetProfile gets current user profile
// @Summary Get user profile
// @Description Get current user profile information
// @Tags users
// @Tags Users
// @Accept json
// @Produce json
// @Security BearerAuth
@@ -158,7 +152,7 @@ func (h *Handler) ResetPassword(c echo.Context) error {
// @Failure 401 {object} response.APIResponse
// @Failure 404 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /users/profile [get]
// @Router /profile [get]
func (h *Handler) GetProfile(c echo.Context) error {
// Extract user ID from JWT token context
userID, err := GetUserIDFromContext(c)
@@ -177,7 +171,7 @@ func (h *Handler) GetProfile(c echo.Context) error {
// UpdateProfile updates current user profile
// @Summary Update user profile
// @Description Update current user profile information
// @Tags users
// @Tags Users
// @Accept json
// @Produce json
// @Security BearerAuth
@@ -186,10 +180,13 @@ func (h *Handler) GetProfile(c echo.Context) error {
// @Failure 400 {object} response.APIResponse
// @Failure 401 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /users/profile [put]
// @Router /profile [put]
func (h *Handler) UpdateProfile(c echo.Context) error {
// TODO: Extract user ID from JWT token
userID := uuid.New() // Placeholder - should be extracted from JWT
// Extract user ID from JWT token context
userID, err := GetUserIDFromContext(c)
if err != nil {
return response.Unauthorized(c, "User not authenticated")
}
form, err := response.Parse[UpdateUserForm](c)
if err != nil {
@@ -207,7 +204,7 @@ func (h *Handler) UpdateProfile(c echo.Context) error {
// ChangePassword changes current user password
// @Summary Change password
// @Description Change current user password
// @Tags users
// @Tags Users
// @Accept json
// @Produce json
// @Security BearerAuth
@@ -216,10 +213,12 @@ func (h *Handler) UpdateProfile(c echo.Context) error {
// @Failure 400 {object} response.APIResponse
// @Failure 401 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /users/change-password [put]
// @Router /change-password [put]
func (h *Handler) ChangePassword(c echo.Context) error {
// TODO: Extract user ID from JWT token
userID := uuid.New() // Placeholder - should be extracted from JWT
userID, err := GetUserIDFromContext(c)
if err != nil {
return response.Unauthorized(c, "User not authenticated")
}
form, err := response.Parse[ChangePasswordForm](c)
if err != nil {
@@ -239,14 +238,14 @@ func (h *Handler) ChangePassword(c echo.Context) error {
// Logout handles user logout
// @Summary Logout user
// @Description Logout current user and invalidate tokens
// @Tags users
// @Tags Users
// @Accept json
// @Produce json
// @Security BearerAuth
// @Success 200 {object} response.APIResponse
// @Failure 401 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /users/logout [post]
// @Router /logout [delete]
func (h *Handler) Logout(c echo.Context) error {
// Extract user ID and access token from context
userID, err := GetUserIDFromContext(c)
@@ -272,7 +271,7 @@ func (h *Handler) Logout(c echo.Context) error {
// CreateUser creates a new user (admin only)
// @Summary Create user
// @Description Create a new user (admin only)
// @Tags users
// @Tags Users
// @Accept json
// @Produce json
// @Security BearerAuth
@@ -282,7 +281,7 @@ func (h *Handler) Logout(c echo.Context) error {
// @Failure 401 {object} response.APIResponse
// @Failure 403 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /users/admin [post]
// @Router /users [post]
func (h *Handler) CreateUser(c echo.Context) error {
// Get current user ID from JWT token
currentUserID, err := GetUserIDFromContext(c)
@@ -312,7 +311,7 @@ func (h *Handler) CreateUser(c echo.Context) error {
// ListUsers lists users with search and filters (admin only)
// @Summary List users
// @Description List users with search and filters (admin only)
// @Tags users
// @Tags Users
// @Accept json
// @Produce json
// @Security BearerAuth
@@ -329,7 +328,7 @@ func (h *Handler) CreateUser(c echo.Context) error {
// @Failure 401 {object} response.APIResponse
// @Failure 403 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /users/admin [get]
// @Router /users [get]
func (h *Handler) ListUsers(c echo.Context) error {
var form ListUsersForm
if err := c.Bind(&form); err != nil {
@@ -353,7 +352,7 @@ func (h *Handler) ListUsers(c echo.Context) error {
// GetUserByID gets a user by ID (admin only)
// @Summary Get user by ID
// @Description Get user details by ID (admin only)
// @Tags users
// @Tags Users
// @Accept json
// @Produce json
// @Security BearerAuth
@@ -364,7 +363,7 @@ func (h *Handler) ListUsers(c echo.Context) error {
// @Failure 403 {object} response.APIResponse
// @Failure 404 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /users/admin/{id} [get]
// @Router /users/{id} [get]
func (h *Handler) GetUserByID(c echo.Context) error {
idStr := c.Param("id")
userID, err := uuid.Parse(idStr)
@@ -383,7 +382,7 @@ func (h *Handler) GetUserByID(c echo.Context) error {
// UpdateUser updates a user (admin only)
// @Summary Update user
// @Description Update user information (admin only)
// @Tags users
// @Tags Users
// @Accept json
// @Produce json
// @Security BearerAuth
@@ -395,10 +394,13 @@ func (h *Handler) GetUserByID(c echo.Context) error {
// @Failure 403 {object} response.APIResponse
// @Failure 404 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /users/admin/{id} [put]
// @Router /users/{id} [put]
func (h *Handler) UpdateUser(c echo.Context) error {
// TODO: Get current user ID from JWT token
currentUserID := uuid.New() // Placeholder
// Extract user ID from JWT token context
currentUserID, err := GetUserIDFromContext(c)
if err != nil {
return response.Unauthorized(c, "User not authenticated")
}
idStr := c.Param("id")
userID, err := uuid.Parse(idStr)
@@ -428,7 +430,7 @@ func (h *Handler) UpdateUser(c echo.Context) error {
// DeleteUser deletes a user (admin only)
// @Summary Delete user
// @Description Delete a user (admin only)
// @Tags users
// @Tags Users
// @Accept json
// @Produce json
// @Security BearerAuth
@@ -439,10 +441,13 @@ func (h *Handler) UpdateUser(c echo.Context) error {
// @Failure 403 {object} response.APIResponse
// @Failure 404 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /users/admin/{id} [delete]
// @Router /users/{id} [delete]
func (h *Handler) DeleteUser(c echo.Context) error {
// TODO: Get current user ID from JWT token
currentUserID := uuid.New() // Placeholder
// Extract user ID from JWT token context
currentUserID, err := GetUserIDFromContext(c)
if err != nil {
return response.Unauthorized(c, "User not authenticated")
}
idStr := c.Param("id")
userID, err := uuid.Parse(idStr)
@@ -463,7 +468,7 @@ func (h *Handler) DeleteUser(c echo.Context) error {
// UpdateUserStatus updates user status (admin only)
// @Summary Update user status
// @Description Update user account status (admin only)
// @Tags users
// @Tags Users
// @Accept json
// @Produce json
// @Security BearerAuth
@@ -475,10 +480,12 @@ func (h *Handler) DeleteUser(c echo.Context) error {
// @Failure 403 {object} response.APIResponse
// @Failure 404 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /users/admin/{id}/status [put]
// @Router /users/{id}/status [put]
func (h *Handler) UpdateUserStatus(c echo.Context) error {
// TODO: Get current user ID from JWT token
currentUserID := uuid.New() // Placeholder
currentUserID, err := GetUserIDFromContext(c)
if err != nil {
return response.Unauthorized(c, "User not authenticated")
}
idStr := c.Param("id")
userID, err := uuid.Parse(idStr)
@@ -510,7 +517,7 @@ func (h *Handler) UpdateUserStatus(c echo.Context) error {
// UpdateUserRole updates user role (admin only)
// @Summary Update user role
// @Description Update user role (admin only)
// @Tags users
// @Tags Users
// @Accept json
// @Produce json
// @Security BearerAuth
@@ -522,10 +529,13 @@ func (h *Handler) UpdateUserStatus(c echo.Context) error {
// @Failure 403 {object} response.APIResponse
// @Failure 404 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /users/admin/{id}/role [put]
// @Router /users/{id}/role [put]
func (h *Handler) UpdateUserRole(c echo.Context) error {
// TODO: Get current user ID from JWT token
currentUserID := uuid.New() // Placeholder
// Extract user ID from JWT token context
currentUserID, err := GetUserIDFromContext(c)
if err != nil {
return response.Unauthorized(c, "User not authenticated")
}
idStr := c.Param("id")
userID, err := uuid.Parse(idStr)
@@ -557,19 +567,19 @@ func (h *Handler) UpdateUserRole(c echo.Context) error {
// GetUsersByCompanyID gets users by company ID (admin only)
// @Summary Get users by company ID
// @Description Get users belonging to a specific company (admin only)
// @Tags users
// @Tags Users
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param company_id path string true "Company ID"
// @Param limit query int false "Limit results"
// @Param offset query int false "Offset results"
// @Success 200 {object} response.APIResponse
// @Success 200 {object} response.APIResponse{data=map[string]interface{}} "Users with pagination metadata"
// @Failure 400 {object} response.APIResponse
// @Failure 401 {object} response.APIResponse
// @Failure 403 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /users/admin/company/{company_id} [get]
// @Router /users/company/{company_id} [get]
func (h *Handler) GetUsersByCompanyID(c echo.Context) error {
companyIDStr := c.Param("company_id")
companyID, err := uuid.Parse(companyIDStr)
@@ -621,19 +631,19 @@ func (h *Handler) GetUsersByCompanyID(c echo.Context) error {
// GetUsersByRole gets users by role (admin only)
// @Summary Get users by role
// @Description Get users with a specific role (admin only)
// @Tags users
// @Tags Users
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param role path string true "User role"
// @Param limit query int false "Limit results"
// @Param offset query int false "Offset results"
// @Success 200 {object} response.APIResponse
// @Success 200 {object} response.APIResponse{data=map[string]interface{}} "Users with pagination metadata"
// @Failure 400 {object} response.APIResponse
// @Failure 401 {object} response.APIResponse
// @Failure 403 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /users/admin/role/{role} [get]
// @Router /users/role/{role} [get]
func (h *Handler) GetUsersByRole(c echo.Context) error {
roleStr := c.Param("role")
role := UserRole(roleStr)