diff --git a/lib/routes/api/accounts.js b/lib/routes/api/accounts.js index 4b90588..f3d02c6 100644 --- a/lib/routes/api/accounts.js +++ b/lib/routes/api/accounts.js @@ -140,6 +140,11 @@ router.put('/:sid/VoipCarriers/:voip_carrier_sid', async(req, res) => { const logger = req.app.locals.logger; try { + if (process.env.JAMBONES_ADMIN_CARRIER == 1 && (!req.user.hasScope('service_provider') + && !req.user.hasScope('admin'))) { + throw new DbErrorBadRequest('insufficient privileges'); + } + const sid = parseVoipCarrierSid(req); const account_sid = parseAccountSid(req); await validateRequest(req, account_sid); @@ -159,6 +164,10 @@ router.post('/:sid/VoipCarriers', async(req, res) => { const logger = req.app.locals.logger; const payload = req.body; try { + if (process.env.JAMBONES_ADMIN_CARRIER == 1 && (!req.user.hasScope('service_provider') + || !!req.user.hasScope('admin'))) { + throw new DbErrorBadRequest('insufficient privileges'); + } const account_sid = parseAccountSid(req); await validateRequest(req, account_sid); // Set the service_provder_sid to the relevent value for the account @@ -298,7 +307,8 @@ function validateUpdateCall(opts) { 'tag', 'dtmf', 'conferenceParticipantAction', - 'dub' + 'dub', + 'boostAudioSignal' ] .reduce((acc, prop) => (opts[prop] ? ++acc : acc), 0); @@ -560,6 +570,8 @@ router.post('/', async(req, res) => { } delete obj[prop]; } + //force sip realm to lowercase + if (obj.sip_realm) { obj.sip_realm = obj.sip_realm.toLowerCase(); } logger.debug(`Attempting to add account ${JSON.stringify(obj)}`); const uuid = await Account.make(obj); @@ -802,6 +814,9 @@ router.put('/:sid', async(req, res) => { encryptBucketCredential(obj, storedBucketCredentials); + //force sip realm to lowercase + if (obj.sip_realm) { obj.sip_realm = obj.sip_realm.toLowerCase();} + const rowsAffected = await Account.update(sid, obj); if (rowsAffected === 0) { return res.status(404).end(); diff --git a/lib/routes/api/applications.js b/lib/routes/api/applications.js index 6020bec..50e0d3a 100644 --- a/lib/routes/api/applications.js +++ b/lib/routes/api/applications.js @@ -101,6 +101,20 @@ async function validateUpdate(req, sid) { if (req.body.call_status_hook && typeof req.body.call_hook !== 'object') { throw new DbErrorBadRequest('\'call_status_hook\' must be an object when updating an application'); } + + let urlError; + if (req.body.call_hook) { + urlError = await isInvalidUrl(req.body.call_hook.url); + if (urlError) { + throw new DbErrorBadRequest(`call_hook ${urlError}`); + } + } + if (req.body.call_status_hook) { + urlError = await isInvalidUrl(req.body.call_status_hook.url); + if (urlError) { + throw new DbErrorBadRequest(`call_status_hook ${urlError}`); + } + } } async function validateDelete(req, sid) { @@ -290,9 +304,6 @@ router.put('/:sid', async(req, res) => { obj[`${prop}_sid`] = sid; } } - else { - obj[`${prop}_sid`] = null; - } delete obj[prop]; } diff --git a/lib/routes/api/phone-numbers.js b/lib/routes/api/phone-numbers.js index a87d75a..3d22869 100644 --- a/lib/routes/api/phone-numbers.js +++ b/lib/routes/api/phone-numbers.js @@ -19,6 +19,11 @@ const hasWhitespace = (str) => /\s/.test(str); /* check for required fields when adding */ async function validateAdd(req) { try { + if (process.env.JAMBONES_ADMIN_CARRIER == 1 && (!req.user.hasScope('service_provider') + && !req.user.hasScope('admin'))) { + throw new DbErrorBadRequest('insufficient privileges'); + } + /* account level user can only act on carriers associated to his/her account */ if (req.user.hasAccountAuth) { req.body.account_sid = req.user.account_sid; diff --git a/lib/routes/api/sip-gateways.js b/lib/routes/api/sip-gateways.js index 1246270..199b2da 100644 --- a/lib/routes/api/sip-gateways.js +++ b/lib/routes/api/sip-gateways.js @@ -45,6 +45,12 @@ const validate = async(req, sid) => { const {netmask, ipv4, inbound, outbound} = req.body; let voip_carrier_sid; + if (process.env.JAMBONES_ADMIN_CARRIER == 1 && (!req.user.hasScope('service_provider') + && !req.user.hasScope('admin'))) { + throw new DbErrorBadRequest('insufficient privileges'); + } + + if (sid) { const gateway = await lookupSipGatewayBySid(sid); if (!gateway) throw new DbErrorBadRequest('invalid sip_gateway_sid'); diff --git a/lib/routes/api/speech-credentials.js b/lib/routes/api/speech-credentials.js index ead1e2f..f63118e 100644 --- a/lib/routes/api/speech-credentials.js +++ b/lib/routes/api/speech-credentials.js @@ -166,6 +166,7 @@ const encryptCredential = (obj) => { engine_version, service_version, api_uri, + houndify_server_uri, options } = obj; @@ -329,7 +330,7 @@ const encryptCredential = (obj) => { assert(client_id, 'invalid houndify speech credential: client_id is required'); assert(client_key, 'invalid houndify speech credential: client_key is required'); assert(user_id, 'invalid houndify speech credential: user_id is required'); - const houndifyData = JSON.stringify({client_id, client_key, user_id}); + const houndifyData = JSON.stringify({client_id, client_key, user_id, houndify_server_uri}); return encrypt(houndifyData); case 'voxist': @@ -557,7 +558,8 @@ router.put('/:sid', async(req, res) => { speechmatics_stt_uri, resemble_tts_use_tls, resemble_tts_uri, - api_uri + api_uri, + houndify_server_uri } = req.body; const newCred = { @@ -593,7 +595,8 @@ router.put('/:sid', async(req, res) => { speechmatics_stt_uri, resemble_tts_uri, resemble_tts_use_tls, - api_uri + api_uri, + houndify_server_uri }; logger.info({o, newCred}, 'updating speech credential with this new credential'); obj.credential = encryptCredential(newCred); diff --git a/lib/routes/api/subscriptions.js b/lib/routes/api/subscriptions.js index 2961bf0..2324ca0 100644 --- a/lib/routes/api/subscriptions.js +++ b/lib/routes/api/subscriptions.js @@ -17,6 +17,7 @@ const { } = require('../../utils/stripe-utils'); const {setupFreeTrial} = require('./utils'); const sysError = require('../error'); +const Product = require('../../models/product'); const actions = [ 'upgrade-to-paid', 'downgrade-to-free', @@ -24,6 +25,8 @@ const actions = [ 'update-quantities' ]; +const MIN_VOICE_CALL_SESSION_QUANTITY = 5; + const handleError = async(logger, method, res, err) => { if ('StatusError' === err.name) { const text = await err.text(); @@ -146,6 +149,22 @@ const upgradeToPaidPlan = async(req, res) => { await handleSubscriptionOutcome(req, res, subscription); }; + +const validateProductQuantities = async(products) => { + const availableProducts = await Product.retrieveAll(); + const voiceCallSessionsProductSid = + availableProducts.find((p) => p.category === 'voice_call_session')?.product_sid; + if (voiceCallSessionsProductSid) { + const invalid = products.find((p) => { + return (p.product_sid === voiceCallSessionsProductSid && + (typeof p.quantity !== 'number' || p.quantity < MIN_VOICE_CALL_SESSION_QUANTITY)); + }); + if (invalid) { + throw new DbErrorBadRequest('invalid voice call session value, minimum is ' + + MIN_VOICE_CALL_SESSION_QUANTITY); + } + } +}; const downgradeToFreePlan = async(req, res) => { const logger = req.app.locals.logger; const {account_sid} = req.user; @@ -291,11 +310,11 @@ router.post('/', async(req, res) => { if ('update-payment-method' === action && typeof payment_method_id !== 'string') { throw new DbErrorBadRequest('missing payment_method_id'); } - if ('upgrade-to-paid' === action && (!Array.isArray(products) || 0 === products.length)) { - throw new DbErrorBadRequest('missing products'); - } - if ('update-quantities' === action && (!Array.isArray(products) || 0 === products.length)) { - throw new DbErrorBadRequest('missing products'); + if (['update-quantities', 'upgrade-to-paid'].includes(action)) { + if ((!Array.isArray(products) || 0 === products.length)) { + throw new DbErrorBadRequest('missing products'); + } + await validateProductQuantities(products); } switch (action) { diff --git a/lib/routes/api/voip-carriers.js b/lib/routes/api/voip-carriers.js index f1e8753..a94b3a7 100644 --- a/lib/routes/api/voip-carriers.js +++ b/lib/routes/api/voip-carriers.js @@ -9,6 +9,11 @@ const { parseVoipCarrierSid } = require('./utils'); const validate = async(req) => { const {lookupAppBySid, lookupAccountBySid} = req.app.locals; + if (process.env.JAMBONES_ADMIN_CARRIER == 1 && (!req.user.hasScope('service_provider') + && !req.user.hasScope('admin'))) { + throw new DbErrorBadRequest('insufficient privileges'); + } + /* account level user can only act on carriers associated to his/her account */ if (req.user.hasAccountAuth) { req.body.account_sid = req.user.account_sid; @@ -45,6 +50,12 @@ const validateUpdate = async(req, sid) => { const validateDelete = async(req, sid) => { const {lookupCarrierBySid} = req.app.locals; + if (process.env.JAMBONES_ADMIN_CARRIER == 1 && (!req.user.hasScope('service_provider') + && !req.user.hasScope('admin'))) { + throw new DbErrorBadRequest('insufficient privileges'); + } + + if (req.user.hasAccountAuth) { /* can only update carriers for the user's account */ const carrier = await lookupCarrierBySid(sid); diff --git a/lib/routes/stripe/webhook.js b/lib/routes/stripe/webhook.js index fc5cc16..a792b84 100644 --- a/lib/routes/stripe/webhook.js +++ b/lib/routes/stripe/webhook.js @@ -13,6 +13,10 @@ const handleInvoicePaymentSucceeded = async(logger, obj) => { const sub = await retrieveSubscription(logger, subscription); if ('active' === sub.status) { const {account_sid} = sub.metadata; + if (!account_sid) { + logger.info({subscription}, `handleInvoicePaymentSucceeded: received subscription ${sub.id} without account_sid`); + return; + } if (await Account.activateSubscription(logger, account_sid, sub.id, 'subscription_create' === obj.billing_reason ? 'upgrade to paid plan' : 'change plan details')) { logger.info(`handleInvoicePaymentSucceeded: activated subscription for account ${account_sid}`); @@ -35,6 +39,10 @@ const handleInvoicePaymentFailed = async(logger, obj) => { const sub = await retrieveSubscription(logger, subscription); logger.debug({obj}, `payment for ${obj.billing_reason} failed, subscription status is ${sub.status}`); const {account_sid} = sub.metadata; + if (!account_sid) { + logger.info({subscription}, `handleInvoicePaymentFailed: received subscription ${sub.id} without account_sid`); + return; + } if (await Account.deactivateSubscription(logger, account_sid, 'payment failed')) { logger.info(`handleInvoicePaymentFailed: deactivated subscription for account ${account_sid}`); } diff --git a/lib/utils/encrypt-decrypt.js b/lib/utils/encrypt-decrypt.js index 0368171..772e6c9 100644 --- a/lib/utils/encrypt-decrypt.js +++ b/lib/utils/encrypt-decrypt.js @@ -50,18 +50,15 @@ function isObscureKey(bucketCredentials) { service_key = '', connection_string = '' } = bucketCredentials || {}; - let pattern; + // Pattern matches: 4-6 any characters followed by one or more X's + const pattern = /^.{4,6}X+$/; switch (vendor) { case 'aws_s3': case 's3_compatible': - pattern = /^([A-Za-z0-9]{4,6}X+$)/; return pattern.test(secret_access_key); case 'azure': - pattern = /^([A-Za-z0-9:]{4,6}X+$)/; return pattern.test(connection_string); - case 'google': { - pattern = /^([A-Za-z0-9]{4,6}X+$)/; let {private_key} = JSON.parse(service_key); const key_header = '-----BEGIN PRIVATE KEY-----\n'; private_key = private_key.slice(key_header.length, private_key.length); diff --git a/lib/utils/speech-utils.js b/lib/utils/speech-utils.js index e27eaa2..40937b7 100644 --- a/lib/utils/speech-utils.js +++ b/lib/utils/speech-utils.js @@ -676,8 +676,10 @@ const testHoundifyStt = async(logger, credentials) => { requestInfo: { UserID: user_id || 'test_user', Latitude: 37.388309, - Longitude: -121.973968 + Longitude: -121.973968, }, + // custom endpint is used only for feature server. + // ...(houndify_server_uri && {endpoint: houndify_server_uri}), // Audio format configuration sampleRate: 16000, @@ -887,6 +889,7 @@ function decryptCredential(obj, credential, logger, isObscureKey = true) { obj.client_key = isObscureKey ? obscureKey(o.client_key) : o.client_key; obj.client_id = o.client_id; obj.user_id = o.user_id; + obj.houndify_server_uri = o.houndify_server_uri; } else if ('resemble' === obj.vendor) { const o = JSON.parse(decrypt(credential)); obj.api_key = isObscureKey ? obscureKey(o.api_key) : o.api_key; diff --git a/package-lock.json b/package-lock.json index f376446..790dd9e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,7 +23,7 @@ "@jambonz/realtimedb-helpers": "^0.8.15", "@jambonz/speech-utils": "^0.2.27", "@jambonz/time-series": "^0.2.8", - "@jambonz/verb-specifications": "^0.0.118", + "@jambonz/verb-specifications": "^0.0.122", "@soniox/soniox-node": "^1.2.2", "ajv": "^8.17.1", "argon2": "^0.40.1", @@ -4346,9 +4346,9 @@ } }, "node_modules/@jambonz/verb-specifications": { - "version": "0.0.118", - "resolved": "https://registry.npmjs.org/@jambonz/verb-specifications/-/verb-specifications-0.0.118.tgz", - "integrity": "sha512-1dGnc6TUCehjt1yGNuqh1uzk1xw9HhUm39aVUosQMHlnT0fK0ItikeJ0uttTjFastHNmPPxqJwb20wOvVGTCFg==", + "version": "0.0.122", + "resolved": "https://registry.npmjs.org/@jambonz/verb-specifications/-/verb-specifications-0.0.122.tgz", + "integrity": "sha512-7xqaULhKFywJ2ZuyiYt77iiJwJ+8b98Zt1X4+OqZ7Cdjhfo7S6KnR66XRVJHnekXbmfVv58kB0KWUux5TG//Sw==", "license": "MIT", "dependencies": { "debug": "^4.3.4", diff --git a/package.json b/package.json index 54b1e51..86f6335 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "@jambonz/realtimedb-helpers": "^0.8.15", "@jambonz/speech-utils": "^0.2.27", "@jambonz/time-series": "^0.2.8", - "@jambonz/verb-specifications": "^0.0.118", + "@jambonz/verb-specifications": "^0.0.122", "@soniox/soniox-node": "^1.2.2", "ajv": "^8.17.1", "argon2": "^0.40.1",