Repository: acuaterra-app/acuaterra-app-backend
Pull request #32: AQ 33 User Edit
Author: login: theAranda88
Created: 2025-03-14T05:03:38Z | Merged: 2025-03-14T22:07:35Z

### Link Jira
[AQ-33](https://aquaterra-sena.atlassian.net/browse/AQ-33?atlOrigin=eyJpIjoiOWE0ZWQ2MmY1ZGJjNGIxN2JjNDhhYWFlYzU0MTI1YWIiLCJwIjoiaiJ9)

## Description
   Add the module that allows editing a user created by the admin and the owner.
   Add role validator logic for the PUT route, leaving all fields editable.
## New Environments
  
### Screenshots
  ![image](https://github.com/user-attachments/assets/c0b55bcc-1f93-4e47-8322-344722d22632)


Diff:
diff --git a/backend/app/controllers/shared/user.controller.js b/backend/app/controllers/shared/user.controller.js
index 4501ea4..b9b8842 100644
--- a/backend/app/controllers/shared/user.controller.js
+++ b/backend/app/controllers/shared/user.controller.js
@@ -85,13 +85,31 @@ class UserController {
         }
     }
 
-    static async upgrade(req, res) {
+    async update(req, res) {
         try {
-            const result = await UserService.editUser(req.params.id, req.body);
-            const response = ApiResponse.createApiResponse("User updated successfully", result)
+            const { id } = req.params;
+            const userData = req.body;
+
+            const updatedUser = await this.userService.editUser(id, userData);
+
+            const response = ApiResponse.createApiResponse(
+                "User updated successfully",
+                [ updatedUser ]
+            );
             return res.json(response);
         } catch (error) {
-            res.status(500).json({ error: `User update failed: ${error.message}` });
+            console.error("Error updating user:", error);
+            const response = ApiResponse.createApiResponse(
+                "Error updating user",
+                [],
+                [{ msg: error.message }]
+            );
+
+            if (error.message.includes("not found")) {
+                return res.status(404).json(response);
+            }
+
+            return res.status(500).json(response);
         }
     }
 
diff --git a/backend/app/middleware/validateUserUpdate.middleware.js b/backend/app/middleware/validateUserUpdate.middleware.js
new file mode 100644
index 0000000..1100b1a
--- /dev/null
+++ b/backend/app/middleware/validateUserUpdate.middleware.js
@@ -0,0 +1,45 @@
+const {ROLES: Role} = require("../enums/roles.enum");
+const ApiResponse = require("../utils/apiResponse");
+
+class ValidateUserUpdateMiddleware {
+
+    async validateUserUpdate(req, res, next) {
+        try {
+            const { id_rol } = req.body;
+            const idRolToUpdate = parseInt(id_rol);
+            const authenticatedUser = req.user;
+
+            if (authenticatedUser.id_rol === Role.ADMIN) {
+                if (idRolToUpdate !== Role.ADMIN && idRolToUpdate !== Role.OWNER) {
+                    return res.status(403).json(
+                        ApiResponse.createApiResponse('Authorization failed', [], [{
+                            'error': 'Admins can only edit Admins or Owners roles'
+                        }])
+                    );
+                }
+            }
+
+            if (authenticatedUser.id_rol === Role.OWNER) {
+                if (idRolToUpdate !== Role.USER) {
+                    return res.status(403).json(
+                        ApiResponse.createApiResponse('Authorization failed', [], [{
+                            'error': 'Owners can only edit Users roles'
+                        }])
+                    );
+                }
+            }
+
+            next();
+
+        } catch (error) {
+            console.error('User update validation error:', error);
+            return res.status(500).json(
+                ApiResponse.createApiResponse('Server error', [], [{
+                    'error': 'Error validating user update role'
+                }])
+            );
+        }
+    }
+}
+
+module.exports = ValidateUserUpdateMiddleware;
\ No newline at end of file
diff --git a/backend/app/routes/admin/user.admin.route.js b/backend/app/routes/admin/user.admin.route.js
index ccbadec..5eca0a2 100644
--- a/backend/app/routes/admin/user.admin.route.js
+++ b/backend/app/routes/admin/user.admin.route.js
@@ -26,7 +26,6 @@ router.get('/',
     (req, res) => userController.index(req, res)
 );
 router.get('/:id',  validateTokenMiddleware.validate.bind(validateTokenMiddleware), UserController.find);
-router.put('/:id',  validateTokenMiddleware.validate.bind(validateTokenMiddleware), UserController.upgrade);
 router.delete('/:id',  validateTokenMiddleware.validate.bind(validateTokenMiddleware), UserController.delete);
 
 module.exports = router;
diff --git a/backend/app/routes/shared/user.route.js b/backend/app/routes/shared/user.route.js
index 895bc96..c300f4c 100644
--- a/backend/app/routes/shared/user.route.js
+++ b/backend/app/routes/shared/user.route.js
@@ -11,7 +11,9 @@ const Mailer = require('../../utils/Mailer');
 const { ROLES: Role } = require("../../enums/roles.enum");
 const ValidateRoleMiddleware = require("../../middleware/validateRole.middleware");
 const ValidateUserCreationMiddleware = require("../../middleware/validateUserCreation.middleware");
+const ValidateUserUpdateMiddleware = require("../../middleware/validateUserUpdate.middleware");
 
+const validateUserUpdate = new ValidateUserUpdateMiddleware();
 const mailer = new Mailer(process.env.RESEND_API_KEY);
 const validateTokenMiddleware = new ValidateTokenMiddleware(new BlackListService());
 const userService = new UserService(mailer);
@@ -28,5 +30,13 @@ router.post(
     (req, res) => userController.register(req, res)
     );
 
-module.exports = router;
+router.put(
+    '/:id',
+    validateTokenMiddleware.validate.bind(validateTokenMiddleware),
+    validate(validateUserRegistration),
+    validateRoleMiddleware.validate([Role.ADMIN, Role.OWNER]),
+    (req, res, next) => validateUserUpdate.validateUserUpdate(req, res, next),
+    (req, res) => userController.update(req, res)
+);
 
+module.exports = router;
\ No newline at end of file
diff --git a/backend/app/services/shared/user.service.js b/backend/app/services/shared/user.service.js
index f3b4c96..fa03adf 100644
--- a/backend/app/services/shared/user.service.js
+++ b/backend/app/services/shared/user.service.js
@@ -1,5 +1,4 @@
 const { User, Rol } = require('../../../models');
-const { Sequelize } = require('sequelize');
 const bcrypt = require('bcrypt');
 const ejs = require('ejs');
 const path = require('path');
@@ -81,14 +80,17 @@ class UserService {
         }
     }
 
-    static async findUserById(id) {
+    async findUserById(id) {
         try {
-            const user = await User.findOne({ where: { id } });
+            const user = await User.findByPk(id);
+
             if (!user) {
-                throw new Error('No se encontró el usuario.');
+                throw new Error(`User with id ${id} not found`);
             }
+
             return user;
         } catch (error) {
+            console.error(`Error finding user with id ${id}:`, error);
             throw error;
         }
     }
@@ -119,20 +121,34 @@ class UserService {
         }
     }
 
-    static async editUser(id_user, nuevoUser) {
+    async editUser(id, userData) {
         try {
-            // Hash password if it's being updated
-            if (nuevoUser.password) {
-                nuevoUser.password = await bcrypt.hash(nuevoUser.password, 10);
-            }
-            
-            const user = await User.findByPk(id_user)
+            const user = await User.findByPk(id);
+
             if (!user) {
-                throw new Error('No se encontró el usuario.');
+                throw new Error(`Usuario con id ${id} no encontrado`);
             }
-            await user.update(nuevoUser);
-            return user;
+
+            const {
+                name, email, dni, id_rol, address, contact = [] } = userData;
+
+            await User.update({
+                name,
+                email,
+                dni,
+                id_rol,
+                address,
+                contact
+            }, {
+                where: { id }
+            });
+
+            return await User.findByPk(id, {
+                attributes: ['id', 'name', 'email', 'dni', 'id_rol', 'address', 'contact']
+            });
+
         } catch (error) {
+            console.error(`Error updating user with id ${id}:`, error);
             throw error;
         }
     }
diff --git a/backend/app/swagger/paths/shared/users/edit_user.json b/backend/app/swagger/paths/shared/users/edit_user.json
new file mode 100644
index 0000000..e810e9a
--- /dev/null
+++ b/backend/app/swagger/paths/shared/users/edit_user.json
@@ -0,0 +1,182 @@
+{
+    "/api/v2/shared/users/{id}": {
+        "put": {
+            "summary": "Update user",
+            "description": "Allows updating the information of an existing user specified by their ID",
+            "tags": [
+                "Shared/Users"
+            ],
+            "parameters": [
+                {
+                    "in": "path",
+                    "name": "id",
+                    "required": true,
+                    "schema": {
+                        "type": "integer",
+                        "minimum": 1
+                    },
+                    "description": "Unique ID of the user to update"
+                }
+            ],
+            "requestBody": {
+                "required": true,
+                "content": {
+                    "application/json": {
+                        "schema": {
+                            "type": "object",
+                            "properties": {
+                                "name": {
+                                    "type": "string",
+                                    "description": "Full name of the user",
+                                    "example": "UserEdit"
+                                },
+                                "email": {
+                                    "type": "string",
+                                    "format": "email",
+                                    "description": "User's email address",
+                                    "example": "UserEdit1@gmail.com"
+                                },
+                                "dni": {
+                                    "type": "string",
+                                    "description": "Identity document number",
+                                    "example": "1234560011"
+                                },
+                                "id_rol": {
+                                    "type": "string",
+                                    "description": "User role ID",
+                                    "example": "3"
+                                },
+                                "address": {
+                                    "type": "string",
+                                    "description": "User's physical address",
+                                    "example": "123 Main Street"
+                                },
+                                "contact": {
+                                    "type": "string",
+                                    "description": "User's contact number",
+                                    "example": "11232143"
+                                }
+                            },
+                            "required": ["name", "email", "dni", "id_rol", "address", "contact"  ]
+                        }
+                    }
+                }
+            },
+            "responses": {
+                "200": {
+                    "description": "User updated successfully",
+                    "content": {
+                        "application/json": {
+                            "schema": {
+                                "type": "object",
+                                "properties": {
+                                    "message": {
+                                        "type": "string",
+                                        "example": "User updated successfully"
+                                    },
+                                    "data": {
+                                        "type": "object",
+                                        "properties": {
+                                            "id": {
+                                                "type": "integer",
+                                                "description": "ID of the updated user"
+                                            },
+                                            "name": {
+                                                "type": "string",
+                                                "description": "Updated user name"
+                                            },
+                                            "email": {
+                                                "type": "string",
+                                                "format": "email",
+                                                "description": "Updated email address"
+                                            },
+                                            "dni": {
+                                                "type": "string",
+                                                "description": "Updated document number"
+                                            },
+                                            "id_rol": {
+                                                "type": "integer",
+                                                "description": "Updated role ID"
+                                            },
+                                            "address": {
+                                                "type": "string",
+                                                "description": "Updated address",
+                                                "nullable": true
+                                            },
+                                            "contact": {
+                                                "type": "string",
+                                                "description": "Updated contact number",
+                                                "nullable": true
+                                            },
+                                            "updatedAt": {
+                                                "type": "string",
+                                                "format": "date-time",
+                                                "description": "Date and time of the last update"
+                                            }
+                                        }
+                                    },
+                                    "errors": {
+                                        "type": "array",
+                                        "items": {
+                                            "type": "string"
+                                        },
+                                        "description": "List of errors, if any"
+                                    }
+                                },
+                                "required": ["message", "data", "errors"]
+                            },
+                            "example": {
+                                "message": "User updated successfully",
+                                "data": {
+                                    "id": 12,
+                                    "name": "UserEdit",
+                                    "email": "UserEdit1@gmail.com",
+                                    "dni": "1234560011",
+                                    "id_rol": 3,
+                                    "address": "123 Main Street",
+                                    "contact": "11232143",
+                                    "updatedAt": "2025-03-07T04:51:36.000Z"
+                                },
+                                "errors": []
+                            }
+                        }
+                    }
+                },
+                "400": {
+                    "description": "Data validation error",
+                    "content": {
+                        "application/json": {
+                            "schema": {
+                                "type": "object",
+                                "properties": {
+                                    "message": {
+                                        "type": "string",
+                                        "example": "Data validation error"
+                                    },
+                                    "errors": {
+                                        "type": "array",
+                                        "items": {
+                                            "type": "string"
+                                        }
+                                    }
+                                }
+                            }
+                        }
+                    }
+                },
+                "401": {
+                    "description": "Unauthorized - User is not authenticated"
+                },
+                "403": {
+                    "description": "Forbidden - User does not have permission to perform this action"
+                },
+                "404": {
+                    "description": "User not found"
+                },
+                "500": {
+                    "description": "Internal server error"
+                }
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/test-resources/user.postman_collection.json b/test-resources/user.postman_collection.json
index e873120..9395058 100644
--- a/test-resources/user.postman_collection.json
+++ b/test-resources/user.postman_collection.json
@@ -1,9 +1,9 @@
 {
 	"info": {
-		"_postman_id": "bfe54c08-3527-4478-84e5-26e6c3b854fa",
+		"_postman_id": "15e37455-3225-4448-bb8e-913946b2a512",
 		"name": "user",
 		"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
-		"_exporter_id": "13906554"
+		"_exporter_id": "37390175"
 	},
 	"item": [
 		{
@@ -37,6 +37,50 @@
 								}
 							},
 							"response": []
+						},
+						{
+							"name": "Edit",
+							"request": {
+								"method": "PUT",
+								"header": [
+									{
+										"key": "Authorization",
+										"value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwiZG5pIjoiMjQ0MTg2MDA2MyIsImlhdCI6MTc0MTkxMzg3MSwiZXhwIjoxNzQxOTMxODcxfQ.D6xHYiGE-2SppB73ky258PgyUapEchwnViujQ7Lk8QM",
+										"type": "text"
+									}
+								],
+								"body": {
+									"mode": "raw",
+									"raw": "{\r\n    \"name\": \"UserEdit\",\r\n    \"email\": \"UserEdit1@gmail.com\",\r\n    \"dni\": \"1234560011\",\r\n    \"id_rol\": \"3\",\r\n    \"address\": \"calle siempre viva 123\",\r\n    \"contact\": \"11232143\"\r\n}",
+									"options": {
+										"raw": {
+											"language": "json"
+										}
+									}
+								},
+								"url": {
+									"raw": "{{server}}/api/v2/shared/users/5",
+									"host": [
+										"{{server}}"
+									],
+									"path": [
+										"api",
+										"v2",
+										"shared",
+										"users",
+										"5"
+									]
+								}
+							},
+							"response": []
+						},
+						{
+							"name": "Create User",
+							"request": {
+								"method": "GET",
+								"header": []
+							},
+							"response": []
 						}
 					]
 				},


Reviews:
user: login: nsusana01
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-14T13:25:40Z
user: login: nsusana01
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-14T21:39:02Z
user: login: nsusana01
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-14T21:39:07Z
user: login: nsusana01
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-03-14T22:02:09Z
user: login: theAranda88
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-14T22:07:16Z

Review comments:
diff_hunk: @@ -81,14 +80,17 @@ class UserService {
         }
     }
 
-    static async findUserById(id) {
+    async findUserById(id) {
         try {
-            const user = await User.findOne({ where: { id } });
+            const user = await User.findByPk(id);
+
             if (!user) {
-                throw new Error('No se encontró el usuario.');
+                throw new Error(`User with id ${id} not found`);
path: backend/app/services/shared/user.service.js
user: login: nsusana01
body: This logic of validating if a user exists could be moved to validator.
created_at: 2025-03-14T13:25:40Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,45 @@
+const {ROLES: Role} = require("../enums/roles.enum");
+const ApiResponse = require("../utils/apiResponse");
+
+class ValidateUserUpdateMiddleware {
+
+    async validateUserUpdate(req, res, next) {
+        try {
+            const { id_rol } = req.body;
+            const idRolToUpdate = parseInt(id_rol);
+            const authenticatedUser = req.user;
+
+            if (authenticatedUser.id_rol === Role.ADMIN) {
+                if (idRolToUpdate !== Role.ADMIN && idRolToUpdate !== Role.OWNER) {
+                    return res.status(403).json(
+                        ApiResponse.createApiResponse('Authorization failed', [], [{
+                            'error': 'Admins can only edit Admins or Owners roles'
+                        }])
+                    );
+                }
+            }
+
+            if (authenticatedUser.id_rol === Role.OWNER) {
+                if (idRolToUpdate !== Role.USER) {
+                    return res.status(403).json(
+                        ApiResponse.createApiResponse('Authorization failed', [], [{
+                            'error': 'Owners can only edit Users roles'
path: backend/app/middleware/validateUserUpdate.middleware.js
user: login: nsusana01
body: The errors should be an array
created_at: 2025-03-14T21:39:02Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,45 @@
+const {ROLES: Role} = require("../enums/roles.enum");
+const ApiResponse = require("../utils/apiResponse");
+
+class ValidateUserUpdateMiddleware {
+
+    async validateUserUpdate(req, res, next) {
+        try {
+            const { id_rol } = req.body;
+            const idRolToUpdate = parseInt(id_rol);
+            const authenticatedUser = req.user;
+
+            if (authenticatedUser.id_rol === Role.ADMIN) {
+                if (idRolToUpdate !== Role.ADMIN && idRolToUpdate !== Role.OWNER) {
+                    return res.status(403).json(
+                        ApiResponse.createApiResponse('Authorization failed', [], [{
+                            'error': 'Admins can only edit Admins or Owners roles'
path: backend/app/middleware/validateUserUpdate.middleware.js
user: login: nsusana01
body: The errors should be an array
created_at: 2025-03-14T21:39:06Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,45 @@
+const {ROLES: Role} = require("../enums/roles.enum");
+const ApiResponse = require("../utils/apiResponse");
+
+class ValidateUserUpdateMiddleware {
+
+    async validateUserUpdate(req, res, next) {
+        try {
+            const { id_rol } = req.body;
+            const idRolToUpdate = parseInt(id_rol);
+            const authenticatedUser = req.user;
+
+            if (authenticatedUser.id_rol === Role.ADMIN) {
+                if (idRolToUpdate !== Role.ADMIN && idRolToUpdate !== Role.OWNER) {
+                    return res.status(403).json(
+                        ApiResponse.createApiResponse('Authorization failed', [], [{
+                            'error': 'Admins can only edit Admins or Owners roles'
path: backend/app/middleware/validateUserUpdate.middleware.js
user: login: theAranda88
body: ok
created_at: 2025-03-14T22:07:16Z
author_association: COLLABORATOR

PR comments:
user: login: vercel[bot]
created_at: 2025-03-14T05:03:42Z
body: [vc]: #3mLU+n6x6+ygEiqkLqB6S+i3jMIqnwhEuWfBrXQnkmA=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJhY3VhdGVycmEtYXBwLWJhY2tlbmQiLCJsaXZlRmVlZGJhY2siOnsicmVzb2x2ZWQiOjAsInVucmVzb2x2ZWQiOjAsInRvdGFsIjowLCJsaW5rIjoiYWN1YXRlcnJhLWFwcC1iYWNrZW5kLWdpdC1mZWF0dXItM2RiOTQzLWFjdWF0ZXJyYS1hcHBzLXByb2plY3RzLnZlcmNlbC5hcHAifSwiaW5zcGVjdG9yVXJsIjoiaHR0cHM6Ly92ZXJjZWwuY29tL2FjdWF0ZXJyYS1hcHBzLXByb2plY3RzL2FjdWF0ZXJyYS1hcHAtYmFja2VuZC84VlF0RnBkNlZFV1l4WjY4eGRKRjZvQ250aUxzIiwicHJldmlld1VybCI6ImFjdWF0ZXJyYS1hcHAtYmFja2VuZC1naXQtZmVhdHVyLTNkYjk0My1hY3VhdGVycmEtYXBwcy1wcm9qZWN0cy52ZXJjZWwuYXBwIiwibmV4dENvbW1pdFN0YXR1cyI6IkRFUExPWUVEIiwicm9vdERpcmVjdG9yeSI6ImJhY2tlbmQifV19
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **acuaterra-app-backend** | ✅ Ready ([Inspect](https://vercel.com/acuaterra-apps-projects/acuaterra-app-backend/8VQtFpd6VEWYxZ68xdJF6oCntiLs)) | [Visit Preview](https://acuaterra-app-backend-git-featur-3db943-acuaterra-apps-projects.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/acuaterra-app-backend-git-featur-3db943-acuaterra-apps-projects.vercel.app?via=pr-comment-feedback-link) | Mar 14, 2025 8:45pm |


author_association: NONE
user: login: nsusana01
created_at: 2025-03-14T13:28:59Z
body: I didn't see the validator logic ? in what part are you validating if the authenticated user is an admin or a owner ? then with this there should be a logic that identifies if the user is admin then he can edit admin, owner. If the authenticated user is owner he only can edit user (monitor) role  
author_association: COLLABORATOR
user: login: theAranda88
created_at: 2025-03-14T20:45:55Z
body: Add role validator logic for the PUT route, leaving all fields editable.
author_association: COLLABORATOR