From 38844939b570066463c4974af0bc6b08e5fe540c Mon Sep 17 00:00:00 2001 From: "n.nakhostin" Date: Sat, 6 Sep 2025 14:00:22 +0330 Subject: [PATCH] Refactor User Management API and Update Documentation - Changed API tags from "Admin-Users" to "Admin-Authorization" for better clarity in user management endpoints. - Removed unused endpoints for retrieving users by company ID and role, streamlining the user management functionality. - Updated user entity and forms to reflect new example values for improved clarity in API documentation. - Enhanced pagination handling in user listing responses, ensuring consistent metadata structure. - Updated API documentation to reflect changes in endpoint structure and response formats, improving clarity for API consumers. --- cmd/web/docs/docs.go | 340 ++---------------------- cmd/web/docs/swagger.json | 340 ++---------------------- cmd/web/docs/swagger.yaml | 234 ++-------------- cmd/web/router/routes.go | 3 - internal/user/entity.go | 34 ++- internal/user/form.go | 92 ++++--- internal/user/handler.go | 235 ++-------------- internal/user/repository.go | 515 +++++++++++++----------------------- internal/user/service.go | 457 +++++++++++++------------------- pkg/mongo/utils.go | 12 +- pkg/response/response.go | 39 +++ 11 files changed, 567 insertions(+), 1734 deletions(-) diff --git a/cmd/web/docs/docs.go b/cmd/web/docs/docs.go index e66e328..a86c012 100644 --- a/cmd/web/docs/docs.go +++ b/cmd/web/docs/docs.go @@ -3455,7 +3455,7 @@ const docTemplate = `{ "application/json" ], "tags": [ - "Admin-Users" + "Admin-Authorization" ], "summary": "Get authenticated user profile", "responses": { @@ -3511,7 +3511,7 @@ const docTemplate = `{ "application/json" ], "tags": [ - "Admin-Users" + "Admin-Authorization" ], "summary": "Update authenticated user profile", "parameters": [ @@ -3592,7 +3592,7 @@ const docTemplate = `{ "application/json" ], "tags": [ - "Admin-Users" + "Admin-Authorization" ], "summary": "Change user password", "parameters": [ @@ -4353,12 +4353,6 @@ const docTemplate = `{ "name": "role", "in": "query" }, - { - "type": "string", - "description": "Company ID filter", - "name": "company_id", - "in": "query" - }, { "type": "integer", "description": "Limit results", @@ -4515,178 +4509,6 @@ const docTemplate = `{ } } }, - "/admin/v1/users/company/{company_id}": { - "get": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "Get users belonging to a specific company (admin only)", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Admin-Users" - ], - "summary": "Get users by company ID", - "parameters": [ - { - "type": "string", - "description": "Company ID", - "name": "company_id", - "in": "path", - "required": true - }, - { - "type": "integer", - "description": "Limit results", - "name": "limit", - "in": "query" - }, - { - "type": "integer", - "description": "Offset results", - "name": "offset", - "in": "query" - } - ], - "responses": { - "200": { - "description": "Users with pagination metadata", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.APIResponse" - }, - { - "type": "object", - "properties": { - "data": { - "type": "object", - "additionalProperties": true - } - } - } - ] - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "403": { - "description": "Forbidden", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - } - } - } - }, - "/admin/v1/users/role/{role}": { - "get": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "Get users with a specific role (admin only)", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Admin-Users" - ], - "summary": "Get users by role", - "parameters": [ - { - "type": "string", - "description": "User role", - "name": "role", - "in": "path", - "required": true - }, - { - "type": "integer", - "description": "Limit results", - "name": "limit", - "in": "query" - }, - { - "type": "integer", - "description": "Offset results", - "name": "offset", - "in": "query" - } - ], - "responses": { - "200": { - "description": "Users with pagination metadata", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.APIResponse" - }, - { - "type": "object", - "properties": { - "data": { - "type": "object", - "additionalProperties": true - } - } - } - ] - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "403": { - "description": "Forbidden", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - } - } - } - }, "/admin/v1/users/{id}": { "get": { "security": [ @@ -4917,82 +4739,6 @@ const docTemplate = `{ } } }, - "/admin/v1/users/{id}/role": { - "put": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "Update user role (admin only)", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Admin-Users" - ], - "summary": "Update user role", - "parameters": [ - { - "type": "string", - "description": "User ID", - "name": "id", - "in": "path", - "required": true - }, - { - "description": "Role update data", - "name": "role", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/user.UpdateUserRoleForm" - } - } - ], - "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" - } - }, - "403": { - "description": "Forbidden", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "404": { - "description": "Not Found", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - } - } - } - }, "/admin/v1/users/{id}/status": { "put": { "security": [ @@ -8301,12 +8047,12 @@ const docTemplate = `{ "new_password": { "description": "New password (minimum 8 characters)", "type": "string", - "example": "NewPass456!" + "example": "NewAdmin!1234" }, "old_password": { "description": "Current password for verification", "type": "string", - "example": "OldPassword123!" + "example": "Admin!1234" } } }, @@ -8321,42 +8067,42 @@ const docTemplate = `{ "email": { "description": "Valid email address", "type": "string", - "example": "john.smith@company.com" + "example": "admin@opplens.com" }, "full_name": { "description": "Full name of the user", "type": "string", - "example": "John Smith" + "example": "Admin User" }, "password": { "description": "Password (minimum 8 characters)", "type": "string", - "example": "SecurePass123!" + "example": "Admin!1234" }, "phone": { "description": "Optional phone number", "type": "string", - "example": "+1234567890" + "example": "+18289784438" }, "position": { "description": "Optional job position", "type": "string", - "example": "Senior Developer" + "example": "Lead" }, "profile_image": { "description": "Optional profile image URL", "type": "string", - "example": "https://example.com/avatar.jpg" + "example": "" }, "role": { "description": "User role (admin, manager, operator, viewer)", "type": "string", - "example": "manager" + "example": "admin" }, "username": { "description": "Unique username (alphanumeric only)", "type": "string", - "example": "johnsmith" + "example": "admin" } } }, @@ -8366,12 +8112,12 @@ const docTemplate = `{ "password": { "description": "User password", "type": "string", - "example": "Nima.1998" + "example": "Admin!1234" }, "username": { "description": "Username or email address", "type": "string", - "example": "nakhostin" + "example": "admin" } } }, @@ -8391,70 +8137,52 @@ const docTemplate = `{ "email": { "description": "Email address for password reset", "type": "string", - "example": "john.smith@company.com" + "example": "admin@opplens.com" } } }, "user.UpdateUserForm": { "type": "object", "properties": { - "company_id": { - "description": "Updated company ID", - "type": "string", - "example": "123e4567-e89b-12d3-a456-426614174000" - }, "department": { "description": "Updated department", "type": "string", - "example": "Product Management" + "example": "Information Technology" }, "email": { "description": "Updated email address", "type": "string", - "example": "john.smith@newcompany.com" + "example": "admin@opplens.com" }, "full_name": { "description": "Updated full name", "type": "string", - "example": "John Smith" + "example": "Admin User" }, "phone": { "description": "Updated phone number", "type": "string", - "example": "+1234567890" + "example": "+18289784438" }, "position": { "description": "Updated position", "type": "string", - "example": "Tech Lead" + "example": "Lead" }, "profile_image": { "description": "Updated profile image URL", "type": "string", - "example": "https://example.com/new-avatar.jpg" + "example": "" }, "role": { "description": "Updated user role", "type": "string", "example": "admin" }, - "status": { - "description": "Updated account status", - "type": "string", - "example": "active" - }, "username": { "description": "Updated username", "type": "string", - "example": "johnsmith" - } - } - }, - "user.UpdateUserRoleForm": { - "type": "object", - "properties": { - "role": { - "type": "string" + "example": "admin" } } }, @@ -8469,17 +8197,8 @@ const docTemplate = `{ "user.UserListResponse": { "type": "object", "properties": { - "limit": { - "type": "integer" - }, - "offset": { - "type": "integer" - }, - "total": { - "type": "integer" - }, - "total_pages": { - "type": "integer" + "meta": { + "$ref": "#/definitions/response.Meta" }, "users": { "type": "array", @@ -8492,15 +8211,9 @@ const docTemplate = `{ "user.UserResponse": { "type": "object", "properties": { - "company_id": { - "type": "string" - }, "created_at": { "type": "integer" }, - "created_by": { - "type": "string" - }, "department": { "type": "string" }, @@ -8537,9 +8250,6 @@ const docTemplate = `{ "updated_at": { "type": "integer" }, - "updated_by": { - "type": "string" - }, "username": { "type": "string" } diff --git a/cmd/web/docs/swagger.json b/cmd/web/docs/swagger.json index beac4d2..80975a4 100644 --- a/cmd/web/docs/swagger.json +++ b/cmd/web/docs/swagger.json @@ -3449,7 +3449,7 @@ "application/json" ], "tags": [ - "Admin-Users" + "Admin-Authorization" ], "summary": "Get authenticated user profile", "responses": { @@ -3505,7 +3505,7 @@ "application/json" ], "tags": [ - "Admin-Users" + "Admin-Authorization" ], "summary": "Update authenticated user profile", "parameters": [ @@ -3586,7 +3586,7 @@ "application/json" ], "tags": [ - "Admin-Users" + "Admin-Authorization" ], "summary": "Change user password", "parameters": [ @@ -4347,12 +4347,6 @@ "name": "role", "in": "query" }, - { - "type": "string", - "description": "Company ID filter", - "name": "company_id", - "in": "query" - }, { "type": "integer", "description": "Limit results", @@ -4509,178 +4503,6 @@ } } }, - "/admin/v1/users/company/{company_id}": { - "get": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "Get users belonging to a specific company (admin only)", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Admin-Users" - ], - "summary": "Get users by company ID", - "parameters": [ - { - "type": "string", - "description": "Company ID", - "name": "company_id", - "in": "path", - "required": true - }, - { - "type": "integer", - "description": "Limit results", - "name": "limit", - "in": "query" - }, - { - "type": "integer", - "description": "Offset results", - "name": "offset", - "in": "query" - } - ], - "responses": { - "200": { - "description": "Users with pagination metadata", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.APIResponse" - }, - { - "type": "object", - "properties": { - "data": { - "type": "object", - "additionalProperties": true - } - } - } - ] - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "403": { - "description": "Forbidden", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - } - } - } - }, - "/admin/v1/users/role/{role}": { - "get": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "Get users with a specific role (admin only)", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Admin-Users" - ], - "summary": "Get users by role", - "parameters": [ - { - "type": "string", - "description": "User role", - "name": "role", - "in": "path", - "required": true - }, - { - "type": "integer", - "description": "Limit results", - "name": "limit", - "in": "query" - }, - { - "type": "integer", - "description": "Offset results", - "name": "offset", - "in": "query" - } - ], - "responses": { - "200": { - "description": "Users with pagination metadata", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.APIResponse" - }, - { - "type": "object", - "properties": { - "data": { - "type": "object", - "additionalProperties": true - } - } - } - ] - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "403": { - "description": "Forbidden", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - } - } - } - }, "/admin/v1/users/{id}": { "get": { "security": [ @@ -4911,82 +4733,6 @@ } } }, - "/admin/v1/users/{id}/role": { - "put": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "Update user role (admin only)", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Admin-Users" - ], - "summary": "Update user role", - "parameters": [ - { - "type": "string", - "description": "User ID", - "name": "id", - "in": "path", - "required": true - }, - { - "description": "Role update data", - "name": "role", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/user.UpdateUserRoleForm" - } - } - ], - "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" - } - }, - "403": { - "description": "Forbidden", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "404": { - "description": "Not Found", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - } - } - } - }, "/admin/v1/users/{id}/status": { "put": { "security": [ @@ -8295,12 +8041,12 @@ "new_password": { "description": "New password (minimum 8 characters)", "type": "string", - "example": "NewPass456!" + "example": "NewAdmin!1234" }, "old_password": { "description": "Current password for verification", "type": "string", - "example": "OldPassword123!" + "example": "Admin!1234" } } }, @@ -8315,42 +8061,42 @@ "email": { "description": "Valid email address", "type": "string", - "example": "john.smith@company.com" + "example": "admin@opplens.com" }, "full_name": { "description": "Full name of the user", "type": "string", - "example": "John Smith" + "example": "Admin User" }, "password": { "description": "Password (minimum 8 characters)", "type": "string", - "example": "SecurePass123!" + "example": "Admin!1234" }, "phone": { "description": "Optional phone number", "type": "string", - "example": "+1234567890" + "example": "+18289784438" }, "position": { "description": "Optional job position", "type": "string", - "example": "Senior Developer" + "example": "Lead" }, "profile_image": { "description": "Optional profile image URL", "type": "string", - "example": "https://example.com/avatar.jpg" + "example": "" }, "role": { "description": "User role (admin, manager, operator, viewer)", "type": "string", - "example": "manager" + "example": "admin" }, "username": { "description": "Unique username (alphanumeric only)", "type": "string", - "example": "johnsmith" + "example": "admin" } } }, @@ -8360,12 +8106,12 @@ "password": { "description": "User password", "type": "string", - "example": "Nima.1998" + "example": "Admin!1234" }, "username": { "description": "Username or email address", "type": "string", - "example": "nakhostin" + "example": "admin" } } }, @@ -8385,70 +8131,52 @@ "email": { "description": "Email address for password reset", "type": "string", - "example": "john.smith@company.com" + "example": "admin@opplens.com" } } }, "user.UpdateUserForm": { "type": "object", "properties": { - "company_id": { - "description": "Updated company ID", - "type": "string", - "example": "123e4567-e89b-12d3-a456-426614174000" - }, "department": { "description": "Updated department", "type": "string", - "example": "Product Management" + "example": "Information Technology" }, "email": { "description": "Updated email address", "type": "string", - "example": "john.smith@newcompany.com" + "example": "admin@opplens.com" }, "full_name": { "description": "Updated full name", "type": "string", - "example": "John Smith" + "example": "Admin User" }, "phone": { "description": "Updated phone number", "type": "string", - "example": "+1234567890" + "example": "+18289784438" }, "position": { "description": "Updated position", "type": "string", - "example": "Tech Lead" + "example": "Lead" }, "profile_image": { "description": "Updated profile image URL", "type": "string", - "example": "https://example.com/new-avatar.jpg" + "example": "" }, "role": { "description": "Updated user role", "type": "string", "example": "admin" }, - "status": { - "description": "Updated account status", - "type": "string", - "example": "active" - }, "username": { "description": "Updated username", "type": "string", - "example": "johnsmith" - } - } - }, - "user.UpdateUserRoleForm": { - "type": "object", - "properties": { - "role": { - "type": "string" + "example": "admin" } } }, @@ -8463,17 +8191,8 @@ "user.UserListResponse": { "type": "object", "properties": { - "limit": { - "type": "integer" - }, - "offset": { - "type": "integer" - }, - "total": { - "type": "integer" - }, - "total_pages": { - "type": "integer" + "meta": { + "$ref": "#/definitions/response.Meta" }, "users": { "type": "array", @@ -8486,15 +8205,9 @@ "user.UserResponse": { "type": "object", "properties": { - "company_id": { - "type": "string" - }, "created_at": { "type": "integer" }, - "created_by": { - "type": "string" - }, "department": { "type": "string" }, @@ -8531,9 +8244,6 @@ "updated_at": { "type": "integer" }, - "updated_by": { - "type": "string" - }, "username": { "type": "string" } diff --git a/cmd/web/docs/swagger.yaml b/cmd/web/docs/swagger.yaml index 03077d4..a863855 100644 --- a/cmd/web/docs/swagger.yaml +++ b/cmd/web/docs/swagger.yaml @@ -1121,11 +1121,11 @@ definitions: properties: new_password: description: New password (minimum 8 characters) - example: NewPass456! + example: NewAdmin!1234 type: string old_password: description: Current password for verification - example: OldPassword123! + example: Admin!1234 type: string type: object user.CreateUserForm: @@ -1136,46 +1136,46 @@ definitions: type: string email: description: Valid email address - example: john.smith@company.com + example: admin@opplens.com type: string full_name: description: Full name of the user - example: John Smith + example: Admin User type: string password: description: Password (minimum 8 characters) - example: SecurePass123! + example: Admin!1234 type: string phone: description: Optional phone number - example: "+1234567890" + example: "+18289784438" type: string position: description: Optional job position - example: Senior Developer + example: Lead type: string profile_image: description: Optional profile image URL - example: https://example.com/avatar.jpg + example: "" type: string role: description: User role (admin, manager, operator, viewer) - example: manager + example: admin type: string username: description: Unique username (alphanumeric only) - example: johnsmith + example: admin type: string type: object user.LoginForm: properties: password: description: User password - example: Nima.1998 + example: Admin!1234 type: string username: description: Username or email address - example: nakhostin + example: admin type: string type: object user.RefreshTokenForm: @@ -1189,55 +1189,42 @@ definitions: properties: email: description: Email address for password reset - example: john.smith@company.com + example: admin@opplens.com type: string type: object user.UpdateUserForm: properties: - company_id: - description: Updated company ID - example: 123e4567-e89b-12d3-a456-426614174000 - type: string department: description: Updated department - example: Product Management + example: Information Technology type: string email: description: Updated email address - example: john.smith@newcompany.com + example: admin@opplens.com type: string full_name: description: Updated full name - example: John Smith + example: Admin User type: string phone: description: Updated phone number - example: "+1234567890" + example: "+18289784438" type: string position: description: Updated position - example: Tech Lead + example: Lead type: string profile_image: description: Updated profile image URL - example: https://example.com/new-avatar.jpg + example: "" type: string role: description: Updated user role example: admin type: string - status: - description: Updated account status - example: active - type: string username: description: Updated username - example: johnsmith - type: string - type: object - user.UpdateUserRoleForm: - properties: - role: + example: admin type: string type: object user.UpdateUserStatusForm: @@ -1247,14 +1234,8 @@ definitions: type: object user.UserListResponse: properties: - limit: - type: integer - offset: - type: integer - total: - type: integer - total_pages: - type: integer + meta: + $ref: '#/definitions/response.Meta' users: items: $ref: '#/definitions/user.UserResponse' @@ -1262,12 +1243,8 @@ definitions: type: object user.UserResponse: properties: - company_id: - type: string created_at: type: integer - created_by: - type: string department: type: string email: @@ -1292,8 +1269,6 @@ definitions: type: string updated_at: type: integer - updated_by: - type: string username: type: string type: object @@ -3521,7 +3496,7 @@ paths: - BearerAuth: [] summary: Get authenticated user profile tags: - - Admin-Users + - Admin-Authorization put: consumes: - application/json @@ -3572,7 +3547,7 @@ paths: - BearerAuth: [] summary: Update authenticated user profile tags: - - Admin-Users + - Admin-Authorization /admin/v1/profile/change-password: put: consumes: @@ -3616,7 +3591,7 @@ paths: - BearerAuth: [] summary: Change user password tags: - - Admin-Users + - Admin-Authorization /admin/v1/profile/login: post: consumes: @@ -4070,10 +4045,6 @@ paths: in: query name: role type: string - - description: Company ID filter - in: query - name: company_id - type: string - description: Limit results in: query name: limit @@ -4322,55 +4293,6 @@ paths: summary: Update user tags: - Admin-Users - /admin/v1/users/{id}/role: - put: - consumes: - - application/json - description: Update user role (admin only) - parameters: - - description: User ID - in: path - name: id - required: true - type: string - - description: Role update data - in: body - name: role - required: true - schema: - $ref: '#/definitions/user.UpdateUserRoleForm' - 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' - "403": - description: Forbidden - 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: Update user role - tags: - - Admin-Users /admin/v1/users/{id}/status: put: consumes: @@ -4420,112 +4342,6 @@ paths: summary: Update user status tags: - Admin-Users - /admin/v1/users/company/{company_id}: - get: - consumes: - - application/json - description: Get users belonging to a specific company (admin only) - parameters: - - description: Company ID - in: path - name: company_id - required: true - type: string - - description: Limit results - in: query - name: limit - type: integer - - description: Offset results - in: query - name: offset - type: integer - produces: - - application/json - responses: - "200": - description: Users with pagination metadata - schema: - allOf: - - $ref: '#/definitions/response.APIResponse' - - properties: - data: - additionalProperties: true - type: object - type: object - "400": - description: Bad Request - schema: - $ref: '#/definitions/response.APIResponse' - "401": - description: Unauthorized - schema: - $ref: '#/definitions/response.APIResponse' - "403": - description: Forbidden - schema: - $ref: '#/definitions/response.APIResponse' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/response.APIResponse' - security: - - BearerAuth: [] - summary: Get users by company ID - tags: - - Admin-Users - /admin/v1/users/role/{role}: - get: - consumes: - - application/json - description: Get users with a specific role (admin only) - parameters: - - description: User role - in: path - name: role - required: true - type: string - - description: Limit results - in: query - name: limit - type: integer - - description: Offset results - in: query - name: offset - type: integer - produces: - - application/json - responses: - "200": - description: Users with pagination metadata - schema: - allOf: - - $ref: '#/definitions/response.APIResponse' - - properties: - data: - additionalProperties: true - type: object - type: object - "400": - description: Bad Request - schema: - $ref: '#/definitions/response.APIResponse' - "401": - description: Unauthorized - schema: - $ref: '#/definitions/response.APIResponse' - "403": - description: Forbidden - schema: - $ref: '#/definitions/response.APIResponse' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/response.APIResponse' - security: - - BearerAuth: [] - summary: Get users by role - tags: - - Admin-Users /api/v1/companies: get: consumes: diff --git a/cmd/web/router/routes.go b/cmd/web/router/routes.go index a66cd92..80135d9 100644 --- a/cmd/web/router/routes.go +++ b/cmd/web/router/routes.go @@ -24,9 +24,6 @@ func RegisterAdminRoutes(e *echo.Echo, userHandler *user.Handler, companyHandler adminUsersGP.PUT("/:id", userHandler.UpdateUser) adminUsersGP.DELETE("/:id", userHandler.DeleteUser) adminUsersGP.PUT("/:id/status", userHandler.UpdateUserStatus) - adminUsersGP.PUT("/:id/role", userHandler.UpdateUserRole) - adminUsersGP.GET("/company/:company_id", userHandler.GetUsersByCompanyID) - adminUsersGP.GET("/role/:role", userHandler.GetUsersByRole) } // Admin user profile diff --git a/internal/user/entity.go b/internal/user/entity.go index 8c66699..f3bb7bb 100644 --- a/internal/user/entity.go +++ b/internal/user/entity.go @@ -28,20 +28,18 @@ const ( // User represents a system user (admin/manager/operator) type User struct { mongo.Model `bson:",inline"` - FullName string `bson:"full_name" json:"full_name"` - Username string `bson:"username" json:"username"` - Email string `bson:"email" json:"email"` - Password string `bson:"password" json:"-"` // Never serialize password - Role UserRole `bson:"role" json:"role"` - Status UserStatus `bson:"status" json:"status"` - Department *string `bson:"department,omitempty" json:"department,omitempty"` - Position *string `bson:"position,omitempty" json:"position,omitempty"` - Phone *string `bson:"phone,omitempty" json:"phone,omitempty"` - ProfileImage *string `bson:"profile_image,omitempty" json:"profile_image,omitempty"` - IsVerified bool `bson:"is_verified" json:"is_verified"` - LastLoginAt *int64 `bson:"last_login_at,omitempty" json:"last_login_at,omitempty"` // Unix timestamp - CreatedBy *string `bson:"created_by,omitempty" json:"created_by,omitempty"` - UpdatedBy *string `bson:"updated_by,omitempty" json:"updated_by,omitempty"` + FullName string `bson:"full_name"` + Username string `bson:"username"` + Email string `bson:"email"` + Password string `bson:"password" json:"-"` + Role UserRole `bson:"role"` + Status UserStatus `bson:"status"` + Department *string `bson:"department"` + Position *string `bson:"position"` + Phone *string `bson:"phone"` + ProfileImage *string `bson:"profile_image"` + IsVerified bool `bson:"is_verified"` + LastLoginAt *int64 `bson:"last_login_at"` } // SetID sets the user ID (implements IDSetter interface) @@ -54,22 +52,22 @@ func (u *User) GetID() string { return u.ID.Hex() } -// SetCreatedAt sets the created timestamp (implements Timestampable interface) +// SetCreatedAt sets the created timestamp (implements Timestamp interface) func (u *User) SetCreatedAt(timestamp int64) { u.CreatedAt = timestamp } -// SetUpdatedAt sets the updated timestamp (implements Timestampable interface) +// SetUpdatedAt sets the updated timestamp (implements Timestamp interface) func (u *User) SetUpdatedAt(timestamp int64) { u.UpdatedAt = timestamp } -// GetCreatedAt returns the created timestamp (implements Timestampable interface) +// GetCreatedAt returns the created timestamp (implements Timestamp interface) func (u *User) GetCreatedAt() int64 { return u.CreatedAt } -// GetUpdatedAt returns the updated timestamp (implements Timestampable interface) +// GetUpdatedAt returns the updated timestamp (implements Timestamp interface) func (u *User) GetUpdatedAt() int64 { return u.UpdatedAt } diff --git a/internal/user/form.go b/internal/user/form.go index 5b43e18..a317b57 100644 --- a/internal/user/form.go +++ b/internal/user/form.go @@ -1,40 +1,40 @@ package user +import "tm/pkg/response" + // User Registration DTOs // CreateUserForm represents the data required to create a new user account type CreateUserForm struct { - FullName string `json:"full_name" valid:"required,length(2|100)" example:"John Smith"` // Full name of the user - Username string `json:"username" valid:"required,alphanum,length(3|30)" example:"johnsmith"` // Unique username (alphanumeric only) - Email string `json:"email" valid:"required,email" example:"john.smith@company.com"` // Valid email address - Password string `json:"password" valid:"required,length(8|128)" example:"SecurePass123!"` // Password (minimum 8 characters) - Role string `json:"role" valid:"required,in(admin|manager|operator|viewer)" example:"manager"` // User role (admin, manager, operator, viewer) - Department *string `json:"department,omitempty" valid:"optional,length(2|100)" example:"Information Technology"` // Optional department name - Position *string `json:"position,omitempty" valid:"optional,length(2|100)" example:"Senior Developer"` // Optional job position - Phone *string `json:"phone,omitempty" valid:"optional,length(10|20)" example:"+1234567890"` // Optional phone number - ProfileImage *string `json:"profile_image,omitempty" valid:"optional,url" example:"https://example.com/avatar.jpg"` // Optional profile image URL + FullName string `json:"full_name" valid:"required,length(2|100)" example:"Admin User"` // Full name of the user + Username string `json:"username" valid:"required,alphanum,length(3|30)" example:"admin"` // Unique username (alphanumeric only) + Email string `json:"email" valid:"required,email" example:"admin@opplens.com"` // Valid email address + Password string `json:"password" valid:"required,length(8|128)" example:"Admin!1234"` // Password (minimum 8 characters) + Role string `json:"role" valid:"required,in(admin|manager|operator|viewer)" example:"admin"` // User role (admin, manager, operator, viewer) + Department *string `json:"department" valid:"optional,length(2|100)" example:"Information Technology"` // Optional department name + Position *string `json:"position" valid:"optional,length(2|100)" example:"Lead"` // Optional job position + Phone *string `json:"phone" valid:"optional,length(10|20)" example:"+18289784438"` // Optional phone number + ProfileImage *string `json:"profile_image" valid:"optional,url" example:""` // Optional profile image URL } // UpdateUserForm represents the data for updating an existing user account type UpdateUserForm struct { - FullName *string `json:"full_name,omitempty" valid:"optional,length(2|100)" example:"John Smith"` // Updated full name - Username *string `json:"username,omitempty" valid:"optional,alphanum,length(3|30)" example:"johnsmith"` // Updated username - Email *string `json:"email,omitempty" valid:"optional,email" example:"john.smith@newcompany.com"` // Updated email address - Role *string `json:"role,omitempty" valid:"optional,in(admin|manager|operator|viewer)" example:"admin"` // Updated user role - CompanyID *string `json:"company_id,omitempty" valid:"optional,uuid" example:"123e4567-e89b-12d3-a456-426614174000"` // Updated company ID - Department *string `json:"department,omitempty" valid:"optional,length(2|100)" example:"Product Management"` // Updated department - Position *string `json:"position,omitempty" valid:"optional,length(2|100)" example:"Tech Lead"` // Updated position - Phone *string `json:"phone,omitempty" valid:"optional,length(10|20)" example:"+1234567890"` // Updated phone number - ProfileImage *string `json:"profile_image,omitempty" valid:"optional,url" example:"https://example.com/new-avatar.jpg"` // Updated profile image URL - Status *string `json:"status,omitempty" valid:"optional,in(active|inactive|suspended)" example:"active"` // Updated account status + FullName *string `json:"full_name" valid:"optional,length(2|100)" example:"Admin User"` // Updated full name + Username *string `json:"username" valid:"optional,alphanum,length(3|30)" example:"admin"` // Updated username + Email *string `json:"email" valid:"optional,email" example:"admin@opplens.com"` // Updated email address + Role *string `json:"role" valid:"optional,in(admin|manager|operator|viewer)" example:"admin"` // Updated user role + Department *string `json:"department" valid:"optional,length(2|100)" example:"Information Technology"` // Updated department + Position *string `json:"position" valid:"optional,length(2|100)" example:"Lead"` // Updated position + Phone *string `json:"phone" valid:"optional,length(10|20)" example:"+18289784438"` // Updated phone number + ProfileImage *string `json:"profile_image" valid:"optional,url" example:""` // Updated profile image URL } // User Authentication DTOs // LoginForm represents the credentials required for user authentication type LoginForm struct { - Username string `json:"username" valid:"required" example:"nakhostin"` // Username or email address - Password string `json:"password" valid:"required" example:"Nima.1998"` // User password + Username string `json:"username" valid:"required" example:"admin"` // Username or email address + Password string `json:"password" valid:"required" example:"Admin!1234"` // User password } // RefreshTokenForm represents the refresh token required to generate new access tokens @@ -44,22 +44,22 @@ type RefreshTokenForm struct { // ChangePasswordForm represents the data required to change user password type ChangePasswordForm struct { - OldPassword string `json:"old_password" valid:"required" example:"OldPassword123!"` // Current password for verification - NewPassword string `json:"new_password" valid:"required,length(8|128)" example:"NewPass456!"` // New password (minimum 8 characters) + OldPassword string `json:"old_password" valid:"required" example:"Admin!1234"` // Current password for verification + NewPassword string `json:"new_password" valid:"required,length(8|128)" example:"NewAdmin!1234"` // New password (minimum 8 characters) } // UpdateProfileForm represents the data for updating user profile information type UpdateProfileForm struct { - FullName *string `json:"full_name,omitempty" valid:"optional,length(2|100)" example:"John Smith"` // Updated full name - Department *string `json:"department,omitempty" valid:"optional,length(2|100)" example:"Engineering"` // Updated department - Position *string `json:"position,omitempty" valid:"optional,length(2|100)" example:"Senior Engineer"` // Updated position - Phone *string `json:"phone,omitempty" valid:"optional,length(10|20)" example:"+1234567890"` // Updated phone number - ProfileImage *string `json:"profile_image,omitempty" valid:"optional,url" example:"https://example.com/profile.jpg"` // Updated profile image URL + FullName *string `json:"full_name,omitempty" valid:"optional,length(2|100)" example:"Admin User"` // Updated full name + Department *string `json:"department,omitempty" valid:"optional,length(2|100)" example:"Information Technology"` // Updated department + Position *string `json:"position,omitempty" valid:"optional,length(2|100)" example:"Lead"` // Updated position + Phone *string `json:"phone,omitempty" valid:"optional,length(10|20)" example:"+18289784438"` // Updated phone number + ProfileImage *string `json:"profile_image,omitempty" valid:"optional,url" example:""` // Updated profile image URL } // ResetPasswordForm represents the email address for password reset type ResetPasswordForm struct { - Email string `json:"email" valid:"required,email" example:"john.smith@company.com"` // Email address for password reset + Email string `json:"email" valid:"required,email" example:"admin@opplens.com"` // Email address for password reset } // Admin DTOs @@ -67,7 +67,7 @@ type ListUsersForm struct { Search *string `query:"search" valid:"optional"` Status *string `query:"status" valid:"optional,in(active|inactive|suspended)"` Role *string `query:"role" valid:"optional,in(admin|manager|operator|viewer)"` - CompanyID *string `query:"company_id" valid:"optional,uuid"` + CompanyID *string `query:"company_id" valid:"optional"` 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|username|role|created_at|last_login_at)"` @@ -90,17 +90,14 @@ type UserResponse struct { Email string `json:"email"` Role string `json:"role"` Status string `json:"status"` - CompanyID *string `json:"company_id,omitempty"` - Department *string `json:"department,omitempty"` - Position *string `json:"position,omitempty"` - Phone *string `json:"phone,omitempty"` - ProfileImage *string `json:"profile_image,omitempty"` + Department *string `json:"department"` + Position *string `json:"position"` + Phone *string `json:"phone"` + ProfileImage *string `json:"profile_image"` IsVerified bool `json:"is_verified"` - LastLoginAt *int64 `json:"last_login_at,omitempty"` - CreatedAt int64 `json:"created_at"` + LastLoginAt *int64 `json:"last_login_at"` UpdatedAt int64 `json:"updated_at"` - CreatedBy *string `json:"created_by,omitempty"` - UpdatedBy *string `json:"updated_by,omitempty"` + CreatedAt int64 `json:"created_at"` } type AuthResponse struct { @@ -111,11 +108,14 @@ type AuthResponse struct { } type UserListResponse struct { - Users []*UserResponse `json:"users"` - Total int64 `json:"total"` - Limit int `json:"limit"` - Offset int `json:"offset"` - TotalPages int `json:"total_pages"` + Users []*UserResponse `json:"users"` + Meta *response.Meta `json:"meta"` +} + +type SearchUsersForm struct { + Search *string `query:"search" valid:"optional"` + Status *string `query:"status" valid:"optional,in(active|inactive|suspended)"` + Role *string `query:"role" valid:"optional,in(admin|manager|operator|viewer)"` } // Helper function to convert User to UserResponse @@ -133,9 +133,7 @@ func (u *User) ToResponse() *UserResponse { ProfileImage: u.ProfileImage, IsVerified: u.IsVerified, LastLoginAt: u.LastLoginAt, - CreatedAt: u.CreatedAt, UpdatedAt: u.UpdatedAt, - CreatedBy: u.CreatedBy, - UpdatedBy: u.UpdatedBy, + CreatedAt: u.CreatedAt, } } diff --git a/internal/user/handler.go b/internal/user/handler.go index 04d3bbe..6127b40 100644 --- a/internal/user/handler.go +++ b/internal/user/handler.go @@ -1,7 +1,6 @@ package user import ( - "strconv" "tm/pkg/authorization" "tm/pkg/logger" "tm/pkg/response" @@ -117,7 +116,7 @@ func (h *Handler) ResetPassword(c echo.Context) error { // GetProfile gets current user profile // @Summary Get authenticated user profile // @Description Retrieve complete profile information for the currently authenticated user including personal details, role information, permissions, and account status. This endpoint requires valid authentication token. -// @Tags Admin-Users +// @Tags Admin-Authorization // @Accept json // @Produce json // @Security BearerAuth @@ -133,7 +132,7 @@ func (h *Handler) GetProfile(c echo.Context) error { return response.Unauthorized(c, "User not authenticated") } - user, err := h.service.GetProfile(c.Request().Context(), userID) + user, err := h.service.GetUserByID(c.Request().Context(), userID) if err != nil { return response.NotFound(c, "User not found") } @@ -144,7 +143,7 @@ func (h *Handler) GetProfile(c echo.Context) error { // UpdateProfile updates current user profile // @Summary Update authenticated user profile // @Description Update profile information for the currently authenticated user including personal details, contact information, and preferences. Only the authenticated user can update their own profile. -// @Tags Admin-Users +// @Tags Admin-Authorization // @Accept json // @Produce json // @Security BearerAuth @@ -168,7 +167,7 @@ func (h *Handler) UpdateProfile(c echo.Context) error { return response.BadRequest(c, "Invalid request format", "") } - user, err := h.service.UpdateUser(c.Request().Context(), userID, form, &userID) + user, err := h.service.Update(c.Request().Context(), userID, form) if err != nil { return response.BadRequest(c, err.Error(), "") } @@ -179,7 +178,7 @@ func (h *Handler) UpdateProfile(c echo.Context) error { // ChangePassword changes current user password // @Summary Change user password // @Description Change password for the currently authenticated user. Requires current password verification and enforces password policy. This operation invalidates all existing sessions and requires re-authentication. -// @Tags Admin-Users +// @Tags Admin-Authorization // @Accept json // @Produce json // @Security BearerAuth @@ -261,14 +260,8 @@ func (h *Handler) Logout(c echo.Context) error { // @Failure 500 {object} response.APIResponse "Internal server error" // @Router /admin/v1/users [post] func (h *Handler) CreateUser(c echo.Context) error { - // Get current user ID from JWT token - currentUserID, err := GetUserIDFromContext(c) + form, err := response.Parse[CreateUserForm](c) if err != nil { - return response.Unauthorized(c, "User not authenticated") - } - - var form CreateUserForm - if err := c.Bind(&form); err != nil { return response.BadRequest(c, "Invalid request format", "") } @@ -278,7 +271,7 @@ func (h *Handler) CreateUser(c echo.Context) error { } // Call service - user, err := h.service.CreateUser(c.Request().Context(), &form, ¤tUserID) + user, err := h.service.Register(c.Request().Context(), form) if err != nil { return response.BadRequest(c, err.Error(), "") } @@ -296,7 +289,6 @@ func (h *Handler) CreateUser(c echo.Context) error { // @Param search query string false "Search term" // @Param status query string false "User status filter" // @Param role query string false "User role filter" -// @Param company_id query string false "Company ID filter" // @Param limit query int false "Limit results" // @Param offset query int false "Offset results" // @Param sort_by query string false "Sort field" @@ -308,8 +300,8 @@ func (h *Handler) CreateUser(c echo.Context) error { // @Failure 500 {object} response.APIResponse // @Router /admin/v1/users [get] func (h *Handler) ListUsers(c echo.Context) error { - var form ListUsersForm - if err := c.Bind(&form); err != nil { + form, err := response.Parse[SearchUsersForm](c) + if err != nil { return response.BadRequest(c, "Invalid request format", "") } @@ -319,7 +311,7 @@ func (h *Handler) ListUsers(c echo.Context) error { } // Call service - users, err := h.service.ListUsers(c.Request().Context(), &form) + users, err := h.service.Search(c.Request().Context(), form, response.NewPagination(c)) if err != nil { return response.InternalServerError(c, "Failed to list users") } @@ -370,16 +362,10 @@ func (h *Handler) GetUserByID(c echo.Context) error { // @Failure 500 {object} response.APIResponse // @Router /admin/v1/users/{id} [put] func (h *Handler) UpdateUser(c echo.Context) error { - // 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") - var form UpdateUserForm - if err := c.Bind(&form); err != nil { + form, err := response.Parse[UpdateUserForm](c) + if err != nil { return response.BadRequest(c, "Invalid request format", "") } @@ -389,7 +375,7 @@ func (h *Handler) UpdateUser(c echo.Context) error { } // Call service - user, err := h.service.UpdateUser(c.Request().Context(), idStr, &form, ¤tUserID) + user, err := h.service.Update(c.Request().Context(), idStr, form) if err != nil { return response.BadRequest(c, err.Error(), "") } @@ -413,15 +399,9 @@ func (h *Handler) UpdateUser(c echo.Context) error { // @Failure 500 {object} response.APIResponse // @Router /admin/v1/users/{id} [delete] func (h *Handler) DeleteUser(c echo.Context) error { - // Extract user ID from JWT token context - currentUserID, err := GetUserIDFromContext(c) - if err != nil { - return response.Unauthorized(c, "User not authenticated") - } + id := c.Param("id") - idStr := c.Param("id") - - err = h.service.DeleteUser(c.Request().Context(), idStr, ¤tUserID) + err := h.service.Delete(c.Request().Context(), id) if err != nil { return response.BadRequest(c, err.Error(), "") } @@ -448,15 +428,10 @@ func (h *Handler) DeleteUser(c echo.Context) error { // @Failure 500 {object} response.APIResponse // @Router /admin/v1/users/{id}/status [put] func (h *Handler) UpdateUserStatus(c echo.Context) error { - currentUserID, err := GetUserIDFromContext(c) - if err != nil { - return response.Unauthorized(c, "User not authenticated") - } - idStr := c.Param("id") - var form UpdateUserStatusForm - if err := c.Bind(&form); err != nil { + form, err := response.Parse[UpdateUserStatusForm](c) + if err != nil { return response.BadRequest(c, "Invalid request format", "") } @@ -466,7 +441,7 @@ func (h *Handler) UpdateUserStatus(c echo.Context) error { } // Call service - err = h.service.UpdateUserStatus(c.Request().Context(), idStr, &form, ¤tUserID) + err = h.service.UpdateStatus(c.Request().Context(), idStr, form) if err != nil { return response.BadRequest(c, err.Error(), "") } @@ -475,177 +450,3 @@ func (h *Handler) UpdateUserStatus(c echo.Context) error { "message": "User status updated successfully", }, "User status updated successfully") } - -// UpdateUserRole updates user role (admin only) -// @Summary Update user role -// @Description Update user role (admin only) -// @Tags Admin-Users -// @Accept json -// @Produce json -// @Security BearerAuth -// @Param id path string true "User ID" -// @Param role body UpdateUserRoleForm true "Role update data" -// @Success 200 {object} response.APIResponse -// @Failure 400 {object} response.APIResponse -// @Failure 401 {object} response.APIResponse -// @Failure 403 {object} response.APIResponse -// @Failure 404 {object} response.APIResponse -// @Failure 500 {object} response.APIResponse -// @Router /admin/v1/users/{id}/role [put] -func (h *Handler) UpdateUserRole(c echo.Context) error { - // 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") - - var form UpdateUserRoleForm - if err := c.Bind(&form); err != nil { - return response.BadRequest(c, "Invalid request format", "") - } - - // Validate form - if _, err := govalidator.ValidateStruct(form); err != nil { - return response.ValidationError(c, err.Error(), "") - } - - // Call service - err = h.service.UpdateUserRole(c.Request().Context(), idStr, &form, ¤tUserID) - if err != nil { - return response.BadRequest(c, err.Error(), "") - } - - return response.Success(c, map[string]interface{}{ - "message": "User role updated successfully", - }, "User role updated successfully") -} - -// 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 Admin-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{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 /admin/v1/users/company/{company_id} [get] -func (h *Handler) GetUsersByCompanyID(c echo.Context) error { - companyIDStr := c.Param("company_id") - - // Get query parameters - limitStr := c.QueryParam("limit") - if limitStr == "" { - limitStr = "20" - } - offsetStr := c.QueryParam("offset") - if offsetStr == "" { - offsetStr = "0" - } - - limit, err := strconv.Atoi(limitStr) - if err != nil { - return response.BadRequest(c, "Invalid limit parameter", "") - } - - offset, err := strconv.Atoi(offsetStr) - if err != nil { - return response.BadRequest(c, "Invalid offset parameter", "") - } - - // Call service - users, total, err := h.service.GetUsersByCompanyID(c.Request().Context(), companyIDStr, limit, offset) - if err != nil { - return response.InternalServerError(c, "Failed to get users by company") - } - - // Convert to responses - var userResponses []*UserResponse - for _, user := range users { - userResponses = append(userResponses, user.ToResponse()) - } - - return response.SuccessWithMeta(c, map[string]interface{}{ - "users": userResponses, - }, &response.Meta{ - Total: int(total), - Limit: limit, - Offset: offset, - }, "Users retrieved successfully") -} - -// GetUsersByRole gets users by role (admin only) -// @Summary Get users by role -// @Description Get users with a specific role (admin only) -// @Tags Admin-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{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 /admin/v1/users/role/{role} [get] -func (h *Handler) GetUsersByRole(c echo.Context) error { - roleStr := c.Param("role") - role := UserRole(roleStr) - - // Validate role - if !h.validator.IsValidRole(role) { - return response.BadRequest(c, "Invalid role", "") - } - - // Get query parameters - limitStr := c.QueryParam("limit") - if limitStr == "" { - limitStr = "20" - } - offsetStr := c.QueryParam("offset") - if offsetStr == "" { - offsetStr = "0" - } - - limit, err := strconv.Atoi(limitStr) - if err != nil { - return response.BadRequest(c, "Invalid limit parameter", "") - } - - offset, err := strconv.Atoi(offsetStr) - if err != nil { - return response.BadRequest(c, "Invalid offset parameter", "") - } - - // Call service - users, err := h.service.GetUsersByRole(c.Request().Context(), role, limit, offset) - if err != nil { - return response.InternalServerError(c, "Failed to get users by role") - } - - // Convert to responses - var userResponses []*UserResponse - for _, user := range users { - userResponses = append(userResponses, user.ToResponse()) - } - - return response.SuccessWithMeta(c, map[string]interface{}{ - "users": userResponses, - "role": role, - }, &response.Meta{ - Limit: limit, - Offset: offset, - }, "Users retrieved successfully") -} - -// AuthMiddleware and AdminMiddleware are now implemented in middleware.go diff --git a/internal/user/repository.go b/internal/user/repository.go index f53d62f..f19dab6 100644 --- a/internal/user/repository.go +++ b/internal/user/repository.go @@ -5,44 +5,35 @@ import ( "errors" "time" "tm/pkg/logger" - mongopkg "tm/pkg/mongo" + orm "tm/pkg/mongo" + "tm/pkg/response" "go.mongodb.org/mongo-driver/bson" ) // Repository defines methods for user data access type Repository interface { + Login(ctx context.Context, id string) error Create(ctx context.Context, user *User) error + Update(ctx context.Context, user *User) error + Delete(ctx context.Context, id string) error GetByID(ctx context.Context, id string) (*User, error) GetByEmail(ctx context.Context, email string) (*User, error) GetByUsername(ctx context.Context, username string) (*User, error) - Update(ctx context.Context, user *User) error - Delete(ctx context.Context, id string) error - List(ctx context.Context, limit, offset int) ([]*User, error) - Search(ctx context.Context, search string, status *string, role *string, companyID *string, limit, offset int, sortBy, sortOrder string) ([]*User, error) - GetByCompanyID(ctx context.Context, companyID string, limit, offset int) ([]*User, error) - GetByRole(ctx context.Context, role UserRole, limit, offset int) ([]*User, error) - UpdateLastLogin(ctx context.Context, id string) error - CountByCompanyID(ctx context.Context, companyID string) (int64, error) + Search(ctx context.Context, form *SearchUsersForm, pagination *response.Pagination) ([]User, int64, error) } // userRepository implements the Repository interface using the MongoDB ORM type userRepository struct { - ormRepo mongopkg.Repository[User] + ormRepo orm.Repository[User] logger logger.Logger } // NewUserRepository creates a new user repository -func NewUserRepository(mongoManager *mongopkg.ConnectionManager, logger logger.Logger) Repository { +func NewUserRepository(mongoManager *orm.ConnectionManager, logger logger.Logger) Repository { // Create indexes using the ORM's index management - indexes := []mongopkg.Index{ - *mongopkg.CreateUniqueIndex("email_idx", bson.D{{Key: "email", Value: 1}}), - *mongopkg.CreateUniqueIndex("username_idx", bson.D{{Key: "username", Value: 1}}), - *mongopkg.NewIndex("company_id_idx", bson.D{{Key: "company_id", Value: 1}}), - *mongopkg.NewIndex("role_idx", bson.D{{Key: "role", Value: 1}}), - *mongopkg.NewIndex("status_idx", bson.D{{Key: "status", Value: 1}}), - *mongopkg.NewIndex("created_at_idx", bson.D{{Key: "created_at", Value: -1}}), - *mongopkg.CreateTextIndex("search_idx", "full_name", "email", "username", "department", "position"), + indexes := []orm.Index{ + *orm.CreateTextIndex("search_idx", "full_name", "email", "username"), } // Create indexes @@ -54,7 +45,7 @@ func NewUserRepository(mongoManager *mongopkg.ConnectionManager, logger logger.L } // Create ORM repository - ormRepo := mongopkg.NewRepository[User](mongoManager.GetCollection("users"), logger) + ormRepo := orm.NewRepository[User](mongoManager.GetCollection("users"), logger) return &userRepository{ ormRepo: ormRepo, @@ -62,300 +53,8 @@ func NewUserRepository(mongoManager *mongopkg.ConnectionManager, logger logger.L } } -// Create creates a new user -func (r *userRepository) Create(ctx context.Context, user *User) error { - // Set created/updated timestamps using Unix timestamps - now := time.Now().Unix() - user.SetCreatedAt(now) - user.SetUpdatedAt(now) - - // Use ORM to create user - err := r.ormRepo.Create(ctx, user) - if err != nil { - r.logger.Error("Failed to create user", map[string]interface{}{ - "error": err.Error(), - "email": user.Email, - "user_id": user.ID, - }) - return err - } - - r.logger.Info("User created successfully", map[string]interface{}{ - "user_id": user.ID, - "email": user.Email, - "role": user.Role, - }) - - return nil -} - -// GetByID retrieves a user by ID -func (r *userRepository) GetByID(ctx context.Context, id string) (*User, error) { - user, err := r.ormRepo.FindByID(ctx, id) - if err != nil { - if errors.Is(err, mongopkg.ErrDocumentNotFound) { - return nil, errors.New("user not found") - } - r.logger.Error("Failed to get user by ID", map[string]interface{}{ - "error": err.Error(), - "user_id": id, - }) - return nil, err - } - - return user, nil -} - -// GetByEmail retrieves a user by email -func (r *userRepository) GetByEmail(ctx context.Context, email string) (*User, error) { - filter := bson.M{"email": email} - user, err := r.ormRepo.FindOne(ctx, filter) - if err != nil { - if errors.Is(err, mongopkg.ErrDocumentNotFound) { - return nil, errors.New("user not found") - } - r.logger.Error("Failed to get user by email", map[string]interface{}{ - "error": err.Error(), - "email": email, - }) - return nil, err - } - - return user, nil -} - -// GetByUsername retrieves a user by username -func (r *userRepository) GetByUsername(ctx context.Context, username string) (*User, error) { - filter := bson.M{"username": username} - user, err := r.ormRepo.FindOne(ctx, filter) - if err != nil { - if errors.Is(err, mongopkg.ErrDocumentNotFound) { - return nil, errors.New("user not found") - } - r.logger.Error("Failed to get user by username", map[string]interface{}{ - "error": err.Error(), - "username": username, - }) - return nil, err - } - - return user, nil -} - -// Update updates a user -func (r *userRepository) Update(ctx context.Context, user *User) error { - // Set updated timestamp using Unix timestamp - user.SetUpdatedAt(time.Now().Unix()) - - // Use ORM to update user - err := r.ormRepo.Update(ctx, user) - if err != nil { - r.logger.Error("Failed to update user", map[string]interface{}{ - "error": err.Error(), - "user_id": user.ID, - }) - return err - } - - r.logger.Info("User updated successfully", map[string]interface{}{ - "user_id": user.ID, - "email": user.Email, - }) - - return nil -} - -// Delete deletes a user (soft delete by setting status to inactive) -func (r *userRepository) Delete(ctx context.Context, id string) error { - // Get user first - user, err := r.GetByID(ctx, id) - if err != nil { - return err - } - - // Update status to inactive - user.Status = UserStatusInactive - user.SetUpdatedAt(time.Now().Unix()) - - // Update in database - err = r.ormRepo.Update(ctx, user) - if err != nil { - r.logger.Error("Failed to delete user", map[string]interface{}{ - "error": err.Error(), - "user_id": id, - }) - return err - } - - r.logger.Info("User deleted successfully", map[string]interface{}{ - "user_id": id, - }) - - return nil -} - -// List retrieves users with pagination -func (r *userRepository) List(ctx context.Context, limit, offset int) ([]*User, error) { - // Build pagination - pagination := mongopkg.NewPaginationBuilder(). - Limit(limit). - Skip(offset). - SortDesc("created_at"). - Build() - - // Only active users by default - filter := bson.M{"status": bson.M{"$ne": UserStatusInactive}} - - // Use ORM to find users - result, err := r.ormRepo.FindAll(ctx, filter, pagination) - if err != nil { - r.logger.Error("Failed to list users", map[string]interface{}{ - "error": err.Error(), - "limit": limit, - "offset": offset, - }) - return nil, err - } - - // Convert []User to []*User - users := make([]*User, len(result.Items)) - for i := range result.Items { - users[i] = &result.Items[i] - } - - return users, nil -} - -// Search retrieves users with search and filters -func (r *userRepository) Search(ctx context.Context, search string, status *string, role *string, companyID *string, limit, offset int, sortBy, sortOrder string) ([]*User, error) { - // Build filter - filter := bson.M{} - - if search != "" { - filter["$text"] = bson.M{"$search": search} - } - - if status != nil { - filter["status"] = *status - } - - if role != nil { - filter["role"] = *role - } - - if companyID != nil { - filter["company_id"] = *companyID - } - - // Build pagination - pagination := mongopkg.NewPaginationBuilder(). - Limit(limit). - Skip(offset) - - // Set sort - if sortBy != "" { - if sortOrder == "desc" { - pagination.SortDesc(sortBy) - } else { - pagination.SortAsc(sortBy) - } - } else { - pagination.SortDesc("created_at") // Default sort - } - - // Use ORM to find users - result, err := r.ormRepo.FindAll(ctx, filter, pagination.Build()) - if err != nil { - r.logger.Error("Failed to search users", map[string]interface{}{ - "error": err.Error(), - "search": search, - }) - return nil, err - } - - // Convert []User to []*User - users := make([]*User, len(result.Items)) - for i := range result.Items { - users[i] = &result.Items[i] - } - - return users, nil -} - -// GetByCompanyID retrieves users by company ID with pagination -func (r *userRepository) GetByCompanyID(ctx context.Context, companyID string, limit, offset int) ([]*User, error) { - // Build pagination - pagination := mongopkg.NewPaginationBuilder(). - Limit(limit). - Skip(offset). - SortDesc("created_at"). - Build() - - // Filter by company ID and active status - filter := bson.M{ - "company_id": companyID, - "status": bson.M{"$ne": UserStatusInactive}, - } - - // Use ORM to find users - result, err := r.ormRepo.FindAll(ctx, filter, pagination) - if err != nil { - r.logger.Error("Failed to get users by company ID", map[string]interface{}{ - "error": err.Error(), - "company_id": companyID, - "limit": limit, - "offset": offset, - }) - return nil, err - } - - // Convert []User to []*User - users := make([]*User, len(result.Items)) - for i := range result.Items { - users[i] = &result.Items[i] - } - - return users, nil -} - -// GetByRole retrieves users by role with pagination -func (r *userRepository) GetByRole(ctx context.Context, role UserRole, limit, offset int) ([]*User, error) { - // Build pagination - pagination := mongopkg.NewPaginationBuilder(). - Limit(limit). - Skip(offset). - SortDesc("created_at"). - Build() - - // Filter by role and active status - filter := bson.M{ - "role": role, - "status": bson.M{"$ne": UserStatusInactive}, - } - - // Use ORM to find users - result, err := r.ormRepo.FindAll(ctx, filter, pagination) - if err != nil { - r.logger.Error("Failed to get users by role", map[string]interface{}{ - "error": err.Error(), - "role": role, - "limit": limit, - "offset": offset, - }) - return nil, err - } - - // Convert []User to []*User - users := make([]*User, len(result.Items)) - for i := range result.Items { - users[i] = &result.Items[i] - } - - return users, nil -} - -// UpdateLastLogin updates the last login timestamp -func (r *userRepository) UpdateLastLogin(ctx context.Context, id string) error { +// Login updates the last login timestamp +func (r *userRepository) Login(ctx context.Context, id string) error { // Get user first user, err := r.GetByID(ctx, id) if err != nil { @@ -383,21 +82,185 @@ func (r *userRepository) UpdateLastLogin(ctx context.Context, id string) error { return nil } -// CountByCompanyID counts users by company ID -func (r *userRepository) CountByCompanyID(ctx context.Context, companyID string) (int64, error) { - filter := bson.M{ - "company_id": companyID, - "status": bson.M{"$ne": UserStatusInactive}, - } +// Create creates a new user +func (r *userRepository) Create(ctx context.Context, user *User) error { + // Set created timestamp using Unix timestamp + now := time.Now().Unix() + user.SetCreatedAt(now) - count, err := r.ormRepo.Count(ctx, filter) + // Use ORM to create user + err := r.ormRepo.Create(ctx, user) if err != nil { - r.logger.Error("Failed to count users by company ID", map[string]interface{}{ - "error": err.Error(), - "company_id": companyID, + r.logger.Error("Failed to create user", map[string]interface{}{ + "error": err.Error(), + "email": user.Email, + "user_id": user.ID, }) - return 0, err + return err } - return count, nil + r.logger.Info("User created successfully", map[string]interface{}{ + "user_id": user.ID, + "email": user.Email, + "role": user.Role, + }) + + return nil +} + +// Update updates a user +func (r *userRepository) Update(ctx context.Context, user *User) error { + // Set updated timestamp using Unix timestamp + user.SetUpdatedAt(time.Now().Unix()) + + // Use ORM to update user + err := r.ormRepo.Update(ctx, user) + if err != nil { + r.logger.Error("Failed to update user", map[string]interface{}{ + "error": err.Error(), + "user_id": user.ID, + }) + return err + } + + r.logger.Info("User updated successfully", map[string]interface{}{ + "user_id": user.ID, + "email": user.Email, + }) + + return nil +} + +// GetByID retrieves a user by ID +func (r *userRepository) GetByID(ctx context.Context, id string) (*User, error) { + user, err := r.ormRepo.FindByID(ctx, id) + if err != nil { + if errors.Is(err, orm.ErrDocumentNotFound) { + return nil, errors.New("user not found") + } + r.logger.Error("Failed to get user by ID", map[string]interface{}{ + "error": err.Error(), + "user_id": id, + }) + return nil, err + } + + return user, nil +} + +// GetByEmail retrieves a user by email +func (r *userRepository) GetByEmail(ctx context.Context, email string) (*User, error) { + user, err := r.ormRepo.FindOne(ctx, bson.M{"email": email}) + if err != nil { + if errors.Is(err, orm.ErrDocumentNotFound) { + return nil, errors.New("user not found") + } + r.logger.Error("Failed to get user by email", map[string]interface{}{ + "error": err.Error(), + "email": email, + }) + return nil, err + } + + return user, nil +} + +// GetByUsername retrieves a user by username +func (r *userRepository) GetByUsername(ctx context.Context, username string) (*User, error) { + user, err := r.ormRepo.FindOne(ctx, bson.M{"username": username}) + if err != nil { + if errors.Is(err, orm.ErrDocumentNotFound) { + return nil, errors.New("user not found") + } + r.logger.Error("Failed to get user by username", map[string]interface{}{ + "error": err.Error(), + "username": username, + }) + return nil, err + } + + return user, nil +} + +// Delete deletes a user (soft delete by setting status to inactive) +func (r *userRepository) Delete(ctx context.Context, id string) error { + // Get user first + _, err := r.GetByID(ctx, id) + if err != nil { + r.logger.Error("Failed to delete user", map[string]interface{}{ + "error": err.Error(), + "user_id": id, + }) + if errors.Is(err, orm.ErrDocumentNotFound) { + return errors.New("user not found") + } + + return err + } + + // Delete in database + err = r.ormRepo.Delete(ctx, id) + if err != nil { + r.logger.Error("Failed to delete user", map[string]interface{}{ + "error": err.Error(), + "user_id": id, + }) + return err + } + + r.logger.Info("User deleted successfully", map[string]interface{}{ + "user_id": id, + }) + + return nil +} + +// List retrieves users with pagination +func (r *userRepository) Search(ctx context.Context, form *SearchUsersForm, pagination *response.Pagination) ([]User, int64, error) { + filter := bson.M{} + + if form.Search != nil { + filter["$or"] = bson.A{ + bson.M{"full_name": bson.M{"$regex": form.Search, "$options": "i"}}, + bson.M{"email": bson.M{"$regex": form.Search, "$options": "i"}}, + bson.M{"username": bson.M{"$regex": form.Search, "$options": "i"}}, + } + } + + if form.Status != nil { + filter["status"] = form.Status + } + + if form.Role != nil { + filter["role"] = form.Role + } + + // Build sort + sort := "created_at" + if pagination.SortBy != "" { + sort = pagination.SortBy + } + + sortOrder := "desc" + if pagination.SortOrder != "" { + sortOrder = pagination.SortOrder + } + + // Build pagination + paginationBuilder := orm.NewPaginationBuilder(). + Limit(pagination.Limit). + Skip(pagination.Offset). + SortBy(sort, sortOrder). + Build() + + // Use ORM to find users + result, err := r.ormRepo.FindAll(ctx, filter, paginationBuilder) + if err != nil { + r.logger.Error("Failed to list users", map[string]interface{}{ + "error": err.Error(), + }) + return nil, 0, err + } + + return result.Items, result.TotalCount, nil } diff --git a/internal/user/service.go b/internal/user/service.go index 1e1a2a5..b27d29e 100644 --- a/internal/user/service.go +++ b/internal/user/service.go @@ -7,6 +7,7 @@ import ( "time" "tm/pkg/authorization" "tm/pkg/logger" + "tm/pkg/response" "go.mongodb.org/mongo-driver/bson/primitive" "golang.org/x/crypto/bcrypt" @@ -14,21 +15,20 @@ import ( // Service defines business logic for user operations type Service interface { - CreateUser(ctx context.Context, form *CreateUserForm, createdBy *string) (*User, error) + // Main CRUD + Register(ctx context.Context, form *CreateUserForm) (*User, error) + Update(ctx context.Context, id string, form *UpdateUserForm) (*User, error) + UpdateStatus(ctx context.Context, id string, form *UpdateUserStatusForm) error + GetUserByID(ctx context.Context, userID string) (*User, error) + Search(ctx context.Context, form *SearchUsersForm, pagination *response.Pagination) (*UserListResponse, error) + Delete(ctx context.Context, id string) error + + // Profile Login(ctx context.Context, form *LoginForm) (*AuthResponse, error) - RefreshToken(ctx context.Context, refreshToken string) (*AuthResponse, error) - GetProfile(ctx context.Context, userID string) (*User, error) UpdateProfile(ctx context.Context, userID string, form *UpdateProfileForm) error + RefreshToken(ctx context.Context, refreshToken string) (*AuthResponse, error) ChangePassword(ctx context.Context, userID string, form *ChangePasswordForm) error ResetPassword(ctx context.Context, form *ResetPasswordForm) error - GetUserByID(ctx context.Context, id string) (*User, error) - UpdateUser(ctx context.Context, id string, form *UpdateUserForm, updatedBy *string) (*User, error) - DeleteUser(ctx context.Context, id string, deletedBy *string) error - ListUsers(ctx context.Context, form *ListUsersForm) (*UserListResponse, error) - UpdateUserStatus(ctx context.Context, id string, form *UpdateUserStatusForm, updatedBy *string) error - UpdateUserRole(ctx context.Context, id string, form *UpdateUserRoleForm, updatedBy *string) error - GetUsersByCompanyID(ctx context.Context, companyID string, limit, offset int) ([]*User, int64, error) - GetUsersByRole(ctx context.Context, role UserRole, limit, offset int) ([]*User, error) Logout(ctx context.Context, userID string, accessToken string) error } @@ -50,8 +50,8 @@ func NewUserService(repository Repository, logger logger.Logger, authService aut } } -// CreateUser creates a new user -func (s *userService) CreateUser(ctx context.Context, form *CreateUserForm, createdBy *string) (*User, error) { +// Register creates a new user +func (s *userService) Register(ctx context.Context, form *CreateUserForm) (*User, error) { // Check if email already exists existingUser, _ := s.repository.GetByEmail(ctx, form.Email) if existingUser != nil { @@ -91,14 +91,13 @@ func (s *userService) CreateUser(ctx context.Context, form *CreateUserForm, crea Position: form.Position, Phone: form.Phone, ProfileImage: form.ProfileImage, - IsVerified: false, - CreatedBy: createdBy, + IsVerified: true, } // Save to database err = s.repository.Create(ctx, user) if err != nil { - s.logger.Error("Failed to create user", map[string]interface{}{ + s.logger.Error("Failed to register user", map[string]interface{}{ "error": err.Error(), "email": form.Email, "username": form.Username, @@ -106,17 +105,169 @@ func (s *userService) CreateUser(ctx context.Context, form *CreateUserForm, crea return nil, err } - s.logger.Info("User created successfully", map[string]interface{}{ - "user_id": user.ID, - "email": user.Email, - "username": user.Username, - "role": user.Role, - "created_by": createdBy, + s.logger.Info("User registered successfully", map[string]interface{}{ + "user_id": user.ID, + "email": user.Email, + "username": user.Username, + "role": user.Role, }) return user, nil } +// Update updates user information +func (s *userService) Update(ctx context.Context, id string, form *UpdateUserForm) (*User, error) { + // Get user + user, err := s.repository.GetByID(ctx, id) + if err != nil { + return nil, errors.New("user not found") + } + + // Update fields if provided + if form.FullName != nil { + user.FullName = *form.FullName + } + + if form.Username != nil { + // Check if username already exists + existingUser, _ := s.repository.GetByUsername(ctx, *form.Username) + if existingUser != nil && existingUser.ID.Hex() != id { + return nil, errors.New("username already exists") + } + user.Username = *form.Username + } + + if form.Email != nil { + // Check if email already exists + existingUser, _ := s.repository.GetByEmail(ctx, *form.Email) + if existingUser != nil && existingUser.ID.Hex() != id { + return nil, errors.New("email already exists") + } + user.Email = *form.Email + } + + if form.Role != nil { + role := UserRole(*form.Role) + if !s.validator.IsValidRole(role) { + return nil, errors.New("invalid role") + } + user.Role = role + } + + if form.Department != nil { + user.Department = form.Department + } + + if form.Position != nil { + user.Position = form.Position + } + + if form.Phone != nil { + user.Phone = form.Phone + } + + if form.ProfileImage != nil { + user.ProfileImage = form.ProfileImage + } + + // Update in database + err = s.repository.Update(ctx, user) + if err != nil { + s.logger.Error("Failed to update user", map[string]interface{}{ + "error": err.Error(), + "user_id": id, + }) + return nil, errors.New("failed to update user") + } + + s.logger.Info("User updated successfully", map[string]interface{}{ + "user_id": id, + }) + + return user, nil +} + +// UpdateStatus updates user status +func (s *userService) UpdateStatus(ctx context.Context, id string, form *UpdateUserStatusForm) error { + // Get user + user, err := s.repository.GetByID(ctx, id) + if err != nil { + return errors.New("user not found") + } + + // Update status + user.Status = UserStatus(form.Status) + + // Update in database + err = s.repository.Update(ctx, user) + if err != nil { + s.logger.Error("Failed to update user status", map[string]interface{}{ + "error": err.Error(), + "user_id": id, + "status": form.Status, + }) + return errors.New("failed to update user status") + } + + s.logger.Info("User status updated successfully", map[string]interface{}{ + "user_id": id, + "status": form.Status, + }) + + return nil +} + +// GetUserByID retrieves a user by ID +func (s *userService) GetUserByID(ctx context.Context, id string) (*User, error) { + user, err := s.repository.GetByID(ctx, id) + if err != nil { + return nil, err + } + + return user, nil +} + +// Search searches users with search and filters +func (s *userService) Search(ctx context.Context, form *SearchUsersForm, pagination *response.Pagination) (*UserListResponse, error) { + // Get users + users, total, err := s.repository.Search(ctx, form, pagination) + if err != nil { + s.logger.Error("Failed to list users", map[string]interface{}{ + "error": err.Error(), + }) + return nil, errors.New("failed to list users") + } + + // Convert to responses + var userResponses []*UserResponse + for _, user := range users { + userResponses = append(userResponses, user.ToResponse()) + } + + return &UserListResponse{ + Users: userResponses, + Meta: pagination.Response(total), + }, nil +} + +// Delete deletes a user (hard delete) +func (s *userService) Delete(ctx context.Context, id string) error { + err := s.repository.Delete(ctx, id) + if err != nil { + s.logger.Error("Failed to delete user", map[string]interface{}{ + "error": err.Error(), + "user_id": id, + }) + return errors.New("failed to delete user") + } + + s.logger.Info("User deleted successfully", map[string]interface{}{ + "user_id": id, + }) + + return nil +} + // Login authenticates a user func (s *userService) Login(ctx context.Context, form *LoginForm) (*AuthResponse, error) { // Find user by email or username @@ -153,7 +304,7 @@ func (s *userService) Login(ctx context.Context, form *LoginForm) (*AuthResponse } // Update last login - err = s.repository.UpdateLastLogin(ctx, user.ID.Hex()) + err = s.repository.Login(ctx, user.ID.Hex()) if err != nil { s.logger.Error("Failed to update last login", map[string]interface{}{ "error": err.Error(), @@ -315,16 +466,6 @@ func (s *userService) ResetPassword(ctx context.Context, form *ResetPasswordForm return nil } -// GetUserByID retrieves a user by ID -func (s *userService) GetUserByID(ctx context.Context, id string) (*User, error) { - user, err := s.repository.GetByID(ctx, id) - if err != nil { - return nil, err - } - - return user, nil -} - // GetProfile retrieves the current user's profile func (s *userService) GetProfile(ctx context.Context, userID string) (*User, error) { user, err := s.repository.GetByID(ctx, userID) @@ -391,212 +532,8 @@ func (s *userService) UpdateProfile(ctx context.Context, userID string, form *Up return nil } -// UpdateUser updates user information -func (s *userService) UpdateUser(ctx context.Context, id string, form *UpdateUserForm, updatedBy *string) (*User, error) { - // Get user - user, err := s.repository.GetByID(ctx, id) - if err != nil { - return nil, errors.New("user not found") - } - - // Update fields if provided - if form.FullName != nil { - user.FullName = *form.FullName - } - - if form.Username != nil { - // Check if username already exists - existingUser, _ := s.repository.GetByUsername(ctx, *form.Username) - if existingUser != nil && existingUser.ID.Hex() != id { - return nil, errors.New("username already exists") - } - user.Username = *form.Username - } - - if form.Email != nil { - // Check if email already exists - existingUser, _ := s.repository.GetByEmail(ctx, *form.Email) - if existingUser != nil && existingUser.ID.Hex() != id { - return nil, errors.New("email already exists") - } - user.Email = *form.Email - } - - if form.Role != nil { - role := UserRole(*form.Role) - if !s.validator.IsValidRole(role) { - return nil, errors.New("invalid role") - } - user.Role = role - } - - if form.Department != nil { - user.Department = form.Department - } - - if form.Position != nil { - user.Position = form.Position - } - - if form.Phone != nil { - user.Phone = form.Phone - } - - if form.ProfileImage != nil { - user.ProfileImage = form.ProfileImage - } - - if form.Status != nil { - user.Status = UserStatus(*form.Status) - } - - // Set updated by - user.UpdatedBy = updatedBy - - // Update in database - err = s.repository.Update(ctx, user) - if err != nil { - s.logger.Error("Failed to update user", map[string]interface{}{ - "error": err.Error(), - "user_id": id, - }) - return nil, errors.New("failed to update user") - } - - s.logger.Info("User updated successfully", map[string]interface{}{ - "user_id": id, - "updated_by": *updatedBy, - }) - - return user, nil -} - -// DeleteUser deletes a user (soft delete) -func (s *userService) DeleteUser(ctx context.Context, id string, deletedBy *string) error { - // Get user to check if exists - user, err := s.repository.GetByID(ctx, id) - if err != nil { - return errors.New("user not found") - } - - // Set updated by before deletion - user.UpdatedBy = deletedBy - - // Update user status to inactive (soft delete) - user.Status = UserStatusInactive - err = s.repository.Update(ctx, user) - if err != nil { - s.logger.Error("Failed to delete user", map[string]interface{}{ - "error": err.Error(), - "user_id": id, - }) - return errors.New("failed to delete user") - } - - s.logger.Info("User deleted successfully", map[string]interface{}{ - "user_id": id, - "deleted_by": *deletedBy, - }) - - return nil -} - -// ListUsers lists users with search and filters -func (s *userService) ListUsers(ctx context.Context, form *ListUsersForm) (*UserListResponse, 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 - } - - // Parse company ID if provided - var companyID *string - if form.CompanyID != nil { - companyID = form.CompanyID - } - - // Get users - users, err := s.repository.Search(ctx, search, form.Status, form.Role, companyID, limit, offset, sortBy, sortOrder) - if err != nil { - s.logger.Error("Failed to list users", map[string]interface{}{ - "error": err.Error(), - }) - return nil, errors.New("failed to list users") - } - - // Convert to responses - var userResponses []*UserResponse - for _, user := range users { - userResponses = append(userResponses, user.ToResponse()) - } - - // TODO: Implement proper count for pagination metadata - total := int64(len(users)) // This should be a separate count query - totalPages := (total + int64(limit) - 1) / int64(limit) - - return &UserListResponse{ - Users: userResponses, - Total: total, - Limit: limit, - Offset: offset, - TotalPages: int(totalPages), - }, nil -} - -// UpdateUserStatus updates user status -func (s *userService) UpdateUserStatus(ctx context.Context, id string, form *UpdateUserStatusForm, updatedBy *string) error { - // Get user - user, err := s.repository.GetByID(ctx, id) - if err != nil { - return errors.New("user not found") - } - - // Update status - user.Status = UserStatus(form.Status) - user.UpdatedBy = updatedBy - - // Update in database - err = s.repository.Update(ctx, user) - if err != nil { - s.logger.Error("Failed to update user status", map[string]interface{}{ - "error": err.Error(), - "user_id": id, - "status": form.Status, - }) - return errors.New("failed to update user status") - } - - s.logger.Info("User status updated successfully", map[string]interface{}{ - "user_id": id, - "status": form.Status, - "updated_by": *updatedBy, - }) - - return nil -} - // UpdateUserRole updates user role -func (s *userService) UpdateUserRole(ctx context.Context, id string, form *UpdateUserRoleForm, updatedBy *string) error { +func (s *userService) UpdateRole(ctx context.Context, id string, form *UpdateUserRoleForm) error { // Get user user, err := s.repository.GetByID(ctx, id) if err != nil { @@ -611,7 +548,6 @@ func (s *userService) UpdateUserRole(ctx context.Context, id string, form *Updat // Update role user.Role = role - user.UpdatedBy = updatedBy // Update in database err = s.repository.Update(ctx, user) @@ -625,52 +561,13 @@ func (s *userService) UpdateUserRole(ctx context.Context, id string, form *Updat } s.logger.Info("User role updated successfully", map[string]interface{}{ - "user_id": id, - "role": form.Role, - "updated_by": *updatedBy, + "user_id": id, + "role": form.Role, }) return nil } -// GetUsersByCompanyID retrieves users by company ID with pagination -func (s *userService) GetUsersByCompanyID(ctx context.Context, companyID string, limit, offset int) ([]*User, int64, error) { - users, err := s.repository.GetByCompanyID(ctx, companyID, limit, offset) - if err != nil { - s.logger.Error("Failed to get users by company ID", map[string]interface{}{ - "error": err.Error(), - "company_id": companyID, - }) - return nil, 0, errors.New("failed to get users by company") - } - - // Get total count - total, err := s.repository.CountByCompanyID(ctx, companyID) - if err != nil { - s.logger.Error("Failed to count users by company ID", map[string]interface{}{ - "error": err.Error(), - "company_id": companyID, - }) - return users, 0, nil // Return users even if count fails - } - - return users, total, nil -} - -// GetUsersByRole retrieves users by role with pagination -func (s *userService) GetUsersByRole(ctx context.Context, role UserRole, limit, offset int) ([]*User, error) { - users, err := s.repository.GetByRole(ctx, role, limit, offset) - if err != nil { - s.logger.Error("Failed to get users by role", map[string]interface{}{ - "error": err.Error(), - "role": role, - }) - return nil, errors.New("failed to get users by role") - } - - return users, nil -} - // Logout logs out a user and invalidates tokens func (s *userService) Logout(ctx context.Context, userID string, accessToken string) error { // Invalidate the access token diff --git a/pkg/mongo/utils.go b/pkg/mongo/utils.go index 6026ffe..1808b39 100644 --- a/pkg/mongo/utils.go +++ b/pkg/mongo/utils.go @@ -279,20 +279,24 @@ func (pb *PaginationBuilder) Cursor(cursor string) *PaginationBuilder { } // SortBy sets the sort field and order -func (pb *PaginationBuilder) SortBy(field string, order int) *PaginationBuilder { +func (pb *PaginationBuilder) SortBy(field string, order string) *PaginationBuilder { pb.pagination.SortField = field - pb.pagination.SortOrder = order + if order == "asc" { + pb.pagination.SortOrder = 1 + } else { + pb.pagination.SortOrder = -1 + } return pb } // SortAsc sorts in ascending order func (pb *PaginationBuilder) SortAsc(field string) *PaginationBuilder { - return pb.SortBy(field, 1) + return pb.SortBy(field, "asc") } // SortDesc sorts in descending order func (pb *PaginationBuilder) SortDesc(field string) *PaginationBuilder { - return pb.SortBy(field, -1) + return pb.SortBy(field, "desc") } // Build returns the final pagination options diff --git a/pkg/response/response.go b/pkg/response/response.go index 33d5e59..eefbb4e 100644 --- a/pkg/response/response.go +++ b/pkg/response/response.go @@ -2,6 +2,7 @@ package response import ( "net/http" + "strconv" "github.com/asaskevich/govalidator" "github.com/labstack/echo/v4" @@ -32,6 +33,13 @@ type Meta struct { Pages int `json:"pages"` } +type Pagination struct { + Limit int `query:"limit" valid:"optional,range(1|100)" default:"20"` + Offset int `query:"offset" valid:"optional,min(0)" default:"0"` + SortBy string `query:"sort_by" valid:"optional"` + SortOrder string `query:"sort_order" valid:"optional,in(asc|desc)"` +} + // Success returns a successful response func Success(c echo.Context, data interface{}, message string) error { return c.JSON(http.StatusOK, APIResponse{ @@ -171,3 +179,34 @@ func Parse[T any](c echo.Context) (*T, error) { return &form, nil } + +func NewPagination(c echo.Context) *Pagination { + limit, err := strconv.Atoi(c.QueryParam("limit")) + if err != nil { + limit = 20 + } + + offset, err := strconv.Atoi(c.QueryParam("offset")) + if err != nil { + offset = 0 + } + + return &Pagination{ + Limit: limit, + Offset: offset, + SortBy: c.QueryParam("sort_by"), + SortOrder: c.QueryParam("sort_order"), + } +} + +// Pagination calculates pagination metadata +func (p *Pagination) Response(total int64) *Meta { + pages := (total + int64(p.Limit) - 1) / int64(p.Limit) + return &Meta{ + Total: int(total), + Limit: p.Limit, + Offset: p.Offset, + Page: int(p.Offset/p.Limit) + 1, + Pages: int(pages), + } +}