Compare commits

...

101 Commits

Author SHA1 Message Date
Quan HL
08c1b8f708 feat: add metadata for create call 2023-05-07 09:45:36 +07: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
Dave Horton
1b764b31e6 update statement for sbc_addresses.last_updated 2023-04-06 09:15:11 -04:00
Markus Frindt
009396becc Feature/delay middleware (#146)
* add delay middleware to login and signin routes

* Different delay for sendStatus and json

---------

Co-authored-by: Markus Frindt <m.frindt@cognigy.com>
2023-04-06 08:25:45 -04:00
Dave Horton
84305e30cc add sbc_addresses.last_updated 2023-04-06 07:37:27 -04:00
EgleH
9c7f8b4e7b fix small issues in the code (#145)
Co-authored-by: eglehelms <e.helms@cognigy.com>
2023-04-06 07:31:14 -04:00
EgleH
b2dce18c7a Limit access to resources according to user scoped Account or SP (#140)
* limit access to resources according to user scope

* fix error change

* speech credentials validation

* fix speech credentials validation

* fix the issues that didnt allow tests to pass

* speech credential validation

* retrieve speech cred list

* fixt speech credential test valodation

* check scope of smpp-gateways

* check scope of smpp-gateways

* testing time

* /signin for hosted system needs to return scope in jwt

* fix user delete route and adjust tests

* get refactor

---------

Co-authored-by: eglehelms <e.helms@cognigy.com>
Co-authored-by: Dave Horton <daveh@beachdognet.com>
Co-authored-by: Guilherme Rauen <g.rauen@cognigy.com>
2023-04-05 14:20:51 -04:00
Paulo Telles
8f93b69af0 block retries (#144)
* block retries

* block retries

* fixed logginAttempsBlocked typo

---------

Co-authored-by: p.souza <p.souza@cognigy.com>
2023-04-05 10:47:25 -04:00
Markus Frindt
127b690ae2 add nocache middleware (#143)
* add nocache middleware

* add nocache middleware

---------

Co-authored-by: Markus Frindt <m.frindt@cognigy.com>
2023-04-04 14:52:01 -04:00
Hoan Luu Huu
3ad19eca3c feat: carrier register status (#141)
* feat: carrier register status

* update homer to query register pcap

* fix: homer

* fix: remove homer changes

* fix: homer issue
2023-04-03 13:21:38 -04:00
Dave Horton
efe7e22109 increase size of voip_carriers.register_status 2023-04-03 09:50:38 -04:00
Dave Horton
7a67ed704c add voip_carriers.register_status 2023-04-01 19:09:03 -04:00
Markus Frindt
97b17d9e1d Improved invalidation of JWT in redis (#139)
* Improved invalidation of JWT in redis

* use jwt as default value in generateRedisKey

* import logger in app.js

---------

Co-authored-by: Markus Frindt <m.frindt@cognigy.com>
2023-03-31 16:19:34 -04:00
Dave Horton
57110ede76 fix: Dockerfile to reduce vulnerabilities (#137)
The following vulnerabilities are fixed with an upgrade:
- https://snyk.io/vuln/SNYK-ALPINE316-OPENSSL-3368756
- https://snyk.io/vuln/SNYK-ALPINE316-OPENSSL-3368756
- https://snyk.io/vuln/SNYK-ALPINE316-OPENSSL-5291792
- https://snyk.io/vuln/SNYK-ALPINE316-OPENSSL-5291792

Co-authored-by: snyk-bot <snyk-bot@snyk.io>
2023-03-31 07:57:08 -04:00
Guilherme Rauen
d656857509 extend sid validation to all routes (#138)
Co-authored-by: Guilherme Rauen <g.rauen@cognigy.com>
2023-03-31 07:46:33 -04:00
Hoan Luu Huu
bb705fe808 feat: custom email vendor (#130)
* feat: custom email vendor

* feat: custom email vendor

* feat: custom email vendor

* feat: custom email vendor

---------

Co-authored-by: Quan HL <quanluuhoang8@gmail.com>
2023-03-29 12:48:53 -04:00
Guilherme Rauen
789a0ba3ff Fix SQL Injection Vulnerabilities (#134)
* avoid sql injections

* linter

* fix test using random sid

* add some test cases

* remove tests that don't use the new validation

* add test

* linter

* fix tests

* add test

---------

Co-authored-by: Guilherme Rauen <g.rauen@cognigy.com>
2023-03-29 12:36:51 -04:00
EgleH
27cb7c471a Add passwordSettings validation (#136)
* add password Settings validation

* fix test failing because of pass validation

---------

Co-authored-by: eglehelms <e.helms@cognigy.com>
2023-03-29 08:54:05 -04:00
Dave Horton
39260f0b47 bump version 2023-03-28 14:14:44 -04:00
Anton Voylenko
75a2b42d65 update README (#135) 2023-03-27 14:17:03 -04:00
Anton Voylenko
518a9163fb add ENCRYPTION_SECRET variable (#132) 2023-03-25 15:34:09 -04:00
Hoan Luu Huu
5fb4bd7bd1 feat: add nuance on-premise (#131)
* feat: add nuance on-premise

* feat: update fetch nuance credential

* fix: update

* fix nuance tts test against on-prem, refactor aws/google tts testing to use speech-utils package

---------

Co-authored-by: Quan HL <quanluuhoang8@gmail.com>
Co-authored-by: Dave Horton <daveh@beachdognet.com>
2023-03-25 11:20:44 -04:00
Dave Horton
409ad68123 fix tests for AWS speech 2023-03-24 08:59:16 -04:00
Dave Horton
17afb7102a update speech-utils with fix for aws tts 2023-03-24 08:19:18 -04:00
Anton Voylenko
6e7cb9b332 update README (#129) 2023-03-23 15:44:45 -04:00
Anton Voylenko
34f83e323c update swagger yaml (#127) 2023-03-23 09:08:38 -04:00
Anton Voylenko
00af458cb3 Migrate to argon2 from argon2-ffi (#126) 2023-03-22 16:15:01 -04:00
Dave Horton
389017a5c4 update to latest speech-utils 2023-03-20 15:37:35 -04:00
Dave Horton
c4cc6c51ee eliminate parsing of jwt to support either jwt or api key (#124)
* eliminate parsing of jwt to support either jwt or api key

* fixes for preventing non-authorized changes to users

* update to AWS v3 api
2023-03-14 18:54:56 -04:00
Dave Horton
aea7388ba0 refactor of speech-utils (#123) 2023-03-14 10:01:05 -04:00
Dave Horton
3d86292a90 prevent updates to users that would move them to a different account … (#122)
* prevent updates to users that would move them to a different account or service provider, or make them admin users

* bugfix: when updating account as admin user, verify the account sid

* validate account sid when SP user tries to update
2023-03-08 11:38:49 -05:00
Dave Horton
08962fe7ba bugfix: get of speech credential was not returning soniox api_key 2023-03-03 13:49:36 -05:00
Dave Horton
e573f6ab06 change property names for custom speech 2023-03-02 15:28:53 -05:00
Dave Horton
4934e2a1ca add support for custom speech api (#121) 2023-03-02 14:13:41 -05:00
EgleH
cc384995ea add support for soniox speech (#120)
Co-authored-by: eglehelms <e.helms@cognigy.com>
2023-02-26 11:31:39 -05:00
Dave Horton
d4506fb8fa bump version 2023-02-24 10:05:14 -05:00
Dave Horton
042a2c37dc update dockerfile 2023-02-23 08:21:30 -05:00
EgleH
6da1903dee Update node to node:18.14.0-alpine3.16 (#117) 2023-02-21 07:54:26 -05:00
dependabot[bot]
10009d903e Bump undici from 5.11.0 to 5.19.1 (#115)
Bumps [undici](https://github.com/nodejs/undici) from 5.11.0 to 5.19.1.
- [Release notes](https://github.com/nodejs/undici/releases)
- [Commits](https://github.com/nodejs/undici/compare/v5.11.0...v5.19.1)

---
updated-dependencies:
- dependency-name: undici
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-02-16 17:33:12 -05:00
Snyk bot
69a72c5e43 fix: upgrade aws-sdk from 2.1238.0 to 2.1302.0 (#110)
Snyk has created this PR to upgrade aws-sdk from 2.1238.0 to 2.1302.0.

See this package in npm:
https://www.npmjs.com/package/aws-sdk

See this project in Snyk:
https://app.snyk.io/org/davehorton/project/b7e09765-19b2-4aa5-90ba-10432e250041?utm_source=github&utm_medium=referral&page=upgrade-pr
2023-02-16 13:31:57 -05:00
Dave Horton
f7f3881d70 update to @jambonz/verb-specifications (#109) 2023-02-15 10:15:06 -05:00
Hoan Luu Huu
4d48c6946c feat: start using verb-specifications (#107)
* feat: start using verb-specifications

* fix: verb specification v2

* fix vulnerabilities

---------

Co-authored-by: Quan HL <quanluuhoang8@gmail.com>
Co-authored-by: Dave Horton <daveh@beachdognet.com>
2023-02-14 17:24:19 -05:00
Snyk bot
5b48fc8a07 fix: Dockerfile to reduce vulnerabilities (#106)
The following vulnerabilities are fixed with an upgrade:
- https://snyk.io/vuln/SNYK-ALPINE316-OPENSSL-3314623
- https://snyk.io/vuln/SNYK-ALPINE316-OPENSSL-3314624
- https://snyk.io/vuln/SNYK-ALPINE316-OPENSSL-3314624
- https://snyk.io/vuln/SNYK-ALPINE316-OPENSSL-3314641
- https://snyk.io/vuln/SNYK-ALPINE316-OPENSSL-3314643
2023-02-12 22:50:13 -05:00
Hoan Luu Huu
f46be95551 feat: nvidia speech credential (#105)
* feat: nvidia speech credential

* fix: riva_server_uri

* fix: riva_server_uri

---------

Co-authored-by: Quan HL <quanluuhoang8@gmail.com>
2023-02-10 08:31:49 -05:00
EgleH
d4f2be3dc1 Add check for users to delete function (#104)
* add check for users to delete function

* fix typo

* fix active admin check

---------

Co-authored-by: eglehelms <e.helms@cognigy.com>
2023-02-08 07:16:29 -05:00
Dave Horton
a46c24b3aa change applications.app_json to TEXT as it could hold large strings 2023-02-03 16:34:11 -05:00
AFelix53
f5c833720a feat: added twilio gateways (#99) 2023-01-30 10:16:27 -05:00
EgleH
4d2cc15de4 Bug/speech creds get all with no sp sid (#102)
* backwards compatibility

* fetch account and sp speech remove duplicates

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

* update gh actions

---------

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

* modify prev commit to put app_json on applications table

* feat: add app_json to applications endpoint

---------

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

* add another fallback for service_provider_sid

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

* not allow same names for shared and account carriers

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

* apply review comments

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

* apply review comments

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

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

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

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

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

* return permissions as an array of string

* basic GET, POST, DELETE user api calls

* add permission checks

* hide hashed_password

* cleanup

* add check if admin user is active

* return account and serviceProvider sid un user object

* add more values to user PUT

* logout user after self delete, fix scope assignment

* add admin scope user tests

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

* fixes for ibm speech

* add limits license_count and voice_call_minutes

* update limits enum again

* rebase to main

* allow predefined carriers and speech credentials for Account user

* reverse the hasAccountPermissions changes

* SpeechCredentials permissions

* fix /Users/me api non-saas jambonz

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

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

* return permissions as an array of string

* Add JWT expiration environment variable (#74)

* allow fromHost in createCall REST API

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

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

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

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

* return only the jwt-token in the api response

* update swagger.yaml

* add /users api

* apply review comments

* add users test case

* added User model

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

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

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

* return permissions as an array of string

* Add JWT expiration environment variable (#74)

* allow fromHost in createCall REST API

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

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

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

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

* return only the jwt-token in the api response

Co-authored-by: EgleH <egle.helms@gmail.com>
Co-authored-by: eglehelms <e.helms@cognigy.com>
2022-10-27 07:39:11 -04:00
Dave Horton
110c4ed0d8 allow fromHost in createCall REST API 2022-10-25 13:22:30 -04:00
Dave Horton
7890de8c8f update deps 2022-10-23 15:22:36 -04:00
Dave Horton
b65dc7080c Dh password settings (#72)
* update package-lock.json

* Feat: password settings for account (#65)

* feat: password settings for account

* feat: password settings for account

* fix: review comments

* fix: review comments

* fix: review comments

* return empty json

* fix: after review

Co-authored-by: xquanluu <110280845+xquanluu@users.noreply.github.com>
2022-10-23 14:11:00 -04:00
Markus Frindt
9d0be0f8e1 [snyk] Fix vulnerabilities (#70)
Co-authored-by: Markus Frindt <m.frindt@cognigy.com>
2022-10-20 21:34:07 -04:00
99 changed files with 14005 additions and 4098 deletions

View File

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

View File

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

View File

@@ -2,16 +2,8 @@ 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 +12,41 @@ jobs:
if: github.event_name == 'push'
steps:
- uses: actions/checkout@v2
- 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=$GITHUB_REPOSITORY
# 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

1
.gitignore vendored
View File

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

View File

@@ -1,4 +1,4 @@
FROM --platform=linux/amd64 node:18.8.0-alpine 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
@@ -20,4 +20,4 @@ ARG NODE_ENV
ENV NODE_ENV $NODE_ENV
CMD [ "node", "app.js" ]
CMD [ "node", "app.js" ]

View File

@@ -1,4 +1,4 @@
FROM --platform=linux/amd64 node:18.8.0-alpine 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

@@ -1,29 +1,44 @@
# jambonz-api-server ![Build Status](https://github.com/jambonz/jambonz-api-server/workflows/CI/badge.svg)
Jambones REST API server.
Jambones REST API server of the jambones platform.
## Configuration
This process requires the following environment variables to be set.
Configuration is provided via environment variables:
```
JAMBONES_MYSQL_HOST
JAMBONES_MYSQL_USER
JAMBONES_MYSQL_PASSWORD
JAMBONES_MYSQL_DATABASE
JAMBONES_MYSQL_CONNECTION_LIMIT # defaults to 10
JAMBONES_REDIS_HOST
JAMBONES_REDIS_PORT
JAMBONES_LOGLEVEL # defaults to info
JAMBONES_API_VERSION # defaults to v1
HTTP_PORT # defaults to 3000
```
| variable | meaning | required?|
|----------|----------|---------|
|JWT_SECRET| secret for signing JWT token |yes|
|JWT_EXPIRES_IN| expiration time for JWT token(in minutes) |no|
|ENCRYPTION_SECRET| secret for credential encryption(JWT_SECRET is deprecated) |yes|
|HTTP_PORT| tcp port to listen on for API requests from jambonz-api-server |no|
|JAMBONES_LOGLEVEL| log level for application, 'info' or 'debug' |no|
|JAMBONES_MYSQL_HOST| mysql host |yes|
|JAMBONES_MYSQL_USER| mysql username |yes|
|JAMBONES_MYSQL_PASSWORD| mysql password |yes|
|JAMBONES_MYSQL_DATABASE| mysql data |yes|
|JAMBONES_MYSQL_PORT| mysql port |no|
|JAMBONES_MYSQL_CONNECTION_LIMIT| mysql connection limit |no|
|JAMBONES_REDIS_HOST| redis host |yes|
|JAMBONES_REDIS_PORT| redis port |no|
|RATE_LIMIT_WINDOWS_MINS| rate limit window |no|
|RATE_LIMIT_MAX_PER_WINDOW| number of requests per window |no|
|JAMBONES_TRUST_PROXY| trust proxies, must be a number |no|
|JAMBONES_API_VERSION| api version |no|
|JAMBONES_TIME_SERIES_HOST| influxdb host |yes|
|JAMBONES_CLUSTER_ID| cluster id |no|
|HOMER_BASE_URL| HOMER URL |no|
|HOMER_USERNAME| HOMER username |no|
|HOMER_PASSWORD| HOMER password |no|
|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|
#### 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:
- [create_db.sql](db/create_db.sql), which creates the database and associated user (you may want to edit the username and password),
- [jambones-sql.sql](db/jambones-sql.sql), which creates the schema,
- [create-admin-token.sql](db/create-admin-token.sql), which creates an admin-level auth token that can be used for testing/exercising the API.
- [seed-production-database-open-source.sql](db/seed-production-database-open-source.sql), which seeds the database with initial dataset(accounts, permissions, api keys, applications etc).
- [create-admin-user.sql](db/create-admin-user.sql), which creates admin user with password set to "admin". The password will be forced to change after the first login.
> Note: due to the dependency on the npmjs [mysql](https://www.npmjs.com/package/mysql) package, the mysql database must be configured to use sql [native authentication](https://medium.com/@crmcmullen/how-to-run-mysql-8-0-with-native-password-authentication-502de5bac661).

30
app.js
View File

@@ -1,15 +1,9 @@
const assert = require('assert');
const opts = Object.assign({
timestamp: () => {
return `, "time": "${new Date().toISOString()}"`;
}
}, {
level: process.env.JAMBONES_LOGLEVEL || 'info'
});
const logger = require('pino')(opts);
const logger = require('./lib/logger');
const express = require('express');
const app = express();
const helmet = require('helmet');
const nocache = require('nocache');
const rateLimit = require('express-rate-limit');
const cors = require('cors');
const passport = require('passport');
@@ -21,6 +15,9 @@ assert.ok(process.env.JAMBONES_MYSQL_HOST &&
process.env.JAMBONES_MYSQL_DATABASE, 'missing JAMBONES_MYSQL_XXX env vars');
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');
const {
queryCdrs,
queryCdrsSP,
@@ -36,13 +33,18 @@ const {
retrieveCall,
deleteCall,
listCalls,
listQueues,
purgeCalls,
retrieveSet,
addKey,
retrieveKey,
deleteKey
} = require('@jambonz/realtimedb-helpers')({
host: process.env.JAMBONES_REDIS_HOST || 'localhost',
deleteKey,
incrKey
} = require('./lib/helpers/realtimedb-helpers');
const {
getTtsVoices
} = require('@jambonz/speech-utils')({
host: process.env.JAMBONES_REDIS_HOST,
port: process.env.JAMBONES_REDIS_PORT || 6379
}, logger);
const {
@@ -63,6 +65,7 @@ const {
}, logger);
const PORT = process.env.HTTP_PORT || 3000;
const authStrategy = require('./lib/auth')(logger, retrieveKey);
const {delayLoginMiddleware} = require('./lib/middleware');
passport.use(authStrategy);
@@ -73,11 +76,14 @@ app.locals = {
retrieveCall,
deleteCall,
listCalls,
listQueues,
purgeCalls,
retrieveSet,
addKey,
incrKey,
retrieveKey,
deleteKey,
getTtsVoices,
lookupAppBySid,
lookupAccountBySid,
lookupAccountByPhoneNumber,
@@ -122,9 +128,11 @@ if (process.env.JAMBONES_TRUST_PROXY) {
app.use(limiter);
app.use(helmet());
app.use(helmet.hidePoweredBy());
app.use(nocache());
app.use(passport.initialize());
app.use(cors());
app.use(express.urlencoded({extended: true}));
app.use(delayLoginMiddleware);
app.use(unless(['/stripe'], express.json()));
app.use('/v1', unless(
[

View File

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

10
db/create-admin-user.sql Normal file
View File

@@ -0,0 +1,10 @@
/* hashed password is "admin" */
insert into users (user_sid, name, email, hashed_password, force_change, provider, email_validated)
values ('12c80508-edf9-4b22-8d09-55abd02648eb', 'admin', 'joe@foo.bar', '$argon2i$v=19$m=65536,t=3,p=4$c2FsdHNhbHRzYWx0c2FsdA$x5OO6gXFXS25oqUU2JvbYqrSgRxBujNUJBq6xv9EgjM', 1, 'local', 1);
insert into user_permissions (user_permissions_sid, user_sid, permission_sid)
values ('8919e0dc-4d69-4de5-be56-a121598d9093', '12c80508-edf9-4b22-8d09-55abd02648eb', 'ffbc342a-546a-11ed-bdc3-0242ac120002');
insert into user_permissions (user_permissions_sid, user_sid, permission_sid)
values ('d6fdf064-0a65-4b17-8b10-5500e956a159', '12c80508-edf9-4b22-8d09-55abd02648eb', 'ffbc3a10-546a-11ed-bdc3-0242ac120002');
insert into user_permissions (user_permissions_sid, user_sid, permission_sid)
values ('f68185dd-0486-4767-a77d-a0b84c1b236e' ,'12c80508-edf9-4b22-8d09-55abd02648eb', 'ffbc3c5e-546a-11ed-bdc3-0242ac120002');

View File

@@ -16,12 +16,18 @@ DROP TABLE IF EXISTS call_routes;
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;
DROP TABLE IF EXISTS password_settings;
DROP TABLE IF EXISTS user_permissions;
DROP TABLE IF EXISTS permissions;
DROP TABLE IF EXISTS predefined_sip_gateways;
DROP TABLE IF EXISTS predefined_smpp_gateways;
@@ -48,6 +54,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;
@@ -79,7 +87,7 @@ CREATE TABLE account_limits
(
account_limits_sid CHAR(36) NOT NULL UNIQUE ,
account_sid CHAR(36) NOT NULL,
category ENUM('api_rate','voice_call_session', 'device') NOT NULL,
category ENUM('api_rate','voice_call_session', 'device','voice_call_minutes','voice_call_session_license', 'voice_call_minutes_license') NOT NULL,
quantity INTEGER NOT NULL,
PRIMARY KEY (account_limits_sid)
);
@@ -132,11 +140,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 pat';
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 ';
CREATE TABLE password_settings
(
@@ -145,6 +165,14 @@ require_digit BOOLEAN NOT NULL DEFAULT false,
require_special_character BOOLEAN NOT NULL DEFAULT false
);
CREATE TABLE permissions
(
permission_sid CHAR(36) NOT NULL UNIQUE ,
name VARCHAR(32) NOT NULL UNIQUE ,
description VARCHAR(255),
PRIMARY KEY (permission_sid)
);
CREATE TABLE predefined_carriers
(
predefined_carrier_sid CHAR(36) NOT NULL UNIQUE ,
@@ -236,7 +264,10 @@ 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)
);
@@ -254,7 +285,7 @@ CREATE TABLE service_provider_limits
(
service_provider_limits_sid CHAR(36) NOT NULL UNIQUE ,
service_provider_sid CHAR(36) NOT NULL,
category ENUM('api_rate','voice_call_session', 'device') NOT NULL,
category ENUM('api_rate','voice_call_session', 'device','voice_call_minutes','voice_call_session_license', 'voice_call_minutes_license') NOT NULL,
quantity INTEGER NOT NULL,
PRIMARY KEY (service_provider_limits_sid)
);
@@ -295,6 +326,13 @@ created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
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 ,
@@ -345,9 +383,18 @@ smpp_inbound_password VARCHAR(64),
register_from_user VARCHAR(128),
register_from_domain VARCHAR(255),
register_public_ip_in_contact BOOLEAN NOT NULL DEFAULT false,
register_status VARCHAR(4096),
PRIMARY KEY (voip_carrier_sid)
) COMMENT='A Carrier or customer PBX that can send or receive calls';
CREATE TABLE user_permissions
(
user_permissions_sid CHAR(36) NOT NULL UNIQUE ,
user_sid CHAR(36) NOT NULL,
permission_sid CHAR(36) NOT NULL,
PRIMARY KEY (user_permissions_sid)
);
CREATE TABLE smpp_gateways
(
smpp_gateway_sid CHAR(36) NOT NULL UNIQUE ,
@@ -365,7 +412,7 @@ PRIMARY KEY (smpp_gateway_sid)
CREATE TABLE phone_numbers
(
phone_number_sid CHAR(36) UNIQUE ,
number VARCHAR(32) NOT NULL UNIQUE ,
number VARCHAR(132) NOT NULL UNIQUE ,
voip_carrier_sid CHAR(36),
account_sid CHAR(36),
application_sid CHAR(36),
@@ -415,6 +462,7 @@ account_sid CHAR(36) COMMENT 'account that this application belongs to (if null,
call_hook_sid CHAR(36) COMMENT 'webhook to call for inbound calls ',
call_status_hook_sid CHAR(36) COMMENT 'webhook to call for call status events',
messaging_hook_sid CHAR(36) COMMENT 'webhook to call for inbound SMS/MMS ',
app_json TEXT,
speech_synthesis_vendor VARCHAR(64) NOT NULL DEFAULT 'google',
speech_synthesis_language VARCHAR(12) NOT NULL DEFAULT 'en-US',
speech_synthesis_voice VARCHAR(64),
@@ -481,6 +529,15 @@ ALTER TABLE call_routes ADD FOREIGN KEY application_sid_idxfk (application_sid)
CREATE INDEX dns_record_sid_idx ON dns_records (dns_record_sid);
ALTER TABLE dns_records ADD FOREIGN KEY account_sid_idxfk_4 (account_sid) REFERENCES accounts (account_sid);
CREATE INDEX 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);
CREATE INDEX predefined_carrier_sid_idx ON predefined_sip_gateways (predefined_carrier_sid);
@@ -561,6 +618,12 @@ ALTER TABLE voip_carriers ADD FOREIGN KEY service_provider_sid_idxfk_7 (service_
ALTER TABLE voip_carriers ADD FOREIGN KEY application_sid_idxfk_2 (application_sid) REFERENCES applications (application_sid);
CREATE INDEX user_permissions_sid_idx ON user_permissions (user_permissions_sid);
CREATE INDEX user_sid_idx ON user_permissions (user_sid);
ALTER TABLE user_permissions ADD FOREIGN KEY user_sid_idxfk (user_sid) REFERENCES users (user_sid) ON DELETE CASCADE;
ALTER TABLE user_permissions ADD FOREIGN KEY permission_sid_idxfk (permission_sid) REFERENCES permissions (permission_sid);
CREATE INDEX smpp_gateway_sid_idx ON smpp_gateways (smpp_gateway_sid);
CREATE INDEX voip_carrier_sid_idx ON smpp_gateways (voip_carrier_sid);
ALTER TABLE smpp_gateways ADD FOREIGN KEY voip_carrier_sid_idxfk (voip_carrier_sid) REFERENCES voip_carriers (voip_carrier_sid);

File diff suppressed because one or more lines are too long

View File

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

View File

@@ -1,5 +1,12 @@
SET FOREIGN_KEY_CHECKS=0;
-- create standard permissions
insert into permissions (permission_sid, name, description)
values
('ffbc342a-546a-11ed-bdc3-0242ac120002', 'VIEW_ONLY', 'Can view data but not make changes'),
('ffbc3a10-546a-11ed-bdc3-0242ac120002', 'PROVISION_SERVICES', 'Can provision services'),
('ffbc3c5e-546a-11ed-bdc3-0242ac120002', 'PROVISION_USERS', 'Can provision users');
insert into sbc_addresses (sbc_address_sid, ipv4, port)
values('f6567ae1-bf97-49af-8931-ca014b689995', '52.55.111.178', 5060);
insert into sbc_addresses (sbc_address_sid, ipv4, port)
@@ -20,6 +27,14 @@ values ('2708b1b3-2736-40ea-b502-c53d8396247f', 'default service provider', 'sip
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');
-- create account level api key
insert into api_keys (api_key_sid, token, service_provider_sid)
values ('3f35518f-5a0d-4c2e-90a5-2407bb3b36fa', '38700987-c7a4-4685-a5bb-af378f9734da', '9351f46a-678c-43f5-b8a6-d4eb58d131af');
-- create SP level api key
insert into api_keys (api_key_sid, token, account_sid)
values ('3f35518f-5a0d-4c2e-90a5-2407bb3b36fs', '38700987-c7a4-4685-a5bb-af378f9734ds', '2708b1b3-2736-40ea-b502-c53d8396247f');
-- create two applications
insert into webhooks(webhook_sid, url, method)
values
@@ -72,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
@@ -91,10 +107,8 @@ VALUES
('91cb050f-9826-4ac9-b736-84a10372a9fe', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '178.22.139.77', 32, 5060, 1, 0),
('58700fad-98bf-4d31-b61e-888c54911b35', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '185.63.140.77', 32, 5060, 1, 0),
('d020fd9e-7fdb-4bca-ae0d-e61b38142873', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '185.63.142.77', 32, 5060, 1, 0),
('441fd2e7-c845-459c-963d-6e917063ed9a', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '185.63.141.77', 32, 5060, 1, 0),
('1e0d3e80-9973-4184-9bec-07ae564f983f', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '185.63.143.77', 32, 5060, 1, 0),
('e56ec745-5f37-443f-afb4-7bbda31ae7ac', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '178.22.140.34', 32, 5060, 1, 0),
('e7447e7e-2c7d-4738-ab53-097c187236ff', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '178.22.143.66', 32, 5060, 1, 0),
('38d8520a-527f-4f8e-8456-f9dfca742561', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '172.86.225.77', 32, 5060, 1, 0),
('834f8b0c-d4c2-4f3e-93d9-cf307995eedd', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '172.86.225.88', 32, 5060, 1, 0),
('5f431d42-48e4-44ce-a311-d946f0b475b6', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', 'out.simwood.com', 32, 5060, 0, 1);

View File

@@ -1,5 +1,12 @@
SET FOREIGN_KEY_CHECKS=0;
-- create standard permissions
insert into permissions (permission_sid, name, description)
values
('ffbc342a-546a-11ed-bdc3-0242ac120002', 'VIEW_ONLY', 'Can view data but not make changes'),
('ffbc3a10-546a-11ed-bdc3-0242ac120002', 'PROVISION_SERVICES', 'Can provision services'),
('ffbc3c5e-546a-11ed-bdc3-0242ac120002', 'PROVISION_USERS', 'Can provision users');
-- create one service provider and account
insert into api_keys (api_key_sid, token)
values ('3f35518f-5a0d-4c2e-90a5-2407bb3b36f0', '38700987-c7a4-4685-a5bb-af378f9734de');
@@ -61,11 +68,15 @@ 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
@@ -85,10 +96,8 @@ VALUES
('91cb050f-9826-4ac9-b736-84a10372a9fe', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '178.22.139.77', 32, 5060, 1, 0),
('58700fad-98bf-4d31-b61e-888c54911b35', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '185.63.140.77', 32, 5060, 1, 0),
('d020fd9e-7fdb-4bca-ae0d-e61b38142873', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '185.63.142.77', 32, 5060, 1, 0),
('441fd2e7-c845-459c-963d-6e917063ed9a', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '185.63.141.77', 32, 5060, 1, 0),
('1e0d3e80-9973-4184-9bec-07ae564f983f', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '185.63.143.77', 32, 5060, 1, 0),
('e56ec745-5f37-443f-afb4-7bbda31ae7ac', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '178.22.140.34', 32, 5060, 1, 0),
('e7447e7e-2c7d-4738-ab53-097c187236ff', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '178.22.143.66', 32, 5060, 1, 0),
('38d8520a-527f-4f8e-8456-f9dfca742561', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '172.86.225.77', 32, 5060, 1, 0),
('834f8b0c-d4c2-4f3e-93d9-cf307995eedd', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '172.86.225.88', 32, 5060, 1, 0),
('5f431d42-48e4-44ce-a311-d946f0b475b6', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', 'out.simwood.com', 32, 5060, 0, 1);
SET FOREIGN_KEY_CHECKS=1;

View File

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

View File

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

View File

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

View File

@@ -1,54 +1,56 @@
const Strategy = require('passport-http-bearer').Strategy;
const {getMysqlConnection} = require('../db');
const {hashString} = require('../utils/password-utils');
const debug = require('debug')('jambonz:api-server');
const {cacheClient} = require('../helpers');
const jwt = require('jsonwebtoken');
const sql = `
SELECT *
FROM api_keys
WHERE api_keys.token = ?`;
function makeStrategy(logger, retrieveKey) {
function makeStrategy(logger) {
return new Strategy(
async function(token, done) {
logger.debug(`validating with token ${token}`);
jwt.verify(token, process.env.JWT_SECRET, async(err, decoded) => {
if (err) {
if (err.name === 'TokenExpiredError') {
logger.debug('jwt expired');
return done(null, false);
}
/* its not a jwt obtained through login, check api leys */
/* its not a jwt obtained through login, check api keys */
checkApiTokens(logger, token, done);
}
else {
/* validated -- make sure it is not on blacklist */
try {
const s = `jwt:${hashString(token)}`;
const result = await retrieveKey(s);
if (result) {
debug(`result from searching for ${s}: ${result}`);
const {user_sid} = decoded;
/* Valid jwt tokens are stored in redis by hashed user_id */
const redisKey = cacheClient.generateRedisKey('jwt', user_sid, 'v2');
const result = await cacheClient.get(redisKey);
if (result === null) {
debug(`result from searching for ${redisKey}: ${result}`);
logger.info('jwt invalidated after logout');
return done(null, false);
}
} catch (err) {
} catch (error) {
debug(err);
logger.info({err}, 'Error checking blacklist for jwt');
logger.info({err}, 'Error checking redis for jwt');
}
const {user_sid, account_sid, email, name} = decoded;
//logger.debug({user_sid, account_sid}, 'successfully validated jwt');
const scope = ['account'];
const { user_sid, service_provider_sid, account_sid, email, name, scope, permissions } = decoded;
const user = {
service_provider_sid,
account_sid,
user_sid,
jwt: token,
email,
name,
hasScope: (s) => s === 'account',
hasAdminAuth: false,
hasServiceProviderAuth: false,
hasAccountAuth: true
permissions,
hasScope: (s) => s === scope,
hasAdminAuth: scope === 'admin',
hasServiceProviderAuth: scope === 'service_provider',
hasAccountAuth: scope === 'account'
};
logger.debug({user}, 'successfully validated jwt');
return done(null, user, {scope});
}
});
@@ -75,26 +77,30 @@ const checkApiTokens = (logger, token, done) => {
}
// found api key
const scope = [];
let scope;
//const scope = [];
if (results[0].account_sid === null && results[0].service_provider_sid === null) {
scope.push.apply(scope, ['admin', 'service_provider', 'account']);
//scope.push.apply(scope, ['admin', 'service_provider', 'account']);
scope = 'admin';
}
else if (results[0].service_provider_sid) {
scope.push.apply(scope, ['service_provider', 'account']);
//scope.push.apply(scope, ['service_provider', 'account']);
scope = 'service_provider';
}
else {
scope.push('account');
//scope.push('account');
scope = 'account';
}
const user = {
account_sid: results[0].account_sid,
service_provider_sid: results[0].service_provider_sid,
hasScope: (s) => scope.includes(s),
hasAdminAuth: scope.length === 3,
hasServiceProviderAuth: scope.includes('service_provider'),
hasAccountAuth: scope.includes('account') && !scope.includes('service_provider')
hasScope: (s) => s === scope,
hasAdminAuth: scope === 'admin',
hasServiceProviderAuth: scope === 'service_provider',
hasAccountAuth: scope === 'account'
};
logger.info(user, `successfully validated with scope ${scope}`);
logger.debug({user}, `successfully validated with scope ${scope}`);
return done(null, user, {scope});
});
});

View File

@@ -0,0 +1,82 @@
const {
addKey: addKeyRedis,
deleteKey: deleteKeyRedis,
retrieveKey: retrieveKeyRedis,
} = require('./realtimedb-helpers');
const { hashString } = require('../utils/password-utils');
const logger = require('../logger');
class CacheClient {
constructor() { }
async set(params) {
const {
redisKey,
value = '1',
time = 3600,
} = params || {};
try {
await addKeyRedis(redisKey, value, time);
} catch (err) {
logger.error('CacheClient.get set', {
error: {
message: err.message,
name: err.name
},
...params
});
}
}
async get(redisKey) {
try {
const result = await retrieveKeyRedis(redisKey);
return result;
} catch (err) {
logger.error('CacheClient.get error', {
error: {
message: err.message,
name: err.name
},
redisKey
});
}
}
async delete(key) {
try {
await deleteKeyRedis(key);
logger.debug('CacheClient.delete key from redis', { key });
} catch (err) {
logger.error('CacheClient.delete error', {
error: {
message: err.message,
name: err.name
},
key
});
}
}
generateRedisKey(type, key, version) {
let suffix = '';
if (version) {
suffix = `:version:${version}`;
}
switch (type) {
case 'reset-link':
return `reset-link:${key}`;
case 'jwt':
default:
return `jwt:${hashString(key)}${suffix}`;
}
}
}
const cacheClient = new CacheClient();
module.exports = { cacheClient };

4
lib/helpers/index.js Normal file
View File

@@ -0,0 +1,4 @@
module.exports = {
...require('./cache-client'),
...require('./realtimedb-helpers'),
};

View File

@@ -0,0 +1,32 @@
const logger = require('../logger');
const {
retrieveCall,
deleteCall,
listCalls,
listQueues,
purgeCalls,
retrieveSet,
addKey,
retrieveKey,
deleteKey,
incrKey,
client: redisClient,
} = require('@jambonz/realtimedb-helpers')({
host: process.env.JAMBONES_REDIS_HOST || 'localhost',
port: process.env.JAMBONES_REDIS_PORT || 6379
}, logger);
module.exports = {
retrieveCall,
deleteCall,
listCalls,
listQueues,
purgeCalls,
retrieveSet,
addKey,
retrieveKey,
deleteKey,
redisClient,
incrKey
};

11
lib/logger.js Normal file
View File

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

32
lib/middleware.js Normal file
View File

@@ -0,0 +1,32 @@
const logger = require('./logger');
function delayLoginMiddleware(req, res, next) {
if (req.path.includes('/login') || req.path.includes('/signin')) {
const min = 200;
const max = 1000;
/* Random delay between 200 - 1000ms */
const sendStatusDelay = Math.floor(Math.random() * (max - min + 1)) + min;
/* the res.json take longer, we decrease the max delay slightly to 0-800ms */
const jsonDelay = Math.floor(Math.random() * 800);
logger.debug(`delayLoginMiddleware: sendStatus ${sendStatusDelay} - json ${jsonDelay}`);
const sendStatus = res.sendStatus;
const json = res.json;
res.sendStatus = function(status) {
setTimeout(() => {
sendStatus.call(res, status);
}, sendStatusDelay);
};
res.json = function(body) {
setTimeout(() => {
json.call(res, body);
}, jsonDelay);
};
}
next();
}
module.exports = {
delayLoginMiddleware
};

View File

@@ -318,6 +318,10 @@ Account.fields = [
name: 'siprec_hook_sid',
type: 'string',
},
{
name: 'lcr_sid',
type: 'string'
}
];
module.exports = Account;

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

@@ -107,7 +107,7 @@ class Model extends Emitter {
if (pk.name in obj) throw new DbErrorBadRequest(`primary key ${pk.name} is immutable`);
getMysqlConnection((err, conn) => {
if (err) return reject(err);
conn.query(`UPDATE ${this.table} SET ? WHERE ${pk.name} = '${sid}'`, obj, (err, results, fields) => {
conn.query(`UPDATE ${this.table} SET ? WHERE ${pk.name} = ?`, [obj, sid], (err, results, fields) => {
conn.release();
if (err) return reject(err);
resolve(results.affectedRows);

View File

@@ -0,0 +1,70 @@
const {promisePool} = require('../db');
class PasswordSettings {
/**
* Retrieve object from database
*/
static async retrieve() {
const [r] = await promisePool.execute(`SELECT * FROM ${this.table}`);
return r;
}
/**
* Update object into the database
*/
static async update(obj) {
let sql = `UPDATE ${this.table} SET `;
const values = [];
const keys = Object.keys(obj);
this.fields.forEach(({name}) => {
if (keys.includes(name)) {
sql = sql + `${name} = ?,`;
values.push(obj[name]);
}
});
if (values.length) {
sql = sql.slice(0, -1);
await promisePool.execute(sql, values);
}
}
/**
* insert object into the database
*/
static async make(obj) {
let params = '', marks = '';
const values = [];
const keys = Object.keys(obj);
this.fields.forEach(({name}) => {
if (keys.includes(name)) {
params = params + `${name},`;
marks = marks + '?,';
values.push(obj[name]);
}
});
if (values.length) {
params = `(${params.slice(0, -1)})`;
marks = `values(${marks.slice(0, -1)})`;
return await promisePool.execute(`INSERT into ${this.table} ${params} ${marks}`, values);
}
}
}
PasswordSettings.table = 'password_settings';
PasswordSettings.fields = [
{
name: 'min_password_length',
type: 'number'
},
{
name: 'require_digit',
type: 'number'
},
{
name: 'require_special_character',
type: 'number'
}
];
module.exports = PasswordSettings;

View File

@@ -89,6 +89,10 @@ ServiceProvider.fields = [
{
name: 'ms_teams_fqdn',
type: 'string',
},
{
name: 'lcr_sid',
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;

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

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

View File

@@ -11,10 +11,16 @@ class VoipCarrier extends Model {
static async retrieveAll(account_sid) {
if (!account_sid) return super.retrieveAll();
const [rows] = await promisePool.query(retrieveSql, account_sid);
if (rows) {
rows.map((r) => r.register_status = JSON.parse(r.register_status || '{}'));
}
return rows;
}
static async retrieveAllForSP(service_provider_sid) {
const [rows] = await promisePool.query(retrieveSqlForSP, service_provider_sid);
if (rows) {
rows.map((r) => r.register_status = JSON.parse(r.register_status || '{}'));
}
return rows;
}
}
@@ -122,6 +128,10 @@ VoipCarrier.fields = [
{
name: 'register_public_ip_in_contact',
type: 'number'
},
{
name: 'register_status',
type: 'string'
}
];

View File

@@ -1,6 +1,6 @@
const router = require('express').Router();
const request = require('request');
const {DbErrorBadRequest, DbErrorUnprocessableRequest} = require('../../utils/errors');
const {DbErrorBadRequest, DbErrorForbidden, DbErrorUnprocessableRequest} = require('../../utils/errors');
const Account = require('../../models/account');
const Application = require('../../models/application');
const Webhook = require('../../models/webhook');
@@ -12,7 +12,14 @@ const { v4: uuidv4 } = require('uuid');
const snakeCase = require('../../utils/snake-case');
const sysError = require('../error');
const {promisePool} = require('../../db');
const {hasAccountPermissions, parseAccountSid, enableSubspace, disableSubspace} = require('./utils');
const {
hasAccountPermissions,
parseAccountSid,
parseCallSid,
enableSubspace,
disableSubspace,
parseVoipCarrierSid
} = require('./utils');
const short = require('short-uuid');
const VoipCarrier = require('../../models/voip-carrier');
const translator = short();
@@ -45,6 +52,36 @@ const stripPort = (hostport) => {
return hostport;
};
const validateRequest = async(req, account_sid) => {
try {
if (req.user.hasScope('admin')) {
return;
}
if (req.user.hasScope('account')) {
if (account_sid === req.user.account_sid) {
return;
}
throw new DbErrorForbidden('insufficient permissions');
}
if (req.user.hasScope('service_provider')) {
const [r] = await promisePool.execute(
'SELECT service_provider_sid from accounts WHERE account_sid = ?', [account_sid]
);
if (r.length === 1 && r[0].service_provider_sid === req.user.service_provider_sid) {
return;
}
throw new DbErrorForbidden('insufficient permissions');
}
} catch (error) {
throw error;
}
};
router.use('/:sid/SpeechCredentials', hasAccountPermissions, require('./speech-credentials'));
router.use('/:sid/RecentCalls', hasAccountPermissions, require('./recent-calls'));
router.use('/:sid/Alerts', hasAccountPermissions, require('./alerts'));
@@ -56,6 +93,7 @@ router.get('/:sid/Applications', async(req, res) => {
const logger = req.app.locals.logger;
try {
const account_sid = parseAccountSid(req);
await validateRequest(req, account_sid);
const results = await Application.retrieveAll(null, account_sid);
res.status(200).json(results);
} catch (err) {
@@ -66,17 +104,39 @@ router.get('/:sid/VoipCarriers', async(req, res) => {
const logger = req.app.locals.logger;
try {
const account_sid = parseAccountSid(req);
await validateRequest(req, account_sid);
const results = await VoipCarrier.retrieveAll(account_sid);
res.status(200).json(results);
} catch (err) {
sysError(logger, res, err);
}
});
router.put('/:sid/VoipCarriers/:voip_carrier_sid', async(req, res) => {
const logger = req.app.locals.logger;
try {
const sid = parseVoipCarrierSid(req);
const account_sid = parseAccountSid(req);
await validateRequest(req, account_sid);
const rowsAffected = await VoipCarrier.update(sid, req.body);
if (rowsAffected === 0) {
return res.sendStatus(404);
}
return res.status(204).end();
} catch (err) {
sysError(logger, res, err);
}
});
router.post('/:sid/VoipCarriers', async(req, res) => {
const logger = req.app.locals.logger;
const payload = req.body;
try {
const account_sid = parseAccountSid(req);
await validateRequest(req, account_sid);
logger.debug({payload}, 'POST /:sid/VoipCarriers');
const uuid = await VoipCarrier.make({
account_sid,
@@ -186,6 +246,7 @@ function validateTo(to) {
}
throw new DbErrorBadRequest(`missing or invalid to property: ${JSON.stringify(to)}`);
}
async function validateCreateCall(logger, sid, req) {
const {lookupAppBySid} = req.app.locals;
const obj = req.body;
@@ -286,7 +347,7 @@ async function validateCreateMessage(logger, sid, req) {
async function validateAdd(req) {
/* account-level token can not be used to add accounts */
if (req.user.hasAccountAuth) {
throw new DbErrorUnprocessableRequest('insufficient permissions to create accounts');
throw new DbErrorForbidden('insufficient permissions');
}
if (req.user.hasServiceProviderAuth && req.user.service_provider_sid) {
/* service providers can only create accounts under themselves */
@@ -305,9 +366,10 @@ async function validateAdd(req) {
throw new DbErrorBadRequest('\'queue_event_hook\' must be an object when adding an account');
}
}
async function validateUpdate(req, sid) {
if (req.user.hasAccountAuth && req.user.account_sid !== sid) {
throw new DbErrorUnprocessableRequest('insufficient privileges to update this account');
throw new DbErrorForbidden('insufficient privileges');
}
if (req.user.hasAccountAuth && req.body.sip_realm) {
throw new DbErrorBadRequest('use POST /Accounts/:sid/sip_realm/:realm to set or change the sip realm');
@@ -315,12 +377,23 @@ async function validateUpdate(req, sid) {
if (req.user.service_provider_sid && !req.user.hasScope('admin')) {
const result = await Account.retrieve(sid);
if (!result || result.length === 0) {
throw new DbErrorBadRequest(`account not found for sid ${sid}`);
}
if (result[0].service_provider_sid !== req.user.service_provider_sid) {
throw new DbErrorUnprocessableRequest('cannot update account from different service provider');
throw new DbErrorForbidden('insufficient privileges');
}
}
if (req.user.hasScope('admin')) {
/* check to be sure that the account_sid exists */
const result = await Account.retrieve(sid);
if (!result || result.length === 0) {
throw new DbErrorBadRequest(`account not found for sid ${sid}`);
}
}
if (req.body.service_provider_sid) throw new DbErrorBadRequest('service_provider_sid may not be modified');
}
async function validateDelete(req, sid) {
if (req.user.hasAccountAuth && req.user.account_sid !== sid) {
throw new DbErrorUnprocessableRequest('insufficient privileges to update this account');
@@ -328,12 +401,11 @@ async function validateDelete(req, sid) {
if (req.user.service_provider_sid && !req.user.hasScope('admin')) {
const result = await Account.retrieve(sid);
if (result[0].service_provider_sid !== req.user.service_provider_sid) {
throw new DbErrorUnprocessableRequest('cannot delete account from different service provider');
throw new DbErrorForbidden('insufficient privileges');
}
}
}
/* add */
router.post('/', async(req, res) => {
const logger = req.app.locals.logger;
@@ -375,8 +447,11 @@ router.get('/', async(req, res) => {
router.get('/:sid', async(req, res) => {
const logger = req.app.locals.logger;
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(req.params.sid, service_provider_sid);
const results = await Account.retrieve(account_sid, service_provider_sid);
if (results.length === 0) return res.status(404).end();
return res.status(200).json(results[0]);
}
@@ -388,13 +463,15 @@ router.get('/:sid', async(req, res) => {
router.get('/:sid/WebhookSecret', async(req, res) => {
const logger = req.app.locals.logger;
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(req.params.sid, service_provider_sid);
const results = await Account.retrieve(account_sid, service_provider_sid);
if (results.length === 0) return res.status(404).end();
let {webhook_secret} = results[0];
if (req.query.regenerate) {
const secret = `wh_secret_${translator.generate()}`;
await Account.update(req.params.sid, {webhook_secret: secret});
await Account.update(account_sid, {webhook_secret: secret});
webhook_secret = secret;
}
return res.status(200).json({webhook_secret});
@@ -407,8 +484,9 @@ router.get('/:sid/WebhookSecret', async(req, res) => {
router.post('/:sid/SubspaceTeleport', async(req, res) => {
const logger = req.app.locals.logger;
try {
const account_sid = parseAccountSid(req);
const service_provider_sid = req.user.hasServiceProviderAuth ? req.user.service_provider_sid : null;
const results = await Account.retrieve(req.params.sid, service_provider_sid);
const results = await Account.retrieve(account_sid, service_provider_sid);
if (results.length === 0) return res.status(404).end();
const {subspace_client_id, subspace_client_secret} = results[0];
const {destination} = req.body;
@@ -421,7 +499,7 @@ router.post('/:sid/SubspaceTeleport', async(req, res) => {
destination: dest
});
logger.info({destination, teleport}, 'SubspaceTeleport - create teleport');
await Account.update(req.params.sid, {
await Account.update(account_sid, {
subspace_sip_teleport_id: teleport.id,
subspace_sip_teleport_destinations: JSON.stringify(teleport.teleport_entry_points)//hacky
});
@@ -439,13 +517,14 @@ router.post('/:sid/SubspaceTeleport', async(req, res) => {
router.delete('/:sid/SubspaceTeleport', async(req, res) => {
const logger = req.app.locals.logger;
try {
const account_sid = parseAccountSid(req);
const service_provider_sid = req.user.hasServiceProviderAuth ? req.user.service_provider_sid : null;
const results = await Account.retrieve(req.params.sid, service_provider_sid);
const results = await Account.retrieve(account_sid, service_provider_sid);
if (results.length === 0) return res.status(404).end();
const {subspace_client_id, subspace_client_secret, subspace_sip_teleport_id} = results[0];
await disableSubspace({subspace_client_id, subspace_client_secret, subspace_sip_teleport_id});
await Account.update(req.params.sid, {
await Account.update(account_sid, {
subspace_sip_teleport_id: null,
subspace_sip_teleport_destinations: null
});
@@ -456,11 +535,14 @@ router.delete('/:sid/SubspaceTeleport', async(req, res) => {
}
});
/* update */
/**
* update
*/
router.put('/:sid', async(req, res) => {
const sid = req.params.sid;
const logger = req.app.locals.logger;
try {
const sid = parseAccountSid(req);
await validateRequest(req, sid);
// create webhooks if provided
const obj = Object.assign({}, req.body);
@@ -520,12 +602,14 @@ router.put('/:sid', async(req, res) => {
/* delete */
router.delete('/:sid', async(req, res) => {
const sid = req.params.sid;
const logger = req.app.locals.logger;
const sqlDeleteGateways = `DELETE from sip_gateways
WHERE voip_carrier_sid IN
(SELECT voip_carrier_sid from voip_carriers where account_sid = ?)`;
try {
const sid = parseAccountSid(req);
await validateRequest(req, sid);
await validateDelete(req, sid);
const [account] = await promisePool.query('SELECT * FROM accounts WHERE account_sid = ?', sid);
@@ -589,13 +673,19 @@ account_subscriptions WHERE account_sid = ?)
}
});
/* retrieve account level api keys */
/**
* retrieve account level api keys
*/
router.get('/:sid/ApiKeys', async(req, res) => {
const logger = req.app.locals.logger;
try {
const results = await ApiKey.retrieveAll(req.params.sid);
const sid = parseAccountSid(req);
await validateRequest(req, sid);
const results = await ApiKey.retrieveAll(sid);
res.status(200).json(results);
updateLastUsed(logger, req.params.sid, req).catch((err) => {});
updateLastUsed(logger, sid, req).catch((err) => {});
} catch (err) {
sysError(logger, res, err);
}
@@ -605,36 +695,40 @@ router.get('/:sid/ApiKeys', async(req, res) => {
* create a new Call
*/
router.post('/:sid/Calls', async(req, res) => {
const sid = req.params.sid;
const setName = `${(process.env.JAMBONES_CLUSTER_ID || 'default')}:active-fs`;
const {retrieveSet, logger} = req.app.locals;
const setName = `${(process.env.JAMBONES_CLUSTER_ID || 'default')}:active-fs`;
const serviceUrl = await getFsUrl(logger, retrieveSet, setName);
if (!serviceUrl) {
res.status(480).json({msg: 'no available feature servers at this time'});
} else {
try {
await validateCreateCall(logger, sid, req);
updateLastUsed(logger, sid, req).catch((err) => {});
request({
url: serviceUrl,
method: 'POST',
json: true,
body: Object.assign(req.body, {account_sid: sid})
}, (err, response, body) => {
if (err) {
logger.error(err, `Error sending createCall POST to ${serviceUrl}`);
return res.sendStatus(500);
}
if (response.statusCode !== 201) {
logger.error({statusCode: response.statusCode}, `Non-success response returned by createCall ${serviceUrl}`);
return res.sendStatus(500);
}
res.status(201).json(body);
});
} catch (err) {
sysError(logger, res, err);
}
return res.status(480).json({msg: 'no available feature servers at this time'});
}
try {
const sid = parseAccountSid(req);
await validateRequest(req, sid);
await validateCreateCall(logger, sid, req);
updateLastUsed(logger, sid, req).catch((err) => {});
request({
url: serviceUrl,
method: 'POST',
json: true,
body: Object.assign(req.body, {account_sid: sid})
}, (err, response, body) => {
if (err) {
logger.error(err, `Error sending createCall POST to ${serviceUrl}`);
return res.sendStatus(500);
}
if (response.statusCode !== 201) {
logger.error({statusCode: response.statusCode}, `Non-success response returned by createCall ${serviceUrl}`);
return res.sendStatus(500);
}
return res.status(201).json(body);
});
} catch (err) {
sysError(logger, res, err);
}
});
@@ -642,11 +736,18 @@ router.post('/:sid/Calls', async(req, res) => {
* retrieve info for a group of calls under an account
*/
router.get('/:sid/Calls', async(req, res) => {
const accountSid = req.params.sid;
const {logger, listCalls} = req.app.locals;
const {direction, from, to, callStatus} = req.query || {};
try {
const calls = await listCalls(accountSid);
const accountSid = parseAccountSid(req);
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) => {});
@@ -659,11 +760,12 @@ router.get('/:sid/Calls', async(req, res) => {
* retrieve single call
*/
router.get('/:sid/Calls/:callSid', async(req, res) => {
const accountSid = req.params.sid;
const callSid = req.params.callSid;
const {logger, retrieveCall} = req.app.locals;
try {
const accountSid = parseAccountSid(req);
await validateRequest(req, accountSid);
const callSid = parseCallSid(req);
const callInfo = await retrieveCall(accountSid, callSid);
if (callInfo) {
logger.debug(callInfo, `retrieved call info for call sid ${callSid}`);
@@ -683,11 +785,12 @@ router.get('/:sid/Calls/:callSid', async(req, res) => {
* delete call
*/
router.delete('/:sid/Calls/:callSid', async(req, res) => {
const accountSid = req.params.sid;
const callSid = req.params.callSid;
const {logger, deleteCall} = req.app.locals;
try {
const accountSid = parseAccountSid(req);
await validateRequest(req, accountSid);
const callSid = parseCallSid(req);
const result = await deleteCall(accountSid, callSid);
if (result) {
logger.debug(`successfully deleted call ${callSid}`);
@@ -707,11 +810,12 @@ router.delete('/:sid/Calls/:callSid', async(req, res) => {
* update a call
*/
const updateCall = async(req, res) => {
const accountSid = req.params.sid;
const callSid = req.params.callSid;
const {logger, retrieveCall} = req.app.locals;
try {
const accountSid = parseAccountSid(req);
await validateRequest(req, accountSid);
const callSid = parseCallSid(req);
validateUpdateCall(req.body);
const call = await retrieveCall(accountSid, callSid);
if (call) {
@@ -738,6 +842,7 @@ const updateCall = async(req, res) => {
router.post('/:sid/Calls/:callSid', async(req, res) => {
await updateCall(req, res);
});
router.put('/:sid/Calls/:callSid', async(req, res) => {
await updateCall(req, res);
});
@@ -746,11 +851,13 @@ router.put('/:sid/Calls/:callSid', async(req, res) => {
* create a new Message
*/
router.post('/:sid/Messages', async(req, res) => {
const account_sid = parseAccountSid(req);
const setName = `${(process.env.JAMBONES_CLUSTER_ID || 'default')}:active-fs`;
const {retrieveSet, logger} = req.app.locals;
try {
const account_sid = parseAccountSid(req);
await validateRequest(req, account_sid);
const setName = `${(process.env.JAMBONES_CLUSTER_ID || 'default')}:active-fs`;
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);
@@ -783,4 +890,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, listQueues} = req.app.locals;
const { search } = req.query || {};
try {
const accountSid = parseAccountSid(req);
await validateRequest(req, accountSid);
const queues = search ? await listQueues(accountSid, search) : await listQueues(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;

View File

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

View File

@@ -1,17 +1,13 @@
const router = require('express').Router();
const sysError = require('../error');
const {DbErrorBadRequest} = require('../../utils/errors');
const { parseServiceProviderSid } = require('./utils');
const parseAccountSid = (url) => {
const arr = /Accounts\/([^\/]*)/.exec(url);
if (arr) return arr[1];
};
const parseServiceProviderSid = (url) => {
const arr = /ServiceProviders\/([^\/]*)/.exec(url);
if (arr) return arr[1];
};
router.get('/', async(req, res) => {
const {logger, queryAlerts, queryAlertsSP} = req.app.locals;
try {

View File

@@ -1,16 +1,48 @@
const router = require('express').Router();
const {DbErrorBadRequest, DbErrorUnprocessableRequest} = require('../../utils/errors');
const {DbErrorBadRequest, DbErrorUnprocessableRequest, DbErrorForbidden} = require('../../utils/errors');
const Application = require('../../models/application');
const Account = require('../../models/account');
const Webhook = require('../../models/webhook');
const {promisePool} = require('../../db');
const decorate = require('./decorate');
const sysError = require('../error');
const { validate } = require('@jambonz/verb-specifications');
const { parseApplicationSid } = require('./utils');
const preconditions = {
'add': validateAdd,
'update': validateUpdate
};
const validateRequest = async(req, account_sid) => {
try {
if (req.user.hasScope('admin')) {
return;
}
if (req.user.hasScope('account')) {
if (account_sid === req.user.account_sid) {
return;
}
throw new DbErrorForbidden('insufficient permissions');
}
if (req.user.hasScope('service_provider')) {
const [r] = await promisePool.execute(
'SELECT service_provider_sid from accounts WHERE account_sid = ?', [account_sid]
);
if (r.length === 1 && r[0].service_provider_sid === req.user.service_provider_sid) {
return;
}
throw new DbErrorForbidden('insufficient permissions');
}
} catch (error) {
throw error;
}
};
/* only user-level tokens can add applications */
async function validateAdd(req) {
if (req.user.account_sid) {
@@ -21,7 +53,7 @@ async function validateAdd(req) {
if (!req.body.account_sid) throw new DbErrorBadRequest('missing required field: \'account_sid\'');
const result = await Account.retrieve(req.body.account_sid, req.user.service_provider_sid);
if (result.length === 0) {
throw new DbErrorBadRequest('insufficient privileges to create an application under the specified account');
throw new DbErrorForbidden('insufficient privileges');
}
}
if (req.body.call_hook && typeof req.body.call_hook !== 'object') {
@@ -33,12 +65,26 @@ async function validateAdd(req) {
}
async function validateUpdate(req, sid) {
if (req.user.account_sid) {
const app = await Application.retrieve(sid);
if (!app || !app.length || app[0].account_sid !== req.user.account_sid) {
throw new DbErrorBadRequest('you may not update or delete an application associated with a different account');
const app = await Application.retrieve(sid);
if (req.user.hasAccountAuth) {
if (!app || 0 === app.length || app[0].account_sid !== req.user.account_sid) {
throw new DbErrorForbidden('insufficient privileges');
}
}
if (req.user.hasServiceProviderAuth) {
const [r] = await promisePool.execute(
'SELECT service_provider_sid from accounts WHERE account_sid = ?', [app[0].account_sid]
);
if (r.length === 1 && r[0].service_provider_sid === req.user.service_provider_sid) {
return;
}
throw new DbErrorForbidden('insufficient permissions');
}
if (req.body.call_hook && typeof req.body.call_hook !== 'object') {
throw new DbErrorBadRequest('\'call_hook\' must be an object when updating an application');
}
@@ -48,13 +94,24 @@ async function validateUpdate(req, sid) {
}
async function validateDelete(req, sid) {
const result = await Application.retrieve(sid);
if (req.user.hasAccountAuth) {
const result = await Application.retrieve(sid);
if (!result || 0 === result.length) throw new DbErrorBadRequest('application does not exist');
if (result[0].account_sid !== req.user.account_sid) {
throw new DbErrorUnprocessableRequest('cannot delete application owned by a different account');
throw new DbErrorUnprocessableRequest('insufficient permissions');
}
}
if (req.user.hasServiceProviderAuth) {
const [r] = await promisePool.execute(
'SELECT service_provider_sid from accounts WHERE account_sid = ?', [result[0].account_sid]
);
if (r.length === 1 && r[0].service_provider_sid === req.user.service_provider_sid) {
return;
}
throw new DbErrorForbidden('insufficient permissions');
}
const assignedPhoneNumbers = await Application.getForeignKeyReferences('phone_numbers.application_sid', sid);
if (assignedPhoneNumbers > 0) throw new DbErrorUnprocessableRequest('cannot delete application with phone numbers');
}
@@ -76,6 +133,16 @@ router.post('/', async(req, res) => {
}
}
// validate app json if required
if (obj['app_json']) {
const app_json = JSON.parse(obj['app_json']);
try {
validate(logger, app_json);
} catch (err) {
throw new DbErrorBadRequest(err);
}
}
const uuid = await Application.make(obj);
res.status(201).json({sid: uuid});
} catch (err) {
@@ -100,10 +167,12 @@ router.get('/', async(req, res) => {
router.get('/:sid', async(req, res) => {
const logger = req.app.locals.logger;
try {
const application_sid = parseApplicationSid(req);
const service_provider_sid = req.user.hasServiceProviderAuth ? req.user.service_provider_sid : null;
const account_sid = req.user.hasAccountAuth ? req.user.account_sid : null;
const results = await Application.retrieve(req.params.sid, service_provider_sid, account_sid);
const results = await Application.retrieve(application_sid, service_provider_sid, account_sid);
if (results.length === 0) return res.status(404).end();
await validateRequest(req, results[0].account_sid);
return res.status(200).json(results[0]);
}
catch (err) {
@@ -113,9 +182,9 @@ router.get('/:sid', async(req, res) => {
/* delete */
router.delete('/:sid', async(req, res) => {
const sid = req.params.sid;
const logger = req.app.locals.logger;
try {
const sid = parseApplicationSid(req);
await validateDelete(req, sid);
const [application] = await promisePool.query('SELECT * FROM applications WHERE application_sid = ?', sid);
@@ -154,9 +223,9 @@ router.delete('/:sid', async(req, res) => {
/* update */
router.put('/:sid', async(req, res) => {
const sid = req.params.sid;
const logger = req.app.locals.logger;
try {
const sid = parseApplicationSid(req);
await validateUpdate(req, sid);
// create webhooks if provided
@@ -179,6 +248,16 @@ router.put('/:sid', async(req, res) => {
delete obj[prop];
}
// validate app json if required
if (obj['app_json']) {
const app_json = JSON.parse(obj['app_json']);
try {
validate(logger, app_json);
} catch (err) {
throw new DbErrorBadRequest(err);
}
}
const rowsAffected = await Application.update(sid, obj);
if (rowsAffected === 0) {
return res.status(404).end();

View File

@@ -1,15 +1,15 @@
const router = require('express').Router();
//const debug = require('debug')('jambonz:api-server');
const {DbErrorBadRequest} = require('../../utils/errors');
const {generateHashedPassword, verifyPassword} = require('../../utils/password-utils');
const {promisePool} = require('../../db');
const {cacheClient} = require('../../helpers');
const sysError = require('../error');
const sqlUpdatePassword = `UPDATE users
SET hashed_password= ?
WHERE user_sid = ?`;
router.post('/', async(req, res) => {
const {logger, retrieveKey, deleteKey} = req.app.locals;
const {logger} = req.app.locals;
const {user_sid} = req.user;
const {old_password, new_password} = req.body;
try {
@@ -26,10 +26,10 @@ router.post('/', async(req, res) => {
const isCorrect = await verifyPassword(r[0].hashed_password, old_password);
if (!isCorrect) {
const key = `reset-link:${old_password}`;
const user_sid = await retrieveKey(key);
const key = cacheClient.generateRedisKey('reset-link', old_password);
const user_sid = await cacheClient.get(key);
if (!user_sid) throw new DbErrorBadRequest('old_password is incorrect');
await deleteKey(key);
await cacheClient.delete(key);
}
}
@@ -42,6 +42,9 @@ router.post('/', async(req, res) => {
sysError(logger, res, err);
return;
}
const redisKey = cacheClient.generateRedisKey('jwt', user_sid, 'v2');
await cacheClient.delete(redisKey);
});
module.exports = router;

View File

@@ -1,6 +1,10 @@
const {DbErrorBadRequest, DbErrorUnprocessableRequest} = require('../../utils/errors');
const { BadRequestError, DbErrorBadRequest, DbErrorUnprocessableRequest } = require('../../utils/errors');
function sysError(logger, res, err) {
if (err instanceof BadRequestError) {
logger.info(err, err.message);
return res.status(400).json({msg: 'Bad request'});
}
if (err instanceof DbErrorBadRequest) {
logger.info(err, 'invalid client request');
return res.status(400).json({msg: err.message});

View File

@@ -4,6 +4,7 @@ const short = require('short-uuid');
const translator = short();
const {validateEmail, emailSimpleText} = require('../../utils/email-utils');
const {promisePool} = require('../../db');
const {cacheClient} = require('../../helpers');
const sysError = require('../error');
const sql = `SELECT * from users user
LEFT JOIN accounts AS acc
@@ -45,6 +46,7 @@ function createResetEmailText(link) {
router.post('/', async(req, res) => {
const {logger, addKey} = req.app.locals;
const {email} = req.body;
let obj;
try {
if (!email || !validateEmail(email)) {
@@ -53,11 +55,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) {
@@ -73,10 +80,14 @@ router.post('/', async(req, res) => {
else {
/* generate a link for this user to reset, send email */
const link = translator.generate();
addKey(`reset-link:${link}`, obj.user.user_sid, 3600)
const redisKey = cacheClient.generateRedisKey('reset-link', link);
addKey(redisKey, obj.user.user_sid, 3600)
.catch((err) => logger.error({err}, 'Error adding reset link to redis'));
emailSimpleText(logger, email, 'Reset password request', createResetEmailText(link));
}
const redisKey = cacheClient.generateRedisKey('jwt', obj.user.user_sid, 'v2');
await cacheClient.delete(redisKey);
});
module.exports = router;

View File

@@ -7,16 +7,17 @@ const isAdminScope = (req, res, next) => {
message: 'insufficient privileges'
});
};
const isAdminOrSPScope = (req, res, next) => {
if (req.user.hasScope('admin') || req.user.hasScope('service_provider')) return next();
res.status(403).json({
status: 'fail',
message: 'insufficient privileges'
});
};
// const isAdminOrSPScope = (req, res, next) => {
// if (req.user.hasScope('admin') || req.user.hasScope('service_provider')) return next();
// res.status(403).json({
// status: 'fail',
// message: 'insufficient privileges'
// });
// };
api.use('/BetaInviteCodes', isAdminScope, require('./beta-invite-codes'));
api.use('/ServiceProviders', isAdminOrSPScope, require('./service-providers'));
api.use('/SystemInformation', isAdminScope, require('./system-information'));
api.use('/ServiceProviders', require('./service-providers'));
api.use('/VoipCarriers', require('./voip-carriers'));
api.use('/Webhooks', require('./webhooks'));
api.use('/SipGateways', require('./sip-gateways'));
@@ -44,6 +45,11 @@ api.use('/Subscriptions', require('./subscriptions'));
api.use('/Invoices', require('./invoices'));
api.use('/InviteCodes', require('./invite-codes'));
api.use('/PredefinedCarriers', require('./predefined-carriers'));
api.use('/PasswordSettings', 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'));
// 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

@@ -22,22 +22,25 @@ DELETE FROM account_limits
WHERE account_sid = ?
AND category = ?
`;
router.post('/', async(req, res) => {
const logger = req.app.locals.logger;
const {
category,
quantity
} = req.body;
const account_sid = parseAccountSid(req);
let service_provider_sid;
if (!account_sid) {
if (!req.user.hasServiceProviderAuth) {
logger.error('POST /SpeechCredentials invalid credentials');
return res.send(403);
}
service_provider_sid = parseServiceProviderSid(req);
}
try {
let service_provider_sid;
const account_sid = parseAccountSid(req);
if (!account_sid) {
if (!req.user.hasServiceProviderAuth && !req.user.hasAdminAuth) {
logger.error('POST /SpeechCredentials invalid credentials');
return res.sendStatus(403);
}
service_provider_sid = parseServiceProviderSid(req);
}
let uuid;
if (account_sid) {
const existing = (await AccountLimits.retrieve(account_sid) || [])
@@ -80,10 +83,11 @@ router.post('/', async(req, res) => {
*/
router.get('/', async(req, res) => {
let service_provider_sid;
const account_sid = parseAccountSid(req);
if (!account_sid) service_provider_sid = parseServiceProviderSid(req);
const logger = req.app.locals.logger;
try {
const account_sid = parseAccountSid(req);
if (!account_sid) service_provider_sid = parseServiceProviderSid(req);
const limits = account_sid ?
await AccountLimits.retrieve(account_sid) :
await ServiceProviderLimits.retrieve(service_provider_sid);
@@ -99,10 +103,11 @@ router.get('/', async(req, res) => {
router.delete('/', async(req, res) => {
const logger = req.app.locals.logger;
const account_sid = parseAccountSid(req);
const {category} = req.query;
const service_provider_sid = parseServiceProviderSid(req);
try {
const account_sid = parseAccountSid(req);
const {category} = req.query;
const service_provider_sid = parseServiceProviderSid(req);
if (account_sid) {
if (category) {
await promisePool.execute(sqlDeleteAccountLimitsByCategory, [account_sid, category]);

View File

@@ -1,61 +1,116 @@
const router = require('express').Router();
const {getMysqlConnection} = require('../../db');
const jwt = require('jsonwebtoken');
const {verifyPassword} = require('../../utils/password-utils');
const {promisePool} = require('../../db');
const {cacheClient} = require('../../helpers');
const Account = require('../../models/account');
const ServiceProvider = require('../../models/service-provider');
const sysError = require('../error');
const retrievePemissionsSql = `
SELECT p.name
FROM permissions p, user_permissions up
WHERE up.permission_sid = p.permission_sid
AND up.user_sid = ?
`;
const retrieveSql = 'SELECT * from users where name = ?';
const tokenSql = 'SELECT token from api_keys where account_sid IS NULL AND service_provider_sid IS NULL';
router.post('/', (req, res) => {
const logger = req.app.locals.logger;
router.post('/', async(req, res) => {
const {logger, incrKey, retrieveKey} = req.app.locals;
const {username, password} = req.body;
if (!username || !password) {
logger.info('Bad POST to /login is missing username or password');
return res.sendStatus(400);
}
getMysqlConnection((err, conn) => {
if (err) {
logger.error({err}, 'Error getting db connection');
try {
const [r] = await promisePool.query(retrieveSql, username);
if (r.length === 0) {
logger.info(`Failed login attempt for user ${username}`);
return res.sendStatus(403);
}
logger.info({r}, 'successfully retrieved user account');
const maxLoginAttempts = process.env.LOGIN_ATTEMPTS_MAX_RETRIES || 6;
const loginAttempsBlocked = await retrieveKey(`login:${r[0].user_sid}`) >= maxLoginAttempts;
if (loginAttempsBlocked) {
logger.info(`User ${r[0].user_sid} was blocked due to excessive login attempts with incorrect credentials.`);
return res.status(403)
.json({error: 'Maximum login attempts reached. Please try again later or reset your password.'});
}
const isCorrect = await verifyPassword(r[0].hashed_password, password);
if (!isCorrect) {
const attempTime = process.env.LOGIN_ATTEMPTS_TIME || 1800;
const newAttempt = await incrKey(`login:${r[0].user_sid}`, attempTime)
.catch((err) => logger.error({err}, 'Error adding logging attempt to redis'));
if (newAttempt >= maxLoginAttempts) {
logger.info(`User ${r[0].user_sid} is now blocked due to excessive login attempts with incorrect credentials.`);
return res.status(403)
.json({error: `Maximum login attempts reached. Please try again in ${attempTime} seconds.`});
}
return res.sendStatus(403);
}
const force_change = !!r[0].force_change;
const [t] = await promisePool.query(tokenSql);
if (t.length === 0) {
logger.error('Database has no admin token provisioned...run reset_admin_password');
return res.sendStatus(500);
}
conn.query(retrieveSql, [username], async(err, results) => {
conn.release();
if (err) {
logger.error({err}, 'Error getting db connection');
return res.sendStatus(500);
}
if (0 === results.length) {
logger.info(`Failed login attempt for user ${username}`);
return res.sendStatus(403);
}
logger.info({results}, 'successfully retrieved account');
const isCorrect = await verifyPassword(results[0].hashed_password, password);
if (!isCorrect) return res.sendStatus(403);
const [p] = await promisePool.query(retrievePemissionsSql, r[0].user_sid);
const permissions = p.map((x) => x.name);
const obj = {user_sid: r[0].user_sid, scope: 'admin', force_change, permissions};
if (r[0].service_provider_sid && r[0].account_sid) {
const account = await Account.retrieve(r[0].account_sid);
const service_provider = await ServiceProvider.retrieve(r[0].service_provider_sid);
obj.scope = 'account';
obj.service_provider_sid = r[0].service_provider_sid;
obj.account_sid = r[0].account_sid;
obj.account_name = account[0].name;
obj.service_provider_name = service_provider[0].name;
}
else if (r[0].service_provider_sid) {
const service_provider = await ServiceProvider.retrieve(r[0].service_provider_sid);
obj.scope = 'service_provider';
obj.service_provider_sid = r[0].service_provider_sid;
obj.service_provider_name = service_provider[0].name;
}
const payload = {
scope: obj.scope,
permissions,
...(obj.service_provider_sid && {
service_provider_sid: obj.service_provider_sid,
service_provider_name: obj.service_provider_name
}),
...(obj.account_sid && {
account_sid: obj.account_sid,
account_name: obj.account_name,
service_provider_name: obj.service_provider_name
}),
user_sid: obj.user_sid
};
const force_change = !!results[0].force_change;
const expiresIn = parseInt(process.env.JWT_EXPIRES_IN || 60) * 60;
const token = jwt.sign(
payload,
process.env.JWT_SECRET,
{ expiresIn }
);
res.json({token, ...obj});
getMysqlConnection((err, conn) => {
if (err) {
logger.error({err}, 'Error getting db connection');
return res.sendStatus(500);
}
conn.query(tokenSql, (err, tokenResults) => {
conn.release();
if (err) {
logger.error({err}, 'Error getting db connection');
return res.sendStatus(500);
}
if (0 === tokenResults.length) {
logger.error('Database has no admin token provisioned...run reset_admin_password');
return res.sendStatus(500);
}
res.json({user_sid: results[0].user_sid, force_change, token: tokenResults[0].token});
});
});
/* Store jwt based on user_id after successful login */
await cacheClient.set({
redisKey: cacheClient.generateRedisKey('jwt', obj.user_sid, 'v2'),
value: token,
time: expiresIn,
});
});
} catch (err) {
sysError(logger, res, err);
}
});

View File

@@ -1,19 +1,18 @@
const router = require('express').Router();
const debug = require('debug')('jambonz:api-server');
const {hashString} = require('../../utils/password-utils');
const {cacheClient} = require('../../helpers');
const sysError = require('../error');
router.post('/', async(req, res) => {
const {logger, addKey} = req.app.locals;
const {jwt} = req.user;
const {logger} = req.app.locals;
const {user_sid} = req.user;
debug(`adding jwt to blacklist: ${jwt}`);
debug(`logout user and invalidate jwt token for user: ${user_sid}`);
try {
/* add key to blacklist */
const s = `jwt:${hashString(jwt)}`;
const result = await addKey(s, '1', 3600);
debug(`result from adding ${s}: ${result}`);
const redisKey = cacheClient.generateRedisKey('jwt', user_sid, 'v2');
await cacheClient.delete(redisKey);
res.sendStatus(204);
} catch (err) {
sysError(logger, res, err);

View File

@@ -0,0 +1,45 @@
const router = require('express').Router();
const sysError = require('../error');
const PasswordSettings = require('../../models/password-settings');
const { DbErrorBadRequest } = require('../../utils/errors');
const validate = (obj) => {
if (obj.min_password_length && (
obj.min_password_length < 8 ||
obj.min_password_length > 20
)) {
throw new DbErrorBadRequest('invalid min_password_length property: should be between 8-20');
}
};
router.post('/', async(req, res) => {
const logger = req.app.locals.logger;
try {
if (!req.user.hasAdminAuth) {
return res.sendStatus(403);
}
validate(req.body);
const [existing] = (await PasswordSettings.retrieve() || []);
if (existing) {
await PasswordSettings.update(req.body);
} else {
await PasswordSettings.make(req.body);
}
res.status(201).json({});
}
catch (err) {
sysError(logger, res, err);
}
});
router.get('/', async(req, res) => {
const logger = req.app.locals.logger;
try {
const [results] = (await PasswordSettings.retrieve() || []);
return res.status(200).json(results || {min_password_length: 8});
}
catch (err) {
sysError(logger, res, err);
}
});
module.exports = router;

View File

@@ -1,8 +1,10 @@
const router = require('express').Router();
const {DbErrorUnprocessableRequest, DbErrorBadRequest} = require('../../utils/errors');
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');
const preconditions = {
'add': validateAdd,
@@ -10,6 +12,7 @@ const preconditions = {
'update': validateUpdate
};
const sysError = require('../error');
const { parsePhoneNumberSid } = require('./utils');
/* check for required fields when adding */
@@ -20,6 +23,10 @@ async function validateAdd(req) {
req.body.account_sid = req.user.account_sid;
}
if (req.user.hasServiceProviderAuth) {
req.body.service_provider_sid = req.user.service_provider_sid;
}
if (!req.body.number) throw new DbErrorBadRequest('number is required');
const formattedNumber = e164(req.body.number);
req.body.number = formattedNumber;
@@ -41,11 +48,11 @@ async function checkInUse(req, sid) {
const phoneNumber = await PhoneNumber.retrieve(sid);
if (req.user.hasAccountAuth) {
if (phoneNumber && phoneNumber.length && phoneNumber[0].account_sid !== req.user.account_sid) {
throw new DbErrorUnprocessableRequest('cannot delete a phone number that belongs to another account');
throw new DbErrorForbidden('insufficient privileges');
}
}
if (!req.user.hasAccountAuth && phoneNumber.account_sid) {
throw new DbErrorUnprocessableRequest('cannot delete phone number that is assigned to an account');
throw new DbErrorForbidden('insufficient privileges');
}
}
@@ -57,10 +64,23 @@ async function validateUpdate(req, sid) {
const phoneNumber = await PhoneNumber.retrieve(sid);
if (req.user.hasAccountAuth) {
if (phoneNumber && phoneNumber.length && phoneNumber[0].account_sid !== req.user.account_sid) {
throw new DbErrorUnprocessableRequest('cannot operate on a phone number that belongs to another account');
throw new DbErrorForbidden('insufficient privileges');
}
}
if (req.user.hasServiceProviderAuth) {
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');
}
}
// TODO: if we are assigning to an account, verify it exists
// TODO: if we are assigning to an application, verify it is associated to the same account
@@ -74,7 +94,9 @@ decorate(router, PhoneNumber, ['add', 'update', 'delete'], preconditions);
router.get('/', async(req, res) => {
const logger = req.app.locals.logger;
try {
const results = await PhoneNumber.retrieveAll(req.user.hasAccountAuth ? req.user.account_sid : null);
const results = req.user.hasAdminAuth ?
await PhoneNumber.retrieveAll(req.user.hasAccountAuth ? req.user.account_sid : null) :
await PhoneNumber.retrieveAllForSP(req.user.service_provider_sid);
res.status(200).json(results);
} catch (err) {
sysError(logger, res, err);
@@ -85,9 +107,19 @@ router.get('/', async(req, res) => {
router.get('/:sid', async(req, res) => {
const logger = req.app.locals.logger;
try {
const sid = parsePhoneNumberSid(req);
const account_sid = req.user.hasAccountAuth ? req.user.account_sid : null;
const results = await PhoneNumber.retrieve(req.params.sid, account_sid);
const results = await PhoneNumber.retrieve(sid, account_sid);
if (results.length === 0) return res.status(404).end();
if (req.user.hasServiceProviderAuth && results.length === 1) {
const account_sid = results[0].account_sid;
const [r] = await promisePool.execute(
'SELECT service_provider_sid from accounts WHERE account_sid = ?', [account_sid]);
if (r.length === 1 && r[0].service_provider_sid !== req.user.service_provider_sid) {
throw new DbErrorBadRequest('insufficient privileges');
}
}
return res.status(200).json(results[0]);
}
catch (err) {

View File

@@ -2,6 +2,8 @@ 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 parseAccountSid = (url) => {
const arr = /Accounts\/([^\/]*)/.exec(url);
if (arr) return arr[1];
@@ -93,4 +95,20 @@ 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);
}
});
module.exports = router;

View File

@@ -4,6 +4,7 @@ const {DbErrorBadRequest, DbErrorUnprocessableRequest} = require('../../utils/er
const {promisePool} = require('../../db');
const {doGithubAuth, doGoogleAuth, doLocalAuth} = require('../../utils/oauth-utils');
const {validateEmail} = require('../../utils/email-utils');
const {cacheClient} = require('../../helpers');
const { v4: uuid } = require('uuid');
const short = require('short-uuid');
const translator = short();
@@ -338,16 +339,19 @@ router.post('/', async(req, res) => {
/* deactivate the old/replaced user */
const [r] = await promisePool.execute('DELETE FROM users WHERE user_sid = ?', [user_sid]);
logger.debug({r}, 'register - removed old user');
const redisKey = cacheClient.generateRedisKey('jwt', user_sid, 'v2');
await cacheClient.delete(redisKey);
}
}
const expiresIn = parseInt(process.env.JWT_EXPIRES_IN || 60) * 60 ;
// generate a json web token for this user
const token = jwt.sign({
user_sid: userProfile.user_sid,
account_sid: userProfile.account_sid,
email: userProfile.email,
name: userProfile.name
}, process.env.JWT_SECRET, { expiresIn: '1h' });
}, process.env.JWT_SECRET, { expiresIn });
logger.debug({
user_sid: userProfile.user_sid,
@@ -356,6 +360,13 @@ router.post('/', async(req, res) => {
res.json({jwt: token, ...userProfile});
/* Store jwt based on user_id after successful login */
await cacheClient.set({
redisKey: cacheClient.generateRedisKey('jwt', userProfile.user_sid, 'v2'),
value: token,
time: expiresIn,
});
} catch (err) {
debug(err, 'Error');
sysError(logger, res, err);

View File

@@ -2,25 +2,53 @@ const router = require('express').Router();
const Sbc = require('../../models/sbc');
const decorate = require('./decorate');
const sysError = require('../error');
//const {DbErrorBadRequest} = require('../../utils/errors');
//const {promisePool} = require('../../db');
const {DbErrorBadRequest} = require('../../utils/errors');
const {promisePool} = require('../../db');
decorate(router, Sbc, ['add', 'delete']);
const validate = (req, res) => {
if (req.user.hasScope('admin')) return;
res.status(403).json({
status: 'fail',
message: 'insufficient privileges'
});
};
const preconditions = {
'add': validate,
'delete': validate
};
decorate(router, Sbc, ['add', 'delete'], preconditions);
/* list */
router.get('/', async(req, res) => {
const logger = req.app.locals.logger;
try {
const service_provider_sid = req.query.service_provider_sid;
/*
let service_provider_sid = req.query.service_provider_sid;
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');
service_provider_sid = r[0].service_provider_sid;
}
if (!service_provider_sid) throw new DbErrorBadRequest('missing service_provider_sid in query');
*/
const results = await Sbc.retrieveAll(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');
}
/** generally, we have a global set of SBCs that all accounts use.
* However, we can have a set of SBCs that are specific for use by a service provider.
*/
let results = await Sbc.retrieveAll(service_provider_sid);
if (results.length === 0) results = await Sbc.retrieveAll();
res.status(200).json(results);
} catch (err) {
sysError(logger, res, err);

View File

@@ -1,6 +1,6 @@
const router = require('express').Router();
const {promisePool} = require('../../db');
const {DbErrorUnprocessableRequest} = require('../../utils/errors');
const {DbErrorForbidden} = require('../../utils/errors');
const Webhook = require('../../models/webhook');
const ServiceProvider = require('../../models/service-provider');
const Account = require('../../models/account');
@@ -8,11 +8,15 @@ const VoipCarrier = require('../../models/voip-carrier');
const Application = require('../../models/application');
const PhoneNumber = require('../../models/phone-number');
const ApiKey = require('../../models/api-key');
const {hasServiceProviderPermissions, parseServiceProviderSid} = require('./utils');
const {
hasServiceProviderPermissions,
parseServiceProviderSid,
parseVoipCarrierSid,
} = require('./utils');
const sysError = require('../error');
const decorate = require('./decorate');
const preconditions = {
'delete': noActiveAccounts
'delete': noActiveAccountsOrUsers
};
const sqlDeleteSipGateways = `DELETE from sip_gateways
WHERE voip_carrier_sid IN (
@@ -27,40 +31,100 @@ WHERE voip_carrier_sid IN (
WHERE service_provider_sid = ?
)`;
/* can not delete a service provider if it has any active accounts */
async function noActiveAccounts(req, sid) {
/* only admin users can add a service provider */
function validateAdd(req) {
if (!req.user.hasAdminAuth) {
throw new DbErrorForbidden('only admin users can add a service provider');
}
}
async function validateRetrieve(req) {
try {
const service_provider_sid = parseServiceProviderSid(req);
if (req.user.hasScope('admin')) {
return;
}
if (req.user.hasScope('service_provider') || req.user.hasScope('account')) {
if (service_provider_sid === req.user.service_provider_sid) return;
}
throw new DbErrorForbidden('insufficient permissions');
} catch (error) {
throw error;
}
}
function validateUpdate(req) {
try {
const service_provider_sid = parseServiceProviderSid(req);
if (req.user.hasScope('admin')) {
return;
}
if (req.user.hasScope('service_provider')) {
if (service_provider_sid === req.user.service_provider_sid) return;
}
throw new DbErrorForbidden('insufficient permissions to update service provider');
} catch (error) {
throw error;
}
}
/* can not delete a service provider if it has any active accounts or users*/
async function noActiveAccountsOrUsers(req, sid) {
if (!req.user.hasAdminAuth) {
throw new DbErrorForbidden('only admin users can delete a service provider');
}
const activeAccounts = await ServiceProvider.getForeignKeyReferences('accounts.service_provider_sid', sid);
if (activeAccounts > 0) throw new DbErrorUnprocessableRequest('cannot delete service provider with active accounts');
const activeUsers = await ServiceProvider.getForeignKeyReferences('users.service_provider_sid', sid);
if (activeAccounts > 0 && activeUsers > 0) throw new DbErrorForbidden('insufficient privileges');
if (activeAccounts > 0) throw new DbErrorForbidden('insufficient privileges');
if (activeUsers > 0) throw new DbErrorForbidden('insufficient privileges');
/* ok we can delete -- no active accounts. remove carriers and speech credentials */
await promisePool.execute('DELETE from speech_credentials WHERE service_provider_sid = ?', [sid]);
await promisePool.query(sqlDeleteSipGateways, [sid]);
await promisePool.query(sqlDeleteSmppGateways, [sid]);
await promisePool.query('DELETE from voip_carriers WHERE service_provider_sid = ?', [sid]);
await promisePool.query('DELETE from api_keys WHERE service_provider_sid = ?', [sid]);
}
decorate(router, ServiceProvider, ['delete'], preconditions);
router.use('/:sid/RecentCalls', hasServiceProviderPermissions, require('./recent-calls'));
router.use('/:sid/Alerts', hasServiceProviderPermissions, require('./alerts'));
router.use('/:sid/SpeechCredentials', hasServiceProviderPermissions, require('./speech-credentials'));
router.use('/:sid/SpeechCredentials', require('./speech-credentials'));
router.use('/:sid/Limits', hasServiceProviderPermissions, require('./limits'));
router.use('/:sid/PredefinedCarriers', hasServiceProviderPermissions, require('./add-from-predefined-carrier'));
router.get('/:sid/Accounts', async(req, res) => {
const logger = req.app.locals.logger;
try {
await validateRetrieve(req);
const service_provider_sid = parseServiceProviderSid(req);
const results = await Account.retrieveAll(service_provider_sid);
let results = await Account.retrieveAll(service_provider_sid);
if (req.user.hasScope('account')) {
results = results.filter((r) => r.account_sid === req.user.account_sid);
}
res.status(200).json(results);
} catch (err) {
sysError(logger, res, err);
}
});
router.get('/:sid/Applications', async(req, res) => {
const logger = req.app.locals.logger;
try {
await validateRetrieve(req);
const service_provider_sid = parseServiceProviderSid(req);
const results = await Application.retrieveAll(service_provider_sid);
let results = await Application.retrieveAll(service_provider_sid);
if (req.user.hasScope('account')) {
results = results.filter((r) => r.account_sid === req.user.account_sid);
}
res.status(200).json(results);
} catch (err) {
sysError(logger, res, err);
@@ -69,8 +133,12 @@ router.get('/:sid/Applications', async(req, res) => {
router.get('/:sid/PhoneNumbers', async(req, res) => {
const logger = req.app.locals.logger;
try {
await validateRetrieve(req);
const service_provider_sid = parseServiceProviderSid(req);
const results = await PhoneNumber.retrieveAllForSP(service_provider_sid);
let results = await PhoneNumber.retrieveAllForSP(service_provider_sid);
if (req.user.hasScope('account')) {
results = results.filter((r) => r.account_sid === req.user.account_sid);
}
res.status(200).json(results);
} catch (err) {
sysError(logger, res, err);
@@ -79,9 +147,15 @@ router.get('/:sid/PhoneNumbers', async(req, res) => {
router.get('/:sid/VoipCarriers', async(req, res) => {
const logger = req.app.locals.logger;
try {
await validateRetrieve(req);
const service_provider_sid = parseServiceProviderSid(req);
const results = await VoipCarrier.retrieveAllForSP(service_provider_sid);
res.status(200).json(results);
const carriers = await VoipCarrier.retrieveAllForSP(service_provider_sid);
if (req.user.hasScope('account')) {
return res.status(200).json(carriers.filter((c) => c.account_sid === req.user.account_sid || !c.account_sid));
}
res.status(200).json(carriers);
} catch (err) {
sysError(logger, res, err);
}
@@ -89,6 +163,7 @@ router.get('/:sid/VoipCarriers', async(req, res) => {
router.post('/:sid/VoipCarriers', async(req, res) => {
const logger = req.app.locals.logger;
try {
validateUpdate(req);
const service_provider_sid = parseServiceProviderSid(req);
const uuid = await VoipCarrier.make({...req.body, service_provider_sid});
res.status(201).json({sid: uuid});
@@ -99,7 +174,9 @@ router.post('/:sid/VoipCarriers', async(req, res) => {
router.put('/:sid/VoipCarriers/:voip_carrier_sid', async(req, res) => {
const logger = req.app.locals.logger;
try {
const rowsAffected = await VoipCarrier.update(req.params.voip_carrier_sid, req.body);
validateUpdate(req);
const sid = parseVoipCarrierSid(req);
const rowsAffected = await VoipCarrier.update(sid, req.body);
if (rowsAffected === 0) {
return res.sendStatus(404);
}
@@ -108,21 +185,15 @@ router.put('/:sid/VoipCarriers/:voip_carrier_sid', async(req, res) => {
sysError(logger, res, err);
}
});
router.get(':sid/Acccounts', async(req, res) => {
const logger = req.app.locals.logger;
try {
const service_provider_sid = parseServiceProviderSid(req);
const results = await Account.retrieveAll(service_provider_sid);
res.status(200).json(results);
} catch (err) {
sysError(logger, res, err);
}
});
router.get('/:sid/ApiKeys', async(req, res) => {
const logger = req.app.locals.logger;
const {sid} = req.params;
try {
const results = await ApiKey.retrieveAllForSP(sid);
await validateRetrieve(req);
let results = await ApiKey.retrieveAllForSP(sid);
if (req.user.hasScope('account')) {
results = results.filter((r) => r.account_sid === req.user.account_sid);
}
res.status(200).json(results);
await ApiKey.updateLastUsed(sid);
} catch (err) {
@@ -134,7 +205,7 @@ router.get('/:sid/ApiKeys', async(req, res) => {
router.post('/', async(req, res) => {
const logger = req.app.locals.logger;
try {
validateAdd(req);
// create webhooks if provided
const obj = Object.assign({}, req.body);
for (const prop of ['registration_hook']) {
@@ -157,6 +228,12 @@ router.get('/', async(req, res) => {
const logger = req.app.locals.logger;
try {
const results = await ServiceProvider.retrieveAll();
logger.debug({results, user: req.user}, 'ServiceProvider.retrieveAll');
if (req.user.hasScope('service_provider') || req.user.hasScope('account')) {
logger.debug(`Filtering results for ${req.user.service_provider_sid}`);
return res.status(200).json(results.filter((e) => req.user.service_provider_sid === e.service_provider_sid));
}
res.status(200).json(results);
} catch (err) {
sysError(logger, res, err);
@@ -167,7 +244,9 @@ router.get('/', async(req, res) => {
router.get('/:sid', async(req, res) => {
const logger = req.app.locals.logger;
try {
const results = await ServiceProvider.retrieve(req.params.sid);
await validateRetrieve(req);
const sid = parseServiceProviderSid(req);
const results = await ServiceProvider.retrieve(sid);
if (results.length === 0) return res.status(404).end();
return res.status(200).json(results[0]);
}
@@ -178,9 +257,11 @@ router.get('/:sid', async(req, res) => {
/* update */
router.put('/:sid', async(req, res) => {
const sid = req.params.sid;
const logger = req.app.locals.logger;
try {
validateUpdate(req);
const sid = parseServiceProviderSid(req);
// create webhooks if provided
const obj = Object.assign({}, req.body);
for (const prop of ['registration_hook']) {
@@ -189,15 +270,14 @@ router.put('/:sid', async(req, res) => {
const sid = obj[prop]['webhook_sid'];
delete obj[prop]['webhook_sid'];
await Webhook.update(sid, obj[prop]);
}
else {
} else {
const sid = await Webhook.make(obj[prop]);
obj[`${prop}_sid`] = sid;
}
}
else {
} else {
obj[`${prop}_sid`] = null;
}
delete obj[prop];
}
@@ -205,6 +285,7 @@ router.put('/:sid', async(req, res) => {
if (rowsAffected === 0) {
return res.status(404).end();
}
res.status(204).end();
} catch (err) {
sysError(logger, res, err);

View File

@@ -3,10 +3,17 @@ const router = require('express').Router();
const {DbErrorBadRequest} = require('../../utils/errors');
const {promisePool} = require('../../db');
const {verifyPassword} = require('../../utils/password-utils');
const {cacheClient} = require('../../helpers');
const jwt = require('jsonwebtoken');
const sysError = require('../error');
const retrievePermissionsSql = `
SELECT p.name
FROM permissions p, user_permissions up
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 */
@@ -52,6 +59,7 @@ router.post('/', async(req, res) => {
email: user.email,
phone: user.phone,
account_sid: user.account_sid,
service_provider_sid: a[0].service_provider_sid,
force_change: !!user.force_change,
provider: user.provider,
provider_userid: user.provider_userid,
@@ -64,11 +72,22 @@ router.post('/', async(req, res) => {
pristine: false
});
const [p] = await promisePool.query(retrievePermissionsSql, user.user_sid);
const permissions = p.map((x) => x.name);
const expiresIn = parseInt(process.env.JWT_EXPIRES_IN || 60) * 60;
// generate a json web token for this session
const token = jwt.sign({
const payload = {
scope: 'account',
permissions,
user_sid: userProfile.user_sid,
account_sid: userProfile.account_sid
}, process.env.JWT_SECRET, { expiresIn: '1h' });
account_sid: userProfile.account_sid,
service_provider_sid: userProfile.service_provider_sid
};
const token = jwt.sign(payload,
process.env.JWT_SECRET,
{ expiresIn }
);
logger.debug({
user_sid: userProfile.user_sid,
@@ -76,6 +95,14 @@ router.post('/', async(req, res) => {
}, 'generated jwt');
res.json({jwt: token, ...userProfile});
/* Store jwt based on user_id after successful login */
await cacheClient.set({
redisKey: cacheClient.generateRedisKey('jwt', userProfile.user_sid, 'v2'),
value: token,
time: expiresIn,
});
} catch (err) {
sysError(logger, res, err);
}

View File

@@ -1,11 +1,46 @@
const router = require('express').Router();
const SipGateway = require('../../models/sip-gateway');
const {DbErrorBadRequest, DbErrorUnprocessableRequest} = require('../../utils/errors');
const {DbErrorBadRequest, DbErrorForbidden} = require('../../utils/errors');
//const {parseSipGatewaySid} = require('./utils');
const decorate = require('./decorate');
const sysError = require('../error');
const checkUserScope = async(req, voip_carrier_sid) => {
const {lookupCarrierBySid} = req.app.locals;
if (!voip_carrier_sid) {
throw new DbErrorBadRequest('missing voip_carrier_sid');
}
if (req.user.hasAdminAuth) return;
if (req.user.hasAccountAuth) {
const carrier = await lookupCarrierBySid(voip_carrier_sid);
if (!carrier) throw new DbErrorBadRequest('invalid voip_carrier_sid');
if ((!carrier.service_provider_sid || carrier.service_provider_sid === req.user.service_provider_sid) &&
(!carrier.account_sid || carrier.account_sid === req.user.account_sid)) {
if (req.method !== 'GET' && !carrier.account_sid) {
throw new DbErrorForbidden('insufficient privileges');
}
return;
}
}
if (req.user.hasServiceProviderAuth) {
const carrier = await lookupCarrierBySid(voip_carrier_sid);
if (!carrier) {
throw new DbErrorBadRequest('invalid voip_carrier_sid');
}
if (carrier.service_provider_sid === req.user.service_provider_sid) {
return;
}
}
throw new DbErrorForbidden('insufficient privileges');
};
const validate = async(req, sid) => {
const {lookupCarrierBySid, lookupSipGatewayBySid} = req.app.locals;
const {lookupSipGatewayBySid} = req.app.locals;
let voip_carrier_sid;
if (sid) {
@@ -17,13 +52,7 @@ const validate = async(req, sid) => {
voip_carrier_sid = req.body.voip_carrier_sid;
if (!voip_carrier_sid) throw new DbErrorBadRequest('missing voip_carrier_sid');
}
if (req.hasAccountAuth) {
const carrier = await lookupCarrierBySid(voip_carrier_sid);
if (!carrier) throw new DbErrorBadRequest('invalid voip_carrier_sid');
if (carrier.account_sid !== req.user.account_sid) {
throw new DbErrorUnprocessableRequest('user can not add gateway for voip_carrier belonging to other account');
}
}
await checkUserScope(req, voip_carrier_sid);
};
const preconditions = {
@@ -39,6 +68,7 @@ router.get('/', async(req, res) => {
const logger = req.app.locals.logger;
const voip_carrier_sid = req.query.voip_carrier_sid;
try {
await checkUserScope(req, voip_carrier_sid);
if (!voip_carrier_sid) {
logger.info('GET /SipGateways missing voip_carrier_sid param');
return res.status(400).json({message: 'missing voip_carrier_sid query param'});

View File

@@ -1,11 +1,38 @@
const router = require('express').Router();
const SmppGateway = require('../../models/smpp-gateway');
const {DbErrorBadRequest, DbErrorUnprocessableRequest} = require('../../utils/errors');
const {DbErrorBadRequest, DbErrorForbidden} = require('../../utils/errors');
const decorate = require('./decorate');
const sysError = require('../error');
const checkUserScope = async(req, voip_carrier_sid) => {
const {lookupCarrierBySid} = req.app.locals;
if (!voip_carrier_sid) {
throw new DbErrorBadRequest('missing voip_carrier_sid');
}
if (req.user.hasAdminAuth) return;
if (req.user.hasAccountAuth) {
const carrier = await lookupCarrierBySid(voip_carrier_sid);
if (!carrier) throw new DbErrorBadRequest('invalid voip_carrier_sid');
if ((!carrier.service_provider_sid || carrier.service_provider_sid === req.user.service_provider_sid) &&
(!carrier.account_sid || carrier.account_sid === req.user.account_sid)) {
return;
}
}
if (req.user.hasServiceProviderAuth) {
const carrier = await lookupCarrierBySid(voip_carrier_sid);
if (!carrier) throw new DbErrorBadRequest('invalid voip_carrier_sid');
if (carrier.service_provider_sid === req.user.service_provider_sid) {
return;
}
}
throw new DbErrorForbidden('insufficient privileges');
};
const validate = async(req, sid) => {
const {lookupCarrierBySid, lookupSmppGatewayBySid} = req.app.locals;
const {lookupSmppGatewayBySid} = req.app.locals;
let voip_carrier_sid;
if (sid) {
@@ -17,13 +44,8 @@ const validate = async(req, sid) => {
voip_carrier_sid = req.body.voip_carrier_sid;
if (!voip_carrier_sid) throw new DbErrorBadRequest('missing voip_carrier_sid');
}
if (req.hasAccountAuth) {
const carrier = await lookupCarrierBySid(voip_carrier_sid);
if (!carrier) throw new DbErrorBadRequest('invalid voip_carrier_sid');
if (carrier.account_sid !== req.user.account_sid) {
throw new DbErrorUnprocessableRequest('user can not add gateway for voip_carrier belonging to other account');
}
}
await checkUserScope(req, voip_carrier_sid);
};
const preconditions = {
@@ -39,6 +61,7 @@ router.get('/', async(req, res) => {
const logger = req.app.locals.logger;
const voip_carrier_sid = req.query.voip_carrier_sid;
try {
await checkUserScope(req, voip_carrier_sid);
if (!voip_carrier_sid) {
logger.info('GET /SmppGateways missing voip_carrier_sid param');
return res.status(400).json({message: 'missing voip_carrier_sid query param'});

View File

@@ -1,10 +1,11 @@
const router = require('express').Router();
const assert = require('assert');
const Account = require('../../models/account');
const SpeechCredential = require('../../models/speech-credential');
const sysError = require('../error');
const {decrypt, encrypt} = require('../../utils/encrypt-decrypt');
const {parseAccountSid, parseServiceProviderSid} = require('./utils');
const {DbErrorUnprocessableRequest} = require('../../utils/errors');
const {parseAccountSid, parseServiceProviderSid, parseSpeechCredentialSid} = require('./utils');
const {DbErrorUnprocessableRequest, DbErrorForbidden} = require('../../utils/errors');
const {
testGoogleTts,
testGoogleStt,
@@ -12,8 +13,91 @@ const {
testAwsStt,
testMicrosoftStt,
testMicrosoftTts,
testWellSaidTts
testWellSaidTts,
testNuanceStt,
testNuanceTts,
testDeepgramStt,
testSonioxStt,
testIbmTts,
testIbmStt
} = require('../../utils/speech-utils');
const {promisePool} = require('../../db');
const validateAdd = async(req) => {
const account_sid = parseAccountSid(req);
const service_provider_sid = parseServiceProviderSid(req);
if (service_provider_sid) {
if (req.user.hasServiceProviderAuth && service_provider_sid !== req.user.service_provider_sid) {
throw new DbErrorForbidden('Insufficient privileges');
}
if (req.user.hasAccountAuth && service_provider_sid !== req.user.service_provider_sid &&
req.body.account_sid !== req.user.account_sid) {
throw new DbErrorForbidden('Insufficient privileges');
}
}
if (account_sid) {
if (req.user.hasAccountAuth && account_sid !== req.user.account_sid) {
throw new DbErrorForbidden('Insufficient privileges');
}
const [r] = await promisePool.execute(
'SELECT service_provider_sid from accounts WHERE account_sid = ?', [account_sid]
);
if (req.user.hasServiceProviderAuth && r[0].service_provider_sid !== req.user.service_provider_sid) {
throw new DbErrorForbidden('Insufficient privileges');
}
}
return;
};
const validateRetrieveUpdateDelete = async(req, speech_credentials) => {
if (req.user.hasServiceProviderAuth && speech_credentials[0].service_provider_sid !== req.user.service_provider_sid) {
throw new DbErrorForbidden('Insufficient privileges');
}
if (req.user.hasAccountAuth && speech_credentials[0].account_sid !== req.user.account_sid) {
throw new DbErrorForbidden('Insufficient privileges');
}
return;
};
const validateRetrieveList = async(req) => {
const service_provider_sid = parseServiceProviderSid(req);
if (service_provider_sid) {
if ((req.user.hasServiceProviderAuth || req.user.hasAccountAuth) &&
service_provider_sid !== req.user.service_provider_sid) {
throw new DbErrorForbidden('Insufficient privileges');
}
}
return;
};
const validateTest = async(req, speech_credentials) => {
if (req.user.hasAdminAuth) {
return;
}
if (!req.user.hasAdminAuth && speech_credentials.service_provider_sid !== req.user.service_provider_sid) {
throw new DbErrorForbidden('Insufficient privileges');
}
if (speech_credentials.service_provider_sid === req.user.service_provider_sid) {
if (req.user.hasServiceProviderAuth) {
return;
}
if (req.user.hasAccountAuth && (!speech_credentials.account_sid ||
speech_credentials.account_sid === req.user.account_sid)) {
return;
}
throw new DbErrorForbidden('Insufficient privileges');
}
};
const obscureKey = (key) => {
const key_spoiler_length = 6;
@@ -35,10 +119,23 @@ const encryptCredential = (obj) => {
aws_region,
api_key,
region,
client_id,
secret,
nuance_tts_uri,
nuance_stt_uri,
use_custom_tts,
custom_tts_endpoint,
use_custom_stt,
custom_stt_endpoint
custom_stt_endpoint,
tts_api_key,
tts_region,
stt_api_key,
stt_region,
riva_server_uri,
instance_id,
custom_stt_url,
custom_tts_url,
auth_token = ''
} = obj;
switch (vendor) {
@@ -78,28 +175,63 @@ const encryptCredential = (obj) => {
const wsData = JSON.stringify({api_key});
return encrypt(wsData);
case 'nuance':
const checked = (client_id && secret) || (nuance_tts_uri || nuance_stt_uri);
assert(checked, 'invalid nuance speech credential: either entered client id and\
secret or entered a nuance_tts_uri or nuance_stt_uri');
const nuanceData = JSON.stringify({client_id, secret, nuance_tts_uri, nuance_stt_uri});
return encrypt(nuanceData);
case 'deepgram':
assert(api_key, 'invalid deepgram speech credential: api_key is required');
const deepgramData = JSON.stringify({api_key});
return encrypt(deepgramData);
case 'ibm':
const ibmData = JSON.stringify({tts_api_key, tts_region, stt_api_key, stt_region, instance_id});
return encrypt(ibmData);
case 'nvidia':
assert(riva_server_uri, 'invalid riva server uri: riva_server_uri is required');
const nvidiaData = JSON.stringify({ riva_server_uri });
return encrypt(nvidiaData);
case 'soniox':
assert(api_key, 'invalid soniox speech credential: api_key is required');
const sonioxData = JSON.stringify({api_key});
return encrypt(sonioxData);
default:
assert(false, `invalid or missing vendor: ${vendor}`);
if (vendor.startsWith('custom:')) {
const customData = JSON.stringify({auth_token, custom_stt_url, custom_tts_url});
return encrypt(customData);
}
else assert(false, `invalid or missing vendor: ${vendor}`);
}
};
router.post('/', async(req, res) => {
const logger = req.app.locals.logger;
const {
use_for_stt,
use_for_tts,
vendor,
} = req.body;
const account_sid = req.user.account_sid || req.body.account_sid;
let service_provider_sid;
if (!account_sid) {
if (!req.user.hasServiceProviderAuth) {
logger.error('POST /SpeechCredentials invalid credentials');
return res.send(403);
}
service_provider_sid = parseServiceProviderSid(req);
}
try {
const {
use_for_stt,
use_for_tts,
vendor,
} = req.body;
const account_sid = req.user.account_sid || req.body.account_sid;
const service_provider_sid = req.user.service_provider_sid ||
req.body.service_provider_sid || parseServiceProviderSid(req);
await validateAdd(req);
if (!account_sid) {
if (!req.user.hasServiceProviderAuth && !req.user.hasAdminAuth) {
logger.error('POST /SpeechCredentials invalid credentials');
return res.sendStatus(403);
}
}
const encrypted_credential = encryptCredential(req.body);
const uuid = await SpeechCredential.make({
account_sid,
@@ -119,17 +251,28 @@ router.post('/', async(req, res) => {
* retrieve all speech credentials for an account
*/
router.get('/', async(req, res) => {
let service_provider_sid;
const account_sid = parseAccountSid(req);
if (!account_sid) service_provider_sid = parseServiceProviderSid(req);
const logger = req.app.locals.logger;
try {
const creds = account_sid ?
await SpeechCredential.retrieveAll(account_sid) :
await SpeechCredential.retrieveAllForSP(service_provider_sid);
const account_sid = parseAccountSid(req) ? parseAccountSid(req) : req.user.account_sid;
const service_provider_sid = parseServiceProviderSid(req);
await validateRetrieveList(req);
const credsAccount = account_sid ? await SpeechCredential.retrieveAll(account_sid) : [];
const credsSP = service_provider_sid ?
await SpeechCredential.retrieveAllForSP(service_provider_sid) :
await SpeechCredential.retrieveAllForSP((await Account.retrieve(account_sid))[0].service_provider_sid);
// filter out duplicates and discard those from other non-matching accounts
let creds = [...new Set([...credsAccount, ...credsSP].map((c) => JSON.stringify(c)))].map((c) => JSON.parse(c));
if (req.user.hasScope('account')) {
creds = creds.filter((c) => c.account_sid === req.user.account_sid || !c.account_sid);
}
res.status(200).json(creds.map((c) => {
const {credential, ...obj} = c;
if ('google' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
const key_header = '-----BEGIN PRIVATE KEY-----\n';
@@ -160,6 +303,45 @@ router.get('/', async(req, res) => {
const o = JSON.parse(decrypt(credential));
obj.api_key = obscureKey(o.api_key);
}
else if ('nuance' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
obj.client_id = o.client_id;
obj.secret = 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;
}
if (req.user.hasAccountAuth && obj.account_sid === null) {
delete obj.api_key;
delete obj.secret_access_key;
delete obj.secret;
delete obj.auth_token;
delete obj.stt_api_key;
delete obj.tts_api_key;
}
return obj;
}));
} catch (err) {
@@ -171,11 +353,14 @@ router.get('/', async(req, res) => {
* retrieve a specific speech credential
*/
router.get('/:sid', async(req, res) => {
const sid = req.params.sid;
const logger = req.app.locals.logger;
try {
const sid = parseSpeechCredentialSid(req);
const cred = await SpeechCredential.retrieve(sid);
if (0 === cred.length) return res.sendStatus(404);
await validateRetrieveUpdateDelete(req, cred);
const {credential, ...obj} = cred[0];
if ('google' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
@@ -205,6 +390,48 @@ router.get('/:sid', async(req, res) => {
const o = JSON.parse(decrypt(credential));
obj.api_key = obscureKey(o.api_key);
}
else if ('nuance' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
obj.client_id = o.client_id;
obj.secret = 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;
}
if (req.user.hasAccountAuth && obj.account_sid === null) {
delete obj.api_key;
delete obj.secret_access_key;
delete obj.secret;
delete obj.auth_token;
delete obj.stt_api_key;
delete obj.tts_api_key;
}
res.status(200).json(obj);
} catch (err) {
sysError(logger, res, err);
@@ -215,9 +442,11 @@ router.get('/:sid', async(req, res) => {
* delete a speech credential
*/
router.delete('/:sid', async(req, res) => {
const sid = req.params.sid;
const logger = req.app.locals.logger;
try {
const sid = parseSpeechCredentialSid(req);
const cred = await SpeechCredential.retrieve(sid);
await validateRetrieveUpdateDelete(req, cred);
const count = await SpeechCredential.remove(sid);
if (0 === count) return res.sendStatus(404);
res.sendStatus(204);
@@ -231,10 +460,11 @@ router.delete('/:sid', async(req, res) => {
* update a speech credential -- we only allow use_for_tts and use_for_stt to be updated
*/
router.put('/:sid', async(req, res) => {
const sid = req.params.sid;
const logger = req.app.locals.logger;
try {
const {use_for_tts, use_for_stt, region, aws_region} = req.body;
const sid = parseSpeechCredentialSid(req);
const {use_for_tts, use_for_stt, region, aws_region, stt_region, tts_region,
riva_server_uri, nuance_tts_uri, nuance_stt_uri} = req.body;
if (typeof use_for_tts === 'undefined' && typeof use_for_stt === 'undefined') {
throw new DbErrorUnprocessableRequest('use_for_tts and use_for_stt are the only updateable fields');
}
@@ -249,6 +479,9 @@ router.put('/:sid', async(req, res) => {
/* update the credential if provided */
try {
const cred = await SpeechCredential.retrieve(sid);
await validateRetrieveUpdateDelete(req, cred);
if (1 === cred.length) {
const {credential, vendor} = cred[0];
const o = JSON.parse(decrypt(credential));
@@ -267,7 +500,12 @@ router.put('/:sid', async(req, res) => {
use_custom_tts,
custom_tts_endpoint,
use_custom_stt,
custom_stt_endpoint
custom_stt_endpoint,
stt_region,
tts_region,
riva_server_uri,
nuance_stt_uri,
nuance_tts_uri
};
logger.info({o, newCred}, 'updating speech credential with this new credential');
obj.credential = encryptCredential(newCred);
@@ -296,12 +534,15 @@ router.put('/:sid', async(req, res) => {
* Test a credential
*/
router.get('/:sid/test', async(req, res) => {
const sid = req.params.sid;
const logger = req.app.locals.logger;
try {
const sid = parseSpeechCredentialSid(req);
const creds = await SpeechCredential.retrieve(sid);
if (!creds || 0 === creds.length) return res.sendStatus(404);
await validateTest(req, creds[0]);
const cred = creds[0];
const credential = JSON.parse(decrypt(cred.credential));
const results = {
@@ -319,7 +560,8 @@ router.get('/:sid/test', async(req, res) => {
if (cred.use_for_tts) {
try {
await testGoogleTts(logger, credential);
const {getTtsVoices} = req.app.locals;
await testGoogleTts(logger, getTtsVoices, credential);
results.tts.status = 'ok';
SpeechCredential.ttsTestResult(sid, true);
} catch (err) {
@@ -341,8 +583,9 @@ router.get('/:sid/test', async(req, res) => {
}
else if (cred.vendor === 'aws') {
if (cred.use_for_tts) {
const {getTtsVoices} = req.app.locals;
try {
await testAwsTts(logger, {
await testAwsTts(logger, getTtsVoices, {
accessKeyId: credential.access_key_id,
secretAccessKey: credential.secret_access_key,
region: credential.aws_region || process.env.AWS_REGION
@@ -419,7 +662,106 @@ router.get('/:sid/test', async(req, res) => {
}
}
}
else if (cred.vendor === 'nuance') {
const {getTtsVoices} = req.app.locals;
const {
client_id,
secret,
nuance_tts_uri,
nuance_stt_uri
} = credential;
if (cred.use_for_tts) {
try {
await testNuanceTts(logger, getTtsVoices, {
client_id,
secret,
nuance_tts_uri
});
results.tts.status = 'ok';
SpeechCredential.ttsTestResult(sid, true);
} catch (err) {
logger.error({err}, 'error testing nuance tts');
const reason = err.statusCode === 401 ?
'invalid client_id or secret' :
(err.message || 'error accessing nuance tts service with provided credentials');
results.tts = {status: 'fail', reason};
SpeechCredential.ttsTestResult(sid, false);
}
}
if (cred.use_for_stt) {
try {
await testNuanceStt(logger, {client_id, secret, nuance_stt_uri});
results.stt.status = 'ok';
SpeechCredential.sttTestResult(sid, true);
} catch (err) {
results.stt = {status: 'fail', reason: err.message};
SpeechCredential.sttTestResult(sid, false);
}
}
}
else if (cred.vendor === 'deepgram') {
const {api_key} = credential;
if (cred.use_for_stt) {
try {
await testDeepgramStt(logger, {api_key});
results.stt.status = 'ok';
SpeechCredential.sttTestResult(sid, true);
} catch (err) {
results.stt = {status: 'fail', reason: err.message};
SpeechCredential.sttTestResult(sid, false);
}
}
}
else if (cred.vendor === 'ibm') {
const {getTtsVoices} = req.app.locals;
if (cred.use_for_tts) {
const {tts_api_key, tts_region} = credential;
try {
await testIbmTts(logger, getTtsVoices, {
tts_api_key,
tts_region
});
results.tts.status = 'ok';
SpeechCredential.ttsTestResult(sid, true);
} catch (err) {
logger.error({err}, 'error testing ibm tts');
const reason = err.statusCode === 401 ?
'invalid api_key or region' :
(err.message || 'error accessing ibm tts service with provided credentials');
results.tts = {status: 'fail', reason};
SpeechCredential.ttsTestResult(sid, false);
}
}
if (cred.use_for_stt) {
const {stt_api_key, stt_region, instance_id} = credential;
try {
await testIbmStt(logger, {stt_region, stt_api_key, instance_id});
results.stt.status = 'ok';
SpeechCredential.sttTestResult(sid, true);
} catch (err) {
results.stt = {status: 'fail', reason: err.message};
SpeechCredential.sttTestResult(sid, false);
}
}
}
else if (cred.vendor === 'soniox') {
const {api_key} = credential;
if (cred.use_for_stt) {
try {
await testSonioxStt(logger, {api_key});
results.stt.status = 'ok';
SpeechCredential.sttTestResult(sid, true);
} catch (err) {
results.stt = {status: 'fail', reason: err.message};
SpeechCredential.sttTestResult(sid, false);
}
}
}
res.status(200).json(results);
} catch (err) {
sysError(logger, res, err);
}

View File

@@ -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

@@ -1,16 +1,22 @@
//const assert = require('assert');
//const debug = require('debug')('jambonz:api-server');
const router = require('express').Router();
const {DbErrorBadRequest} = require('../../utils/errors');
const User = require('../../models/user');
const {DbErrorBadRequest, BadRequestError, DbErrorForbidden} = require('../../utils/errors');
const {generateHashedPassword, verifyPassword} = require('../../utils/password-utils');
const {promisePool} = require('../../db');
const {validatePasswordSettings, parseUserSid} = require('./utils');
const {decrypt} = require('../../utils/encrypt-decrypt');
const {cacheClient} = require('../../helpers');
const sysError = require('../error');
const retrieveMyDetails = `SELECT *
FROM users user
JOIN accounts AS account ON account.account_sid = user.account_sid
LEFT JOIN service_providers as sp ON account.service_provider_sid = sp.service_provider_sid
WHERE user.user_sid = ?`;
const retrieveMyDetails2 = `SELECT *
FROM users user
LEFT JOIN accounts AS account ON account.account_sid = user.account_sid
LEFT JOIN service_providers as sp ON sp.service_provider_sid = user.service_provider_sid
WHERE user.user_sid = ?`;
const retrieveSql = 'SELECT * from users where user_sid = ?';
const retrieveProducts = `SELECT *
FROM account_products
@@ -22,109 +28,290 @@ AND account_subscriptions.pending=0`;
const updateSql = 'UPDATE users set hashed_password = ?, force_change = false WHERE user_sid = ?';
const retrieveStaticIps = 'SELECT * FROM account_static_ips WHERE account_sid = ?';
const validateRequest = async(user_sid, payload) => {
const {old_password, new_password, name, email, email_activation_code} = payload;
const validateRequest = async(user_sid, req) => {
const payload = req.body;
const {
old_password,
new_password,
initial_password,
name,
email,
email_activation_code,
force_change,
is_active
} = payload;
const [r] = await promisePool.query(retrieveSql, user_sid);
if (r.length === 0) return null;
if (r.length === 0) {
throw new DbErrorBadRequest('Invalid request: user_sid does not exist');
}
const user = r[0];
/* it is not allowed for anyone to promote a user to a higher level of authority */
if (null === payload.account_sid || null === payload.service_provider_sid) {
throw new DbErrorBadRequest('Invalid request: user may not be promoted');
}
if (req.user.hasAccountAuth) {
/* account user may not change modify account_sid or service_provider_sid */
if ('account_sid' in payload && payload.account_sid !== user.account_sid) {
throw new DbErrorBadRequest('Invalid request: user may not be promoted or moved to another account');
}
if ('service_provider_sid' in payload && payload.service_provider_sid !== user.service_provider_sid) {
throw new DbErrorBadRequest('Invalid request: user may not be promoted or moved to another service provider');
}
}
if (req.user.hasServiceProviderAuth) {
if ('service_provider_sid' in payload && payload.service_provider_sid !== user.service_provider_sid) {
throw new DbErrorBadRequest('Invalid request: user may not be promoted or moved to another service provider');
}
}
if ('account_sid' in payload) {
const [r] = await promisePool.query('SELECT * FROM accounts WHERE account_sid = ?', payload.account_sid);
if (r.length === 0) throw new DbErrorBadRequest('Invalid request: account_sid does not exist');
const {service_provider_sid} = r[0];
if (service_provider_sid !== user.service_provider_sid) {
throw new DbErrorBadRequest('Invalid request: user may not be moved to another service provider');
}
}
if (initial_password) {
await validatePasswordSettings(initial_password);
}
if ((old_password && !new_password) || (new_password && !old_password)) {
throw new DbErrorBadRequest('new_password and old_password both required');
}
if (new_password) {
await validatePasswordSettings(new_password);
}
if (new_password && name) throw new DbErrorBadRequest('can not change name and password simultaneously');
if (new_password && user.provider !== 'local') {
throw new DbErrorBadRequest('can not change password when using oauth2');
}
if ((email && !email_activation_code) || (email_activation_code && !email)) {
if (email_activation_code && !email) {
throw new DbErrorBadRequest('email and email_activation_code both required');
}
if (!name && !new_password && !email) throw new DbErrorBadRequest('no updates requested');
if (!name && !new_password && !email && !initial_password && !force_change && !is_active)
throw new DbErrorBadRequest('no updates requested');
return user;
};
const getActiveAdminUsers = (users) => {
return users.filter((e) => !e.account_sid && !e.service_provider_sid && e.is_active);
};
const ensureUserActionIsAllowed = (req, user) => {
if (req.user.hasAdminAuth) {
return;
}
if (req.user.hasServiceProviderAuth && req.user.service_provider_sid === user.service_provider_sid) {
return;
}
if (req.user.hasAccountAuth && req.user.account_sid === user.account_sid) {
return;
}
throw new DbErrorForbidden('insufficient permissions');
};
const ensureUserDeletionIsAllowed = (req, activeAdminUsers, user) => {
try {
if (req.user.hasAdminAuth && activeAdminUsers.length === 1 && activeAdminUsers[0].user_sid === user[0].user_sid) {
throw new BadRequestError('cannot delete this admin user - there are no other active admin users');
}
ensureUserActionIsAllowed(req, user[0]);
return;
} catch (error) {
throw error;
}
};
const ensureUserRetrievalIsAllowed = (req, user) => {
try {
ensureUserActionIsAllowed(req, user);
return;
} catch (error) {
throw error;
}
};
router.get('/', async(req, res) => {
const logger = req.app.locals.logger;
let usersList;
try {
let results;
if (req.user.hasAdminAuth) {
results = await User.retrieveAll();
}
else if (req.user.hasAccountAuth) {
results = await User.retrieveAllForAccount(req.user.account_sid, true);
}
else if (req.user.hasServiceProviderAuth) {
results = await User.retrieveAllForServiceProvider(req.user.service_provider_sid, true);
}
if (results.length === 0) throw new Error('failure retrieving users list');
usersList = results.map((user) => {
const {
user_sid,
name,
email,
force_change,
is_active,
account_sid,
service_provider_sid,
account_name,
service_provider_name
} = user;
let scope;
if (account_sid && service_provider_sid) {
scope = 'account';
} else if (service_provider_sid) {
scope = 'service_provider';
} else {
scope = 'admin';
}
const obj = {
user_sid,
name,
email,
scope,
force_change,
is_active,
...(account_sid && {account_sid}),
...(account_name && {account_name}),
...(service_provider_sid && {service_provider_sid}),
...(service_provider_name && {service_provider_name})
};
return obj;
});
} catch (err) {
sysError(logger, res, err);
}
res.status(200).json(usersList);
});
router.get('/me', async(req, res) => {
const logger = req.app.locals.logger;
const {user_sid} = req.user;
if (!user_sid) return res.sendStatus(403);
let payload;
try {
const [r] = await promisePool.query({sql: retrieveMyDetails, nestTables: true}, user_sid);
logger.debug(r, 'retrieved user details');
const payload = r[0];
const {user, account, sp} = payload;
['hashed_password', 'salt', 'phone_activation_code', 'email_activation_code', 'account_sid'].forEach((prop) => {
delete user[prop];
});
['email_validated', 'phone_validated', 'force_change'].forEach((prop) => user[prop] = !!user[prop]);
['is_active'].forEach((prop) => account[prop] = !!account[prop]);
account.root_domain = sp.root_domain;
delete payload.sp;
/* get api keys */
const [keys] = await promisePool.query('SELECT * from api_keys WHERE account_sid = ?', account.account_sid);
payload.api_keys = keys.map((k) => {
return {
api_key_sid: k.api_key_sid,
//token: k.token.replace(/.(?=.{4,}$)/g, '*'),
token: k.token,
last_used: k.last_used,
created_at: k.created_at
};
});
/* get products */
const [products] = await promisePool.query({sql: retrieveProducts, nestTables: true}, account.account_sid);
if (!products.length || !products[0].account_subscriptions) {
throw new Error('account is missing a subscription');
}
const account_subscription = products[0].account_subscriptions;
payload.subscription = {
status: 'active',
account_subscription_sid: account_subscription.account_subscription_sid,
start_date: account_subscription.effective_start_date,
products: products.map((prd) => {
return {
name: prd.products.name,
units: prd.products.unit_label,
quantity: prd.account_products.quantity
};
})
};
if (account_subscription.pending) {
Object.assign(payload.subscription, {
status: 'suspended',
suspend_reason: account_subscription.pending_reason
if (process.env.JAMBONES_HOSTING) {
const [r] = await promisePool.query({sql: retrieveMyDetails, nestTables: true}, user_sid);
logger.debug(r, 'retrieved user details');
payload = r[0];
const {user, account, sp} = payload;
['hashed_password', 'salt', 'phone_activation_code', 'email_activation_code', 'account_sid'].forEach((prop) => {
delete user[prop];
});
}
const {
last4,
exp_month,
exp_year,
card_type,
stripe_statement_descriptor
} = account_subscription;
if (last4) {
const real_last4 = decrypt(last4);
Object.assign(payload.subscription, {
last4: real_last4,
['email_validated', 'phone_validated', 'force_change'].forEach((prop) => user[prop] = !!user[prop]);
['is_active'].forEach((prop) => account[prop] = !!account[prop]);
account.root_domain = sp.root_domain;
delete payload.sp;
/* get api keys */
const [keys] = await promisePool.query('SELECT * from api_keys WHERE account_sid = ?', account.account_sid);
payload.api_keys = keys.map((k) => {
return {
api_key_sid: k.api_key_sid,
//token: k.token.replace(/.(?=.{4,}$)/g, '*'),
token: k.token,
last_used: k.last_used,
created_at: k.created_at
};
});
/* get products */
const [products] = await promisePool.query({sql: retrieveProducts, nestTables: true}, account.account_sid);
if (!products.length || !products[0].account_subscriptions) {
throw new Error('account is missing a subscription');
}
const account_subscription = products[0].account_subscriptions;
payload.subscription = {
status: 'active',
account_subscription_sid: account_subscription.account_subscription_sid,
start_date: account_subscription.effective_start_date,
products: products.map((prd) => {
return {
name: prd.products.name,
units: prd.products.unit_label,
quantity: prd.account_products.quantity
};
})
};
if (account_subscription.pending) {
Object.assign(payload.subscription, {
status: 'suspended',
suspend_reason: account_subscription.pending_reason
});
}
const {
last4,
exp_month,
exp_year,
card_type,
statement_descriptor: stripe_statement_descriptor
stripe_statement_descriptor
} = account_subscription;
if (last4) {
const real_last4 = decrypt(last4);
Object.assign(payload.subscription, {
last4: real_last4,
exp_month,
exp_year,
card_type,
statement_descriptor: stripe_statement_descriptor
});
}
/* get static ips */
const [static_ips] = await promisePool.query(retrieveStaticIps, account.account_sid);
payload.static_ips = static_ips.map((r) => r.public_ipv4);
}
else {
const [r] = await promisePool.query({sql: retrieveMyDetails2, nestTables: true}, user_sid);
logger.debug(r, 'retrieved user details');
payload = r[0];
const {user} = payload;
['hashed_password', 'salt', 'phone_activation_code', 'email_activation_code'].forEach((prop) => {
delete user[prop];
});
['email_validated', 'phone_validated', 'force_change'].forEach((prop) => user[prop] = !!user[prop]);
}
logger.debug({payload}, 'returning user details');
res.json(payload);
} catch (err) {
sysError(logger, res, err);
}
});
router.get('/:user_sid', async(req, res) => {
const logger = req.app.locals.logger;
try {
const user_sid = parseUserSid(req);
const [user] = await User.retrieve(user_sid);
if (!user) {
throw new Error('failure retrieving user');
}
/* get static ips */
const [static_ips] = await promisePool.query(retrieveStaticIps, account.account_sid);
payload.static_ips = static_ips.map((r) => r.public_ipv4);
ensureUserRetrievalIsAllowed(req, user);
logger.debug({payload}, 'returning user details');
res.json(payload);
// eslint-disable-next-line no-unused-vars
const { hashed_password, ...rest } = user;
return res.status(200).json(rest);
} catch (err) {
sysError(logger, res, err);
}
@@ -133,12 +320,32 @@ router.get('/me', async(req, res) => {
router.put('/:user_sid', async(req, res) => {
const logger = req.app.locals.logger;
const {user_sid} = req.params;
const {old_password, new_password, name, email, email_activation_code} = req.body;
const user = await User.retrieve(user_sid);
const {hasAccountAuth, hasServiceProviderAuth, hasAdminAuth} = req.user;
const {
old_password,
new_password,
initial_password,
email_activation_code,
email,
name,
is_active,
force_change,
account_sid,
service_provider_sid
} = req.body;
if (req.user.user_sid && req.user.user_sid !== user_sid) return res.sendStatus(403);
//if (req.user.user_sid && req.user.user_sid !== user_sid) return res.sendStatus(403);
if (!hasAdminAuth &&
!(hasAccountAuth && req.user.account_sid === user[0].account_sid) &&
!(hasServiceProviderAuth && req.user.service_provider_sid === user[0].service_provider_sid) &&
(req.user.user_sid && req.user.user_sid !== user_sid)) {
return res.sendStatus(403);
}
try {
const user = await validateRequest(user_sid, req.body);
const user = await validateRequest(user_sid, req);
if (!user) return res.sendStatus(404);
if (new_password) {
@@ -149,6 +356,11 @@ router.put('/:user_sid', async(req, res) => {
//debug(`PUT /Users/:sid pwd ${old_password} does not match hash ${old_hashed_password}`);
return res.sendStatus(403);
}
if (old_password === new_password) {
throw new Error('new password cannot be your old password');
}
const passwordHash = await generateHashedPassword(new_password);
//debug(`updating hashed_password to ${passwordHash}`);
const r = await promisePool.execute(updateSql, [passwordHash, user_sid]);
@@ -160,10 +372,55 @@ router.put('/:user_sid', async(req, res) => {
if (0 === r.changedRows) throw new Error('database update failed');
}
if (email) {
if (initial_password) {
const passwordHash = await generateHashedPassword(initial_password);
const r = await promisePool.execute(
'UPDATE users SET email = ?, email_activation_code = ?, email_validated = 0 WHERE user_sid = ?',
[email, email_activation_code, user_sid]);
'UPDATE users SET hashed_password = ? WHERE user_sid = ?',
[passwordHash, user_sid]
);
if (0 === r.changedRows) throw new Error('database update failed');
}
if (typeof is_active !== 'undefined') {
const r = await promisePool.execute('UPDATE users SET is_active = ? WHERE user_sid = ?', [is_active, user_sid]);
if (0 === r.changedRows) throw new Error('database update failed');
}
if (typeof force_change !== 'undefined') {
const r = await promisePool.execute(
'UPDATE users SET force_change = ? WHERE user_sid = ?',
[force_change, user_sid]);
if (0 === r.changedRows) throw new Error('database update failed');
}
if (account_sid || account_sid === null) {
const r = await promisePool.execute(
'UPDATE users SET account_sid = ? WHERE user_sid = ?',
[account_sid, user_sid]);
if (0 === r.changedRows) throw new Error('database update failed');
const redisKey = cacheClient.generateRedisKey('jwt', user_sid, 'v2');
await cacheClient.delete(redisKey);
}
if (service_provider_sid || service_provider_sid === null) {
const r = await promisePool.execute(
'UPDATE users SET service_provider_sid = ? WHERE user_sid = ?',
[service_provider_sid, user_sid]);
if (0 === r.changedRows) throw new Error('database update failed');
const redisKey = cacheClient.generateRedisKey('jwt', user_sid, 'v2');
await cacheClient.delete(redisKey);
}
if (email) {
if (email_activation_code) {
const r = await promisePool.execute(
'UPDATE users SET email = ?, email_activation_code = ?, email_validated = 0 WHERE user_sid = ?',
[email, email_activation_code, user_sid]);
if (0 === r.changedRows) throw new Error('database update failed');
}
const r = await promisePool.execute(
'UPDATE users SET email = ? WHERE user_sid = ?',
[email, user_sid]);
if (0 === r.changedRows) throw new Error('database update failed');
if (process.env.NODE_ENV !== 'test') {
@@ -176,5 +433,80 @@ router.put('/:user_sid', async(req, res) => {
}
});
router.post('/', async(req, res) => {
const logger = req.app.locals.logger;
const passwordHash = await generateHashedPassword(req.body.initial_password);
const payload = {
...req.body,
provider: 'local',
hashed_password: passwordHash,
};
const allUsers = await User.retrieveAll();
delete payload.initial_password;
try {
if (req.body.initial_password) {
await validatePasswordSettings(req.body.initial_password);
}
const email = allUsers.find((e) => e.email === payload.email);
const name = allUsers.find((e) => e.name === payload.name);
if (name) {
logger.debug({payload}, 'user with this username already exists');
return res.status(422).json({msg: 'invalid username or email'});
}
if (email) {
logger.debug({payload}, 'user with this email already exists');
return res.status(422).json({msg: 'invalid username or email'});
}
if (req.user.hasAdminAuth) {
logger.debug({payload}, 'POST /users');
const uuid = await User.make(payload);
res.status(201).json({user_sid: uuid});
}
else if (req.user.hasAccountAuth) {
logger.debug({payload}, 'POST /users');
const uuid = await User.make({
...payload,
account_sid: req.user.account_sid,
});
res.status(201).json({user_sid: uuid});
}
else if (req.user.hasServiceProviderAuth) {
logger.debug({payload}, 'POST /users');
const uuid = await User.make({
...payload,
service_provider_sid: req.user.service_provider_sid,
});
res.status(201).json({user_sid: uuid});
}
} catch (err) {
sysError(logger, res, err);
}
});
router.delete('/:user_sid', async(req, res) => {
const logger = req.app.locals.logger;
try {
const user_sid = parseUserSid(req);
const allUsers = await User.retrieveAll();
const activeAdminUsers = getActiveAdminUsers(allUsers);
const user = allUsers.filter((user) => user.user_sid === user_sid);
ensureUserDeletionIsAllowed(req, activeAdminUsers, user);
await User.remove(user_sid);
/* invalidate the jwt of the deleted user */
const redisKey = cacheClient.generateRedisKey('jwt', user_sid, 'v2');
await cacheClient.delete(redisKey);
return res.sendStatus(204);
} catch (err) {
sysError(logger, res, err);
}
});
module.exports = router;

View File

@@ -1,9 +1,10 @@
const { v4: uuid } = require('uuid');
const { v4: uuid, validate } = require('uuid');
const bent = require('bent');
const Account = require('../../models/account');
const {promisePool} = require('../../db');
const {cancelSubscription, detachPaymentMethod} = require('../../utils/stripe-utils');
const freePlans = require('../../utils/free_plans');
const { BadRequestError, DbErrorBadRequest } = require('../../utils/errors');
const insertAccountSubscriptionSql = `INSERT INTO account_subscriptions
(account_subscription_sid, account_sid)
values (?, ?)`;
@@ -137,38 +138,179 @@ const createTestAlerts = async(writeAlerts, AlertType, account_sid) => {
};
const validateSid = (model, req) => {
const arr = new RegExp(`${model}\/([^\/]*)`).exec(req.originalUrl);
if (arr) {
const sid = arr[1];
const sid_validation = validate(sid);
if (!sid_validation) {
throw new BadRequestError(`invalid ${model}Sid format`);
}
return arr[1];
}
return;
};
const parseServiceProviderSid = (req) => {
const arr = /ServiceProviders\/([^\/]*)/.exec(req.originalUrl);
if (arr) return arr[1];
try {
return validateSid('ServiceProviders', req);
} catch (error) {
throw error;
}
};
const parseAccountSid = (req) => {
const arr = /Accounts\/([^\/]*)/.exec(req.originalUrl);
if (arr) return arr[1];
try {
return validateSid('Accounts', req);
} catch (error) {
throw error;
}
};
const hasAccountPermissions = (req, res, next) => {
if (req.user.hasScope('admin')) return next();
if (req.user.hasScope('account')) {
const account_sid = parseAccountSid(req);
if (account_sid === req.user.account_sid) return next();
const parseApplicationSid = (req) => {
try {
return validateSid('Applications', req);
} catch (error) {
throw error;
}
};
const parseCallSid = (req) => {
try {
return validateSid('Calls', req);
} catch (error) {
throw error;
}
};
const parsePhoneNumberSid = (req) => {
try {
return validateSid('PhoneNumbers', req);
} catch (error) {
throw error;
}
};
const parseSpeechCredentialSid = (req) => {
try {
return validateSid('SpeechCredentials', req);
} catch (error) {
throw error;
}
};
const parseVoipCarrierSid = (req) => {
try {
return validateSid('VoipCarriers', req);
} catch (error) {
throw error;
}
};
const parseWebhookSid = (req) => {
try {
return validateSid('Webhooks', req);
} catch (error) {
throw error;
}
};
const parseSipGatewaySid = (req) => {
try {
return validateSid('SipGateways', req);
} catch (error) {
throw error;
}
};
const parseUserSid = (req) => {
try {
return validateSid('Users', req);
} catch (error) {
throw error;
}
};
const parseLcrSid = (req) => {
try {
return validateSid('Lcrs', req);
} catch (error) {
throw error;
}
};
const hasAccountPermissions = async(req, res, next) => {
try {
if (req.user.hasScope('admin')) {
return next();
}
if (req.user.hasScope('service_provider')) {
const service_provider_sid = parseServiceProviderSid(req);
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')) {
const account_sid = parseAccountSid(req);
const service_provider_sid = parseServiceProviderSid(req);
const [r] = await Account.retrieve(account_sid);
if (account_sid) {
if (r && r.account_sid === req.user.account_sid) {
return next();
}
}
if (service_provider_sid) {
if (r && r.service_provider_sid === req.user.service_provider_sid) {
return next();
}
}
}
res.status(403).json({
status: 'fail',
message: 'insufficient privileges'
});
} catch (error) {
throw error;
}
res.status(403).json({
status: 'fail',
message: 'insufficient privileges'
});
};
const hasServiceProviderPermissions = (req, res, next) => {
if (req.user.hasScope('admin')) return next();
if (req.user.hasScope('service_provider')) {
const service_provider_sid = parseServiceProviderSid(req);
if (service_provider_sid === req.user.service_provider_sid) return next();
try {
if (req.user.hasScope('admin')) {
return next();
}
if (req.user.hasScope('service_provider')) {
const service_provider_sid = parseServiceProviderSid(req);
if (service_provider_sid === req.user.service_provider_sid) {
return next();
}
}
res.status(403).json({
status: 'fail',
message: 'insufficient privileges'
});
} catch (error) {
throw error;
}
res.status(403).json({
status: 'fail',
message: 'insufficient privileges'
});
};
const checkLimits = async(req, res, next) => {
@@ -273,15 +415,50 @@ const disableSubspace = async(opts) => {
return;
};
const validatePasswordSettings = async(password) => {
const sql = 'SELECT * from password_settings';
const [rows] = await promisePool.execute(sql);
const specialChars = /[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]+/;
const numbers = /[0-9]+/;
if (rows.length === 0) {
if (password.length < 8 || password.length > 20) {
throw new DbErrorBadRequest('password length must be between 8 and 20');
}
} else {
if (rows[0].min_password_length && password.length < rows[0].min_password_length) {
throw new DbErrorBadRequest(`password must be at least ${rows[0].min_password_length} characters long`);
}
if (rows[0].require_digit === 1 && !numbers.test(password)) {
throw new DbErrorBadRequest('password must contain at least one digit');
}
if (rows[0].require_special_character === 1 && !specialChars.test(password)) {
throw new DbErrorBadRequest('password must contain at least one special character');
}
}
return;
};
module.exports = {
setupFreeTrial,
createTestCdrs,
createTestAlerts,
parseAccountSid,
parseApplicationSid,
parseCallSid,
parsePhoneNumberSid,
parseServiceProviderSid,
parseSpeechCredentialSid,
parseVoipCarrierSid,
parseWebhookSid,
parseSipGatewaySid,
parseUserSid,
parseLcrSid,
hasAccountPermissions,
hasServiceProviderPermissions,
checkLimits,
enableSubspace,
disableSubspace
disableSubspace,
validatePasswordSettings
};

View File

@@ -4,6 +4,7 @@ const VoipCarrier = require('../../models/voip-carrier');
const {promisePool} = require('../../db');
const decorate = require('./decorate');
const sysError = require('../error');
const { parseVoipCarrierSid } = require('./utils');
const validate = async(req) => {
const {lookupAppBySid, lookupAccountBySid} = req.app.locals;
@@ -73,7 +74,14 @@ decorate(router, VoipCarrier, ['add', 'update', 'delete'], preconditions);
router.get('/', async(req, res) => {
const logger = req.app.locals.logger;
try {
const results = await VoipCarrier.retrieveAll(req.user.hasAccountAuth ? req.user.account_sid : null);
const results = req.user.hasAdminAuth ?
await VoipCarrier.retrieveAll(req.user.hasAccountAuth ? req.user.account_sid : null) :
await VoipCarrier.retrieveAllForSP(req.user.service_provider_sid);
if (req.user.hasScope('account')) {
return res.status(200).json(results.filter((c) => c.account_sid === req.user.account_sid || !c.account_sid));
}
res.status(200).json(results);
} catch (err) {
sysError(logger, res, err);
@@ -84,9 +92,24 @@ router.get('/', async(req, res) => {
router.get('/:sid', async(req, res) => {
const logger = req.app.locals.logger;
try {
const sid = parseVoipCarrierSid(req);
const account_sid = req.user.hasAccountAuth ? req.user.account_sid : null;
const results = await VoipCarrier.retrieve(req.params.sid, account_sid);
const results = await VoipCarrier.retrieve(sid, account_sid);
if (results.length === 0) return res.status(404).end();
const ret = results[0];
ret.register_status = JSON.parse(ret.register_status || '{}');
if (req.user.hasServiceProviderAuth && results.length === 1) {
if (results.length === 1 && results[0].service_provider_sid !== req.user.service_provider_sid) {
throw new DbErrorBadRequest('insufficient privileges');
}
}
if (req.user.hasAccountAuth && results.length === 1) {
if (results.length === 1 && results[0].account_sid !== req.user.account_sid) {
throw new DbErrorBadRequest('insufficient privileges');
}
}
return res.status(200).json(results[0]);
}
catch (err) {

View File

@@ -2,15 +2,37 @@ const router = require('express').Router();
const Webhook = require('../../models/webhook');
const decorate = require('./decorate');
const sysError = require('../error');
const {DbErrorForbidden} = require('../../utils/errors');
const { parseWebhookSid } = require('./utils');
const {promisePool} = require('../../db');
decorate(router, Webhook, ['add']);
/* retrieve */
router.get('/:sid', async(req, res) => {
const logger = req.app.locals.logger;
try {
const results = await Webhook.retrieve(req.params.sid);
const sid = parseWebhookSid(req);
const results = await Webhook.retrieve(sid);
if (results.length === 0) return res.status(404).end();
if (req.user.hasAccountAuth) {
/* can only update carriers for the user's account */
if (results[0].account_sid !== req.user.account_sid) {
throw new DbErrorForbidden('insufficient privileges');
}
}
if (req.user.hasServiceProviderAuth) {
const [r] = await promisePool.execute(
'SELECT service_provider_sid from accounts WHERE account_sid = ?', [results[0].account_sid]
);
if (r.length === 1 && r[0].service_provider_sid === req.user.service_provider_sid) {
return;
}
throw new DbErrorForbidden('insufficient permissions');
}
return res.status(200).json(results[0]);
}
catch (err) {

View File

@@ -1,6 +1,15 @@
const {DbErrorBadRequest, DbErrorUnprocessableRequest, DbErrorForbidden} = require('../utils/errors');
const {
BadRequestError,
DbErrorBadRequest,
DbErrorUnprocessableRequest,
DbErrorForbidden
} = require('../utils/errors');
function sysError(logger, res, err) {
if (err instanceof BadRequestError) {
logger.info(err, err.message);
return res.status(400).json({msg: 'Bad request'});
}
if (err instanceof DbErrorBadRequest) {
logger.info(err, 'invalid client request');
return res.status(400).json({msg: err.message});

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,7 @@
const formData = require('form-data');
const Mailgun = require('mailgun.js');
const mailgun = new Mailgun(formData);
const bent = require('bent');
const validateEmail = (email) => {
// eslint-disable-next-line max-len
const re = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
@@ -8,6 +9,44 @@ const validateEmail = (email) => {
};
const emailSimpleText = async(logger, to, subject, text) => {
const from = 'jambonz Support <support@jambonz.org>';
if (process.env.CUSTOM_EMAIL_VENDOR_URL) {
await sendEmailByCustomVendor(logger, from, to, subject, text);
} else {
await sendEmailByMailgun(logger, from, to, subject, text);
}
};
const sendEmailByCustomVendor = async(logger, from, to, subject, text) => {
try {
const post = bent('POST', {
'Content-Type': 'application/json',
...((process.env.CUSTOM_EMAIL_VENDOR_USERNAME && process.env.CUSTOM_EMAIL_VENDOR_PASSWORD) &&
({
'Authorization':`Basic ${Buffer.from(
`${process.env.CUSTOM_EMAIL_VENDOR_USERNAME}:${process.env.CUSTOM_EMAIL_VENDOR_PASSWORD}`
).toString('base64')}`
}))
});
const res = await post(process.env.CUSTOM_EMAIL_VENDOR_URL, {
from,
to,
subject,
text
});
logger.debug({
res
}, 'sent email to custom vendor.');
} catch (err) {
logger.info({
err
}, 'Error sending email From Custom email vendor');
}
};
const sendEmailByMailgun = async(logger, from, to, subject, text) => {
const mg = mailgun.client({
username: 'api',
key: process.env.MAILGUN_API_KEY
@@ -17,14 +56,18 @@ const emailSimpleText = async(logger, to, subject, text) => {
try {
const res = await mg.messages.create(process.env.MAILGUN_DOMAIN, {
from: 'jambonz Support <support@jambonz.org>',
from,
to,
subject,
text
});
logger.debug({res}, 'sent email');
logger.debug({
res
}, 'sent email');
} catch (err) {
logger.info({err}, 'Error sending email');
logger.info({
err
}, 'Error sending email From mailgun');
}
};

View File

@@ -2,9 +2,9 @@ const crypto = require('crypto');
const algorithm = process.env.LEGACY_CRYPTO ? 'aes-256-ctr' : 'aes-256-cbc';
const iv = crypto.randomBytes(16);
const secretKey = crypto.createHash('sha256')
.update(String(process.env.JWT_SECRET))
.update(process.env.ENCRYPTION_SECRET || process.env.JWT_SECRET)
.digest('base64')
.substr(0, 32);
.substring(0, 32);
const encrypt = (text) => {
const cipher = crypto.createCipheriv(algorithm, secretKey, iv);

View File

@@ -1,3 +1,9 @@
class BadRequestError extends Error {
constructor(msg) {
super(msg);
}
}
class DbError extends Error {
constructor(msg) {
super(msg);
@@ -23,6 +29,7 @@ class DbErrorForbidden extends DbError {
}
module.exports = {
BadRequestError,
DbError,
DbErrorBadRequest,
DbErrorUnprocessableRequest,

View File

@@ -39,11 +39,17 @@ const getHomerSipTrace = async(logger, apiKey, callId) => {
const obj = await postJSON('/api/v3/call/transaction', {
param: {
transaction: {
call: true
call: true,
registration: true,
rest: false
},
orlogic: true,
search: {
'1_call': {
callid: [callId]
},
'1_registration': {
callid: [callId]
}
},
},
@@ -67,11 +73,17 @@ const getHomerPcap = async(logger, apiKey, callIds) => {
const stream = await postPcap('/api/v3/export/call/messages/pcap', {
param: {
transaction: {
call: true
call: true,
registration: true,
rest: false
},
orlogic: true,
search: {
'1_call': {
callid: callIds
},
'1_registration': {
callid: callIds
}
},
},

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

@@ -0,0 +1,18 @@
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) {
logger.error({err}, `getJaegerTrace: Error retrieving spans for traceId ${traceId}`);
}
};
module.exports = {
getJaegerTrace
};

View File

@@ -1,18 +1,19 @@
const crypto = require('crypto');
const { argon2i } = require('argon2-ffi');
const argon2 = require('argon2');
const util = require('util');
const { argon2i } = argon2;
const getRandomBytes = util.promisify(crypto.randomBytes);
const generateHashedPassword = async(password) => {
const salt = await getRandomBytes(32);
const passwordHash = await argon2i.hash(password, salt);
const passwordHash = await argon2.hash(password, { type: argon2i, salt });
return passwordHash;
};
const verifyPassword = async(passwordHash, password) => {
const isCorrect = await argon2i.verify(passwordHash, password);
return isCorrect;
const verifyPassword = (passwordHash, password) => {
return argon2.verify(passwordHash, password);
};
const hashString = (s) => crypto.createHash('md5').update(s).digest('hex');

View File

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

10794
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,15 +1,16 @@
{
"name": "jambonz-api-server",
"version": "v0.7.7",
"version": "v0.8.2",
"description": "",
"main": "app.js",
"scripts": {
"start": "node app.js",
"test": "NODE_ENV=test APPLY_JAMBONZ_DB_LIMITS=1 JWT_SECRET=foobarbazzle JAMBONES_MYSQL_HOST=127.0.0.1 JAMBONES_MYSQL_PORT=3360 JAMBONES_MYSQL_USER=jambones_test JAMBONES_MYSQL_PASSWORD=jambones_test JAMBONES_MYSQL_DATABASE=jambones_test JAMBONES_REDIS_HOST=localhost JAMBONES_REDIS_PORT=16379 JAMBONES_TIME_SERIES_HOST=127.0.0.1 JAMBONES_LOGLEVEL=error JAMBONES_CREATE_CALL_URL=http://localhost/v1/createCall K8S=true K8S_FEATURE_SERVER_SERVICE_NAME=127.0.0.1 K8S_FEATURE_SERVER_SERVICE_PORT=3100 node test/ ",
"integration-test": "NODE_ENV=test JAMBONES_TIME_SERIES_HOST=127.0.0.1 AWS_REGION='us-east-1' JAMBONES_CURRENCY=USD JWT_SECRET=foobarbazzle JAMBONES_MYSQL_HOST=127.0.0.1 JAMBONES_MYSQL_PORT=3360 JAMBONES_MYSQL_USER=jambones_test JAMBONES_MYSQL_PASSWORD=jambones_test JAMBONES_MYSQL_DATABASE=jambones_test JAMBONES_REDIS_HOST=localhost JAMBONES_REDIS_PORT=16379 JAMBONES_LOGLEVEL=debug JAMBONES_CREATE_CALL_URL=http://localhost/v1/createCall node test/serve-integration.js",
"integration-test": "NODE_ENV=test JAMBONES_AUTH_USE_JWT=1 JAMBONES_TIME_SERIES_HOST=127.0.0.1 AWS_REGION='us-east-1' JAMBONES_CURRENCY=USD JWT_SECRET=foobarbazzle JAMBONES_MYSQL_HOST=127.0.0.1 JAMBONES_MYSQL_PORT=3360 JAMBONES_MYSQL_USER=jambones_test JAMBONES_MYSQL_PASSWORD=jambones_test JAMBONES_MYSQL_DATABASE=jambones_test JAMBONES_REDIS_HOST=localhost JAMBONES_REDIS_PORT=16379 JAMBONES_LOGLEVEL=debug JAMBONES_CREATE_CALL_URL=http://localhost/v1/createCall node test/serve-integration.js",
"upgrade-db": "node ./db/upgrade-jambonz-db.js",
"coverage": "./node_modules/.bin/nyc --reporter html --report-dir ./coverage npm run test",
"jslint": "eslint app.js lib",
"jslint:fix": "eslint app.js lib --fix",
"prepare": "husky install"
},
"author": "Dave Horton",
@@ -18,24 +19,29 @@
"url": "https://github.com/jambonz/jambonz-api-server.git"
},
"dependencies": {
"@google-cloud/speech": "^4.10.2",
"@google-cloud/text-to-speech": "^3.4.0",
"@jambonz/db-helpers": "^0.6.18",
"@jambonz/realtimedb-helpers": "^0.4.29",
"@aws-sdk/client-transcribe": "^3.290.0",
"@deepgram/sdk": "^1.10.2",
"@google-cloud/speech": "^5.1.0",
"@jambonz/db-helpers": "^0.7.8",
"@jambonz/realtimedb-helpers": "^0.7.1",
"@jambonz/speech-utils": "^0.0.12",
"@jambonz/time-series": "^0.2.5",
"argon2-ffi": "^2.0.0",
"aws-sdk": "^2.1152.0",
"@jambonz/verb-specifications": "^0.0.17",
"@soniox/soniox-node": "^1.1.0",
"argon2": "^0.30.3",
"bent": "^7.3.12",
"cors": "^2.8.5",
"debug": "^4.3.4",
"express": "^4.18.1",
"express-rate-limit": "^6.4.0",
"form-data": "^2.5.1",
"form-urlencoded": "^6.1.0",
"helmet": "^5.1.0",
"jsonwebtoken": "^8.5.1",
"ibm-watson": "^7.1.2",
"jsonwebtoken": "^9.0.0",
"mailgun.js": "^3.7.3",
"microsoft-cognitiveservices-speech-sdk": "^1.24.1",
"mysql2": "^2.3.3",
"nocache": "3.0.4",
"passport": "^0.6.0",
"passport-http-bearer": "^1.0.1",
"pino": "^5.17.0",
@@ -46,8 +52,8 @@
"yamljs": "^0.3.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

@@ -1,6 +1,10 @@
const test = require('tape') ;
const ADMIN_TOKEN = '38700987-c7a4-4685-a5bb-af378f9734de';
const authAdmin = {bearer: ADMIN_TOKEN};
const SP_TOKEN = '38700987-c7a4-4685-a5bb-af378f9734ds';
const authSP = {bearer: ADMIN_TOKEN};
const ACC_TOKEN = '38700987-c7a4-4685-a5bb-af378f9734da';
const authAcc = {bearer: ADMIN_TOKEN};
const request = require('request-promise-native').defaults({
baseUrl: 'http://127.0.0.1:3000/v1'
});
@@ -9,6 +13,12 @@ const {
createServiceProvider,
createPhoneNumber,
deleteObjectBySid} = require('./utils');
const logger = require('../lib/logger');
const { pushBack } = 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);
@@ -219,6 +229,21 @@ test('account tests', async(t) => {
});
t.ok(result.statusCode === 201, 'successfully updated a call session limit to an account');
/* try to update an existing limit for an account giving a invalid sid */
try {
result = await request.post(`/Accounts/invalid-sid/Limits`, {
auth: authAdmin,
json: true,
resolveWithFullResponse: true,
body: {
category: 'voice_call_session',
quantity: 205
}
});
} catch (err) {
t.ok(err.statusCode === 400, 'returns 400 bad request if account sid param is not a valid uuid');
}
/* query all limits for an account */
result = await request.get(`/Accounts/${sid}/Limits`, {
auth: authAdmin,
@@ -232,7 +257,7 @@ test('account tests', async(t) => {
auth: authAdmin,
json: true,
});
//console.log(result);
// console.log(result);
t.ok(result.length === 1 && result[0].quantity === 205, 'successfully queried account limits by category');
/* delete call session limits for a service provider */
@@ -242,6 +267,31 @@ test('account tests', async(t) => {
});
t.ok(result.statusCode === 204, 'successfully deleted a call session limit for an account');
/* query account queues */
await pushBack(`queue:${sid}:test`, 'url1');
await pushBack(`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

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

View File

@@ -127,7 +127,7 @@ test('authentication tests', async(t) => {
sip_realm: 'sip.foo.bar'
}
});
t.ok(result.statusCode === 422 && result.body.msg === 'cannot update account from different service provider',
t.ok(result.statusCode === 403 && result.body.msg === 'insufficient permissions',
'service provider token B cannot be used to update account from service provider A');
/* cannot delete account from different service provider */
@@ -137,7 +137,7 @@ test('authentication tests', async(t) => {
simple: false,
json: true,
});
t.ok(result.statusCode === 422 && result.body.msg === 'cannot delete account from different service provider',
t.ok(result.statusCode === 403 && result.body.msg === 'insufficient permissions',
'service provider token B cannot be used to delete account from service provider A');
/* service provider token A can update account A1 */
@@ -179,7 +179,7 @@ test('authentication tests', async(t) => {
}
});
//console.log(`result: ${JSON.stringify(result)}`);
t.ok(result.statusCode === 422 && result.body.msg === 'insufficient permissions to create accounts',
t.ok(result.statusCode === 403 && result.body.msg === 'insufficient permissions',
'cannot create an account using an account-level token');
/* using account token we see one account */
@@ -200,8 +200,7 @@ test('authentication tests', async(t) => {
sip_realm: 'sip.foo.bar'
}
});
//console.log(`result: ${JSON.stringify(result)}`);
t.ok(result.statusCode === 422 && result.body.msg === 'insufficient privileges to update this account',
t.ok(result.statusCode === 403 && result.body.msg === 'insufficient permissions',
'cannot update account A2 using auth token for account A1');
/* can update an account using an appropriate account-level token */
@@ -251,7 +250,8 @@ test('authentication tests', async(t) => {
}
}
});
t.ok(result.statusCode === 400 && result.body.msg === 'insufficient privileges to create an application under the specified account',
//console.log(`result: ${JSON.stringify(result)}`);
t.ok(result.statusCode === 403 && result.body.msg === 'insufficient privileges',
'cannot create application for account A2 using service provider token B');
result = await request.post('/Applications', {

View File

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

View File

@@ -133,4 +133,14 @@ services:
- "3100:3000/tcp"
networks:
jambonz-api:
ipv4_address: 172.58.0.9
ipv4_address: 172.58.0.9
webhook-tts-scaffold:
image: jambonz/webhook-tts-test-scaffold:latest
ports:
- "3101:3000/tcp"
volumes:
- ./test-apps:/tmp
networks:
jambonz-api:
ipv4_address: 172.58.0.10

29
test/email_utils.js Normal file
View File

@@ -0,0 +1,29 @@
const test = require('tape');
const {emailSimpleText} = require('../lib/utils/email-utils');
const bent = require('bent');
const getJSON = bent('json')
const logger = {
debug: () =>{},
info: () => {}
}
test('email-test', async(t) => {
// Prepare env:
process.env.CUSTOM_EMAIL_VENDOR_URL = 'http://127.0.0.1:3101/custom_email_vendor';
process.env.CUSTOM_EMAIL_VENDOR_USERNAME = 'USERNAME';
process.env.CUSTOM_EMAIL_VENDOR_PASSWORD = 'PASSWORD';
await emailSimpleText(logger, 'test@gmail.com', 'subject', 'body text');
const obj = await getJSON(`http://127.0.0.1:3101/lastRequest/custom_email_vendor`);
t.ok(obj.headers['Content-Type'] == 'application/json');
t.ok(obj.headers.Authorization == 'Basic VVNFUk5BTUU6UEFTU1dPUkQ=');
t.ok(obj.body.from == 'jambonz Support <support@jambonz.org>');
t.ok(obj.body.to == 'test@gmail.com');
t.ok(obj.body.subject == 'subject');
t.ok(obj.body.text == 'body text');
process.env.CUSTOM_EMAIL_VENDOR_URL = null;
process.env.CUSTOM_EMAIL_VENDOR_USERNAME = null;
process.env.CUSTOM_EMAIL_VENDOR_PASSWORD = null;
});

View File

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

View File

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

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

@@ -12,7 +12,15 @@ require('./sbcs');
require('./ms-teams');
require('./speech-credentials');
require('./recent-calls');
require('./users');
require('./login');
require('./webapp_tests');
// require('./homer');
require('./call-test');
require('./password-settings');
require('./email_utils');
require('./system-information');
require('./lcr-carriers-set-entries');
require('./lcr-routes');
require('./lcrs');
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);
}
});

84
test/login.js Normal file
View File

@@ -0,0 +1,84 @@
const test = require('tape') ;
const jwt = require('jsonwebtoken');
const request = require('request-promise-native').defaults({
baseUrl: 'http://127.0.0.1:3000/v1'
});
const exec = require('child_process').exec ;
const {generateHashedPassword} = require('../lib/utils/password-utils');
process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
});
test('add an admin user', (t) => {
exec(`${__dirname}/../db/reset_admin_password.js`, (err, stdout, stderr) => {
console.log(stderr);
console.log(stdout);
if (err) return t.end(err);
t.pass('successfully added admin user');
t.end();
});
});
test('login tests', async(t) => {
const app = require('../app');
const password = 'abcde12345-';
try {
let result;
/* login as admin to get a jwt */
result = await request.post('/login', {
resolveWithFullResponse: true,
json: true,
body: {
username: 'admin',
password: 'admin',
}
});
t.ok(result.statusCode === 200 && result.body.token, 'successfully logged in as admin');
const maxAttempts = process.env.LOGIN_ATTEMPTS_MAX_RETRIES || 6;
const attempTime = process.env.LOGIN_ATTEMPTS_TIME || 1800;
for (let index = 0; index <= maxAttempts; index++) {
if (index === (maxAttempts - 1)) {
attemptResult = await request.post('/login', {
resolveWithFullResponse: true,
json: true,
body: {
username: 'admin',
password: 'adm',
}
}).catch(error => {
t.ok(error.response.statusCode === 403, `Maximum login attempts reached. Please try again in ${attempTime} seconds.`)
});
} else if (index < maxAttempts) {
attemptResult = await request.post('/login', {
resolveWithFullResponse: true,
json: true,
body: {
username: 'admin',
password: 'adm',
}
}).catch(error => t.ok(error.response.statusCode === 403));
} else {
attemptResult = await request.post('/login', {
resolveWithFullResponse: true,
json: true,
body: {
username: 'admin',
password: 'adm',
}
}).catch(error => t.ok(error.response.statusCode === 403, 'Maximum login attempts reached. Please try again later or reset your password.'));
}
}
} catch (err) {
console.error(err);
t.end(err);
}
});

71
test/password-settings.js Normal file
View File

@@ -0,0 +1,71 @@
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('password settings tests', async(t) => {
/* Check Default Password Settings */
result = await request.get('/PasswordSettings', {
auth: authAdmin,
json: true,
});
t.ok(result.min_password_length == 8 &&
!result.require_digit &&
!result.require_special_character, "default password settings is correct!")
/* Post New Password settings*/
result = await request.post('/PasswordSettings', {
auth: authAdmin,
json: true,
resolveWithFullResponse: true,
body: {
min_password_length: 15,
require_digit: 1,
require_special_character: 1
}
});
t.ok(result.statusCode === 201, 'successfully added a password settings');
/* Check Password Settings*/
result = await request.get('/PasswordSettings', {
auth: authAdmin,
json: true,
});
t.ok(result.min_password_length === 15 &&
result.require_digit === 1 &&
result.require_special_character === 1, 'successfully queried password settings');
/* Update Password settings*/
result = await request.post('/PasswordSettings', {
auth: authAdmin,
json: true,
resolveWithFullResponse: true,
body: {
min_password_length: 10,
require_special_character: 0
}
});
t.ok(result.statusCode === 201, 'successfully updated a password settings');
/* Check Password Settings After update*/
result = await request.get('/PasswordSettings', {
auth: authAdmin,
json: true,
});
t.ok(result.min_password_length === 10 &&
result.require_digit === 1 &&
result.require_special_character === 0, 'successfully queried password settings after updated');
});

View File

@@ -61,6 +61,16 @@ test('phone number tests', async(t) => {
});
t.ok(result.number === '16173333456' , 'successfully retrieved phone number by sid');
/* fail to query one phone number with invalid uuid */
try {
result = await request.get(`/PhoneNumbers/foobar`, {
auth: authAdmin,
json: true,
});
} catch (err) {
t.ok(err.statusCode === 400, 'returns 400 bad request if phone number sid param is not a valid uuid');
}
/* delete phone number */
result = await request.delete(`/PhoneNumbers/${sid}`, {
auth: authAdmin,

View File

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

View File

@@ -79,13 +79,14 @@ test('service provider tests', async(t) => {
}
});
//console.log(`result: ${JSON.stringify(result)}`);
t.ok(result.statusCode === 422, 'cannot add two service providers with the same name');
t.ok(result.statusCode === 422, 'cannot add two service providers with the same root domain');
/* query all service providers */
result = await request.get('/ServiceProviders', {
auth: authAdmin,
json: true,
});
//console.log(JSON.stringify(result));
t.ok(result.length === 2 , 'successfully queried all service providers');
/* query one service providers */
@@ -106,6 +107,20 @@ test('service provider tests', async(t) => {
});
t.ok(result.statusCode === 204, 'successfully updated service provider');
/* try to update service providers with invalid sid format*/
try {
result = await request.put(`/ServiceProviders/123`, {
auth: authAdmin,
json: true,
resolveWithFullResponse: true,
body: {
name: 'robb'
}
});
} catch (err) {
t.ok(err.statusCode === 400, 'returns 400 bad request if service provider sid param is not a valid uuid');
}
/* add an api key for a service provider */
result = await request.post(`/ApiKeys`, {
auth: authAdmin,

View File

@@ -75,7 +75,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

@@ -22,6 +22,24 @@ test('speech credentials tests', async(t) => {
const service_provider_sid = await createServiceProvider(request);
const account_sid = await createAccount(request, service_provider_sid);
/* return 400 if invalid sid param is used */
try {
result = await request.post(`/ServiceProviders/foobarbaz/SpeechCredentials`, {
resolveWithFullResponse: true,
simple: false,
auth: authAdmin,
json: true,
body: {
vendor: 'google',
service_key: jsonKey,
use_for_tts: true,
use_for_stt: true
}
});
} catch (err) {
t.ok(err.statusCode === 400, 'returns 400 bad request if service provider sid param is not a valid uuid');
}
/* add a speech credential to a service provider */
result = await request.post(`/ServiceProviders/${service_provider_sid}/SpeechCredentials`, {
resolveWithFullResponse: true,
@@ -30,7 +48,9 @@ test('speech credentials tests', async(t) => {
json: true,
body: {
vendor: 'google',
service_key: jsonKey
service_key: jsonKey,
use_for_tts: true,
use_for_stt: true
}
});
t.ok(result.statusCode === 201, 'successfully added a speech credential to service provider');
@@ -50,7 +70,10 @@ test('speech credentials tests', async(t) => {
await deleteObjectBySid(request, `/ServiceProviders/${service_provider_sid}/SpeechCredentials`, speech_credential_sid);
const token = jwt.sign({
account_sid
account_sid,
service_provider_sid,
scope: 'account',
permissions: ["PROVISION_USERS", "PROVISION_SERVICES", "VIEW_ONLY"]
}, process.env.JWT_SECRET, { expiresIn: '1h' });
const authUser = {bearer: token};
@@ -61,14 +84,16 @@ test('speech credentials tests', async(t) => {
json: true,
body: {
vendor: 'google',
service_key: jsonKey
service_key: jsonKey,
use_for_tts: true,
use_for_stt: true
}
});
t.ok(result.statusCode === 201, 'successfully added speech credential');
const sid1 = result.body.sid;
/* return 403 if invalid account is used */
result = await request.post(`/Accounts/foobarbaz/SpeechCredentials`, {
/* return 403 if invalid account is used - randomSid: bed7ae17-f8b4-4b74-9e5b-4f6318aae9c9 */
result = await request.post(`/Accounts/bed7ae17-f8b4-4b74-9e5b-4f6318aae9c9/SpeechCredentials`, {
resolveWithFullResponse: true,
simple: false,
auth: authUser,
@@ -95,12 +120,20 @@ test('speech credentials tests', async(t) => {
t.ok(result[0].vendor === 'google' && result.length === 1, 'successfully retrieved all speech credentials');
/* return 404 when deleting unknown credentials */
/* return 400 when deleting credentials with invalid uuid */
result = await request.delete(`/Accounts/${account_sid}/SpeechCredentials/foobarbaz`, {
auth: authUser,
resolveWithFullResponse: true,
simple: false
});
t.ok(result.statusCode === 400, 'return 400 when attempting to delete credential with invalid uuid');
/* return 404 when deleting unknown credentials - randomSid: bed7ae17-f8b4-4b74-9e5b-4f6318aae9c9 */
result = await request.delete(`/Accounts/${account_sid}/SpeechCredentials/`, {
auth: authUser,
resolveWithFullResponse: true,
simple: false
});
t.ok(result.statusCode === 404, 'return 404 when attempting to delete unknown credential');
/* delete the credential */
@@ -110,20 +143,20 @@ test('speech credentials tests', async(t) => {
});
t.ok(result.statusCode === 204, 'successfully deleted speech credential');
/* add a credential for microsoft */
if (process.env.MICROSOFT_API_KEY && process.env.MICROSOFT_REGION) {
/* add / test a credential for google */
if (process.env.GCP_JSON_KEY) {
result = await request.post(`/Accounts/${account_sid}/SpeechCredentials`, {
resolveWithFullResponse: true,
auth: authUser,
json: true,
body: {
vendor: 'microsoft',
vendor: 'google',
use_for_tts: true,
api_key: process.env.MICROSOFT_API_KEY,
region: process.env.MICROSOFT_REGION
use_for_stt: true,
service_key: process.env.GCP_JSON_KEY
}
});
t.ok(result.statusCode === 201, 'successfully added speech credential');
t.ok(result.statusCode === 201, 'successfully added speech credential for google');
const ms_sid = result.body.sid;
/* test the speech credential */
@@ -132,7 +165,66 @@ test('speech credentials tests', async(t) => {
auth: authUser,
json: true,
});
console.log(JSON.stringify(result));
//console.log(JSON.stringify(result));
t.ok(result.statusCode === 200 && result.body.tts.status === 'ok', 'successfully tested speech credential for google tts');
t.ok(result.statusCode === 200 && result.body.stt.status === 'ok', 'successfully tested speech credential for google stt');
}
/* add / test a credential for microsoft */
if (process.env.MICROSOFT_API_KEY && process.env.MICROSOFT_REGION) {
result = await request.post(`/Accounts/${account_sid}/SpeechCredentials`, {
resolveWithFullResponse: true,
auth: authUser,
json: true,
body: {
vendor: 'microsoft',
use_for_tts: true,
use_for_stt: true,
api_key: process.env.MICROSOFT_API_KEY,
region: process.env.MICROSOFT_REGION
}
});
t.ok(result.statusCode === 201, 'successfully added speech credential for microsoft');
const ms_sid = result.body.sid;
/* test the speech credential */
result = await request.get(`/Accounts/${account_sid}/SpeechCredentials/${ms_sid}/test`, {
resolveWithFullResponse: true,
auth: authUser,
json: true,
});
//console.log(JSON.stringify(result));
t.ok(result.statusCode === 200 && result.body.tts.status === 'ok', 'successfully tested speech credential for microsoft tts');
t.ok(result.statusCode === 200 && result.body.stt.status === 'ok', 'successfully tested speech credential for microsoft stt');
}
/* add / test a credential for AWS */
if (process.env.AWS_ACCESS_KEY_ID && process.env.AWS_SECRET_ACCESS_KEY && process.env.AWS_REGION) {
result = await request.post(`/Accounts/${account_sid}/SpeechCredentials`, {
resolveWithFullResponse: true,
auth: authUser,
json: true,
body: {
vendor: 'aws',
use_for_tts: true,
use_for_stt: true,
access_key_id: process.env.AWS_ACCESS_KEY_ID,
secret_access_key: process.env.AWS_SECRET_ACCESS_KEY,
aws_region: process.env.AWS_REGION
}
});
t.ok(result.statusCode === 201, 'successfully added speech credential for AWS');
const ms_sid = result.body.sid;
/* test the speech credential */
result = await request.get(`/Accounts/${account_sid}/SpeechCredentials/${ms_sid}/test`, {
resolveWithFullResponse: true,
auth: authUser,
json: true,
});
//console.log(JSON.stringify(result));
t.ok(result.statusCode === 200 && result.body.tts.status === 'ok', 'successfully tested speech credential for AWS tts');
t.ok(result.statusCode === 200 && result.body.stt.status === 'ok', 'successfully tested speech credential for AWS stt');
}
/* add a credential for wellsaid */
@@ -156,7 +248,8 @@ test('speech credentials tests', async(t) => {
auth: authUser,
json: true,
});
console.log(JSON.stringify(result));
//console.log(JSON.stringify(result));
t.ok(result.statusCode === 200 && result.body.tts.status === 'ok', 'successfully tested speech credential for wellsaid');
/* delete the credential */
result = await request.delete(`/Accounts/${account_sid}/SpeechCredentials/${ms_sid}`, {
@@ -166,9 +259,195 @@ test('speech credentials tests', async(t) => {
t.ok(result.statusCode === 204, 'successfully deleted speech credential');
}
/* add a credential for deepgram */
if (process.env.DEEPGRAM_API_KEY) {
result = await request.post(`/Accounts/${account_sid}/SpeechCredentials`, {
resolveWithFullResponse: true,
auth: authUser,
json: true,
body: {
vendor: 'deepgram',
use_for_stt: true,
api_key: process.env.DEEPGRAM_API_KEY
}
});
t.ok(result.statusCode === 201, 'successfully added speech credential for deepgram');
const ms_sid = result.body.sid;
/* test the speech credential */
result = await request.get(`/Accounts/${account_sid}/SpeechCredentials/${ms_sid}/test`, {
resolveWithFullResponse: true,
auth: authUser,
json: true,
});
//console.log(JSON.stringify(result));
t.ok(result.statusCode === 200 && result.body.stt.status === 'ok', 'successfully tested speech credential for deepgram');
/* delete the credential */
result = await request.delete(`/Accounts/${account_sid}/SpeechCredentials/${ms_sid}`, {
auth: authUser,
resolveWithFullResponse: true,
});
t.ok(result.statusCode === 204, 'successfully deleted speech credential');
}
/* add a credential for ibm tts */
if (process.env.IBM_TTS_API_KEY && process.env.IBM_TTS_REGION) {
result = await request.post(`/Accounts/${account_sid}/SpeechCredentials`, {
resolveWithFullResponse: true,
auth: authUser,
json: true,
body: {
vendor: 'ibm',
use_for_tts: true,
tts_api_key: process.env.IBM_TTS_API_KEY,
tts_region: process.env.IBM_TTS_REGION
}
});
t.ok(result.statusCode === 201, 'successfully added speech credential for ibm');
const ms_sid = result.body.sid;
/* test the speech credential */
result = await request.get(`/Accounts/${account_sid}/SpeechCredentials/${ms_sid}/test`, {
resolveWithFullResponse: true,
auth: authUser,
json: true,
});
//console.log(JSON.stringify(result));
t.ok(result.statusCode === 200 && result.body.tts.status === 'ok', 'successfully tested speech credential for ibm tts');
/* delete the credential */
result = await request.delete(`/Accounts/${account_sid}/SpeechCredentials/${ms_sid}`, {
auth: authUser,
resolveWithFullResponse: true,
});
t.ok(result.statusCode === 204, 'successfully deleted speech credential');
}
/* add a credential for ibm stt */
if (process.env.IBM_STT_API_KEY && process.env.IBM_STT_REGION) {
result = await request.post(`/Accounts/${account_sid}/SpeechCredentials`, {
resolveWithFullResponse: true,
auth: authUser,
json: true,
body: {
vendor: 'ibm',
use_for_stt: true,
stt_api_key: process.env.IBM_STT_API_KEY,
stt_region: process.env.IBM_STT_REGION
}
});
t.ok(result.statusCode === 201, 'successfully added speech credential for ibm');
const ms_sid = result.body.sid;
/* test the speech credential */
result = await request.get(`/Accounts/${account_sid}/SpeechCredentials/${ms_sid}/test`, {
resolveWithFullResponse: true,
auth: authUser,
json: true,
});
//console.log(JSON.stringify(result));
t.ok(result.statusCode === 200 && result.body.stt.status === 'ok', 'successfully tested speech credential for ibm stt');
/* delete the credential */
result = await request.delete(`/Accounts/${account_sid}/SpeechCredentials/${ms_sid}`, {
auth: authUser,
resolveWithFullResponse: true,
});
t.ok(result.statusCode === 204, 'successfully deleted speech credential');
}
/* add a credential for Siniox */
if (process.env.SONIOX_API_KEY) {
result = await request.post(`/Accounts/${account_sid}/SpeechCredentials`, {
resolveWithFullResponse: true,
auth: authUser,
json: true,
body: {
vendor: 'soniox',
use_for_stt: true,
api_key: process.env.SONIOX_API_KEY
}
});
t.ok(result.statusCode === 201, 'successfully added speech credential for soniox');
const ms_sid = result.body.sid;
/* test the speech credential */
result = await request.get(`/Accounts/${account_sid}/SpeechCredentials/${ms_sid}/test`, {
resolveWithFullResponse: true,
auth: authUser,
json: true,
});
console.log(JSON.stringify(result));
t.ok(result.statusCode === 200 && result.body.stt.status === 'ok', 'successfully tested speech credential for soniox');
/* delete the credential */
result = await request.delete(`/Accounts/${account_sid}/SpeechCredentials/${ms_sid}`, {
auth: authUser,
resolveWithFullResponse: true,
});
t.ok(result.statusCode === 204, 'successfully deleted speech credential');
}
/* add a credential for nvidia */
result = await request.post(`/Accounts/${account_sid}/SpeechCredentials`, {
resolveWithFullResponse: true,
auth: authUser,
json: true,
body: {
service_provider_sid: service_provider_sid,
vendor: 'nvidia',
use_for_stt: true,
use_for_tts: true,
riva_server_uri: "192.168.1.2:5060"
}
});
t.ok(result.statusCode === 201, 'successfully added speech credential for nvidia');
const ms_sid = result.body.sid;
/* test the speech credential */
result = await request.get(`/Accounts/${account_sid}/SpeechCredentials/${ms_sid}/test`, {
resolveWithFullResponse: true,
auth: authUser,
json: true,
});
// TODO Nvidia test.
t.ok(result.statusCode === 200 && result.body.stt.status === 'not tested', 'successfully tested speech credential for nvida stt');
/* delete the credential */
result = await request.delete(`/Accounts/${account_sid}/SpeechCredentials/${ms_sid}`, {
auth: authUser,
resolveWithFullResponse: true,
});
t.ok(result.statusCode === 204, 'successfully deleted speech credential');
/* add a credential for nuance */
result = await request.post(`/Accounts/${account_sid}/SpeechCredentials`, {
resolveWithFullResponse: true,
auth: authUser,
json: true,
body: {
vendor: 'nuance',
use_for_stt: true,
use_for_tts: true,
client_id: 'client_id',
secret: 'secret',
nuance_tts_uri: "192.168.1.2:5060",
nuance_stt_uri: "192.168.1.2:5061"
}
});
t.ok(result.statusCode === 201, 'successfully added speech credential for nuance');
const nuance_sid = result.body.sid;
/* delete the credential */
result = await request.delete(`/Accounts/${account_sid}/SpeechCredentials/${nuance_sid}`, {
auth: authUser,
resolveWithFullResponse: true,
});
t.ok(result.statusCode === 204, 'successfully deleted speech credential');
await deleteObjectBySid(request, '/Accounts', account_sid);
await deleteObjectBySid(request, '/ServiceProviders', service_provider_sid);
//t.end();
t.end();
}
catch (err) {
console.error(err);

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);
}
});

220
test/users.js Normal file
View File

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

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

@@ -43,6 +43,15 @@ test('voip carrier tests', async(t) => {
});
t.ok(result.name === 'daveh' , 'successfully retrieved voip carrier by sid');
/* fail to query one voip carriers with invalid uuid */
try {
result = await request.get(`/VoipCarriers/123`, {
auth: authAdmin,
json: true,
});
} catch (err) {
t.ok(err.statusCode === 400, 'returns 400 bad request if voip carrier sid param is not a valid uuid');
}
/* update voip carriers */
result = await request.put(`/VoipCarriers/${sid}`, {

View File

@@ -243,17 +243,20 @@ test('webapp tests', async(t) => {
t.ok(result.statusCode === 200 &&
result.body.phonenumbers.length === 1 && result.body.applications.length === 1, 'retrieves test number and application');
/* update user name */
result = await request.put(`/Users/foobar`, {
resolveWithFullResponse: true,
json: true,
simple: false,
auth: authUser,
body: {
name: 'Jane Doe'
}
});
t.ok(result.statusCode === 403, 'rejects attempt to update different user');
/* try to update user name passing an invalid uuid */
try {
await request.put(`/Users/foobar`, {
resolveWithFullResponse: true,
json: true,
simple: false,
auth: authUser,
body: {
name: 'Jane Doe'
}
});
} catch (error) {
t.ok(error.statusCode === 400, 'returns 400 bad request if user sid param is not a valid uuid');
}
/* update user name */
result = await request.put(`/Users/${user_sid}`, {