diff --git a/src/dtos/eventBody.js b/src/dtos/eventBody.js index 24cf627df..4b3425c2b 100644 --- a/src/dtos/eventBody.js +++ b/src/dtos/eventBody.js @@ -1,28 +1,53 @@ 'use strict' + +/** + * Creates an event body DTO with validated fields + * @param {Object} params - The parameters object + * @param {string} params.entity - The entity name + * @param {string} params.eventType - The type of event + * @param {string|number} params.entityId - The entity identifier + * @param {Array<{fieldName: string, oldValue?: any, newValue?: any}>} params.changedValues - Array of changed fields + * @param {Object} params.args - Additional arguments + * @returns {Object|false} The formatted event body or false if validation fails + */ exports.eventBodyDTO = ({ entity, eventType, entityId, changedValues = [], args = {} }) => { - try { - if (!entity || !eventType || !entityId) - throw new Error('Entity, EventType & EntityId values are mandatory for an Event') - const allowedArgs = ['created_at', 'created_by', 'updated_at', 'updated_by'] - const disallowedArgs = Object.keys(args).filter((arg) => !allowedArgs.includes(arg)) - if (disallowedArgs.length > 0) - throw new Error(`Event Args contain disallowed keys: ${disallowedArgs.join(', ')}`) - const changes = changedValues.reduce((result, obj) => { - const { fieldName, oldValue, newValue } = obj - if (!result[fieldName]) result[fieldName] = {} - if (oldValue) result[fieldName].oldValue = oldValue - if (newValue) result[fieldName].newValue = newValue - return result - }, {}) - return { - entity, - eventType, - entityId, - changes, - ...args, - } - } catch (error) { - console.error(error) - return false - } + try { + // Validate required fields + if (!entity || typeof entity !== 'string') throw new Error('Entity must be a valid string') + if (!eventType || typeof eventType !== 'string') throw new Error('EventType must be a valid string') + if (!entityId) throw new Error('EntityId is required') + + // Validate changedValues array structure + if (!Array.isArray(changedValues)) throw new Error('ChangedValues must be an array') + changedValues.forEach(change => { + if (!change.fieldName) throw new Error('Each change must have a fieldName') + }) + + // Validate args + const allowedArgs = ['created_at', 'created_by', 'updated_at', 'updated_by'] + const disallowedArgs = Object.keys(args).filter(arg => !allowedArgs.includes(arg)) + if (disallowedArgs.length > 0) { + throw new Error(`Invalid args: ${disallowedArgs.join(', ')}. Allowed: ${allowedArgs.join(', ')}`) + } + + // Process changes using functional approach + const changes = changedValues.reduce((result, { fieldName, oldValue, newValue }) => ({ + ...result, + [fieldName]: { + ...(oldValue !== undefined && { oldValue }), + ...(newValue !== undefined && { newValue }) + } + }), {}) + + return { + entity, + eventType, + entityId, + changes, + ...args, + } + } catch (error) { + console.error(`EventBodyDTO Error: ${error.message}`) + return false + } } diff --git a/src/middlewares/authenticator.js b/src/middlewares/authenticator.js index 0aaf72973..0486d0020 100644 --- a/src/middlewares/authenticator.js +++ b/src/middlewares/authenticator.js @@ -48,23 +48,22 @@ module.exports = async function (req, res, next) { statusCode: httpStatusCode.unauthorized, responseCode: 'UNAUTHORIZED', }) + try { + let decodedToken let roleValidation = false const authHeader = req.get('X-auth-token') - const internalAccess = common.internalAccessUrls.some((path) => { - if (req.path.includes(path)) { - if (req.headers.internal_access_token === process.env.INTERNAL_ACCESS_TOKEN) return true - else if (!authHeader) { - throw unAuthorizedResponse - } - } - return false - }) + + // Validate request path + if (!req.path) { + throw new Error('Invalid request path') + } - // check if captcha check is enabled in the env - const isCaptchaEnabled = process.env.CAPTCHA_ENABLE.toLowerCase() == 'true' + // Default CAPTCHA_ENABLE to false if not set + const isCaptchaEnabled = process.env.CAPTCHA_ENABLE ? process.env.CAPTCHA_ENABLE.toLowerCase() === 'true' : false + // check if captcha check is enabled in the env if (isCaptchaEnabled) { // check if captcha is enabled for the route const isCaptchaEnabledForRoute = common.captchaEnabledAPIs.includes(req.path) @@ -88,8 +87,6 @@ module.exports = async function (req, res, next) { } }) - if (internalAccess && !authHeader) return next() - if (!authHeader) { try { const isPermissionValid = await checkPermissions(common.PUBLIC_ROLE, req.path, req.method) @@ -106,12 +103,11 @@ module.exports = async function (req, res, next) { } } - // let splittedUrl = req.url.split('/'); - // if (common.uploadUrls.includes(splittedUrl[splittedUrl.length - 1])) { - // if (!req.headers.internal_access_token || process.env.INTERNAL_ACCESS_TOKEN !== req.headers.internal_access_token) { - // throw responses.failureResponse({ message: apiResponses.INCORRECT_INTERNAL_ACCESS_TOKEN, statusCode: httpStatusCode.unauthorized, responseCode: 'UNAUTHORIZED' }); - // } - // } + // Validate auth header format + if (authHeader && (!authHeader.includes(' ') || authHeader.split(' ').length !== 2)) { + throw unAuthorizedResponse + } + const authHeaderArray = authHeader.split(' ') if (authHeaderArray[0] !== 'bearer') throw unAuthorizedResponse try { @@ -192,6 +188,13 @@ module.exports = async function (req, res, next) { req.decodedToken = decodedToken.data return next() } catch (err) { + if (err.name === 'TokenExpiredError') { + return next(responses.failureResponse({ + message: 'ACCESS_TOKEN_EXPIRED', + statusCode: httpStatusCode.unauthorized, + responseCode: 'UNAUTHORIZED', + })) + } next(err) } } diff --git a/src/middlewares/pagination.js b/src/middlewares/pagination.js index 002694b16..48137913f 100644 --- a/src/middlewares/pagination.js +++ b/src/middlewares/pagination.js @@ -8,28 +8,36 @@ const common = require('@constants/common') const httpStatus = require('@generics/http-status') const responses = require('@helpers/responses') function containsSpecialChars(str) { - const specialChars = /[`!#$%^&*()_+\-=\[\]{};':"\\|<>\/?~]/ + const specialChars = /[<>{}()'"\\]/ // More specific special chars pattern return specialChars.test(str) } + module.exports = (req, res, next) => { req.pageNo = req.query.page && Number(req.query.page) > 0 ? Number(req.query.page) : 1 req.pageSize = req.query.limit && Number(req.query.limit) > 0 && Number(req.query.limit) <= 100 ? Number(req.query.limit) : 100 - req.searchText = req.query.search && req.query.search != '' ? decodeURI(req.query.search) : '' - /* let buff = new Buffer(req.searchText, 'base64') - req.searchText = buff.toString('ascii') */ + req.searchText = req.query.search && req.query.search != '' ? decodeURI(req.query.search).trim() : '' + + if (req.searchText.length > 100) { + throw responses.failureResponse({ + message: 'Search text too long', + statusCode: httpStatus.bad_request, + responseCode: 'CLIENT_ERROR', + }) + } + if (containsSpecialChars(req.searchText)) { throw responses.failureResponse({ message: 'Invalid search text', statusCode: httpStatus.bad_request, responseCode: 'CLIENT_ERROR', }) - } else { - delete req.query.page - delete req.query.limit - next() - return } + + delete req.query.page + delete req.query.limit + delete req.query.search + next() } diff --git a/src/middlewares/validator.js b/src/middlewares/validator.js index 10e50282e..7220ff824 100644 --- a/src/middlewares/validator.js +++ b/src/middlewares/validator.js @@ -7,10 +7,20 @@ module.exports = (req, res, next) => { try { - const version = (req.params.version.match(/^v\d+$/) || [])[0] // Match version like v1, v2, etc. - const controllerName = (req.params.controller.match(/^[a-zA-Z0-9_-]+$/) || [])[0] // Allow only alphanumeric characters, underscore, and hyphen - const method = (req.params.method.match(/^[a-zA-Z0-9]+$/) || [])[0] // Allow only alphanumeric characters + if (!req.params.version || !req.params.controller || !req.params.method) { + throw new Error('Missing required path parameters') + } + + const version = (req.params.version.match(/^v\d+$/) || [])[0] + const controllerName = (req.params.controller.match(/^[a-zA-Z0-9_-]+$/) || [])[0] + const method = (req.params.method.match(/^[a-zA-Z0-9]+$/) || [])[0] + + if (!version || !controllerName || !method) { + throw new Error('Invalid path parameters') + } + require(`@validators/${version}/${controllerName}`)[method](req) - } catch {} - next() + } catch (error) { + next(error) + } } diff --git a/src/scripts/insertDefaultOrg.js b/src/scripts/insertDefaultOrg.js index b19894eac..6ae5153f1 100644 --- a/src/scripts/insertDefaultOrg.js +++ b/src/scripts/insertDefaultOrg.js @@ -1,76 +1,118 @@ const { Sequelize } = require('sequelize') require('dotenv').config({ path: '../.env' }) -const nodeEnv = process.env.NODE_ENV || 'development' - -let databaseUrl - -switch (nodeEnv) { - case 'production': - databaseUrl = process.env.PROD_DATABASE_URL - break - case 'test': - databaseUrl = process.env.TEST_DATABASE_URL - break - default: - databaseUrl = process.env.DEV_DATABASE_URL +// Constants +const DEFAULT_ORG = { + NAME: 'Default Organization', + CODE: 'default_code', + DESCRIPTION: 'Default Organisation', + STATUS: 'ACTIVE', } -if (!databaseUrl) { - console.error(`${nodeEnv} DATABASE_URL not found in environment variables.`) - process.exit(1) +// Database configuration +const getDbConfig = (nodeEnv) => { + const configs = { + production: process.env.PROD_DATABASE_URL, + test: process.env.TEST_DATABASE_URL, + development: process.env.DEV_DATABASE_URL, + } + return configs[nodeEnv] || configs.development } -const sequelize = new Sequelize(databaseUrl, { - dialect: 'postgres', - logging: process.env.NODE_ENV === 'development' ? console.log : false, -}) +// SQL Queries with parameterization +const queries = { + check: `SELECT id FROM organizations WHERE code = $1 LIMIT 1`, + insert: ` + INSERT INTO organizations ( + name, code, description, status, updated_at, created_at + ) VALUES ($1, $2, $3, $4, NOW(), NOW()) + RETURNING id + `, + insertCode: ` + INSERT INTO organization_codes ( + code, organization_id, updated_at, created_at + ) VALUES ($1, $2, NOW(), NOW()) + RETURNING organization_id + `, +} -// Raw SQL query to check if a row with 'default_code' already exists -const checkQuery = ` - SELECT id FROM organizations WHERE code = 'default_code' LIMIT 1; -` +async function insertDefaultOrg() { + const nodeEnv = process.env.NODE_ENV || 'development' + const databaseUrl = getDbConfig(nodeEnv) -// Raw SQL query for insertion -const insertQuery = ` - INSERT INTO organizations (name, code, description, status, updated_at, created_at) - VALUES (?, ?, ?, ?, NOW(), NOW()) - RETURNING id; -` + if (!databaseUrl) { + throw new Error(`${nodeEnv} DATABASE_URL not found in environment variables.`) + } -const insertCodeQuery = ` - INSERT INTO organization_codes (code , organization_id, updated_at, created_at) - VALUES (?, ?, NOW(), NOW()) - RETURNING organization_id; -` + const sequelize = new Sequelize(databaseUrl, { + dialect: 'postgres', + logging: nodeEnv === 'development' ? console.log : false, + pool: { + max: 5, + min: 0, + acquire: 30000, + idle: 10000, + }, + }) -const defaultValues = ['Default Organization', 'default_code', 'Default Organisation', 'ACTIVE'] -const queryParams = defaultValues.map((value, index) => (value === 'default' ? null : value)) + let transaction -;(async () => { try { - // Check if a row with 'default_code' already exists - const [existingRow] = await sequelize.query(checkQuery, { raw: true }) + await sequelize.authenticate() + console.log('Database connection established successfully.') + + transaction = await sequelize.transaction() + + // Check existing organization + const [existingRow] = await sequelize.query(queries.check, { + bind: [DEFAULT_ORG.CODE], + transaction, + }) if (existingRow.length > 0) { - const existingRowId = existingRow[0].id console.log( - `A row with code 'default_code' already exists. Existing row ID: ${existingRowId}. Aborting insertion.` + `Organization with code '${DEFAULT_ORG.CODE}' already exists (ID: ${existingRow[0].id})` ) - return + await transaction.commit() + return existingRow[0].id } - // If no existing row, proceed with the insertion - const [result] = await sequelize.query(insertQuery, { replacements: queryParams, raw: true }) - const insertedRowId = result[0].id - const [resultCode] = await sequelize.query(insertCodeQuery, { - replacements: ['default_code', insertedRowId], - raw: true, + + // Insert organization + const [result] = await sequelize.query(queries.insert, { + bind: [ + DEFAULT_ORG.NAME, + DEFAULT_ORG.CODE, + DEFAULT_ORG.DESCRIPTION, + DEFAULT_ORG.STATUS, + ], + transaction, + }) + + const orgId = result[0].id + + // Insert organization code + await sequelize.query(queries.insertCode, { + bind: [DEFAULT_ORG.CODE, orgId], + transaction, }) - console.log('Default org ID:', `\x1b[1m\x1b[32m${insertedRowId}\x1b[0m`) + await transaction.commit() + console.log( + 'Default organization created successfully:', + `\x1b[1m\x1b[32m${orgId}\x1b[0m` + ) + return orgId } catch (error) { - console.error(`Error creating function: ${error.message}`) + if (transaction) await transaction.rollback() + console.error('Error:', error.message) + throw error } finally { - sequelize.close() + await sequelize.close() } -})() +} + +// Execute with proper error handling +insertDefaultOrg().catch((error) => { + console.error('Fatal error:', error) + process.exit(1) +})