diff --git a/src/.env.sample b/src/.env.sample index c9d2bfe89..fcb9b60bc 100644 --- a/src/.env.sample +++ b/src/.env.sample @@ -170,6 +170,9 @@ GENERIC_INVITATION_EMAIL_TEMPLATE_CODE=generic_invite # Allowed host by CORS ALLOWED_HOST = "http://examplDomain.com" +# Downloadabale url exipres after +DOWNLOAD_URL_EXPIRATION_DURATION = 120000 + #database url DATABASE_URL=postgres://postgres:postgres@localhost:5432/elevate-user diff --git a/src/constants/blacklistConfig.js b/src/constants/blacklistConfig.js index 0830cf6b9..f6e21fc1f 100644 --- a/src/constants/blacklistConfig.js +++ b/src/constants/blacklistConfig.js @@ -84,7 +84,6 @@ const account = { registrationOtp: [ 'id', 'email_verified', - 'name', 'gender', 'location', 'about', diff --git a/src/constants/common.js b/src/constants/common.js index 159efde6f..ce9011075 100644 --- a/src/constants/common.js +++ b/src/constants/common.js @@ -28,7 +28,7 @@ module.exports = { '/user/v1/user-role/default', ], notificationEmailType: 'email', - accessTokenExpiry: `${process.env.ACCESS_TOKEN_EXPIRY}d`, + accessTokenExpiry: process.env.ACCESS_TOKEN_EXPIRY, refreshTokenExpiry: `${process.env.REFRESH_TOKEN_EXPIRY}d`, refreshTokenExpiryInMs: Number(process.env.REFRESH_TOKEN_EXPIRY) * 24 * 60 * 60 * 1000, refreshTokenLimit: 3, diff --git a/src/envVariables.js b/src/envVariables.js index b4c013c92..89ccb51bc 100644 --- a/src/envVariables.js +++ b/src/envVariables.js @@ -100,11 +100,11 @@ let enviromentVariables = { optional: process.env.CLOUD_STORAGE === 'AZURE' ? false : true, }, ACCESS_TOKEN_EXPIRY: { - message: 'Required access token expiry in days', + message: 'Required access token expiry', optional: false, }, REFRESH_TOKEN_EXPIRY: { - message: 'Required refresh token expiry in days', + message: 'Required refresh token expiry', optional: false, }, API_DOC_URL: { @@ -241,6 +241,22 @@ let enviromentVariables = { optional: true, default: '*', }, + PASSWORD_POLICY_REGEX: { + message: 'Required password policy', + optional: true, + default: '/^(?=.*[A-Z])(?=.*d)(?=.*[!@#$%^&*()_+{}|:<>?~`-=[];,./])[^ ]{11,}$/', + }, + PASSWORD_POLICY_MESSAGE: { + message: 'Required password policy message', + optional: true, + default: + 'Password must have at least one uppercase letter, one number, one special character, and be at least 10 characters long', + }, + DOWNLOAD_URL_EXPIRATION_DURATION: { + message: 'Required downloadable url expiration time', + optional: true, + default: 3600000, + }, } let success = true diff --git a/src/generics/utils.js b/src/generics/utils.js index 7a57f1fc8..2e45d706f 100644 --- a/src/generics/utils.js +++ b/src/generics/utils.js @@ -53,8 +53,9 @@ const getDownloadableUrl = async (imgPath) => { bucketName: process.env.DEFAULT_GCP_BUCKET_NAME, gcpProjectId: process.env.GCP_PROJECT_ID, gcpJsonFilePath: path.join(__dirname, '../', process.env.GCP_PATH), + expiry: Date.now() + parseFloat(process.env.DOWNLOAD_URL_EXPIRATION_DURATION), } - imgPath = await GcpFileHelper.getDownloadableUrl(options) + imgPath = await GcpFileHelper.getSignedDownloadableUrl(options) } else if (process.env.CLOUD_STORAGE === 'AWS') { const options = { destFilePath: imgPath, diff --git a/src/locales/en.json b/src/locales/en.json index 9cba64a1a..b5dad36d7 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -113,5 +113,7 @@ "ROLES_HAS_EMPTY_LIST": "Empty roles list", "COLUMN_DOES_NOT_EXISTS": "Role column does not exists", "PERMISSION_DENIED": "You do not have the required permissions to access this resource. Please contact your administrator for assistance.", - "RELATED_ORG_REMOVAL_FAILED": "Requested organization not related the organization. Please check the values." + "RELATED_ORG_REMOVAL_FAILED": "Requested organization not related the organization. Please check the values.", + "INAVLID_ORG_ROLE_REQ": "Invalid organisation request" + } diff --git a/src/middlewares/validator.js b/src/middlewares/validator.js index 657baf27f..10e50282e 100644 --- a/src/middlewares/validator.js +++ b/src/middlewares/validator.js @@ -7,7 +7,10 @@ module.exports = (req, res, next) => { try { - require(`@validators/${req.params.version}/${req.params.controller}`)[req.params.method](req) + 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 + require(`@validators/${version}/${controllerName}`)[method](req) } catch {} next() } diff --git a/src/package.json b/src/package.json index c6da7f3ee..39f87028e 100644 --- a/src/package.json +++ b/src/package.json @@ -37,7 +37,7 @@ "crypto": "^1.0.1", "csvtojson": "^2.0.10", "dotenv": "^10.0.0", - "elevate-cloud-storage": "2.0.0", + "elevate-cloud-storage": "2.6.1", "elevate-encryption": "^1.0.1", "elevate-logger": "^3.1.0", "elevate-node-cache": "^1.0.6", diff --git a/src/routes/index.js b/src/routes/index.js index 6e6928d31..b73743c62 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -12,15 +12,94 @@ const expressValidator = require('express-validator') const fs = require('fs') const { elevateLog, correlationId } = require('elevate-logger') const logger = elevateLog.init() +const path = require('path') module.exports = (app) => { app.use(authenticator) app.use(pagination) app.use(expressValidator()) + async function getAllowedControllers(directoryPath) { + try { + const getAllFilesAndDirectories = (dir) => { + let filesAndDirectories = [] + fs.readdirSync(dir).forEach((item) => { + const itemPath = path.join(dir, item) + const stat = fs.statSync(itemPath) + if (stat.isDirectory()) { + filesAndDirectories.push({ + name: item, + type: 'directory', + path: itemPath, + }) + filesAndDirectories = filesAndDirectories.concat(getAllFilesAndDirectories(itemPath)) + } else { + filesAndDirectories.push({ + name: item, + type: 'file', + path: itemPath, + }) + } + }) + return filesAndDirectories + } + + const allFilesAndDirectories = getAllFilesAndDirectories(directoryPath) + const allowedControllers = allFilesAndDirectories + .filter((item) => item.type === 'file' && item.name.endsWith('.js')) + .map((item) => path.basename(item.name, '.js')) // Remove the ".js" extension + const allowedVersions = allFilesAndDirectories + .filter((item) => item.type === 'directory') + .map((item) => item.name) + + return { + allowedControllers, + allowedVersions, + } + } catch (err) { + console.error('Unable to scan directory:', err) + return { + allowedControllers: [], + directories: [], + } + } + } async function router(req, res, next) { let controllerResponse let validationError + 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 file = req.params.file ? (req.params.file.match(/^[a-zA-Z0-9_-]+$/) || [])[0] : null // Same validation as controller, or null if file is not provided + const method = (req.params.method.match(/^[a-zA-Z0-9]+$/) || [])[0] // Allow only alphanumeric characters + try { + if (!version || !controllerName || !method || (req.params.file && !file)) { + // Invalid input, return an error response + const error = new Error('Invalid Path') + error.statusCode = 400 + throw error + } + + const directoryPath = path.resolve(__dirname, '..', 'controllers') + + const { allowedControllers, allowedVersions } = await getAllowedControllers(directoryPath) + + // Validate version + if (!allowedVersions.includes(version)) { + const error = new Error('Invalid version.') + error.statusCode = 400 + throw error + } + + // Validate controller + allowedControllers.push('cloud-services') + if (!allowedControllers.includes(controllerName)) { + const error = new Error('Invalid controller.') + error.statusCode = 400 + throw error + } + } catch (error) { + return next(error) + } /* Check for input validation error */ try { @@ -53,16 +132,14 @@ module.exports = (app) => { '.js' ) if (folderExists) { - controller = require(`@controllers/${req.params.version}/${req.params.controller}/${req.params.file}`) + controller = require(`@controllers/${version}/${controllerName}/${file}`) } else { - controller = require(`@controllers/${req.params.version}/${req.params.controller}`) + controller = require(`@controllers/${version}/${controllerName}`) } } else { - controller = require(`@controllers/${req.params.version}/${req.params.controller}`) + controller = require(`@controllers/${version}/${controllerName}`) } - controllerResponse = new controller()[req.params.method] - ? await new controller()[req.params.method](req) - : next() + controllerResponse = new controller()[method] ? await new controller()[method](req) : next() } catch (error) { // If controller or service throws some random error return next(error) diff --git a/src/services/org-admin.js b/src/services/org-admin.js index 3b5d136bc..bd496d222 100644 --- a/src/services/org-admin.js +++ b/src/services/org-admin.js @@ -237,6 +237,19 @@ module.exports = class OrgAdminHelper { const requestId = bodyData.request_id delete bodyData.request_id + const requestDetail = await orgRoleReqQueries.requestDetails({ + id: requestId, + organization_id: tokenInformation.organization_id, + }) + + if (requestDetail.status !== common.REQUESTED_STATUS) { + return responses.failureResponse({ + message: 'INAVLID_ORG_ROLE_REQ', + statusCode: httpStatusCode.bad_request, + responseCode: 'CLIENT_ERROR', + }) + } + bodyData.handled_by = tokenInformation.id const rowsAffected = await orgRoleReqQueries.update( { id: requestId, organization_id: tokenInformation.organization_id }, diff --git a/src/validators/v1/account.js b/src/validators/v1/account.js index 3d9491666..f0af585fc 100644 --- a/src/validators/v1/account.js +++ b/src/validators/v1/account.js @@ -28,7 +28,13 @@ module.exports = { .withMessage('email is invalid') .normalizeEmail({ gmail_remove_dots: false }) - req.checkBody('password').trim().notEmpty().withMessage('password field is empty') + req.checkBody('password') + .notEmpty() + .withMessage('Password field is empty') + .matches(process.env.PASSWORD_POLICY_REGEX) + .withMessage(process.env.PASSWORD_POLICY_MESSAGE) + .custom((value) => !/\s/.test(value)) + .withMessage('Password cannot contain spaces') if (req.body.role) { req.checkBody('role').trim().not().isIn([common.ADMIN_ROLE]).withMessage("User does't have admin access") @@ -73,7 +79,13 @@ module.exports = { resetPassword: (req) => { req.body = filterRequestBody(req.body, account.resetPassword) req.checkBody('email').notEmpty().withMessage('email field is empty').isEmail().withMessage('email is invalid') - req.checkBody('password').notEmpty().withMessage('password field is empty') + req.checkBody('password') + .notEmpty() + .withMessage('Password field is empty') + .matches(process.env.PASSWORD_POLICY_REGEX) + .withMessage(process.env.PASSWORD_POLICY_MESSAGE) + .custom((value) => !/\s/.test(value)) + .withMessage('Password cannot contain spaces') req.checkBody('otp') .notEmpty() diff --git a/src/validators/v1/admin.js b/src/validators/v1/admin.js index e5aaa41d9..93ed9b889 100644 --- a/src/validators/v1/admin.js +++ b/src/validators/v1/admin.js @@ -30,7 +30,13 @@ module.exports = { .withMessage('email is invalid') .normalizeEmail() - req.checkBody('password').trim().notEmpty().withMessage('password field is empty') + req.checkBody('password') + .notEmpty() + .withMessage('Password field is empty') + .matches(process.env.PASSWORD_POLICY_REGEX) + .withMessage(process.env.PASSWORD_POLICY_MESSAGE) + .custom((value) => !/\s/.test(value)) + .withMessage('Password cannot contain spaces') }, login: (req) => { diff --git a/src/validators/v1/organization.js b/src/validators/v1/organization.js index 58651f2f2..9a0bcf838 100644 --- a/src/validators/v1/organization.js +++ b/src/validators/v1/organization.js @@ -21,6 +21,7 @@ module.exports = { .trim() .notEmpty() .withMessage('description field is empty') + .not() .matches(/(\b)(on\S+)(\s*)=|javascript:|<(|\/|[^\/>][^>]+|\/[^>][^>]+)>/gi) .withMessage('invalid description') req.checkBody('domains').trim().notEmpty().withMessage('domains field is empty') @@ -42,6 +43,7 @@ module.exports = { .trim() .notEmpty() .withMessage('description field is empty') + .not() .matches(/(\b)(on\S+)(\s*)=|javascript:|<(|\/|[^\/>][^>]+|\/[^>][^>]+)>/gi) .withMessage('invalid description') }, diff --git a/src/validators/v1/user.js b/src/validators/v1/user.js index 40feb3f7e..a0ceb2c2f 100644 --- a/src/validators/v1/user.js +++ b/src/validators/v1/user.js @@ -38,6 +38,7 @@ module.exports = { .trim() .notEmpty() .withMessage('about field is empty') + .not() .matches(/(\b)(on\S+)(\s*)=|javascript:|<(|\/|[^\/>][^>]+|\/[^>][^>]+)>/gi) .withMessage('invalid about')