const router = require('express').Router(); const request = require('request'); const {DbErrorBadRequest, DbErrorUnprocessableRequest} = require('../../utils/errors'); const Account = require('../../models/account'); const Webhook = require('../../models/webhook'); const ServiceProvider = require('../../models/service-provider'); const decorate = require('./decorate'); const snakeCase = require('../../utils/snake-case'); const sysError = require('./error'); const preconditions = { 'add': validateAdd, 'update': validateUpdate, 'delete': validateDelete }; function coerceNumbers(callInfo) { if (Array.isArray(callInfo)) { return callInfo.map((ci) => { if (ci.duration) ci.duration = parseInt(ci.duration); if (ci.sip_status) ci.sip_status = parseInt(ci.sip_status); return ci; }); } if (callInfo.duration) callInfo.duration = parseInt(callInfo.duration); if (callInfo.sip_status) callInfo.sip_status = parseInt(callInfo.sip_status); return callInfo; } function validateUpdateCall(opts) { // only one type of update can be supplied per request const hasWhisper = opts.whisper; const count = [ 'call_hook', 'call_status', 'listen_status', 'mute_status'] .reduce((acc, prop) => (opts[prop] ? ++acc : acc), 0); switch (count) { case 0: // whisper is allowed on its own, or with one of the others if (!hasWhisper) throw new DbErrorBadRequest('no valid options supplied to updateCall'); break; case 1: // good break; default: throw new DbErrorBadRequest('multiple options are not allowed in updateCall'); } if (opts.call_hook && !opts.call_hook.url) throw new DbErrorBadRequest('missing call_hook.url'); if (opts.call_status && !['completed', 'no-answer'].includes(opts.call_status)) { throw new DbErrorBadRequest('invalid call_status'); } if (opts.listen_status && !['pause', 'silence', 'resume'].includes(opts.listen_status)) { throw new DbErrorBadRequest('invalid listen_status'); } if (opts.mute_status && !['mute', 'unmute'].includes(opts.mute_status)) { throw new DbErrorBadRequest('invalid mute_status'); } } function validateTo(to) { if (to && typeof to === 'object') { switch (to.type) { case 'phone': if (typeof to.number === 'string') return; break; case 'user': if (typeof to.name === 'string') return; break; case 'sip': if (typeof to.sipUri === 'string') return; break; } } throw new DbErrorBadRequest(`missing or invalid to property: ${JSON.stringify(to)}`); } async function validateCreateCall(logger, sid, req) { const {lookupApplicationBySid} = req.app.locals; const obj = req.body; if (req.user.account_sid !== sid) throw new DbErrorBadRequest(`unauthorized createCall request for account ${sid}`); obj.account_sid = sid; if (!obj.from) throw new DbErrorBadRequest('missing from parameter'); validateTo(obj.to); if (obj.application_sid) { try { logger.debug(`Accounts:validateCreateCall retrieving application ${obj.application_sid}`); const application = await lookupApplicationBySid(obj.application_sid); logger.debug(`Accounts:validateCreateCall retrieved application ${JSON.stringify(application)}`); Object.assign(obj, { call_hook: application.call_hook, call_status_hook: application.call_status_hook, speech_synthesis_vendor: application.speech_synthesis_vendor, speech_synthesis_voice: application.speech_synthesis_voice, speech_recognizer_vendor: application.speech_recognizer_vendor, speech_recognizer_language: application.speech_recognizer_language }); logger.debug({obj, application}, 'Accounts:validateCreateCall augmented with application settings'); } catch (err) { logger.error(err, `Accounts:validateCreateCall error retrieving application for sid ${obj.application_sid}`); throw new DbErrorBadRequest(`application_sid not found ${obj.application_sid}`); } } else { Object.assign(obj, { speech_synthesis_vendor: 'google', speech_synthesis_voice: 'en-US-Wavenet-C', speech_recognizer_vendor: 'google', speech_recognizer_language: 'en-US' }); } if (!obj.call_hook || (obj.call_hook && !obj.call_hook.url)) { throw new DbErrorBadRequest('either url or application_sid required'); } } async function validateAdd(req) { /* account-level token can not be used to add accounts */ if (req.user.hasAccountAuth) { throw new DbErrorUnprocessableRequest('insufficient permissions to create accounts'); } if (req.user.hasServiceProviderAuth) { /* service providers can only create accounts under themselves */ req.body.service_provider_sid = req.user.service_provider_sid; } if (req.body.service_provider_sid) { const result = await ServiceProvider.retrieve(req.body.service_provider_sid); if (!result || result.length === 0) { throw new DbErrorBadRequest(`service_provider not found for sid ${req.body.service_provider_sid}`); } } if (req.body.registration_hook && typeof req.body.registration_hook !== 'object') { throw new DbErrorBadRequest('\'registration_hook\' must be an object when adding an account'); } } async function validateUpdate(req, sid) { if (req.user.hasAccountAuth && req.user.account_sid !== sid) { throw new DbErrorUnprocessableRequest('insufficient privileges to update this account'); } if (req.user.service_provider_sid && !req.user.hasScope('admin')) { const result = await Account.retrieve(sid); if (result[0].service_provider_sid !== req.user.service_provider_sid) { throw new DbErrorUnprocessableRequest('cannot update account from different service provider'); } } if (req.body.service_provider_sid) throw new DbErrorBadRequest('service_provider_sid may not be modified'); } async function validateDelete(req, sid) { if (req.user.hasAccountAuth && req.user.account_sid !== sid) { throw new DbErrorUnprocessableRequest('insufficient privileges to update this account'); } const assignedPhoneNumbers = await Account.getForeignKeyReferences('phone_numbers.account_sid', sid); if (assignedPhoneNumbers > 0) throw new DbErrorUnprocessableRequest('cannot delete account with phone numbers'); if (req.user.service_provider_sid && !req.user.hasScope('admin')) { const result = await Account.retrieve(sid); if (result[0].service_provider_sid !== req.user.service_provider_sid) { throw new DbErrorUnprocessableRequest('cannot delete account from different service provider'); } } } decorate(router, Account, ['delete'], preconditions); /* add */ router.post('/', async(req, res) => { const logger = req.app.locals.logger; try { await validateAdd(req); // create webhooks if provided const obj = Object.assign({}, req.body); for (const prop of ['registration_hook']) { if (obj[prop]) { obj[`${prop}_sid`] = await Webhook.make(obj[prop]); delete obj[prop]; } } //logger.debug(`Attempting to add account ${JSON.stringify(obj)}`); const uuid = await Account.make(obj); res.status(201).json({sid: uuid}); } catch (err) { sysError(logger, res, err); } }); /* list */ router.get('/', async(req, res) => { const logger = req.app.locals.logger; try { const service_provider_sid = req.user.hasServiceProviderAuth ? req.user.service_provider_sid : null; const account_sid = req.user.hasAccountAuth ? req.user.account_sid : null; const results = await Account.retrieveAll(service_provider_sid, account_sid); res.status(200).json(results); } catch (err) { sysError(logger, res, err); } }); /* retrieve */ router.get('/:sid', async(req, res) => { const logger = req.app.locals.logger; try { const service_provider_sid = req.user.hasServiceProviderAuth ? req.user.service_provider_sid : null; const results = await Account.retrieve(req.params.sid, service_provider_sid); if (results.length === 0) return res.status(404).end(); return res.status(200).json(results[0]); } catch (err) { sysError(logger, res, err); } }); /* update */ router.put('/:sid', async(req, res) => { const sid = req.params.sid; const logger = req.app.locals.logger; try { // create webhooks if provided const obj = Object.assign({}, req.body); for (const prop of ['registration_hook']) { if (prop in obj && Object.keys(obj[prop]).length) { if ('webhook_sid' in obj[prop]) { const sid = obj[prop]['webhook_sid']; delete obj[prop]['webhook_sid']; await Webhook.update(sid, obj[prop]); } else { const sid = await Webhook.make(obj[prop]); obj[`${prop}_sid`] = sid; } } else { obj[`${prop}_sid`] = null; } delete obj[prop]; } await validateUpdate(req, sid); const rowsAffected = await Account.update(sid, obj); if (rowsAffected === 0) { return res.status(404).end(); } res.status(204).end(); } catch (err) { sysError(logger, res, err); } }); /** * create a new Call */ router.post('/:sid/Calls', async(req, res) => { const sid = req.params.sid; const logger = req.app.locals.logger; try { const serviceUrl = process.env.JAMBONES_CREATE_CALL_URL; await validateCreateCall(logger, sid, req); logger.debug({payload: req.body}, `sending POST to ${serviceUrl}`); request({ url: serviceUrl, method: 'POST', json: true, body: req.body }, (err, response, body) => { if (err) { logger.error(err, `Error sending createCall POST to ${serviceUrl}`); return res.sendStatus(500); } if (response.statusCode !== 201) { logger.error({statusCode: response.statusCode}, `Non-success response returned by createCall ${serviceUrl}`); return res.sendStatus(500); } res.status(201).json(body); }); } catch (err) { sysError(logger, res, err); } }); /** * retrieve info for a group of calls under an account */ router.get('/:sid/Calls', async(req, res) => { const accountSid = req.params.sid; const {logger, listCalls} = req.app.locals; try { const calls = await listCalls(accountSid); logger.debug(`retrieved ${calls.length} calls for account sid ${accountSid}`); res.status(200).json(coerceNumbers(snakeCase(calls))); } catch (err) { sysError(logger, res, err); } }); /** * retrieve single call */ router.get('/:sid/Calls/:callSid', async(req, res) => { const accountSid = req.params.sid; const callSid = req.params.callSid; const {logger, retrieveCall} = req.app.locals; try { const callInfo = await retrieveCall(accountSid, callSid); if (callInfo) { logger.debug(callInfo, `retrieved call info for call sid ${callSid}`); res.status(200).json(coerceNumbers(snakeCase(callInfo))); } else { logger.debug(`call not found for call sid ${callSid}`); res.sendStatus(404); } } catch (err) { sysError(logger, res, err); } }); /** * delete call */ router.delete('/:sid/Calls/:callSid', async(req, res) => { const accountSid = req.params.sid; const callSid = req.params.callSid; const {logger, deleteCall} = req.app.locals; try { const result = await deleteCall(accountSid, callSid); if (result) { logger.debug(`successfully deleted call ${callSid}`); res.sendStatus(204); } else { logger.debug(`call not found for call sid ${callSid}`); res.sendStatus(404); } } catch (err) { sysError(logger, res, err); } }); /** * update a call */ router.post('/:sid/Calls/:callSid', async(req, res) => { const accountSid = req.params.sid; const callSid = req.params.callSid; const {logger, retrieveCall} = req.app.locals; try { validateUpdateCall(req.body); const call = await retrieveCall(accountSid, callSid); if (call) { const url = `${call.serviceUrl}/${process.env.JAMBONES_API_VERSION || 'v1'}/updateCall/${callSid}`; logger.debug({call, url, payload: req.body}, `updateCall: retrieved call info for call sid ${callSid}`); request({ url: url, method: 'POST', json: true, body: req.body }).pipe(res); } else { logger.debug(`updateCall: call not found for call sid ${callSid}`); res.sendStatus(404); } } catch (err) { sysError(logger, res, err); } }); module.exports = router;