Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
57 changes: 56 additions & 1 deletion src/controllers/user.controller.js
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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);
}
};
103 changes: 103 additions & 0 deletions src/docs/swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down Expand Up @@ -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": {
Expand Down
6 changes: 5 additions & 1 deletion src/middlewares/verifyUserPermission.middleware.js
Original file line number Diff line number Diff line change
@@ -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
Expand Down
8 changes: 8 additions & 0 deletions src/routes/user.routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -32,4 +33,11 @@ router.put(
updateUserAccount,
);

router.put(
'/api/users/update-password/:id',
verifyAccessToken,
verifyUserPermission,
updateUserPassword,
);

export default router;
32 changes: 6 additions & 26 deletions src/validations/user.validation.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
};