Compare commits

..

10 Commits

Author SHA1 Message Date
Dave Horton
f4f1827019 minor change in limits enum 2022-11-27 15:29:23 -05:00
Dave Horton
8263b46fff update limits enum again 2022-11-26 12:56:18 -05:00
Dave Horton
b6699a37c3 add limits license_count and voice_call_minutes 2022-11-25 11:03:20 -05:00
Guilherme Rauen
0fc3c95cc5 update node image to the latest and most secure (#81)
Co-authored-by: Guilherme Rauen <g.rauen@cognigy.com>
2022-11-11 17:44:40 -05:00
Dave Horton
aeebf6aee0 add real testing of microsoft stt 2022-11-10 18:11:04 -05:00
Dave Horton
8026f45467 update deps 2022-11-05 10:44:19 -04:00
Dave Horton
505884e68e update deps 2022-11-04 08:31:41 -04:00
Dave Horton
9e4d6eb88b update deps 2022-11-02 13:39:33 -04:00
Dave Horton
e1cfbe5010 update db-helpers 2022-11-01 21:21:38 -04:00
Dave Horton
b8dc0b2f05 update google speech libs 2022-10-31 11:15:40 -04:00
187 changed files with 9248 additions and 37217 deletions

View File

@@ -1,23 +1,19 @@
name: CI
on: [push, pull_request, workflow_dispatch]
on:
push:
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Docker Compose
run: |
sudo curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
docker-compose --version
- uses: actions/setup-node@v4
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: lts/*
node-version: 14
- run: npm install
- run: npm run jslint
- run: npm test
- run: npm run test:encrypt-decrypt

View File

@@ -2,10 +2,16 @@ 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:
@@ -14,42 +20,32 @@ jobs:
if: github.event_name == 'push'
steps:
- name: Checkout code
uses: actions/checkout@v4
- uses: actions/checkout@v2
- name: prepare tag
id: prepare_tag
- 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
run: |
IMAGE_ID=jambonz/db-create
IMAGE_ID=ghcr.io/${{ github.repository_owner }}/$IMAGE_NAME
# Strip git ref prefix from version
VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,')
# Change all uppercase to lowercase
IMAGE_ID=$(echo $IMAGE_ID | tr '[A-Z]' '[a-z]')
# Strip "v" prefix from tag name
[[ "${{ github.ref }}" == "refs/tags/"* ]] && VERSION=$(echo $VERSION | sed -e 's/^v//')
# Strip git ref prefix from version
VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,')
# Use Docker `latest` tag convention
[ "$VERSION" == "main" ] && VERSION=latest
# Strip "v" prefix from tag name
[[ "${{ github.ref }}" == "refs/tags/"* ]] && VERSION=$(echo $VERSION | sed -e 's/^v//')
echo IMAGE_ID=$IMAGE_ID
echo VERSION=$VERSION
# Use Docker `latest` tag convention
[ "$VERSION" == "main" ] && VERSION=latest
echo "image_id=$IMAGE_ID" >> $GITHUB_OUTPUT
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo IMAGE_ID=$IMAGE_ID
echo VERSION=$VERSION
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push Docker image
uses: docker/build-push-action@v6
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
docker tag $IMAGE_NAME $IMAGE_ID:$VERSION
docker push $IMAGE_ID:$VERSION

View File

@@ -2,10 +2,16 @@ 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:
@@ -14,41 +20,32 @@ jobs:
if: github.event_name == 'push'
steps:
- name: Checkout code
uses: actions/checkout@v4
- uses: actions/checkout@v2
- name: prepare tag
id: prepare_tag
- 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
run: |
IMAGE_ID=jambonz/api-server
IMAGE_ID=ghcr.io/${{ github.repository_owner }}/$IMAGE_NAME
# Strip git ref prefix from version
VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,')
# Change all uppercase to lowercase
IMAGE_ID=$(echo $IMAGE_ID | tr '[A-Z]' '[a-z]')
# Strip "v" prefix from tag name
[[ "${{ github.ref }}" == "refs/tags/"* ]] && VERSION=$(echo $VERSION | sed -e 's/^v//')
# Strip git ref prefix from version
VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,')
# Use Docker `latest` tag convention
[ "$VERSION" == "main" ] && VERSION=latest
# Strip "v" prefix from tag name
[[ "${{ github.ref }}" == "refs/tags/"* ]] && VERSION=$(echo $VERSION | sed -e 's/^v//')
echo IMAGE_ID=$IMAGE_ID
echo VERSION=$VERSION
# Use Docker `latest` tag convention
[ "$VERSION" == "main" ] && VERSION=latest
echo "image_id=$IMAGE_ID" >> $GITHUB_OUTPUT
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo IMAGE_ID=$IMAGE_ID
echo VERSION=$VERSION
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push Docker image
uses: docker/build-push-action@v6
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
docker tag $IMAGE_NAME $IMAGE_ID:$VERSION
docker push $IMAGE_ID:$VERSION

View File

@@ -1,10 +1,10 @@
FROM --platform=linux/amd64 node:24-alpine AS base
FROM --platform=linux/amd64 node:18.12.1-alpine3.16 as base
RUN apk --update --no-cache add --virtual .builds-deps build-base python3
WORKDIR /opt/app/
FROM base AS build
FROM base as build
COPY package.json package-lock.json ./
@@ -18,6 +18,6 @@ COPY --from=build /opt/app /opt/app/
ARG NODE_ENV
ENV NODE_ENV=$NODE_ENV
ENV NODE_ENV $NODE_ENV
CMD [ "node", "app.js" ]
CMD [ "node", "app.js" ]

View File

@@ -1,10 +1,10 @@
FROM --platform=linux/amd64 node:24-alpine AS base
FROM --platform=linux/amd64 node:18.9.0-alpine3.16 as base
RUN apk --update --no-cache add --virtual .builds-deps build-base python3
WORKDIR /opt/app/
FROM base AS build
FROM base as build
COPY package.json package-lock.json ./
@@ -18,6 +18,6 @@ COPY --from=build /opt/app /opt/app/
ARG NODE_ENV
ENV NODE_ENV=$NODE_ENV
ENV NODE_ENV $NODE_ENV
CMD [ "npm", "run", "upgrade-db" ]

View File

@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2018-2024 FirstFive8, Inc.
Copyright (c) 2021 Drachtio Communications Services, LLC
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

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

152
app.js
View File

@@ -1,30 +1,26 @@
const assert = require('assert');
const logger = require('./lib/logger');
const opts = Object.assign({
timestamp: () => {
return `, "time": "${new Date().toISOString()}"`;
}
}, {
level: process.env.JAMBONES_LOGLEVEL || 'info'
});
const logger = require('pino')(opts);
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');
const {verifyViewOnlyUser} = require('./lib/middleware');
const routes = require('./lib/routes');
const Registrar = require('@jambonz/mw-registrar');
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');
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_REDIS_HOST, 'missing JAMBONES_REDIS_HOST env var');
assert.ok(process.env.JAMBONES_TIME_SERIES_HOST, 'missing JAMBONES_TIME_SERIES_HOST env var');
assert.ok(process.env.ENCRYPTION_SECRET || process.env.JWT_SECRET, 'missing ENCRYPTION_SECRET env var');
assert.ok(process.env.JWT_SECRET, 'missing JWT_SECRET env var');
const {
queryCdrs,
queryCdrsSP,
@@ -37,28 +33,18 @@ const {
logger, process.env.JAMBONES_TIME_SERIES_HOST
);
const {
client,
retrieveCall,
deleteCall,
listCalls,
listSortedSets,
purgeCalls,
retrieveSet,
addKey,
retrieveKey,
deleteKey,
incrKey,
listConferences,
getCallCount
} = require('./lib/helpers/realtimedb-helpers');
const {
getTtsVoices,
getTtsSize,
purgeTtsCache,
getAwsAuthToken,
getVerbioAccessToken,
synthAudio
} = require('@jambonz/speech-utils')({}, logger);
deleteKey
} = require('@jambonz/realtimedb-helpers')({
host: process.env.JAMBONES_REDIS_HOST || 'localhost',
port: process.env.JAMBONES_REDIS_PORT || 6379
}, logger);
const {
lookupAppBySid,
lookupAccountBySid,
@@ -66,8 +52,7 @@ const {
lookupAppByPhoneNumber,
lookupCarrierBySid,
lookupSipGatewayBySid,
lookupSmppGatewayBySid,
lookupClientByAccountAndUsername
lookupSmppGatewayBySid
} = require('@jambonz/db-helpers')({
host: process.env.JAMBONES_MYSQL_HOST,
user: process.env.JAMBONES_MYSQL_USER,
@@ -78,33 +63,21 @@ 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);
app.locals = app.locals || {};
app.locals = {
...app.locals,
registrar: new Registrar(logger, client),
logger,
retrieveCall,
deleteCall,
listCalls,
listSortedSets,
listConferences,
purgeCalls,
retrieveSet,
addKey,
incrKey,
retrieveKey,
deleteKey,
getTtsVoices,
getTtsSize,
getAwsAuthToken,
getVerbioAccessToken,
purgeTtsCache,
synthAudio,
lookupAppBySid,
lookupAccountBySid,
lookupAccountByPhoneNumber,
@@ -112,15 +85,13 @@ app.locals = {
lookupCarrierBySid,
lookupSipGatewayBySid,
lookupSmppGatewayBySid,
lookupClientByAccountAndUsername,
queryCdrs,
queryCdrsSP,
queryAlerts,
queryAlertsSP,
writeCdrs,
writeAlerts,
AlertType,
getCallCount
AlertType
};
const unless = (paths, middleware) => {
@@ -130,35 +101,13 @@ const unless = (paths, middleware) => {
};
};
const RATE_LIMIT_BY = process.env.RATE_LIMIT_BY || 'system';
const limiter = rateLimit({
windowMs: (process.env.RATE_LIMIT_WINDOWS_MINS || 5) * 60 * 1000, // 5 minutes
max: process.env.RATE_LIMIT_MAX_PER_WINDOW || 600, // Limit each IP to 600 requests per `window`
standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
legacyHeaders: false, // Disable the `X-RateLimit-*` headers
keyGenerator: (req, res) => {
switch (RATE_LIMIT_BY) {
case 'system':
return '127.0.0.1';
case 'apikey':
// uses shared limit for requests without an authorization header
const token = req.headers.authorization?.split(' ')[1] || '127.0.0.1';
return token;
case 'ip':
return req.headers['x-real-ip'];
default:
return '127.0.0.1';
}
}
});
// 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) {
@@ -170,19 +119,12 @@ if (process.env.JAMBONES_TRUST_PROXY) {
});
}
}
const disableRateLimit = process.env.DISABLE_RATE_LIMITS === 'true' || process.env.DISABLE_RATE_LIMITS === '1';
if (!disableRateLimit) {
app.use(limiter);
}
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(
[
@@ -196,19 +138,6 @@ app.use('/v1', unless(
'/InviteCodes',
'/PredefinedCarriers'
], passport.authenticate('bearer', {session: false})));
app.use('/v1', unless(
[
'/register',
'/forgot-password',
'/signin',
'/login',
'/messaging',
'/outboundSMS',
'/AccountTest',
'/InviteCodes',
'/PredefinedCarriers',
'/logout'
], verifyViewOnlyUser));
app.use('/', routes);
app.use((err, req, res, next) => {
logger.error(err, 'burped error');
@@ -217,52 +146,7 @@ app.use((err, req, res, next) => {
});
});
logger.info(`listening for HTTP traffic on port ${PORT}`);
const server = app.listen(PORT);
const isValidWsKey = (hdr) => {
const username = process.env.JAMBONZ_RECORD_WS_USERNAME || process.env.JAMBONES_RECORD_WS_USERNAME;
const password = process.env.JAMBONZ_RECORD_WS_PASSWORD || process.env.JAMBONES_RECORD_WS_PASSWORD;
if (username && password) {
if (!hdr) {
// auth header is missing
return false;
}
const token = Buffer.from(`${username}:${password}`).toString('base64');
const arr = /^Basic (.*)$/.exec(hdr);
if (!Array.isArray(arr)) {
// malformed auth header
return false;
}
return arr[1] === token;
}
return true;
};
server.on('upgrade', (request, socket, head) => {
logger.debug({
url: request.url,
headers: request.headers,
}, 'received upgrade request');
/* verify the path starts with /transcribe */
if (!request.url.includes('/record/')) {
logger.info(`unhandled path: ${request.url}`);
return socket.write('HTTP/1.1 404 Not Found \r\n\r\n', () => socket.destroy());
}
/* verify the api key */
if (!isValidWsKey(request.headers['authorization'])) {
logger.info(`invalid auth header: ${request.headers['authorization'] || 'authorization header missing'}`);
return socket.write('HTTP/1.1 403 Forbidden \r\n\r\n', () => socket.destroy());
}
/* complete the upgrade */
wsServer.handleUpgrade(request, socket, head, (ws) => {
logger.debug(`upgraded to websocket, url: ${request.url}`);
wsServer.emit('connection', ws, request.url);
});
});
app.listen(PORT);
// purge old calls from active call set every 10 mins
async function purge() {

View File

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

View File

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

View File

@@ -1,22 +0,0 @@
#!/bin/bash
# This script exports the 'jambones' database (schema and data)
# from the source MySQL server into a file.
# Configuration variables
SOURCE_HOST=
DB_USER=
DB_PASS=
DB_NAME=
EXPORT_FILE="jambones_export.sql"
# Export the database using mysqldump
echo "Exporting database '$DB_NAME' from $SOURCE_HOST..."
mysqldump -h "$SOURCE_HOST" -u "$DB_USER" -p"$DB_PASS" "$DB_NAME" > "$EXPORT_FILE"
# Check for errors
if [ $? -eq 0 ]; then
echo "Database export successful. Export file created: $EXPORT_FILE"
else
echo "Error exporting database '$DB_NAME'."
exit 1
fi

View File

@@ -1,31 +0,0 @@
#!/bin/bash
# This script imports the SQL dump file into the target MySQL server.
# It first drops the existing 'jambones' database (if it exists),
# recreates it, and then imports the dump file.
# Configuration variables
TARGET_HOST=
DB_USER=
DB_PASS=
DB_NAME=
IMPORT_FILE="jambones_export.sql"
# Drop the existing database (if any) and create a new one
echo "Dropping and recreating database '$DB_NAME' on $TARGET_HOST..."
mysql -h "$TARGET_HOST" -u "$DB_USER" -p"$DB_PASS" -e "DROP DATABASE IF EXISTS \`$DB_NAME\`; CREATE DATABASE \`$DB_NAME\`;"
if [ $? -ne 0 ]; then
echo "Error dropping/creating database '$DB_NAME'."
exit 1
fi
# Import the SQL dump into the newly created database
echo "Importing dump file '$IMPORT_FILE' into database '$DB_NAME'..."
mysql -h "$TARGET_HOST" -u "$DB_USER" -p"$DB_PASS" "$DB_NAME" < "$IMPORT_FILE"
if [ $? -eq 0 ]; then
echo "Database import successful."
else
echo "Error importing the database."
exit 1
fi

View File

@@ -14,22 +14,14 @@ 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;
DROP TABLE IF EXISTS password_settings;
DROP TABLE IF EXISTS user_permissions;
DROP TABLE IF EXISTS permissions;
DROP TABLE IF EXISTS predefined_sip_gateways;
DROP TABLE IF EXISTS predefined_smpp_gateways;
@@ -54,12 +46,8 @@ DROP TABLE IF EXISTS signup_history;
DROP TABLE IF EXISTS smpp_addresses;
DROP TABLE IF EXISTS google_custom_voices;
DROP TABLE IF EXISTS speech_credentials;
DROP TABLE IF EXISTS system_information;
DROP TABLE IF EXISTS users;
DROP TABLE IF EXISTS smpp_gateways;
@@ -132,19 +120,6 @@ 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),
allow_direct_app_calling BOOLEAN NOT NULL DEFAULT 1,
allow_direct_queue_calling BOOLEAN NOT NULL DEFAULT 1,
allow_direct_user_calling BOOLEAN NOT NULL DEFAULT 1,
PRIMARY KEY (client_sid)
);
CREATE TABLE dns_records
(
dns_record_sid CHAR(36) NOT NULL UNIQUE ,
@@ -157,23 +132,11 @@ 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 COMMENT 'lower priority routes are attempted first',
priority INTEGER NOT NULL UNIQUE COMMENT 'lower priority routes are attempted first',
PRIMARY KEY (lcr_route_sid)
) COMMENT='An ordered list of digit patterns in an LCR table. The pat';
CREATE TABLE lcr
(
lcr_sid CHAR(36) NOT NULL UNIQUE ,
name VARCHAR(64) COMMENT 'User-assigned name for this LCR table',
is_active BOOLEAN NOT NULL DEFAULT 1,
default_carrier_set_entry_sid CHAR(36) COMMENT 'default carrier/route to use when no digit match based results are found.',
service_provider_sid CHAR(36),
account_sid CHAR(36),
PRIMARY KEY (lcr_sid)
) COMMENT='An LCR (least cost routing) table that is used by a service ';
) COMMENT='Least cost routing table';
CREATE TABLE password_settings
(
@@ -182,14 +145,6 @@ require_digit BOOLEAN NOT NULL DEFAULT false,
require_special_character BOOLEAN NOT NULL DEFAULT false
);
CREATE TABLE permissions
(
permission_sid CHAR(36) NOT NULL UNIQUE ,
name VARCHAR(32) NOT NULL UNIQUE ,
description VARCHAR(255),
PRIMARY KEY (permission_sid)
);
CREATE TABLE predefined_carriers
(
predefined_carrier_sid CHAR(36) NOT NULL UNIQUE ,
@@ -204,7 +159,6 @@ tech_prefix VARCHAR(16) COMMENT 'tech prefix to prepend to outbound calls to thi
inbound_auth_username VARCHAR(64),
inbound_auth_password VARCHAR(64),
diversion VARCHAR(32),
trunk_type ENUM('static_ip','auth','reg') NOT NULL DEFAULT 'static_ip',
PRIMARY KEY (predefined_carrier_sid)
);
@@ -282,10 +236,7 @@ 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)
);
@@ -341,31 +292,9 @@ last_tested DATETIME,
tts_tested_ok BOOLEAN,
stt_tested_ok BOOLEAN,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
label VARCHAR(64),
PRIMARY KEY (speech_credential_sid)
);
CREATE TABLE google_custom_voices
(
google_custom_voice_sid CHAR(36) NOT NULL UNIQUE ,
speech_credential_sid CHAR(36) NOT NULL,
model VARCHAR(512) NOT NULL,
reported_usage ENUM('REPORTED_USAGE_UNSPECIFIED','REALTIME','OFFLINE') DEFAULT 'REALTIME',
name VARCHAR(64) NOT NULL,
voice_cloning_key MEDIUMTEXT,
use_voice_cloning_key BOOLEAN DEFAULT false,
PRIMARY KEY (google_custom_voice_sid)
);
CREATE TABLE system_information
(
domain_name VARCHAR(255),
sip_domain_name VARCHAR(255),
monitoring_domain_name VARCHAR(255),
private_network_cidr VARCHAR(8192),
log_level ENUM('info', 'debug') NOT NULL DEFAULT 'info'
);
CREATE TABLE users
(
user_sid CHAR(36) NOT NULL UNIQUE ,
@@ -416,21 +345,9 @@ 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),
dtmf_type ENUM('rfc2833','tones','info') NOT NULL DEFAULT 'rfc2833',
outbound_sip_proxy VARCHAR(255),
trunk_type ENUM('static_ip','auth','reg') NOT NULL DEFAULT 'static_ip',
PRIMARY KEY (voip_carrier_sid)
) COMMENT='A Carrier or customer PBX that can send or receive calls';
CREATE TABLE user_permissions
(
user_permissions_sid CHAR(36) NOT NULL UNIQUE ,
user_sid CHAR(36) NOT NULL,
permission_sid CHAR(36) NOT NULL,
PRIMARY KEY (user_permissions_sid)
);
CREATE TABLE smpp_gateways
(
smpp_gateway_sid CHAR(36) NOT NULL UNIQUE ,
@@ -448,7 +365,7 @@ PRIMARY KEY (smpp_gateway_sid)
CREATE TABLE phone_numbers
(
phone_number_sid CHAR(36) UNIQUE ,
number VARCHAR(132) NOT NULL,
number VARCHAR(32) NOT NULL UNIQUE ,
voip_carrier_sid CHAR(36),
account_sid CHAR(36),
application_sid CHAR(36),
@@ -461,15 +378,11 @@ CREATE TABLE sip_gateways
sip_gateway_sid CHAR(36),
ipv4 VARCHAR(128) NOT NULL COMMENT 'ip address or DNS name of the gateway. For gateways providing inbound calling service, ip address is required.',
netmask INTEGER NOT NULL DEFAULT 32,
port INTEGER COMMENT 'sip signaling port',
port INTEGER NOT NULL DEFAULT 5060 COMMENT 'sip signaling port',
inbound BOOLEAN NOT NULL COMMENT 'if true, whitelist this IP to allow inbound calls from the gateway',
outbound BOOLEAN NOT NULL COMMENT 'if true, include in least-cost routing when placing calls to the PSTN',
voip_carrier_sid CHAR(36) NOT NULL,
is_active BOOLEAN NOT NULL DEFAULT 1,
send_options_ping BOOLEAN NOT NULL DEFAULT 0,
use_sips_scheme BOOLEAN NOT NULL DEFAULT 0,
pad_crypto BOOLEAN NOT NULL DEFAULT 0,
protocol ENUM('udp','tcp','tls', 'tls/srtp') DEFAULT 'udp' COMMENT 'Outbound call protocol',
PRIMARY KEY (sip_gateway_sid)
) COMMENT='A whitelisted sip gateway used for origination/termination';
@@ -502,25 +415,12 @@ account_sid CHAR(36) COMMENT 'account that this application belongs to (if null,
call_hook_sid CHAR(36) COMMENT 'webhook to call for inbound calls ',
call_status_hook_sid CHAR(36) COMMENT 'webhook to call for call status events',
messaging_hook_sid CHAR(36) COMMENT 'webhook to call for inbound SMS/MMS ',
app_json TEXT,
speech_synthesis_vendor VARCHAR(64) NOT NULL DEFAULT 'google',
speech_synthesis_language VARCHAR(12) NOT NULL DEFAULT 'en-US',
speech_synthesis_voice VARCHAR(256) DEFAULT 'en-US-Standard-C',
speech_synthesis_label VARCHAR(64),
speech_synthesis_voice VARCHAR(64),
speech_recognizer_vendor VARCHAR(64) NOT NULL DEFAULT 'google',
speech_recognizer_language VARCHAR(64) NOT NULL DEFAULT 'en-US',
speech_recognizer_label VARCHAR(64),
use_for_fallback_speech BOOLEAN DEFAULT false,
fallback_speech_synthesis_vendor VARCHAR(64),
fallback_speech_synthesis_language VARCHAR(12),
fallback_speech_synthesis_voice VARCHAR(256),
fallback_speech_synthesis_label VARCHAR(64),
fallback_speech_recognizer_vendor VARCHAR(64),
fallback_speech_recognizer_language VARCHAR(64),
fallback_speech_recognizer_label VARCHAR(64),
env_vars TEXT,
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 ';
@@ -558,10 +458,6 @@ 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',
enable_debug_log BOOLEAN NOT NULL DEFAULT false,
PRIMARY KEY (account_sid)
) COMMENT='An enterprise that uses the platform for comm services';
@@ -582,21 +478,9 @@ 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);
CREATE INDEX predefined_carrier_sid_idx ON predefined_sip_gateways (predefined_carrier_sid);
@@ -649,6 +533,8 @@ CREATE INDEX smpp_address_sid_idx ON smpp_addresses (smpp_address_sid);
CREATE INDEX service_provider_sid_idx ON smpp_addresses (service_provider_sid);
ALTER TABLE smpp_addresses ADD FOREIGN KEY service_provider_sid_idxfk_4 (service_provider_sid) REFERENCES service_providers (service_provider_sid);
CREATE UNIQUE INDEX speech_credentials_idx_1 ON speech_credentials (vendor,account_sid);
CREATE INDEX speech_credential_sid_idx ON speech_credentials (speech_credential_sid);
CREATE INDEX service_provider_sid_idx ON speech_credentials (service_provider_sid);
ALTER TABLE speech_credentials ADD FOREIGN KEY service_provider_sid_idxfk_5 (service_provider_sid) REFERENCES service_providers (service_provider_sid);
@@ -656,10 +542,6 @@ ALTER TABLE speech_credentials ADD FOREIGN KEY service_provider_sid_idxfk_5 (ser
CREATE INDEX account_sid_idx ON speech_credentials (account_sid);
ALTER TABLE speech_credentials ADD FOREIGN KEY account_sid_idxfk_8 (account_sid) REFERENCES accounts (account_sid);
CREATE INDEX google_custom_voice_sid_idx ON google_custom_voices (google_custom_voice_sid);
CREATE INDEX speech_credential_sid_idx ON google_custom_voices (speech_credential_sid);
ALTER TABLE google_custom_voices ADD FOREIGN KEY speech_credential_sid_idxfk (speech_credential_sid) REFERENCES speech_credentials (speech_credential_sid) ON DELETE CASCADE;
CREATE INDEX user_sid_idx ON users (user_sid);
CREATE INDEX email_idx ON users (email);
CREATE INDEX phone_idx ON users (phone);
@@ -679,18 +561,10 @@ ALTER TABLE voip_carriers ADD FOREIGN KEY service_provider_sid_idxfk_7 (service_
ALTER TABLE voip_carriers ADD FOREIGN KEY application_sid_idxfk_2 (application_sid) REFERENCES applications (application_sid);
CREATE INDEX user_permissions_sid_idx ON user_permissions (user_permissions_sid);
CREATE INDEX user_sid_idx ON user_permissions (user_sid);
ALTER TABLE user_permissions ADD FOREIGN KEY user_sid_idxfk (user_sid) REFERENCES users (user_sid) ON DELETE CASCADE;
ALTER TABLE user_permissions ADD FOREIGN KEY permission_sid_idxfk (permission_sid) REFERENCES permissions (permission_sid);
CREATE INDEX smpp_gateway_sid_idx ON smpp_gateways (smpp_gateway_sid);
CREATE INDEX voip_carrier_sid_idx ON smpp_gateways (voip_carrier_sid);
ALTER TABLE smpp_gateways ADD FOREIGN KEY voip_carrier_sid_idxfk (voip_carrier_sid) REFERENCES voip_carriers (voip_carrier_sid);
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);
@@ -705,12 +579,6 @@ ALTER TABLE phone_numbers ADD FOREIGN KEY service_provider_sid_idxfk_8 (service_
CREATE INDEX sip_gateway_idx_hostport ON sip_gateways (ipv4,port);
CREATE INDEX idx_sip_gateways_inbound_carrier ON sip_gateways (inbound,voip_carrier_sid);
CREATE INDEX idx_sip_gateways_inbound_lookup ON sip_gateways (inbound,netmask,ipv4);
CREATE INDEX idx_sip_gateways_inbound_netmask ON sip_gateways (inbound,netmask);
CREATE INDEX voip_carrier_sid_idx ON sip_gateways (voip_carrier_sid);
ALTER TABLE sip_gateways ADD FOREIGN KEY voip_carrier_sid_idxfk_2 (voip_carrier_sid) REFERENCES voip_carriers (voip_carrier_sid);

File diff suppressed because one or more lines are too long

View File

@@ -1,11 +0,0 @@
/* remove VIEW_ONLY permission for admin user as it will prevent write operations*/
delete from user_permissions;
delete from permissions;
insert into permissions (permission_sid, name, description)
values
('ffbc342a-546a-11ed-bdc3-0242ac120002', 'VIEW_ONLY', 'Can view data but not make changes'),
('ffbc3a10-546a-11ed-bdc3-0242ac120002', 'PROVISION_SERVICES', 'Can provision services'),
('ffbc3c5e-546a-11ed-bdc3-0242ac120002', 'PROVISION_USERS', 'Can provision users');

View File

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

View File

@@ -1,12 +1,5 @@
SET FOREIGN_KEY_CHECKS=0;
-- create standard permissions
insert into permissions (permission_sid, name, description)
values
('ffbc342a-546a-11ed-bdc3-0242ac120002', 'VIEW_ONLY', 'Can view data but not make changes'),
('ffbc3a10-546a-11ed-bdc3-0242ac120002', 'PROVISION_SERVICES', 'Can provision services'),
('ffbc3c5e-546a-11ed-bdc3-0242ac120002', 'PROVISION_USERS', 'Can provision users');
insert into sbc_addresses (sbc_address_sid, ipv4, port)
values('f6567ae1-bf97-49af-8931-ca014b689995', '52.55.111.178', 5060);
insert into sbc_addresses (sbc_address_sid, ipv4, port)
@@ -22,25 +15,17 @@ values ('3f35518f-5a0d-4c2e-90a5-2407bb3b36f0', '38700987-c7a4-4685-a5bb-af378f9
-- create one service provider and one account
insert into service_providers (service_provider_sid, name, root_domain)
values ('2708b1b3-2736-40ea-b502-c53d8396247f', 'default service provider', 'sip.jambonz.cloud');
values ('2708b1b3-2736-40ea-b502-c53d8396247f', 'default service provider', 'sip.jambonz.us');
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
('84e3db00-b172-4e46-b54b-a503fdb19e4a', 'https://public-apps.jambonz.cloud/call-status', 'POST'),
('d31568d0-b193-4a05-8ff6-778369bc6efe', 'https://public-apps.jambonz.cloud/hello-world', 'POST'),
('81844b05-714d-4295-8bf3-3b0640a4bf02', 'https://public-apps.jambonz.cloud/dial-time', 'POST');
('84e3db00-b172-4e46-b54b-a503fdb19e4a', 'https://public-apps.jambonz.us/call-status', 'POST'),
('d31568d0-b193-4a05-8ff6-778369bc6efe', 'https://public-apps.jambonz.us/hello-world', 'POST'),
('81844b05-714d-4295-8bf3-3b0640a4bf02', 'https://public-apps.jambonz.us/dial-time', 'POST');
insert into applications (application_sid, account_sid, name, call_hook_sid, call_status_hook_sid, speech_synthesis_vendor, speech_synthesis_language, speech_synthesis_voice, speech_recognizer_vendor, speech_recognizer_language)
VALUES
@@ -87,7 +72,6 @@ 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
@@ -107,8 +91,10 @@ VALUES
('91cb050f-9826-4ac9-b736-84a10372a9fe', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '178.22.139.77', 32, 5060, 1, 0),
('58700fad-98bf-4d31-b61e-888c54911b35', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '185.63.140.77', 32, 5060, 1, 0),
('d020fd9e-7fdb-4bca-ae0d-e61b38142873', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '185.63.142.77', 32, 5060, 1, 0),
('38d8520a-527f-4f8e-8456-f9dfca742561', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '172.86.225.77', 32, 5060, 1, 0),
('834f8b0c-d4c2-4f3e-93d9-cf307995eedd', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '172.86.225.88', 32, 5060, 1, 0),
('441fd2e7-c845-459c-963d-6e917063ed9a', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '185.63.141.77', 32, 5060, 1, 0),
('1e0d3e80-9973-4184-9bec-07ae564f983f', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '185.63.143.77', 32, 5060, 1, 0),
('e56ec745-5f37-443f-afb4-7bbda31ae7ac', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '178.22.140.34', 32, 5060, 1, 0),
('e7447e7e-2c7d-4738-ab53-097c187236ff', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '178.22.143.66', 32, 5060, 1, 0),
('5f431d42-48e4-44ce-a311-d946f0b475b6', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', 'out.simwood.com', 32, 5060, 0, 1);

View File

@@ -1,12 +1,5 @@
SET FOREIGN_KEY_CHECKS=0;
-- create standard permissions
insert into permissions (permission_sid, name, description)
values
('ffbc342a-546a-11ed-bdc3-0242ac120002', 'VIEW_ONLY', 'Can view data but not make changes'),
('ffbc3a10-546a-11ed-bdc3-0242ac120002', 'PROVISION_SERVICES', 'Can provision services'),
('ffbc3c5e-546a-11ed-bdc3-0242ac120002', 'PROVISION_USERS', 'Can provision users');
-- create one service provider and account
insert into api_keys (api_key_sid, token)
values ('3f35518f-5a0d-4c2e-90a5-2407bb3b36f0', '38700987-c7a4-4685-a5bb-af378f9734de');
@@ -24,9 +17,10 @@ values ('09e92f3c-9d73-4303-b63f-3668574862ce', '1cf2f4f4-64c4-4249-9a3e-5bb4cb5
-- create two applications
insert into webhooks(webhook_sid, url, method)
values
('84e3db00-b172-4e46-b54b-a503fdb19e4a', 'https://public-apps.jambonz.cloud/call-status', 'POST'),
('d31568d0-b193-4a05-8ff6-778369bc6efe', 'https://public-apps.jambonz.cloud/hello-world', 'POST'),
('81844b05-714d-4295-8bf3-3b0640a4bf02', 'https://public-apps.jambonz.cloud/dial-time', 'POST');
('84e3db00-b172-4e46-b54b-a503fdb19e4a', 'https://public-apps.jambonz.us/call-status', 'POST'),
('d31568d0-b193-4a05-8ff6-778369bc6efe', 'https://public-apps.jambonz.us/hello-world', 'POST'),
('81844b05-714d-4295-8bf3-3b0640a4bf02', 'https://public-apps.jambonz.us/dial-time', 'POST');
insert into applications (application_sid, account_sid, name, call_hook_sid, call_status_hook_sid, speech_synthesis_vendor, speech_synthesis_language, speech_synthesis_voice, speech_recognizer_vendor, speech_recognizer_language)
VALUES
('7087fe50-8acb-4f3b-b820-97b573723aab', '9351f46a-678c-43f5-b8a6-d4eb58d131af', 'hello world', 'd31568d0-b193-4a05-8ff6-778369bc6efe', '84e3db00-b172-4e46-b54b-a503fdb19e4a', 'google', 'en-US', 'en-US-Wavenet-C', 'google', 'en-US'),
@@ -72,7 +66,6 @@ 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
@@ -92,8 +85,10 @@ VALUES
('91cb050f-9826-4ac9-b736-84a10372a9fe', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '178.22.139.77', 32, 5060, 1, 0),
('58700fad-98bf-4d31-b61e-888c54911b35', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '185.63.140.77', 32, 5060, 1, 0),
('d020fd9e-7fdb-4bca-ae0d-e61b38142873', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '185.63.142.77', 32, 5060, 1, 0),
('38d8520a-527f-4f8e-8456-f9dfca742561', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '172.86.225.77', 32, 5060, 1, 0),
('834f8b0c-d4c2-4f3e-93d9-cf307995eedd', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '172.86.225.88', 32, 5060, 1, 0),
('441fd2e7-c845-459c-963d-6e917063ed9a', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '185.63.141.77', 32, 5060, 1, 0),
('1e0d3e80-9973-4184-9bec-07ae564f983f', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '185.63.143.77', 32, 5060, 1, 0),
('e56ec745-5f37-443f-afb4-7bbda31ae7ac', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '178.22.140.34', 32, 5060, 1, 0),
('e7447e7e-2c7d-4738-ab53-097c187236ff', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '178.22.143.66', 32, 5060, 1, 0),
('5f431d42-48e4-44ce-a311-d946f0b475b6', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', 'out.simwood.com', 32, 5060, 0, 1);
SET FOREIGN_KEY_CHECKS=1;

View File

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

View File

@@ -6,7 +6,6 @@ const {readFile} = require('fs/promises');
const {execSync} = require('child_process');
const {version:desiredVersion} = require('../package.json');
const logger = require('pino')();
const fs = require('fs');
logger.info(`upgrade-jambonz-db: desired version ${desiredVersion}`);
@@ -23,20 +22,6 @@ const opts = {
port: process.env.JAMBONES_MYSQL_PORT || 3306,
multipleStatements: true
};
const rejectUnauthorized = process.env.JAMBONES_MYSQL_REJECT_UNAUTHORIZED;
const ssl_ca_file = process.env.JAMBONES_MYSQL_SSL_CA_FILE;
const ssl_cert_file = process.env.JAMBONES_MYSQL_SSL_CERT_FILE;
const ssl_key_file = process.env.JAMBONES_MYSQL_SSL_KEY_FILE;
const sslFilesProvided = Boolean(ssl_ca_file && ssl_cert_file && ssl_key_file);
if (rejectUnauthorized !== undefined || sslFilesProvided) {
opts.ssl = {
...(rejectUnauthorized !== undefined && { rejectUnauthorized: rejectUnauthorized === '0' ? false : true }),
...(ssl_ca_file && { ca: fs.readFileSync(ssl_ca_file) }),
...(ssl_cert_file && { cert: fs.readFileSync(ssl_cert_file) }),
...(ssl_key_file && { key: fs.readFileSync(ssl_key_file) })
};
}
const sql = {
'7006': [
@@ -72,173 +57,9 @@ const sql = {
'ALTER TABLE `voip_carriers` ADD COLUMN `register_from_user` VARCHAR(128)',
'ALTER TABLE `voip_carriers` ADD COLUMN `register_from_domain` VARCHAR(256)',
'ALTER TABLE `voip_carriers` ADD COLUMN `register_public_ip_in_contact` BOOLEAN NOT NULL DEFAULT false'
],
'8000': [
'ALTER TABLE `applications` ADD COLUMN `app_json` TEXT',
'ALTER TABLE voip_carriers CHANGE register_public_domain_in_contact register_public_ip_in_contact BOOLEAN',
'alter table phone_numbers modify number varchar(132) NOT NULL UNIQUE',
`CREATE TABLE permissions
(
permission_sid CHAR(36) NOT NULL UNIQUE ,
name VARCHAR(32) NOT NULL UNIQUE ,
description VARCHAR(255),
PRIMARY KEY (permission_sid)
)`,
`CREATE TABLE user_permissions
(
user_permissions_sid CHAR(36) NOT NULL UNIQUE ,
user_sid CHAR(36) NOT NULL,
permission_sid CHAR(36) NOT NULL,
PRIMARY KEY (user_permissions_sid)
)`,
`CREATE TABLE password_settings
(
min_password_length INTEGER NOT NULL DEFAULT 8,
require_digit BOOLEAN NOT NULL DEFAULT false,
require_special_character BOOLEAN NOT NULL DEFAULT false
)`,
'CREATE INDEX user_permissions_sid_idx ON user_permissions (user_permissions_sid)',
'CREATE INDEX user_sid_idx ON user_permissions (user_sid)',
'ALTER TABLE user_permissions ADD FOREIGN KEY user_sid_idxfk (user_sid) REFERENCES users (user_sid) ON DELETE CASCADE',
'ALTER TABLE user_permissions ADD FOREIGN KEY permission_sid_idxfk (permission_sid) REFERENCES permissions (permission_sid)',
'ALTER TABLE `users` ADD COLUMN `is_active` BOOLEAN NOT NULL default true',
],
'8003': [
'SET FOREIGN_KEY_CHECKS=0',
'ALTER TABLE `voip_carriers` ADD COLUMN `register_status` VARCHAR(4096)',
'ALTER TABLE `sbc_addresses` ADD COLUMN `last_updated` DATETIME',
'ALTER TABLE `sbc_addresses` ADD COLUMN `tls_port` INTEGER',
'ALTER TABLE `sbc_addresses` ADD COLUMN `wss_port` INTEGER',
`CREATE TABLE system_information
(
domain_name VARCHAR(255),
sip_domain_name VARCHAR(255),
monitoring_domain_name VARCHAR(255)
)`,
'DROP TABLE IF EXISTS `lcr_routes`',
'DROP TABLE IF EXISTS `lcr_carrier_set_entry`',
`CREATE TABLE lcr_routes
(
lcr_route_sid CHAR(36),
lcr_sid CHAR(36) NOT NULL,
regex VARCHAR(32) NOT NULL COMMENT 'regex-based pattern match against dialed number, used for LCR routing of PSTN calls',
description VARCHAR(1024),
priority INTEGER NOT NULL COMMENT 'lower priority routes are attempted first',
PRIMARY KEY (lcr_route_sid)
)`,
`CREATE TABLE lcr
(
lcr_sid CHAR(36) NOT NULL UNIQUE ,
name VARCHAR(64) COMMENT 'User-assigned name for this LCR table',
is_active BOOLEAN NOT NULL DEFAULT 1,
default_carrier_set_entry_sid CHAR(36) COMMENT 'default carrier/route to use when no digit match based results are found.',
service_provider_sid CHAR(36),
account_sid CHAR(36),
PRIMARY KEY (lcr_sid)
)`,
`CREATE TABLE lcr_carrier_set_entry
(
lcr_carrier_set_entry_sid CHAR(36),
workload INTEGER NOT NULL DEFAULT 1 COMMENT 'represents a proportion of traffic to send through the associated carrier; can be used for load balancing traffic across carriers with a common priority for a destination',
lcr_route_sid CHAR(36) NOT NULL,
voip_carrier_sid CHAR(36) NOT NULL,
priority INTEGER NOT NULL DEFAULT 0 COMMENT 'lower priority carriers are attempted first',
PRIMARY KEY (lcr_carrier_set_entry_sid)
)`,
'CREATE INDEX lcr_sid_idx ON lcr_routes (lcr_sid)',
'ALTER TABLE lcr_routes ADD FOREIGN KEY lcr_sid_idxfk (lcr_sid) REFERENCES lcr (lcr_sid)',
'CREATE INDEX lcr_sid_idx ON lcr (lcr_sid)',
'ALTER TABLE lcr ADD FOREIGN KEY default_carrier_set_entry_sid_idxfk (default_carrier_set_entry_sid) REFERENCES lcr_carrier_set_entry (lcr_carrier_set_entry_sid)',
'CREATE INDEX service_provider_sid_idx ON lcr (service_provider_sid)',
'CREATE INDEX account_sid_idx ON lcr (account_sid)',
'ALTER TABLE lcr_carrier_set_entry ADD FOREIGN KEY lcr_route_sid_idxfk (lcr_route_sid) REFERENCES lcr_routes (lcr_route_sid)',
'ALTER TABLE lcr_carrier_set_entry ADD FOREIGN KEY voip_carrier_sid_idxfk_3 (voip_carrier_sid) REFERENCES voip_carriers (voip_carrier_sid)',
'SET FOREIGN_KEY_CHECKS=1',
],
'8004': [
'alter table accounts add column record_all_calls BOOLEAN NOT NULL DEFAULT false',
'alter table accounts add column bucket_credential VARCHAR(8192)',
'alter table accounts add column record_format VARCHAR(16) NOT NULL DEFAULT \'mp3\'',
'alter table applications add column record_all_calls BOOLEAN NOT NULL DEFAULT false',
'alter table phone_numbers DROP INDEX number',
'create unique index phone_numbers_unique_idx_voip_carrier_number ON phone_numbers (number,voip_carrier_sid)',
`CREATE TABLE clients
(
client_sid CHAR(36) NOT NULL UNIQUE ,
account_sid CHAR(36) NOT NULL,
is_active BOOLEAN NOT NULL DEFAULT 1,
username VARCHAR(64),
password VARCHAR(1024),
PRIMARY KEY (client_sid)
)`,
'CREATE INDEX client_sid_idx ON clients (client_sid)',
'ALTER TABLE clients ADD CONSTRAINT account_sid_idxfk_13 FOREIGN KEY account_sid_idxfk_13 (account_sid) REFERENCES accounts (account_sid)',
'ALTER TABLE sip_gateways ADD COLUMN protocol ENUM(\'udp\',\'tcp\',\'tls\', \'tls/srtp\') DEFAULT \'udp\''
],
'8005': [
'DROP INDEX speech_credentials_idx_1 ON speech_credentials',
'ALTER TABLE speech_credentials ADD COLUMN label VARCHAR(64)',
'ALTER TABLE applications ADD COLUMN speech_synthesis_label VARCHAR(64)',
'ALTER TABLE applications ADD COLUMN speech_recognizer_label VARCHAR(64)',
'ALTER TABLE applications ADD COLUMN use_for_fallback_speech BOOLEAN DEFAULT false',
'ALTER TABLE applications ADD COLUMN fallback_speech_synthesis_vendor VARCHAR(64)',
'ALTER TABLE applications ADD COLUMN fallback_speech_synthesis_language VARCHAR(12)',
'ALTER TABLE applications ADD COLUMN fallback_speech_synthesis_voice VARCHAR(64)',
'ALTER TABLE applications ADD COLUMN fallback_speech_synthesis_label VARCHAR(64)',
'ALTER TABLE applications ADD COLUMN fallback_speech_recognizer_vendor VARCHAR(64)',
'ALTER TABLE applications ADD COLUMN fallback_speech_recognizer_language VARCHAR(64)',
'ALTER TABLE applications ADD COLUMN fallback_speech_recognizer_label VARCHAR(64)',
'ALTER TABLE sip_gateways ADD COLUMN pad_crypto BOOLEAN NOT NULL DEFAULT 0',
'ALTER TABLE sip_gateways MODIFY port INTEGER',
`CREATE TABLE google_custom_voices
(
google_custom_voice_sid CHAR(36) NOT NULL UNIQUE ,
speech_credential_sid CHAR(36) NOT NULL,
model VARCHAR(512) NOT NULL,
reported_usage ENUM('REPORTED_USAGE_UNSPECIFIED','REALTIME','OFFLINE') DEFAULT 'REALTIME',
name VARCHAR(64) NOT NULL,
PRIMARY KEY (google_custom_voice_sid)
)
`,
'CREATE INDEX google_custom_voice_sid_idx ON google_custom_voices (google_custom_voice_sid)',
'CREATE INDEX speech_credential_sid_idx ON google_custom_voices (speech_credential_sid)',
'ALTER TABLE google_custom_voices ADD FOREIGN KEY speech_credential_sid_idxfk (speech_credential_sid) REFERENCES speech_credentials (speech_credential_sid) ON DELETE CASCADE',
'ALTER TABLE clients ADD COLUMN allow_direct_queue_calling BOOLEAN NOT NULL DEFAULT 1',
'ALTER TABLE clients ADD COLUMN allow_direct_user_calling BOOLEAN NOT NULL DEFAULT 1',
'ALTER TABLE clients ADD COLUMN allow_direct_app_calling BOOLEAN NOT NULL DEFAULT 1',
],
9000: [
'ALTER TABLE sip_gateways ADD COLUMN send_options_ping BOOLEAN NOT NULL DEFAULT 0',
'ALTER TABLE applications MODIFY COLUMN speech_synthesis_voice VARCHAR(256)',
'ALTER TABLE applications MODIFY COLUMN fallback_speech_synthesis_voice VARCHAR(256)',
'ALTER TABLE sip_gateways ADD COLUMN use_sips_scheme BOOLEAN NOT NULL DEFAULT 0',
],
9002: [
'ALTER TABLE system_information ADD COLUMN private_network_cidr VARCHAR(8192)',
'ALTER TABLE system_information ADD COLUMN log_level ENUM(\'info\', \'debug\') NOT NULL DEFAULT \'info\'',
'ALTER TABLE accounts ADD COLUMN enable_debug_log BOOLEAN NOT NULL DEFAULT false',
'ALTER TABLE google_custom_voices ADD COLUMN use_voice_cloning_key BOOLEAN DEFAULT false',
'ALTER TABLE google_custom_voices ADD COLUMN voice_cloning_key MEDIUMTEXT',
],
9003: [
'ALTER TABLE google_custom_voices ADD COLUMN voice_cloning_key MEDIUMTEXT',
'ALTER TABLE google_custom_voices ADD COLUMN use_voice_cloning_key BOOLEAN DEFAULT false',
'ALTER TABLE voip_carriers ADD COLUMN dtmf_type ENUM(\'rfc2833\',\'tones\',\'info\') NOT NULL DEFAULT \'rfc2833\'',
'ALTER TABLE voip_carriers ADD COLUMN outbound_sip_proxy VARCHAR(255)',
],
9004: [
'ALTER TABLE applications ADD COLUMN env_vars TEXT',
],
9005: [
'UPDATE applications SET speech_synthesis_voice = \'en-US-Standard-C\' WHERE speech_synthesis_voice IS NULL AND speech_synthesis_vendor = \'google\' AND speech_synthesis_language = \'en-US\'',
'ALTER TABLE applications MODIFY COLUMN speech_synthesis_voice VARCHAR(255) DEFAULT \'en-US-Standard-C\'',
'ALTER TABLE voip_carriers ADD COLUMN trunk_type ENUM(\'static_ip\',\'auth\',\'reg\') NOT NULL DEFAULT \'static_ip\'',
'ALTER TABLE predefined_carriers ADD COLUMN trunk_type ENUM(\'static_ip\',\'auth\',\'reg\') NOT NULL DEFAULT \'static_ip\'',
'CREATE INDEX idx_sip_gateways_inbound_carrier ON sip_gateways (inbound,voip_carrier_sid)',
'CREATE INDEX idx_sip_gateways_inbound_lookup ON sip_gateways (inbound,netmask,ipv4)',
'CREATE INDEX idx_sip_gateways_inbound_netmask ON sip_gateways (inbound,netmask)'
],
]
};
const doIt = async() => {
let connection;
try {
@@ -264,15 +85,6 @@ const doIt = async() => {
if (val < 7006) upgrades.push(...sql['7006']);
if (val < 7007) upgrades.push(...sql['7007']);
if (val < 8000) upgrades.push(...sql['8000']);
if (val < 8003) upgrades.push(...sql['8003']);
if (val < 8004) upgrades.push(...sql['8004']);
if (val < 8005) upgrades.push(...sql['8005']);
if (val < 9000) upgrades.push(...sql['9000']);
if (val < 9002) upgrades.push(...sql['9002']);
if (val < 9003) upgrades.push(...sql['9003']);
if (val < 9004) upgrades.push(...sql['9004']);
if (val < 9005) upgrades.push(...sql['9005']);
// perform all upgrades
logger.info({upgrades}, 'applying schema upgrades..');

View File

@@ -2,7 +2,7 @@ SET FOREIGN_KEY_CHECKS=0;
-- create one service provider
insert into service_providers (service_provider_sid, name, description, root_domain)
values ('2708b1b3-2736-40ea-b502-c53d8396247f', 'jambonz.cloud', 'jambonz.cloud service provider', 'sip.yakeeda.com');
values ('2708b1b3-2736-40ea-b502-c53d8396247f', 'jambonz.us', 'jambonz.us service provider', 'sip.yakeeda.com');
insert into api_keys (api_key_sid, token)
values ('3f35518f-5a0d-4c2e-90a5-2407bb3b36f0', '38700987-c7a4-4685-a5bb-af378f9734de');
@@ -19,8 +19,8 @@ insert into sip_gateways (sip_gateway_sid, voip_carrier_sid, ipv4, port, inbound
values ('46b727eb-c7dc-44fa-b063-96e48d408e4a', '5145b436-2f38-4029-8d4c-fd8c67831c7a', '3.3.3.3', 5060, 1, 1, 1);
-- create the test application and test phone number
insert into webhooks (webhook_sid, url, method) values ('d9c205c6-a129-443e-a9c0-d1bb437d4bb7', 'https://flows.jambonz.cloud/testCall', 'POST');
insert into webhooks (webhook_sid, url, method) values ('6ac36aeb-6bd0-428a-80a1-aed95640a296', 'https://flows.jambonz.cloud/callStatus', 'POST');
insert into webhooks (webhook_sid, url, method) values ('d9c205c6-a129-443e-a9c0-d1bb437d4bb7', 'https://flows.jambonz.us/testCall', 'POST');
insert into webhooks (webhook_sid, url, method) values ('6ac36aeb-6bd0-428a-80a1-aed95640a296', 'https://flows.jambonz.us/callStatus', 'POST');
insert into applications (application_sid, name, service_provider_sid, call_hook_sid, call_status_hook_sid,
speech_synthesis_vendor, speech_synthesis_language, speech_synthesis_voice, speech_recognizer_vendor, speech_recognizer_language)
values ('7a489343-02ed-471e-8df0-fc5e1b98ce8f', 'Test application', '2708b1b3-2736-40ea-b502-c53d8396247f',
@@ -85,6 +85,9 @@ VALUES
-- simwood gateways
insert into predefined_sip_gateways (predefined_sip_gateway_sid, predefined_carrier_sid, ipv4, netmask, port, inbound, outbound)
VALUES
('91cb050f-9826-4ac9-b736-84a10372a9fe', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '149.91.14.0', 24, 5060, 1, 0),
('58700fad-98bf-4d31-b61e-888c54911b35', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '154.51.137.96', 27, 5060, 1, 0),
('d020fd9e-7fdb-4bca-ae0d-e61b38142873', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '78.40.245.160', 27, 5060, 1, 0),
('441fd2e7-c845-459c-963d-6e917063ed9a', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '178.22.136.24', 29, 5060, 1, 0),
('1e0d3e80-9973-4184-9bec-07ae564f983f', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '178.22.136.28', 28, 5060, 1, 0),
('e56ec745-5f37-443f-afb4-7bbda31ae7ac', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '178.22.140.48', 28, 5060, 1, 0),
@@ -95,7 +98,7 @@ VALUES
('b6ae6240-55ac-4c11-892f-a71b2155ea60', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '185.63.142.0', 26, 5060, 1, 0),
('5a976337-164b-408e-8748-d8bfb4bd5d76', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '185.63.143.0', 26, 5060, 1, 0),
('ed0434ca-7f26-4624-9523-0419d0d2924d', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '178.22.139.0', 26, 5060, 1, 0),
('6bfb55e5-e248-48dc-a104-4f3eedd7d7de', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '172.86.225.0', 24, 5060, 1, 0),
('d1a594c2-c14f-4ead-b621-96129bc87886', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '172.86.224.0', 24, 5060, 1, 0),
('5f431d42-48e4-44ce-a311-d946f0b475b6', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', 'out.simwood.com', 32, 5060, 0, 1);

View File

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

View File

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

View File

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

View File

@@ -1,35 +0,0 @@
const logger = require('../logger');
const {
client,
retrieveCall,
deleteCall,
listCalls,
listSortedSets,
purgeCalls,
retrieveSet,
addKey,
retrieveKey,
deleteKey,
incrKey,
client: redisClient,
listConferences,
getCallCount
} = require('@jambonz/realtimedb-helpers')({}, logger);
module.exports = {
client,
retrieveCall,
deleteCall,
listCalls,
listSortedSets,
purgeCalls,
retrieveSet,
addKey,
retrieveKey,
deleteKey,
redisClient,
incrKey,
listConferences,
getCallCount
};

View File

@@ -1,7 +0,0 @@
const opts = {
level: process.env.JAMBONES_LOGLEVEL || 'info'
};
const pino = require('pino');
const logger = pino(opts);
module.exports = logger;

View File

@@ -1,53 +0,0 @@
const logger = require('./logger');
const {UserPermissionError} = require('./utils/errors');
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();
}
function verifyViewOnlyUser(req, res, next) {
// Skip check for GET requests
if (req.method === 'GET') {
return next();
}
// current user is changing their password which shuould be allowed
if (req.body?.old_password && req.body?.new_password) {
return next();
}
// Check if user is read-only
if (req.user && !!req.user.is_view_only) {
const upError = new UserPermissionError('User has view-only access');
upError.status = 403;
throw upError;
}
next();
}
module.exports = {
delayLoginMiddleware,
verifyViewOnlyUser
};

View File

@@ -4,7 +4,7 @@ const {getMysqlConnection} = require('../db');
const {promisePool} = require('../db');
const { v4: uuid } = require('uuid');
const {encrypt, decrypt} = require('../utils/encrypt-decrypt');
const {encrypt} = require('../utils/encrypt-decrypt');
const retrieveSql = `SELECT * from accounts acc
LEFT JOIN webhooks AS rh
@@ -34,7 +34,7 @@ AND effective_end_date IS NULL
AND pending=0`;
const updatePaymentInfoSql = `UPDATE account_subscriptions
SET last4 = ?, stripe_payment_method_id=?, exp_month = ?, exp_year = ?, card_type = ?
SET last4 = ?, exp_month = ?, exp_year = ?, card_type = ?
WHERE account_sid = ?
AND effective_end_date IS NULL`;
@@ -55,17 +55,6 @@ WHERE account_sid = ?
AND effective_end_date IS NULL
AND pending = 0`;
const extractBucketCredential = (obj) => {
try {
const {bucket_credential} = obj;
if (bucket_credential) {
obj.bucket_credential = JSON.parse(decrypt(bucket_credential));
}
} catch (error) {
console.error('Error while decrypting data', error);
}
};
function transmogrifyResults(results) {
return results.map((row) => {
const obj = row.acc;
@@ -86,8 +75,6 @@ function transmogrifyResults(results) {
else obj.queue_event_hook = null;
delete obj.queue_event_hook_sid;
extractBucketCredential(obj);
return obj;
});
}
@@ -203,18 +190,17 @@ class Account extends Model {
debug(r3, 'Account.activateSubscription - replaced old subscription');
/* update account.plan to paid, if it isnt already */
/* update account.is_active to 1, if account is deactivated */
await promisePool.execute(
'UPDATE accounts SET plan_type = \'paid\', is_active = 1 WHERE account_sid = ?',
'UPDATE accounts SET plan_type = \'paid\' WHERE account_sid = ?',
[account_sid]);
return true;
}
static async updatePaymentInfo(logger, account_sid, pm) {
const {id, card} = pm;
const {card} = pm;
const last4_encrypted = encrypt(card.last4);
await promisePool.execute(updatePaymentInfoSql,
[last4_encrypted, id, card.exp_month, card.exp_year, card.brand, account_sid]);
[last4_encrypted, card.exp_month, card.exp_year, card.brand, account_sid]);
}
static async provisionPendingSubscription(logger, account_sid, products, payment_method, subscription_id) {
@@ -252,6 +238,7 @@ class Account extends Model {
}));
return account_subscription_sid;
}
}
Account.table = 'accounts';
@@ -331,18 +318,6 @@ Account.fields = [
name: 'siprec_hook_sid',
type: 'string',
},
{
name: 'record_all_calls',
type: 'number'
},
{
name: 'record_format',
type: 'string'
},
{
name: 'bucket_credential',
type: 'string'
}
];
module.exports = Account;

View File

@@ -47,14 +47,10 @@ class ApiKey extends Model {
}
/**
* update last_used api key for an account
* (only if last_used is null or more than a minute ago)
*/
* update last_used api key for an account
*/
static updateLastUsed(account_sid) {
const sql = `UPDATE api_keys
SET last_used = NOW()
WHERE account_sid = ?
AND (last_used IS NULL OR last_used < NOW() - INTERVAL 1 MINUTE)`;
const sql = 'UPDATE api_keys SET last_used = NOW() WHERE account_sid = ?';
const args = [account_sid];
return new Promise((resolve, reject) => {

View File

@@ -36,118 +36,20 @@ class Application extends Model {
super();
}
static _criteriaBuilder(obj, args) {
let sql = '';
if (obj.account_sid) {
sql += ' AND app.account_sid = ?';
args.push(obj.account_sid);
}
if (obj.service_provider_sid) {
sql += ' AND app.account_sid in (SELECT account_sid from accounts WHERE service_provider_sid = ?)';
args.push(obj.service_provider_sid);
}
if (obj.name) {
sql += ' AND app.name LIKE ?';
args.push(`%${obj.name}%`);
}
return sql;
}
static countAll(obj) {
const args = [];
const criteriaClause = Application._criteriaBuilder(obj, args);
// Only use "WHERE 1 = 1" if there are no filters
// Otherwise start with the actual filter for better index usage
let sql;
if (criteriaClause) {
// Remove leading ' AND ' from criteriaBuilder output and use as WHERE clause
sql = 'SELECT COUNT(*) AS count FROM applications app WHERE ' + criteriaClause.substring(5);
} else {
// No filters provided - count all applications
sql = 'SELECT COUNT(*) AS count FROM applications app WHERE 1 = 1';
}
return new Promise((resolve, reject) => {
getMysqlConnection((err, conn) => {
if (err) return reject(err);
conn.query({sql}, args, (err, results) => {
conn.release();
if (err) return reject(err);
resolve(results[0].count);
});
});
});
}
/**
* list all applications - for all service providers, for one service provider, or for one account,
* or by an optional name
* list all applications - for all service providers, for one service provider, or for one account
*/
static retrieveAll(obj) {
const { page, page_size = 50 } = obj || {};
// If pagination is requested, first get the application IDs
if (page !== null && page !== undefined) {
let idSql = 'SELECT application_sid, name FROM applications app WHERE 1 = 1';
const idArgs = [];
idSql += Application._criteriaBuilder(obj, idArgs);
idSql += ' ORDER BY app.name';
const limit = Number(page_size);
const offset = Number(page > 0 ? (page - 1) : page) * limit;
idSql += ' LIMIT ? OFFSET ?';
idArgs.push(limit);
idArgs.push(offset);
return new Promise((resolve, reject) => {
getMysqlConnection((err, conn) => {
if (err) return reject(err);
// Get paginated application IDs
conn.query(idSql, idArgs, (err, idResults) => {
if (err) {
conn.release();
return reject(err);
}
if (idResults.length === 0) {
conn.release();
return resolve([]);
}
// Get full data for these applications
const appIds = idResults.map((row) => row.application_sid);
const placeholders = appIds.map(() => '?').join(',');
const fullSql = `${retrieveSql}
WHERE app.application_sid IN (${placeholders}) ORDER BY app.name`;
conn.query({sql: fullSql, nestTables: true}, appIds, (err, results) => {
conn.release();
if (err) return reject(err);
const r = transmogrifyResults(results);
resolve(r);
});
});
});
});
}
// No pagination - use original query
static retrieveAll(service_provider_sid, account_sid) {
let sql = retrieveSql;
const args = [];
const criteriaClause = Application._criteriaBuilder(obj, args);
// Only use "WHERE 1 = 1" if there are no filters
// Otherwise start with the actual filter for better index usage
let sql;
if (criteriaClause) {
// Remove leading ' AND ' from criteriaBuilder output and use as WHERE clause
sql = retrieveSql + ' WHERE ' + criteriaClause.substring(5);
} else {
// No filters provided - must list all applications
sql = retrieveSql + ' WHERE 1 = 1';
if (account_sid) {
sql = `${sql} WHERE app.account_sid = ?`;
args.push(account_sid);
}
else if (service_provider_sid) {
sql = `${sql} WHERE account_sid in (SELECT account_sid from accounts WHERE service_provider_sid = ?)`;
args.push(service_provider_sid);
}
sql += ' ORDER BY app.application_sid';
return new Promise((resolve, reject) => {
getMysqlConnection((err, conn) => {
if (err) return reject(err);
@@ -218,10 +120,6 @@ Application.fields = [
{
name: 'messaging_hook_sid',
type: 'string',
},
{
name: 'record_all_calls',
type: 'number',
}
];

View File

@@ -1,70 +0,0 @@
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.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 = acc.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'
},
{
name: 'allow_direct_app_calling',
type: 'number'
},
{
name: 'allow_direct_queue_calling',
type: 'number'
},
{
name: 'allow_direct_user_calling',
type: 'number'
}
];
module.exports = Client;

View File

@@ -1,61 +0,0 @@
const Model = require('./model');
const {promisePool} = require('../db');
class GoogleCustomVoice extends Model {
constructor() {
super();
}
static async retrieveAllBySpeechCredentialSid(speech_credential_sid) {
const sql = `SELECT * FROM ${this.table} WHERE speech_credential_sid = ?`;
const [rows] = await promisePool.query(sql, speech_credential_sid);
return rows;
}
static async deleteAllBySpeechCredentialSid(speech_credential_sid) {
const sql = `DELETE FROM ${this.table} WHERE speech_credential_sid = ?`;
const [rows] = await promisePool.query(sql, speech_credential_sid);
return rows;
}
static async retrieveAllByLabel(service_provider_sid, account_sid, label) {
let sql;
if (account_sid) {
sql = `SELECT gcv.* FROM ${this.table} gcv
LEFT JOIN speech_credentials sc ON gcv.speech_credential_sid = sc.speech_credential_sid
WHERE sc.account_sid = ? OR (sc.account_sid is NULL && sc.service_provider_sid = ?)
${label ? 'AND label = ?' : 'AND label is NULL'}`;
} else {
sql = `SELECT gcv.* FROM ${this.table} gcv
LEFT JOIN speech_credentials sc ON gcv.speech_credential_sid = sc.speech_credential_sid
WHERE sc.service_provider_sid = ? ${label ? 'AND label = ?' : 'AND label is NULL'}`;
}
const [rows] = await promisePool.query(sql, [...(account_sid ?
[account_sid, service_provider_sid] : [service_provider_sid]), label]);
return rows;
}
}
GoogleCustomVoice.table = 'google_custom_voices';
GoogleCustomVoice.fields = [
{
name: 'google_custom_voice_sid',
type: 'string',
primaryKey: true
},
{
name: 'model',
type: 'string',
required: true
},
{
name: 'reported_usage',
type: 'number'
},
{
name: 'name',
type: 'string',
required: true
}
];
module.exports = GoogleCustomVoice;

View File

@@ -1,47 +0,0 @@
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;

View File

@@ -1,54 +0,0 @@
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;

View File

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

View File

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

View File

@@ -1,45 +0,0 @@
const Model = require('./model');
const {promisePool} = require('../db');
const sqlAll = `
SELECT * from permissions
`;
const sqlByName = `
SELECT * from permissions where name = ?
`;
class Permissions extends Model {
constructor() {
super();
}
static async retrieveAll() {
const [rows] = await promisePool.query(sqlAll);
return rows;
}
static async retrieveByName(name) {
const [rows] = await promisePool.query(sqlByName, [name]);
return rows;
}
}
Permissions.table = 'permissions';
Permissions.fields = [
{
name: 'permission_sid',
type: 'string',
primaryKey: true
},
{
name: 'name',
type: 'string',
required: true
},
{
name: 'description',
type: 'string',
required: true
}
];
module.exports = Permissions;

View File

@@ -1,7 +1,6 @@
const Model = require('./model');
const {promisePool} = require('../db');
const sqlRetrieveAll = 'SELECT * from phone_numbers WHERE account_sid = ? ORDER BY number';
const sqlRetrieveOne = 'SELECT * from phone_numbers WHERE phone_number_sid = ? AND account_sid = ? ORDER BY number';
const sql = 'SELECT * from phone_numbers WHERE account_sid = ?';
const sqlSP = `SELECT *
FROM phone_numbers
WHERE account_sid IN
@@ -9,7 +8,7 @@ WHERE account_sid IN
SELECT account_sid
FROM accounts
WHERE service_provider_sid = ?
) ORDER BY number`;
)`;
class PhoneNumber extends Model {
constructor() {
@@ -17,8 +16,8 @@ class PhoneNumber extends Model {
}
static async retrieveAll(account_sid) {
if (!account_sid) return await super.retrieveAll();
const [rows] = await promisePool.query(sqlRetrieveAll, account_sid);
if (!account_sid) return super.retrieveAll();
const [rows] = await promisePool.query(sql, account_sid);
return rows;
}
static async retrieveAllForSP(service_provider_sid) {
@@ -26,55 +25,12 @@ class PhoneNumber extends Model {
return rows;
}
static _criteriaBuilder(obj, params) {
let sql = '';
if (obj.service_provider_sid) {
sql += ' AND account_sid IN (SELECT account_sid FROM accounts WHERE service_provider_sid = ?)';
params.push(obj.service_provider_sid);
}
if (obj.account_sid) {
sql += ' AND account_sid = ?';
params.push(obj.account_sid);
}
if (obj.filter) {
sql += ' AND number LIKE ?';
params.push(`%${obj.filter}%`);
}
return sql;
}
static async countAll(obj) {
let sql = 'SELECT COUNT(*) AS count FROM phone_numbers WHERE 1 = 1';
const args = [];
sql += PhoneNumber._criteriaBuilder(obj, args);
const [rows] = await promisePool.query(sql, args);
return rows[0].count;
}
static async retrieveAllByCriteria(obj) {
let sql = 'SELECT * FROM phone_numbers WHERE 1=1';
const params = [];
const { page, page_size = 50 } = obj || {};
sql += PhoneNumber._criteriaBuilder(obj, params);
sql += ' ORDER BY number';
if (page !== null && page !== undefined) {
const limit = Number(page_size);
const offset = Number(page > 0 ? (page - 1) : page) * limit;
sql += ' LIMIT ? OFFSET ?';
params.push(limit);
params.push(offset);
}
const [rows] = await promisePool.query(sql, params);
return rows;
}
/**
* retrieve a phone number
*/
static async retrieve(sid, account_sid) {
if (!account_sid) return super.retrieve(sid);
const [rows] = await promisePool.query(sqlRetrieveOne, [sid, account_sid]);
const [rows] = await promisePool.query(`${sql} AND phone_number_sid = ?`, [account_sid, sid]);
return rows;
}
}

View File

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

View File

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

View File

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

View File

@@ -1,42 +0,0 @@
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',
},
{
name: 'private_network_cidr',
type: 'string',
},
];
module.exports = SystemInformation;

View File

@@ -1,53 +0,0 @@
const Model = require('./model');
const {promisePool} = require('../db');
const sqlAll = `
SELECT * from user_permissions
`;
const sqlByUserIdPermissionSid = `
SELECT * from user_permissions where user_sid = ? and permission_sid = ?
`;
const sqlByUserId = `
SELECT * from user_permissions where user_sid = ?
`;
class UserPermissions extends Model {
constructor() {
super();
}
static async retrieveAll() {
const [rows] = await promisePool.query(sqlAll);
return rows;
}
static async retrieveByUserIdPermissionSid(user_sid, permission_sid) {
const [rows] = await promisePool.query(sqlByUserIdPermissionSid, [user_sid, permission_sid]);
return rows;
}
static async retrieveByUserId(user_sid) {
const [rows] = await promisePool.query(sqlByUserId, [user_sid]);
return rows;
}
}
UserPermissions.table = 'user_permissions';
UserPermissions.fields = [
{
name: 'user_permissions_sid',
type: 'string',
primaryKey: true
},
{
name: 'user_sid',
type: 'string',
required: true
},
{
name: 'permission_sid',
type: 'string',
required: true
}
];
module.exports = UserPermissions;

View File

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

View File

@@ -8,70 +8,13 @@ class VoipCarrier extends Model {
constructor() {
super();
}
static _criteriaBuilder(obj, args) {
let sql = '';
if (obj.account_sid) {
// carrier belong to an account when
// 1. account_sid is set
// 2. account_sid is null and service_provider_sid matches the account's service_provider_sid
sql += ` AND (vc.account_sid = ? OR
(vc.account_sid IS NULL AND vc.service_provider_sid IN
(SELECT service_provider_sid FROM accounts WHERE account_sid = ?))
)`;
args.push(obj.account_sid);
args.push(obj.account_sid);
}
if (obj.service_provider_sid) {
sql += ' AND vc.service_provider_sid = ?';
args.push(obj.service_provider_sid);
}
if (obj.name) {
sql += ' AND vc.name LIKE ?';
args.push(`%${obj.name}%`);
}
return sql;
}
static async countAll(obj) {
let sql = 'SELECT COUNT(*) AS count FROM voip_carriers vc WHERE 1 = 1';
const args = [];
sql += VoipCarrier._criteriaBuilder(obj, args);
const [rows] = await promisePool.query(sql, args);
return rows[0].count;
}
static async retrieveByCriteria(obj) {
let sql = 'SELECT * from voip_carriers vc WHERE 1 =1';
const args = [];
sql += VoipCarrier._criteriaBuilder(obj, args);
if (obj.page !== null && obj.page !== undefined) {
const limit = Number(obj.page_size || 50);
const offset = (Number(obj.page) - 1) * limit;
sql += ' LIMIT ? OFFSET ?';
args.push(limit, offset);
}
const [rows] = await promisePool.query(sql, args);
if (rows) {
rows.map((r) => r.register_status = JSON.parse(r.register_status || '{}'));
}
return rows;
}
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;
}
}
@@ -112,10 +55,6 @@ VoipCarrier.fields = [
name: 'requires_register',
type: 'number'
},
{
name: 'register_use_tls',
type: 'number'
},
{
name: 'register_username',
type: 'string'
@@ -183,18 +122,6 @@ VoipCarrier.fields = [
{
name: 'register_public_ip_in_contact',
type: 'number'
},
{
name: 'register_status',
type: 'string'
},
{
name: 'dtmf_type',
type: 'string'
},
{
name: 'sip_proxy',
type: 'string'
}
];

View File

@@ -1,71 +0,0 @@
const { Writable } = require('stream');
const { BlobServiceClient } = require('@azure/storage-blob');
const { v4: uuidv4 } = require('uuid');
const streamBuffers = require('stream-buffers');
class AzureStorageUploadStream extends Writable {
constructor(logger, opts) {
super(opts);
const blobServiceClient = BlobServiceClient.fromConnectionString(opts.connection_string);
this.blockBlobClient = blobServiceClient.getContainerClient(opts.bucketName).getBlockBlobClient(opts.Key);
this.metadata = opts.metadata;
this.blocks = [];
this.bufferSize = 2 * 1024 * 1024; // Buffer size set to 2MB
this.buffer = new streamBuffers.WritableStreamBuffer({
initialSize: this.bufferSize,
incrementAmount: this.bufferSize
});
}
async _write(chunk, encoding, callback) {
this.buffer.write(chunk, encoding);
if (this.buffer.size() >= this.bufferSize) {
const blockID = uuidv4().replace(/-/g, '');
this.blocks.push(blockID);
try {
const dataToWrite = this.buffer.getContents();
await this.blockBlobClient.stageBlock(blockID, dataToWrite, dataToWrite.length);
callback();
} catch (error) {
callback(error);
}
} else {
callback();
}
}
async _final(callback) {
// Write any remaining data in buffer
if (this.buffer.size() > 0) {
const remainingData = this.buffer.getContents();
const blockID = uuidv4().replace(/-/g, '');
this.blocks.push(blockID);
try {
await this.blockBlobClient.stageBlock(blockID, remainingData, remainingData.length);
} catch (error) {
callback(error);
return;
}
}
try {
await this.blockBlobClient.commitBlockList(this.blocks);
// remove all null/undefined props
const filteredObj = Object.entries(this.metadata).reduce((acc, [key, val]) => {
if (val !== undefined && val !== null) acc[key] = val;
return acc;
}, {});
await this.blockBlobClient.setMetadata(filteredObj);
callback();
} catch (error) {
callback(error);
}
}
}
module.exports = AzureStorageUploadStream;

View File

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

View File

@@ -1,62 +0,0 @@
const { Storage } = require('@google-cloud/storage');
const { Writable } = require('stream');
const streamBuffers = require('stream-buffers');
class GoogleStorageUploadStream extends Writable {
constructor(logger, opts) {
super(opts);
this.logger = logger;
this.metadata = opts.metadata;
const storage = new Storage(opts.bucketCredential);
this.gcsFile = storage.bucket(opts.bucketName).file(opts.Key);
this.writeStream = this.gcsFile.createWriteStream();
this.bufferSize = 2 * 1024 * 1024; // Buffer size set to 2MB
this.buffer = new streamBuffers.WritableStreamBuffer({
initialSize: this.bufferSize,
incrementAmount: this.bufferSize
});
this.writeStream.on('error', (err) => this.logger.error(err));
this.writeStream.on('finish', () => {
this.logger.info('Google storage Upload completed.');
this._addMetadata();
});
}
_write(chunk, encoding, callback) {
this.buffer.write(chunk, encoding);
// Write to GCS when buffer reaches desired size
if (this.buffer.size() >= this.bufferSize) {
const dataToWrite = this.buffer.getContents();
this.writeStream.write(dataToWrite, callback);
} else {
callback();
}
}
_final(callback) {
// Write any remaining data in the buffer to GCS
if (this.buffer.size() > 0) {
const remainingData = this.buffer.getContents();
this.writeStream.write(remainingData);
}
this.writeStream.end();
this.writeStream.once('finish', callback);
}
async _addMetadata() {
try {
await this.gcsFile.setMetadata({metadata: this.metadata});
this.logger.info('Google storage Upload and metadata setting completed.');
} catch (err) {
this.logger.error(err, 'Google storage An error occurred while setting metadata');
}
}
}
module.exports = GoogleStorageUploadStream;

View File

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

View File

@@ -1,113 +0,0 @@
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 = [];
// accumulate incoming chunks to avoid O(n^2) Buffer.concat on every write
this.chunks = [];
this.bufferedBytes = 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 _uploadPart(bodyBuffer) {
const uploadPartCommand = new UploadPartCommand({
Bucket: this.bucketName,
Key: this.objectKey,
PartNumber: this.partNumber,
UploadId: this.uploadId,
Body: bodyBuffer,
});
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();
}
// accumulate without concatenating on every write
this.chunks.push(chunk);
this.bufferedBytes += chunk.length;
if (this.bufferedBytes >= this.minPartSize) {
const partBuffer = Buffer.concat(this.chunks, this.bufferedBytes);
// reset accumulators before awaiting upload to allow GC
this.chunks = [];
this.bufferedBytes = 0;
await this._uploadPart(partBuffer);
}
callback(null);
} catch (error) {
callback(error);
}
}
async _finalize(err) {
try {
if (this.bufferedBytes > 0) {
const finalBuffer = Buffer.concat(this.chunks, this.bufferedBytes);
this.chunks = [];
this.bufferedBytes = 0;
await this._uploadPart(finalBuffer);
}
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;

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
const router = require('express').Router();
const assert = require('assert');
const {DbErrorBadRequest, DbErrorForbidden, DbErrorUnprocessableRequest} = require('../../utils/errors');
const request = require('request');
const {DbErrorBadRequest, DbErrorUnprocessableRequest} = require('../../utils/errors');
const Account = require('../../models/account');
const Application = require('../../models/application');
const Webhook = require('../../models/webhook');
@@ -12,20 +12,9 @@ const { v4: uuidv4 } = require('uuid');
const snakeCase = require('../../utils/snake-case');
const sysError = require('../error');
const {promisePool} = require('../../db');
const {
hasAccountPermissions,
parseAccountSid,
parseCallSid,
enableSubspace,
disableSubspace,
parseVoipCarrierSid,
hasValue,
} = require('./utils');
const {hasAccountPermissions, parseAccountSid, enableSubspace, disableSubspace} = require('./utils');
const short = require('short-uuid');
const VoipCarrier = require('../../models/voip-carrier');
const { encrypt, obscureBucketCredentialsSensitiveData,
isObscureKey, decrypt } = require('../../utils/encrypt-decrypt');
const { testS3Storage, testGoogleStorage, testAzureStorage } = require('../../utils/storage-utils');
const translator = short();
let idx = 0;
@@ -42,42 +31,18 @@ const getFsUrl = async(logger, retrieveSet, setName) => {
logger.info('No available feature servers to handle createCall API request');
return ;
}
const f = fs[idx++ % fs.length];
logger.debug({fs}, `feature servers available for createCall API request, selecting ${f}`);
return `${f}/v1/createCall`;
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`;
} catch (err) {
logger.error({err}, 'getFsUrl: error retreving feature servers from redis');
}
};
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;
}
const stripPort = (hostport) => {
const arr = /^(.*):(.*)$/.exec(hostport);
if (arr) return arr[1];
return hostport;
};
router.use('/:sid/SpeechCredentials', hasAccountPermissions, require('./speech-credentials'));
@@ -87,40 +52,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 {page, page_size, name} = req.query || {};
const isPaginationRequest = page !== null && page !== undefined;
let results = [];
let total = 0;
if (isPaginationRequest) {
total = await Application.countAll({account_sid, name});
results = await Application.retrieveAll({
account_sid, name, page, page_size
});
} else {
results = await Application.retrieveAll({account_sid});
}
const ret = results.map((a) => {
if (a.env_vars) {
a.env_vars = JSON.parse(decrypt(a.env_vars));
return a;
} else {
return a;
}
});
const body = isPaginationRequest ? {
total,
page: Number(page),
page_size: Number(page_size),
data: ret
} : ret;
res.status(200).json(body);
const results = await Application.retrieveAll(null, account_sid);
res.status(200).json(results);
} catch (err) {
sysError(logger, res, err);
}
@@ -129,51 +66,17 @@ router.get('/:sid/VoipCarriers', async(req, res) => {
const logger = req.app.locals.logger;
try {
const account_sid = parseAccountSid(req);
await validateRequest(req, account_sid);
const results = await VoipCarrier.retrieveAll(account_sid);
res.status(200).json(results);
} catch (err) {
sysError(logger, res, err);
}
});
router.put('/:sid/VoipCarriers/:voip_carrier_sid', async(req, res) => {
const logger = req.app.locals.logger;
try {
if (process.env.JAMBONES_ADMIN_CARRIER == 1 && (!req.user.hasScope('service_provider')
&& !req.user.hasScope('admin'))) {
throw new DbErrorBadRequest('insufficient privileges');
}
const sid = parseVoipCarrierSid(req);
const account_sid = parseAccountSid(req);
await validateRequest(req, account_sid);
const rowsAffected = await VoipCarrier.update(sid, req.body);
if (rowsAffected === 0) {
return res.sendStatus(404);
}
return res.status(204).end();
} catch (err) {
sysError(logger, res, err);
}
});
router.post('/:sid/VoipCarriers', async(req, res) => {
const logger = req.app.locals.logger;
const payload = req.body;
try {
if (process.env.JAMBONES_ADMIN_CARRIER == 1 && (!req.user.hasScope('service_provider')
|| !!req.user.hasScope('admin'))) {
throw new DbErrorBadRequest('insufficient privileges');
}
const account_sid = parseAccountSid(req);
await validateRequest(req, account_sid);
// Set the service_provder_sid to the relevent value for the account
const account = await Account.retrieve(req.user.account_sid);
payload.service_provider_sid = account[0].service_provider_sid;
logger.debug({payload}, 'POST /:sid/VoipCarriers');
const uuid = await VoipCarrier.make({
account_sid,
@@ -185,89 +88,6 @@ router.post('/:sid/VoipCarriers', async(req, res) => {
}
});
router.get('/:sid/RegisteredSipUsers', async(req, res) => {
const {logger, registrar} = req.app.locals;
try {
const account_sid = parseAccountSid(req);
await validateRequest(req, account_sid);
const result = await Account.retrieve(account_sid);
if (!result || result.length === 0) {
throw new DbErrorBadRequest(`account not found for sid ${account_sid}`);
}
if (!result[0].sip_realm) {
throw new DbErrorBadRequest('account does not have sip_realm configuration');
}
const users = await registrar.getRegisteredUsersForRealm(result[0].sip_realm);
res.status(200).json(users.map((u) => `${u}@${result[0].sip_realm}`));
} catch (err) {
sysError(logger, res, err);
}
});
router.post('/:sid/RegisteredSipUsers', async(req, res) => {
const {logger, registrar} = req.app.locals;
const users = req.body;
try {
const account_sid = parseAccountSid(req);
await validateRequest(req, account_sid);
const result = await Account.retrieve(account_sid);
if (!result || result.length === 0) {
throw new DbErrorBadRequest(`account not found for sid ${account_sid}`);
}
if (!result[0].sip_realm) {
throw new DbErrorBadRequest('account does not have sip_realm configuration');
}
if (!users || !Array.isArray(users) || users.length === 0) {
return res.status(200).json(await registrar.getRegisteredUsersDetailsForRealm(result[0].sip_realm));
}
const ret = [];
for (const u of users) {
const user = await registrar.query(`${u}@${result[0].sip_realm}`) || {
name: u,
contact: null,
expiryTime: 0,
protocol: null
};
ret.push({
name: u,
...user,
registered_status: user.expiryTime > 0 ? 'active' : 'inactive',
});
}
res.status(200).json(ret);
} catch (err) {
sysError(logger, res, err);
}
});
router.get('/:sid/RegisteredSipUsers/:client', async(req, res) => {
const {logger, registrar, lookupClientByAccountAndUsername} = req.app.locals;
const client = req.params.client;
try {
const account_sid = parseAccountSid(req);
await validateRequest(req, account_sid);
const result = await Account.retrieve(account_sid);
if (!result || result.length === 0) {
throw new DbErrorBadRequest(`account not found for sid ${account_sid}`);
}
const user = await registrar.query(`${client}@${result[0].sip_realm}`);
const [clientDb] = await lookupClientByAccountAndUsername(account_sid, client);
res.status(200).json({
name: client,
contact: user ? user.contact : null,
expiryTime: user ? user.expiryTime : 0,
protocol: user ? user.protocol : null,
allow_direct_app_calling: clientDb ? clientDb.allow_direct_app_calling : 0,
allow_direct_queue_calling: clientDb ? clientDb.allow_direct_queue_calling : 0,
allow_direct_user_calling: clientDb ? clientDb.allow_direct_user_calling : 0,
registered_status: user ? 'active' : 'inactive',
proxy: user ? user.proxy : null
});
} catch (err) {
sysError(logger, res, err);
}
});
function coerceNumbers(callInfo) {
if (Array.isArray(callInfo)) {
return callInfo.map((ci) => {
@@ -298,18 +118,11 @@ function validateUpdateCall(opts) {
'child_call_hook',
'call_status',
'listen_status',
'transcribe_status',
'conf_hold_status',
'conf_mute_status',
'mute_status',
'sip_request',
'record',
'tag',
'dtmf',
'conferenceParticipantAction',
'dub',
'boostAudioSignal',
'media_path'
'record'
]
.reduce((acc, prop) => (opts[prop] ? ++acc : acc), 0);
@@ -345,37 +158,15 @@ function validateUpdateCall(opts) {
throw new DbErrorBadRequest('invalid conf_mute_status');
}
if (opts.sip_request &&
(!opts.sip_request.method || !opts.sip_request.content_type || !opts.sip_request.content)) {
throw new DbErrorBadRequest('sip_request requires method, content_type and content properties');
(!opts.sip_request.method && !opts.sip_request.content_type || !opts.sip_request.content_type)) {
throw new DbErrorBadRequest('sip_request requires content_type and content properties');
}
if (opts.record && !opts.record.action) {
throw new DbErrorBadRequest('record requires action property');
}
if (opts.dtmf && !opts.dtmf.digit) {
throw new DbErrorBadRequest('invalid dtmf');
}
if ('startCallRecording' === opts.record?.action && !opts.record.siprecServerURL) {
throw new DbErrorBadRequest('record requires siprecServerURL property when starting recording');
}
if (opts.tag && (typeof opts.tag !== 'object' || Array.isArray(opts.tag) || opts.tag === null)) {
throw new DbErrorBadRequest('invalid tag data');
}
if (opts.conferenceParticipantAction) {
if (!['tag', 'untag', 'coach', 'uncoach', 'mute', 'unmute', 'hold', 'unhold']
.includes(opts.conferenceParticipantAction.action)) {
throw new DbErrorBadRequest(
`conferenceParticipantAction invalid action property ${opts.conferenceParticipantAction.action}`);
}
if ('tag' == opts.conferenceParticipantAction.action && !opts.conferenceParticipantAction.tag) {
throw new DbErrorBadRequest('conferenceParticipantAction requires tag property when action is \'tag\'');
}
if ('coach' == opts.conferenceParticipantAction.action && !opts.conferenceParticipantAction.tag) {
throw new DbErrorBadRequest('conferenceParticipantAction requires tag property when action is \'coach\'');
}
}
if (opts.media_path && !['no-media', 'partial-media', 'full-media'].includes(opts.media_path)) {
throw new DbErrorBadRequest('invalid media_path');
}
}
function validateTo(to) {
@@ -395,15 +186,11 @@ function validateTo(to) {
}
throw new DbErrorBadRequest(`missing or invalid to property: ${JSON.stringify(to)}`);
}
async function validateCreateCall(logger, sid, req) {
const {lookupAppBySid} = req.app.locals;
const obj = req.body;
if (req.user.hasServiceProviderAuth ||
req.user.hasAccountAuth && req.user.account_sid !== sid) {
throw new DbErrorBadRequest(`unauthorized createCall request for account ${sid}`);
}
if (req.user.account_sid !== sid) throw new DbErrorBadRequest(`unauthorized createCall request for account ${sid}`);
obj.account_sid = sid;
if (!obj.from) throw new DbErrorBadRequest('missing from parameter');
@@ -415,7 +202,6 @@ 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,
@@ -500,7 +286,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 DbErrorForbidden('insufficient permissions');
throw new DbErrorUnprocessableRequest('insufficient permissions to create accounts');
}
if (req.user.hasServiceProviderAuth && req.user.service_provider_sid) {
/* service providers can only create accounts under themselves */
@@ -519,10 +305,9 @@ async function validateAdd(req) {
throw new DbErrorBadRequest('\'queue_event_hook\' must be an object when adding an account');
}
}
async function validateUpdate(req, sid) {
if (req.user.hasAccountAuth && req.user.account_sid !== sid) {
throw new DbErrorForbidden('insufficient privileges');
throw new DbErrorUnprocessableRequest('insufficient privileges to update this account');
}
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');
@@ -530,23 +315,12 @@ async function validateUpdate(req, sid) {
if (req.user.service_provider_sid && !req.user.hasScope('admin')) {
const result = await Account.retrieve(sid);
if (!result || result.length === 0) {
throw new DbErrorBadRequest(`account not found for sid ${sid}`);
}
if (result[0].service_provider_sid !== req.user.service_provider_sid) {
throw new DbErrorForbidden('insufficient privileges');
}
}
if (req.user.hasScope('admin')) {
/* check to be sure that the account_sid exists */
const result = await Account.retrieve(sid);
if (!result || result.length === 0) {
throw new DbErrorBadRequest(`account not found for sid ${sid}`);
throw new DbErrorUnprocessableRequest('cannot update account from different service provider');
}
}
if (req.body.service_provider_sid) throw new DbErrorBadRequest('service_provider_sid may not be modified');
}
async function validateDelete(req, sid) {
if (req.user.hasAccountAuth && req.user.account_sid !== sid) {
throw new DbErrorUnprocessableRequest('insufficient privileges to update this account');
@@ -554,11 +328,12 @@ 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 DbErrorForbidden('insufficient privileges');
throw new DbErrorUnprocessableRequest('cannot delete account from different service provider');
}
}
}
/* add */
router.post('/', async(req, res) => {
const logger = req.app.locals.logger;
@@ -574,8 +349,6 @@ router.post('/', async(req, res) => {
}
delete obj[prop];
}
//force sip realm to lowercase
if (obj.sip_realm) { obj.sip_realm = obj.sip_realm.toLowerCase(); }
logger.debug(`Attempting to add account ${JSON.stringify(obj)}`);
const uuid = await Account.make(obj);
@@ -602,15 +375,10 @@ router.get('/', async(req, res) => {
router.get('/:sid', async(req, res) => {
const logger = req.app.locals.logger;
try {
const account_sid = parseAccountSid(req);
await validateRequest(req, account_sid);
const service_provider_sid = req.user.hasServiceProviderAuth ? req.user.service_provider_sid : null;
const [result] = await Account.retrieve(account_sid, service_provider_sid) || [];
if (!result) return res.status(404).end();
result.bucket_credential = obscureBucketCredentialsSensitiveData(result.bucket_credential);
return res.status(200).json(result);
const results = await Account.retrieve(req.params.sid, service_provider_sid);
if (results.length === 0) return res.status(404).end();
return res.status(200).json(results[0]);
}
catch (err) {
sysError(logger, res, err);
@@ -620,15 +388,13 @@ router.get('/:sid', async(req, res) => {
router.get('/:sid/WebhookSecret', async(req, res) => {
const logger = req.app.locals.logger;
try {
const account_sid = parseAccountSid(req);
await validateRequest(req, account_sid);
const service_provider_sid = req.user.hasServiceProviderAuth ? req.user.service_provider_sid : null;
const results = await Account.retrieve(account_sid, service_provider_sid);
const results = await Account.retrieve(req.params.sid, service_provider_sid);
if (results.length === 0) return res.status(404).end();
let {webhook_secret} = results[0];
if (req.query.regenerate) {
const secret = `wh_secret_${translator.generate()}`;
await Account.update(account_sid, {webhook_secret: secret});
await Account.update(req.params.sid, {webhook_secret: secret});
webhook_secret = secret;
}
return res.status(200).json({webhook_secret});
@@ -641,9 +407,8 @@ router.get('/:sid/WebhookSecret', async(req, res) => {
router.post('/:sid/SubspaceTeleport', async(req, res) => {
const logger = req.app.locals.logger;
try {
const account_sid = parseAccountSid(req);
const service_provider_sid = req.user.hasServiceProviderAuth ? req.user.service_provider_sid : null;
const results = await Account.retrieve(account_sid, service_provider_sid);
const results = await Account.retrieve(req.params.sid, service_provider_sid);
if (results.length === 0) return res.status(404).end();
const {subspace_client_id, subspace_client_secret} = results[0];
const {destination} = req.body;
@@ -656,7 +421,7 @@ router.post('/:sid/SubspaceTeleport', async(req, res) => {
destination: dest
});
logger.info({destination, teleport}, 'SubspaceTeleport - create teleport');
await Account.update(account_sid, {
await Account.update(req.params.sid, {
subspace_sip_teleport_id: teleport.id,
subspace_sip_teleport_destinations: JSON.stringify(teleport.teleport_entry_points)//hacky
});
@@ -674,14 +439,13 @@ router.post('/:sid/SubspaceTeleport', async(req, res) => {
router.delete('/:sid/SubspaceTeleport', async(req, res) => {
const logger = req.app.locals.logger;
try {
const account_sid = parseAccountSid(req);
const service_provider_sid = req.user.hasServiceProviderAuth ? req.user.service_provider_sid : null;
const results = await Account.retrieve(account_sid, service_provider_sid);
const results = await Account.retrieve(req.params.sid, service_provider_sid);
if (results.length === 0) return res.status(404).end();
const {subspace_client_id, subspace_client_secret, subspace_sip_teleport_id} = results[0];
await disableSubspace({subspace_client_id, subspace_client_secret, subspace_sip_teleport_id});
await Account.update(account_sid, {
await Account.update(req.params.sid, {
subspace_sip_teleport_id: null,
subspace_sip_teleport_destinations: null
});
@@ -692,82 +456,11 @@ router.delete('/:sid/SubspaceTeleport', async(req, res) => {
}
});
function encryptBucketCredential(obj, storedCredentials = {}) {
if (!hasValue(obj?.bucket_credential)) return;
const {
vendor,
region,
name,
access_key_id,
tags,
endpoint,
} = obj.bucket_credential;
let {
secret_access_key,
service_key,
connection_string,
} = 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');
if (isObscureKey(obj.bucket_credential) && hasValue(storedCredentials)) {
secret_access_key = storedCredentials.secret_access_key;
}
const awsData = JSON.stringify({vendor, region, name, access_key_id,
secret_access_key, tags});
obj.bucket_credential = encrypt(awsData);
break;
case 's3_compatible':
assert(access_key_id, 'invalid aws S3 bucket credential: access_key_id is required');
assert(secret_access_key, 'invalid aws S3 bucket credential: secret_access_key is required');
assert(name, 'invalid aws bucket name: name is required');
assert(endpoint, 'invalid endpoint uri: endpoint is required');
if (isObscureKey(obj.bucket_credential) && hasValue(storedCredentials)) {
secret_access_key = storedCredentials.secret_access_key;
}
const s3Data = JSON.stringify({vendor, endpoint, name, access_key_id,
secret_access_key, tags,
...(region && {region})
});
obj.bucket_credential = encrypt(s3Data);
break;
case 'google':
assert(service_key, 'invalid google cloud storage credential: service_key is required');
if (isObscureKey(obj.bucket_credential) && hasValue(storedCredentials)) {
service_key = storedCredentials.service_key;
}
const googleData = JSON.stringify({vendor, name, service_key, tags});
obj.bucket_credential = encrypt(googleData);
break;
case 'azure':
assert(name, 'invalid azure container name: name is required');
assert(connection_string, 'invalid azure cloud storage credential: connection_string is required');
if (isObscureKey(obj.bucket_credential) && hasValue(storedCredentials)) {
connection_string = storedCredentials.connection_string;
}
const azureData = JSON.stringify({vendor, name, connection_string, tags});
obj.bucket_credential = encrypt(azureData);
break;
case 'none':
obj.bucket_credential = null;
break;
default:
throw new DbErrorBadRequest(`unknown storage vendor: ${vendor}`);
}
}
/**
* update
*/
/* update */
router.put('/:sid', async(req, res) => {
const sid = req.params.sid;
const logger = req.app.locals.logger;
try {
const sid = parseAccountSid(req);
await validateRequest(req, sid);
// create webhooks if provided
const obj = Object.assign({}, req.body);
@@ -806,21 +499,6 @@ router.put('/:sid', async(req, res) => {
delete obj.registration_hook;
delete obj.queue_event_hook;
let storedBucketCredentials = {};
if (isObscureKey(obj?.bucket_credential)) {
const [account] = await Account.retrieve(sid) || [];
/* to avoid overwriting valid credentials with the obscured secret,
* that the frontend might send, we pass the stored account bucket credentials
* in the case it is a obscured key, we replace it with the stored one
*/
storedBucketCredentials = account.bucket_credential;
}
encryptBucketCredential(obj, storedBucketCredentials);
//force sip realm to lowercase
if (obj.sip_realm) { obj.sip_realm = obj.sip_realm.toLowerCase();}
const rowsAffected = await Account.update(sid, obj);
if (rowsAffected === 0) {
return res.status(404).end();
@@ -842,14 +520,12 @@ router.put('/:sid', async(req, res) => {
/* delete */
router.delete('/:sid', async(req, res) => {
const sid = req.params.sid;
const logger = req.app.locals.logger;
const sqlDeleteGateways = `DELETE from sip_gateways
WHERE voip_carrier_sid IN
(SELECT voip_carrier_sid from voip_carriers where account_sid = ?)`;
try {
const sid = parseAccountSid(req);
await validateRequest(req, sid);
await validateDelete(req, sid);
const [account] = await promisePool.query('SELECT * FROM accounts WHERE account_sid = ?', sid);
@@ -913,64 +589,13 @@ 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);
/* if the req.body bucket credentials contain an obscured key, replace with stored account.bucket_credential */
if (isObscureKey(req.body)) {
const service_provider_sid = req.user.hasServiceProviderAuth ? req.user.service_provider_sid : null;
const [account] = await Account.retrieve(account_sid, service_provider_sid) || [];
if (!account) return res.status(404).end();
req.body = account.bucket_credential;
}
const {vendor, name, region, access_key_id, secret_access_key, service_key, connection_string, endpoint} = req.body;
const ret = {
status: 'not tested'
};
switch (vendor) {
case 'aws_s3':
await testS3Storage(logger, {vendor, name, region, access_key_id, secret_access_key});
ret.status = 'ok';
break;
case 's3_compatible':
await testS3Storage(logger, {vendor, name, endpoint, access_key_id, secret_access_key});
ret.status = 'ok';
break;
case 'google':
await testGoogleStorage(logger, {vendor, name, service_key});
ret.status = 'ok';
break;
case 'azure':
await testAzureStorage(logger, {vendor, name, connection_string});
ret.status = 'ok';
break;
default:
throw new DbErrorBadRequest(`Does not support test for ${vendor}`);
}
return res.status(200).json(ret);
}
catch (err) {
return res.status(200).json({status: 'failed', reason: err.message});
}
});
/**
* retrieve account level api keys
*/
/* retrieve account level api keys */
router.get('/:sid/ApiKeys', async(req, res) => {
const logger = req.app.locals.logger;
try {
const sid = parseAccountSid(req);
await validateRequest(req, sid);
const results = await ApiKey.retrieveAll(sid);
const results = await ApiKey.retrieveAll(req.params.sid);
res.status(200).json(results);
updateLastUsed(logger, sid, req).catch((err) => {});
updateLastUsed(logger, req.params.sid, req).catch((err) => {});
} catch (err) {
sysError(logger, res, err);
}
@@ -980,41 +605,36 @@ router.get('/:sid/ApiKeys', async(req, res) => {
* create a new Call
*/
router.post('/:sid/Calls', async(req, res) => {
const sid = req.params.sid;
const setName = `${(process.env.JAMBONES_CLUSTER_ID || 'default')}:active-fs`;
const {retrieveSet, logger} = req.app.locals;
const setName = `${(process.env.JAMBONES_CLUSTER_ID || 'default')}:fs-service-url`;
const serviceUrl = await getFsUrl(logger, retrieveSet, setName);
if (!serviceUrl) {
return res.status(480).json({msg: 'no available feature servers at this time'});
}
try {
const sid = parseAccountSid(req);
await validateRequest(req, sid);
await validateCreateCall(logger, sid, req);
updateLastUsed(logger, sid, req).catch((err) => {});
const response = await fetch(serviceUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(Object.assign(req.body, {account_sid: sid}))
});
if (!response.ok) {
logger.error(`Error sending createCall POST to ${serviceUrl}`);
return res.sendStatus(500);
res.status(480).json({msg: 'no available feature servers at this time'});
} else {
try {
await validateCreateCall(logger, sid, req);
updateLastUsed(logger, sid, req).catch((err) => {});
request({
url: serviceUrl,
method: 'POST',
json: true,
body: Object.assign(req.body, {account_sid: sid})
}, (err, response, body) => {
if (err) {
logger.error(err, `Error sending createCall POST to ${serviceUrl}`);
return res.sendStatus(500);
}
if (response.statusCode !== 201) {
logger.error({statusCode: response.statusCode}, `Non-success response returned by createCall ${serviceUrl}`);
return res.sendStatus(500);
}
res.status(201).json(body);
});
} catch (err) {
sysError(logger, res, err);
}
if (response.status !== 201) {
logger.error(`Non-success response returned by createCall ${serviceUrl}`);
return res.sendStatus(500);
}
const body = await response.json();
return res.status(201).json(body);
} catch (err) {
sysError(logger, res, err);
}
});
@@ -1022,18 +642,11 @@ router.post('/:sid/Calls', async(req, res) => {
* retrieve info for a group of calls under an account
*/
router.get('/:sid/Calls', async(req, res) => {
const accountSid = req.params.sid;
const {logger, listCalls} = req.app.locals;
const {direction, from, to, callStatus} = req.query || {};
try {
const accountSid = parseAccountSid(req);
await validateRequest(req, accountSid);
const calls = await listCalls({
accountSid,
direction,
from,
to,
callStatus
});
const calls = await listCalls(accountSid);
logger.debug(`retrieved ${calls.length} calls for account sid ${accountSid}`);
res.status(200).json(coerceNumbers(snakeCase(calls)));
updateLastUsed(logger, accountSid, req).catch((err) => {});
@@ -1046,12 +659,11 @@ router.get('/:sid/Calls', async(req, res) => {
* retrieve single call
*/
router.get('/:sid/Calls/:callSid', async(req, res) => {
const accountSid = req.params.sid;
const callSid = req.params.callSid;
const {logger, retrieveCall} = req.app.locals;
try {
const accountSid = parseAccountSid(req);
await validateRequest(req, accountSid);
const callSid = parseCallSid(req);
const callInfo = await retrieveCall(accountSid, callSid);
if (callInfo) {
logger.debug(callInfo, `retrieved call info for call sid ${callSid}`);
@@ -1071,12 +683,11 @@ router.get('/:sid/Calls/:callSid', async(req, res) => {
* delete call
*/
router.delete('/:sid/Calls/:callSid', async(req, res) => {
const accountSid = req.params.sid;
const callSid = req.params.callSid;
const {logger, deleteCall} = req.app.locals;
try {
const accountSid = parseAccountSid(req);
await validateRequest(req, accountSid);
const callSid = parseCallSid(req);
const result = await deleteCall(accountSid, callSid);
if (result) {
logger.debug(`successfully deleted call ${callSid}`);
@@ -1096,61 +707,28 @@ router.delete('/:sid/Calls/:callSid', async(req, res) => {
* update a call
*/
const updateCall = async(req, res) => {
const accountSid = req.params.sid;
const callSid = req.params.callSid;
const {logger, retrieveCall} = req.app.locals;
try {
const accountSid = parseAccountSid(req);
await validateRequest(req, accountSid);
const callSid = parseCallSid(req);
validateUpdateCall(req.body);
updateLastUsed(logger, accountSid, req).catch((err) => {});
const call = await retrieveCall(accountSid, callSid);
if (call) {
const url = `${call.serviceUrl}/${process.env.JAMBONES_API_VERSION || 'v1'}/updateCall/${callSid}`;
logger.debug({call, url, payload: req.body}, `updateCall: retrieved call info for call sid ${callSid}`);
const response = await fetch(url, {
request({
url: url,
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(req.body)
});
if (!response.ok) {
try {
const text = await response.text();
logger.error(`Error sending updateCall POST to ${url}, status: ${response.status} body: ${text}`);
// Try to parse as JSON if there's content
if (text) {
try {
const body = JSON.parse(text);
return res.status(response.status).json(body);
} catch {
// Not valid JSON
return res.status(response.status).send(text);
}
}
return res.sendStatus(response.status);
} catch (err) {
logger.error({err}, `updateCall: error reading response from ${url}`);
return res.sendStatus(response.status);
}
}
if (response.status === 200) {
// feature server return json for sip_request command
// with 200 OK
const body = await response.json();
return res.status(200).json(body);
} else {
// rest commander returns 202 Accepted for all other commands
return res.sendStatus(response.status);
}
json: true,
body: req.body
}).pipe(res);
}
else {
logger.debug(`updateCall: call not found for call sid ${callSid}`);
res.sendStatus(404);
}
updateLastUsed(logger, accountSid, req).catch((err) => {});
} catch (err) {
sysError(logger, res, err);
}
@@ -1160,7 +738,6 @@ const updateCall = async(req, res) => {
router.post('/:sid/Calls/:callSid', async(req, res) => {
await updateCall(req, res);
});
router.put('/:sid/Calls/:callSid', async(req, res) => {
await updateCall(req, res);
});
@@ -1169,17 +746,13 @@ router.put('/:sid/Calls/:callSid', async(req, res) => {
* create a new Message
*/
router.post('/:sid/Messages', async(req, res) => {
const account_sid = parseAccountSid(req);
const setName = `${(process.env.JAMBONES_CLUSTER_ID || 'default')}:active-fs`;
const {retrieveSet, logger} = req.app.locals;
try {
const account_sid = parseAccountSid(req);
await validateRequest(req, account_sid);
const setName = `${(process.env.JAMBONES_CLUSTER_ID || 'default')}:fs-service-url`;
const serviceUrl = await getFsUrl(logger, retrieveSet, setName);
if (!serviceUrl) {
return res.status(480).json({msg: 'no available feature servers at this time'});
}
if (!serviceUrl) res.json({msg: 'no available feature servers at this time'}).status(480);
await validateCreateMessage(logger, account_sid, req);
const payload = {
@@ -1189,73 +762,22 @@ router.post('/:sid/Messages', async(req, res) => {
};
logger.debug({payload}, `sending createMessage API request to to ${serviceUrl}`);
updateLastUsed(logger, account_sid, req).catch(() => {});
const response = await fetch(serviceUrl, {
request({
url: serviceUrl,
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
json: true,
body: payload
}, (err, response, body) => {
if (err) {
logger.error(err, `Error sending createMessage POST to ${serviceUrl}`);
return res.sendStatus(500);
}
if (response.statusCode !== 200) {
logger.error({statusCode: response.statusCode}, `Non-success response returned by createMessage ${serviceUrl}`);
return body ? res.status(response.statusCode).json(body) : res.sendStatus(response.statusCode);
}
res.status(201).json(body);
});
if (!response.ok) {
logger.error(`Error sending createMessage POST to ${serviceUrl}`);
return res.sendStatus(500);
}
const body = await response.json();
return res.status(response.status).json(body);
} catch (err) {
sysError(logger, res, err);
}
});
/**
* 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);
}
});
/**
* retrieve info for a list of conferences under an account
*/
router.get('/:sid/Conferences', async(req, res) => {
const {logger, listConferences} = req.app.locals;
try {
const accountSid = parseAccountSid(req);
await validateRequest(req, accountSid);
const conferences = await listConferences(accountSid);
logger.debug(`retrieved ${conferences.length} queues for account sid ${accountSid}`);
res.status(200).json(conferences.map((c) => c.split(':').pop()));
updateLastUsed(logger, accountSid, req).catch((err) => {});
} catch (err) {
sysError(logger, res, err);
}
});
/**
* retrieve counts of calls under an account
*/
router.get('/:sid/CallCount', async(req, res) => {
const {logger, getCallCount} = req.app.locals;
try {
const accountSid = parseAccountSid(req);
await validateRequest(req, accountSid);
const count = await getCallCount(accountSid);
count.outbound = Number(count.outbound);
count.inbound = Number(count.inbound);
logger.debug(`retrieved, outbound: ${count.outbound}, inbound: ${count.inbound}, for account sid ${accountSid}`);
res.status(200).json(count);
updateLastUsed(logger, accountSid, req).catch((err) => {});
} catch (err) {
sysError(logger, res, err);
}

View File

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

View File

@@ -1,13 +1,17 @@
const router = require('express').Router();
const sysError = require('../error');
const {DbErrorBadRequest} = require('../../utils/errors');
const { parseServiceProviderSid } = require('./utils');
const parseAccountSid = (url) => {
const arr = /Accounts\/([^\/]*)/.exec(url);
if (arr) return arr[1];
};
const parseServiceProviderSid = (url) => {
const arr = /ServiceProviders\/([^\/]*)/.exec(url);
if (arr) return arr[1];
};
router.get('/', async(req, res) => {
const {logger, queryAlerts, queryAlertsSP} = req.app.locals;
try {
@@ -16,7 +20,7 @@ router.get('/', async(req, res) => {
const service_provider_sid = account_sid ? null : parseServiceProviderSid(req.originalUrl);
const {page, count, alert_type, days, start, end} = req.query || {};
if (!page || page < 1) throw new DbErrorBadRequest('missing or invalid "page" query arg');
if (!count || count > 500) throw new DbErrorBadRequest('missing or invalid "count" query arg');
if (!count || count < 25 || count > 500) throw new DbErrorBadRequest('missing or invalid "count" query arg');
if (account_sid) {
const data = await queryAlerts({

View File

@@ -1,47 +0,0 @@
const router = require('express').Router();
const sysError = require('../error');
const { fetchAppEnvSchema, validateAppEnvSchema } = require('../../utils/appenv_utils');
const URL = require('url').URL;
const isValidUrl = (s) => {
const protocols = ['https:', 'http:', 'ws:', 'wss:'];
try {
const url = new URL(s);
if (protocols.includes(url.protocol)) {
return true;
}
else {
return false;
}
} catch (err) {
return false;
}
};
/* get appenv schema for endpoint */
router.get('/', async(req, res) => {
const logger = req.app.locals.logger;
const url = req.query.url;
if (!isValidUrl(url)) {
sysError(logger, res, 'Invalid URL');
} else {
try {
const appenv = await fetchAppEnvSchema(logger, url);
if (appenv && validateAppEnvSchema(appenv)) {
return res.status(200).json(appenv);
} else if (appenv) {
return res.status(400).json({
msg: 'Invalid appenv schema',
});
} else {
return res.status(204).end(); //No appenv returned from url, normal scenario
}
}
catch (err) {
sysError(logger, res, err);
}
}
});
module.exports = router;

View File

@@ -1,49 +1,15 @@
const router = require('express').Router();
const {DbErrorBadRequest, DbErrorUnprocessableRequest, DbErrorForbidden} = require('../../utils/errors');
const {DbErrorBadRequest, DbErrorUnprocessableRequest} = require('../../utils/errors');
const Application = require('../../models/application');
const Account = require('../../models/account');
const Webhook = require('../../models/webhook');
const {promisePool} = require('../../db');
const decorate = require('./decorate');
const sysError = require('../error');
const { validate } = require('@jambonz/verb-specifications');
const { parseApplicationSid, isInvalidUrl } = require('./utils');
const preconditions = {
'add': validateAdd,
'update': validateUpdate
};
const { fetchAppEnvSchema, validateAppEnvData } = require('../../utils/appenv_utils');
const {decrypt, encrypt} = require('../../utils/encrypt-decrypt');
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) {
@@ -55,7 +21,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 DbErrorForbidden('insufficient privileges');
throw new DbErrorBadRequest('insufficient privileges to create an application under the specified account');
}
}
if (req.body.call_hook && typeof req.body.call_hook !== 'object') {
@@ -64,78 +30,31 @@ async function validateAdd(req) {
if (req.body.call_status_hook && typeof req.body.call_hook !== 'object') {
throw new DbErrorBadRequest('\'call_status_hook\' must be an object when adding an application');
}
let urlError = await isInvalidUrl(req.body.call_hook.url);
if (urlError) {
throw new DbErrorBadRequest(`call_hook ${urlError}`);
}
urlError = await isInvalidUrl(req.body.call_status_hook.url);
if (urlError) {
throw new DbErrorBadRequest(`call_status_hook ${urlError}`);
}
}
async function validateUpdate(req, sid) {
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.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');
}
}
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');
}
if (req.body.call_status_hook && typeof req.body.call_hook !== 'object') {
throw new DbErrorBadRequest('\'call_status_hook\' must be an object when updating an application');
}
let urlError;
if (req.body.call_hook) {
urlError = await isInvalidUrl(req.body.call_hook.url);
if (urlError) {
throw new DbErrorBadRequest(`call_hook ${urlError}`);
}
}
if (req.body.call_status_hook) {
urlError = await isInvalidUrl(req.body.call_status_hook.url);
if (urlError) {
throw new DbErrorBadRequest(`call_status_hook ${urlError}`);
}
}
}
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('insufficient permissions');
throw new DbErrorUnprocessableRequest('cannot delete application owned by a different account');
}
}
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');
}
@@ -157,26 +76,6 @@ router.post('/', async(req, res) => {
}
}
// validate app json if required
if (obj['app_json']) {
const app_json = JSON.parse(obj['app_json']);
try {
validate(logger, app_json);
} catch (err) {
throw new DbErrorBadRequest(err);
}
}
// validate env_vars data if required
if (obj['env_vars']) {
const appenvschema = await fetchAppEnvSchema(logger, req.body.call_hook.url);
const errors = await validateAppEnvData(appenvschema, obj['env_vars']);
if (errors) {
throw new DbErrorBadRequest(errors);
} else {
obj['env_vars'] = encrypt(JSON.stringify(obj['env_vars']));
}
}
const uuid = await Application.make(obj);
res.status(201).json({sid: uuid});
} catch (err) {
@@ -187,34 +86,11 @@ router.post('/', async(req, res) => {
/* list */
router.get('/', async(req, res) => {
const logger = req.app.locals.logger;
const {page, page_size, name} = req.query || {};
const isPaginationRequest = page !== null && page !== undefined;
try {
const service_provider_sid = req.user.hasServiceProviderAuth ? req.user.service_provider_sid : null;
const account_sid = req.user.hasAccountAuth ? req.user.account_sid : null;
let results = [];
let total = 0;
if (isPaginationRequest) {
total = await Application.countAll({service_provider_sid, account_sid, name});
}
results = await Application.retrieveAll({
service_provider_sid, account_sid, name, page, page_size
});
const ret = results.map((a) => {
if (a.env_vars) {
a.env_vars = JSON.parse(decrypt(a.env_vars));
return a;
} else {
return a;
}
});
const body = isPaginationRequest ? {
total,
page: Number(page),
page_size: Number(page_size),
data: ret
} : ret;
res.status(200).json(body);
const results = await Application.retrieveAll(service_provider_sid, account_sid);
res.status(200).json(results);
} catch (err) {
sysError(logger, res, err);
}
@@ -224,15 +100,10 @@ router.get('/', async(req, res) => {
router.get('/:sid', async(req, res) => {
const logger = req.app.locals.logger;
try {
const application_sid = parseApplicationSid(req);
const service_provider_sid = req.user.hasServiceProviderAuth ? req.user.service_provider_sid : null;
const account_sid = req.user.hasAccountAuth ? req.user.account_sid : null;
const results = await Application.retrieve(application_sid, service_provider_sid, account_sid);
const results = await Application.retrieve(req.params.sid, service_provider_sid, account_sid);
if (results.length === 0) return res.status(404).end();
await validateRequest(req, results[0].account_sid);
if (results[0].env_vars) {
results[0].env_vars = JSON.parse(decrypt(results[0].env_vars));
}
return res.status(200).json(results[0]);
}
catch (err) {
@@ -242,9 +113,9 @@ router.get('/:sid', async(req, res) => {
/* delete */
router.delete('/:sid', async(req, res) => {
const sid = req.params.sid;
const logger = req.app.locals.logger;
try {
const sid = parseApplicationSid(req);
await validateDelete(req, sid);
const [application] = await promisePool.query('SELECT * FROM applications WHERE application_sid = ?', sid);
@@ -283,12 +154,10 @@ router.delete('/:sid', async(req, res) => {
/* update */
router.put('/:sid', async(req, res) => {
const sid = req.params.sid;
const logger = req.app.locals.logger;
try {
const sid = parseApplicationSid(req);
await validateUpdate(req, sid);
const service_provider_sid = req.user.hasServiceProviderAuth ? req.user.service_provider_sid : null;
const account_sid = req.user.hasAccountAuth ? req.user.account_sid : null;
// create webhooks if provided
const obj = Object.assign({}, req.body);
@@ -304,34 +173,12 @@ router.put('/:sid', async(req, res) => {
obj[`${prop}_sid`] = sid;
}
}
else {
obj[`${prop}_sid`] = null;
}
delete obj[prop];
}
// validate app json if required
if (obj['app_json']) {
const app_json = JSON.parse(obj['app_json']);
try {
validate(logger, app_json);
} catch (err) {
throw new DbErrorBadRequest(err);
}
}
// validate env_vars data if required
if (obj['env_vars']) {
const applications = await Application.retrieve(sid, service_provider_sid, account_sid);
const call_hook_url = req.body.call_hook ?
(req.body.call_hook.url || req.body.call_hook) :
applications[0].call_hook.url;
const appenvschema = await fetchAppEnvSchema(logger, call_hook_url);
const errors = await validateAppEnvData(appenvschema, obj['env_vars']);
if (errors) {
throw new DbErrorBadRequest(errors);
} else {
obj['env_vars'] = encrypt(JSON.stringify(obj['env_vars']));
}
}
const rowsAffected = await Application.update(sid, obj);
if (rowsAffected === 0) {
return res.status(404).end();

View File

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

View File

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

View File

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

View File

@@ -4,9 +4,7 @@ const short = require('short-uuid');
const translator = short();
const {validateEmail, emailSimpleText} = require('../../utils/email-utils');
const {promisePool} = require('../../db');
const {cacheClient} = require('../../helpers');
const sysError = require('../error');
const assert = require('assert');
const sql = `SELECT * from users user
LEFT JOIN accounts AS acc
ON acc.account_sid = user.account_sid
@@ -27,8 +25,7 @@ function createOauthEmailText(provider) {
}
function createResetEmailText(link) {
assert(process.env.JAMBONZ_BASE_URL, 'process.env.JAMBONZ_BASE_URL is missing');
const baseUrl = process.env.JAMBONZ_BASE_URL;
const baseUrl = 'http://localhost:3001';
return `Hi there!
@@ -48,26 +45,19 @@ function createResetEmailText(link) {
router.post('/', async(req, res) => {
const {logger, addKey} = req.app.locals;
const {email} = req.body;
let obj;
try {
if (!email || !validateEmail(email)) {
logger.info({email}, 'Bad POST to /forgot-password is missing email or invalid email');
return res.status(400).json({error: 'invalid or missing email'});
}
const [r] = await promisePool.query({sql, nestTables: true}, email);
if (0 === r.length) {
logger.info(`user not found: ${email}`);
return res.status(400).json({error: 'failed to reset your password'});
return res.status(400).json({error: 'email does not exist'});
}
obj = r[0];
if (!obj.user.is_active) {
logger.info({user: obj.user.name, obj}, '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({account_sid: obj.acc.account_sid, obj}, 'account is inactive');
return res.status(400).json({error: 'failed to reset your password'});
if (!obj.acc.is_active) {
return res.status(400).json({error: 'you may not reset the password of an inactive account'});
}
res.sendStatus(204);
} catch (err) {
@@ -83,14 +73,10 @@ router.post('/', async(req, res) => {
else {
/* generate a link for this user to reset, send email */
const link = translator.generate();
const redisKey = cacheClient.generateRedisKey('reset-link', link);
addKey(redisKey, obj.user.user_sid, 3600)
addKey(`reset-link:${link}`, obj.user.user_sid, 3600)
.catch((err) => logger.error({err}, 'Error adding reset link to redis'));
emailSimpleText(logger, email, 'Reset password request', createResetEmailText(link));
}
const redisKey = cacheClient.generateRedisKey('jwt', obj.user.user_sid, 'v2');
await cacheClient.delete(redisKey);
});
module.exports = router;

View File

@@ -1,137 +0,0 @@
const router = require('express').Router();
const GoogleCustomVoice = require('../../models/google-custom-voice');
const SpeechCredential = require('../../models/speech-credential');
const decorate = require('./decorate');
const {DbErrorBadRequest, DbErrorForbidden} = require('../../utils/errors');
const sysError = require('../error');
const multer = require('multer');
const upload = multer({ dest: '/tmp/csv/' });
const fs = require('fs');
const validateCredentialPermission = async(req) => {
const credential = await SpeechCredential.retrieve(req.body.speech_credential_sid);
if (!credential || credential.length === 0) {
throw new DbErrorBadRequest('Invalid speech_credential_sid');
}
const cred = credential[0];
if (req.user.hasServiceProviderAuth && cred.service_provider_sid !== req.user.service_provider_sid) {
throw new DbErrorForbidden('Insufficient privileges');
}
if (req.user.hasAccountAuth && cred.account_sid !== req.user.account_sid) {
throw new DbErrorForbidden('Insufficient privileges');
}
};
const validateAdd = async(req) => {
if (!req.body.speech_credential_sid) {
throw new DbErrorBadRequest('missing speech_credential_sid');
}
await validateCredentialPermission(req);
};
const validateUpdate = async(req) => {
if (req.body.speech_credential_sid) {
await validateCredentialPermission(req);
}
};
const preconditions = {
add: validateAdd,
update: validateUpdate,
};
decorate(router, GoogleCustomVoice, ['add', 'retrieve', 'update', 'delete'], preconditions);
const voiceCloningKeySubString = (voice_cloning_key) => {
return voice_cloning_key ? voice_cloning_key.substring(0, 100) + '...' : undefined;
};
router.get('/: sid', async(req, res) => {
const logger = req.app.locals.logger;
try {
const {sid} = req.params;
const account_sid = req.user.account_sid;
const service_provider_sid = req.user.service_provider_sid;
const google_voice = await GoogleCustomVoice.retrieve(sid);
google_voice.voice_cloning_key = voiceCloningKeySubString(google_voice.voice_cloning_key);
if (!google_voice) {
return res.sendStatus(404);
}
if (req.user.hasScope('service_provider') && google_voice.service_provider_sid !== service_provider_sid ||
req.user.hasScope('account') && google_voice.account_sid !== account_sid) {
throw new DbErrorForbidden('Insufficient privileges');
}
return res.status(200).json(google_voice);
} catch (err) {
sysError(logger, res, err);
}
});
router.get('/', async(req, res) => {
const logger = req.app.locals.logger;
const account_sid = req.user.account_sid || req.query.account_sid;
const service_provider_sid = req.user.service_provider_sid || req.query.service_provider_sid;
const speech_credential_sid = req.query.speech_credential_sid;
const label = req.query.label;
try {
let results = [];
if (speech_credential_sid) {
const [cred] = await SpeechCredential.retrieve(speech_credential_sid);
if (!cred) {
return res.sendStatus(404);
}
if (account_sid && cred.account_sid && cred.account_sid !== account_sid) {
throw new DbErrorForbidden('Insufficient privileges');
}
if (service_provider_sid && cred.service_provider_sid && cred.service_provider_sid !== service_provider_sid) {
throw new DbErrorForbidden('Insufficient privileges');
}
results = await GoogleCustomVoice.retrieveAllBySpeechCredentialSid(speech_credential_sid);
} else {
if (!account_sid && !service_provider_sid) {
throw new DbErrorBadRequest('missing account_sid or service_provider_sid in query parameters');
}
results = await GoogleCustomVoice.retrieveAllByLabel(service_provider_sid, account_sid, label);
}
res.status(200).json(results.map((r) => {
r.voice_cloning_key = voiceCloningKeySubString(r.voice_cloning_key);
return r;
}));
} catch (err) {
sysError(logger, res, err);
}
});
router.post('/:sid/VoiceCloningKey', upload.single('file'), async(req, res) => {
const {logger} = req.app.locals;
const {sid} = req.params;
const account_sid = req.user.account_sid;
const service_provider_sid = req.user.service_provider_sid;
try {
const google_voice = await GoogleCustomVoice.retrieve(sid);
if (!google_voice) {
return res.sendStatus(404);
}
if (req.user.hasScope('service_provider') && google_voice.service_provider_sid !== service_provider_sid ||
req.user.hasScope('account') && google_voice.account_sid !== account_sid) {
throw new DbErrorForbidden('Insufficient privileges');
}
const voice_cloning_key = Buffer.from(fs.readFileSync(req.file.path)).toString();
await GoogleCustomVoice.update(sid, {
voice_cloning_key
});
fs.unlinkSync(req.file.path);
return res.sendStatus(204);
} catch (err) {
sysError(logger, res, err);
}
});
module.exports = router;

View File

@@ -7,18 +7,16 @@ const isAdminScope = (req, res, next) => {
message: 'insufficient privileges'
});
};
// const isAdminOrSPScope = (req, res, next) => {
// if (req.user.hasScope('admin') || req.user.hasScope('service_provider')) return next();
// res.status(403).json({
// status: 'fail',
// message: 'insufficient privileges'
// });
// };
const isAdminOrSPScope = (req, res, next) => {
if (req.user.hasScope('admin') || req.user.hasScope('service_provider')) return next();
res.status(403).json({
status: 'fail',
message: 'insufficient privileges'
});
};
api.use('/BetaInviteCodes', isAdminScope, require('./beta-invite-codes'));
api.use('/SystemInformation', isAdminScope, require('./system-information'));
api.use('/TtsCache', isAdminScope, require('./tts-cache'));
api.use('/ServiceProviders', require('./service-providers'));
api.use('/ServiceProviders', isAdminOrSPScope, require('./service-providers'));
api.use('/VoipCarriers', require('./voip-carriers'));
api.use('/Webhooks', require('./webhooks'));
api.use('/SipGateways', require('./sip-gateways'));
@@ -39,7 +37,6 @@ api.use('/change-password', require('./change-password'));
api.use('/ActivationCode', require('./activation-code'));
api.use('/Availability', require('./availability'));
api.use('/AccountTest', require('./account-test'));
api.use('/AppEnv', require('./appenv'));
//api.use('/Products', require('./products'));
api.use('/Prices', require('./prices'));
api.use('/StripeCustomerId', require('./stripe-customer-id'));
@@ -47,14 +44,7 @@ api.use('/Subscriptions', require('./subscriptions'));
api.use('/Invoices', require('./invoices'));
api.use('/InviteCodes', require('./invite-codes'));
api.use('/PredefinedCarriers', require('./predefined-carriers'));
api.use('/PasswordSettings', 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'));
// Google Custom Voices
api.use('/GoogleCustomVoices', require('./google-custom-voices'));
api.use('/PasswordSettings', isAdminScope, require('./password-settings'));
// messaging
api.use('/Smpps', require('./smpps')); // our smpp server info

View File

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

View File

@@ -1,96 +0,0 @@
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;

View File

@@ -1,225 +0,0 @@
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);
const validateLcrBatchAdd = async(lcr_sid, body, lookupCarrierBySid) => {
for (const lcr_route of body) {
lcr_route.lcr_sid = lcr_sid;
if (!lcr_route.lcr_carrier_set_entries || lcr_route.lcr_carrier_set_entries.length === 0) {
throw new DbErrorBadRequest('Lcr Route batch process require lcr_carrier_set_entries');
}
for (const entry of lcr_route.lcr_carrier_set_entries) {
// check voip_carrier_sid is exist
if (!entry.voip_carrier_sid) {
throw new DbErrorBadRequest('One of lcr_carrier_set_entries is missing voip_carrier_sid');
}
const carrier = await lookupCarrierBySid(entry.voip_carrier_sid);
if (!carrier) {
throw new DbErrorBadRequest('unknown voip_carrier_sid');
}
}
}
};
const addNewLcrRoute = async(lcr_route) => {
const lcr_sid = lcr_route.lcr_sid;
const lcr_carrier_set_entries = lcr_route.lcr_carrier_set_entries;
delete lcr_route.lcr_carrier_set_entries;
const lcr_route_sid = await LcrRoutes.make(lcr_route);
for (const entry of lcr_carrier_set_entries) {
entry.lcr_route_sid = lcr_route_sid;
const lcr_carrier_set_entry_sid = await LcrCarrierSetEntry.make(entry);
if (lcr_route.priority === 9999) {
// this is default lcr set entry
const [lcr] = await Lcr.retrieve(lcr_sid);
if (lcr) {
lcr.default_carrier_set_entry_sid = lcr_carrier_set_entry_sid;
delete lcr.lcr_sid;
await Lcr.update(lcr_sid, lcr);
}
}
}
};
router.post('/:sid/Routes', async(req, res) => {
const results = await Lcr.retrieve(req.params.sid);
if (results.length === 0) return res.sendStatus(404);
const {logger, lookupCarrierBySid} = req.app.locals;
try {
const body = req.body;
await validateLcrBatchAdd(req.params.sid, body, lookupCarrierBySid);
for (const lcr_route of body) {
await addNewLcrRoute(lcr_route, lookupCarrierBySid);
}
res.sendStatus(204);
} catch (err) {
sysError(logger, res, err);
}
});
router.put('/:sid/Routes', async(req, res) => {
const results = await Lcr.retrieve(req.params.sid);
if (results.length === 0) return res.sendStatus(404);
const {logger, lookupCarrierBySid} = req.app.locals;
try {
const body = req.body;
await validateLcrBatchAdd(req.params.sid, body, lookupCarrierBySid);
for (const lcr_route of body) {
if (lcr_route.lcr_route_sid) {
const lcr_route_sid = lcr_route.lcr_route_sid;
delete lcr_route.lcr_route_sid;
const lcr_carrier_set_entries = lcr_route.lcr_carrier_set_entries;
delete lcr_route.lcr_carrier_set_entries;
await LcrRoutes.update(lcr_route_sid, lcr_route);
for (const entry of lcr_carrier_set_entries) {
const lcr_carrier_set_entry_sid = entry.lcr_carrier_set_entry_sid;
delete entry.lcr_carrier_set_entry_sid;
await LcrCarrierSetEntry.update(lcr_carrier_set_entry_sid, entry);
}
} else {
// Route is not available yet, let create it now
await addNewLcrRoute(lcr_route, lookupCarrierBySid);
}
}
res.sendStatus(204);
} catch (err) {
sysError(logger, res, err);
}
});
router.get('/', async(req, res) => {
const logger = req.app.locals.logger;
try {
const results = req.user.hasAdminAuth ?
await Lcr.retrieveAll() : req.user.hasAccountAuth ?
await Lcr.retrieveAllByAccountSid(req.user.hasAccountAuth ? req.user.account_sid : null) :
await Lcr.retrieveAllByServiceProviderSid(req.user.service_provider_sid);
for (const lcr of results) {
lcr.number_routes = await LcrRoutes.countAllByLcrSid(lcr.lcr_sid);
}
res.status(200).json(results);
} catch (err) {
sysError(logger, res, err);
}
});
router.get('/:sid', async(req, res) => {
const logger = req.app.locals.logger;
try {
const results = await Lcr.retrieve(req.params.sid);
if (results.length === 0) return res.sendStatus(404);
const lcr = results[0];
if (req.user.hasAccountAuth && lcr.account_sid !== req.user.account_sid) {
return res.sendStatus(404);
} else if (req.user.hasServiceProviderAuth && lcr.service_provider_sid !== req.user.service_provider_sid) {
return res.sendStatus(404);
}
lcr.number_routes = await LcrRoutes.countAllByLcrSid(lcr.lcr_sid);
return res.status(200).json(lcr);
} catch (err) {
sysError(logger, res, err);
}
});
module.exports = router;

View File

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

View File

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

View File

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

View File

@@ -15,9 +15,6 @@ const validate = (obj) => {
router.post('/', async(req, res) => {
const logger = req.app.locals.logger;
try {
if (!req.user.hasAdminAuth) {
return res.sendStatus(403);
}
validate(req.body);
const [existing] = (await PasswordSettings.retrieve() || []);
if (existing) {

View File

@@ -1,10 +1,8 @@
const router = require('express').Router();
const {DbErrorBadRequest, DbErrorForbidden} = require('../../utils/errors');
const {DbErrorUnprocessableRequest, DbErrorBadRequest} = 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,
@@ -12,29 +10,17 @@ const preconditions = {
'update': validateUpdate
};
const sysError = require('../error');
const { parsePhoneNumberSid } = require('./utils');
const hasWhitespace = (str) => /\s/.test(str);
/* check for required fields when adding */
async function validateAdd(req) {
try {
if (process.env.JAMBONES_ADMIN_CARRIER == 1 && (!req.user.hasScope('service_provider')
&& !req.user.hasScope('admin'))) {
throw new DbErrorBadRequest('insufficient privileges');
}
/* account level user can only act on carriers associated to his/her account */
if (req.user.hasAccountAuth) {
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');
if (hasWhitespace(req.body.number)) throw new DbErrorBadRequest('number cannot contain whitespace');
const formattedNumber = e164(req.body.number);
req.body.number = formattedNumber;
} catch (err) {
@@ -47,10 +33,6 @@ async function validateAdd(req) {
if (!result || result.length === 0) {
throw new DbErrorBadRequest(`voip_carrier not found for sid ${req.body.voip_carrier_sid}`);
}
const carrier = result[0];
if (carrier.account_sid && req.body.account_sid && req.body.account_sid !== carrier.account_sid) {
throw new DbErrorBadRequest('voip_carrier_sid does not belong to the account');
}
}
}
@@ -59,11 +41,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 DbErrorForbidden('insufficient privileges');
throw new DbErrorUnprocessableRequest('cannot delete a phone number that belongs to another account');
}
}
if (!req.user.hasAccountAuth && phoneNumber.account_sid) {
throw new DbErrorForbidden('insufficient privileges');
throw new DbErrorUnprocessableRequest('cannot delete phone number that is assigned to an account');
}
}
@@ -75,23 +57,10 @@ 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 DbErrorForbidden('insufficient privileges');
throw new DbErrorUnprocessableRequest('cannot operate on a phone number that belongs to another account');
}
}
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
@@ -104,35 +73,9 @@ decorate(router, PhoneNumber, ['add', 'update', 'delete'], preconditions);
/* list */
router.get('/', async(req, res) => {
const logger = req.app.locals.logger;
const {service_provider_sid: query_service_provider_sid,
account_sid: query_account_sid, filter, page, page_size} = req.query;
const isPaginationRequest = page !== null && page !== undefined;
let service_provider_sid = null, account_sid = query_account_sid;
if (req.user.hasAccountAuth) {
account_sid = req.user.account_sid;
} else if (req.user.hasServiceProviderAuth) {
service_provider_sid = req.user.service_provider_sid;
} else {
// admin user can query all phone numbers
service_provider_sid = query_service_provider_sid;
account_sid = query_account_sid;
}
try {
let total = 0;
if (isPaginationRequest) {
total = await PhoneNumber.countAll({service_provider_sid, account_sid, filter});
}
const results = await PhoneNumber.retrieveAllByCriteria({
service_provider_sid, account_sid, filter, page, page_size
});
const body = isPaginationRequest ? {
total,
page: Number(page),
page_size: Number(page_size),
data: results,
} : results;
res.status(200).json(body);
const results = await PhoneNumber.retrieveAll(req.user.hasAccountAuth ? req.user.account_sid : null);
res.status(200).json(results);
} catch (err) {
sysError(logger, res, err);
}
@@ -142,22 +85,9 @@ router.get('/', async(req, res) => {
router.get('/:sid', async(req, res) => {
const logger = req.app.locals.logger;
try {
const sid = parsePhoneNumberSid(req);
const account_sid = req.user.hasAccountAuth ? req.user.account_sid : null;
const results = await PhoneNumber.retrieve(sid, account_sid);
const results = await PhoneNumber.retrieve(req.params.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');
}
}
if (req.user.hasAccountAuth && results.length > 1) {
return res.status(200).json(results.filter((r) => r.phone_number_sid === sid)[0]);
}
return res.status(200).json(results[0]);
}
catch (err) {

View File

@@ -2,18 +2,6 @@ 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 { CloudWatchLogsClient, FilterLogEventsCommand } = require('@aws-sdk/client-cloudwatch-logs');
const {
getS3Object,
getGoogleStorageObject,
getAzureStorageObject,
deleteS3Object,
deleteGoogleStorageObject,
deleteAzureStorageObject
} = require('../../utils/storage-utils');
const parseAccountSid = (url) => {
const arr = /Accounts\/([^\/]*)/.exec(url);
if (arr) return arr[1];
@@ -30,9 +18,9 @@ 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, filter} = req.query || {};
const {page, count, trunk, direction, days, answered, start, end} = req.query || {};
if (!page || page < 1) throw new DbErrorBadRequest('missing or invalid "page" query arg');
if (!count || count > 500) throw new DbErrorBadRequest('missing or invalid "count" query arg');
if (!count || count < 25 || count > 500) throw new DbErrorBadRequest('missing or invalid "count" query arg');
if (account_sid) {
const data = await queryCdrs({
@@ -45,7 +33,6 @@ router.get('/', async(req, res) => {
answered,
start: days ? undefined : start,
end: days ? undefined : end,
filter
});
res.status(200).json(data);
}
@@ -60,7 +47,6 @@ router.get('/', async(req, res) => {
answered,
start: days ? undefined : start,
end: days ? undefined : end,
filter
});
res.status(200).json(data);
}
@@ -86,12 +72,12 @@ router.get('/:call_id', async(req, res) => {
}
});
router.get('/:call_id/:method/pcap', async(req, res) => {
router.get('/:call_id/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], req.params.method);
const stream = await getHomerPcap(logger, token, [req.params.call_id]);
if (!stream) {
logger.info(`getHomerApiKey: unable to get sip traces from Homer for ${req.params.call_id}`);
return res.sendStatus(404);
@@ -107,165 +93,4 @@ router.get('/:call_id/:method/pcap', async(req, res) => {
}
});
router.get('/:call_sid/logs', async(req, res) => {
const {logger, queryCdrs} = req.app.locals;
const aws_region = process.env.AWS_REGION;
const {call_sid} = req.params;
const {logGroupName = 'jambonz-feature_server'} = req.query;
const account_sid = parseAccountSid(req.originalUrl);
if (!aws_region) {
return res.status(400).send({msg: 'Logs are only available in AWS environments'});
}
if (!account_sid) {
return res.status(400).send({msg: 'account_sid is required,' +
'please use /Accounts/{account_sid}/RecentCalls/{call_sid}/logs'});
}
try {
//find back the call in CDR to get timestame of the call
// this allow us limit search in cloudwatch logs
const data = await queryCdrs({
account_sid,
filter: call_sid,
page: 0,
page_size: 50
});
if (!data || data.data.length === 0) {
return res.status(404).send({msg: 'Call not found'});
}
const {
attempted_at, //2025-02-24T13:11:51.969Z
terminated_at, //2025-02-24T13:11:56.153Z
sip_callid
} = data.data[0];
const TIMEBUFFER = 60; //60 seconds
const startTime = new Date(attempted_at).getTime() - TIMEBUFFER * 1000;
const endTime = new Date(terminated_at).getTime() + TIMEBUFFER * 1000;
const client = new CloudWatchLogsClient({ region: aws_region });
let params = {
logGroupName,
startTime,
endTime,
filterPattern: `{ ($.callSid = "${call_sid}") || ($.callId = "${sip_callid}") }`
};
const command = new FilterLogEventsCommand(params);
const response = await client.send(command);
// if response have nextToken, we need to fetch all logs
while (response.nextToken) {
params = {
...params,
nextToken: response.nextToken
};
const command = new FilterLogEventsCommand(params);
const response2 = await client.send(command);
response.events = response.events.concat(response2.events);
response.nextToken = response2.nextToken;
}
let logs = [];
if (response.events && response.events.length > 0) {
logs = response.events.map((e) => e.message);
}
res.status(200).json(logs);
} catch (err) {
logger.error({err}, 'Cannot fetch logs from cloudwatch');
res.status(500).send({msg: err.message});
}
});
router.get('/trace/:trace_id', async(req, res) => {
const {logger} = req.app.locals;
const {trace_id} = req.params;
try {
const obj = await getJaegerTrace(logger, trace_id);
if (!obj) {
logger.info(`/RecentCalls: unable to get spans from jaeger for ${trace_id}`);
return res.sendStatus(404);
}
res.status(200).json(obj.result);
} catch (err) {
logger.error({err}, `/RecentCalls error retrieving jaeger trace ${trace_id}`);
res.sendStatus(500);
}
});
router.get('/:call_sid/record/:year/:month/:day/:format', async(req, res) => {
const {logger} = req.app.locals;
const {call_sid, year, month, day, format} = req.params;
try {
const account_sid = parseAccountSid(req.originalUrl);
const r = await Account.retrieve(account_sid);
if (r.length === 0 || !r[0].bucket_credential) return res.sendStatus(404);
const {bucket_credential} = r[0];
const getOptions = {
...bucket_credential,
key: `${year}/${month}/${day}/${call_sid}.${format || 'mp3'}`
};
let stream;
switch (bucket_credential.vendor) {
case 'aws_s3':
case 's3_compatible':
stream = await getS3Object(logger, getOptions);
break;
case 'google':
stream = await getGoogleStorageObject(logger, getOptions);
break;
case 'azure':
stream = await getAzureStorageObject(logger, getOptions);
break;
default:
logger.error(`There is no handler for fetching record from ${bucket_credential.vendor}`);
return res.sendStatus(500);
}
res.set({
'Content-Type': `audio/${format || 'mp3'}`
});
if (stream) {
stream.pipe(res);
} else {
return res.sendStatus(404);
}
} catch (err) {
logger.error({err}, ` error retrieving recording ${call_sid}`);
res.sendStatus(404);
}
});
router.delete('/:call_sid/record/:year/:month/:day/:format', async(req, res) => {
const {logger} = req.app.locals;
const {call_sid, year, month, day, format} = req.params;
try {
const account_sid = parseAccountSid(req.originalUrl);
const r = await Account.retrieve(account_sid);
if (r.length === 0 || !r[0].bucket_credential) return res.sendStatus(404);
const {bucket_credential} = r[0];
const deleteOptions = {
...bucket_credential,
key: `${year}/${month}/${day}/${call_sid}.${format || 'mp3'}`
};
switch (bucket_credential.vendor) {
case 'aws_s3':
case 's3_compatible':
await deleteS3Object(logger, deleteOptions);
break;
case 'google':
await deleteGoogleStorageObject(logger, deleteOptions);
break;
case 'azure':
await deleteAzureStorageObject(logger, deleteOptions);
break;
default:
logger.error(`There is no handler for deleting record from ${bucket_credential.vendor}`);
return res.sendStatus(500);
}
res.sendStatus(204);
} catch (err) {
logger.error({err}, ` error deleting recording ${call_sid}`);
res.sendStatus(404);
}
});
module.exports = router;

View File

@@ -3,8 +3,7 @@ const debug = require('debug')('jambonz:api-server');
const {DbErrorBadRequest, DbErrorUnprocessableRequest} = require('../../utils/errors');
const {promisePool} = require('../../db');
const {doGithubAuth, doGoogleAuth, doLocalAuth} = require('../../utils/oauth-utils');
const {validateEmail, emailSimpleText} = require('../../utils/email-utils');
const {cacheClient} = require('../../helpers');
const {validateEmail} = require('../../utils/email-utils');
const { v4: uuid } = require('uuid');
const short = require('short-uuid');
const translator = short();
@@ -12,14 +11,12 @@ const jwt = require('jsonwebtoken');
const {setupFreeTrial, createTestCdrs, createTestAlerts} = require('./utils');
const {generateHashedPassword} = require('../../utils/password-utils');
const sysError = require('../error');
const insertUserSql = `INSERT into users
(user_sid, account_sid, name, email, provider, provider_userid, email_validated)
values (?, ?, ?, ?, ?, ?, 1)`;
const insertUserLocalSql = `INSERT into users
(user_sid, account_sid, name, email, email_activation_code, email_validated, provider,
hashed_password, service_provider_sid)
values (?, ?, ?, ?, ?, 0, 'local', ?, ?)`;
(user_sid, account_sid, name, email, email_activation_code, email_validated, provider, hashed_password)
values (?, ?, ?, ?, ?, 0, 'local', ?)`;
const insertAccountSql = `INSERT into accounts
(account_sid, service_provider_sid, name, is_active, webhook_secret, trial_end_date)
values (?, ?, ?, ?, ?, CURDATE() + INTERVAL 21 DAY)`;
@@ -37,19 +34,8 @@ const insertSignupHistorySql = `INSERT into signup_history
(email, name)
values (?, ?)`;
const slackEmail = `Hi there and welcome to jambonz!
We are excited to have you on board. Feel free to join the community on Slack at https://joinslack.jambonz.org,
where you can connect with other jambonz users, ask questions, share your experiences, and learn from others.
Hope to see you there!
Best,
DaveH and the jambonz team`;
const addLocalUser = async(logger, user_sid, account_sid,
name, email, email_activation_code, passwordHash, service_provider_sid) => {
name, email, email_activation_code, passwordHash) => {
const [r] = await promisePool.execute(insertUserLocalSql,
[
user_sid,
@@ -57,8 +43,7 @@ const addLocalUser = async(logger, user_sid, account_sid,
name,
email,
email_activation_code,
passwordHash,
service_provider_sid
passwordHash
]);
debug({r}, 'Result from adding user');
};
@@ -159,7 +144,7 @@ router.post('/', async(req, res) => {
const user = await doGithubAuth(logger, req.body);
logger.info({user}, 'retrieved user details from github');
Object.assign(userProfile, {
name: user.email,
name: user.name,
email: user.email,
email_validated: user.email_validated,
avatar_url: user.avatar_url,
@@ -171,7 +156,7 @@ router.post('/', async(req, res) => {
const user = await doGoogleAuth(logger, req.body);
logger.info({user}, 'retrieved user details from google');
Object.assign(userProfile, {
name: user.email || user.email,
name: user.name || user.email,
email: user.email,
email_validated: user.verified_email,
picture: user.picture,
@@ -184,7 +169,7 @@ router.post('/', async(req, res) => {
logger.info({user}, 'retrieved user details for local provider');
debug({user}, 'retrieved user details for local provider');
Object.assign(userProfile, {
name: user.email,
name: user.name,
email: user.email,
provider: 'local',
email_activation_code: user.email_activation_code
@@ -294,8 +279,7 @@ router.post('/', async(req, res) => {
const passwordHash = await generateHashedPassword(req.body.password);
debug(`hashed password: ${passwordHash}`);
await addLocalUser(logger, userProfile.user_sid, userProfile.account_sid,
userProfile.name, userProfile.email, userProfile.email_activation_code,
passwordHash, req.body.service_provider_sid);
userProfile.name, userProfile.email, userProfile.email_activation_code, passwordHash);
debug('added local user');
}
else {
@@ -308,25 +292,17 @@ router.post('/', async(req, res) => {
const callStatusSid = uuid();
const helloWordSid = uuid();
const dialTimeSid = uuid();
const echoSid = uuid();
/* 4 webhooks */
await promisePool.execute(insertWebookSql,
[callStatusSid, 'https://public-apps.jambonz.cloud/call-status', 'POST']);
await promisePool.execute(insertWebookSql,
[helloWordSid, 'https://public-apps.jambonz.cloud/hello-world', 'POST']);
await promisePool.execute(insertWebookSql,
[dialTimeSid, 'https://public-apps.jambonz.cloud/dial-time', 'POST']);
await promisePool.execute(insertWebookSql,
[echoSid, 'https://public-apps.jambonz.cloud/echo', 'POST']);
/* 3 webhooks */
await promisePool.execute(insertWebookSql, [callStatusSid, 'https://public-apps.jambonz.us/call-status', 'POST']);
await promisePool.execute(insertWebookSql, [helloWordSid, 'https://public-apps.jambonz.us/hello-world', 'POST']);
await promisePool.execute(insertWebookSql, [dialTimeSid, 'https://public-apps.jambonz.us/dial-time', 'POST']);
/* 2 applications */
await promisePool.execute(insertApplicationSql, [uuid(), userProfile.account_sid, 'hello world',
helloWordSid, callStatusSid, 'google', 'en-US', 'en-US-Wavenet-C', 'google', 'en-US']);
await promisePool.execute(insertApplicationSql, [uuid(), userProfile.account_sid, 'dial time clock',
dialTimeSid, callStatusSid, 'google', 'en-US', 'en-US-Wavenet-C', 'google', 'en-US']);
await promisePool.execute(insertApplicationSql, [uuid(), userProfile.account_sid, 'simple echo test',
echoSid, callStatusSid, 'google', 'en-US', 'en-US-Wavenet-C', 'google', 'en-US']);
Object.assign(userProfile, {
pristine: true,
@@ -336,15 +312,6 @@ router.post('/', async(req, res) => {
tutorial_completion: 0,
scope: 'read-write'
});
// send invite to Slack
if (process.env.SEND_SLACK_INVITE_ON_SIGNUP) {
try {
emailSimpleText(logger, userProfile.email, 'Welcome to jambonz!', slackEmail);
} catch (err) {
logger.info({err}, 'Error sending slack invite');
}
}
}
else if (user_sid) {
/* add a new user for existing account */
@@ -359,7 +326,7 @@ router.post('/', async(req, res) => {
await addLocalUser(logger, userProfile.user_sid, userProfile.account_sid,
userProfile.name, userProfile.email, userProfile.email_activation_code,
passwordHash, req.body.service_provider_sid);
passwordHash);
/* note: we deactivate the old user once the new email is validated */
}
@@ -371,38 +338,24 @@ router.post('/', async(req, res) => {
/* deactivate the old/replaced user */
const [r] = await promisePool.execute('DELETE FROM users WHERE user_sid = ?', [user_sid]);
logger.debug({r}, 'register - removed old user');
const redisKey = cacheClient.generateRedisKey('jwt', user_sid, 'v2');
await cacheClient.delete(redisKey);
}
}
const expiresIn = parseInt(process.env.JWT_EXPIRES_IN || 60) * 60 ;
// generate a json web token for this user
const token = jwt.sign({
user_sid: userProfile.user_sid,
account_sid: userProfile.account_sid,
service_provider_sid: req.body.service_provider_sid,
scope: 'account',
email: userProfile.email,
name: userProfile.name
}, process.env.JWT_SECRET, { expiresIn });
}, process.env.JWT_SECRET, { expiresIn: '1h' });
logger.debug({
user_sid: userProfile.user_sid,
account_sid: userProfile.account_sid
}, 'generated jwt');
// Remove activation code from the response data!
delete userProfile.email_activation_code;
res.json({jwt: token, ...userProfile});
/* Store jwt based on user_id after successful login */
await cacheClient.set({
redisKey: cacheClient.generateRedisKey('jwt', userProfile.user_sid, 'v2'),
value: token,
time: expiresIn,
});
} catch (err) {
debug(err, 'Error');
sysError(logger, res, err);

View File

@@ -2,46 +2,25 @@ 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');
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);
decorate(router, Sbc, ['add', 'delete']);
/* list */
router.get('/', async(req, res) => {
const logger = req.app.locals.logger;
try {
let service_provider_sid = req.query.service_provider_sid;
const 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 DbErrorBadRequest('invalid account_sid');
if (0 === r.length) throw new Error('invalid account_sid');
service_provider_sid = r[0].service_provider_sid;
}
if (req.user.hasServiceProviderAuth) {
service_provider_sid = req.user.service_provider_sid;
}
/** 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();
if (!service_provider_sid) throw new DbErrorBadRequest('missing service_provider_sid in query');
*/
const results = await Sbc.retrieveAll(service_provider_sid);
res.status(200).json(results);
} catch (err) {
sysError(logger, res, err);

View File

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

View File

@@ -3,17 +3,10 @@ const router = require('express').Router();
const {DbErrorBadRequest} = require('../../utils/errors');
const {promisePool} = require('../../db');
const {verifyPassword} = require('../../utils/password-utils');
const {cacheClient} = require('../../helpers');
const jwt = require('jsonwebtoken');
const sysError = require('../error');
const retrievePermissionsSql = `
SELECT p.name
FROM permissions p, user_permissions up
WHERE up.permission_sid = p.permission_sid
AND up.user_sid = ?
`;
const validateRequest = (req) => {
const validateRequest = async(req) => {
const {email, password} = req.body || {};
/* check required properties are there */
@@ -59,7 +52,6 @@ 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,
@@ -72,22 +64,11 @@ 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 payload = {
scope: 'account',
permissions,
const token = jwt.sign({
user_sid: userProfile.user_sid,
account_sid: userProfile.account_sid,
service_provider_sid: userProfile.service_provider_sid
};
const token = jwt.sign(payload,
process.env.JWT_SECRET,
{ expiresIn }
);
account_sid: userProfile.account_sid
}, process.env.JWT_SECRET, { expiresIn: '1h' });
logger.debug({
user_sid: userProfile.user_sid,
@@ -95,14 +76,6 @@ router.post('/', async(req, res) => {
}, 'generated jwt');
res.json({jwt: token, ...userProfile});
/* Store jwt based on user_id after successful login */
await cacheClient.set({
redisKey: cacheClient.generateRedisKey('jwt', userProfile.user_sid, 'v2'),
value: token,
time: expiresIn,
});
} catch (err) {
sysError(logger, res, err);
}

View File

@@ -1,56 +1,13 @@
const router = require('express').Router();
const SipGateway = require('../../models/sip-gateway');
const {DbErrorBadRequest, DbErrorForbidden} = require('../../utils/errors');
//const {parseSipGatewaySid} = require('./utils');
const {DbErrorBadRequest, DbErrorUnprocessableRequest} = require('../../utils/errors');
const decorate = require('./decorate');
const sysError = require('../error');
const net = require('net');
const hasWhitespace = (str) => /\s/.test(str);
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.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 {lookupSipGatewayBySid} = req.app.locals;
const {netmask, ipv4, inbound, outbound} = req.body;
const {lookupCarrierBySid, lookupSipGatewayBySid} = req.app.locals;
let voip_carrier_sid;
if (process.env.JAMBONES_ADMIN_CARRIER == 1 && (!req.user.hasScope('service_provider')
&& !req.user.hasScope('admin'))) {
throw new DbErrorBadRequest('insufficient privileges');
}
if (sid) {
const gateway = await lookupSipGatewayBySid(sid);
if (!gateway) throw new DbErrorBadRequest('invalid sip_gateway_sid');
@@ -60,22 +17,13 @@ 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 (netmask &&
process.env.JAMBONZ_MIN_GATEWAY_NETMASK &&
parseInt(netmask) < process.env.JAMBONZ_MIN_GATEWAY_NETMASK) {
throw new DbErrorBadRequest(
`netmask required to have value equal or greater than ${process.env.JAMBONZ_MIN_GATEWAY_NETMASK}`);
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');
}
}
if (hasWhitespace(ipv4)) {
throw new DbErrorBadRequest('Gateway must not contain whitespace');
}
if (inbound && !net.isIPv4(ipv4)) {
throw new DbErrorBadRequest('Inbound gateway must be IPv4 address');
}
if (!inbound && outbound && (netmask && netmask != 32)) {
throw new DbErrorBadRequest('For outbound only gateway netmask can only be 32');
}
await checkUserScope(req, voip_carrier_sid);
};
const preconditions = {
@@ -91,7 +39,6 @@ router.get('/', async(req, res) => {
const logger = req.app.locals.logger;
const voip_carrier_sid = req.query.voip_carrier_sid;
try {
await checkUserScope(req, voip_carrier_sid);
if (!voip_carrier_sid) {
logger.info('GET /SipGateways missing voip_carrier_sid param');
return res.status(400).json({message: 'missing voip_carrier_sid query param'});

View File

@@ -31,7 +31,6 @@ router.post('/:sip_realm', async(req, res) => {
const [sbcs] = await promisePool.query('SELECT ipv4 from sbc_addresses');
if (sbcs.length === 0) throw new Error('no SBC addresses provisioned in the database!');
const ips = sbcs.map((s) => s.ipv4);
const uniqueIps = [...new Set(ips)];
/* retrieve existing dns records */
const [old_recs] = await promisePool.query('SELECT record_id from dns_records WHERE account_sid = ?',
@@ -49,7 +48,7 @@ router.post('/:sip_realm', async(req, res) => {
}
/* add the dns records */
const records = await createDnsRecords(logger, domain, subdomain, uniqueIps);
const records = await createDnsRecords(logger, domain, subdomain, ips);
if (!records) throw new Error(`failure updating dns records for ${sip_realm}`);
const values = records.map((r) => {
return `('${uuid()}', '${account_sid}', '${r.type}', ${r.id})`;

View File

@@ -1,38 +1,11 @@
const router = require('express').Router();
const SmppGateway = require('../../models/smpp-gateway');
const {DbErrorBadRequest, DbErrorForbidden} = require('../../utils/errors');
const {DbErrorBadRequest, DbErrorUnprocessableRequest} = 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 {lookupSmppGatewayBySid} = req.app.locals;
const {lookupCarrierBySid, lookupSmppGatewayBySid} = req.app.locals;
let voip_carrier_sid;
if (sid) {
@@ -44,8 +17,13 @@ const validate = async(req, sid) => {
voip_carrier_sid = req.body.voip_carrier_sid;
if (!voip_carrier_sid) throw new DbErrorBadRequest('missing voip_carrier_sid');
}
await checkUserScope(req, 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');
}
}
};
const preconditions = {
@@ -61,7 +39,6 @@ router.get('/', async(req, res) => {
const logger = req.app.locals.logger;
const voip_carrier_sid = req.query.voip_carrier_sid;
try {
await checkUserScope(req, voip_carrier_sid);
if (!voip_carrier_sid) {
logger.info('GET /SmppGateways missing voip_carrier_sid param');
return res.status(400).json({message: 'missing voip_carrier_sid query param'});

View File

@@ -1,4 +1,5 @@
const router = require('express').Router();
const request = require('request');
const getProvider = require('../../utils/sms-provider');
const { v4: uuidv4 } = require('uuid');
const sysError = require('../error');
@@ -13,14 +14,20 @@ const getFsUrl = async(logger, retrieveSet, setName, provider) => {
logger.info('No available feature servers to handle createCall API request');
return ;
}
const f = fs[idx++ % fs.length];
logger.info({fs}, `feature servers available for createCall API request, selecting ${f}`);
return `${f}/v1/messaging/${provider}`;
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}`;
} 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);
@@ -37,7 +44,7 @@ router.post('/:provider', async(req, res) => {
lookupAppByPhoneNumber,
logger
} = req.app.locals;
const setName = `${process.env.JAMBONES_CLUSTER_ID || 'default'}:fs-service-url`;
const setName = `${process.env.JAMBONES_CLUSTER_ID || 'default'}:active-fs`;
logger.debug({path: req.path, body: req.body}, 'incomingSMS from carrier');
// search for provider module
@@ -121,19 +128,25 @@ router.post('/:provider', async(req, res) => {
logger.info({payload, url: serviceUrl}, `sending incomingSms API request to FS at ${serviceUrl}`);
const response = await fetch(serviceUrl, {
request({
url: serviceUrl,
method: 'POST',
body: JSON.stringify(payload),
headers: {'Content-Type': 'application/json'},
});
if (!response.ok) {
logger.error({response}, `Error sending incomingSms POST to ${serviceUrl}`);
json: true,
body: payload,
},
async(err, response, body) => {
if (err) {
logger.error(err, `Error sending incomingSms POST to ${serviceUrl}`);
return res.sendStatus(500);
}
if (200 === response.statusCode) {
// success
logger.info({body}, 'sending response to provider for incomingSMS');
return doSendResponse(res, respondFn, body);
}
logger.error({statusCode: response.statusCode}, `Non-success response returned by incomingSms ${serviceUrl}`);
return res.sendStatus(500);
}
const body = await response.json();
logger.info({body}, 'sending response to provider for incomingSMS');
return doSendResponse(res, respondFn, body);
});
} catch (err) {
sysError(logger, res, err);
}

File diff suppressed because it is too large Load Diff

View File

@@ -17,7 +17,6 @@ const {
} = require('../../utils/stripe-utils');
const {setupFreeTrial} = require('./utils');
const sysError = require('../error');
const Product = require('../../models/product');
const actions = [
'upgrade-to-paid',
'downgrade-to-free',
@@ -25,8 +24,6 @@ const actions = [
'update-quantities'
];
const MIN_VOICE_CALL_SESSION_QUANTITY = 5;
const handleError = async(logger, method, res, err) => {
if ('StatusError' === err.name) {
const text = await err.text();
@@ -149,22 +146,6 @@ const upgradeToPaidPlan = async(req, res) => {
await handleSubscriptionOutcome(req, res, subscription);
};
const validateProductQuantities = async(products) => {
const availableProducts = await Product.retrieveAll();
const voiceCallSessionsProductSid =
availableProducts.find((p) => p.category === 'voice_call_session')?.product_sid;
if (voiceCallSessionsProductSid) {
const invalid = products.find((p) => {
return (p.product_sid === voiceCallSessionsProductSid &&
(typeof p.quantity !== 'number' || p.quantity < MIN_VOICE_CALL_SESSION_QUANTITY));
});
if (invalid) {
throw new DbErrorBadRequest('invalid voice call session value, minimum is ' +
MIN_VOICE_CALL_SESSION_QUANTITY);
}
}
};
const downgradeToFreePlan = async(req, res) => {
const logger = req.app.locals.logger;
const {account_sid} = req.user;
@@ -248,7 +229,7 @@ const updateQuantities = async(req, res) => {
const obj = {
quantity: product.quantity,
};
return Object.assign(obj, existingItem ? {id: existingItem.id} : {price: product.price_id});
return Object.assign(obj, existingItem ? {id: existingItem.id} : {price_id: product.price_id});
});
if (dry_run) {
@@ -310,11 +291,11 @@ router.post('/', async(req, res) => {
if ('update-payment-method' === action && typeof payment_method_id !== 'string') {
throw new DbErrorBadRequest('missing payment_method_id');
}
if (['update-quantities', 'upgrade-to-paid'].includes(action)) {
if ((!Array.isArray(products) || 0 === products.length)) {
throw new DbErrorBadRequest('missing products');
}
await validateProductQuantities(products);
if ('upgrade-to-paid' === action && (!Array.isArray(products) || 0 === products.length)) {
throw new DbErrorBadRequest('missing products');
}
if ('update-quantities' === action && (!Array.isArray(products) || 0 === products.length)) {
throw new DbErrorBadRequest('missing products');
}
switch (action) {

View File

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

View File

@@ -1,137 +0,0 @@
const router = require('express').Router();
const {
parseAccountSid
} = require('./utils');
const SpeechCredential = require('../../models/speech-credential');
const fs = require('fs');
const { v4: uuidv4 } = require('uuid');
const {DbErrorBadRequest} = require('../../utils/errors');
const Account = require('../../models/account');
const sysError = require('../error');
const { getSpeechCredential, decryptCredential } = require('../../utils/speech-utils');
const PCMToMP3Encoder = require('../../record/encoder');
const { pipeline } = require('stream');
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});
});
router.post('/Synthesize', async(req, res) => {
const {logger, synthAudio} = req.app.locals;
try {
const accountSid = parseAccountSid(req);
const body = req.body;
const encodingMp3 = req.body.encodingMp3 || false;
if (!body.speech_credential_sid || !body.text || !body.language || !body.voice) {
throw new DbErrorBadRequest('speech_credential_sid, text, language, voice are all required');
}
const result = await Account.retrieve(accountSid);
if (!result || result.length === 0 || !result[0].is_active) {
throw new DbErrorBadRequest(`Account not found for sid ${accountSid}`);
}
const credentials = await SpeechCredential.retrieve(body.speech_credential_sid);
if (!credentials || credentials.length === 0) {
throw new
DbErrorBadRequest(`There is no available speech credential for ${body.speech_credential_sid}`);
}
const {credential, ...obj} = credentials[0];
decryptCredential(obj, credential, logger, false);
const cred = getSpeechCredential(obj, logger);
const { text, language, engine = 'standard' } = body;
const salt = uuidv4();
/* parse Nuance voices into name and model */
let voice = body.voice;
let model;
if (cred.vendor === 'nuance' && voice) {
const arr = /([A-Za-z-]*)\s+-\s+(enhanced|standard)/.exec(voice);
if (arr) {
voice = arr[1];
model = arr[2];
}
} else if (cred.vendor === 'deepgram') {
model = voice;
}
const stats = {
histogram: () => {},
increment: () => {},
};
const { filePath } = await synthAudio(stats, {
account_sid: accountSid,
text,
vendor: cred.vendor,
language,
voice,
engine,
model,
salt,
credentials: cred,
disableTtsCache: false,
disableTtsStreaming: true
});
let contentType = 'audio/mpeg';
let readStream = fs.createReadStream(filePath);
if (['nuance', 'nvidia'].includes(cred.vendor) ||
(
process.env.JAMBONES_TTS_TRIM_SILENCE &&
['microsoft', 'azure'].includes(cred.vendor)
)
) {
if (encodingMp3) {
readStream = readStream
.pipe(new PCMToMP3Encoder({
channels: 1,
sampleRate: 8000,
bitRate: 128
}, logger));
} else {
contentType = 'application/octet-stream';
}
}
res.writeHead(200, {
'Content-Type': contentType,
});
pipeline(readStream, res, (err) => {
if (err) {
logger.error('ttscache/Synthesize failed:', err);
if (!res.headersSent) {
res.status(500).end('Server error');
}
}
fs.unlink(filePath, (unlinkErr) => {
if (unlinkErr) throw unlinkErr;
logger.info(`${filePath} was deleted`);
});
});
} catch (err) {
sysError(logger, res, err);
}
});
module.exports = router;

View File

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

View File

@@ -1,17 +1,17 @@
const { v4: uuid, validate } = require('uuid');
const URL = require('url').URL;
const isValidHostname = require('is-valid-hostname');
const { v4: uuid } = require('uuid');
const bent = require('bent');
const Account = require('../../models/account');
const {promisePool} = require('../../db');
const {cancelSubscription, detachPaymentMethod} = require('../../utils/stripe-utils');
const freePlans = require('../../utils/free_plans');
const { BadRequestError, DbErrorBadRequest } = require('../../utils/errors');
const insertAccountSubscriptionSql = `INSERT INTO account_subscriptions
(account_subscription_sid, account_sid)
values (?, ?)`;
const replaceOldSubscriptionSql = `UPDATE account_subscriptions
SET effective_end_date = CURRENT_TIMESTAMP, change_reason = ?
WHERE account_subscription_sid = ?`;
//const request = require('request');
//require('request-debug')(request);
const setupFreeTrial = async(logger, account_sid, isReturningUser) => {
const sid = uuid();
@@ -137,183 +137,38 @@ const createTestAlerts = async(writeAlerts, AlertType, account_sid) => {
};
const validateSid = (model, req) => {
const arr = new RegExp(`${model}\/([^\/]*)`).exec(req.originalUrl);
if (arr) {
const sid = arr[1];
const sid_validation = validate(sid);
if (!sid_validation) {
throw new BadRequestError(`invalid ${model}Sid format`);
}
return arr[1];
}
return;
};
const parseServiceProviderSid = (req) => {
try {
return validateSid('ServiceProviders', req);
} catch (error) {
throw error;
}
const arr = /ServiceProviders\/([^\/]*)/.exec(req.originalUrl);
if (arr) return arr[1];
};
const parseAccountSid = (req) => {
try {
return validateSid('Accounts', req);
} catch (error) {
throw error;
}
const arr = /Accounts\/([^\/]*)/.exec(req.originalUrl);
if (arr) return arr[1];
};
const parseApplicationSid = (req) => {
try {
return validateSid('Applications', req);
} catch (error) {
throw error;
}
};
const parseCallSid = (req) => {
try {
return validateSid('Calls', req);
} catch (error) {
throw error;
}
};
const parsePhoneNumberSid = (req) => {
try {
return validateSid('PhoneNumbers', req);
} catch (error) {
throw error;
}
};
const parseSpeechCredentialSid = (req) => {
try {
return validateSid('SpeechCredentials', req);
} catch (error) {
throw error;
}
};
const parseVoipCarrierSid = (req) => {
try {
return validateSid('VoipCarriers', req);
} catch (error) {
throw error;
}
};
const parseWebhookSid = (req) => {
try {
return validateSid('Webhooks', req);
} catch (error) {
throw error;
}
};
const parseSipGatewaySid = (req) => {
try {
return validateSid('SipGateways', req);
} catch (error) {
throw error;
}
};
const parseUserSid = (req) => {
try {
return validateSid('Users', req);
} catch (error) {
throw error;
}
};
const parseLcrSid = (req) => {
try {
return validateSid('Lcrs', req);
} catch (error) {
throw error;
}
};
const hasAccountPermissions = async(req, res, next) => {
try {
if (req.user.hasScope('admin')) {
return next();
}
if (req.user.hasScope('service_provider')) {
const service_provider_sid = parseServiceProviderSid(req);
const account_sid = parseAccountSid(req);
if (service_provider_sid) {
if (service_provider_sid === req.user.service_provider_sid) {
return next();
}
}
if (account_sid) {
const [r] = await Account.retrieve(account_sid);
if (r && r.service_provider_sid === req.user.service_provider_sid) {
return next();
}
}
}
if (req.user.hasScope('account')) {
const account_sid = parseAccountSid(req);
const service_provider_sid = parseServiceProviderSid(req);
const [r] = await Account.retrieve(account_sid);
if (account_sid) {
if (r && r.account_sid === req.user.account_sid) {
return next();
}
}
if (service_provider_sid) {
if (r && r.service_provider_sid === req.user.service_provider_sid) {
return next();
}
}
}
res.status(403).json({
status: 'fail',
message: 'insufficient privileges'
});
} catch (error) {
// return 400 on errors
res.status(400).json({
status: 'fail',
message: error.message
});
const hasAccountPermissions = (req, res, next) => {
if (req.user.hasScope('admin')) return next();
if (req.user.hasScope('account')) {
const account_sid = parseAccountSid(req);
if (account_sid === req.user.account_sid) return next();
}
res.status(403).json({
status: 'fail',
message: 'insufficient privileges'
});
};
const hasServiceProviderPermissions = (req, res, next) => {
try {
if (req.user.hasScope('admin')) {
return next();
}
if (req.user.hasScope('service_provider')) {
const service_provider_sid = parseServiceProviderSid(req);
if (service_provider_sid === req.user.service_provider_sid) {
return next();
}
}
res.status(403).json({
status: 'fail',
message: 'insufficient privileges'
});
} catch (error) {
throw error;
if (req.user.hasScope('admin')) return next();
if (req.user.hasScope('service_provider')) {
const service_provider_sid = parseServiceProviderSid(req);
if (service_provider_sid === req.user.service_provider_sid) return next();
}
res.status(403).json({
status: 'fail',
message: 'insufficient privileges'
});
};
const checkLimits = async(req, res, next) => {
@@ -373,44 +228,35 @@ const checkLimits = async(req, res, next) => {
};
const getSubspaceJWT = async(id, secret) => {
const response = await fetch('https://id.subspace.com/oauth/token', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
const postJwt = bent('https://id.subspace.com', 'POST', 'json', 200);
const jwt = await postJwt('/oauth/token',
{
client_id: id,
client_secret: secret,
audience: 'https://api.subspace.com/',
grant_type: 'client_credentials',
})
});
if (!response.ok) {
throw new Error(`Failed to get JWT: ${response.status} ${response.statusText}`);
}
const jwt = await response.json();
}
);
return jwt.access_token;
};
const enableSubspace = async(opts) => {
const {subspace_client_id, subspace_client_secret, destination} = opts;
const accessToken = await getSubspaceJWT(subspace_client_id, subspace_client_secret);
const response = await fetch('https://api.subspace.com/v1/sipteleport', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${accessToken}`
},
body: JSON.stringify({
const postTeleport = bent('https://api.subspace.com', 'POST', 'json', 200);
const teleport = await postTeleport('/v1/sipteleport',
{
name: 'Jambonz',
destination,
status: 'ENABLED'
})
});
if (!response.ok) {
throw new Error(`Failed to enable teleport: ${response.status} ${response.statusText}`);
}
const teleport = await response.json();
},
{
'Content-Type': 'application/json',
Authorization: `Bearer ${accessToken}`
}
);
return teleport;
};
@@ -418,101 +264,24 @@ const disableSubspace = async(opts) => {
const {subspace_client_id, subspace_client_secret, subspace_sip_teleport_id} = opts;
const accessToken = await getSubspaceJWT(subspace_client_id, subspace_client_secret);
const relativeUrl = `/v1/sipteleport/${subspace_sip_teleport_id}`;
const response = await fetch(`https://api.subspace.com${relativeUrl}`, {
method: 'DELETE',
headers: {
const deleteTeleport = bent('https://api.subspace.com', 'DELETE', 'json', 200);
await deleteTeleport(relativeUrl, {},
{
Authorization: `Bearer ${accessToken}`
}
});
if (!response.ok) {
throw new Error(`Failed to delete teleport: ${response.status} ${response.statusText}`);
}
};
const validatePasswordSettings = async(password) => {
const sql = 'SELECT * from password_settings';
const [rows] = await promisePool.execute(sql);
const specialChars = /[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]+/;
const numbers = /[0-9]+/;
if (rows.length === 0) {
if (password.length < 8 || password.length > 20) {
throw new DbErrorBadRequest('password length must be between 8 and 20');
}
} else {
if (rows[0].min_password_length && password.length < rows[0].min_password_length) {
throw new DbErrorBadRequest(`password must be at least ${rows[0].min_password_length} characters long`);
}
if (rows[0].require_digit === 1 && !numbers.test(password)) {
throw new DbErrorBadRequest('password must contain at least one digit');
}
if (rows[0].require_special_character === 1 && !specialChars.test(password)) {
throw new DbErrorBadRequest('password must contain at least one special character');
}
}
);
return;
};
function hasValue(data) {
if (typeof data === 'string') {
return data && data.length > 0;
} else if (Array.isArray(data)) {
return data && data.length > 0;
} else if (typeof data === 'object') {
return data && Object.keys(data).length > 0;
} else if (typeof data === 'number') {
return data !== null;
} else if (typeof data === 'boolean') {
return data !== null;
} else {
return false;
}
}
const isInvalidUrl = async(s) => {
const protocols = ['https:', 'http:', 'ws:', 'wss:'];
try {
const url = new URL(s);
if (s.length != s.trim().length) {
return 'URL contains leading/trailing whitespace';
}
else if (!isValidHostname(url.hostname)) {
return `URL has invalid hostname ${url.hostname}`;
}
else if (!protocols.includes(url.protocol)) {
return `URL has missing or invalid protocol ${url.protocol}`;
}
else {
return false;
}
} catch (err) {
return 'URL is invalid';
}
};
module.exports = {
setupFreeTrial,
createTestCdrs,
createTestAlerts,
parseAccountSid,
parseApplicationSid,
parseCallSid,
parsePhoneNumberSid,
parseServiceProviderSid,
parseSpeechCredentialSid,
parseVoipCarrierSid,
parseWebhookSid,
parseSipGatewaySid,
parseUserSid,
parseLcrSid,
hasAccountPermissions,
hasServiceProviderPermissions,
checkLimits,
enableSubspace,
disableSubspace,
validatePasswordSettings,
hasValue,
isInvalidUrl
disableSubspace
};

View File

@@ -4,16 +4,10 @@ const VoipCarrier = require('../../models/voip-carrier');
const {promisePool} = require('../../db');
const decorate = require('./decorate');
const sysError = require('../error');
const { parseVoipCarrierSid } = require('./utils');
const validate = async(req) => {
const {lookupAppBySid, lookupAccountBySid} = req.app.locals;
if (process.env.JAMBONES_ADMIN_CARRIER == 1 && (!req.user.hasScope('service_provider')
&& !req.user.hasScope('admin'))) {
throw new DbErrorBadRequest('insufficient privileges');
}
/* account level user can only act on carriers associated to his/her account */
if (req.user.hasAccountAuth) {
req.body.account_sid = req.user.account_sid;
@@ -50,12 +44,6 @@ const validateUpdate = async(req, sid) => {
const validateDelete = async(req, sid) => {
const {lookupCarrierBySid} = req.app.locals;
if (process.env.JAMBONES_ADMIN_CARRIER == 1 && (!req.user.hasScope('service_provider')
&& !req.user.hasScope('admin'))) {
throw new DbErrorBadRequest('insufficient privileges');
}
if (req.user.hasAccountAuth) {
/* can only update carriers for the user's account */
const carrier = await lookupCarrierBySid(sid);
@@ -84,36 +72,9 @@ decorate(router, VoipCarrier, ['add', 'update', 'delete'], preconditions);
/* list */
router.get('/', async(req, res) => {
const logger = req.app.locals.logger;
const {account_sid: query_account_sid, name, page, page_size} = req.query || {};
const isPaginationRequest = page !== null && page !== undefined;
let service_provider_sid = null, account_sid = query_account_sid;
if (req.user.hasAccountAuth) {
account_sid = req.user.account_sid;
} else if (req.user.hasServiceProviderAuth) {
service_provider_sid = req.user.service_provider_sid;
}
try {
let total = 0;
if (isPaginationRequest) {
total = await VoipCarrier.countAll({service_provider_sid, account_sid, name});
}
const carriers = await VoipCarrier.retrieveByCriteria({
service_provider_sid,
account_sid,
name,
page,
page_size,
});
const body = isPaginationRequest ? {
total,
page: Number(page),
page_size: Number(page_size),
data: carriers,
} : carriers;
res.status(200).json(body);
const results = await VoipCarrier.retrieveAll(req.user.hasAccountAuth ? req.user.account_sid : null);
res.status(200).json(results);
} catch (err) {
sysError(logger, res, err);
}
@@ -123,24 +84,9 @@ router.get('/', async(req, res) => {
router.get('/:sid', async(req, res) => {
const logger = req.app.locals.logger;
try {
const sid = parseVoipCarrierSid(req);
const account_sid = req.user.hasAccountAuth ? req.user.account_sid : null;
const results = await VoipCarrier.retrieve(sid, account_sid);
const results = await VoipCarrier.retrieve(req.params.sid, account_sid);
if (results.length === 0) return res.status(404).end();
const ret = results[0];
ret.register_status = JSON.parse(ret.register_status || '{}');
if (req.user.hasServiceProviderAuth && results.length === 1) {
if (results.length === 1 && results[0].service_provider_sid !== req.user.service_provider_sid) {
throw new DbErrorBadRequest('insufficient privileges');
}
}
if (req.user.hasAccountAuth && results.length === 1) {
if (results.length === 1 && results[0].account_sid !== req.user.account_sid) {
throw new DbErrorBadRequest('insufficient privileges');
}
}
return res.status(200).json(results[0]);
}
catch (err) {

View File

@@ -2,37 +2,15 @@ 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);
const results = await Webhook.retrieve(req.params.sid);
if (results.length === 0) return res.status(404).end();
if (req.user.hasAccountAuth) {
/* can only update carriers for the user's account */
if (results[0].account_sid !== req.user.account_sid) {
throw new DbErrorForbidden('insufficient privileges');
}
}
if (req.user.hasServiceProviderAuth) {
const [r] = await promisePool.execute(
'SELECT service_provider_sid from accounts WHERE account_sid = ?', [results[0].account_sid]
);
if (r.length === 1 && r[0].service_provider_sid === req.user.service_provider_sid) {
return;
}
throw new DbErrorForbidden('insufficient permissions');
}
return res.status(200).json(results[0]);
}
catch (err) {

View File

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

View File

@@ -13,10 +13,6 @@ const handleInvoicePaymentSucceeded = async(logger, obj) => {
const sub = await retrieveSubscription(logger, subscription);
if ('active' === sub.status) {
const {account_sid} = sub.metadata;
if (!account_sid) {
logger.info({subscription}, `handleInvoicePaymentSucceeded: received subscription ${sub.id} without account_sid`);
return;
}
if (await Account.activateSubscription(logger, account_sid, sub.id,
'subscription_create' === obj.billing_reason ? 'upgrade to paid plan' : 'change plan details')) {
logger.info(`handleInvoicePaymentSucceeded: activated subscription for account ${account_sid}`);
@@ -39,10 +35,6 @@ const handleInvoicePaymentFailed = async(logger, obj) => {
const sub = await retrieveSubscription(logger, subscription);
logger.debug({obj}, `payment for ${obj.billing_reason} failed, subscription status is ${sub.status}`);
const {account_sid} = sub.metadata;
if (!account_sid) {
logger.info({subscription}, `handleInvoicePaymentFailed: received subscription ${sub.id} without account_sid`);
return;
}
if (await Account.deactivateSubscription(logger, account_sid, 'payment failed')) {
logger.info(`handleInvoicePaymentFailed: deactivated subscription for account ${account_sid}`);
}
@@ -69,7 +61,8 @@ router.post('/', express.raw({type: 'application/json'}), async(req, res) => {
}
/* process event */
if (evt?.type?.startsWith('invoice.')) handleInvoiceEvents(logger, evt);
logger.info(`received webhook: ${evt.type}`);
if (evt.type.startsWith('invoice.')) handleInvoiceEvents(logger, evt);
else {
logger.debug(evt, 'unhandled stripe webook');
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,50 +0,0 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"additionalProperties": false,
"minProperties": 1,
"patternProperties": {
"^[a-zA-Z0-9_]+$": {
"type": "object",
"properties": {
"description": {
"type": "string"
},
"type": {
"type": "string",
"enum": [
"string",
"number",
"boolean"
]
},
"required": {
"type": "boolean"
},
"default": {
"oneOf": [
{
"type": "string"
},
{
"type": "number"
},
{
"type": "boolean"
}
]
},
"enum": {
"type": "array"
},
"obscure": {
"type": "boolean"
}
},
"required": [
"type",
"description"
]
}
}
}

View File

@@ -1,78 +0,0 @@
const Ajv = require('ajv');
const assert = require('assert');
const ajv = new Ajv();
const schemaSchema = require('./appenv_schemaSchema.json');
const validateAppEnvSchema = (schema) => {
const validate = ajv.compile(schemaSchema);
return validate(schema);
};
//Currently this request is not signed with the webhook secret as it is outside an account
const fetchAppEnvSchema = async(logger, url) => {
// Translate WebSocket URLs to HTTP equivalents (case-insensitive)
let fetchUrl = url;
if (url.toLowerCase().startsWith('ws://')) {
fetchUrl = 'http://' + url.substring(5);
} else if (url.toLowerCase().startsWith('wss://')) {
fetchUrl = 'https://' + url.substring(6);
}
try {
const response = await fetch(fetchUrl, {
method: 'OPTIONS',
headers: {
Accept: 'application/json'
}
});
if (!response.ok) {
logger.info(`Failure to fetch app env schema ${response.status} ${response.statusText}`);
return false;
}
const schema = await response.json();
return schema;
}
catch (e) {
logger.info(`Failure to fetch app env schema ${e}`);
return false;
}
};
const validateAppEnvData = async(schema, data) => {
const schemaKeys = Object.keys(schema);
const dataKeys = Object.keys(data);
let errorMsg = false;
// Check for required keys
schemaKeys.forEach((k) => {
if (schema[k].required) {
if (!dataKeys.includes(k)) {
errorMsg = `Missing required value env_vars.${k}`;
console.log(errorMsg);
}
}
});
//Validate the values
dataKeys.forEach((k) => {
if (schemaKeys.includes(k)) {
try {
// Check value is correct type
assert(typeof data[k] == schema[k].type);
// if enum check value is valid
if (schema[k].enum) {
assert(schema[k].enum.includes(data[k]));
}
} catch (error) {
errorMsg = `Invalid value/type for env_vars.${k}`;
}
}
});
return errorMsg;
};
module.exports = {
validateAppEnvSchema,
fetchAppEnvSchema,
validateAppEnvData
};

View File

@@ -1,5 +1,6 @@
if (!process.env.JAMBONES_HOSTING) return;
const bent = require('bent');
const crypto = require('crypto');
const assert = require('assert');
const domains = new Map();
@@ -25,20 +26,17 @@ const createAuthHeaders = () => {
const getDnsDomainId = async(logger, name) => {
checkAsserts();
const headers = createAuthHeaders();
const response = await fetch(`${process.env.DME_BASE_URL}/dns/managed`, {
method: 'GET',
headers
});
if (!response.ok) {
logger.error({response}, 'Error retrieving domains');
return;
}
const result = await response.json();
debug(result, 'getDnsDomainId: all domains');
if (Array.isArray(result.data)) {
const domain = result.data.find((o) => o.name === name);
if (domain) return domain.id;
debug(`getDnsDomainId: failed to find domain ${name}`);
const get = bent(process.env.DME_BASE_URL, 'GET', 'json', headers);
try {
const result = await get('/dns/managed');
debug(result, 'getDnsDomainId: all domains');
if (Array.isArray(result.data)) {
const domain = result.data.find((o) => o.name === name);
if (domain) return domain.id;
debug(`getDnsDomainId: failed to find domain ${name}`);
}
} catch (err) {
logger.error({err}, 'Error retrieving domains');
}
};
@@ -82,20 +80,16 @@ const createDnsRecords = async(logger, domain, name, value, ttl = 3600) => {
];
const headers = createAuthHeaders();
const records = [...a_records, ...srv_records];
const response = await fetch(`${process.env.DME_BASE_URL}/dns/managed/${domainId}/records/createMulti`, {
method: 'POST',
headers,
body: JSON.stringify(records)
});
if (!response.ok) {
logger.error({response}, 'Error creating records');
return;
}
const result = await response.json();
logger.debug({result}, 'createDnsRecords: created records');
if (201 === response.status) {
return result;
const post = bent(process.env.DME_BASE_URL, 'POST', 201, 400, headers);
logger.debug({records}, 'Attemting to create dns records');
const res = await post(`/dns/managed/${domainId}/records/createMulti`,
[...a_records, ...srv_records]);
if (201 === res.statusCode) {
const str = await res.text();
return JSON.parse(str);
}
logger.error({res}, 'Error creating records');
} catch (err) {
logger.error({err}, 'Error retrieving domains');
}
@@ -104,6 +98,7 @@ const createDnsRecords = async(logger, domain, name, value, ttl = 3600) => {
const deleteDnsRecords = async(logger, domain, recIds) => {
checkAsserts();
const headers = createAuthHeaders();
const del = bent(process.env.DME_BASE_URL, 'DELETE', 200, headers);
try {
if (!domains.has(domain)) {
const domainId = await getDnsDomainId(logger, domain);
@@ -112,10 +107,7 @@ const deleteDnsRecords = async(logger, domain, recIds) => {
}
const domainId = domains.get(domain);
const url = `/dns/managed/${domainId}/records?${recIds.map((r) => `ids=${r}`).join('&')}`;
await fetch(`${process.env.DME_BASE_URL}${url}`, {
method: 'DELETE',
headers
});
await del(url);
return true;
} catch (err) {
console.error(err);

View File

@@ -8,68 +8,23 @@ const validateEmail = (email) => {
};
const emailSimpleText = async(logger, to, subject, text) => {
const from = 'jambonz Support <support@jambonz.org>';
if (process.env.CUSTOM_EMAIL_VENDOR_URL) {
await sendEmailByCustomVendor(logger, from, to, subject, text);
} else {
await sendEmailByMailgun(logger, from, to, subject, text);
}
};
const sendEmailByCustomVendor = async(logger, from, to, subject, text) => {
const response = await fetch(process.env.CUSTOM_EMAIL_VENDOR_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...((process.env.CUSTOM_EMAIL_VENDOR_USERNAME && process.env.CUSTOM_EMAIL_VENDOR_PASSWORD) &&
({
'Authorization':`Basic ${Buffer.from(
`${process.env.CUSTOM_EMAIL_VENDOR_USERNAME}:${process.env.CUSTOM_EMAIL_VENDOR_PASSWORD}`
).toString('base64')}`
}))
},
body: JSON.stringify({
from,
to,
subject,
text
})
const mg = mailgun.client({
username: 'api',
key: process.env.MAILGUN_API_KEY
});
if (!response.ok) {
logger.error({response}, 'Error sending email to custom vendor');
return;
}
const res = await response.json();
logger.debug({
res
}, 'sent email to custom vendor.');
};
const sendEmailByMailgun = async(logger, from, to, subject, text) => {
if (!process.env.MAILGUN_API_KEY) throw new Error('MAILGUN_API_KEY env variable is not defined!');
if (!process.env.MAILGUN_DOMAIN) throw new Error('MAILGUN_DOMAIN env variable is not defined!');
const mg = mailgun.client({
username: 'api',
key: process.env.MAILGUN_API_KEY,
...(process.env.MAILGUN_URL && {url: process.env.MAILGUN_URL})
});
try {
const res = await mg.messages.create(process.env.MAILGUN_DOMAIN, {
from,
from: 'jambonz Support <support@jambonz.org>',
to,
subject,
text
});
logger.debug({
res
}, 'sent email');
logger.debug({res}, 'sent email');
} catch (err) {
logger.info({
err
}, 'Error sending email From mailgun');
logger.info({err}, 'Error sending email');
}
};

View File

@@ -2,9 +2,9 @@ const crypto = require('crypto');
const algorithm = process.env.LEGACY_CRYPTO ? 'aes-256-ctr' : 'aes-256-cbc';
const iv = crypto.randomBytes(16);
const secretKey = crypto.createHash('sha256')
.update(process.env.ENCRYPTION_SECRET || process.env.JWT_SECRET)
.update(String(process.env.JWT_SECRET))
.digest('base64')
.substring(0, 32);
.substr(0, 32);
const encrypt = (text) => {
const cipher = crypto.createCipheriv(algorithm, secretKey, iv);
@@ -17,103 +17,13 @@ const encrypt = (text) => {
};
const decrypt = (data) => {
try {
const hash = JSON.parse(data);
const decipher = crypto.createDecipheriv(algorithm, secretKey, Buffer.from(hash.iv, 'hex'));
const decrpyted = Buffer.concat([decipher.update(Buffer.from(hash.content, 'hex')), decipher.final()]);
return decrpyted.toString();
} catch (error) {
console.error('Error while decrypting data', error);
return '{}';
}
const hash = JSON.parse(data);
const decipher = crypto.createDecipheriv(algorithm, secretKey, Buffer.from(hash.iv, 'hex'));
const decrpyted = Buffer.concat([decipher.update(Buffer.from(hash.content, 'hex')), decipher.final()]);
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)}`;
};
function isObscureKey(bucketCredentials) {
if (!bucketCredentials) {
return false;
}
try {
const {
vendor,
secret_access_key = '',
service_key = '',
connection_string = ''
} = bucketCredentials || {};
// Pattern matches: 4-6 any characters followed by one or more X's
const pattern = /^.{4,6}X+$/;
switch (vendor) {
case 'aws_s3':
case 's3_compatible':
return pattern.test(secret_access_key);
case 'azure':
return pattern.test(connection_string);
case 'google': {
let {private_key} = JSON.parse(service_key);
const key_header = '-----BEGIN PRIVATE KEY-----\n';
private_key = private_key.slice(key_header.length, private_key.length);
return pattern.test(private_key || '');
}
}
return false;
} catch (error) {
console.log('Error in isObscureKey', error);
return false;
}
}
/**
* obscure sensitive data in bucket credentials
* an obscured key contains of 6 'spoiled' characters of the key followed by 'X' characters
* '123456XXXXXXXXXXXXXXXXXXXXXXXX'
* @param {*} obj
* @returns
*/
function obscureBucketCredentialsSensitiveData(obj) {
if (!obj) return obj;
const {vendor, service_key, connection_string, secret_access_key} = obj;
switch (vendor) {
case 'aws_s3':
case 's3_compatible':
obj.secret_access_key = obscureKey(secret_access_key);
break;
case 'google':
const o = JSON.parse(service_key);
let private_key = o.private_key;
if (!isObscureKey(obj)) {
const key_header = '-----BEGIN PRIVATE KEY-----\n';
private_key = o.private_key.slice(key_header.length, o.private_key.length);
private_key = `${key_header}${obscureKey(private_key)}`;
}
const obscured = {
...o,
private_key
};
obj.service_key = JSON.stringify(obscured);
break;
case 'azure':
obj.connection_string = obscureKey(connection_string);
break;
}
return obj;
}
module.exports = {
encrypt,
decrypt,
obscureKey,
isObscureKey,
obscureBucketCredentialsSensitiveData,
decrypt
};

View File

@@ -1,9 +1,3 @@
class BadRequestError extends Error {
constructor(msg) {
super(msg);
}
}
class DbError extends Error {
constructor(msg) {
super(msg);
@@ -27,17 +21,10 @@ class DbErrorForbidden extends DbError {
super(msg);
}
}
class UserPermissionError extends Error {
constructor(msg) {
super(msg);
}
}
module.exports = {
BadRequestError,
DbError,
DbErrorBadRequest,
DbErrorUnprocessableRequest,
DbErrorForbidden,
UserPermissionError
DbErrorForbidden
};

View File

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

View File

@@ -1,11 +1,15 @@
const debug = require('debug')('jambonz:api-server');
const bent = require('bent');
const basicAuth = (apiKey) => {
const header = `Bearer ${apiKey}`;
return {Authorization: header};
};
const { Readable } = require('stream');
const postJSON = bent(process.env.HOMER_BASE_URL || 'http://127.0.0.1', 'POST', 'json', 200, 201);
const postPcap = bent(process.env.HOMER_BASE_URL || 'http://127.0.0.1', 'POST', 200, {
'Content-Type': 'application/json',
'Accept': 'application/json, text/plain, */*',
});
const SEVEN_DAYS_IN_MS = (1000 * 3600 * 24 * 7);
const HOMER_BASE_URL = process.env.HOMER_BASE_URL || 'http://127.0.0.1';
const getHomerApiKey = async(logger) => {
if (!process.env.HOMER_BASE_URL || !process.env.HOMER_USERNAME || !process.env.HOMER_PASSWORD) {
@@ -13,21 +17,11 @@ const getHomerApiKey = async(logger) => {
}
try {
const response = await fetch(`${HOMER_BASE_URL}/api/v3/auth`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
username: process.env.HOMER_USERNAME,
password: process.env.HOMER_PASSWORD
})
const obj = await postJSON('/api/v3/auth', {
username: process.env.HOMER_USERNAME,
password: process.env.HOMER_PASSWORD
});
if (!response.ok) {
logger.error({response}, 'Error retrieving apikey');
return;
}
const obj = await response.json();
debug(obj);
logger.debug({obj}, `getHomerApiKey for user ${process.env.HOMER_USERNAME}`);
return obj.token;
} catch (err) {
@@ -42,91 +36,51 @@ const getHomerSipTrace = async(logger, apiKey, callId) => {
}
try {
const now = Date.now();
const response = await fetch(`${HOMER_BASE_URL}/api/v3/call/transaction`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...basicAuth(apiKey)
},
body: JSON.stringify({
param: {
transaction: {
call: true,
registration: true,
rest: false
},
orlogic: true,
search: {
'1_call': {
callid: [callId]
},
'1_registration': {
callid: [callId]
}
},
const obj = await postJSON('/api/v3/call/transaction', {
param: {
transaction: {
call: true
},
timestamp: {
from: now - SEVEN_DAYS_IN_MS,
to: now
}
})
});
if (!response.ok) {
logger.error({response}, 'Error retrieving messages');
return;
}
const obj = await response.json();
search: {
'1_call': {
callid: [callId]
}
},
},
timestamp: {
from: now - SEVEN_DAYS_IN_MS,
to: now
}
}, basicAuth(apiKey));
return obj;
} catch (err) {
logger.info({err}, `getHomerSipTrace: Error retrieving messages for callid ${callId}`);
}
};
const getHomerPcap = async(logger, apiKey, callIds, method) => {
const getHomerPcap = async(logger, apiKey, callIds) => {
if (!process.env.HOMER_BASE_URL || !process.env.HOMER_USERNAME || !process.env.HOMER_PASSWORD) {
logger.debug('getHomerPcap: Homer integration not installed');
}
try {
const now = Date.now();
const response = await fetch(`${HOMER_BASE_URL}/api/v3/export/call/messages/pcap`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...basicAuth(apiKey)
},
body: JSON.stringify({
param: {
transaction: {
call: method === 'invite',
registration: method === 'register',
rest: false
},
orlogic: true,
search: {
...(method === 'invite' && {
'1_call': {
callid: callIds
}
})
,
...(method === 'register' && {
'1_registration': {
callid: callIds
}
})
},
const stream = await postPcap('/api/v3/export/call/messages/pcap', {
param: {
transaction: {
call: true
},
timestamp: {
from: now - SEVEN_DAYS_IN_MS,
to: now
}
})
});
if (!response.ok) {
logger.error({response}, 'Error retrieving messages');
return;
}
return Readable.fromWeb(response.body);
search: {
'1_call': {
callid: callIds
}
},
},
timestamp: {
from: now - SEVEN_DAYS_IN_MS,
to: now
}
}, basicAuth(apiKey));
return stream;
} catch (err) {
logger.info({err}, `getHomerPcap: Error retrieving messages for callid ${callIds}`);
}

View File

@@ -1,23 +0,0 @@
const JAEGER_BASE_URL = process.env.JAEGER_BASE_URL || 'http://127.0.0.1';
const getJaegerTrace = async(logger, traceId) => {
if (!process.env.JAEGER_BASE_URL) {
logger.debug('getJaegerTrace: jaeger integration not installed');
return null;
}
try {
const response = await fetch(`${JAEGER_BASE_URL}/api/v3/traces/${traceId}`);
if (!response.ok) {
logger.error({response}, 'Error retrieving spans');
return;
}
return await response.json();
} 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
};

Some files were not shown because too many files have changed in this diff Show More