diff --git a/Documentation/Auth-API.md b/Documentation/Auth-API.md index ae2fb68..74604e7 100644 --- a/Documentation/Auth-API.md +++ b/Documentation/Auth-API.md @@ -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 +``` + +### 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 Deletes the current refresh session when available and clears both authentication cookies. diff --git a/app/controllers/auth.controller.js b/app/controllers/auth.controller.js index dbb480c..4780f89 100644 --- a/app/controllers/auth.controller.js +++ b/app/controllers/auth.controller.js @@ -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 exports.logout = async (req, res) => { @@ -390,9 +502,7 @@ exports.logout = async (req, res) => { if (session) { deleteRefreshSession(session.sessionId); - console.log( - `Refresh session deleted: ${session.sessionId}` - ); + console.log(`Refresh session deleted: ${session.sessionId}`); } } @@ -400,20 +510,14 @@ exports.logout = async (req, res) => { res.clearCookie("access_token", { httpOnly: true, secure: process.env.NODE_ENV === "production", - sameSite: - process.env.NODE_ENV === "production" - ? "None" - : "Lax", + sameSite: process.env.NODE_ENV === "production" ? "None" : "Lax", }); // 5. Clear refresh token cookie res.clearCookie("refresh_token", { httpOnly: true, secure: process.env.NODE_ENV === "production", - sameSite: - process.env.NODE_ENV === "production" - ? "None" - : "Lax", + sameSite: process.env.NODE_ENV === "production" ? "None" : "Lax", }); // 6. Send response @@ -421,7 +525,6 @@ exports.logout = async (req, res) => { success: true, message: "Logged out successfully", }); - } catch (error) { console.error("LOGOUT ERROR:", error); diff --git a/app/routes/auth.routes.js b/app/routes/auth.routes.js index 9818a5b..d22bacd 100644 --- a/app/routes/auth.routes.js +++ b/app/routes/auth.routes.js @@ -27,6 +27,7 @@ router.post('/login', authController.login); router.post('/refresh', authController.refreshToken); router.post('/forgot-password', authController.forgotPassword); router.post('/reset-password', authController.resetPassword); +router.post('/change-password', authenticate, authController.changePassword); router.post('/logout', authController.logout); module.exports = router;