diff --git a/cmd/web/bootstrap.go b/cmd/web/bootstrap.go index 768e303..2756073 100644 --- a/cmd/web/bootstrap.go +++ b/cmd/web/bootstrap.go @@ -116,13 +116,7 @@ func initHTTPServer(conf infra.Config, log logger.Logger) *echo.Echo { }) // Add health check endpoint - e.GET("/health", func(c echo.Context) error { - return c.JSON(http.StatusOK, map[string]interface{}{ - "status": "healthy", - "time": time.Now().Unix(), - "version": "1.0.0", - }) - }) + e.GET("/admin/v1/health", healthHandler) // Add Swagger documentation endpoint e.GET("/swagger/*", echoSwagger.WrapHandler) diff --git a/cmd/web/docs/docs.go b/cmd/web/docs/docs.go index 754bb6c..730915d 100644 --- a/cmd/web/docs/docs.go +++ b/cmd/web/docs/docs.go @@ -24,289 +24,14 @@ const docTemplate = `{ "host": "{{.Host}}", "basePath": "{{.BasePath}}", "paths": { - "/admin/customers": { - "get": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "Retrieve a paginated list of customers (admin only)", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "customers" - ], - "summary": "List customers", - "parameters": [ - { - "type": "integer", - "default": 1, - "description": "Page number", - "name": "page", - "in": "query" - }, - { - "type": "integer", - "default": 20, - "description": "Number of items per page", - "name": "limit", - "in": "query" - }, - { - "type": "string", - "description": "Search term", - "name": "search", - "in": "query" - } - ], - "responses": { - "200": { - "description": "Customers retrieved successfully", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.APIResponse" - }, - { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/definitions/customer.CustomerResponse" - } - } - } - } - ] - } - }, - "400": { - "description": "Bad request", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "500": { - "description": "Internal server error", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - } - } - } - }, - "/admin/customers/device-tokens": { - "get": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "Retrieve all device tokens (admin only)", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "customers" - ], - "summary": "Get all device tokens", - "responses": { - "200": { - "description": "Device tokens retrieved successfully", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.APIResponse" - }, - { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "type": "string" - } - } - } - } - ] - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "500": { - "description": "Internal server error", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - } - } - } - }, - "/admin/customers/register": { - "post": { - "description": "Register a new customer with the provided information", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "customers" - ], - "summary": "Register a new customer", - "parameters": [ - { - "description": "Customer registration information", - "name": "customer", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/customer.RegisterCustomerForm" - } - } - ], - "responses": { - "201": { - "description": "Customer registered successfully", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.APIResponse" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/customer.CustomerResponse" - } - } - } - ] - } - }, - "400": { - "description": "Bad request", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "409": { - "description": "Customer already exists", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "500": { - "description": "Internal server error", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - } - } - } - }, - "/admin/customers/{id}": { - "get": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "Retrieve customer information by ID (admin only)", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "customers" - ], - "summary": "Get customer by ID", - "parameters": [ - { - "type": "string", - "description": "Customer ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "Customer retrieved successfully", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.APIResponse" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/customer.CustomerResponse" - } - } - } - ] - } - }, - "400": { - "description": "Bad request", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "404": { - "description": "Customer not found", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "500": { - "description": "Internal server error", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - } - } - } - }, - "/admin/customers/{id}/status": { + "/change-password": { "put": { "security": [ { "BearerAuth": [] } ], - "description": "Update customer status (admin only)", + "description": "Change current user password", "consumes": [ "application/json" ], @@ -314,99 +39,29 @@ const docTemplate = `{ "application/json" ], "tags": [ - "customers" + "Users" ], - "summary": "Update customer status", + "summary": "Change password", "parameters": [ { - "type": "string", - "description": "Customer ID", - "name": "id", - "in": "path", - "required": true - }, - { - "description": "Status update information", - "name": "status", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/customer.UpdateCustomerStatusForm" - } - } - ], - "responses": { - "200": { - "description": "Customer status updated successfully", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "400": { - "description": "Bad request", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "404": { - "description": "Customer not found", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "500": { - "description": "Internal server error", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - } - } - } - }, - "/customers/change-password": { - "put": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "Change the authenticated customer's password", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "customers" - ], - "summary": "Change customer password", - "parameters": [ - { - "description": "Password change information", + "description": "Password change data", "name": "password", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/customer.ChangePasswordForm" + "$ref": "#/definitions/user.ChangePasswordForm" } } ], "responses": { "200": { - "description": "Password changed successfully", + "description": "OK", "schema": { "$ref": "#/definitions/response.APIResponse" } }, "400": { - "description": "Bad request", + "description": "Bad Request", "schema": { "$ref": "#/definitions/response.APIResponse" } @@ -418,7 +73,7 @@ const docTemplate = `{ } }, "500": { - "description": "Internal server error", + "description": "Internal Server Error", "schema": { "$ref": "#/definitions/response.APIResponse" } @@ -426,14 +81,9 @@ const docTemplate = `{ } } }, - "/customers/device-token": { - "post": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "Add a device token for push notifications", + "/health": { + "get": { + "description": "Get server health status", "consumes": [ "application/json" ], @@ -441,29 +91,64 @@ const docTemplate = `{ "application/json" ], "tags": [ - "customers" + "Health" ], - "summary": "Add device token", + "summary": "Health check", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/main.HealthResponse" + } + } + } + } + }, + "/login": { + "post": { + "description": "Authenticate user with username and password", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Authorization" + ], + "summary": "Login user", "parameters": [ { - "description": "Device token information", - "name": "device_token", + "description": "Login credentials", + "name": "login", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/customer.AddDeviceTokenForm" + "$ref": "#/definitions/user.LoginForm" } } ], "responses": { "200": { - "description": "Device token added successfully", + "description": "OK", "schema": { - "$ref": "#/definitions/response.APIResponse" + "allOf": [ + { + "$ref": "#/definitions/response.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/user.AuthResponse" + } + } + } + ] } }, "400": { - "description": "Bad request", + "description": "Bad Request", "schema": { "$ref": "#/definitions/response.APIResponse" } @@ -475,20 +160,22 @@ const docTemplate = `{ } }, "500": { - "description": "Internal server error", + "description": "Internal Server Error", "schema": { "$ref": "#/definitions/response.APIResponse" } } } - }, + } + }, + "/logout": { "delete": { "security": [ { "BearerAuth": [] } ], - "description": "Remove a device token for push notifications", + "description": "Logout current user and invalidate tokens", "consumes": [ "application/json" ], @@ -496,29 +183,12 @@ const docTemplate = `{ "application/json" ], "tags": [ - "customers" - ], - "summary": "Remove device token", - "parameters": [ - { - "description": "Device token information", - "name": "device_token", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/customer.RemoveDeviceTokenForm" - } - } + "Users" ], + "summary": "Logout user", "responses": { "200": { - "description": "Device token removed successfully", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "400": { - "description": "Bad request", + "description": "OK", "schema": { "$ref": "#/definitions/response.APIResponse" } @@ -530,7 +200,7 @@ const docTemplate = `{ } }, "500": { - "description": "Internal server error", + "description": "Internal Server Error", "schema": { "$ref": "#/definitions/response.APIResponse" } @@ -538,135 +208,14 @@ const docTemplate = `{ } } }, - "/customers/login": { - "post": { - "description": "Authenticate customer with email and password", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "customers" - ], - "summary": "Customer login", - "parameters": [ - { - "description": "Login credentials", - "name": "credentials", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/customer.LoginForm" - } - } - ], - "responses": { - "200": { - "description": "Login successful", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.APIResponse" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/customer.AuthResponse" - } - } - } - ] - } - }, - "400": { - "description": "Bad request", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "401": { - "description": "Invalid credentials", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "500": { - "description": "Internal server error", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - } - } - } - }, - "/customers/logout": { - "post": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "Logout customer and invalidate device token", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "customers" - ], - "summary": "Customer logout", - "parameters": [ - { - "description": "Logout request with device token", - "name": "logout", - "in": "body", - "required": true, - "schema": { - "type": "object" - } - } - ], - "responses": { - "200": { - "description": "Logged out successfully", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "400": { - "description": "Bad request", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "500": { - "description": "Internal server error", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - } - } - } - }, - "/customers/profile": { + "/profile": { "get": { "security": [ { "BearerAuth": [] } ], - "description": "Retrieve the authenticated customer's profile information", + "description": "Get current user profile information", "consumes": [ "application/json" ], @@ -674,12 +223,12 @@ const docTemplate = `{ "application/json" ], "tags": [ - "customers" + "Users" ], - "summary": "Get customer profile", + "summary": "Get user profile", "responses": { "200": { - "description": "Profile retrieved successfully", + "description": "OK", "schema": { "allOf": [ { @@ -689,7 +238,7 @@ const docTemplate = `{ "type": "object", "properties": { "data": { - "$ref": "#/definitions/customer.CustomerResponse" + "$ref": "#/definitions/user.UserResponse" } } } @@ -703,13 +252,13 @@ const docTemplate = `{ } }, "404": { - "description": "Customer not found", + "description": "Not Found", "schema": { "$ref": "#/definitions/response.APIResponse" } }, "500": { - "description": "Internal server error", + "description": "Internal Server Error", "schema": { "$ref": "#/definitions/response.APIResponse" } @@ -722,7 +271,7 @@ const docTemplate = `{ "BearerAuth": [] } ], - "description": "Update the authenticated customer's profile information", + "description": "Update current user profile information", "consumes": [ "application/json" ], @@ -730,23 +279,23 @@ const docTemplate = `{ "application/json" ], "tags": [ - "customers" + "Users" ], - "summary": "Update customer profile", + "summary": "Update user profile", "parameters": [ { - "description": "Profile update information", + "description": "Profile update data", "name": "profile", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/customer.UpdateCustomerForm" + "$ref": "#/definitions/user.UpdateUserForm" } } ], "responses": { "200": { - "description": "Profile updated successfully", + "description": "OK", "schema": { "allOf": [ { @@ -756,7 +305,7 @@ const docTemplate = `{ "type": "object", "properties": { "data": { - "$ref": "#/definitions/customer.CustomerResponse" + "$ref": "#/definitions/user.UserResponse" } } } @@ -764,7 +313,7 @@ const docTemplate = `{ } }, "400": { - "description": "Bad request", + "description": "Bad Request", "schema": { "$ref": "#/definitions/response.APIResponse" } @@ -776,7 +325,7 @@ const docTemplate = `{ } }, "500": { - "description": "Internal server error", + "description": "Internal Server Error", "schema": { "$ref": "#/definitions/response.APIResponse" } @@ -784,9 +333,9 @@ const docTemplate = `{ } } }, - "/customers/refresh-token": { + "/refresh-token": { "post": { - "description": "Refresh the access token using a refresh token", + "description": "Refresh access token using refresh token", "consumes": [ "application/json" ], @@ -794,23 +343,23 @@ const docTemplate = `{ "application/json" ], "tags": [ - "customers" + "Authorization" ], "summary": "Refresh access token", "parameters": [ { - "description": "Refresh token information", - "name": "token", + "description": "Refresh token", + "name": "refresh", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/customer.RefreshTokenForm" + "$ref": "#/definitions/user.RefreshTokenForm" } } ], "responses": { "200": { - "description": "Token refreshed successfully", + "description": "OK", "schema": { "allOf": [ { @@ -820,7 +369,7 @@ const docTemplate = `{ "type": "object", "properties": { "data": { - "$ref": "#/definitions/customer.AuthResponse" + "$ref": "#/definitions/user.AuthResponse" } } } @@ -828,19 +377,19 @@ const docTemplate = `{ } }, "400": { - "description": "Bad request", + "description": "Bad Request", "schema": { "$ref": "#/definitions/response.APIResponse" } }, "401": { - "description": "Invalid refresh token", + "description": "Unauthorized", "schema": { "$ref": "#/definitions/response.APIResponse" } }, "500": { - "description": "Internal server error", + "description": "Internal Server Error", "schema": { "$ref": "#/definitions/response.APIResponse" } @@ -848,7 +397,53 @@ const docTemplate = `{ } } }, - "/users/admin": { + "/reset-password": { + "post": { + "description": "Send password reset email to user", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Authorization" + ], + "summary": "Reset password", + "parameters": [ + { + "description": "Password reset request", + "name": "reset", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/user.ResetPasswordForm" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/users": { "get": { "security": [ { @@ -863,7 +458,7 @@ const docTemplate = `{ "application/json" ], "tags": [ - "users" + "Users" ], "summary": "List users", "parameters": [ @@ -975,7 +570,7 @@ const docTemplate = `{ "application/json" ], "tags": [ - "users" + "Users" ], "summary": "Create user", "parameters": [ @@ -1035,7 +630,7 @@ const docTemplate = `{ } } }, - "/users/admin/company/{company_id}": { + "/users/company/{company_id}": { "get": { "security": [ { @@ -1050,7 +645,7 @@ const docTemplate = `{ "application/json" ], "tags": [ - "users" + "Users" ], "summary": "Get users by company ID", "parameters": [ @@ -1076,9 +671,22 @@ const docTemplate = `{ ], "responses": { "200": { - "description": "OK", + "description": "Users with pagination metadata", "schema": { - "$ref": "#/definitions/response.APIResponse" + "allOf": [ + { + "$ref": "#/definitions/response.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": true + } + } + } + ] } }, "400": { @@ -1108,7 +716,7 @@ const docTemplate = `{ } } }, - "/users/admin/role/{role}": { + "/users/role/{role}": { "get": { "security": [ { @@ -1123,7 +731,7 @@ const docTemplate = `{ "application/json" ], "tags": [ - "users" + "Users" ], "summary": "Get users by role", "parameters": [ @@ -1149,9 +757,22 @@ const docTemplate = `{ ], "responses": { "200": { - "description": "OK", + "description": "Users with pagination metadata", "schema": { - "$ref": "#/definitions/response.APIResponse" + "allOf": [ + { + "$ref": "#/definitions/response.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": true + } + } + } + ] } }, "400": { @@ -1181,7 +802,7 @@ const docTemplate = `{ } } }, - "/users/admin/{id}": { + "/users/{id}": { "get": { "security": [ { @@ -1196,7 +817,7 @@ const docTemplate = `{ "application/json" ], "tags": [ - "users" + "Users" ], "summary": "Get user by ID", "parameters": [ @@ -1273,7 +894,7 @@ const docTemplate = `{ "application/json" ], "tags": [ - "users" + "Users" ], "summary": "Update user", "parameters": [ @@ -1359,7 +980,7 @@ const docTemplate = `{ "application/json" ], "tags": [ - "users" + "Users" ], "summary": "Delete user", "parameters": [ @@ -1411,7 +1032,7 @@ const docTemplate = `{ } } }, - "/users/admin/{id}/role": { + "/users/{id}/role": { "put": { "security": [ { @@ -1426,7 +1047,7 @@ const docTemplate = `{ "application/json" ], "tags": [ - "users" + "Users" ], "summary": "Update user role", "parameters": [ @@ -1487,7 +1108,7 @@ const docTemplate = `{ } } }, - "/users/admin/{id}/status": { + "/users/{id}/status": { "put": { "security": [ { @@ -1502,7 +1123,7 @@ const docTemplate = `{ "application/json" ], "tags": [ - "users" + "Users" ], "summary": "Update user status", "parameters": [ @@ -1562,629 +1183,20 @@ const docTemplate = `{ } } } - }, - "/users/change-password": { - "put": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "Change current user password", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "users" - ], - "summary": "Change password", - "parameters": [ - { - "description": "Password change data", - "name": "password", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/user.ChangePasswordForm" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - } - } - } - }, - "/users/login": { - "post": { - "description": "Authenticate user with email/username and password", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "users" - ], - "summary": "Login user", - "parameters": [ - { - "description": "Login credentials", - "name": "login", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/user.LoginForm" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.APIResponse" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/user.AuthResponse" - } - } - } - ] - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - } - } - } - }, - "/users/logout": { - "post": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "Logout current user and invalidate tokens", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "users" - ], - "summary": "Logout user", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - } - } - } - }, - "/users/profile": { - "get": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "Get current user profile information", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "users" - ], - "summary": "Get user profile", - "responses": { - "200": { - "description": "OK", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.APIResponse" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/user.UserResponse" - } - } - } - ] - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "404": { - "description": "Not Found", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - } - } - }, - "put": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "Update current user profile information", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "users" - ], - "summary": "Update user profile", - "parameters": [ - { - "description": "Profile update data", - "name": "profile", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/user.UpdateUserForm" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.APIResponse" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/user.UserResponse" - } - } - } - ] - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - } - } - } - }, - "/users/refresh-token": { - "post": { - "description": "Refresh access token using refresh token", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "users" - ], - "summary": "Refresh access token", - "parameters": [ - { - "description": "Refresh token", - "name": "refresh", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/user.RefreshTokenForm" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.APIResponse" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/user.AuthResponse" - } - } - } - ] - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - } - } - } - }, - "/users/reset-password": { - "post": { - "description": "Send password reset email to user", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "users" - ], - "summary": "Reset password", - "parameters": [ - { - "description": "Password reset request", - "name": "reset", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/user.ResetPasswordForm" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - } - } - } } }, "definitions": { - "customer.AddDeviceTokenForm": { - "type": "object", - "properties": { - "device_token": { - "type": "string" - }, - "device_type": { - "type": "string" - } - } - }, - "customer.AuthResponse": { - "type": "object", - "properties": { - "access_token": { - "type": "string" - }, - "customer": { - "$ref": "#/definitions/customer.CustomerResponse" - }, - "expires_at": { - "type": "integer" - }, - "refresh_token": { - "type": "string" - } - } - }, - "customer.ChangePasswordForm": { - "type": "object", - "properties": { - "new_password": { - "type": "string" - }, - "old_password": { - "type": "string" - } - } - }, - "customer.CustomerResponse": { - "type": "object", - "properties": { - "birthdate": { - "type": "integer" - }, - "company_id": { - "type": "string" - }, - "created_at": { - "type": "integer" - }, - "device_tokens": { - "type": "array", - "items": { - "$ref": "#/definitions/customer.DeviceToken" - } - }, - "email": { - "type": "string" - }, - "full_name": { - "type": "string" - }, - "gender": { - "type": "string" - }, - "id": { - "type": "string" - }, - "is_verified": { - "type": "boolean" - }, - "last_login_at": { - "type": "integer" - }, - "mobile": { - "type": "string" - }, - "national_id": { - "type": "string" - }, - "profile_image": { - "type": "string" - }, - "status": { - "type": "string" - }, - "updated_at": { - "type": "integer" - }, - "username": { - "type": "string" - } - } - }, - "customer.DeviceToken": { - "type": "object", - "properties": { - "createdAt": { - "description": "Unix timestamp", - "type": "integer" - }, - "deviceType": { - "$ref": "#/definitions/customer.DeviceType" - }, - "token": { - "type": "string" - }, - "updatedAt": { - "description": "Unix timestamp", - "type": "integer" - } - } - }, - "customer.DeviceType": { - "type": "string", - "enum": [ - "android", - "ios" - ], - "x-enum-varnames": [ - "DeviceTypeAndroid", - "DeviceTypeIOS" - ] - }, - "customer.LoginForm": { - "type": "object", - "properties": { - "email_or_mobile": { - "type": "string" - }, - "password": { - "type": "string" - } - } - }, - "customer.RefreshTokenForm": { - "type": "object", - "properties": { - "refresh_token": { - "type": "string" - } - } - }, - "customer.RegisterCustomerForm": { - "type": "object", - "properties": { - "birthdate": { - "type": "integer" - }, - "company_id": { - "type": "string" - }, - "email": { - "type": "string" - }, - "full_name": { - "type": "string" - }, - "gender": { - "type": "string" - }, - "mobile": { - "type": "string" - }, - "national_id": { - "type": "string" - }, - "password": { - "type": "string" - }, - "profile_image": { - "type": "string" - }, - "username": { - "type": "string" - } - } - }, - "customer.RemoveDeviceTokenForm": { - "type": "object", - "properties": { - "device_token": { - "type": "string" - } - } - }, - "customer.UpdateCustomerForm": { - "type": "object", - "properties": { - "birthdate": { - "type": "integer" - }, - "email": { - "type": "string" - }, - "full_name": { - "type": "string" - }, - "gender": { - "type": "string" - }, - "mobile": { - "type": "string" - }, - "national_id": { - "type": "string" - }, - "profile_image": { - "type": "string" - }, - "status": { - "type": "string" - }, - "username": { - "type": "string" - } - } - }, - "customer.UpdateCustomerStatusForm": { + "main.HealthResponse": { "type": "object", "properties": { "status": { "type": "string" + }, + "time": { + "type": "integer" + }, + "version": { + "type": "string" } } }, @@ -2306,10 +1318,10 @@ const docTemplate = `{ "user.LoginForm": { "type": "object", "properties": { - "email_or_username": { + "password": { "type": "string" }, - "password": { + "username": { "type": "string" } } @@ -2471,20 +1483,16 @@ const docTemplate = `{ }, "tags": [ { - "description": "Customer management operations", - "name": "customers" + "description": "User management operations including authentication, profile management, and admin operations", + "name": "Users" }, { - "description": "User management operations", - "name": "users" + "description": "Authentication operations including login, logout, and token management", + "name": "Authorization" }, { "description": "Health check operations", - "name": "health" - }, - { - "description": "Authentication operations", - "name": "auth" + "name": "Health" } ] }` @@ -2493,7 +1501,7 @@ const docTemplate = `{ var SwaggerInfo = &swag.Spec{ Version: "1.0.0", Host: "localhost:8081", - BasePath: "/api/v1", + BasePath: "/admin/v1", Schemes: []string{}, Title: "Tender Management API", Description: "This is the API documentation for the Tender Management System.", diff --git a/cmd/web/docs/swagger.json b/cmd/web/docs/swagger.json index b913ea6..4d6022d 100644 --- a/cmd/web/docs/swagger.json +++ b/cmd/web/docs/swagger.json @@ -16,291 +16,16 @@ "version": "1.0.0" }, "host": "localhost:8081", - "basePath": "/api/v1", + "basePath": "/admin/v1", "paths": { - "/admin/customers": { - "get": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "Retrieve a paginated list of customers (admin only)", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "customers" - ], - "summary": "List customers", - "parameters": [ - { - "type": "integer", - "default": 1, - "description": "Page number", - "name": "page", - "in": "query" - }, - { - "type": "integer", - "default": 20, - "description": "Number of items per page", - "name": "limit", - "in": "query" - }, - { - "type": "string", - "description": "Search term", - "name": "search", - "in": "query" - } - ], - "responses": { - "200": { - "description": "Customers retrieved successfully", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.APIResponse" - }, - { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/definitions/customer.CustomerResponse" - } - } - } - } - ] - } - }, - "400": { - "description": "Bad request", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "500": { - "description": "Internal server error", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - } - } - } - }, - "/admin/customers/device-tokens": { - "get": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "Retrieve all device tokens (admin only)", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "customers" - ], - "summary": "Get all device tokens", - "responses": { - "200": { - "description": "Device tokens retrieved successfully", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.APIResponse" - }, - { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "type": "string" - } - } - } - } - ] - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "500": { - "description": "Internal server error", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - } - } - } - }, - "/admin/customers/register": { - "post": { - "description": "Register a new customer with the provided information", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "customers" - ], - "summary": "Register a new customer", - "parameters": [ - { - "description": "Customer registration information", - "name": "customer", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/customer.RegisterCustomerForm" - } - } - ], - "responses": { - "201": { - "description": "Customer registered successfully", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.APIResponse" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/customer.CustomerResponse" - } - } - } - ] - } - }, - "400": { - "description": "Bad request", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "409": { - "description": "Customer already exists", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "500": { - "description": "Internal server error", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - } - } - } - }, - "/admin/customers/{id}": { - "get": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "Retrieve customer information by ID (admin only)", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "customers" - ], - "summary": "Get customer by ID", - "parameters": [ - { - "type": "string", - "description": "Customer ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "Customer retrieved successfully", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.APIResponse" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/customer.CustomerResponse" - } - } - } - ] - } - }, - "400": { - "description": "Bad request", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "404": { - "description": "Customer not found", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "500": { - "description": "Internal server error", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - } - } - } - }, - "/admin/customers/{id}/status": { + "/change-password": { "put": { "security": [ { "BearerAuth": [] } ], - "description": "Update customer status (admin only)", + "description": "Change current user password", "consumes": [ "application/json" ], @@ -308,99 +33,29 @@ "application/json" ], "tags": [ - "customers" + "Users" ], - "summary": "Update customer status", + "summary": "Change password", "parameters": [ { - "type": "string", - "description": "Customer ID", - "name": "id", - "in": "path", - "required": true - }, - { - "description": "Status update information", - "name": "status", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/customer.UpdateCustomerStatusForm" - } - } - ], - "responses": { - "200": { - "description": "Customer status updated successfully", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "400": { - "description": "Bad request", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "404": { - "description": "Customer not found", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "500": { - "description": "Internal server error", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - } - } - } - }, - "/customers/change-password": { - "put": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "Change the authenticated customer's password", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "customers" - ], - "summary": "Change customer password", - "parameters": [ - { - "description": "Password change information", + "description": "Password change data", "name": "password", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/customer.ChangePasswordForm" + "$ref": "#/definitions/user.ChangePasswordForm" } } ], "responses": { "200": { - "description": "Password changed successfully", + "description": "OK", "schema": { "$ref": "#/definitions/response.APIResponse" } }, "400": { - "description": "Bad request", + "description": "Bad Request", "schema": { "$ref": "#/definitions/response.APIResponse" } @@ -412,7 +67,7 @@ } }, "500": { - "description": "Internal server error", + "description": "Internal Server Error", "schema": { "$ref": "#/definitions/response.APIResponse" } @@ -420,14 +75,9 @@ } } }, - "/customers/device-token": { - "post": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "Add a device token for push notifications", + "/health": { + "get": { + "description": "Get server health status", "consumes": [ "application/json" ], @@ -435,29 +85,64 @@ "application/json" ], "tags": [ - "customers" + "Health" ], - "summary": "Add device token", + "summary": "Health check", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/main.HealthResponse" + } + } + } + } + }, + "/login": { + "post": { + "description": "Authenticate user with username and password", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Authorization" + ], + "summary": "Login user", "parameters": [ { - "description": "Device token information", - "name": "device_token", + "description": "Login credentials", + "name": "login", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/customer.AddDeviceTokenForm" + "$ref": "#/definitions/user.LoginForm" } } ], "responses": { "200": { - "description": "Device token added successfully", + "description": "OK", "schema": { - "$ref": "#/definitions/response.APIResponse" + "allOf": [ + { + "$ref": "#/definitions/response.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/user.AuthResponse" + } + } + } + ] } }, "400": { - "description": "Bad request", + "description": "Bad Request", "schema": { "$ref": "#/definitions/response.APIResponse" } @@ -469,20 +154,22 @@ } }, "500": { - "description": "Internal server error", + "description": "Internal Server Error", "schema": { "$ref": "#/definitions/response.APIResponse" } } } - }, + } + }, + "/logout": { "delete": { "security": [ { "BearerAuth": [] } ], - "description": "Remove a device token for push notifications", + "description": "Logout current user and invalidate tokens", "consumes": [ "application/json" ], @@ -490,29 +177,12 @@ "application/json" ], "tags": [ - "customers" - ], - "summary": "Remove device token", - "parameters": [ - { - "description": "Device token information", - "name": "device_token", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/customer.RemoveDeviceTokenForm" - } - } + "Users" ], + "summary": "Logout user", "responses": { "200": { - "description": "Device token removed successfully", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "400": { - "description": "Bad request", + "description": "OK", "schema": { "$ref": "#/definitions/response.APIResponse" } @@ -524,7 +194,7 @@ } }, "500": { - "description": "Internal server error", + "description": "Internal Server Error", "schema": { "$ref": "#/definitions/response.APIResponse" } @@ -532,135 +202,14 @@ } } }, - "/customers/login": { - "post": { - "description": "Authenticate customer with email and password", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "customers" - ], - "summary": "Customer login", - "parameters": [ - { - "description": "Login credentials", - "name": "credentials", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/customer.LoginForm" - } - } - ], - "responses": { - "200": { - "description": "Login successful", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.APIResponse" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/customer.AuthResponse" - } - } - } - ] - } - }, - "400": { - "description": "Bad request", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "401": { - "description": "Invalid credentials", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "500": { - "description": "Internal server error", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - } - } - } - }, - "/customers/logout": { - "post": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "Logout customer and invalidate device token", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "customers" - ], - "summary": "Customer logout", - "parameters": [ - { - "description": "Logout request with device token", - "name": "logout", - "in": "body", - "required": true, - "schema": { - "type": "object" - } - } - ], - "responses": { - "200": { - "description": "Logged out successfully", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "400": { - "description": "Bad request", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "500": { - "description": "Internal server error", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - } - } - } - }, - "/customers/profile": { + "/profile": { "get": { "security": [ { "BearerAuth": [] } ], - "description": "Retrieve the authenticated customer's profile information", + "description": "Get current user profile information", "consumes": [ "application/json" ], @@ -668,12 +217,12 @@ "application/json" ], "tags": [ - "customers" + "Users" ], - "summary": "Get customer profile", + "summary": "Get user profile", "responses": { "200": { - "description": "Profile retrieved successfully", + "description": "OK", "schema": { "allOf": [ { @@ -683,7 +232,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/definitions/customer.CustomerResponse" + "$ref": "#/definitions/user.UserResponse" } } } @@ -697,13 +246,13 @@ } }, "404": { - "description": "Customer not found", + "description": "Not Found", "schema": { "$ref": "#/definitions/response.APIResponse" } }, "500": { - "description": "Internal server error", + "description": "Internal Server Error", "schema": { "$ref": "#/definitions/response.APIResponse" } @@ -716,7 +265,7 @@ "BearerAuth": [] } ], - "description": "Update the authenticated customer's profile information", + "description": "Update current user profile information", "consumes": [ "application/json" ], @@ -724,23 +273,23 @@ "application/json" ], "tags": [ - "customers" + "Users" ], - "summary": "Update customer profile", + "summary": "Update user profile", "parameters": [ { - "description": "Profile update information", + "description": "Profile update data", "name": "profile", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/customer.UpdateCustomerForm" + "$ref": "#/definitions/user.UpdateUserForm" } } ], "responses": { "200": { - "description": "Profile updated successfully", + "description": "OK", "schema": { "allOf": [ { @@ -750,7 +299,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/definitions/customer.CustomerResponse" + "$ref": "#/definitions/user.UserResponse" } } } @@ -758,7 +307,7 @@ } }, "400": { - "description": "Bad request", + "description": "Bad Request", "schema": { "$ref": "#/definitions/response.APIResponse" } @@ -770,7 +319,7 @@ } }, "500": { - "description": "Internal server error", + "description": "Internal Server Error", "schema": { "$ref": "#/definitions/response.APIResponse" } @@ -778,9 +327,9 @@ } } }, - "/customers/refresh-token": { + "/refresh-token": { "post": { - "description": "Refresh the access token using a refresh token", + "description": "Refresh access token using refresh token", "consumes": [ "application/json" ], @@ -788,23 +337,23 @@ "application/json" ], "tags": [ - "customers" + "Authorization" ], "summary": "Refresh access token", "parameters": [ { - "description": "Refresh token information", - "name": "token", + "description": "Refresh token", + "name": "refresh", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/customer.RefreshTokenForm" + "$ref": "#/definitions/user.RefreshTokenForm" } } ], "responses": { "200": { - "description": "Token refreshed successfully", + "description": "OK", "schema": { "allOf": [ { @@ -814,7 +363,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/definitions/customer.AuthResponse" + "$ref": "#/definitions/user.AuthResponse" } } } @@ -822,19 +371,19 @@ } }, "400": { - "description": "Bad request", + "description": "Bad Request", "schema": { "$ref": "#/definitions/response.APIResponse" } }, "401": { - "description": "Invalid refresh token", + "description": "Unauthorized", "schema": { "$ref": "#/definitions/response.APIResponse" } }, "500": { - "description": "Internal server error", + "description": "Internal Server Error", "schema": { "$ref": "#/definitions/response.APIResponse" } @@ -842,7 +391,53 @@ } } }, - "/users/admin": { + "/reset-password": { + "post": { + "description": "Send password reset email to user", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Authorization" + ], + "summary": "Reset password", + "parameters": [ + { + "description": "Password reset request", + "name": "reset", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/user.ResetPasswordForm" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/users": { "get": { "security": [ { @@ -857,7 +452,7 @@ "application/json" ], "tags": [ - "users" + "Users" ], "summary": "List users", "parameters": [ @@ -969,7 +564,7 @@ "application/json" ], "tags": [ - "users" + "Users" ], "summary": "Create user", "parameters": [ @@ -1029,7 +624,7 @@ } } }, - "/users/admin/company/{company_id}": { + "/users/company/{company_id}": { "get": { "security": [ { @@ -1044,7 +639,7 @@ "application/json" ], "tags": [ - "users" + "Users" ], "summary": "Get users by company ID", "parameters": [ @@ -1070,9 +665,22 @@ ], "responses": { "200": { - "description": "OK", + "description": "Users with pagination metadata", "schema": { - "$ref": "#/definitions/response.APIResponse" + "allOf": [ + { + "$ref": "#/definitions/response.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": true + } + } + } + ] } }, "400": { @@ -1102,7 +710,7 @@ } } }, - "/users/admin/role/{role}": { + "/users/role/{role}": { "get": { "security": [ { @@ -1117,7 +725,7 @@ "application/json" ], "tags": [ - "users" + "Users" ], "summary": "Get users by role", "parameters": [ @@ -1143,9 +751,22 @@ ], "responses": { "200": { - "description": "OK", + "description": "Users with pagination metadata", "schema": { - "$ref": "#/definitions/response.APIResponse" + "allOf": [ + { + "$ref": "#/definitions/response.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": true + } + } + } + ] } }, "400": { @@ -1175,7 +796,7 @@ } } }, - "/users/admin/{id}": { + "/users/{id}": { "get": { "security": [ { @@ -1190,7 +811,7 @@ "application/json" ], "tags": [ - "users" + "Users" ], "summary": "Get user by ID", "parameters": [ @@ -1267,7 +888,7 @@ "application/json" ], "tags": [ - "users" + "Users" ], "summary": "Update user", "parameters": [ @@ -1353,7 +974,7 @@ "application/json" ], "tags": [ - "users" + "Users" ], "summary": "Delete user", "parameters": [ @@ -1405,7 +1026,7 @@ } } }, - "/users/admin/{id}/role": { + "/users/{id}/role": { "put": { "security": [ { @@ -1420,7 +1041,7 @@ "application/json" ], "tags": [ - "users" + "Users" ], "summary": "Update user role", "parameters": [ @@ -1481,7 +1102,7 @@ } } }, - "/users/admin/{id}/status": { + "/users/{id}/status": { "put": { "security": [ { @@ -1496,7 +1117,7 @@ "application/json" ], "tags": [ - "users" + "Users" ], "summary": "Update user status", "parameters": [ @@ -1556,629 +1177,20 @@ } } } - }, - "/users/change-password": { - "put": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "Change current user password", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "users" - ], - "summary": "Change password", - "parameters": [ - { - "description": "Password change data", - "name": "password", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/user.ChangePasswordForm" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - } - } - } - }, - "/users/login": { - "post": { - "description": "Authenticate user with email/username and password", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "users" - ], - "summary": "Login user", - "parameters": [ - { - "description": "Login credentials", - "name": "login", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/user.LoginForm" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.APIResponse" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/user.AuthResponse" - } - } - } - ] - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - } - } - } - }, - "/users/logout": { - "post": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "Logout current user and invalidate tokens", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "users" - ], - "summary": "Logout user", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - } - } - } - }, - "/users/profile": { - "get": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "Get current user profile information", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "users" - ], - "summary": "Get user profile", - "responses": { - "200": { - "description": "OK", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.APIResponse" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/user.UserResponse" - } - } - } - ] - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "404": { - "description": "Not Found", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - } - } - }, - "put": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "Update current user profile information", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "users" - ], - "summary": "Update user profile", - "parameters": [ - { - "description": "Profile update data", - "name": "profile", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/user.UpdateUserForm" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.APIResponse" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/user.UserResponse" - } - } - } - ] - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - } - } - } - }, - "/users/refresh-token": { - "post": { - "description": "Refresh access token using refresh token", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "users" - ], - "summary": "Refresh access token", - "parameters": [ - { - "description": "Refresh token", - "name": "refresh", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/user.RefreshTokenForm" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.APIResponse" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/user.AuthResponse" - } - } - } - ] - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - } - } - } - }, - "/users/reset-password": { - "post": { - "description": "Send password reset email to user", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "users" - ], - "summary": "Reset password", - "parameters": [ - { - "description": "Password reset request", - "name": "reset", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/user.ResetPasswordForm" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - } - } - } } }, "definitions": { - "customer.AddDeviceTokenForm": { - "type": "object", - "properties": { - "device_token": { - "type": "string" - }, - "device_type": { - "type": "string" - } - } - }, - "customer.AuthResponse": { - "type": "object", - "properties": { - "access_token": { - "type": "string" - }, - "customer": { - "$ref": "#/definitions/customer.CustomerResponse" - }, - "expires_at": { - "type": "integer" - }, - "refresh_token": { - "type": "string" - } - } - }, - "customer.ChangePasswordForm": { - "type": "object", - "properties": { - "new_password": { - "type": "string" - }, - "old_password": { - "type": "string" - } - } - }, - "customer.CustomerResponse": { - "type": "object", - "properties": { - "birthdate": { - "type": "integer" - }, - "company_id": { - "type": "string" - }, - "created_at": { - "type": "integer" - }, - "device_tokens": { - "type": "array", - "items": { - "$ref": "#/definitions/customer.DeviceToken" - } - }, - "email": { - "type": "string" - }, - "full_name": { - "type": "string" - }, - "gender": { - "type": "string" - }, - "id": { - "type": "string" - }, - "is_verified": { - "type": "boolean" - }, - "last_login_at": { - "type": "integer" - }, - "mobile": { - "type": "string" - }, - "national_id": { - "type": "string" - }, - "profile_image": { - "type": "string" - }, - "status": { - "type": "string" - }, - "updated_at": { - "type": "integer" - }, - "username": { - "type": "string" - } - } - }, - "customer.DeviceToken": { - "type": "object", - "properties": { - "createdAt": { - "description": "Unix timestamp", - "type": "integer" - }, - "deviceType": { - "$ref": "#/definitions/customer.DeviceType" - }, - "token": { - "type": "string" - }, - "updatedAt": { - "description": "Unix timestamp", - "type": "integer" - } - } - }, - "customer.DeviceType": { - "type": "string", - "enum": [ - "android", - "ios" - ], - "x-enum-varnames": [ - "DeviceTypeAndroid", - "DeviceTypeIOS" - ] - }, - "customer.LoginForm": { - "type": "object", - "properties": { - "email_or_mobile": { - "type": "string" - }, - "password": { - "type": "string" - } - } - }, - "customer.RefreshTokenForm": { - "type": "object", - "properties": { - "refresh_token": { - "type": "string" - } - } - }, - "customer.RegisterCustomerForm": { - "type": "object", - "properties": { - "birthdate": { - "type": "integer" - }, - "company_id": { - "type": "string" - }, - "email": { - "type": "string" - }, - "full_name": { - "type": "string" - }, - "gender": { - "type": "string" - }, - "mobile": { - "type": "string" - }, - "national_id": { - "type": "string" - }, - "password": { - "type": "string" - }, - "profile_image": { - "type": "string" - }, - "username": { - "type": "string" - } - } - }, - "customer.RemoveDeviceTokenForm": { - "type": "object", - "properties": { - "device_token": { - "type": "string" - } - } - }, - "customer.UpdateCustomerForm": { - "type": "object", - "properties": { - "birthdate": { - "type": "integer" - }, - "email": { - "type": "string" - }, - "full_name": { - "type": "string" - }, - "gender": { - "type": "string" - }, - "mobile": { - "type": "string" - }, - "national_id": { - "type": "string" - }, - "profile_image": { - "type": "string" - }, - "status": { - "type": "string" - }, - "username": { - "type": "string" - } - } - }, - "customer.UpdateCustomerStatusForm": { + "main.HealthResponse": { "type": "object", "properties": { "status": { "type": "string" + }, + "time": { + "type": "integer" + }, + "version": { + "type": "string" } } }, @@ -2300,10 +1312,10 @@ "user.LoginForm": { "type": "object", "properties": { - "email_or_username": { + "password": { "type": "string" }, - "password": { + "username": { "type": "string" } } @@ -2465,20 +1477,16 @@ }, "tags": [ { - "description": "Customer management operations", - "name": "customers" + "description": "User management operations including authentication, profile management, and admin operations", + "name": "Users" }, { - "description": "User management operations", - "name": "users" + "description": "Authentication operations including login, logout, and token management", + "name": "Authorization" }, { "description": "Health check operations", - "name": "health" - }, - { - "description": "Authentication operations", - "name": "auth" + "name": "Health" } ] } \ No newline at end of file diff --git a/cmd/web/docs/swagger.yaml b/cmd/web/docs/swagger.yaml index 4a9df92..74261b3 100644 --- a/cmd/web/docs/swagger.yaml +++ b/cmd/web/docs/swagger.yaml @@ -1,153 +1,13 @@ -basePath: /api/v1 +basePath: /admin/v1 definitions: - customer.AddDeviceTokenForm: - properties: - device_token: - type: string - device_type: - type: string - type: object - customer.AuthResponse: - properties: - access_token: - type: string - customer: - $ref: '#/definitions/customer.CustomerResponse' - expires_at: - type: integer - refresh_token: - type: string - type: object - customer.ChangePasswordForm: - properties: - new_password: - type: string - old_password: - type: string - type: object - customer.CustomerResponse: - properties: - birthdate: - type: integer - company_id: - type: string - created_at: - type: integer - device_tokens: - items: - $ref: '#/definitions/customer.DeviceToken' - type: array - email: - type: string - full_name: - type: string - gender: - type: string - id: - type: string - is_verified: - type: boolean - last_login_at: - type: integer - mobile: - type: string - national_id: - type: string - profile_image: - type: string - status: - type: string - updated_at: - type: integer - username: - type: string - type: object - customer.DeviceToken: - properties: - createdAt: - description: Unix timestamp - type: integer - deviceType: - $ref: '#/definitions/customer.DeviceType' - token: - type: string - updatedAt: - description: Unix timestamp - type: integer - type: object - customer.DeviceType: - enum: - - android - - ios - type: string - x-enum-varnames: - - DeviceTypeAndroid - - DeviceTypeIOS - customer.LoginForm: - properties: - email_or_mobile: - type: string - password: - type: string - type: object - customer.RefreshTokenForm: - properties: - refresh_token: - type: string - type: object - customer.RegisterCustomerForm: - properties: - birthdate: - type: integer - company_id: - type: string - email: - type: string - full_name: - type: string - gender: - type: string - mobile: - type: string - national_id: - type: string - password: - type: string - profile_image: - type: string - username: - type: string - type: object - customer.RemoveDeviceTokenForm: - properties: - device_token: - type: string - type: object - customer.UpdateCustomerForm: - properties: - birthdate: - type: integer - email: - type: string - full_name: - type: string - gender: - type: string - mobile: - type: string - national_id: - type: string - profile_image: - type: string - status: - type: string - username: - type: string - type: object - customer.UpdateCustomerStatusForm: + main.HealthResponse: properties: status: type: string + time: + type: integer + version: + type: string type: object response.APIError: properties: @@ -226,10 +86,10 @@ definitions: type: object user.LoginForm: properties: - email_or_username: - type: string password: type: string + username: + type: string type: object user.RefreshTokenForm: properties: @@ -340,238 +200,27 @@ info: title: Tender Management API version: 1.0.0 paths: - /admin/customers: - get: - consumes: - - application/json - description: Retrieve a paginated list of customers (admin only) - parameters: - - default: 1 - description: Page number - in: query - name: page - type: integer - - default: 20 - description: Number of items per page - in: query - name: limit - type: integer - - description: Search term - in: query - name: search - type: string - produces: - - application/json - responses: - "200": - description: Customers retrieved successfully - schema: - allOf: - - $ref: '#/definitions/response.APIResponse' - - properties: - data: - items: - $ref: '#/definitions/customer.CustomerResponse' - type: array - type: object - "400": - description: Bad request - schema: - $ref: '#/definitions/response.APIResponse' - "401": - description: Unauthorized - schema: - $ref: '#/definitions/response.APIResponse' - "500": - description: Internal server error - schema: - $ref: '#/definitions/response.APIResponse' - security: - - BearerAuth: [] - summary: List customers - tags: - - customers - /admin/customers/{id}: - get: - consumes: - - application/json - description: Retrieve customer information by ID (admin only) - parameters: - - description: Customer ID - in: path - name: id - required: true - type: string - produces: - - application/json - responses: - "200": - description: Customer retrieved successfully - schema: - allOf: - - $ref: '#/definitions/response.APIResponse' - - properties: - data: - $ref: '#/definitions/customer.CustomerResponse' - type: object - "400": - description: Bad request - schema: - $ref: '#/definitions/response.APIResponse' - "401": - description: Unauthorized - schema: - $ref: '#/definitions/response.APIResponse' - "404": - description: Customer not found - schema: - $ref: '#/definitions/response.APIResponse' - "500": - description: Internal server error - schema: - $ref: '#/definitions/response.APIResponse' - security: - - BearerAuth: [] - summary: Get customer by ID - tags: - - customers - /admin/customers/{id}/status: + /change-password: put: consumes: - application/json - description: Update customer status (admin only) + description: Change current user password parameters: - - description: Customer ID - in: path - name: id - required: true - type: string - - description: Status update information - in: body - name: status - required: true - schema: - $ref: '#/definitions/customer.UpdateCustomerStatusForm' - produces: - - application/json - responses: - "200": - description: Customer status updated successfully - schema: - $ref: '#/definitions/response.APIResponse' - "400": - description: Bad request - schema: - $ref: '#/definitions/response.APIResponse' - "401": - description: Unauthorized - schema: - $ref: '#/definitions/response.APIResponse' - "404": - description: Customer not found - schema: - $ref: '#/definitions/response.APIResponse' - "500": - description: Internal server error - schema: - $ref: '#/definitions/response.APIResponse' - security: - - BearerAuth: [] - summary: Update customer status - tags: - - customers - /admin/customers/device-tokens: - get: - consumes: - - application/json - description: Retrieve all device tokens (admin only) - produces: - - application/json - responses: - "200": - description: Device tokens retrieved successfully - schema: - allOf: - - $ref: '#/definitions/response.APIResponse' - - properties: - data: - items: - type: string - type: array - type: object - "401": - description: Unauthorized - schema: - $ref: '#/definitions/response.APIResponse' - "500": - description: Internal server error - schema: - $ref: '#/definitions/response.APIResponse' - security: - - BearerAuth: [] - summary: Get all device tokens - tags: - - customers - /admin/customers/register: - post: - consumes: - - application/json - description: Register a new customer with the provided information - parameters: - - description: Customer registration information - in: body - name: customer - required: true - schema: - $ref: '#/definitions/customer.RegisterCustomerForm' - produces: - - application/json - responses: - "201": - description: Customer registered successfully - schema: - allOf: - - $ref: '#/definitions/response.APIResponse' - - properties: - data: - $ref: '#/definitions/customer.CustomerResponse' - type: object - "400": - description: Bad request - schema: - $ref: '#/definitions/response.APIResponse' - "409": - description: Customer already exists - schema: - $ref: '#/definitions/response.APIResponse' - "500": - description: Internal server error - schema: - $ref: '#/definitions/response.APIResponse' - summary: Register a new customer - tags: - - customers - /customers/change-password: - put: - consumes: - - application/json - description: Change the authenticated customer's password - parameters: - - description: Password change information + - description: Password change data in: body name: password required: true schema: - $ref: '#/definitions/customer.ChangePasswordForm' + $ref: '#/definitions/user.ChangePasswordForm' produces: - application/json responses: "200": - description: Password changed successfully + description: OK schema: $ref: '#/definitions/response.APIResponse' "400": - description: Bad request + description: Bad Request schema: $ref: '#/definitions/response.APIResponse' "401": @@ -579,219 +228,152 @@ paths: schema: $ref: '#/definitions/response.APIResponse' "500": - description: Internal server error + description: Internal Server Error schema: $ref: '#/definitions/response.APIResponse' security: - BearerAuth: [] - summary: Change customer password + summary: Change password tags: - - customers - /customers/device-token: - delete: - consumes: - - application/json - description: Remove a device token for push notifications - parameters: - - description: Device token information - in: body - name: device_token - required: true - schema: - $ref: '#/definitions/customer.RemoveDeviceTokenForm' - produces: - - application/json - responses: - "200": - description: Device token removed successfully - schema: - $ref: '#/definitions/response.APIResponse' - "400": - description: Bad request - schema: - $ref: '#/definitions/response.APIResponse' - "401": - description: Unauthorized - schema: - $ref: '#/definitions/response.APIResponse' - "500": - description: Internal server error - schema: - $ref: '#/definitions/response.APIResponse' - security: - - BearerAuth: [] - summary: Remove device token - tags: - - customers - post: - consumes: - - application/json - description: Add a device token for push notifications - parameters: - - description: Device token information - in: body - name: device_token - required: true - schema: - $ref: '#/definitions/customer.AddDeviceTokenForm' - produces: - - application/json - responses: - "200": - description: Device token added successfully - schema: - $ref: '#/definitions/response.APIResponse' - "400": - description: Bad request - schema: - $ref: '#/definitions/response.APIResponse' - "401": - description: Unauthorized - schema: - $ref: '#/definitions/response.APIResponse' - "500": - description: Internal server error - schema: - $ref: '#/definitions/response.APIResponse' - security: - - BearerAuth: [] - summary: Add device token - tags: - - customers - /customers/login: - post: - consumes: - - application/json - description: Authenticate customer with email and password - parameters: - - description: Login credentials - in: body - name: credentials - required: true - schema: - $ref: '#/definitions/customer.LoginForm' - produces: - - application/json - responses: - "200": - description: Login successful - schema: - allOf: - - $ref: '#/definitions/response.APIResponse' - - properties: - data: - $ref: '#/definitions/customer.AuthResponse' - type: object - "400": - description: Bad request - schema: - $ref: '#/definitions/response.APIResponse' - "401": - description: Invalid credentials - schema: - $ref: '#/definitions/response.APIResponse' - "500": - description: Internal server error - schema: - $ref: '#/definitions/response.APIResponse' - summary: Customer login - tags: - - customers - /customers/logout: - post: - consumes: - - application/json - description: Logout customer and invalidate device token - parameters: - - description: Logout request with device token - in: body - name: logout - required: true - schema: - type: object - produces: - - application/json - responses: - "200": - description: Logged out successfully - schema: - $ref: '#/definitions/response.APIResponse' - "400": - description: Bad request - schema: - $ref: '#/definitions/response.APIResponse' - "401": - description: Unauthorized - schema: - $ref: '#/definitions/response.APIResponse' - "500": - description: Internal server error - schema: - $ref: '#/definitions/response.APIResponse' - security: - - BearerAuth: [] - summary: Customer logout - tags: - - customers - /customers/profile: + - Users + /health: get: consumes: - application/json - description: Retrieve the authenticated customer's profile information + description: Get server health status produces: - application/json responses: "200": - description: Profile retrieved successfully + description: OK + schema: + $ref: '#/definitions/main.HealthResponse' + summary: Health check + tags: + - Health + /login: + post: + consumes: + - application/json + description: Authenticate user with username and password + parameters: + - description: Login credentials + in: body + name: login + required: true + schema: + $ref: '#/definitions/user.LoginForm' + produces: + - application/json + responses: + "200": + description: OK schema: allOf: - $ref: '#/definitions/response.APIResponse' - properties: data: - $ref: '#/definitions/customer.CustomerResponse' + $ref: '#/definitions/user.AuthResponse' + type: object + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + summary: Login user + tags: + - Authorization + /logout: + delete: + consumes: + - application/json + description: Logout current user and invalidate tokens + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/response.APIResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + security: + - BearerAuth: [] + summary: Logout user + tags: + - Users + /profile: + get: + consumes: + - application/json + description: Get current user profile information + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/response.APIResponse' + - properties: + data: + $ref: '#/definitions/user.UserResponse' type: object "401": description: Unauthorized schema: $ref: '#/definitions/response.APIResponse' "404": - description: Customer not found + description: Not Found schema: $ref: '#/definitions/response.APIResponse' "500": - description: Internal server error + description: Internal Server Error schema: $ref: '#/definitions/response.APIResponse' security: - BearerAuth: [] - summary: Get customer profile + summary: Get user profile tags: - - customers + - Users put: consumes: - application/json - description: Update the authenticated customer's profile information + description: Update current user profile information parameters: - - description: Profile update information + - description: Profile update data in: body name: profile required: true schema: - $ref: '#/definitions/customer.UpdateCustomerForm' + $ref: '#/definitions/user.UpdateUserForm' produces: - application/json responses: "200": - description: Profile updated successfully + description: OK schema: allOf: - $ref: '#/definitions/response.APIResponse' - properties: data: - $ref: '#/definitions/customer.CustomerResponse' + $ref: '#/definitions/user.UserResponse' type: object "400": - description: Bad request + description: Bad Request schema: $ref: '#/definitions/response.APIResponse' "401": @@ -799,54 +381,84 @@ paths: schema: $ref: '#/definitions/response.APIResponse' "500": - description: Internal server error + description: Internal Server Error schema: $ref: '#/definitions/response.APIResponse' security: - BearerAuth: [] - summary: Update customer profile + summary: Update user profile tags: - - customers - /customers/refresh-token: + - Users + /refresh-token: post: consumes: - application/json - description: Refresh the access token using a refresh token + description: Refresh access token using refresh token parameters: - - description: Refresh token information + - description: Refresh token in: body - name: token + name: refresh required: true schema: - $ref: '#/definitions/customer.RefreshTokenForm' + $ref: '#/definitions/user.RefreshTokenForm' produces: - application/json responses: "200": - description: Token refreshed successfully + description: OK schema: allOf: - $ref: '#/definitions/response.APIResponse' - properties: data: - $ref: '#/definitions/customer.AuthResponse' + $ref: '#/definitions/user.AuthResponse' type: object "400": - description: Bad request + description: Bad Request schema: $ref: '#/definitions/response.APIResponse' "401": - description: Invalid refresh token + description: Unauthorized schema: $ref: '#/definitions/response.APIResponse' "500": - description: Internal server error + description: Internal Server Error schema: $ref: '#/definitions/response.APIResponse' summary: Refresh access token tags: - - customers - /users/admin: + - Authorization + /reset-password: + post: + consumes: + - application/json + description: Send password reset email to user + parameters: + - description: Password reset request + in: body + name: reset + required: true + schema: + $ref: '#/definitions/user.ResetPasswordForm' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/response.APIResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + summary: Reset password + tags: + - Authorization + /users: get: consumes: - application/json @@ -916,7 +528,7 @@ paths: - BearerAuth: [] summary: List users tags: - - users + - Users post: consumes: - application/json @@ -960,8 +572,8 @@ paths: - BearerAuth: [] summary: Create user tags: - - users - /users/admin/{id}: + - Users + /users/{id}: delete: consumes: - application/json @@ -1003,7 +615,7 @@ paths: - BearerAuth: [] summary: Delete user tags: - - users + - Users get: consumes: - application/json @@ -1050,7 +662,7 @@ paths: - BearerAuth: [] summary: Get user by ID tags: - - users + - Users put: consumes: - application/json @@ -1103,8 +715,8 @@ paths: - BearerAuth: [] summary: Update user tags: - - users - /users/admin/{id}/role: + - Users + /users/{id}/role: put: consumes: - application/json @@ -1152,8 +764,8 @@ paths: - BearerAuth: [] summary: Update user role tags: - - users - /users/admin/{id}/status: + - Users + /users/{id}/status: put: consumes: - application/json @@ -1201,8 +813,8 @@ paths: - BearerAuth: [] summary: Update user status tags: - - users - /users/admin/company/{company_id}: + - Users + /users/company/{company_id}: get: consumes: - application/json @@ -1225,9 +837,15 @@ paths: - application/json responses: "200": - description: OK + description: Users with pagination metadata schema: - $ref: '#/definitions/response.APIResponse' + allOf: + - $ref: '#/definitions/response.APIResponse' + - properties: + data: + additionalProperties: true + type: object + type: object "400": description: Bad Request schema: @@ -1248,8 +866,8 @@ paths: - BearerAuth: [] summary: Get users by company ID tags: - - users - /users/admin/role/{role}: + - Users + /users/role/{role}: get: consumes: - application/json @@ -1272,9 +890,15 @@ paths: - application/json responses: "200": - description: OK + description: Users with pagination metadata schema: - $ref: '#/definitions/response.APIResponse' + allOf: + - $ref: '#/definitions/response.APIResponse' + - properties: + data: + additionalProperties: true + type: object + type: object "400": description: Bad Request schema: @@ -1295,250 +919,7 @@ paths: - BearerAuth: [] summary: Get users by role tags: - - users - /users/change-password: - put: - consumes: - - application/json - description: Change current user password - parameters: - - description: Password change data - in: body - name: password - required: true - schema: - $ref: '#/definitions/user.ChangePasswordForm' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/response.APIResponse' - "400": - description: Bad Request - schema: - $ref: '#/definitions/response.APIResponse' - "401": - description: Unauthorized - schema: - $ref: '#/definitions/response.APIResponse' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/response.APIResponse' - security: - - BearerAuth: [] - summary: Change password - tags: - - users - /users/login: - post: - consumes: - - application/json - description: Authenticate user with email/username and password - parameters: - - description: Login credentials - in: body - name: login - required: true - schema: - $ref: '#/definitions/user.LoginForm' - produces: - - application/json - responses: - "200": - description: OK - schema: - allOf: - - $ref: '#/definitions/response.APIResponse' - - properties: - data: - $ref: '#/definitions/user.AuthResponse' - type: object - "400": - description: Bad Request - schema: - $ref: '#/definitions/response.APIResponse' - "401": - description: Unauthorized - schema: - $ref: '#/definitions/response.APIResponse' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/response.APIResponse' - summary: Login user - tags: - - users - /users/logout: - post: - consumes: - - application/json - description: Logout current user and invalidate tokens - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/response.APIResponse' - "401": - description: Unauthorized - schema: - $ref: '#/definitions/response.APIResponse' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/response.APIResponse' - security: - - BearerAuth: [] - summary: Logout user - tags: - - users - /users/profile: - get: - consumes: - - application/json - description: Get current user profile information - produces: - - application/json - responses: - "200": - description: OK - schema: - allOf: - - $ref: '#/definitions/response.APIResponse' - - properties: - data: - $ref: '#/definitions/user.UserResponse' - type: object - "401": - description: Unauthorized - schema: - $ref: '#/definitions/response.APIResponse' - "404": - description: Not Found - schema: - $ref: '#/definitions/response.APIResponse' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/response.APIResponse' - security: - - BearerAuth: [] - summary: Get user profile - tags: - - users - put: - consumes: - - application/json - description: Update current user profile information - parameters: - - description: Profile update data - in: body - name: profile - required: true - schema: - $ref: '#/definitions/user.UpdateUserForm' - produces: - - application/json - responses: - "200": - description: OK - schema: - allOf: - - $ref: '#/definitions/response.APIResponse' - - properties: - data: - $ref: '#/definitions/user.UserResponse' - type: object - "400": - description: Bad Request - schema: - $ref: '#/definitions/response.APIResponse' - "401": - description: Unauthorized - schema: - $ref: '#/definitions/response.APIResponse' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/response.APIResponse' - security: - - BearerAuth: [] - summary: Update user profile - tags: - - users - /users/refresh-token: - post: - consumes: - - application/json - description: Refresh access token using refresh token - parameters: - - description: Refresh token - in: body - name: refresh - required: true - schema: - $ref: '#/definitions/user.RefreshTokenForm' - produces: - - application/json - responses: - "200": - description: OK - schema: - allOf: - - $ref: '#/definitions/response.APIResponse' - - properties: - data: - $ref: '#/definitions/user.AuthResponse' - type: object - "400": - description: Bad Request - schema: - $ref: '#/definitions/response.APIResponse' - "401": - description: Unauthorized - schema: - $ref: '#/definitions/response.APIResponse' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/response.APIResponse' - summary: Refresh access token - tags: - - users - /users/reset-password: - post: - consumes: - - application/json - description: Send password reset email to user - parameters: - - description: Password reset request - in: body - name: reset - required: true - schema: - $ref: '#/definitions/user.ResetPasswordForm' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/response.APIResponse' - "400": - description: Bad Request - schema: - $ref: '#/definitions/response.APIResponse' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/response.APIResponse' - summary: Reset password - tags: - - users + - Users securityDefinitions: BearerAuth: description: Type "Bearer" followed by a space and JWT token. @@ -1547,11 +928,10 @@ securityDefinitions: type: apiKey swagger: "2.0" tags: -- description: Customer management operations - name: customers -- description: User management operations - name: users +- description: User management operations including authentication, profile management, + and admin operations + name: Users +- description: Authentication operations including login, logout, and token management + name: Authorization - description: Health check operations - name: health -- description: Authentication operations - name: auth + name: Health diff --git a/cmd/web/health.go b/cmd/web/health.go new file mode 100644 index 0000000..73d8332 --- /dev/null +++ b/cmd/web/health.go @@ -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"` +} diff --git a/cmd/web/main.go b/cmd/web/main.go index 97e43c4..9532887 100644 --- a/cmd/web/main.go +++ b/cmd/web/main.go @@ -1,22 +1,3 @@ -// Package main Tender Management API -// -// This is the API documentation for the Tender Management System. -// -// Schemes: http, https -// Host: localhost:8081 -// BasePath: /api/v1 -// Version: 1.0.0 -// -// Consumes: -// - application/json -// -// Produces: -// - application/json -// -// Security: -// - bearer -// -// swagger:meta package main // @title Tender Management API @@ -32,31 +13,27 @@ package main // @license.url http://www.apache.org/licenses/LICENSE-2.0.html // @host localhost:8081 -// @BasePath /api/v1 +// @BasePath /admin/v1 // @securityDefinitions.apikey BearerAuth // @in header // @name Authorization // @description Type "Bearer" followed by a space and JWT token. -// @tag.name customers -// @tag.description Customer management operations +// @tag.name Users +// @tag.description User management operations including authentication, profile management, and admin operations -// @tag.name users -// @tag.description User management operations +// @tag.name Authorization +// @tag.description Authentication operations including login, logout, and token management -// @tag.name health +// @tag.name Health // @tag.description Health check operations -// @tag.name auth -// @tag.description Authentication operations - import ( "context" "fmt" "net/http" "time" - "tm/internal/customer" "tm/internal/user" _ "tm/cmd/web/docs" // This is generated by swag @@ -92,8 +69,8 @@ func main() { // Initialize authorization service authService := initAuthorizationService(conf.Auth.JWT, redisClient, logger) + // Initialize repositories with MongoDB connection manager - customerRepository := customer.NewCustomerRepository(mongoManager, logger) userRepository := user.NewUserRepository(mongoManager, logger) logger.Info("Repositories initialized successfully", map[string]interface{}{ @@ -104,7 +81,6 @@ func main() { userValidator := user.NewValidationService() // Initialize services with repositories - customerService := customer.NewCustomerService(customerRepository, logger) userService := user.NewUserService(userRepository, logger, authService, userValidator) logger.Info("Services initialized successfully", map[string]interface{}{ @@ -112,7 +88,6 @@ func main() { }) // Initialize handlers with services - customerHandler := customer.NewHandler(customerService) userHandler := user.NewUserHandler(userService, logger, userValidator, authService) logger.Info("Handlers initialized successfully", map[string]interface{}{ @@ -123,7 +98,6 @@ func main() { e := initHTTPServer(conf, logger) // Register routes - customerHandler.RegisterRoutes(e) userHandler.RegisterRoutes(e) // Start server diff --git a/internal/customer/README.md b/internal/customer/README.md deleted file mode 100644 index 6a4e153..0000000 --- a/internal/customer/README.md +++ /dev/null @@ -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 -X-Customer-ID: -``` - -#### 5. Update Profile -```http -PUT /api/customers/profile -Authorization: Bearer -X-Customer-ID: -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 -X-Customer-ID: -Content-Type: application/json - -{ - "old_password": "currentpassword", - "new_password": "newsecurepassword123" -} -``` - -#### 7. Add Device Token -```http -POST /api/customers/device-token -Authorization: Bearer -X-Customer-ID: -Content-Type: application/json - -{ - "device_token": "fcm_token_here", - "device_type": "android" -} -``` - -#### 8. Remove Device Token -```http -DELETE /api/customers/device-token -Authorization: Bearer -X-Customer-ID: -Content-Type: application/json - -{ - "device_token": "fcm_token_here" -} -``` - -#### 9. Logout -```http -POST /api/customers/logout -Authorization: Bearer -X-Customer-ID: -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 -``` - -**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 -``` - -#### 12. Update Customer Status -```http -PUT /api/admin/customers/{customer_id}/status -Authorization: Bearer -Content-Type: application/json - -{ - "status": "inactive" -} -``` - -#### 13. Get All Device Tokens -```http -GET /api/admin/customers/device-tokens -Authorization: Bearer -``` - -**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 " \ - -H "X-Customer-ID: " \ - -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 " -``` - -## 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 \ No newline at end of file diff --git a/internal/customer/aggregate.go b/internal/customer/aggregate.go deleted file mode 100644 index f77155d..0000000 --- a/internal/customer/aggregate.go +++ /dev/null @@ -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"` -} diff --git a/internal/customer/entity.go b/internal/customer/entity.go deleted file mode 100644 index 9463e74..0000000 --- a/internal/customer/entity.go +++ /dev/null @@ -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 -} diff --git a/internal/customer/form.go b/internal/customer/form.go deleted file mode 100644 index afda35b..0000000 --- a/internal/customer/form.go +++ /dev/null @@ -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, - } -} diff --git a/internal/customer/handler.go b/internal/customer/handler.go deleted file mode 100644 index 2bc7012..0000000 --- a/internal/customer/handler.go +++ /dev/null @@ -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) - } -} diff --git a/internal/customer/repository.go b/internal/customer/repository.go deleted file mode 100644 index 4992a81..0000000 --- a/internal/customer/repository.go +++ /dev/null @@ -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 -} diff --git a/internal/customer/service.go b/internal/customer/service.go deleted file mode 100644 index 7161d4c..0000000 --- a/internal/customer/service.go +++ /dev/null @@ -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 -} diff --git a/internal/customer/validation.go b/internal/customer/validation.go deleted file mode 100644 index 6368720..0000000 --- a/internal/customer/validation.go +++ /dev/null @@ -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 - } - })) -} diff --git a/internal/user/handler.go b/internal/user/handler.go index cff4b8e..c5d14fb 100644 --- a/internal/user/handler.go +++ b/internal/user/handler.go @@ -31,46 +31,40 @@ func NewUserHandler(service Service, logger logger.Logger, validator ValidationS // RegisterRoutes registers user routes func (h *Handler) RegisterRoutes(e *echo.Echo) { - v1 := e.Group("/api/v1") + v1 := e.Group("/admin/v1") - userGroup := v1.Group("/users") - { - // Public routes - userGroup.POST("/login", h.Login) - userGroup.POST("/refresh-token", h.RefreshToken) - userGroup.POST("/reset-password", h.ResetPassword) + // Public routes (no authentication required) + authorizationGP := v1.Group("") + authorizationGP.POST("/login", h.Login) + authorizationGP.POST("/refresh-token", h.RefreshToken) + authorizationGP.POST("/reset-password", h.ResetPassword) - // Protected routes (require authentication) - authGroup := userGroup.Group("") - // authGroup.Use(h.AuthMiddleware()) - { - authGroup.GET("/profile", h.GetProfile) - authGroup.PUT("/profile", h.UpdateProfile) - authGroup.PUT("/change-password", h.ChangePassword) - authGroup.POST("/logout", h.Logout) + // Protected routes (require authentication) + profileGP := v1.Group("") + profileGP.Use(h.AuthMiddleware()) + profileGP.GET("/profile", h.GetProfile) + profileGP.PUT("/profile", h.UpdateProfile) + profileGP.PUT("/change-password", h.ChangePassword) + profileGP.DELETE("/logout", h.Logout) - // Admin routes - adminGroup := authGroup.Group("/admin") - // adminGroup.Use(h.AdminMiddleware()) - { - adminGroup.POST("", h.CreateUser) - adminGroup.GET("", h.ListUsers) - adminGroup.GET("/:id", h.GetUserByID) - adminGroup.PUT("/:id", h.UpdateUser) - adminGroup.DELETE("/:id", h.DeleteUser) - adminGroup.PUT("/:id/status", h.UpdateUserStatus) - adminGroup.PUT("/:id/role", h.UpdateUserRole) - adminGroup.GET("/company/:company_id", h.GetUsersByCompanyID) - adminGroup.GET("/role/:role", h.GetUsersByRole) - } - } - } + // Admin routes (require admin role) + adminGroup := v1.Group("/users") + adminGroup.Use(h.AuthMiddleware(), h.AdminMiddleware()) + adminGroup.POST("", h.CreateUser) + adminGroup.GET("", h.ListUsers) + adminGroup.GET("/:id", h.GetUserByID) + adminGroup.PUT("/:id", h.UpdateUser) + adminGroup.DELETE("/:id", h.DeleteUser) + adminGroup.PUT("/:id/status", h.UpdateUserStatus) + adminGroup.PUT("/:id/role", h.UpdateUserRole) + adminGroup.GET("/company/:company_id", h.GetUsersByCompanyID) + adminGroup.GET("/role/:role", h.GetUsersByRole) } // Login handles user login // @Summary Login user -// @Description Authenticate user with email/username and password -// @Tags users +// @Description Authenticate user with username and password +// @Tags Authorization // @Accept json // @Produce json // @Param login body LoginForm true "Login credentials" @@ -78,7 +72,7 @@ func (h *Handler) RegisterRoutes(e *echo.Echo) { // @Failure 400 {object} response.APIResponse // @Failure 401 {object} response.APIResponse // @Failure 500 {object} response.APIResponse -// @Router /users/login [post] +// @Router /login [post] func (h *Handler) Login(c echo.Context) error { form, err := response.Parse[LoginForm](c) if err != nil { @@ -97,7 +91,7 @@ func (h *Handler) Login(c echo.Context) error { // RefreshToken handles token refresh // @Summary Refresh access token // @Description Refresh access token using refresh token -// @Tags users +// @Tags Authorization // @Accept json // @Produce json // @Param refresh body RefreshTokenForm true "Refresh token" @@ -105,7 +99,7 @@ func (h *Handler) Login(c echo.Context) error { // @Failure 400 {object} response.APIResponse // @Failure 401 {object} response.APIResponse // @Failure 500 {object} response.APIResponse -// @Router /users/refresh-token [post] +// @Router /refresh-token [post] func (h *Handler) RefreshToken(c echo.Context) error { form, err := response.Parse[RefreshTokenForm](c) if err != nil { @@ -123,14 +117,14 @@ func (h *Handler) RefreshToken(c echo.Context) error { // ResetPassword handles password reset request // @Summary Reset password // @Description Send password reset email to user -// @Tags users +// @Tags Authorization // @Accept json // @Produce json // @Param reset body ResetPasswordForm true "Password reset request" // @Success 200 {object} response.APIResponse // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse -// @Router /users/reset-password [post] +// @Router /reset-password [post] func (h *Handler) ResetPassword(c echo.Context) error { form, err := response.Parse[ResetPasswordForm](c) if err != nil { @@ -150,7 +144,7 @@ func (h *Handler) ResetPassword(c echo.Context) error { // GetProfile gets current user profile // @Summary Get user profile // @Description Get current user profile information -// @Tags users +// @Tags Users // @Accept json // @Produce json // @Security BearerAuth @@ -158,7 +152,7 @@ func (h *Handler) ResetPassword(c echo.Context) error { // @Failure 401 {object} response.APIResponse // @Failure 404 {object} response.APIResponse // @Failure 500 {object} response.APIResponse -// @Router /users/profile [get] +// @Router /profile [get] func (h *Handler) GetProfile(c echo.Context) error { // Extract user ID from JWT token context userID, err := GetUserIDFromContext(c) @@ -177,7 +171,7 @@ func (h *Handler) GetProfile(c echo.Context) error { // UpdateProfile updates current user profile // @Summary Update user profile // @Description Update current user profile information -// @Tags users +// @Tags Users // @Accept json // @Produce json // @Security BearerAuth @@ -186,10 +180,13 @@ func (h *Handler) GetProfile(c echo.Context) error { // @Failure 400 {object} response.APIResponse // @Failure 401 {object} response.APIResponse // @Failure 500 {object} response.APIResponse -// @Router /users/profile [put] +// @Router /profile [put] func (h *Handler) UpdateProfile(c echo.Context) error { - // TODO: Extract user ID from JWT token - userID := uuid.New() // Placeholder - should be extracted from JWT + // Extract user ID from JWT token context + userID, err := GetUserIDFromContext(c) + if err != nil { + return response.Unauthorized(c, "User not authenticated") + } form, err := response.Parse[UpdateUserForm](c) if err != nil { @@ -207,7 +204,7 @@ func (h *Handler) UpdateProfile(c echo.Context) error { // ChangePassword changes current user password // @Summary Change password // @Description Change current user password -// @Tags users +// @Tags Users // @Accept json // @Produce json // @Security BearerAuth @@ -216,10 +213,12 @@ func (h *Handler) UpdateProfile(c echo.Context) error { // @Failure 400 {object} response.APIResponse // @Failure 401 {object} response.APIResponse // @Failure 500 {object} response.APIResponse -// @Router /users/change-password [put] +// @Router /change-password [put] func (h *Handler) ChangePassword(c echo.Context) error { - // TODO: Extract user ID from JWT token - userID := uuid.New() // Placeholder - should be extracted from JWT + userID, err := GetUserIDFromContext(c) + if err != nil { + return response.Unauthorized(c, "User not authenticated") + } form, err := response.Parse[ChangePasswordForm](c) if err != nil { @@ -239,14 +238,14 @@ func (h *Handler) ChangePassword(c echo.Context) error { // Logout handles user logout // @Summary Logout user // @Description Logout current user and invalidate tokens -// @Tags users +// @Tags Users // @Accept json // @Produce json // @Security BearerAuth // @Success 200 {object} response.APIResponse // @Failure 401 {object} response.APIResponse // @Failure 500 {object} response.APIResponse -// @Router /users/logout [post] +// @Router /logout [delete] func (h *Handler) Logout(c echo.Context) error { // Extract user ID and access token from context userID, err := GetUserIDFromContext(c) @@ -272,7 +271,7 @@ func (h *Handler) Logout(c echo.Context) error { // CreateUser creates a new user (admin only) // @Summary Create user // @Description Create a new user (admin only) -// @Tags users +// @Tags Users // @Accept json // @Produce json // @Security BearerAuth @@ -282,7 +281,7 @@ func (h *Handler) Logout(c echo.Context) error { // @Failure 401 {object} response.APIResponse // @Failure 403 {object} response.APIResponse // @Failure 500 {object} response.APIResponse -// @Router /users/admin [post] +// @Router /users [post] func (h *Handler) CreateUser(c echo.Context) error { // Get current user ID from JWT token currentUserID, err := GetUserIDFromContext(c) @@ -312,7 +311,7 @@ func (h *Handler) CreateUser(c echo.Context) error { // ListUsers lists users with search and filters (admin only) // @Summary List users // @Description List users with search and filters (admin only) -// @Tags users +// @Tags Users // @Accept json // @Produce json // @Security BearerAuth @@ -329,7 +328,7 @@ func (h *Handler) CreateUser(c echo.Context) error { // @Failure 401 {object} response.APIResponse // @Failure 403 {object} response.APIResponse // @Failure 500 {object} response.APIResponse -// @Router /users/admin [get] +// @Router /users [get] func (h *Handler) ListUsers(c echo.Context) error { var form ListUsersForm if err := c.Bind(&form); err != nil { @@ -353,7 +352,7 @@ func (h *Handler) ListUsers(c echo.Context) error { // GetUserByID gets a user by ID (admin only) // @Summary Get user by ID // @Description Get user details by ID (admin only) -// @Tags users +// @Tags Users // @Accept json // @Produce json // @Security BearerAuth @@ -364,7 +363,7 @@ func (h *Handler) ListUsers(c echo.Context) error { // @Failure 403 {object} response.APIResponse // @Failure 404 {object} response.APIResponse // @Failure 500 {object} response.APIResponse -// @Router /users/admin/{id} [get] +// @Router /users/{id} [get] func (h *Handler) GetUserByID(c echo.Context) error { idStr := c.Param("id") userID, err := uuid.Parse(idStr) @@ -383,7 +382,7 @@ func (h *Handler) GetUserByID(c echo.Context) error { // UpdateUser updates a user (admin only) // @Summary Update user // @Description Update user information (admin only) -// @Tags users +// @Tags Users // @Accept json // @Produce json // @Security BearerAuth @@ -395,10 +394,13 @@ func (h *Handler) GetUserByID(c echo.Context) error { // @Failure 403 {object} response.APIResponse // @Failure 404 {object} response.APIResponse // @Failure 500 {object} response.APIResponse -// @Router /users/admin/{id} [put] +// @Router /users/{id} [put] func (h *Handler) UpdateUser(c echo.Context) error { - // TODO: Get current user ID from JWT token - currentUserID := uuid.New() // Placeholder + // Extract user ID from JWT token context + currentUserID, err := GetUserIDFromContext(c) + if err != nil { + return response.Unauthorized(c, "User not authenticated") + } idStr := c.Param("id") userID, err := uuid.Parse(idStr) @@ -428,7 +430,7 @@ func (h *Handler) UpdateUser(c echo.Context) error { // DeleteUser deletes a user (admin only) // @Summary Delete user // @Description Delete a user (admin only) -// @Tags users +// @Tags Users // @Accept json // @Produce json // @Security BearerAuth @@ -439,10 +441,13 @@ func (h *Handler) UpdateUser(c echo.Context) error { // @Failure 403 {object} response.APIResponse // @Failure 404 {object} response.APIResponse // @Failure 500 {object} response.APIResponse -// @Router /users/admin/{id} [delete] +// @Router /users/{id} [delete] func (h *Handler) DeleteUser(c echo.Context) error { - // TODO: Get current user ID from JWT token - currentUserID := uuid.New() // Placeholder + // Extract user ID from JWT token context + currentUserID, err := GetUserIDFromContext(c) + if err != nil { + return response.Unauthorized(c, "User not authenticated") + } idStr := c.Param("id") userID, err := uuid.Parse(idStr) @@ -463,7 +468,7 @@ func (h *Handler) DeleteUser(c echo.Context) error { // UpdateUserStatus updates user status (admin only) // @Summary Update user status // @Description Update user account status (admin only) -// @Tags users +// @Tags Users // @Accept json // @Produce json // @Security BearerAuth @@ -475,10 +480,12 @@ func (h *Handler) DeleteUser(c echo.Context) error { // @Failure 403 {object} response.APIResponse // @Failure 404 {object} response.APIResponse // @Failure 500 {object} response.APIResponse -// @Router /users/admin/{id}/status [put] +// @Router /users/{id}/status [put] func (h *Handler) UpdateUserStatus(c echo.Context) error { - // TODO: Get current user ID from JWT token - currentUserID := uuid.New() // Placeholder + currentUserID, err := GetUserIDFromContext(c) + if err != nil { + return response.Unauthorized(c, "User not authenticated") + } idStr := c.Param("id") userID, err := uuid.Parse(idStr) @@ -510,7 +517,7 @@ func (h *Handler) UpdateUserStatus(c echo.Context) error { // UpdateUserRole updates user role (admin only) // @Summary Update user role // @Description Update user role (admin only) -// @Tags users +// @Tags Users // @Accept json // @Produce json // @Security BearerAuth @@ -522,10 +529,13 @@ func (h *Handler) UpdateUserStatus(c echo.Context) error { // @Failure 403 {object} response.APIResponse // @Failure 404 {object} response.APIResponse // @Failure 500 {object} response.APIResponse -// @Router /users/admin/{id}/role [put] +// @Router /users/{id}/role [put] func (h *Handler) UpdateUserRole(c echo.Context) error { - // TODO: Get current user ID from JWT token - currentUserID := uuid.New() // Placeholder + // Extract user ID from JWT token context + currentUserID, err := GetUserIDFromContext(c) + if err != nil { + return response.Unauthorized(c, "User not authenticated") + } idStr := c.Param("id") userID, err := uuid.Parse(idStr) @@ -557,19 +567,19 @@ func (h *Handler) UpdateUserRole(c echo.Context) error { // GetUsersByCompanyID gets users by company ID (admin only) // @Summary Get users by company ID // @Description Get users belonging to a specific company (admin only) -// @Tags users +// @Tags Users // @Accept json // @Produce json // @Security BearerAuth // @Param company_id path string true "Company ID" // @Param limit query int false "Limit results" // @Param offset query int false "Offset results" -// @Success 200 {object} response.APIResponse +// @Success 200 {object} response.APIResponse{data=map[string]interface{}} "Users with pagination metadata" // @Failure 400 {object} response.APIResponse // @Failure 401 {object} response.APIResponse // @Failure 403 {object} response.APIResponse // @Failure 500 {object} response.APIResponse -// @Router /users/admin/company/{company_id} [get] +// @Router /users/company/{company_id} [get] func (h *Handler) GetUsersByCompanyID(c echo.Context) error { companyIDStr := c.Param("company_id") companyID, err := uuid.Parse(companyIDStr) @@ -621,19 +631,19 @@ func (h *Handler) GetUsersByCompanyID(c echo.Context) error { // GetUsersByRole gets users by role (admin only) // @Summary Get users by role // @Description Get users with a specific role (admin only) -// @Tags users +// @Tags Users // @Accept json // @Produce json // @Security BearerAuth // @Param role path string true "User role" // @Param limit query int false "Limit results" // @Param offset query int false "Offset results" -// @Success 200 {object} response.APIResponse +// @Success 200 {object} response.APIResponse{data=map[string]interface{}} "Users with pagination metadata" // @Failure 400 {object} response.APIResponse // @Failure 401 {object} response.APIResponse // @Failure 403 {object} response.APIResponse // @Failure 500 {object} response.APIResponse -// @Router /users/admin/role/{role} [get] +// @Router /users/role/{role} [get] func (h *Handler) GetUsersByRole(c echo.Context) error { roleStr := c.Param("role") role := UserRole(roleStr)