Add change password functionality with validation and email notification

This commit is contained in:
Isuru Bimsara
2026-08-21 21:42:19 +05:30
parent 7766614898
commit efb054f54d
3 changed files with 202 additions and 12 deletions
+86
View File
@@ -233,6 +233,92 @@ The new password must contain at least one uppercase letter, one lowercase lette
--- ---
## Change Password
Changes the authenticated user's password. After a successful change, all refresh sessions are revoked, authentication cookies are cleared, and the user must log in again.
**Endpoint:** `POST` [http://localhost:3070/api/auth/change-password](http://localhost:3070/api/auth/change-password)
**Authentication:** Required
The access token may be supplied through the `access_token` cookie or as a Bearer token:
```http
Authorization: Bearer <access-token>
```
### Request body
```json
{
"currentPassword": "CurrentPassword@1234",
"newPassword": "NewPassword@5678",
"confirmPassword": "NewPassword@5678"
}
```
The new password:
- Must match `confirmPassword`
- Must differ from the current password
- Must contain at least one uppercase letter
- Must contain at least one lowercase letter
- Must contain at least one symbol
- Must contain at least four digits
### Success response — `200 OK`
```json
{
"success": true,
"message": "Password changed successfully. Please login again."
}
```
### Error responses
#### `400 Bad Request`
Returned when required fields are missing, the passwords do not match, the new password does not satisfy the password policy, or it matches the current password.
```json
{
"success": false,
"message": "New password and confirm password do not match"
}
```
#### `401 Unauthorized`
Returned when authentication fails or the current password is incorrect.
```json
{
"success": false,
"message": "Current password is incorrect"
}
```
#### `404 Not Found`
```json
{
"success": false,
"message": "User not found"
}
```
#### `500 Internal Server Error`
```json
{
"success": false,
"message": "Failed to change password"
}
```
---
## Logout ## Logout
Deletes the current refresh session when available and clears both authentication cookies. Deletes the current refresh session when available and clears both authentication cookies.
+115 -12
View File
@@ -374,7 +374,119 @@ exports.resetPassword = async (req, res) => {
} }
}; };
exports.changePassword = async (req, res) => {
try {
// User ID comes from authenticate middleware
const userId = req.user.id;
const { currentPassword, newPassword, confirmPassword } = req.body;
// 1. Check required fields
if (!currentPassword || !newPassword || !confirmPassword) {
return res.status(400).send({
success: false,
message:
"Current password, new password and confirm password are required",
});
}
// 2. Check new password and confirmation
if (newPassword !== confirmPassword) {
return res.status(400).send({
success: false,
message: "New password and confirm password do not match",
});
}
// 3. Validate password policy
const passwordValid = validatePassword(newPassword);
if (!passwordValid) {
return res.status(400).send({
success: false,
message: "New password does not meet the required criteria",
});
}
// 4. Get logged-in user from database
const user = await User.findByPk(userId);
if (!user) {
return res.status(404).send({
success: false,
message: "User not found",
});
}
// 5. Check current password
const currentPasswordValid = await checkPassword(
currentPassword,
user.password,
);
if (!currentPasswordValid) {
return res.status(401).send({
success: false,
message: "Current password is incorrect",
});
}
// 6. Make sure new password is different
const sameAsOldPassword = await checkPassword(newPassword, user.password);
if (sameAsOldPassword) {
return res.status(400).send({
success: false,
message: "New password must be different from current password",
});
}
// 7. Hash new password
const hashedPassword = await hashPassword(newPassword);
// 8. Update user
user.password = hashedPassword;
user.passwordChangedAt = new Date();
await user.save();
// 9. Revoke all refresh sessions
deleteAllUserSessions(user.id);
// 10. Clear auth cookies
res.clearCookie("access_token", {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: process.env.NODE_ENV === "production" ? "None" : "Lax",
});
res.clearCookie("refresh_token", {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: process.env.NODE_ENV === "production" ? "None" : "Lax",
});
// 11. Send confirmation email
try {
await sendPasswordChangedEmail(user.email, user.firstName);
} catch (mailError) {
console.error("PASSWORD CHANGED EMAIL ERROR:", mailError);
}
// 12. Response
return res.status(200).send({
success: true,
message: "Password changed successfully. Please login again.",
});
} catch (error) {
console.error("CHANGE PASSWORD ERROR:", error);
return res.status(500).send({
success: false,
message: "Failed to change password",
});
}
};
// Logout: Clear the JWT cookie // Logout: Clear the JWT cookie
exports.logout = async (req, res) => { exports.logout = async (req, res) => {
@@ -390,9 +502,7 @@ exports.logout = async (req, res) => {
if (session) { if (session) {
deleteRefreshSession(session.sessionId); deleteRefreshSession(session.sessionId);
console.log( console.log(`Refresh session deleted: ${session.sessionId}`);
`Refresh session deleted: ${session.sessionId}`
);
} }
} }
@@ -400,20 +510,14 @@ exports.logout = async (req, res) => {
res.clearCookie("access_token", { res.clearCookie("access_token", {
httpOnly: true, httpOnly: true,
secure: process.env.NODE_ENV === "production", secure: process.env.NODE_ENV === "production",
sameSite: sameSite: process.env.NODE_ENV === "production" ? "None" : "Lax",
process.env.NODE_ENV === "production"
? "None"
: "Lax",
}); });
// 5. Clear refresh token cookie // 5. Clear refresh token cookie
res.clearCookie("refresh_token", { res.clearCookie("refresh_token", {
httpOnly: true, httpOnly: true,
secure: process.env.NODE_ENV === "production", secure: process.env.NODE_ENV === "production",
sameSite: sameSite: process.env.NODE_ENV === "production" ? "None" : "Lax",
process.env.NODE_ENV === "production"
? "None"
: "Lax",
}); });
// 6. Send response // 6. Send response
@@ -421,7 +525,6 @@ exports.logout = async (req, res) => {
success: true, success: true,
message: "Logged out successfully", message: "Logged out successfully",
}); });
} catch (error) { } catch (error) {
console.error("LOGOUT ERROR:", error); console.error("LOGOUT ERROR:", error);
+1
View File
@@ -27,6 +27,7 @@ router.post('/login', authController.login);
router.post('/refresh', authController.refreshToken); router.post('/refresh', authController.refreshToken);
router.post('/forgot-password', authController.forgotPassword); router.post('/forgot-password', authController.forgotPassword);
router.post('/reset-password', authController.resetPassword); router.post('/reset-password', authController.resetPassword);
router.post('/change-password', authenticate, authController.changePassword);
router.post('/logout', authController.logout); router.post('/logout', authController.logout);
module.exports = router; module.exports = router;