mirror of
https://github.com/jambonz/jambonz-api-server.git
synced 2026-01-25 02:08:24 +00:00
Compare commits
30 Commits
v0.8.0-rc1
...
v0.8.2-rc1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bb705fe808 | ||
|
|
789a0ba3ff | ||
|
|
27cb7c471a | ||
|
|
39260f0b47 | ||
|
|
75a2b42d65 | ||
|
|
518a9163fb | ||
|
|
5fb4bd7bd1 | ||
|
|
409ad68123 | ||
|
|
17afb7102a | ||
|
|
6e7cb9b332 | ||
|
|
34f83e323c | ||
|
|
00af458cb3 | ||
|
|
389017a5c4 | ||
|
|
c4cc6c51ee | ||
|
|
aea7388ba0 | ||
|
|
3d86292a90 | ||
|
|
08962fe7ba | ||
|
|
e573f6ab06 | ||
|
|
4934e2a1ca | ||
|
|
cc384995ea | ||
|
|
d4506fb8fa | ||
|
|
042a2c37dc | ||
|
|
6da1903dee | ||
|
|
10009d903e | ||
|
|
69a72c5e43 | ||
|
|
f7f3881d70 | ||
|
|
4d48c6946c | ||
|
|
5b48fc8a07 | ||
|
|
f46be95551 | ||
|
|
d4f2be3dc1 |
@@ -1,4 +1,4 @@
|
||||
FROM --platform=linux/amd64 node:18.12.1-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" ]
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
47
README.md
47
README.md
@@ -1,29 +1,44 @@
|
||||
# jambonz-api-server 
|
||||
|
||||
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
12
app.js
@@ -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
10
db/create-admin-user.sql
Normal 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');
|
||||
@@ -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});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -107,7 +107,7 @@ class Model extends Emitter {
|
||||
if (pk.name in obj) throw new DbErrorBadRequest(`primary key ${pk.name} is immutable`);
|
||||
getMysqlConnection((err, conn) => {
|
||||
if (err) return reject(err);
|
||||
conn.query(`UPDATE ${this.table} SET ? WHERE ${pk.name} = '${sid}'`, obj, (err, results, fields) => {
|
||||
conn.query(`UPDATE ${this.table} SET ? WHERE ${pk.name} = ?`, [obj, sid], (err, results, fields) => {
|
||||
conn.release();
|
||||
if (err) return reject(err);
|
||||
resolve(results.affectedRows);
|
||||
|
||||
@@ -46,17 +46,34 @@ const stripPort = (hostport) => {
|
||||
};
|
||||
|
||||
const validateUpdateForCarrier = async(req) => {
|
||||
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');
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -344,10 +361,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) {
|
||||
@@ -775,11 +802,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);
|
||||
|
||||
@@ -24,13 +24,13 @@ router.post('/:sid', async(req, res) => {
|
||||
let service_provider_sid;
|
||||
const {account_sid} = req.user;
|
||||
|
||||
if (!account_sid) {
|
||||
service_provider_sid = parseServiceProviderSid(req);
|
||||
} else {
|
||||
service_provider_sid = req.user.service_provider_sid;
|
||||
}
|
||||
|
||||
try {
|
||||
if (!account_sid) {
|
||||
service_provider_sid = parseServiceProviderSid(req);
|
||||
} else {
|
||||
service_provider_sid = req.user.service_provider_sid;
|
||||
}
|
||||
|
||||
const [template] = await PredefinedCarrier.retrieve(sid);
|
||||
logger.debug({template}, `Retrieved template carrier for sid ${sid}`);
|
||||
if (!template) return res.sendStatus(404);
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
const router = require('express').Router();
|
||||
const sysError = require('../error');
|
||||
const {DbErrorBadRequest} = require('../../utils/errors');
|
||||
const { parseServiceProviderSid } = require('./utils');
|
||||
|
||||
const parseAccountSid = (url) => {
|
||||
const arr = /Accounts\/([^\/]*)/.exec(url);
|
||||
if (arr) return arr[1];
|
||||
};
|
||||
|
||||
const parseServiceProviderSid = (url) => {
|
||||
const arr = /ServiceProviders\/([^\/]*)/.exec(url);
|
||||
if (arr) return arr[1];
|
||||
};
|
||||
|
||||
router.get('/', async(req, res) => {
|
||||
const {logger, queryAlerts, queryAlertsSP} = req.app.locals;
|
||||
try {
|
||||
|
||||
@@ -6,7 +6,7 @@ const Webhook = require('../../models/webhook');
|
||||
const {promisePool} = require('../../db');
|
||||
const decorate = require('./decorate');
|
||||
const sysError = require('../error');
|
||||
const jambonzAppJsonValidator = require('../../utils/jambonz-app-json-validation');
|
||||
const { validate } = require('@jambonz/verb-specifications');
|
||||
const preconditions = {
|
||||
'add': validateAdd,
|
||||
'update': validateUpdate
|
||||
@@ -80,7 +80,11 @@ router.post('/', async(req, res) => {
|
||||
// validate app json if required
|
||||
if (obj['app_json']) {
|
||||
const app_json = JSON.parse(obj['app_json']);
|
||||
jambonzAppJsonValidator(logger, app_json);
|
||||
try {
|
||||
validate(logger, app_json);
|
||||
} catch (err) {
|
||||
throw new DbErrorBadRequest(err);
|
||||
}
|
||||
}
|
||||
|
||||
const uuid = await Application.make(obj);
|
||||
@@ -189,7 +193,11 @@ router.put('/:sid', async(req, res) => {
|
||||
// validate app json if required
|
||||
if (obj['app_json']) {
|
||||
const app_json = JSON.parse(obj['app_json']);
|
||||
jambonzAppJsonValidator(logger, app_json);
|
||||
try {
|
||||
validate(logger, app_json);
|
||||
} catch (err) {
|
||||
throw new DbErrorBadRequest(err);
|
||||
}
|
||||
}
|
||||
|
||||
const rowsAffected = await Application.update(sid, obj);
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
const {DbErrorBadRequest, DbErrorUnprocessableRequest} = require('../../utils/errors');
|
||||
const { BadRequestError, DbErrorBadRequest, DbErrorUnprocessableRequest } = require('../../utils/errors');
|
||||
|
||||
function sysError(logger, res, err) {
|
||||
if (err instanceof BadRequestError) {
|
||||
logger.info(err, err.message);
|
||||
return res.status(400).json({msg: 'Bad request'});
|
||||
}
|
||||
if (err instanceof DbErrorBadRequest) {
|
||||
logger.info(err, 'invalid client request');
|
||||
return res.status(400).json({msg: err.message});
|
||||
|
||||
@@ -28,16 +28,18 @@ router.post('/', async(req, res) => {
|
||||
category,
|
||||
quantity
|
||||
} = req.body;
|
||||
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 service_provider_sid;
|
||||
const account_sid = parseAccountSid(req);
|
||||
if (!account_sid) {
|
||||
if (!req.user.hasServiceProviderAuth && !req.user.hasAdminAuth) {
|
||||
logger.error('POST /SpeechCredentials invalid credentials');
|
||||
return res.sendStatus(403);
|
||||
}
|
||||
service_provider_sid = parseServiceProviderSid(req);
|
||||
}
|
||||
|
||||
let uuid;
|
||||
if (account_sid) {
|
||||
const existing = (await AccountLimits.retrieve(account_sid) || [])
|
||||
@@ -80,10 +82,11 @@ router.post('/', async(req, res) => {
|
||||
*/
|
||||
router.get('/', async(req, res) => {
|
||||
let service_provider_sid;
|
||||
const account_sid = parseAccountSid(req);
|
||||
if (!account_sid) service_provider_sid = parseServiceProviderSid(req);
|
||||
const logger = req.app.locals.logger;
|
||||
|
||||
try {
|
||||
const account_sid = parseAccountSid(req);
|
||||
if (!account_sid) service_provider_sid = parseServiceProviderSid(req);
|
||||
const limits = account_sid ?
|
||||
await AccountLimits.retrieve(account_sid) :
|
||||
await ServiceProviderLimits.retrieve(service_provider_sid);
|
||||
@@ -99,10 +102,11 @@ router.get('/', async(req, res) => {
|
||||
|
||||
router.delete('/', async(req, res) => {
|
||||
const logger = req.app.locals.logger;
|
||||
const account_sid = parseAccountSid(req);
|
||||
const {category} = req.query;
|
||||
const service_provider_sid = parseServiceProviderSid(req);
|
||||
|
||||
try {
|
||||
const account_sid = parseAccountSid(req);
|
||||
const {category} = req.query;
|
||||
const service_provider_sid = parseServiceProviderSid(req);
|
||||
if (account_sid) {
|
||||
if (category) {
|
||||
await promisePool.execute(sqlDeleteAccountLimitsByCategory, [account_sid, category]);
|
||||
|
||||
@@ -15,6 +15,9 @@ 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) {
|
||||
|
||||
@@ -12,7 +12,7 @@ const {hasServiceProviderPermissions, parseServiceProviderSid} = require('./util
|
||||
const sysError = require('../error');
|
||||
const decorate = require('./decorate');
|
||||
const preconditions = {
|
||||
'delete': noActiveAccounts
|
||||
'delete': noActiveAccountsOrUsers
|
||||
};
|
||||
const sqlDeleteSipGateways = `DELETE from sip_gateways
|
||||
WHERE voip_carrier_sid IN (
|
||||
@@ -35,36 +35,64 @@ function validateAdd(req) {
|
||||
}
|
||||
|
||||
async function validateRetrieve(req) {
|
||||
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 ;
|
||||
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;
|
||||
}
|
||||
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) {
|
||||
if (req.user.hasScope('admin')) return ;
|
||||
if (req.user.hasScope('service_provider')) {
|
||||
try {
|
||||
const service_provider_sid = parseServiceProviderSid(req);
|
||||
if (service_provider_sid === req.user.service_provider_sid) return ;
|
||||
|
||||
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;
|
||||
}
|
||||
throw new DbErrorForbidden('insufficient permissions to update service provider');
|
||||
}
|
||||
|
||||
/* can not delete a service provider if it has any active accounts */
|
||||
async function noActiveAccounts(req, sid) {
|
||||
async function noActiveAccountsOrUsers(req, sid) {
|
||||
if (!req.user.hasAdminAuth) {
|
||||
throw new DbErrorForbidden('only admin users can delete a service provider');
|
||||
}
|
||||
const activeAccounts = await ServiceProvider.getForeignKeyReferences('accounts.service_provider_sid', sid);
|
||||
const activeUsers = await ServiceProvider.getForeignKeyReferences('users.service_provider_sid', sid);
|
||||
if (activeAccounts > 0 && activeUsers > 0) throw new DbErrorUnprocessableRequest(
|
||||
'cannot delete service provider with active accounts or users'
|
||||
);
|
||||
|
||||
if (activeAccounts > 0) throw new DbErrorUnprocessableRequest('cannot delete service provider with active accounts');
|
||||
if (activeUsers > 0) throw new DbErrorUnprocessableRequest(
|
||||
'cannot delete service provider with active service provider level users'
|
||||
);
|
||||
|
||||
/* ok we can delete -- no active accounts. remove carriers and speech credentials */
|
||||
await promisePool.execute('DELETE from speech_credentials WHERE service_provider_sid = ?', [sid]);
|
||||
@@ -183,7 +211,6 @@ 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']) {
|
||||
@@ -204,7 +231,6 @@ 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');
|
||||
@@ -234,10 +260,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);
|
||||
@@ -247,15 +273,14 @@ router.put('/:sid', async(req, res) => {
|
||||
const sid = obj[prop]['webhook_sid'];
|
||||
delete obj[prop]['webhook_sid'];
|
||||
await Webhook.update(sid, obj[prop]);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
const sid = await Webhook.make(obj[prop]);
|
||||
obj[`${prop}_sid`] = sid;
|
||||
}
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
obj[`${prop}_sid`] = null;
|
||||
}
|
||||
|
||||
delete obj[prop];
|
||||
}
|
||||
|
||||
@@ -263,6 +288,7 @@ router.put('/:sid', async(req, res) => {
|
||||
if (rowsAffected === 0) {
|
||||
return res.status(404).end();
|
||||
}
|
||||
|
||||
res.status(204).end();
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
|
||||
@@ -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,
|
||||
@@ -51,7 +54,11 @@ const encryptCredential = (obj) => {
|
||||
tts_region,
|
||||
stt_api_key,
|
||||
stt_region,
|
||||
instance_id
|
||||
riva_server_uri,
|
||||
instance_id,
|
||||
custom_stt_url,
|
||||
custom_tts_url,
|
||||
auth_token = ''
|
||||
} = obj;
|
||||
|
||||
switch (vendor) {
|
||||
@@ -92,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':
|
||||
@@ -106,28 +114,45 @@ const encryptCredential = (obj) => {
|
||||
const ibmData = JSON.stringify({tts_api_key, tts_region, stt_api_key, stt_region, instance_id});
|
||||
return encrypt(ibmData);
|
||||
|
||||
case 'nvidia':
|
||||
assert(riva_server_uri, 'invalid riva server uri: riva_server_uri is required');
|
||||
const nvidiaData = JSON.stringify({ riva_server_uri });
|
||||
return encrypt(nvidiaData);
|
||||
|
||||
case 'soniox':
|
||||
assert(api_key, 'invalid soniox speech credential: api_key is required');
|
||||
const sonioxData = JSON.stringify({api_key});
|
||||
return encrypt(sonioxData);
|
||||
|
||||
default:
|
||||
assert(false, `invalid or missing vendor: ${vendor}`);
|
||||
if (vendor.startsWith('custom:')) {
|
||||
const customData = JSON.stringify({auth_token, custom_stt_url, custom_tts_url});
|
||||
return encrypt(customData);
|
||||
}
|
||||
else assert(false, `invalid or missing vendor: ${vendor}`);
|
||||
}
|
||||
};
|
||||
|
||||
router.post('/', async(req, res) => {
|
||||
const logger = req.app.locals.logger;
|
||||
const {
|
||||
use_for_stt,
|
||||
use_for_tts,
|
||||
vendor,
|
||||
} = req.body;
|
||||
const account_sid = req.user.account_sid || req.body.account_sid;
|
||||
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 {
|
||||
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 encrypted_credential = encryptCredential(req.body);
|
||||
const uuid = await SpeechCredential.make({
|
||||
account_sid,
|
||||
@@ -147,10 +172,11 @@ 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) :
|
||||
@@ -197,7 +223,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));
|
||||
@@ -210,6 +236,19 @@ router.get('/', async(req, res) => {
|
||||
obj.stt_api_key = obscureKey(o.stt_api_key);
|
||||
obj.stt_region = o.stt_region;
|
||||
obj.instance_id = o.instance_id;
|
||||
} else if ('nvidia' == obj.vendor) {
|
||||
const o = JSON.parse(decrypt(credential));
|
||||
obj.riva_server_uri = o.riva_server_uri;
|
||||
}
|
||||
else if ('soniox' === obj.vendor) {
|
||||
const o = JSON.parse(decrypt(credential));
|
||||
obj.api_key = obscureKey(o.api_key);
|
||||
}
|
||||
else if (obj.vendor.startsWith('custom:')) {
|
||||
const o = JSON.parse(decrypt(credential));
|
||||
obj.auth_token = obscureKey(o.auth_token);
|
||||
obj.custom_stt_url = o.custom_stt_url;
|
||||
obj.custom_tts_url = o.custom_tts_url;
|
||||
}
|
||||
return obj;
|
||||
}));
|
||||
@@ -259,7 +298,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));
|
||||
@@ -272,6 +313,19 @@ router.get('/:sid', async(req, res) => {
|
||||
obj.stt_api_key = obscureKey(o.stt_api_key);
|
||||
obj.stt_region = o.stt_region;
|
||||
obj.instance_id = o.instance_id;
|
||||
} else if ('nvidia' == obj.vendor) {
|
||||
const o = JSON.parse(decrypt(credential));
|
||||
obj.riva_server_uri = o.riva_server_uri;
|
||||
}
|
||||
else if ('soniox' === obj.vendor) {
|
||||
const o = JSON.parse(decrypt(credential));
|
||||
obj.api_key = obscureKey(o.api_key);
|
||||
}
|
||||
else if (obj.vendor.startsWith('custom:')) {
|
||||
const o = JSON.parse(decrypt(credential));
|
||||
obj.auth_token = obscureKey(o.auth_token);
|
||||
obj.custom_stt_url = o.custom_stt_url;
|
||||
obj.custom_tts_url = o.custom_tts_url;
|
||||
}
|
||||
res.status(200).json(obj);
|
||||
} catch (err) {
|
||||
@@ -302,7 +356,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} = 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');
|
||||
}
|
||||
@@ -337,7 +392,10 @@ router.put('/:sid', async(req, res) => {
|
||||
use_custom_stt,
|
||||
custom_stt_endpoint,
|
||||
stt_region,
|
||||
tts_region
|
||||
tts_region,
|
||||
riva_server_uri,
|
||||
nuance_stt_uri,
|
||||
nuance_tts_uri
|
||||
};
|
||||
logger.info({o, newCred}, 'updating speech credential with this new credential');
|
||||
obj.credential = encryptCredential(newCred);
|
||||
@@ -389,7 +447,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) {
|
||||
@@ -411,8 +470,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
|
||||
@@ -494,13 +554,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);
|
||||
@@ -515,7 +578,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) {
|
||||
@@ -570,8 +633,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);
|
||||
}
|
||||
|
||||
@@ -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,7 +28,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,15 +38,53 @@ 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 (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');
|
||||
@@ -62,23 +101,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 +256,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 +264,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 +281,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 +297,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,11 +398,12 @@ 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);
|
||||
|
||||
@@ -385,30 +417,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,23 +446,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' && 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',
|
||||
@@ -447,12 +474,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;
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
const { v4: uuid } = require('uuid');
|
||||
const { v4: uuid, validate } = require('uuid');
|
||||
const bent = require('bent');
|
||||
const Account = require('../../models/account');
|
||||
const {promisePool} = require('../../db');
|
||||
const {cancelSubscription, detachPaymentMethod} = require('../../utils/stripe-utils');
|
||||
const freePlans = require('../../utils/free_plans');
|
||||
const { BadRequestError, DbErrorBadRequest } = require('../../utils/errors');
|
||||
const insertAccountSubscriptionSql = `INSERT INTO account_subscriptions
|
||||
(account_subscription_sid, account_sid)
|
||||
values (?, ?)`;
|
||||
@@ -139,37 +140,76 @@ const createTestAlerts = async(writeAlerts, AlertType, account_sid) => {
|
||||
|
||||
const parseServiceProviderSid = (req) => {
|
||||
const arr = /ServiceProviders\/([^\/]*)/.exec(req.originalUrl);
|
||||
if (arr) return arr[1];
|
||||
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];
|
||||
}
|
||||
};
|
||||
|
||||
const parseAccountSid = (req) => {
|
||||
const arr = /Accounts\/([^\/]*)/.exec(req.originalUrl);
|
||||
if (arr) return arr[1];
|
||||
if (arr) {
|
||||
const sid = arr[1];
|
||||
const sid_validation = validate(sid);
|
||||
if (!sid_validation) {
|
||||
throw new BadRequestError('invalid account_sid format');
|
||||
}
|
||||
|
||||
return arr[1];
|
||||
}
|
||||
};
|
||||
|
||||
const hasAccountPermissions = (req, res, next) => {
|
||||
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();
|
||||
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;
|
||||
}
|
||||
res.status(403).json({
|
||||
status: 'fail',
|
||||
message: 'insufficient privileges'
|
||||
});
|
||||
};
|
||||
|
||||
const hasServiceProviderPermissions = (req, res, next) => {
|
||||
if (req.user.hasScope('admin')) return next();
|
||||
if (req.user.hasScope('service_provider')) {
|
||||
const service_provider_sid = parseServiceProviderSid(req);
|
||||
if (service_provider_sid === req.user.service_provider_sid) return next();
|
||||
try {
|
||||
if (req.user.hasScope('admin')) {
|
||||
return next();
|
||||
}
|
||||
|
||||
if (req.user.hasScope('service_provider')) {
|
||||
const service_provider_sid = parseServiceProviderSid(req);
|
||||
if (service_provider_sid === req.user.service_provider_sid) {
|
||||
return next();
|
||||
}
|
||||
}
|
||||
|
||||
res.status(403).json({
|
||||
status: 'fail',
|
||||
message: 'insufficient privileges'
|
||||
});
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
res.status(403).json({
|
||||
status: 'fail',
|
||||
message: 'insufficient privileges'
|
||||
});
|
||||
};
|
||||
|
||||
const checkLimits = async(req, res, next) => {
|
||||
@@ -274,6 +314,31 @@ 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,
|
||||
@@ -284,5 +349,6 @@ module.exports = {
|
||||
hasServiceProviderPermissions,
|
||||
checkLimits,
|
||||
enableSubspace,
|
||||
disableSubspace
|
||||
disableSubspace,
|
||||
validatePasswordSettings
|
||||
};
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
const {DbErrorBadRequest, DbErrorUnprocessableRequest, DbErrorForbidden} = require('../utils/errors');
|
||||
const {
|
||||
BadRequestError,
|
||||
DbErrorBadRequest,
|
||||
DbErrorUnprocessableRequest,
|
||||
DbErrorForbidden
|
||||
} = require('../utils/errors');
|
||||
|
||||
function sysError(logger, res, err) {
|
||||
if (err instanceof BadRequestError) {
|
||||
logger.info(err, err.message);
|
||||
return res.status(400).json({msg: 'Bad request'});
|
||||
}
|
||||
if (err instanceof DbErrorBadRequest) {
|
||||
logger.info(err, 'invalid client request');
|
||||
return res.status(400).json({msg: err.message});
|
||||
|
||||
@@ -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:
|
||||
@@ -4262,6 +4477,8 @@ components:
|
||||
type: boolean
|
||||
stt_tested_ok:
|
||||
type: boolean
|
||||
riva_server_uri:
|
||||
type: string
|
||||
SpeechCredentialUpdate:
|
||||
properties:
|
||||
use_for_tts:
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
const formData = require('form-data');
|
||||
const Mailgun = require('mailgun.js');
|
||||
const mailgun = new Mailgun(formData);
|
||||
const bent = require('bent');
|
||||
const validateEmail = (email) => {
|
||||
// eslint-disable-next-line max-len
|
||||
const re = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
|
||||
@@ -8,6 +9,44 @@ const validateEmail = (email) => {
|
||||
};
|
||||
|
||||
const emailSimpleText = async(logger, to, subject, text) => {
|
||||
const from = 'jambonz Support <support@jambonz.org>';
|
||||
if (process.env.CUSTOM_EMAIL_VENDOR_URL) {
|
||||
await sendEmailByCustomVendor(logger, from, to, subject, text);
|
||||
} else {
|
||||
await sendEmailByMailgun(logger, from, to, subject, text);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
const sendEmailByCustomVendor = async(logger, from, to, subject, text) => {
|
||||
try {
|
||||
const post = bent('POST', {
|
||||
'Content-Type': 'application/json',
|
||||
...((process.env.CUSTOM_EMAIL_VENDOR_USERNAME && process.env.CUSTOM_EMAIL_VENDOR_PASSWORD) &&
|
||||
({
|
||||
'Authorization':`Basic ${Buffer.from(
|
||||
`${process.env.CUSTOM_EMAIL_VENDOR_USERNAME}:${process.env.CUSTOM_EMAIL_VENDOR_PASSWORD}`
|
||||
).toString('base64')}`
|
||||
}))
|
||||
});
|
||||
|
||||
const res = await post(process.env.CUSTOM_EMAIL_VENDOR_URL, {
|
||||
from,
|
||||
to,
|
||||
subject,
|
||||
text
|
||||
});
|
||||
logger.debug({
|
||||
res
|
||||
}, 'sent email to custom vendor.');
|
||||
} catch (err) {
|
||||
logger.info({
|
||||
err
|
||||
}, 'Error sending email From Custom email vendor');
|
||||
}
|
||||
};
|
||||
|
||||
const sendEmailByMailgun = async(logger, from, to, subject, text) => {
|
||||
const mg = mailgun.client({
|
||||
username: 'api',
|
||||
key: process.env.MAILGUN_API_KEY
|
||||
@@ -17,14 +56,18 @@ const emailSimpleText = async(logger, to, subject, text) => {
|
||||
|
||||
try {
|
||||
const res = await mg.messages.create(process.env.MAILGUN_DOMAIN, {
|
||||
from: 'jambonz Support <support@jambonz.org>',
|
||||
from,
|
||||
to,
|
||||
subject,
|
||||
text
|
||||
});
|
||||
logger.debug({res}, 'sent email');
|
||||
logger.debug({
|
||||
res
|
||||
}, 'sent email');
|
||||
} catch (err) {
|
||||
logger.info({err}, 'Error sending email');
|
||||
logger.info({
|
||||
err
|
||||
}, 'Error sending email From mailgun');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
class BadRequestError extends Error {
|
||||
constructor(msg) {
|
||||
super(msg);
|
||||
}
|
||||
}
|
||||
|
||||
class DbError extends Error {
|
||||
constructor(msg) {
|
||||
super(msg);
|
||||
@@ -23,6 +29,7 @@ class DbErrorForbidden extends DbError {
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
BadRequestError,
|
||||
DbError,
|
||||
DbErrorBadRequest,
|
||||
DbErrorUnprocessableRequest,
|
||||
|
||||
@@ -1,112 +0,0 @@
|
||||
const {DbErrorBadRequest} = require('../utils/errors');
|
||||
const assert = require('assert');
|
||||
const _specData = require('./specs');
|
||||
const specs = new Map();
|
||||
for (const key in _specData) { specs.set(key, _specData[key]); }
|
||||
|
||||
function normalizeJambones(logger, obj) {
|
||||
if (!Array.isArray(obj)) throw new DbErrorBadRequest('malformed jambonz payload: must be array');
|
||||
const document = [];
|
||||
for (const tdata of obj) {
|
||||
if (typeof tdata !== 'object') throw new DbErrorBadRequest('malformed jambonz payload: must be array of objects');
|
||||
if ('verb' in tdata) {
|
||||
// {verb: 'say', text: 'foo..bar'..}
|
||||
const name = tdata.verb;
|
||||
const o = {};
|
||||
Object.keys(tdata)
|
||||
.filter((k) => k !== 'verb')
|
||||
.forEach((k) => o[k] = tdata[k]);
|
||||
const o2 = {};
|
||||
o2[name] = o;
|
||||
document.push(o2);
|
||||
}
|
||||
else if (Object.keys(tdata).length === 1) {
|
||||
// {'say': {..}}
|
||||
document.push(tdata);
|
||||
}
|
||||
else {
|
||||
logger.info(tdata, 'malformed jambonz payload: missing verb property');
|
||||
throw new DbErrorBadRequest('malformed jambonz payload: missing verb property');
|
||||
}
|
||||
}
|
||||
logger.debug({ document }, `normalizeJambones: returning document with ${document.length} tasks`);
|
||||
return document;
|
||||
}
|
||||
|
||||
function validate(logger, obj) {
|
||||
normalizeJambones(logger, obj).map((tdata) => {
|
||||
const keys = Object.keys(tdata);
|
||||
const name = keys[0];
|
||||
const data = tdata[name];
|
||||
validateVerb(name, data, logger);
|
||||
});
|
||||
}
|
||||
|
||||
function validateVerb(name, data, logger) {
|
||||
logger.debug(`validating ${name} with data ${JSON.stringify(data)}`);
|
||||
// validate the instruction is supported
|
||||
if (!specs.has(name)) throw new DbErrorBadRequest(`invalid instruction: ${name}`);
|
||||
|
||||
// check type of each element and make sure required elements are present
|
||||
const specData = specs.get(name);
|
||||
let required = specData.required || [];
|
||||
for (const dKey in data) {
|
||||
if (dKey in specData.properties) {
|
||||
const dVal = data[dKey];
|
||||
const dSpec = specData.properties[dKey];
|
||||
logger.debug(`Task:validate validating property ${dKey} with value ${JSON.stringify(dVal)}`);
|
||||
|
||||
if (typeof dSpec === 'string' && dSpec === 'array') {
|
||||
if (!Array.isArray(dVal)) throw new DbErrorBadRequest(`${name}: property ${dKey} is not an array`);
|
||||
}
|
||||
else if (typeof dSpec === 'string' && dSpec.includes('|')) {
|
||||
const types = dSpec.split('|').map((t) => t.trim());
|
||||
if (!types.includes(typeof dVal) && !(types.includes('array') && Array.isArray(dVal))) {
|
||||
throw new DbErrorBadRequest(`${name}: property ${dKey} has invalid data type, must be one of ${types}`);
|
||||
}
|
||||
}
|
||||
else if (typeof dSpec === 'string' && ['number', 'string', 'object', 'boolean'].includes(dSpec)) {
|
||||
// simple types
|
||||
if (typeof dVal !== specData.properties[dKey]) {
|
||||
throw new DbErrorBadRequest(`${name}: property ${dKey} has invalid data type`);
|
||||
}
|
||||
}
|
||||
else if (Array.isArray(dSpec) && dSpec[0].startsWith('#')) {
|
||||
const name = dSpec[0].slice(1);
|
||||
for (const item of dVal) {
|
||||
validate(name, item);
|
||||
}
|
||||
}
|
||||
else if (typeof dSpec === 'object') {
|
||||
// complex types
|
||||
const type = dSpec.type;
|
||||
assert.ok(['number', 'string', 'object', 'boolean'].includes(type),
|
||||
`invalid or missing type in spec ${JSON.stringify(dSpec)}`);
|
||||
if (type === 'string' && dSpec.enum) {
|
||||
assert.ok(Array.isArray(dSpec.enum), `enum must be an array ${JSON.stringify(dSpec.enum)}`);
|
||||
if (!dSpec.enum.includes(dVal)) {
|
||||
throw new DbErrorBadRequest(`invalid value ${dVal} must be one of ${dSpec.enum}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (typeof dSpec === 'string' && dSpec.startsWith('#')) {
|
||||
// reference to another datatype (i.e. nested type)
|
||||
const name = dSpec.slice(1);
|
||||
//const obj = {};
|
||||
//obj[name] = dVal;
|
||||
validate(name, dVal);
|
||||
}
|
||||
else {
|
||||
assert.ok(0, `invalid spec ${JSON.stringify(dSpec)}`);
|
||||
}
|
||||
required = required.filter((item) => item !== dKey);
|
||||
}
|
||||
else if (dKey === '_') {
|
||||
/* no op: allow arbitrary info to be carried here, used by conference e.g in transfer */
|
||||
}
|
||||
else throw new DbErrorBadRequest(`${name}: unknown property ${dKey}`);
|
||||
}
|
||||
if (required.length > 0) throw new DbErrorBadRequest(`${name}: missing value for ${required}`);
|
||||
}
|
||||
|
||||
module.exports = validate;
|
||||
@@ -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');
|
||||
|
||||
@@ -1,759 +0,0 @@
|
||||
{
|
||||
"sip:decline": {
|
||||
"properties": {
|
||||
"id": "string",
|
||||
"status": "number",
|
||||
"reason": "string",
|
||||
"headers": "object"
|
||||
},
|
||||
"required": [
|
||||
"status"
|
||||
]
|
||||
},
|
||||
"sip:request": {
|
||||
"properties": {
|
||||
"id": "string",
|
||||
"method": "string",
|
||||
"body": "string",
|
||||
"headers": "object",
|
||||
"actionHook": "object|string"
|
||||
},
|
||||
"required": [
|
||||
"method"
|
||||
]
|
||||
},
|
||||
"sip:refer": {
|
||||
"properties": {
|
||||
"id": "string",
|
||||
"referTo": "string",
|
||||
"referredBy": "string",
|
||||
"headers": "object",
|
||||
"actionHook": "object|string",
|
||||
"eventHook": "object|string"
|
||||
},
|
||||
"required": [
|
||||
"referTo"
|
||||
]
|
||||
},
|
||||
"config": {
|
||||
"properties": {
|
||||
"id": "string",
|
||||
"synthesizer": "#synthesizer",
|
||||
"recognizer": "#recognizer",
|
||||
"bargeIn": "#bargeIn",
|
||||
"record": "#recordOptions",
|
||||
"amd": "#amd",
|
||||
"notifyEvents": "boolean"
|
||||
},
|
||||
"required": []
|
||||
},
|
||||
"bargeIn": {
|
||||
"properties": {
|
||||
"enable": "boolean",
|
||||
"sticky": "boolean",
|
||||
"actionHook": "object|string",
|
||||
"input": "array",
|
||||
"finishOnKey": "string",
|
||||
"numDigits": "number",
|
||||
"minDigits": "number",
|
||||
"maxDigits": "number",
|
||||
"interDigitTimeout": "number",
|
||||
"dtmfBargein": "boolean",
|
||||
"minBargeinWordCount": "number"
|
||||
},
|
||||
"required": [
|
||||
"enable"
|
||||
]
|
||||
},
|
||||
"dequeue": {
|
||||
"properties": {
|
||||
"id": "string",
|
||||
"name": "string",
|
||||
"actionHook": "object|string",
|
||||
"timeout": "number",
|
||||
"beep": "boolean"
|
||||
},
|
||||
"required": [
|
||||
"name"
|
||||
]
|
||||
},
|
||||
"enqueue": {
|
||||
"properties": {
|
||||
"id": "string",
|
||||
"name": "string",
|
||||
"actionHook": "object|string",
|
||||
"waitHook": "object|string",
|
||||
"_": "object"
|
||||
},
|
||||
"required": [
|
||||
"name"
|
||||
]
|
||||
},
|
||||
"leave": {
|
||||
"properties": {
|
||||
"id": "string"
|
||||
}
|
||||
},
|
||||
"hangup": {
|
||||
"properties": {
|
||||
"id": "string",
|
||||
"headers": "object"
|
||||
},
|
||||
"required": [
|
||||
]
|
||||
},
|
||||
"play": {
|
||||
"properties": {
|
||||
"id": "string",
|
||||
"url": "string|array",
|
||||
"loop": "number|string",
|
||||
"earlyMedia": "boolean",
|
||||
"seekOffset": "number|string",
|
||||
"timeoutSecs": "number|string",
|
||||
"actionHook": "object|string"
|
||||
},
|
||||
"required": [
|
||||
"url"
|
||||
]
|
||||
},
|
||||
"say": {
|
||||
"properties": {
|
||||
"id": "string",
|
||||
"text": "string|array",
|
||||
"loop": "number|string",
|
||||
"synthesizer": "#synthesizer",
|
||||
"earlyMedia": "boolean",
|
||||
"disableTtsCache": "boolean"
|
||||
},
|
||||
"required": [
|
||||
"text"
|
||||
]
|
||||
},
|
||||
"gather": {
|
||||
"properties": {
|
||||
"id": "string",
|
||||
"actionHook": "object|string",
|
||||
"finishOnKey": "string",
|
||||
"input": "array",
|
||||
"numDigits": "number",
|
||||
"minDigits": "number",
|
||||
"maxDigits": "number",
|
||||
"interDigitTimeout": "number",
|
||||
"partialResultHook": "object|string",
|
||||
"speechTimeout": "number",
|
||||
"listenDuringPrompt": "boolean",
|
||||
"dtmfBargein": "boolean",
|
||||
"bargein": "boolean",
|
||||
"minBargeinWordCount": "number",
|
||||
"timeout": "number",
|
||||
"recognizer": "#recognizer",
|
||||
"play": "#play",
|
||||
"say": "#say"
|
||||
},
|
||||
"required": [
|
||||
]
|
||||
},
|
||||
"conference": {
|
||||
"properties": {
|
||||
"id": "string",
|
||||
"name": "string",
|
||||
"beep": "boolean",
|
||||
"startConferenceOnEnter": "boolean",
|
||||
"endConferenceOnExit": "boolean",
|
||||
"maxParticipants": "number",
|
||||
"joinMuted": "boolean",
|
||||
"actionHook": "object|string",
|
||||
"waitHook": "object|string",
|
||||
"statusEvents": "array",
|
||||
"statusHook": "object|string",
|
||||
"enterHook": "object|string",
|
||||
"record": "#record"
|
||||
},
|
||||
"required": [
|
||||
"name"
|
||||
]
|
||||
},
|
||||
"dial": {
|
||||
"properties": {
|
||||
"id": "string",
|
||||
"actionHook": "object|string",
|
||||
"answerOnBridge": "boolean",
|
||||
"callerId": "string",
|
||||
"confirmHook": "object|string",
|
||||
"referHook": "object|string",
|
||||
"dialMusic": "string",
|
||||
"dtmfCapture": "object",
|
||||
"dtmfHook": "object|string",
|
||||
"headers": "object",
|
||||
"listen": "#listen",
|
||||
"target": ["#target"],
|
||||
"timeLimit": "number",
|
||||
"timeout": "number",
|
||||
"proxy": "string",
|
||||
"transcribe": "#transcribe",
|
||||
"amd": "#amd"
|
||||
},
|
||||
"required": [
|
||||
"target"
|
||||
]
|
||||
},
|
||||
"dialogflow": {
|
||||
"properties": {
|
||||
"id": "string",
|
||||
"credentials": "object|string",
|
||||
"project": "string",
|
||||
"environment": "string",
|
||||
"region": {
|
||||
"type": "string",
|
||||
"enum": ["europe-west1", "europe-west2", "australia-southeast1", "asia-northeast1"]
|
||||
},
|
||||
"lang": "string",
|
||||
"actionHook": "object|string",
|
||||
"eventHook": "object|string",
|
||||
"events": "[string]",
|
||||
"welcomeEvent": "string",
|
||||
"welcomeEventParams": "object",
|
||||
"noInputTimeout": "number",
|
||||
"noInputEvent": "string",
|
||||
"passDtmfAsTextInput": "boolean",
|
||||
"thinkingMusic": "string",
|
||||
"tts": "#synthesizer",
|
||||
"bargein": "boolean"
|
||||
},
|
||||
"required": [
|
||||
"project",
|
||||
"credentials",
|
||||
"lang"
|
||||
]
|
||||
},
|
||||
"dtmf": {
|
||||
"properties": {
|
||||
"id": "string",
|
||||
"dtmf": "string",
|
||||
"duration": "number"
|
||||
},
|
||||
"required": [
|
||||
"dtmf"
|
||||
]
|
||||
},
|
||||
"lex": {
|
||||
"properties": {
|
||||
"id": "string",
|
||||
"botId": "string",
|
||||
"botAlias": "string",
|
||||
"credentials": "object",
|
||||
"region": "string",
|
||||
"locale": "string",
|
||||
"intent": "#lexIntent",
|
||||
"welcomeMessage": "string",
|
||||
"metadata": "object",
|
||||
"bargein": "boolean",
|
||||
"passDtmf": "boolean",
|
||||
"actionHook": "object|string",
|
||||
"eventHook": "object|string",
|
||||
"noInputTimeout": "number",
|
||||
"tts": "#synthesizer"
|
||||
},
|
||||
"required": [
|
||||
"botId",
|
||||
"botAlias",
|
||||
"region",
|
||||
"credentials"
|
||||
]
|
||||
},
|
||||
"listen": {
|
||||
"properties": {
|
||||
"id": "string",
|
||||
"actionHook": "object|string",
|
||||
"auth": "#auth",
|
||||
"finishOnKey": "string",
|
||||
"maxLength": "number",
|
||||
"metadata": "object",
|
||||
"mixType": {
|
||||
"type": "string",
|
||||
"enum": ["mono", "stereo", "mixed"]
|
||||
},
|
||||
"passDtmf": "boolean",
|
||||
"playBeep": "boolean",
|
||||
"sampleRate": "number",
|
||||
"timeout": "number",
|
||||
"transcribe": "#transcribe",
|
||||
"url": "string",
|
||||
"wsAuth": "#auth",
|
||||
"earlyMedia": "boolean"
|
||||
},
|
||||
"required": [
|
||||
"url"
|
||||
]
|
||||
},
|
||||
"message": {
|
||||
"properties": {
|
||||
"id": "string",
|
||||
"carrier": "string",
|
||||
"account_sid": "string",
|
||||
"message_sid": "string",
|
||||
"to": "string",
|
||||
"from": "string",
|
||||
"text": "string",
|
||||
"media": "string|array",
|
||||
"actionHook": "object|string"
|
||||
},
|
||||
"required": [
|
||||
"to",
|
||||
"from"
|
||||
]
|
||||
},
|
||||
"pause": {
|
||||
"properties": {
|
||||
"id": "string",
|
||||
"length": "number"
|
||||
},
|
||||
"required": [
|
||||
"length"
|
||||
]
|
||||
},
|
||||
"rasa": {
|
||||
"properties": {
|
||||
"id": "string",
|
||||
"url": "string",
|
||||
"recognizer": "#recognizer",
|
||||
"tts": "#synthesizer",
|
||||
"prompt": "string",
|
||||
"actionHook": "object|string",
|
||||
"eventHook": "object|string"
|
||||
},
|
||||
"required": [
|
||||
"url"
|
||||
]
|
||||
},
|
||||
"record": {
|
||||
"properties": {
|
||||
"path": "string"
|
||||
},
|
||||
"required": [
|
||||
"path"
|
||||
]
|
||||
},
|
||||
"recordOptions": {
|
||||
"properties": {
|
||||
"action": {
|
||||
"type": "string",
|
||||
"enum": ["startCallRecording", "stopCallRecording", "pauseCallRecording", "resumeCallRecording"]
|
||||
},
|
||||
"recordingID": "string",
|
||||
"siprecServerURL": "string"
|
||||
},
|
||||
"required": [
|
||||
"action"
|
||||
]
|
||||
},
|
||||
"redirect": {
|
||||
"properties": {
|
||||
"id": "string",
|
||||
"actionHook": "object|string"
|
||||
},
|
||||
"required": [
|
||||
"actionHook"
|
||||
]
|
||||
},
|
||||
"rest:dial": {
|
||||
"properties": {
|
||||
"id": "string",
|
||||
"account_sid": "string",
|
||||
"application_sid": "string",
|
||||
"call_hook": "object|string",
|
||||
"call_status_hook": "object|string",
|
||||
"from": "string",
|
||||
"fromHost": "string",
|
||||
"speech_synthesis_vendor": "string",
|
||||
"speech_synthesis_voice": "string",
|
||||
"speech_synthesis_language": "string",
|
||||
"speech_recognizer_vendor": "string",
|
||||
"speech_recognizer_language": "string",
|
||||
"tag": "object",
|
||||
"to": "#target",
|
||||
"headers": "object",
|
||||
"timeout": "number"
|
||||
},
|
||||
"required": [
|
||||
"call_hook",
|
||||
"from",
|
||||
"to"
|
||||
]
|
||||
},
|
||||
"tag": {
|
||||
"properties": {
|
||||
"id": "string",
|
||||
"data": "object"
|
||||
},
|
||||
"required": [
|
||||
"data"
|
||||
]
|
||||
},
|
||||
"transcribe": {
|
||||
"properties": {
|
||||
"id": "string",
|
||||
"transcriptionHook": "string",
|
||||
"recognizer": "#recognizer",
|
||||
"earlyMedia": "boolean"
|
||||
},
|
||||
"required": [
|
||||
"recognizer"
|
||||
]
|
||||
},
|
||||
"target": {
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["phone", "sip", "user", "teams"]
|
||||
},
|
||||
"confirmHook": "object|string",
|
||||
"method": {
|
||||
"type": "string",
|
||||
"enum": ["GET", "POST"]
|
||||
},
|
||||
"headers": "object",
|
||||
"from": "#dialFrom",
|
||||
"name": "string",
|
||||
"number": "string",
|
||||
"sipUri": "string",
|
||||
"auth": "#auth",
|
||||
"vmail": "boolean",
|
||||
"tenant": "string",
|
||||
"trunk": "string",
|
||||
"overrideTo": "string"
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
]
|
||||
},
|
||||
"dialFrom": {
|
||||
"properties": {
|
||||
"user": "string",
|
||||
"host": "string"
|
||||
},
|
||||
"required": [
|
||||
]
|
||||
},
|
||||
"auth": {
|
||||
"properties": {
|
||||
"username": "string",
|
||||
"password": "string"
|
||||
},
|
||||
"required": [
|
||||
"username",
|
||||
"password"
|
||||
]
|
||||
},
|
||||
"synthesizer": {
|
||||
"properties": {
|
||||
"vendor": {
|
||||
"type": "string",
|
||||
"enum": ["google", "aws", "polly", "microsoft", "nuance", "ibm", "default"]
|
||||
},
|
||||
"language": "string",
|
||||
"voice": "string",
|
||||
"engine": {
|
||||
"type": "string",
|
||||
"enum": ["standard", "neural"]
|
||||
},
|
||||
"gender": {
|
||||
"type": "string",
|
||||
"enum": ["MALE", "FEMALE", "NEUTRAL"]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"vendor"
|
||||
]
|
||||
},
|
||||
"recognizer": {
|
||||
"properties": {
|
||||
"vendor": {
|
||||
"type": "string",
|
||||
"enum": ["google", "aws", "microsoft", "nuance", "deepgram", "ibm", "default"]
|
||||
},
|
||||
"language": "string",
|
||||
"vad": "#vad",
|
||||
"hints": "array",
|
||||
"hintsBoost": "number",
|
||||
"altLanguages": "array",
|
||||
"profanityFilter": "boolean",
|
||||
"interim": "boolean",
|
||||
"singleUtterance": "boolean",
|
||||
"dualChannel": "boolean",
|
||||
"separateRecognitionPerChannel": "boolean",
|
||||
"punctuation": "boolean",
|
||||
"enhancedModel": "boolean",
|
||||
"words": "boolean",
|
||||
"diarization": "boolean",
|
||||
"diarizationMinSpeakers": "number",
|
||||
"diarizationMaxSpeakers": "number",
|
||||
"interactionType": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"unspecified",
|
||||
"discussion",
|
||||
"presentation",
|
||||
"phone_call",
|
||||
"voicemail",
|
||||
"voice_search",
|
||||
"voice_command",
|
||||
"dictation"
|
||||
]
|
||||
},
|
||||
"naicsCode": "number",
|
||||
"identifyChannels": "boolean",
|
||||
"vocabularyName": "string",
|
||||
"vocabularyFilterName": "string",
|
||||
"filterMethod": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"remove",
|
||||
"mask",
|
||||
"tag"
|
||||
]
|
||||
},
|
||||
"model": "string",
|
||||
"outputFormat": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"simple",
|
||||
"detailed"
|
||||
]
|
||||
},
|
||||
"profanityOption": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"masked",
|
||||
"removed",
|
||||
"raw"
|
||||
]
|
||||
},
|
||||
"requestSnr": "boolean",
|
||||
"initialSpeechTimeoutMs": "number",
|
||||
"azureServiceEndpoint": "string",
|
||||
"azureSttEndpointId": "string",
|
||||
"asrDtmfTerminationDigit": "string",
|
||||
"asrTimeout": "number",
|
||||
"nuanceOptions": "#nuanceOptions",
|
||||
"deepgramOptions": "#deepgramOptions",
|
||||
"ibmOptions": "#ibmOptions"
|
||||
},
|
||||
"required": [
|
||||
"vendor"
|
||||
]
|
||||
},
|
||||
"ibmOptions": {
|
||||
"properties": {
|
||||
"sttApiKey": "string",
|
||||
"sttRegion": "string",
|
||||
"ttsApiKey": "string",
|
||||
"ttsRegion": "string",
|
||||
"instanceId": "string",
|
||||
"model": "string",
|
||||
"languageCustomizationId": "string",
|
||||
"acousticCustomizationId": "string",
|
||||
"baseModelVersion": "string",
|
||||
"watsonMetadata": "string",
|
||||
"watsonLearningOptOut": "boolean"
|
||||
},
|
||||
"required": [
|
||||
]
|
||||
},
|
||||
"deepgramOptions": {
|
||||
"properties": {
|
||||
"apiKey": "string",
|
||||
"tier": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"enhanced",
|
||||
"base"
|
||||
]
|
||||
},
|
||||
"model": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"general",
|
||||
"meeting",
|
||||
"phonecall",
|
||||
"voicemail",
|
||||
"finance",
|
||||
"conversationalai",
|
||||
"video",
|
||||
"custom"
|
||||
]
|
||||
},
|
||||
"customModel": "string",
|
||||
"version": "string",
|
||||
"punctuate": "boolean",
|
||||
"profanityFilter": "boolean",
|
||||
"redact": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"pci",
|
||||
"numbers",
|
||||
"true",
|
||||
"ssn"
|
||||
]
|
||||
},
|
||||
"diarize": "boolean",
|
||||
"diarizeVersion": "string",
|
||||
"ner": "boolean",
|
||||
"multichannel": "boolean",
|
||||
"alternatives": "number",
|
||||
"numerals": "boolean",
|
||||
"search": "array",
|
||||
"replace": "array",
|
||||
"keywords": "array",
|
||||
"endpointing": "boolean",
|
||||
"vadTurnoff": "number",
|
||||
"tag": "string"
|
||||
}
|
||||
},
|
||||
"nuanceOptions": {
|
||||
"properties": {
|
||||
"clientId": "string",
|
||||
"secret": "string",
|
||||
"kryptonEndpoint": "string",
|
||||
"topic": "string",
|
||||
"utteranceDetectionMode": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"single",
|
||||
"multiple",
|
||||
"disabled"
|
||||
]
|
||||
},
|
||||
"punctuation": "boolean",
|
||||
"profanityFilter": "boolean",
|
||||
"includeTokenization": "boolean",
|
||||
"discardSpeakerAdaptation": "boolean",
|
||||
"suppressCallRecording": "boolean",
|
||||
"maskLoadFailures": "boolean",
|
||||
"suppressInitialCapitalization": "boolean",
|
||||
"allowZeroBaseLmWeight": "boolean",
|
||||
"filterWakeupWord": "boolean",
|
||||
"resultType": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"final",
|
||||
"partial",
|
||||
"immutable_partial"
|
||||
]
|
||||
},
|
||||
"noInputTimeoutMs": "number",
|
||||
"recognitionTimeoutMs": "number",
|
||||
"utteranceEndSilenceMs": "number",
|
||||
"maxHypotheses": "number",
|
||||
"speechDomain": "string",
|
||||
"formatting": "#formatting",
|
||||
"clientData": "object",
|
||||
"userId": "string",
|
||||
"speechDetectionSensitivity": "number",
|
||||
"resources": ["#resource"]
|
||||
},
|
||||
"required": [
|
||||
]
|
||||
},
|
||||
"resource": {
|
||||
"properties": {
|
||||
"externalReference": "#resourceReference",
|
||||
"inlineWordset": "string",
|
||||
"builtin": "string",
|
||||
"inlineGrammar": "string",
|
||||
"wakeupWord": "[string]",
|
||||
"weightName": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"defaultWeight",
|
||||
"lowest",
|
||||
"low",
|
||||
"medium",
|
||||
"high",
|
||||
"highest"
|
||||
]
|
||||
},
|
||||
"weightValue": "number",
|
||||
"reuse": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"undefined_reuse",
|
||||
"low_reuse",
|
||||
"high_reuse"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
]
|
||||
},
|
||||
"resourceReference": {
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"undefined_resource_type",
|
||||
"wordset",
|
||||
"compiled_wordset",
|
||||
"domain_lm",
|
||||
"speaker_profile",
|
||||
"grammar",
|
||||
"settings"
|
||||
]
|
||||
},
|
||||
"uri": "string",
|
||||
"maxLoadFailures": "boolean",
|
||||
"requestTimeoutMs": "number",
|
||||
"headers": "object"
|
||||
},
|
||||
"required": [
|
||||
]
|
||||
},
|
||||
"formatting": {
|
||||
"properties": {
|
||||
"scheme": "string",
|
||||
"options": "object"
|
||||
},
|
||||
"required": [
|
||||
"scheme",
|
||||
"options"
|
||||
]
|
||||
},
|
||||
"lexIntent": {
|
||||
"properties": {
|
||||
"name": "string",
|
||||
"slots": "object"
|
||||
},
|
||||
"required": [
|
||||
"name"
|
||||
]
|
||||
},
|
||||
"vad": {
|
||||
"properties": {
|
||||
"enable": "boolean",
|
||||
"voiceMs": "number",
|
||||
"mode": "number"
|
||||
},
|
||||
"required": [
|
||||
"enable"
|
||||
]
|
||||
},
|
||||
"amd": {
|
||||
"properties": {
|
||||
"actionHook": "object|string",
|
||||
"thresholdWordCount": "number",
|
||||
"timers": "#amdTimers",
|
||||
"recognizer": "#recognizer"
|
||||
},
|
||||
"required": [
|
||||
"actionHook"
|
||||
]
|
||||
},
|
||||
"amdTimers": {
|
||||
"properties": {
|
||||
"noSpeechTimeoutMs": "number",
|
||||
"decisionTimeoutMs": "number",
|
||||
"toneTimeoutMs": "number",
|
||||
"greetingCompletionTimeoutMs": "number"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,10 +32,10 @@ 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) => {
|
||||
@@ -103,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) => {
|
||||
@@ -181,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;
|
||||
};
|
||||
|
||||
@@ -226,5 +250,6 @@ module.exports = {
|
||||
testNuanceStt,
|
||||
testDeepgramStt,
|
||||
testIbmTts,
|
||||
testIbmStt
|
||||
testIbmStt,
|
||||
testSonioxStt
|
||||
};
|
||||
|
||||
3622
package-lock.json
generated
3622
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
13
package.json
13
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "jambonz-api-server",
|
||||
"version": "v0.8.0",
|
||||
"version": "v0.8.2",
|
||||
"description": "",
|
||||
"main": "app.js",
|
||||
"scripts": {
|
||||
@@ -18,21 +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",
|
||||
"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",
|
||||
|
||||
@@ -219,6 +219,21 @@ test('account tests', async(t) => {
|
||||
});
|
||||
t.ok(result.statusCode === 201, 'successfully updated a call session limit to an account');
|
||||
|
||||
/* try to update an existing limit for an account giving a invalid sid */
|
||||
try {
|
||||
result = await request.post(`/Accounts/invalid-sid/Limits`, {
|
||||
auth: authAdmin,
|
||||
json: true,
|
||||
resolveWithFullResponse: true,
|
||||
body: {
|
||||
category: 'voice_call_session',
|
||||
quantity: 205
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
t.ok(err.statusCode === 400, 'returns 400 bad request if sid param is not a valid uuid');
|
||||
}
|
||||
|
||||
/* query all limits for an account */
|
||||
result = await request.get(`/Accounts/${sid}/Limits`, {
|
||||
auth: authAdmin,
|
||||
|
||||
@@ -133,4 +133,14 @@ services:
|
||||
- "3100:3000/tcp"
|
||||
networks:
|
||||
jambonz-api:
|
||||
ipv4_address: 172.58.0.9
|
||||
ipv4_address: 172.58.0.9
|
||||
|
||||
webhook-tts-scaffold:
|
||||
image: jambonz/webhook-tts-test-scaffold:latest
|
||||
ports:
|
||||
- "3101:3000/tcp"
|
||||
volumes:
|
||||
- ./test-apps:/tmp
|
||||
networks:
|
||||
jambonz-api:
|
||||
ipv4_address: 172.58.0.10
|
||||
29
test/email_utils.js
Normal file
29
test/email_utils.js
Normal file
@@ -0,0 +1,29 @@
|
||||
const test = require('tape');
|
||||
const {emailSimpleText} = require('../lib/utils/email-utils');
|
||||
const bent = require('bent');
|
||||
const getJSON = bent('json')
|
||||
const logger = {
|
||||
debug: () =>{},
|
||||
info: () => {}
|
||||
}
|
||||
|
||||
test('email-test', async(t) => {
|
||||
// Prepare env:
|
||||
process.env.CUSTOM_EMAIL_VENDOR_URL = 'http://127.0.0.1:3101/custom_email_vendor';
|
||||
process.env.CUSTOM_EMAIL_VENDOR_USERNAME = 'USERNAME';
|
||||
process.env.CUSTOM_EMAIL_VENDOR_PASSWORD = 'PASSWORD';
|
||||
|
||||
await emailSimpleText(logger, 'test@gmail.com', 'subject', 'body text');
|
||||
|
||||
const obj = await getJSON(`http://127.0.0.1:3101/lastRequest/custom_email_vendor`);
|
||||
t.ok(obj.headers['Content-Type'] == 'application/json');
|
||||
t.ok(obj.headers.Authorization == 'Basic VVNFUk5BTUU6UEFTU1dPUkQ=');
|
||||
t.ok(obj.body.from == 'jambonz Support <support@jambonz.org>');
|
||||
t.ok(obj.body.to == 'test@gmail.com');
|
||||
t.ok(obj.body.subject == 'subject');
|
||||
t.ok(obj.body.text == 'body text');
|
||||
|
||||
process.env.CUSTOM_EMAIL_VENDOR_URL = null;
|
||||
process.env.CUSTOM_EMAIL_VENDOR_USERNAME = null;
|
||||
process.env.CUSTOM_EMAIL_VENDOR_PASSWORD = null;
|
||||
});
|
||||
@@ -17,4 +17,5 @@ require('./webapp_tests');
|
||||
// require('./homer');
|
||||
require('./call-test');
|
||||
require('./password-settings');
|
||||
require('./email_utils');
|
||||
require('./docker_stop');
|
||||
|
||||
@@ -107,6 +107,20 @@ test('service provider tests', async(t) => {
|
||||
});
|
||||
t.ok(result.statusCode === 204, 'successfully updated service provider');
|
||||
|
||||
/* try to update service providers with invalid sid format*/
|
||||
try {
|
||||
result = await request.put(`/ServiceProviders/123`, {
|
||||
auth: authAdmin,
|
||||
json: true,
|
||||
resolveWithFullResponse: true,
|
||||
body: {
|
||||
name: 'robb'
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
t.ok(err.statusCode === 400, 'returns 400 bad request if sid param is not a valid uuid');
|
||||
}
|
||||
|
||||
/* add an api key for a service provider */
|
||||
result = await request.post(`/ApiKeys`, {
|
||||
auth: authAdmin,
|
||||
|
||||
@@ -22,6 +22,24 @@ test('speech credentials tests', async(t) => {
|
||||
const service_provider_sid = await createServiceProvider(request);
|
||||
const account_sid = await createAccount(request, service_provider_sid);
|
||||
|
||||
/* return 400 if invalid sid param is used */
|
||||
try {
|
||||
result = await request.post(`/ServiceProviders/foobarbaz/SpeechCredentials`, {
|
||||
resolveWithFullResponse: true,
|
||||
simple: false,
|
||||
auth: authAdmin,
|
||||
json: true,
|
||||
body: {
|
||||
vendor: 'google',
|
||||
service_key: jsonKey,
|
||||
use_for_tts: true,
|
||||
use_for_stt: true
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
t.ok(err.statusCode === 400, 'returns 400 bad request if 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,
|
||||
@@ -73,8 +91,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 */
|
||||
result = await request.post(`/Accounts/foobarbaz/SpeechCredentials`, {
|
||||
/* return 403 if invalid account is used - randomSid: bed7ae17-f8b4-4b74-9e5b-4f6318aae9c9 */
|
||||
result = await request.post(`/Accounts/bed7ae17-f8b4-4b74-9e5b-4f6318aae9c9/SpeechCredentials`, {
|
||||
resolveWithFullResponse: true,
|
||||
simple: false,
|
||||
auth: authUser,
|
||||
@@ -171,6 +189,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,9 +347,97 @@ 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,
|
||||
auth: authUser,
|
||||
json: true,
|
||||
body: {
|
||||
vendor: 'nvidia',
|
||||
use_for_stt: true,
|
||||
use_for_tts: true,
|
||||
riva_server_uri: "192.168.1.2:5060"
|
||||
}
|
||||
});
|
||||
t.ok(result.statusCode === 201, 'successfully added speech credential for nvidia');
|
||||
const ms_sid = result.body.sid;
|
||||
|
||||
/* test the speech credential */
|
||||
result = await request.get(`/Accounts/${account_sid}/SpeechCredentials/${ms_sid}/test`, {
|
||||
resolveWithFullResponse: true,
|
||||
auth: authUser,
|
||||
json: true,
|
||||
});
|
||||
// TODO Nvidia test.
|
||||
t.ok(result.statusCode === 200 && result.body.stt.status === 'not tested', 'successfully tested speech credential for nvida stt');
|
||||
|
||||
/* delete the credential */
|
||||
result = await request.delete(`/Accounts/${account_sid}/SpeechCredentials/${ms_sid}`, {
|
||||
auth: authUser,
|
||||
resolveWithFullResponse: true,
|
||||
});
|
||||
t.ok(result.statusCode === 204, 'successfully deleted speech credential');
|
||||
|
||||
/* add a credential for nuance */
|
||||
result = await request.post(`/Accounts/${account_sid}/SpeechCredentials`, {
|
||||
resolveWithFullResponse: true,
|
||||
auth: authUser,
|
||||
json: true,
|
||||
body: {
|
||||
vendor: 'nuance',
|
||||
use_for_stt: true,
|
||||
use_for_tts: true,
|
||||
client_id: 'client_id',
|
||||
secret: 'secret',
|
||||
nuance_tts_uri: "192.168.1.2:5060",
|
||||
nuance_stt_uri: "192.168.1.2:5061"
|
||||
}
|
||||
});
|
||||
t.ok(result.statusCode === 201, 'successfully added speech credential for nuance');
|
||||
const nuance_sid = result.body.sid;
|
||||
|
||||
/* delete the credential */
|
||||
result = await request.delete(`/Accounts/${account_sid}/SpeechCredentials/${nuance_sid}`, {
|
||||
auth: authUser,
|
||||
resolveWithFullResponse: true,
|
||||
});
|
||||
t.ok(result.statusCode === 204, 'successfully deleted speech credential');
|
||||
|
||||
await deleteObjectBySid(request, '/Accounts', account_sid);
|
||||
await deleteObjectBySid(request, '/ServiceProviders', service_provider_sid);
|
||||
//t.end();
|
||||
t.end();
|
||||
}
|
||||
catch (err) {
|
||||
console.error(err);
|
||||
|
||||
@@ -23,7 +23,7 @@ test('add an admin user', (t) => {
|
||||
|
||||
test('user tests', async(t) => {
|
||||
const app = require('../app');
|
||||
const password = await generateHashedPassword('abcd1234-');
|
||||
const password = 'abcde12345-';
|
||||
try {
|
||||
let result;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user