From a24b42be2331f6a8afc78a7280fdc38b8d918680 Mon Sep 17 00:00:00 2001 From: vishnu Date: Tue, 26 Mar 2024 22:21:43 +0530 Subject: [PATCH 01/10] user-sessions table added to user service --- Dockerfile | 2 +- ...240326110128-create-user-sessions-table.js | 43 +++++++++ src/database/models/user-sessions.js | 46 ++++++++++ src/database/queries/user-sessions.js | 91 +++++++++++++++++++ 4 files changed, 181 insertions(+), 1 deletion(-) create mode 100644 src/database/migrations/20240326110128-create-user-sessions-table.js create mode 100644 src/database/models/user-sessions.js create mode 100644 src/database/queries/user-sessions.js diff --git a/Dockerfile b/Dockerfile index d9688ab63..ddb04c98d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:16 +FROM node:20 #Set working directory WORKDIR /var/src/ diff --git a/src/database/migrations/20240326110128-create-user-sessions-table.js b/src/database/migrations/20240326110128-create-user-sessions-table.js new file mode 100644 index 000000000..663b335e1 --- /dev/null +++ b/src/database/migrations/20240326110128-create-user-sessions-table.js @@ -0,0 +1,43 @@ +'use strict' + +/** @type {import('sequelize-cli').Migration} */ +module.exports = { + async up(queryInterface, Sequelize) { + await queryInterface.createTable('user_sessions', { + id: { + type: Sequelize.INTEGER, + allowNull: false, + primaryKey: true, + autoIncrement: true, + }, + user_id: { + type: Sequelize.INTEGER, + allowNull: false, + }, + started_at: { + type: Sequelize.DATE, + allowNull: false, + }, + ended_at: { + type: Sequelize.DATE, + allowNull: true, + }, + token: { + type: Sequelize.STRING, + allowNull: false, + }, + device_info: { + type: Sequelize.JSONB, + allowNull: true, + }, + refresh_token: { + type: Sequelize.STRING, + allowNull: false, + }, + }) + }, + + async down(queryInterface, Sequelize) { + await queryInterface.dropTable('user_sessions') + }, +} diff --git a/src/database/models/user-sessions.js b/src/database/models/user-sessions.js new file mode 100644 index 000000000..00fdec943 --- /dev/null +++ b/src/database/models/user-sessions.js @@ -0,0 +1,46 @@ +'use strict' +module.exports = (sequelize, DataTypes) => { + const UserSessions = sequelize.define( + 'UserSessions', + { + id: { + type: DataTypes.INTEGER, + allowNull: false, + primaryKey: true, + autoIncrement: true, + }, + user_id: { + type: DataTypes.INTEGER, + allowNull: false, + }, + started_at: { + type: DataTypes.DATE, + allowNull: false, + }, + ended_at: { + type: DataTypes.DATE, + allowNull: true, + }, + token: { + type: DataTypes.STRING, + allowNull: false, + }, + device_info: { + type: DataTypes.JSONB, + allowNull: true, + }, + refresh_token: { + type: DataTypes.STRING, + allowNull: false, + }, + }, + { + sequelize, + modelName: 'UserSessions', + tableName: 'user_sessions', + freezeTableName: true, + paranoid: true, + } + ) + return UserSessions +} diff --git a/src/database/queries/user-sessions.js b/src/database/queries/user-sessions.js new file mode 100644 index 000000000..599d60819 --- /dev/null +++ b/src/database/queries/user-sessions.js @@ -0,0 +1,91 @@ +/** + * name : queries/user-sessions.js + * author : Vishnu + * created-date : 26-Mar-2024 + * Description : user-sessions table query methods. + */ + +// Dependencies +'use strict' +const UserSessions = require('@database/models/index').UserSessions +const { Op } = require('sequelize') + +/** + * Find one record based on the provided filter. + * @param {Object} filter - The filter object to specify the condition for selecting the record. + * @param {Object} options - Additional options for the query (optional). + * @returns {Promise} - A promise that resolves to the found record or an error. + */ +exports.findOne = async (filter, options = {}) => { + try { + return await UserSessions.findOne({ + where: filter, + ...options, + raw: true, + }) + } catch (error) { + return error + } +} + +/** + * Find a record by its primary key. + * @param {number|string} id - The primary key value of the record. + * @returns {Promise} - A promise that resolves to the found record or an error. + */ +exports.findByPk = async (id) => { + try { + return await UserSessions.findByPk(id, { raw: true }) + } catch (error) { + return error + } +} + +/** + * Find all records based on the provided filter. + * @param {Object} filter - The filter object to specify the condition for selecting records. + * @param {Object} options - Additional options for the query (optional). + * @returns {Promise} - A promise that resolves to an array of found records or an error. + */ +exports.findAll = async (filter, options = {}) => { + try { + return await UserSessions.findAll({ + where: filter, + ...options, + raw: true, + }) + } catch (error) { + return error + } +} + +/** + * Update records based on the provided filter and update data. + * @param {Object} filter - The filter object to specify the condition for updating records. + * @param {Object} update - The update data to be applied to matching records. + * @param {Object} options - Additional options for the update operation (optional). + * @returns {Promise} - A promise that resolves to the number of updated records or an error. + */ +exports.update = async (filter, update, options = {}) => { + try { + return await await Session.update(update, { + where: filter, + ...options, + }) + } catch (error) { + return error + } +} + +/** + * Create a new record with the provided data. + * @param {Object} data - The data object representing the record to be created. + * @returns {Promise} - A promise that resolves to the created record or an error. + */ +exports.create = async (data) => { + try { + return await UserSessions.create(data, { returning: true }) + } catch (error) { + throw error + } +} From 12db6ce2f4440d550b78a44688628d062c1af865 Mon Sep 17 00:00:00 2001 From: vishnu Date: Wed, 27 Mar 2024 11:43:43 +0530 Subject: [PATCH 02/10] user sessions --- src/controllers/v1/account.js | 3 +- ...240326110128-create-user-sessions-table.js | 4 +- src/database/models/user-sessions.js | 4 +- src/locales/en.json | 3 +- src/services/account.js | 23 ++++++- src/services/user-sessions.js | 62 +++++++++++++++++++ 6 files changed, 92 insertions(+), 7 deletions(-) create mode 100644 src/services/user-sessions.js diff --git a/src/controllers/v1/account.js b/src/controllers/v1/account.js index b5fa81738..5b0f36dd7 100644 --- a/src/controllers/v1/account.js +++ b/src/controllers/v1/account.js @@ -45,8 +45,9 @@ module.exports = class Account { async login(req) { const params = req.body + const device_info = req.headers && req.headers['device-info'] ? req.headers['device-info'] : {} try { - const loggedInAccount = await accountService.login(params) + const loggedInAccount = await accountService.login(params, device_info) return loggedInAccount } catch (error) { return error diff --git a/src/database/migrations/20240326110128-create-user-sessions-table.js b/src/database/migrations/20240326110128-create-user-sessions-table.js index 663b335e1..dbe955923 100644 --- a/src/database/migrations/20240326110128-create-user-sessions-table.js +++ b/src/database/migrations/20240326110128-create-user-sessions-table.js @@ -24,7 +24,7 @@ module.exports = { }, token: { type: Sequelize.STRING, - allowNull: false, + allowNull: true, }, device_info: { type: Sequelize.JSONB, @@ -32,7 +32,7 @@ module.exports = { }, refresh_token: { type: Sequelize.STRING, - allowNull: false, + allowNull: true, }, }) }, diff --git a/src/database/models/user-sessions.js b/src/database/models/user-sessions.js index 00fdec943..b70ce7e43 100644 --- a/src/database/models/user-sessions.js +++ b/src/database/models/user-sessions.js @@ -23,7 +23,7 @@ module.exports = (sequelize, DataTypes) => { }, token: { type: DataTypes.STRING, - allowNull: false, + allowNull: true, }, device_info: { type: DataTypes.JSONB, @@ -31,7 +31,7 @@ module.exports = (sequelize, DataTypes) => { }, refresh_token: { type: DataTypes.STRING, - allowNull: false, + allowNull: true, }, }, { diff --git a/src/locales/en.json b/src/locales/en.json index 9f754e900..59bb49d8e 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -117,5 +117,6 @@ "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." + "PASSWORD_CHANGED_SUCCESSFULLY": "Password changed successfully.", + "USER_SESSION_CREATED_SUCCESSFULLY": "User session created successfully" } diff --git a/src/services/account.js b/src/services/account.js index d3fd8cc71..aaf9f18b6 100644 --- a/src/services/account.js +++ b/src/services/account.js @@ -28,6 +28,7 @@ const { removeDefaultOrgEntityTypes } = require('@generics/utils') const UserCredentialQueries = require('@database/queries/userCredential') const emailEncryption = require('@utils/emailEncryption') const responses = require('@helpers/responses') +const userSessions = require('@services/user-sessions') module.exports = class AccountHelper { /** * create account @@ -358,10 +359,11 @@ module.exports = class AccountHelper { * @param {Object} bodyData -request body contains user login deatils. * @param {String} bodyData.email - user email. * @param {String} bodyData.password - user password. + * @param {Object} deviceInformation - device information * @returns {JSON} - returns susccess or failure of login details. */ - static async login(bodyData) { + static async login(bodyData, deviceInformation) { try { const plaintextEmailId = bodyData.email.toLowerCase() const encryptedEmailId = emailEncryption.encrypt(plaintextEmailId) @@ -419,6 +421,14 @@ module.exports = class AccountHelper { }) } + // create user session entry and add session_id to token data + const userSessionDetails = await userSessions.createUserSession( + user.id, // userid + '', // refresh token + '', // Access token + deviceInformation + ) + console.log('userSessionDetails : ', userSessionDetails) const tokenDetail = { data: { id: user.id, @@ -492,6 +502,17 @@ module.exports = class AccountHelper { user.email = plaintextEmailId const result = { access_token: accessToken, refresh_token: refreshToken, user } + // update user-sessions with refresh token and access token + // update filter + // const updatedUserSession = await userSessions.createUserSession( + // user.id, // userid + // "", // refresh token + // "", // Access token + // deviceInformation + // ) + + // save data in redis against session_id, write a function for this + return responses.successResponse({ statusCode: httpStatusCode.ok, message: 'LOGGED_IN_SUCCESSFULLY', diff --git a/src/services/user-sessions.js b/src/services/user-sessions.js new file mode 100644 index 000000000..d0900168e --- /dev/null +++ b/src/services/user-sessions.js @@ -0,0 +1,62 @@ +/** + * name : services/user-sessions.js + * author : Vishnu + * created-date : 26-Mar-2024 + * Description : user-sessions business logic. + */ + +// Dependencies +const userSessionsQueries = require('@database/queries/user-sessions') +const httpStatusCode = require('@generics/http-status') +const responses = require('@helpers/responses') + +// create user-session +module.exports = class UserSessionsHelper { + static async createUserSession(userId, refreshToken = '', accessToken = '', deviceInfo) { + try { + const userSessionDetails = { + user_id: userId, + device_info: deviceInfo, + started_at: Date.now(), + } + if (accessToken !== '') { + userSessionDetails.token = accessToken + } + if (accessToken !== '') { + userSessionDetails.refresh_token = refreshToken + } + console.log('user sessions details : ', userSessionDetails) + // create userSession + const userSession = await userSessionsQueries.create(userSessionDetails) + + console.log('userSessions : ', userSession) + + return responses.successResponse({ + statusCode: httpStatusCode.created, + message: 'USER_SESSION_CREATED_SUCCESSFULLY', + result: userSession, + }) + } catch (error) { + console.log(error) + throw error + } + } + + static async updateUserSession(filter, update, options = {}) { + try { + const result = await userSessionsQueries.update(filter, update, options) + return responses.successResponse({ + statusCode: httpStatusCode.ok, + message: 'USER_SESSION_UPDATED_SUCCESSFULLY', + result: result, + }) + } catch (error) { + console.log(error) + throw error + } + } +} + +// update-user session +// add entry to redis +// update user session entry in redis From b11acb2165ac3cffccd4e2784a91e06e157e5712 Mon Sep 17 00:00:00 2001 From: vishnu Date: Thu, 28 Mar 2024 14:56:20 +0530 Subject: [PATCH 03/10] changes pushed for internal api testing. code clean up and optimisations pending --- src/.env.sample | 3 + src/constants/common.js | 1 + src/controllers/v1/account.js | 45 +++++- ...240326110128-create-user-sessions-table.js | 17 ++- ...240328084048-update-session-permissions.js | 48 ++++++ ...28084246-update-session-role-permission.js | 139 ++++++++++++++++++ src/database/models/user-sessions.js | 6 +- src/database/queries/user-sessions.js | 2 +- src/envVariables.js | 5 + src/generics/utils.js | 24 +++ src/locales/en.json | 5 +- src/middlewares/authenticator.js | 15 ++ src/services/account.js | 135 +++++++++++++++-- src/services/user-sessions.js | 134 ++++++++++++++++- 14 files changed, 552 insertions(+), 27 deletions(-) create mode 100644 src/database/migrations/20240328084048-update-session-permissions.js create mode 100644 src/database/migrations/20240328084246-update-session-role-permission.js diff --git a/src/.env.sample b/src/.env.sample index fcb9b60bc..5ab60d59f 100644 --- a/src/.env.sample +++ b/src/.env.sample @@ -176,3 +176,6 @@ DOWNLOAD_URL_EXPIRATION_DURATION = 120000 #database url DATABASE_URL=postgres://postgres:postgres@localhost:5432/elevate-user +#allowed idle time +ALLOWED_IDLE_TIME=300000 + diff --git a/src/constants/common.js b/src/constants/common.js index ce9011075..7925c3ee3 100644 --- a/src/constants/common.js +++ b/src/constants/common.js @@ -59,6 +59,7 @@ module.exports = { roleAssociationName: 'user_roles', ACTIVE_STATUS: 'ACTIVE', INACTIVE_STATUS: 'INACTIVE', + EXPIRED_STATUS: 'EXPIRED', MENTOR_ROLE: 'mentor', MENTEE_ROLE: 'mentee', SESSION_MANAGER_ROLE: 'session_manager', diff --git a/src/controllers/v1/account.js b/src/controllers/v1/account.js index 5b0f36dd7..8f23a3a9f 100644 --- a/src/controllers/v1/account.js +++ b/src/controllers/v1/account.js @@ -7,6 +7,7 @@ // Dependencies const accountService = require('@services/account') +const userSessionsService = require('@services/user-sessions') module.exports = class Account { /** @@ -24,8 +25,9 @@ module.exports = class Account { async create(req) { const params = req.body + const device_info = req.headers && req.headers['device-info'] ? req.headers['device-info'] : {} try { - const createdAccount = await accountService.create(params) + const createdAccount = await accountService.create(params, device_info) return createdAccount } catch (error) { return error @@ -129,7 +131,8 @@ module.exports = class Account { async resetPassword(req) { const params = req.body try { - const result = await accountService.resetPassword(params) + const deviceInfo = req.headers && req.headers['device-info'] ? req.headers['device-info'] : {} + const result = await accountService.resetPassword(params, deviceInfo) return result } catch (error) { return error @@ -267,4 +270,42 @@ module.exports = class Account { return error } } + + /** + * Retrieve user sessions based on the request parameters. + * @param {Object} req - The request object containing query parameters and decoded token. + * @returns {Promise} - A promise that resolves to the user session details. + */ + + async sessions(req) { + try { + const filter = req.query && req.query.status ? req.query.status.toUpperCase() : '' + const userSessionDetails = await userSessionsService.list( + req.decodedToken.id, + filter, + req.pageSize, + req.pageNo + ) + return userSessionDetails + } catch (error) { + return error + } + } + + /** + * Validate a user session based on the provided token. + * @param {Object} req - The request object containing the token in the request body. + * @param {string} req.body.token - The token to validate the user session. + * @returns {Promise} - A promise that resolves to the validation result of the user session. + */ + + async validateUserSession(req) { + try { + const token = req.body.token + const validateUserSession = await userSessionsService.validateUserSession(token) + return validateUserSession + } catch (error) { + return error + } + } } diff --git a/src/database/migrations/20240326110128-create-user-sessions-table.js b/src/database/migrations/20240326110128-create-user-sessions-table.js index dbe955923..7e0762776 100644 --- a/src/database/migrations/20240326110128-create-user-sessions-table.js +++ b/src/database/migrations/20240326110128-create-user-sessions-table.js @@ -15,7 +15,7 @@ module.exports = { allowNull: false, }, started_at: { - type: Sequelize.DATE, + type: Sequelize.BIGINT, allowNull: false, }, ended_at: { @@ -23,7 +23,7 @@ module.exports = { allowNull: true, }, token: { - type: Sequelize.STRING, + type: Sequelize.TEXT, allowNull: true, }, device_info: { @@ -31,9 +31,20 @@ module.exports = { allowNull: true, }, refresh_token: { - type: Sequelize.STRING, + type: Sequelize.TEXT, allowNull: true, }, + created_at: { + allowNull: false, + type: Sequelize.DATE, + }, + updated_at: { + allowNull: false, + type: Sequelize.DATE, + }, + deleted_at: { + type: Sequelize.DATE, + }, }) }, diff --git a/src/database/migrations/20240328084048-update-session-permissions.js b/src/database/migrations/20240328084048-update-session-permissions.js new file mode 100644 index 000000000..a6f3df195 --- /dev/null +++ b/src/database/migrations/20240328084048-update-session-permissions.js @@ -0,0 +1,48 @@ +'use strict' + +/** @type {import('sequelize-cli').Migration} */ +module.exports = { + async up(queryInterface, Sequelize) { + try { + const permissionsData = [ + { + code: 'get_user_sessions', + module: 'account', + request_type: ['GET'], + api_path: '/user/v1/account/sessions', + status: 'ACTIVE', + }, + { + code: 'validate_user_sessions', + module: 'account', + request_type: ['POST'], + api_path: '/user/v1/account/validateUserSession', + 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/20240328084246-update-session-role-permission.js b/src/database/migrations/20240328084246-update-session-role-permission.js new file mode 100644 index 000000000..f86db8dd8 --- /dev/null +++ b/src/database/migrations/20240328084246-update-session-role-permission.js @@ -0,0 +1,139 @@ +'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', ['GET'], '/user/v1/account/sessions'), + module: 'account', + request_type: ['GET'], + api_path: '/user/v1/account/sessions', + }, + { + role_title: common.ORG_ADMIN_ROLE, + permission_id: await getPermissionId('account', ['GET'], '/user/v1/account/sessions'), + module: 'account', + request_type: ['GET'], + api_path: '/user/v1/account/sessions', + }, + { + role_title: common.USER_ROLE, + permission_id: await getPermissionId('account', ['GET'], '/user/v1/account/sessions'), + module: 'account', + request_type: ['GET'], + api_path: '/user/v1/account/sessions', + }, + { + role_title: common.ADMIN_ROLE, + permission_id: await getPermissionId('account', ['GET'], '/user/v1/account/sessions'), + module: 'account', + request_type: ['GET'], + api_path: '/user/v1/account/sessions', + }, + { + role_title: common.SESSION_MANAGER_ROLE, + permission_id: await getPermissionId('account', ['GET'], '/user/v1/account/sessions'), + module: 'account', + request_type: ['GET'], + api_path: '/user/v1/account/sessions', + }, + { + role_title: common.MENTEE_ROLE, + permission_id: await getPermissionId('account', ['GET'], '/user/v1/account/sessions'), + module: 'account', + request_type: ['GET'], + api_path: '/user/v1/account/sessions', + }, + + { + role_title: common.MENTOR_ROLE, + permission_id: await getPermissionId('account', ['POST'], '/user/v1/account/validateUserSession'), + module: 'account', + request_type: ['POST'], + api_path: '/user/v1/account/validateUserSession', + }, + { + role_title: common.MENTEE_ROLE, + permission_id: await getPermissionId('account', ['POST'], '/user/v1/account/validateUserSession'), + module: 'account', + request_type: ['POST'], + api_path: '/user/v1/account/validateUserSession', + }, + { + role_title: common.ORG_ADMIN_ROLE, + permission_id: await getPermissionId('account', ['POST'], '/user/v1/account/validateUserSession'), + module: 'account', + request_type: ['POST'], + api_path: '/user/v1/account/validateUserSession', + }, + { + role_title: common.USER_ROLE, + permission_id: await getPermissionId('account', ['POST'], '/user/v1/account/validateUserSession'), + module: 'account', + request_type: ['POST'], + api_path: '/user/v1/account/validateUserSession', + }, + { + role_title: common.ADMIN_ROLE, + permission_id: await getPermissionId('account', ['POST'], '/user/v1/account/validateUserSession'), + module: 'account', + request_type: ['POST'], + api_path: '/user/v1/account/validateUserSession', + }, + { + role_title: common.SESSION_MANAGER_ROLE, + permission_id: await getPermissionId('account', ['POST'], '/user/v1/account/validateUserSession'), + module: 'account', + request_type: ['POST'], + api_path: '/user/v1/account/validateUserSession', + }, + ]) + + 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/database/models/user-sessions.js b/src/database/models/user-sessions.js index b70ce7e43..50f6e88bb 100644 --- a/src/database/models/user-sessions.js +++ b/src/database/models/user-sessions.js @@ -14,7 +14,7 @@ module.exports = (sequelize, DataTypes) => { allowNull: false, }, started_at: { - type: DataTypes.DATE, + type: DataTypes.BIGINT, allowNull: false, }, ended_at: { @@ -22,7 +22,7 @@ module.exports = (sequelize, DataTypes) => { allowNull: true, }, token: { - type: DataTypes.STRING, + type: DataTypes.TEXT, allowNull: true, }, device_info: { @@ -30,7 +30,7 @@ module.exports = (sequelize, DataTypes) => { allowNull: true, }, refresh_token: { - type: DataTypes.STRING, + type: DataTypes.TEXT, allowNull: true, }, }, diff --git a/src/database/queries/user-sessions.js b/src/database/queries/user-sessions.js index 599d60819..37abb8762 100644 --- a/src/database/queries/user-sessions.js +++ b/src/database/queries/user-sessions.js @@ -68,7 +68,7 @@ exports.findAll = async (filter, options = {}) => { */ exports.update = async (filter, update, options = {}) => { try { - return await await Session.update(update, { + return await await UserSessions.update(update, { where: filter, ...options, }) diff --git a/src/envVariables.js b/src/envVariables.js index 941d068eb..7a8341d85 100644 --- a/src/envVariables.js +++ b/src/envVariables.js @@ -256,6 +256,11 @@ let enviromentVariables = { optional: true, default: 3600000, }, + ALLOWED_IDLE_TIME: { + message: 'Require allowed idle time', + optional: true, + default: 3600, + }, } let success = true diff --git a/src/generics/utils.js b/src/generics/utils.js index 2e45d706f..843f85218 100644 --- a/src/generics/utils.js +++ b/src/generics/utils.js @@ -434,6 +434,29 @@ const getRoleTitlesFromId = (roleIds = [], roleList = []) => { }) } +const convertDurationToSeconds = (duration) => { + const timeUnits = { + s: 1, + m: 60, + h: 3600, + d: 86400, + } + + const match = /^(\d*\.?\d*)([smhd])$/.exec(duration) + if (!match) { + throw new Error('Invalid duration format') + } + + const value = parseFloat(match[1]) + const unit = match[2] + + if (!(unit in timeUnits)) { + throw new Error('Invalid duration unit') + } + + return value * timeUnits[unit] +} + module.exports = { generateToken, hashPassword, @@ -464,4 +487,5 @@ module.exports = { isValidName, generateWhereClause, getRoleTitlesFromId, + convertDurationToSeconds, } diff --git a/src/locales/en.json b/src/locales/en.json index 59bb49d8e..913f85808 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -118,5 +118,8 @@ "INCORRECT_OLD_PASSWORD": "Invalid old password", "SAME_PASSWORD_ERROR": "New password cannot be same as old password", "PASSWORD_CHANGED_SUCCESSFULLY": "Password changed successfully.", - "USER_SESSION_CREATED_SUCCESSFULLY": "User session created successfully" + "USER_SESSION_CREATED_SUCCESSFULLY": "User session created successfully", + "USER_SESSION_UPDATED_CESSFULLY": "User session updated successfully", + "USER_SESSION_VALIDATED_SUCCESSFULLY": "User session validated successfully", + "USER_SESSION_FETCHED_SUCCESSFULLY": "User sessions fetched successfully" } diff --git a/src/middlewares/authenticator.js b/src/middlewares/authenticator.js index caed59bad..ce1b63364 100644 --- a/src/middlewares/authenticator.js +++ b/src/middlewares/authenticator.js @@ -14,6 +14,7 @@ const roleQueries = require('@database/queries/user-role') const rolePermissionMappingQueries = require('@database/queries/role-permission-mapping') const { Op } = require('sequelize') const responses = require('@helpers/responses') +const utilsHelper = require('@generics/utils') async function checkPermissions(roleTitle, requestPath, requestMethod) { const parts = requestPath.match(/[^/]+/g) @@ -95,6 +96,20 @@ module.exports = async function (req, res, next) { try { decodedToken = jwt.verify(authHeaderArray[1], process.env.ACCESS_TOKEN_SECRET) + // Get redis key for session + const sessionId = decodedToken.data.session_id.toString() + // Get data from redis + const redisData = await utilsHelper.redisGet(sessionId) + + // If data is not in redis, token is invalid + if (!redisData || redisData.accessToken !== authHeaderArray[1]) { + throw unAuthorizedResponse + } + + // Renew the TTL if allowed idle time is greater than zero + if (process.env.ALLOWED_IDLE_TIME != null) { + await utilsHelper.redisSet(sessionId, redisData, process.env.ALLOWED_IDLE_TIME) + } } catch (err) { if (err.name === 'TokenExpiredError') { throw responses.failureResponse({ diff --git a/src/services/account.js b/src/services/account.js index aaf9f18b6..ae3cbd4f9 100644 --- a/src/services/account.js +++ b/src/services/account.js @@ -43,7 +43,7 @@ module.exports = class AccountHelper { * @returns {JSON} - returns account creation details. */ - static async create(bodyData) { + static async create(bodyData, deviceInfo) { const projection = ['password', 'refresh_tokens'] try { @@ -229,10 +229,19 @@ module.exports = class AccountHelper { } ) + // create user session entry and add session_id to token data + const userSessionDetails = await userSessions.createUserSession( + user.id, // userid + '', // refresh token + '', // Access token + deviceInfo + ) + const tokenDetail = { data: { id: user.id, name: user.name, + session_id: userSessionDetails.result.id, organization_id: user.organization_id, roles: roleData, }, @@ -262,12 +271,15 @@ module.exports = class AccountHelper { process.env.ACCESS_TOKEN_SECRET, common.accessTokenExpiry ) + const refreshToken = utilsHelper.generateToken( tokenDetail, process.env.REFRESH_TOKEN_SECRET, common.refreshTokenExpiry ) + await this.updateUserSessionAndsetRedisData(userSessionDetails.result.id, accessToken, refreshToken) + let refresh_token = new Array() refresh_token.push({ token: refreshToken, @@ -365,6 +377,7 @@ module.exports = class AccountHelper { static async login(bodyData, deviceInformation) { try { + console.log('Yeaahhhhh +++= :', common.accessTokenExpiry) const plaintextEmailId = bodyData.email.toLowerCase() const encryptedEmailId = emailEncryption.encrypt(plaintextEmailId) const userCredentials = await UserCredentialQueries.findOne({ @@ -428,11 +441,12 @@ module.exports = class AccountHelper { '', // Access token deviceInformation ) - console.log('userSessionDetails : ', userSessionDetails) + console.log('userSessionDetails ****************: ', userSessionDetails.result.id) const tokenDetail = { data: { id: user.id, name: user.name, + session_id: userSessionDetails.result.id, organization_id: user.organization_id, roles: roles, }, @@ -502,16 +516,7 @@ module.exports = class AccountHelper { user.email = plaintextEmailId const result = { access_token: accessToken, refresh_token: refreshToken, user } - // update user-sessions with refresh token and access token - // update filter - // const updatedUserSession = await userSessions.createUserSession( - // user.id, // userid - // "", // refresh token - // "", // Access token - // deviceInformation - // ) - - // save data in redis against session_id, write a function for this + await this.updateUserSessionAndsetRedisData(userSessionDetails.result.id, accessToken, refreshToken) return responses.successResponse({ statusCode: httpStatusCode.ok, @@ -625,6 +630,23 @@ module.exports = class AccountHelper { }) } + /* check if redis data is present*/ + // Get redis key for session + console.log(' decoed token data ><<<<<<>>>: ', decodedToken) + const sessionId = decodedToken.data.session_id.toString() + // Get data from redis + let redisData = await utilsHelper.redisGet(sessionId) + console.log('check+++hhhhhhhhh++++++137redisData :', redisData, sessionId) + + // If data is not in redis, token is invalid + if (!redisData || redisData.refreshToken !== bodyData.refresh_token) { + return responses.failureResponse({ + message: 'REFRESH_TOKEN_NOT_FOUND', + statusCode: httpStatusCode.unauthorized, + responseCode: 'CLIENT_ERROR', + }) + } + /* Generate new access token */ const accessToken = utilsHelper.generateToken( { data: decodedToken.data }, @@ -632,6 +654,28 @@ module.exports = class AccountHelper { common.accessTokenExpiry ) + let expiryTime = process.env.ALLOWED_IDLE_TIME + if (process.env.ALLOWED_IDLE_TIME == null) { + console.log('inside if..............') + expiryTime = utilsHelper.convertDurationToSeconds(common.accessTokenExpiry) + } + console.log('++++++++++++############ : ', expiryTime, process.env.ALLOWED_IDLE_TIME) + redisData.accessToken = accessToken + const res = await utilsHelper.redisSet(sessionId, redisData, expiryTime) + console.log('response redis set ::: ', res) + + // update user-sessions with access token + let check = await userSessions.updateUserSession( + { + id: decodedToken.data.id, + }, + { + token: accessToken, + } + ) + + console.log('wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww : ', check) + return responses.successResponse({ statusCode: httpStatusCode.ok, message: 'ACCESS_TOKEN_GENERATED_SUCCESSFULLY', @@ -806,7 +850,7 @@ module.exports = class AccountHelper { * @returns {JSON} - returns password reset response */ - static async resetPassword(bodyData) { + static async resetPassword(bodyData, deviceInfo) { const projection = ['location'] try { const plaintextEmailId = bodyData.email.toLowerCase() @@ -866,17 +910,30 @@ module.exports = class AccountHelper { }) } bodyData.password = utilsHelper.hashPassword(bodyData.password) + + // create user session entry and add session_id to token data + const userSessionDetails = await userSessions.createUserSession( + user.id, // userid + '', // refresh token + '', // Access token + deviceInfo + ) const tokenDetail = { data: { id: user.id, name: user.name, + session_id: userSessionDetails.result.id, organization_id: user.organization_id, roles, }, } - const accessToken = utilsHelper.generateToken(tokenDetail, process.env.ACCESS_TOKEN_SECRET, '1d') - const refreshToken = utilsHelper.generateToken(tokenDetail, process.env.REFRESH_TOKEN_SECRET, '183d') + const accessToken = (tokenDetail, process.env.ACCESS_TOKEN_SECRET, common.accessTokenExpiry) + const refreshToken = utilsHelper.generateToken( + tokenDetail, + process.env.REFRESH_TOKEN_SECRET, + common.refreshTokenExpiry + ) let currentToken = { token: refreshToken, @@ -938,6 +995,9 @@ module.exports = class AccountHelper { } user.email = plaintextEmailId + /**update a new session entry with redis insert */ + await this.updateUserSessionAndsetRedisData(userSessionDetails.result.id, accessToken, refreshToken) + const result = { access_token: accessToken, refresh_token: refreshToken, user } return responses.successResponse({ statusCode: httpStatusCode.ok, @@ -1371,4 +1431,49 @@ module.exports = class AccountHelper { throw error } } + + static async updateUserSessionAndsetRedisData(userSessionId, accessToken, refreshToken) { + try { + console.log(' ggggggggggggggggggggggggggggggggggggggggggg : ', userSessionId, accessToken, refreshToken) + // update user-sessions with refresh token and access token + let check = await userSessions.updateUserSession( + { + id: userSessionId, + }, + { + token: accessToken, + refresh_token: refreshToken, + } + ) + console.log('checkkkkkkkkkkkkkkkkkkkk+++++++++', check) + + // save data in redis against session_id, write a function for this + const redisData = { + accessToken: accessToken, + refreshToken: refreshToken, + } + /** Allowed idle time set to zero (infinity indicator here) + * set TTL of redis to accessTokenExpiry. + * Else it will be there in redis permenantly and will affect listing of user sessions + */ + + let expiryTime = process.env.ALLOWED_IDLE_TIME + if (process.env.ALLOWED_IDLE_TIME == null) { + console.log('inside if..............') + expiryTime = utilsHelper.convertDurationToSeconds(common.accessTokenExpiry) + } + console.log('++++++++++++############ : ', expiryTime, process.env.ALLOWED_IDLE_TIME) + const redisKey = userSessionId.toString() + const res = await utilsHelper.redisSet(redisKey, redisData, expiryTime) + + const result = {} + return responses.successResponse({ + statusCode: httpStatusCode.ok, + message: 'USER_SESSION_UPDATED_CESSFULLY', + result, + }) + } catch (error) { + throw error + } + } } diff --git a/src/services/user-sessions.js b/src/services/user-sessions.js index d0900168e..71607d307 100644 --- a/src/services/user-sessions.js +++ b/src/services/user-sessions.js @@ -7,8 +7,11 @@ // Dependencies const userSessionsQueries = require('@database/queries/user-sessions') +const utilsHelper = require('@generics/utils') const httpStatusCode = require('@generics/http-status') const responses = require('@helpers/responses') +const common = require('@constants/common') +const jwt = require('jsonwebtoken') // create user-session module.exports = class UserSessionsHelper { @@ -17,7 +20,7 @@ module.exports = class UserSessionsHelper { const userSessionDetails = { user_id: userId, device_info: deviceInfo, - started_at: Date.now(), + started_at: Math.floor(new Date().getTime() / 1000), } if (accessToken !== '') { userSessionDetails.token = accessToken @@ -25,7 +28,7 @@ module.exports = class UserSessionsHelper { if (accessToken !== '') { userSessionDetails.refresh_token = refreshToken } - console.log('user sessions details : ', userSessionDetails) + console.log('user sessions details >>>>>>>>>>>>>>>>>>>>>>>%%%%%%%%%%%%%: ', userSessionDetails) // create userSession const userSession = await userSessionsQueries.create(userSessionDetails) @@ -55,6 +58,133 @@ module.exports = class UserSessionsHelper { throw error } } + + /** + * Retrieve user sessions based on user ID, status, limit, and page. + * @param {number} userId - The ID of the user. + * @param {string} status - The status of the user sessions (e.g., 'ACTIVE', ''). + * @param {number} limit - The maximum number of user sessions to retrieve per page. + * @param {number} page - The page number for pagination. + * @returns {Promise} - A promise that resolves to the user session details. + */ + + static async list(userId, status, limit, page) { + try { + const filter = { + user_id: userId, + } + const offset = (page - 1) * limit + + // If ended at is null, the status can be active. after verification with redis we can confirm + if (status === common.ACTIVE_STATUS) { + filter.ended_at = null + } + + // create userSession + const userSessions = await userSessionsQueries.findAll(filter) + const activeSessions = [] + const inActiveSessions = [] + for (const session of userSessions) { + const id = session.id.toString() // Convert ID to string + const redisData = await utilsHelper.redisGet(id) + let statusToSend = status + if (redisData === null) { + if (status === common.ACTIVE_STATUS) { + continue // Skip this element if data is not in Redis and status is active + } else { + statusToSend = common.INACTIVE_STATUS + } + } else { + statusToSend = common.ACTIVE_STATUS + } + + if (status === common.ACTIVE_STATUS && statusToSend === common.ACTIVE_STATUS) { + const responseObj = { + id: session.id, + device_info: session.device_info, + status: statusToSend, + login_time: session.started_at, + logout_time: session.ended_at, + } + activeSessions.push(responseObj) + } else if (status === '') { + const responseObj = { + id: session.id, + device_info: session.device_info, + status: statusToSend, + login_time: session.started_at, + logout_time: session.ended_at, + } + responseObj.status === common.ACTIVE_STATUS + ? activeSessions.push(responseObj) + : inActiveSessions.push(responseObj) + } + } + console.log('activeSessions : ', activeSessions) + console.log('inActiveSessions : ', inActiveSessions) + + const result = [...activeSessions, ...inActiveSessions] + + // Paginate the result array + const paginatedResult = result.slice(offset, offset + limit) + + return responses.successResponse({ + statusCode: httpStatusCode.created, + message: 'USER_SESSION_FETCHED_SUCCESSFULLY', + result: { + data: paginatedResult, + count: result.length, + }, + }) + } catch (error) { + console.log(error) + throw error + } + } + + static async validateUserSession(token) { + // token validation failure message + const unAuthorizedResponse = responses.failureResponse({ + message: 'UNAUTHORIZED_REQUEST', + statusCode: httpStatusCode.unauthorized, + responseCode: 'UNAUTHORIZED', + }) + + const tokenArray = token.split(' ') + + // If not bearer throw error + if (tokenArray[0] !== 'bearer') { + throw unAuthorizedResponse + } + try { + const decodedToken = jwt.verify(tokenArray[1], process.env.ACCESS_TOKEN_SECRET) + const sessionId = decodedToken.data.session_id.toString() + + const redisData = await utilsHelper.redisGet(sessionId) + + // If data is not in redis, token is invalid + if (!redisData || redisData.accessToken !== tokenArray[1]) { + throw unAuthorizedResponse + } + + // Renew the TTL if allowed idle is not infinite + if (process.env.ALLOWED_IDLE_TIME == null) { + await utilsHelper.redisSet(sessionId, redisData, process.env.ALLOWED_IDLE_TIME) + } + + return responses.successResponse({ + statusCode: httpStatusCode.ok, + message: 'USER_SESSION_VALIDATED_SUCCESSFULLY', + result: { + data: { + user_session_active: true, + }, + }, + }) + } catch (err) { + throw unAuthorizedResponse + } + } } // update-user session From 8114f1c02d214abc52f8ab69393e5c9c9581dbdf Mon Sep 17 00:00:00 2001 From: vishnu Date: Thu, 28 Mar 2024 15:25:56 +0530 Subject: [PATCH 04/10] local merge check --- src/services/account.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/services/account.js b/src/services/account.js index ae3cbd4f9..35622856c 100644 --- a/src/services/account.js +++ b/src/services/account.js @@ -634,6 +634,7 @@ module.exports = class AccountHelper { // Get redis key for session console.log(' decoed token data ><<<<<<>>>: ', decodedToken) const sessionId = decodedToken.data.session_id.toString() + // Get data from redis let redisData = await utilsHelper.redisGet(sessionId) console.log('check+++hhhhhhhhh++++++137redisData :', redisData, sessionId) From 1d736406a25edef961d0c4a656f2d588a60ee539 Mon Sep 17 00:00:00 2001 From: vishnu Date: Thu, 28 Mar 2024 21:22:36 +0530 Subject: [PATCH 05/10] code clean up before merge --- src/controllers/v1/account.js | 4 +- ...240326110128-create-user-sessions-table.js | 2 +- src/database/models/user-sessions.js | 2 +- src/envVariables.js | 2 +- src/locales/en.json | 3 +- src/services/account.js | 132 ++++++++++++++---- src/services/admin.js | 16 +++ src/services/user-sessions.js | 95 +++++++++++-- 8 files changed, 212 insertions(+), 44 deletions(-) diff --git a/src/controllers/v1/account.js b/src/controllers/v1/account.js index 8f23a3a9f..36069c8d4 100644 --- a/src/controllers/v1/account.js +++ b/src/controllers/v1/account.js @@ -8,7 +8,6 @@ // Dependencies const accountService = require('@services/account') const userSessionsService = require('@services/user-sessions') - module.exports = class Account { /** * create mentee account @@ -72,7 +71,8 @@ module.exports = class Account { const loggedOutAccount = await accountService.logout( req.body, req.decodedToken.id, - req.decodedToken.organization_id + req.decodedToken.organization_id, + req.decodedToken.session_id ) return loggedOutAccount } catch (error) { diff --git a/src/database/migrations/20240326110128-create-user-sessions-table.js b/src/database/migrations/20240326110128-create-user-sessions-table.js index 7e0762776..2a558fff6 100644 --- a/src/database/migrations/20240326110128-create-user-sessions-table.js +++ b/src/database/migrations/20240326110128-create-user-sessions-table.js @@ -19,7 +19,7 @@ module.exports = { allowNull: false, }, ended_at: { - type: Sequelize.DATE, + type: Sequelize.BIGINT, allowNull: true, }, token: { diff --git a/src/database/models/user-sessions.js b/src/database/models/user-sessions.js index 50f6e88bb..ef7d0f957 100644 --- a/src/database/models/user-sessions.js +++ b/src/database/models/user-sessions.js @@ -18,7 +18,7 @@ module.exports = (sequelize, DataTypes) => { allowNull: false, }, ended_at: { - type: DataTypes.DATE, + type: DataTypes.BIGINT, allowNull: true, }, token: { diff --git a/src/envVariables.js b/src/envVariables.js index 7a8341d85..99ad93417 100644 --- a/src/envVariables.js +++ b/src/envVariables.js @@ -259,7 +259,7 @@ let enviromentVariables = { ALLOWED_IDLE_TIME: { message: 'Require allowed idle time', optional: true, - default: 3600, + default: 0, }, } diff --git a/src/locales/en.json b/src/locales/en.json index 913f85808..f2d11be23 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -121,5 +121,6 @@ "USER_SESSION_CREATED_SUCCESSFULLY": "User session created successfully", "USER_SESSION_UPDATED_CESSFULLY": "User session updated successfully", "USER_SESSION_VALIDATED_SUCCESSFULLY": "User session validated successfully", - "USER_SESSION_FETCHED_SUCCESSFULLY": "User sessions fetched successfully" + "USER_SESSION_FETCHED_SUCCESSFULLY": "User sessions fetched successfully", + "USER_SESSIONS_REMOVED_SUCCESSFULLY": "User sesions removed successfully" } diff --git a/src/services/account.js b/src/services/account.js index 35622856c..b3bcb0aff 100644 --- a/src/services/account.js +++ b/src/services/account.js @@ -28,7 +28,7 @@ const { removeDefaultOrgEntityTypes } = require('@generics/utils') const UserCredentialQueries = require('@database/queries/userCredential') const emailEncryption = require('@utils/emailEncryption') const responses = require('@helpers/responses') -const userSessions = require('@services/user-sessions') +const userSessionsService = require('@services/user-sessions') module.exports = class AccountHelper { /** * create account @@ -40,6 +40,7 @@ module.exports = class AccountHelper { * @param {Boolean} bodyData.isAMentor - is a mentor or not . * @param {String} bodyData.email - user email. * @param {String} bodyData.password - user password. + * @param {Object} deviceInfo - Device information * @returns {JSON} - returns account creation details. */ @@ -229,8 +230,11 @@ module.exports = class AccountHelper { } ) - // create user session entry and add session_id to token data - const userSessionDetails = await userSessions.createUserSession( + /** + * create user session entry and add session_id to token data + * Entry should be created first, the session_id has to be added to token creation data + */ + const userSessionDetails = await userSessionsService.createUserSession( user.id, // userid '', // refresh token '', // Access token @@ -278,6 +282,11 @@ module.exports = class AccountHelper { common.refreshTokenExpiry ) + /** + * This function call will do below things + * 1: create redis entry for the session + * 2: update user-session with token and refresh_token + */ await this.updateUserSessionAndsetRedisData(userSessionDetails.result.id, accessToken, refreshToken) let refresh_token = new Array() @@ -377,7 +386,6 @@ module.exports = class AccountHelper { static async login(bodyData, deviceInformation) { try { - console.log('Yeaahhhhh +++= :', common.accessTokenExpiry) const plaintextEmailId = bodyData.email.toLowerCase() const encryptedEmailId = emailEncryption.encrypt(plaintextEmailId) const userCredentials = await UserCredentialQueries.findOne({ @@ -435,13 +443,13 @@ module.exports = class AccountHelper { } // create user session entry and add session_id to token data - const userSessionDetails = await userSessions.createUserSession( + const userSessionDetails = await userSessionsService.createUserSession( user.id, // userid '', // refresh token '', // Access token deviceInformation ) - console.log('userSessionDetails ****************: ', userSessionDetails.result.id) + const tokenDetail = { data: { id: user.id, @@ -516,6 +524,11 @@ module.exports = class AccountHelper { user.email = plaintextEmailId const result = { access_token: accessToken, refresh_token: refreshToken, user } + /** + * This function call will do below things + * 1: create redis entry for the session + * 2: update user-session with token and refresh_token + */ await this.updateUserSessionAndsetRedisData(userSessionDetails.result.id, accessToken, refreshToken) return responses.successResponse({ @@ -540,7 +553,7 @@ module.exports = class AccountHelper { * @returns {JSON} - returns accounts loggedout information. */ - static async logout(bodyData, user_id, organization_id) { + static async logout(bodyData, user_id, organization_id, userSessionId) { try { const user = await userQueries.findOne({ id: user_id, organization_id }) if (!user) { @@ -550,10 +563,34 @@ module.exports = class AccountHelper { responseCode: 'UNAUTHORIZED', }) } + /** + * Aquire user session_id based on the requests + */ + let userSessions = [] + if (bodyData.userSessionIds && bodyData.userSessionIds.length > 0) { + userSessions = bodyData.userSessionIds + } else { + userSessions.push(userSessionId) + } + + let tokenToRemove = [] + if (userSessions.length > 0) { + const userSessionData = await userSessionsService.findUserSession( + { + id: userSessions, + }, + { + attributes: ['refresh_token'], + } + ) + tokenToRemove = userSessionData.map(({ refresh_token }) => refresh_token) + } + + await userSessionsService.removeUserSessions(userSessions) let refreshTokens = user.refresh_tokens ? user.refresh_tokens : [] - refreshTokens = refreshTokens.filter(function (tokenData) { - return tokenData.token !== bodyData.refresh_token + refreshTokens = tokenToRemove.filter(function (tokenData) { + return !bodyData.refresh_token.includes(tokenData.token) }) /* Destroy refresh token for user */ @@ -632,12 +669,31 @@ module.exports = class AccountHelper { /* check if redis data is present*/ // Get redis key for session - console.log(' decoed token data ><<<<<<>>>: ', decodedToken) const sessionId = decodedToken.data.session_id.toString() // Get data from redis - let redisData = await utilsHelper.redisGet(sessionId) - console.log('check+++hhhhhhhhh++++++137redisData :', redisData, sessionId) + let redisData = {} + redisData = await utilsHelper.redisGet(sessionId) + + // if idle time set to infinity then db check should be done + if (!redisData && process.env.ALLOWED_IDLE_TIME == null) { + const userSessionData = await userSessionsService.findUserSession( + { + id: decodedToken.data.session_id, + }, + { + attributes: ['refresh_token'], + } + ) + if (!userSessionData) { + return responses.failureResponse({ + message: 'REFRESH_TOKEN_NOT_FOUND', + statusCode: httpStatusCode.unauthorized, + responseCode: 'CLIENT_ERROR', + }) + } + redisData.refreshToken = userSessionData[0].refresh_token + } // If data is not in redis, token is invalid if (!redisData || redisData.refreshToken !== bodyData.refresh_token) { @@ -655,18 +711,19 @@ module.exports = class AccountHelper { common.accessTokenExpiry ) + /** + * When idle tine is infinity set TTL to access token expiry + * If not redis data won't expire and timeout session will show as active in listing + */ let expiryTime = process.env.ALLOWED_IDLE_TIME if (process.env.ALLOWED_IDLE_TIME == null) { - console.log('inside if..............') expiryTime = utilsHelper.convertDurationToSeconds(common.accessTokenExpiry) } - console.log('++++++++++++############ : ', expiryTime, process.env.ALLOWED_IDLE_TIME) redisData.accessToken = accessToken const res = await utilsHelper.redisSet(sessionId, redisData, expiryTime) - console.log('response redis set ::: ', res) // update user-sessions with access token - let check = await userSessions.updateUserSession( + let check = await userSessionsService.updateUserSession( { id: decodedToken.data.id, }, @@ -674,9 +731,6 @@ module.exports = class AccountHelper { token: accessToken, } ) - - console.log('wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww : ', check) - return responses.successResponse({ statusCode: httpStatusCode.ok, message: 'ACCESS_TOKEN_GENERATED_SUCCESSFULLY', @@ -913,7 +967,7 @@ module.exports = class AccountHelper { bodyData.password = utilsHelper.hashPassword(bodyData.password) // create user session entry and add session_id to token data - const userSessionDetails = await userSessions.createUserSession( + const userSessionDetails = await userSessionsService.createUserSession( user.id, // userid '', // refresh token '', // Access token @@ -997,6 +1051,11 @@ module.exports = class AccountHelper { user.email = plaintextEmailId /**update a new session entry with redis insert */ + /** + * This function call will do below things + * 1: create redis entry for the session + * 2: update user-session with token and refresh_token + */ await this.updateUserSessionAndsetRedisData(userSessionDetails.result.id, accessToken, refreshToken) const result = { access_token: accessToken, refresh_token: refreshToken, user } @@ -1393,7 +1452,7 @@ module.exports = class AccountHelper { } bodyData.newPassword = utilsHelper.hashPassword(bodyData.newPassword) - const updateParams = { password: bodyData.newPassword } + const updateParams = { password: bodyData.newPassword, refresh_tokens: [] } await userQueries.updateUser( { id: user.id, organization_id: userCredentials.organization_id }, @@ -1402,6 +1461,23 @@ module.exports = class AccountHelper { await UserCredentialQueries.updateUser({ email: userCredentials.email }, { password: bodyData.newPassword }) await utilsHelper.redisDel(userCredentials.email) + // Find active sessions of user and remove them + const userSessionData = await userSessionsService.findUserSession( + { + user_id: userId, + ended_at: null, + }, + { + attributes: ['id'], + } + ) + const userSessionIds = userSessionData.map(({ id }) => id) + /** + * 1: Remove redis data + * 2: Update ended_at in user-sessions + */ + await userSessionsService.removeUserSessions(userSessionIds) + const templateData = await notificationTemplateQueries.findOneEmailTemplate( process.env.CHANGE_PASSWORD_TEMPLATE_CODE ) @@ -1433,11 +1509,18 @@ module.exports = class AccountHelper { } } + /** + * Update the user session with access token and refresh token, and set the data in Redis. + * @param {number} userSessionId - The ID of the user session to update. + * @param {string} accessToken - The new access token. + * @param {string} refreshToken - The new refresh token. + * @returns {Promise} - A promise that resolves to a success response after updating the user session and setting data in Redis. + * @throws {Error} - Throws an error if the update operation fails. + */ static async updateUserSessionAndsetRedisData(userSessionId, accessToken, refreshToken) { try { - console.log(' ggggggggggggggggggggggggggggggggggggggggggg : ', userSessionId, accessToken, refreshToken) // update user-sessions with refresh token and access token - let check = await userSessions.updateUserSession( + let check = await userSessionsService.updateUserSession( { id: userSessionId, }, @@ -1446,7 +1529,6 @@ module.exports = class AccountHelper { refresh_token: refreshToken, } ) - console.log('checkkkkkkkkkkkkkkkkkkkk+++++++++', check) // save data in redis against session_id, write a function for this const redisData = { @@ -1460,10 +1542,8 @@ module.exports = class AccountHelper { let expiryTime = process.env.ALLOWED_IDLE_TIME if (process.env.ALLOWED_IDLE_TIME == null) { - console.log('inside if..............') expiryTime = utilsHelper.convertDurationToSeconds(common.accessTokenExpiry) } - console.log('++++++++++++############ : ', expiryTime, process.env.ALLOWED_IDLE_TIME) const redisKey = userSessionId.toString() const res = await utilsHelper.redisSet(redisKey, redisData, expiryTime) diff --git a/src/services/admin.js b/src/services/admin.js index f367d071d..b1046864e 100644 --- a/src/services/admin.js +++ b/src/services/admin.js @@ -20,6 +20,7 @@ const UserCredentialQueries = require('@database/queries/userCredential') const adminService = require('../generics/materializedViews') const emailEncryption = require('@utils/emailEncryption') const responses = require('@helpers/responses') +const userSessionsService = require('@services/user-sessions') module.exports = class AdminHelper { /** @@ -49,6 +50,21 @@ module.exports = class AdminHelper { await utils.redisDel(common.redisUserPrefix + userId.toString()) + /** + * Using userId get his active sessions + */ + const userSessionData = await userSessionsService.findUserSession( + { + user_id: userId, + ended_at: null, + }, + { + attributes: ['id'], + } + ) + const userSessionIds = userSessionData.map(({ id }) => id) + await userSessionsService.removeUserSessions(userSessionIds) + //code for remove user folder from cloud return responses.successResponse({ diff --git a/src/services/user-sessions.js b/src/services/user-sessions.js index 71607d307..38f72d124 100644 --- a/src/services/user-sessions.js +++ b/src/services/user-sessions.js @@ -15,8 +15,21 @@ const jwt = require('jsonwebtoken') // create user-session module.exports = class UserSessionsHelper { + /** + * Create a user session. + * @param {number} userId - The ID of the user. + * @param {string} [refreshToken=''] - Optional. The refresh token associated with the session. + * @param {string} [accessToken=''] - Optional. The access token associated with the session. + * @param {Object} deviceInfo - Information about the device used for the session. + * @returns {Promise} - A promise that resolves to a success response with the created session details. + * @throws {Error} - Throws an error if any issue occurs during the process. + */ + static async createUserSession(userId, refreshToken = '', accessToken = '', deviceInfo) { try { + /** + * data for user-session creation + */ const userSessionDetails = { user_id: userId, device_info: deviceInfo, @@ -28,12 +41,10 @@ module.exports = class UserSessionsHelper { if (accessToken !== '') { userSessionDetails.refresh_token = refreshToken } - console.log('user sessions details >>>>>>>>>>>>>>>>>>>>>>>%%%%%%%%%%%%%: ', userSessionDetails) + // create userSession const userSession = await userSessionsQueries.create(userSessionDetails) - console.log('userSessions : ', userSession) - return responses.successResponse({ statusCode: httpStatusCode.created, message: 'USER_SESSION_CREATED_SUCCESSFULLY', @@ -45,6 +56,15 @@ module.exports = class UserSessionsHelper { } } + /** + * Update a user session. + * @param {Object} filter - The filter criteria to select the user session(s) to update. + * @param {Object} update - The data to be updated for the user session(s). + * @param {Object} [options={}] - Optional. Additional options for the update operation. + * @returns {Promise} - A promise that resolves to a success response with the updated session details. + * @throws {Error} - Throws an error if any issue occurs during the process. + */ + static async updateUserSession(filter, update, options = {}) { try { const result = await userSessionsQueries.update(filter, update, options) @@ -54,7 +74,6 @@ module.exports = class UserSessionsHelper { result: result, }) } catch (error) { - console.log(error) throw error } } @@ -92,7 +111,9 @@ module.exports = class UserSessionsHelper { if (status === common.ACTIVE_STATUS) { continue // Skip this element if data is not in Redis and status is active } else { - statusToSend = common.INACTIVE_STATUS + session.ended_at == null + ? (statusToSend = common.EXPIRED_STATUS) + : (statusToSend = common.INACTIVE_STATUS) } } else { statusToSend = common.ACTIVE_STATUS @@ -120,12 +141,11 @@ module.exports = class UserSessionsHelper { : inActiveSessions.push(responseObj) } } - console.log('activeSessions : ', activeSessions) - console.log('inActiveSessions : ', inActiveSessions) const result = [...activeSessions, ...inActiveSessions] // Paginate the result array + // The response is accumulated from two places. db and redis. So pagination is not possible on the fly const paginatedResult = result.slice(offset, offset + limit) return responses.successResponse({ @@ -137,11 +157,66 @@ module.exports = class UserSessionsHelper { }, }) } catch (error) { - console.log(error) throw error } } + /** + * Remove user sessions from both database and Redis. + * @param {number[]} userSessionIds - An array of user session IDs to be removed. + * @returns {Promise} - A promise that resolves to a success response upon successful removal. + */ + + static async removeUserSessions(userSessionIds) { + try { + // Delete user sessions from Redis + for (const sessionId of userSessionIds) { + await utilsHelper.redisDel(sessionId.toString()) + } + + // Update ended_at of user sessions in the database + const currentTime = Math.floor(Date.now() / 1000) // Current epoch time in seconds + const updateResult = await userSessionsQueries.update({ id: userSessionIds }, { ended_at: currentTime }) + + // Check if the update was successful + if (updateResult instanceof Error) { + throw updateResult // Throw error if update failed + } + + // Return success response + const result = {} + return responses.successResponse({ + statusCode: httpStatusCode.ok, + message: 'USER_SESSIONS_REMOVED_SUCCESSFULLY', + result, + }) + } catch (error) { + throw error + } + } + + /** + * Find user sessions based on the provided filter and options. + * @param {Object} filter - The filter criteria to find user sessions. + * @param {Object} [options={}] - Optional. Additional options for the query. + * @returns {Promise} - A promise that resolves to an array of user session objects. + * @throws {Error} - Throws an error if any issue occurs during the process. + */ + static async findUserSession(filter, options = {}) { + try { + return await userSessionsQueries.findAll(filter, options) + } catch (error) { + throw error + } + } + + /** + * Validate the user session token. + * @param {string} token - The token to validate. + * @returns {Promise} - A promise that resolves to a success response if the token is valid, otherwise throws an error. + * @throws {Error} - Throws an error if the token validation fails. + */ + static async validateUserSession(token) { // token validation failure message const unAuthorizedResponse = responses.failureResponse({ @@ -186,7 +261,3 @@ module.exports = class UserSessionsHelper { } } } - -// update-user session -// add entry to redis -// update user session entry in redis From 741614b75001bef8645be69feefc534291513b64 Mon Sep 17 00:00:00 2001 From: vishnu Date: Thu, 28 Mar 2024 21:31:24 +0530 Subject: [PATCH 06/10] internal url updated --- src/constants/common.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/constants/common.js b/src/constants/common.js index 7925c3ee3..602b33ca9 100644 --- a/src/constants/common.js +++ b/src/constants/common.js @@ -26,6 +26,7 @@ module.exports = { '/user/v1/account/search', '/user/v1/organization/list', '/user/v1/user-role/default', + '/user/v1/account/validateUserSession', ], notificationEmailType: 'email', accessTokenExpiry: process.env.ACCESS_TOKEN_EXPIRY, From a9ae2e75d71aedfe06aa822236c73cea8314f127 Mon Sep 17 00:00:00 2001 From: vishnu Date: Fri, 29 Mar 2024 16:21:02 +0530 Subject: [PATCH 07/10] user table and related api changes --- src/controllers/v1/admin.js | 3 +- ...-token-and-login-time-column-from-users.js | 22 ++ src/database/models/users.js | 2 - src/services/account.js | 193 ++---------------- src/services/admin.js | 35 +++- src/services/user-sessions.js | 49 +++++ 6 files changed, 127 insertions(+), 177 deletions(-) create mode 100644 src/database/migrations/20240329082324-remove-refresh-token-and-login-time-column-from-users.js diff --git a/src/controllers/v1/admin.js b/src/controllers/v1/admin.js index 46f10fea5..73c432403 100644 --- a/src/controllers/v1/admin.js +++ b/src/controllers/v1/admin.js @@ -78,7 +78,8 @@ module.exports = class Admin { async login(req) { try { - const loggedInAccount = await adminService.login(req.body) + const device_info = req.headers && req.headers['device-info'] ? req.headers['device-info'] : {} + const loggedInAccount = await adminService.login(req.body, device_info) return loggedInAccount } catch (error) { return error diff --git a/src/database/migrations/20240329082324-remove-refresh-token-and-login-time-column-from-users.js b/src/database/migrations/20240329082324-remove-refresh-token-and-login-time-column-from-users.js new file mode 100644 index 000000000..c8ed62cf4 --- /dev/null +++ b/src/database/migrations/20240329082324-remove-refresh-token-and-login-time-column-from-users.js @@ -0,0 +1,22 @@ +'use strict' + +module.exports = { + up: async (queryInterface, Sequelize) => { + // Drop the materialized view + await queryInterface.sequelize.query('DROP MATERIALIZED VIEW IF EXISTS m_users') + + // Remove the columns from the users table + await queryInterface.removeColumn('users', 'last_logged_in_at') + await queryInterface.removeColumn('users', 'refresh_tokens') + }, + + down: async (queryInterface, Sequelize) => { + // Add back the columns to the users table + await queryInterface.addColumn('users', 'last_logged_in_at', { + type: Sequelize.DATE, + }) + await queryInterface.addColumn('users', 'refresh_tokens', { + type: Sequelize.ARRAY(Sequelize.JSONB), + }) + }, +} diff --git a/src/database/models/users.js b/src/database/models/users.js index 0f110e0de..aea7ed740 100644 --- a/src/database/models/users.js +++ b/src/database/models/users.js @@ -35,12 +35,10 @@ module.exports = (sequelize, DataTypes) => { defaultValue: 'ACTIVE', }, image: DataTypes.STRING, - last_logged_in_at: DataTypes.DATE, has_accepted_terms_and_conditions: { type: DataTypes.BOOLEAN, defaultValue: false, }, - refresh_tokens: DataTypes.ARRAY(DataTypes.JSONB), languages: DataTypes.ARRAY(DataTypes.STRING), preferred_language: { type: DataTypes.STRING, diff --git a/src/services/account.js b/src/services/account.js index b3bcb0aff..2fab75f0b 100644 --- a/src/services/account.js +++ b/src/services/account.js @@ -45,7 +45,7 @@ module.exports = class AccountHelper { */ static async create(bodyData, deviceInfo) { - const projection = ['password', 'refresh_tokens'] + const projection = ['password'] try { const plaintextEmailId = bodyData.email.toLowerCase() @@ -287,21 +287,11 @@ module.exports = class AccountHelper { * 1: create redis entry for the session * 2: update user-session with token and refresh_token */ - await this.updateUserSessionAndsetRedisData(userSessionDetails.result.id, accessToken, refreshToken) - - let refresh_token = new Array() - refresh_token.push({ - token: refreshToken, - exp: new Date().getTime() + common.refreshTokenExpiryInMs, - userId: user.id, - }) - - const update = { - refresh_tokens: refresh_token, - last_logged_in_at: new Date().getTime(), - } - - await userQueries.updateUser({ id: user.id, organization_id: user.organization_id }, update) + await userSessionsService.updateUserSessionAndsetRedisData( + userSessionDetails.result.id, + accessToken, + refreshToken + ) await utilsHelper.redisDel(encryptedEmailId) //make the user as org admin @@ -471,33 +461,7 @@ module.exports = class AccountHelper { common.refreshTokenExpiry ) - let currentToken = { - token: refreshToken, - exp: new Date().getTime() + common.refreshTokenExpiryInMs, - userId: user.id, - } - - let userTokens = user.refresh_tokens ? user.refresh_tokens : [] - let noOfTokensToKeep = common.refreshTokenLimit - 1 - let refreshTokens = [] - - if (userTokens && userTokens.length >= common.refreshTokenLimit) { - refreshTokens = userTokens.splice(-noOfTokensToKeep) - } else { - refreshTokens = userTokens - } - - refreshTokens.push(currentToken) - - const updateParams = { - refresh_tokens: refreshTokens, - last_logged_in_at: new Date().getTime(), - } - - await userQueries.updateUser({ id: user.id, organization_id: user.organization_id }, updateParams) - delete user.password - delete user.refresh_tokens //Change to let defaultOrg = await organizationQueries.findOne( @@ -529,7 +493,11 @@ module.exports = class AccountHelper { * 1: create redis entry for the session * 2: update user-session with token and refresh_token */ - await this.updateUserSessionAndsetRedisData(userSessionDetails.result.id, accessToken, refreshToken) + await userSessionsService.updateUserSessionAndsetRedisData( + userSessionDetails.result.id, + accessToken, + refreshToken + ) return responses.successResponse({ statusCode: httpStatusCode.ok, @@ -573,41 +541,8 @@ module.exports = class AccountHelper { userSessions.push(userSessionId) } - let tokenToRemove = [] - if (userSessions.length > 0) { - const userSessionData = await userSessionsService.findUserSession( - { - id: userSessions, - }, - { - attributes: ['refresh_token'], - } - ) - tokenToRemove = userSessionData.map(({ refresh_token }) => refresh_token) - } - await userSessionsService.removeUserSessions(userSessions) - let refreshTokens = user.refresh_tokens ? user.refresh_tokens : [] - refreshTokens = tokenToRemove.filter(function (tokenData) { - return !bodyData.refresh_token.includes(tokenData.token) - }) - - /* Destroy refresh token for user */ - const [affectedRows, updatedData] = await userQueries.updateUser( - { id: user.id, organization_id: user.organization_id }, - { refresh_tokens: refreshTokens } - ) - - /* If user doc not updated because of stored token does not matched with bodyData.refreshToken */ - if (affectedRows == 0) { - return responses.failureResponse({ - message: 'INVALID_REFRESH_TOKEN', - statusCode: httpStatusCode.unauthorized, - responseCode: 'UNAUTHORIZED', - }) - } - return responses.successResponse({ statusCode: httpStatusCode.ok, message: 'LOGGED_OUT_SUCCESSFULLY', @@ -649,24 +584,6 @@ module.exports = class AccountHelper { }) } - /* Check valid refresh token stored in db */ - if (!user.refresh_tokens.length) { - return responses.failureResponse({ - message: 'REFRESH_TOKEN_NOT_FOUND', - statusCode: httpStatusCode.unauthorized, - responseCode: 'CLIENT_ERROR', - }) - } - - const token = user.refresh_tokens.find((tokenData) => tokenData.token === bodyData.refresh_token) - if (!token) { - return responses.failureResponse({ - message: 'REFRESH_TOKEN_NOT_FOUND', - statusCode: httpStatusCode.unauthorized, - responseCode: 'CLIENT_ERROR', - }) - } - /* check if redis data is present*/ // Get redis key for session const sessionId = decodedToken.data.session_id.toString() @@ -983,38 +900,17 @@ module.exports = class AccountHelper { }, } - const accessToken = (tokenDetail, process.env.ACCESS_TOKEN_SECRET, common.accessTokenExpiry) + const accessToken = utilsHelper.generateToken( + tokenDetail, + process.env.ACCESS_TOKEN_SECRET, + common.accessTokenExpiry + ) const refreshToken = utilsHelper.generateToken( tokenDetail, process.env.REFRESH_TOKEN_SECRET, common.refreshTokenExpiry ) - let currentToken = { - token: refreshToken, - exp: new Date().getTime() + common.refreshTokenExpiryInMs, - userId: user.id, - } - - let userTokens = user.refresh_tokens ? user.refresh_tokens : [] - let noOfTokensToKeep = common.refreshTokenLimit - 1 - let refreshTokens = [] - - if (userTokens && userTokens.length >= common.refreshTokenLimit) - refreshTokens = userTokens.splice(-noOfTokensToKeep) - else refreshTokens = userTokens - - refreshTokens.push(currentToken) - const updateParams = { - refresh_tokens: refreshTokens, - lastLoggedInAt: new Date().getTime(), - password: bodyData.password, - } - - await userQueries.updateUser( - { id: user.id, organization_id: userCredentials.organization_id }, - updateParams - ) await UserCredentialQueries.updateUser( { email: encryptedEmailId, @@ -1056,7 +952,11 @@ module.exports = class AccountHelper { * 1: create redis entry for the session * 2: update user-session with token and refresh_token */ - await this.updateUserSessionAndsetRedisData(userSessionDetails.result.id, accessToken, refreshToken) + await userSessionsService.updateUserSessionAndsetRedisData( + userSessionDetails.result.id, + accessToken, + refreshToken + ) const result = { access_token: accessToken, refresh_token: refreshToken, user } return responses.successResponse({ @@ -1452,7 +1352,7 @@ module.exports = class AccountHelper { } bodyData.newPassword = utilsHelper.hashPassword(bodyData.newPassword) - const updateParams = { password: bodyData.newPassword, refresh_tokens: [] } + const updateParams = { password: bodyData.newPassword } await userQueries.updateUser( { id: user.id, organization_id: userCredentials.organization_id }, @@ -1508,53 +1408,4 @@ module.exports = class AccountHelper { throw error } } - - /** - * Update the user session with access token and refresh token, and set the data in Redis. - * @param {number} userSessionId - The ID of the user session to update. - * @param {string} accessToken - The new access token. - * @param {string} refreshToken - The new refresh token. - * @returns {Promise} - A promise that resolves to a success response after updating the user session and setting data in Redis. - * @throws {Error} - Throws an error if the update operation fails. - */ - static async updateUserSessionAndsetRedisData(userSessionId, accessToken, refreshToken) { - try { - // update user-sessions with refresh token and access token - let check = await userSessionsService.updateUserSession( - { - id: userSessionId, - }, - { - token: accessToken, - refresh_token: refreshToken, - } - ) - - // save data in redis against session_id, write a function for this - const redisData = { - accessToken: accessToken, - refreshToken: refreshToken, - } - /** Allowed idle time set to zero (infinity indicator here) - * set TTL of redis to accessTokenExpiry. - * Else it will be there in redis permenantly and will affect listing of user sessions - */ - - let expiryTime = process.env.ALLOWED_IDLE_TIME - if (process.env.ALLOWED_IDLE_TIME == null) { - expiryTime = utilsHelper.convertDurationToSeconds(common.accessTokenExpiry) - } - const redisKey = userSessionId.toString() - const res = await utilsHelper.redisSet(redisKey, redisData, expiryTime) - - const result = {} - return responses.successResponse({ - statusCode: httpStatusCode.ok, - message: 'USER_SESSION_UPDATED_CESSFULLY', - result, - }) - } catch (error) { - throw error - } - } } diff --git a/src/services/admin.js b/src/services/admin.js index b1046864e..8f9a07fd1 100644 --- a/src/services/admin.js +++ b/src/services/admin.js @@ -152,9 +152,10 @@ module.exports = class AdminHelper { * @param {Object} bodyData - user login data. * @param {string} bodyData.email - email. * @param {string} bodyData.password - email. + * @param {string} deviceInformation - device information. * @returns {JSON} - returns login response */ - static async login(bodyData) { + static async login(bodyData, deviceInformation) { try { const plaintextEmailId = bodyData.email.toLowerCase() const encryptedEmailId = emailEncryption.encrypt(plaintextEmailId) @@ -203,10 +204,19 @@ module.exports = class AdminHelper { }) } + // create user session entry and add session_id to token data + const userSessionDetails = await userSessionsService.createUserSession( + user.id, // userid + '', // refresh token + '', // Access token + deviceInformation + ) + const tokenDetail = { data: { id: user.id, name: user.name, + session_id: userSessionDetails.result.id, organization_id: user.organization_id, roles: roles, }, @@ -214,8 +224,27 @@ module.exports = class AdminHelper { user.user_roles = roles - const accessToken = utils.generateToken(tokenDetail, process.env.ACCESS_TOKEN_SECRET, '1d') - const refreshToken = utils.generateToken(tokenDetail, process.env.REFRESH_TOKEN_SECRET, '183d') + const accessToken = utils.generateToken( + tokenDetail, + process.env.ACCESS_TOKEN_SECRET, + common.accessTokenExpiry + ) + const refreshToken = utils.generateToken( + tokenDetail, + process.env.REFRESH_TOKEN_SECRET, + common.refreshTokenExpiry + ) + + /** + * This function call will do below things + * 1: create redis entry for the session + * 2: update user-session with token and refresh_token + */ + await userSessionsService.updateUserSessionAndsetRedisData( + userSessionDetails.result.id, + accessToken, + refreshToken + ) delete user.password const result = { access_token: accessToken, refresh_token: refreshToken, user } diff --git a/src/services/user-sessions.js b/src/services/user-sessions.js index 38f72d124..eba027e5b 100644 --- a/src/services/user-sessions.js +++ b/src/services/user-sessions.js @@ -260,4 +260,53 @@ module.exports = class UserSessionsHelper { throw unAuthorizedResponse } } + + /** + * Update the user session with access token and refresh token, and set the data in Redis. + * @param {number} userSessionId - The ID of the user session to update. + * @param {string} accessToken - The new access token. + * @param {string} refreshToken - The new refresh token. + * @returns {Promise} - A promise that resolves to a success response after updating the user session and setting data in Redis. + * @throws {Error} - Throws an error if the update operation fails. + */ + static async updateUserSessionAndsetRedisData(userSessionId, accessToken, refreshToken) { + try { + // update user-sessions with refresh token and access token + await this.updateUserSession( + { + id: userSessionId, + }, + { + token: accessToken, + refresh_token: refreshToken, + } + ) + + // save data in redis against session_id, write a function for this + const redisData = { + accessToken: accessToken, + refreshToken: refreshToken, + } + /** Allowed idle time set to zero (infinity indicator here) + * set TTL of redis to accessTokenExpiry. + * Else it will be there in redis permenantly and will affect listing of user sessions + */ + + let expiryTime = process.env.ALLOWED_IDLE_TIME + if (process.env.ALLOWED_IDLE_TIME == null) { + expiryTime = utilsHelper.convertDurationToSeconds(common.accessTokenExpiry) + } + const redisKey = userSessionId.toString() + await utilsHelper.redisSet(redisKey, redisData, expiryTime) + + const result = {} + return responses.successResponse({ + statusCode: httpStatusCode.ok, + message: 'USER_SESSION_UPDATED_CESSFULLY', + result, + }) + } catch (error) { + throw error + } + } } From 49497e1adafc9a51af9125802fd5ec9aadd03a9f Mon Sep 17 00:00:00 2001 From: vishnu Date: Fri, 29 Mar 2024 17:31:43 +0530 Subject: [PATCH 08/10] configurable active session check added --- src/.env.sample | 3 +++ src/envVariables.js | 5 +++++ src/locales/en.json | 3 ++- src/services/account.js | 11 +++++++++++ src/services/user-sessions.js | 34 ++++++++++++++++++++++++++++++++++ 5 files changed, 55 insertions(+), 1 deletion(-) diff --git a/src/.env.sample b/src/.env.sample index 27d48ca61..88f9ff893 100644 --- a/src/.env.sample +++ b/src/.env.sample @@ -181,3 +181,6 @@ ALLOWED_IDLE_TIME=300000 # Expiry time for the signed urls SIGNED_URL_EXPIRY_IN_MILLISECONDS = 120000 + +# Allowed active sessions +ALLOWED_ACTIVE_SESSIONS = 5 diff --git a/src/envVariables.js b/src/envVariables.js index bd547ff43..a6c24db4d 100644 --- a/src/envVariables.js +++ b/src/envVariables.js @@ -293,6 +293,11 @@ let enviromentVariables = { optional: true, default: 3600000, }, + ALLOWED_ACTIVE_SESSIONS: { + message: 'Require allowed active sessions', + optional: true, + default: 0, + }, } let success = true diff --git a/src/locales/en.json b/src/locales/en.json index e02111d3d..7eda9ffad 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -122,5 +122,6 @@ "USER_SESSION_VALIDATED_SUCCESSFULLY": "User session validated successfully", "USER_SESSION_FETCHED_SUCCESSFULLY": "User sessions fetched successfully", "USER_SESSIONS_REMOVED_SUCCESSFULLY": "User sesions removed successfully", - "PASSWORD_CHANGED_SUCCESSFULLY": "Your password has been changed successfully. Please log-in to continue." + "PASSWORD_CHANGED_SUCCESSFULLY": "Your password has been changed successfully. Please log-in to continue.", + "ACTIVE_SESSION_LIMIT_EXCEEDED": "Sorry! Your allowed active session limi exceeded. Please log-off from other sessions to continue" } diff --git a/src/services/account.js b/src/services/account.js index 2fab75f0b..dc47ac01c 100644 --- a/src/services/account.js +++ b/src/services/account.js @@ -404,6 +404,17 @@ module.exports = class AccountHelper { responseCode: 'CLIENT_ERROR', }) } + // check if login is allowed or not + if (process.env.ALLOWED_ACTIVE_SESSIONS != null) { + const activeSessionCount = await userSessionsService.activeUserSessionCounts(user.id) + if (activeSessionCount >= process.env.ALLOWED_ACTIVE_SESSIONS) { + return responses.failureResponse({ + message: 'ACTIVE_SESSION_LIMIT_EXCEEDED', + statusCode: httpStatusCode.not_acceptable, + responseCode: 'CLIENT_ERROR', + }) + } + } let roles = await roleQueries.findAll( { id: user.roles, status: common.ACTIVE_STATUS }, diff --git a/src/services/user-sessions.js b/src/services/user-sessions.js index eba027e5b..6b227c6ee 100644 --- a/src/services/user-sessions.js +++ b/src/services/user-sessions.js @@ -309,4 +309,38 @@ module.exports = class UserSessionsHelper { throw error } } + + /** + * Retrieve the count of active user sessions for a given userId. + * @param {number} userId - The ID of the user for which to retrieve active sessions. + * @returns {Promise} - A Promise that resolves to the count of active user sessions. + * @throws {Error} - If an error occurs while retrieving the count of active user sessions. + */ + static async activeUserSessionCounts(userId) { + try { + // Define filter criteria + const filterQuery = { + user_id: userId, + ended_at: null, + } + + // Fetch user sessions based on filter criteria + const userSessions = await userSessionsQueries.findAll(filterQuery) + + // Initialize count of active sessions + let activeSession = 0 + + // Loop through user sessions and check if each session exists in Redis + for (const session of userSessions) { + const id = session.id.toString() + const redisData = await utilsHelper.redisGet(id) + if (redisData !== null) { + activeSession++ + } + } + return activeSession + } catch (error) { + throw error + } + } } From 3663260210e6c617523c24257fec046d85e9a301 Mon Sep 17 00:00:00 2001 From: vishnu Date: Sat, 30 Mar 2024 17:58:50 +0530 Subject: [PATCH 09/10] parse device info --- src/services/user-sessions.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/services/user-sessions.js b/src/services/user-sessions.js index 6b227c6ee..d259bb032 100644 --- a/src/services/user-sessions.js +++ b/src/services/user-sessions.js @@ -122,7 +122,7 @@ module.exports = class UserSessionsHelper { if (status === common.ACTIVE_STATUS && statusToSend === common.ACTIVE_STATUS) { const responseObj = { id: session.id, - device_info: session.device_info, + device_info: JSON.parse(session.device_info), status: statusToSend, login_time: session.started_at, logout_time: session.ended_at, @@ -131,7 +131,7 @@ module.exports = class UserSessionsHelper { } else if (status === '') { const responseObj = { id: session.id, - device_info: session.device_info, + device_info: JSON.parse(session.device_info), status: statusToSend, login_time: session.started_at, logout_time: session.ended_at, From ff9266628aabaaca036aa5f58b65edc9118d5b89 Mon Sep 17 00:00:00 2001 From: vishnu Date: Sat, 30 Mar 2024 19:48:23 +0530 Subject: [PATCH 10/10] device info --- src/controllers/v1/account.js | 6 +++--- src/services/user-sessions.js | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/controllers/v1/account.js b/src/controllers/v1/account.js index 36069c8d4..071aca529 100644 --- a/src/controllers/v1/account.js +++ b/src/controllers/v1/account.js @@ -24,7 +24,7 @@ module.exports = class Account { async create(req) { const params = req.body - const device_info = req.headers && req.headers['device-info'] ? req.headers['device-info'] : {} + const device_info = req.headers && req.headers['device-info'] ? JSON.parse(req.headers['device-info']) : {} try { const createdAccount = await accountService.create(params, device_info) return createdAccount @@ -46,7 +46,7 @@ module.exports = class Account { async login(req) { const params = req.body - const device_info = req.headers && req.headers['device-info'] ? req.headers['device-info'] : {} + const device_info = req.headers && req.headers['device-info'] ? JSON.parse(req.headers['device-info']) : {} try { const loggedInAccount = await accountService.login(params, device_info) return loggedInAccount @@ -131,7 +131,7 @@ module.exports = class Account { async resetPassword(req) { const params = req.body try { - const deviceInfo = req.headers && req.headers['device-info'] ? req.headers['device-info'] : {} + const deviceInfo = req.headers && req.headers['device-info'] ? JSON.parse(req.headers['device-info']) : {} const result = await accountService.resetPassword(params, deviceInfo) return result } catch (error) { diff --git a/src/services/user-sessions.js b/src/services/user-sessions.js index d259bb032..6b227c6ee 100644 --- a/src/services/user-sessions.js +++ b/src/services/user-sessions.js @@ -122,7 +122,7 @@ module.exports = class UserSessionsHelper { if (status === common.ACTIVE_STATUS && statusToSend === common.ACTIVE_STATUS) { const responseObj = { id: session.id, - device_info: JSON.parse(session.device_info), + device_info: session.device_info, status: statusToSend, login_time: session.started_at, logout_time: session.ended_at, @@ -131,7 +131,7 @@ module.exports = class UserSessionsHelper { } else if (status === '') { const responseObj = { id: session.id, - device_info: JSON.parse(session.device_info), + device_info: session.device_info, status: statusToSend, login_time: session.started_at, logout_time: session.ended_at,