diff --git a/README.md b/README.md index a5c735f..02beb45 100644 --- a/README.md +++ b/README.md @@ -31,8 +31,10 @@ base url: `http://localhost:3000` ### User -- **Get all Users**: GET `/api/users` -- **Get a specific Users**: GET `/api/Users/:userId` +- **Get all Users**: GET `/api/users/all` +- **Get a specific User**: GET `/api/users/:id` +- **Update a User**: PUT `/api/users/:id` +- **Update User Password**: PUT `/api/users/update-password/:id` ### Organization diff --git a/src/controllers/user.controller.js b/src/controllers/user.controller.js index dd53f39..054d92a 100644 --- a/src/controllers/user.controller.js +++ b/src/controllers/user.controller.js @@ -1,5 +1,9 @@ import prisma from '../config/prismaClient.js'; -import { updateUserAccountValidation } from '../validations/user.validation.js'; +import { comparePassword, hashPassword } from '../utils/password.utils.js'; +import { + updatePasswordValidation, + updateUserAccountValidation, +} from '../validations/user.validation.js'; /* eslint no-undef:off */ export const getAllUsers = async (req, res, next) => { try { @@ -141,3 +145,54 @@ export const updateUserAccount = async (req, res, next) => { next(error); } }; + +export const updateUserPassword = async (req, res, next) => { + try { + // Validate the request body + const { error, value } = updatePasswordValidation(req.body); + if (error) { + return res.status(400).json({ message: error.details[0].message }); + } + + const { id } = req.params; // Extract user ID from request parameters + const { oldPassword, newPassword } = value; + + // Fetch the user by ID + const user = await prisma.user.findUnique({ + where: { id }, + select: { id: true, password: true }, + }); + + if (!user) { + return res.status(404).json({ message: 'User not found' }); + } + + // Compare the old password with the stored password + const isMatch = await comparePassword(oldPassword, user.password); + if (!isMatch) { + return res.status(400).json({ message: 'Incorrect old password' }); + } + + // Ensure the new password is different from the old password + if (oldPassword === newPassword) { + return res + .status(400) + .json({ message: 'New password must be different from the old one' }); + } + + // Hash the new password + const hashedPassword = await hashPassword(newPassword); + + // Update the user's password in the database + await prisma.user.update({ + where: { id }, + data: { password: hashedPassword }, + }); + + // Respond with a success message + return res.status(200).json({ message: 'Password updated successfully' }); + } catch (error) { + // Pass any errors to the error-handling middleware + next(error); + } +}; diff --git a/src/docs/swagger.json b/src/docs/swagger.json index 572d546..cb20bef 100644 --- a/src/docs/swagger.json +++ b/src/docs/swagger.json @@ -849,6 +849,90 @@ } } } + }, + "/api/users/update-password": { + "put": { + "tags": ["Users"], + "summary": "Update user password", + "description": "Allows a user to update their password by providing the old and new passwords.", + "operationId": "updateUserPassword", + "security": [{ "bearerAuth": [] }], + "requestBody": { + "description": "Password update data", + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": ["email", "oldPassword", "newPassword"], + "properties": { + "email": { + "type": "string", + "format": "email", + "example": "user@example.com" + }, + "oldPassword": { + "type": "string", + "example": "oldPassword123" + }, + "newPassword": { + "type": "string", + "example": "newPassword456" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Password updated successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "example": "Password updated successfully" + } + } + } + } + } + }, + "400": { + "description": "Bad request - Validation error or incorrect old password", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "User not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } } }, "components": { @@ -1744,6 +1828,25 @@ "example": "+1234567890" } } + }, + "UpdatePasswordRequest": { + "type": "object", + "required": ["email", "oldPassword", "newPassword"], + "properties": { + "email": { + "type": "string", + "format": "email", + "example": "user@example.com" + }, + "oldPassword": { + "type": "string", + "example": "oldPassword123" + }, + "newPassword": { + "type": "string", + "example": "newPassword456" + } + } } }, "responses": { diff --git a/src/middlewares/verifyUserPermission.middleware.js b/src/middlewares/verifyUserPermission.middleware.js index 5969def..03a4acb 100644 --- a/src/middlewares/verifyUserPermission.middleware.js +++ b/src/middlewares/verifyUserPermission.middleware.js @@ -1,5 +1,9 @@ export const verifyUserPermission = (req, res, next) => { - if (req.params.id === req.user.id || req.user.role === 'ADMIN') { + if ( + req.params.id === req.user.id || + req.user.role === 'ADMIN' || + req.user.email === req.params.email + ) { return next(); } // Otherwise deny access diff --git a/src/routes/user.routes.js b/src/routes/user.routes.js index a15de4e..0c7b153 100644 --- a/src/routes/user.routes.js +++ b/src/routes/user.routes.js @@ -3,6 +3,7 @@ import { getAllUsers, getUserById, updateUserAccount, + updateUserPassword, } from '../controllers/user.controller.js'; import { verifyAccessToken } from '../middlewares/auth.middleware.js'; import { verifyAdminPermission } from '../middlewares/verifyAdminPermission.middleware.js'; @@ -32,4 +33,11 @@ router.put( updateUserAccount, ); +router.put( + '/api/users/update-password/:id', + verifyAccessToken, + verifyUserPermission, + updateUserPassword, +); + export default router; diff --git a/src/validations/user.validation.js b/src/validations/user.validation.js index 20c9ef4..eb40769 100644 --- a/src/validations/user.validation.js +++ b/src/validations/user.validation.js @@ -29,38 +29,18 @@ export const updateUserAccountValidation = (obj) => { bio: Joi.string().max(1000).messages({ 'string.max': 'Bio cannot exceed 1000 characters', }), - }); + }).options({ allowUnknown: true }); // Allow unknown fields return schema.validate(obj, { abortEarly: false }); }; -export const updateUserPasswordValidation = (obj) => { +export const updatePasswordValidation = (obj) => { const schema = Joi.object({ - currentPassword: Joi.string().required().messages({ - 'any.required': 'Current password is required', + oldPassword: Joi.string().required(), + newPassword: Joi.string().min(8).required().messages({ + 'string.min': 'New password must be at least 8 characters long', }), - newPassword: Joi.string() - .required() - .min(8) - .max(32) - .regex( - /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/, - ) - .messages({ - 'string.empty': 'New password cannot be empty', - 'string.min': 'Password must be at least 8 characters', - 'string.max': 'Password cannot exceed 32 characters', - 'string.pattern.base': - 'Password must contain at least one uppercase letter, one lowercase letter, one number, and one special character', - }), - confirmPassword: Joi.string() - .valid(Joi.ref('newPassword')) - .required() - .messages({ - 'any.only': 'Passwords do not match', - 'any.required': 'Confirm password is required', - }), }); - return schema.validate(obj, { abortEarly: false }); + return schema.validate(obj); };