Merge branch 'hotfix/improvment-struct'
This commit is contained in:
@@ -116,13 +116,7 @@ func initHTTPServer(conf infra.Config, log logger.Logger) *echo.Echo {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Add health check endpoint
|
// Add health check endpoint
|
||||||
e.GET("/health", func(c echo.Context) error {
|
e.GET("/admin/v1/health", healthHandler)
|
||||||
return c.JSON(http.StatusOK, map[string]interface{}{
|
|
||||||
"status": "healthy",
|
|
||||||
"time": time.Now().Unix(),
|
|
||||||
"version": "1.0.0",
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// Add Swagger documentation endpoint
|
// Add Swagger documentation endpoint
|
||||||
e.GET("/swagger/*", echoSwagger.WrapHandler)
|
e.GET("/swagger/*", echoSwagger.WrapHandler)
|
||||||
|
|||||||
+194
-1186
File diff suppressed because it is too large
Load Diff
+194
-1186
File diff suppressed because it is too large
Load Diff
+191
-811
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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
|
package main
|
||||||
|
|
||||||
// @title Tender Management API
|
// @title Tender Management API
|
||||||
@@ -32,31 +13,27 @@ package main
|
|||||||
// @license.url http://www.apache.org/licenses/LICENSE-2.0.html
|
// @license.url http://www.apache.org/licenses/LICENSE-2.0.html
|
||||||
|
|
||||||
// @host localhost:8081
|
// @host localhost:8081
|
||||||
// @BasePath /api/v1
|
// @BasePath /admin/v1
|
||||||
|
|
||||||
// @securityDefinitions.apikey BearerAuth
|
// @securityDefinitions.apikey BearerAuth
|
||||||
// @in header
|
// @in header
|
||||||
// @name Authorization
|
// @name Authorization
|
||||||
// @description Type "Bearer" followed by a space and JWT token.
|
// @description Type "Bearer" followed by a space and JWT token.
|
||||||
|
|
||||||
// @tag.name customers
|
// @tag.name Users
|
||||||
// @tag.description Customer management operations
|
// @tag.description User management operations including authentication, profile management, and admin operations
|
||||||
|
|
||||||
// @tag.name users
|
// @tag.name Authorization
|
||||||
// @tag.description User management operations
|
// @tag.description Authentication operations including login, logout, and token management
|
||||||
|
|
||||||
// @tag.name health
|
// @tag.name Health
|
||||||
// @tag.description Health check operations
|
// @tag.description Health check operations
|
||||||
|
|
||||||
// @tag.name auth
|
|
||||||
// @tag.description Authentication operations
|
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
"tm/internal/customer"
|
|
||||||
"tm/internal/user"
|
"tm/internal/user"
|
||||||
|
|
||||||
_ "tm/cmd/web/docs" // This is generated by swag
|
_ "tm/cmd/web/docs" // This is generated by swag
|
||||||
@@ -92,8 +69,8 @@ func main() {
|
|||||||
|
|
||||||
// Initialize authorization service
|
// Initialize authorization service
|
||||||
authService := initAuthorizationService(conf.Auth.JWT, redisClient, logger)
|
authService := initAuthorizationService(conf.Auth.JWT, redisClient, logger)
|
||||||
|
|
||||||
// Initialize repositories with MongoDB connection manager
|
// Initialize repositories with MongoDB connection manager
|
||||||
customerRepository := customer.NewCustomerRepository(mongoManager, logger)
|
|
||||||
userRepository := user.NewUserRepository(mongoManager, logger)
|
userRepository := user.NewUserRepository(mongoManager, logger)
|
||||||
|
|
||||||
logger.Info("Repositories initialized successfully", map[string]interface{}{
|
logger.Info("Repositories initialized successfully", map[string]interface{}{
|
||||||
@@ -104,7 +81,6 @@ func main() {
|
|||||||
userValidator := user.NewValidationService()
|
userValidator := user.NewValidationService()
|
||||||
|
|
||||||
// Initialize services with repositories
|
// Initialize services with repositories
|
||||||
customerService := customer.NewCustomerService(customerRepository, logger)
|
|
||||||
userService := user.NewUserService(userRepository, logger, authService, userValidator)
|
userService := user.NewUserService(userRepository, logger, authService, userValidator)
|
||||||
|
|
||||||
logger.Info("Services initialized successfully", map[string]interface{}{
|
logger.Info("Services initialized successfully", map[string]interface{}{
|
||||||
@@ -112,7 +88,6 @@ func main() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Initialize handlers with services
|
// Initialize handlers with services
|
||||||
customerHandler := customer.NewHandler(customerService)
|
|
||||||
userHandler := user.NewUserHandler(userService, logger, userValidator, authService)
|
userHandler := user.NewUserHandler(userService, logger, userValidator, authService)
|
||||||
|
|
||||||
logger.Info("Handlers initialized successfully", map[string]interface{}{
|
logger.Info("Handlers initialized successfully", map[string]interface{}{
|
||||||
@@ -123,7 +98,6 @@ func main() {
|
|||||||
e := initHTTPServer(conf, logger)
|
e := initHTTPServer(conf, logger)
|
||||||
|
|
||||||
// Register routes
|
// Register routes
|
||||||
customerHandler.RegisterRoutes(e)
|
|
||||||
userHandler.RegisterRoutes(e)
|
userHandler.RegisterRoutes(e)
|
||||||
|
|
||||||
// Start server
|
// Start server
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -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"`
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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
@@ -31,28 +31,25 @@ func NewUserHandler(service Service, logger logger.Logger, validator ValidationS
|
|||||||
|
|
||||||
// RegisterRoutes registers user routes
|
// RegisterRoutes registers user routes
|
||||||
func (h *Handler) RegisterRoutes(e *echo.Echo) {
|
func (h *Handler) RegisterRoutes(e *echo.Echo) {
|
||||||
v1 := e.Group("/api/v1")
|
v1 := e.Group("/admin/v1")
|
||||||
|
|
||||||
userGroup := v1.Group("/users")
|
// Public routes (no authentication required)
|
||||||
{
|
authorizationGP := v1.Group("")
|
||||||
// Public routes
|
authorizationGP.POST("/login", h.Login)
|
||||||
userGroup.POST("/login", h.Login)
|
authorizationGP.POST("/refresh-token", h.RefreshToken)
|
||||||
userGroup.POST("/refresh-token", h.RefreshToken)
|
authorizationGP.POST("/reset-password", h.ResetPassword)
|
||||||
userGroup.POST("/reset-password", h.ResetPassword)
|
|
||||||
|
|
||||||
// Protected routes (require authentication)
|
// Protected routes (require authentication)
|
||||||
authGroup := userGroup.Group("")
|
profileGP := v1.Group("")
|
||||||
// authGroup.Use(h.AuthMiddleware())
|
profileGP.Use(h.AuthMiddleware())
|
||||||
{
|
profileGP.GET("/profile", h.GetProfile)
|
||||||
authGroup.GET("/profile", h.GetProfile)
|
profileGP.PUT("/profile", h.UpdateProfile)
|
||||||
authGroup.PUT("/profile", h.UpdateProfile)
|
profileGP.PUT("/change-password", h.ChangePassword)
|
||||||
authGroup.PUT("/change-password", h.ChangePassword)
|
profileGP.DELETE("/logout", h.Logout)
|
||||||
authGroup.POST("/logout", h.Logout)
|
|
||||||
|
|
||||||
// Admin routes
|
// Admin routes (require admin role)
|
||||||
adminGroup := authGroup.Group("/admin")
|
adminGroup := v1.Group("/users")
|
||||||
// adminGroup.Use(h.AdminMiddleware())
|
adminGroup.Use(h.AuthMiddleware(), h.AdminMiddleware())
|
||||||
{
|
|
||||||
adminGroup.POST("", h.CreateUser)
|
adminGroup.POST("", h.CreateUser)
|
||||||
adminGroup.GET("", h.ListUsers)
|
adminGroup.GET("", h.ListUsers)
|
||||||
adminGroup.GET("/:id", h.GetUserByID)
|
adminGroup.GET("/:id", h.GetUserByID)
|
||||||
@@ -63,14 +60,11 @@ func (h *Handler) RegisterRoutes(e *echo.Echo) {
|
|||||||
adminGroup.GET("/company/:company_id", h.GetUsersByCompanyID)
|
adminGroup.GET("/company/:company_id", h.GetUsersByCompanyID)
|
||||||
adminGroup.GET("/role/:role", h.GetUsersByRole)
|
adminGroup.GET("/role/:role", h.GetUsersByRole)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Login handles user login
|
// Login handles user login
|
||||||
// @Summary Login user
|
// @Summary Login user
|
||||||
// @Description Authenticate user with email/username and password
|
// @Description Authenticate user with username and password
|
||||||
// @Tags users
|
// @Tags Authorization
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param login body LoginForm true "Login credentials"
|
// @Param login body LoginForm true "Login credentials"
|
||||||
@@ -78,7 +72,7 @@ func (h *Handler) RegisterRoutes(e *echo.Echo) {
|
|||||||
// @Failure 400 {object} response.APIResponse
|
// @Failure 400 {object} response.APIResponse
|
||||||
// @Failure 401 {object} response.APIResponse
|
// @Failure 401 {object} response.APIResponse
|
||||||
// @Failure 500 {object} response.APIResponse
|
// @Failure 500 {object} response.APIResponse
|
||||||
// @Router /users/login [post]
|
// @Router /login [post]
|
||||||
func (h *Handler) Login(c echo.Context) error {
|
func (h *Handler) Login(c echo.Context) error {
|
||||||
form, err := response.Parse[LoginForm](c)
|
form, err := response.Parse[LoginForm](c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -97,7 +91,7 @@ func (h *Handler) Login(c echo.Context) error {
|
|||||||
// RefreshToken handles token refresh
|
// RefreshToken handles token refresh
|
||||||
// @Summary Refresh access token
|
// @Summary Refresh access token
|
||||||
// @Description Refresh access token using refresh token
|
// @Description Refresh access token using refresh token
|
||||||
// @Tags users
|
// @Tags Authorization
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param refresh body RefreshTokenForm true "Refresh token"
|
// @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 400 {object} response.APIResponse
|
||||||
// @Failure 401 {object} response.APIResponse
|
// @Failure 401 {object} response.APIResponse
|
||||||
// @Failure 500 {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 {
|
func (h *Handler) RefreshToken(c echo.Context) error {
|
||||||
form, err := response.Parse[RefreshTokenForm](c)
|
form, err := response.Parse[RefreshTokenForm](c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -123,14 +117,14 @@ func (h *Handler) RefreshToken(c echo.Context) error {
|
|||||||
// ResetPassword handles password reset request
|
// ResetPassword handles password reset request
|
||||||
// @Summary Reset password
|
// @Summary Reset password
|
||||||
// @Description Send password reset email to user
|
// @Description Send password reset email to user
|
||||||
// @Tags users
|
// @Tags Authorization
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param reset body ResetPasswordForm true "Password reset request"
|
// @Param reset body ResetPasswordForm true "Password reset request"
|
||||||
// @Success 200 {object} response.APIResponse
|
// @Success 200 {object} response.APIResponse
|
||||||
// @Failure 400 {object} response.APIResponse
|
// @Failure 400 {object} response.APIResponse
|
||||||
// @Failure 500 {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 {
|
func (h *Handler) ResetPassword(c echo.Context) error {
|
||||||
form, err := response.Parse[ResetPasswordForm](c)
|
form, err := response.Parse[ResetPasswordForm](c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -150,7 +144,7 @@ func (h *Handler) ResetPassword(c echo.Context) error {
|
|||||||
// GetProfile gets current user profile
|
// GetProfile gets current user profile
|
||||||
// @Summary Get user profile
|
// @Summary Get user profile
|
||||||
// @Description Get current user profile information
|
// @Description Get current user profile information
|
||||||
// @Tags users
|
// @Tags Users
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Security BearerAuth
|
// @Security BearerAuth
|
||||||
@@ -158,7 +152,7 @@ func (h *Handler) ResetPassword(c echo.Context) error {
|
|||||||
// @Failure 401 {object} response.APIResponse
|
// @Failure 401 {object} response.APIResponse
|
||||||
// @Failure 404 {object} response.APIResponse
|
// @Failure 404 {object} response.APIResponse
|
||||||
// @Failure 500 {object} response.APIResponse
|
// @Failure 500 {object} response.APIResponse
|
||||||
// @Router /users/profile [get]
|
// @Router /profile [get]
|
||||||
func (h *Handler) GetProfile(c echo.Context) error {
|
func (h *Handler) GetProfile(c echo.Context) error {
|
||||||
// Extract user ID from JWT token context
|
// Extract user ID from JWT token context
|
||||||
userID, err := GetUserIDFromContext(c)
|
userID, err := GetUserIDFromContext(c)
|
||||||
@@ -177,7 +171,7 @@ func (h *Handler) GetProfile(c echo.Context) error {
|
|||||||
// UpdateProfile updates current user profile
|
// UpdateProfile updates current user profile
|
||||||
// @Summary Update user profile
|
// @Summary Update user profile
|
||||||
// @Description Update current user profile information
|
// @Description Update current user profile information
|
||||||
// @Tags users
|
// @Tags Users
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Security BearerAuth
|
// @Security BearerAuth
|
||||||
@@ -186,10 +180,13 @@ func (h *Handler) GetProfile(c echo.Context) error {
|
|||||||
// @Failure 400 {object} response.APIResponse
|
// @Failure 400 {object} response.APIResponse
|
||||||
// @Failure 401 {object} response.APIResponse
|
// @Failure 401 {object} response.APIResponse
|
||||||
// @Failure 500 {object} response.APIResponse
|
// @Failure 500 {object} response.APIResponse
|
||||||
// @Router /users/profile [put]
|
// @Router /profile [put]
|
||||||
func (h *Handler) UpdateProfile(c echo.Context) error {
|
func (h *Handler) UpdateProfile(c echo.Context) error {
|
||||||
// TODO: Extract user ID from JWT token
|
// Extract user ID from JWT token context
|
||||||
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[UpdateUserForm](c)
|
form, err := response.Parse[UpdateUserForm](c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -207,7 +204,7 @@ func (h *Handler) UpdateProfile(c echo.Context) error {
|
|||||||
// ChangePassword changes current user password
|
// ChangePassword changes current user password
|
||||||
// @Summary Change password
|
// @Summary Change password
|
||||||
// @Description Change current user password
|
// @Description Change current user password
|
||||||
// @Tags users
|
// @Tags Users
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Security BearerAuth
|
// @Security BearerAuth
|
||||||
@@ -216,10 +213,12 @@ func (h *Handler) UpdateProfile(c echo.Context) error {
|
|||||||
// @Failure 400 {object} response.APIResponse
|
// @Failure 400 {object} response.APIResponse
|
||||||
// @Failure 401 {object} response.APIResponse
|
// @Failure 401 {object} response.APIResponse
|
||||||
// @Failure 500 {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 {
|
func (h *Handler) ChangePassword(c echo.Context) error {
|
||||||
// TODO: Extract user ID from JWT token
|
userID, err := GetUserIDFromContext(c)
|
||||||
userID := uuid.New() // Placeholder - should be extracted from JWT
|
if err != nil {
|
||||||
|
return response.Unauthorized(c, "User not authenticated")
|
||||||
|
}
|
||||||
|
|
||||||
form, err := response.Parse[ChangePasswordForm](c)
|
form, err := response.Parse[ChangePasswordForm](c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -239,14 +238,14 @@ func (h *Handler) ChangePassword(c echo.Context) error {
|
|||||||
// Logout handles user logout
|
// Logout handles user logout
|
||||||
// @Summary Logout user
|
// @Summary Logout user
|
||||||
// @Description Logout current user and invalidate tokens
|
// @Description Logout current user and invalidate tokens
|
||||||
// @Tags users
|
// @Tags Users
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Security BearerAuth
|
// @Security BearerAuth
|
||||||
// @Success 200 {object} response.APIResponse
|
// @Success 200 {object} response.APIResponse
|
||||||
// @Failure 401 {object} response.APIResponse
|
// @Failure 401 {object} response.APIResponse
|
||||||
// @Failure 500 {object} response.APIResponse
|
// @Failure 500 {object} response.APIResponse
|
||||||
// @Router /users/logout [post]
|
// @Router /logout [delete]
|
||||||
func (h *Handler) Logout(c echo.Context) error {
|
func (h *Handler) Logout(c echo.Context) error {
|
||||||
// Extract user ID and access token from context
|
// Extract user ID and access token from context
|
||||||
userID, err := GetUserIDFromContext(c)
|
userID, err := GetUserIDFromContext(c)
|
||||||
@@ -272,7 +271,7 @@ func (h *Handler) Logout(c echo.Context) error {
|
|||||||
// CreateUser creates a new user (admin only)
|
// CreateUser creates a new user (admin only)
|
||||||
// @Summary Create user
|
// @Summary Create user
|
||||||
// @Description Create a new user (admin only)
|
// @Description Create a new user (admin only)
|
||||||
// @Tags users
|
// @Tags Users
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Security BearerAuth
|
// @Security BearerAuth
|
||||||
@@ -282,7 +281,7 @@ func (h *Handler) Logout(c echo.Context) error {
|
|||||||
// @Failure 401 {object} response.APIResponse
|
// @Failure 401 {object} response.APIResponse
|
||||||
// @Failure 403 {object} response.APIResponse
|
// @Failure 403 {object} response.APIResponse
|
||||||
// @Failure 500 {object} response.APIResponse
|
// @Failure 500 {object} response.APIResponse
|
||||||
// @Router /users/admin [post]
|
// @Router /users [post]
|
||||||
func (h *Handler) CreateUser(c echo.Context) error {
|
func (h *Handler) CreateUser(c echo.Context) error {
|
||||||
// Get current user ID from JWT token
|
// Get current user ID from JWT token
|
||||||
currentUserID, err := GetUserIDFromContext(c)
|
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)
|
// ListUsers lists users with search and filters (admin only)
|
||||||
// @Summary List users
|
// @Summary List users
|
||||||
// @Description List users with search and filters (admin only)
|
// @Description List users with search and filters (admin only)
|
||||||
// @Tags users
|
// @Tags Users
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Security BearerAuth
|
// @Security BearerAuth
|
||||||
@@ -329,7 +328,7 @@ func (h *Handler) CreateUser(c echo.Context) error {
|
|||||||
// @Failure 401 {object} response.APIResponse
|
// @Failure 401 {object} response.APIResponse
|
||||||
// @Failure 403 {object} response.APIResponse
|
// @Failure 403 {object} response.APIResponse
|
||||||
// @Failure 500 {object} response.APIResponse
|
// @Failure 500 {object} response.APIResponse
|
||||||
// @Router /users/admin [get]
|
// @Router /users [get]
|
||||||
func (h *Handler) ListUsers(c echo.Context) error {
|
func (h *Handler) ListUsers(c echo.Context) error {
|
||||||
var form ListUsersForm
|
var form ListUsersForm
|
||||||
if err := c.Bind(&form); err != nil {
|
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)
|
// GetUserByID gets a user by ID (admin only)
|
||||||
// @Summary Get user by ID
|
// @Summary Get user by ID
|
||||||
// @Description Get user details by ID (admin only)
|
// @Description Get user details by ID (admin only)
|
||||||
// @Tags users
|
// @Tags Users
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Security BearerAuth
|
// @Security BearerAuth
|
||||||
@@ -364,7 +363,7 @@ func (h *Handler) ListUsers(c echo.Context) error {
|
|||||||
// @Failure 403 {object} response.APIResponse
|
// @Failure 403 {object} response.APIResponse
|
||||||
// @Failure 404 {object} response.APIResponse
|
// @Failure 404 {object} response.APIResponse
|
||||||
// @Failure 500 {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 {
|
func (h *Handler) GetUserByID(c echo.Context) error {
|
||||||
idStr := c.Param("id")
|
idStr := c.Param("id")
|
||||||
userID, err := uuid.Parse(idStr)
|
userID, err := uuid.Parse(idStr)
|
||||||
@@ -383,7 +382,7 @@ func (h *Handler) GetUserByID(c echo.Context) error {
|
|||||||
// UpdateUser updates a user (admin only)
|
// UpdateUser updates a user (admin only)
|
||||||
// @Summary Update user
|
// @Summary Update user
|
||||||
// @Description Update user information (admin only)
|
// @Description Update user information (admin only)
|
||||||
// @Tags users
|
// @Tags Users
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Security BearerAuth
|
// @Security BearerAuth
|
||||||
@@ -395,10 +394,13 @@ func (h *Handler) GetUserByID(c echo.Context) error {
|
|||||||
// @Failure 403 {object} response.APIResponse
|
// @Failure 403 {object} response.APIResponse
|
||||||
// @Failure 404 {object} response.APIResponse
|
// @Failure 404 {object} response.APIResponse
|
||||||
// @Failure 500 {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 {
|
func (h *Handler) UpdateUser(c echo.Context) error {
|
||||||
// TODO: Get current user ID from JWT token
|
// Extract user ID from JWT token context
|
||||||
currentUserID := uuid.New() // Placeholder
|
currentUserID, err := GetUserIDFromContext(c)
|
||||||
|
if err != nil {
|
||||||
|
return response.Unauthorized(c, "User not authenticated")
|
||||||
|
}
|
||||||
|
|
||||||
idStr := c.Param("id")
|
idStr := c.Param("id")
|
||||||
userID, err := uuid.Parse(idStr)
|
userID, err := uuid.Parse(idStr)
|
||||||
@@ -428,7 +430,7 @@ func (h *Handler) UpdateUser(c echo.Context) error {
|
|||||||
// DeleteUser deletes a user (admin only)
|
// DeleteUser deletes a user (admin only)
|
||||||
// @Summary Delete user
|
// @Summary Delete user
|
||||||
// @Description Delete a user (admin only)
|
// @Description Delete a user (admin only)
|
||||||
// @Tags users
|
// @Tags Users
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Security BearerAuth
|
// @Security BearerAuth
|
||||||
@@ -439,10 +441,13 @@ func (h *Handler) UpdateUser(c echo.Context) error {
|
|||||||
// @Failure 403 {object} response.APIResponse
|
// @Failure 403 {object} response.APIResponse
|
||||||
// @Failure 404 {object} response.APIResponse
|
// @Failure 404 {object} response.APIResponse
|
||||||
// @Failure 500 {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 {
|
func (h *Handler) DeleteUser(c echo.Context) error {
|
||||||
// TODO: Get current user ID from JWT token
|
// Extract user ID from JWT token context
|
||||||
currentUserID := uuid.New() // Placeholder
|
currentUserID, err := GetUserIDFromContext(c)
|
||||||
|
if err != nil {
|
||||||
|
return response.Unauthorized(c, "User not authenticated")
|
||||||
|
}
|
||||||
|
|
||||||
idStr := c.Param("id")
|
idStr := c.Param("id")
|
||||||
userID, err := uuid.Parse(idStr)
|
userID, err := uuid.Parse(idStr)
|
||||||
@@ -463,7 +468,7 @@ func (h *Handler) DeleteUser(c echo.Context) error {
|
|||||||
// UpdateUserStatus updates user status (admin only)
|
// UpdateUserStatus updates user status (admin only)
|
||||||
// @Summary Update user status
|
// @Summary Update user status
|
||||||
// @Description Update user account status (admin only)
|
// @Description Update user account status (admin only)
|
||||||
// @Tags users
|
// @Tags Users
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Security BearerAuth
|
// @Security BearerAuth
|
||||||
@@ -475,10 +480,12 @@ func (h *Handler) DeleteUser(c echo.Context) error {
|
|||||||
// @Failure 403 {object} response.APIResponse
|
// @Failure 403 {object} response.APIResponse
|
||||||
// @Failure 404 {object} response.APIResponse
|
// @Failure 404 {object} response.APIResponse
|
||||||
// @Failure 500 {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 {
|
func (h *Handler) UpdateUserStatus(c echo.Context) error {
|
||||||
// TODO: Get current user ID from JWT token
|
currentUserID, err := GetUserIDFromContext(c)
|
||||||
currentUserID := uuid.New() // Placeholder
|
if err != nil {
|
||||||
|
return response.Unauthorized(c, "User not authenticated")
|
||||||
|
}
|
||||||
|
|
||||||
idStr := c.Param("id")
|
idStr := c.Param("id")
|
||||||
userID, err := uuid.Parse(idStr)
|
userID, err := uuid.Parse(idStr)
|
||||||
@@ -510,7 +517,7 @@ func (h *Handler) UpdateUserStatus(c echo.Context) error {
|
|||||||
// UpdateUserRole updates user role (admin only)
|
// UpdateUserRole updates user role (admin only)
|
||||||
// @Summary Update user role
|
// @Summary Update user role
|
||||||
// @Description Update user role (admin only)
|
// @Description Update user role (admin only)
|
||||||
// @Tags users
|
// @Tags Users
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Security BearerAuth
|
// @Security BearerAuth
|
||||||
@@ -522,10 +529,13 @@ func (h *Handler) UpdateUserStatus(c echo.Context) error {
|
|||||||
// @Failure 403 {object} response.APIResponse
|
// @Failure 403 {object} response.APIResponse
|
||||||
// @Failure 404 {object} response.APIResponse
|
// @Failure 404 {object} response.APIResponse
|
||||||
// @Failure 500 {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 {
|
func (h *Handler) UpdateUserRole(c echo.Context) error {
|
||||||
// TODO: Get current user ID from JWT token
|
// Extract user ID from JWT token context
|
||||||
currentUserID := uuid.New() // Placeholder
|
currentUserID, err := GetUserIDFromContext(c)
|
||||||
|
if err != nil {
|
||||||
|
return response.Unauthorized(c, "User not authenticated")
|
||||||
|
}
|
||||||
|
|
||||||
idStr := c.Param("id")
|
idStr := c.Param("id")
|
||||||
userID, err := uuid.Parse(idStr)
|
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)
|
// GetUsersByCompanyID gets users by company ID (admin only)
|
||||||
// @Summary Get users by company ID
|
// @Summary Get users by company ID
|
||||||
// @Description Get users belonging to a specific company (admin only)
|
// @Description Get users belonging to a specific company (admin only)
|
||||||
// @Tags users
|
// @Tags Users
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Security BearerAuth
|
// @Security BearerAuth
|
||||||
// @Param company_id path string true "Company ID"
|
// @Param company_id path string true "Company ID"
|
||||||
// @Param limit query int false "Limit results"
|
// @Param limit query int false "Limit results"
|
||||||
// @Param offset query int false "Offset 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 400 {object} response.APIResponse
|
||||||
// @Failure 401 {object} response.APIResponse
|
// @Failure 401 {object} response.APIResponse
|
||||||
// @Failure 403 {object} response.APIResponse
|
// @Failure 403 {object} response.APIResponse
|
||||||
// @Failure 500 {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 {
|
func (h *Handler) GetUsersByCompanyID(c echo.Context) error {
|
||||||
companyIDStr := c.Param("company_id")
|
companyIDStr := c.Param("company_id")
|
||||||
companyID, err := uuid.Parse(companyIDStr)
|
companyID, err := uuid.Parse(companyIDStr)
|
||||||
@@ -621,19 +631,19 @@ func (h *Handler) GetUsersByCompanyID(c echo.Context) error {
|
|||||||
// GetUsersByRole gets users by role (admin only)
|
// GetUsersByRole gets users by role (admin only)
|
||||||
// @Summary Get users by role
|
// @Summary Get users by role
|
||||||
// @Description Get users with a specific role (admin only)
|
// @Description Get users with a specific role (admin only)
|
||||||
// @Tags users
|
// @Tags Users
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Security BearerAuth
|
// @Security BearerAuth
|
||||||
// @Param role path string true "User role"
|
// @Param role path string true "User role"
|
||||||
// @Param limit query int false "Limit results"
|
// @Param limit query int false "Limit results"
|
||||||
// @Param offset query int false "Offset 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 400 {object} response.APIResponse
|
||||||
// @Failure 401 {object} response.APIResponse
|
// @Failure 401 {object} response.APIResponse
|
||||||
// @Failure 403 {object} response.APIResponse
|
// @Failure 403 {object} response.APIResponse
|
||||||
// @Failure 500 {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 {
|
func (h *Handler) GetUsersByRole(c echo.Context) error {
|
||||||
roleStr := c.Param("role")
|
roleStr := c.Param("role")
|
||||||
role := UserRole(roleStr)
|
role := UserRole(roleStr)
|
||||||
|
|||||||
Reference in New Issue
Block a user