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/.env.sample b/src/.env.sample index 8c94049f4..88f9ff893 100644 --- a/src/.env.sample +++ b/src/.env.sample @@ -176,5 +176,11 @@ DOWNLOAD_URL_EXPIRATION_DURATION = 120000 #database url DATABASE_URL=postgres://postgres:postgres@localhost:5432/elevate-user +#allowed idle time +ALLOWED_IDLE_TIME=300000 + # Expiry time for the signed urls -SIGNED_URL_EXPIRY_IN_MILLISECONDS = 120000 \ No newline at end of file +SIGNED_URL_EXPIRY_IN_MILLISECONDS = 120000 + +# Allowed active sessions +ALLOWED_ACTIVE_SESSIONS = 5 diff --git a/src/constants/common.js b/src/constants/common.js index 9724f558a..5170a5d68 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, @@ -59,6 +60,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 b5fa81738..071aca529 100644 --- a/src/controllers/v1/account.js +++ b/src/controllers/v1/account.js @@ -7,7 +7,7 @@ // Dependencies const accountService = require('@services/account') - +const userSessionsService = require('@services/user-sessions') module.exports = class Account { /** * create mentee account @@ -24,8 +24,9 @@ module.exports = class Account { async create(req) { const params = req.body + const device_info = req.headers && req.headers['device-info'] ? JSON.parse(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 @@ -45,8 +46,9 @@ module.exports = class Account { async login(req) { const params = req.body + const device_info = req.headers && req.headers['device-info'] ? JSON.parse(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 @@ -69,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) { @@ -128,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'] ? JSON.parse(req.headers['device-info']) : {} + const result = await accountService.resetPassword(params, deviceInfo) return result } catch (error) { return error @@ -266,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<Object>} - 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<Object>} - 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/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/20240326110128-create-user-sessions-table.js b/src/database/migrations/20240326110128-create-user-sessions-table.js new file mode 100644 index 000000000..2a558fff6 --- /dev/null +++ b/src/database/migrations/20240326110128-create-user-sessions-table.js @@ -0,0 +1,54 @@ +'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.BIGINT, + allowNull: false, + }, + ended_at: { + type: Sequelize.BIGINT, + allowNull: true, + }, + token: { + type: Sequelize.TEXT, + allowNull: true, + }, + device_info: { + type: Sequelize.JSONB, + allowNull: true, + }, + refresh_token: { + type: Sequelize.TEXT, + allowNull: true, + }, + created_at: { + allowNull: false, + type: Sequelize.DATE, + }, + updated_at: { + allowNull: false, + type: Sequelize.DATE, + }, + deleted_at: { + type: Sequelize.DATE, + }, + }) + }, + + async down(queryInterface, Sequelize) { + await queryInterface.dropTable('user_sessions') + }, +} 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/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/migrations/20240401123752-make-user-id-primary-key-in-user-sessions-table.js b/src/database/migrations/20240401123752-make-user-id-primary-key-in-user-sessions-table.js new file mode 100644 index 000000000..6786b39a9 --- /dev/null +++ b/src/database/migrations/20240401123752-make-user-id-primary-key-in-user-sessions-table.js @@ -0,0 +1,27 @@ +'use strict' + +module.exports = { + async up(queryInterface, Sequelize) { + // Remove the primary key constraint from 'id' + await queryInterface.removeConstraint('user_sessions', 'user_sessions_pkey') + + // Add a unique constraint for the combination of 'id' and 'user_id' + await queryInterface.addConstraint('user_sessions', { + fields: ['id', 'user_id'], + type: 'primary key', + name: 'user_sessions_pkey', + }) + }, + + async down(queryInterface, Sequelize) { + // Remove the unique constraint for the combination of 'id' and 'user_id' + await queryInterface.removeConstraint('user_sessions', 'user_sessions_pkey') + + // Add back the primary key constraint on 'id' + await queryInterface.addConstraint('user_sessions', { + type: 'primary key', + fields: ['id'], + name: 'user_sessions_pkey', + }) + }, +} diff --git a/src/database/models/user-sessions.js b/src/database/models/user-sessions.js new file mode 100644 index 000000000..7b961e137 --- /dev/null +++ b/src/database/models/user-sessions.js @@ -0,0 +1,47 @@ +'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, + primaryKey: true, + }, + started_at: { + type: DataTypes.BIGINT, + allowNull: false, + }, + ended_at: { + type: DataTypes.BIGINT, + allowNull: true, + }, + token: { + type: DataTypes.TEXT, + allowNull: true, + }, + device_info: { + type: DataTypes.JSONB, + allowNull: true, + }, + refresh_token: { + type: DataTypes.TEXT, + allowNull: true, + }, + }, + { + sequelize, + modelName: 'UserSessions', + tableName: 'user_sessions', + freezeTableName: true, + paranoid: true, + } + ) + return UserSessions +} 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/database/queries/user-sessions.js b/src/database/queries/user-sessions.js new file mode 100644 index 000000000..37abb8762 --- /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<Object>} - 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<Object>} - 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<Array>} - 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<number>} - A promise that resolves to the number of updated records or an error. + */ +exports.update = async (filter, update, options = {}) => { + try { + return await await UserSessions.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<Object>} - 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 + } +} diff --git a/src/envVariables.js b/src/envVariables.js index e7d8410a4..a6c24db4d 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: 0, + }, CHANGE_PASSWORD_TEMPLATE_CODE: { message: 'Required change password email template code', optional: false, @@ -288,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/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 ec5ae542f..7eda9ffad 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -117,5 +117,11 @@ "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": "Your password has been changed successfully. Please log-in to continue." + "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_SESSIONS_REMOVED_SUCCESSFULLY": "User sesions removed successfully", + "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/middlewares/authenticator.js b/src/middlewares/authenticator.js index 5f303c345..c80443f22 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') const { verifyCaptchaToken } = require('@utils/captcha') async function checkPermissions(roleTitle, requestPath, requestMethod) { @@ -116,6 +117,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 d3fd8cc71..ef1d6b84b 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 userSessionsService = require('@services/user-sessions') module.exports = class AccountHelper { /** * create account @@ -39,11 +40,12 @@ 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. */ - static async create(bodyData) { - const projection = ['password', 'refresh_tokens'] + static async create(bodyData, deviceInfo) { + const projection = ['password'] try { const plaintextEmailId = bodyData.email.toLowerCase() @@ -228,10 +230,22 @@ module.exports = class AccountHelper { } ) + /** + * 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 + deviceInfo + ) + const tokenDetail = { data: { id: user.id, name: user.name, + session_id: userSessionDetails.result.id, organization_id: user.organization_id, roles: roleData, }, @@ -261,25 +275,23 @@ module.exports = class AccountHelper { process.env.ACCESS_TOKEN_SECRET, common.accessTokenExpiry ) + const refreshToken = utilsHelper.generateToken( tokenDetail, process.env.REFRESH_TOKEN_SECRET, common.refreshTokenExpiry ) - 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) + /** + * 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 + ) await utilsHelper.redisDel(encryptedEmailId) //make the user as org admin @@ -358,10 +370,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) @@ -391,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 }, @@ -419,10 +443,19 @@ module.exports = class AccountHelper { }) } + // 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, }, @@ -439,33 +472,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( @@ -492,6 +499,17 @@ 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 userSessionsService.updateUserSessionAndsetRedisData( + userSessionDetails.result.id, + accessToken, + refreshToken + ) + return responses.successResponse({ statusCode: httpStatusCode.ok, message: 'LOGGED_IN_SUCCESSFULLY', @@ -514,7 +532,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) { @@ -524,27 +542,18 @@ module.exports = class AccountHelper { responseCode: 'UNAUTHORIZED', }) } - - let refreshTokens = user.refresh_tokens ? user.refresh_tokens : [] - refreshTokens = refreshTokens.filter(function (tokenData) { - return tokenData.token !== bodyData.refresh_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', - }) + /** + * Aquire user session_id based on the requests + */ + let userSessions = [] + if (bodyData.userSessionIds && bodyData.userSessionIds.length > 0) { + userSessions = bodyData.userSessionIds + } else { + userSessions.push(userSessionId) } + await userSessionsService.removeUserSessions(userSessions) + return responses.successResponse({ statusCode: httpStatusCode.ok, message: 'LOGGED_OUT_SUCCESSFULLY', @@ -586,17 +595,36 @@ 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', - }) + /* check if redis data is present*/ + // Get redis key for session + const sessionId = decodedToken.data.session_id.toString() + + // Get data from redis + 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 } - const token = user.refresh_tokens.find((tokenData) => tokenData.token === bodyData.refresh_token) - if (!token) { + // 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, @@ -611,6 +639,26 @@ 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) { + expiryTime = utilsHelper.convertDurationToSeconds(common.accessTokenExpiry) + } + redisData.accessToken = accessToken + const res = await utilsHelper.redisSet(sessionId, redisData, expiryTime) + + // update user-sessions with access token + let check = await userSessionsService.updateUserSession( + { + id: decodedToken.data.id, + }, + { + token: accessToken, + } + ) return responses.successResponse({ statusCode: httpStatusCode.ok, message: 'ACCESS_TOKEN_GENERATED_SUCCESSFULLY', @@ -785,7 +833,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() @@ -845,43 +893,35 @@ module.exports = class AccountHelper { }) } bodyData.password = utilsHelper.hashPassword(bodyData.password) + + // create user session entry and add session_id to token data + const userSessionDetails = await userSessionsService.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') - - 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 + const accessToken = utilsHelper.generateToken( + tokenDetail, + process.env.ACCESS_TOKEN_SECRET, + common.accessTokenExpiry ) + const refreshToken = utilsHelper.generateToken( + tokenDetail, + process.env.REFRESH_TOKEN_SECRET, + common.refreshTokenExpiry + ) + await UserCredentialQueries.updateUser( { email: encryptedEmailId, @@ -917,6 +957,18 @@ 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 userSessionsService.updateUserSessionAndsetRedisData( + userSessionDetails.result.id, + accessToken, + refreshToken + ) + const result = { access_token: accessToken, refresh_token: refreshToken, user } return responses.successResponse({ statusCode: httpStatusCode.ok, @@ -1311,7 +1363,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 }, @@ -1320,6 +1372,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 ) diff --git a/src/services/admin.js b/src/services/admin.js index f367d071d..42a5276d7 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({ @@ -136,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) @@ -187,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, }, @@ -198,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 } @@ -551,11 +596,9 @@ function _removeUserKeys() { 'gender', 'about', 'share_link', - 'last_logged_in_at', 'preferred_language', 'location', 'languages', - 'refresh_tokens', 'image', 'roles', ] diff --git a/src/services/user-sessions.js b/src/services/user-sessions.js new file mode 100644 index 000000000..64b60a058 --- /dev/null +++ b/src/services/user-sessions.js @@ -0,0 +1,346 @@ +/** + * 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 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 { + /** + * 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<Object>} - 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, + started_at: Math.floor(new Date().getTime() / 1000), + } + if (accessToken !== '') { + userSessionDetails.token = accessToken + } + if (accessToken !== '') { + userSessionDetails.refresh_token = refreshToken + } + + // create userSession + const userSession = await userSessionsQueries.create(userSessionDetails) + + return responses.successResponse({ + statusCode: httpStatusCode.created, + message: 'USER_SESSION_CREATED_SUCCESSFULLY', + result: userSession, + }) + } catch (error) { + console.log(error) + throw error + } + } + + /** + * 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<Object>} - 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) + return responses.successResponse({ + statusCode: httpStatusCode.ok, + message: 'USER_SESSION_UPDATED_SUCCESSFULLY', + result: result, + }) + } catch (error) { + 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<Object>} - 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 { + session.ended_at == null + ? (statusToSend = common.EXPIRED_STATUS) + : (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) + } + } + + 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({ + statusCode: httpStatusCode.created, + message: 'USER_SESSION_FETCHED_SUCCESSFULLY', + result: { + data: paginatedResult, + count: result.length, + }, + }) + } catch (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<Object>} - 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<Object[]>} - 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<Object>} - 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({ + 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 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<Object>} - 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 + } + } + + /** + * 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<number>} - 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 + } + } +} diff --git a/src/validators/v1/organization.js b/src/validators/v1/organization.js index 9a0bcf838..dfb6b6f2d 100644 --- a/src/validators/v1/organization.js +++ b/src/validators/v1/organization.js @@ -21,8 +21,7 @@ module.exports = { .trim() .notEmpty() .withMessage('description field is empty') - .not() - .matches(/(\b)(on\S+)(\s*)=|javascript:|<(|\/|[^\/>][^>]+|\/[^>][^>]+)>/gi) + .matches(/^[a-zA-Z0-9\-.,\s]+$/) .withMessage('invalid description') req.checkBody('domains').trim().notEmpty().withMessage('domains field is empty') }, @@ -43,8 +42,7 @@ module.exports = { .trim() .notEmpty() .withMessage('description field is empty') - .not() - .matches(/(\b)(on\S+)(\s*)=|javascript:|<(|\/|[^\/>][^>]+|\/[^>][^>]+)>/gi) + .matches(/^[a-zA-Z0-9\-.,\s]+$/) .withMessage('invalid description') }, diff --git a/src/validators/v1/user.js b/src/validators/v1/user.js index a0ceb2c2f..91c49f40b 100644 --- a/src/validators/v1/user.js +++ b/src/validators/v1/user.js @@ -38,8 +38,7 @@ module.exports = { .trim() .notEmpty() .withMessage('about field is empty') - .not() - .matches(/(\b)(on\S+)(\s*)=|javascript:|<(|\/|[^\/>][^>]+|\/[^>][^>]+)>/gi) + .matches(/^[a-zA-Z0-9\-.,\s]+$/) .withMessage('invalid about') req.checkBody('has_accepted_terms_and_conditions')