mirror of
https://github.com/jambonz/jambonz-api-server.git
synced 2026-01-25 02:08:24 +00:00
Compare commits
58 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b4237beeeb | ||
|
|
0406e42c19 | ||
|
|
533cd2f47d | ||
|
|
742884cc72 | ||
|
|
9fccfa2a73 | ||
|
|
3356b7302a | ||
|
|
9f533ed17c | ||
|
|
a0797a3a4c | ||
|
|
0b33ef0c2c | ||
|
|
71ecf453f8 | ||
|
|
494f1cf784 | ||
|
|
da74e2526a | ||
|
|
e35a03c7ad | ||
|
|
46fb9b8875 | ||
|
|
f9df2b3028 | ||
|
|
32ff023b14 | ||
|
|
f3d3afee73 | ||
|
|
3c8cbd97c5 | ||
|
|
eba9c98412 | ||
|
|
c2065ef787 | ||
|
|
307787526d | ||
|
|
3141646dfd | ||
|
|
cac6e2117d | ||
|
|
6d34d6f886 | ||
|
|
964afc1660 | ||
|
|
d09dca47b9 | ||
|
|
f3ec847474 | ||
|
|
cf7ce675f5 | ||
|
|
34895daf4f | ||
|
|
b06032b5f0 | ||
|
|
3486ff958c | ||
|
|
f79f96b884 | ||
|
|
2aa3d40268 | ||
|
|
148fc49f06 | ||
|
|
02806a109c | ||
|
|
077c791e37 | ||
|
|
4b70c6458a | ||
|
|
aadb0b15f2 | ||
|
|
3997f57365 | ||
|
|
c97874ed1f | ||
|
|
1dcc92a177 | ||
|
|
105aa16ffe | ||
|
|
a574045f8a | ||
|
|
af3d03bef9 | ||
|
|
5b1b50c3a3 | ||
|
|
ba431aeb35 | ||
|
|
36607b505f | ||
|
|
616a0b364d | ||
|
|
1b764b31e6 | ||
|
|
009396becc | ||
|
|
84305e30cc | ||
|
|
9c7f8b4e7b | ||
|
|
b2dce18c7a | ||
|
|
8f93b69af0 | ||
|
|
127b690ae2 | ||
|
|
3ad19eca3c | ||
|
|
efe7e22109 | ||
|
|
7a67ed704c |
60
.github/workflows/docker-publish-dbcreate.yml
vendored
60
.github/workflows/docker-publish-dbcreate.yml
vendored
@@ -2,16 +2,10 @@ name: Docker
|
||||
|
||||
on:
|
||||
push:
|
||||
# Publish `main` as Docker `latest` image.
|
||||
branches:
|
||||
- main
|
||||
|
||||
# Publish `v1.2.3` tags as releases.
|
||||
tags:
|
||||
- v*
|
||||
|
||||
env:
|
||||
IMAGE_NAME: db-create
|
||||
- '*'
|
||||
|
||||
jobs:
|
||||
push:
|
||||
@@ -20,32 +14,42 @@ jobs:
|
||||
if: github.event_name == 'push'
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Build image
|
||||
run: docker build . --file Dockerfile.db-create --tag $IMAGE_NAME
|
||||
|
||||
- name: Log into registry
|
||||
run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
|
||||
|
||||
- name: Push image
|
||||
- name: prepare tag
|
||||
id: prepare_tag
|
||||
run: |
|
||||
IMAGE_ID=ghcr.io/${{ github.repository_owner }}/$IMAGE_NAME
|
||||
IMAGE_ID=jambonz/db-create
|
||||
|
||||
# Change all uppercase to lowercase
|
||||
IMAGE_ID=$(echo $IMAGE_ID | tr '[A-Z]' '[a-z]')
|
||||
# Strip git ref prefix from version
|
||||
VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,')
|
||||
|
||||
# Strip git ref prefix from version
|
||||
VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,')
|
||||
# Strip "v" prefix from tag name
|
||||
[[ "${{ github.ref }}" == "refs/tags/"* ]] && VERSION=$(echo $VERSION | sed -e 's/^v//')
|
||||
|
||||
# Strip "v" prefix from tag name
|
||||
[[ "${{ github.ref }}" == "refs/tags/"* ]] && VERSION=$(echo $VERSION | sed -e 's/^v//')
|
||||
# Use Docker `latest` tag convention
|
||||
[ "$VERSION" == "main" ] && VERSION=latest
|
||||
|
||||
# Use Docker `latest` tag convention
|
||||
[ "$VERSION" == "main" ] && VERSION=latest
|
||||
echo IMAGE_ID=$IMAGE_ID
|
||||
echo VERSION=$VERSION
|
||||
|
||||
echo IMAGE_ID=$IMAGE_ID
|
||||
echo VERSION=$VERSION
|
||||
echo "image_id=$IMAGE_ID" >> $GITHUB_OUTPUT
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
docker tag $IMAGE_NAME $IMAGE_ID:$VERSION
|
||||
docker push $IMAGE_ID:$VERSION
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile.db-create
|
||||
push: true
|
||||
tags: ${{ steps.prepare_tag.outputs.image_id }}:${{ steps.prepare_tag.outputs.version }}
|
||||
build-args: |
|
||||
GITHUB_REPOSITORY=$GITHUB_REPOSITORY
|
||||
GITHUB_REF=$GITHUB_REF
|
||||
|
||||
59
.github/workflows/docker-publish.yml
vendored
59
.github/workflows/docker-publish.yml
vendored
@@ -2,16 +2,10 @@ name: Docker
|
||||
|
||||
on:
|
||||
push:
|
||||
# Publish `main` as Docker `latest` image.
|
||||
branches:
|
||||
- main
|
||||
|
||||
# Publish `v1.2.3` tags as releases.
|
||||
tags:
|
||||
- v*
|
||||
|
||||
env:
|
||||
IMAGE_NAME: api-server
|
||||
- '*'
|
||||
|
||||
jobs:
|
||||
push:
|
||||
@@ -20,32 +14,41 @@ jobs:
|
||||
if: github.event_name == 'push'
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Build image
|
||||
run: docker build . --file Dockerfile --tag $IMAGE_NAME
|
||||
|
||||
- name: Log into registry
|
||||
run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
|
||||
|
||||
- name: Push image
|
||||
- name: prepare tag
|
||||
id: prepare_tag
|
||||
run: |
|
||||
IMAGE_ID=ghcr.io/${{ github.repository_owner }}/$IMAGE_NAME
|
||||
IMAGE_ID=jambonz/api-server
|
||||
|
||||
# Change all uppercase to lowercase
|
||||
IMAGE_ID=$(echo $IMAGE_ID | tr '[A-Z]' '[a-z]')
|
||||
# Strip git ref prefix from version
|
||||
VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,')
|
||||
|
||||
# Strip git ref prefix from version
|
||||
VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,')
|
||||
# Strip "v" prefix from tag name
|
||||
[[ "${{ github.ref }}" == "refs/tags/"* ]] && VERSION=$(echo $VERSION | sed -e 's/^v//')
|
||||
|
||||
# Strip "v" prefix from tag name
|
||||
[[ "${{ github.ref }}" == "refs/tags/"* ]] && VERSION=$(echo $VERSION | sed -e 's/^v//')
|
||||
# Use Docker `latest` tag convention
|
||||
[ "$VERSION" == "main" ] && VERSION=latest
|
||||
|
||||
# Use Docker `latest` tag convention
|
||||
[ "$VERSION" == "main" ] && VERSION=latest
|
||||
echo IMAGE_ID=$IMAGE_ID
|
||||
echo VERSION=$VERSION
|
||||
|
||||
echo IMAGE_ID=$IMAGE_ID
|
||||
echo VERSION=$VERSION
|
||||
echo "image_id=$IMAGE_ID" >> $GITHUB_OUTPUT
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
docker tag $IMAGE_NAME $IMAGE_ID:$VERSION
|
||||
docker push $IMAGE_ID:$VERSION
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
tags: ${{ steps.prepare_tag.outputs.image_id }}:${{ steps.prepare_tag.outputs.version }}
|
||||
build-args: |
|
||||
GITHUB_REPOSITORY=$GITHUB_REPOSITORY
|
||||
GITHUB_REF=$GITHUB_REF
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM --platform=linux/amd64 node:18.14.1-alpine3.16 as base
|
||||
FROM --platform=linux/amd64 node:18.15-alpine3.16 as base
|
||||
|
||||
RUN apk --update --no-cache add --virtual .builds-deps build-base python3
|
||||
|
||||
|
||||
67
app.js
67
app.js
@@ -3,6 +3,7 @@ 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');
|
||||
@@ -12,7 +13,12 @@ assert.ok(process.env.JAMBONES_MYSQL_HOST &&
|
||||
process.env.JAMBONES_MYSQL_USER &&
|
||||
process.env.JAMBONES_MYSQL_PASSWORD &&
|
||||
process.env.JAMBONES_MYSQL_DATABASE, 'missing JAMBONES_MYSQL_XXX env vars');
|
||||
assert.ok(process.env.JAMBONES_REDIS_HOST, 'missing JAMBONES_REDIS_HOST env var');
|
||||
if (process.env.JAMBONES_REDIS_SENTINELS) {
|
||||
assert.ok(process.env.JAMBONES_REDIS_SENTINEL_MASTER_NAME,
|
||||
'missing JAMBONES_REDIS_SENTINEL_MASTER_NAME env var, JAMBONES_REDIS_SENTINEL_PASSWORD env var is optional');
|
||||
} else {
|
||||
assert.ok(process.env.JAMBONES_REDIS_HOST, 'missing JAMBONES_REDIS_HOST env var');
|
||||
}
|
||||
assert.ok(process.env.JAMBONES_TIME_SERIES_HOST, 'missing JAMBONES_TIME_SERIES_HOST env var');
|
||||
assert.ok(process.env.ENCRYPTION_SECRET || process.env.JWT_SECRET, 'missing ENCRYPTION_SECRET env var');
|
||||
assert.ok(process.env.JWT_SECRET, 'missing JWT_SECRET env var');
|
||||
@@ -32,15 +38,20 @@ const {
|
||||
retrieveCall,
|
||||
deleteCall,
|
||||
listCalls,
|
||||
listSortedSets,
|
||||
purgeCalls,
|
||||
retrieveSet,
|
||||
addKey,
|
||||
retrieveKey,
|
||||
deleteKey,
|
||||
incrKey,
|
||||
JAMBONES_REDIS_SENTINELS
|
||||
} = require('./lib/helpers/realtimedb-helpers');
|
||||
const {
|
||||
getTtsVoices
|
||||
} = require('@jambonz/speech-utils')({
|
||||
getTtsVoices,
|
||||
getTtsSize,
|
||||
purgeTtsCache
|
||||
} = require('@jambonz/speech-utils')(JAMBONES_REDIS_SENTINELS || {
|
||||
host: process.env.JAMBONES_REDIS_HOST,
|
||||
port: process.env.JAMBONES_REDIS_PORT || 6379
|
||||
}, logger);
|
||||
@@ -62,6 +73,8 @@ const {
|
||||
}, logger);
|
||||
const PORT = process.env.HTTP_PORT || 3000;
|
||||
const authStrategy = require('./lib/auth')(logger, retrieveKey);
|
||||
const {delayLoginMiddleware} = require('./lib/middleware');
|
||||
const Websocket = require('ws');
|
||||
|
||||
passport.use(authStrategy);
|
||||
|
||||
@@ -72,12 +85,16 @@ app.locals = {
|
||||
retrieveCall,
|
||||
deleteCall,
|
||||
listCalls,
|
||||
listSortedSets,
|
||||
purgeCalls,
|
||||
retrieveSet,
|
||||
addKey,
|
||||
incrKey,
|
||||
retrieveKey,
|
||||
deleteKey,
|
||||
getTtsVoices,
|
||||
getTtsSize,
|
||||
purgeTtsCache,
|
||||
lookupAppBySid,
|
||||
lookupAccountBySid,
|
||||
lookupAccountByPhoneNumber,
|
||||
@@ -108,6 +125,12 @@ const limiter = rateLimit({
|
||||
legacyHeaders: false, // Disable the `X-RateLimit-*` headers
|
||||
});
|
||||
|
||||
// Setup websocket for recording audio
|
||||
const recordWsServer = require('./lib/record');
|
||||
const wsServer = new Websocket.Server({ noServer: true });
|
||||
wsServer.setMaxListeners(0);
|
||||
wsServer.on('connection', recordWsServer.bind(null, logger));
|
||||
|
||||
if (process.env.JAMBONES_TRUST_PROXY) {
|
||||
const proxyCount = parseInt(process.env.JAMBONES_TRUST_PROXY);
|
||||
if (!isNaN(proxyCount) && proxyCount > 0) {
|
||||
@@ -122,9 +145,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(
|
||||
[
|
||||
@@ -146,7 +171,41 @@ app.use((err, req, res, next) => {
|
||||
});
|
||||
});
|
||||
logger.info(`listening for HTTP traffic on port ${PORT}`);
|
||||
app.listen(PORT);
|
||||
const server = app.listen(PORT);
|
||||
|
||||
|
||||
const isValidWsKey = (hdr) => {
|
||||
const username = process.env.JAMBONZ_RECORD_WS_USERNAME;
|
||||
const password = process.env.JAMBONZ_RECORD_WS_PASSWORD;
|
||||
const token = Buffer.from(`${username}:${password}`).toString('base64');
|
||||
const arr = /^Basic (.*)$/.exec(hdr);
|
||||
return arr[1] === token;
|
||||
};
|
||||
|
||||
server.on('upgrade', (request, socket, head) => {
|
||||
logger.debug({
|
||||
url: request.url,
|
||||
headers: request.headers,
|
||||
}, 'received upgrade request');
|
||||
|
||||
/* verify the path starts with /transcribe */
|
||||
if (!request.url.includes('/record/')) {
|
||||
logger.info(`unhandled path: ${request.url}`);
|
||||
return socket.write('HTTP/1.1 404 Not Found \r\n\r\n', () => socket.destroy());
|
||||
}
|
||||
|
||||
/* verify the api key */
|
||||
if (!isValidWsKey(request.headers['authorization'])) {
|
||||
logger.info(`invalid auth header: ${request.headers['authorization']}`);
|
||||
return socket.write('HTTP/1.1 403 Forbidden \r\n\r\n', () => socket.destroy());
|
||||
}
|
||||
|
||||
/* complete the upgrade */
|
||||
wsServer.handleUpgrade(request, socket, head, (ws) => {
|
||||
logger.info(`upgraded to websocket, url: ${request.url}`);
|
||||
wsServer.emit('connection', ws, request.url);
|
||||
});
|
||||
});
|
||||
|
||||
// purge old calls from active call set every 10 mins
|
||||
async function purge() {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
/* SQLEditor (MySQL (2))*/
|
||||
|
||||
SET FOREIGN_KEY_CHECKS=0;
|
||||
|
||||
DROP TABLE IF EXISTS account_static_ips;
|
||||
@@ -14,8 +13,12 @@ DROP TABLE IF EXISTS beta_invite_codes;
|
||||
|
||||
DROP TABLE IF EXISTS call_routes;
|
||||
|
||||
DROP TABLE IF EXISTS clients;
|
||||
|
||||
DROP TABLE IF EXISTS dns_records;
|
||||
|
||||
DROP TABLE IF EXISTS lcr;
|
||||
|
||||
DROP TABLE IF EXISTS lcr_carrier_set_entry;
|
||||
|
||||
DROP TABLE IF EXISTS lcr_routes;
|
||||
@@ -52,6 +55,8 @@ DROP TABLE IF EXISTS smpp_addresses;
|
||||
|
||||
DROP TABLE IF EXISTS speech_credentials;
|
||||
|
||||
DROP TABLE IF EXISTS system_information;
|
||||
|
||||
DROP TABLE IF EXISTS users;
|
||||
|
||||
DROP TABLE IF EXISTS smpp_gateways;
|
||||
@@ -124,6 +129,16 @@ application_sid CHAR(36) NOT NULL,
|
||||
PRIMARY KEY (call_route_sid)
|
||||
) COMMENT='a regex-based pattern match for call routing';
|
||||
|
||||
CREATE TABLE clients
|
||||
(
|
||||
client_sid CHAR(36) NOT NULL UNIQUE ,
|
||||
account_sid CHAR(36) NOT NULL,
|
||||
is_active BOOLEAN NOT NULL DEFAULT 1,
|
||||
username VARCHAR(64),
|
||||
password VARCHAR(1024),
|
||||
PRIMARY KEY (client_sid)
|
||||
);
|
||||
|
||||
CREATE TABLE dns_records
|
||||
(
|
||||
dns_record_sid CHAR(36) NOT NULL UNIQUE ,
|
||||
@@ -136,11 +151,23 @@ PRIMARY KEY (dns_record_sid)
|
||||
CREATE TABLE lcr_routes
|
||||
(
|
||||
lcr_route_sid CHAR(36),
|
||||
lcr_sid CHAR(36) NOT NULL,
|
||||
regex VARCHAR(32) NOT NULL COMMENT 'regex-based pattern match against dialed number, used for LCR routing of PSTN calls',
|
||||
description VARCHAR(1024),
|
||||
priority INTEGER NOT NULL UNIQUE COMMENT 'lower priority routes are attempted first',
|
||||
priority INTEGER NOT NULL COMMENT 'lower priority routes are attempted first',
|
||||
PRIMARY KEY (lcr_route_sid)
|
||||
) COMMENT='Least cost routing table';
|
||||
) COMMENT='An ordered list of digit patterns in an LCR table. The patterns are tested in sequence until one matches';
|
||||
|
||||
CREATE TABLE lcr
|
||||
(
|
||||
lcr_sid CHAR(36) NOT NULL UNIQUE ,
|
||||
name VARCHAR(64) COMMENT 'User-assigned name for this LCR table',
|
||||
is_active BOOLEAN NOT NULL DEFAULT 1,
|
||||
default_carrier_set_entry_sid CHAR(36) COMMENT 'default carrier/route to use when no digit match based results are found.',
|
||||
service_provider_sid CHAR(36),
|
||||
account_sid CHAR(36),
|
||||
PRIMARY KEY (lcr_sid)
|
||||
) COMMENT='An LCR (least cost routing) table that is used by a service provider or account to make decisions about routing outbound calls when multiple carriers are available.';
|
||||
|
||||
CREATE TABLE password_settings
|
||||
(
|
||||
@@ -248,7 +275,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)
|
||||
);
|
||||
|
||||
@@ -307,6 +337,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 ,
|
||||
@@ -357,6 +394,7 @@ 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';
|
||||
|
||||
@@ -385,7 +423,7 @@ PRIMARY KEY (smpp_gateway_sid)
|
||||
CREATE TABLE phone_numbers
|
||||
(
|
||||
phone_number_sid CHAR(36) UNIQUE ,
|
||||
number VARCHAR(132) NOT NULL UNIQUE ,
|
||||
number VARCHAR(132) NOT NULL,
|
||||
voip_carrier_sid CHAR(36),
|
||||
account_sid CHAR(36),
|
||||
application_sid CHAR(36),
|
||||
@@ -403,6 +441,7 @@ inbound BOOLEAN NOT NULL COMMENT 'if true, whitelist this IP to allow inbound ca
|
||||
outbound BOOLEAN NOT NULL COMMENT 'if true, include in least-cost routing when placing calls to the PSTN',
|
||||
voip_carrier_sid CHAR(36) NOT NULL,
|
||||
is_active BOOLEAN NOT NULL DEFAULT 1,
|
||||
protocol ENUM('udp','tcp','tls', 'tls/srtp') DEFAULT 'udp' COMMENT 'Outbound call protocol',
|
||||
PRIMARY KEY (sip_gateway_sid)
|
||||
) COMMENT='A whitelisted sip gateway used for origination/termination';
|
||||
|
||||
@@ -442,6 +481,7 @@ speech_synthesis_voice VARCHAR(64),
|
||||
speech_recognizer_vendor VARCHAR(64) NOT NULL DEFAULT 'google',
|
||||
speech_recognizer_language VARCHAR(64) NOT NULL DEFAULT 'en-US',
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
record_all_calls BOOLEAN NOT NULL DEFAULT false,
|
||||
PRIMARY KEY (application_sid)
|
||||
) COMMENT='A defined set of behaviors to be applied to phone calls ';
|
||||
|
||||
@@ -479,6 +519,9 @@ subspace_client_secret VARCHAR(255),
|
||||
subspace_sip_teleport_id VARCHAR(255),
|
||||
subspace_sip_teleport_destinations VARCHAR(255),
|
||||
siprec_hook_sid CHAR(36),
|
||||
record_all_calls BOOLEAN NOT NULL DEFAULT false,
|
||||
record_format VARCHAR(16) NOT NULL DEFAULT 'mp3',
|
||||
bucket_credential VARCHAR(8192) COMMENT 'credential used to authenticate with storage service',
|
||||
PRIMARY KEY (account_sid)
|
||||
) COMMENT='An enterprise that uses the platform for comm services';
|
||||
|
||||
@@ -499,9 +542,20 @@ ALTER TABLE call_routes ADD FOREIGN KEY account_sid_idxfk_3 (account_sid) REFERE
|
||||
|
||||
ALTER TABLE call_routes ADD FOREIGN KEY application_sid_idxfk (application_sid) REFERENCES applications (application_sid);
|
||||
|
||||
CREATE INDEX client_sid_idx ON clients (client_sid);
|
||||
ALTER TABLE clients ADD CONSTRAINT account_sid_idxfk_13 FOREIGN KEY account_sid_idxfk_13 (account_sid) REFERENCES accounts (account_sid);
|
||||
|
||||
CREATE INDEX dns_record_sid_idx ON dns_records (dns_record_sid);
|
||||
ALTER TABLE dns_records ADD FOREIGN KEY account_sid_idxfk_4 (account_sid) REFERENCES accounts (account_sid);
|
||||
|
||||
CREATE INDEX lcr_sid_idx ON lcr_routes (lcr_sid);
|
||||
ALTER TABLE lcr_routes ADD FOREIGN KEY lcr_sid_idxfk (lcr_sid) REFERENCES lcr (lcr_sid);
|
||||
|
||||
CREATE INDEX lcr_sid_idx ON lcr (lcr_sid);
|
||||
ALTER TABLE lcr ADD FOREIGN KEY default_carrier_set_entry_sid_idxfk (default_carrier_set_entry_sid) REFERENCES lcr_carrier_set_entry (lcr_carrier_set_entry_sid);
|
||||
|
||||
CREATE INDEX service_provider_sid_idx ON lcr (service_provider_sid);
|
||||
CREATE INDEX account_sid_idx ON lcr (account_sid);
|
||||
CREATE INDEX permission_sid_idx ON permissions (permission_sid);
|
||||
CREATE INDEX predefined_carrier_sid_idx ON predefined_carriers (predefined_carrier_sid);
|
||||
CREATE INDEX predefined_sip_gateway_sid_idx ON predefined_sip_gateways (predefined_sip_gateway_sid);
|
||||
@@ -593,6 +647,8 @@ CREATE INDEX smpp_gateway_sid_idx ON smpp_gateways (smpp_gateway_sid);
|
||||
CREATE INDEX voip_carrier_sid_idx ON smpp_gateways (voip_carrier_sid);
|
||||
ALTER TABLE smpp_gateways ADD FOREIGN KEY voip_carrier_sid_idxfk (voip_carrier_sid) REFERENCES voip_carriers (voip_carrier_sid);
|
||||
|
||||
CREATE UNIQUE INDEX phone_numbers_unique_idx_voip_carrier_number ON phone_numbers (number,voip_carrier_sid);
|
||||
|
||||
CREATE INDEX phone_number_sid_idx ON phone_numbers (phone_number_sid);
|
||||
CREATE INDEX number_idx ON phone_numbers (number);
|
||||
CREATE INDEX voip_carrier_sid_idx ON phone_numbers (voip_carrier_sid);
|
||||
@@ -647,5 +703,4 @@ ALTER TABLE accounts ADD FOREIGN KEY queue_event_hook_sid_idxfk (queue_event_hoo
|
||||
ALTER TABLE accounts ADD FOREIGN KEY device_calling_application_sid_idxfk (device_calling_application_sid) REFERENCES applications (application_sid);
|
||||
|
||||
ALTER TABLE accounts ADD FOREIGN KEY siprec_hook_sid_idxfk (siprec_hook_sid) REFERENCES applications (application_sid);
|
||||
|
||||
SET FOREIGN_KEY_CHECKS=1;
|
||||
407
db/jambones.sqs
407
db/jambones.sqs
File diff suppressed because one or more lines are too long
@@ -27,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
|
||||
@@ -79,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
|
||||
|
||||
@@ -68,14 +68,12 @@ VALUES
|
||||
('d2ccfcb1-9198-4fe9-a0ca-6e49395837c4', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '54.172.60.0', 30, 5060, 1, 0),
|
||||
('6b1d0032-4430-41f1-87c6-f22233d394ef', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '54.244.51.0', 30, 5060, 1, 0),
|
||||
('0de40217-8bd5-4aa8-a9fd-1994282953c6', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '54.171.127.192', 30, 5060, 1, 0),
|
||||
('48b108e3-1ce7-4f18-a4cb-e41e63688bdf', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '54.171.127.193', 30, 5060, 1, 0),
|
||||
('d9131a69-fe44-4c2a-ba82-4adc81f628dd', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '54.171.127.194', 30, 5060, 1, 0),
|
||||
('34a6a311-4bd6-49ca-aa77-edd3cb92c6e1', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '54.171.127.195', 30, 5060, 1, 0),
|
||||
('37bc0b20-b53c-4c31-95a6-f82b1c3713e3', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '35.156.191.128', 30, 5060, 1, 0),
|
||||
('39791f4e-b612-4882-a37e-e92711a39f3f', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '54.65.63.192', 30, 5060, 1, 0),
|
||||
('81a0c8cb-a33e-42da-8f20-99083da6f02f', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '54.252.254.64', 30, 5060, 1, 0),
|
||||
('eeeef07a-46b8-4ffe-a4f2-04eb32ca889e', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '54.169.127.128', 30, 5060, 1, 0),
|
||||
('fbb6c194-4b68-4dff-9b42-52412be1c39e', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '177.71.206.192', 30, 5060, 1, 0),
|
||||
('973e7824-0cf3-4645-88e4-d2460ddb8577', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '168.86.128.0', 18, 5060, 1, 0),
|
||||
('3ed1dd12-e1a7-44ff-811a-3cc5dc13dc72', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '<your-domain>.pstn.twilio.com', 32, 5060, 0, 1);
|
||||
|
||||
-- voxbone gateways
|
||||
|
||||
@@ -53,6 +53,7 @@ VALUES
|
||||
('81a0c8cb-a33e-42da-8f20-99083da6f02f', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '54.252.254.64', 30, 5060, 1, 0),
|
||||
('eeeef07a-46b8-4ffe-a4f2-04eb32ca889e', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '54.169.127.128', 30, 5060, 1, 0),
|
||||
('fbb6c194-4b68-4dff-9b42-52412be1c39e', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '177.71.206.192', 30, 5060, 1, 0),
|
||||
('973e7824-0cf3-4645-88e4-d2460ddb8577', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '168.86.128.0', 18, 5060, 1, 0),
|
||||
('3ed1dd12-e1a7-44ff-811a-3cc5dc13dc72', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '<your-domain>.pstn.twilio.com', 32, 5060, 0, 1);
|
||||
|
||||
-- voxbone gateways
|
||||
|
||||
@@ -87,6 +87,77 @@ const sql = {
|
||||
'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: [
|
||||
'SET FOREIGN_KEY_CHECKS=0',
|
||||
'ALTER TABLE `voip_carriers` ADD COLUMN `register_status` VARCHAR(4096)',
|
||||
'ALTER TABLE `sbc_addresses` ADD COLUMN `last_updated` DATETIME',
|
||||
'ALTER TABLE `sbc_addresses` ADD COLUMN `tls_port` INTEGER',
|
||||
'ALTER TABLE `sbc_addresses` ADD COLUMN `wss_port` INTEGER',
|
||||
`CREATE TABLE system_information
|
||||
(
|
||||
domain_name VARCHAR(255),
|
||||
sip_domain_name VARCHAR(255),
|
||||
monitoring_domain_name VARCHAR(255)
|
||||
)`,
|
||||
'DROP TABLE IF EXISTS `lcr_routes`',
|
||||
'DROP TABLE IF EXISTS `lcr_carrier_set_entry`',
|
||||
`CREATE TABLE lcr_routes
|
||||
(
|
||||
lcr_route_sid CHAR(36),
|
||||
lcr_sid CHAR(36) NOT NULL,
|
||||
regex VARCHAR(32) NOT NULL COMMENT 'regex-based pattern match against dialed number, used for LCR routing of PSTN calls',
|
||||
description VARCHAR(1024),
|
||||
priority INTEGER NOT NULL COMMENT 'lower priority routes are attempted first',
|
||||
PRIMARY KEY (lcr_route_sid)
|
||||
)`,
|
||||
`CREATE TABLE lcr
|
||||
(
|
||||
lcr_sid CHAR(36) NOT NULL UNIQUE ,
|
||||
name VARCHAR(64) COMMENT 'User-assigned name for this LCR table',
|
||||
is_active BOOLEAN NOT NULL DEFAULT 1,
|
||||
default_carrier_set_entry_sid CHAR(36) COMMENT 'default carrier/route to use when no digit match based results are found.',
|
||||
service_provider_sid CHAR(36),
|
||||
account_sid CHAR(36),
|
||||
PRIMARY KEY (lcr_sid)
|
||||
)`,
|
||||
`CREATE TABLE lcr_carrier_set_entry
|
||||
(
|
||||
lcr_carrier_set_entry_sid CHAR(36),
|
||||
workload INTEGER NOT NULL DEFAULT 1 COMMENT 'represents a proportion of traffic to send through the associated carrier; can be used for load balancing traffic across carriers with a common priority for a destination',
|
||||
lcr_route_sid CHAR(36) NOT NULL,
|
||||
voip_carrier_sid CHAR(36) NOT NULL,
|
||||
priority INTEGER NOT NULL DEFAULT 0 COMMENT 'lower priority carriers are attempted first',
|
||||
PRIMARY KEY (lcr_carrier_set_entry_sid)
|
||||
)`,
|
||||
'CREATE INDEX lcr_sid_idx ON lcr_routes (lcr_sid)',
|
||||
'ALTER TABLE lcr_routes ADD FOREIGN KEY lcr_sid_idxfk (lcr_sid) REFERENCES lcr (lcr_sid)',
|
||||
'CREATE INDEX lcr_sid_idx ON lcr (lcr_sid)',
|
||||
'ALTER TABLE lcr ADD FOREIGN KEY default_carrier_set_entry_sid_idxfk (default_carrier_set_entry_sid) REFERENCES lcr_carrier_set_entry (lcr_carrier_set_entry_sid)',
|
||||
'CREATE INDEX service_provider_sid_idx ON lcr (service_provider_sid)',
|
||||
'CREATE INDEX account_sid_idx ON lcr (account_sid)',
|
||||
'ALTER TABLE lcr_carrier_set_entry ADD FOREIGN KEY lcr_route_sid_idxfk (lcr_route_sid) REFERENCES lcr_routes (lcr_route_sid)',
|
||||
'ALTER TABLE lcr_carrier_set_entry ADD FOREIGN KEY voip_carrier_sid_idxfk_3 (voip_carrier_sid) REFERENCES voip_carriers (voip_carrier_sid)',
|
||||
'SET FOREIGN_KEY_CHECKS=1',
|
||||
],
|
||||
8004: [
|
||||
'alter table accounts add column record_all_calls BOOLEAN NOT NULL DEFAULT false',
|
||||
'alter table accounts add column bucket_credential VARCHAR(8192)',
|
||||
'alter table accounts add column record_format VARCHAR(16) NOT NULL DEFAULT \'mp3\'',
|
||||
'alter table applications add column record_all_calls BOOLEAN NOT NULL DEFAULT false',
|
||||
'alter table phone_numbers DROP INDEX number',
|
||||
'create unique index phone_numbers_unique_idx_voip_carrier_number ON phone_numbers (number,voip_carrier_sid)',
|
||||
`CREATE TABLE clients
|
||||
(
|
||||
client_sid CHAR(36) NOT NULL UNIQUE ,
|
||||
account_sid CHAR(36) NOT NULL,
|
||||
is_active BOOLEAN NOT NULL DEFAULT 1,
|
||||
username VARCHAR(64),
|
||||
password VARCHAR(1024),
|
||||
PRIMARY KEY (client_sid)
|
||||
)`,
|
||||
'CREATE INDEX client_sid_idx ON clients (client_sid)',
|
||||
'ALTER TABLE clients ADD CONSTRAINT account_sid_idxfk_13 FOREIGN KEY account_sid_idxfk_13 (account_sid) REFERENCES accounts (account_sid)'
|
||||
]
|
||||
};
|
||||
|
||||
@@ -116,6 +187,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']);
|
||||
if (val < 8004) upgrades.push(...sql['8004']);
|
||||
|
||||
// perform all upgrades
|
||||
logger.info({upgrades}, 'applying schema upgrades..');
|
||||
|
||||
@@ -1,16 +1,39 @@
|
||||
const logger = require('../logger');
|
||||
|
||||
const JAMBONES_REDIS_SENTINELS = process.env.JAMBONES_REDIS_SENTINELS ? {
|
||||
sentinels: process.env.JAMBONES_REDIS_SENTINELS.split(',').map((sentinel) => {
|
||||
let host, port = 26379;
|
||||
if (sentinel.includes(':')) {
|
||||
const arr = sentinel.split(':');
|
||||
host = arr[0];
|
||||
port = parseInt(arr[1], 10);
|
||||
} else {
|
||||
host = sentinel;
|
||||
}
|
||||
return {host, port};
|
||||
}),
|
||||
name: process.env.JAMBONES_REDIS_SENTINEL_MASTER_NAME,
|
||||
...(process.env.JAMBONES_REDIS_SENTINEL_PASSWORD && {
|
||||
password: process.env.JAMBONES_REDIS_SENTINEL_PASSWORD
|
||||
}),
|
||||
...(process.env.JAMBONES_REDIS_SENTINEL_USERNAME && {
|
||||
username: process.env.JAMBONES_REDIS_SENTINEL_USERNAME
|
||||
})
|
||||
} : null;
|
||||
|
||||
const {
|
||||
retrieveCall,
|
||||
deleteCall,
|
||||
listCalls,
|
||||
listSortedSets,
|
||||
purgeCalls,
|
||||
retrieveSet,
|
||||
addKey,
|
||||
retrieveKey,
|
||||
deleteKey,
|
||||
incrKey,
|
||||
client: redisClient,
|
||||
} = require('@jambonz/realtimedb-helpers')({
|
||||
} = require('@jambonz/realtimedb-helpers')(JAMBONES_REDIS_SENTINELS || {
|
||||
host: process.env.JAMBONES_REDIS_HOST || 'localhost',
|
||||
port: process.env.JAMBONES_REDIS_PORT || 6379
|
||||
}, logger);
|
||||
@@ -19,10 +42,13 @@ module.exports = {
|
||||
retrieveCall,
|
||||
deleteCall,
|
||||
listCalls,
|
||||
listSortedSets,
|
||||
purgeCalls,
|
||||
retrieveSet,
|
||||
addKey,
|
||||
retrieveKey,
|
||||
deleteKey,
|
||||
redisClient,
|
||||
incrKey,
|
||||
JAMBONES_REDIS_SENTINELS
|
||||
};
|
||||
|
||||
32
lib/middleware.js
Normal file
32
lib/middleware.js
Normal 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
|
||||
};
|
||||
@@ -4,7 +4,7 @@ const {getMysqlConnection} = require('../db');
|
||||
const {promisePool} = require('../db');
|
||||
const { v4: uuid } = require('uuid');
|
||||
|
||||
const {encrypt} = require('../utils/encrypt-decrypt');
|
||||
const {encrypt, decrypt} = require('../utils/encrypt-decrypt');
|
||||
|
||||
const retrieveSql = `SELECT * from accounts acc
|
||||
LEFT JOIN webhooks AS rh
|
||||
@@ -55,6 +55,13 @@ WHERE account_sid = ?
|
||||
AND effective_end_date IS NULL
|
||||
AND pending = 0`;
|
||||
|
||||
const extractBucketCredential = (obj) => {
|
||||
const {bucket_credential} = obj;
|
||||
if (bucket_credential) {
|
||||
obj.bucket_credential = JSON.parse(decrypt(bucket_credential));
|
||||
}
|
||||
};
|
||||
|
||||
function transmogrifyResults(results) {
|
||||
return results.map((row) => {
|
||||
const obj = row.acc;
|
||||
@@ -75,6 +82,8 @@ function transmogrifyResults(results) {
|
||||
else obj.queue_event_hook = null;
|
||||
delete obj.queue_event_hook_sid;
|
||||
|
||||
extractBucketCredential(obj);
|
||||
|
||||
return obj;
|
||||
});
|
||||
}
|
||||
@@ -238,7 +247,6 @@ class Account extends Model {
|
||||
}));
|
||||
return account_subscription_sid;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Account.table = 'accounts';
|
||||
@@ -318,6 +326,18 @@ Account.fields = [
|
||||
name: 'siprec_hook_sid',
|
||||
type: 'string',
|
||||
},
|
||||
{
|
||||
name: 'record_all_calls',
|
||||
type: 'number'
|
||||
},
|
||||
{
|
||||
name: 'record_format',
|
||||
type: 'string'
|
||||
},
|
||||
{
|
||||
name: 'bucket_credential',
|
||||
type: 'string'
|
||||
}
|
||||
];
|
||||
|
||||
module.exports = Account;
|
||||
|
||||
@@ -120,6 +120,10 @@ Application.fields = [
|
||||
{
|
||||
name: 'messaging_hook_sid',
|
||||
type: 'string',
|
||||
},
|
||||
{
|
||||
name: 'record_all_calls',
|
||||
type: 'number',
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
58
lib/models/client.js
Normal file
58
lib/models/client.js
Normal file
@@ -0,0 +1,58 @@
|
||||
const Model = require('./model');
|
||||
const {promisePool} = require('../db');
|
||||
|
||||
class Client extends Model {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
static async retrieveAllByAccountSid(account_sid) {
|
||||
const sql = `SELECT * FROM ${this.table} WHERE account_sid = ?`;
|
||||
const [rows] = await promisePool.query(sql, account_sid);
|
||||
return rows;
|
||||
}
|
||||
|
||||
static async retrieveAllByServiceProviderSid(service_provider_sid) {
|
||||
const sql = `SELECT c.client_sid, c.account_sid, c.is_active, c.username, c.hashed_password
|
||||
FROM ${this.table} AS c LEFT JOIN accounts AS acc ON c.account_sid = acc.account_sid
|
||||
LEFT JOIN service_providers AS sp ON sp.service_provider_sid = accs.service_provider_sid
|
||||
WHERE sp.service_provider_sid = ?`;
|
||||
const [rows] = await promisePool.query(sql, service_provider_sid);
|
||||
return rows;
|
||||
}
|
||||
|
||||
static async retrieveByAccountSidAndUserName(account_sid, username) {
|
||||
const sql = `SELECT * FROM ${this.table} WHERE account_sid = ? AND username = ?`;
|
||||
const [rows] = await promisePool.query(sql, [account_sid, username]);
|
||||
return rows;
|
||||
}
|
||||
}
|
||||
|
||||
Client.table = 'clients';
|
||||
Client.fields = [
|
||||
{
|
||||
name: 'client_sid',
|
||||
type: 'string',
|
||||
primaryKey: true
|
||||
},
|
||||
{
|
||||
name: 'account_sid',
|
||||
type: 'string',
|
||||
required: true
|
||||
},
|
||||
{
|
||||
name: 'is_active',
|
||||
type: 'number'
|
||||
},
|
||||
{
|
||||
name: 'username',
|
||||
type: 'string',
|
||||
required: true
|
||||
},
|
||||
{
|
||||
name: 'password',
|
||||
type: 'string'
|
||||
}
|
||||
];
|
||||
|
||||
module.exports = Client;
|
||||
47
lib/models/lcr-carrier-set-entry.js
Normal file
47
lib/models/lcr-carrier-set-entry.js
Normal 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
54
lib/models/lcr-route.js
Normal 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
54
lib/models/lcr.js
Normal 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;
|
||||
@@ -1,6 +1,6 @@
|
||||
const Model = require('./model');
|
||||
const {promisePool} = require('../db');
|
||||
const sql = 'SELECT * from phone_numbers WHERE account_sid = ?';
|
||||
const sql = 'SELECT * from phone_numbers WHERE account_sid = ? ORDER BY number';
|
||||
const sqlSP = `SELECT *
|
||||
FROM phone_numbers
|
||||
WHERE account_sid IN
|
||||
@@ -8,7 +8,7 @@ WHERE account_sid IN
|
||||
SELECT account_sid
|
||||
FROM accounts
|
||||
WHERE service_provider_sid = ?
|
||||
)`;
|
||||
) ORDER BY number`;
|
||||
|
||||
class PhoneNumber extends Model {
|
||||
constructor() {
|
||||
|
||||
@@ -89,6 +89,10 @@ ServiceProvider.fields = [
|
||||
{
|
||||
name: 'ms_teams_fqdn',
|
||||
type: 'string',
|
||||
},
|
||||
{
|
||||
name: 'lcr_sid',
|
||||
type: 'string'
|
||||
}
|
||||
|
||||
];
|
||||
|
||||
@@ -58,6 +58,10 @@ SipGateway.fields = [
|
||||
{
|
||||
name: 'application_sid',
|
||||
type: 'string'
|
||||
},
|
||||
{
|
||||
name: 'protocol',
|
||||
type: 'string'
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
const Model = require('./model');
|
||||
const {promisePool} = require('../db');
|
||||
const retrieveSql = 'SELECT * from speech_credentials WHERE account_sid = ?';
|
||||
const retrieveSqlForSP = 'SELECT * from speech_credentials WHERE service_provider_sid = ?';
|
||||
const retrieveSqlForSP = 'SELECT * from speech_credentials WHERE service_provider_sid = ? and account_sid is null';
|
||||
|
||||
class SpeechCredential extends Model {
|
||||
constructor() {
|
||||
|
||||
38
lib/models/system-information.js
Normal file
38
lib/models/system-information.js
Normal 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;
|
||||
@@ -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'
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
54
lib/record/encoder.js
Normal file
54
lib/record/encoder.js
Normal file
@@ -0,0 +1,54 @@
|
||||
const { Transform } = require('stream');
|
||||
const lamejs = require('@jambonz/lamejs');
|
||||
|
||||
class PCMToMP3Encoder extends Transform {
|
||||
constructor(options) {
|
||||
super(options);
|
||||
|
||||
const channels = options.channels || 1;
|
||||
const sampleRate = options.sampleRate || 8000;
|
||||
const bitRate = options.bitRate || 128;
|
||||
|
||||
this.encoder = new lamejs.Mp3Encoder(channels, sampleRate, bitRate);
|
||||
this.channels = channels;
|
||||
}
|
||||
|
||||
_transform(chunk, encoding, callback) {
|
||||
// Convert chunk buffer into Int16Array for lamejs
|
||||
const samples = new Int16Array(chunk.buffer, chunk.byteOffset, chunk.length / 2);
|
||||
|
||||
// Split input samples into left and right channel arrays if stereo
|
||||
let leftChannel, rightChannel;
|
||||
if (this.channels === 2) {
|
||||
leftChannel = new Int16Array(samples.length / 2);
|
||||
rightChannel = new Int16Array(samples.length / 2);
|
||||
|
||||
for (let i = 0; i < samples.length; i += 2) {
|
||||
leftChannel[i / 2] = samples[i];
|
||||
rightChannel[i / 2] = samples[i + 1];
|
||||
}
|
||||
} else {
|
||||
leftChannel = samples;
|
||||
}
|
||||
|
||||
// Encode the input data
|
||||
const mp3Data = this.encoder.encodeBuffer(leftChannel, rightChannel);
|
||||
|
||||
if (mp3Data.length > 0) {
|
||||
this.push(Buffer.from(mp3Data));
|
||||
}
|
||||
callback();
|
||||
}
|
||||
|
||||
_flush(callback) {
|
||||
// Finalize encoding and flush the internal buffers
|
||||
const mp3Data = this.encoder.flush();
|
||||
|
||||
if (mp3Data.length > 0) {
|
||||
this.push(Buffer.from(mp3Data));
|
||||
}
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = PCMToMP3Encoder;
|
||||
17
lib/record/index.js
Normal file
17
lib/record/index.js
Normal file
@@ -0,0 +1,17 @@
|
||||
|
||||
const path = require('node:path');
|
||||
async function record(logger, socket, url) {
|
||||
const p = path.basename(url);
|
||||
const idx = p.lastIndexOf('/');
|
||||
const vendor = p.substring(idx + 1);
|
||||
switch (vendor) {
|
||||
case 'aws_s3':
|
||||
return require('./s3')(logger, socket);
|
||||
default:
|
||||
logger.info(`unknown bucket vendor: ${vendor}`);
|
||||
socket.send(`unknown bucket vendor: ${vendor}`);
|
||||
socket.close();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = record;
|
||||
103
lib/record/s3-multipart-upload-stream.js
Normal file
103
lib/record/s3-multipart-upload-stream.js
Normal file
@@ -0,0 +1,103 @@
|
||||
const { Writable } = require('stream');
|
||||
const {
|
||||
S3Client,
|
||||
CreateMultipartUploadCommand,
|
||||
UploadPartCommand,
|
||||
CompleteMultipartUploadCommand,
|
||||
} = require('@aws-sdk/client-s3');
|
||||
|
||||
class S3MultipartUploadStream extends Writable {
|
||||
constructor(logger, opts) {
|
||||
super(opts);
|
||||
this.logger = logger;
|
||||
this.bucketName = opts.bucketName;
|
||||
this.objectKey = opts.Key;
|
||||
this.uploadId = null;
|
||||
this.partNumber = 1;
|
||||
this.multipartETags = [];
|
||||
this.buffer = Buffer.alloc(0);
|
||||
this.minPartSize = 5 * 1024 * 1024; // 5 MB
|
||||
this.s3 = new S3Client(opts.bucketCredential);
|
||||
this.metadata = opts.metadata;
|
||||
}
|
||||
|
||||
async _initMultipartUpload() {
|
||||
const command = new CreateMultipartUploadCommand({
|
||||
Bucket: this.bucketName,
|
||||
Key: this.objectKey,
|
||||
Metadata: this.metadata
|
||||
});
|
||||
const response = await this.s3.send(command);
|
||||
return response.UploadId;
|
||||
}
|
||||
|
||||
async _uploadBuffer() {
|
||||
const uploadPartCommand = new UploadPartCommand({
|
||||
Bucket: this.bucketName,
|
||||
Key: this.objectKey,
|
||||
PartNumber: this.partNumber,
|
||||
UploadId: this.uploadId,
|
||||
Body: this.buffer,
|
||||
});
|
||||
|
||||
const uploadPartResponse = await this.s3.send(uploadPartCommand);
|
||||
this.multipartETags.push({
|
||||
ETag: uploadPartResponse.ETag,
|
||||
PartNumber: this.partNumber,
|
||||
});
|
||||
this.partNumber += 1;
|
||||
}
|
||||
|
||||
async _write(chunk, encoding, callback) {
|
||||
try {
|
||||
if (!this.uploadId) {
|
||||
this.uploadId = await this._initMultipartUpload();
|
||||
}
|
||||
|
||||
this.buffer = Buffer.concat([this.buffer, chunk]);
|
||||
|
||||
if (this.buffer.length >= this.minPartSize) {
|
||||
await this._uploadBuffer();
|
||||
this.buffer = Buffer.alloc(0);
|
||||
}
|
||||
|
||||
callback(null);
|
||||
} catch (error) {
|
||||
callback(error);
|
||||
}
|
||||
}
|
||||
|
||||
async _finalize(err) {
|
||||
try {
|
||||
if (this.buffer.length > 0) {
|
||||
await this._uploadBuffer();
|
||||
}
|
||||
|
||||
const completeMultipartUploadCommand = new CompleteMultipartUploadCommand({
|
||||
Bucket: this.bucketName,
|
||||
Key: this.objectKey,
|
||||
MultipartUpload: {
|
||||
Parts: this.multipartETags.sort((a, b) => a.PartNumber - b.PartNumber),
|
||||
},
|
||||
UploadId: this.uploadId,
|
||||
});
|
||||
|
||||
await this.s3.send(completeMultipartUploadCommand);
|
||||
this.logger.info('Finished upload to S3');
|
||||
} catch (error) {
|
||||
this.logger.error('Error completing multipart upload:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async _final(callback) {
|
||||
try {
|
||||
await this._finalize();
|
||||
callback(null);
|
||||
} catch (error) {
|
||||
callback(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = S3MultipartUploadStream;
|
||||
95
lib/record/s3.js
Normal file
95
lib/record/s3.js
Normal file
@@ -0,0 +1,95 @@
|
||||
const Account = require('../models/account');
|
||||
const Websocket = require('ws');
|
||||
const PCMToMP3Encoder = require('./encoder');
|
||||
const S3MultipartUploadStream = require('./s3-multipart-upload-stream');
|
||||
const wav = require('wav');
|
||||
|
||||
async function upload(logger, socket) {
|
||||
|
||||
socket._recvInitialMetadata = false;
|
||||
socket.on('message', async function(data, isBinary) {
|
||||
try {
|
||||
if (!isBinary && !socket._recvInitialMetadata) {
|
||||
socket._recvInitialMetadata = true;
|
||||
logger.debug(`initial metadata: ${data}`);
|
||||
const obj = JSON.parse(data.toString());
|
||||
logger.info({obj}, 'received JSON message from jambonz');
|
||||
const {sampleRate, accountSid, callSid, direction, from, to,
|
||||
callId, applicationSid, originatingSipIp, originatingSipTrunkName} = obj;
|
||||
const account = await Account.retrieve(accountSid);
|
||||
if (account && account.length && account[0].bucket_credential) {
|
||||
const obj = account[0].bucket_credential;
|
||||
// add tags to metadata
|
||||
const metadata = {
|
||||
accountSid,
|
||||
callSid,
|
||||
direction,
|
||||
from,
|
||||
to,
|
||||
callId,
|
||||
applicationSid,
|
||||
originatingSipIp,
|
||||
originatingSipTrunkName,
|
||||
sampleRate: `${sampleRate}`
|
||||
};
|
||||
if (obj.tags && obj.tags.length) {
|
||||
obj.tags.forEach((tag) => {
|
||||
metadata[tag.Key] = tag.Value;
|
||||
});
|
||||
}
|
||||
// create S3 path
|
||||
const day = new Date();
|
||||
let Key = `${day.getFullYear()}/${(day.getMonth() + 1).toString().padStart(2, '0')}`;
|
||||
Key += `/${day.getDate().toString().padStart(2, '0')}/${callSid}.${account[0].record_format}`;
|
||||
|
||||
// Uploader
|
||||
const uploaderOpts = {
|
||||
bucketName: obj.name,
|
||||
Key,
|
||||
metadata,
|
||||
bucketCredential: {
|
||||
credentials: {
|
||||
accessKeyId: obj.access_key_id,
|
||||
secretAccessKey: obj.secret_access_key,
|
||||
},
|
||||
region: obj.region || 'us-east-1'
|
||||
}
|
||||
};
|
||||
const uploadStream = new S3MultipartUploadStream(logger, uploaderOpts);
|
||||
|
||||
/**encoder */
|
||||
let encoder;
|
||||
if (obj.output_format === 'wav') {
|
||||
encoder = new wav.Writer({ channels: 2, sampleRate, bitDepth: 16 });
|
||||
} else {
|
||||
// default is mp3
|
||||
encoder = new PCMToMP3Encoder({
|
||||
channels: 2,
|
||||
sampleRate: sampleRate,
|
||||
bitrate: 128
|
||||
});
|
||||
}
|
||||
/* start streaming data */
|
||||
const duplex = Websocket.createWebSocketStream(socket);
|
||||
duplex.pipe(encoder).pipe(uploadStream);
|
||||
} else {
|
||||
logger.info(`account ${accountSid} does not have any bucket credential, close the socket`);
|
||||
socket.close();
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error({err, data}, 'error parsing message during connection');
|
||||
}
|
||||
});
|
||||
socket.on('error', function(err) {
|
||||
logger.error({err}, 'aws upload: error');
|
||||
});
|
||||
socket.on('close', (data) => {
|
||||
logger.info({data}, 'aws_s3: close');
|
||||
});
|
||||
socket.on('end', function(err) {
|
||||
logger.error({err}, 'aws upload: socket closed from jambonz');
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = upload;
|
||||
@@ -1,6 +1,7 @@
|
||||
const router = require('express').Router();
|
||||
const assert = require('assert');
|
||||
const request = require('request');
|
||||
const {DbErrorBadRequest, DbErrorForbidden, DbErrorUnprocessableRequest} = require('../../utils/errors');
|
||||
const {DbErrorBadRequest, DbErrorForbidden, DbErrorUnprocessableRequest, DbError} = require('../../utils/errors');
|
||||
const Account = require('../../models/account');
|
||||
const Application = require('../../models/application');
|
||||
const Webhook = require('../../models/webhook');
|
||||
@@ -22,6 +23,8 @@ const {
|
||||
} = require('./utils');
|
||||
const short = require('short-uuid');
|
||||
const VoipCarrier = require('../../models/voip-carrier');
|
||||
const { encrypt, decrypt } = require('../../utils/encrypt-decrypt');
|
||||
const { testAwsS3 } = require('../../utils/storage-utils');
|
||||
const translator = short();
|
||||
|
||||
let idx = 0;
|
||||
@@ -38,21 +41,15 @@ const getFsUrl = async(logger, retrieveSet, setName) => {
|
||||
logger.info('No available feature servers to handle createCall API request');
|
||||
return ;
|
||||
}
|
||||
const ip = stripPort(fs[idx++ % fs.length]);
|
||||
logger.info({fs}, `feature servers available for createCall API request, selecting ${ip}`);
|
||||
return `http://${ip}:3000/v1/createCall`;
|
||||
const f = fs[idx++ % fs.length];
|
||||
logger.info({fs}, `feature servers available for createCall API request, selecting ${f}`);
|
||||
return `${f}/v1/createCall`;
|
||||
} catch (err) {
|
||||
logger.error({err}, 'getFsUrl: error retreving feature servers from redis');
|
||||
}
|
||||
};
|
||||
|
||||
const stripPort = (hostport) => {
|
||||
const arr = /^(.*):(.*)$/.exec(hostport);
|
||||
if (arr) return arr[1];
|
||||
return hostport;
|
||||
};
|
||||
|
||||
const validateUpdateForCarrier = async(req, account_sid) => {
|
||||
const validateRequest = async(req, account_sid) => {
|
||||
try {
|
||||
if (req.user.hasScope('admin')) {
|
||||
return;
|
||||
@@ -63,7 +60,7 @@ const validateUpdateForCarrier = async(req, account_sid) => {
|
||||
return;
|
||||
}
|
||||
|
||||
throw new DbErrorForbidden('insufficient permissions to update account');
|
||||
throw new DbErrorForbidden('insufficient permissions');
|
||||
}
|
||||
|
||||
if (req.user.hasScope('service_provider')) {
|
||||
@@ -75,7 +72,7 @@ const validateUpdateForCarrier = async(req, account_sid) => {
|
||||
return;
|
||||
}
|
||||
|
||||
throw new DbErrorForbidden('insufficient permissions to update account');
|
||||
throw new DbErrorForbidden('insufficient permissions');
|
||||
}
|
||||
} catch (error) {
|
||||
throw error;
|
||||
@@ -89,10 +86,12 @@ router.use('/:sid/Charges', hasAccountPermissions, require('./charges'));
|
||||
router.use('/:sid/SipRealms', hasAccountPermissions, require('./sip-realm'));
|
||||
router.use('/:sid/PredefinedCarriers', hasAccountPermissions, require('./add-from-predefined-carrier'));
|
||||
router.use('/:sid/Limits', hasAccountPermissions, require('./limits'));
|
||||
router.use('/:sid/TtsCache', hasAccountPermissions, require('./tts-cache'));
|
||||
router.get('/:sid/Applications', async(req, res) => {
|
||||
const logger = req.app.locals.logger;
|
||||
try {
|
||||
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) {
|
||||
@@ -103,6 +102,7 @@ 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) {
|
||||
@@ -115,7 +115,7 @@ router.put('/:sid/VoipCarriers/:voip_carrier_sid', async(req, res) => {
|
||||
try {
|
||||
const sid = parseVoipCarrierSid(req);
|
||||
const account_sid = parseAccountSid(req);
|
||||
await validateUpdateForCarrier(req, account_sid);
|
||||
await validateRequest(req, account_sid);
|
||||
|
||||
const rowsAffected = await VoipCarrier.update(sid, req.body);
|
||||
if (rowsAffected === 0) {
|
||||
@@ -133,6 +133,8 @@ router.post('/:sid/VoipCarriers', async(req, res) => {
|
||||
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,
|
||||
@@ -259,6 +261,7 @@ async function validateCreateCall(logger, sid, req) {
|
||||
const application = await lookupAppBySid(obj.application_sid);
|
||||
Object.assign(obj, {
|
||||
call_hook: application.call_hook,
|
||||
app_json: application.app_json,
|
||||
call_status_hook: application.call_status_hook,
|
||||
speech_synthesis_vendor: application.speech_synthesis_vendor,
|
||||
speech_synthesis_language: application.speech_synthesis_language,
|
||||
@@ -343,7 +346,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 */
|
||||
@@ -365,7 +368,7 @@ async function validateAdd(req) {
|
||||
|
||||
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');
|
||||
@@ -377,7 +380,7 @@ async function validateUpdate(req, sid) {
|
||||
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')) {
|
||||
@@ -397,7 +400,7 @@ 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');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -444,6 +447,7 @@ 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(account_sid, service_provider_sid);
|
||||
if (results.length === 0) return res.status(404).end();
|
||||
@@ -458,6 +462,7 @@ 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(account_sid, service_provider_sid);
|
||||
if (results.length === 0) return res.status(404).end();
|
||||
@@ -528,6 +533,35 @@ router.delete('/:sid/SubspaceTeleport', async(req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
function encryptBucketCredential(obj) {
|
||||
if (!obj.bucket_credential) return;
|
||||
const {
|
||||
vendor,
|
||||
region,
|
||||
name,
|
||||
access_key_id,
|
||||
secret_access_key,
|
||||
tags
|
||||
} = obj.bucket_credential;
|
||||
|
||||
switch (vendor) {
|
||||
case 'aws_s3':
|
||||
assert(access_key_id, 'invalid aws S3 bucket credential: access_key_id is required');
|
||||
assert(secret_access_key, 'invalid aws S3 bucket credential: secret_access_key is required');
|
||||
assert(name, 'invalid aws bucket name: name is required');
|
||||
assert(region, 'invalid aws bucket region: region is required');
|
||||
const awsData = JSON.stringify({vendor, region, name, access_key_id,
|
||||
secret_access_key, tags});
|
||||
obj.bucket_credential = encrypt(awsData);
|
||||
break;
|
||||
case 'none':
|
||||
obj.bucket_credential = null;
|
||||
break;
|
||||
default:
|
||||
throw DbErrorBadRequest(`unknow storage vendor: ${vendor}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* update
|
||||
*/
|
||||
@@ -535,6 +569,7 @@ router.put('/:sid', async(req, res) => {
|
||||
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);
|
||||
@@ -573,6 +608,8 @@ router.put('/:sid', async(req, res) => {
|
||||
delete obj.registration_hook;
|
||||
delete obj.queue_event_hook;
|
||||
|
||||
encryptBucketCredential(obj);
|
||||
|
||||
const rowsAffected = await Account.update(sid, obj);
|
||||
if (rowsAffected === 0) {
|
||||
return res.status(404).end();
|
||||
@@ -601,6 +638,7 @@ router.delete('/:sid', async(req, res) => {
|
||||
|
||||
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);
|
||||
@@ -664,6 +702,51 @@ account_subscriptions WHERE account_sid = ?)
|
||||
}
|
||||
});
|
||||
|
||||
/* Test Bucket credential Keys */
|
||||
router.post('/:sid/BucketCredentialTest', async(req, res) => {
|
||||
const logger = req.app.locals.logger;
|
||||
try {
|
||||
const account_sid = parseAccountSid(req);
|
||||
await validateRequest(req, account_sid);
|
||||
let {vendor, name, region, access_key_id, secret_access_key} = req.body;
|
||||
const ret = {
|
||||
status: 'not tested'
|
||||
};
|
||||
|
||||
if (secret_access_key.endsWith('XXXXXX')) {
|
||||
// this is when the password already saved in account
|
||||
const service_provider_sid = req.user.hasServiceProviderAuth ? req.user.service_provider_sid : null;
|
||||
const results = await Account.retrieve(account_sid, service_provider_sid);
|
||||
if (results.length === 0) throw new DbError('Invalid Account Sid');
|
||||
const {bucket_credential} = results[0];
|
||||
if (bucket_credential) {
|
||||
const o = JSON.parse(decrypt(bucket_credential));
|
||||
vendor = o.vendor;
|
||||
switch (vendor) {
|
||||
case 'aws_s3':
|
||||
name = o.name;
|
||||
region = o.region;
|
||||
access_key_id = o.access_key_id;
|
||||
secret_access_key = o.secret_access_key;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
switch (vendor) {
|
||||
case 'aws_s3':
|
||||
await testAwsS3(logger, {vendor, name, region, access_key_id, secret_access_key});
|
||||
ret.status = 'ok';
|
||||
break;
|
||||
default:
|
||||
throw new DbErrorBadRequest(`Does not support test for ${vendor}`);
|
||||
}
|
||||
return res.status(200).json(ret);
|
||||
}
|
||||
catch (err) {
|
||||
return res.status(200).json({status: 'failed', reason: err.message});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* retrieve account level api keys
|
||||
*/
|
||||
@@ -672,6 +755,8 @@ router.get('/:sid/ApiKeys', async(req, res) => {
|
||||
|
||||
try {
|
||||
const sid = parseAccountSid(req);
|
||||
await validateRequest(req, sid);
|
||||
|
||||
const results = await ApiKey.retrieveAll(sid);
|
||||
res.status(200).json(results);
|
||||
updateLastUsed(logger, sid, req).catch((err) => {});
|
||||
@@ -685,7 +770,7 @@ router.get('/:sid/ApiKeys', async(req, res) => {
|
||||
*/
|
||||
router.post('/:sid/Calls', async(req, res) => {
|
||||
const {retrieveSet, logger} = req.app.locals;
|
||||
const setName = `${(process.env.JAMBONES_CLUSTER_ID || 'default')}:active-fs`;
|
||||
const setName = `${(process.env.JAMBONES_CLUSTER_ID || 'default')}:fs-service-url`;
|
||||
const serviceUrl = await getFsUrl(logger, retrieveSet, setName);
|
||||
|
||||
if (!serviceUrl) {
|
||||
@@ -694,6 +779,8 @@ router.post('/:sid/Calls', async(req, res) => {
|
||||
|
||||
try {
|
||||
const sid = parseAccountSid(req);
|
||||
await validateRequest(req, sid);
|
||||
|
||||
await validateCreateCall(logger, sid, req);
|
||||
updateLastUsed(logger, sid, req).catch((err) => {});
|
||||
request({
|
||||
@@ -724,10 +811,17 @@ router.post('/:sid/Calls', async(req, res) => {
|
||||
*/
|
||||
router.get('/:sid/Calls', async(req, res) => {
|
||||
const {logger, listCalls} = req.app.locals;
|
||||
|
||||
const {direction, from, to, callStatus} = req.query || {};
|
||||
try {
|
||||
const accountSid = parseAccountSid(req);
|
||||
const calls = await listCalls(accountSid);
|
||||
await validateRequest(req, accountSid);
|
||||
const calls = await listCalls({
|
||||
accountSid,
|
||||
direction,
|
||||
from,
|
||||
to,
|
||||
callStatus
|
||||
});
|
||||
logger.debug(`retrieved ${calls.length} calls for account sid ${accountSid}`);
|
||||
res.status(200).json(coerceNumbers(snakeCase(calls)));
|
||||
updateLastUsed(logger, accountSid, req).catch((err) => {});
|
||||
@@ -744,6 +838,7 @@ router.get('/:sid/Calls/:callSid', async(req, res) => {
|
||||
|
||||
try {
|
||||
const accountSid = parseAccountSid(req);
|
||||
await validateRequest(req, accountSid);
|
||||
const callSid = parseCallSid(req);
|
||||
const callInfo = await retrieveCall(accountSid, callSid);
|
||||
if (callInfo) {
|
||||
@@ -768,6 +863,7 @@ router.delete('/:sid/Calls/:callSid', async(req, res) => {
|
||||
|
||||
try {
|
||||
const accountSid = parseAccountSid(req);
|
||||
await validateRequest(req, accountSid);
|
||||
const callSid = parseCallSid(req);
|
||||
const result = await deleteCall(accountSid, callSid);
|
||||
if (result) {
|
||||
@@ -792,6 +888,7 @@ const updateCall = async(req, res) => {
|
||||
|
||||
try {
|
||||
const accountSid = parseAccountSid(req);
|
||||
await validateRequest(req, accountSid);
|
||||
const callSid = parseCallSid(req);
|
||||
validateUpdateCall(req.body);
|
||||
const call = await retrieveCall(accountSid, callSid);
|
||||
@@ -832,7 +929,9 @@ router.post('/:sid/Messages', async(req, res) => {
|
||||
|
||||
try {
|
||||
const account_sid = parseAccountSid(req);
|
||||
const setName = `${(process.env.JAMBONES_CLUSTER_ID || 'default')}:active-fs`;
|
||||
await validateRequest(req, account_sid);
|
||||
|
||||
const setName = `${(process.env.JAMBONES_CLUSTER_ID || 'default')}:fs-service-url`;
|
||||
const serviceUrl = await getFsUrl(logger, retrieveSet, setName);
|
||||
if (!serviceUrl) res.json({msg: 'no available feature servers at this time'}).status(480);
|
||||
await validateCreateMessage(logger, account_sid, req);
|
||||
@@ -865,4 +964,22 @@ router.post('/:sid/Messages', async(req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* retrieve info for a group of queues under an account
|
||||
*/
|
||||
router.get('/:sid/Queues', async(req, res) => {
|
||||
const {logger, listSortedSets} = req.app.locals;
|
||||
const { search } = req.query || {};
|
||||
try {
|
||||
const accountSid = parseAccountSid(req);
|
||||
await validateRequest(req, accountSid);
|
||||
const queues = search ? await listSortedSets(accountSid, search) : await listSortedSets(accountSid);
|
||||
logger.debug(`retrieved ${queues.length} queues for account sid ${accountSid}`);
|
||||
res.status(200).json(queues);
|
||||
updateLastUsed(logger, accountSid, req).catch((err) => {});
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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');
|
||||
@@ -13,6 +13,36 @@ const preconditions = {
|
||||
'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) {
|
||||
@@ -23,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') {
|
||||
@@ -35,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');
|
||||
}
|
||||
@@ -50,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');
|
||||
}
|
||||
@@ -117,6 +172,7 @@ router.get('/:sid', async(req, res) => {
|
||||
const account_sid = req.user.hasAccountAuth ? req.user.account_sid : null;
|
||||
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) {
|
||||
|
||||
88
lib/routes/api/clients.js
Normal file
88
lib/routes/api/clients.js
Normal file
@@ -0,0 +1,88 @@
|
||||
const router = require('express').Router();
|
||||
const decorate = require('./decorate');
|
||||
const sysError = require('../error');
|
||||
const Client = require('../../models/client');
|
||||
const Account = require('../../models/account');
|
||||
const { DbErrorBadRequest, DbErrorForbidden } = require('../../utils/errors');
|
||||
const { encrypt, decrypt, obscureKey } = require('../../utils/encrypt-decrypt');
|
||||
|
||||
const commonCheck = async(req) => {
|
||||
if (req.user.hasAccountAuth) {
|
||||
req.body.account_sid = req.user.account_sid;
|
||||
} else if (req.user.hasServiceProviderAuth && req.body.account_sid) {
|
||||
const accounts = await Account.retrieve(req.body.account_sid, req.user.service_provider_sid);
|
||||
if (accounts.length === 0) {
|
||||
throw new DbErrorForbidden('insufficient permissions');
|
||||
}
|
||||
}
|
||||
|
||||
if (req.body.password) {
|
||||
req.body.password = encrypt(req.body.password);
|
||||
}
|
||||
};
|
||||
|
||||
const validateAdd = async(req) => {
|
||||
await commonCheck(req);
|
||||
|
||||
const clients = await Client.retrieveByAccountSidAndUserName(req.body.account_sid, req.body.username);
|
||||
if (clients.length) {
|
||||
throw new DbErrorBadRequest('the client\'s username already exists');
|
||||
}
|
||||
};
|
||||
|
||||
const validateUpdate = async(req, sid) => {
|
||||
await commonCheck(req);
|
||||
|
||||
const clients = await Client.retrieveByAccountSidAndUserName(req.body.account_sid, req.body.username);
|
||||
if (clients.length && clients[0].client_sid !== sid) {
|
||||
throw new DbErrorBadRequest('the client\'s username already exists');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const preconditions = {
|
||||
add: validateAdd,
|
||||
update: validateUpdate,
|
||||
};
|
||||
|
||||
decorate(router, Client, ['add', 'update', 'delete'], preconditions);
|
||||
|
||||
router.get('/', async(req, res) => {
|
||||
const logger = req.app.locals.logger;
|
||||
try {
|
||||
const results = req.user.hasAdminAuth ?
|
||||
await Client.retrieveAll() : req.user.hasAccountAuth ?
|
||||
await Client.retrieveAllByAccountSid(req.user.hasAccountAuth ? req.user.account_sid : null) :
|
||||
await Client.retrieveAllByServiceProviderSid(req.user.service_provider_sid);
|
||||
const ret = results.map((c) => {
|
||||
c.password = obscureKey(decrypt(c.password), 1);
|
||||
return c;
|
||||
});
|
||||
res.status(200).json(ret);
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/:sid', async(req, res) => {
|
||||
const logger = req.app.locals.logger;
|
||||
try {
|
||||
const results = await Client.retrieve(req.params.sid);
|
||||
if (results.length === 0) return res.sendStatus(404);
|
||||
const client = results[0];
|
||||
client.password = obscureKey(decrypt(client.password), 1);
|
||||
if (req.user.hasAccountAuth && client.account_sid !== req.user.account_sid) {
|
||||
return res.sendStatus(404);
|
||||
} else if (req.user.hasServiceProviderAuth) {
|
||||
const accounts = await Account.retrieve(client.account_sid, req.user.service_provider_sid);
|
||||
if (!accounts.length) {
|
||||
return res.sendStatus(404);
|
||||
}
|
||||
}
|
||||
return res.status(200).json(client);
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -46,7 +46,7 @@ function createResetEmailText(link) {
|
||||
router.post('/', async(req, res) => {
|
||||
const {logger, addKey} = req.app.locals;
|
||||
const {email} = req.body;
|
||||
const {user_sid} = req.user;
|
||||
|
||||
let obj;
|
||||
try {
|
||||
if (!email || !validateEmail(email)) {
|
||||
@@ -55,11 +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) {
|
||||
@@ -81,7 +86,7 @@ router.post('/', async(req, res) => {
|
||||
emailSimpleText(logger, email, 'Reset password request', createResetEmailText(link));
|
||||
}
|
||||
|
||||
const redisKey = cacheClient.generateRedisKey('jwt', user_sid, 'v2');
|
||||
const redisKey = cacheClient.generateRedisKey('jwt', obj.user.user_sid, 'v2');
|
||||
await cacheClient.delete(redisKey);
|
||||
});
|
||||
|
||||
|
||||
@@ -16,6 +16,8 @@ const isAdminScope = (req, res, next) => {
|
||||
// };
|
||||
|
||||
api.use('/BetaInviteCodes', isAdminScope, require('./beta-invite-codes'));
|
||||
api.use('/SystemInformation', isAdminScope, require('./system-information'));
|
||||
api.use('/TtsCache', isAdminScope, require('./tts-cache'));
|
||||
api.use('/ServiceProviders', require('./service-providers'));
|
||||
api.use('/VoipCarriers', require('./voip-carriers'));
|
||||
api.use('/Webhooks', require('./webhooks'));
|
||||
@@ -45,6 +47,11 @@ api.use('/Invoices', require('./invoices'));
|
||||
api.use('/InviteCodes', require('./invite-codes'));
|
||||
api.use('/PredefinedCarriers', require('./predefined-carriers'));
|
||||
api.use('/PasswordSettings', require('./password-settings'));
|
||||
// Least Cost Routing
|
||||
api.use('/Lcrs', require('./lcrs'));
|
||||
api.use('/LcrRoutes', require('./lcr-routes'));
|
||||
api.use('/LcrCarrierSetEntries', require('./lcr-carrier-set-entries'));
|
||||
api.use('/Clients', require('./clients'));
|
||||
|
||||
// messaging
|
||||
api.use('/Smpps', require('./smpps')); // our smpp server info
|
||||
|
||||
65
lib/routes/api/lcr-carrier-set-entries.js
Normal file
65
lib/routes/api/lcr-carrier-set-entries.js
Normal 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;
|
||||
96
lib/routes/api/lcr-routes.js
Normal file
96
lib/routes/api/lcr-routes.js
Normal 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
138
lib/routes/api/lcrs.js
Normal 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;
|
||||
@@ -17,7 +17,7 @@ const tokenSql = 'SELECT token from api_keys where account_sid IS NULL AND servi
|
||||
|
||||
|
||||
router.post('/', async(req, res) => {
|
||||
const logger = req.app.locals.logger;
|
||||
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');
|
||||
@@ -31,8 +31,28 @@ router.post('/', async(req, res) => {
|
||||
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) return res.sendStatus(403);
|
||||
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) {
|
||||
|
||||
@@ -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,
|
||||
@@ -21,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;
|
||||
@@ -42,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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,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
|
||||
@@ -75,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);
|
||||
@@ -90,6 +111,15 @@ router.get('/:sid', async(req, res) => {
|
||||
const account_sid = req.user.hasAccountAuth ? req.user.account_sid : null;
|
||||
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) {
|
||||
|
||||
@@ -2,6 +2,10 @@ const router = require('express').Router();
|
||||
const sysError = require('../error');
|
||||
const {DbErrorBadRequest} = require('../../utils/errors');
|
||||
const {getHomerApiKey, getHomerSipTrace, getHomerPcap} = require('../../utils/homer-utils');
|
||||
const {getJaegerTrace} = require('../../utils/jaeger-utils');
|
||||
const Account = require('../../models/account');
|
||||
const { getS3Object } = require('../../utils/storage-utils');
|
||||
|
||||
const parseAccountSid = (url) => {
|
||||
const arr = /Accounts\/([^\/]*)/.exec(url);
|
||||
if (arr) return arr[1];
|
||||
@@ -18,7 +22,7 @@ router.get('/', async(req, res) => {
|
||||
logger.debug({opts: req.query}, 'GET /RecentCalls');
|
||||
const account_sid = parseAccountSid(req.originalUrl);
|
||||
const service_provider_sid = account_sid ? null : parseServiceProviderSid(req.originalUrl);
|
||||
const {page, count, trunk, direction, days, answered, start, end} = req.query || {};
|
||||
const {page, count, trunk, direction, days, answered, start, end, from, to} = req.query || {};
|
||||
if (!page || page < 1) throw new DbErrorBadRequest('missing or invalid "page" query arg');
|
||||
if (!count || count < 25 || count > 500) throw new DbErrorBadRequest('missing or invalid "count" query arg');
|
||||
|
||||
@@ -33,6 +37,8 @@ router.get('/', async(req, res) => {
|
||||
answered,
|
||||
start: days ? undefined : start,
|
||||
end: days ? undefined : end,
|
||||
from,
|
||||
to
|
||||
});
|
||||
res.status(200).json(data);
|
||||
}
|
||||
@@ -47,6 +53,8 @@ router.get('/', async(req, res) => {
|
||||
answered,
|
||||
start: days ? undefined : start,
|
||||
end: days ? undefined : end,
|
||||
from,
|
||||
to
|
||||
});
|
||||
res.status(200).json(data);
|
||||
}
|
||||
@@ -72,12 +80,12 @@ router.get('/:call_id', async(req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/:call_id/pcap', async(req, res) => {
|
||||
router.get('/:call_id/:method/pcap', async(req, res) => {
|
||||
const {logger} = req.app.locals;
|
||||
try {
|
||||
const token = await getHomerApiKey(logger);
|
||||
if (!token) return res.sendStatus(400, {msg: 'getHomerApiKey: Failed to get Homer API token; check server config'});
|
||||
const stream = await getHomerPcap(logger, token, [req.params.call_id]);
|
||||
const stream = await getHomerPcap(logger, token, [req.params.call_id], req.params.method);
|
||||
if (!stream) {
|
||||
logger.info(`getHomerApiKey: unable to get sip traces from Homer for ${req.params.call_id}`);
|
||||
return res.sendStatus(404);
|
||||
@@ -93,4 +101,50 @@ router.get('/:call_id/pcap', async(req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/trace/:trace_id', async(req, res) => {
|
||||
const {logger} = req.app.locals;
|
||||
const {trace_id} = req.params;
|
||||
try {
|
||||
const obj = await getJaegerTrace(logger, trace_id);
|
||||
if (!obj) {
|
||||
logger.info(`/RecentCalls: unable to get spans from jaeger for ${trace_id}`);
|
||||
return res.sendStatus(404);
|
||||
}
|
||||
res.status(200).json(obj.result);
|
||||
} catch (err) {
|
||||
logger.error({err}, `/RecentCalls error retrieving jaeger trace ${trace_id}`);
|
||||
res.sendStatus(500);
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/:call_sid/record/:year/:month/:day/:format', async(req, res) => {
|
||||
const {logger} = req.app.locals;
|
||||
const {call_sid, year, month, day, format} = req.params;
|
||||
|
||||
try {
|
||||
const account_sid = parseAccountSid(req.originalUrl);
|
||||
const r = await Account.retrieve(account_sid);
|
||||
if (r.length === 0 || !r[0].bucket_credential) return res.sendStatus(404);
|
||||
const {bucket_credential} = r[0];
|
||||
switch (bucket_credential.vendor) {
|
||||
case 'aws_s3':
|
||||
const getS3Options = {
|
||||
...bucket_credential,
|
||||
key: `${year}/${month}/${day}/${call_sid}.${format || 'mp3'}`
|
||||
};
|
||||
const stream = await getS3Object(logger, getS3Options);
|
||||
res.set({
|
||||
'Content-Type': `audio/${format || 'mp3'}`
|
||||
});
|
||||
stream.pipe(res);
|
||||
break;
|
||||
default:
|
||||
logger.error(`There is no handler for fetching record from ${bucket_credential.vendor}`);
|
||||
return res.sendStatus(500);
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error({err}, ` error retrieving recording ${call_sid}`);
|
||||
res.sendStatus(404);
|
||||
}
|
||||
});
|
||||
module.exports = router;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const router = require('express').Router();
|
||||
const {promisePool} = require('../../db');
|
||||
const {DbErrorUnprocessableRequest, DbErrorForbidden} = 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,7 +8,11 @@ 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, parseVoipCarrierSid} = require('./utils');
|
||||
const {
|
||||
hasServiceProviderPermissions,
|
||||
parseServiceProviderSid,
|
||||
parseVoipCarrierSid,
|
||||
} = require('./utils');
|
||||
const sysError = require('../error');
|
||||
const decorate = require('./decorate');
|
||||
const preconditions = {
|
||||
@@ -42,18 +46,11 @@ async function validateRetrieve(req) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.user.hasScope('service_provider')) {
|
||||
if (service_provider_sid === req.user.service_provider_sid) return ;
|
||||
if (req.user.hasScope('service_provider') || req.user.hasScope('account')) {
|
||||
if (service_provider_sid === req.user.service_provider_sid) return;
|
||||
}
|
||||
|
||||
if (req.user.hasScope('account')) {
|
||||
/* allow account users to retrieve service provider data from parent SP */
|
||||
const sid = req.user.account_sid;
|
||||
const [r] = await promisePool.execute('SELECT service_provider_sid from accounts WHERE account_sid = ?', [sid]);
|
||||
if (r.length === 1 && r[0].service_provider_sid === req.user.service_provider_sid) return;
|
||||
}
|
||||
|
||||
throw new DbErrorForbidden('insufficient permissions to update service provider');
|
||||
throw new DbErrorForbidden('insufficient permissions');
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
@@ -84,14 +81,10 @@ async function noActiveAccountsOrUsers(req, sid) {
|
||||
}
|
||||
const activeAccounts = await ServiceProvider.getForeignKeyReferences('accounts.service_provider_sid', sid);
|
||||
const activeUsers = await ServiceProvider.getForeignKeyReferences('users.service_provider_sid', sid);
|
||||
if (activeAccounts > 0 && activeUsers > 0) throw new DbErrorUnprocessableRequest(
|
||||
'cannot delete service provider with active accounts or users'
|
||||
);
|
||||
if (activeAccounts > 0 && activeUsers > 0) throw new DbErrorForbidden('insufficient privileges');
|
||||
|
||||
if (activeAccounts > 0) throw new DbErrorUnprocessableRequest('cannot delete service provider with active accounts');
|
||||
if (activeUsers > 0) throw new DbErrorUnprocessableRequest(
|
||||
'cannot delete service provider with active service provider level users'
|
||||
);
|
||||
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]);
|
||||
@@ -122,6 +115,7 @@ router.get('/:sid/Accounts', async(req, res) => {
|
||||
sysError(logger, res, err);
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/:sid/Applications', async(req, res) => {
|
||||
const logger = req.app.locals.logger;
|
||||
try {
|
||||
@@ -250,6 +244,7 @@ router.get('/', async(req, res) => {
|
||||
router.get('/:sid', async(req, res) => {
|
||||
const logger = req.app.locals.logger;
|
||||
try {
|
||||
await validateRetrieve(req);
|
||||
const sid = parseServiceProviderSid(req);
|
||||
const results = await ServiceProvider.retrieve(sid);
|
||||
if (results.length === 0) return res.status(404).end();
|
||||
|
||||
@@ -6,8 +6,14 @@ 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 */
|
||||
@@ -53,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,
|
||||
@@ -65,12 +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 });
|
||||
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,
|
||||
|
||||
@@ -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'});
|
||||
|
||||
@@ -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'});
|
||||
|
||||
@@ -14,20 +14,14 @@ const getFsUrl = async(logger, retrieveSet, setName, provider) => {
|
||||
logger.info('No available feature servers to handle createCall API request');
|
||||
return ;
|
||||
}
|
||||
const ip = stripPort(fs[idx++ % fs.length]);
|
||||
logger.info({fs}, `feature servers available for createCall API request, selecting ${ip}`);
|
||||
return `http://${ip}:3000/v1/messaging/${provider}`;
|
||||
const f = fs[idx++ % fs.length];
|
||||
logger.info({fs}, `feature servers available for createCall API request, selecting ${f}`);
|
||||
return `${f}/v1/messaging/${provider}`;
|
||||
} catch (err) {
|
||||
logger.error({err}, 'getFsUrl: error retreving feature servers from redis');
|
||||
}
|
||||
};
|
||||
|
||||
const stripPort = (hostport) => {
|
||||
const arr = /^(.*):(.*)$/.exec(hostport);
|
||||
if (arr) return arr[1];
|
||||
return hostport;
|
||||
};
|
||||
|
||||
const doSendResponse = async(res, respondFn, body) => {
|
||||
if (typeof respondFn === 'number') res.sendStatus(respondFn);
|
||||
else if (typeof respondFn !== 'function') res.sendStatus(200);
|
||||
@@ -44,7 +38,7 @@ router.post('/:provider', async(req, res) => {
|
||||
lookupAppByPhoneNumber,
|
||||
logger
|
||||
} = req.app.locals;
|
||||
const setName = `${process.env.JAMBONES_CLUSTER_ID || 'default'}:active-fs`;
|
||||
const setName = `${process.env.JAMBONES_CLUSTER_ID || 'default'}:fs-service-url`;
|
||||
logger.debug({path: req.path, body: req.body}, 'incomingSMS from carrier');
|
||||
|
||||
// search for provider module
|
||||
|
||||
@@ -3,9 +3,9 @@ const assert = require('assert');
|
||||
const Account = require('../../models/account');
|
||||
const SpeechCredential = require('../../models/speech-credential');
|
||||
const sysError = require('../error');
|
||||
const {decrypt, encrypt} = require('../../utils/encrypt-decrypt');
|
||||
const {decrypt, encrypt, obscureKey} = require('../../utils/encrypt-decrypt');
|
||||
const {parseAccountSid, parseServiceProviderSid, parseSpeechCredentialSid} = require('./utils');
|
||||
const {DbErrorUnprocessableRequest} = require('../../utils/errors');
|
||||
const {DbErrorUnprocessableRequest, DbErrorForbidden} = require('../../utils/errors');
|
||||
const {
|
||||
testGoogleTts,
|
||||
testGoogleStt,
|
||||
@@ -21,16 +21,82 @@ const {
|
||||
testIbmTts,
|
||||
testIbmStt
|
||||
} = require('../../utils/speech-utils');
|
||||
const {promisePool} = require('../../db');
|
||||
|
||||
const obscureKey = (key) => {
|
||||
const key_spoiler_length = 6;
|
||||
const key_spoiler_char = 'X';
|
||||
const validateAdd = async(req) => {
|
||||
const account_sid = parseAccountSid(req);
|
||||
const service_provider_sid = parseServiceProviderSid(req);
|
||||
|
||||
if (key.length <= key_spoiler_length) {
|
||||
return key;
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
return `${key.slice(0, key_spoiler_length)}${key_spoiler_char.repeat(key.length - key_spoiler_length)}`;
|
||||
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 encryptCredential = (obj) => {
|
||||
@@ -146,6 +212,8 @@ router.post('/', async(req, res) => {
|
||||
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');
|
||||
@@ -175,8 +243,11 @@ router.get('/', async(req, res) => {
|
||||
const logger = req.app.locals.logger;
|
||||
|
||||
try {
|
||||
const account_sid = parseAccountSid(req) || req.user.account_sid;
|
||||
const service_provider_sid = parseServiceProviderSid(req) || req.user.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) :
|
||||
@@ -190,6 +261,7 @@ router.get('/', async(req, res) => {
|
||||
|
||||
res.status(200).json(creds.map((c) => {
|
||||
const {credential, ...obj} = c;
|
||||
|
||||
if ('google' === obj.vendor) {
|
||||
const o = JSON.parse(decrypt(credential));
|
||||
const key_header = '-----BEGIN PRIVATE KEY-----\n';
|
||||
@@ -250,6 +322,15 @@ router.get('/', async(req, res) => {
|
||||
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) {
|
||||
@@ -266,6 +347,9 @@ router.get('/:sid', async(req, res) => {
|
||||
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));
|
||||
@@ -327,6 +411,16 @@ router.get('/:sid', async(req, res) => {
|
||||
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);
|
||||
@@ -340,6 +434,8 @@ router.delete('/:sid', async(req, res) => {
|
||||
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);
|
||||
@@ -372,6 +468,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));
|
||||
@@ -428,8 +527,11 @@ router.get('/:sid/test', async(req, res) => {
|
||||
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 = {
|
||||
|
||||
14
lib/routes/api/system-information.js
Normal file
14
lib/routes/api/system-information.js
Normal 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;
|
||||
29
lib/routes/api/tts-cache.js
Normal file
29
lib/routes/api/tts-cache.js
Normal file
@@ -0,0 +1,29 @@
|
||||
const router = require('express').Router();
|
||||
const {
|
||||
parseAccountSid
|
||||
} = require('./utils');
|
||||
|
||||
router.delete('/', async(req, res) => {
|
||||
const {purgeTtsCache} = req.app.locals;
|
||||
const account_sid = parseAccountSid(req);
|
||||
if (account_sid) {
|
||||
await purgeTtsCache({account_sid});
|
||||
} else {
|
||||
await purgeTtsCache();
|
||||
}
|
||||
res.sendStatus(204);
|
||||
});
|
||||
|
||||
router.get('/', async(req, res) => {
|
||||
const {getTtsSize} = req.app.locals;
|
||||
const account_sid = parseAccountSid(req);
|
||||
let size = 0;
|
||||
if (account_sid) {
|
||||
size = await getTtsSize(`tts:${account_sid}:*`);
|
||||
} else {
|
||||
size = await getTtsSize();
|
||||
}
|
||||
res.status(200).json({size});
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -1,9 +1,9 @@
|
||||
const router = require('express').Router();
|
||||
const User = require('../../models/user');
|
||||
const {DbErrorBadRequest} = require('../../utils/errors');
|
||||
const {DbErrorBadRequest, BadRequestError, DbErrorForbidden} = require('../../utils/errors');
|
||||
const {generateHashedPassword, verifyPassword} = require('../../utils/password-utils');
|
||||
const {promisePool} = require('../../db');
|
||||
const {validatePasswordSettings} = require('./utils');
|
||||
const {validatePasswordSettings, parseUserSid} = require('./utils');
|
||||
const {decrypt} = require('../../utils/encrypt-decrypt');
|
||||
const {cacheClient} = require('../../helpers');
|
||||
const sysError = require('../error');
|
||||
@@ -99,6 +99,48 @@ const validateRequest = async(user_sid, req) => {
|
||||
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;
|
||||
|
||||
@@ -256,22 +298,20 @@ router.get('/me', async(req, res) => {
|
||||
|
||||
router.get('/:user_sid', async(req, res) => {
|
||||
const logger = req.app.locals.logger;
|
||||
const {user_sid} = req.params;
|
||||
|
||||
try {
|
||||
const user_sid = parseUserSid(req);
|
||||
const [user] = await User.retrieve(user_sid);
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const {hashed_password, ...rest} = user;
|
||||
if (!user) throw new Error('failure retrieving user');
|
||||
|
||||
if (req.user.hasAdminAuth ||
|
||||
req.user.hasAccountAuth && req.user.account_sid === user.account_sid ||
|
||||
req.user.hasServiceProviderAuth && req.user.service_provider_sid === user.service_provider_sid) {
|
||||
res.status(200).json(rest);
|
||||
} else {
|
||||
res.sendStatus(403);
|
||||
if (!user) {
|
||||
throw new Error('failure retrieving user');
|
||||
}
|
||||
|
||||
ensureUserRetrievalIsAllowed(req, user);
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const { hashed_password, ...rest } = user;
|
||||
return res.status(200).json(rest);
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
}
|
||||
@@ -413,12 +453,12 @@ router.post('/', async(req, res) => {
|
||||
|
||||
if (name) {
|
||||
logger.debug({payload}, 'user with this username already exists');
|
||||
return res.status(422).json({msg: 'user with this username already exists'});
|
||||
return res.status(422).json({msg: 'invalid username or email'});
|
||||
}
|
||||
|
||||
if (email) {
|
||||
logger.debug({payload}, 'user with this email already exists');
|
||||
return res.status(422).json({msg: 'user with this email already exists'});
|
||||
return res.status(422).json({msg: 'invalid username or email'});
|
||||
}
|
||||
|
||||
if (req.user.hasAdminAuth) {
|
||||
@@ -449,28 +489,21 @@ router.post('/', async(req, res) => {
|
||||
|
||||
router.delete('/:user_sid', async(req, res) => {
|
||||
const logger = req.app.locals.logger;
|
||||
const {user_sid} = req.params;
|
||||
const allUsers = await User.retrieveAll();
|
||||
const activeAdminUsers = allUsers.filter((e) => !e.account_sid && !e.service_provider_sid && e.is_active);
|
||||
const user = await User.retrieve(user_sid);
|
||||
|
||||
try {
|
||||
if (req.user.hasAdminAuth && activeAdminUsers.length === 1) {
|
||||
throw new Error('cannot delete this admin user - there are no other active admin users');
|
||||
}
|
||||
const user_sid = parseUserSid(req);
|
||||
const allUsers = await User.retrieveAll();
|
||||
const activeAdminUsers = getActiveAdminUsers(allUsers);
|
||||
const user = allUsers.filter((user) => user.user_sid === user_sid);
|
||||
|
||||
if (req.user.hasAdminAuth ||
|
||||
(req.user.hasAccountAuth && req.user.account_sid === user[0].account_sid) ||
|
||||
(req.user.hasServiceProviderAuth && req.user.service_provider_sid === user[0].service_provider_sid)) {
|
||||
await User.remove(user_sid);
|
||||
/* invalidate the jwt of the deleted user */
|
||||
const redisKey = cacheClient.generateRedisKey('jwt', user_sid, 'v2');
|
||||
await cacheClient.delete(redisKey);
|
||||
ensureUserDeletionIsAllowed(req, activeAdminUsers, user);
|
||||
await User.remove(user_sid);
|
||||
|
||||
return res.sendStatus(204);
|
||||
} else {
|
||||
throw new DbErrorBadRequest('invalid request');
|
||||
}
|
||||
/* 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);
|
||||
}
|
||||
|
||||
@@ -218,20 +218,67 @@ const parseWebhookSid = (req) => {
|
||||
}
|
||||
};
|
||||
|
||||
const hasAccountPermissions = (req, res, next) => {
|
||||
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')) {
|
||||
return next();
|
||||
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);
|
||||
if (account_sid === req.user.account_sid) {
|
||||
return next();
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -405,6 +452,9 @@ module.exports = {
|
||||
parseSpeechCredentialSid,
|
||||
parseVoipCarrierSid,
|
||||
parseWebhookSid,
|
||||
parseSipGatewaySid,
|
||||
parseUserSid,
|
||||
parseLcrSid,
|
||||
hasAccountPermissions,
|
||||
hasServiceProviderPermissions,
|
||||
checkLimits,
|
||||
|
||||
@@ -74,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);
|
||||
@@ -89,6 +96,20 @@ router.get('/:sid', async(req, res) => {
|
||||
const account_sid = req.user.hasAccountAuth ? req.user.account_sid : null;
|
||||
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) {
|
||||
|
||||
@@ -2,17 +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 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) {
|
||||
|
||||
@@ -38,6 +38,12 @@ tags:
|
||||
description: Webhooks operations
|
||||
- name: Microsoft Teams Tenants
|
||||
description: Microsoft Teams Tenants operations
|
||||
- name: Lcrs
|
||||
description: Least Cost Routing operations
|
||||
- name: LcrRoutes
|
||||
description: Least Cost Routing Routes operations
|
||||
- name: LcrCarrierSetEntries
|
||||
description: Least Cost Routing Carrier Set Entries operation
|
||||
paths:
|
||||
/BetaInviteCodes:
|
||||
post:
|
||||
@@ -2182,6 +2188,58 @@ paths:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GeneralError'
|
||||
/ServiceProviders/{ServiceProviderSid}/Lcrs:
|
||||
parameters:
|
||||
- name: ServiceProviderSid
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
get:
|
||||
tags:
|
||||
- Service Providers
|
||||
summary: get all Least Cost Routings for a service provider
|
||||
operationId: getServiceProviderLcrs
|
||||
responses:
|
||||
200:
|
||||
description: Least cost routing listing
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Lcr'
|
||||
403:
|
||||
description: unauthorized
|
||||
404:
|
||||
description: service provider not found
|
||||
post:
|
||||
tags:
|
||||
- Service Providers
|
||||
summary: create a Lest cost routing
|
||||
operationId: createLcrForServiceProvider
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
description: name or Least Cost Routing
|
||||
example: twilioLcr
|
||||
required:
|
||||
- name
|
||||
responses:
|
||||
201:
|
||||
description: service provider Lcr successfully created
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SuccessfulAdd'
|
||||
404:
|
||||
description: service provider not found
|
||||
/Accounts/{AccountSid}/Limits:
|
||||
post:
|
||||
tags:
|
||||
@@ -2988,6 +3046,18 @@ paths:
|
||||
enum:
|
||||
- inbound
|
||||
- outbound
|
||||
- in: query
|
||||
name: from
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
description: calling number to retrieve
|
||||
- in: query
|
||||
name: to
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
description: called number to retrieve
|
||||
get:
|
||||
tags:
|
||||
- Accounts
|
||||
@@ -3187,6 +3257,18 @@ paths:
|
||||
enum:
|
||||
- inbound
|
||||
- outbound
|
||||
- in: query
|
||||
name: from
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
description: calling number to retrieve
|
||||
- in: query
|
||||
name: to
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
description: called number to retrieve
|
||||
get:
|
||||
tags:
|
||||
- Service Providers
|
||||
@@ -3743,8 +3825,13 @@ paths:
|
||||
type: string
|
||||
format: uuid
|
||||
description: The application to use to control this call. Either applicationSid or url is required.
|
||||
answerOnBridge:
|
||||
type: boolean
|
||||
description: If set to true, the inbound call will ring until the number that was dialed answers the call, and at that point a 200 OK will be sent on the inbound leg. If false, the inbound call will be answered immediately as the outbound call is placed.
|
||||
example: false
|
||||
call_hook:
|
||||
$ref: '#/components/schemas/Webhook'
|
||||
example: {"url": "https://acme.com/callhook", "method": "POST"}
|
||||
call_status_hook:
|
||||
$ref: '#/components/schemas/Webhook'
|
||||
example: {"url": "https://acme.com/status", "method": "POST"}
|
||||
@@ -3758,15 +3845,23 @@ paths:
|
||||
example: "blf.finotel.com"
|
||||
timeout:
|
||||
type: integer
|
||||
description: the number of seconds to wait for call to be answered. Defaults to 60.
|
||||
description: The number of seconds to wait for call to be answered. Defaults to 60.
|
||||
example: 30
|
||||
timeLimit:
|
||||
type: integer
|
||||
description: The max length of call in seconds
|
||||
example: 60
|
||||
tag:
|
||||
type: object
|
||||
description: initial set of customer-supplied metadata to associate with the call (see jambonz 'tag' verb)
|
||||
description: Initial set of customer-supplied metadata to associate with the call (see jambonz 'tag' verb)
|
||||
example: {"callCount": 10}
|
||||
to:
|
||||
$ref: '#/components/schemas/Target'
|
||||
description: destination for call
|
||||
description: Destination for call
|
||||
headers:
|
||||
type: object
|
||||
description: The customer SIP headers to associate with the call
|
||||
example: {"X-Custom-Header": "Hello"}
|
||||
responses:
|
||||
201:
|
||||
description: call successfully created
|
||||
@@ -3793,6 +3888,43 @@ paths:
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- in: query
|
||||
name: direction
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
enum:
|
||||
- inbound
|
||||
- outbound
|
||||
description: call direction to retrieve
|
||||
- in: query
|
||||
name: from
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
description: calling number to retrieve
|
||||
- in: query
|
||||
name: to
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
description: called number to retrieve
|
||||
- in: query
|
||||
name: callStatus
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
enum:
|
||||
- trying
|
||||
- ringing
|
||||
- early-media
|
||||
- in-progress
|
||||
- completed
|
||||
- failed
|
||||
- busy
|
||||
- no-answer
|
||||
- queued
|
||||
description: call status to retrieve
|
||||
responses:
|
||||
200:
|
||||
description: list of calls for a specified account
|
||||
@@ -3879,6 +4011,8 @@ paths:
|
||||
properties:
|
||||
call_hook:
|
||||
$ref: '#/components/schemas/Webhook'
|
||||
child_call_hook:
|
||||
$ref: '#/components/schemas/Webhook'
|
||||
call_status:
|
||||
type: string
|
||||
enum:
|
||||
@@ -3898,6 +4032,7 @@ paths:
|
||||
type: string
|
||||
enum:
|
||||
- pause
|
||||
- silence
|
||||
- resume
|
||||
mute_status:
|
||||
type: string
|
||||
@@ -4001,7 +4136,479 @@ paths:
|
||||
smpp_err_code:
|
||||
type: string
|
||||
400:
|
||||
description: bad request
|
||||
description: bad request
|
||||
/Accounts/{AccountSid}/Queues:
|
||||
parameters:
|
||||
- name: AccountSid
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
- in: query
|
||||
name: search
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
description: queue name of data to retrieve
|
||||
get:
|
||||
tags:
|
||||
- Accounts
|
||||
summary: retrieve active queues for an account
|
||||
operationId: listQueues
|
||||
responses:
|
||||
200:
|
||||
description: retrieve active queues records for a specified account
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
length:
|
||||
type: string
|
||||
/Lcrs:
|
||||
post:
|
||||
tags:
|
||||
- Lcrs
|
||||
summary: create a Least Cost Routing
|
||||
operationId: createLcr
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
description: name or Least Cost Routing
|
||||
example: twilioLcr
|
||||
required:
|
||||
- name
|
||||
responses:
|
||||
201:
|
||||
description: Least Cost Routing successfully created
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SuccessfulAdd'
|
||||
400:
|
||||
description: bad request
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GeneralError'
|
||||
422:
|
||||
description: unprocessable entity
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GeneralError'
|
||||
500:
|
||||
description: system error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GeneralError'
|
||||
get:
|
||||
tags:
|
||||
- Lcrs
|
||||
summary: list least cost routings
|
||||
operationId: listLeastCostRoutings
|
||||
responses:
|
||||
200:
|
||||
description: list of least cost routings
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Lcr'
|
||||
500:
|
||||
description: system error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GeneralError'
|
||||
/Lcrs/{LcrSid}:
|
||||
parameters:
|
||||
- name: LcrSid
|
||||
in: path
|
||||
required: true
|
||||
style: simple
|
||||
explode: false
|
||||
schema:
|
||||
type: string
|
||||
delete:
|
||||
tags:
|
||||
- Lcrs
|
||||
summary: delete a least cost routing
|
||||
operationId: deleteLeastCostRouting
|
||||
responses:
|
||||
204:
|
||||
description: least cost routing successfully deleted
|
||||
404:
|
||||
description: least cost routing not found
|
||||
422:
|
||||
description: unprocessable entity
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GeneralError'
|
||||
example:
|
||||
msg: a service provider with active accounts can not be deleted
|
||||
500:
|
||||
description: system error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GeneralError'
|
||||
get:
|
||||
tags:
|
||||
- Lcrs
|
||||
summary: retrieve least cost routing
|
||||
operationId: getLeastCostRouting
|
||||
responses:
|
||||
200:
|
||||
description: least cost routing found
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Lcr'
|
||||
404:
|
||||
description: least cost routing not found
|
||||
500:
|
||||
description: system error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GeneralError'
|
||||
put:
|
||||
tags:
|
||||
- Lcrs
|
||||
summary: update least cost routing
|
||||
operationId: updateLeastCostRouting
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Lcr'
|
||||
responses:
|
||||
204:
|
||||
description: least cost routing updated
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Lcr'
|
||||
400:
|
||||
description: bad request
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GeneralError'
|
||||
404:
|
||||
description: least cost routing not found
|
||||
500:
|
||||
description: system error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GeneralError'
|
||||
/LcrRoutes:
|
||||
post:
|
||||
tags:
|
||||
- LcrRoutes
|
||||
summary: create a Least Cost Routing Routes
|
||||
operationId: createLcrRoutes
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/LcrRoute'
|
||||
required:
|
||||
- lcr_sid
|
||||
- regex
|
||||
- priority
|
||||
responses:
|
||||
201:
|
||||
description: Least Cost Routing Route successfully created
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SuccessfulAdd'
|
||||
400:
|
||||
description: bad request
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GeneralError'
|
||||
422:
|
||||
description: unprocessable entity
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GeneralError'
|
||||
500:
|
||||
description: system error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GeneralError'
|
||||
get:
|
||||
tags:
|
||||
- LcrRoutes
|
||||
summary: list least cost routings routes
|
||||
operationId: listLeastCostRoutingRoutes
|
||||
responses:
|
||||
200:
|
||||
description: list of least cost routing routes
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/LcrRoute'
|
||||
500:
|
||||
description: system error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GeneralError'
|
||||
/LcrRoutes/{LcrRouteSid}:
|
||||
parameters:
|
||||
- name: LcrRouteSid
|
||||
in: path
|
||||
required: true
|
||||
style: simple
|
||||
explode: false
|
||||
schema:
|
||||
type: string
|
||||
delete:
|
||||
tags:
|
||||
- LcrRoutes
|
||||
summary: delete a least cost routing route
|
||||
operationId: deleteLeastCostRoutingRoute
|
||||
responses:
|
||||
204:
|
||||
description: least cost routing route successfully deleted
|
||||
404:
|
||||
description: least cost routing route not found
|
||||
422:
|
||||
description: unprocessable entity
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GeneralError'
|
||||
example:
|
||||
msg: a service provider with active accounts can not be deleted
|
||||
500:
|
||||
description: system error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GeneralError'
|
||||
get:
|
||||
tags:
|
||||
- LcrRoutes
|
||||
summary: retrieve least cost routing route
|
||||
operationId: getLeastCostRoutingRoute
|
||||
responses:
|
||||
200:
|
||||
description: least cost routing route found
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/LcrRoute'
|
||||
404:
|
||||
description: least cost routing route not found
|
||||
500:
|
||||
description: system error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GeneralError'
|
||||
put:
|
||||
tags:
|
||||
- LcrRoutes
|
||||
summary: update least cost routing route
|
||||
operationId: updateLeastCostRoutingRoute
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/LcrRoute'
|
||||
responses:
|
||||
204:
|
||||
description: least cost routing route updated
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/LcrRoute'
|
||||
400:
|
||||
description: bad request
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GeneralError'
|
||||
404:
|
||||
description: least cost routing route not found
|
||||
500:
|
||||
description: system error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GeneralError'
|
||||
/LcrCarrierSetEntries:
|
||||
post:
|
||||
tags:
|
||||
- LcrCarrierSetEntries
|
||||
summary: create a Least Cost Routing Carrier Set Entry
|
||||
operationId: createLcrCarrierSetEntry
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/LcrCarrierSetEntry'
|
||||
required:
|
||||
- lcr_route_sid
|
||||
- voip_carrier_sid
|
||||
- priority
|
||||
responses:
|
||||
201:
|
||||
description: Least Cost Routing Carrier Set Entry successfully created
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SuccessfulAdd'
|
||||
400:
|
||||
description: bad request
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GeneralError'
|
||||
422:
|
||||
description: unprocessable entity
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GeneralError'
|
||||
500:
|
||||
description: system error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GeneralError'
|
||||
get:
|
||||
tags:
|
||||
- LcrCarrierSetEntries
|
||||
summary: list least cost routings routes
|
||||
operationId: listLeastCostRoutingCarrierSetEntries
|
||||
responses:
|
||||
200:
|
||||
description: list of least cost routing carrier set entries
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/LcrCarrierSetEntry'
|
||||
500:
|
||||
description: system error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GeneralError'
|
||||
/LcrCarrierSetEntries/{LcrCarrierSetEntrySid}:
|
||||
parameters:
|
||||
- name: LcrCarrierSetEntrySid
|
||||
in: path
|
||||
required: true
|
||||
style: simple
|
||||
explode: false
|
||||
schema:
|
||||
type: string
|
||||
delete:
|
||||
tags:
|
||||
- LcrCarrierSetEntries
|
||||
summary: delete a least cost routing carrier set entry
|
||||
operationId: deleteLeastCostRoutingCarrierSetEntry
|
||||
responses:
|
||||
204:
|
||||
description: least cost routing carrier set entry successfully deleted
|
||||
404:
|
||||
description: least cost routing carrier set entry not found
|
||||
422:
|
||||
description: unprocessable entity
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GeneralError'
|
||||
example:
|
||||
msg: a service provider with active accounts can not be deleted
|
||||
500:
|
||||
description: system error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GeneralError'
|
||||
get:
|
||||
tags:
|
||||
- LcrCarrierSetEntries
|
||||
summary: retrieve least cost routing carrier set entry
|
||||
operationId: getLeastCostRoutingCarrierSetEntry
|
||||
responses:
|
||||
200:
|
||||
description: least cost routing carrier set entry found
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/LcrCarrierSetEntry'
|
||||
404:
|
||||
description: least cost routing carrier set entry not found
|
||||
500:
|
||||
description: system error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GeneralError'
|
||||
put:
|
||||
tags:
|
||||
- LcrCarrierSetEntries
|
||||
summary: update least cost routing carrier set entry
|
||||
operationId: updateLeastCostRoutingCarrierSetEntry
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/LcrCarrierSetEntry'
|
||||
responses:
|
||||
204:
|
||||
description: least cost routing carrier set entry updated
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/LcrCarrierSetEntry'
|
||||
400:
|
||||
description: bad request
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GeneralError'
|
||||
404:
|
||||
description: least cost routing carrier set entry not found
|
||||
500:
|
||||
description: system error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GeneralError'
|
||||
components:
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
@@ -4410,10 +5017,21 @@ components:
|
||||
- phone
|
||||
- sip
|
||||
- user
|
||||
- teams
|
||||
number:
|
||||
type: string
|
||||
sipUri:
|
||||
type: string
|
||||
tenant:
|
||||
type: string
|
||||
description: Microsoft Teams customer tenant domain name
|
||||
trunk:
|
||||
type: string
|
||||
vmail:
|
||||
type: boolean
|
||||
description: Dial directly into user's voicemail to leave a message
|
||||
overrideTo:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
auth:
|
||||
@@ -4898,6 +5516,57 @@ components:
|
||||
- voice_call_session
|
||||
- api_limit
|
||||
- devices
|
||||
Lcr:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
example: twilioLcr
|
||||
default_carrier_set_entry_sid:
|
||||
type: string
|
||||
example: 3fa85f64-5717-4562-b3fc-2c963f66afa6
|
||||
required:
|
||||
- name
|
||||
LcrRoute:
|
||||
type: object
|
||||
properties:
|
||||
lcr_sid:
|
||||
type: string
|
||||
example: 3fa85f64-5717-4562-b3fc-2c963f66afa6
|
||||
regex:
|
||||
type: string
|
||||
description: out going call Phone number regex
|
||||
example: 1*
|
||||
priority:
|
||||
type: number
|
||||
example: 1
|
||||
description:
|
||||
type: string
|
||||
example: this is example description
|
||||
required:
|
||||
- lcr_sid
|
||||
- regex
|
||||
- priority
|
||||
LcrCarrierSetEntry:
|
||||
type: object
|
||||
properties:
|
||||
workload:
|
||||
type: number
|
||||
example: 90
|
||||
description: traffic distribution value
|
||||
lcr_route_sid:
|
||||
type: string
|
||||
example: 3fa85f64-5717-4562-b3fc-2c963f66afa6
|
||||
voip_carrier_sid:
|
||||
type: string
|
||||
example: 3fa85f64-5717-4562-b3fc-2c963f66afa6
|
||||
priority:
|
||||
type: number
|
||||
example: 1
|
||||
required:
|
||||
- lcr_route_sid
|
||||
- voip_carrier_sid
|
||||
- priority
|
||||
|
||||
security:
|
||||
- bearerAuth: []
|
||||
@@ -23,7 +23,18 @@ const decrypt = (data) => {
|
||||
return decrpyted.toString();
|
||||
};
|
||||
|
||||
const obscureKey = (key, key_spoiler_length = 6) => {
|
||||
const key_spoiler_char = 'X';
|
||||
|
||||
if (!key || key.length <= key_spoiler_length) {
|
||||
return key;
|
||||
}
|
||||
|
||||
return `${key.slice(0, key_spoiler_length)}${key_spoiler_char.repeat(key.length - key_spoiler_length)}`;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
encrypt,
|
||||
decrypt
|
||||
decrypt,
|
||||
obscureKey
|
||||
};
|
||||
|
||||
@@ -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]
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -58,7 +64,7 @@ const getHomerSipTrace = async(logger, apiKey, callId) => {
|
||||
}
|
||||
};
|
||||
|
||||
const getHomerPcap = async(logger, apiKey, callIds) => {
|
||||
const getHomerPcap = async(logger, apiKey, callIds, method) => {
|
||||
if (!process.env.HOMER_BASE_URL || !process.env.HOMER_USERNAME || !process.env.HOMER_PASSWORD) {
|
||||
logger.debug('getHomerPcap: Homer integration not installed');
|
||||
}
|
||||
@@ -67,12 +73,23 @@ const getHomerPcap = async(logger, apiKey, callIds) => {
|
||||
const stream = await postPcap('/api/v3/export/call/messages/pcap', {
|
||||
param: {
|
||||
transaction: {
|
||||
call: true
|
||||
call: method === 'invite',
|
||||
registration: method === 'register',
|
||||
rest: false
|
||||
},
|
||||
orlogic: true,
|
||||
search: {
|
||||
'1_call': {
|
||||
callid: callIds
|
||||
}
|
||||
...(method === 'invite' && {
|
||||
'1_call': {
|
||||
callid: callIds
|
||||
}
|
||||
})
|
||||
,
|
||||
...(method === 'register' && {
|
||||
'1_registration': {
|
||||
callid: callIds
|
||||
}
|
||||
})
|
||||
},
|
||||
},
|
||||
timestamp: {
|
||||
|
||||
19
lib/utils/jaeger-utils.js
Normal file
19
lib/utils/jaeger-utils.js
Normal file
@@ -0,0 +1,19 @@
|
||||
const bent = require('bent');
|
||||
const getJSON = bent(process.env.JAEGER_BASE_URL || 'http://127.0.0.1', 'GET', 'json', 200);
|
||||
|
||||
const getJaegerTrace = async(logger, traceId) => {
|
||||
if (!process.env.JAEGER_BASE_URL) {
|
||||
logger.debug('getJaegerTrace: jaeger integration not installed');
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return await getJSON(`/api/v3/traces/${traceId}`);
|
||||
} catch (err) {
|
||||
const url = `${process.env.JAEGER_BASE_URL}/api/traces/${traceId}`;
|
||||
logger.error({err, traceId}, `getJaegerTrace: Error retrieving spans from ${url}`);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
getJaegerTrace
|
||||
};
|
||||
42
lib/utils/storage-utils.js
Normal file
42
lib/utils/storage-utils.js
Normal file
@@ -0,0 +1,42 @@
|
||||
const { S3Client, PutObjectCommand, GetObjectCommand } = require('@aws-sdk/client-s3');
|
||||
|
||||
async function testAwsS3(logger, opts) {
|
||||
const s3 = new S3Client({
|
||||
credentials: {
|
||||
accessKeyId: opts.access_key_id,
|
||||
secretAccessKey: opts.secret_access_key,
|
||||
},
|
||||
region: opts.region || 'us-east-1'
|
||||
});
|
||||
|
||||
const input = {
|
||||
'Body': 'Hello From Jambonz',
|
||||
'Bucket': opts.name,
|
||||
'Key': 'jambonz-sample.text'
|
||||
};
|
||||
|
||||
const command = new PutObjectCommand(input);
|
||||
|
||||
await s3.send(command);
|
||||
}
|
||||
|
||||
async function getS3Object(logger, opts) {
|
||||
const s3 = new S3Client({
|
||||
credentials: {
|
||||
accessKeyId: opts.access_key_id,
|
||||
secretAccessKey: opts.secret_access_key,
|
||||
},
|
||||
region: opts.region || 'us-east-1'
|
||||
});
|
||||
const command = new GetObjectCommand({
|
||||
Bucket: opts.name,
|
||||
Key: opts.key
|
||||
});
|
||||
const res = await s3.send(command);
|
||||
return res.Body;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
testAwsS3,
|
||||
getS3Object
|
||||
};
|
||||
8801
package-lock.json
generated
8801
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
31
package.json
31
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "jambonz-api-server",
|
||||
"version": "v0.8.2",
|
||||
"version": "0.8.4",
|
||||
"description": "",
|
||||
"main": "app.js",
|
||||
"scripts": {
|
||||
@@ -19,15 +19,17 @@
|
||||
"url": "https://github.com/jambonz/jambonz-api-server.git"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-transcribe": "^3.290.0",
|
||||
"@deepgram/sdk": "^1.10.2",
|
||||
"@google-cloud/speech": "^5.1.0",
|
||||
"@jambonz/db-helpers": "^0.7.3",
|
||||
"@jambonz/realtimedb-helpers": "^0.7.0",
|
||||
"@jambonz/speech-utils": "^0.0.8",
|
||||
"@jambonz/time-series": "^0.2.5",
|
||||
"@jambonz/verb-specifications": "^0.0.3",
|
||||
"@soniox/soniox-node": "^1.1.0",
|
||||
"@aws-sdk/client-transcribe": "^3.348.0",
|
||||
"@aws-sdk/client-s3": "^3.348.0",
|
||||
"@deepgram/sdk": "^1.21.0",
|
||||
"@google-cloud/speech": "^5.2.0",
|
||||
"@jambonz/db-helpers": "^0.9.0",
|
||||
"@jambonz/realtimedb-helpers": "^0.8.6",
|
||||
"@jambonz/speech-utils": "^0.0.15",
|
||||
"@jambonz/time-series": "^0.2.7",
|
||||
"@jambonz/verb-specifications": "^0.0.24",
|
||||
"@jambonz/lamejs": "^1.2.2",
|
||||
"@soniox/soniox-node": "^1.1.1",
|
||||
"argon2": "^0.30.3",
|
||||
"bent": "^7.3.12",
|
||||
"cors": "^2.8.5",
|
||||
@@ -41,6 +43,7 @@
|
||||
"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",
|
||||
@@ -48,11 +51,13 @@
|
||||
"stripe": "^8.222.0",
|
||||
"swagger-ui-express": "^4.4.0",
|
||||
"uuid": "^8.3.2",
|
||||
"yamljs": "^0.3.0"
|
||||
"yamljs": "^0.3.0",
|
||||
"ws": "^8.12.1",
|
||||
"wav": "^1.0.2"
|
||||
},
|
||||
"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",
|
||||
|
||||
@@ -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 { addToSortedSet } = require('@jambonz/realtimedb-helpers')({
|
||||
host: process.env.JAMBONES_REDIS_HOST,
|
||||
port: process.env.JAMBONES_REDIS_PORT || 6379
|
||||
}, logger);
|
||||
|
||||
|
||||
process.on('unhandledRejection', (reason, p) => {
|
||||
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
|
||||
@@ -158,11 +168,33 @@ test('account tests', async(t) => {
|
||||
queue_event_hook: {
|
||||
url: 'http://example.com/q',
|
||||
method: 'post'
|
||||
},
|
||||
record_all_calls: true,
|
||||
record_format: 'wav',
|
||||
bucket_credential: {
|
||||
vendor: 'aws_s3',
|
||||
region: 'us-east-1',
|
||||
name: 'recordings',
|
||||
access_key_id: 'access_key_id',
|
||||
secret_access_key: 'secret access key'
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
t.ok(result.statusCode === 204, 'successfully updated account using account level token');
|
||||
|
||||
/* verify that bucket credential is updated*/
|
||||
result = await request.get(`/Accounts/${sid}`, {
|
||||
auth: {bearer: accountLevelToken},
|
||||
json: true,
|
||||
});
|
||||
|
||||
t.ok(result.bucket_credential.vendor === 'aws_s3', 'bucket_vendor was updated');
|
||||
t.ok(result.bucket_credential.name === 'recordings', 'bucket_name was updated');
|
||||
t.ok(result.bucket_credential.access_key_id === 'access_key_id', 'bucket_access_key_id was updated');
|
||||
t.ok(result.record_all_calls === 1, 'record_all_calls was updated');
|
||||
t.ok(result.record_format === 'wav', 'record_format was updated');
|
||||
|
||||
/* verify that account level api key last_used was updated*/
|
||||
result = await request.get(`/Accounts/${sid}/ApiKeys`, {
|
||||
auth: {bearer: accountLevelToken},
|
||||
@@ -257,6 +289,31 @@ test('account tests', async(t) => {
|
||||
});
|
||||
t.ok(result.statusCode === 204, 'successfully deleted a call session limit for an account');
|
||||
|
||||
/* query account queues */
|
||||
await addToSortedSet(`queue:${sid}:test`, 'url1');
|
||||
await addToSortedSet(`queue:${sid}:dummy`, 'url2');
|
||||
|
||||
result = await request.get(`/Accounts/${sid}/Queues`, {
|
||||
auth: authAdmin,
|
||||
resolveWithFullResponse: true,
|
||||
json: true,
|
||||
});
|
||||
t.ok(result.statusCode === 200 && result.body.length === 2, 'successfully queried account queues info for an account');
|
||||
|
||||
result = await request.get(`/Accounts/${sid}/Queues?search=test`, {
|
||||
auth: authAdmin,
|
||||
resolveWithFullResponse: true,
|
||||
json: true,
|
||||
});
|
||||
t.ok(result.statusCode === 200 && result.body.length === 1, 'successfully queried account queue info with search for an account');
|
||||
|
||||
result = await request.get(`/Accounts/29d41725-9d3a-4f89-9f0b-f32b3e4d3159/Queues`, {
|
||||
auth: authAdmin,
|
||||
resolveWithFullResponse: true,
|
||||
json: true,
|
||||
});
|
||||
t.ok(result.statusCode === 200 && result.body.length === 0, 'successfully queried account queue info with for an invalid account');
|
||||
|
||||
/* delete account */
|
||||
result = await request.delete(`/Accounts/${sid}`, {
|
||||
auth: authAdmin,
|
||||
|
||||
@@ -125,7 +125,8 @@ test('application tests', async(t) => {
|
||||
"X-Reason" : "maximum call duration exceeded"\
|
||||
}\
|
||||
}\
|
||||
]'
|
||||
]',
|
||||
record_all_calls: true
|
||||
}
|
||||
});
|
||||
t.ok(result.statusCode === 204, 'successfully updated application');
|
||||
@@ -138,6 +139,7 @@ test('application tests', async(t) => {
|
||||
t.ok(result.messaging_hook.url === 'http://example2.com/mms' , 'successfully updated messaging_hook');
|
||||
app_json = JSON.parse(result.app_json);
|
||||
t.ok(app_json[0].verb === 'hangup', 'successfully updated app_json from application')
|
||||
t.ok(result.record_all_calls === 1, 'successfully updated record_all_calls from application')
|
||||
|
||||
/* remove applications app_json*/
|
||||
result = await request.put(`/Applications/${sid}`, {
|
||||
|
||||
12
test/auth.js
12
test/auth.js
@@ -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', {
|
||||
|
||||
@@ -83,4 +83,74 @@ test('Create Call Success Without Synthesizer in Payload', async (t) => {
|
||||
}
|
||||
}).then(data => { t.ok(false, 'Create Call should not be success') })
|
||||
.catch(error => { t.ok(error.response.statusCode === 400, 'Call failed for no synthesizer') });
|
||||
});
|
||||
|
||||
test("Create call with application sid and app_json", async(t) => {
|
||||
const app = require('../app');
|
||||
|
||||
const service_provider_sid = await createServiceProvider(request, 'account3_has_synthesizer');
|
||||
const account_sid = await createAccount(request, service_provider_sid, 'account3_has_synthesizer');
|
||||
|
||||
const token = jwt.sign({
|
||||
account_sid,
|
||||
scope: "account",
|
||||
permissions: ["PROVISION_USERS", "PROVISION_SERVICES", "VIEW_ONLY"]
|
||||
}, process.env.JWT_SECRET, { expiresIn: '1h' });
|
||||
const authUser = { bearer: token };
|
||||
const speech_sid = await createGoogleSpeechCredentials(request, account_sid, null, authUser, true, true);
|
||||
|
||||
// GIVEN
|
||||
/* add an application */
|
||||
|
||||
const app_json = '[\
|
||||
{\
|
||||
"verb": "play",\
|
||||
"url": "https://example.com/example.mp3",\
|
||||
"timeoutSecs": 10,\
|
||||
"seekOffset": 8000,\
|
||||
"actionHook": "/play/action"\
|
||||
}\
|
||||
]';
|
||||
let result = await request.post('/Applications', {
|
||||
resolveWithFullResponse: true,
|
||||
auth: authUser,
|
||||
json: true,
|
||||
body: {
|
||||
name: 'daveh',
|
||||
account_sid,
|
||||
call_hook: {
|
||||
url: 'http://example.com'
|
||||
},
|
||||
call_status_hook: {
|
||||
url: 'http://example.com/status',
|
||||
method: 'POST'
|
||||
},
|
||||
messaging_hook: {
|
||||
url: 'http://example.com/sms'
|
||||
},
|
||||
app_json
|
||||
}
|
||||
});
|
||||
t.ok(result.statusCode === 201, 'successfully created application');
|
||||
const sid = result.body.sid;
|
||||
|
||||
// WHEN
|
||||
result = await request.post(`/Accounts/${account_sid}/Calls`, {
|
||||
resolveWithFullResponse: true,
|
||||
auth: authUser,
|
||||
json: true,
|
||||
body: {
|
||||
application_sid: sid,
|
||||
from: "15083778299",
|
||||
to: {
|
||||
type: "phone",
|
||||
number: "15089084809"
|
||||
},
|
||||
}
|
||||
});
|
||||
// THEN
|
||||
t.ok(result.statusCode === 201, 'successfully created Call without Synthesizer && application_sid');
|
||||
const fs_request = await getLastRequestFromFeatureServer('15083778299_createCall');
|
||||
const obj = JSON.parse(fs_request);
|
||||
t.ok(obj.body.app_json == app_json, 'app_json successfully added')
|
||||
});
|
||||
119
test/clients.js
Normal file
119
test/clients.js
Normal file
@@ -0,0 +1,119 @@
|
||||
const test = require('tape') ;
|
||||
const ADMIN_TOKEN = '38700987-c7a4-4685-a5bb-af378f9734de';
|
||||
const authAdmin = {bearer: ADMIN_TOKEN};
|
||||
const request = require('request-promise-native').defaults({
|
||||
baseUrl: 'http://127.0.0.1:3000/v1'
|
||||
});
|
||||
|
||||
|
||||
process.on('unhandledRejection', (reason, p) => {
|
||||
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
|
||||
});
|
||||
|
||||
test('client test', async(t) => {
|
||||
const app = require('../app');
|
||||
|
||||
try {
|
||||
let result;
|
||||
/* add a service provider */
|
||||
result = await request.post('/ServiceProviders', {
|
||||
resolveWithFullResponse: true,
|
||||
auth: authAdmin,
|
||||
json: true,
|
||||
body: {
|
||||
name: 'client_sp',
|
||||
}
|
||||
});
|
||||
t.ok(result.statusCode === 201, 'successfully created client service provider');
|
||||
const sp_sid = result.body.sid;
|
||||
|
||||
/* add an account */
|
||||
result = await request.post('/Accounts', {
|
||||
resolveWithFullResponse: true,
|
||||
auth: authAdmin,
|
||||
json: true,
|
||||
body: {
|
||||
name: 'sample_account',
|
||||
service_provider_sid: sp_sid,
|
||||
registration_hook: {
|
||||
url: 'http://example.com/reg',
|
||||
method: 'get'
|
||||
},
|
||||
webhook_secret: 'foobar'
|
||||
}
|
||||
});
|
||||
t.ok(result.statusCode === 201, 'successfully created account');
|
||||
const account_sid = result.body.sid;
|
||||
|
||||
/* add new entity */
|
||||
result = await request.post('/Clients', {
|
||||
resolveWithFullResponse: true,
|
||||
auth: authAdmin,
|
||||
json: true,
|
||||
body: {
|
||||
account_sid,
|
||||
username: 'client1',
|
||||
password: 'sdf12412',
|
||||
is_active: 1
|
||||
}
|
||||
});
|
||||
t.ok(result.statusCode === 201, 'successfully created Client');
|
||||
const sid = result.body.sid;
|
||||
|
||||
/* query all entity */
|
||||
result = await request.get('/Clients', {
|
||||
auth: authAdmin,
|
||||
json: true,
|
||||
});
|
||||
t.ok(result.length === 1 , 'successfully queried all Clients');
|
||||
|
||||
/* query one entity */
|
||||
result = await request.get(`/Clients/${sid}`, {
|
||||
auth: authAdmin,
|
||||
json: true,
|
||||
});
|
||||
t.ok(result.account_sid === account_sid , 'successfully retrieved Client by sid');
|
||||
t.ok(result.client_sid, 'successfully retrieved Client by sid');
|
||||
t.ok(result.username === 'client1', 'successfully retrieved Client by sid');
|
||||
t.ok(result.is_active === 1 , 'successfully retrieved Client by sid');
|
||||
t.ok(result.password === 'sXXXXXXX' , 'successfully retrieved Client by sid');
|
||||
|
||||
/* update the entity */
|
||||
result = await request.put(`/Clients/${sid}`, {
|
||||
auth: authAdmin,
|
||||
json: true,
|
||||
resolveWithFullResponse: true,
|
||||
body: {
|
||||
is_active: 0
|
||||
}
|
||||
});
|
||||
t.ok(result.statusCode === 204, 'successfully updated Client');
|
||||
/* query one entity */
|
||||
result = await request.get(`/Clients/${sid}`, {
|
||||
auth: authAdmin,
|
||||
json: true,
|
||||
});
|
||||
t.ok(result.is_active === 0 , 'successfully updated Client');
|
||||
t.ok(result.password === 'sXXXXXXX' , 'successfully retrieved Client by sid');
|
||||
|
||||
/* delete Client */
|
||||
result = await request.delete(`/Clients/${sid}`, {
|
||||
resolveWithFullResponse: true,
|
||||
simple: false,
|
||||
json: true,
|
||||
auth: authAdmin
|
||||
});
|
||||
t.ok(result.statusCode === 204, 'successfully deleted Clients');
|
||||
|
||||
/* query all entity */
|
||||
result = await request.get('/Clients', {
|
||||
auth: authAdmin,
|
||||
json: true,
|
||||
});
|
||||
t.ok(result.length === 0 , 'successfully queried all Clients');
|
||||
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
t.end(err);
|
||||
}
|
||||
})
|
||||
325
test/forgot-password.js
Normal file
325
test/forgot-password.js
Normal 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");
|
||||
}
|
||||
});
|
||||
@@ -13,9 +13,16 @@ 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('./tts-cache');
|
||||
require('./clients');
|
||||
require('./docker_stop');
|
||||
|
||||
103
test/lcr-carriers-set-entries.js
Normal file
103
test/lcr-carriers-set-entries.js
Normal 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
93
test/lcr-routes.js
Normal 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
76
test/lcrs.js
Normal 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
84
test/login.js
Normal 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);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -76,6 +76,30 @@ test('recent calls tests', async(t) => {
|
||||
json: true,
|
||||
});
|
||||
t.ok(result.data.length === 5, 'retrieved 5 recent calls by account');
|
||||
|
||||
result = await request.get(`/Accounts/${account_sid}/RecentCalls?page=1&count=25&from=16`, {
|
||||
auth: authUser,
|
||||
json: true,
|
||||
});
|
||||
t.ok(result.data.length === 0, 'retrieved 5 recent calls by account and from');
|
||||
|
||||
result = await request.get(`/Accounts/${account_sid}/RecentCalls?page=1&count=25&from=15`, {
|
||||
auth: authUser,
|
||||
json: true,
|
||||
});
|
||||
t.ok(result.data.length === 5, 'retrieved 5 recent calls by account and from');
|
||||
|
||||
result = await request.get(`/Accounts/${account_sid}/RecentCalls?page=1&count=25&to=19`, {
|
||||
auth: authUser,
|
||||
json: true,
|
||||
});
|
||||
t.ok(result.data.length === 0, 'retrieved 5 recent calls by account and to');
|
||||
|
||||
result = await request.get(`/Accounts/${account_sid}/RecentCalls?page=1&count=25&to=18`, {
|
||||
auth: authUser,
|
||||
json: true,
|
||||
});
|
||||
t.ok(result.data.length === 5, 'retrieved 5 recent calls by account and to');
|
||||
//console.log({data: result.data}, 'Account recent calls');
|
||||
|
||||
/* query last 7 days by service provider */
|
||||
@@ -84,6 +108,30 @@ test('recent calls tests', async(t) => {
|
||||
json: true,
|
||||
});
|
||||
t.ok(result.data.length === 5, 'retrieved 5 recent calls by service provider');
|
||||
|
||||
result = await request.get(`/ServiceProviders/${service_provider_sid}/RecentCalls?page=1&count=25&from=16`, {
|
||||
auth: authAdmin,
|
||||
json: true,
|
||||
});
|
||||
t.ok(result.data.length === 0, 'retrieved 5 recent calls by service provider and from');
|
||||
|
||||
result = await request.get(`/ServiceProviders/${service_provider_sid}/RecentCalls?page=1&count=25&from=15`, {
|
||||
auth: authAdmin,
|
||||
json: true,
|
||||
});
|
||||
t.ok(result.data.length === 5, 'retrieved 5 recent calls by service provider and from');
|
||||
|
||||
result = await request.get(`/ServiceProviders/${service_provider_sid}/RecentCalls?page=1&count=25&to=19`, {
|
||||
auth: authAdmin,
|
||||
json: true,
|
||||
});
|
||||
t.ok(result.data.length === 0, 'retrieved 5 recent calls by service provider and to');
|
||||
|
||||
result = await request.get(`/ServiceProviders/${service_provider_sid}/RecentCalls?page=1&count=25&to=18`, {
|
||||
auth: authAdmin,
|
||||
json: true,
|
||||
});
|
||||
t.ok(result.data.length === 5, 'retrieved 5 recent calls by service provider and to');
|
||||
//console.log({data: result.data}, 'SP recent calls');
|
||||
|
||||
/* pull sip traces and pcap from homer */
|
||||
|
||||
@@ -27,7 +27,8 @@ test('sip gateway tests', async(t) => {
|
||||
ipv4: '192.168.1.1',
|
||||
netmask: 32,
|
||||
inbound: true,
|
||||
outbound: true
|
||||
outbound: true,
|
||||
protocol: 'tcp'
|
||||
}
|
||||
});
|
||||
t.ok(result.statusCode === 201, 'successfully created sip gateway ');
|
||||
@@ -48,6 +49,7 @@ test('sip gateway tests', async(t) => {
|
||||
});
|
||||
//console.log(`result: ${JSON.stringify(result)}`);
|
||||
t.ok(result.ipv4 === '192.168.1.1' , 'successfully retrieved voip carrier by sid');
|
||||
t.ok(result.protocol === 'tcp' , 'successfully retrieved voip carrier by sid');
|
||||
|
||||
|
||||
/* update sip gateway */
|
||||
@@ -58,7 +60,8 @@ test('sip gateway tests', async(t) => {
|
||||
body: {
|
||||
port: 5061,
|
||||
netmask:24,
|
||||
outbound: false
|
||||
outbound: false,
|
||||
protocol: 'udp'
|
||||
}
|
||||
});
|
||||
t.ok(result.statusCode === 204, 'successfully updated voip carrier');
|
||||
@@ -75,7 +78,7 @@ test('sip gateway tests', async(t) => {
|
||||
|
||||
await deleteObjectBySid(request, '/VoipCarriers', voip_carrier_sid);
|
||||
|
||||
//t.end();
|
||||
t.end();
|
||||
}
|
||||
catch (err) {
|
||||
console.error(err);
|
||||
|
||||
@@ -71,6 +71,7 @@ test('speech credentials tests', async(t) => {
|
||||
|
||||
const token = jwt.sign({
|
||||
account_sid,
|
||||
service_provider_sid,
|
||||
scope: 'account',
|
||||
permissions: ["PROVISION_USERS", "PROVISION_SERVICES", "VIEW_ONLY"]
|
||||
}, process.env.JWT_SECRET, { expiresIn: '1h' });
|
||||
@@ -393,6 +394,7 @@ test('speech credentials tests', async(t) => {
|
||||
auth: authUser,
|
||||
json: true,
|
||||
body: {
|
||||
service_provider_sid: service_provider_sid,
|
||||
vendor: 'nvidia',
|
||||
use_for_stt: true,
|
||||
use_for_tts: true,
|
||||
|
||||
64
test/system-information.js
Normal file
64
test/system-information.js
Normal file
@@ -0,0 +1,64 @@
|
||||
const test = require('tape') ;
|
||||
const jwt = require('jsonwebtoken');
|
||||
const ADMIN_TOKEN = '38700987-c7a4-4685-a5bb-af378f9734de';
|
||||
const authAdmin = {bearer: ADMIN_TOKEN};
|
||||
const request = require('request-promise-native').defaults({
|
||||
baseUrl: 'http://127.0.0.1:3000/v1'
|
||||
});
|
||||
|
||||
test('system information test', async(t) => {
|
||||
const app = require('../app');
|
||||
try {
|
||||
let result = await request.post('/SystemInformation', {
|
||||
resolveWithFullResponse: true,
|
||||
auth: authAdmin,
|
||||
json: true,
|
||||
body: {
|
||||
domain_name: 'test.com',
|
||||
sip_domain_name: 'sip.test.com',
|
||||
monitoring_domain_name: 'monitor.test.com'
|
||||
}
|
||||
});
|
||||
t.ok(result.statusCode === 201, 'successfully created system information ');
|
||||
let body = result.body;
|
||||
t.ok(body.domain_name === 'test.com', 'added domain_name ok');
|
||||
t.ok(body.sip_domain_name === 'sip.test.com', 'added sip_domain_name ok');
|
||||
t.ok(body.monitoring_domain_name === 'monitor.test.com', 'added monitoring_domain_name ok');
|
||||
|
||||
result = await request.get('/SystemInformation', {
|
||||
auth: authAdmin,
|
||||
json: true,
|
||||
});
|
||||
t.ok(result.domain_name === 'test.com', 'get domain_name ok');
|
||||
t.ok(result.sip_domain_name === 'sip.test.com', 'get sip_domain_name ok');
|
||||
t.ok(result.monitoring_domain_name === 'monitor.test.com', 'get monitoring_domain_name ok');
|
||||
|
||||
result = await request.post('/SystemInformation', {
|
||||
resolveWithFullResponse: true,
|
||||
auth: authAdmin,
|
||||
json: true,
|
||||
body: {
|
||||
domain_name: 'test1.com',
|
||||
sip_domain_name: 'sip1.test.com',
|
||||
monitoring_domain_name: 'monitor1.test.com'
|
||||
}
|
||||
});
|
||||
t.ok(result.statusCode === 201, 'successfully updated system information ');
|
||||
body = result.body;
|
||||
t.ok(body.domain_name === 'test1.com', 'updated domain_name ok');
|
||||
t.ok(body.sip_domain_name === 'sip1.test.com', 'updated sip_domain_name ok');
|
||||
t.ok(body.monitoring_domain_name === 'monitor1.test.com', 'updated monitoring_domain_name ok');
|
||||
|
||||
result = await request.get('/SystemInformation', {
|
||||
auth: authAdmin,
|
||||
json: true,
|
||||
});
|
||||
t.ok(result.domain_name === 'test1.com', 'get domain_name ok');
|
||||
t.ok(result.sip_domain_name === 'sip1.test.com', 'get sip_domain_name ok');
|
||||
t.ok(result.monitoring_domain_name === 'monitor1.test.com', 'get monitoring_domain_name ok');
|
||||
|
||||
} catch(err) {
|
||||
console.error(err);
|
||||
t.end(err);
|
||||
}
|
||||
});
|
||||
56
test/tts-cache.js
Normal file
56
test/tts-cache.js
Normal file
@@ -0,0 +1,56 @@
|
||||
const test = require('tape') ;
|
||||
const jwt = require('jsonwebtoken');
|
||||
const ADMIN_TOKEN = '38700987-c7a4-4685-a5bb-af378f9734de';
|
||||
const authAdmin = {bearer: ADMIN_TOKEN};
|
||||
const request = require('request-promise-native').defaults({
|
||||
baseUrl: 'http://127.0.0.1:3000/v1'
|
||||
});
|
||||
const crypto = require('crypto');
|
||||
const logger = require('../lib/logger');
|
||||
|
||||
const {
|
||||
client,
|
||||
} = require('@jambonz/speech-utils')({
|
||||
host: process.env.JAMBONES_REDIS_HOST,
|
||||
port: process.env.JAMBONES_REDIS_PORT || 6379
|
||||
}, logger);
|
||||
|
||||
function makeSynthKey({account_sid = '', vendor, language, voice, engine = '', text}) {
|
||||
const hash = crypto.createHash('sha1');
|
||||
hash.update(`${language}:${vendor}:${voice}:${engine}:${text}`);
|
||||
return `tts${account_sid ? (':' + account_sid) : ''}:${hash.digest('hex')}`;
|
||||
}
|
||||
|
||||
test('tts-cache', async(t) => {
|
||||
const app = require('../app');
|
||||
try {
|
||||
// create caches
|
||||
const minRecords = 8;
|
||||
for (const i in Array(minRecords).fill(0)) {
|
||||
await client.set(makeSynthKey({vendor: i, language: i, voice: i, engine: i, text: i}), i);
|
||||
}
|
||||
|
||||
let result = await request.get('/TtsCache', {
|
||||
auth: authAdmin,
|
||||
json: true,
|
||||
});
|
||||
|
||||
t.ok(result.size === minRecords, 'get cache correctly');
|
||||
|
||||
result = await request.delete('/TtsCache', {
|
||||
auth: authAdmin,
|
||||
resolveWithFullResponse: true,
|
||||
});
|
||||
t.ok(result.statusCode === 204, 'successfully deleted application after removing phone number');
|
||||
|
||||
result = await request.get('/TtsCache', {
|
||||
auth: authAdmin,
|
||||
json: true,
|
||||
});
|
||||
|
||||
t.ok(result.size === 0, 'deleted cache successfully');
|
||||
} catch(err) {
|
||||
console.error(err);
|
||||
t.end(err);
|
||||
}
|
||||
});
|
||||
@@ -83,7 +83,7 @@ test('user tests', async(t) => {
|
||||
}
|
||||
});
|
||||
t.ok(result.statusCode === 201 && result.body.user_sid, 'service_provider scope user created');
|
||||
const sp_user_sid = result.body.sid;
|
||||
const sp_user_sid = result.body.user_sid;
|
||||
|
||||
/* add an account */
|
||||
result = await request.post('/Accounts', {
|
||||
@@ -119,7 +119,7 @@ test('user tests', async(t) => {
|
||||
}
|
||||
});
|
||||
t.ok(result.statusCode === 201 && result.body.user_sid, 'account scope user created');
|
||||
const account_user_sid = result.body.sid;
|
||||
const account_user_sid = result.body.user_sid;
|
||||
|
||||
/* retrieve list of users */
|
||||
result = await request.get(`/Users`, {
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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}`, {
|
||||
|
||||
Reference in New Issue
Block a user