Compare commits

..

3 Commits

Author SHA1 Message Date
Quan HL
4e59bcb9fa fix: update custom stt/tss url 2023-03-05 06:33:25 +07:00
Dave Horton
07421d830e change property names for custom speech 2023-03-02 15:28:25 -05:00
Dave Horton
5c687aebdd add support for custom speech api 2023-03-02 14:04:03 -05:00
32 changed files with 554 additions and 3386 deletions

View File

@@ -1,44 +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|
```
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).

12
app.js
View File

@@ -21,9 +21,6 @@ assert.ok(process.env.JAMBONES_MYSQL_HOST &&
process.env.JAMBONES_MYSQL_DATABASE, 'missing JAMBONES_MYSQL_XXX env vars');
assert.ok(process.env.JAMBONES_REDIS_HOST, 'missing JAMBONES_REDIS_HOST env var');
assert.ok(process.env.JAMBONES_TIME_SERIES_HOST, 'missing JAMBONES_TIME_SERIES_HOST env var');
assert.ok(process.env.ENCRYPTION_SECRET || process.env.JWT_SECRET, 'missing ENCRYPTION_SECRET env var');
assert.ok(process.env.JWT_SECRET, 'missing JWT_SECRET env var');
const {
queryCdrs,
queryCdrsSP,
@@ -44,14 +41,9 @@ const {
addKey,
retrieveKey,
deleteKey,
} = require('@jambonz/realtimedb-helpers')({
host: process.env.JAMBONES_REDIS_HOST,
port: process.env.JAMBONES_REDIS_PORT || 6379
}, logger);
const {
getTtsVoices
} = require('@jambonz/speech-utils')({
host: process.env.JAMBONES_REDIS_HOST,
} = require('@jambonz/realtimedb-helpers')({
host: process.env.JAMBONES_REDIS_HOST || 'localhost',
port: process.env.JAMBONES_REDIS_PORT || 6379
}, logger);
const {

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

@@ -99,7 +99,7 @@ const checkApiTokens = (logger, token, done) => {
hasServiceProviderAuth: scope === 'service_provider',
hasAccountAuth: scope === 'account'
};
logger.debug({user}, `successfully validated with scope ${scope}`);
logger.info(user, `successfully validated with scope ${scope}`);
return done(null, user, {scope});
});
});

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

@@ -86,6 +86,14 @@ SpeechCredential.fields = [
{
name: 'last_tested',
type: 'date'
},
{
name: 'custom_tts_url',
type: 'date'
},
{
name: 'custom_stt_url',
type: 'date'
}
];

View File

@@ -46,34 +46,17 @@ const stripPort = (hostport) => {
};
const validateUpdateForCarrier = async(req) => {
try {
const account_sid = parseAccountSid(req);
if (req.user.hasScope('admin')) {
return;
}
if (req.user.hasScope('account')) {
if (account_sid === req.user.account_sid) {
return;
}
throw new DbErrorForbidden('insufficient permissions to update account');
}
if (req.user.hasScope('service_provider')) {
const [r] = await promisePool.execute(
'SELECT service_provider_sid from accounts WHERE account_sid = ?', [account_sid]
);
if (r.length === 1 && r[0].service_provider_sid === req.user.service_provider_sid) {
return;
}
throw new DbErrorForbidden('insufficient permissions to update account');
}
} catch (error) {
throw error;
const account_sid = parseAccountSid(req);
if (req.user.hasScope('admin')) return ;
if (req.user.hasScope('account')) {
if (account_sid === req.user.account_sid) return ;
throw new DbErrorForbidden('insufficient permissions to update account');
}
if (req.user.hasScope('service_provider')) {
const [r] = await promisePool.execute(
'SELECT service_provider_sid from accounts WHERE account_sid = ?', [account_sid]);
if (r.length === 1 && r[0].service_provider_sid === req.user.service_provider_sid) return;
throw new DbErrorForbidden('insufficient permissions to update account');
}
};
@@ -361,20 +344,10 @@ async function validateUpdate(req, sid) {
if (req.user.service_provider_sid && !req.user.hasScope('admin')) {
const result = await Account.retrieve(sid);
if (!result || result.length === 0) {
throw new DbErrorBadRequest(`account not found for sid ${sid}`);
}
if (result[0].service_provider_sid !== req.user.service_provider_sid) {
throw new DbErrorUnprocessableRequest('cannot update account from different service provider');
}
}
if (req.user.hasScope('admin')) {
/* check to be sure that the account_sid exists */
const result = await Account.retrieve(sid);
if (!result || result.length === 0) {
throw new DbErrorBadRequest(`account not found for sid ${sid}`);
}
}
if (req.body.service_provider_sid) throw new DbErrorBadRequest('service_provider_sid may not be modified');
}
async function validateDelete(req, sid) {
@@ -802,11 +775,11 @@ 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);
const setName = `${(process.env.JAMBONES_CLUSTER_ID || 'default')}:active-fs`;
const serviceUrl = await getFsUrl(logger, retrieveSet, setName);
if (!serviceUrl) res.json({msg: 'no available feature servers at this time'}).status(480);
await validateCreateMessage(logger, account_sid, req);

View File

@@ -24,13 +24,13 @@ router.post('/:sid', async(req, res) => {
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) {
service_provider_sid = parseServiceProviderSid(req);
} else {
service_provider_sid = req.user.service_provider_sid;
}
try {
const [template] = await PredefinedCarrier.retrieve(sid);
logger.debug({template}, `Retrieved template carrier for sid ${sid}`);
if (!template) return res.sendStatus(404);

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 {

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

@@ -28,18 +28,16 @@ router.post('/', async(req, res) => {
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 && !req.user.hasAdminAuth) {
logger.error('POST /SpeechCredentials invalid credentials');
return res.sendStatus(403);
}
service_provider_sid = parseServiceProviderSid(req);
}
try {
let uuid;
if (account_sid) {
const existing = (await AccountLimits.retrieve(account_sid) || [])
@@ -82,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);
@@ -102,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

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

@@ -35,47 +35,27 @@ function validateAdd(req) {
}
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')) {
/* allow account users to retrieve service provider data from parent SP */
const sid = req.user.account_sid;
const [r] = await promisePool.execute('SELECT service_provider_sid from accounts WHERE account_sid = ?', [sid]);
if (r.length === 1 && r[0].service_provider_sid === req.user.service_provider_sid) return;
}
throw new DbErrorForbidden('insufficient permissions to update service provider');
} catch (error) {
throw error;
const service_provider_sid = parseServiceProviderSid(req);
if (req.user.hasScope('admin')) return ;
if (req.user.hasScope('service_provider')) {
if (service_provider_sid === req.user.service_provider_sid) return ;
}
if (req.user.hasScope('account')) {
/* allow account users to retrieve service provider data from parent SP */
const sid = req.user.account_sid;
const [r] = await promisePool.execute('SELECT service_provider_sid from accounts WHERE account_sid = ?', [sid]);
if (r.length === 1 && r[0].service_provider_sid === req.user.service_provider_sid) return;
}
throw new DbErrorForbidden('insufficient permissions to update service provider');
}
function validateUpdate(req) {
try {
if (req.user.hasScope('admin')) return ;
if (req.user.hasScope('service_provider')) {
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) {
console.log('Passing forward the error received');
throw error;
if (service_provider_sid === req.user.service_provider_sid) return ;
}
throw new DbErrorForbidden('insufficient permissions to update service provider');
}
/* can not delete a service provider if it has any active accounts */
@@ -211,6 +191,7 @@ 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']) {
@@ -231,6 +212,7 @@ router.post('/', async(req, res) => {
/* list */
router.get('/', async(req, res) => {
const logger = req.app.locals.logger;
try {
const results = await ServiceProvider.retrieveAll();
logger.debug({results, user: req.user}, 'ServiceProvider.retrieveAll');
@@ -260,10 +242,10 @@ 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 = req.params.sid;
// create webhooks if provided
const obj = Object.assign({}, req.body);
@@ -273,14 +255,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];
}
@@ -288,7 +271,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

@@ -44,8 +44,6 @@ const encryptCredential = (obj) => {
region,
client_id,
secret,
nuance_tts_uri,
nuance_stt_uri,
use_custom_tts,
custom_tts_endpoint,
use_custom_stt,
@@ -99,10 +97,9 @@ const encryptCredential = (obj) => {
return encrypt(wsData);
case 'nuance':
const checked = (client_id && secret) || (nuance_tts_uri || nuance_stt_uri);
assert(checked, 'invalid nuance speech credential: either entered client id and\
secret or entered a nuance_tts_uri or nuance_stt_uri');
const nuanceData = JSON.stringify({client_id, secret, nuance_tts_uri, nuance_stt_uri});
assert(client_id, 'invalid nuance speech credential: client_id is required');
assert(secret, 'invalid nuance speech credential: secret is required');
const nuanceData = JSON.stringify({client_id, secret});
return encrypt(nuanceData);
case 'deepgram':
@@ -135,24 +132,21 @@ const encryptCredential = (obj) => {
router.post('/', async(req, res) => {
const logger = req.app.locals.logger;
try {
const {
use_for_stt,
use_for_tts,
vendor,
} = req.body;
const account_sid = req.user.account_sid || req.body.account_sid;
const service_provider_sid = req.user.service_provider_sid ||
req.body.service_provider_sid || parseServiceProviderSid(req);
if (!account_sid) {
if (!req.user.hasServiceProviderAuth && !req.user.hasAdminAuth) {
logger.error('POST /SpeechCredentials invalid credentials');
return res.sendStatus(403);
}
const {
use_for_stt,
use_for_tts,
vendor,
} = req.body;
const account_sid = req.user.account_sid || req.body.account_sid;
const service_provider_sid = req.user.service_provider_sid ||
req.body.service_provider_sid || parseServiceProviderSid(req);
if (!account_sid) {
if (!req.user.hasServiceProviderAuth && !req.user.hasAdminAuth) {
logger.error('POST /SpeechCredentials invalid credentials');
return res.sendStatus(403);
}
}
try {
const encrypted_credential = encryptCredential(req.body);
const uuid = await SpeechCredential.make({
account_sid,
@@ -172,11 +166,10 @@ router.post('/', async(req, res) => {
* retrieve all speech credentials for an account
*/
router.get('/', async(req, res) => {
const account_sid = parseAccountSid(req) || req.user.account_sid;
const service_provider_sid = parseServiceProviderSid(req) || req.user.service_provider_sid;
const logger = req.app.locals.logger;
try {
const account_sid = parseAccountSid(req) || req.user.account_sid;
const service_provider_sid = parseServiceProviderSid(req) || req.user.service_provider_sid;
const credsAccount = account_sid ? await SpeechCredential.retrieveAll(account_sid) : [];
const credsSP = service_provider_sid ?
await SpeechCredential.retrieveAllForSP(service_provider_sid) :
@@ -223,7 +216,7 @@ router.get('/', async(req, res) => {
else if ('nuance' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
obj.client_id = o.client_id;
obj.secret = o.secret ? obscureKey(o.secret) : null;
obj.secret = obscureKey(o.secret);
}
else if ('deepgram' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
@@ -298,9 +291,7 @@ router.get('/:sid', async(req, res) => {
else if ('nuance' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
obj.client_id = o.client_id;
obj.secret = o.secret ? obscureKey(o.secret) : null;
obj.nuance_tts_uri = o.nuance_tts_uri;
obj.nuance_stt_uri = o.nuance_stt_uri;
obj.secret = obscureKey(o.secret);
}
else if ('deepgram' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
@@ -317,10 +308,6 @@ router.get('/:sid', async(req, res) => {
const o = JSON.parse(decrypt(credential));
obj.riva_server_uri = o.riva_server_uri;
}
else if ('soniox' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
obj.api_key = obscureKey(o.api_key);
}
else if (obj.vendor.startsWith('custom:')) {
const o = JSON.parse(decrypt(credential));
obj.auth_token = obscureKey(o.auth_token);
@@ -357,7 +344,7 @@ router.put('/:sid', async(req, res) => {
const logger = req.app.locals.logger;
try {
const {use_for_tts, use_for_stt, region, aws_region, stt_region, tts_region,
riva_server_uri, nuance_tts_uri, nuance_stt_uri} = req.body;
riva_server_uri, custom_stt_url, custom_tts_url} = req.body;
if (typeof use_for_tts === 'undefined' && typeof use_for_stt === 'undefined') {
throw new DbErrorUnprocessableRequest('use_for_tts and use_for_stt are the only updateable fields');
}
@@ -394,8 +381,8 @@ router.put('/:sid', async(req, res) => {
stt_region,
tts_region,
riva_server_uri,
nuance_stt_uri,
nuance_tts_uri
custom_stt_url,
custom_tts_url
};
logger.info({o, newCred}, 'updating speech credential with this new credential');
obj.credential = encryptCredential(newCred);
@@ -447,8 +434,7 @@ router.get('/:sid/test', async(req, res) => {
if (cred.use_for_tts) {
try {
const {getTtsVoices} = req.app.locals;
await testGoogleTts(logger, getTtsVoices, credential);
await testGoogleTts(logger, credential);
results.tts.status = 'ok';
SpeechCredential.ttsTestResult(sid, true);
} catch (err) {
@@ -470,9 +456,8 @@ router.get('/:sid/test', async(req, res) => {
}
else if (cred.vendor === 'aws') {
if (cred.use_for_tts) {
const {getTtsVoices} = req.app.locals;
try {
await testAwsTts(logger, getTtsVoices, {
await testAwsTts(logger, {
accessKeyId: credential.access_key_id,
secretAccessKey: credential.secret_access_key,
region: credential.aws_region || process.env.AWS_REGION
@@ -554,16 +539,13 @@ router.get('/:sid/test', async(req, res) => {
const {
client_id,
secret,
nuance_tts_uri,
nuance_stt_uri
secret
} = credential;
if (cred.use_for_tts) {
try {
await testNuanceTts(logger, getTtsVoices, {
client_id,
secret,
nuance_tts_uri
secret
});
results.tts.status = 'ok';
SpeechCredential.ttsTestResult(sid, true);
@@ -578,7 +560,7 @@ router.get('/:sid/test', async(req, res) => {
}
if (cred.use_for_stt) {
try {
await testNuanceStt(logger, {client_id, secret, nuance_stt_uri});
await testNuanceStt(logger, {client_id, secret});
results.stt.status = 'ok';
SpeechCredential.sttTestResult(sid, true);
} catch (err) {

View File

@@ -1,10 +1,10 @@
const router = require('express').Router();
const User = require('../../models/user');
const jwt = require('jsonwebtoken');
const request = require('request');
const {DbErrorBadRequest} = require('../../utils/errors');
const {generateHashedPassword, verifyPassword} = require('../../utils/password-utils');
const {promisePool} = require('../../db');
const {validatePasswordSettings} = require('./utils');
const {decrypt} = require('../../utils/encrypt-decrypt');
const sysError = require('../error');
const retrieveMyDetails = `SELECT *
@@ -28,8 +28,7 @@ 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 validateRequest = async(user_sid, payload) => {
const {
old_password,
new_password,
@@ -38,53 +37,15 @@ const validateRequest = async(user_sid, req) => {
email,
email_activation_code,
force_change,
is_active
} = payload;
is_active} = 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');
@@ -101,18 +62,23 @@ const validateRequest = async(user_sid, req) => {
router.get('/', async(req, res) => {
const logger = req.app.locals.logger;
const token = req.user.jwt;
const decodedJwt = jwt.verify(token, process.env.JWT_SECRET);
let usersList;
try {
let results;
if (req.user.hasAdminAuth) {
if (decodedJwt.scope === 'admin') {
results = await User.retrieveAll();
}
else if (req.user.hasAccountAuth) {
results = await User.retrieveAllForAccount(req.user.account_sid, true);
else if (decodedJwt.scope === 'account') {
results = await User.retrieveAllForAccount(decodedJwt.account_sid, true);
}
else if (req.user.hasServiceProviderAuth) {
results = await User.retrieveAllForServiceProvider(req.user.service_provider_sid, true);
else if (decodedJwt.scope === 'service_provider') {
results = await User.retrieveAllForServiceProvider(decodedJwt.service_provider_sid, true);
}
else {
throw new DbErrorBadRequest(`invalid scope: ${decodedJwt.scope}`);
}
if (results.length === 0) throw new Error('failure retrieving users list');
@@ -256,6 +222,8 @@ router.get('/me', async(req, res) => {
router.get('/:user_sid', async(req, res) => {
const logger = req.app.locals.logger;
const token = req.user.jwt;
const decodedJwt = jwt.verify(token, process.env.JWT_SECRET);
const {user_sid} = req.params;
try {
@@ -264,9 +232,9 @@ router.get('/:user_sid', async(req, res) => {
const {hashed_password, ...rest} = user;
if (!user) throw new Error('failure retrieving user');
if (req.user.hasAdminAuth ||
req.user.hasAccountAuth && req.user.account_sid === user.account_sid ||
req.user.hasServiceProviderAuth && req.user.service_provider_sid === user.service_provider_sid) {
if (decodedJwt.scope === 'admin' ||
decodedJwt.scope === 'account' && decodedJwt.account_sid === user.account_sid ||
decodedJwt.scope === 'service_provider' && decodedJwt.service_provider_sid === user.service_provider_sid) {
res.status(200).json(rest);
} else {
res.sendStatus(403);
@@ -281,7 +249,8 @@ 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 token = req.user.jwt;
const decodedJwt = jwt.verify(token, process.env.JWT_SECRET);
const {
old_password,
new_password,
@@ -297,15 +266,15 @@ router.put('/:user_sid', async(req, res) => {
//if (req.user.user_sid && req.user.user_sid !== user_sid) return res.sendStatus(403);
if (!hasAdminAuth &&
!(hasAccountAuth && req.user.account_sid === user[0].account_sid) &&
!(hasServiceProviderAuth && req.user.service_provider_sid === user[0].service_provider_sid) &&
if (decodedJwt.scope !== 'admin' &&
!(decodedJwt.scope === 'account' && decodedJwt.account_sid === user[0].account_sid) &&
!(decodedJwt.scope === 'service_provider' && decodedJwt.service_provider_sid === user[0].service_provider_sid) &&
(req.user.user_sid && req.user.user_sid !== user_sid)) {
return res.sendStatus(403);
}
try {
const user = await validateRequest(user_sid, req);
const user = await validateRequest(user_sid, req.body);
if (!user) return res.sendStatus(404);
if (new_password) {
@@ -398,12 +367,11 @@ router.post('/', async(req, res) => {
hashed_password: passwordHash,
};
const allUsers = await User.retrieveAll();
const token = req.user.jwt;
const decodedJwt = jwt.verify(token, process.env.JWT_SECRET);
delete payload.initial_password;
try {
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);
@@ -417,27 +385,30 @@ router.post('/', async(req, res) => {
return res.status(422).json({msg: 'user with this email already exists'});
}
if (req.user.hasAdminAuth) {
if (decodedJwt.scope === 'admin') {
logger.debug({payload}, 'POST /users');
const uuid = await User.make(payload);
res.status(201).json({user_sid: uuid});
}
else if (req.user.hasAccountAuth) {
else if (decodedJwt.scope === 'account') {
logger.debug({payload}, 'POST /users');
const uuid = await User.make({
...payload,
account_sid: req.user.account_sid,
account_sid: decodedJwt.account_sid,
});
res.status(201).json({user_sid: uuid});
}
else if (req.user.hasServiceProviderAuth) {
else if (decodedJwt.scope === 'service_provider') {
logger.debug({payload}, 'POST /users');
const uuid = await User.make({
...payload,
service_provider_sid: req.user.service_provider_sid,
service_provider_sid: decodedJwt.service_provider_sid,
});
res.status(201).json({user_sid: uuid});
}
else {
throw new DbErrorBadRequest(`invalid scope: ${decodedJwt.scope}`);
}
} catch (err) {
sysError(logger, res, err);
}
@@ -446,21 +417,24 @@ router.post('/', async(req, res) => {
router.delete('/:user_sid', async(req, res) => {
const logger = req.app.locals.logger;
const {user_sid} = req.params;
const token = req.user.jwt;
const decodedJwt = jwt.verify(token, process.env.JWT_SECRET);
const allUsers = await User.retrieveAll();
const activeAdminUsers = allUsers.filter((e) => !e.account_sid && !e.service_provider_sid && e.is_active);
const user = await User.retrieve(user_sid);
try {
if (req.user.hasAdminAuth && activeAdminUsers.length === 1) {
if (decodedJwt.scope === 'admin' && !user.account_sid && !user.service_provider_sid &&
activeAdminUsers.length === 1) {
throw new Error('cannot delete this admin user - there are no other active admin users');
}
if (req.user.hasAdminAuth ||
(req.user.hasAccountAuth && req.user.account_sid === user[0].account_sid) ||
(req.user.hasServiceProviderAuth && req.user.service_provider_sid === user[0].service_provider_sid)) {
if (decodedJwt.scope === 'admin' ||
(decodedJwt.scope === 'account' && decodedJwt.account_sid === user[0].account_sid) ||
(decodedJwt.scope === 'service_provider' && decodedJwt.service_provider_sid === user[0].service_provider_sid)) {
await User.remove(user_sid);
//logout user after self-delete
if (req.user.user_sid === user_sid) {
if (decodedJwt.user_sid === user_sid) {
request({
url:'http://localhost:3000/v1/logout',
method: 'POST',
@@ -474,11 +448,12 @@ router.delete('/:user_sid', async(req, res) => {
}
return res.sendStatus(204);
} else {
throw new DbErrorBadRequest('invalid request');
throw new DbErrorBadRequest(`invalid scope: ${decodedJwt.scope}`);
}
} catch (err) {
sysError(logger, res, err);
}
});
module.exports = router;

View File

@@ -1,10 +1,9 @@
const { v4: uuid, validate } = require('uuid');
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 (?, ?)`;
@@ -140,76 +139,37 @@ const createTestAlerts = async(writeAlerts, AlertType, account_sid) => {
const parseServiceProviderSid = (req) => {
const arr = /ServiceProviders\/([^\/]*)/.exec(req.originalUrl);
if (arr) {
const sid = arr[1];
const sid_validation = validate(sid);
if (!sid_validation) {
throw new BadRequestError('invalid service_provider_sid format');
}
return arr[1];
}
if (arr) return arr[1];
};
const parseAccountSid = (req) => {
const arr = /Accounts\/([^\/]*)/.exec(req.originalUrl);
if (arr) {
const sid = arr[1];
const sid_validation = validate(sid);
if (!sid_validation) {
throw new BadRequestError('invalid account_sid format');
}
return arr[1];
}
if (arr) return arr[1];
};
const hasAccountPermissions = (req, res, next) => {
try {
if (req.user.hasScope('admin')) {
return next();
}
if (req.user.hasScope('service_provider')) {
return next();
}
if (req.user.hasScope('account')) {
const account_sid = parseAccountSid(req);
if (account_sid === req.user.account_sid) {
return next();
}
}
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')) 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) => {
@@ -314,31 +274,6 @@ const disableSubspace = async(opts) => {
return;
};
const validatePasswordSettings = async(password) => {
const sql = 'SELECT * from password_settings';
const [rows] = await promisePool.execute(sql);
const specialChars = /[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]+/;
const numbers = /[0-9]+/;
if (rows.length === 0) {
if (password.length < 8 || password.length > 20) {
throw new DbErrorBadRequest('password length must be between 8 and 20');
}
} else {
if (rows[0].min_password_length && password.length < rows[0].min_password_length) {
throw new DbErrorBadRequest(`password must be at least ${rows[0].min_password_length} characters long`);
}
if (rows[0].require_digit === 1 && !numbers.test(password)) {
throw new DbErrorBadRequest('password must contain at least one digit');
}
if (rows[0].require_special_character === 1 && !specialChars.test(password)) {
throw new DbErrorBadRequest('password must contain at least one special character');
}
}
return;
};
module.exports = {
setupFreeTrial,
createTestCdrs,
@@ -349,6 +284,5 @@ module.exports = {
hasServiceProviderPermissions,
checkLimits,
enableSubspace,
disableSubspace,
validatePasswordSettings
disableSubspace
};

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

@@ -10,34 +10,7 @@ info:
version: 1.0.0
servers:
- url: /v1
description: jambonz API server
tags:
- name: Authentication
description: Authentication operations
- name: Accounts
description: Accounts operations
- name: Users
description: Users operations
- name: Applications
description: Applications operations
- name: Phone Numbers
description: Phone Numbers operations
- name: Api Keys
description: Api Keys operations
- name: Service Providers
description: Service Providers operations
- name: SBCs
description: SBCs operations
- name: Voip Carriers
description: Voip Carriers operations
- name: Sip Gateways
description: Sip Gateways operations
- name: Smpp Gateways
description: Smpp Gateways operations
- name: Webhooks
description: Webhooks operations
- name: Microsoft Teams Tenants
description: Microsoft Teams Tenants operations
description: development server
paths:
/BetaInviteCodes:
post:
@@ -106,8 +79,6 @@ paths:
type: string
format: uuid
post:
tags:
- Accounts
summary: add a VoiPCarrier to an account based on PredefinedCarrier template
operationId: createVoipCarrierFromTemplate
responses:
@@ -125,8 +96,6 @@ paths:
$ref: '#/components/schemas/GeneralError'
/Sbcs:
post:
tags:
- SBCs
summary: add an SBC address
operationId: createSbc
requestBody:
@@ -165,9 +134,7 @@ paths:
schema:
$ref: '#/components/schemas/GeneralError'
get:
tags:
- SBCs
summary: retrieve public IP addresses of the jambonz SBCs
summary: retrieve public IP addresses of the jambonz Sbcs
operationId: listSbcs
parameters:
- in: query
@@ -204,9 +171,7 @@ paths:
schema:
type: string
delete:
tags:
- SBCs
summary: delete SBC address
summary: delete sbc address
operationId: deleteSbcAddress
responses:
200:
@@ -275,8 +240,6 @@ paths:
/ApiKeys:
post:
tags:
- Api Keys
summary: create an api key
operationId: createApikey
requestBody:
@@ -314,7 +277,7 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
/ApiKeys/{ApiKeySid}:
/Apikeys/{ApiKeySid}:
parameters:
- name: ApiKeySid
in: path
@@ -322,8 +285,6 @@ paths:
schema:
type: string
delete:
tags:
- Api Keys
summary: delete api key
operationId: deleteApiKey
responses:
@@ -333,8 +294,6 @@ paths:
description: api key or account not found
/signin:
post:
tags:
- Authentication
summary: sign in using email and password
operationId: loginUser
requestBody:
@@ -376,8 +335,6 @@ paths:
$ref: '#/components/schemas/GeneralError'
/logout:
post:
tags:
- Authentication
summary: log out and deactivate jwt
operationId: logoutUser
responses:
@@ -391,8 +348,6 @@ paths:
$ref: '#/components/schemas/GeneralError'
/forgot-password:
post:
tags:
- Authentication
summary: send link to reset password
operationId: forgotPassword
requestBody:
@@ -422,8 +377,6 @@ paths:
$ref: '#/components/schemas/GeneralError'
/change-password:
post:
tags:
- Authentication
summary: changePassword
operationId: changePassword
requestBody:
@@ -455,8 +408,6 @@ paths:
$ref: '#/components/schemas/GeneralError'
/register:
post:
tags:
- Authentication
summary: create a new user and account
operationId: registerUser
requestBody:
@@ -565,9 +516,7 @@ paths:
schema:
$ref: '#/components/schemas/GeneralError'
/Users:
get:
tags:
- Users
get:
summary: list all users
operationId: listUsers
responses:
@@ -598,8 +547,6 @@ paths:
schema:
type: string
get:
tags:
- Users
summary: retrieve user information
operationId: getUser
requestBody:
@@ -636,8 +583,6 @@ paths:
schema:
$ref: '#/components/schemas/GeneralError'
put:
tags:
- Users
summary: update user information
operationId: updateUser
requestBody:
@@ -658,6 +603,8 @@ paths:
new_password:
type: string
description: new password
name:
type: string
is_active:
type: boolean
force_change:
@@ -682,8 +629,6 @@ paths:
schema:
$ref: '#/components/schemas/GeneralError'
post:
tags:
- Users
summary: create a new user
operationId: createUser
requestBody:
@@ -704,6 +649,8 @@ paths:
type: string
permissions:
type: array
force_change:
type: boolean
old_password:
type: string
description: existing password, which is to be replaced
@@ -723,8 +670,6 @@ paths:
schema:
$ref: '#/components/schemas/GeneralError'
delete:
tags:
- Users
summary: delete a user
operationId: deleteUser
responses:
@@ -742,8 +687,6 @@ paths:
$ref: '#/components/schemas/GeneralError'
/Users/me:
get:
tags:
- Users
summary: retrieve details about logged-in user and associated account
operationId: getMyDetails
responses:
@@ -909,8 +852,6 @@ paths:
/ActivationCode:
post:
tags:
- Authentication
summary: send an activation code to the user
operationId: sendActivationCode
requestBody:
@@ -956,8 +897,6 @@ paths:
schema:
type: string
put:
tags:
- Authentication
summary: validate an activation code
operationId: validateActivationCode
requestBody:
@@ -1037,8 +976,6 @@ paths:
schema:
type: string
get:
tags:
- Webhooks
summary: retrieve webhook
operationId: getWebhook
responses:
@@ -1059,8 +996,6 @@ paths:
/VoipCarriers:
post:
tags:
- Voip Carriers
summary: create voip carrier
operationId: createVoipCarrier
requestBody:
@@ -1148,8 +1083,6 @@ paths:
schema:
$ref: '#/components/schemas/GeneralError'
get:
tags:
- Voip Carriers
summary: list voip carriers
operationId: listVoipCarriers
responses:
@@ -1177,8 +1110,6 @@ paths:
schema:
type: string
delete:
tags:
- Voip Carriers
summary: delete a voip carrier
operationId: deleteVoipCarrier
responses:
@@ -1201,8 +1132,6 @@ paths:
schema:
$ref: '#/components/schemas/GeneralError'
get:
tags:
- Voip Carriers
summary: retrieve voip carrier
operationId: getVoipCarrier
responses:
@@ -1221,8 +1150,6 @@ paths:
schema:
$ref: '#/components/schemas/GeneralError'
put:
tags:
- Voip Carriers
summary: update voip carrier
operationId: updateVoipCarrier
parameters:
@@ -1262,8 +1189,6 @@ paths:
/SipGateways:
post:
tags:
- Sip Gateways
summary: create sip gateway
operationId: createSipGateway
requestBody:
@@ -1315,8 +1240,6 @@ paths:
schema:
$ref: '#/components/schemas/GeneralError'
get:
tags:
- Sip Gateways
summary: list sip gateways
operationId: listSipGateways
parameters:
@@ -1351,8 +1274,6 @@ paths:
schema:
type: string
delete:
tags:
- Sip Gateways
summary: delete a sip gateway
operationId: deleteSipGateway
responses:
@@ -1367,8 +1288,6 @@ paths:
schema:
$ref: '#/components/schemas/GeneralError'
get:
tags:
- Sip Gateways
summary: retrieve sip gateway
operationId: getSipGateway
responses:
@@ -1387,8 +1306,6 @@ paths:
schema:
$ref: '#/components/schemas/GeneralError'
put:
tags:
- Sip Gateways
summary: update sip gateway
operationId: updateSipGateway
requestBody:
@@ -1415,8 +1332,6 @@ paths:
$ref: '#/components/schemas/GeneralError'
/SmppGateways:
post:
tags:
- Smpp Gateways
summary: create smpp gateway
operationId: createSmppGateway
requestBody:
@@ -1472,8 +1387,6 @@ paths:
schema:
$ref: '#/components/schemas/GeneralError'
get:
tags:
- Smpp Gateways
summary: list smpp gateways
operationId: listSmppGateways
responses:
@@ -1501,8 +1414,6 @@ paths:
schema:
type: string
delete:
tags:
- Smpp Gateways
summary: delete a smpp gateway
operationId: deleteSmppGateway
responses:
@@ -1517,8 +1428,6 @@ paths:
schema:
$ref: '#/components/schemas/GeneralError'
get:
tags:
- Smpp Gateways
summary: retrieve smpp gateway
operationId: getSmppGateway
responses:
@@ -1537,9 +1446,7 @@ paths:
schema:
$ref: '#/components/schemas/GeneralError'
put:
tags:
- Smpp Gateways
summary: update smpp gateway
summary: update sip gateway
operationId: updateSmppGateway
requestBody:
content:
@@ -1565,8 +1472,6 @@ paths:
$ref: '#/components/schemas/GeneralError'
/PhoneNumbers:
post:
tags:
- Phone Numbers
summary: provision a phone number into inventory from a Voip Carrier
operationId: provisionPhoneNumber
requestBody:
@@ -1622,8 +1527,6 @@ paths:
schema:
$ref: '#/components/schemas/GeneralError'
get:
tags:
- Phone Numbers
summary: list phone numbers
operationId: listProvisionedPhoneNumbers
responses:
@@ -1651,8 +1554,6 @@ paths:
schema:
type: string
delete:
tags:
- Phone Numbers
summary: delete a phone number
operationId: deletePhoneNumber
responses:
@@ -1675,8 +1576,6 @@ paths:
schema:
$ref: '#/components/schemas/GeneralError'
get:
tags:
- Phone Numbers
summary: retrieve phone number
operationId: getPhoneNumber
responses:
@@ -1695,8 +1594,6 @@ paths:
schema:
$ref: '#/components/schemas/GeneralError'
put:
tags:
- Phone Numbers
summary: update phone number
operationId: updatePhoneNumber
requestBody:
@@ -1728,8 +1625,6 @@ paths:
/ServiceProviders:
post:
tags:
- Service Providers
summary: create service provider
operationId: createServiceProvider
requestBody:
@@ -1772,8 +1667,6 @@ paths:
$ref: '#/components/schemas/GeneralError'
get:
tags:
- Service Providers
summary: list service providers
operationId: listServiceProviders
responses:
@@ -1802,8 +1695,6 @@ paths:
schema:
type: string
delete:
tags:
- Service Providers
summary: delete a service provider
operationId: deleteServiceProvider
responses:
@@ -1825,8 +1716,6 @@ paths:
$ref: '#/components/schemas/GeneralError'
get:
tags:
- Service Providers
summary: retrieve service provider
operationId: getServiceProvider
responses:
@@ -1844,10 +1733,8 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
put:
tags:
- Service Providers
summary: update service provider
operationId: updateServiceProvider
requestBody:
@@ -1882,8 +1769,6 @@ paths:
type: string
format: uuid
get:
tags:
- Service Providers
summary: get all accounts for a service provider
operationId: getServiceProviderAccounts
responses:
@@ -1909,8 +1794,6 @@ paths:
type: string
format: uuid
get:
tags:
- Service Providers
summary: get all carriers for a service provider
operationId: getServiceProviderCarriers
responses:
@@ -1927,8 +1810,6 @@ paths:
404:
description: service provider not found
post:
tags:
- Service Providers
summary: create a carrier
operationId: createCarrierForServiceProvider
requestBody:
@@ -1960,8 +1841,6 @@ paths:
type: string
format: uuid
post:
tags:
- Service Providers
summary: add a VoiPCarrier to a service provider based on PredefinedCarrier template
operationId: createVoipCarrierFromTemplate
responses:
@@ -1987,8 +1866,6 @@ paths:
type: string
format: uuid
post:
tags:
- Service Providers
summary: create a speech credential for a service provider
operationId: addSpeechCredentialForSeerviceProvider
requestBody:
@@ -2020,8 +1897,6 @@ paths:
type: string
format: uuid
get:
tags:
- Service Providers
summary: get a specific speech credential
operationId: getSpeechCredential
responses:
@@ -2034,8 +1909,6 @@ paths:
404:
description: credential not found
put:
tags:
- Service Providers
summary: update a speech credential
operationId: updateSpeechCredential
requestBody:
@@ -2055,8 +1928,6 @@ paths:
schema:
$ref: '#/components/schemas/GeneralError'
delete:
tags:
- Service Providers
summary: delete a speech credential
operationId: deleteSpeechCredential
responses:
@@ -2066,8 +1937,6 @@ paths:
description: credential not found
/ServiceProviders/{ServiceProviderSid}/SpeechCredentials/{SpeechCredentialSid}/test:
get:
tags:
- Service Providers
summary: test a speech credential
operationId: testSpeechCredential
parameters:
@@ -2122,8 +1991,6 @@ paths:
$ref: '#/components/schemas/GeneralError'
/ServiceProviders/{ServiceProviderSid}/Limits:
post:
tags:
- Service Providers
summary: create a limit for a service provider
operationId: addLimitForServiceProvider
parameters:
@@ -2154,8 +2021,6 @@ paths:
schema:
$ref: '#/components/schemas/GeneralError'
get:
tags:
- Service Providers
summary: retrieve call capacity and other limits from the service provider
operationId: getServiceProviderLimits
parameters:
@@ -2184,8 +2049,6 @@ paths:
$ref: '#/components/schemas/GeneralError'
/Accounts/{AccountSid}/Limits:
post:
tags:
- Accounts
summary: create a limit for an account
operationId: addLimitForAccount
parameters:
@@ -2216,8 +2079,6 @@ paths:
schema:
$ref: '#/components/schemas/GeneralError'
get:
tags:
- Accounts
summary: retrieve call capacity and other limits from the account
operationId: getAccountLimits
parameters:
@@ -2246,8 +2107,6 @@ paths:
$ref: '#/components/schemas/GeneralError'
/MicrosoftTeamsTenants:
post:
tags:
- Microsoft Teams Tenants
summary: provision a customer tenant for MS Teams
operationId: createMsTeamsTenant
requestBody:
@@ -2295,8 +2154,6 @@ paths:
schema:
$ref: '#/components/schemas/GeneralError'
get:
tags:
- Microsoft Teams Tenants
summary: list MS Teams tenants
operationId: listMsTeamsTenants
responses:
@@ -2323,8 +2180,6 @@ paths:
type: string
format: uuid
delete:
tags:
- Microsoft Teams Tenants
summary: delete an MS Teams tenant
operationId: deleteTenant
responses:
@@ -2339,8 +2194,6 @@ paths:
schema:
$ref: '#/components/schemas/GeneralError'
get:
tags:
- Microsoft Teams Tenants
summary: retrieve an MS Teams tenant
operationId: getTenant
responses:
@@ -2360,9 +2213,7 @@ paths:
$ref: '#/components/schemas/GeneralError'
put:
tags:
- Microsoft Teams Tenants
summary: update an MS Teams tenant
summary: update tenant
operationId: putTenant
requestBody:
content:
@@ -2390,8 +2241,6 @@ paths:
/Accounts:
post:
tags:
- Accounts
summary: create an account
operationId: createAccount
requestBody:
@@ -2559,8 +2408,6 @@ paths:
address should be blacklisted by the platform (0 means forever).
get:
tags:
- Accounts
summary: list accounts
operationId: listAccounts
responses:
@@ -2588,8 +2435,6 @@ paths:
type: string
format: uuid
delete:
tags:
- Accounts
summary: delete an account
operationId: deleteAccount
responses:
@@ -2610,8 +2455,6 @@ paths:
schema:
$ref: '#/components/schemas/GeneralError'
get:
tags:
- Accounts
summary: retrieve account
operationId: getAccount
responses:
@@ -2631,8 +2474,6 @@ paths:
$ref: '#/components/schemas/GeneralError'
put:
tags:
- Accounts
summary: update account
operationId: updateAccount
requestBody:
@@ -2671,8 +2512,6 @@ paths:
schema:
type: boolean
get:
tags:
- Accounts
summary: get webhook signing secret, regenerating if requested
operationId: getWebhookSecret
responses:
@@ -2703,8 +2542,6 @@ paths:
type: string
format: uuid
get:
tags:
- Accounts
summary: get all api keys for an account
operationId: getAccountApiKeys
responses:
@@ -2739,8 +2576,6 @@ paths:
schema:
type: string
post:
tags:
- Accounts
summary: add or change the sip realm
operationId: createSipRealm
responses:
@@ -2769,8 +2604,6 @@ paths:
format: uuid
post:
tags:
- Accounts
summary: add a speech credential
operationId: createSpeechCredential
requestBody:
@@ -2798,8 +2631,6 @@ paths:
schema:
$ref: '#/components/schemas/GeneralError'
get:
tags:
- Accounts
summary: retrieve all speech credentials for an account
operationId: listSpeechCredentials
responses:
@@ -2828,8 +2659,6 @@ paths:
type: string
format: uuid
get:
tags:
- Accounts
summary: get a specific speech credential
operationId: getSpeechCredential
responses:
@@ -2842,8 +2671,6 @@ paths:
404:
description: credential not found
put:
tags:
- Accounts
summary: update a speech credential
operationId: updateSpeechCredential
requestBody:
@@ -2863,8 +2690,6 @@ paths:
schema:
$ref: '#/components/schemas/GeneralError'
delete:
tags:
- Accounts
summary: delete a speech credential
operationId: deleteSpeechCredential
responses:
@@ -2874,8 +2699,6 @@ paths:
description: credential not found
/Accounts/{AccountSid}/SpeechCredentials/{SpeechCredentialSid}/test:
get:
tags:
- Accounts
summary: test a speech credential
operationId: testSpeechCredential
parameters:
@@ -2989,8 +2812,6 @@ paths:
- inbound
- outbound
get:
tags:
- Accounts
summary: retrieve recent calls for an account
operationId: listRecentCalls
responses:
@@ -3087,8 +2908,6 @@ paths:
schema:
type: string
get:
tags:
- Accounts
summary: retrieve sip trace detail for a call
operationId: getRecentCallTrace
responses:
@@ -3114,8 +2933,6 @@ paths:
schema:
type: string
get:
tags:
- Service Providers
summary: retrieve pcap for a call
operationId: getRecentCallTrace
responses:
@@ -3188,8 +3005,6 @@ paths:
- inbound
- outbound
get:
tags:
- Service Providers
summary: retrieve recent calls for an account
operationId: listRecentCalls
responses:
@@ -3289,8 +3104,6 @@ paths:
schema:
type: string
get:
tags:
- Service Providers
summary: retrieve sip trace detail for a call
operationId: getRecentCallTrace
responses:
@@ -3316,8 +3129,6 @@ paths:
schema:
type: string
get:
tags:
- Accounts
summary: retrieve pcap for a call
operationId: getRecentCallTrace
responses:
@@ -3390,8 +3201,6 @@ paths:
- device-limit
- api-limit
get:
tags:
- Service Providers
summary: retrieve alerts for a service provider
operationId: listAlerts
responses:
@@ -3502,8 +3311,6 @@ paths:
- device-limit
- api-limit
get:
tags:
- Accounts
summary: retrieve alerts for an account
operationId: listAlerts
responses:
@@ -3552,8 +3359,6 @@ paths:
description: account not found
/Applications:
post:
tags:
- Applications
summary: create application
operationId: createApplication
requestBody:
@@ -3619,8 +3424,6 @@ paths:
schema:
$ref: '#/components/schemas/GeneralError'
get:
tags:
- Applications
summary: list applications
operationId: listApplications
responses:
@@ -3649,8 +3452,6 @@ paths:
schema:
type: string
delete:
tags:
- Applications
summary: delete an application
operationId: deleteApplication
responses:
@@ -3671,8 +3472,6 @@ paths:
schema:
$ref: '#/components/schemas/GeneralError'
get:
tags:
- Applications
summary: retrieve an application
responses:
200:
@@ -3690,8 +3489,6 @@ paths:
schema:
$ref: '#/components/schemas/GeneralError'
put:
tags:
- Applications
summary: update application
operationId: updateApplication
requestBody:
@@ -3720,8 +3517,6 @@ paths:
/Accounts/{AccountSid}/Calls:
post:
tags:
- Accounts
summary: create a call
operationId: createCall
parameters:
@@ -3783,8 +3578,6 @@ paths:
400:
description: bad request
get:
tags:
- Accounts
summary: list calls
operationId: listCalls
parameters:
@@ -3825,8 +3618,6 @@ paths:
schema:
type: string
delete:
tags:
- Accounts
summary: delete a call
operationId: deleteCall
responses:
@@ -3847,8 +3638,6 @@ paths:
schema:
$ref: '#/components/schemas/GeneralError'
get:
tags:
- Accounts
summary: retrieve a call
operationId: getCall
responses:
@@ -3867,8 +3656,6 @@ paths:
schema:
$ref: '#/components/schemas/GeneralError'
post:
tags:
- Accounts
summary: update a call
operationId: updateCall
requestBody:
@@ -3954,8 +3741,6 @@ paths:
$ref: '#/components/schemas/GeneralError'
/Accounts/{AccountSid}/Messages:
post:
tags:
- Accounts
summary: create an outgoing SMS message
operationId: createMessage
parameters:

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,7 @@
const ttsGoogle = require('@google-cloud/text-to-speech');
const sttGoogle = require('@google-cloud/speech').v1p1beta1;
const { TranscribeClient, ListVocabulariesCommand } = require('@aws-sdk/client-transcribe');
const Polly = require('aws-sdk/clients/polly');
const AWS = require('aws-sdk');
const { Deepgram } = require('@deepgram/sdk');
const sdk = require('microsoft-cognitiveservices-speech-sdk');
const { SpeechClient } = require('@soniox/soniox-node');
@@ -32,10 +34,9 @@ const testNuanceStt = async(logger, credentials) => {
return true;
};
const testGoogleTts = async(logger, getTtsVoices, credentials) => {
const voices = await getTtsVoices({vendor: 'google', credentials});
return voices;
const testGoogleTts = async(logger, credentials) => {
const client = new ttsGoogle.TextToSpeechClient({credentials});
await client.listVoices();
};
const testGoogleStt = async(logger, credentials) => {
@@ -119,33 +120,25 @@ const testMicrosoftStt = async(logger, credentials) => {
});
};
const testAwsTts = async(logger, getTtsVoices, credentials) => {
try {
const voices = await getTtsVoices({vendor: 'aws', credentials});
return voices;
} catch (err) {
logger.info({err}, 'testMicrosoftTts - failed to list voices for region ${region}');
throw err;
}
const testAwsTts = (logger, credentials) => {
const polly = new Polly(credentials);
return new Promise((resolve, reject) => {
polly.describeVoices({LanguageCode: 'en-US'}, (err, data) => {
if (err) return reject(err);
resolve();
});
});
};
const testAwsStt = async(logger, credentials) => {
try {
const {region, accessKeyId, secretAccessKey} = credentials;
const client = new TranscribeClient({
region,
credentials: {
accessKeyId,
secretAccessKey
}
const testAwsStt = (logger, credentials) => {
const transcribeservice = new AWS.TranscribeService(credentials);
return new Promise((resolve, reject) => {
transcribeservice.listVocabularies((err, data) => {
if (err) return reject(err);
logger.info({data}, 'retrieved language models');
resolve();
});
const command = new ListVocabulariesCommand({});
const response = await client.send(command);
return response;
} catch (err) {
logger.info({err}, 'testMicrosoftTts - failed to list voices for region ${region}');
throw err;
}
});
};
const testMicrosoftTts = async(logger, credentials) => {
@@ -205,7 +198,7 @@ const testWellSaidTts = async(logger, credentials) => {
const testIbmTts = async(logger, getTtsVoices, credentials) => {
const {tts_api_key, tts_region} = credentials;
const voices = await getTtsVoices({vendor: 'ibm', credentials: {tts_api_key, tts_region}});
const voices = await getTtsVoices({vendor: 'ibm', credentials: {api_key: tts_api_key, region: tts_region}});
return voices;
};

2851
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "jambonz-api-server",
"version": "v0.8.2",
"version": "v0.8.1",
"description": "",
"main": "app.js",
"scripts": {
@@ -18,16 +18,16 @@
"url": "https://github.com/jambonz/jambonz-api-server.git"
},
"dependencies": {
"@aws-sdk/client-transcribe": "^3.290.0",
"@deepgram/sdk": "^1.10.2",
"@google-cloud/speech": "^5.1.0",
"@google-cloud/text-to-speech": "^4.0.3",
"@jambonz/db-helpers": "^0.7.3",
"@jambonz/realtimedb-helpers": "^0.7.0",
"@jambonz/speech-utils": "^0.0.8",
"@jambonz/realtimedb-helpers": "^0.6.0",
"@jambonz/time-series": "^0.2.5",
"@jambonz/verb-specifications": "^0.0.3",
"@soniox/soniox-node": "^1.1.0",
"argon2": "^0.30.3",
"argon2-ffi": "^2.0.0",
"aws-sdk": "^2.1302.0",
"bent": "^7.3.12",
"cors": "^2.8.5",
"debug": "^4.3.4",

View File

@@ -219,21 +219,6 @@ test('account tests', async(t) => {
});
t.ok(result.statusCode === 201, 'successfully updated a call session limit to an account');
/* try to update an existing limit for an account giving a invalid sid */
try {
result = await request.post(`/Accounts/invalid-sid/Limits`, {
auth: authAdmin,
json: true,
resolveWithFullResponse: true,
body: {
category: 'voice_call_session',
quantity: 205
}
});
} catch (err) {
t.ok(err.statusCode === 400, 'returns 400 bad request if sid param is not a valid uuid');
}
/* query all limits for an account */
result = await request.get(`/Accounts/${sid}/Limits`, {
auth: authAdmin,

View File

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

View File

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

View File

@@ -17,5 +17,4 @@ require('./webapp_tests');
// require('./homer');
require('./call-test');
require('./password-settings');
require('./email_utils');
require('./docker_stop');

View File

@@ -107,20 +107,6 @@ test('service provider tests', async(t) => {
});
t.ok(result.statusCode === 204, 'successfully updated service provider');
/* try to update service providers with invalid sid format*/
try {
result = await request.put(`/ServiceProviders/123`, {
auth: authAdmin,
json: true,
resolveWithFullResponse: true,
body: {
name: 'robb'
}
});
} catch (err) {
t.ok(err.statusCode === 400, 'returns 400 bad request if sid param is not a valid uuid');
}
/* add an api key for a service provider */
result = await request.post(`/ApiKeys`, {
auth: authAdmin,

View File

@@ -22,24 +22,6 @@ test('speech credentials tests', async(t) => {
const service_provider_sid = await createServiceProvider(request);
const account_sid = await createAccount(request, service_provider_sid);
/* return 400 if invalid sid param is used */
try {
result = await request.post(`/ServiceProviders/foobarbaz/SpeechCredentials`, {
resolveWithFullResponse: true,
simple: false,
auth: authAdmin,
json: true,
body: {
vendor: 'google',
service_key: jsonKey,
use_for_tts: true,
use_for_stt: true
}
});
} catch (err) {
t.ok(err.statusCode === 400, 'returns 400 bad request if sid param is not a valid uuid');
}
/* add a speech credential to a service provider */
result = await request.post(`/ServiceProviders/${service_provider_sid}/SpeechCredentials`, {
resolveWithFullResponse: true,
@@ -91,8 +73,8 @@ test('speech credentials tests', async(t) => {
t.ok(result.statusCode === 201, 'successfully added speech credential');
const sid1 = result.body.sid;
/* return 403 if invalid account is used - randomSid: bed7ae17-f8b4-4b74-9e5b-4f6318aae9c9 */
result = await request.post(`/Accounts/bed7ae17-f8b4-4b74-9e5b-4f6318aae9c9/SpeechCredentials`, {
/* return 403 if invalid account is used */
result = await request.post(`/Accounts/foobarbaz/SpeechCredentials`, {
resolveWithFullResponse: true,
simple: false,
auth: authUser,
@@ -189,35 +171,6 @@ test('speech credentials tests', async(t) => {
t.ok(result.statusCode === 200 && result.body.stt.status === 'ok', 'successfully tested speech credential for microsoft stt');
}
/* add / test a credential for AWS */
if (process.env.AWS_ACCESS_KEY_ID && process.env.AWS_SECRET_ACCESS_KEY && process.env.AWS_REGION) {
result = await request.post(`/Accounts/${account_sid}/SpeechCredentials`, {
resolveWithFullResponse: true,
auth: authUser,
json: true,
body: {
vendor: 'aws',
use_for_tts: true,
use_for_stt: true,
access_key_id: process.env.AWS_ACCESS_KEY_ID,
secret_access_key: process.env.AWS_SECRET_ACCESS_KEY,
aws_region: process.env.AWS_REGION
}
});
t.ok(result.statusCode === 201, 'successfully added speech credential for AWS');
const ms_sid = result.body.sid;
/* test the speech credential */
result = await request.get(`/Accounts/${account_sid}/SpeechCredentials/${ms_sid}/test`, {
resolveWithFullResponse: true,
auth: authUser,
json: true,
});
//console.log(JSON.stringify(result));
t.ok(result.statusCode === 200 && result.body.tts.status === 'ok', 'successfully tested speech credential for AWS tts');
t.ok(result.statusCode === 200 && result.body.stt.status === 'ok', 'successfully tested speech credential for AWS stt');
}
/* add a credential for wellsaid */
if (process.env.WELLSAID_API_KEY) {
result = await request.post(`/Accounts/${account_sid}/SpeechCredentials`, {
@@ -410,34 +363,9 @@ test('speech credentials tests', async(t) => {
});
t.ok(result.statusCode === 204, 'successfully deleted speech credential');
/* add a credential for nuance */
result = await request.post(`/Accounts/${account_sid}/SpeechCredentials`, {
resolveWithFullResponse: true,
auth: authUser,
json: true,
body: {
vendor: 'nuance',
use_for_stt: true,
use_for_tts: true,
client_id: 'client_id',
secret: 'secret',
nuance_tts_uri: "192.168.1.2:5060",
nuance_stt_uri: "192.168.1.2:5061"
}
});
t.ok(result.statusCode === 201, 'successfully added speech credential for nuance');
const nuance_sid = result.body.sid;
/* delete the credential */
result = await request.delete(`/Accounts/${account_sid}/SpeechCredentials/${nuance_sid}`, {
auth: authUser,
resolveWithFullResponse: true,
});
t.ok(result.statusCode === 204, 'successfully deleted speech credential');
await deleteObjectBySid(request, '/Accounts', account_sid);
await deleteObjectBySid(request, '/ServiceProviders', service_provider_sid);
t.end();
//t.end();
}
catch (err) {
console.error(err);

View File

@@ -23,7 +23,7 @@ test('add an admin user', (t) => {
test('user tests', async(t) => {
const app = require('../app');
const password = 'abcde12345-';
const password = await generateHashedPassword('abcd1234-');
try {
let result;