Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 50 additions & 25 deletions src/dtos/eventBody.js
Original file line number Diff line number Diff line change
@@ -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
}
}
41 changes: 22 additions & 19 deletions src/middlewares/authenticator.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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 {
Expand Down Expand Up @@ -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)
}
}
26 changes: 17 additions & 9 deletions src/middlewares/pagination.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
20 changes: 15 additions & 5 deletions src/middlewares/validator.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
148 changes: 95 additions & 53 deletions src/scripts/insertDefaultOrg.js
Original file line number Diff line number Diff line change
@@ -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)
})