Add change password functionality with validation and email notification
This commit is contained in:
@@ -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.
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user