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
63 changes: 63 additions & 0 deletions src/controllers/auth.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,68 @@ const refreshToken = async (req, res, next) => {
return next(error);
}
};
/**
* Change authenticated user's password
* PATCH /api/auth/change-password
*
* Add this function to src/controllers/auth.controller.js
* and include it in the module.exports object at the bottom.
*/
const changePassword = async (req, res, next) => {
try {
const { currentPassword, newPassword } = req.body;

// req.userId is set by the authenticate middleware (same as logout)
const user = await User.findById(req.userId).select('+password');
if (!user) {
const error = new Error('User not found');
error.statusCode = 404;
error.isOperational = true;
return next(error);
}

// Verify the supplied current password against the stored hash
const isCurrentPasswordValid = await bcrypt.compare(
currentPassword,
user.password
);
if (!isCurrentPasswordValid) {
const error = new Error('Current password is incorrect');
error.statusCode = 401;
error.isOperational = true;
return next(error);
}

// Prevent reuse of the same password
const isSamePassword = await bcrypt.compare(newPassword, user.password);
if (isSamePassword) {
const error = new Error(
'New password must be different from the current password'
);
error.statusCode = 400;
error.isOperational = true;
return next(error);
}

// Assign plain-text password — the User model's pre-save hook hashes it
user.password = newPassword;

// Invalidate all existing refresh tokens (force re-login on other devices)
user.refreshTokenHash = null;
user.refreshTokenExpiresAt = null;

await user.save();

return sendSuccess(
res,
{},
200,
'Password changed successfully. Please log in again.'
);
} catch (error) {
return next(error);
}
};

module.exports = {
register,
Expand All @@ -448,4 +510,5 @@ module.exports = {
forgotPassword,
verifyEmail,
refreshToken,
changePassword,
};
10 changes: 10 additions & 0 deletions src/routes/auth.routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const {
forgotPassword,
verifyEmail,
refreshToken,
changePassword,
} = require('../controllers/auth.controller');
const validate = require('../middlewares/validate');
const authenticate = require('../middlewares/auth');
Expand All @@ -16,6 +17,7 @@ const {
resetPasswordSchema,
forgotPasswordSchema,
refreshTokenSchema,
changePasswordSchema,
} = require('../validators/auth.validators');

const router = express.Router();
Expand Down Expand Up @@ -45,4 +47,12 @@ router.get('/verify-email/:token', verifyEmail);
// POST /api/auth/refresh-token - Refresh access token using refresh token
router.post('/refresh-token', validate(refreshTokenSchema), refreshToken);

// PATCH /api/auth/change-password - Change password for authenticated user
router.patch(
'/change-password',
authenticate,
validate(changePasswordSchema),
changePassword
);

module.exports = router;
13 changes: 13 additions & 0 deletions src/validators/auth.validators.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,23 @@ const refreshTokenSchema = Joi.object({
}),
});

const changePasswordSchema = Joi.object({
currentPassword: Joi.string().required().messages({
'string.empty': 'Current password is required',
'any.required': 'Current password is required',
}),
newPassword: Joi.string().min(8).required().messages({
'string.empty': 'New password is required',
'string.min': 'New password must be at least 8 characters',
'any.required': 'New password is required',
}),
});

module.exports = {
registerSchema,
loginSchema,
resetPasswordSchema,
forgotPasswordSchema,
refreshTokenSchema,
changePasswordSchema,
};