Compare commits

..

23 Commits

Author SHA1 Message Date
Anton Voylenko
75a2b42d65 update README (#135) 2023-03-27 14:17:03 -04:00
Anton Voylenko
518a9163fb add ENCRYPTION_SECRET variable (#132) 2023-03-25 15:34:09 -04:00
Hoan Luu Huu
5fb4bd7bd1 feat: add nuance on-premise (#131)
* feat: add nuance on-premise

* feat: update fetch nuance credential

* fix: update

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

---------

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

* fixes for preventing non-authorized changes to users

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

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

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

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

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

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

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

* fix: verb specification v2

* fix vulnerabilities

---------

Co-authored-by: Quan HL <quanluuhoang8@gmail.com>
Co-authored-by: Dave Horton <daveh@beachdognet.com>
2023-02-14 17:24:19 -05:00
17 changed files with 3408 additions and 830 deletions

View File

@@ -1,4 +1,4 @@
FROM --platform=linux/amd64 node:18-alpine3.16 as base
FROM --platform=linux/amd64 node:18.14.1-alpine3.16 as base
RUN apk --update --no-cache add --virtual .builds-deps build-base python3
@@ -20,4 +20,4 @@ ARG NODE_ENV
ENV NODE_ENV $NODE_ENV
CMD [ "node", "app.js" ]
CMD [ "node", "app.js" ]

View File

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

View File

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

12
app.js
View File

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

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

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

View File

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

View File

@@ -344,10 +344,20 @@ 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) {

View File

@@ -6,7 +6,7 @@ const Webhook = require('../../models/webhook');
const {promisePool} = require('../../db');
const decorate = require('./decorate');
const sysError = require('../error');
const { validate } = require('verb-specifications');
const { validate } = require('@jambonz/verb-specifications');
const preconditions = {
'add': validateAdd,
'update': validateUpdate

View File

@@ -17,6 +17,7 @@ const {
testNuanceStt,
testNuanceTts,
testDeepgramStt,
testSonioxStt,
testIbmTts,
testIbmStt
} = require('../../utils/speech-utils');
@@ -43,6 +44,8 @@ const encryptCredential = (obj) => {
region,
client_id,
secret,
nuance_tts_uri,
nuance_stt_uri,
use_custom_tts,
custom_tts_endpoint,
use_custom_stt,
@@ -52,7 +55,10 @@ const encryptCredential = (obj) => {
stt_api_key,
stt_region,
riva_server_uri,
instance_id
instance_id,
custom_stt_url,
custom_tts_url,
auth_token = ''
} = obj;
switch (vendor) {
@@ -93,9 +99,10 @@ const encryptCredential = (obj) => {
return encrypt(wsData);
case 'nuance':
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});
const checked = (client_id && secret) || (nuance_tts_uri || nuance_stt_uri);
assert(checked, 'invalid nuance speech credential: either entered client id and\
secret or entered a nuance_tts_uri or nuance_stt_uri');
const nuanceData = JSON.stringify({client_id, secret, nuance_tts_uri, nuance_stt_uri});
return encrypt(nuanceData);
case 'deepgram':
@@ -112,8 +119,17 @@ const encryptCredential = (obj) => {
const nvidiaData = JSON.stringify({ riva_server_uri });
return encrypt(nvidiaData);
case 'soniox':
assert(api_key, 'invalid soniox speech credential: api_key is required');
const sonioxData = JSON.stringify({api_key});
return encrypt(sonioxData);
default:
assert(false, `invalid or missing vendor: ${vendor}`);
if (vendor.startsWith('custom:')) {
const customData = JSON.stringify({auth_token, custom_stt_url, custom_tts_url});
return encrypt(customData);
}
else assert(false, `invalid or missing vendor: ${vendor}`);
}
};
@@ -203,7 +219,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 = obscureKey(o.secret);
obj.secret = o.secret ? obscureKey(o.secret) : null;
}
else if ('deepgram' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
@@ -220,6 +236,16 @@ router.get('/', 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);
obj.custom_stt_url = o.custom_stt_url;
obj.custom_tts_url = o.custom_tts_url;
}
return obj;
}));
} catch (err) {
@@ -268,7 +294,9 @@ 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 = obscureKey(o.secret);
obj.secret = o.secret ? obscureKey(o.secret) : null;
obj.nuance_tts_uri = o.nuance_tts_uri;
obj.nuance_stt_uri = o.nuance_stt_uri;
}
else if ('deepgram' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
@@ -285,6 +313,16 @@ 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);
obj.custom_stt_url = o.custom_stt_url;
obj.custom_tts_url = o.custom_tts_url;
}
res.status(200).json(obj);
} catch (err) {
sysError(logger, res, err);
@@ -314,7 +352,8 @@ router.put('/:sid', async(req, res) => {
const sid = req.params.sid;
const logger = req.app.locals.logger;
try {
const {use_for_tts, use_for_stt, region, aws_region, stt_region, tts_region, riva_server_uri} = req.body;
const {use_for_tts, use_for_stt, region, aws_region, stt_region, tts_region,
riva_server_uri, nuance_tts_uri, nuance_stt_uri} = req.body;
if (typeof use_for_tts === 'undefined' && typeof use_for_stt === 'undefined') {
throw new DbErrorUnprocessableRequest('use_for_tts and use_for_stt are the only updateable fields');
}
@@ -350,7 +389,9 @@ router.put('/:sid', async(req, res) => {
custom_stt_endpoint,
stt_region,
tts_region,
riva_server_uri
riva_server_uri,
nuance_stt_uri,
nuance_tts_uri
};
logger.info({o, newCred}, 'updating speech credential with this new credential');
obj.credential = encryptCredential(newCred);
@@ -402,7 +443,8 @@ router.get('/:sid/test', async(req, res) => {
if (cred.use_for_tts) {
try {
await testGoogleTts(logger, credential);
const {getTtsVoices} = req.app.locals;
await testGoogleTts(logger, getTtsVoices, credential);
results.tts.status = 'ok';
SpeechCredential.ttsTestResult(sid, true);
} catch (err) {
@@ -424,8 +466,9 @@ router.get('/:sid/test', async(req, res) => {
}
else if (cred.vendor === 'aws') {
if (cred.use_for_tts) {
const {getTtsVoices} = req.app.locals;
try {
await testAwsTts(logger, {
await testAwsTts(logger, getTtsVoices, {
accessKeyId: credential.access_key_id,
secretAccessKey: credential.secret_access_key,
region: credential.aws_region || process.env.AWS_REGION
@@ -507,13 +550,16 @@ router.get('/:sid/test', async(req, res) => {
const {
client_id,
secret
secret,
nuance_tts_uri,
nuance_stt_uri
} = credential;
if (cred.use_for_tts) {
try {
await testNuanceTts(logger, getTtsVoices, {
client_id,
secret
secret,
nuance_tts_uri
});
results.tts.status = 'ok';
SpeechCredential.ttsTestResult(sid, true);
@@ -528,7 +574,7 @@ router.get('/:sid/test', async(req, res) => {
}
if (cred.use_for_stt) {
try {
await testNuanceStt(logger, {client_id, secret});
await testNuanceStt(logger, {client_id, secret, nuance_stt_uri});
results.stt.status = 'ok';
SpeechCredential.sttTestResult(sid, true);
} catch (err) {
@@ -583,8 +629,22 @@ router.get('/:sid/test', async(req, res) => {
}
}
}
else if (cred.vendor === 'soniox') {
const {api_key} = credential;
if (cred.use_for_stt) {
try {
await testSonioxStt(logger, {api_key});
results.stt.status = 'ok';
SpeechCredential.sttTestResult(sid, true);
} catch (err) {
results.stt = {status: 'fail', reason: err.message};
SpeechCredential.sttTestResult(sid, false);
}
}
}
res.status(200).json(results);
} catch (err) {
sysError(logger, res, err);
}

View File

@@ -1,6 +1,5 @@
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');
@@ -28,7 +27,8 @@ AND account_subscriptions.pending=0`;
const updateSql = 'UPDATE users set hashed_password = ?, force_change = false WHERE user_sid = ?';
const retrieveStaticIps = 'SELECT * FROM account_static_ips WHERE account_sid = ?';
const validateRequest = async(user_sid, payload) => {
const validateRequest = async(user_sid, req) => {
const payload = req.body;
const {
old_password,
new_password,
@@ -37,12 +37,43 @@ const validateRequest = async(user_sid, payload) => {
email,
email_activation_code,
force_change,
is_active} = payload;
is_active
} = payload;
const [r] = await promisePool.query(retrieveSql, user_sid);
if (r.length === 0) return null;
if (r.length === 0) {
throw new DbErrorBadRequest('Invalid request: user_sid does not exist');
}
const user = r[0];
/* it is not allowed for anyone to promote a user to a higher level of authority */
if (null === payload.account_sid || null === payload.service_provider_sid) {
throw new DbErrorBadRequest('Invalid request: user may not be promoted');
}
if (req.user.hasAccountAuth) {
/* account user may not change modify account_sid or service_provider_sid */
if ('account_sid' in payload && payload.account_sid !== user.account_sid) {
throw new DbErrorBadRequest('Invalid request: user may not be promoted or moved to another account');
}
if ('service_provider_sid' in payload && payload.service_provider_sid !== user.service_provider_sid) {
throw new DbErrorBadRequest('Invalid request: user may not be promoted or moved to another service provider');
}
}
if (req.user.hasServiceProviderAuth) {
if ('service_provider_sid' in payload && payload.service_provider_sid !== user.service_provider_sid) {
throw new DbErrorBadRequest('Invalid request: user may not be promoted or moved to another service provider');
}
}
if ('account_sid' in payload) {
const [r] = await promisePool.query('SELECT * FROM accounts WHERE account_sid = ?', payload.account_sid);
if (r.length === 0) throw new DbErrorBadRequest('Invalid request: account_sid does not exist');
const {service_provider_sid} = r[0];
if (service_provider_sid !== user.service_provider_sid) {
throw new DbErrorBadRequest('Invalid request: user may not be moved to another service provider');
}
}
if ((old_password && !new_password) || (new_password && !old_password)) {
throw new DbErrorBadRequest('new_password and old_password both required');
}
@@ -62,23 +93,18 @@ const validateRequest = async(user_sid, payload) => {
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 (decodedJwt.scope === 'admin') {
if (req.user.hasAdminAuth) {
results = await User.retrieveAll();
}
else if (decodedJwt.scope === 'account') {
results = await User.retrieveAllForAccount(decodedJwt.account_sid, true);
else if (req.user.hasAccountAuth) {
results = await User.retrieveAllForAccount(req.user.account_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}`);
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');
@@ -222,8 +248,6 @@ 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 {
@@ -232,9 +256,9 @@ router.get('/:user_sid', async(req, res) => {
const {hashed_password, ...rest} = user;
if (!user) throw new Error('failure retrieving user');
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) {
if (req.user.hasAdminAuth ||
req.user.hasAccountAuth && req.user.account_sid === user.account_sid ||
req.user.hasServiceProviderAuth && req.user.service_provider_sid === user.service_provider_sid) {
res.status(200).json(rest);
} else {
res.sendStatus(403);
@@ -249,8 +273,7 @@ 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 token = req.user.jwt;
const decodedJwt = jwt.verify(token, process.env.JWT_SECRET);
const {hasAccountAuth, hasServiceProviderAuth, hasAdminAuth} = req.user;
const {
old_password,
new_password,
@@ -266,15 +289,15 @@ router.put('/:user_sid', async(req, res) => {
//if (req.user.user_sid && req.user.user_sid !== user_sid) return res.sendStatus(403);
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) &&
if (!hasAdminAuth &&
!(hasAccountAuth && req.user.account_sid === user[0].account_sid) &&
!(hasServiceProviderAuth && req.user.service_provider_sid === user[0].service_provider_sid) &&
(req.user.user_sid && req.user.user_sid !== user_sid)) {
return res.sendStatus(403);
}
try {
const user = await validateRequest(user_sid, req.body);
const user = await validateRequest(user_sid, req);
if (!user) return res.sendStatus(404);
if (new_password) {
@@ -367,8 +390,6 @@ 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 {
@@ -385,30 +406,27 @@ router.post('/', async(req, res) => {
return res.status(422).json({msg: 'user with this email already exists'});
}
if (decodedJwt.scope === 'admin') {
if (req.user.hasAdminAuth) {
logger.debug({payload}, 'POST /users');
const uuid = await User.make(payload);
res.status(201).json({user_sid: uuid});
}
else if (decodedJwt.scope === 'account') {
else if (req.user.hasAccountAuth) {
logger.debug({payload}, 'POST /users');
const uuid = await User.make({
...payload,
account_sid: decodedJwt.account_sid,
account_sid: req.user.account_sid,
});
res.status(201).json({user_sid: uuid});
}
else if (decodedJwt.scope === 'service_provider') {
else if (req.user.hasServiceProviderAuth) {
logger.debug({payload}, 'POST /users');
const uuid = await User.make({
...payload,
service_provider_sid: decodedJwt.service_provider_sid,
service_provider_sid: req.user.service_provider_sid,
});
res.status(201).json({user_sid: uuid});
}
else {
throw new DbErrorBadRequest(`invalid scope: ${decodedJwt.scope}`);
}
} catch (err) {
sysError(logger, res, err);
}
@@ -417,24 +435,21 @@ router.post('/', async(req, res) => {
router.delete('/:user_sid', async(req, res) => {
const logger = req.app.locals.logger;
const {user_sid} = req.params;
const 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 (decodedJwt.scope === 'admin' && !user.account_sid && !user.service_provider_sid &&
activeAdminUsers.length === 1) {
if (req.user.hasAdminAuth && activeAdminUsers.length === 1) {
throw new Error('cannot delete this admin user - there are no other active admin users');
}
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)) {
if (req.user.hasAdminAuth ||
(req.user.hasAccountAuth && req.user.account_sid === user[0].account_sid) ||
(req.user.hasServiceProviderAuth && req.user.service_provider_sid === user[0].service_provider_sid)) {
await User.remove(user_sid);
//logout user after self-delete
if (decodedJwt.user_sid === user_sid) {
if (req.user.user_sid === user_sid) {
request({
url:'http://localhost:3000/v1/logout',
method: 'POST',
@@ -448,12 +463,11 @@ router.delete('/:user_sid', async(req, res) => {
}
return res.sendStatus(204);
} else {
throw new DbErrorBadRequest(`invalid scope: ${decodedJwt.scope}`);
throw new DbErrorBadRequest('invalid request');
}
} catch (err) {
sysError(logger, res, err);
}
});
module.exports = router;

View File

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

View File

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

View File

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

View File

@@ -1,12 +1,28 @@
const ttsGoogle = require('@google-cloud/text-to-speech');
const sttGoogle = require('@google-cloud/speech').v1p1beta1;
const Polly = require('aws-sdk/clients/polly');
const AWS = require('aws-sdk');
const { TranscribeClient, ListVocabulariesCommand } = require('@aws-sdk/client-transcribe');
const { Deepgram } = require('@deepgram/sdk');
const sdk = require('microsoft-cognitiveservices-speech-sdk');
const { SpeechClient } = require('@soniox/soniox-node');
const bent = require('bent');
const fs = require('fs');
const testSonioxStt = async(logger, credentials) => {
const api_key = credentials;
const soniox = new SpeechClient(api_key);
return new Promise(async(resolve, reject) => {
try {
const result = await soniox.transcribeFileShort('data/test_audio.wav');
if (result.words.length > 0) resolve(result);
else reject(new Error('no transcript returned'));
} catch (error) {
logger.info({error}, 'failed to get soniox transcript');
reject(error);
}
});
};
const testNuanceTts = async(logger, getTtsVoices, credentials) => {
const voices = await getTtsVoices({vendor: 'nuance', credentials});
return voices;
@@ -16,9 +32,10 @@ const testNuanceStt = async(logger, credentials) => {
return true;
};
const testGoogleTts = async(logger, credentials) => {
const client = new ttsGoogle.TextToSpeechClient({credentials});
await client.listVoices();
const testGoogleTts = async(logger, getTtsVoices, credentials) => {
const voices = await getTtsVoices({vendor: 'google', credentials});
return voices;
};
const testGoogleStt = async(logger, credentials) => {
@@ -102,25 +119,33 @@ const testMicrosoftStt = async(logger, credentials) => {
});
};
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 testAwsTts = async(logger, getTtsVoices, credentials) => {
try {
const voices = await getTtsVoices({vendor: 'aws', credentials});
return voices;
} catch (err) {
logger.info({err}, 'testMicrosoftTts - failed to list voices for region ${region}');
throw err;
}
};
const testAwsStt = (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 testAwsStt = async(logger, credentials) => {
try {
const {region, accessKeyId, secretAccessKey} = credentials;
const client = new TranscribeClient({
region,
credentials: {
accessKeyId,
secretAccessKey
}
});
});
const command = new ListVocabulariesCommand({});
const response = await client.send(command);
return response;
} catch (err) {
logger.info({err}, 'testMicrosoftTts - failed to list voices for region ${region}');
throw err;
}
};
const testMicrosoftTts = async(logger, credentials) => {
@@ -180,7 +205,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: {api_key: tts_api_key, region: tts_region}});
const voices = await getTtsVoices({vendor: 'ibm', credentials: {tts_api_key, tts_region}});
return voices;
};
@@ -225,5 +250,6 @@ module.exports = {
testNuanceStt,
testDeepgramStt,
testIbmTts,
testIbmStt
testIbmStt,
testSonioxStt
};

3527
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.0",
"version": "v0.8.1",
"description": "",
"main": "app.js",
"scripts": {
@@ -18,22 +18,22 @@
"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.6.0",
"@jambonz/realtimedb-helpers": "^0.7.0",
"@jambonz/speech-utils": "^0.0.8",
"@jambonz/time-series": "^0.2.5",
"verb-specifications": "^0.0.3",
"argon2-ffi": "^2.0.0",
"aws-sdk": "^2.1152.0",
"@jambonz/verb-specifications": "^0.0.3",
"@soniox/soniox-node": "^1.1.0",
"argon2": "^0.30.3",
"bent": "^7.3.12",
"cors": "^2.8.5",
"debug": "^4.3.4",
"express": "^4.18.1",
"express-rate-limit": "^6.4.0",
"form-data": "^2.5.1",
"form-urlencoded": "^6.1.0",
"helmet": "^5.1.0",
"ibm-watson": "^7.1.2",
"jsonwebtoken": "^9.0.0",

View File

@@ -171,6 +171,35 @@ 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`, {
@@ -300,6 +329,38 @@ test('speech credentials tests', async(t) => {
t.ok(result.statusCode === 204, 'successfully deleted speech credential');
}
/* add a credential for Siniox */
if (process.env.SONIOX_API_KEY) {
result = await request.post(`/Accounts/${account_sid}/SpeechCredentials`, {
resolveWithFullResponse: true,
auth: authUser,
json: true,
body: {
vendor: 'soniox',
use_for_stt: true,
api_key: process.env.SONIOX_API_KEY
}
});
t.ok(result.statusCode === 201, 'successfully added speech credential for soniox');
const ms_sid = result.body.sid;
/* test the speech credential */
result = await request.get(`/Accounts/${account_sid}/SpeechCredentials/${ms_sid}/test`, {
resolveWithFullResponse: true,
auth: authUser,
json: true,
});
console.log(JSON.stringify(result));
t.ok(result.statusCode === 200 && result.body.stt.status === 'ok', 'successfully tested speech credential for soniox');
/* delete the credential */
result = await request.delete(`/Accounts/${account_sid}/SpeechCredentials/${ms_sid}`, {
auth: authUser,
resolveWithFullResponse: true,
});
t.ok(result.statusCode === 204, 'successfully deleted speech credential');
}
/* add a credential for nvidia */
result = await request.post(`/Accounts/${account_sid}/SpeechCredentials`, {
resolveWithFullResponse: true,
@@ -331,9 +392,34 @@ 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);