From c8f09627cb881983a263300f0757e5d4608c50f3 Mon Sep 17 00:00:00 2001 From: sumanvpacewisdom Date: Tue, 26 Mar 2024 13:52:32 +0530 Subject: [PATCH 1/3] Change Password API --- .../MentorED-Users.postman_collection.json | 47 ++++++ src/controllers/v1/account.js | 22 +++ ...07-change-password-email--template-data.js | 36 +++++ ...40326060546-change-password-permissions.js | 41 +++++ ...060601-change-password-role-permissions.js | 96 ++++++++++++ src/envVariables.js | 5 +- src/services/account.js | 143 ++++++++++++++++++ 7 files changed, 387 insertions(+), 3 deletions(-) create mode 100644 src/database/migrations/20240325113407-change-password-email--template-data.js create mode 100644 src/database/migrations/20240326060546-change-password-permissions.js create mode 100644 src/database/migrations/20240326060601-change-password-role-permissions.js 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..6202a5cc0 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, req.decodedToken.name) + 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/services/account.js b/src/services/account.js index 3c77fa6f6..7f1fcd7b4 100644 --- a/src/services/account.js +++ b/src/services/account.js @@ -1253,4 +1253,147 @@ 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, userName) { + const projection = ['location'] + try { + const userCredentials = await UserCredentialQueries.findOne({ user_id: userId }) + const plaintextEmailId = emailEncryption.decrypt(userCredentials.email) + if (!userCredentials) { + return responses.failureResponse({ + message: 'USER_DOESNOT_EXISTS', + statusCode: httpStatusCode.bad_request, + responseCode: 'CLIENT_ERROR', + }) + } + 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', + }) + } + let roles = await roleQueries.findAll({ id: user.roles, status: common.ACTIVE_STATUS }) + if (!roles) { + return responses.failureResponse({ + message: 'ROLE_NOT_FOUND', + statusCode: httpStatusCode.not_acceptable, + responseCode: 'CLIENT_ERROR', + }) + } + user.user_roles = roles + + 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: 'INCORRECT_OLD_PASSWORD', + statusCode: httpStatusCode.bad_request, + responseCode: 'CLIENT_ERROR', + }) + } + bodyData.newPassword = utilsHelper.hashPassword(bodyData.newPassword) + + const updateParams = { + lastLoggedInAt: new Date().getTime(), + 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) + + delete user.newPassword + + 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: userName, + }), + }, + } + + await kafkaCommunication.pushEmailToKafka(payload) + } + + let defaultOrg = await organizationQueries.findOne( + { code: process.env.DEFAULT_ORGANISATION_CODE }, + { attributes: ['id'] } + ) + let defaultOrgId = defaultOrg.id + const modelName = await userQueries.getModelName() + + let validationData = await entityTypeQueries.findUserEntityTypesAndEntities({ + status: 'ACTIVE', + organization_id: { + [Op.in]: [user.organization_id, defaultOrgId], + }, + model_names: { [Op.contains]: [modelName] }, + }) + + const prunedEntities = removeDefaultOrgEntityTypes(validationData, user.organization_id) + user = utils.processDbResponse(user, prunedEntities) + + if (user && user.image) { + user.image = await utils.getDownloadableUrl(user.image) + } + user.email = plaintextEmailId + + const result = {} + return responses.successResponse({ + statusCode: httpStatusCode.ok, + message: 'PASSWORD_CHANGED_SUCCESSFULLY', + result, + }) + } catch (error) { + console.log(error) + throw error + } + } } From 45d2f941e73bee9e5e317f4f4ca4d92360404b8c Mon Sep 17 00:00:00 2001 From: sumanvpacewisdom Date: Tue, 26 Mar 2024 15:39:53 +0530 Subject: [PATCH 2/3] Comment changes --- src/controllers/v1/account.js | 2 +- src/locales/en.json | 6 ++-- src/services/account.js | 60 +++++------------------------------ 3 files changed, 13 insertions(+), 55 deletions(-) diff --git a/src/controllers/v1/account.js b/src/controllers/v1/account.js index 6202a5cc0..b5fa81738 100644 --- a/src/controllers/v1/account.js +++ b/src/controllers/v1/account.js @@ -260,7 +260,7 @@ module.exports = class Account { async changePassword(req) { try { - const result = await accountService.changePassword(req.body, req.decodedToken.id, req.decodedToken.name) + const result = await accountService.changePassword(req.body, req.decodedToken.id) return result } catch (error) { return error 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 7f1fcd7b4..af8844d7e 100644 --- a/src/services/account.js +++ b/src/services/account.js @@ -1267,11 +1267,10 @@ module.exports = class AccountHelper { * @returns {JSON} - password changed response */ - static async changePassword(bodyData, userId, userName) { + static async changePassword(bodyData, userId) { const projection = ['location'] try { const userCredentials = await UserCredentialQueries.findOne({ user_id: userId }) - const plaintextEmailId = emailEncryption.decrypt(userCredentials.email) if (!userCredentials) { return responses.failureResponse({ message: 'USER_DOESNOT_EXISTS', @@ -1279,13 +1278,11 @@ module.exports = class AccountHelper { 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, - }, - } + { attributes: { exclude: projection } } ) if (!user) { return responses.failureResponse({ @@ -1294,15 +1291,6 @@ module.exports = class AccountHelper { responseCode: 'CLIENT_ERROR', }) } - let roles = await roleQueries.findAll({ id: user.roles, status: common.ACTIVE_STATUS }) - if (!roles) { - return responses.failureResponse({ - message: 'ROLE_NOT_FOUND', - statusCode: httpStatusCode.not_acceptable, - responseCode: 'CLIENT_ERROR', - }) - } - user.user_roles = roles const verifyOldPassword = utilsHelper.comparePassword(bodyData.oldPassword, userCredentials.password) if (!verifyOldPassword) { @@ -1316,28 +1304,20 @@ module.exports = class AccountHelper { const isPasswordSame = bcryptJs.compareSync(bodyData.newPassword, userCredentials.password) if (isPasswordSame) { return responses.failureResponse({ - message: 'INCORRECT_OLD_PASSWORD', + message: 'SAME_PASSWORD_ERROR', statusCode: httpStatusCode.bad_request, responseCode: 'CLIENT_ERROR', }) } bodyData.newPassword = utilsHelper.hashPassword(bodyData.newPassword) - const updateParams = { - lastLoggedInAt: new Date().getTime(), - password: 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 UserCredentialQueries.updateUser({ email: userCredentials.email }, { password: bodyData.newPassword }) await utilsHelper.redisDel(userCredentials.email) delete user.newPassword @@ -1354,37 +1334,13 @@ module.exports = class AccountHelper { to: plaintextEmailId, subject: templateData.subject, body: utilsHelper.composeEmailBody(templateData.body, { - name: userName, + name: user.name, }), }, } - await kafkaCommunication.pushEmailToKafka(payload) } - let defaultOrg = await organizationQueries.findOne( - { code: process.env.DEFAULT_ORGANISATION_CODE }, - { attributes: ['id'] } - ) - let defaultOrgId = defaultOrg.id - const modelName = await userQueries.getModelName() - - let validationData = await entityTypeQueries.findUserEntityTypesAndEntities({ - status: 'ACTIVE', - organization_id: { - [Op.in]: [user.organization_id, defaultOrgId], - }, - model_names: { [Op.contains]: [modelName] }, - }) - - const prunedEntities = removeDefaultOrgEntityTypes(validationData, user.organization_id) - user = utils.processDbResponse(user, prunedEntities) - - if (user && user.image) { - user.image = await utils.getDownloadableUrl(user.image) - } - user.email = plaintextEmailId - const result = {} return responses.successResponse({ statusCode: httpStatusCode.ok, From fbfb5dfffcfb03b772e07daf76c04c1d6eac2b08 Mon Sep 17 00:00:00 2001 From: sumanvpacewisdom Date: Tue, 26 Mar 2024 15:49:00 +0530 Subject: [PATCH 3/3] comment changes --- src/services/account.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/services/account.js b/src/services/account.js index af8844d7e..2b740c47d 100644 --- a/src/services/account.js +++ b/src/services/account.js @@ -1320,8 +1320,6 @@ module.exports = class AccountHelper { await UserCredentialQueries.updateUser({ email: userCredentials.email }, { password: bodyData.newPassword }) await utilsHelper.redisDel(userCredentials.email) - delete user.newPassword - const templateData = await notificationTemplateQueries.findOneEmailTemplate( process.env.CHANGE_PASSWORD_TEMPLATE_CODE )