Compare commits

...

39 Commits

Author SHA1 Message Date
Dave Horton
a46c24b3aa change applications.app_json to TEXT as it could hold large strings 2023-02-03 16:34:11 -05:00
AFelix53
f5c833720a feat: added twilio gateways (#99) 2023-01-30 10:16:27 -05:00
EgleH
4d2cc15de4 Bug/speech creds get all with no sp sid (#102)
* backwards compatibility

* fetch account and sp speech remove duplicates

* fix retrieval of SP credentials associated to an account level user

* update gh actions

---------

Co-authored-by: eglehelms <e.helms@cognigy.com>
Co-authored-by: Dave Horton <daveh@beachdognet.com>
2023-01-30 10:06:16 -05:00
Hoan Luu Huu
019599741a feat: add app_json to applications endpoint (#100)
* added phone_numbers.app_json column

* modify prev commit to put app_json on applications table

* feat: add app_json to applications endpoint

---------

Co-authored-by: Dave Horton <daveh@beachdognet.com>
Co-authored-by: Quan HL <quanluuhoang8@gmail.com>
2023-01-28 10:02:04 -05:00
EgleH
f2c2623b28 Speech should always be attached to an SP (#98)
* user will always be attached to SP, thus always provide SP sid

* add another fallback for service_provider_sid

* fix the email and username check in user creation that was crashing the server

* not allow same names for shared and account carriers

Co-authored-by: eglehelms <e.helms@cognigy.com>
2023-01-26 07:53:29 -05:00
EgleH
6c494786c8 add check to not allow the same password to be user as new (#97)
Co-authored-by: eglehelms <e.helms@cognigy.com>
2023-01-23 07:59:33 -05:00
EgleH
80ee1d06d7 Return scoped carriers and speech creds in /SP GET call (#96)
* return scoped carriers and speech creds in /SP GET call

* apply review comments

Co-authored-by: eglehelms <e.helms@cognigy.com>
2023-01-19 10:53:37 -05:00
EgleH
274377960e Allow falsy values for force_change and Is_active in PUT user call (#95)
* allow falsy values

* apply review comments

Co-authored-by: eglehelms <e.helms@cognigy.com>
2023-01-19 10:39:57 -05:00
Charles Chance
02bba9d981 Update Simwood gateways (#92) 2023-01-12 07:48:47 -05:00
Dave Horton
642a6615a0 additional db upgrade statements for 0.8 2023-01-10 10:28:46 -05:00
Dave Horton
b0f317b545 final fix for proper alter table 2023-01-09 15:53:10 -05:00
Dave Horton
43d991ef1c fix error in db update statement 2023-01-09 15:41:24 -05:00
Dave Horton
5ab1ad9056 fixes to update db script for 0.8.0 2023-01-09 14:51:54 -05:00
Dave Horton
37062fa720 gh: run tests on PR 2023-01-09 10:13:08 -05:00
Dave Horton
e42634d726 error case: creating user with duplicate email, return json with msg for toast 2022-12-26 13:19:24 -06:00
Dave Horton
5a9e22df5e add PUT for Account level Carrier 2022-12-26 11:14:41 -06:00
Dave Horton
e75eae4e24 Bugfix/fix permissions (#89)
* protect service provider data retrieval so only admin users and appropriate service provider users can access

* allow accounts to access ServiceProvider API but filter response data so they dont see other accounts or data they should not
2022-12-25 14:52:57 -06:00
Snyk bot
2000e7de90 fix: package.json & package-lock.json to reduce vulnerabilities (#88)
The following vulnerabilities are fixed with an upgrade:
- https://snyk.io/vuln/SNYK-JS-JSONWEBTOKEN-3180020
- https://snyk.io/vuln/SNYK-JS-JSONWEBTOKEN-3180022
- https://snyk.io/vuln/SNYK-JS-JSONWEBTOKEN-3180024
- https://snyk.io/vuln/SNYK-JS-JSONWEBTOKEN-3180026
2022-12-22 12:01:04 -06:00
Dave Horton
317a094b3e return account name and sp name in getUsers (#87) 2022-12-19 15:05:35 -05:00
Dave Horton
86953b9524 encode account name and sp name as part of jwt (#86) 2022-12-19 08:50:23 -05:00
Dave Horton
c6fd24bc13 restore service_provider_limits.category enum and same for account 2022-12-14 19:50:20 -05:00
Dave Horton
21a81c224f switch to jwt auth 2022-12-12 21:06:11 -05:00
Dave Horton
59445d62cc upgrade script needs to create permissions table 2022-12-12 21:03:30 -05:00
Dave Horton
4a78c5c1fc allow numbers of up to 132 chars in phone_number table 2022-12-12 13:12:54 -05:00
Dave Horton
89f25d7eda save instance_id as part of ibm stt credential 2022-12-10 09:20:44 -05:00
Dave Horton
49491fe1c4 update qs 2022-12-10 09:16:00 -05:00
dependabot[bot]
add9f2cb99 Bump express from 4.17.1 to 4.17.3 in /test/feature-server-test-scaffold (#84)
Bumps [express](https://github.com/expressjs/express) from 4.17.1 to 4.17.3.
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/master/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.17.1...4.17.3)

---
updated-dependencies:
- dependency-name: express
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-12-10 09:13:46 -05:00
EgleH
dd2176bf89 feature/user-api-calls (#80)
* initial changes for jwt auth

* return permissions as an array of string

* basic GET, POST, DELETE user api calls

* add permission checks

* hide hashed_password

* cleanup

* add check if admin user is active

* return account and serviceProvider sid un user object

* add more values to user PUT

* logout user after self delete, fix scope assignment

* add admin scope user tests

* fix test case and align jwt and api key data model in req.user

* fixes for ibm speech

* add limits license_count and voice_call_minutes

* update limits enum again

* rebase to main

* allow predefined carriers and speech credentials for Account user

* reverse the hasAccountPermissions changes

* SpeechCredentials permissions

* fix /Users/me api non-saas jambonz

Co-authored-by: Dave Horton <daveh@beachdognet.com>
Co-authored-by: eglehelms <e.helms@cognigy.com>
2022-12-10 09:12:05 -05:00
Dave Horton
fadbe116c2 update limits enum again 2022-11-26 12:55:00 -05:00
Dave Horton
02e3358c8f add limits license_count and voice_call_minutes 2022-11-25 11:01:43 -05:00
Dave Horton
7bb78a9a2a fixes for ibm speech 2022-11-25 10:55:57 -05:00
Dave Horton
5e070324ae add support for ibm speech credentials, and test for ibm tts (#82)
* add support for ibm speech credentials, and test for ibm tts

* add stt testing for ibm watson
2022-11-21 21:16:26 -05:00
Guilherme Rauen
5be286d3db update node image to the latest and most secure (#81)
Co-authored-by: Guilherme Rauen <g.rauen@cognigy.com>
2022-11-11 11:23:38 -05:00
Dave Horton
0fc8500361 added testing of microsoft stt credential 2022-11-10 17:54:40 -05:00
Dave Horton
ee5e25bb8d add support for deepgram STT 2022-11-10 14:34:04 -05:00
Dave Horton
1b67d5f89d Feature/add users api (#77)
* initial changes for jwt auth

* return permissions as an array of string

* Add JWT expiration environment variable (#74)

* allow fromHost in createCall REST API

* add JWT_EXPIRES_IN=<mins> env variable, 60 mins by default

* add jwt expiration in register.js and signin.js

* fix tests - add permissions and scope to encoded obj in jwt

Co-authored-by: Dave Horton <daveh@beachdognet.com>
Co-authored-by: eglehelms <e.helms@cognigy.com>

* return only the jwt-token in the api response

* update swagger.yaml

* add /users api

* apply review comments

* add users test case

* added User model

* bugfix: admin user should be able to create a carrier for a service provider

Co-authored-by: EgleH <egle.helms@gmail.com>
Co-authored-by: eglehelms <e.helms@cognigy.com>
2022-11-07 13:47:18 -05:00
Dave Horton
46eee0cc60 initial changes to support nuance speech (#78)
* initial changes to support nuance speech

* fixes from testing
2022-10-31 09:59:36 -04:00
Dave Horton
8d303390b7 jwt contains {scope, permissions, user_sid, account_sid, service_provider_sid}, force_change now in the body of /login response 2022-10-27 10:50:17 -04:00
Dave Horton
35214a04dc Feature/jwt auth (#75)
* initial changes for jwt auth

* return permissions as an array of string

* Add JWT expiration environment variable (#74)

* allow fromHost in createCall REST API

* add JWT_EXPIRES_IN=<mins> env variable, 60 mins by default

* add jwt expiration in register.js and signin.js

* fix tests - add permissions and scope to encoded obj in jwt

Co-authored-by: Dave Horton <daveh@beachdognet.com>
Co-authored-by: eglehelms <e.helms@cognigy.com>

* return only the jwt-token in the api response

Co-authored-by: EgleH <egle.helms@gmail.com>
Co-authored-by: eglehelms <e.helms@cognigy.com>
2022-10-27 07:39:11 -04:00
44 changed files with 4984 additions and 1706 deletions

View File

@@ -1,17 +1,15 @@
name: CI
on:
push:
workflow_dispatch:
on: [push, pull_request, workflow_dispatch]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 14
node-version: lts/*
- run: npm install
- run: npm run jslint
- run: npm test

View File

@@ -20,7 +20,7 @@ jobs:
if: github.event_name == 'push'
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Build image
run: docker build . --file Dockerfile.db-create --tag $IMAGE_NAME

View File

@@ -20,7 +20,7 @@ jobs:
if: github.event_name == 'push'
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Build image
run: docker build . --file Dockerfile --tag $IMAGE_NAME

1
.gitignore vendored
View File

@@ -1,6 +1,7 @@
# Logs
logs
*.log
run-tests.sh
# Runtime data
pids

View File

@@ -1,4 +1,4 @@
FROM --platform=linux/amd64 node:18.9.0-alpine3.16 as base
FROM --platform=linux/amd64 node:18.12.1-alpine3.16 as base
RUN apk --update --no-cache add --virtual .builds-deps build-base python3

4
app.js
View File

@@ -40,7 +40,8 @@ const {
retrieveSet,
addKey,
retrieveKey,
deleteKey
deleteKey,
getTtsVoices
} = require('@jambonz/realtimedb-helpers')({
host: process.env.JAMBONES_REDIS_HOST || 'localhost',
port: process.env.JAMBONES_REDIS_PORT || 6379
@@ -78,6 +79,7 @@ app.locals = {
addKey,
retrieveKey,
deleteKey,
getTtsVoices,
lookupAppBySid,
lookupAccountBySid,
lookupAccountByPhoneNumber,

View File

@@ -45,8 +45,6 @@ VALUES
('91cb050f-9826-4ac9-b736-84a10372a9fe', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '178.22.139.77', 32, 5060, 1, 0),
('58700fad-98bf-4d31-b61e-888c54911b35', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '185.63.140.77', 32, 5060, 1, 0),
('d020fd9e-7fdb-4bca-ae0d-e61b38142873', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '185.63.142.77', 32, 5060, 1, 0),
('441fd2e7-c845-459c-963d-6e917063ed9a', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '185.63.141.77', 32, 5060, 1, 0),
('1e0d3e80-9973-4184-9bec-07ae564f983f', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '185.63.143.77', 32, 5060, 1, 0),
('e56ec745-5f37-443f-afb4-7bbda31ae7ac', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '178.22.140.34', 32, 5060, 1, 0),
('e7447e7e-2c7d-4738-ab53-097c187236ff', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '178.22.143.66', 32, 5060, 1, 0),
('38d8520a-527f-4f8e-8456-f9dfca742561', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '172.86.225.77', 32, 5060, 1, 0),
('834f8b0c-d4c2-4f3e-93d9-cf307995eedd', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '172.86.225.88', 32, 5060, 1, 0),
('5f431d42-48e4-44ce-a311-d946f0b475b6', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', 'out.simwood.com', 32, 5060, 0, 1);

View File

@@ -22,6 +22,10 @@ DROP TABLE IF EXISTS lcr_routes;
DROP TABLE IF EXISTS password_settings;
DROP TABLE IF EXISTS user_permissions;
DROP TABLE IF EXISTS permissions;
DROP TABLE IF EXISTS predefined_sip_gateways;
DROP TABLE IF EXISTS predefined_smpp_gateways;
@@ -79,7 +83,7 @@ CREATE TABLE account_limits
(
account_limits_sid CHAR(36) NOT NULL UNIQUE ,
account_sid CHAR(36) NOT NULL,
category ENUM('api_rate','voice_call_session', 'device') NOT NULL,
category ENUM('api_rate','voice_call_session', 'device','voice_call_minutes','voice_call_session_license', 'voice_call_minutes_license') NOT NULL,
quantity INTEGER NOT NULL,
PRIMARY KEY (account_limits_sid)
);
@@ -145,6 +149,14 @@ require_digit BOOLEAN NOT NULL DEFAULT false,
require_special_character BOOLEAN NOT NULL DEFAULT false
);
CREATE TABLE permissions
(
permission_sid CHAR(36) NOT NULL UNIQUE ,
name VARCHAR(32) NOT NULL UNIQUE ,
description VARCHAR(255),
PRIMARY KEY (permission_sid)
);
CREATE TABLE predefined_carriers
(
predefined_carrier_sid CHAR(36) NOT NULL UNIQUE ,
@@ -254,7 +266,7 @@ CREATE TABLE service_provider_limits
(
service_provider_limits_sid CHAR(36) NOT NULL UNIQUE ,
service_provider_sid CHAR(36) NOT NULL,
category ENUM('api_rate','voice_call_session', 'device') NOT NULL,
category ENUM('api_rate','voice_call_session', 'device','voice_call_minutes','voice_call_session_license', 'voice_call_minutes_license') NOT NULL,
quantity INTEGER NOT NULL,
PRIMARY KEY (service_provider_limits_sid)
);
@@ -348,6 +360,14 @@ register_public_ip_in_contact BOOLEAN NOT NULL DEFAULT false,
PRIMARY KEY (voip_carrier_sid)
) COMMENT='A Carrier or customer PBX that can send or receive calls';
CREATE TABLE user_permissions
(
user_permissions_sid CHAR(36) NOT NULL UNIQUE ,
user_sid CHAR(36) NOT NULL,
permission_sid CHAR(36) NOT NULL,
PRIMARY KEY (user_permissions_sid)
);
CREATE TABLE smpp_gateways
(
smpp_gateway_sid CHAR(36) NOT NULL UNIQUE ,
@@ -365,7 +385,7 @@ PRIMARY KEY (smpp_gateway_sid)
CREATE TABLE phone_numbers
(
phone_number_sid CHAR(36) UNIQUE ,
number VARCHAR(32) NOT NULL UNIQUE ,
number VARCHAR(132) NOT NULL UNIQUE ,
voip_carrier_sid CHAR(36),
account_sid CHAR(36),
application_sid CHAR(36),
@@ -415,6 +435,7 @@ account_sid CHAR(36) COMMENT 'account that this application belongs to (if null,
call_hook_sid CHAR(36) COMMENT 'webhook to call for inbound calls ',
call_status_hook_sid CHAR(36) COMMENT 'webhook to call for call status events',
messaging_hook_sid CHAR(36) COMMENT 'webhook to call for inbound SMS/MMS ',
app_json TEXT,
speech_synthesis_vendor VARCHAR(64) NOT NULL DEFAULT 'google',
speech_synthesis_language VARCHAR(12) NOT NULL DEFAULT 'en-US',
speech_synthesis_voice VARCHAR(64),
@@ -481,6 +502,7 @@ ALTER TABLE call_routes ADD FOREIGN KEY application_sid_idxfk (application_sid)
CREATE INDEX dns_record_sid_idx ON dns_records (dns_record_sid);
ALTER TABLE dns_records ADD FOREIGN KEY account_sid_idxfk_4 (account_sid) REFERENCES accounts (account_sid);
CREATE INDEX permission_sid_idx ON permissions (permission_sid);
CREATE INDEX predefined_carrier_sid_idx ON predefined_carriers (predefined_carrier_sid);
CREATE INDEX predefined_sip_gateway_sid_idx ON predefined_sip_gateways (predefined_sip_gateway_sid);
CREATE INDEX predefined_carrier_sid_idx ON predefined_sip_gateways (predefined_carrier_sid);
@@ -561,6 +583,12 @@ ALTER TABLE voip_carriers ADD FOREIGN KEY service_provider_sid_idxfk_7 (service_
ALTER TABLE voip_carriers ADD FOREIGN KEY application_sid_idxfk_2 (application_sid) REFERENCES applications (application_sid);
CREATE INDEX user_permissions_sid_idx ON user_permissions (user_permissions_sid);
CREATE INDEX user_sid_idx ON user_permissions (user_sid);
ALTER TABLE user_permissions ADD FOREIGN KEY user_sid_idxfk (user_sid) REFERENCES users (user_sid) ON DELETE CASCADE;
ALTER TABLE user_permissions ADD FOREIGN KEY permission_sid_idxfk (permission_sid) REFERENCES permissions (permission_sid);
CREATE INDEX smpp_gateway_sid_idx ON smpp_gateways (smpp_gateway_sid);
CREATE INDEX voip_carrier_sid_idx ON smpp_gateways (voip_carrier_sid);
ALTER TABLE smpp_gateways ADD FOREIGN KEY voip_carrier_sid_idxfk (voip_carrier_sid) REFERENCES voip_carriers (voip_carrier_sid);
@@ -620,4 +648,4 @@ ALTER TABLE accounts ADD FOREIGN KEY device_calling_application_sid_idxfk (devic
ALTER TABLE accounts ADD FOREIGN KEY siprec_hook_sid_idxfk (siprec_hook_sid) REFERENCES applications (application_sid);
SET FOREIGN_KEY_CHECKS=1;
SET FOREIGN_KEY_CHECKS=1;

File diff suppressed because one or more lines are too long

View File

@@ -12,6 +12,9 @@ values (?, ?)`;
const sqlQueryAccount = 'SELECT * from accounts LEFT JOIN api_keys ON api_keys.account_sid = accounts.account_sid';
const sqlAddAccountToken = `INSERT into api_keys (api_key_sid, token, account_sid)
VALUES (?, ?, ?)`;
const sqlInsertPermissions = `
INSERT into user_permissions (user_permissions_sid, user_sid, permission_sid)
VALUES (?,?,?)`;
const password = process.env.JAMBONES_ADMIN_INITIAL_PASSWORD || 'admin';
console.log(`reset_admin_password, initial admin password is ${password}`);
@@ -21,6 +24,7 @@ const doIt = async() => {
const sid = uuidv4();
await promisePool.execute('DELETE from users where name = "admin"');
await promisePool.execute('DELETE from api_keys where account_sid is null and service_provider_sid is null');
await promisePool.execute(sqlInsert,
[
sid,
@@ -34,6 +38,12 @@ const doIt = async() => {
);
await promisePool.execute(sqlInsertAdminToken, [uuidv4(), uuidv4()]);
/* assign all permissions to the admin user */
const [p] = await promisePool.query('SELECT * from permissions');
for (const perm of p) {
await promisePool.execute(sqlInsertPermissions, [uuidv4(), sid, perm.permission_sid]);
}
/* create admin token for single account */
const [r] = await promisePool.query({sql: sqlQueryAccount, nestTables: true});
if (1 === r.length && r[0].api_keys.api_key_sid === null) {

View File

@@ -1,5 +1,12 @@
SET FOREIGN_KEY_CHECKS=0;
-- create standard permissions
insert into permissions (permission_sid, name, description)
values
('ffbc342a-546a-11ed-bdc3-0242ac120002', 'VIEW_ONLY', 'Can view data but not make changes'),
('ffbc3a10-546a-11ed-bdc3-0242ac120002', 'PROVISION_SERVICES', 'Can provision services'),
('ffbc3c5e-546a-11ed-bdc3-0242ac120002', 'PROVISION_USERS', 'Can provision users');
insert into sbc_addresses (sbc_address_sid, ipv4, port)
values('f6567ae1-bf97-49af-8931-ca014b689995', '52.55.111.178', 5060);
insert into sbc_addresses (sbc_address_sid, ipv4, port)
@@ -91,10 +98,8 @@ VALUES
('91cb050f-9826-4ac9-b736-84a10372a9fe', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '178.22.139.77', 32, 5060, 1, 0),
('58700fad-98bf-4d31-b61e-888c54911b35', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '185.63.140.77', 32, 5060, 1, 0),
('d020fd9e-7fdb-4bca-ae0d-e61b38142873', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '185.63.142.77', 32, 5060, 1, 0),
('441fd2e7-c845-459c-963d-6e917063ed9a', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '185.63.141.77', 32, 5060, 1, 0),
('1e0d3e80-9973-4184-9bec-07ae564f983f', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '185.63.143.77', 32, 5060, 1, 0),
('e56ec745-5f37-443f-afb4-7bbda31ae7ac', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '178.22.140.34', 32, 5060, 1, 0),
('e7447e7e-2c7d-4738-ab53-097c187236ff', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '178.22.143.66', 32, 5060, 1, 0),
('38d8520a-527f-4f8e-8456-f9dfca742561', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '172.86.225.77', 32, 5060, 1, 0),
('834f8b0c-d4c2-4f3e-93d9-cf307995eedd', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '172.86.225.88', 32, 5060, 1, 0),
('5f431d42-48e4-44ce-a311-d946f0b475b6', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', 'out.simwood.com', 32, 5060, 0, 1);

View File

@@ -1,5 +1,12 @@
SET FOREIGN_KEY_CHECKS=0;
-- create standard permissions
insert into permissions (permission_sid, name, description)
values
('ffbc342a-546a-11ed-bdc3-0242ac120002', 'VIEW_ONLY', 'Can view data but not make changes'),
('ffbc3a10-546a-11ed-bdc3-0242ac120002', 'PROVISION_SERVICES', 'Can provision services'),
('ffbc3c5e-546a-11ed-bdc3-0242ac120002', 'PROVISION_USERS', 'Can provision users');
-- create one service provider and account
insert into api_keys (api_key_sid, token)
values ('3f35518f-5a0d-4c2e-90a5-2407bb3b36f0', '38700987-c7a4-4685-a5bb-af378f9734de');
@@ -61,6 +68,9 @@ VALUES
('d2ccfcb1-9198-4fe9-a0ca-6e49395837c4', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '54.172.60.0', 30, 5060, 1, 0),
('6b1d0032-4430-41f1-87c6-f22233d394ef', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '54.244.51.0', 30, 5060, 1, 0),
('0de40217-8bd5-4aa8-a9fd-1994282953c6', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '54.171.127.192', 30, 5060, 1, 0),
('48b108e3-1ce7-4f18-a4cb-e41e63688bdf', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '54.171.127.193', 30, 5060, 1, 0),
('d9131a69-fe44-4c2a-ba82-4adc81f628dd', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '54.171.127.194', 30, 5060, 1, 0),
('34a6a311-4bd6-49ca-aa77-edd3cb92c6e1', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '54.171.127.195', 30, 5060, 1, 0),
('37bc0b20-b53c-4c31-95a6-f82b1c3713e3', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '35.156.191.128', 30, 5060, 1, 0),
('39791f4e-b612-4882-a37e-e92711a39f3f', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '54.65.63.192', 30, 5060, 1, 0),
('81a0c8cb-a33e-42da-8f20-99083da6f02f', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '54.252.254.64', 30, 5060, 1, 0),
@@ -85,10 +95,8 @@ VALUES
('91cb050f-9826-4ac9-b736-84a10372a9fe', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '178.22.139.77', 32, 5060, 1, 0),
('58700fad-98bf-4d31-b61e-888c54911b35', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '185.63.140.77', 32, 5060, 1, 0),
('d020fd9e-7fdb-4bca-ae0d-e61b38142873', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '185.63.142.77', 32, 5060, 1, 0),
('441fd2e7-c845-459c-963d-6e917063ed9a', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '185.63.141.77', 32, 5060, 1, 0),
('1e0d3e80-9973-4184-9bec-07ae564f983f', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '185.63.143.77', 32, 5060, 1, 0),
('e56ec745-5f37-443f-afb4-7bbda31ae7ac', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '178.22.140.34', 32, 5060, 1, 0),
('e7447e7e-2c7d-4738-ab53-097c187236ff', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '178.22.143.66', 32, 5060, 1, 0),
('38d8520a-527f-4f8e-8456-f9dfca742561', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '172.86.225.77', 32, 5060, 1, 0),
('834f8b0c-d4c2-4f3e-93d9-cf307995eedd', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '172.86.225.88', 32, 5060, 1, 0),
('5f431d42-48e4-44ce-a311-d946f0b475b6', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', 'out.simwood.com', 32, 5060, 0, 1);
SET FOREIGN_KEY_CHECKS=1;

View File

@@ -1,5 +1,12 @@
SET FOREIGN_KEY_CHECKS=0;
-- create standard permissions
insert into permissions (permission_sid, name, description)
values
('ffbc342a-546a-11ed-bdc3-0242ac120002', 'VIEW_ONLY', 'Can view data but not make changes'),
('ffbc3a10-546a-11ed-bdc3-0242ac120002', 'PROVISION_SERVICES', 'Can provision services'),
('ffbc3c5e-546a-11ed-bdc3-0242ac120002', 'PROVISION_USERS', 'Can provision users');
-- create one service provider
insert into service_providers (service_provider_sid, name, description, root_domain)
values ('2708b1b3-2736-40ea-b502-c53d8396247f', 'sip.jambonz.xyz', 'jambonz.xyz service provider', 'sip.jambonz.xyz');
@@ -65,10 +72,8 @@ VALUES
('91cb050f-9826-4ac9-b736-84a10372a9fe', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '178.22.139.77', 32, 5060, 1, 0),
('58700fad-98bf-4d31-b61e-888c54911b35', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '185.63.140.77', 32, 5060, 1, 0),
('d020fd9e-7fdb-4bca-ae0d-e61b38142873', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '185.63.142.77', 32, 5060, 1, 0),
('441fd2e7-c845-459c-963d-6e917063ed9a', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '185.63.141.77', 32, 5060, 1, 0),
('1e0d3e80-9973-4184-9bec-07ae564f983f', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '185.63.143.77', 32, 5060, 1, 0),
('e56ec745-5f37-443f-afb4-7bbda31ae7ac', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '178.22.140.34', 32, 5060, 1, 0),
('e7447e7e-2c7d-4738-ab53-097c187236ff', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '178.22.143.66', 32, 5060, 1, 0),
('38d8520a-527f-4f8e-8456-f9dfca742561', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '172.86.225.77', 32, 5060, 1, 0),
('834f8b0c-d4c2-4f3e-93d9-cf307995eedd', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '172.86.225.88', 32, 5060, 1, 0),
('5f431d42-48e4-44ce-a311-d946f0b475b6', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', 'out.simwood.com', 32, 5060, 0, 1);
SET FOREIGN_KEY_CHECKS=1;

View File

@@ -57,6 +57,36 @@ const sql = {
'ALTER TABLE `voip_carriers` ADD COLUMN `register_from_user` VARCHAR(128)',
'ALTER TABLE `voip_carriers` ADD COLUMN `register_from_domain` VARCHAR(256)',
'ALTER TABLE `voip_carriers` ADD COLUMN `register_public_ip_in_contact` BOOLEAN NOT NULL DEFAULT false'
],
'8000': [
'ALTER TABLE `applications` ADD COLUMN `app_json` TEXT',
'ALTER TABLE voip_carriers CHANGE register_public_domain_in_contact register_public_ip_in_contact BOOLEAN',
'alter table phone_numbers modify number varchar(132) NOT NULL UNIQUE',
`CREATE TABLE permissions
(
permission_sid CHAR(36) NOT NULL UNIQUE ,
name VARCHAR(32) NOT NULL UNIQUE ,
description VARCHAR(255),
PRIMARY KEY (permission_sid)
)`,
`CREATE TABLE user_permissions
(
user_permissions_sid CHAR(36) NOT NULL UNIQUE ,
user_sid CHAR(36) NOT NULL,
permission_sid CHAR(36) NOT NULL,
PRIMARY KEY (user_permissions_sid)
)`,
`CREATE TABLE password_settings
(
min_password_length INTEGER NOT NULL DEFAULT 8,
require_digit BOOLEAN NOT NULL DEFAULT false,
require_special_character BOOLEAN NOT NULL DEFAULT false
)`,
'CREATE INDEX user_permissions_sid_idx ON user_permissions (user_permissions_sid)',
'CREATE INDEX user_sid_idx ON user_permissions (user_sid)',
'ALTER TABLE user_permissions ADD FOREIGN KEY user_sid_idxfk (user_sid) REFERENCES users (user_sid) ON DELETE CASCADE',
'ALTER TABLE user_permissions ADD FOREIGN KEY permission_sid_idxfk (permission_sid) REFERENCES permissions (permission_sid)',
'ALTER TABLE `users` ADD COLUMN `is_active` BOOLEAN NOT NULL default true',
]
};
@@ -85,6 +115,7 @@ const doIt = async() => {
if (val < 7006) upgrades.push(...sql['7006']);
if (val < 7007) upgrades.push(...sql['7007']);
if (val < 8000) upgrades.push(...sql['8000']);
// perform all upgrades
logger.info({upgrades}, 'applying schema upgrades..');

View File

@@ -85,9 +85,6 @@ VALUES
-- simwood gateways
insert into predefined_sip_gateways (predefined_sip_gateway_sid, predefined_carrier_sid, ipv4, netmask, port, inbound, outbound)
VALUES
('91cb050f-9826-4ac9-b736-84a10372a9fe', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '149.91.14.0', 24, 5060, 1, 0),
('58700fad-98bf-4d31-b61e-888c54911b35', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '154.51.137.96', 27, 5060, 1, 0),
('d020fd9e-7fdb-4bca-ae0d-e61b38142873', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '78.40.245.160', 27, 5060, 1, 0),
('441fd2e7-c845-459c-963d-6e917063ed9a', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '178.22.136.24', 29, 5060, 1, 0),
('1e0d3e80-9973-4184-9bec-07ae564f983f', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '178.22.136.28', 28, 5060, 1, 0),
('e56ec745-5f37-443f-afb4-7bbda31ae7ac', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '178.22.140.48', 28, 5060, 1, 0),
@@ -98,7 +95,7 @@ VALUES
('b6ae6240-55ac-4c11-892f-a71b2155ea60', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '185.63.142.0', 26, 5060, 1, 0),
('5a976337-164b-408e-8748-d8bfb4bd5d76', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '185.63.143.0', 26, 5060, 1, 0),
('ed0434ca-7f26-4624-9523-0419d0d2924d', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '178.22.139.0', 26, 5060, 1, 0),
('d1a594c2-c14f-4ead-b621-96129bc87886', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '172.86.224.0', 24, 5060, 1, 0),
('6bfb55e5-e248-48dc-a104-4f3eedd7d7de', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '172.86.225.0', 24, 5060, 1, 0),
('5f431d42-48e4-44ce-a311-d946f0b475b6', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', 'out.simwood.com', 32, 5060, 0, 1);

View File

@@ -11,7 +11,7 @@ const sql = `
function makeStrategy(logger, retrieveKey) {
return new Strategy(
async function(token, done) {
logger.debug(`validating with token ${token}`);
//logger.debug(`validating with token ${token}`);
jwt.verify(token, process.env.JWT_SECRET, async(err, decoded) => {
if (err) {
if (err.name === 'TokenExpiredError') {
@@ -35,20 +35,21 @@ function makeStrategy(logger, retrieveKey) {
debug(err);
logger.info({err}, 'Error checking blacklist for jwt');
}
const {user_sid, account_sid, email, name} = decoded;
//logger.debug({user_sid, account_sid}, 'successfully validated jwt');
const scope = ['account'];
const {user_sid, service_provider_sid, account_sid, email, name, scope, permissions} = decoded;
const user = {
service_provider_sid,
account_sid,
user_sid,
jwt: token,
email,
name,
hasScope: (s) => s === 'account',
hasAdminAuth: false,
hasServiceProviderAuth: false,
hasAccountAuth: true
permissions,
hasScope: (s) => s === scope,
hasAdminAuth: scope === 'admin',
hasServiceProviderAuth: scope === 'service_provider',
hasAccountAuth: scope === 'account'
};
logger.debug({user}, 'successfully validated jwt');
return done(null, user, {scope});
}
});
@@ -75,24 +76,28 @@ const checkApiTokens = (logger, token, done) => {
}
// found api key
const scope = [];
let scope;
//const scope = [];
if (results[0].account_sid === null && results[0].service_provider_sid === null) {
scope.push.apply(scope, ['admin', 'service_provider', 'account']);
//scope.push.apply(scope, ['admin', 'service_provider', 'account']);
scope = 'admin';
}
else if (results[0].service_provider_sid) {
scope.push.apply(scope, ['service_provider', 'account']);
//scope.push.apply(scope, ['service_provider', 'account']);
scope = 'service_provider';
}
else {
scope.push('account');
//scope.push('account');
scope = 'account';
}
const user = {
account_sid: results[0].account_sid,
service_provider_sid: results[0].service_provider_sid,
hasScope: (s) => scope.includes(s),
hasAdminAuth: scope.length === 3,
hasServiceProviderAuth: scope.includes('service_provider'),
hasAccountAuth: scope.includes('account') && !scope.includes('service_provider')
hasScope: (s) => s === scope,
hasAdminAuth: scope === 'admin',
hasServiceProviderAuth: scope === 'service_provider',
hasAccountAuth: scope === 'account'
};
logger.info(user, `successfully validated with scope ${scope}`);
return done(null, user, {scope});

117
lib/models/user.js Normal file
View File

@@ -0,0 +1,117 @@
const Model = require('./model');
const {promisePool} = require('../db');
const sqlAll = `
SELECT u.user_sid, u.name, u.email, u.account_sid, u.service_provider_sid, u.is_active,
u.force_change, u.phone, u.pending_email, u.provider, u.provider_userid,
u.email_activation_code, u.email_validated,
sp.name as service_provider_name, acc.name as account_name
FROM users u
LEFT JOIN service_providers as sp ON u.service_provider_sid = sp.service_provider_sid
LEFT JOIN accounts acc ON u.account_sid = acc.account_sid
`;
const sqlAccount = `
SELECT u.user_sid, u.name, u.email, u.account_sid, u.service_provider_sid, u.is_active,
u.force_change, u.phone, u.pending_email, u.provider, u.provider_userid,
u.email_activation_code, u.email_validated,
sp.name as service_provider_name, acc.name as account_name
FROM users u
LEFT JOIN service_providers as sp ON u.service_provider_sid = sp.service_provider_sid
LEFT JOIN accounts acc ON u.account_sid = acc.account_sid
WHERE u.account_sid = ?
`;
const sqlSP = `
SELECT u.user_sid, u.name, u.email, u.account_sid, u.service_provider_sid, u.is_active,
u.force_change, u.phone, u.pending_email, u.provider, u.provider_userid,
u.email_activation_code, u.email_validated,
sp.name as service_provider_name, acc.name as account_name
FROM users u
LEFT JOIN service_providers as sp ON u.service_provider_sid = sp.service_provider_sid
LEFT JOIN accounts acc ON u.account_sid = acc.account_sid
WHERE u.service_provider_sid = ?
`;
class User extends Model {
constructor() {
super();
}
static async retrieveAll() {
const [rows] = await promisePool.query(sqlAll);
return rows;
}
static async retrieveAllForAccount(account_sid) {
const [rows] = await promisePool.query(sqlAccount, [account_sid]);
return rows;
}
static async retrieveAllForServiceProvider(service_provider_sid) {
const [rows] = await promisePool.query(sqlSP, [service_provider_sid]);
return rows;
}
}
User.table = 'users';
User.fields = [
{
name: 'user_sid',
type: 'string',
primaryKey: true
},
{
name: 'name',
type: 'string',
required: true
},
{
name: 'email',
type: 'string',
required: true
},
{
name: 'pending_email',
type: 'string'
},
{
name: 'phone',
type: 'string'
},
{
name: 'hashed_password',
type: 'string'
},
{
name: 'account_sid',
type: 'string'
},
{
name: 'service_provider_sid',
type: 'string'
},
{
name: 'force_change',
type: 'number'
},
{
name: 'provider',
type: 'string'
},
{
name: 'provider_userid',
type: 'string'
},
{
name: 'email_activation_code',
type: 'string'
},
{
name: 'email_validated',
type: 'number'
},
{
name: 'is_active',
type: 'number'
},
];
module.exports = User;

View File

@@ -1,6 +1,6 @@
const router = require('express').Router();
const request = require('request');
const {DbErrorBadRequest, DbErrorUnprocessableRequest} = require('../../utils/errors');
const {DbErrorBadRequest, DbErrorForbidden, DbErrorUnprocessableRequest} = require('../../utils/errors');
const Account = require('../../models/account');
const Application = require('../../models/application');
const Webhook = require('../../models/webhook');
@@ -45,6 +45,21 @@ const stripPort = (hostport) => {
return hostport;
};
const validateUpdateForCarrier = async(req) => {
const account_sid = parseAccountSid(req);
if (req.user.hasScope('admin')) return ;
if (req.user.hasScope('account')) {
if (account_sid === req.user.account_sid) return ;
throw new DbErrorForbidden('insufficient permissions to update account');
}
if (req.user.hasScope('service_provider')) {
const [r] = await promisePool.execute(
'SELECT service_provider_sid from accounts WHERE account_sid = ?', [account_sid]);
if (r.length === 1 && r[0].service_provider_sid === req.user.service_provider_sid) return;
throw new DbErrorForbidden('insufficient permissions to update account');
}
};
router.use('/:sid/SpeechCredentials', hasAccountPermissions, require('./speech-credentials'));
router.use('/:sid/RecentCalls', hasAccountPermissions, require('./recent-calls'));
router.use('/:sid/Alerts', hasAccountPermissions, require('./alerts'));
@@ -72,6 +87,20 @@ router.get('/:sid/VoipCarriers', async(req, res) => {
sysError(logger, res, err);
}
});
router.put('/:sid/VoipCarriers/:voip_carrier_sid', async(req, res) => {
const logger = req.app.locals.logger;
try {
await validateUpdateForCarrier(req);
const rowsAffected = await VoipCarrier.update(req.params.voip_carrier_sid, req.body);
if (rowsAffected === 0) {
return res.sendStatus(404);
}
res.status(204).end();
} catch (err) {
sysError(logger, res, err);
}
});
router.post('/:sid/VoipCarriers', async(req, res) => {
const logger = req.app.locals.logger;
const payload = req.body;

View File

@@ -8,9 +8,7 @@ const short = require('short-uuid');
const {promisePool} = require('../../db');
const sysError = require('../error');
const sqlSelectCarrierByName = `SELECT * FROM voip_carriers
WHERE account_sid = ?
AND name = ?`;
const sqlSelectCarrierByNameForSP = `SELECT * FROM voip_carriers
WHERE service_provider_sid = ?
AND name = ?`;
@@ -25,22 +23,20 @@ router.post('/:sid', async(req, res) => {
const {sid } = req.params;
let service_provider_sid;
const {account_sid} = req.user;
if (!account_sid) {
if (!req.user.hasScope('service_provider')) {
logger.error({user: req.user}, 'invalid creds');
return res.sendStatus(403);
}
service_provider_sid = parseServiceProviderSid(req);
} else {
service_provider_sid = req.user.service_provider_sid;
}
try {
const [template] = await PredefinedCarrier.retrieve(sid);
logger.debug({template}, `Retrieved template carrier for sid ${sid}`);
if (!template) return res.sendStatus(404);
/* make sure not to add the same carrier twice */
const [r2] = account_sid ?
await promisePool.query(sqlSelectCarrierByName, [account_sid, template.name]) :
await promisePool.query(sqlSelectCarrierByNameForSP, [service_provider_sid, template.name]);
const [r2] = await promisePool.query(sqlSelectCarrierByNameForSP, [service_provider_sid, template.name]);
if (r2.length > 0) {
template.name = `${template.name}-${short.generate()}`;

View File

@@ -6,6 +6,7 @@ const Webhook = require('../../models/webhook');
const {promisePool} = require('../../db');
const decorate = require('./decorate');
const sysError = require('../error');
const jambonzAppJsonValidator = require('../../utils/jambonz-app-json-validation');
const preconditions = {
'add': validateAdd,
'update': validateUpdate
@@ -76,6 +77,12 @@ router.post('/', async(req, res) => {
}
}
// validate app json if required
if (obj['app_json']) {
const app_json = JSON.parse(obj['app_json']);
jambonzAppJsonValidator(logger, app_json);
}
const uuid = await Application.make(obj);
res.status(201).json({sid: uuid});
} catch (err) {
@@ -179,6 +186,12 @@ router.put('/:sid', async(req, res) => {
delete obj[prop];
}
// validate app json if required
if (obj['app_json']) {
const app_json = JSON.parse(obj['app_json']);
jambonzAppJsonValidator(logger, app_json);
}
const rowsAffected = await Application.update(sid, obj);
if (rowsAffected === 0) {
return res.status(404).end();

View File

@@ -7,16 +7,16 @@ const isAdminScope = (req, res, next) => {
message: 'insufficient privileges'
});
};
const isAdminOrSPScope = (req, res, next) => {
if (req.user.hasScope('admin') || req.user.hasScope('service_provider')) return next();
res.status(403).json({
status: 'fail',
message: 'insufficient privileges'
});
};
// const isAdminOrSPScope = (req, res, next) => {
// if (req.user.hasScope('admin') || req.user.hasScope('service_provider')) return next();
// res.status(403).json({
// status: 'fail',
// message: 'insufficient privileges'
// });
// };
api.use('/BetaInviteCodes', isAdminScope, require('./beta-invite-codes'));
api.use('/ServiceProviders', isAdminOrSPScope, require('./service-providers'));
api.use('/ServiceProviders', require('./service-providers'));
api.use('/VoipCarriers', require('./voip-carriers'));
api.use('/Webhooks', require('./webhooks'));
api.use('/SipGateways', require('./sip-gateways'));
@@ -44,7 +44,7 @@ api.use('/Subscriptions', require('./subscriptions'));
api.use('/Invoices', require('./invoices'));
api.use('/InviteCodes', require('./invite-codes'));
api.use('/PredefinedCarriers', require('./predefined-carriers'));
api.use('/PasswordSettings', isAdminScope, require('./password-settings'));
api.use('/PasswordSettings', require('./password-settings'));
// messaging
api.use('/Smpps', require('./smpps')); // our smpp server info

View File

@@ -31,9 +31,9 @@ router.post('/', async(req, res) => {
const account_sid = parseAccountSid(req);
let service_provider_sid;
if (!account_sid) {
if (!req.user.hasServiceProviderAuth) {
if (!req.user.hasServiceProviderAuth && !req.user.hasAdminAuth) {
logger.error('POST /SpeechCredentials invalid credentials');
return res.send(403);
return res.sendStatus(403);
}
service_provider_sid = parseServiceProviderSid(req);
}

View File

@@ -1,12 +1,21 @@
const router = require('express').Router();
const {getMysqlConnection} = require('../../db');
const jwt = require('jsonwebtoken');
const {verifyPassword} = require('../../utils/password-utils');
const {promisePool} = require('../../db');
const Account = require('../../models/account');
const ServiceProvider = require('../../models/service-provider');
const sysError = require('../error');
const retrievePemissionsSql = `
SELECT p.name
FROM permissions p, user_permissions up
WHERE up.permission_sid = p.permission_sid
AND up.user_sid = ?
`;
const retrieveSql = 'SELECT * from users where name = ?';
const tokenSql = 'SELECT token from api_keys where account_sid IS NULL AND service_provider_sid IS NULL';
router.post('/', (req, res) => {
router.post('/', async(req, res) => {
const logger = req.app.locals.logger;
const {username, password} = req.body;
if (!username || !password) {
@@ -14,48 +23,63 @@ router.post('/', (req, res) => {
return res.sendStatus(400);
}
getMysqlConnection((err, conn) => {
if (err) {
logger.error({err}, 'Error getting db connection');
try {
const [r] = await promisePool.query(retrieveSql, username);
if (r.length === 0) {
logger.info(`Failed login attempt for user ${username}`);
return res.sendStatus(403);
}
logger.info({r}, 'successfully retrieved user account');
const isCorrect = await verifyPassword(r[0].hashed_password, password);
if (!isCorrect) return res.sendStatus(403);
const force_change = !!r[0].force_change;
const [t] = await promisePool.query(tokenSql);
if (t.length === 0) {
logger.error('Database has no admin token provisioned...run reset_admin_password');
return res.sendStatus(500);
}
conn.query(retrieveSql, [username], async(err, results) => {
conn.release();
if (err) {
logger.error({err}, 'Error getting db connection');
return res.sendStatus(500);
}
if (0 === results.length) {
logger.info(`Failed login attempt for user ${username}`);
return res.sendStatus(403);
}
logger.info({results}, 'successfully retrieved account');
const isCorrect = await verifyPassword(results[0].hashed_password, password);
if (!isCorrect) return res.sendStatus(403);
const force_change = !!results[0].force_change;
getMysqlConnection((err, conn) => {
if (err) {
logger.error({err}, 'Error getting db connection');
return res.sendStatus(500);
}
conn.query(tokenSql, (err, tokenResults) => {
conn.release();
if (err) {
logger.error({err}, 'Error getting db connection');
return res.sendStatus(500);
}
if (0 === tokenResults.length) {
logger.error('Database has no admin token provisioned...run reset_admin_password');
return res.sendStatus(500);
}
res.json({user_sid: results[0].user_sid, force_change, token: tokenResults[0].token});
});
});
});
});
const [p] = await promisePool.query(retrievePemissionsSql, r[0].user_sid);
const permissions = p.map((x) => x.name);
const obj = {user_sid: r[0].user_sid, scope: 'admin', force_change, permissions};
if (r[0].service_provider_sid && r[0].account_sid) {
const account = await Account.retrieve(r[0].account_sid);
const service_provider = await ServiceProvider.retrieve(r[0].service_provider_sid);
obj.scope = 'account';
obj.service_provider_sid = r[0].service_provider_sid;
obj.account_sid = r[0].account_sid;
obj.account_name = account[0].name;
obj.service_provider_name = service_provider[0].name;
}
else if (r[0].service_provider_sid) {
const service_provider = await ServiceProvider.retrieve(r[0].service_provider_sid);
obj.scope = 'service_provider';
obj.service_provider_sid = r[0].service_provider_sid;
obj.service_provider_name = service_provider[0].name;
}
const payload = {
scope: obj.scope,
permissions,
...(obj.service_provider_sid && {
service_provider_sid: obj.service_provider_sid,
service_provider_name: obj.service_provider_name
}),
...(obj.account_sid && {
account_sid: obj.account_sid,
account_name: obj.account_name,
service_provider_name: obj.service_provider_name
}),
user_sid: obj.user_sid
};
const token = jwt.sign(
payload,
process.env.JWT_SECRET,
{ expiresIn: parseInt(process.env.JWT_EXPIRES_IN || 60) * 60 }
);
res.json({token, ...obj});
} catch (err) {
sysError(logger, res, err);
}
});

View File

@@ -347,7 +347,7 @@ router.post('/', async(req, res) => {
account_sid: userProfile.account_sid,
email: userProfile.email,
name: userProfile.name
}, process.env.JWT_SECRET, { expiresIn: '1h' });
}, process.env.JWT_SECRET, { expiresIn: parseInt(process.env.JWT_EXPIRES_IN || 60) * 60 });
logger.debug({
user_sid: userProfile.user_sid,

View File

@@ -1,6 +1,6 @@
const router = require('express').Router();
const {promisePool} = require('../../db');
const {DbErrorUnprocessableRequest} = require('../../utils/errors');
const {DbErrorUnprocessableRequest, DbErrorForbidden} = require('../../utils/errors');
const Webhook = require('../../models/webhook');
const ServiceProvider = require('../../models/service-provider');
const Account = require('../../models/account');
@@ -27,8 +27,42 @@ WHERE voip_carrier_sid IN (
WHERE service_provider_sid = ?
)`;
/* only admin users can add a service provider */
function validateAdd(req) {
if (!req.user.hasAdminAuth) {
throw new DbErrorForbidden('only admin users can add a service provider');
}
}
async function validateRetrieve(req) {
const service_provider_sid = parseServiceProviderSid(req);
if (req.user.hasScope('admin')) return ;
if (req.user.hasScope('service_provider')) {
if (service_provider_sid === req.user.service_provider_sid) return ;
}
if (req.user.hasScope('account')) {
/* allow account users to retrieve service provider data from parent SP */
const sid = req.user.account_sid;
const [r] = await promisePool.execute('SELECT service_provider_sid from accounts WHERE account_sid = ?', [sid]);
if (r.length === 1 && r[0].service_provider_sid === req.user.service_provider_sid) return;
}
throw new DbErrorForbidden('insufficient permissions to update service provider');
}
function validateUpdate(req) {
if (req.user.hasScope('admin')) return ;
if (req.user.hasScope('service_provider')) {
const service_provider_sid = parseServiceProviderSid(req);
if (service_provider_sid === req.user.service_provider_sid) return ;
}
throw new DbErrorForbidden('insufficient permissions to update service provider');
}
/* can not delete a service provider if it has any active accounts */
async function noActiveAccounts(req, sid) {
if (!req.user.hasAdminAuth) {
throw new DbErrorForbidden('only admin users can delete a service provider');
}
const activeAccounts = await ServiceProvider.getForeignKeyReferences('accounts.service_provider_sid', sid);
if (activeAccounts > 0) throw new DbErrorUnprocessableRequest('cannot delete service provider with active accounts');
@@ -43,14 +77,18 @@ decorate(router, ServiceProvider, ['delete'], preconditions);
router.use('/:sid/RecentCalls', hasServiceProviderPermissions, require('./recent-calls'));
router.use('/:sid/Alerts', hasServiceProviderPermissions, require('./alerts'));
router.use('/:sid/SpeechCredentials', hasServiceProviderPermissions, require('./speech-credentials'));
router.use('/:sid/SpeechCredentials', require('./speech-credentials'));
router.use('/:sid/Limits', hasServiceProviderPermissions, require('./limits'));
router.use('/:sid/PredefinedCarriers', hasServiceProviderPermissions, require('./add-from-predefined-carrier'));
router.get('/:sid/Accounts', async(req, res) => {
const logger = req.app.locals.logger;
try {
await validateRetrieve(req);
const service_provider_sid = parseServiceProviderSid(req);
const results = await Account.retrieveAll(service_provider_sid);
let results = await Account.retrieveAll(service_provider_sid);
if (req.user.hasScope('account')) {
results = results.filter((r) => r.account_sid === req.user.account_sid);
}
res.status(200).json(results);
} catch (err) {
sysError(logger, res, err);
@@ -59,8 +97,12 @@ router.get('/:sid/Accounts', async(req, res) => {
router.get('/:sid/Applications', async(req, res) => {
const logger = req.app.locals.logger;
try {
await validateRetrieve(req);
const service_provider_sid = parseServiceProviderSid(req);
const results = await Application.retrieveAll(service_provider_sid);
let results = await Application.retrieveAll(service_provider_sid);
if (req.user.hasScope('account')) {
results = results.filter((r) => r.account_sid === req.user.account_sid);
}
res.status(200).json(results);
} catch (err) {
sysError(logger, res, err);
@@ -69,8 +111,12 @@ router.get('/:sid/Applications', async(req, res) => {
router.get('/:sid/PhoneNumbers', async(req, res) => {
const logger = req.app.locals.logger;
try {
await validateRetrieve(req);
const service_provider_sid = parseServiceProviderSid(req);
const results = await PhoneNumber.retrieveAllForSP(service_provider_sid);
let results = await PhoneNumber.retrieveAllForSP(service_provider_sid);
if (req.user.hasScope('account')) {
results = results.filter((r) => r.account_sid === req.user.account_sid);
}
res.status(200).json(results);
} catch (err) {
sysError(logger, res, err);
@@ -79,9 +125,15 @@ router.get('/:sid/PhoneNumbers', async(req, res) => {
router.get('/:sid/VoipCarriers', async(req, res) => {
const logger = req.app.locals.logger;
try {
await validateRetrieve(req);
const service_provider_sid = parseServiceProviderSid(req);
const results = await VoipCarrier.retrieveAllForSP(service_provider_sid);
res.status(200).json(results);
const carriers = await VoipCarrier.retrieveAllForSP(service_provider_sid);
if (req.user.hasScope('account')) {
return res.status(200).json(carriers.filter((c) => c.account_sid === req.user.account_sid || !c.account_sid));
}
res.status(200).json(carriers);
} catch (err) {
sysError(logger, res, err);
}
@@ -89,6 +141,7 @@ router.get('/:sid/VoipCarriers', async(req, res) => {
router.post('/:sid/VoipCarriers', async(req, res) => {
const logger = req.app.locals.logger;
try {
validateUpdate(req);
const service_provider_sid = parseServiceProviderSid(req);
const uuid = await VoipCarrier.make({...req.body, service_provider_sid});
res.status(201).json({sid: uuid});
@@ -99,6 +152,7 @@ router.post('/:sid/VoipCarriers', async(req, res) => {
router.put('/:sid/VoipCarriers/:voip_carrier_sid', async(req, res) => {
const logger = req.app.locals.logger;
try {
validateUpdate(req);
const rowsAffected = await VoipCarrier.update(req.params.voip_carrier_sid, req.body);
if (rowsAffected === 0) {
return res.sendStatus(404);
@@ -108,21 +162,15 @@ router.put('/:sid/VoipCarriers/:voip_carrier_sid', async(req, res) => {
sysError(logger, res, err);
}
});
router.get(':sid/Acccounts', async(req, res) => {
const logger = req.app.locals.logger;
try {
const service_provider_sid = parseServiceProviderSid(req);
const results = await Account.retrieveAll(service_provider_sid);
res.status(200).json(results);
} catch (err) {
sysError(logger, res, err);
}
});
router.get('/:sid/ApiKeys', async(req, res) => {
const logger = req.app.locals.logger;
const {sid} = req.params;
try {
const results = await ApiKey.retrieveAllForSP(sid);
await validateRetrieve(req);
let results = await ApiKey.retrieveAllForSP(sid);
if (req.user.hasScope('account')) {
results = results.filter((r) => r.account_sid === req.user.account_sid);
}
res.status(200).json(results);
await ApiKey.updateLastUsed(sid);
} catch (err) {
@@ -134,6 +182,7 @@ router.get('/:sid/ApiKeys', async(req, res) => {
router.post('/', async(req, res) => {
const logger = req.app.locals.logger;
try {
validateAdd(req);
// create webhooks if provided
const obj = Object.assign({}, req.body);
@@ -155,8 +204,15 @@ router.post('/', async(req, res) => {
/* list */
router.get('/', async(req, res) => {
const logger = req.app.locals.logger;
try {
const results = await ServiceProvider.retrieveAll();
logger.debug({results, user: req.user}, 'ServiceProvider.retrieveAll');
if (req.user.hasScope('service_provider') || req.user.hasScope('account')) {
logger.debug(`Filtering results for ${req.user.service_provider_sid}`);
return res.status(200).json(results.filter((e) => req.user.service_provider_sid === e.service_provider_sid));
}
res.status(200).json(results);
} catch (err) {
sysError(logger, res, err);
@@ -181,6 +237,8 @@ router.put('/:sid', async(req, res) => {
const sid = req.params.sid;
const logger = req.app.locals.logger;
try {
validateUpdate(req);
// create webhooks if provided
const obj = Object.assign({}, req.body);
for (const prop of ['registration_hook']) {

View File

@@ -68,7 +68,7 @@ router.post('/', async(req, res) => {
const token = jwt.sign({
user_sid: userProfile.user_sid,
account_sid: userProfile.account_sid
}, process.env.JWT_SECRET, { expiresIn: '1h' });
}, process.env.JWT_SECRET, { expiresIn: parseInt(process.env.JWT_EXPIRES_IN || 60) * 60 });
logger.debug({
user_sid: userProfile.user_sid,

View File

@@ -1,5 +1,6 @@
const router = require('express').Router();
const assert = require('assert');
const Account = require('../../models/account');
const SpeechCredential = require('../../models/speech-credential');
const sysError = require('../error');
const {decrypt, encrypt} = require('../../utils/encrypt-decrypt');
@@ -12,7 +13,12 @@ const {
testAwsStt,
testMicrosoftStt,
testMicrosoftTts,
testWellSaidTts
testWellSaidTts,
testNuanceStt,
testNuanceTts,
testDeepgramStt,
testIbmTts,
testIbmStt
} = require('../../utils/speech-utils');
const obscureKey = (key) => {
@@ -35,10 +41,17 @@ const encryptCredential = (obj) => {
aws_region,
api_key,
region,
client_id,
secret,
use_custom_tts,
custom_tts_endpoint,
use_custom_stt,
custom_stt_endpoint
custom_stt_endpoint,
tts_api_key,
tts_region,
stt_api_key,
stt_region,
instance_id
} = obj;
switch (vendor) {
@@ -78,6 +91,21 @@ const encryptCredential = (obj) => {
const wsData = JSON.stringify({api_key});
return encrypt(wsData);
case 'nuance':
assert(client_id, 'invalid nuance speech credential: client_id is required');
assert(secret, 'invalid nuance speech credential: secret is required');
const nuanceData = JSON.stringify({client_id, secret});
return encrypt(nuanceData);
case 'deepgram':
assert(api_key, 'invalid deepgram speech credential: api_key is required');
const deepgramData = JSON.stringify({api_key});
return encrypt(deepgramData);
case 'ibm':
const ibmData = JSON.stringify({tts_api_key, tts_region, stt_api_key, stt_region, instance_id});
return encrypt(ibmData);
default:
assert(false, `invalid or missing vendor: ${vendor}`);
}
@@ -91,13 +119,13 @@ router.post('/', async(req, res) => {
vendor,
} = req.body;
const account_sid = req.user.account_sid || req.body.account_sid;
let service_provider_sid;
const service_provider_sid = req.user.service_provider_sid ||
req.body.service_provider_sid || parseServiceProviderSid(req);
if (!account_sid) {
if (!req.user.hasServiceProviderAuth) {
if (!req.user.hasServiceProviderAuth && !req.user.hasAdminAuth) {
logger.error('POST /SpeechCredentials invalid credentials');
return res.send(403);
return res.sendStatus(403);
}
service_provider_sid = parseServiceProviderSid(req);
}
try {
const encrypted_credential = encryptCredential(req.body);
@@ -119,14 +147,20 @@ router.post('/', async(req, res) => {
* retrieve all speech credentials for an account
*/
router.get('/', async(req, res) => {
let service_provider_sid;
const account_sid = parseAccountSid(req);
if (!account_sid) service_provider_sid = parseServiceProviderSid(req);
const account_sid = parseAccountSid(req) || req.user.account_sid;
const service_provider_sid = parseServiceProviderSid(req) || req.user.service_provider_sid;
const logger = req.app.locals.logger;
try {
const creds = account_sid ?
await SpeechCredential.retrieveAll(account_sid) :
await SpeechCredential.retrieveAllForSP(service_provider_sid);
const credsAccount = account_sid ? await SpeechCredential.retrieveAll(account_sid) : [];
const credsSP = service_provider_sid ?
await SpeechCredential.retrieveAllForSP(service_provider_sid) :
await SpeechCredential.retrieveAllForSP((await Account.retrieve(account_sid))[0].service_provider_sid);
// filter out duplicates and discard those from other non-matching accounts
let creds = [...new Set([...credsAccount, ...credsSP].map((c) => JSON.stringify(c)))].map((c) => JSON.parse(c));
if (req.user.hasScope('account')) {
creds = creds.filter((c) => c.account_sid === req.user.account_sid || !c.account_sid);
}
res.status(200).json(creds.map((c) => {
const {credential, ...obj} = c;
@@ -160,6 +194,23 @@ router.get('/', async(req, res) => {
const o = JSON.parse(decrypt(credential));
obj.api_key = obscureKey(o.api_key);
}
else if ('nuance' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
obj.client_id = o.client_id;
obj.secret = obscureKey(o.secret);
}
else if ('deepgram' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
obj.api_key = obscureKey(o.api_key);
}
else if ('ibm' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
obj.tts_api_key = obscureKey(o.tts_api_key);
obj.tts_region = o.tts_region;
obj.stt_api_key = obscureKey(o.stt_api_key);
obj.stt_region = o.stt_region;
obj.instance_id = o.instance_id;
}
return obj;
}));
} catch (err) {
@@ -205,6 +256,23 @@ router.get('/:sid', async(req, res) => {
const o = JSON.parse(decrypt(credential));
obj.api_key = obscureKey(o.api_key);
}
else if ('nuance' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
obj.client_id = o.client_id;
obj.secret = obscureKey(o.secret);
}
else if ('deepgram' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
obj.api_key = obscureKey(o.api_key);
}
else if ('ibm' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
obj.tts_api_key = obscureKey(o.tts_api_key);
obj.tts_region = o.tts_region;
obj.stt_api_key = obscureKey(o.stt_api_key);
obj.stt_region = o.stt_region;
obj.instance_id = o.instance_id;
}
res.status(200).json(obj);
} catch (err) {
sysError(logger, res, err);
@@ -234,7 +302,7 @@ router.put('/:sid', async(req, res) => {
const sid = req.params.sid;
const logger = req.app.locals.logger;
try {
const {use_for_tts, use_for_stt, region, aws_region} = req.body;
const {use_for_tts, use_for_stt, region, aws_region, stt_region, tts_region} = req.body;
if (typeof use_for_tts === 'undefined' && typeof use_for_stt === 'undefined') {
throw new DbErrorUnprocessableRequest('use_for_tts and use_for_stt are the only updateable fields');
}
@@ -267,7 +335,9 @@ router.put('/:sid', async(req, res) => {
use_custom_tts,
custom_tts_endpoint,
use_custom_stt,
custom_stt_endpoint
custom_stt_endpoint,
stt_region,
tts_region
};
logger.info({o, newCred}, 'updating speech credential with this new credential');
obj.credential = encryptCredential(newCred);
@@ -419,6 +489,88 @@ router.get('/:sid/test', async(req, res) => {
}
}
}
else if (cred.vendor === 'nuance') {
const {getTtsVoices} = req.app.locals;
const {
client_id,
secret
} = credential;
if (cred.use_for_tts) {
try {
await testNuanceTts(logger, getTtsVoices, {
client_id,
secret
});
results.tts.status = 'ok';
SpeechCredential.ttsTestResult(sid, true);
} catch (err) {
logger.error({err}, 'error testing nuance tts');
const reason = err.statusCode === 401 ?
'invalid client_id or secret' :
(err.message || 'error accessing nuance tts service with provided credentials');
results.tts = {status: 'fail', reason};
SpeechCredential.ttsTestResult(sid, false);
}
}
if (cred.use_for_stt) {
try {
await testNuanceStt(logger, {client_id, secret});
results.stt.status = 'ok';
SpeechCredential.sttTestResult(sid, true);
} catch (err) {
results.stt = {status: 'fail', reason: err.message};
SpeechCredential.sttTestResult(sid, false);
}
}
}
else if (cred.vendor === 'deepgram') {
const {api_key} = credential;
if (cred.use_for_stt) {
try {
await testDeepgramStt(logger, {api_key});
results.stt.status = 'ok';
SpeechCredential.sttTestResult(sid, true);
} catch (err) {
results.stt = {status: 'fail', reason: err.message};
SpeechCredential.sttTestResult(sid, false);
}
}
}
else if (cred.vendor === 'ibm') {
const {getTtsVoices} = req.app.locals;
if (cred.use_for_tts) {
const {tts_api_key, tts_region} = credential;
try {
await testIbmTts(logger, getTtsVoices, {
tts_api_key,
tts_region
});
results.tts.status = 'ok';
SpeechCredential.ttsTestResult(sid, true);
} catch (err) {
logger.error({err}, 'error testing ibm tts');
const reason = err.statusCode === 401 ?
'invalid api_key or region' :
(err.message || 'error accessing ibm tts service with provided credentials');
results.tts = {status: 'fail', reason};
SpeechCredential.ttsTestResult(sid, false);
}
}
if (cred.use_for_stt) {
const {stt_api_key, stt_region, instance_id} = credential;
try {
await testIbmStt(logger, {stt_region, stt_api_key, instance_id});
results.stt.status = 'ok';
SpeechCredential.sttTestResult(sid, true);
} catch (err) {
results.stt = {status: 'fail', reason: err.message};
SpeechCredential.sttTestResult(sid, false);
}
}
}
res.status(200).json(results);
} catch (err) {
sysError(logger, res, err);

View File

@@ -1,6 +1,7 @@
//const assert = require('assert');
//const debug = require('debug')('jambonz:api-server');
const router = require('express').Router();
const User = require('../../models/user');
const jwt = require('jsonwebtoken');
const request = require('request');
const {DbErrorBadRequest} = require('../../utils/errors');
const {generateHashedPassword, verifyPassword} = require('../../utils/password-utils');
const {promisePool} = require('../../db');
@@ -11,6 +12,11 @@ FROM users user
JOIN accounts AS account ON account.account_sid = user.account_sid
LEFT JOIN service_providers as sp ON account.service_provider_sid = sp.service_provider_sid
WHERE user.user_sid = ?`;
const retrieveMyDetails2 = `SELECT *
FROM users user
LEFT JOIN accounts AS account ON account.account_sid = user.account_sid
LEFT JOIN service_providers as sp ON sp.service_provider_sid = user.service_provider_sid
WHERE user.user_sid = ?`;
const retrieveSql = 'SELECT * from users where user_sid = ?';
const retrieveProducts = `SELECT *
FROM account_products
@@ -23,7 +29,15 @@ const updateSql = 'UPDATE users set hashed_password = ?, force_change = false WH
const retrieveStaticIps = 'SELECT * FROM account_static_ips WHERE account_sid = ?';
const validateRequest = async(user_sid, payload) => {
const {old_password, new_password, name, email, email_activation_code} = payload;
const {
old_password,
new_password,
initial_password,
name,
email,
email_activation_code,
force_change,
is_active} = payload;
const [r] = await promisePool.query(retrieveSql, user_sid);
if (r.length === 0) return null;
@@ -37,94 +51,195 @@ const validateRequest = async(user_sid, payload) => {
throw new DbErrorBadRequest('can not change password when using oauth2');
}
if ((email && !email_activation_code) || (email_activation_code && !email)) {
if (email_activation_code && !email) {
throw new DbErrorBadRequest('email and email_activation_code both required');
}
if (!name && !new_password && !email) throw new DbErrorBadRequest('no updates requested');
if (!name && !new_password && !email && !initial_password && !force_change && !is_active)
throw new DbErrorBadRequest('no updates requested');
return user;
};
router.get('/', async(req, res) => {
const logger = req.app.locals.logger;
const token = req.user.jwt;
const decodedJwt = jwt.verify(token, process.env.JWT_SECRET);
let usersList;
try {
let results;
if (decodedJwt.scope === 'admin') {
results = await User.retrieveAll();
}
else if (decodedJwt.scope === 'account') {
results = await User.retrieveAllForAccount(decodedJwt.account_sid, true);
}
else if (decodedJwt.scope === 'service_provider') {
results = await User.retrieveAllForServiceProvider(decodedJwt.service_provider_sid, true);
}
else {
throw new DbErrorBadRequest(`invalid scope: ${decodedJwt.scope}`);
}
if (results.length === 0) throw new Error('failure retrieving users list');
usersList = results.map((user) => {
const {
user_sid,
name,
email,
force_change,
is_active,
account_sid,
service_provider_sid,
account_name,
service_provider_name
} = user;
let scope;
if (account_sid && service_provider_sid) {
scope = 'account';
} else if (service_provider_sid) {
scope = 'service_provider';
} else {
scope = 'admin';
}
const obj = {
user_sid,
name,
email,
scope,
force_change,
is_active,
...(account_sid && {account_sid}),
...(account_name && {account_name}),
...(service_provider_sid && {service_provider_sid}),
...(service_provider_name && {service_provider_name})
};
return obj;
});
} catch (err) {
sysError(logger, res, err);
}
res.status(200).json(usersList);
});
router.get('/me', async(req, res) => {
const logger = req.app.locals.logger;
const {user_sid} = req.user;
if (!user_sid) return res.sendStatus(403);
let payload;
try {
const [r] = await promisePool.query({sql: retrieveMyDetails, nestTables: true}, user_sid);
logger.debug(r, 'retrieved user details');
const payload = r[0];
const {user, account, sp} = payload;
['hashed_password', 'salt', 'phone_activation_code', 'email_activation_code', 'account_sid'].forEach((prop) => {
delete user[prop];
});
['email_validated', 'phone_validated', 'force_change'].forEach((prop) => user[prop] = !!user[prop]);
['is_active'].forEach((prop) => account[prop] = !!account[prop]);
account.root_domain = sp.root_domain;
delete payload.sp;
/* get api keys */
const [keys] = await promisePool.query('SELECT * from api_keys WHERE account_sid = ?', account.account_sid);
payload.api_keys = keys.map((k) => {
return {
api_key_sid: k.api_key_sid,
//token: k.token.replace(/.(?=.{4,}$)/g, '*'),
token: k.token,
last_used: k.last_used,
created_at: k.created_at
};
});
/* get products */
const [products] = await promisePool.query({sql: retrieveProducts, nestTables: true}, account.account_sid);
if (!products.length || !products[0].account_subscriptions) {
throw new Error('account is missing a subscription');
}
const account_subscription = products[0].account_subscriptions;
payload.subscription = {
status: 'active',
account_subscription_sid: account_subscription.account_subscription_sid,
start_date: account_subscription.effective_start_date,
products: products.map((prd) => {
return {
name: prd.products.name,
units: prd.products.unit_label,
quantity: prd.account_products.quantity
};
})
};
if (account_subscription.pending) {
Object.assign(payload.subscription, {
status: 'suspended',
suspend_reason: account_subscription.pending_reason
if (process.env.JAMBONES_HOSTING) {
const [r] = await promisePool.query({sql: retrieveMyDetails, nestTables: true}, user_sid);
logger.debug(r, 'retrieved user details');
payload = r[0];
const {user, account, sp} = payload;
['hashed_password', 'salt', 'phone_activation_code', 'email_activation_code', 'account_sid'].forEach((prop) => {
delete user[prop];
});
}
const {
last4,
exp_month,
exp_year,
card_type,
stripe_statement_descriptor
} = account_subscription;
if (last4) {
const real_last4 = decrypt(last4);
Object.assign(payload.subscription, {
last4: real_last4,
['email_validated', 'phone_validated', 'force_change'].forEach((prop) => user[prop] = !!user[prop]);
['is_active'].forEach((prop) => account[prop] = !!account[prop]);
account.root_domain = sp.root_domain;
delete payload.sp;
/* get api keys */
const [keys] = await promisePool.query('SELECT * from api_keys WHERE account_sid = ?', account.account_sid);
payload.api_keys = keys.map((k) => {
return {
api_key_sid: k.api_key_sid,
//token: k.token.replace(/.(?=.{4,}$)/g, '*'),
token: k.token,
last_used: k.last_used,
created_at: k.created_at
};
});
/* get products */
const [products] = await promisePool.query({sql: retrieveProducts, nestTables: true}, account.account_sid);
if (!products.length || !products[0].account_subscriptions) {
throw new Error('account is missing a subscription');
}
const account_subscription = products[0].account_subscriptions;
payload.subscription = {
status: 'active',
account_subscription_sid: account_subscription.account_subscription_sid,
start_date: account_subscription.effective_start_date,
products: products.map((prd) => {
return {
name: prd.products.name,
units: prd.products.unit_label,
quantity: prd.account_products.quantity
};
})
};
if (account_subscription.pending) {
Object.assign(payload.subscription, {
status: 'suspended',
suspend_reason: account_subscription.pending_reason
});
}
const {
last4,
exp_month,
exp_year,
card_type,
statement_descriptor: stripe_statement_descriptor
stripe_statement_descriptor
} = account_subscription;
if (last4) {
const real_last4 = decrypt(last4);
Object.assign(payload.subscription, {
last4: real_last4,
exp_month,
exp_year,
card_type,
statement_descriptor: stripe_statement_descriptor
});
}
/* get static ips */
const [static_ips] = await promisePool.query(retrieveStaticIps, account.account_sid);
payload.static_ips = static_ips.map((r) => r.public_ipv4);
}
else {
const [r] = await promisePool.query({sql: retrieveMyDetails2, nestTables: true}, user_sid);
logger.debug(r, 'retrieved user details');
payload = r[0];
const {user} = payload;
['hashed_password', 'salt', 'phone_activation_code', 'email_activation_code'].forEach((prop) => {
delete user[prop];
});
['email_validated', 'phone_validated', 'force_change'].forEach((prop) => user[prop] = !!user[prop]);
}
logger.debug({payload}, 'returning user details');
res.json(payload);
} catch (err) {
sysError(logger, res, err);
}
});
router.get('/:user_sid', async(req, res) => {
const logger = req.app.locals.logger;
const token = req.user.jwt;
const decodedJwt = jwt.verify(token, process.env.JWT_SECRET);
const {user_sid} = req.params;
try {
const [user] = await User.retrieve(user_sid);
// eslint-disable-next-line no-unused-vars
const {hashed_password, ...rest} = user;
if (!user) throw new Error('failure retrieving user');
if (decodedJwt.scope === 'admin' ||
decodedJwt.scope === 'account' && decodedJwt.account_sid === user.account_sid ||
decodedJwt.scope === 'service_provider' && decodedJwt.service_provider_sid === user.service_provider_sid) {
res.status(200).json(rest);
} else {
res.sendStatus(403);
}
/* get static ips */
const [static_ips] = await promisePool.query(retrieveStaticIps, account.account_sid);
payload.static_ips = static_ips.map((r) => r.public_ipv4);
logger.debug({payload}, 'returning user details');
res.json(payload);
} catch (err) {
sysError(logger, res, err);
}
@@ -133,9 +248,30 @@ router.get('/me', async(req, res) => {
router.put('/:user_sid', async(req, res) => {
const logger = req.app.locals.logger;
const {user_sid} = req.params;
const {old_password, new_password, name, email, email_activation_code} = req.body;
const user = await User.retrieve(user_sid);
const token = req.user.jwt;
const decodedJwt = jwt.verify(token, process.env.JWT_SECRET);
const {
old_password,
new_password,
initial_password,
email_activation_code,
email,
name,
is_active,
force_change,
account_sid,
service_provider_sid
} = req.body;
if (req.user.user_sid && req.user.user_sid !== user_sid) return res.sendStatus(403);
//if (req.user.user_sid && req.user.user_sid !== user_sid) return res.sendStatus(403);
if (decodedJwt.scope !== 'admin' &&
!(decodedJwt.scope === 'account' && decodedJwt.account_sid === user[0].account_sid) &&
!(decodedJwt.scope === 'service_provider' && decodedJwt.service_provider_sid === user[0].service_provider_sid) &&
(req.user.user_sid && req.user.user_sid !== user_sid)) {
return res.sendStatus(403);
}
try {
const user = await validateRequest(user_sid, req.body);
@@ -149,6 +285,11 @@ router.put('/:user_sid', async(req, res) => {
//debug(`PUT /Users/:sid pwd ${old_password} does not match hash ${old_hashed_password}`);
return res.sendStatus(403);
}
if (old_password === new_password) {
throw new Error('new password cannot be your old password');
}
const passwordHash = await generateHashedPassword(new_password);
//debug(`updating hashed_password to ${passwordHash}`);
const r = await promisePool.execute(updateSql, [passwordHash, user_sid]);
@@ -160,10 +301,51 @@ router.put('/:user_sid', async(req, res) => {
if (0 === r.changedRows) throw new Error('database update failed');
}
if (email) {
if (initial_password) {
const passwordHash = await generateHashedPassword(initial_password);
const r = await promisePool.execute(
'UPDATE users SET email = ?, email_activation_code = ?, email_validated = 0 WHERE user_sid = ?',
[email, email_activation_code, user_sid]);
'UPDATE users SET hashed_password = ? WHERE user_sid = ?',
[passwordHash, user_sid]
);
if (0 === r.changedRows) throw new Error('database update failed');
}
if (typeof is_active !== 'undefined') {
const r = await promisePool.execute('UPDATE users SET is_active = ? WHERE user_sid = ?', [is_active, user_sid]);
if (0 === r.changedRows) throw new Error('database update failed');
}
if (typeof force_change !== 'undefined') {
const r = await promisePool.execute(
'UPDATE users SET force_change = ? WHERE user_sid = ?',
[force_change, user_sid]);
if (0 === r.changedRows) throw new Error('database update failed');
}
if (account_sid || account_sid === null) {
const r = await promisePool.execute(
'UPDATE users SET account_sid = ? WHERE user_sid = ?',
[account_sid, user_sid]);
if (0 === r.changedRows) throw new Error('database update failed');
}
if (service_provider_sid || service_provider_sid === null) {
const r = await promisePool.execute(
'UPDATE users SET service_provider_sid = ? WHERE user_sid = ?',
[service_provider_sid, user_sid]);
if (0 === r.changedRows) throw new Error('database update failed');
}
if (email) {
if (email_activation_code) {
const r = await promisePool.execute(
'UPDATE users SET email = ?, email_activation_code = ?, email_validated = 0 WHERE user_sid = ?',
[email, email_activation_code, user_sid]);
if (0 === r.changedRows) throw new Error('database update failed');
}
const r = await promisePool.execute(
'UPDATE users SET email = ? WHERE user_sid = ?',
[email, user_sid]);
if (0 === r.changedRows) throw new Error('database update failed');
if (process.env.NODE_ENV !== 'test') {
@@ -176,5 +358,101 @@ router.put('/:user_sid', async(req, res) => {
}
});
router.post('/', async(req, res) => {
const logger = req.app.locals.logger;
const passwordHash = await generateHashedPassword(req.body.initial_password);
const payload = {
...req.body,
provider: 'local',
hashed_password: passwordHash,
};
const allUsers = await User.retrieveAll();
const token = req.user.jwt;
const decodedJwt = jwt.verify(token, process.env.JWT_SECRET);
delete payload.initial_password;
try {
const email = allUsers.find((e) => e.email === payload.email);
const name = allUsers.find((e) => e.name === payload.name);
if (name) {
logger.debug({payload}, 'user with this username already exists');
return res.status(422).json({msg: 'user with this username already exists'});
}
if (email) {
logger.debug({payload}, 'user with this email already exists');
return res.status(422).json({msg: 'user with this email already exists'});
}
if (decodedJwt.scope === 'admin') {
logger.debug({payload}, 'POST /users');
const uuid = await User.make(payload);
res.status(201).json({user_sid: uuid});
}
else if (decodedJwt.scope === 'account') {
logger.debug({payload}, 'POST /users');
const uuid = await User.make({
...payload,
account_sid: decodedJwt.account_sid,
});
res.status(201).json({user_sid: uuid});
}
else if (decodedJwt.scope === 'service_provider') {
logger.debug({payload}, 'POST /users');
const uuid = await User.make({
...payload,
service_provider_sid: decodedJwt.service_provider_sid,
});
res.status(201).json({user_sid: uuid});
}
else {
throw new DbErrorBadRequest(`invalid scope: ${decodedJwt.scope}`);
}
} catch (err) {
sysError(logger, res, err);
}
});
router.delete('/:user_sid', async(req, res) => {
const logger = req.app.locals.logger;
const {user_sid} = req.params;
const token = req.user.jwt;
const decodedJwt = jwt.verify(token, process.env.JWT_SECRET);
const allUsers = await User.retrieveAll();
const activeAdminUsers = allUsers.filter((e) => !e.account_sid && !e.service_provider_sid && e.is_active);
const user = await User.retrieve(user_sid);
try {
if (decodedJwt.scope === 'admin' && activeAdminUsers.length === 1) {
throw new Error('cannot delete this admin user - there are no other active admin users');
}
if (decodedJwt.scope === 'admin' ||
(decodedJwt.scope === 'account' && decodedJwt.account_sid === user[0].account_sid) ||
(decodedJwt.scope === 'service_provider' && decodedJwt.service_provider_sid === user[0].service_provider_sid)) {
await User.remove(user_sid);
//logout user after self-delete
if (decodedJwt.user_sid === user_sid) {
request({
url:'http://localhost:3000/v1/logout',
method: 'POST',
}, (err) => {
if (err) {
logger.error(err, 'could not log out user');
return res.sendStatus(500);
}
logger.debug({user}, 'user deleted and logged out');
});
}
return res.sendStatus(204);
} else {
throw new DbErrorBadRequest(`invalid scope: ${decodedJwt.scope}`);
}
} catch (err) {
sysError(logger, res, err);
}
});
module.exports = router;

View File

@@ -149,6 +149,7 @@ const parseAccountSid = (req) => {
const hasAccountPermissions = (req, res, next) => {
if (req.user.hasScope('admin')) return next();
if (req.user.hasScope('service_provider')) return next();
if (req.user.hasScope('account')) {
const account_sid = parseAccountSid(req);
if (account_sid === req.user.account_sid) return next();

View File

@@ -515,6 +515,28 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
/Users:
get:
summary: list all users
operationId: listUsers
responses:
200:
description: list of users
content:
application/json:
schema:
type:
array
items:
$ref: '#/components/schemas/Users'
403:
description: unauthorized
500:
description: system error
content:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
/Users/{UserSid}:
parameters:
- name: UserSid
@@ -524,6 +546,42 @@ paths:
explode: false
schema:
type: string
get:
summary: retrieve user information
operationId: getUser
requestBody:
content:
application/json:
schema:
type: object
properties:
name:
type: string
email:
type: string
is_active:
type: boolean
force_change:
type: boolean
scope:
type: string
permissions:
type: array
responses:
204:
description: user information
403:
description: user information
content:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
500:
description: system error
content:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
put:
summary: update user information
operationId: updateUser
@@ -545,11 +603,21 @@ paths:
new_password:
type: string
description: new password
name:
type: string
is_active:
type: boolean
force_change:
type: boolean
scope:
type: string
permissions:
type: array
responses:
204:
description: user updated
403:
description: password change failed
description: user update failed
content:
application/json:
schema:
@@ -560,6 +628,63 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
post:
summary: create a new user
operationId: createUser
requestBody:
content:
application/json:
schema:
type: object
properties:
name:
type: string
email:
type: string
is_active:
type: boolean
force_change:
type: boolean
scope:
type: string
permissions:
type: array
force_change:
type: boolean
old_password:
type: string
description: existing password, which is to be replaced
responses:
204:
description: user created
403:
description: user creation failed
content:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
500:
description: system error
content:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
delete:
summary: delete a user
operationId: deleteUser
responses:
204:
description: user deleted
404:
description: user not found
403:
description: unauthorized
500:
description: system error
content:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
/Users/me:
get:
summary: retrieve details about logged-in user and associated account
@@ -3257,6 +3382,9 @@ paths:
messaging_hook:
$ref: '#/components/schemas/Webhook'
description: application webhook to handle inbound SMS/MMS messages
app_json:
type: string
description: Voice Application Json, call_hook will not be invoked if app_json is provided
speech_synthesis_vendor:
type: string
speech_synthesis_voice:

View File

@@ -0,0 +1,112 @@
const {DbErrorBadRequest} = require('../utils/errors');
const assert = require('assert');
const _specData = require('./specs');
const specs = new Map();
for (const key in _specData) { specs.set(key, _specData[key]); }
function normalizeJambones(logger, obj) {
if (!Array.isArray(obj)) throw new DbErrorBadRequest('malformed jambonz payload: must be array');
const document = [];
for (const tdata of obj) {
if (typeof tdata !== 'object') throw new DbErrorBadRequest('malformed jambonz payload: must be array of objects');
if ('verb' in tdata) {
// {verb: 'say', text: 'foo..bar'..}
const name = tdata.verb;
const o = {};
Object.keys(tdata)
.filter((k) => k !== 'verb')
.forEach((k) => o[k] = tdata[k]);
const o2 = {};
o2[name] = o;
document.push(o2);
}
else if (Object.keys(tdata).length === 1) {
// {'say': {..}}
document.push(tdata);
}
else {
logger.info(tdata, 'malformed jambonz payload: missing verb property');
throw new DbErrorBadRequest('malformed jambonz payload: missing verb property');
}
}
logger.debug({ document }, `normalizeJambones: returning document with ${document.length} tasks`);
return document;
}
function validate(logger, obj) {
normalizeJambones(logger, obj).map((tdata) => {
const keys = Object.keys(tdata);
const name = keys[0];
const data = tdata[name];
validateVerb(name, data, logger);
});
}
function validateVerb(name, data, logger) {
logger.debug(`validating ${name} with data ${JSON.stringify(data)}`);
// validate the instruction is supported
if (!specs.has(name)) throw new DbErrorBadRequest(`invalid instruction: ${name}`);
// check type of each element and make sure required elements are present
const specData = specs.get(name);
let required = specData.required || [];
for (const dKey in data) {
if (dKey in specData.properties) {
const dVal = data[dKey];
const dSpec = specData.properties[dKey];
logger.debug(`Task:validate validating property ${dKey} with value ${JSON.stringify(dVal)}`);
if (typeof dSpec === 'string' && dSpec === 'array') {
if (!Array.isArray(dVal)) throw new DbErrorBadRequest(`${name}: property ${dKey} is not an array`);
}
else if (typeof dSpec === 'string' && dSpec.includes('|')) {
const types = dSpec.split('|').map((t) => t.trim());
if (!types.includes(typeof dVal) && !(types.includes('array') && Array.isArray(dVal))) {
throw new DbErrorBadRequest(`${name}: property ${dKey} has invalid data type, must be one of ${types}`);
}
}
else if (typeof dSpec === 'string' && ['number', 'string', 'object', 'boolean'].includes(dSpec)) {
// simple types
if (typeof dVal !== specData.properties[dKey]) {
throw new DbErrorBadRequest(`${name}: property ${dKey} has invalid data type`);
}
}
else if (Array.isArray(dSpec) && dSpec[0].startsWith('#')) {
const name = dSpec[0].slice(1);
for (const item of dVal) {
validate(name, item);
}
}
else if (typeof dSpec === 'object') {
// complex types
const type = dSpec.type;
assert.ok(['number', 'string', 'object', 'boolean'].includes(type),
`invalid or missing type in spec ${JSON.stringify(dSpec)}`);
if (type === 'string' && dSpec.enum) {
assert.ok(Array.isArray(dSpec.enum), `enum must be an array ${JSON.stringify(dSpec.enum)}`);
if (!dSpec.enum.includes(dVal)) {
throw new DbErrorBadRequest(`invalid value ${dVal} must be one of ${dSpec.enum}`);
}
}
}
else if (typeof dSpec === 'string' && dSpec.startsWith('#')) {
// reference to another datatype (i.e. nested type)
const name = dSpec.slice(1);
//const obj = {};
//obj[name] = dVal;
validate(name, dVal);
}
else {
assert.ok(0, `invalid spec ${JSON.stringify(dSpec)}`);
}
required = required.filter((item) => item !== dKey);
}
else if (dKey === '_') {
/* no op: allow arbitrary info to be carried here, used by conference e.g in transfer */
}
else throw new DbErrorBadRequest(`${name}: unknown property ${dKey}`);
}
if (required.length > 0) throw new DbErrorBadRequest(`${name}: missing value for ${required}`);
}
module.exports = validate;

759
lib/utils/specs.json Normal file
View File

@@ -0,0 +1,759 @@
{
"sip:decline": {
"properties": {
"id": "string",
"status": "number",
"reason": "string",
"headers": "object"
},
"required": [
"status"
]
},
"sip:request": {
"properties": {
"id": "string",
"method": "string",
"body": "string",
"headers": "object",
"actionHook": "object|string"
},
"required": [
"method"
]
},
"sip:refer": {
"properties": {
"id": "string",
"referTo": "string",
"referredBy": "string",
"headers": "object",
"actionHook": "object|string",
"eventHook": "object|string"
},
"required": [
"referTo"
]
},
"config": {
"properties": {
"id": "string",
"synthesizer": "#synthesizer",
"recognizer": "#recognizer",
"bargeIn": "#bargeIn",
"record": "#recordOptions",
"amd": "#amd",
"notifyEvents": "boolean"
},
"required": []
},
"bargeIn": {
"properties": {
"enable": "boolean",
"sticky": "boolean",
"actionHook": "object|string",
"input": "array",
"finishOnKey": "string",
"numDigits": "number",
"minDigits": "number",
"maxDigits": "number",
"interDigitTimeout": "number",
"dtmfBargein": "boolean",
"minBargeinWordCount": "number"
},
"required": [
"enable"
]
},
"dequeue": {
"properties": {
"id": "string",
"name": "string",
"actionHook": "object|string",
"timeout": "number",
"beep": "boolean"
},
"required": [
"name"
]
},
"enqueue": {
"properties": {
"id": "string",
"name": "string",
"actionHook": "object|string",
"waitHook": "object|string",
"_": "object"
},
"required": [
"name"
]
},
"leave": {
"properties": {
"id": "string"
}
},
"hangup": {
"properties": {
"id": "string",
"headers": "object"
},
"required": [
]
},
"play": {
"properties": {
"id": "string",
"url": "string|array",
"loop": "number|string",
"earlyMedia": "boolean",
"seekOffset": "number|string",
"timeoutSecs": "number|string",
"actionHook": "object|string"
},
"required": [
"url"
]
},
"say": {
"properties": {
"id": "string",
"text": "string|array",
"loop": "number|string",
"synthesizer": "#synthesizer",
"earlyMedia": "boolean",
"disableTtsCache": "boolean"
},
"required": [
"text"
]
},
"gather": {
"properties": {
"id": "string",
"actionHook": "object|string",
"finishOnKey": "string",
"input": "array",
"numDigits": "number",
"minDigits": "number",
"maxDigits": "number",
"interDigitTimeout": "number",
"partialResultHook": "object|string",
"speechTimeout": "number",
"listenDuringPrompt": "boolean",
"dtmfBargein": "boolean",
"bargein": "boolean",
"minBargeinWordCount": "number",
"timeout": "number",
"recognizer": "#recognizer",
"play": "#play",
"say": "#say"
},
"required": [
]
},
"conference": {
"properties": {
"id": "string",
"name": "string",
"beep": "boolean",
"startConferenceOnEnter": "boolean",
"endConferenceOnExit": "boolean",
"maxParticipants": "number",
"joinMuted": "boolean",
"actionHook": "object|string",
"waitHook": "object|string",
"statusEvents": "array",
"statusHook": "object|string",
"enterHook": "object|string",
"record": "#record"
},
"required": [
"name"
]
},
"dial": {
"properties": {
"id": "string",
"actionHook": "object|string",
"answerOnBridge": "boolean",
"callerId": "string",
"confirmHook": "object|string",
"referHook": "object|string",
"dialMusic": "string",
"dtmfCapture": "object",
"dtmfHook": "object|string",
"headers": "object",
"listen": "#listen",
"target": ["#target"],
"timeLimit": "number",
"timeout": "number",
"proxy": "string",
"transcribe": "#transcribe",
"amd": "#amd"
},
"required": [
"target"
]
},
"dialogflow": {
"properties": {
"id": "string",
"credentials": "object|string",
"project": "string",
"environment": "string",
"region": {
"type": "string",
"enum": ["europe-west1", "europe-west2", "australia-southeast1", "asia-northeast1"]
},
"lang": "string",
"actionHook": "object|string",
"eventHook": "object|string",
"events": "[string]",
"welcomeEvent": "string",
"welcomeEventParams": "object",
"noInputTimeout": "number",
"noInputEvent": "string",
"passDtmfAsTextInput": "boolean",
"thinkingMusic": "string",
"tts": "#synthesizer",
"bargein": "boolean"
},
"required": [
"project",
"credentials",
"lang"
]
},
"dtmf": {
"properties": {
"id": "string",
"dtmf": "string",
"duration": "number"
},
"required": [
"dtmf"
]
},
"lex": {
"properties": {
"id": "string",
"botId": "string",
"botAlias": "string",
"credentials": "object",
"region": "string",
"locale": "string",
"intent": "#lexIntent",
"welcomeMessage": "string",
"metadata": "object",
"bargein": "boolean",
"passDtmf": "boolean",
"actionHook": "object|string",
"eventHook": "object|string",
"noInputTimeout": "number",
"tts": "#synthesizer"
},
"required": [
"botId",
"botAlias",
"region",
"credentials"
]
},
"listen": {
"properties": {
"id": "string",
"actionHook": "object|string",
"auth": "#auth",
"finishOnKey": "string",
"maxLength": "number",
"metadata": "object",
"mixType": {
"type": "string",
"enum": ["mono", "stereo", "mixed"]
},
"passDtmf": "boolean",
"playBeep": "boolean",
"sampleRate": "number",
"timeout": "number",
"transcribe": "#transcribe",
"url": "string",
"wsAuth": "#auth",
"earlyMedia": "boolean"
},
"required": [
"url"
]
},
"message": {
"properties": {
"id": "string",
"carrier": "string",
"account_sid": "string",
"message_sid": "string",
"to": "string",
"from": "string",
"text": "string",
"media": "string|array",
"actionHook": "object|string"
},
"required": [
"to",
"from"
]
},
"pause": {
"properties": {
"id": "string",
"length": "number"
},
"required": [
"length"
]
},
"rasa": {
"properties": {
"id": "string",
"url": "string",
"recognizer": "#recognizer",
"tts": "#synthesizer",
"prompt": "string",
"actionHook": "object|string",
"eventHook": "object|string"
},
"required": [
"url"
]
},
"record": {
"properties": {
"path": "string"
},
"required": [
"path"
]
},
"recordOptions": {
"properties": {
"action": {
"type": "string",
"enum": ["startCallRecording", "stopCallRecording", "pauseCallRecording", "resumeCallRecording"]
},
"recordingID": "string",
"siprecServerURL": "string"
},
"required": [
"action"
]
},
"redirect": {
"properties": {
"id": "string",
"actionHook": "object|string"
},
"required": [
"actionHook"
]
},
"rest:dial": {
"properties": {
"id": "string",
"account_sid": "string",
"application_sid": "string",
"call_hook": "object|string",
"call_status_hook": "object|string",
"from": "string",
"fromHost": "string",
"speech_synthesis_vendor": "string",
"speech_synthesis_voice": "string",
"speech_synthesis_language": "string",
"speech_recognizer_vendor": "string",
"speech_recognizer_language": "string",
"tag": "object",
"to": "#target",
"headers": "object",
"timeout": "number"
},
"required": [
"call_hook",
"from",
"to"
]
},
"tag": {
"properties": {
"id": "string",
"data": "object"
},
"required": [
"data"
]
},
"transcribe": {
"properties": {
"id": "string",
"transcriptionHook": "string",
"recognizer": "#recognizer",
"earlyMedia": "boolean"
},
"required": [
"recognizer"
]
},
"target": {
"properties": {
"type": {
"type": "string",
"enum": ["phone", "sip", "user", "teams"]
},
"confirmHook": "object|string",
"method": {
"type": "string",
"enum": ["GET", "POST"]
},
"headers": "object",
"from": "#dialFrom",
"name": "string",
"number": "string",
"sipUri": "string",
"auth": "#auth",
"vmail": "boolean",
"tenant": "string",
"trunk": "string",
"overrideTo": "string"
},
"required": [
"type"
]
},
"dialFrom": {
"properties": {
"user": "string",
"host": "string"
},
"required": [
]
},
"auth": {
"properties": {
"username": "string",
"password": "string"
},
"required": [
"username",
"password"
]
},
"synthesizer": {
"properties": {
"vendor": {
"type": "string",
"enum": ["google", "aws", "polly", "microsoft", "nuance", "ibm", "default"]
},
"language": "string",
"voice": "string",
"engine": {
"type": "string",
"enum": ["standard", "neural"]
},
"gender": {
"type": "string",
"enum": ["MALE", "FEMALE", "NEUTRAL"]
}
},
"required": [
"vendor"
]
},
"recognizer": {
"properties": {
"vendor": {
"type": "string",
"enum": ["google", "aws", "microsoft", "nuance", "deepgram", "ibm", "default"]
},
"language": "string",
"vad": "#vad",
"hints": "array",
"hintsBoost": "number",
"altLanguages": "array",
"profanityFilter": "boolean",
"interim": "boolean",
"singleUtterance": "boolean",
"dualChannel": "boolean",
"separateRecognitionPerChannel": "boolean",
"punctuation": "boolean",
"enhancedModel": "boolean",
"words": "boolean",
"diarization": "boolean",
"diarizationMinSpeakers": "number",
"diarizationMaxSpeakers": "number",
"interactionType": {
"type": "string",
"enum": [
"unspecified",
"discussion",
"presentation",
"phone_call",
"voicemail",
"voice_search",
"voice_command",
"dictation"
]
},
"naicsCode": "number",
"identifyChannels": "boolean",
"vocabularyName": "string",
"vocabularyFilterName": "string",
"filterMethod": {
"type": "string",
"enum": [
"remove",
"mask",
"tag"
]
},
"model": "string",
"outputFormat": {
"type": "string",
"enum": [
"simple",
"detailed"
]
},
"profanityOption": {
"type": "string",
"enum": [
"masked",
"removed",
"raw"
]
},
"requestSnr": "boolean",
"initialSpeechTimeoutMs": "number",
"azureServiceEndpoint": "string",
"azureSttEndpointId": "string",
"asrDtmfTerminationDigit": "string",
"asrTimeout": "number",
"nuanceOptions": "#nuanceOptions",
"deepgramOptions": "#deepgramOptions",
"ibmOptions": "#ibmOptions"
},
"required": [
"vendor"
]
},
"ibmOptions": {
"properties": {
"sttApiKey": "string",
"sttRegion": "string",
"ttsApiKey": "string",
"ttsRegion": "string",
"instanceId": "string",
"model": "string",
"languageCustomizationId": "string",
"acousticCustomizationId": "string",
"baseModelVersion": "string",
"watsonMetadata": "string",
"watsonLearningOptOut": "boolean"
},
"required": [
]
},
"deepgramOptions": {
"properties": {
"apiKey": "string",
"tier": {
"type": "string",
"enum": [
"enhanced",
"base"
]
},
"model": {
"type": "string",
"enum": [
"general",
"meeting",
"phonecall",
"voicemail",
"finance",
"conversationalai",
"video",
"custom"
]
},
"customModel": "string",
"version": "string",
"punctuate": "boolean",
"profanityFilter": "boolean",
"redact": {
"type": "string",
"enum": [
"pci",
"numbers",
"true",
"ssn"
]
},
"diarize": "boolean",
"diarizeVersion": "string",
"ner": "boolean",
"multichannel": "boolean",
"alternatives": "number",
"numerals": "boolean",
"search": "array",
"replace": "array",
"keywords": "array",
"endpointing": "boolean",
"vadTurnoff": "number",
"tag": "string"
}
},
"nuanceOptions": {
"properties": {
"clientId": "string",
"secret": "string",
"kryptonEndpoint": "string",
"topic": "string",
"utteranceDetectionMode": {
"type": "string",
"enum": [
"single",
"multiple",
"disabled"
]
},
"punctuation": "boolean",
"profanityFilter": "boolean",
"includeTokenization": "boolean",
"discardSpeakerAdaptation": "boolean",
"suppressCallRecording": "boolean",
"maskLoadFailures": "boolean",
"suppressInitialCapitalization": "boolean",
"allowZeroBaseLmWeight": "boolean",
"filterWakeupWord": "boolean",
"resultType": {
"type": "string",
"enum": [
"final",
"partial",
"immutable_partial"
]
},
"noInputTimeoutMs": "number",
"recognitionTimeoutMs": "number",
"utteranceEndSilenceMs": "number",
"maxHypotheses": "number",
"speechDomain": "string",
"formatting": "#formatting",
"clientData": "object",
"userId": "string",
"speechDetectionSensitivity": "number",
"resources": ["#resource"]
},
"required": [
]
},
"resource": {
"properties": {
"externalReference": "#resourceReference",
"inlineWordset": "string",
"builtin": "string",
"inlineGrammar": "string",
"wakeupWord": "[string]",
"weightName": {
"type": "string",
"enum": [
"defaultWeight",
"lowest",
"low",
"medium",
"high",
"highest"
]
},
"weightValue": "number",
"reuse": {
"type": "string",
"enum": [
"undefined_reuse",
"low_reuse",
"high_reuse"
]
}
},
"required": [
]
},
"resourceReference": {
"properties": {
"type": {
"type": "string",
"enum": [
"undefined_resource_type",
"wordset",
"compiled_wordset",
"domain_lm",
"speaker_profile",
"grammar",
"settings"
]
},
"uri": "string",
"maxLoadFailures": "boolean",
"requestTimeoutMs": "number",
"headers": "object"
},
"required": [
]
},
"formatting": {
"properties": {
"scheme": "string",
"options": "object"
},
"required": [
"scheme",
"options"
]
},
"lexIntent": {
"properties": {
"name": "string",
"slots": "object"
},
"required": [
"name"
]
},
"vad": {
"properties": {
"enable": "boolean",
"voiceMs": "number",
"mode": "number"
},
"required": [
"enable"
]
},
"amd": {
"properties": {
"actionHook": "object|string",
"thresholdWordCount": "number",
"timers": "#amdTimers",
"recognizer": "#recognizer"
},
"required": [
"actionHook"
]
},
"amdTimers": {
"properties": {
"noSpeechTimeoutMs": "number",
"decisionTimeoutMs": "number",
"toneTimeoutMs": "number",
"greetingCompletionTimeoutMs": "number"
}
}
}

View File

@@ -2,9 +2,21 @@ const ttsGoogle = require('@google-cloud/text-to-speech');
const sttGoogle = require('@google-cloud/speech').v1p1beta1;
const Polly = require('aws-sdk/clients/polly');
const AWS = require('aws-sdk');
const { Deepgram } = require('@deepgram/sdk');
const sdk = require('microsoft-cognitiveservices-speech-sdk');
const bent = require('bent');
const fs = require('fs');
const testNuanceTts = async(logger, getTtsVoices, credentials) => {
const voices = await getTtsVoices({vendor: 'nuance', credentials});
return voices;
};
const testNuanceStt = async(logger, credentials) => {
//TODO
return true;
};
const testGoogleTts = async(logger, credentials) => {
const client = new ttsGoogle.TextToSpeechClient({credentials});
await client.listVoices();
@@ -32,6 +44,65 @@ const testGoogleStt = async(logger, credentials) => {
}
};
const testDeepgramStt = async(logger, credentials) => {
const {api_key} = credentials;
const deepgram = new Deepgram(api_key);
const mimetype = 'audio/wav';
const source = {
buffer: fs.readFileSync(`${__dirname}/../../data/test_audio.wav`),
mimetype: mimetype
};
return new Promise((resolve, reject) => {
// Send the audio to Deepgram and get the response
deepgram.transcription
.preRecorded(source, {punctuate: true})
.then((response) => {
//logger.debug({response}, 'got transcript');
if (response?.results?.channels[0]?.alternatives?.length > 0) resolve(response);
else reject(new Error('no transcript returned'));
return;
})
.catch((err) => {
logger.info({err}, 'failed to get deepgram transcript');
reject(err);
});
});
};
const testMicrosoftStt = async(logger, credentials) => {
const {api_key, region} = credentials;
const speechConfig = sdk.SpeechConfig.fromSubscription(api_key, region);
const audioConfig = sdk.AudioConfig.fromWavFileInput(fs.readFileSync(`${__dirname}/../../data/test_audio.wav`));
speechConfig.speechRecognitionLanguage = 'en-US';
const speechRecognizer = new sdk.SpeechRecognizer(speechConfig, audioConfig);
return new Promise((resolve, reject) => {
speechRecognizer.recognizeOnceAsync((result) => {
switch (result.reason) {
case sdk.ResultReason.RecognizedSpeech:
resolve();
break;
case sdk.ResultReason.NoMatch:
reject('Speech could not be recognized.');
break;
case sdk.ResultReason.Canceled:
const cancellation = sdk.CancellationDetails.fromResult(result);
logger.info(`CANCELED: Reason=${cancellation.reason}`);
if (cancellation.reason == sdk.CancellationReason.Error) {
logger.info(`CANCELED: ErrorCode=${cancellation.ErrorCode}`);
logger.info(`CANCELED: ErrorDetails=${cancellation.errorDetails}`);
}
reject(cancellation.reason);
break;
}
speechRecognizer.close();
});
});
};
const testAwsTts = (logger, credentials) => {
const polly = new Polly(credentials);
return new Promise((resolve, reject) => {
@@ -89,11 +160,6 @@ const testMicrosoftTts = async(logger, credentials) => {
}
};
const testMicrosoftStt = async(logger, credentials) => {
//TODO
return true;
};
const testWellSaidTts = async(logger, credentials) => {
const {api_key} = credentials;
try {
@@ -113,6 +179,35 @@ const testWellSaidTts = async(logger, credentials) => {
}
};
const testIbmTts = async(logger, getTtsVoices, credentials) => {
const {tts_api_key, tts_region} = credentials;
const voices = await getTtsVoices({vendor: 'ibm', credentials: {api_key: tts_api_key, region: tts_region}});
return voices;
};
const testIbmStt = async(logger, credentials) => {
const {stt_api_key, stt_region} = credentials;
const SpeechToTextV1 = require('ibm-watson/speech-to-text/v1');
const { IamAuthenticator } = require('ibm-watson/auth');
const speechToText = new SpeechToTextV1({
authenticator: new IamAuthenticator({
apikey: stt_api_key
}),
serviceUrl: `https://api.${stt_region}.speech-to-text.watson.cloud.ibm.com`
});
return new Promise((resolve, reject) => {
speechToText.listModels()
.then((speechModels) => {
logger.debug({speechModels}, 'got IBM speech models');
return resolve();
})
.catch((err) => {
logger.info({err}, 'failed to get speech models');
reject(err);
});
});
};
const testWellSaidStt = async(logger, credentials) => {
//TODO
return true;
@@ -127,4 +222,9 @@ module.exports = {
testMicrosoftTts,
testMicrosoftStt,
testWellSaidStt,
testNuanceTts,
testNuanceStt,
testDeepgramStt,
testIbmTts,
testIbmStt
};

3250
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,12 +1,12 @@
{
"name": "jambonz-api-server",
"version": "v0.7.7",
"version": "v0.8.0",
"description": "",
"main": "app.js",
"scripts": {
"start": "node app.js",
"test": "NODE_ENV=test APPLY_JAMBONZ_DB_LIMITS=1 JWT_SECRET=foobarbazzle JAMBONES_MYSQL_HOST=127.0.0.1 JAMBONES_MYSQL_PORT=3360 JAMBONES_MYSQL_USER=jambones_test JAMBONES_MYSQL_PASSWORD=jambones_test JAMBONES_MYSQL_DATABASE=jambones_test JAMBONES_REDIS_HOST=localhost JAMBONES_REDIS_PORT=16379 JAMBONES_TIME_SERIES_HOST=127.0.0.1 JAMBONES_LOGLEVEL=error JAMBONES_CREATE_CALL_URL=http://localhost/v1/createCall K8S=true K8S_FEATURE_SERVER_SERVICE_NAME=127.0.0.1 K8S_FEATURE_SERVER_SERVICE_PORT=3100 node test/ ",
"integration-test": "NODE_ENV=test JAMBONES_TIME_SERIES_HOST=127.0.0.1 AWS_REGION='us-east-1' JAMBONES_CURRENCY=USD JWT_SECRET=foobarbazzle JAMBONES_MYSQL_HOST=127.0.0.1 JAMBONES_MYSQL_PORT=3360 JAMBONES_MYSQL_USER=jambones_test JAMBONES_MYSQL_PASSWORD=jambones_test JAMBONES_MYSQL_DATABASE=jambones_test JAMBONES_REDIS_HOST=localhost JAMBONES_REDIS_PORT=16379 JAMBONES_LOGLEVEL=debug JAMBONES_CREATE_CALL_URL=http://localhost/v1/createCall node test/serve-integration.js",
"integration-test": "NODE_ENV=test JAMBONES_AUTH_USE_JWT=1 JAMBONES_TIME_SERIES_HOST=127.0.0.1 AWS_REGION='us-east-1' JAMBONES_CURRENCY=USD JWT_SECRET=foobarbazzle JAMBONES_MYSQL_HOST=127.0.0.1 JAMBONES_MYSQL_PORT=3360 JAMBONES_MYSQL_USER=jambones_test JAMBONES_MYSQL_PASSWORD=jambones_test JAMBONES_MYSQL_DATABASE=jambones_test JAMBONES_REDIS_HOST=localhost JAMBONES_REDIS_PORT=16379 JAMBONES_LOGLEVEL=debug JAMBONES_CREATE_CALL_URL=http://localhost/v1/createCall node test/serve-integration.js",
"upgrade-db": "node ./db/upgrade-jambonz-db.js",
"coverage": "./node_modules/.bin/nyc --reporter html --report-dir ./coverage npm run test",
"jslint": "eslint app.js lib",
@@ -18,10 +18,11 @@
"url": "https://github.com/jambonz/jambonz-api-server.git"
},
"dependencies": {
"@google-cloud/speech": "^4.10.2",
"@google-cloud/text-to-speech": "^3.4.0",
"@jambonz/db-helpers": "^0.6.19",
"@jambonz/realtimedb-helpers": "^0.4.35",
"@deepgram/sdk": "^1.10.2",
"@google-cloud/speech": "^5.1.0",
"@google-cloud/text-to-speech": "^4.0.3",
"@jambonz/db-helpers": "^0.7.3",
"@jambonz/realtimedb-helpers": "^0.6.0",
"@jambonz/time-series": "^0.2.5",
"argon2-ffi": "^2.0.0",
"aws-sdk": "^2.1152.0",
@@ -33,8 +34,10 @@
"form-data": "^2.5.1",
"form-urlencoded": "^6.1.0",
"helmet": "^5.1.0",
"jsonwebtoken": "^8.5.1",
"ibm-watson": "^7.1.2",
"jsonwebtoken": "^9.0.0",
"mailgun.js": "^3.7.3",
"microsoft-cognitiveservices-speech-sdk": "^1.24.1",
"mysql2": "^2.3.3",
"passport": "^0.6.0",
"passport-http-bearer": "^1.0.1",

View File

@@ -23,6 +23,37 @@ test('application tests', async(t) => {
const service_provider_sid = await createServiceProvider(request);
const phone_number_sid = await createPhoneNumber(request, voip_carrier_sid);
const account_sid = await createAccount(request, service_provider_sid);
/* add an invalid application app_json */
result = await request.post('/Applications', {
resolveWithFullResponse: true,
simple: false,
auth: authAdmin,
json: true,
body: {
name: 'daveh',
account_sid,
call_hook: {
url: 'http://example.com'
},
call_status_hook: {
url: 'http://example.com/status',
method: 'POST'
},
messaging_hook: {
url: 'http://example.com/sms'
},
app_json : '[\
{\
"verb": "play",\
"timeoutSecs": 10,\
"seekOffset": 8000,\
"actionHook": "/play/action"\
}\
]'
}
});
t.ok(result.statusCode === 400, 'Cant create application with invalid app_josn');
/* add an application */
result = await request.post('/Applications', {
@@ -41,7 +72,16 @@ test('application tests', async(t) => {
},
messaging_hook: {
url: 'http://example.com/sms'
}
},
app_json : '[\
{\
"verb": "play",\
"url": "https://example.com/example.mp3",\
"timeoutSecs": 10,\
"seekOffset": 8000,\
"actionHook": "/play/action"\
}\
]'
}
});
t.ok(result.statusCode === 201, 'successfully created application');
@@ -62,6 +102,9 @@ test('application tests', async(t) => {
});
t.ok(result.name === 'daveh' , 'successfully retrieved application by sid');
t.ok(result.messaging_hook.url === 'http://example.com/sms' , 'successfully retrieved messaging_hook from application');
let app_json = JSON.parse(result.app_json);
t.ok(app_json[0].verb === 'play', 'successfully retrieved app_json from application')
/* update applications */
result = await request.put(`/Applications/${sid}`, {
@@ -74,7 +117,15 @@ test('application tests', async(t) => {
},
messaging_hook: {
url: 'http://example2.com/mms'
}
},
app_json : '[\
{\
"verb": "hangup",\
"headers": {\
"X-Reason" : "maximum call duration exceeded"\
}\
}\
]'
}
});
t.ok(result.statusCode === 204, 'successfully updated application');
@@ -85,6 +136,57 @@ test('application tests', async(t) => {
json: true,
});
t.ok(result.messaging_hook.url === 'http://example2.com/mms' , 'successfully updated messaging_hook');
app_json = JSON.parse(result.app_json);
t.ok(app_json[0].verb === 'hangup', 'successfully updated app_json from application')
/* remove applications app_json*/
result = await request.put(`/Applications/${sid}`, {
auth: authAdmin,
json: true,
resolveWithFullResponse: true,
body: {
call_hook: {
url: 'http://example2.com'
},
messaging_hook: {
url: 'http://example2.com/mms'
},
app_json : null
}
});
t.ok(result.statusCode === 204, 'successfully updated application');
/* validate messaging hook was updated */
result = await request.get(`/Applications/${sid}`, {
auth: authAdmin,
json: true,
});
t.ok(result.app_json == undefined, 'successfully removed app_json from application')
/* Update invalid applications app_json*/
result = await request.put(`/Applications/${sid}`, {
auth: authAdmin,
json: true,
resolveWithFullResponse: true,
simple: false,
body: {
call_hook: {
url: 'http://example2.com'
},
messaging_hook: {
url: 'http://example2.com/mms'
},
app_json : '[\
{\
"verb": "play",\
"timeoutSecs": 10,\
"seekOffset": 8000,\
"actionHook": "/play/action"\
}\
]'
}
});
t.ok(result.statusCode === 400, 'Cant update invalid application app_json');
/* assign phone number to application */
result = await request.put(`/PhoneNumbers/${phone_number_sid}`, {

View File

@@ -18,7 +18,9 @@ test('Create Call Success With Synthesizer in Payload', async (t) => {
const service_provider_sid = await createServiceProvider(request, 'account_has_synthesizer');
const account_sid = await createAccount(request, service_provider_sid, 'account_has_synthesizer');
const token = jwt.sign({
account_sid
account_sid,
scope: "account",
permissions: ["PROVISION_USERS", "PROVISION_SERVICES", "VIEW_ONLY"]
}, process.env.JWT_SECRET, { expiresIn: '1h' });
const authUser = { bearer: token };
const speech_sid = await createGoogleSpeechCredentials(request, account_sid, null, authUser, true, true)
@@ -58,7 +60,9 @@ test('Create Call Success Without Synthesizer in Payload', async (t) => {
const service_provider_sid = await createServiceProvider(request, 'account2_has_synthesizer');
const account_sid = await createAccount(request, service_provider_sid, 'account2_has_synthesizer');
const token = jwt.sign({
account_sid
account_sid,
scope: "account",
permissions: ["PROVISION_USERS", "PROVISION_SERVICES", "VIEW_ONLY"]
}, process.env.JWT_SECRET, { expiresIn: '1h' });
const authUser = { bearer: token };
const speech_sid = await createGoogleSpeechCredentials(request, account_sid, null, authUser, true, true)

View File

@@ -9,17 +9,17 @@
"version": "1.0.0",
"license": "MIT",
"dependencies": {
"express": "^4.17.1",
"express": "^4.17.3",
"uuid": "^8.3.2"
}
},
"node_modules/accepts": {
"version": "1.3.7",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz",
"integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==",
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
"integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
"dependencies": {
"mime-types": "~2.1.24",
"negotiator": "0.6.2"
"mime-types": "~2.1.34",
"negotiator": "0.6.3"
},
"engines": {
"node": ">= 0.6"
@@ -31,39 +31,39 @@
"integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI="
},
"node_modules/body-parser": {
"version": "1.19.0",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz",
"integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==",
"version": "1.19.2",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.2.tgz",
"integrity": "sha512-SAAwOxgoCKMGs9uUAUFHygfLAyaniaoun6I8mFY9pRAJL9+Kec34aU+oIjDhTycub1jozEfEwx1W1IuOYxVSFw==",
"dependencies": {
"bytes": "3.1.0",
"bytes": "3.1.2",
"content-type": "~1.0.4",
"debug": "2.6.9",
"depd": "~1.1.2",
"http-errors": "1.7.2",
"http-errors": "1.8.1",
"iconv-lite": "0.4.24",
"on-finished": "~2.3.0",
"qs": "6.7.0",
"raw-body": "2.4.0",
"type-is": "~1.6.17"
"qs": "6.9.7",
"raw-body": "2.4.3",
"type-is": "~1.6.18"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/bytes": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
"integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==",
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/content-disposition": {
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz",
"integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==",
"version": "0.5.4",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
"integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
"dependencies": {
"safe-buffer": "5.1.2"
"safe-buffer": "5.2.1"
},
"engines": {
"node": ">= 0.6"
@@ -78,9 +78,9 @@
}
},
"node_modules/cookie": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz",
"integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==",
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz",
"integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==",
"engines": {
"node": ">= 0.6"
}
@@ -101,7 +101,7 @@
"node_modules/depd": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
"integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=",
"integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==",
"engines": {
"node": ">= 0.6"
}
@@ -109,7 +109,7 @@
"node_modules/destroy": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
"integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA="
"integrity": "sha512-3NdhDuEXnfun/z7x9GOElY49LoqVHoGScmOKwmxhsS8N5Y+Z8KyPPDnaSzqWgYt/ji4mqwfTS34Htrk0zPIXVg=="
},
"node_modules/ee-first": {
"version": "1.1.1",
@@ -132,22 +132,22 @@
"node_modules/etag": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
"integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=",
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/express": {
"version": "4.17.1",
"resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz",
"integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==",
"version": "4.17.3",
"resolved": "https://registry.npmjs.org/express/-/express-4.17.3.tgz",
"integrity": "sha512-yuSQpz5I+Ch7gFrPCk4/c+dIBKlQUxtgwqzph132bsT6qhuzss6I8cLJQz7B3rFblzd6wtcI0ZbGltH/C4LjUg==",
"dependencies": {
"accepts": "~1.3.7",
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
"body-parser": "1.19.0",
"content-disposition": "0.5.3",
"body-parser": "1.19.2",
"content-disposition": "0.5.4",
"content-type": "~1.0.4",
"cookie": "0.4.0",
"cookie": "0.4.2",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "~1.1.2",
@@ -161,13 +161,13 @@
"on-finished": "~2.3.0",
"parseurl": "~1.3.3",
"path-to-regexp": "0.1.7",
"proxy-addr": "~2.0.5",
"qs": "6.7.0",
"proxy-addr": "~2.0.7",
"qs": "6.9.7",
"range-parser": "~1.2.1",
"safe-buffer": "5.1.2",
"send": "0.17.1",
"serve-static": "1.14.1",
"setprototypeof": "1.1.1",
"safe-buffer": "5.2.1",
"send": "0.17.2",
"serve-static": "1.14.2",
"setprototypeof": "1.2.0",
"statuses": "~1.5.0",
"type-is": "~1.6.18",
"utils-merge": "1.0.1",
@@ -195,9 +195,9 @@
}
},
"node_modules/forwarded": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz",
"integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=",
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
"integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
"engines": {
"node": ">= 0.6"
}
@@ -205,21 +205,21 @@
"node_modules/fresh": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
"integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=",
"integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/http-errors": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz",
"integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==",
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz",
"integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==",
"dependencies": {
"depd": "~1.1.2",
"inherits": "2.0.3",
"setprototypeof": "1.1.1",
"inherits": "2.0.4",
"setprototypeof": "1.2.0",
"statuses": ">= 1.5.0 < 2",
"toidentifier": "1.0.0"
"toidentifier": "1.0.1"
},
"engines": {
"node": ">= 0.6"
@@ -237,9 +237,9 @@
}
},
"node_modules/inherits": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"node_modules/ipaddr.js": {
"version": "1.9.1",
@@ -252,7 +252,7 @@
"node_modules/media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
"integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=",
"integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
"engines": {
"node": ">= 0.6"
}
@@ -282,19 +282,19 @@
}
},
"node_modules/mime-db": {
"version": "1.45.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.45.0.tgz",
"integrity": "sha512-CkqLUxUk15hofLoLyljJSrukZi8mAtgd+yE5uO4tqRZsdsAJKv0O+rFMhVDRJgozy+yG6md5KwuXhD4ocIoP+w==",
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.28",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.28.tgz",
"integrity": "sha512-0TO2yJ5YHYr7M2zzT7gDU1tbwHxEUWBCLt0lscSNpcdAfFyJOVEpRYNS7EXVcTLNj/25QO8gulHC5JtTzSE2UQ==",
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"dependencies": {
"mime-db": "1.45.0"
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
@@ -306,9 +306,9 @@
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
},
"node_modules/negotiator": {
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz",
"integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==",
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
"integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
"engines": {
"node": ">= 0.6"
}
@@ -338,11 +338,11 @@
"integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w="
},
"node_modules/proxy-addr": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz",
"integrity": "sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw==",
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
"integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
"dependencies": {
"forwarded": "~0.1.2",
"forwarded": "0.2.0",
"ipaddr.js": "1.9.1"
},
"engines": {
@@ -350,11 +350,14 @@
}
},
"node_modules/qs": {
"version": "6.7.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz",
"integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==",
"version": "6.9.7",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.9.7.tgz",
"integrity": "sha512-IhMFgUmuNpyRfxA90umL7ByLlgRXu6tIfKPpF5TmcfRLlLCckfP/g3IQmju6jjpu+Hh8rA+2p6A27ZSPOOHdKw==",
"engines": {
"node": ">=0.6"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/range-parser": {
@@ -366,12 +369,12 @@
}
},
"node_modules/raw-body": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz",
"integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==",
"version": "2.4.3",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.3.tgz",
"integrity": "sha512-UlTNLIcu0uzb4D2f4WltY6cVjLi+/jEN4lgEUj3E04tpMDpUlkBo/eSn6zou9hum2VMNpCCUone0O0WeJim07g==",
"dependencies": {
"bytes": "3.1.0",
"http-errors": "1.7.2",
"bytes": "3.1.2",
"http-errors": "1.8.1",
"iconv-lite": "0.4.24",
"unpipe": "1.0.0"
},
@@ -380,9 +383,23 @@
}
},
"node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
]
},
"node_modules/safer-buffer": {
"version": "2.1.2",
@@ -390,9 +407,9 @@
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"node_modules/send": {
"version": "0.17.1",
"resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz",
"integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==",
"version": "0.17.2",
"resolved": "https://registry.npmjs.org/send/-/send-0.17.2.tgz",
"integrity": "sha512-UJYB6wFSJE3G00nEivR5rgWp8c2xXvJ3OPWPhmuteU0IKj8nKbG3DrjiOmLwpnHGYWAVwA69zmTm++YG0Hmwww==",
"dependencies": {
"debug": "2.6.9",
"depd": "~1.1.2",
@@ -401,9 +418,9 @@
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"fresh": "0.5.2",
"http-errors": "~1.7.2",
"http-errors": "1.8.1",
"mime": "1.6.0",
"ms": "2.1.1",
"ms": "2.1.3",
"on-finished": "~2.3.0",
"range-parser": "~1.2.1",
"statuses": "~1.5.0"
@@ -413,28 +430,28 @@
}
},
"node_modules/send/node_modules/ms": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
"integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg=="
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
},
"node_modules/serve-static": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz",
"integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==",
"version": "1.14.2",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.2.tgz",
"integrity": "sha512-+TMNA9AFxUEGuC0z2mevogSnn9MXKb4fa7ngeRMJaaGv8vTwnIEkKi+QGvPt33HSnf8pRS+WGM0EbMtCJLKMBQ==",
"dependencies": {
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"parseurl": "~1.3.3",
"send": "0.17.1"
"send": "0.17.2"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/setprototypeof": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz",
"integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw=="
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
},
"node_modules/statuses": {
"version": "1.5.0",
@@ -445,9 +462,9 @@
}
},
"node_modules/toidentifier": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz",
"integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==",
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
"engines": {
"node": ">=0.6"
}
@@ -499,12 +516,12 @@
},
"dependencies": {
"accepts": {
"version": "1.3.7",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz",
"integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==",
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
"integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
"requires": {
"mime-types": "~2.1.24",
"negotiator": "0.6.2"
"mime-types": "~2.1.34",
"negotiator": "0.6.3"
}
},
"array-flatten": {
@@ -513,33 +530,33 @@
"integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI="
},
"body-parser": {
"version": "1.19.0",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz",
"integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==",
"version": "1.19.2",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.2.tgz",
"integrity": "sha512-SAAwOxgoCKMGs9uUAUFHygfLAyaniaoun6I8mFY9pRAJL9+Kec34aU+oIjDhTycub1jozEfEwx1W1IuOYxVSFw==",
"requires": {
"bytes": "3.1.0",
"bytes": "3.1.2",
"content-type": "~1.0.4",
"debug": "2.6.9",
"depd": "~1.1.2",
"http-errors": "1.7.2",
"http-errors": "1.8.1",
"iconv-lite": "0.4.24",
"on-finished": "~2.3.0",
"qs": "6.7.0",
"raw-body": "2.4.0",
"type-is": "~1.6.17"
"qs": "6.9.7",
"raw-body": "2.4.3",
"type-is": "~1.6.18"
}
},
"bytes": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
"integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg=="
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="
},
"content-disposition": {
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz",
"integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==",
"version": "0.5.4",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
"integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
"requires": {
"safe-buffer": "5.1.2"
"safe-buffer": "5.2.1"
}
},
"content-type": {
@@ -548,9 +565,9 @@
"integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA=="
},
"cookie": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz",
"integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg=="
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz",
"integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA=="
},
"cookie-signature": {
"version": "1.0.6",
@@ -568,12 +585,12 @@
"depd": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
"integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak="
"integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ=="
},
"destroy": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
"integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA="
"integrity": "sha512-3NdhDuEXnfun/z7x9GOElY49LoqVHoGScmOKwmxhsS8N5Y+Z8KyPPDnaSzqWgYt/ji4mqwfTS34Htrk0zPIXVg=="
},
"ee-first": {
"version": "1.1.1",
@@ -593,19 +610,19 @@
"etag": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
"integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc="
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="
},
"express": {
"version": "4.17.1",
"resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz",
"integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==",
"version": "4.17.3",
"resolved": "https://registry.npmjs.org/express/-/express-4.17.3.tgz",
"integrity": "sha512-yuSQpz5I+Ch7gFrPCk4/c+dIBKlQUxtgwqzph132bsT6qhuzss6I8cLJQz7B3rFblzd6wtcI0ZbGltH/C4LjUg==",
"requires": {
"accepts": "~1.3.7",
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
"body-parser": "1.19.0",
"content-disposition": "0.5.3",
"body-parser": "1.19.2",
"content-disposition": "0.5.4",
"content-type": "~1.0.4",
"cookie": "0.4.0",
"cookie": "0.4.2",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "~1.1.2",
@@ -619,13 +636,13 @@
"on-finished": "~2.3.0",
"parseurl": "~1.3.3",
"path-to-regexp": "0.1.7",
"proxy-addr": "~2.0.5",
"qs": "6.7.0",
"proxy-addr": "~2.0.7",
"qs": "6.9.7",
"range-parser": "~1.2.1",
"safe-buffer": "5.1.2",
"send": "0.17.1",
"serve-static": "1.14.1",
"setprototypeof": "1.1.1",
"safe-buffer": "5.2.1",
"send": "0.17.2",
"serve-static": "1.14.2",
"setprototypeof": "1.2.0",
"statuses": "~1.5.0",
"type-is": "~1.6.18",
"utils-merge": "1.0.1",
@@ -647,25 +664,25 @@
}
},
"forwarded": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz",
"integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ="
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
"integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="
},
"fresh": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
"integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac="
"integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="
},
"http-errors": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz",
"integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==",
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz",
"integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==",
"requires": {
"depd": "~1.1.2",
"inherits": "2.0.3",
"setprototypeof": "1.1.1",
"inherits": "2.0.4",
"setprototypeof": "1.2.0",
"statuses": ">= 1.5.0 < 2",
"toidentifier": "1.0.0"
"toidentifier": "1.0.1"
}
},
"iconv-lite": {
@@ -677,9 +694,9 @@
}
},
"inherits": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"ipaddr.js": {
"version": "1.9.1",
@@ -689,7 +706,7 @@
"media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
"integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g="
"integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="
},
"merge-descriptors": {
"version": "1.0.1",
@@ -707,16 +724,16 @@
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="
},
"mime-db": {
"version": "1.45.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.45.0.tgz",
"integrity": "sha512-CkqLUxUk15hofLoLyljJSrukZi8mAtgd+yE5uO4tqRZsdsAJKv0O+rFMhVDRJgozy+yG6md5KwuXhD4ocIoP+w=="
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="
},
"mime-types": {
"version": "2.1.28",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.28.tgz",
"integrity": "sha512-0TO2yJ5YHYr7M2zzT7gDU1tbwHxEUWBCLt0lscSNpcdAfFyJOVEpRYNS7EXVcTLNj/25QO8gulHC5JtTzSE2UQ==",
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"requires": {
"mime-db": "1.45.0"
"mime-db": "1.52.0"
}
},
"ms": {
@@ -725,9 +742,9 @@
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
},
"negotiator": {
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz",
"integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw=="
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
"integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="
},
"on-finished": {
"version": "2.3.0",
@@ -748,18 +765,18 @@
"integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w="
},
"proxy-addr": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz",
"integrity": "sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw==",
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
"integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
"requires": {
"forwarded": "~0.1.2",
"forwarded": "0.2.0",
"ipaddr.js": "1.9.1"
}
},
"qs": {
"version": "6.7.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz",
"integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ=="
"version": "6.9.7",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.9.7.tgz",
"integrity": "sha512-IhMFgUmuNpyRfxA90umL7ByLlgRXu6tIfKPpF5TmcfRLlLCckfP/g3IQmju6jjpu+Hh8rA+2p6A27ZSPOOHdKw=="
},
"range-parser": {
"version": "1.2.1",
@@ -767,20 +784,20 @@
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="
},
"raw-body": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz",
"integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==",
"version": "2.4.3",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.3.tgz",
"integrity": "sha512-UlTNLIcu0uzb4D2f4WltY6cVjLi+/jEN4lgEUj3E04tpMDpUlkBo/eSn6zou9hum2VMNpCCUone0O0WeJim07g==",
"requires": {
"bytes": "3.1.0",
"http-errors": "1.7.2",
"bytes": "3.1.2",
"http-errors": "1.8.1",
"iconv-lite": "0.4.24",
"unpipe": "1.0.0"
}
},
"safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
},
"safer-buffer": {
"version": "2.1.2",
@@ -788,9 +805,9 @@
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"send": {
"version": "0.17.1",
"resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz",
"integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==",
"version": "0.17.2",
"resolved": "https://registry.npmjs.org/send/-/send-0.17.2.tgz",
"integrity": "sha512-UJYB6wFSJE3G00nEivR5rgWp8c2xXvJ3OPWPhmuteU0IKj8nKbG3DrjiOmLwpnHGYWAVwA69zmTm++YG0Hmwww==",
"requires": {
"debug": "2.6.9",
"depd": "~1.1.2",
@@ -799,36 +816,36 @@
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"fresh": "0.5.2",
"http-errors": "~1.7.2",
"http-errors": "1.8.1",
"mime": "1.6.0",
"ms": "2.1.1",
"ms": "2.1.3",
"on-finished": "~2.3.0",
"range-parser": "~1.2.1",
"statuses": "~1.5.0"
},
"dependencies": {
"ms": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
"integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg=="
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
}
}
},
"serve-static": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz",
"integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==",
"version": "1.14.2",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.2.tgz",
"integrity": "sha512-+TMNA9AFxUEGuC0z2mevogSnn9MXKb4fa7ngeRMJaaGv8vTwnIEkKi+QGvPt33HSnf8pRS+WGM0EbMtCJLKMBQ==",
"requires": {
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"parseurl": "~1.3.3",
"send": "0.17.1"
"send": "0.17.2"
}
},
"setprototypeof": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz",
"integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw=="
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
},
"statuses": {
"version": "1.5.0",
@@ -836,9 +853,9 @@
"integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow="
},
"toidentifier": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz",
"integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw=="
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="
},
"type-is": {
"version": "1.6.18",

View File

@@ -9,7 +9,7 @@
"author": "Dave Horton",
"license": "MIT",
"dependencies": {
"express": "^4.17.1",
"express": "^4.17.3",
"uuid": "^8.3.2"
}
}

View File

@@ -12,6 +12,7 @@ require('./sbcs');
require('./ms-teams');
require('./speech-credentials');
require('./recent-calls');
require('./users');
require('./webapp_tests');
// require('./homer');
require('./call-test');

View File

@@ -24,12 +24,16 @@ test('recent calls tests', async(t) => {
const account_sid = await createAccount(request, service_provider_sid);
const token = jwt.sign({
account_sid
account_sid,
scope: "account",
permissions: ["PROVISION_USERS", "PROVISION_SERVICES", "VIEW_ONLY"]
}, process.env.JWT_SECRET, { expiresIn: '1h' });
const authUser = {bearer: token};
const tokenSP = jwt.sign({
service_provider_sid
service_provider_sid,
scope: "account",
permissions: ["PROVISION_USERS", "PROVISION_SERVICES", "VIEW_ONLY"]
}, process.env.JWT_SECRET, { expiresIn: '1h' });
const authUserSP = {bearer: token};

View File

@@ -79,13 +79,14 @@ test('service provider tests', async(t) => {
}
});
//console.log(`result: ${JSON.stringify(result)}`);
t.ok(result.statusCode === 422, 'cannot add two service providers with the same name');
t.ok(result.statusCode === 422, 'cannot add two service providers with the same root domain');
/* query all service providers */
result = await request.get('/ServiceProviders', {
auth: authAdmin,
json: true,
});
//console.log(JSON.stringify(result));
t.ok(result.length === 2 , 'successfully queried all service providers');
/* query one service providers */

View File

@@ -30,7 +30,9 @@ test('speech credentials tests', async(t) => {
json: true,
body: {
vendor: 'google',
service_key: jsonKey
service_key: jsonKey,
use_for_tts: true,
use_for_stt: true
}
});
t.ok(result.statusCode === 201, 'successfully added a speech credential to service provider');
@@ -50,7 +52,9 @@ test('speech credentials tests', async(t) => {
await deleteObjectBySid(request, `/ServiceProviders/${service_provider_sid}/SpeechCredentials`, speech_credential_sid);
const token = jwt.sign({
account_sid
account_sid,
scope: 'account',
permissions: ["PROVISION_USERS", "PROVISION_SERVICES", "VIEW_ONLY"]
}, process.env.JWT_SECRET, { expiresIn: '1h' });
const authUser = {bearer: token};
@@ -61,7 +65,9 @@ test('speech credentials tests', async(t) => {
json: true,
body: {
vendor: 'google',
service_key: jsonKey
service_key: jsonKey,
use_for_tts: true,
use_for_stt: true
}
});
t.ok(result.statusCode === 201, 'successfully added speech credential');
@@ -110,20 +116,20 @@ test('speech credentials tests', async(t) => {
});
t.ok(result.statusCode === 204, 'successfully deleted speech credential');
/* add a credential for microsoft */
if (process.env.MICROSOFT_API_KEY && process.env.MICROSOFT_REGION) {
/* add / test a credential for google */
if (process.env.GCP_JSON_KEY) {
result = await request.post(`/Accounts/${account_sid}/SpeechCredentials`, {
resolveWithFullResponse: true,
auth: authUser,
json: true,
body: {
vendor: 'microsoft',
vendor: 'google',
use_for_tts: true,
api_key: process.env.MICROSOFT_API_KEY,
region: process.env.MICROSOFT_REGION
use_for_stt: true,
service_key: process.env.GCP_JSON_KEY
}
});
t.ok(result.statusCode === 201, 'successfully added speech credential');
t.ok(result.statusCode === 201, 'successfully added speech credential for google');
const ms_sid = result.body.sid;
/* test the speech credential */
@@ -132,7 +138,37 @@ test('speech credentials tests', async(t) => {
auth: authUser,
json: true,
});
console.log(JSON.stringify(result));
//console.log(JSON.stringify(result));
t.ok(result.statusCode === 200 && result.body.tts.status === 'ok', 'successfully tested speech credential for google tts');
t.ok(result.statusCode === 200 && result.body.stt.status === 'ok', 'successfully tested speech credential for google stt');
}
/* add / test a credential for microsoft */
if (process.env.MICROSOFT_API_KEY && process.env.MICROSOFT_REGION) {
result = await request.post(`/Accounts/${account_sid}/SpeechCredentials`, {
resolveWithFullResponse: true,
auth: authUser,
json: true,
body: {
vendor: 'microsoft',
use_for_tts: true,
use_for_stt: true,
api_key: process.env.MICROSOFT_API_KEY,
region: process.env.MICROSOFT_REGION
}
});
t.ok(result.statusCode === 201, 'successfully added speech credential for microsoft');
const ms_sid = result.body.sid;
/* test the speech credential */
result = await request.get(`/Accounts/${account_sid}/SpeechCredentials/${ms_sid}/test`, {
resolveWithFullResponse: true,
auth: authUser,
json: true,
});
//console.log(JSON.stringify(result));
t.ok(result.statusCode === 200 && result.body.tts.status === 'ok', 'successfully tested speech credential for microsoft tts');
t.ok(result.statusCode === 200 && result.body.stt.status === 'ok', 'successfully tested speech credential for microsoft stt');
}
/* add a credential for wellsaid */
@@ -156,7 +192,105 @@ test('speech credentials tests', async(t) => {
auth: authUser,
json: true,
});
console.log(JSON.stringify(result));
//console.log(JSON.stringify(result));
t.ok(result.statusCode === 200 && result.body.tts.status === 'ok', 'successfully tested speech credential for wellsaid');
/* delete the credential */
result = await request.delete(`/Accounts/${account_sid}/SpeechCredentials/${ms_sid}`, {
auth: authUser,
resolveWithFullResponse: true,
});
t.ok(result.statusCode === 204, 'successfully deleted speech credential');
}
/* add a credential for deepgram */
if (process.env.DEEPGRAM_API_KEY) {
result = await request.post(`/Accounts/${account_sid}/SpeechCredentials`, {
resolveWithFullResponse: true,
auth: authUser,
json: true,
body: {
vendor: 'deepgram',
use_for_stt: true,
api_key: process.env.DEEPGRAM_API_KEY
}
});
t.ok(result.statusCode === 201, 'successfully added speech credential for deepgram');
const ms_sid = result.body.sid;
/* test the speech credential */
result = await request.get(`/Accounts/${account_sid}/SpeechCredentials/${ms_sid}/test`, {
resolveWithFullResponse: true,
auth: authUser,
json: true,
});
//console.log(JSON.stringify(result));
t.ok(result.statusCode === 200 && result.body.stt.status === 'ok', 'successfully tested speech credential for deepgram');
/* delete the credential */
result = await request.delete(`/Accounts/${account_sid}/SpeechCredentials/${ms_sid}`, {
auth: authUser,
resolveWithFullResponse: true,
});
t.ok(result.statusCode === 204, 'successfully deleted speech credential');
}
/* add a credential for ibm tts */
if (process.env.IBM_TTS_API_KEY && process.env.IBM_TTS_REGION) {
result = await request.post(`/Accounts/${account_sid}/SpeechCredentials`, {
resolveWithFullResponse: true,
auth: authUser,
json: true,
body: {
vendor: 'ibm',
use_for_tts: true,
tts_api_key: process.env.IBM_TTS_API_KEY,
tts_region: process.env.IBM_TTS_REGION
}
});
t.ok(result.statusCode === 201, 'successfully added speech credential for ibm');
const ms_sid = result.body.sid;
/* test the speech credential */
result = await request.get(`/Accounts/${account_sid}/SpeechCredentials/${ms_sid}/test`, {
resolveWithFullResponse: true,
auth: authUser,
json: true,
});
//console.log(JSON.stringify(result));
t.ok(result.statusCode === 200 && result.body.tts.status === 'ok', 'successfully tested speech credential for ibm tts');
/* delete the credential */
result = await request.delete(`/Accounts/${account_sid}/SpeechCredentials/${ms_sid}`, {
auth: authUser,
resolveWithFullResponse: true,
});
t.ok(result.statusCode === 204, 'successfully deleted speech credential');
}
/* add a credential for ibm stt */
if (process.env.IBM_STT_API_KEY && process.env.IBM_STT_REGION) {
result = await request.post(`/Accounts/${account_sid}/SpeechCredentials`, {
resolveWithFullResponse: true,
auth: authUser,
json: true,
body: {
vendor: 'ibm',
use_for_stt: true,
stt_api_key: process.env.IBM_STT_API_KEY,
stt_region: process.env.IBM_STT_REGION
}
});
t.ok(result.statusCode === 201, 'successfully added speech credential for ibm');
const ms_sid = result.body.sid;
/* test the speech credential */
result = await request.get(`/Accounts/${account_sid}/SpeechCredentials/${ms_sid}/test`, {
resolveWithFullResponse: true,
auth: authUser,
json: true,
});
//console.log(JSON.stringify(result));
t.ok(result.statusCode === 200 && result.body.stt.status === 'ok', 'successfully tested speech credential for ibm stt');
/* delete the credential */
result = await request.delete(`/Accounts/${account_sid}/SpeechCredentials/${ms_sid}`, {

220
test/users.js Normal file
View File

@@ -0,0 +1,220 @@
const test = require('tape') ;
const jwt = require('jsonwebtoken');
const request = require('request-promise-native').defaults({
baseUrl: 'http://127.0.0.1:3000/v1'
});
const exec = require('child_process').exec ;
const {generateHashedPassword} = require('../lib/utils/password-utils');
process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
});
test('add an admin user', (t) => {
exec(`${__dirname}/../db/reset_admin_password.js`, (err, stdout, stderr) => {
console.log(stderr);
console.log(stdout);
if (err) return t.end(err);
t.pass('successfully added admin user');
t.end();
});
});
test('user tests', async(t) => {
const app = require('../app');
const password = await generateHashedPassword('abcd1234-');
try {
let result;
/* login as admin to get a jwt */
result = await request.post('/login', {
resolveWithFullResponse: true,
json: true,
body: {
username: 'admin',
password: 'admin',
}
});
t.ok(result.statusCode === 200 && result.body.token, 'successfully logged in as admin');
const authAdmin = {bearer: result.body.token};
const decodedJwt = jwt.verify(result.body.token, process.env.JWT_SECRET);
/* add admin user */
result = await request.post(`/Users`, {
resolveWithFullResponse: true,
json: true,
auth: authAdmin,
body: {
name: 'admin2',
email: 'admin2@jambonz.com',
is_active: true,
force_change: true,
initial_password: password,
}
});
t.ok(result.statusCode === 201 && result.body.user_sid, 'admin user created');
const admin_user_sid = result.body.user_sid;
/* add a service provider */
result = await request.post('/ServiceProviders', {
resolveWithFullResponse: true,
auth: authAdmin,
json: true,
body: {
name: 'sp',
}
});
t.ok(result.statusCode === 201, 'successfully created service provider');
const sp_sid = result.body.sid;
/* add service_provider user */
result = await request.post(`/Users`, {
resolveWithFullResponse: true,
json: true,
auth: authAdmin,
body: {
name: 'service_provider',
email: 'sp@jambonz.com',
is_active: true,
force_change: true,
initial_password: password,
service_provider_sid: sp_sid,
}
});
t.ok(result.statusCode === 201 && result.body.user_sid, 'service_provider scope user created');
const sp_user_sid = result.body.sid;
/* add an account */
result = await request.post('/Accounts', {
resolveWithFullResponse: true,
auth: authAdmin,
json: true,
body: {
name: 'sample_account',
service_provider_sid: sp_sid,
registration_hook: {
url: 'http://example.com/reg',
method: 'get'
},
webhook_secret: 'foobar'
}
});
t.ok(result.statusCode === 201, 'successfully created account');
const account_sid = result.body.sid;
/* add account user */
result = await request.post(`/Users`, {
resolveWithFullResponse: true,
json: true,
auth: authAdmin,
body: {
name: 'account',
email: 'account@jambonz.com',
is_active: true,
force_change: true,
initial_password: password,
service_provider_sid: sp_sid,
account_sid: account_sid
}
});
t.ok(result.statusCode === 201 && result.body.user_sid, 'account scope user created');
const account_user_sid = result.body.sid;
/* retrieve list of users */
result = await request.get(`/Users`, {
resolveWithFullResponse: true,
json: true,
auth: authAdmin,
});
t.ok(result.statusCode === 200 && result.body.length, 'successfully user list');
/* delete account user */
result = await request.delete(`/Users/${account_user_sid}`, {
resolveWithFullResponse: true,
json: true,
auth: authAdmin,
});
t.ok(result.statusCode === 204, 'account scope user deleted');
/* delete sp user */
result = await request.delete(`/Users/${sp_user_sid}`, {
resolveWithFullResponse: true,
json: true,
auth: authAdmin,
});
t.ok(result.statusCode === 204, 'account scope user deleted');
/* delete admin user */
result = await request.delete(`/Users/${admin_user_sid}`, {
resolveWithFullResponse: true,
json: true,
auth: authAdmin,
});
t.ok(result.statusCode === 204, 'account scope user deleted');
// /* self delete as admin user */
// result = await request.delete(`/Users/${decodedJwt.user_sid}`, {
// resolveWithFullResponse: true,
// json: true,
// auth: authAdmin,
// });
// t.ok(result.statusCode === 500 && result.error.msg === 'cannot delete this admin user - there are no other active admin users');
/* add another service_provider user */
result = await request.post(`/Users`, {
resolveWithFullResponse: true,
json: true,
auth: authAdmin,
body: {
name: 'service_provider1',
email: 'sp1@jambonz.com',
is_active: true,
force_change: false,
initial_password: password,
service_provider_sid: sp_sid,
}
});
t.ok(result.statusCode === 201 && result.body.user_sid, 'service_provider scope user created');
/* logout as sp to get a jwt */
result = await request.post('/logout', {
resolveWithFullResponse: true,
auth: authAdmin,
json: true,
});
t.ok(result.statusCode === 204, 'successfully logged out');
// /* login as sp user to get a jwt */
// result = await request.post('/login', {
// resolveWithFullResponse: true,
// json: true,
// body: {
// username: 'service_provider1',
// password: 'abcd1234-',
// }
// });
// t.ok(result.statusCode === 200 && result.body.token, 'successfully logged in as sp');
// const authSPUser = {bearer: result.body.token};
// result = await request.post(`/Users`, {
// resolveWithFullResponse: true,
// json: true,
// auth: authSPUser,
// body: {
// name: 'sp2',
// email: 'sp2@jambonz.com',
// is_active: true,
// force_change: false,
// initial_password: password,
// }
// });
// t.ok(result.statusCode === 403, 'sp user cannot create admin users');
} catch (err) {
console.error(err);
t.end(err);
}
});