diff --git a/src/api-doc/MentorED-Users.postman_collection.json b/src/api-doc/MentorED-Users.postman_collection.json index 86505b02f..089183e0e 100644 --- a/src/api-doc/MentorED-Users.postman_collection.json +++ b/src/api-doc/MentorED-Users.postman_collection.json @@ -450,6 +450,53 @@ } ] }, + + { + "name": "Change Password", + "request": { + "method": "POST", + "header": [ + { + "key": "X-auth-token", + "value": "bearer {{token}}", + "type": "text" + } + ], + "body": { + "mode": "urlencoded", + "urlencoded": [ + { + "key": "oldPassword", + "value": "password", + "type": "text" + }, + { + "key": "newPassword", + "value": "Password@123", + "type": "text" + } + ] + }, + "url": { + "raw": "{{UserDevBaseUrl}}user/v1/account/changePassword", + "host": ["{{UserDevBaseUrl}}user"], + "path": ["v1", "account", "changePassword"], + "query": [ + { + "key": "oldPassword", + "value": "password", + "disabled": true + }, + { + "key": "newPassword", + "value": "Password@123", + "disabled": true + } + ] + } + }, + "response": [] + }, { "name": "User", "item": [ diff --git a/src/controllers/v1/account.js b/src/controllers/v1/account.js index 9ac9c015a..b5fa81738 100644 --- a/src/controllers/v1/account.js +++ b/src/controllers/v1/account.js @@ -244,4 +244,26 @@ module.exports = class Account { return error } } + + /** + * change password + * @method + * @name changePassword + * @param {Object} req -request data. + * @param {Object} req.decodedToken.id - UserId. + * @param {string} req.body - request body contains user password + * @param {string} req.body.OldPassword - user Old Password. + * @param {string} req.body.NewPassword - user New Password. + * @param {string} req.body.ConfirmNewPassword - user Confirming New Password. + * @returns {JSON} - password changed response + */ + + async changePassword(req) { + try { + const result = await accountService.changePassword(req.body, req.decodedToken.id) + return result + } catch (error) { + return error + } + } } diff --git a/src/database/migrations/20240325113407-change-password-email--template-data.js b/src/database/migrations/20240325113407-change-password-email--template-data.js new file mode 100644 index 000000000..8371e564f --- /dev/null +++ b/src/database/migrations/20240325113407-change-password-email--template-data.js @@ -0,0 +1,36 @@ +const moment = require('moment') + +module.exports = { + up: async (queryInterface, Sequelize) => { + const defaultOrgId = queryInterface.sequelize.options.defaultOrgId + if (!defaultOrgId) { + throw new Error('Default org ID is undefined. Please make sure it is set in sequelize options.') + } + const emailTemplates = [ + { + code: 'change_password', + subject: 'Password Change', + body: '
Dear {name},
Your password has been changed successfully.', + }, + ] + + let notificationTemplateData = [] + emailTemplates.forEach(async function (emailTemplate) { + emailTemplate['status'] = 'ACTIVE' + emailTemplate['type'] = 'email' + emailTemplate['updated_at'] = moment().format() + emailTemplate['created_at'] = moment().format() + emailTemplate['organization_id'] = defaultOrgId + emailTemplate['email_footer'] = 'email_footer' + emailTemplate['email_header'] = 'email_header' + + notificationTemplateData.push(emailTemplate) + }) + + await queryInterface.bulkInsert('notification_templates', notificationTemplateData, {}) + }, + + down: async (queryInterface, Sequelize) => { + await queryInterface.bulkDelete('notification_templates', null, {}) + }, +} diff --git a/src/database/migrations/20240326060546-change-password-permissions.js b/src/database/migrations/20240326060546-change-password-permissions.js new file mode 100644 index 000000000..7ba2f6292 --- /dev/null +++ b/src/database/migrations/20240326060546-change-password-permissions.js @@ -0,0 +1,41 @@ +'use strict' + +/** @type {import('sequelize-cli').Migration} */ +module.exports = { + async up(queryInterface, Sequelize) { + try { + const permissionsData = [ + { + code: 'change_password_update', + module: 'account', + request_type: ['POST'], + api_path: '/user/v1/account/changePassword', + status: 'ACTIVE', + }, + ] + + // Batch insert permissions + await queryInterface.bulkInsert( + 'permissions', + permissionsData.map((permission) => ({ + ...permission, + created_at: new Date(), + updated_at: new Date(), + })) + ) + } catch (error) { + console.error('Error in migration:', error) + throw error + } + }, + + async down(queryInterface, Sequelize) { + try { + // Rollback migration by deleting all permissions + await queryInterface.bulkDelete('permissions', null, {}) + } catch (error) { + console.error('Error in rollback migration:', error) + throw error + } + }, +} diff --git a/src/database/migrations/20240326060601-change-password-role-permissions.js b/src/database/migrations/20240326060601-change-password-role-permissions.js new file mode 100644 index 000000000..f2e018b8c --- /dev/null +++ b/src/database/migrations/20240326060601-change-password-role-permissions.js @@ -0,0 +1,96 @@ +'use strict' + +require('module-alias/register') +require('dotenv').config() +const common = require('@constants/common') +const Permissions = require('@database/models/index').Permission + +const getPermissionId = async (module, request_type, api_path) => { + try { + const permission = await Permissions.findOne({ + where: { module, request_type, api_path }, + }) + if (!permission) { + throw new Error( + `Permission not found for module: ${module}, request_type: ${request_type}, api_path: ${api_path}` + ) + } + return permission.id + } catch (error) { + throw new Error(`Error while fetching permission: ${error.message}`) + } +} + +module.exports = { + up: async (queryInterface, Sequelize) => { + try { + const rolePermissionsData = await Promise.all([ + { + role_title: common.MENTOR_ROLE, + permission_id: await getPermissionId('account', ['POST'], '/user/v1/account/changePassword'), + module: 'account', + request_type: ['POST'], + api_path: '/user/v1/account/changePassword', + }, + { + role_title: common.MENTEE_ROLE, + permission_id: await getPermissionId('account', ['POST'], '/user/v1/account/changePassword'), + module: 'account', + request_type: ['POST'], + api_path: '/user/v1/account/changePassword', + }, + { + role_title: common.ORG_ADMIN_ROLE, + permission_id: await getPermissionId('account', ['POST'], '/user/v1/account/changePassword'), + module: 'account', + request_type: ['POST'], + api_path: '/user/v1/account/changePassword', + }, + { + role_title: common.USER_ROLE, + permission_id: await getPermissionId('account', ['POST'], '/user/v1/account/changePassword'), + module: 'account', + request_type: ['POST'], + api_path: '/user/v1/account/changePassword', + }, + { + role_title: common.ADMIN_ROLE, + permission_id: await getPermissionId('account', ['POST'], '/user/v1/account/changePassword'), + module: 'account', + request_type: ['POST'], + api_path: '/user/v1/account/changePassword', + }, + { + role_title: common.SESSION_MANAGER_ROLE, + permission_id: await getPermissionId('account', ['POST'], '/user/v1/account/changePassword'), + module: 'account', + request_type: ['POST'], + api_path: '/user/v1/account/changePassword', + }, + ]) + + await queryInterface.bulkInsert( + 'role_permission_mapping', + rolePermissionsData.map((data) => ({ + ...data, + created_at: new Date(), + updated_at: new Date(), + created_by: 0, + })) + ) + } catch (error) { + console.log(error) + console.error(`Migration error: ${error.message}`) + throw error + } + }, + + down: async (queryInterface, Sequelize) => { + try { + await queryInterface.bulkDelete('role_permission_mapping', null, {}) + } catch (error) { + console.error(`Rollback migration error: ${error.message}`) + throw error + } + }, +} diff --git a/src/envVariables.js b/src/envVariables.js index a8a3ba38a..941d068eb 100644 --- a/src/envVariables.js +++ b/src/envVariables.js @@ -244,13 +244,12 @@ let enviromentVariables = { PASSWORD_POLICY_REGEX: { message: 'Required password policy', optional: true, - default: '^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$%^&*-]).{10,}$', + default: '^.{8,}$', }, PASSWORD_POLICY_MESSAGE: { message: 'Required password policy message', optional: true, - default: - 'Password must have at least one uppercase letter, one number, one special character, and be at least 10 characters long', + default: 'Password must have at least 8 characters long', }, DOWNLOAD_URL_EXPIRATION_DURATION: { message: 'Required downloadable url expiration time', diff --git a/src/locales/en.json b/src/locales/en.json index b5dad36d7..9f754e900 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -114,6 +114,8 @@ "COLUMN_DOES_NOT_EXISTS": "Role column does not exists", "PERMISSION_DENIED": "You do not have the required permissions to access this resource. Please contact your administrator for assistance.", "RELATED_ORG_REMOVAL_FAILED": "Requested organization not related the organization. Please check the values.", - "INAVLID_ORG_ROLE_REQ": "Invalid organisation request" - + "INAVLID_ORG_ROLE_REQ": "Invalid organisation request", + "INCORRECT_OLD_PASSWORD": "Invalid old password", + "SAME_PASSWORD_ERROR": "New password cannot be same as old password", + "PASSWORD_CHANGED_SUCCESSFULLY": "Password changed successfully." } diff --git a/src/services/account.js b/src/services/account.js index 3c77fa6f6..2b740c47d 100644 --- a/src/services/account.js +++ b/src/services/account.js @@ -1253,4 +1253,101 @@ module.exports = class AccountHelper { throw error } } + + /** + * change password + * @method + * @name changePassword + * @param {Object} req -request data. + * @param {Object} req.decodedToken.id - UserId. + * @param {string} req.body - request body contains user password + * @param {string} req.body.OldPassword - user Old Password. + * @param {string} req.body.NewPassword - user New Password. + * @param {string} req.body.ConfirmNewPassword - user Confirming New Password. + * @returns {JSON} - password changed response + */ + + static async changePassword(bodyData, userId) { + const projection = ['location'] + try { + const userCredentials = await UserCredentialQueries.findOne({ user_id: userId }) + if (!userCredentials) { + return responses.failureResponse({ + message: 'USER_DOESNOT_EXISTS', + statusCode: httpStatusCode.bad_request, + responseCode: 'CLIENT_ERROR', + }) + } + const plaintextEmailId = emailEncryption.decrypt(userCredentials.email) + + let user = await userQueries.findOne( + { id: userCredentials.user_id, organization_id: userCredentials.organization_id }, + { attributes: { exclude: projection } } + ) + if (!user) { + return responses.failureResponse({ + message: 'USER_DOESNOT_EXISTS', + statusCode: httpStatusCode.bad_request, + responseCode: 'CLIENT_ERROR', + }) + } + + const verifyOldPassword = utilsHelper.comparePassword(bodyData.oldPassword, userCredentials.password) + if (!verifyOldPassword) { + return responses.failureResponse({ + message: 'INCORRECT_OLD_PASSWORD', + statusCode: httpStatusCode.bad_request, + responseCode: 'CLIENT_ERROR', + }) + } + + const isPasswordSame = bcryptJs.compareSync(bodyData.newPassword, userCredentials.password) + if (isPasswordSame) { + return responses.failureResponse({ + message: 'SAME_PASSWORD_ERROR', + statusCode: httpStatusCode.bad_request, + responseCode: 'CLIENT_ERROR', + }) + } + bodyData.newPassword = utilsHelper.hashPassword(bodyData.newPassword) + + const updateParams = { password: bodyData.newPassword } + + await userQueries.updateUser( + { id: user.id, organization_id: userCredentials.organization_id }, + updateParams + ) + await UserCredentialQueries.updateUser({ email: userCredentials.email }, { password: bodyData.newPassword }) + await utilsHelper.redisDel(userCredentials.email) + + const templateData = await notificationTemplateQueries.findOneEmailTemplate( + process.env.CHANGE_PASSWORD_TEMPLATE_CODE + ) + + if (templateData) { + // Push successful registration email to kafka + const payload = { + type: common.notificationEmailType, + email: { + to: plaintextEmailId, + subject: templateData.subject, + body: utilsHelper.composeEmailBody(templateData.body, { + name: user.name, + }), + }, + } + await kafkaCommunication.pushEmailToKafka(payload) + } + + const result = {} + return responses.successResponse({ + statusCode: httpStatusCode.ok, + message: 'PASSWORD_CHANGED_SUCCESSFULLY', + result, + }) + } catch (error) { + console.log(error) + throw error + } + } }