From af79d92e718495c072ab5b18c05f13b8da8f9b19 Mon Sep 17 00:00:00 2001 From: Dave Horton Date: Thu, 17 Jun 2021 09:01:58 -0400 Subject: [PATCH] added API for retrieving queues --- app.js | 4 ++++ lib/routes/api/accounts.js | 3 +++ lib/routes/api/queues.js | 20 +++++++++++++++++ lib/routes/api/utils.js | 40 +++++++++++++++++++++++++++++++++ lib/swagger/swagger.yaml | 46 ++++++++++++++++++++++++++++++++++++-- package.json | 2 +- test/accounts.js | 31 +++++++++++++++++++++++++ 7 files changed, 143 insertions(+), 3 deletions(-) create mode 100644 lib/routes/api/queues.js create mode 100644 lib/routes/api/utils.js diff --git a/app.js b/app.js index 2272897..94d1331 100644 --- a/app.js +++ b/app.js @@ -24,6 +24,8 @@ const { retrieveCall, deleteCall, listCalls, + pushBack, + listQueues, purgeCalls, retrieveSet } = require('@jambonz/realtimedb-helpers')({ @@ -53,6 +55,8 @@ Object.assign(app.locals, { retrieveCall, deleteCall, listCalls, + pushBack, + listQueues, purgeCalls, retrieveSet, lookupAppBySid, diff --git a/lib/routes/api/accounts.js b/lib/routes/api/accounts.js index c6fef8e..cf62462 100644 --- a/lib/routes/api/accounts.js +++ b/lib/routes/api/accounts.js @@ -9,8 +9,11 @@ const uuidv4 = require('uuid/v4'); const snakeCase = require('../../utils/snake-case'); const sysError = require('./error'); const {promisePool} = require('../../db'); +const {hasAccountPermissions} = require('./utils'); let idx = 0; +router.use('/:sid/Queues', hasAccountPermissions, require('./queues')); + function coerceNumbers(callInfo) { if (Array.isArray(callInfo)) { return callInfo.map((ci) => { diff --git a/lib/routes/api/queues.js b/lib/routes/api/queues.js new file mode 100644 index 0000000..c72b439 --- /dev/null +++ b/lib/routes/api/queues.js @@ -0,0 +1,20 @@ +const router = require('express').Router(); +const sysError = require('./error'); +const {parseAccountSid} = require('./utils'); + +/** + * retrieve queues for an account + */ +router.get('/', async(req, res) => { + const account_sid = parseAccountSid(req); + const {logger, listQueues} = req.app.locals; + try { + const queues = await listQueues(account_sid, req.query.name); + res.status(200).json(queues); + } catch (err) { + sysError(logger, res, err); + } +}); + + +module.exports = router; diff --git a/lib/routes/api/utils.js b/lib/routes/api/utils.js new file mode 100644 index 0000000..ad481e7 --- /dev/null +++ b/lib/routes/api/utils.js @@ -0,0 +1,40 @@ +const parseServiceProviderSid = (req) => { + const arr = /ServiceProviders\/([^\/]*)/.exec(req.originalUrl); + if (arr) return arr[1]; +}; + +const parseAccountSid = (req) => { + const arr = /Accounts\/([^\/]*)/.exec(req.originalUrl); + if (arr) return arr[1]; +}; + +const hasAccountPermissions = (req, res, next) => { + if (req.user.hasScope('admin')) return next(); + if (req.user.hasScope('account')) { + const account_sid = parseAccountSid(req); + if (account_sid === req.user.account_sid) return next(); + } + res.status(403).json({ + status: 'fail', + message: 'insufficient privileges' + }); +}; + +const hasServiceProviderPermissions = (req, res, next) => { + if (req.user.hasScope('admin')) return next(); + if (req.user.hasScope('service_provider')) { + const service_provider_sid = parseServiceProviderSid(req); + if (service_provider_sid === req.user.service_provider_sid) return next(); + } + res.status(403).json({ + status: 'fail', + message: 'insufficient privileges' + }); +}; + +module.exports = { + parseAccountSid, + parseServiceProviderSid, + hasAccountPermissions, + hasServiceProviderPermissions +}; diff --git a/lib/swagger/swagger.yaml b/lib/swagger/swagger.yaml index 8d06716..c496f28 100644 --- a/lib/swagger/swagger.yaml +++ b/lib/swagger/swagger.yaml @@ -1402,7 +1402,37 @@ paths: schema: $ref: '#/components/schemas/GeneralError' - + /Accounts/{AccountSid}/Queues: + get: + summary: list queues for an Account + parameters: + - name: AccountSid + in: path + required: true + schema: + type: string + - name: + - in: query + name: name + required: false + schema: + type: string + description: a queue name or glob-style pattern matching one or more queues to return + responses: + 200: + description: list of queues for this account matching the name query param, if provided + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Queue' + 500: + description: system error + content: + application/json: + schema: + $ref: '#/components/schemas/GeneralError' + /Accounts/{AccountSid}/Calls: post: summary: create a call @@ -1989,6 +2019,18 @@ components: - from - to example: {"from": "13394445678", "to": "16173333456", "text": "please call when you can"} - + Queue: + properties: + name: + type: string + example: support + length: + type: number + format: integer + example: 5 + required: + - name + - length + example: {"name": "new-orders", length: 12} security: - bearerAuth: [] \ No newline at end of file diff --git a/package.json b/package.json index 30b5aa7..1626540 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ }, "dependencies": { "@jambonz/db-helpers": "^0.6.11", - "@jambonz/realtimedb-helpers": "^0.4.1", + "@jambonz/realtimedb-helpers": "^0.4.3", "cors": "^2.8.5", "express": "^4.17.1", "mysql2": "^2.2.5", diff --git a/test/accounts.js b/test/accounts.js index 06fd382..e81e360 100644 --- a/test/accounts.js +++ b/test/accounts.js @@ -16,6 +16,7 @@ process.on('unhandledRejection', (reason, p) => { test('account tests', async(t) => { const app = require('../app'); + const {pushBack} = app.locals; let sid; try { let result; @@ -122,6 +123,36 @@ test('account tests', async(t) => { }); t.ok(result.statusCode === 204, 'successfully assigned phone number to account'); + /* retrieve queues for account */ + result = await request.get(`/Accounts/${sid}/Queues`, { + auth: {bearer: accountLevelToken}, + json: true, + resolveWithFullResponse: true, + }); + t.ok(result.statusCode === 200 && 0 === result.body.length, + 'successfully retrieved an empty array when no queues exist'); + + await pushBack(`queue:${sid}:customer-support`, 'https://ip:300/v1/enqueue/foobar'); + await pushBack(`queue:${sid}:customer-support`, 'https://ip:300/v1/enqueue/bazzle'); + await pushBack(`queue:${sid}:sales-new-orders`, 'https://ip:300/v1/enqueue/bazzle'); + await pushBack(`queue:${sid}:sales-returns`, 'https://ip:300/v1/enqueue/bazzle'); + + result = await request.get(`/Accounts/${sid}/Queues`, { + auth: {bearer: accountLevelToken}, + json: true, + resolveWithFullResponse: true, + }); + console.log(`retrieved queues: ${result.statusCode}: ${JSON.stringify(result.body)}`); + //t.ok(result.statusCode === 200 && 0 === result.body.length, + // 'successfully retrieved an empty array when no queues exist'); + + result = await request.get(`/Accounts/${sid}/Queues?name=sales-*`, { + auth: {bearer: accountLevelToken}, + json: true, + resolveWithFullResponse: true, + }); + console.log(`retrieved queues: ${result.statusCode}: ${JSON.stringify(result.body)}`); + /* cannot delete account that has phone numbers assigned */ result = await request.delete(`/Accounts/${sid}`, { auth: authAdmin,