Compare commits

...

84 Commits

Author SHA1 Message Date
Hoan Luu Huu
936a9244ba add sentinal Password option (#239) 2023-10-04 19:21:36 -04:00
Markus Frindt
0522ae408c Fix the query to retrieve a phone number as account user (#237)
Co-authored-by: Markus Frindt <m.frindt@cognigy.com>
2023-09-27 10:09:54 -04:00
Hoan Luu Huu
9c788cdedc add cobalt stt (#233)
* add cobalt stt

* fix review comments
2023-09-26 09:00:16 -04:00
Hoan Luu Huu
cbc5e2d6f7 fix jambones-sql.sql missing FOREIGN_KEY_CHECKS (#236) 2023-09-25 19:57:43 -04:00
Hoan Luu Huu
f4d6fd14b8 allow sip port is null (#232)
* allow sip port is null

* update upgrade script

* fix review comment
2023-09-25 19:54:43 -04:00
Anton Voylenko
b190334839 validate recording auth (#235) 2023-09-24 08:19:13 -04:00
Hoan Luu Huu
209a58ff51 add try catch for mp3 encoder (#234) 2023-09-20 22:31:17 -04:00
Dave Horton
f8720bab9f update to jambonz.cloud for saas 2023-09-20 20:56:18 -04:00
Dave Horton
77363d54d1 #230 - support for option to pad crypto on outdial using srtp (#231) 2023-09-15 13:34:03 -04:00
Markus Frindt
ad483ba0b7 Add try catch to getUpload to catch init errors with invalid credentials (#229)
* add try catch to getUpload to catch init errors with invalid credentials

* properly handle errors occured while streaming

---------

Co-authored-by: Markus Frindt <m.frindt@cognigy.com>
2023-09-13 07:52:28 -04:00
Anton Voylenko
02c9a951d4 S3 compatible storage (#228)
* compatible credential test

* support s3 compatible storages

* fix typo

* change logging

* add missing option
2023-09-12 12:25:06 -04:00
EgleH
d5f5e3a86f Filter phone numbers result (#227)
Co-authored-by: eglehelms <e.helms@cognigy.com>
Co-authored-by: Hoan Luu Huu <110280845+xquanluu@users.noreply.github.com>
2023-08-31 12:04:16 -04:00
Hoan Luu Huu
62cea3a9e9 update LCC transcribe status (#225) 2023-08-30 22:54:56 -04:00
Hoan Luu Huu
6d3bfd527e feat azure fromhost (#214)
* feat azure fromhost

* wip

* wip

* wip
2023-08-30 21:06:03 -04:00
Hoan Luu Huu
9002bacf8f fix account level get phone number (#217)
* fix account level get phone number

* fix account level get phone number
2023-08-30 09:24:29 -04:00
Hoan Luu Huu
92473454d6 support delete record (#224)
* support delete record

* wip

* wip
2023-08-23 12:51:49 -04:00
Hoan Luu Huu
1c2280af88 fix fallback init sql (#223) 2023-08-22 19:28:43 -04:00
Hoan Luu Huu
7d16bdd774 feat fallback speech vendors (#220)
* feat fallback speech vendors

* wip

* update verb specification
2023-08-22 09:22:39 -04:00
Hoan Luu Huu
79e1bc8d12 support moh (#219) 2023-08-22 08:05:09 -04:00
Hoan Luu Huu
9d24ef6238 Support azure storage (#221)
* azure storage

* azure uploader

* azure uploader

* azure uploader

* fix
2023-08-22 07:50:30 -04:00
Dave Horton
042ad9f629 update to jambonz.cloud 2023-08-18 08:41:17 -04:00
Hoan Luu Huu
7351f0ad68 feat support multi speech credential with diff labels and same vendor (#218)
* feat support multi speech credential with diff labels and same vendor

* fix review comment

* wip

* fix review comments

* update verb spec version
2023-08-15 08:53:16 -04:00
Dave Horton
de7b74f898 fix exception when receiving webhook with no type (#213) 2023-08-03 19:34:38 -04:00
Hoan Luu Huu
d361f1aeb1 fix record all call does not work on wav format (#211)
* fix #210

* fix throw error without new

* fix throw error without new
2023-08-01 07:53:58 -04:00
Hoan Luu Huu
f3d002cfca fix record format (#210)
* fix record format

* fix assert require

* fix assert require
2023-07-30 22:42:38 -04:00
Hoan Luu Huu
3121c2a197 fix hosted app, register by email (#196)
* fix hosted app, register by email

* update mailgun configuration

* update payment method when update card

* fix

* fix

* fix

* change free plan settings

* fix forgot password

* fix forgot password

* fix

* fix
2023-07-30 22:35:38 -04:00
Anton Voylenko
b7bdf300c6 fix sip request payload validation (#209) 2023-07-29 11:13:02 -04:00
Hoan Luu Huu
c96159268e feat google storage (#207)
* feat google storage

* feat google storage

* add google storage writablestream

* add google storage writablestream

* add google storage writablestream

* add metadata to google storage

* add metadata to google storage

* add metadata to google storage

* add tags to google storage

* fix

* fix

* fix

* fix
2023-07-28 12:04:40 -04:00
Dave Horton
8e200251ca slight change to pino logger construction (#206)
* slight change to pino logger construction

* remove console.log in test

* added test logging back in

* test
2023-07-23 11:26:57 -04:00
Hoan Luu Huu
898f3aec4a update verb specification (#204) 2023-07-20 09:00:18 -04:00
Hoan Luu Huu
6f85752352 fix custom speech cannot update urls (#199) 2023-07-17 19:15:04 -04:00
dependabot[bot]
fe7cc9ad58 Bump fast-xml-parser, @aws-sdk/client-transcribe, @aws-sdk/client-s3 and @aws-sdk/client-polly (#192)
Bumps [fast-xml-parser](https://github.com/NaturalIntelligence/fast-xml-parser) to 4.2.5 and updates ancestor dependencies [fast-xml-parser](https://github.com/NaturalIntelligence/fast-xml-parser), [@aws-sdk/client-transcribe](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/clients/client-transcribe), [@aws-sdk/client-s3](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/clients/client-s3) and [@aws-sdk/client-polly](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/clients/client-polly). These dependencies need to be updated together.


Updates `fast-xml-parser` from 4.2.4 to 4.2.5
- [Release notes](https://github.com/NaturalIntelligence/fast-xml-parser/releases)
- [Changelog](https://github.com/NaturalIntelligence/fast-xml-parser/blob/master/CHANGELOG.md)
- [Commits](https://github.com/NaturalIntelligence/fast-xml-parser/compare/v4.2.4...v4.2.5)

Updates `@aws-sdk/client-transcribe` from 3.348.0 to 3.359.0
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/clients/client-transcribe/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.359.0/clients/client-transcribe)

Updates `@aws-sdk/client-s3` from 3.348.0 to 3.359.0
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/clients/client-s3/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.359.0/clients/client-s3)

Updates `@aws-sdk/client-polly` from 3.348.0 to 3.359.0
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/clients/client-polly/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.359.0/clients/client-polly)

---
updated-dependencies:
- dependency-name: fast-xml-parser
  dependency-type: indirect
- dependency-name: "@aws-sdk/client-transcribe"
  dependency-type: direct:production
- dependency-name: "@aws-sdk/client-s3"
  dependency-type: direct:production
- dependency-name: "@aws-sdk/client-polly"
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-05 10:44:54 +01:00
Hoan Luu Huu
1ffdfebdb2 multi srs (#189) 2023-07-05 08:15:46 +01:00
Dave Horton
dcf1895920 db_upgrade: add missing schema change to add sip_gateways.protocol 2023-07-03 13:38:17 +01:00
Hoan Luu Huu
c509b9d277 feat add recent call filter (#197)
* feat add recent call filter

* update timeseries

* add filter to swagger recent call
2023-07-03 08:25:05 +01:00
Hoan Luu Huu
eff8474997 fix sp user cannot fetch sbcs (#195) 2023-06-29 11:06:59 +01:00
Dave Horton
b4237beeeb minor logging 2023-06-28 09:22:17 +01:00
Dave Horton
0406e42c19 logging 2023-06-25 14:08:15 +01:00
Dave Horton
533cd2f47d minor logging 2023-06-25 14:01:30 +01:00
Dave Horton
742884cc72 fix parens in upgrade script 2023-06-25 13:07:42 +01:00
Dave Horton
9fccfa2a73 bugfix: 0.8.4 schema upgrades were not being applied 2023-06-25 12:58:21 +01:00
Dave Horton
3356b7302a 0.8.4 2023-06-24 20:23:28 +01:00
Hoan Luu Huu
9f533ed17c use fs-service-url redis cache set (#191) 2023-06-23 14:26:33 +01:00
Hoan Luu Huu
a0797a3a4c encrypt client password and fix upgrade db script (#188)
* encrypt client password and fix upgrade db script

* encrypt client password and fix upgrade db script

* obfuscate client password
2023-06-15 20:46:22 -04:00
Hoan Luu Huu
0b33ef0c2c Feat: jambonz Client (#185)
* feat: client schema change

* feat: add testcases

* fix typo

* hash client password

* fix fk

* upgrade script

* fix failing testcase
2023-06-14 21:04:14 -04:00
Dave Horton
71ecf453f8 allow identical phone_numbers to exist from different carriers (#186) 2023-06-14 08:23:08 -04:00
Dave Horton
494f1cf784 update dependencies (#184) 2023-06-09 15:20:35 -04:00
Snyk bot
da74e2526a fix: package.json & package-lock.json to reduce vulnerabilities (#182)
The following vulnerabilities are fixed with an upgrade:
- https://snyk.io/vuln/SNYK-JS-FASTXMLPARSER-5668858
2023-06-09 15:01:12 -04:00
Hoan Luu Huu
e35a03c7ad feat: Record all calls (#169)
* feat: schema change

* feat: record all calls

* add bucket test for S3

* wip: add S3 upload stream implementation

* wip

* wip: add ws server

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip: modify sub folder

* wip: add record endpoint

* wip: add record endpoint

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* fix: failing testcase

* bucket credentials with tags

* add tagging

* wip

* wip

* wip

* wip

* wip

* wip

* fixed phone number is not in order

* feat: schema change

* feat: record all calls

* add bucket test for S3

* wip: add S3 upload stream implementation

* wip

* wip: add ws server

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip: modify sub folder

* wip: add record endpoint

* wip: add record endpoint

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* fix: failing testcase

* bucket credentials with tags

* add tagging

* wip

* wip

* wip

* wip

* wip

* fixed phone number is not in order

* add schema changes to upgrade script

* use aws-sdk v3

* jambonz lamejs

* jambonz lamejs

* add back wav encoder

* wip: add record format to schema

* add record_format

* fix: record file ext

* fix: record file ext

* fix: record file ext

* fix: record file ext

* fix download audio

* bug fix: dtmf metadata is causing closure of websocket

* fix: add extra data to S3 metadata

* upgrade db script

* bugfix: region was being ignored in test s3 upload

---------

Co-authored-by: Dave Horton <daveh@beachdognet.com>
2023-06-09 14:57:06 -04:00
Hoan Luu Huu
46fb9b8875 feat: filter call by from and to (#180) 2023-06-08 19:36:54 -04:00
Hoan Luu Huu
f9df2b3028 feat: sentinel configuration (#178)
* feat: sentinel configuration

* update

* redis update
2023-06-07 10:04:03 -04:00
Anton Voylenko
32ff023b14 feat: support sorted set queues (#177)
* feat: support sorted set queues

* fix: tune tests for queue
2023-06-06 16:14:38 -04:00
Hoan Luu Huu
f3d3afee73 feat: clear account tts cache (#176)
* feat: clear account tts cache

* get parsed account_sid
2023-06-02 07:40:14 -04:00
Hoan Luu Huu
3c8cbd97c5 fix: app_json is applied to outbound call (#173) 2023-06-01 10:20:25 -04:00
Hoan Luu Huu
eba9c98412 feat tts clear cache (#175)
* feat tts clear cache

* feat tts clear cache
2023-06-01 07:48:02 -04:00
Dave Horton
c2065ef787 fix twilio sip gateway addresses, previously had an invalid CIDR 2023-05-31 09:29:05 -04:00
Dave Horton
307787526d bugfix: one account could potentially use speech creds from a different account 2023-05-30 14:58:54 -04:00
Hoan Luu Huu
3141646dfd update db-helper and redis (#174) 2023-05-29 09:50:22 -04:00
Dave Horton
cac6e2117d fix typo in prev commit 2023-05-24 09:12:55 -04:00
Dave Horton
6d34d6f886 docker build fix for db-create job 2023-05-24 09:11:36 -04:00
Dave Horton
964afc1660 fix docker build 2023-05-24 09:05:34 -04:00
Hoan Luu Huu
d09dca47b9 wip (#172) 2023-05-24 08:29:12 -04:00
Dave Horton
f3ec847474 fix docker build 2023-05-15 14:07:39 -04:00
Dave Horton
cf7ce675f5 fix to db upgrade script 2023-05-15 09:54:34 -04:00
Hoan Luu Huu
34895daf4f fix admin setting issue (#168) 2023-05-11 20:27:19 -04:00
Dave Horton
b06032b5f0 0.8.3 2023-05-11 09:24:57 -04:00
Hoan Luu Huu
3486ff958c feat: add protocol to sip-gateways (#166)
* feat: add protocol to sip-gateways

* add tls/srtp options

* fix sql

* update db script has new changes

---------

Co-authored-by: Dave Horton <daveh@beachdognet.com>
2023-05-10 15:41:39 -04:00
Dave Horton
f79f96b884 update deps 2023-05-08 13:10:44 -04:00
Hoan Luu Huu
2aa3d40268 fix: remove metadata out of rest:dial (#165) 2023-05-07 08:33:56 -04:00
Hoan Luu Huu
148fc49f06 feat: add metadata for create call (#164) 2023-05-07 07:21:46 -04:00
Dave Horton
02806a109c added schema changes for LCR (#150)
* added schema changes for LCR

* fix FK

* first draft

* force drop table

* add testcases

* swagger updated

* update code

* wip: add service provider LCR

* fix userpermission on lcr

* add lcr.is_active

* remove FK constraints on lcr

* wip

* wip

* wip

* fix: review comments

* fix: final review

* fix: final review

* fix: update database schema

* fix: update database schema

* fix: update database schema

* update schema

* fix: review comments

* lcr_routes.priority should not be unique

* fix review comments

---------

Co-authored-by: Quan HL <quan.luuhoang8@gmail.com>
2023-05-05 20:09:34 -04:00
Dave Horton
077c791e37 update integration test data with new twilio IP range 2023-05-04 13:14:28 -04:00
Hoan Luu Huu
4b70c6458a feat: system information (#162) 2023-05-04 13:12:29 -04:00
Paulo Telles
aadb0b15f2 change response text to avoid reveal user's data (#161)
* change response text to avoid reveal user's data

* include log into forgot password

---------

Co-authored-by: p.souza <p.souza@cognigy.com>
2023-05-04 08:39:04 -04:00
Anton Voylenko
3997f57365 update swagger docs (#157) 2023-05-01 10:50:22 -04:00
Dave Horton
c97874ed1f add tls_port and wss_port to sbc_addresses, update some deps (#160)
* add tls_port and wss_port to sbc_addresses, update some deps

* add system_information table
2023-05-01 10:45:19 -04:00
Markus Frindt
1dcc92a177 Fix bug in forgot-password req.user destruction (#159)
* Fix bug in forgot-password req.user destruction

* add test for forgot password

---------

Co-authored-by: Markus Frindt <m.frindt@cognigy.com>
2023-04-28 08:43:23 -04:00
EgleH
105aa16ffe SP users were not able to update Phone numbers (#158)
Co-authored-by: eglehelms <e.helms@cognigy.com>
2023-04-24 07:46:57 -04:00
Anton Voylenko
a574045f8a endpoint to retrieve active queues (#156) 2023-04-22 14:48:07 -04:00
Anton Voylenko
af3d03bef9 support filtering for retrieve info endpoint (#153)
* support filtering for retrieve info endpoint

* bump realtimedb-helpers
2023-04-19 07:33:24 -04:00
Anton Voylenko
5b1b50c3a3 remove unnecessary await (#152) 2023-04-18 13:01:38 -04:00
EgleH
ba431aeb35 Fix 403 for SP calling RecentCalls/Alerts via /Accounts route (#149)
* fix 403 for SP calling RecentCalls/Alerts via /Accounts route

* update base image

* update base image

---------

Co-authored-by: eglehelms <e.helms@cognigy.com>
2023-04-12 13:22:40 -04:00
Antony Jukes
36607b505f added retrieve jaeger trace endpoint. (#147) 2023-04-10 13:35:22 -04:00
Dave Horton
616a0b364d push to docker 2023-04-10 09:40:50 -04:00
79 changed files with 10194 additions and 4826 deletions

View File

@@ -2,16 +2,10 @@ name: Docker
on:
push:
# Publish `main` as Docker `latest` image.
branches:
- main
# Publish `v1.2.3` tags as releases.
tags:
- v*
env:
IMAGE_NAME: db-create
- '*'
jobs:
push:
@@ -20,32 +14,42 @@ jobs:
if: github.event_name == 'push'
steps:
- uses: actions/checkout@v3
- name: Checkout code
uses: actions/checkout@v3
- name: Build image
run: docker build . --file Dockerfile.db-create --tag $IMAGE_NAME
- name: Log into registry
run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
- name: Push image
- name: prepare tag
id: prepare_tag
run: |
IMAGE_ID=ghcr.io/${{ github.repository_owner }}/$IMAGE_NAME
IMAGE_ID=jambonz/db-create
# Change all uppercase to lowercase
IMAGE_ID=$(echo $IMAGE_ID | tr '[A-Z]' '[a-z]')
# Strip git ref prefix from version
VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,')
# Strip git ref prefix from version
VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,')
# Strip "v" prefix from tag name
[[ "${{ github.ref }}" == "refs/tags/"* ]] && VERSION=$(echo $VERSION | sed -e 's/^v//')
# Strip "v" prefix from tag name
[[ "${{ github.ref }}" == "refs/tags/"* ]] && VERSION=$(echo $VERSION | sed -e 's/^v//')
# Use Docker `latest` tag convention
[ "$VERSION" == "main" ] && VERSION=latest
# Use Docker `latest` tag convention
[ "$VERSION" == "main" ] && VERSION=latest
echo IMAGE_ID=$IMAGE_ID
echo VERSION=$VERSION
echo IMAGE_ID=$IMAGE_ID
echo VERSION=$VERSION
echo "image_id=$IMAGE_ID" >> $GITHUB_OUTPUT
echo "version=$VERSION" >> $GITHUB_OUTPUT
docker tag $IMAGE_NAME $IMAGE_ID:$VERSION
docker push $IMAGE_ID:$VERSION
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push Docker image
uses: docker/build-push-action@v4
with:
context: .
file: ./Dockerfile.db-create
push: true
tags: ${{ steps.prepare_tag.outputs.image_id }}:${{ steps.prepare_tag.outputs.version }}
build-args: |
GITHUB_REPOSITORY=$GITHUB_REPOSITORY
GITHUB_REF=$GITHUB_REF

View File

@@ -2,16 +2,10 @@ name: Docker
on:
push:
# Publish `main` as Docker `latest` image.
branches:
- main
# Publish `v1.2.3` tags as releases.
tags:
- v*
env:
IMAGE_NAME: api-server
- '*'
jobs:
push:
@@ -20,32 +14,41 @@ jobs:
if: github.event_name == 'push'
steps:
- uses: actions/checkout@v3
- name: Checkout code
uses: actions/checkout@v3
- name: Build image
run: docker build . --file Dockerfile --tag $IMAGE_NAME
- name: Log into registry
run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
- name: Push image
- name: prepare tag
id: prepare_tag
run: |
IMAGE_ID=ghcr.io/${{ github.repository_owner }}/$IMAGE_NAME
IMAGE_ID=jambonz/api-server
# Change all uppercase to lowercase
IMAGE_ID=$(echo $IMAGE_ID | tr '[A-Z]' '[a-z]')
# Strip git ref prefix from version
VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,')
# Strip git ref prefix from version
VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,')
# Strip "v" prefix from tag name
[[ "${{ github.ref }}" == "refs/tags/"* ]] && VERSION=$(echo $VERSION | sed -e 's/^v//')
# Strip "v" prefix from tag name
[[ "${{ github.ref }}" == "refs/tags/"* ]] && VERSION=$(echo $VERSION | sed -e 's/^v//')
# Use Docker `latest` tag convention
[ "$VERSION" == "main" ] && VERSION=latest
# Use Docker `latest` tag convention
[ "$VERSION" == "main" ] && VERSION=latest
echo IMAGE_ID=$IMAGE_ID
echo VERSION=$VERSION
echo IMAGE_ID=$IMAGE_ID
echo VERSION=$VERSION
echo "image_id=$IMAGE_ID" >> $GITHUB_OUTPUT
echo "version=$VERSION" >> $GITHUB_OUTPUT
docker tag $IMAGE_NAME $IMAGE_ID:$VERSION
docker push $IMAGE_ID:$VERSION
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push Docker image
uses: docker/build-push-action@v4
with:
context: .
push: true
tags: ${{ steps.prepare_tag.outputs.image_id }}:${{ steps.prepare_tag.outputs.version }}
build-args: |
GITHUB_REPOSITORY=$GITHUB_REPOSITORY
GITHUB_REF=$GITHUB_REF

View File

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

View File

@@ -33,6 +33,8 @@ Configuration is provided via environment variables:
|K8S| service running as kubernetes service |no|
|K8S_FEATURE_SERVER_SERVICE_NAME| feature server name(required for K8S) |no|
|K8S_FEATURE_SERVER_SERVICE_PORT| feature server port(required for K8S) |no|
|JAMBONZ_RECORD_WS_USERNAME| recording websocket username|no|
|JAMBONZ_RECORD_WS_PASSWORD| recording websocket password|no|
#### Database dependency
A mysql database is used to store long-lived objects such as Accounts, Applications, etc. To create the database schema, use or review the scripts in the 'db' folder, particularly:

74
app.js
View File

@@ -13,7 +13,12 @@ assert.ok(process.env.JAMBONES_MYSQL_HOST &&
process.env.JAMBONES_MYSQL_USER &&
process.env.JAMBONES_MYSQL_PASSWORD &&
process.env.JAMBONES_MYSQL_DATABASE, 'missing JAMBONES_MYSQL_XXX env vars');
assert.ok(process.env.JAMBONES_REDIS_HOST, 'missing JAMBONES_REDIS_HOST env var');
if (process.env.JAMBONES_REDIS_SENTINELS) {
assert.ok(process.env.JAMBONES_REDIS_SENTINEL_MASTER_NAME,
'missing JAMBONES_REDIS_SENTINEL_MASTER_NAME env var, JAMBONES_REDIS_SENTINEL_PASSWORD env var is optional');
} else {
assert.ok(process.env.JAMBONES_REDIS_HOST, 'missing JAMBONES_REDIS_HOST env var');
}
assert.ok(process.env.JAMBONES_TIME_SERIES_HOST, 'missing JAMBONES_TIME_SERIES_HOST env var');
assert.ok(process.env.ENCRYPTION_SECRET || process.env.JWT_SECRET, 'missing ENCRYPTION_SECRET env var');
assert.ok(process.env.JWT_SECRET, 'missing JWT_SECRET env var');
@@ -33,16 +38,20 @@ const {
retrieveCall,
deleteCall,
listCalls,
listSortedSets,
purgeCalls,
retrieveSet,
addKey,
retrieveKey,
deleteKey,
incrKey
incrKey,
JAMBONES_REDIS_SENTINELS
} = require('./lib/helpers/realtimedb-helpers');
const {
getTtsVoices
} = require('@jambonz/speech-utils')({
getTtsVoices,
getTtsSize,
purgeTtsCache
} = require('@jambonz/speech-utils')(JAMBONES_REDIS_SENTINELS || {
host: process.env.JAMBONES_REDIS_HOST,
port: process.env.JAMBONES_REDIS_PORT || 6379
}, logger);
@@ -65,6 +74,7 @@ const {
const PORT = process.env.HTTP_PORT || 3000;
const authStrategy = require('./lib/auth')(logger, retrieveKey);
const {delayLoginMiddleware} = require('./lib/middleware');
const Websocket = require('ws');
passport.use(authStrategy);
@@ -75,6 +85,7 @@ app.locals = {
retrieveCall,
deleteCall,
listCalls,
listSortedSets,
purgeCalls,
retrieveSet,
addKey,
@@ -82,6 +93,8 @@ app.locals = {
retrieveKey,
deleteKey,
getTtsVoices,
getTtsSize,
purgeTtsCache,
lookupAppBySid,
lookupAccountBySid,
lookupAccountByPhoneNumber,
@@ -112,6 +125,12 @@ const limiter = rateLimit({
legacyHeaders: false, // Disable the `X-RateLimit-*` headers
});
// Setup websocket for recording audio
const recordWsServer = require('./lib/record');
const wsServer = new Websocket.Server({ noServer: true });
wsServer.setMaxListeners(0);
wsServer.on('connection', recordWsServer.bind(null, logger));
if (process.env.JAMBONES_TRUST_PROXY) {
const proxyCount = parseInt(process.env.JAMBONES_TRUST_PROXY);
if (!isNaN(proxyCount) && proxyCount > 0) {
@@ -152,7 +171,52 @@ app.use((err, req, res, next) => {
});
});
logger.info(`listening for HTTP traffic on port ${PORT}`);
app.listen(PORT);
const server = app.listen(PORT);
const isValidWsKey = (hdr) => {
const username = process.env.JAMBONZ_RECORD_WS_USERNAME || process.env.JAMBONES_RECORD_WS_USERNAME;
const password = process.env.JAMBONZ_RECORD_WS_PASSWORD || process.env.JAMBONES_RECORD_WS_PASSWORD;
if (username && password) {
if (!hdr) {
// auth header is missing
return false;
}
const token = Buffer.from(`${username}:${password}`).toString('base64');
const arr = /^Basic (.*)$/.exec(hdr);
if (!Array.isArray(arr)) {
// malformed auth header
return false;
}
return arr[1] === token;
}
return true;
};
server.on('upgrade', (request, socket, head) => {
logger.debug({
url: request.url,
headers: request.headers,
}, 'received upgrade request');
/* verify the path starts with /transcribe */
if (!request.url.includes('/record/')) {
logger.info(`unhandled path: ${request.url}`);
return socket.write('HTTP/1.1 404 Not Found \r\n\r\n', () => socket.destroy());
}
/* verify the api key */
if (!isValidWsKey(request.headers['authorization'])) {
logger.info(`invalid auth header: ${request.headers['authorization'] || 'authorization header missing'}`);
return socket.write('HTTP/1.1 403 Forbidden \r\n\r\n', () => socket.destroy());
}
/* complete the upgrade */
wsServer.handleUpgrade(request, socket, head, (ws) => {
logger.info(`upgraded to websocket, url: ${request.url}`);
wsServer.emit('connection', ws, request.url);
});
});
// purge old calls from active call set every 10 mins
async function purge() {

View File

@@ -1,5 +1,4 @@
/* SQLEditor (MySQL (2))*/
SET FOREIGN_KEY_CHECKS=0;
DROP TABLE IF EXISTS account_static_ips;
@@ -14,8 +13,12 @@ DROP TABLE IF EXISTS beta_invite_codes;
DROP TABLE IF EXISTS call_routes;
DROP TABLE IF EXISTS clients;
DROP TABLE IF EXISTS dns_records;
DROP TABLE IF EXISTS lcr;
DROP TABLE IF EXISTS lcr_carrier_set_entry;
DROP TABLE IF EXISTS lcr_routes;
@@ -52,6 +55,8 @@ DROP TABLE IF EXISTS smpp_addresses;
DROP TABLE IF EXISTS speech_credentials;
DROP TABLE IF EXISTS system_information;
DROP TABLE IF EXISTS users;
DROP TABLE IF EXISTS smpp_gateways;
@@ -124,6 +129,16 @@ application_sid CHAR(36) NOT NULL,
PRIMARY KEY (call_route_sid)
) COMMENT='a regex-based pattern match for call routing';
CREATE TABLE clients
(
client_sid CHAR(36) NOT NULL UNIQUE ,
account_sid CHAR(36) NOT NULL,
is_active BOOLEAN NOT NULL DEFAULT 1,
username VARCHAR(64),
password VARCHAR(1024),
PRIMARY KEY (client_sid)
);
CREATE TABLE dns_records
(
dns_record_sid CHAR(36) NOT NULL UNIQUE ,
@@ -136,11 +151,23 @@ PRIMARY KEY (dns_record_sid)
CREATE TABLE lcr_routes
(
lcr_route_sid CHAR(36),
lcr_sid CHAR(36) NOT NULL,
regex VARCHAR(32) NOT NULL COMMENT 'regex-based pattern match against dialed number, used for LCR routing of PSTN calls',
description VARCHAR(1024),
priority INTEGER NOT NULL UNIQUE COMMENT 'lower priority routes are attempted first',
priority INTEGER NOT NULL COMMENT 'lower priority routes are attempted first',
PRIMARY KEY (lcr_route_sid)
) COMMENT='Least cost routing table';
) COMMENT='An ordered list of digit patterns in an LCR table. The patterns are tested in sequence until one matches';
CREATE TABLE lcr
(
lcr_sid CHAR(36) NOT NULL UNIQUE ,
name VARCHAR(64) COMMENT 'User-assigned name for this LCR table',
is_active BOOLEAN NOT NULL DEFAULT 1,
default_carrier_set_entry_sid CHAR(36) COMMENT 'default carrier/route to use when no digit match based results are found.',
service_provider_sid CHAR(36),
account_sid CHAR(36),
PRIMARY KEY (lcr_sid)
) COMMENT='An LCR (least cost routing) table that is used by a service provider or account to make decisions about routing outbound calls when multiple carriers are available.';
CREATE TABLE password_settings
(
@@ -248,6 +275,8 @@ CREATE TABLE sbc_addresses
sbc_address_sid CHAR(36) NOT NULL UNIQUE ,
ipv4 VARCHAR(255) NOT NULL,
port INTEGER NOT NULL DEFAULT 5060,
tls_port INTEGER,
wss_port INTEGER,
service_provider_sid CHAR(36),
last_updated DATETIME,
PRIMARY KEY (sbc_address_sid)
@@ -305,9 +334,17 @@ last_tested DATETIME,
tts_tested_ok BOOLEAN,
stt_tested_ok BOOLEAN,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
label VARCHAR(64),
PRIMARY KEY (speech_credential_sid)
);
CREATE TABLE system_information
(
domain_name VARCHAR(255),
sip_domain_name VARCHAR(255),
monitoring_domain_name VARCHAR(255)
);
CREATE TABLE users
(
user_sid CHAR(36) NOT NULL UNIQUE ,
@@ -387,7 +424,7 @@ PRIMARY KEY (smpp_gateway_sid)
CREATE TABLE phone_numbers
(
phone_number_sid CHAR(36) UNIQUE ,
number VARCHAR(132) NOT NULL UNIQUE ,
number VARCHAR(132) NOT NULL,
voip_carrier_sid CHAR(36),
account_sid CHAR(36),
application_sid CHAR(36),
@@ -400,11 +437,13 @@ CREATE TABLE sip_gateways
sip_gateway_sid CHAR(36),
ipv4 VARCHAR(128) NOT NULL COMMENT 'ip address or DNS name of the gateway. For gateways providing inbound calling service, ip address is required.',
netmask INTEGER NOT NULL DEFAULT 32,
port INTEGER NOT NULL DEFAULT 5060 COMMENT 'sip signaling port',
port INTEGER COMMENT 'sip signaling port',
inbound BOOLEAN NOT NULL COMMENT 'if true, whitelist this IP to allow inbound calls from the gateway',
outbound BOOLEAN NOT NULL COMMENT 'if true, include in least-cost routing when placing calls to the PSTN',
voip_carrier_sid CHAR(36) NOT NULL,
is_active BOOLEAN NOT NULL DEFAULT 1,
pad_crypto BOOLEAN NOT NULL DEFAULT 0,
protocol ENUM('udp','tcp','tls', 'tls/srtp') DEFAULT 'udp' COMMENT 'Outbound call protocol',
PRIMARY KEY (sip_gateway_sid)
) COMMENT='A whitelisted sip gateway used for origination/termination';
@@ -441,9 +480,20 @@ 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),
speech_synthesis_label VARCHAR(64),
speech_recognizer_vendor VARCHAR(64) NOT NULL DEFAULT 'google',
speech_recognizer_language VARCHAR(64) NOT NULL DEFAULT 'en-US',
speech_recognizer_label VARCHAR(64),
use_for_fallback_speech BOOLEAN DEFAULT false,
fallback_speech_synthesis_vendor VARCHAR(64),
fallback_speech_synthesis_language VARCHAR(12),
fallback_speech_synthesis_voice VARCHAR(64),
fallback_speech_synthesis_label VARCHAR(64),
fallback_speech_recognizer_vendor VARCHAR(64),
fallback_speech_recognizer_language VARCHAR(64),
fallback_speech_recognizer_label VARCHAR(64),
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
record_all_calls BOOLEAN NOT NULL DEFAULT false,
PRIMARY KEY (application_sid)
) COMMENT='A defined set of behaviors to be applied to phone calls ';
@@ -481,6 +531,9 @@ subspace_client_secret VARCHAR(255),
subspace_sip_teleport_id VARCHAR(255),
subspace_sip_teleport_destinations VARCHAR(255),
siprec_hook_sid CHAR(36),
record_all_calls BOOLEAN NOT NULL DEFAULT false,
record_format VARCHAR(16) NOT NULL DEFAULT 'mp3',
bucket_credential VARCHAR(8192) COMMENT 'credential used to authenticate with storage service',
PRIMARY KEY (account_sid)
) COMMENT='An enterprise that uses the platform for comm services';
@@ -501,9 +554,20 @@ ALTER TABLE call_routes ADD FOREIGN KEY account_sid_idxfk_3 (account_sid) REFERE
ALTER TABLE call_routes ADD FOREIGN KEY application_sid_idxfk (application_sid) REFERENCES applications (application_sid);
CREATE INDEX client_sid_idx ON clients (client_sid);
ALTER TABLE clients ADD CONSTRAINT account_sid_idxfk_13 FOREIGN KEY account_sid_idxfk_13 (account_sid) REFERENCES accounts (account_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 lcr_sid_idx ON lcr_routes (lcr_sid);
ALTER TABLE lcr_routes ADD FOREIGN KEY lcr_sid_idxfk (lcr_sid) REFERENCES lcr (lcr_sid);
CREATE INDEX lcr_sid_idx ON lcr (lcr_sid);
ALTER TABLE lcr ADD FOREIGN KEY default_carrier_set_entry_sid_idxfk (default_carrier_set_entry_sid) REFERENCES lcr_carrier_set_entry (lcr_carrier_set_entry_sid);
CREATE INDEX service_provider_sid_idx ON lcr (service_provider_sid);
CREATE INDEX account_sid_idx ON lcr (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);
@@ -557,8 +621,6 @@ CREATE INDEX smpp_address_sid_idx ON smpp_addresses (smpp_address_sid);
CREATE INDEX service_provider_sid_idx ON smpp_addresses (service_provider_sid);
ALTER TABLE smpp_addresses ADD FOREIGN KEY service_provider_sid_idxfk_4 (service_provider_sid) REFERENCES service_providers (service_provider_sid);
CREATE UNIQUE INDEX speech_credentials_idx_1 ON speech_credentials (vendor,account_sid);
CREATE INDEX speech_credential_sid_idx ON speech_credentials (speech_credential_sid);
CREATE INDEX service_provider_sid_idx ON speech_credentials (service_provider_sid);
ALTER TABLE speech_credentials ADD FOREIGN KEY service_provider_sid_idxfk_5 (service_provider_sid) REFERENCES service_providers (service_provider_sid);
@@ -595,6 +657,8 @@ 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);
CREATE UNIQUE INDEX phone_numbers_unique_idx_voip_carrier_number ON phone_numbers (number,voip_carrier_sid);
CREATE INDEX phone_number_sid_idx ON phone_numbers (phone_number_sid);
CREATE INDEX number_idx ON phone_numbers (number);
CREATE INDEX voip_carrier_sid_idx ON phone_numbers (voip_carrier_sid);
@@ -649,5 +713,4 @@ ALTER TABLE accounts ADD FOREIGN KEY queue_event_hook_sid_idxfk (queue_event_hoo
ALTER TABLE accounts ADD FOREIGN KEY device_calling_application_sid_idxfk (device_calling_application_sid) REFERENCES applications (application_sid);
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

@@ -22,7 +22,7 @@ values ('3f35518f-5a0d-4c2e-90a5-2407bb3b36f0', '38700987-c7a4-4685-a5bb-af378f9
-- create one service provider and one account
insert into service_providers (service_provider_sid, name, root_domain)
values ('2708b1b3-2736-40ea-b502-c53d8396247f', 'default service provider', 'sip.jambonz.us');
values ('2708b1b3-2736-40ea-b502-c53d8396247f', 'default service provider', 'sip.jambonz.cloud');
insert into accounts (account_sid, service_provider_sid, name, webhook_secret)
values ('9351f46a-678c-43f5-b8a6-d4eb58d131af','2708b1b3-2736-40ea-b502-c53d8396247f', 'default account', 'wh_secret_cJqgtMDPzDhhnjmaJH6Mtk');
@@ -38,9 +38,9 @@ values ('3f35518f-5a0d-4c2e-90a5-2407bb3b36fs', '38700987-c7a4-4685-a5bb-af378f9
-- create two applications
insert into webhooks(webhook_sid, url, method)
values
('84e3db00-b172-4e46-b54b-a503fdb19e4a', 'https://public-apps.jambonz.us/call-status', 'POST'),
('d31568d0-b193-4a05-8ff6-778369bc6efe', 'https://public-apps.jambonz.us/hello-world', 'POST'),
('81844b05-714d-4295-8bf3-3b0640a4bf02', 'https://public-apps.jambonz.us/dial-time', 'POST');
('84e3db00-b172-4e46-b54b-a503fdb19e4a', 'https://public-apps.jambonz.cloud/call-status', 'POST'),
('d31568d0-b193-4a05-8ff6-778369bc6efe', 'https://public-apps.jambonz.cloud/hello-world', 'POST'),
('81844b05-714d-4295-8bf3-3b0640a4bf02', 'https://public-apps.jambonz.cloud/dial-time', 'POST');
insert into applications (application_sid, account_sid, name, call_hook_sid, call_status_hook_sid, speech_synthesis_vendor, speech_synthesis_language, speech_synthesis_voice, speech_recognizer_vendor, speech_recognizer_language)
VALUES
@@ -87,6 +87,7 @@ VALUES
('81a0c8cb-a33e-42da-8f20-99083da6f02f', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '54.252.254.64', 30, 5060, 1, 0),
('eeeef07a-46b8-4ffe-a4f2-04eb32ca889e', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '54.169.127.128', 30, 5060, 1, 0),
('fbb6c194-4b68-4dff-9b42-52412be1c39e', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '177.71.206.192', 30, 5060, 1, 0),
('973e7824-0cf3-4645-88e4-d2460ddb8577', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '168.86.128.0', 18, 5060, 1, 0),
('3ed1dd12-e1a7-44ff-811a-3cc5dc13dc72', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '<your-domain>.pstn.twilio.com', 32, 5060, 0, 1);
-- voxbone gateways

View File

@@ -24,10 +24,9 @@ values ('09e92f3c-9d73-4303-b63f-3668574862ce', '1cf2f4f4-64c4-4249-9a3e-5bb4cb5
-- create two applications
insert into webhooks(webhook_sid, url, method)
values
('84e3db00-b172-4e46-b54b-a503fdb19e4a', 'https://public-apps.jambonz.us/call-status', 'POST'),
('d31568d0-b193-4a05-8ff6-778369bc6efe', 'https://public-apps.jambonz.us/hello-world', 'POST'),
('81844b05-714d-4295-8bf3-3b0640a4bf02', 'https://public-apps.jambonz.us/dial-time', 'POST');
('84e3db00-b172-4e46-b54b-a503fdb19e4a', 'https://public-apps.jambonz.cloud/call-status', 'POST'),
('d31568d0-b193-4a05-8ff6-778369bc6efe', 'https://public-apps.jambonz.cloud/hello-world', 'POST'),
('81844b05-714d-4295-8bf3-3b0640a4bf02', 'https://public-apps.jambonz.cloud/dial-time', 'POST');
insert into applications (application_sid, account_sid, name, call_hook_sid, call_status_hook_sid, speech_synthesis_vendor, speech_synthesis_language, speech_synthesis_voice, speech_recognizer_vendor, speech_recognizer_language)
VALUES
('7087fe50-8acb-4f3b-b820-97b573723aab', '9351f46a-678c-43f5-b8a6-d4eb58d131af', 'hello world', 'd31568d0-b193-4a05-8ff6-778369bc6efe', '84e3db00-b172-4e46-b54b-a503fdb19e4a', 'google', 'en-US', 'en-US-Wavenet-C', 'google', 'en-US'),
@@ -68,14 +67,12 @@ 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),
('eeeef07a-46b8-4ffe-a4f2-04eb32ca889e', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '54.169.127.128', 30, 5060, 1, 0),
('fbb6c194-4b68-4dff-9b42-52412be1c39e', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '177.71.206.192', 30, 5060, 1, 0),
('973e7824-0cf3-4645-88e4-d2460ddb8577', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '168.86.128.0', 18, 5060, 1, 0),
('3ed1dd12-e1a7-44ff-811a-3cc5dc13dc72', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '<your-domain>.pstn.twilio.com', 32, 5060, 0, 1);
-- voxbone gateways

View File

@@ -53,6 +53,7 @@ VALUES
('81a0c8cb-a33e-42da-8f20-99083da6f02f', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '54.252.254.64', 30, 5060, 1, 0),
('eeeef07a-46b8-4ffe-a4f2-04eb32ca889e', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '54.169.127.128', 30, 5060, 1, 0),
('fbb6c194-4b68-4dff-9b42-52412be1c39e', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '177.71.206.192', 30, 5060, 1, 0),
('973e7824-0cf3-4645-88e4-d2460ddb8577', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '168.86.128.0', 18, 5060, 1, 0),
('3ed1dd12-e1a7-44ff-811a-3cc5dc13dc72', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '<your-domain>.pstn.twilio.com', 32, 5060, 0, 1);
-- voxbone gateways

View File

@@ -89,8 +89,92 @@ const sql = {
'ALTER TABLE `users` ADD COLUMN `is_active` BOOLEAN NOT NULL default true',
],
8003: [
'SET FOREIGN_KEY_CHECKS=0',
'ALTER TABLE `voip_carriers` ADD COLUMN `register_status` VARCHAR(4096)',
'ALTER TABLE `sbc_addresses` ADD COLUMN `last_updated` DATETIME',
'ALTER TABLE `sbc_addresses` ADD COLUMN `tls_port` INTEGER',
'ALTER TABLE `sbc_addresses` ADD COLUMN `wss_port` INTEGER',
`CREATE TABLE system_information
(
domain_name VARCHAR(255),
sip_domain_name VARCHAR(255),
monitoring_domain_name VARCHAR(255)
)`,
'DROP TABLE IF EXISTS `lcr_routes`',
'DROP TABLE IF EXISTS `lcr_carrier_set_entry`',
`CREATE TABLE lcr_routes
(
lcr_route_sid CHAR(36),
lcr_sid CHAR(36) NOT NULL,
regex VARCHAR(32) NOT NULL COMMENT 'regex-based pattern match against dialed number, used for LCR routing of PSTN calls',
description VARCHAR(1024),
priority INTEGER NOT NULL COMMENT 'lower priority routes are attempted first',
PRIMARY KEY (lcr_route_sid)
)`,
`CREATE TABLE lcr
(
lcr_sid CHAR(36) NOT NULL UNIQUE ,
name VARCHAR(64) COMMENT 'User-assigned name for this LCR table',
is_active BOOLEAN NOT NULL DEFAULT 1,
default_carrier_set_entry_sid CHAR(36) COMMENT 'default carrier/route to use when no digit match based results are found.',
service_provider_sid CHAR(36),
account_sid CHAR(36),
PRIMARY KEY (lcr_sid)
)`,
`CREATE TABLE lcr_carrier_set_entry
(
lcr_carrier_set_entry_sid CHAR(36),
workload INTEGER NOT NULL DEFAULT 1 COMMENT 'represents a proportion of traffic to send through the associated carrier; can be used for load balancing traffic across carriers with a common priority for a destination',
lcr_route_sid CHAR(36) NOT NULL,
voip_carrier_sid CHAR(36) NOT NULL,
priority INTEGER NOT NULL DEFAULT 0 COMMENT 'lower priority carriers are attempted first',
PRIMARY KEY (lcr_carrier_set_entry_sid)
)`,
'CREATE INDEX lcr_sid_idx ON lcr_routes (lcr_sid)',
'ALTER TABLE lcr_routes ADD FOREIGN KEY lcr_sid_idxfk (lcr_sid) REFERENCES lcr (lcr_sid)',
'CREATE INDEX lcr_sid_idx ON lcr (lcr_sid)',
'ALTER TABLE lcr ADD FOREIGN KEY default_carrier_set_entry_sid_idxfk (default_carrier_set_entry_sid) REFERENCES lcr_carrier_set_entry (lcr_carrier_set_entry_sid)',
'CREATE INDEX service_provider_sid_idx ON lcr (service_provider_sid)',
'CREATE INDEX account_sid_idx ON lcr (account_sid)',
'ALTER TABLE lcr_carrier_set_entry ADD FOREIGN KEY lcr_route_sid_idxfk (lcr_route_sid) REFERENCES lcr_routes (lcr_route_sid)',
'ALTER TABLE lcr_carrier_set_entry ADD FOREIGN KEY voip_carrier_sid_idxfk_3 (voip_carrier_sid) REFERENCES voip_carriers (voip_carrier_sid)',
'SET FOREIGN_KEY_CHECKS=1',
],
8004: [
'alter table accounts add column record_all_calls BOOLEAN NOT NULL DEFAULT false',
'alter table accounts add column bucket_credential VARCHAR(8192)',
'alter table accounts add column record_format VARCHAR(16) NOT NULL DEFAULT \'mp3\'',
'alter table applications add column record_all_calls BOOLEAN NOT NULL DEFAULT false',
'alter table phone_numbers DROP INDEX number',
'create unique index phone_numbers_unique_idx_voip_carrier_number ON phone_numbers (number,voip_carrier_sid)',
`CREATE TABLE clients
(
client_sid CHAR(36) NOT NULL UNIQUE ,
account_sid CHAR(36) NOT NULL,
is_active BOOLEAN NOT NULL DEFAULT 1,
username VARCHAR(64),
password VARCHAR(1024),
PRIMARY KEY (client_sid)
)`,
'CREATE INDEX client_sid_idx ON clients (client_sid)',
'ALTER TABLE clients ADD CONSTRAINT account_sid_idxfk_13 FOREIGN KEY account_sid_idxfk_13 (account_sid) REFERENCES accounts (account_sid)',
'ALTER TABLE sip_gateways ADD COLUMN protocol ENUM(\'udp\',\'tcp\',\'tls\', \'tls/srtp\') DEFAULT \'udp\''
],
8005: [
'DROP INDEX speech_credentials_idx_1 ON speech_credentials',
'ALTER TABLE speech_credentials ADD COLUMN label VARCHAR(64)',
'ALTER TABLE applications ADD COLUMN speech_synthesis_label VARCHAR(64)',
'ALTER TABLE applications ADD COLUMN speech_recognizer_label VARCHAR(64)',
'ALTER TABLE applications ADD COLUMN use_for_fallback_speech BOOLEAN DEFAULT false',
'ALTER TABLE applications ADD COLUMN fallback_speech_synthesis_vendor VARCHAR(64)',
'ALTER TABLE applications ADD COLUMN fallback_speech_synthesis_language VARCHAR(12)',
'ALTER TABLE applications ADD COLUMN fallback_speech_synthesis_voice VARCHAR(64)',
'ALTER TABLE applications ADD COLUMN fallback_speech_synthesis_label VARCHAR(64)',
'ALTER TABLE applications ADD COLUMN fallback_speech_recognizer_vendor VARCHAR(64)',
'ALTER TABLE applications ADD COLUMN fallback_speech_recognizer_language VARCHAR(64)',
'ALTER TABLE applications ADD COLUMN fallback_speech_recognizer_label VARCHAR(64)',
'ALTER TABLE sip_gateways ADD COLUMN pad_crypto BOOLEAN NOT NULL DEFAULT 0',
'ALTER TABLE sip_gateways MODIFY port INTEGER'
]
};
@@ -120,6 +204,9 @@ const doIt = async() => {
if (val < 7006) upgrades.push(...sql['7006']);
if (val < 7007) upgrades.push(...sql['7007']);
if (val < 8000) upgrades.push(...sql['8000']);
if (val < 8003) upgrades.push(...sql['8003']);
if (val < 8004) upgrades.push(...sql['8004']);
if (val < 8005) upgrades.push(...sql['8005']);
// perform all upgrades
logger.info({upgrades}, 'applying schema upgrades..');

View File

@@ -2,7 +2,7 @@ SET FOREIGN_KEY_CHECKS=0;
-- create one service provider
insert into service_providers (service_provider_sid, name, description, root_domain)
values ('2708b1b3-2736-40ea-b502-c53d8396247f', 'jambonz.us', 'jambonz.us service provider', 'sip.yakeeda.com');
values ('2708b1b3-2736-40ea-b502-c53d8396247f', 'jambonz.cloud', 'jambonz.cloud service provider', 'sip.yakeeda.com');
insert into api_keys (api_key_sid, token)
values ('3f35518f-5a0d-4c2e-90a5-2407bb3b36f0', '38700987-c7a4-4685-a5bb-af378f9734de');
@@ -19,8 +19,8 @@ insert into sip_gateways (sip_gateway_sid, voip_carrier_sid, ipv4, port, inbound
values ('46b727eb-c7dc-44fa-b063-96e48d408e4a', '5145b436-2f38-4029-8d4c-fd8c67831c7a', '3.3.3.3', 5060, 1, 1, 1);
-- create the test application and test phone number
insert into webhooks (webhook_sid, url, method) values ('d9c205c6-a129-443e-a9c0-d1bb437d4bb7', 'https://flows.jambonz.us/testCall', 'POST');
insert into webhooks (webhook_sid, url, method) values ('6ac36aeb-6bd0-428a-80a1-aed95640a296', 'https://flows.jambonz.us/callStatus', 'POST');
insert into webhooks (webhook_sid, url, method) values ('d9c205c6-a129-443e-a9c0-d1bb437d4bb7', 'https://flows.jambonz.cloud/testCall', 'POST');
insert into webhooks (webhook_sid, url, method) values ('6ac36aeb-6bd0-428a-80a1-aed95640a296', 'https://flows.jambonz.cloud/callStatus', 'POST');
insert into applications (application_sid, name, service_provider_sid, call_hook_sid, call_status_hook_sid,
speech_synthesis_vendor, speech_synthesis_language, speech_synthesis_voice, speech_recognizer_vendor, speech_recognizer_language)
values ('7a489343-02ed-471e-8df0-fc5e1b98ce8f', 'Test application', '2708b1b3-2736-40ea-b502-c53d8396247f',

View File

@@ -1,9 +1,34 @@
const logger = require('../logger');
const JAMBONES_REDIS_SENTINELS = process.env.JAMBONES_REDIS_SENTINELS ? {
sentinels: process.env.JAMBONES_REDIS_SENTINELS.split(',').map((sentinel) => {
let host, port = 26379;
if (sentinel.includes(':')) {
const arr = sentinel.split(':');
host = arr[0];
port = parseInt(arr[1], 10);
} else {
host = sentinel;
}
return {host, port};
}),
name: process.env.JAMBONES_REDIS_SENTINEL_MASTER_NAME,
...(process.env.JAMBONES_REDIS_SENTINEL_PASSWORD && {
password: process.env.JAMBONES_REDIS_SENTINEL_PASSWORD
}),
...(process.env.JAMBONES_REDIS_SENTINEL_USERNAME && {
username: process.env.JAMBONES_REDIS_SENTINEL_USERNAME
}),
...(process.env.JAMBONES_REDIS_SENTINEL_SENTINAL_PASSWORD && {
sentinelPassword: process.env.JAMBONES_REDIS_SENTINEL_SENTINAL_PASSWORD
}),
} : null;
const {
retrieveCall,
deleteCall,
listCalls,
listSortedSets,
purgeCalls,
retrieveSet,
addKey,
@@ -11,7 +36,7 @@ const {
deleteKey,
incrKey,
client: redisClient,
} = require('@jambonz/realtimedb-helpers')({
} = require('@jambonz/realtimedb-helpers')(JAMBONES_REDIS_SENTINELS || {
host: process.env.JAMBONES_REDIS_HOST || 'localhost',
port: process.env.JAMBONES_REDIS_PORT || 6379
}, logger);
@@ -20,11 +45,13 @@ module.exports = {
retrieveCall,
deleteCall,
listCalls,
listSortedSets,
purgeCalls,
retrieveSet,
addKey,
retrieveKey,
deleteKey,
redisClient,
incrKey
incrKey,
JAMBONES_REDIS_SENTINELS
};

View File

@@ -1,11 +1,7 @@
const opts = Object.assign({
timestamp: () => {
return `, "time": "${new Date().toISOString()}"`;
}
}, {
const opts = {
level: process.env.JAMBONES_LOGLEVEL || 'info'
});
const logger = require('pino')(opts);
};
const pino = require('pino');
const logger = pino(opts, pino.destination(1, {sync: false}));
module.exports = logger;

View File

@@ -4,7 +4,7 @@ const {getMysqlConnection} = require('../db');
const {promisePool} = require('../db');
const { v4: uuid } = require('uuid');
const {encrypt} = require('../utils/encrypt-decrypt');
const {encrypt, decrypt} = require('../utils/encrypt-decrypt');
const retrieveSql = `SELECT * from accounts acc
LEFT JOIN webhooks AS rh
@@ -34,7 +34,7 @@ AND effective_end_date IS NULL
AND pending=0`;
const updatePaymentInfoSql = `UPDATE account_subscriptions
SET last4 = ?, exp_month = ?, exp_year = ?, card_type = ?
SET last4 = ?, stripe_payment_method_id=?, exp_month = ?, exp_year = ?, card_type = ?
WHERE account_sid = ?
AND effective_end_date IS NULL`;
@@ -55,6 +55,13 @@ WHERE account_sid = ?
AND effective_end_date IS NULL
AND pending = 0`;
const extractBucketCredential = (obj) => {
const {bucket_credential} = obj;
if (bucket_credential) {
obj.bucket_credential = JSON.parse(decrypt(bucket_credential));
}
};
function transmogrifyResults(results) {
return results.map((row) => {
const obj = row.acc;
@@ -75,6 +82,8 @@ function transmogrifyResults(results) {
else obj.queue_event_hook = null;
delete obj.queue_event_hook_sid;
extractBucketCredential(obj);
return obj;
});
}
@@ -197,10 +206,10 @@ class Account extends Model {
}
static async updatePaymentInfo(logger, account_sid, pm) {
const {card} = pm;
const {id, card} = pm;
const last4_encrypted = encrypt(card.last4);
await promisePool.execute(updatePaymentInfoSql,
[last4_encrypted, card.exp_month, card.exp_year, card.brand, account_sid]);
[last4_encrypted, id, card.exp_month, card.exp_year, card.brand, account_sid]);
}
static async provisionPendingSubscription(logger, account_sid, products, payment_method, subscription_id) {
@@ -238,7 +247,6 @@ class Account extends Model {
}));
return account_subscription_sid;
}
}
Account.table = 'accounts';
@@ -318,6 +326,18 @@ Account.fields = [
name: 'siprec_hook_sid',
type: 'string',
},
{
name: 'record_all_calls',
type: 'number'
},
{
name: 'record_format',
type: 'string'
},
{
name: 'bucket_credential',
type: 'string'
}
];
module.exports = Account;

View File

@@ -120,6 +120,10 @@ Application.fields = [
{
name: 'messaging_hook_sid',
type: 'string',
},
{
name: 'record_all_calls',
type: 'number',
}
];

58
lib/models/client.js Normal file
View File

@@ -0,0 +1,58 @@
const Model = require('./model');
const {promisePool} = require('../db');
class Client extends Model {
constructor() {
super();
}
static async retrieveAllByAccountSid(account_sid) {
const sql = `SELECT * FROM ${this.table} WHERE account_sid = ?`;
const [rows] = await promisePool.query(sql, account_sid);
return rows;
}
static async retrieveAllByServiceProviderSid(service_provider_sid) {
const sql = `SELECT c.client_sid, c.account_sid, c.is_active, c.username, c.hashed_password
FROM ${this.table} AS c LEFT JOIN accounts AS acc ON c.account_sid = acc.account_sid
LEFT JOIN service_providers AS sp ON sp.service_provider_sid = accs.service_provider_sid
WHERE sp.service_provider_sid = ?`;
const [rows] = await promisePool.query(sql, service_provider_sid);
return rows;
}
static async retrieveByAccountSidAndUserName(account_sid, username) {
const sql = `SELECT * FROM ${this.table} WHERE account_sid = ? AND username = ?`;
const [rows] = await promisePool.query(sql, [account_sid, username]);
return rows;
}
}
Client.table = 'clients';
Client.fields = [
{
name: 'client_sid',
type: 'string',
primaryKey: true
},
{
name: 'account_sid',
type: 'string',
required: true
},
{
name: 'is_active',
type: 'number'
},
{
name: 'username',
type: 'string',
required: true
},
{
name: 'password',
type: 'string'
}
];
module.exports = Client;

View File

@@ -0,0 +1,47 @@
const Model = require('./model');
const {promisePool} = require('../db');
class LcrCarrierSetEntry extends Model {
constructor() {
super();
}
static async retrieveAllByLcrRouteSid(sid) {
const sql = `SELECT * FROM ${this.table} WHERE lcr_route_sid = ? ORDER BY priority`;
const [rows] = await promisePool.query(sql, sid);
return rows;
}
static async deleteByLcrRouteSid(sid) {
const sql = `DELETE FROM ${this.table} WHERE lcr_route_sid = ?`;
const [rows] = await promisePool.query(sql, sid);
return rows.affectedRows;
}
}
LcrCarrierSetEntry.table = 'lcr_carrier_set_entry';
LcrCarrierSetEntry.fields = [
{
name: 'lcr_carrier_set_entry_sid',
type: 'string',
primaryKey: true
},
{
name: 'workload',
type: 'number'
},
{
name: 'lcr_route_sid',
type: 'string'
},
{
name: 'voip_carrier_sid',
type: 'string'
},
{
name: 'priority',
type: 'number'
}
];
module.exports = LcrCarrierSetEntry;

54
lib/models/lcr-route.js Normal file
View File

@@ -0,0 +1,54 @@
const Model = require('./model');
const {promisePool} = require('../db');
class LcrRoutes extends Model {
constructor() {
super();
}
static async retrieveAllByLcrSid(sid) {
const sql = `SELECT * FROM ${this.table} WHERE lcr_sid = ? ORDER BY priority`;
const [rows] = await promisePool.query(sql, sid);
return rows;
}
static async deleteByLcrSid(sid) {
const sql = `DELETE FROM ${this.table} WHERE lcr_sid = ?`;
const [rows] = await promisePool.query(sql, sid);
return rows.affectedRows;
}
static async countAllByLcrSid(sid) {
const sql = `SELECT COUNT(*) AS count FROM ${this.table} WHERE lcr_sid = ?`;
const [rows] = await promisePool.query(sql, sid);
return rows.length ? rows[0].count : 0;
}
}
LcrRoutes.table = 'lcr_routes';
LcrRoutes.fields = [
{
name: 'lcr_route_sid',
type: 'string',
primaryKey: true
},
{
name: 'lcr_sid',
type: 'string'
},
{
name: 'regex',
type: 'string'
},
{
name: 'description',
type: 'string'
},
{
name: 'priority',
type: 'number'
}
];
module.exports = LcrRoutes;

54
lib/models/lcr.js Normal file
View File

@@ -0,0 +1,54 @@
const Model = require('./model');
const {promisePool} = require('../db');
class Lcr extends Model {
constructor() {
super();
}
static async retrieveAllByAccountSid(account_sid) {
const sql = `SELECT * FROM ${this.table} WHERE account_sid = ?`;
const [rows] = await promisePool.query(sql, account_sid);
return rows;
}
static async retrieveAllByServiceProviderSid(sid) {
const sql = `SELECT * FROM ${this.table} WHERE service_provider_sid = ?`;
const [rows] = await promisePool.query(sql, sid);
return rows;
}
static async releaseDefaultEntry(sid) {
const sql = `UPDATE ${this.table} SET default_carrier_set_entry_sid = null WHERE lcr_sid = ?`;
const [rows] = await promisePool.query(sql, sid);
return rows;
}
}
Lcr.table = 'lcr';
Lcr.fields = [
{
name: 'lcr_sid',
type: 'string',
primaryKey: true
},
{
name: 'name',
type: 'string',
required: true
},
{
name: 'account_sid',
type: 'string'
},
{
name: 'service_provider_sid',
type: 'string'
},
{
name: 'default_carrier_set_entry_sid',
type: 'string'
}
];
module.exports = Lcr;

View File

@@ -1,6 +1,7 @@
const Model = require('./model');
const {promisePool} = require('../db');
const sql = 'SELECT * from phone_numbers WHERE account_sid = ?';
const sqlRetrieveAll = 'SELECT * from phone_numbers WHERE account_sid = ? ORDER BY number';
const sqlRetrieveOne = 'SELECT * from phone_numbers WHERE phone_number_sid = ? AND account_sid = ? ORDER BY number';
const sqlSP = `SELECT *
FROM phone_numbers
WHERE account_sid IN
@@ -8,7 +9,7 @@ WHERE account_sid IN
SELECT account_sid
FROM accounts
WHERE service_provider_sid = ?
)`;
) ORDER BY number`;
class PhoneNumber extends Model {
constructor() {
@@ -16,8 +17,8 @@ class PhoneNumber extends Model {
}
static async retrieveAll(account_sid) {
if (!account_sid) return super.retrieveAll();
const [rows] = await promisePool.query(sql, account_sid);
if (!account_sid) return await super.retrieveAll();
const [rows] = await promisePool.query(sqlRetrieveAll, account_sid);
return rows;
}
static async retrieveAllForSP(service_provider_sid) {
@@ -30,7 +31,7 @@ class PhoneNumber extends Model {
*/
static async retrieve(sid, account_sid) {
if (!account_sid) return super.retrieve(sid);
const [rows] = await promisePool.query(`${sql} AND phone_number_sid = ?`, [account_sid, sid]);
const [rows] = await promisePool.query(sqlRetrieveOne, [sid, account_sid]);
return rows;
}
}

View File

@@ -89,6 +89,10 @@ ServiceProvider.fields = [
{
name: 'ms_teams_fqdn',
type: 'string',
},
{
name: 'lcr_sid',
type: 'string'
}
];

View File

@@ -51,6 +51,10 @@ SipGateway.fields = [
name: 'is_active',
type: 'number'
},
{
name: 'pad_crypto',
type: 'number'
},
{
name: 'account_sid',
type: 'string'
@@ -58,6 +62,10 @@ SipGateway.fields = [
{
name: 'application_sid',
type: 'string'
},
{
name: 'protocol',
type: 'string'
}
];

View File

@@ -1,7 +1,7 @@
const Model = require('./model');
const {promisePool} = require('../db');
const retrieveSql = 'SELECT * from speech_credentials WHERE account_sid = ?';
const retrieveSqlForSP = 'SELECT * from speech_credentials WHERE service_provider_sid = ?';
const retrieveSqlForSP = 'SELECT * from speech_credentials WHERE service_provider_sid = ? and account_sid is null';
class SpeechCredential extends Model {
constructor() {
@@ -20,6 +20,17 @@ class SpeechCredential extends Model {
return rows;
}
static async isAvailableVendorAndLabel(service_provider_sid, account_sid, vendor, label) {
let sql;
if (account_sid) {
sql = 'SELECT * FROM speech_credentials WHERE account_sid = ? AND vendor = ? AND label = ?';
} else {
sql = 'SELECT * FROM speech_credentials WHERE service_provider_sid = ? AND vendor = ? AND label = ?';
}
const [rows] = await promisePool.query(sql, [account_sid ? account_sid : service_provider_sid, vendor, label]);
return rows;
}
static async disableStt(account_sid) {
await promisePool.execute('UPDATE speech_credentials SET use_for_stt = 0 WHERE account_sid = ?', [account_sid]);
}
@@ -86,6 +97,10 @@ SpeechCredential.fields = [
{
name: 'last_tested',
type: 'date'
},
{
name: 'label',
type: 'string'
}
];

View File

@@ -0,0 +1,38 @@
const Model = require('./model');
const { promisePool } = require('../db');
class SystemInformation extends Model {
constructor() {
super();
}
static async add(body) {
let [sysInfo] = await this.retrieveAll();
if (sysInfo) {
const sql = `UPDATE ${this.table} SET ?`;
await promisePool.query(sql, body);
} else {
const sql = `INSERT INTO ${this.table} SET ?`;
await promisePool.query(sql, body);
}
[sysInfo] = await this.retrieveAll();
return sysInfo;
}
}
SystemInformation.table = 'system_information';
SystemInformation.fields = [
{
name: 'domain_name',
type: 'string',
},
{
name: 'sip_domain_name',
type: 'string',
},
{
name: 'monitoring_domain_name',
type: 'string',
},
];
module.exports = SystemInformation;

View File

@@ -0,0 +1,41 @@
const { Writable } = require('stream');
const { BlobServiceClient } = require('@azure/storage-blob');
const { v4: uuidv4 } = require('uuid');
class AzureStorageUploadStream extends Writable {
constructor(logger, opts) {
super(opts);
const blobServiceClient = BlobServiceClient.fromConnectionString(opts.connection_string);
this.blockBlobClient = blobServiceClient.getContainerClient(opts.bucketName).getBlockBlobClient(opts.Key);
this.metadata = opts.metadata;
this.blocks = [];
}
async _write(chunk, encoding, callback) {
const blockID = uuidv4().replace(/-/g, '');
this.blocks.push(blockID);
try {
await this.blockBlobClient.stageBlock(blockID, chunk, chunk.length);
callback();
} catch (error) {
callback(error);
}
}
async _final(callback) {
try {
await this.blockBlobClient.commitBlockList(this.blocks);
// remove all null/undefined props
const filteredObj = Object.entries(this.metadata).reduce((acc, [key, val]) => {
if (val !== undefined && val !== null) acc[key] = val;
return acc;
}, {});
await this.blockBlobClient.setMetadata(filteredObj);
callback();
} catch (error) {
callback(error);
}
}
}
module.exports = AzureStorageUploadStream;

61
lib/record/encoder.js Normal file
View File

@@ -0,0 +1,61 @@
const { Transform } = require('stream');
const lamejs = require('@jambonz/lamejs');
class PCMToMP3Encoder extends Transform {
constructor(options, logger) {
super(options);
const channels = options.channels || 1;
const sampleRate = options.sampleRate || 8000;
const bitRate = options.bitRate || 128;
this.encoder = new lamejs.Mp3Encoder(channels, sampleRate, bitRate);
this.channels = channels;
this.logger = logger;
}
_transform(chunk, encoding, callback) {
try {
// Convert chunk buffer into Int16Array for lamejs
const samples = new Int16Array(chunk.buffer, chunk.byteOffset, chunk.length / 2);
// Split input samples into left and right channel arrays if stereo
let leftChannel, rightChannel;
if (this.channels === 2) {
leftChannel = new Int16Array(samples.length / 2);
rightChannel = new Int16Array(samples.length / 2);
for (let i = 0; i < samples.length; i += 2) {
leftChannel[i / 2] = samples[i];
rightChannel[i / 2] = samples[i + 1];
}
} else {
leftChannel = samples;
}
// Encode the input data
const mp3Data = this.encoder.encodeBuffer(leftChannel, rightChannel);
if (mp3Data.length > 0) {
this.push(Buffer.from(mp3Data));
}
callback();
} catch (err) {
this.logger.error(
{ err },
'Error while mp3 transform');
}
}
_flush(callback) {
// Finalize encoding and flush the internal buffers
const mp3Data = this.encoder.flush();
if (mp3Data.length > 0) {
this.push(Buffer.from(mp3Data));
}
callback();
}
}
module.exports = PCMToMP3Encoder;

View File

@@ -0,0 +1,41 @@
const { Storage } = require('@google-cloud/storage');
const { Writable } = require('stream');
class GoogleStorageUploadStream extends Writable {
constructor(logger, opts) {
super(opts);
this.logger = logger;
this.metadata = opts.metadata;
const storage = new Storage(opts.bucketCredential);
this.gcsFile = storage.bucket(opts.bucketName).file(opts.Key);
this.writeStream = this.gcsFile.createWriteStream();
this.writeStream.on('error', (err) => this.logger.error(err));
this.writeStream.on('finish', () => {
this.logger.info('google storage Upload completed.');
this._addMetadata();
});
}
_write(chunk, encoding, callback) {
this.writeStream.write(chunk, encoding, callback);
}
_final(callback) {
this.writeStream.end();
this.writeStream.once('finish', callback);
}
async _addMetadata() {
try {
await this.gcsFile.setMetadata({metadata: this.metadata});
this.logger.info('Google storage Upload and metadata setting completed.');
} catch (err) {
this.logger.error(err, 'Google storage An error occurred while setting metadata');
}
}
}
module.exports = GoogleStorageUploadStream;

6
lib/record/index.js Normal file
View File

@@ -0,0 +1,6 @@
async function record(logger, socket) {
return require('./upload')(logger, socket);
}
module.exports = record;

View File

@@ -0,0 +1,103 @@
const { Writable } = require('stream');
const {
S3Client,
CreateMultipartUploadCommand,
UploadPartCommand,
CompleteMultipartUploadCommand,
} = require('@aws-sdk/client-s3');
class S3MultipartUploadStream extends Writable {
constructor(logger, opts) {
super(opts);
this.logger = logger;
this.bucketName = opts.bucketName;
this.objectKey = opts.objectKey;
this.uploadId = null;
this.partNumber = 1;
this.multipartETags = [];
this.buffer = Buffer.alloc(0);
this.minPartSize = 5 * 1024 * 1024; // 5 MB
this.s3 = new S3Client(opts.bucketCredential);
this.metadata = opts.metadata;
}
async _initMultipartUpload() {
const command = new CreateMultipartUploadCommand({
Bucket: this.bucketName,
Key: this.objectKey,
Metadata: this.metadata
});
const response = await this.s3.send(command);
return response.UploadId;
}
async _uploadBuffer() {
const uploadPartCommand = new UploadPartCommand({
Bucket: this.bucketName,
Key: this.objectKey,
PartNumber: this.partNumber,
UploadId: this.uploadId,
Body: this.buffer,
});
const uploadPartResponse = await this.s3.send(uploadPartCommand);
this.multipartETags.push({
ETag: uploadPartResponse.ETag,
PartNumber: this.partNumber,
});
this.partNumber += 1;
}
async _write(chunk, encoding, callback) {
try {
if (!this.uploadId) {
this.uploadId = await this._initMultipartUpload();
}
this.buffer = Buffer.concat([this.buffer, chunk]);
if (this.buffer.length >= this.minPartSize) {
await this._uploadBuffer();
this.buffer = Buffer.alloc(0);
}
callback(null);
} catch (error) {
callback(error);
}
}
async _finalize(err) {
try {
if (this.buffer.length > 0) {
await this._uploadBuffer();
}
const completeMultipartUploadCommand = new CompleteMultipartUploadCommand({
Bucket: this.bucketName,
Key: this.objectKey,
MultipartUpload: {
Parts: this.multipartETags.sort((a, b) => a.PartNumber - b.PartNumber),
},
UploadId: this.uploadId,
});
await this.s3.send(completeMultipartUploadCommand);
this.logger.info('Finished upload to S3');
} catch (error) {
this.logger.error('Error completing multipart upload:', error);
throw error;
}
}
async _final(callback) {
try {
await this._finalize();
callback(null);
} catch (error) {
callback(error);
}
}
}
module.exports = S3MultipartUploadStream;

99
lib/record/upload.js Normal file
View File

@@ -0,0 +1,99 @@
const Account = require('../models/account');
const Websocket = require('ws');
const PCMToMP3Encoder = require('./encoder');
const wav = require('wav');
const { getUploader } = require('./utils');
async function upload(logger, socket) {
socket._recvInitialMetadata = false;
socket.on('message', async function(data, isBinary) {
try {
if (!isBinary && !socket._recvInitialMetadata) {
socket._recvInitialMetadata = true;
logger.debug(`initial metadata: ${data}`);
const obj = JSON.parse(data.toString());
logger.info({ obj }, 'received JSON message from jambonz');
const { sampleRate, accountSid, callSid, direction, from, to,
callId, applicationSid, originatingSipIp, originatingSipTrunkName } = obj;
const account = await Account.retrieve(accountSid);
if (account && account.length && account[0].bucket_credential) {
const obj = account[0].bucket_credential;
// add tags to metadata
const metadata = {
accountSid,
callSid,
direction,
from,
to,
callId,
applicationSid,
originatingSipIp,
originatingSipTrunkName,
sampleRate: `${sampleRate}`
};
if (obj.tags && obj.tags.length) {
obj.tags.forEach((tag) => {
metadata[tag.Key] = tag.Value;
});
}
// create S3 path
const day = new Date();
let key = `${day.getFullYear()}/${(day.getMonth() + 1).toString().padStart(2, '0')}`;
key += `/${day.getDate().toString().padStart(2, '0')}/${callSid}.${account[0].record_format}`;
// Uploader
const uploadStream = getUploader(key, metadata, obj, logger);
if (!uploadStream) {
logger.info('There is no available record uploader, close the socket.');
socket.close();
}
/**encoder */
let encoder;
if (account[0].record_format === 'wav') {
encoder = new wav.Writer({ channels: 2, sampleRate, bitDepth: 16 });
} else {
// default is mp3
encoder = new PCMToMP3Encoder({
channels: 2,
sampleRate: sampleRate,
bitrate: 128
}, logger);
}
const handleError = (err, streamType) => {
logger.error(
{ err },
`Error while streaming for vendor: ${obj.vendor}, pipe: ${streamType}: ${err.message}`
);
};
/* start streaming data */
const duplex = Websocket.createWebSocketStream(socket);
duplex
.on('error', (err) => handleError(err, 'duplex'))
.pipe(encoder)
.on('error', (err) => handleError(err, 'encoder'))
.pipe(uploadStream)
.on('error', (err) => handleError(err, 'uploadStream'));
} else {
logger.info(`account ${accountSid} does not have any bucket credential, close the socket`);
socket.close();
}
}
} catch (err) {
logger.error({ err, data }, 'error parsing message during connection');
}
});
socket.on('error', function(err) {
logger.error({ err }, 'record upload: error');
});
socket.on('close', (data) => {
logger.info({ data }, 'record upload: close');
});
socket.on('end', function(err) {
logger.error({ err }, 'record upload: socket closed from jambonz');
});
}
module.exports = upload;

58
lib/record/utils.js Normal file
View File

@@ -0,0 +1,58 @@
const AzureStorageUploadStream = require('./azure-storage');
const GoogleStorageUploadStream = require('./google-storage');
const S3MultipartUploadStream = require('./s3-multipart-upload-stream');
const getUploader = (key, metadata, bucket_credential, logger) => {
const uploaderOpts = {
bucketName: bucket_credential.name,
objectKey: key,
metadata
};
try {
switch (bucket_credential.vendor) {
case 'aws_s3':
uploaderOpts.bucketCredential = {
credentials: {
accessKeyId: bucket_credential.access_key_id,
secretAccessKey: bucket_credential.secret_access_key,
},
region: bucket_credential.region || 'us-east-1'
};
return new S3MultipartUploadStream(logger, uploaderOpts);
case 's3_compatible':
uploaderOpts.bucketCredential = {
endpoint: bucket_credential.endpoint,
credentials: {
accessKeyId: bucket_credential.access_key_id,
secretAccessKey: bucket_credential.secret_access_key,
},
region: 'us-east-1',
forcePathStyle: true
};
return new S3MultipartUploadStream(logger, uploaderOpts);
case 'google':
const serviceKey = JSON.parse(bucket_credential.service_key);
uploaderOpts.bucketCredential = {
projectId: serviceKey.project_id,
credentials: {
client_email: serviceKey.client_email,
private_key: serviceKey.private_key
}
};
return new GoogleStorageUploadStream(logger, uploaderOpts);
case 'azure':
uploaderOpts.connection_string = bucket_credential.connection_string;
return new AzureStorageUploadStream(logger, uploaderOpts);
default:
logger.error(`unknown bucket vendor: ${bucket_credential.vendor}`);
break;
}
} catch (err) {
logger.error(`Error creating uploader, vendor: ${bucket_credential.vendor}, reason: ${err.message}`);
}
return null;
};
module.exports = {
getUploader
};

View File

@@ -1,4 +1,5 @@
const router = require('express').Router();
const assert = require('assert');
const request = require('request');
const {DbErrorBadRequest, DbErrorForbidden, DbErrorUnprocessableRequest} = require('../../utils/errors');
const Account = require('../../models/account');
@@ -22,6 +23,8 @@ const {
} = require('./utils');
const short = require('short-uuid');
const VoipCarrier = require('../../models/voip-carrier');
const { encrypt } = require('../../utils/encrypt-decrypt');
const { testS3Storage, testGoogleStorage, testAzureStorage } = require('../../utils/storage-utils');
const translator = short();
let idx = 0;
@@ -38,20 +41,14 @@ const getFsUrl = async(logger, retrieveSet, setName) => {
logger.info('No available feature servers to handle createCall API request');
return ;
}
const ip = stripPort(fs[idx++ % fs.length]);
logger.info({fs}, `feature servers available for createCall API request, selecting ${ip}`);
return `http://${ip}:3000/v1/createCall`;
const f = fs[idx++ % fs.length];
logger.info({fs}, `feature servers available for createCall API request, selecting ${f}`);
return `${f}/v1/createCall`;
} catch (err) {
logger.error({err}, 'getFsUrl: error retreving feature servers from redis');
}
};
const stripPort = (hostport) => {
const arr = /^(.*):(.*)$/.exec(hostport);
if (arr) return arr[1];
return hostport;
};
const validateRequest = async(req, account_sid) => {
try {
if (req.user.hasScope('admin')) {
@@ -89,6 +86,7 @@ router.use('/:sid/Charges', hasAccountPermissions, require('./charges'));
router.use('/:sid/SipRealms', hasAccountPermissions, require('./sip-realm'));
router.use('/:sid/PredefinedCarriers', hasAccountPermissions, require('./add-from-predefined-carrier'));
router.use('/:sid/Limits', hasAccountPermissions, require('./limits'));
router.use('/:sid/TtsCache', hasAccountPermissions, require('./tts-cache'));
router.get('/:sid/Applications', async(req, res) => {
const logger = req.app.locals.logger;
try {
@@ -117,7 +115,7 @@ router.put('/:sid/VoipCarriers/:voip_carrier_sid', async(req, res) => {
try {
const sid = parseVoipCarrierSid(req);
const account_sid = parseAccountSid(req);
await await validateRequest(req, account_sid);
await validateRequest(req, account_sid);
const rowsAffected = await VoipCarrier.update(sid, req.body);
if (rowsAffected === 0) {
@@ -135,7 +133,7 @@ router.post('/:sid/VoipCarriers', async(req, res) => {
const payload = req.body;
try {
const account_sid = parseAccountSid(req);
await await validateRequest(req, account_sid);
await validateRequest(req, account_sid);
logger.debug({payload}, 'POST /:sid/VoipCarriers');
const uuid = await VoipCarrier.make({
@@ -178,6 +176,7 @@ function validateUpdateCall(opts) {
'child_call_hook',
'call_status',
'listen_status',
'transcribe_status',
'conf_hold_status',
'conf_mute_status',
'mute_status',
@@ -218,8 +217,8 @@ function validateUpdateCall(opts) {
throw new DbErrorBadRequest('invalid conf_mute_status');
}
if (opts.sip_request &&
(!opts.sip_request.method && !opts.sip_request.content_type || !opts.sip_request.content_type)) {
throw new DbErrorBadRequest('sip_request requires content_type and content properties');
(!opts.sip_request.method || !opts.sip_request.content_type || !opts.sip_request.content)) {
throw new DbErrorBadRequest('sip_request requires method, content_type and content properties');
}
if (opts.record && !opts.record.action) {
throw new DbErrorBadRequest('record requires action property');
@@ -263,6 +262,7 @@ async function validateCreateCall(logger, sid, req) {
const application = await lookupAppBySid(obj.application_sid);
Object.assign(obj, {
call_hook: application.call_hook,
app_json: application.app_json,
call_status_hook: application.call_status_hook,
speech_synthesis_vendor: application.speech_synthesis_vendor,
speech_synthesis_language: application.speech_synthesis_language,
@@ -449,7 +449,6 @@ router.get('/:sid', async(req, res) => {
try {
const account_sid = parseAccountSid(req);
await validateRequest(req, account_sid);
const service_provider_sid = req.user.hasServiceProviderAuth ? req.user.service_provider_sid : null;
const results = await Account.retrieve(account_sid, service_provider_sid);
if (results.length === 0) return res.status(404).end();
@@ -535,6 +534,58 @@ router.delete('/:sid/SubspaceTeleport', async(req, res) => {
}
});
function encryptBucketCredential(obj) {
if (!obj.bucket_credential) return;
const {
vendor,
region,
name,
access_key_id,
secret_access_key,
tags,
service_key,
connection_string,
endpoint
} = obj.bucket_credential;
switch (vendor) {
case 'aws_s3':
assert(access_key_id, 'invalid aws S3 bucket credential: access_key_id is required');
assert(secret_access_key, 'invalid aws S3 bucket credential: secret_access_key is required');
assert(name, 'invalid aws bucket name: name is required');
assert(region, 'invalid aws bucket region: region is required');
const awsData = JSON.stringify({vendor, region, name, access_key_id,
secret_access_key, tags});
obj.bucket_credential = encrypt(awsData);
break;
case 's3_compatible':
assert(access_key_id, 'invalid aws S3 bucket credential: access_key_id is required');
assert(secret_access_key, 'invalid aws S3 bucket credential: secret_access_key is required');
assert(name, 'invalid aws bucket name: name is required');
assert(endpoint, 'invalid endpoint uri: endpoint is required');
const s3Data = JSON.stringify({vendor, endpoint, name, access_key_id,
secret_access_key, tags});
obj.bucket_credential = encrypt(s3Data);
break;
case 'google':
assert(service_key, 'invalid google cloud storage credential: service_key is required');
const googleData = JSON.stringify({vendor, name, service_key, tags});
obj.bucket_credential = encrypt(googleData);
break;
case 'azure':
assert(name, 'invalid azure container name: name is required');
assert(connection_string, 'invalid azure cloud storage credential: connection_string is required');
const azureData = JSON.stringify({vendor, name, connection_string, tags});
obj.bucket_credential = encrypt(azureData);
break;
case 'none':
obj.bucket_credential = null;
break;
default:
throw new DbErrorBadRequest(`unknown storage vendor: ${vendor}`);
}
}
/**
* update
*/
@@ -542,7 +593,7 @@ router.put('/:sid', async(req, res) => {
const logger = req.app.locals.logger;
try {
const sid = parseAccountSid(req);
await await validateRequest(req, sid);
await validateRequest(req, sid);
// create webhooks if provided
const obj = Object.assign({}, req.body);
@@ -581,6 +632,8 @@ router.put('/:sid', async(req, res) => {
delete obj.registration_hook;
delete obj.queue_event_hook;
encryptBucketCredential(obj);
const rowsAffected = await Account.update(sid, obj);
if (rowsAffected === 0) {
return res.status(404).end();
@@ -609,7 +662,7 @@ router.delete('/:sid', async(req, res) => {
try {
const sid = parseAccountSid(req);
await await validateRequest(req, sid);
await validateRequest(req, sid);
await validateDelete(req, sid);
const [account] = await promisePool.query('SELECT * FROM accounts WHERE account_sid = ?', sid);
@@ -673,6 +726,44 @@ account_subscriptions WHERE account_sid = ?)
}
});
/* Test Bucket credential Keys */
router.post('/:sid/BucketCredentialTest', async(req, res) => {
const logger = req.app.locals.logger;
try {
const account_sid = parseAccountSid(req);
await validateRequest(req, account_sid);
const {vendor, name, region, access_key_id, secret_access_key, service_key, connection_string, endpoint} = req.body;
const ret = {
status: 'not tested'
};
switch (vendor) {
case 'aws_s3':
await testS3Storage(logger, {vendor, name, region, access_key_id, secret_access_key});
ret.status = 'ok';
break;
case 's3_compatible':
await testS3Storage(logger, {vendor, name, endpoint, access_key_id, secret_access_key});
ret.status = 'ok';
break;
case 'google':
await testGoogleStorage(logger, {vendor, name, service_key});
ret.status = 'ok';
break;
case 'azure':
await testAzureStorage(logger, {vendor, name, connection_string});
ret.status = 'ok';
break;
default:
throw new DbErrorBadRequest(`Does not support test for ${vendor}`);
}
return res.status(200).json(ret);
}
catch (err) {
return res.status(200).json({status: 'failed', reason: err.message});
}
});
/**
* retrieve account level api keys
*/
@@ -681,7 +772,7 @@ router.get('/:sid/ApiKeys', async(req, res) => {
try {
const sid = parseAccountSid(req);
await await validateRequest(req, sid);
await validateRequest(req, sid);
const results = await ApiKey.retrieveAll(sid);
res.status(200).json(results);
@@ -696,7 +787,7 @@ router.get('/:sid/ApiKeys', async(req, res) => {
*/
router.post('/:sid/Calls', async(req, res) => {
const {retrieveSet, logger} = req.app.locals;
const setName = `${(process.env.JAMBONES_CLUSTER_ID || 'default')}:active-fs`;
const setName = `${(process.env.JAMBONES_CLUSTER_ID || 'default')}:fs-service-url`;
const serviceUrl = await getFsUrl(logger, retrieveSet, setName);
if (!serviceUrl) {
@@ -705,7 +796,7 @@ router.post('/:sid/Calls', async(req, res) => {
try {
const sid = parseAccountSid(req);
await await validateRequest(req, sid);
await validateRequest(req, sid);
await validateCreateCall(logger, sid, req);
updateLastUsed(logger, sid, req).catch((err) => {});
@@ -737,11 +828,17 @@ router.post('/:sid/Calls', async(req, res) => {
*/
router.get('/:sid/Calls', async(req, res) => {
const {logger, listCalls} = req.app.locals;
const {direction, from, to, callStatus} = req.query || {};
try {
const accountSid = parseAccountSid(req);
await await validateRequest(req, accountSid);
const calls = await listCalls(accountSid);
await validateRequest(req, accountSid);
const calls = await listCalls({
accountSid,
direction,
from,
to,
callStatus
});
logger.debug(`retrieved ${calls.length} calls for account sid ${accountSid}`);
res.status(200).json(coerceNumbers(snakeCase(calls)));
updateLastUsed(logger, accountSid, req).catch((err) => {});
@@ -758,7 +855,7 @@ router.get('/:sid/Calls/:callSid', async(req, res) => {
try {
const accountSid = parseAccountSid(req);
await await validateRequest(req, accountSid);
await validateRequest(req, accountSid);
const callSid = parseCallSid(req);
const callInfo = await retrieveCall(accountSid, callSid);
if (callInfo) {
@@ -783,7 +880,7 @@ router.delete('/:sid/Calls/:callSid', async(req, res) => {
try {
const accountSid = parseAccountSid(req);
await await validateRequest(req, accountSid);
await validateRequest(req, accountSid);
const callSid = parseCallSid(req);
const result = await deleteCall(accountSid, callSid);
if (result) {
@@ -808,7 +905,7 @@ const updateCall = async(req, res) => {
try {
const accountSid = parseAccountSid(req);
await await validateRequest(req, accountSid);
await validateRequest(req, accountSid);
const callSid = parseCallSid(req);
validateUpdateCall(req.body);
const call = await retrieveCall(accountSid, callSid);
@@ -849,9 +946,9 @@ router.post('/:sid/Messages', async(req, res) => {
try {
const account_sid = parseAccountSid(req);
await await validateRequest(req, account_sid);
await validateRequest(req, account_sid);
const setName = `${(process.env.JAMBONES_CLUSTER_ID || 'default')}:active-fs`;
const setName = `${(process.env.JAMBONES_CLUSTER_ID || 'default')}:fs-service-url`;
const serviceUrl = await getFsUrl(logger, retrieveSet, setName);
if (!serviceUrl) res.json({msg: 'no available feature servers at this time'}).status(480);
await validateCreateMessage(logger, account_sid, req);
@@ -884,4 +981,22 @@ router.post('/:sid/Messages', async(req, res) => {
}
});
/**
* retrieve info for a group of queues under an account
*/
router.get('/:sid/Queues', async(req, res) => {
const {logger, listSortedSets} = req.app.locals;
const { search } = req.query || {};
try {
const accountSid = parseAccountSid(req);
await validateRequest(req, accountSid);
const queues = search ? await listSortedSets(accountSid, search) : await listSortedSets(accountSid);
logger.debug(`retrieved ${queues.length} queues for account sid ${accountSid}`);
res.status(200).json(queues);
updateLastUsed(logger, accountSid, req).catch((err) => {});
} catch (err) {
sysError(logger, res, err);
}
});
module.exports = router;

88
lib/routes/api/clients.js Normal file
View File

@@ -0,0 +1,88 @@
const router = require('express').Router();
const decorate = require('./decorate');
const sysError = require('../error');
const Client = require('../../models/client');
const Account = require('../../models/account');
const { DbErrorBadRequest, DbErrorForbidden } = require('../../utils/errors');
const { encrypt, decrypt, obscureKey } = require('../../utils/encrypt-decrypt');
const commonCheck = async(req) => {
if (req.user.hasAccountAuth) {
req.body.account_sid = req.user.account_sid;
} else if (req.user.hasServiceProviderAuth && req.body.account_sid) {
const accounts = await Account.retrieve(req.body.account_sid, req.user.service_provider_sid);
if (accounts.length === 0) {
throw new DbErrorForbidden('insufficient permissions');
}
}
if (req.body.password) {
req.body.password = encrypt(req.body.password);
}
};
const validateAdd = async(req) => {
await commonCheck(req);
const clients = await Client.retrieveByAccountSidAndUserName(req.body.account_sid, req.body.username);
if (clients.length) {
throw new DbErrorBadRequest('the client\'s username already exists');
}
};
const validateUpdate = async(req, sid) => {
await commonCheck(req);
const clients = await Client.retrieveByAccountSidAndUserName(req.body.account_sid, req.body.username);
if (clients.length && clients[0].client_sid !== sid) {
throw new DbErrorBadRequest('the client\'s username already exists');
}
};
const preconditions = {
add: validateAdd,
update: validateUpdate,
};
decorate(router, Client, ['add', 'update', 'delete'], preconditions);
router.get('/', async(req, res) => {
const logger = req.app.locals.logger;
try {
const results = req.user.hasAdminAuth ?
await Client.retrieveAll() : req.user.hasAccountAuth ?
await Client.retrieveAllByAccountSid(req.user.hasAccountAuth ? req.user.account_sid : null) :
await Client.retrieveAllByServiceProviderSid(req.user.service_provider_sid);
const ret = results.map((c) => {
c.password = obscureKey(decrypt(c.password), 1);
return c;
});
res.status(200).json(ret);
} catch (err) {
sysError(logger, res, err);
}
});
router.get('/:sid', async(req, res) => {
const logger = req.app.locals.logger;
try {
const results = await Client.retrieve(req.params.sid);
if (results.length === 0) return res.sendStatus(404);
const client = results[0];
client.password = obscureKey(decrypt(client.password), 1);
if (req.user.hasAccountAuth && client.account_sid !== req.user.account_sid) {
return res.sendStatus(404);
} else if (req.user.hasServiceProviderAuth) {
const accounts = await Account.retrieve(client.account_sid, req.user.service_provider_sid);
if (!accounts.length) {
return res.sendStatus(404);
}
}
return res.status(200).json(client);
} catch (err) {
sysError(logger, res, err);
}
});
module.exports = router;

View File

@@ -6,6 +6,7 @@ const {validateEmail, emailSimpleText} = require('../../utils/email-utils');
const {promisePool} = require('../../db');
const {cacheClient} = require('../../helpers');
const sysError = require('../error');
const assert = require('assert');
const sql = `SELECT * from users user
LEFT JOIN accounts AS acc
ON acc.account_sid = user.account_sid
@@ -26,7 +27,8 @@ function createOauthEmailText(provider) {
}
function createResetEmailText(link) {
const baseUrl = 'http://localhost:3001';
assert(process.env.JAMBONZ_BASE_URL, 'process.env.JAMBONZ_BASE_URL is missing');
const baseUrl = process.env.JAMBONZ_BASE_URL;
return `Hi there!
@@ -46,7 +48,7 @@ function createResetEmailText(link) {
router.post('/', async(req, res) => {
const {logger, addKey} = req.app.locals;
const {email} = req.body;
const {user_sid} = req.user;
let obj;
try {
if (!email || !validateEmail(email)) {
@@ -55,11 +57,16 @@ router.post('/', async(req, res) => {
const [r] = await promisePool.query({sql, nestTables: true}, email);
if (0 === r.length) {
return res.status(400).json({error: 'email does not exist'});
logger.info('user not found');
return res.status(400).json({error: 'failed to reset your password'});
}
obj = r[0];
if (!obj.acc.is_active) {
return res.status(400).json({error: 'you may not reset the password of an inactive account'});
if (!obj.user.is_active) {
logger.info(obj.user.name, 'user is inactive');
return res.status(400).json({error: 'failed to reset your password'});
} else if (obj.acc.account_sid !== null && !obj.acc.is_active) {
logger.info(obj.acc.account_sid, 'account is inactive');
return res.status(400).json({error: 'failed to reset your password'});
}
res.sendStatus(204);
} catch (err) {
@@ -81,7 +88,7 @@ router.post('/', async(req, res) => {
emailSimpleText(logger, email, 'Reset password request', createResetEmailText(link));
}
const redisKey = cacheClient.generateRedisKey('jwt', user_sid, 'v2');
const redisKey = cacheClient.generateRedisKey('jwt', obj.user.user_sid, 'v2');
await cacheClient.delete(redisKey);
});

View File

@@ -16,6 +16,8 @@ const isAdminScope = (req, res, next) => {
// };
api.use('/BetaInviteCodes', isAdminScope, require('./beta-invite-codes'));
api.use('/SystemInformation', isAdminScope, require('./system-information'));
api.use('/TtsCache', isAdminScope, require('./tts-cache'));
api.use('/ServiceProviders', require('./service-providers'));
api.use('/VoipCarriers', require('./voip-carriers'));
api.use('/Webhooks', require('./webhooks'));
@@ -45,6 +47,11 @@ api.use('/Invoices', require('./invoices'));
api.use('/InviteCodes', require('./invite-codes'));
api.use('/PredefinedCarriers', require('./predefined-carriers'));
api.use('/PasswordSettings', require('./password-settings'));
// Least Cost Routing
api.use('/Lcrs', require('./lcrs'));
api.use('/LcrRoutes', require('./lcr-routes'));
api.use('/LcrCarrierSetEntries', require('./lcr-carrier-set-entries'));
api.use('/Clients', require('./clients'));
// messaging
api.use('/Smpps', require('./smpps')); // our smpp server info

View File

@@ -0,0 +1,65 @@
const router = require('express').Router();
const LcrCarrierSetEntry = require('../../models/lcr-carrier-set-entry');
const LcrRoute = require('../../models/lcr-route');
const decorate = require('./decorate');
const {DbErrorBadRequest} = require('../../utils/errors');
const sysError = require('../error');
const validateAdd = async(req) => {
const {lookupCarrierBySid} = req.app.locals;
if (!req.body.lcr_route_sid) {
throw new DbErrorBadRequest('missing lcr_route_sid');
}
// check lcr_route_sid is exist
const lcrRoute = await LcrRoute.retrieve(req.body.lcr_route_sid);
if (lcrRoute.length === 0) {
throw new DbErrorBadRequest('unknown lcr_route_sid');
}
// check voip_carrier_sid is exist
if (!req.body.voip_carrier_sid) {
throw new DbErrorBadRequest('missing voip_carrier_sid');
}
const carrier = await lookupCarrierBySid(req.body.voip_carrier_sid);
if (!carrier) {
throw new DbErrorBadRequest('unknown voip_carrier_sid');
}
};
const validateUpdate = async(req) => {
const {lookupCarrierBySid} = req.app.locals;
if (req.body.lcr_route_sid) {
const lcrRoute = await LcrRoute.retrieve(req.body.lcr_route_sid);
if (lcrRoute.length === 0) {
throw new DbErrorBadRequest('unknown lcr_route_sid');
}
}
// check voip_carrier_sid is exist
if (req.body.voip_carrier_sid) {
const carrier = await lookupCarrierBySid(req.body.voip_carrier_sid);
if (!carrier) {
throw new DbErrorBadRequest('unknown voip_carrier_sid');
}
}
};
const preconditions = {
add: validateAdd,
update: validateUpdate,
};
decorate(router, LcrCarrierSetEntry, ['add', 'retrieve', 'update', 'delete'], preconditions);
router.get('/', async(req, res) => {
const logger = req.app.locals.logger;
const lcr_route_sid = req.query.lcr_route_sid;
try {
const results = await LcrCarrierSetEntry.retrieveAllByLcrRouteSid(lcr_route_sid);
res.status(200).json(results);
} catch (err) {
sysError(logger, res, err);
}
});
module.exports = router;

View File

@@ -0,0 +1,96 @@
const router = require('express').Router();
const LcrRoute = require('../../models/lcr-route');
const Lcr = require('../../models/lcr');
const LcrCarrierSetEntry = require('../../models/lcr-carrier-set-entry');
const decorate = require('./decorate');
const {DbErrorBadRequest} = require('../../utils/errors');
const sysError = require('../error');
const validateAdd = async(req) => {
// check if lcr sid is available
if (!req.body.lcr_sid) {
throw new DbErrorBadRequest('missing parameter lcr_sid');
}
const lcr = await Lcr.retrieve(req.body.lcr_sid);
if (lcr.length === 0) {
throw new DbErrorBadRequest('unknown lcr_sid');
}
};
const validateUpdate = async(req) => {
if (req.body.lcr_sid) {
const lcr = await Lcr.retrieve(req.body.lcr_sid);
if (lcr.length === 0) {
throw new DbErrorBadRequest('unknown lcr_sid');
}
}
};
const validateDelete = async(req, sid) => {
// delete all lcr carrier set entries
await LcrCarrierSetEntry.deleteByLcrRouteSid(sid);
};
const checkUserScope = async(req, lcr_sid) => {
if (!lcr_sid) {
throw new DbErrorBadRequest('missing lcr_sid');
}
if (req.user.hasAdminAuth) return;
const lcrList = await Lcr.retrieve(lcr_sid);
if (lcrList.length === 0) throw new DbErrorBadRequest('unknown lcr_sid');
const lcr = lcrList[0];
if (req.user.hasAccountAuth) {
if (!lcr.account_sid || lcr.account_sid !== req.user.account_sid) {
throw new DbErrorBadRequest('unknown lcr_sid');
}
}
if (req.user.hasServiceProviderAuth) {
if (!lcr.service_provider_sid || lcr.service_provider_sid !== req.user.service_provider_sid) {
throw new DbErrorBadRequest('unknown lcr_sid');
}
}
};
const preconditions = {
add: validateAdd,
update: validateUpdate,
delete: validateDelete,
};
decorate(router, LcrRoute, ['add', 'update', 'delete'], preconditions);
router.get('/', async(req, res) => {
const logger = req.app.locals.logger;
const lcr_sid = req.query.lcr_sid;
try {
await checkUserScope(req, lcr_sid);
const results = await LcrRoute.retrieveAllByLcrSid(lcr_sid);
for (const r of results) {
r.lcr_carrier_set_entries = await LcrCarrierSetEntry.retrieveAllByLcrRouteSid(r.lcr_route_sid);
}
res.status(200).json(results);
} catch (err) {
sysError(logger, res, err);
}
});
router.get('/:sid', async(req, res) => {
const logger = req.app.locals.logger;
const lcr_route_sid = req.params.sid;
try {
const results = await LcrRoute.retrieve(lcr_route_sid);
if (results.length === 0) return res.sendStatus(404);
const route = results[0];
route.lcr_carrier_set_entries = await LcrCarrierSetEntry.retrieveAllByLcrRouteSid(route.lcr_route_sid);
res.status(200).json(route);
} catch (err) {
sysError(logger, res, err);
}
});
module.exports = router;

138
lib/routes/api/lcrs.js Normal file
View File

@@ -0,0 +1,138 @@
const router = require('express').Router();
const Lcr = require('../../models/lcr');
const LcrCarrierSetEntry = require('../../models/lcr-carrier-set-entry');
const LcrRoutes = require('../../models/lcr-route');
const decorate = require('./decorate');
const {DbErrorBadRequest} = require('../../utils/errors');
const sysError = require('../error');
const ServiceProvider = require('../../models/service-provider');
const validateAssociatedTarget = async(req, sid) => {
const {lookupAccountBySid} = req.app.locals;
if (req.body.account_sid) {
// Add only for account
req.body.service_provider_sid = null;
const account = await lookupAccountBySid(req.body.account_sid);
if (!account) throw new DbErrorBadRequest('unknown account_sid');
const lcr = await Lcr.retrieveAllByAccountSid(req.body.account_sid);
if (lcr.length > 0 && (!sid || sid !== lcr[0].lcr_sid)) {
throw new DbErrorBadRequest(`Account: ${account.name} already has an active call routing table.`);
}
} else if (req.body.service_provider_sid) {
const serviceProviders = await ServiceProvider.retrieve(req.body.service_provider_sid);
if (serviceProviders.length === 0) throw new DbErrorBadRequest('unknown service_provider_sid');
const serviceProvider = serviceProviders[0];
const lcr = await Lcr.retrieveAllByServiceProviderSid(req.body.service_provider_sid);
if (lcr.length > 0 && (!sid || sid !== lcr[0].lcr_sid)) {
throw new DbErrorBadRequest(`Service Provider: ${serviceProvider.name} already
has an active call routing table.`);
}
}
};
const validateAdd = async(req) => {
if (req.user.hasAccountAuth) {
// Account just create LCR for himself
req.body.account_sid = req.user.account_sid;
} else if (req.user.hasServiceProviderAuth) {
// SP just can create LCR for himself
req.body.service_provider_sid = req.user.service_provider_sid;
req.body.account_sid = null;
}
await validateAssociatedTarget(req);
// check if lcr_carrier_set_entry is available
if (req.body.lcr_carrier_set_entry) {
const e = await LcrCarrierSetEntry.retrieve(req.body.lcr_carrier_set_entry);
if (e.length === 0) throw new DbErrorBadRequest('unknown lcr_carrier_set_entry');
}
};
const validateUserPermissionForExistingEntity = async(req, sid) => {
const r = await Lcr.retrieve(sid);
if (r.length === 0) {
throw new DbErrorBadRequest('unknown lcr_sid');
}
const lcr = r[0];
if (req.user.hasAccountAuth) {
if (lcr.account_sid != req.user.account_sid) {
throw new DbErrorBadRequest('unknown lcr_sid');
}
} else if (req.user.hasServiceProviderAuth) {
if (lcr.service_provider_sid != req.user.service_provider_sid) {
throw new DbErrorBadRequest('unknown lcr_sid');
}
}
};
const validateUpdate = async(req, sid) => {
await validateUserPermissionForExistingEntity(req, sid);
await validateAssociatedTarget(req, sid);
};
const validateDelete = async(req, sid) => {
if (req.user.hasAccountAuth) {
/* can only delete Lcr for the user's account */
const r = await Lcr.retrieve(sid);
const lcr = r.length > 0 ? r[0] : null;
if (!lcr || (req.user.account_sid && lcr.account_sid != req.user.account_sid)) {
throw new DbErrorBadRequest('unknown lcr_sid');
}
}
await Lcr.releaseDefaultEntry(sid);
// fetch lcr route
const lcr_routes = await LcrRoutes.retrieveAllByLcrSid(sid);
// delete all lcr carrier set entries
for (const e of lcr_routes) {
await LcrCarrierSetEntry.deleteByLcrRouteSid(e.lcr_route_sid);
}
// delete all lcr routes
await LcrRoutes.deleteByLcrSid(sid);
};
const preconditions = {
add: validateAdd,
update: validateUpdate,
delete: validateDelete
};
decorate(router, Lcr, ['add', 'update', 'delete'], preconditions);
router.get('/', async(req, res) => {
const logger = req.app.locals.logger;
try {
const results = req.user.hasAdminAuth ?
await Lcr.retrieveAll() : req.user.hasAccountAuth ?
await Lcr.retrieveAllByAccountSid(req.user.hasAccountAuth ? req.user.account_sid : null) :
await Lcr.retrieveAllByServiceProviderSid(req.user.service_provider_sid);
for (const lcr of results) {
lcr.number_routes = await LcrRoutes.countAllByLcrSid(lcr.lcr_sid);
}
res.status(200).json(results);
} catch (err) {
sysError(logger, res, err);
}
});
router.get('/:sid', async(req, res) => {
const logger = req.app.locals.logger;
try {
const results = await Lcr.retrieve(req.params.sid);
if (results.length === 0) return res.sendStatus(404);
const lcr = results[0];
if (req.user.hasAccountAuth && lcr.account_sid !== req.user.account_sid) {
return res.sendStatus(404);
} else if (req.user.hasServiceProviderAuth && lcr.service_provider_sid !== req.user.service_provider_sid) {
return res.sendStatus(404);
}
lcr.number_routes = await LcrRoutes.countAllByLcrSid(lcr.lcr_sid);
return res.status(200).json(lcr);
} catch (err) {
sysError(logger, res, err);
}
});
module.exports = router;

View File

@@ -2,6 +2,7 @@ const router = require('express').Router();
const {DbErrorBadRequest, DbErrorForbidden} = require('../../utils/errors');
const PhoneNumber = require('../../models/phone-number');
const VoipCarrier = require('../../models/voip-carrier');
const Account = require('../../models/account');
const decorate = require('./decorate');
const {promisePool} = require('../../db');
const {e164} = require('../../utils/phone-number-utils');
@@ -67,7 +68,16 @@ async function validateUpdate(req, sid) {
}
}
if (req.user.hasServiceProviderAuth) {
if (phoneNumber && phoneNumber.length && phoneNumber[0].service_provider_sid !== req.user.service_provider_sid) {
let service_provider_sid;
if (!phoneNumber[0].service_provider_sid) {
const [r] = await Account.retrieve(phoneNumber[0].account_sid);
service_provider_sid = r.service_provider_sid;
} else {
service_provider_sid = phoneNumber[0].service_provider_sid;
}
if (phoneNumber && phoneNumber.length && service_provider_sid !== req.user.service_provider_sid) {
throw new DbErrorForbidden('insufficient privileges');
}
}
@@ -84,9 +94,9 @@ decorate(router, PhoneNumber, ['add', 'update', 'delete'], preconditions);
router.get('/', async(req, res) => {
const logger = req.app.locals.logger;
try {
const results = req.user.hasAdminAuth ?
await PhoneNumber.retrieveAll(req.user.hasAccountAuth ? req.user.account_sid : null) :
await PhoneNumber.retrieveAllForSP(req.user.service_provider_sid);
const results = req.user.hasServiceProviderAuth ?
await PhoneNumber.retrieveAllForSP(req.user.service_provider_sid) :
await PhoneNumber.retrieveAll(req.user.hasAccountAuth ? req.user.account_sid : null);
res.status(200).json(results);
} catch (err) {
sysError(logger, res, err);
@@ -110,6 +120,9 @@ router.get('/:sid', async(req, res) => {
throw new DbErrorBadRequest('insufficient privileges');
}
}
if (req.user.hasAccountAuth && results.length > 1) {
return res.status(200).json(results.filter((r) => r.phone_number_sid === sid)[0]);
}
return res.status(200).json(results[0]);
}
catch (err) {

View File

@@ -2,6 +2,17 @@ const router = require('express').Router();
const sysError = require('../error');
const {DbErrorBadRequest} = require('../../utils/errors');
const {getHomerApiKey, getHomerSipTrace, getHomerPcap} = require('../../utils/homer-utils');
const {getJaegerTrace} = require('../../utils/jaeger-utils');
const Account = require('../../models/account');
const {
getS3Object,
getGoogleStorageObject,
getAzureStorageObject,
deleteS3Object,
deleteGoogleStorageObject,
deleteAzureStorageObject
} = require('../../utils/storage-utils');
const parseAccountSid = (url) => {
const arr = /Accounts\/([^\/]*)/.exec(url);
if (arr) return arr[1];
@@ -18,7 +29,7 @@ router.get('/', async(req, res) => {
logger.debug({opts: req.query}, 'GET /RecentCalls');
const account_sid = parseAccountSid(req.originalUrl);
const service_provider_sid = account_sid ? null : parseServiceProviderSid(req.originalUrl);
const {page, count, trunk, direction, days, answered, start, end} = req.query || {};
const {page, count, trunk, direction, days, answered, start, end, filter} = req.query || {};
if (!page || page < 1) throw new DbErrorBadRequest('missing or invalid "page" query arg');
if (!count || count < 25 || count > 500) throw new DbErrorBadRequest('missing or invalid "count" query arg');
@@ -33,6 +44,7 @@ router.get('/', async(req, res) => {
answered,
start: days ? undefined : start,
end: days ? undefined : end,
filter
});
res.status(200).json(data);
}
@@ -47,6 +59,7 @@ router.get('/', async(req, res) => {
answered,
start: days ? undefined : start,
end: days ? undefined : end,
filter
});
res.status(200).json(data);
}
@@ -72,12 +85,12 @@ router.get('/:call_id', async(req, res) => {
}
});
router.get('/:call_id/pcap', async(req, res) => {
router.get('/:call_id/:method/pcap', async(req, res) => {
const {logger} = req.app.locals;
try {
const token = await getHomerApiKey(logger);
if (!token) return res.sendStatus(400, {msg: 'getHomerApiKey: Failed to get Homer API token; check server config'});
const stream = await getHomerPcap(logger, token, [req.params.call_id]);
const stream = await getHomerPcap(logger, token, [req.params.call_id], req.params.method);
if (!stream) {
logger.info(`getHomerApiKey: unable to get sip traces from Homer for ${req.params.call_id}`);
return res.sendStatus(404);
@@ -93,4 +106,100 @@ router.get('/:call_id/pcap', async(req, res) => {
}
});
router.get('/trace/:trace_id', async(req, res) => {
const {logger} = req.app.locals;
const {trace_id} = req.params;
try {
const obj = await getJaegerTrace(logger, trace_id);
if (!obj) {
logger.info(`/RecentCalls: unable to get spans from jaeger for ${trace_id}`);
return res.sendStatus(404);
}
res.status(200).json(obj.result);
} catch (err) {
logger.error({err}, `/RecentCalls error retrieving jaeger trace ${trace_id}`);
res.sendStatus(500);
}
});
router.get('/:call_sid/record/:year/:month/:day/:format', async(req, res) => {
const {logger} = req.app.locals;
const {call_sid, year, month, day, format} = req.params;
try {
const account_sid = parseAccountSid(req.originalUrl);
const r = await Account.retrieve(account_sid);
if (r.length === 0 || !r[0].bucket_credential) return res.sendStatus(404);
const {bucket_credential} = r[0];
const getOptions = {
...bucket_credential,
key: `${year}/${month}/${day}/${call_sid}.${format || 'mp3'}`
};
let stream;
switch (bucket_credential.vendor) {
case 'aws_s3':
case 's3_compatible':
stream = await getS3Object(logger, getOptions);
break;
case 'google':
stream = await getGoogleStorageObject(logger, getOptions);
break;
case 'azure':
stream = await getAzureStorageObject(logger, getOptions);
break;
default:
logger.error(`There is no handler for fetching record from ${bucket_credential.vendor}`);
return res.sendStatus(500);
}
res.set({
'Content-Type': `audio/${format || 'mp3'}`
});
if (stream) {
stream.pipe(res);
} else {
return res.sendStatus(404);
}
} catch (err) {
logger.error({err}, ` error retrieving recording ${call_sid}`);
res.sendStatus(404);
}
});
router.delete('/:call_sid/record/:year/:month/:day/:format', async(req, res) => {
const {logger} = req.app.locals;
const {call_sid, year, month, day, format} = req.params;
try {
const account_sid = parseAccountSid(req.originalUrl);
const r = await Account.retrieve(account_sid);
if (r.length === 0 || !r[0].bucket_credential) return res.sendStatus(404);
const {bucket_credential} = r[0];
const deleteOptions = {
...bucket_credential,
key: `${year}/${month}/${day}/${call_sid}.${format || 'mp3'}`
};
switch (bucket_credential.vendor) {
case 'aws_s3':
case 's3_compatible':
await deleteS3Object(logger, deleteOptions);
break;
case 'google':
await deleteGoogleStorageObject(logger, deleteOptions);
break;
case 'azure':
await deleteAzureStorageObject(logger, deleteOptions);
break;
default:
logger.error(`There is no handler for deleting record from ${bucket_credential.vendor}`);
return res.sendStatus(500);
}
res.sendStatus(204);
} catch (err) {
logger.error({err}, ` error deleting recording ${call_sid}`);
res.sendStatus(404);
}
});
module.exports = router;

View File

@@ -16,8 +16,9 @@ const insertUserSql = `INSERT into users
(user_sid, account_sid, name, email, provider, provider_userid, email_validated)
values (?, ?, ?, ?, ?, ?, 1)`;
const insertUserLocalSql = `INSERT into users
(user_sid, account_sid, name, email, email_activation_code, email_validated, provider, hashed_password)
values (?, ?, ?, ?, ?, 0, 'local', ?)`;
(user_sid, account_sid, name, email, email_activation_code, email_validated, provider,
hashed_password, service_provider_sid)
values (?, ?, ?, ?, ?, 0, 'local', ?, ?)`;
const insertAccountSql = `INSERT into accounts
(account_sid, service_provider_sid, name, is_active, webhook_secret, trial_end_date)
values (?, ?, ?, ?, ?, CURDATE() + INTERVAL 21 DAY)`;
@@ -36,7 +37,7 @@ const insertSignupHistorySql = `INSERT into signup_history
values (?, ?)`;
const addLocalUser = async(logger, user_sid, account_sid,
name, email, email_activation_code, passwordHash) => {
name, email, email_activation_code, passwordHash, service_provider_sid) => {
const [r] = await promisePool.execute(insertUserLocalSql,
[
user_sid,
@@ -44,7 +45,8 @@ const addLocalUser = async(logger, user_sid, account_sid,
name,
email,
email_activation_code,
passwordHash
passwordHash,
service_provider_sid
]);
debug({r}, 'Result from adding user');
};
@@ -145,7 +147,7 @@ router.post('/', async(req, res) => {
const user = await doGithubAuth(logger, req.body);
logger.info({user}, 'retrieved user details from github');
Object.assign(userProfile, {
name: user.name,
name: user.email,
email: user.email,
email_validated: user.email_validated,
avatar_url: user.avatar_url,
@@ -157,7 +159,7 @@ router.post('/', async(req, res) => {
const user = await doGoogleAuth(logger, req.body);
logger.info({user}, 'retrieved user details from google');
Object.assign(userProfile, {
name: user.name || user.email,
name: user.email || user.email,
email: user.email,
email_validated: user.verified_email,
picture: user.picture,
@@ -170,7 +172,7 @@ router.post('/', async(req, res) => {
logger.info({user}, 'retrieved user details for local provider');
debug({user}, 'retrieved user details for local provider');
Object.assign(userProfile, {
name: user.name,
name: user.email,
email: user.email,
provider: 'local',
email_activation_code: user.email_activation_code
@@ -280,7 +282,8 @@ router.post('/', async(req, res) => {
const passwordHash = await generateHashedPassword(req.body.password);
debug(`hashed password: ${passwordHash}`);
await addLocalUser(logger, userProfile.user_sid, userProfile.account_sid,
userProfile.name, userProfile.email, userProfile.email_activation_code, passwordHash);
userProfile.name, userProfile.email, userProfile.email_activation_code,
passwordHash, req.body.service_provider_sid);
debug('added local user');
}
else {
@@ -293,17 +296,25 @@ router.post('/', async(req, res) => {
const callStatusSid = uuid();
const helloWordSid = uuid();
const dialTimeSid = uuid();
const echoSid = uuid();
/* 3 webhooks */
await promisePool.execute(insertWebookSql, [callStatusSid, 'https://public-apps.jambonz.us/call-status', 'POST']);
await promisePool.execute(insertWebookSql, [helloWordSid, 'https://public-apps.jambonz.us/hello-world', 'POST']);
await promisePool.execute(insertWebookSql, [dialTimeSid, 'https://public-apps.jambonz.us/dial-time', 'POST']);
/* 4 webhooks */
await promisePool.execute(insertWebookSql,
[callStatusSid, 'https://public-apps.jambonz.cloud/call-status', 'POST']);
await promisePool.execute(insertWebookSql,
[helloWordSid, 'https://public-apps.jambonz.cloud/hello-world', 'POST']);
await promisePool.execute(insertWebookSql,
[dialTimeSid, 'https://public-apps.jambonz.cloud/dial-time', 'POST']);
await promisePool.execute(insertWebookSql,
[echoSid, 'https://public-apps.jambonz.cloud/echo', 'POST']);
/* 2 applications */
await promisePool.execute(insertApplicationSql, [uuid(), userProfile.account_sid, 'hello world',
helloWordSid, callStatusSid, 'google', 'en-US', 'en-US-Wavenet-C', 'google', 'en-US']);
await promisePool.execute(insertApplicationSql, [uuid(), userProfile.account_sid, 'dial time clock',
dialTimeSid, callStatusSid, 'google', 'en-US', 'en-US-Wavenet-C', 'google', 'en-US']);
await promisePool.execute(insertApplicationSql, [uuid(), userProfile.account_sid, 'simple echo test',
echoSid, callStatusSid, 'google', 'en-US', 'en-US-Wavenet-C', 'google', 'en-US']);
Object.assign(userProfile, {
pristine: true,
@@ -327,7 +338,7 @@ router.post('/', async(req, res) => {
await addLocalUser(logger, userProfile.user_sid, userProfile.account_sid,
userProfile.name, userProfile.email, userProfile.email_activation_code,
passwordHash);
passwordHash, req.body.service_provider_sid);
/* note: we deactivate the old user once the new email is validated */
}
@@ -349,6 +360,8 @@ router.post('/', async(req, res) => {
const token = jwt.sign({
user_sid: userProfile.user_sid,
account_sid: userProfile.account_sid,
service_provider_sid: req.body.service_provider_sid,
scope: 'account',
email: userProfile.email,
name: userProfile.name
}, process.env.JWT_SECRET, { expiresIn });

View File

@@ -28,20 +28,13 @@ router.get('/', async(req, res) => {
if (req.user.hasAccountAuth) {
const [r] = await promisePool.query('SELECT * from accounts WHERE account_sid = ?', req.user.account_sid);
if (0 === r.length) throw new Error('invalid account_sid');
if (0 === r.length) throw new DbErrorBadRequest('invalid account_sid');
service_provider_sid = r[0].service_provider_sid;
}
if (req.user.hasServiceProviderAuth) {
const [r] = await promisePool.query(
'SELECT * from service_providers where service_provider_sid = ?',
service_provider_sid);
if (0 === r.length) throw new Error('invalid account_sid');
service_provider_sid = r[0].service_provider_sid;
if (!service_provider_sid) throw new DbErrorBadRequest('missing service_provider_sid in query');
service_provider_sid = req.user.service_provider_sid;
}
/** generally, we have a global set of SBCs that all accounts use.

View File

@@ -13,7 +13,7 @@ WHERE up.permission_sid = p.permission_sid
AND up.user_sid = ?
`;
const validateRequest = async(req) => {
const validateRequest = (req) => {
const {email, password} = req.body || {};
/* check required properties are there */

View File

@@ -14,20 +14,14 @@ const getFsUrl = async(logger, retrieveSet, setName, provider) => {
logger.info('No available feature servers to handle createCall API request');
return ;
}
const ip = stripPort(fs[idx++ % fs.length]);
logger.info({fs}, `feature servers available for createCall API request, selecting ${ip}`);
return `http://${ip}:3000/v1/messaging/${provider}`;
const f = fs[idx++ % fs.length];
logger.info({fs}, `feature servers available for createCall API request, selecting ${f}`);
return `${f}/v1/messaging/${provider}`;
} catch (err) {
logger.error({err}, 'getFsUrl: error retreving feature servers from redis');
}
};
const stripPort = (hostport) => {
const arr = /^(.*):(.*)$/.exec(hostport);
if (arr) return arr[1];
return hostport;
};
const doSendResponse = async(res, respondFn, body) => {
if (typeof respondFn === 'number') res.sendStatus(respondFn);
else if (typeof respondFn !== 'function') res.sendStatus(200);
@@ -44,7 +38,7 @@ router.post('/:provider', async(req, res) => {
lookupAppByPhoneNumber,
logger
} = req.app.locals;
const setName = `${process.env.JAMBONES_CLUSTER_ID || 'default'}:active-fs`;
const setName = `${process.env.JAMBONES_CLUSTER_ID || 'default'}:fs-service-url`;
logger.debug({path: req.path, body: req.body}, 'incomingSMS from carrier');
// search for provider module

View File

@@ -3,7 +3,7 @@ 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');
const {decrypt, encrypt, obscureKey} = require('../../utils/encrypt-decrypt');
const {parseAccountSid, parseServiceProviderSid, parseSpeechCredentialSid} = require('./utils');
const {DbErrorUnprocessableRequest, DbErrorForbidden} = require('../../utils/errors');
const {
@@ -99,17 +99,6 @@ const validateTest = async(req, speech_credentials) => {
}
};
const obscureKey = (key) => {
const key_spoiler_length = 6;
const key_spoiler_char = 'X';
if (key.length <= key_spoiler_length) {
return key;
}
return `${key.slice(0, key_spoiler_length)}${key_spoiler_char.repeat(key.length - key_spoiler_length)}`;
};
const encryptCredential = (obj) => {
const {
vendor,
@@ -125,8 +114,10 @@ const encryptCredential = (obj) => {
nuance_stt_uri,
use_custom_tts,
custom_tts_endpoint,
custom_tts_endpoint_url,
use_custom_stt,
custom_stt_endpoint,
custom_stt_endpoint_url,
tts_api_key,
tts_region,
stt_api_key,
@@ -135,7 +126,8 @@ const encryptCredential = (obj) => {
instance_id,
custom_stt_url,
custom_tts_url,
auth_token = ''
auth_token = '',
cobalt_server_uri
} = obj;
switch (vendor) {
@@ -158,15 +150,19 @@ const encryptCredential = (obj) => {
return encrypt(awsData);
case 'microsoft':
assert(region, 'invalid azure speech credential: region is required');
assert(api_key, 'invalid azure speech credential: api_key is required');
if (!custom_tts_endpoint_url && !custom_stt_endpoint_url) {
assert(region, 'invalid azure speech credential: region is required');
assert(api_key, 'invalid azure speech credential: api_key is required');
}
const azureData = JSON.stringify({
region,
api_key,
...(region && {region}),
...(api_key && {api_key}),
use_custom_tts,
custom_tts_endpoint,
custom_tts_endpoint_url,
use_custom_stt,
custom_stt_endpoint
custom_stt_endpoint,
custom_stt_endpoint_url
});
return encrypt(azureData);
@@ -201,6 +197,11 @@ const encryptCredential = (obj) => {
const sonioxData = JSON.stringify({api_key});
return encrypt(sonioxData);
case 'cobalt':
assert(cobalt_server_uri, 'invalid cobalt speech credential: cobalt_server_uri is required');
const cobaltData = JSON.stringify({cobalt_server_uri});
return encrypt(cobaltData);
default:
if (vendor.startsWith('custom:')) {
const customData = JSON.stringify({auth_token, custom_stt_url, custom_tts_url});
@@ -218,6 +219,7 @@ router.post('/', async(req, res) => {
use_for_stt,
use_for_tts,
vendor,
label
} = req.body;
const account_sid = req.user.account_sid || req.body.account_sid;
const service_provider_sid = req.user.service_provider_sid ||
@@ -232,11 +234,21 @@ router.post('/', async(req, res) => {
}
}
// Check if vendor and label is already used for account or SP
if (label) {
const existingSpeech = await SpeechCredential.isAvailableVendorAndLabel(
service_provider_sid, account_sid, vendor, label);
if (existingSpeech.length > 0) {
throw new DbErrorUnprocessableRequest(`Label ${label} is already in use for another speech credential`);
}
}
const encrypted_credential = encryptCredential(req.body);
const uuid = await SpeechCredential.make({
account_sid,
service_provider_sid,
vendor,
label,
use_for_tts,
use_for_stt,
credential: encrypted_credential
@@ -247,6 +259,72 @@ router.post('/', async(req, res) => {
}
});
function decryptCredential(obj, credential, logger) {
if ('google' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
const key_header = '-----BEGIN PRIVATE KEY-----\n';
const obscured = {
...o,
private_key: `${key_header}${obscureKey(o.private_key.slice(key_header.length, o.private_key.length))}`
};
obj.service_key = obscured;
}
else if ('aws' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
obj.access_key_id = o.access_key_id;
obj.secret_access_key = obscureKey(o.secret_access_key);
obj.aws_region = o.aws_region;
logger.info({obj, o}, 'retrieving aws speech credential');
}
else if ('microsoft' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
obj.api_key = obscureKey(o.api_key);
obj.region = o.region;
obj.use_custom_tts = o.use_custom_tts;
obj.custom_tts_endpoint = o.custom_tts_endpoint;
obj.custom_tts_endpoint_url = o.custom_tts_endpoint_url;
obj.use_custom_stt = o.use_custom_stt;
obj.custom_stt_endpoint = o.custom_stt_endpoint;
obj.custom_stt_endpoint_url = o.custom_stt_endpoint_url;
logger.info({obj, o}, 'retrieving azure speech credential');
}
else if ('wellsaid' === obj.vendor) {
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 = o.secret ? obscureKey(o.secret) : null;
}
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;
} else if ('nvidia' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
obj.riva_server_uri = o.riva_server_uri;
} else if ('cobalt' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
obj.cobalt_server_uri = o.cobalt_server_uri;
} else if ('soniox' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
obj.api_key = obscureKey(o.api_key);
} else if (obj.vendor.startsWith('custom:')) {
const o = JSON.parse(decrypt(credential));
obj.auth_token = obscureKey(o.auth_token);
obj.custom_stt_url = o.custom_stt_url;
obj.custom_tts_url = o.custom_tts_url;
}
}
/**
* retrieve all speech credentials for an account
*/
@@ -273,66 +351,7 @@ router.get('/', async(req, res) => {
res.status(200).json(creds.map((c) => {
const {credential, ...obj} = c;
if ('google' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
const key_header = '-----BEGIN PRIVATE KEY-----\n';
const obscured = {
...o,
private_key: `${key_header}${obscureKey(o.private_key.slice(key_header.length, o.private_key.length))}`
};
obj.service_key = obscured;
}
else if ('aws' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
obj.access_key_id = o.access_key_id;
obj.secret_access_key = obscureKey(o.secret_access_key);
obj.aws_region = o.aws_region;
logger.info({obj, o}, 'retrieving aws speech credential');
}
else if ('microsoft' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
obj.api_key = obscureKey(o.api_key);
obj.region = o.region;
obj.use_custom_tts = o.use_custom_tts;
obj.custom_tts_endpoint = o.custom_tts_endpoint;
obj.use_custom_stt = o.use_custom_stt;
obj.custom_stt_endpoint = o.custom_stt_endpoint;
logger.info({obj, o}, 'retrieving azure speech credential');
}
else if ('wellsaid' === obj.vendor) {
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 = o.secret ? obscureKey(o.secret) : null;
}
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;
} else if ('nvidia' == obj.vendor) {
const o = JSON.parse(decrypt(credential));
obj.riva_server_uri = o.riva_server_uri;
}
else if ('soniox' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
obj.api_key = obscureKey(o.api_key);
}
else if (obj.vendor.startsWith('custom:')) {
const o = JSON.parse(decrypt(credential));
obj.auth_token = obscureKey(o.auth_token);
obj.custom_stt_url = o.custom_stt_url;
obj.custom_tts_url = o.custom_tts_url;
}
decryptCredential(obj, credential, logger);
if (req.user.hasAccountAuth && obj.account_sid === null) {
delete obj.api_key;
@@ -362,66 +381,7 @@ router.get('/:sid', async(req, res) => {
await validateRetrieveUpdateDelete(req, cred);
const {credential, ...obj} = cred[0];
if ('google' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
const key_header = '-----BEGIN PRIVATE KEY-----\n';
const obscured = {
...o,
private_key: `${key_header}${obscureKey(o.private_key.slice(key_header.length, o.private_key.length))}`
};
obj.service_key = JSON.stringify(obscured);
}
else if ('aws' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
obj.access_key_id = o.access_key_id;
obj.secret_access_key = obscureKey(o.secret_access_key);
obj.aws_region = o.aws_region;
}
else if ('microsoft' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
obj.api_key = obscureKey(o.api_key);
obj.region = o.region;
obj.use_custom_tts = o.use_custom_tts;
obj.custom_tts_endpoint = o.custom_tts_endpoint;
obj.use_custom_stt = o.use_custom_stt;
obj.custom_stt_endpoint = o.custom_stt_endpoint;
}
else if ('wellsaid' === obj.vendor) {
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 = o.secret ? obscureKey(o.secret) : null;
obj.nuance_tts_uri = o.nuance_tts_uri;
obj.nuance_stt_uri = o.nuance_stt_uri;
}
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;
} else if ('nvidia' == obj.vendor) {
const o = JSON.parse(decrypt(credential));
obj.riva_server_uri = o.riva_server_uri;
}
else if ('soniox' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
obj.api_key = obscureKey(o.api_key);
}
else if (obj.vendor.startsWith('custom:')) {
const o = JSON.parse(decrypt(credential));
obj.auth_token = obscureKey(o.auth_token);
obj.custom_stt_url = o.custom_stt_url;
obj.custom_tts_url = o.custom_tts_url;
}
decryptCredential(obj, credential, logger);
if (req.user.hasAccountAuth && obj.account_sid === null) {
delete obj.api_key;
@@ -488,8 +448,13 @@ router.put('/:sid', async(req, res) => {
const {
use_custom_tts,
custom_tts_endpoint,
custom_tts_endpoint_url,
use_custom_stt,
custom_stt_endpoint
custom_stt_endpoint,
custom_stt_endpoint_url,
custom_stt_url,
custom_tts_url,
cobalt_server_uri
} = req.body;
const newCred = {
@@ -499,13 +464,18 @@ router.put('/:sid', async(req, res) => {
aws_region,
use_custom_tts,
custom_tts_endpoint,
custom_tts_endpoint_url,
use_custom_stt,
custom_stt_endpoint,
custom_stt_endpoint_url,
stt_region,
tts_region,
riva_server_uri,
nuance_stt_uri,
nuance_tts_uri
nuance_tts_uri,
custom_stt_url,
custom_tts_url,
cobalt_server_uri
};
logger.info({o, newCred}, 'updating speech credential with this new credential');
obj.credential = encryptCredential(newCred);
@@ -618,8 +588,10 @@ router.get('/:sid/test', async(req, res) => {
region,
use_custom_tts,
custom_tts_endpoint,
custom_tts_endpoint_url,
use_custom_stt,
custom_stt_endpoint
custom_stt_endpoint,
custom_stt_endpoint_url
} = credential;
if (cred.use_for_tts) {
try {
@@ -628,8 +600,10 @@ router.get('/:sid/test', async(req, res) => {
region,
use_custom_tts,
custom_tts_endpoint,
custom_tts_endpoint_url,
use_custom_stt,
custom_stt_endpoint
custom_stt_endpoint,
custom_stt_endpoint_url
});
results.tts.status = 'ok';
SpeechCredential.ttsTestResult(sid, true);

View File

@@ -0,0 +1,14 @@
const router = require('express').Router();
const SystemInformation = require('../../models/system-information');
router.post('/', async(req, res) => {
const sysInfo = await SystemInformation.add(req.body);
res.status(201).json(sysInfo);
});
router.get('/', async(req, res) => {
const [sysInfo] = await SystemInformation.retrieveAll();
res.status(200).json(sysInfo || {});
});
module.exports = router;

View File

@@ -0,0 +1,29 @@
const router = require('express').Router();
const {
parseAccountSid
} = require('./utils');
router.delete('/', async(req, res) => {
const {purgeTtsCache} = req.app.locals;
const account_sid = parseAccountSid(req);
if (account_sid) {
await purgeTtsCache({account_sid});
} else {
await purgeTtsCache();
}
res.sendStatus(204);
});
router.get('/', async(req, res) => {
const {getTtsSize} = req.app.locals;
const account_sid = parseAccountSid(req);
let size = 0;
if (account_sid) {
size = await getTtsSize(`tts:${account_sid}:*`);
} else {
size = await getTtsSize();
}
res.status(200).json({size});
});
module.exports = router;

View File

@@ -338,8 +338,8 @@ router.put('/:user_sid', async(req, res) => {
//if (req.user.user_sid && req.user.user_sid !== user_sid) return res.sendStatus(403);
if (!hasAdminAuth &&
!(hasAccountAuth && req.user.account_sid === user[0].account_sid) &&
!(hasServiceProviderAuth && req.user.service_provider_sid === user[0].service_provider_sid) &&
!(hasAccountAuth && user[0] && req.user.account_sid === user[0].account_sid) &&
!(hasServiceProviderAuth && user[0] && req.user.service_provider_sid === user[0].service_provider_sid) &&
(req.user.user_sid && req.user.user_sid !== user_sid)) {
return res.sendStatus(403);
}
@@ -453,12 +453,12 @@ router.post('/', async(req, res) => {
if (name) {
logger.debug({payload}, 'user with this username already exists');
return res.status(422).json({msg: 'user with this username already exists'});
return res.status(422).json({msg: 'invalid username or email'});
}
if (email) {
logger.debug({payload}, 'user with this email already exists');
return res.status(422).json({msg: 'user with this email already exists'});
return res.status(422).json({msg: 'invalid username or email'});
}
if (req.user.hasAdminAuth) {

View File

@@ -234,6 +234,14 @@ const parseUserSid = (req) => {
}
};
const parseLcrSid = (req) => {
try {
return validateSid('Lcrs', req);
} catch (error) {
throw error;
}
};
const hasAccountPermissions = async(req, res, next) => {
try {
if (req.user.hasScope('admin')) {
@@ -242,7 +250,18 @@ const hasAccountPermissions = async(req, res, next) => {
if (req.user.hasScope('service_provider')) {
const service_provider_sid = parseServiceProviderSid(req);
if (service_provider_sid === req.user.service_provider_sid) return next();
const account_sid = parseAccountSid(req);
if (service_provider_sid) {
if (service_provider_sid === req.user.service_provider_sid) {
return next();
}
}
if (account_sid) {
const [r] = await Account.retrieve(account_sid);
if (r && r.service_provider_sid === req.user.service_provider_sid) {
return next();
}
}
}
if (req.user.hasScope('account')) {
@@ -435,6 +454,7 @@ module.exports = {
parseWebhookSid,
parseSipGatewaySid,
parseUserSid,
parseLcrSid,
hasAccountPermissions,
hasServiceProviderPermissions,
checkLimits,

View File

@@ -61,8 +61,7 @@ router.post('/', express.raw({type: 'application/json'}), async(req, res) => {
}
/* process event */
logger.info(`received webhook: ${evt.type}`);
if (evt.type.startsWith('invoice.')) handleInvoiceEvents(logger, evt);
if (evt?.type?.startsWith('invoice.')) handleInvoiceEvents(logger, evt);
else {
logger.debug(evt, 'unhandled stripe webook');
}

View File

@@ -38,6 +38,12 @@ tags:
description: Webhooks operations
- name: Microsoft Teams Tenants
description: Microsoft Teams Tenants operations
- name: Lcrs
description: Least Cost Routing operations
- name: LcrRoutes
description: Least Cost Routing Routes operations
- name: LcrCarrierSetEntries
description: Least Cost Routing Carrier Set Entries operation
paths:
/BetaInviteCodes:
post:
@@ -780,7 +786,7 @@ paths:
required: true
schema:
type: string
example: mycorp.sip.jambonz.us
example: mycorp.sip.jambonz.cloud
responses:
200:
description: indicates whether value is already in use
@@ -2182,6 +2188,58 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
/ServiceProviders/{ServiceProviderSid}/Lcrs:
parameters:
- name: ServiceProviderSid
in: path
required: true
schema:
type: string
format: uuid
get:
tags:
- Service Providers
summary: get all Least Cost Routings for a service provider
operationId: getServiceProviderLcrs
responses:
200:
description: Least cost routing listing
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Lcr'
403:
description: unauthorized
404:
description: service provider not found
post:
tags:
- Service Providers
summary: create a Lest cost routing
operationId: createLcrForServiceProvider
requestBody:
content:
application/json:
schema:
type: object
properties:
name:
type: string
description: name or Least Cost Routing
example: twilioLcr
required:
- name
responses:
201:
description: service provider Lcr successfully created
content:
application/json:
schema:
$ref: '#/components/schemas/SuccessfulAdd'
404:
description: service provider not found
/Accounts/{AccountSid}/Limits:
post:
tags:
@@ -2988,6 +3046,12 @@ paths:
enum:
- inbound
- outbound
- in: query
name: filter
required: false
schema:
type: string
description: Filter value can be caller ID, callee ID or call Sid
get:
tags:
- Accounts
@@ -3187,6 +3251,18 @@ paths:
enum:
- inbound
- outbound
- in: query
name: from
required: false
schema:
type: string
description: calling number to retrieve
- in: query
name: to
required: false
schema:
type: string
description: called number to retrieve
get:
tags:
- Service Providers
@@ -3743,8 +3819,13 @@ paths:
type: string
format: uuid
description: The application to use to control this call. Either applicationSid or url is required.
answerOnBridge:
type: boolean
description: If set to true, the inbound call will ring until the number that was dialed answers the call, and at that point a 200 OK will be sent on the inbound leg. If false, the inbound call will be answered immediately as the outbound call is placed.
example: false
call_hook:
$ref: '#/components/schemas/Webhook'
example: {"url": "https://acme.com/callhook", "method": "POST"}
call_status_hook:
$ref: '#/components/schemas/Webhook'
example: {"url": "https://acme.com/status", "method": "POST"}
@@ -3758,15 +3839,23 @@ paths:
example: "blf.finotel.com"
timeout:
type: integer
description: the number of seconds to wait for call to be answered. Defaults to 60.
description: The number of seconds to wait for call to be answered. Defaults to 60.
example: 30
timeLimit:
type: integer
description: The max length of call in seconds
example: 60
tag:
type: object
description: initial set of customer-supplied metadata to associate with the call (see jambonz 'tag' verb)
description: Initial set of customer-supplied metadata to associate with the call (see jambonz 'tag' verb)
example: {"callCount": 10}
to:
$ref: '#/components/schemas/Target'
description: destination for call
description: Destination for call
headers:
type: object
description: The customer SIP headers to associate with the call
example: {"X-Custom-Header": "Hello"}
responses:
201:
description: call successfully created
@@ -3793,6 +3882,43 @@ paths:
required: true
schema:
type: string
- in: query
name: direction
required: false
schema:
type: string
enum:
- inbound
- outbound
description: call direction to retrieve
- in: query
name: from
required: false
schema:
type: string
description: calling number to retrieve
- in: query
name: to
required: false
schema:
type: string
description: called number to retrieve
- in: query
name: callStatus
required: false
schema:
type: string
enum:
- trying
- ringing
- early-media
- in-progress
- completed
- failed
- busy
- no-answer
- queued
description: call status to retrieve
responses:
200:
description: list of calls for a specified account
@@ -3879,6 +4005,8 @@ paths:
properties:
call_hook:
$ref: '#/components/schemas/Webhook'
child_call_hook:
$ref: '#/components/schemas/Webhook'
call_status:
type: string
enum:
@@ -3898,6 +4026,7 @@ paths:
type: string
enum:
- pause
- silence
- resume
mute_status:
type: string
@@ -4001,7 +4130,479 @@ paths:
smpp_err_code:
type: string
400:
description: bad request
description: bad request
/Accounts/{AccountSid}/Queues:
parameters:
- name: AccountSid
in: path
required: true
schema:
type: string
format: uuid
- in: query
name: search
required: false
schema:
type: string
description: queue name of data to retrieve
get:
tags:
- Accounts
summary: retrieve active queues for an account
operationId: listQueues
responses:
200:
description: retrieve active queues records for a specified account
content:
application/json:
schema:
type: array
items:
type: object
properties:
name:
type: string
length:
type: string
/Lcrs:
post:
tags:
- Lcrs
summary: create a Least Cost Routing
operationId: createLcr
requestBody:
content:
application/json:
schema:
type: object
properties:
name:
type: string
description: name or Least Cost Routing
example: twilioLcr
required:
- name
responses:
201:
description: Least Cost Routing successfully created
content:
application/json:
schema:
$ref: '#/components/schemas/SuccessfulAdd'
400:
description: bad request
content:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
422:
description: unprocessable entity
content:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
500:
description: system error
content:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
get:
tags:
- Lcrs
summary: list least cost routings
operationId: listLeastCostRoutings
responses:
200:
description: list of least cost routings
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Lcr'
500:
description: system error
content:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
/Lcrs/{LcrSid}:
parameters:
- name: LcrSid
in: path
required: true
style: simple
explode: false
schema:
type: string
delete:
tags:
- Lcrs
summary: delete a least cost routing
operationId: deleteLeastCostRouting
responses:
204:
description: least cost routing successfully deleted
404:
description: least cost routing not found
422:
description: unprocessable entity
content:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
example:
msg: a service provider with active accounts can not be deleted
500:
description: system error
content:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
get:
tags:
- Lcrs
summary: retrieve least cost routing
operationId: getLeastCostRouting
responses:
200:
description: least cost routing found
content:
application/json:
schema:
$ref: '#/components/schemas/Lcr'
404:
description: least cost routing not found
500:
description: system error
content:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
put:
tags:
- Lcrs
summary: update least cost routing
operationId: updateLeastCostRouting
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/Lcr'
responses:
204:
description: least cost routing updated
content:
application/json:
schema:
$ref: '#/components/schemas/Lcr'
400:
description: bad request
content:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
404:
description: least cost routing not found
500:
description: system error
content:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
/LcrRoutes:
post:
tags:
- LcrRoutes
summary: create a Least Cost Routing Routes
operationId: createLcrRoutes
requestBody:
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/LcrRoute'
required:
- lcr_sid
- regex
- priority
responses:
201:
description: Least Cost Routing Route successfully created
content:
application/json:
schema:
$ref: '#/components/schemas/SuccessfulAdd'
400:
description: bad request
content:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
422:
description: unprocessable entity
content:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
500:
description: system error
content:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
get:
tags:
- LcrRoutes
summary: list least cost routings routes
operationId: listLeastCostRoutingRoutes
responses:
200:
description: list of least cost routing routes
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/LcrRoute'
500:
description: system error
content:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
/LcrRoutes/{LcrRouteSid}:
parameters:
- name: LcrRouteSid
in: path
required: true
style: simple
explode: false
schema:
type: string
delete:
tags:
- LcrRoutes
summary: delete a least cost routing route
operationId: deleteLeastCostRoutingRoute
responses:
204:
description: least cost routing route successfully deleted
404:
description: least cost routing route not found
422:
description: unprocessable entity
content:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
example:
msg: a service provider with active accounts can not be deleted
500:
description: system error
content:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
get:
tags:
- LcrRoutes
summary: retrieve least cost routing route
operationId: getLeastCostRoutingRoute
responses:
200:
description: least cost routing route found
content:
application/json:
schema:
$ref: '#/components/schemas/LcrRoute'
404:
description: least cost routing route not found
500:
description: system error
content:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
put:
tags:
- LcrRoutes
summary: update least cost routing route
operationId: updateLeastCostRoutingRoute
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/LcrRoute'
responses:
204:
description: least cost routing route updated
content:
application/json:
schema:
$ref: '#/components/schemas/LcrRoute'
400:
description: bad request
content:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
404:
description: least cost routing route not found
500:
description: system error
content:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
/LcrCarrierSetEntries:
post:
tags:
- LcrCarrierSetEntries
summary: create a Least Cost Routing Carrier Set Entry
operationId: createLcrCarrierSetEntry
requestBody:
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/LcrCarrierSetEntry'
required:
- lcr_route_sid
- voip_carrier_sid
- priority
responses:
201:
description: Least Cost Routing Carrier Set Entry successfully created
content:
application/json:
schema:
$ref: '#/components/schemas/SuccessfulAdd'
400:
description: bad request
content:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
422:
description: unprocessable entity
content:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
500:
description: system error
content:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
get:
tags:
- LcrCarrierSetEntries
summary: list least cost routings routes
operationId: listLeastCostRoutingCarrierSetEntries
responses:
200:
description: list of least cost routing carrier set entries
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/LcrCarrierSetEntry'
500:
description: system error
content:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
/LcrCarrierSetEntries/{LcrCarrierSetEntrySid}:
parameters:
- name: LcrCarrierSetEntrySid
in: path
required: true
style: simple
explode: false
schema:
type: string
delete:
tags:
- LcrCarrierSetEntries
summary: delete a least cost routing carrier set entry
operationId: deleteLeastCostRoutingCarrierSetEntry
responses:
204:
description: least cost routing carrier set entry successfully deleted
404:
description: least cost routing carrier set entry not found
422:
description: unprocessable entity
content:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
example:
msg: a service provider with active accounts can not be deleted
500:
description: system error
content:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
get:
tags:
- LcrCarrierSetEntries
summary: retrieve least cost routing carrier set entry
operationId: getLeastCostRoutingCarrierSetEntry
responses:
200:
description: least cost routing carrier set entry found
content:
application/json:
schema:
$ref: '#/components/schemas/LcrCarrierSetEntry'
404:
description: least cost routing carrier set entry not found
500:
description: system error
content:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
put:
tags:
- LcrCarrierSetEntries
summary: update least cost routing carrier set entry
operationId: updateLeastCostRoutingCarrierSetEntry
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/LcrCarrierSetEntry'
responses:
204:
description: least cost routing carrier set entry updated
content:
application/json:
schema:
$ref: '#/components/schemas/LcrCarrierSetEntry'
400:
description: bad request
content:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
404:
description: least cost routing carrier set entry not found
500:
description: system error
content:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
components:
securitySchemes:
bearerAuth:
@@ -4410,10 +5011,21 @@ components:
- phone
- sip
- user
- teams
number:
type: string
sipUri:
type: string
tenant:
type: string
description: Microsoft Teams customer tenant domain name
trunk:
type: string
vmail:
type: boolean
description: Dial directly into user's voicemail to leave a message
overrideTo:
type: string
name:
type: string
auth:
@@ -4898,6 +5510,57 @@ components:
- voice_call_session
- api_limit
- devices
Lcr:
type: object
properties:
name:
type: string
example: twilioLcr
default_carrier_set_entry_sid:
type: string
example: 3fa85f64-5717-4562-b3fc-2c963f66afa6
required:
- name
LcrRoute:
type: object
properties:
lcr_sid:
type: string
example: 3fa85f64-5717-4562-b3fc-2c963f66afa6
regex:
type: string
description: out going call Phone number regex
example: 1*
priority:
type: number
example: 1
description:
type: string
example: this is example description
required:
- lcr_sid
- regex
- priority
LcrCarrierSetEntry:
type: object
properties:
workload:
type: number
example: 90
description: traffic distribution value
lcr_route_sid:
type: string
example: 3fa85f64-5717-4562-b3fc-2c963f66afa6
voip_carrier_sid:
type: string
example: 3fa85f64-5717-4562-b3fc-2c963f66afa6
priority:
type: number
example: 1
required:
- lcr_route_sid
- voip_carrier_sid
- priority
security:
- bearerAuth: []

View File

@@ -47,13 +47,15 @@ const sendEmailByCustomVendor = async(logger, from, to, subject, text) => {
};
const sendEmailByMailgun = async(logger, from, to, subject, text) => {
const mg = mailgun.client({
username: 'api',
key: process.env.MAILGUN_API_KEY
});
if (!process.env.MAILGUN_API_KEY) throw new Error('MAILGUN_API_KEY env variable is not defined!');
if (!process.env.MAILGUN_DOMAIN) throw new Error('MAILGUN_DOMAIN env variable is not defined!');
const mg = mailgun.client({
username: 'api',
key: process.env.MAILGUN_API_KEY,
...(process.env.MAILGUN_URL && {url: process.env.MAILGUN_URL})
});
try {
const res = await mg.messages.create(process.env.MAILGUN_DOMAIN, {
from,

View File

@@ -23,7 +23,18 @@ const decrypt = (data) => {
return decrpyted.toString();
};
const obscureKey = (key, key_spoiler_length = 6) => {
const key_spoiler_char = 'X';
if (!key || key.length <= key_spoiler_length) {
return key;
}
return `${key.slice(0, key_spoiler_length)}${key_spoiler_char.repeat(key.length - key_spoiler_length)}`;
};
module.exports = {
encrypt,
decrypt
decrypt,
obscureKey
};

View File

@@ -2,7 +2,7 @@
"trial": [
{
"category": "voice_call_session",
"quantity": 20
"quantity": 5
},
{
"category": "device",

View File

@@ -64,7 +64,7 @@ const getHomerSipTrace = async(logger, apiKey, callId) => {
}
};
const getHomerPcap = async(logger, apiKey, callIds) => {
const getHomerPcap = async(logger, apiKey, callIds, method) => {
if (!process.env.HOMER_BASE_URL || !process.env.HOMER_USERNAME || !process.env.HOMER_PASSWORD) {
logger.debug('getHomerPcap: Homer integration not installed');
}
@@ -73,18 +73,23 @@ const getHomerPcap = async(logger, apiKey, callIds) => {
const stream = await postPcap('/api/v3/export/call/messages/pcap', {
param: {
transaction: {
call: true,
registration: true,
call: method === 'invite',
registration: method === 'register',
rest: false
},
orlogic: true,
search: {
'1_call': {
callid: callIds
},
'1_registration': {
callid: callIds
}
...(method === 'invite' && {
'1_call': {
callid: callIds
}
})
,
...(method === 'register' && {
'1_registration': {
callid: callIds
}
})
},
},
timestamp: {

19
lib/utils/jaeger-utils.js Normal file
View File

@@ -0,0 +1,19 @@
const bent = require('bent');
const getJSON = bent(process.env.JAEGER_BASE_URL || 'http://127.0.0.1', 'GET', 'json', 200);
const getJaegerTrace = async(logger, traceId) => {
if (!process.env.JAEGER_BASE_URL) {
logger.debug('getJaegerTrace: jaeger integration not installed');
return null;
}
try {
return await getJSON(`/api/v3/traces/${traceId}`);
} catch (err) {
const url = `${process.env.JAEGER_BASE_URL}/api/traces/${traceId}`;
logger.error({err, traceId}, `getJaegerTrace: Error retrieving spans from ${url}`);
}
};
module.exports = {
getJaegerTrace
};

View File

@@ -0,0 +1 @@
Hello From Jambonz. This file was created because Record all call bucket credential test.

134
lib/utils/storage-utils.js Normal file
View File

@@ -0,0 +1,134 @@
const { S3Client, PutObjectCommand, GetObjectCommand, DeleteObjectCommand } = require('@aws-sdk/client-s3');
const {Storage} = require('@google-cloud/storage');
const fs = require('fs');
const { BlobServiceClient } = require('@azure/storage-blob');
// Azure
async function testAzureStorage(logger, opts) {
const blobServiceClient = BlobServiceClient.fromConnectionString(opts.connection_string);
const containerClient = blobServiceClient.getContainerClient(opts.name);
const blockBlobClient = containerClient.getBlockBlobClient('jambonz-sample.text');
await blockBlobClient.uploadFile(`${__dirname}/jambonz-sample.text`);
}
async function getAzureStorageObject(logger, opts) {
const blobServiceClient = BlobServiceClient.fromConnectionString(opts.connection_string);
const containerClient = blobServiceClient.getContainerClient(opts.name);
const blockBlobClient = containerClient.getBlockBlobClient(opts.key);
const response = await blockBlobClient.download(0);
return response.readableStreamBody;
}
async function deleteAzureStorageObject(logger, opts) {
const blobServiceClient = BlobServiceClient.fromConnectionString(opts.connection_string);
const containerClient = blobServiceClient.getContainerClient(opts.name);
const blockBlobClient = containerClient.getBlockBlobClient(opts.key);
await blockBlobClient.delete();
}
// Google
function _initGoogleClient(opts) {
const serviceKey = JSON.parse(opts.service_key);
return new Storage({
projectId: serviceKey.project_id,
credentials: {
client_email: serviceKey.client_email,
private_key: serviceKey.private_key
},
});
}
async function testGoogleStorage(logger, opts) {
return new Promise((resolve, reject) => {
const storage = _initGoogleClient(opts);
const blob = storage.bucket(opts.name).file('jambonz-sample.text');
fs.createReadStream(`${__dirname}/jambonz-sample.text`)
.pipe(blob.createWriteStream())
.on('error', (err) => reject(err))
.on('finish', () => resolve());
});
}
async function getGoogleStorageObject(logger, opts) {
const storage = _initGoogleClient(opts);
const bucket = storage.bucket(opts.name);
const file = bucket.file(opts.key);
const [exists] = await file.exists();
if (exists) {
return file.createReadStream();
}
}
async function deleteGoogleStorageObject(logger, opts) {
const storage = _initGoogleClient(opts);
const bucket = storage.bucket(opts.name);
const file = bucket.file(opts.key);
await file.delete();
}
// S3
function _initS3Client(opts) {
return new S3Client({
credentials: {
accessKeyId: opts.access_key_id,
secretAccessKey: opts.secret_access_key,
},
region: opts.region || 'us-east-1',
...(opts.vendor === 's3_compatible' && { endpoint: opts.endpoint, forcePathStyle: true })
});
}
async function testS3Storage(logger, opts) {
const s3 = _initS3Client(opts);
const input = {
'Body': 'Hello From Jambonz',
'Bucket': opts.name,
'Key': 'jambonz-sample.text'
};
const command = new PutObjectCommand(input);
await s3.send(command);
}
async function getS3Object(logger, opts) {
const s3 = _initS3Client(opts);
const command = new GetObjectCommand(
{
Bucket: opts.name,
Key: opts.key
}
);
const res = await s3.send(command);
return res.Body;
}
async function deleteS3Object(logger, opts) {
const s3 = _initS3Client(opts);
const command = new DeleteObjectCommand(
{
Bucket: opts.name,
Key: opts.key
}
);
await s3.send(command);
}
module.exports = {
testS3Storage,
getS3Object,
deleteS3Object,
testGoogleStorage,
getGoogleStorageObject,
deleteGoogleStorageObject,
testAzureStorage,
getAzureStorageObject,
deleteAzureStorageObject
};

10173
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "jambonz-api-server",
"version": "v0.8.2",
"version": "0.8.4",
"description": "",
"main": "app.js",
"scripts": {
@@ -19,15 +19,17 @@
"url": "https://github.com/jambonz/jambonz-api-server.git"
},
"dependencies": {
"@aws-sdk/client-transcribe": "^3.290.0",
"@deepgram/sdk": "^1.10.2",
"@google-cloud/speech": "^5.1.0",
"@jambonz/db-helpers": "^0.7.3",
"@jambonz/realtimedb-helpers": "^0.7.0",
"@jambonz/speech-utils": "^0.0.8",
"@jambonz/time-series": "^0.2.5",
"@jambonz/verb-specifications": "^0.0.3",
"@soniox/soniox-node": "^1.1.0",
"@aws-sdk/client-transcribe": "^3.363.0",
"@aws-sdk/client-s3": "^3.363.0",
"@deepgram/sdk": "^1.21.0",
"@google-cloud/speech": "^5.2.0",
"@jambonz/db-helpers": "^0.9.0",
"@jambonz/realtimedb-helpers": "^0.8.6",
"@jambonz/speech-utils": "^0.0.15",
"@jambonz/time-series": "^0.2.8",
"@jambonz/verb-specifications": "^0.0.29",
"@jambonz/lamejs": "^1.2.2",
"@soniox/soniox-node": "^1.1.1",
"argon2": "^0.30.3",
"bent": "^7.3.12",
"cors": "^2.8.5",
@@ -38,7 +40,7 @@
"helmet": "^5.1.0",
"ibm-watson": "^7.1.2",
"jsonwebtoken": "^9.0.0",
"mailgun.js": "^3.7.3",
"mailgun.js": "^9.1.2",
"microsoft-cognitiveservices-speech-sdk": "^1.24.1",
"mysql2": "^2.3.3",
"nocache": "3.0.4",
@@ -49,11 +51,15 @@
"stripe": "^8.222.0",
"swagger-ui-express": "^4.4.0",
"uuid": "^8.3.2",
"yamljs": "^0.3.0"
"yamljs": "^0.3.0",
"ws": "^8.12.1",
"wav": "^1.0.2",
"@google-cloud/storage": "^6.12.0",
"@azure/storage-blob": "^12.15.0"
},
"devDependencies": {
"eslint": "^7.32.0",
"eslint-plugin-promise": "^4.2.1",
"eslint": "^8.39.0",
"eslint-plugin-promise": "^6.1.1",
"husky": "7.0.4",
"nyc": "^15.1.0",
"request": "^2.88.2",

View File

@@ -13,6 +13,12 @@ const {
createServiceProvider,
createPhoneNumber,
deleteObjectBySid} = require('./utils');
const logger = require('../lib/logger');
const { addToSortedSet } = require('@jambonz/realtimedb-helpers')({
host: process.env.JAMBONES_REDIS_HOST,
port: process.env.JAMBONES_REDIS_PORT || 6379
}, logger);
process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
@@ -162,11 +168,33 @@ test('account tests', async(t) => {
queue_event_hook: {
url: 'http://example.com/q',
method: 'post'
},
record_all_calls: true,
record_format: 'wav',
bucket_credential: {
vendor: 'aws_s3',
region: 'us-east-1',
name: 'recordings',
access_key_id: 'access_key_id',
secret_access_key: 'secret access key'
}
}
});
t.ok(result.statusCode === 204, 'successfully updated account using account level token');
/* verify that bucket credential is updated*/
result = await request.get(`/Accounts/${sid}`, {
auth: {bearer: accountLevelToken},
json: true,
});
t.ok(result.bucket_credential.vendor === 'aws_s3', 'bucket_vendor was updated');
t.ok(result.bucket_credential.name === 'recordings', 'bucket_name was updated');
t.ok(result.bucket_credential.access_key_id === 'access_key_id', 'bucket_access_key_id was updated');
t.ok(result.record_all_calls === 1, 'record_all_calls was updated');
t.ok(result.record_format === 'wav', 'record_format was updated');
/* verify that account level api key last_used was updated*/
result = await request.get(`/Accounts/${sid}/ApiKeys`, {
auth: {bearer: accountLevelToken},
@@ -261,6 +289,31 @@ test('account tests', async(t) => {
});
t.ok(result.statusCode === 204, 'successfully deleted a call session limit for an account');
/* query account queues */
await addToSortedSet(`queue:${sid}:test`, 'url1');
await addToSortedSet(`queue:${sid}:dummy`, 'url2');
result = await request.get(`/Accounts/${sid}/Queues`, {
auth: authAdmin,
resolveWithFullResponse: true,
json: true,
});
t.ok(result.statusCode === 200 && result.body.length === 2, 'successfully queried account queues info for an account');
result = await request.get(`/Accounts/${sid}/Queues?search=test`, {
auth: authAdmin,
resolveWithFullResponse: true,
json: true,
});
t.ok(result.statusCode === 200 && result.body.length === 1, 'successfully queried account queue info with search for an account');
result = await request.get(`/Accounts/29d41725-9d3a-4f89-9f0b-f32b3e4d3159/Queues`, {
auth: authAdmin,
resolveWithFullResponse: true,
json: true,
});
t.ok(result.statusCode === 200 && result.body.length === 0, 'successfully queried account queue info with for an invalid account');
/* delete account */
result = await request.delete(`/Accounts/${sid}`, {
auth: authAdmin,

View File

@@ -53,7 +53,7 @@ test('application tests', async(t) => {
]'
}
});
t.ok(result.statusCode === 400, 'Cant create application with invalid app_josn');
t.ok(result.statusCode === 400, 'Cant create application with invalid app_json');
/* add an application */
result = await request.post('/Applications', {
@@ -81,7 +81,15 @@ test('application tests', async(t) => {
"seekOffset": 8000,\
"actionHook": "/play/action"\
}\
]'
]',
use_for_fallback_speech: 1,
fallback_speech_synthesis_vendor: 'google',
fallback_speech_synthesis_language: 'en-US',
fallback_speech_synthesis_voice: 'man',
fallback_speech_synthesis_label: 'label1',
fallback_speech_recognizer_vendor: 'google',
fallback_speech_recognizer_language: 'en-US',
fallback_speech_recognizer_label: 'label1'
}
});
t.ok(result.statusCode === 201, 'successfully created application');
@@ -102,6 +110,14 @@ 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');
t.ok(result.use_for_fallback_speech === 1, 'successfully create use_for_fallback_speech');
t.ok(result.fallback_speech_synthesis_vendor === 'google', 'successfully create fallback_speech_synthesis_vendor');
t.ok(result.fallback_speech_synthesis_language === 'en-US', 'successfully create fallback_speech_synthesis_language');
t.ok(result.fallback_speech_synthesis_voice === 'man', 'successfully create fallback_speech_synthesis_voice');
t.ok(result.fallback_speech_synthesis_label === 'label1', 'successfully create fallback_speech_synthesis_label');
t.ok(result.fallback_speech_recognizer_vendor === 'google', 'successfully create fallback_speech_recognizer_vendor');
t.ok(result.fallback_speech_recognizer_language === 'en-US', 'successfully create fallback_speech_recognizer_language');
t.ok(result.fallback_speech_recognizer_label === 'label1', 'successfully create fallback_speech_recognizer_label');
let app_json = JSON.parse(result.app_json);
t.ok(app_json[0].verb === 'play', 'successfully retrieved app_json from application')
@@ -125,7 +141,16 @@ test('application tests', async(t) => {
"X-Reason" : "maximum call duration exceeded"\
}\
}\
]'
]',
record_all_calls: true,
use_for_fallback_speech: 0,
fallback_speech_synthesis_vendor: 'microsoft',
fallback_speech_synthesis_language: 'en-US',
fallback_speech_synthesis_voice: 'woman',
fallback_speech_synthesis_label: 'label2',
fallback_speech_recognizer_vendor: 'microsoft',
fallback_speech_recognizer_language: 'en-US',
fallback_speech_recognizer_label: 'label2'
}
});
t.ok(result.statusCode === 204, 'successfully updated application');
@@ -138,6 +163,15 @@ test('application tests', async(t) => {
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')
t.ok(result.record_all_calls === 1, 'successfully updated record_all_calls from application')
t.ok(result.use_for_fallback_speech === 0, 'successfully update use_for_fallback_speech');
t.ok(result.fallback_speech_synthesis_vendor === 'microsoft', 'successfully update fallback_speech_synthesis_vendor');
t.ok(result.fallback_speech_synthesis_language === 'en-US', 'successfully update fallback_speech_synthesis_language');
t.ok(result.fallback_speech_synthesis_voice === 'woman', 'successfully update fallback_speech_synthesis_voice');
t.ok(result.fallback_speech_synthesis_label === 'label2', 'successfully update fallback_speech_synthesis_label');
t.ok(result.fallback_speech_recognizer_vendor === 'microsoft', 'successfully update fallback_speech_recognizer_vendor');
t.ok(result.fallback_speech_recognizer_language === 'en-US', 'successfully update fallback_speech_recognizer_language');
t.ok(result.fallback_speech_recognizer_label === 'label2', 'successfully update fallback_speech_recognizer_label');
/* remove applications app_json*/
result = await request.put(`/Applications/${sid}`, {

View File

@@ -31,8 +31,8 @@ test('Create Call Success With Synthesizer in Payload', async (t) => {
auth: authUser,
json: true,
body: {
call_hook: "https://public-apps.jambonz.us/hello-world",
call_status_hook: "https://public-apps.jambonz.us/call-status",
call_hook: "https://public-apps.jambonz.cloud/hello-world",
call_status_hook: "https://public-apps.jambonz.cloud/call-status",
from: "15083778299",
to: {
type: "phone",
@@ -73,8 +73,8 @@ test('Create Call Success Without Synthesizer in Payload', async (t) => {
auth: authUser,
json: true,
body: {
call_hook: "https://public-apps.jambonz.us/hello-world",
call_status_hook: "https://public-apps.jambonz.us/call-status",
call_hook: "https://public-apps.jambonz.cloud/hello-world",
call_status_hook: "https://public-apps.jambonz.cloud/call-status",
from: "15083778299",
to: {
type: "phone",
@@ -83,4 +83,74 @@ test('Create Call Success Without Synthesizer in Payload', async (t) => {
}
}).then(data => { t.ok(false, 'Create Call should not be success') })
.catch(error => { t.ok(error.response.statusCode === 400, 'Call failed for no synthesizer') });
});
test("Create call with application sid and app_json", async(t) => {
const app = require('../app');
const service_provider_sid = await createServiceProvider(request, 'account3_has_synthesizer');
const account_sid = await createAccount(request, service_provider_sid, 'account3_has_synthesizer');
const token = jwt.sign({
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);
// GIVEN
/* add an application */
const app_json = '[\
{\
"verb": "play",\
"url": "https://example.com/example.mp3",\
"timeoutSecs": 10,\
"seekOffset": 8000,\
"actionHook": "/play/action"\
}\
]';
let result = await request.post('/Applications', {
resolveWithFullResponse: true,
auth: authUser,
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
}
});
t.ok(result.statusCode === 201, 'successfully created application');
const sid = result.body.sid;
// WHEN
result = await request.post(`/Accounts/${account_sid}/Calls`, {
resolveWithFullResponse: true,
auth: authUser,
json: true,
body: {
application_sid: sid,
from: "15083778299",
to: {
type: "phone",
number: "15089084809"
},
}
});
// THEN
t.ok(result.statusCode === 201, 'successfully created Call without Synthesizer && application_sid');
const fs_request = await getLastRequestFromFeatureServer('15083778299_createCall');
const obj = JSON.parse(fs_request);
t.ok(obj.body.app_json == app_json, 'app_json successfully added')
});

119
test/clients.js Normal file
View File

@@ -0,0 +1,119 @@
const test = require('tape') ;
const ADMIN_TOKEN = '38700987-c7a4-4685-a5bb-af378f9734de';
const authAdmin = {bearer: ADMIN_TOKEN};
const request = require('request-promise-native').defaults({
baseUrl: 'http://127.0.0.1:3000/v1'
});
process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
});
test('client test', async(t) => {
const app = require('../app');
try {
let result;
/* add a service provider */
result = await request.post('/ServiceProviders', {
resolveWithFullResponse: true,
auth: authAdmin,
json: true,
body: {
name: 'client_sp',
}
});
t.ok(result.statusCode === 201, 'successfully created client service provider');
const sp_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 new entity */
result = await request.post('/Clients', {
resolveWithFullResponse: true,
auth: authAdmin,
json: true,
body: {
account_sid,
username: 'client1',
password: 'sdf12412',
is_active: 1
}
});
t.ok(result.statusCode === 201, 'successfully created Client');
const sid = result.body.sid;
/* query all entity */
result = await request.get('/Clients', {
auth: authAdmin,
json: true,
});
t.ok(result.length === 1 , 'successfully queried all Clients');
/* query one entity */
result = await request.get(`/Clients/${sid}`, {
auth: authAdmin,
json: true,
});
t.ok(result.account_sid === account_sid , 'successfully retrieved Client by sid');
t.ok(result.client_sid, 'successfully retrieved Client by sid');
t.ok(result.username === 'client1', 'successfully retrieved Client by sid');
t.ok(result.is_active === 1 , 'successfully retrieved Client by sid');
t.ok(result.password === 'sXXXXXXX' , 'successfully retrieved Client by sid');
/* update the entity */
result = await request.put(`/Clients/${sid}`, {
auth: authAdmin,
json: true,
resolveWithFullResponse: true,
body: {
is_active: 0
}
});
t.ok(result.statusCode === 204, 'successfully updated Client');
/* query one entity */
result = await request.get(`/Clients/${sid}`, {
auth: authAdmin,
json: true,
});
t.ok(result.is_active === 0 , 'successfully updated Client');
t.ok(result.password === 'sXXXXXXX' , 'successfully retrieved Client by sid');
/* delete Client */
result = await request.delete(`/Clients/${sid}`, {
resolveWithFullResponse: true,
simple: false,
json: true,
auth: authAdmin
});
t.ok(result.statusCode === 204, 'successfully deleted Clients');
/* query all entity */
result = await request.get('/Clients', {
auth: authAdmin,
json: true,
});
t.ok(result.length === 0 , 'successfully queried all Clients');
} catch (err) {
console.error(err);
t.end(err);
}
})

325
test/forgot-password.js Normal file
View File

@@ -0,0 +1,325 @@
const test = require('tape');
const request = require("request-promise-native").defaults({
baseUrl: "http://127.0.0.1:3000/v1",
});
let authAdmin;
let admin_user_sid;
let sp_sid;
let sp_user_sid;
let account_sid;
let account_sid2;
let account_user_sid;
let account_user_sid2;
const password = "12345foobar";
const adminEmail = "joe@foo.bar";
const emailInactiveAccount = 'inactive-account@example.com';
const emailInactiveUser = 'inactive-user@example.com';
test('forgot password - prepare', async (t) => {
/* login as admin to get a jwt */
let 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"
);
authAdmin = { bearer: result.body.token };
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" + Date.now(),
},
});
t.ok(result.statusCode === 201, "successfully created service provider");
sp_sid = result.body.sid;
/* add service_provider user */
const randomNumber = Math.floor(Math.random() * 101);
result = await request.post(`/Users`, {
resolveWithFullResponse: true,
json: true,
auth: authAdmin,
body: {
name: "service_provider" + Date.now(),
email: `sp${randomNumber}@example.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"
);
sp_user_sid = result.body.user_sid;
/* add an account - inactive */
result = await request.post("/Accounts", {
resolveWithFullResponse: true,
auth: authAdmin,
json: true,
body: {
name: "sample_account inactive" + Date.now(),
service_provider_sid: sp_sid,
registration_hook: {
url: "http://example.com/reg",
method: "get",
},
is_active: false,
webhook_secret: "foobar",
},
});
t.ok(result.statusCode === 201, "successfully created account");
account_sid = result.body.sid;
/* add an account - inactive */
result = await request.post("/Accounts", {
resolveWithFullResponse: true,
auth: authAdmin,
json: true,
body: {
name: "sample_account active" + Date.now(),
service_provider_sid: sp_sid,
registration_hook: {
url: "http://example.com/reg",
method: "get",
},
is_active: true,
webhook_secret: "foobar",
},
});
t.ok(result.statusCode === 201, "successfully created account");
account_sid2 = result.body.sid;
/* add account user connected to an inactive account */
result = await request.post(`/Users`, {
resolveWithFullResponse: true,
json: true,
auth: authAdmin,
body: {
name: "account user active - inactive account" + randomNumber,
email: emailInactiveAccount,
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"
);
account_user_sid = result.body.user_sid;
/* add account user that is not active */
result = await request.post(`/Users`, {
resolveWithFullResponse: true,
json: true,
auth: authAdmin,
body: {
name: "account user inactive - active account" + randomNumber,
email: emailInactiveUser,
is_active: false,
force_change: true,
initial_password: password,
service_provider_sid: sp_sid,
account_sid: account_sid2,
},
});
t.ok(
result.statusCode === 201 && result.body.user_sid,
"account scope user created"
);
account_user_sid2 = result.body.user_sid;
});
test('forgot password with valid email', async (t) => {
const res = await request
.post('/forgot-password',
{
resolveWithFullResponse: true,
json: true,
auth: authAdmin,
body: { email: adminEmail }
});
t.equal(res.statusCode, 204, 'returns 204 status code');
t.end();
});
test('forgot password with invalid email', async (t) => {
const statusCode = 400;
const errorMessage = 'invalid or missing email';
const email = 'invalid-email';
try {
await request
.post('/forgot-password', {
resolveWithFullResponse: true,
json: true,
auth: authAdmin,
body: { email }
});
} catch (error) {
t.throws(
() => {
throw error;
},
{
name: "StatusCodeError",
statusCode,
message: `${statusCode} - {"error":"${errorMessage}"}`,
}
);
}
t.end();
});
test('forgot password with non-existent email', async (t) => {
const statusCode = 400;
const errorMessage = 'email does not exist';
const email = 'non-existent-email@example.com';
try {
await request
.post('/forgot-password', {
resolveWithFullResponse: true,
json: true,
auth: authAdmin,
body: { email }
});
} catch (error) {
t.throws(
() => {
throw error;
},
{
name: "StatusCodeError",
statusCode,
message: `${statusCode} - {"error":"${errorMessage}"}`,
}
);
}
t.end();
});
test('forgot password with inactive user', async (t) => {
const statusCode = 400;
const errorMessage = 'you may not reset the password of an inactive user';
try {
await request
.post('/forgot-password', {
resolveWithFullResponse: true,
json: true,
auth: authAdmin,
body: { email: emailInactiveUser }
});
} catch (error) {
t.throws(
() => {
throw error;
},
{
name: "StatusCodeError",
statusCode,
message: `${statusCode} - {"error":"${errorMessage}"}`,
}
);
}
t.end();
});
test('forgot password with inactive account', async (t) => {
const statusCode = 400;
const errorMessage = 'you may not reset the password of an inactive account';
try {
await request
.post('/forgot-password', {
resolveWithFullResponse: true,
json: true,
auth: authAdmin,
body: { email: emailInactiveAccount }
});
} catch (error) {
t.throws(
() => {
throw error;
},
{
name: "StatusCodeError",
statusCode,
message: `${statusCode} - {"error":"${errorMessage}"}`,
}
);
}
t.end();
});
test('cleanup', async (t) => {
/* login as admin to get a jwt */
let 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"
);
authAdmin = { bearer: result.body.token };
/* list users */
result = await request.get(`/Users`, {
resolveWithFullResponse: true,
json: true,
auth: authAdmin,
});
const users = result.body;
/* delete all users except admin */
for (const user of users) {
if (user.user_sid === admin_user_sid) continue;
result = await request.delete(`/Users/${user.user_sid}`, {
resolveWithFullResponse: true,
json: true,
auth: authAdmin,
});
t.ok(result.statusCode === 204, "user deleted");
}
/* list accounts */
result = await request.get(`/Accounts`, {
resolveWithFullResponse: true,
json: true,
auth: authAdmin,
});
const accounts = result.body;
for (const acc of accounts) {
result = await request.delete(`/Accounts/${acc.account_sid}`, {
resolveWithFullResponse: true,
json: true,
auth: authAdmin,
});
t.ok(result.statusCode === 204, "acc deleted");
}
});

View File

@@ -19,4 +19,10 @@ require('./webapp_tests');
require('./call-test');
require('./password-settings');
require('./email_utils');
require('./system-information');
require('./lcr-carriers-set-entries');
require('./lcr-routes');
require('./lcrs');
require('./tts-cache');
require('./clients');
require('./docker_stop');

View File

@@ -0,0 +1,103 @@
const test = require('tape') ;
const ADMIN_TOKEN = '38700987-c7a4-4685-a5bb-af378f9734de';
const authAdmin = {bearer: ADMIN_TOKEN};
const request = require('request-promise-native').defaults({
baseUrl: 'http://127.0.0.1:3000/v1'
});
const {createLcrRoute, createVoipCarrier} = require('./utils');
process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
});
test('lcr carrier set entries test', async(t) => {
const app = require('../app');
let sid;
try {
let result;
const lcr_route = await createLcrRoute(request);
const voip_carrier_sid = await createVoipCarrier(request);
/* add new entity */
result = await request.post('/LcrCarrierSetEntries', {
resolveWithFullResponse: true,
auth: authAdmin,
json: true,
body: {
workload: 1,
lcr_route_sid: lcr_route.lcr_route_sid,
voip_carrier_sid,
priority: 1
}
});
t.ok(result.statusCode === 201, 'successfully created lcr carrier set entry ');
const sid = result.body.sid;
/* query all entity */
result = await request.get('/LcrCarrierSetEntries', {
qs: {lcr_route_sid: lcr_route.lcr_route_sid},
auth: authAdmin,
json: true,
});
t.ok(result.length === 1 , 'successfully queried all lcr carrier set entry');
/* query one entity */
result = await request.get(`/LcrCarrierSetEntries/${sid}`, {
auth: authAdmin,
json: true,
});
t.ok(result.workload === 1 , 'successfully retrieved lcr carrier set entry by sid');
/* update the entity */
result = await request.put(`/LcrCarrierSetEntries/${sid}`, {
auth: authAdmin,
json: true,
resolveWithFullResponse: true,
body: {
priority: 2
}
});
t.ok(result.statusCode === 204, 'successfully updated LcrCarrierSetEntries');
/* query one entity */
result = await request.get(`/LcrCarrierSetEntries/${sid}`, {
auth: authAdmin,
json: true,
});
t.ok(result.priority === 2 , 'successfully updated lcr carrier set entry by sid');
/* delete lcr carrier set entry */
result = await request.delete(`/LcrCarrierSetEntries/${sid}`, {
resolveWithFullResponse: true,
simple: false,
json: true,
auth: authAdmin
});
t.ok(result.statusCode === 204, 'successfully deleted LcrCarrierSetEntries');
/* delete lcr route */
result = await request.delete(`/LcrRoutes/${lcr_route.lcr_route_sid}`, {
resolveWithFullResponse: true,
simple: false,
json: true,
auth: authAdmin
});
t.ok(result.statusCode === 204, 'successfully deleted LcrRoutes');
/* delete lcr */
result = await request.delete(`/Lcrs/${lcr_route.lcr_sid}`, {
resolveWithFullResponse: true,
simple: false,
json: true,
auth: authAdmin
});
t.ok(result.statusCode === 204, 'successfully deleted Lcr');
t.end();
}
catch (err) {
console.error(err);
t.end(err);
}
});

93
test/lcr-routes.js Normal file
View File

@@ -0,0 +1,93 @@
const test = require('tape') ;
const ADMIN_TOKEN = '38700987-c7a4-4685-a5bb-af378f9734de';
const authAdmin = {bearer: ADMIN_TOKEN};
const request = require('request-promise-native').defaults({
baseUrl: 'http://127.0.0.1:3000/v1'
});
const {createLcr} = require('./utils');
process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
});
test('lcr routes test', async(t) => {
const app = require('../app');
let sid;
try {
let result;
const lcr_sid = await createLcr(request);
/* add new entity */
result = await request.post('/LcrRoutes', {
resolveWithFullResponse: true,
auth: authAdmin,
json: true,
body: {
lcr_sid,
regex: '1*',
description: 'description',
priority: 1
}
});
t.ok(result.statusCode === 201, 'successfully created lcr route ');
const sid = result.body.sid;
/* query all entity */
result = await request.get('/LcrRoutes', {
qs: {lcr_sid},
auth: authAdmin,
json: true,
});
t.ok(result.length === 1 , 'successfully queried all lcr route');
/* query one entity */
result = await request.get(`/LcrRoutes/${sid}`, {
auth: authAdmin,
json: true,
});
t.ok(result.priority === 1 , 'successfully retrieved lcr route by sid');
/* update the entity */
result = await request.put(`/LcrRoutes/${sid}`, {
auth: authAdmin,
json: true,
resolveWithFullResponse: true,
body: {
priority: 2
}
});
t.ok(result.statusCode === 204, 'successfully updated Lcr Route');
/* query one entity */
result = await request.get(`/LcrRoutes/${sid}`, {
auth: authAdmin,
json: true,
});
t.ok(result.priority === 2 , 'successfully updated lcr Route by sid');
/* delete lcr Route */
result = await request.delete(`/LcrRoutes/${sid}`, {
resolveWithFullResponse: true,
simple: false,
json: true,
auth: authAdmin
});
t.ok(result.statusCode === 204, 'successfully deleted LcrRoutes');
/* delete lcr */
result = await request.delete(`/Lcrs/${lcr_sid}`, {
resolveWithFullResponse: true,
simple: false,
json: true,
auth: authAdmin
});
t.ok(result.statusCode === 204, 'successfully deleted Lcr');
t.end();
}
catch (err) {
console.error(err);
t.end(err);
}
});

76
test/lcrs.js Normal file
View File

@@ -0,0 +1,76 @@
const test = require('tape') ;
const ADMIN_TOKEN = '38700987-c7a4-4685-a5bb-af378f9734de';
const authAdmin = {bearer: ADMIN_TOKEN};
const request = require('request-promise-native').defaults({
baseUrl: 'http://127.0.0.1:3000/v1'
});
process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
});
test('lcr test', async(t) => {
const app = require('../app');
let sid;
try {
let result;
/* add new entity */
result = await request.post('/Lcrs', {
resolveWithFullResponse: true,
auth: authAdmin,
json: true,
body: {
name: 'name'
}
});
t.ok(result.statusCode === 201, 'successfully created lcr');
const sid = result.body.sid;
/* query all entity */
result = await request.get('/Lcrs', {
auth: authAdmin,
json: true,
});
t.ok(result.length === 1 , 'successfully queried all lcr');
/* query one entity */
result = await request.get(`/Lcrs/${sid}`, {
auth: authAdmin,
json: true,
});
t.ok(result.name === 'name' , 'successfully retrieved lcr by sid');
/* update the entity */
result = await request.put(`/Lcrs/${sid}`, {
auth: authAdmin,
json: true,
resolveWithFullResponse: true,
body: {
name: 'name2'
}
});
t.ok(result.statusCode === 204, 'successfully updated Lcr');
/* query one entity */
result = await request.get(`/Lcrs/${sid}`, {
auth: authAdmin,
json: true,
});
t.ok(result.name === 'name2' , 'successfully updated lcr by sid');
/* delete lcr Route */
result = await request.delete(`/Lcrs/${sid}`, {
resolveWithFullResponse: true,
simple: false,
json: true,
auth: authAdmin
});
t.ok(result.statusCode === 204, 'successfully deleted Lcrs');
t.end();
}
catch (err) {
console.error(err);
t.end(err);
}
});

View File

@@ -7,7 +7,7 @@ const test = async() => {
headers: {
Authorization: `Bearer ${process.env.GH_CODE}`,
Accept: 'application/json',
'User-Agent': 'jambonz.us'
'User-Agent': 'jambonz.cloud'
}
}, (err, response, body) => {
if (err) console.log(error);

View File

@@ -75,8 +75,29 @@ test('recent calls tests', async(t) => {
auth: authUser,
json: true,
});
console.log(JSON.stringify(result));
t.ok(result.data.length === 5, 'retrieved 5 recent calls by account');
//console.log({data: result.data}, 'Account recent calls');
result = await request.get(`/Accounts/${account_sid}/RecentCalls?page=1&count=25&filter=1508`, {
auth: authUser,
json: true,
});
console.log(JSON.stringify(result));
t.ok(result.data.length === 5, 'retrieved 5 recent calls by account and from');
result = await request.get(`/Accounts/${account_sid}/RecentCalls?page=1&count=25&filter=15080`, {
auth: authUser,
json: true,
});
console.log(JSON.stringify(result));
t.ok(result.data.length === 0, 'retrieved 0 recent calls by account and non-matching from');
result = await request.get(`/Accounts/${account_sid}/RecentCalls?page=1&count=25&filter=1888`, {
auth: authUser,
json: true,
});
console.log(JSON.stringify(result));
t.ok(result.data.length === 5, 'retrieved 5 recent calls by account and to');
/* query last 7 days by service provider */
result = await request.get(`/ServiceProviders/${service_provider_sid}/RecentCalls?page=1&count=25`, {
@@ -84,6 +105,19 @@ test('recent calls tests', async(t) => {
json: true,
});
t.ok(result.data.length === 5, 'retrieved 5 recent calls by service provider');
result = await request.get(`/ServiceProviders/${service_provider_sid}/RecentCalls?page=1&count=25&filter=1508`, {
auth: authAdmin,
json: true,
});
t.ok(result.data.length === 5, 'retrieved 5 recent calls by service provider and from');
result = await request.get(`/ServiceProviders/${service_provider_sid}/RecentCalls?page=1&count=25&filter=1888`, {
auth: authAdmin,
json: true,
});
t.ok(result.data.length === 5, 'retrieved 5 recent calls by service provider and to');
//console.log({data: result.data}, 'SP recent calls');
/* pull sip traces and pcap from homer */

View File

@@ -17,6 +17,37 @@ test('sbc_addresses tests', async(t) => {
let result;
const service_provider_sid = await createServiceProvider(request);
/* add service_provider user */
const sp_name = 'sbc_service_provider';
const sp_password = 'password';
result = await request.post(`/Users`, {
resolveWithFullResponse: true,
json: true,
auth: authAdmin,
body: {
name: sp_name,
email: 'sbc_sp@jambonz.com',
is_active: true,
force_change: false,
initial_password: sp_password,
service_provider_sid,
}
});
t.ok(result.statusCode === 201 && result.body.user_sid, 'SBC service_provider scope user created');
const sbc_sp_user_sid = result.body.user_sid;
result = await request.post('/login', {
resolveWithFullResponse: true,
json: true,
body: {
username: sp_name,
password: sp_password,
}
});
t.ok(result.statusCode === 200 && result.body.token, 'successfully logged in as sbc user');
const authSbcSp = {bearer: result.body.token};
/* add a service provider sbc */
result = await request.post('/Sbcs', {
resolveWithFullResponse: true,
@@ -38,6 +69,20 @@ test('sbc_addresses tests', async(t) => {
//console.log(result.body)
t.ok(result.body.length === 1 && result.body[0].ipv4 === '192.168.1.4', 'successfully retrieved service provider sbc');
result = await request.get('/Sbcs', {
resolveWithFullResponse: true,
auth: authSbcSp,
json: true
});
//console.log(result.body)
t.ok(result.body.length === 1 && result.body[0].ipv4 === '192.168.1.4', 'successfully retrieved service provider sbc');
await request.delete(`/Users/${sbc_sp_user_sid}`, {
resolveWithFullResponse: true,
json: true,
auth: authAdmin,
});
await deleteObjectBySid(request, '/Sbcs', sid);
await deleteObjectBySid(request, '/ServiceProviders', service_provider_sid);

View File

@@ -27,7 +27,8 @@ test('sip gateway tests', async(t) => {
ipv4: '192.168.1.1',
netmask: 32,
inbound: true,
outbound: true
outbound: true,
protocol: 'tcp'
}
});
t.ok(result.statusCode === 201, 'successfully created sip gateway ');
@@ -48,6 +49,7 @@ test('sip gateway tests', async(t) => {
});
//console.log(`result: ${JSON.stringify(result)}`);
t.ok(result.ipv4 === '192.168.1.1' , 'successfully retrieved voip carrier by sid');
t.ok(result.protocol === 'tcp' , 'successfully retrieved voip carrier by sid');
/* update sip gateway */
@@ -58,7 +60,8 @@ test('sip gateway tests', async(t) => {
body: {
port: 5061,
netmask:24,
outbound: false
outbound: false,
protocol: 'udp'
}
});
t.ok(result.statusCode === 204, 'successfully updated voip carrier');
@@ -75,7 +78,7 @@ test('sip gateway tests', async(t) => {
await deleteObjectBySid(request, '/VoipCarriers', voip_carrier_sid);
//t.end();
t.end();
}
catch (err) {
console.error(err);

View File

@@ -84,6 +84,7 @@ test('speech credentials tests', async(t) => {
json: true,
body: {
vendor: 'google',
label: 'label1',
service_key: jsonKey,
use_for_tts: true,
use_for_stt: true
@@ -111,6 +112,7 @@ test('speech credentials tests', async(t) => {
json: true,
});
t.ok(result.vendor === 'google' , 'successfully retrieved speech credential by sid');
t.ok(result.label === 'label1' , 'label is successfully created');
/* query all credentials */
result = await request.get(`/Accounts/${account_sid}/SpeechCredentials`, {
@@ -445,6 +447,28 @@ test('speech credentials tests', async(t) => {
});
t.ok(result.statusCode === 204, 'successfully deleted speech credential');
/* add a credential for cobalt */
result = await request.post(`/Accounts/${account_sid}/SpeechCredentials`, {
resolveWithFullResponse: true,
auth: authUser,
json: true,
body: {
vendor: 'cobalt',
use_for_stt: true,
use_for_tts: false,
cobalt_server_uri: '32.32.32.32:2727',
}
});
t.ok(result.statusCode === 201, 'successfully added speech credential for Cobalt');
const cobalt_sid = result.body.sid;
/* delete the credential */
result = await request.delete(`/Accounts/${account_sid}/SpeechCredentials/${cobalt_sid}`, {
auth: authUser,
resolveWithFullResponse: true,
});
t.ok(result.statusCode === 204, 'successfully deleted speech credential for Cobalt');
await deleteObjectBySid(request, '/Accounts', account_sid);
await deleteObjectBySid(request, '/ServiceProviders', service_provider_sid);
t.end();

View File

@@ -0,0 +1,64 @@
const test = require('tape') ;
const jwt = require('jsonwebtoken');
const ADMIN_TOKEN = '38700987-c7a4-4685-a5bb-af378f9734de';
const authAdmin = {bearer: ADMIN_TOKEN};
const request = require('request-promise-native').defaults({
baseUrl: 'http://127.0.0.1:3000/v1'
});
test('system information test', async(t) => {
const app = require('../app');
try {
let result = await request.post('/SystemInformation', {
resolveWithFullResponse: true,
auth: authAdmin,
json: true,
body: {
domain_name: 'test.com',
sip_domain_name: 'sip.test.com',
monitoring_domain_name: 'monitor.test.com'
}
});
t.ok(result.statusCode === 201, 'successfully created system information ');
let body = result.body;
t.ok(body.domain_name === 'test.com', 'added domain_name ok');
t.ok(body.sip_domain_name === 'sip.test.com', 'added sip_domain_name ok');
t.ok(body.monitoring_domain_name === 'monitor.test.com', 'added monitoring_domain_name ok');
result = await request.get('/SystemInformation', {
auth: authAdmin,
json: true,
});
t.ok(result.domain_name === 'test.com', 'get domain_name ok');
t.ok(result.sip_domain_name === 'sip.test.com', 'get sip_domain_name ok');
t.ok(result.monitoring_domain_name === 'monitor.test.com', 'get monitoring_domain_name ok');
result = await request.post('/SystemInformation', {
resolveWithFullResponse: true,
auth: authAdmin,
json: true,
body: {
domain_name: 'test1.com',
sip_domain_name: 'sip1.test.com',
monitoring_domain_name: 'monitor1.test.com'
}
});
t.ok(result.statusCode === 201, 'successfully updated system information ');
body = result.body;
t.ok(body.domain_name === 'test1.com', 'updated domain_name ok');
t.ok(body.sip_domain_name === 'sip1.test.com', 'updated sip_domain_name ok');
t.ok(body.monitoring_domain_name === 'monitor1.test.com', 'updated monitoring_domain_name ok');
result = await request.get('/SystemInformation', {
auth: authAdmin,
json: true,
});
t.ok(result.domain_name === 'test1.com', 'get domain_name ok');
t.ok(result.sip_domain_name === 'sip1.test.com', 'get sip_domain_name ok');
t.ok(result.monitoring_domain_name === 'monitor1.test.com', 'get monitoring_domain_name ok');
} catch(err) {
console.error(err);
t.end(err);
}
});

56
test/tts-cache.js Normal file
View File

@@ -0,0 +1,56 @@
const test = require('tape') ;
const jwt = require('jsonwebtoken');
const ADMIN_TOKEN = '38700987-c7a4-4685-a5bb-af378f9734de';
const authAdmin = {bearer: ADMIN_TOKEN};
const request = require('request-promise-native').defaults({
baseUrl: 'http://127.0.0.1:3000/v1'
});
const crypto = require('crypto');
const logger = require('../lib/logger');
const {
client,
} = require('@jambonz/speech-utils')({
host: process.env.JAMBONES_REDIS_HOST,
port: process.env.JAMBONES_REDIS_PORT || 6379
}, logger);
function makeSynthKey({account_sid = '', vendor, language, voice, engine = '', text}) {
const hash = crypto.createHash('sha1');
hash.update(`${language}:${vendor}:${voice}:${engine}:${text}`);
return `tts${account_sid ? (':' + account_sid) : ''}:${hash.digest('hex')}`;
}
test('tts-cache', async(t) => {
const app = require('../app');
try {
// create caches
const minRecords = 8;
for (const i in Array(minRecords).fill(0)) {
await client.set(makeSynthKey({vendor: i, language: i, voice: i, engine: i, text: i}), i);
}
let result = await request.get('/TtsCache', {
auth: authAdmin,
json: true,
});
t.ok(result.size === minRecords, 'get cache correctly');
result = await request.delete('/TtsCache', {
auth: authAdmin,
resolveWithFullResponse: true,
});
t.ok(result.statusCode === 204, 'successfully deleted application after removing phone number');
result = await request.get('/TtsCache', {
auth: authAdmin,
json: true,
});
t.ok(result.size === 0, 'deleted cache successfully');
} catch(err) {
console.error(err);
t.end(err);
}
});

View File

@@ -29,6 +29,31 @@ async function createVoipCarrier(request, name = 'daveh') {
return result.sid;
}
async function createLcr(request, name = 'lcr') {
const result = await request.post('/Lcrs', {
auth: authAdmin,
json: true,
body: {
name
}
});
return result.sid;
}
async function createLcrRoute(request, lcr_name= 'lcr', lcr_route_regex = "1*", lcr_route_priority = 1) {
const lcr = await createLcr(request, lcr_name);
const result = await request.post('/LcrRoutes', {
auth: authAdmin,
json: true,
body: {
lcr_sid: lcr,
regex: lcr_route_regex,
priority: lcr_route_priority
}
});
return {lcr_sid: lcr, lcr_route_sid: result.sid};
}
async function createPhoneNumber(request, voip_carrier_sid, number = '15083333456') {
const result = await request.post('/PhoneNumbers', {
auth: authAdmin,
@@ -136,5 +161,7 @@ module.exports = {
createApiKey,
deleteObjectBySid,
createGoogleSpeechCredentials,
getLastRequestFromFeatureServer
getLastRequestFromFeatureServer,
createLcr,
createLcrRoute
};

View File

@@ -192,7 +192,7 @@ test('webapp tests', async(t) => {
t.ok(result.statusCode === 200 && result.body.available === true, 'indicates when email is available');
/* check if a subdomain is available */
result = await request.get('/Availability?type=subdomain&value=mycompany.sip.jambonz.us', {
result = await request.get('/Availability?type=subdomain&value=mycompany.sip.jambonz.cloud', {
resolveWithFullResponse: true,
auth: authUser,
json: true,