mirror of
https://github.com/jambonz/jambonz-api-server.git
synced 2026-01-25 02:08:24 +00:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
956da4334f | ||
|
|
c144758d44 | ||
|
|
e24f3472ae | ||
|
|
4c935c7fda | ||
|
|
1c55bad04f | ||
|
|
32a2bfcdb5 | ||
|
|
becc1636b7 |
14
app.js
14
app.js
@@ -7,6 +7,7 @@ const nocache = require('nocache');
|
||||
const rateLimit = require('express-rate-limit');
|
||||
const cors = require('cors');
|
||||
const passport = require('passport');
|
||||
const {verifyViewOnlyUser} = require('./lib/middleware');
|
||||
const routes = require('./lib/routes');
|
||||
const Registrar = require('@jambonz/mw-registrar');
|
||||
|
||||
@@ -172,6 +173,19 @@ app.use('/v1', unless(
|
||||
'/InviteCodes',
|
||||
'/PredefinedCarriers'
|
||||
], passport.authenticate('bearer', {session: false})));
|
||||
app.use('/v1', unless(
|
||||
[
|
||||
'/register',
|
||||
'/forgot-password',
|
||||
'/signin',
|
||||
'/login',
|
||||
'/messaging',
|
||||
'/outboundSMS',
|
||||
'/AccountTest',
|
||||
'/InviteCodes',
|
||||
'/PredefinedCarriers',
|
||||
'/logout'
|
||||
], verifyViewOnlyUser));
|
||||
app.use('/', routes);
|
||||
app.use((err, req, res, next) => {
|
||||
logger.error(err, 'burped error');
|
||||
|
||||
11
db/prepare-permissions-test.sql
Normal file
11
db/prepare-permissions-test.sql
Normal file
@@ -0,0 +1,11 @@
|
||||
/* remove VIEW_ONLY permission for admin user as it will prevent write operations*/
|
||||
delete from user_permissions;
|
||||
|
||||
delete from permissions;
|
||||
|
||||
insert into permissions (permission_sid, name, description)
|
||||
values
|
||||
('ffbc342a-546a-11ed-bdc3-0242ac120002', 'VIEW_ONLY', 'Can view data but not make changes'),
|
||||
('ffbc3a10-546a-11ed-bdc3-0242ac120002', 'PROVISION_SERVICES', 'Can provision services'),
|
||||
('ffbc3c5e-546a-11ed-bdc3-0242ac120002', 'PROVISION_USERS', 'Can provision users');
|
||||
|
||||
@@ -6,6 +6,7 @@ const {readFile} = require('fs/promises');
|
||||
const {execSync} = require('child_process');
|
||||
const {version:desiredVersion} = require('../package.json');
|
||||
const logger = require('pino')();
|
||||
const fs = require('fs');
|
||||
|
||||
logger.info(`upgrade-jambonz-db: desired version ${desiredVersion}`);
|
||||
|
||||
@@ -22,6 +23,20 @@ const opts = {
|
||||
port: process.env.JAMBONES_MYSQL_PORT || 3306,
|
||||
multipleStatements: true
|
||||
};
|
||||
const rejectUnauthorized = process.env.JAMBONES_MYSQL_REJECT_UNAUTHORIZED;
|
||||
const ssl_ca_file = process.env.JAMBONES_MYSQL_SSL_CA_FILE;
|
||||
const ssl_cert_file = process.env.JAMBONES_MYSQL_SSL_CERT_FILE;
|
||||
const ssl_key_file = process.env.JAMBONES_MYSQL_SSL_KEY_FILE;
|
||||
const sslFilesProvided = Boolean(ssl_ca_file && ssl_cert_file && ssl_key_file);
|
||||
if (rejectUnauthorized !== undefined || sslFilesProvided) {
|
||||
opts.ssl = {
|
||||
...(rejectUnauthorized !== undefined && { rejectUnauthorized: rejectUnauthorized === '0' ? false : true }),
|
||||
...(ssl_ca_file && { ca: fs.readFileSync(ssl_ca_file) }),
|
||||
...(ssl_cert_file && { cert: fs.readFileSync(ssl_cert_file) }),
|
||||
...(ssl_key_file && { key: fs.readFileSync(ssl_key_file) })
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
const sql = {
|
||||
'7006': [
|
||||
|
||||
@@ -35,8 +35,8 @@ function makeStrategy(logger) {
|
||||
debug(err);
|
||||
logger.info({err}, 'Error checking redis for jwt');
|
||||
}
|
||||
const { user_sid, service_provider_sid, account_sid, email, name, scope, permissions } = decoded;
|
||||
|
||||
const { user_sid, service_provider_sid, account_sid, email,
|
||||
name, scope, permissions, is_view_only } = decoded;
|
||||
const user = {
|
||||
service_provider_sid,
|
||||
account_sid,
|
||||
@@ -45,6 +45,7 @@ function makeStrategy(logger) {
|
||||
email,
|
||||
name,
|
||||
permissions,
|
||||
is_view_only,
|
||||
hasScope: (s) => s === scope,
|
||||
hasAdminAuth: scope === 'admin',
|
||||
hasServiceProviderAuth: scope === 'service_provider',
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
const logger = require('./logger');
|
||||
const {UserPermissionError} = require('./utils/errors');
|
||||
|
||||
function delayLoginMiddleware(req, res, next) {
|
||||
if (req.path.includes('/login') || req.path.includes('/signin')) {
|
||||
@@ -27,6 +28,26 @@ function delayLoginMiddleware(req, res, next) {
|
||||
next();
|
||||
}
|
||||
|
||||
function verifyViewOnlyUser(req, res, next) {
|
||||
// Skip check for GET requests
|
||||
if (req.method === 'GET') {
|
||||
return next();
|
||||
}
|
||||
// current user is changing their password which shuould be allowed
|
||||
if (req.body?.old_password && req.body?.new_password) {
|
||||
return next();
|
||||
}
|
||||
// Check if user is read-only
|
||||
if (req.user && !!req.user.is_view_only) {
|
||||
const upError = new UserPermissionError('User has view-only access');
|
||||
upError.status = 403;
|
||||
throw upError;
|
||||
}
|
||||
|
||||
next();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
delayLoginMiddleware
|
||||
delayLoginMiddleware,
|
||||
verifyViewOnlyUser
|
||||
};
|
||||
|
||||
45
lib/models/permissions.js
Normal file
45
lib/models/permissions.js
Normal file
@@ -0,0 +1,45 @@
|
||||
const Model = require('./model');
|
||||
const {promisePool} = require('../db');
|
||||
const sqlAll = `
|
||||
SELECT * from permissions
|
||||
`;
|
||||
const sqlByName = `
|
||||
SELECT * from permissions where name = ?
|
||||
`;
|
||||
|
||||
class Permissions extends Model {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
static async retrieveAll() {
|
||||
const [rows] = await promisePool.query(sqlAll);
|
||||
return rows;
|
||||
}
|
||||
|
||||
static async retrieveByName(name) {
|
||||
const [rows] = await promisePool.query(sqlByName, [name]);
|
||||
return rows;
|
||||
}
|
||||
}
|
||||
|
||||
Permissions.table = 'permissions';
|
||||
Permissions.fields = [
|
||||
{
|
||||
name: 'permission_sid',
|
||||
type: 'string',
|
||||
primaryKey: true
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
type: 'string',
|
||||
required: true
|
||||
},
|
||||
{
|
||||
name: 'description',
|
||||
type: 'string',
|
||||
required: true
|
||||
}
|
||||
];
|
||||
|
||||
module.exports = Permissions;
|
||||
53
lib/models/user-permissions.js
Normal file
53
lib/models/user-permissions.js
Normal file
@@ -0,0 +1,53 @@
|
||||
const Model = require('./model');
|
||||
const {promisePool} = require('../db');
|
||||
const sqlAll = `
|
||||
SELECT * from user_permissions
|
||||
`;
|
||||
const sqlByUserIdPermissionSid = `
|
||||
SELECT * from user_permissions where user_sid = ? and permission_sid = ?
|
||||
`;
|
||||
const sqlByUserId = `
|
||||
SELECT * from user_permissions where user_sid = ?
|
||||
`;
|
||||
|
||||
class UserPermissions extends Model {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
static async retrieveAll() {
|
||||
const [rows] = await promisePool.query(sqlAll);
|
||||
return rows;
|
||||
}
|
||||
|
||||
static async retrieveByUserIdPermissionSid(user_sid, permission_sid) {
|
||||
const [rows] = await promisePool.query(sqlByUserIdPermissionSid, [user_sid, permission_sid]);
|
||||
return rows;
|
||||
}
|
||||
static async retrieveByUserId(user_sid) {
|
||||
const [rows] = await promisePool.query(sqlByUserId, [user_sid]);
|
||||
return rows;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
UserPermissions.table = 'user_permissions';
|
||||
UserPermissions.fields = [
|
||||
{
|
||||
name: 'user_permissions_sid',
|
||||
type: 'string',
|
||||
primaryKey: true
|
||||
},
|
||||
{
|
||||
name: 'user_sid',
|
||||
type: 'string',
|
||||
required: true
|
||||
},
|
||||
{
|
||||
name: 'permission_sid',
|
||||
type: 'string',
|
||||
required: true
|
||||
}
|
||||
];
|
||||
|
||||
module.exports = UserPermissions;
|
||||
@@ -29,6 +29,9 @@ router.post('/', async(req, res) => {
|
||||
return res.sendStatus(403);
|
||||
}
|
||||
logger.info({r}, 'successfully retrieved user account');
|
||||
if (r[0].provider !== 'local') {
|
||||
return res.sendStatus(403);
|
||||
}
|
||||
|
||||
const maxLoginAttempts = process.env.LOGIN_ATTEMPTS_MAX_RETRIES || 6;
|
||||
const loginAttempsBlocked = await retrieveKey(`login:${r[0].user_sid}`) >= maxLoginAttempts;
|
||||
@@ -71,9 +74,13 @@ router.post('/', async(req, res) => {
|
||||
obj.service_provider_sid = r[0].service_provider_sid;
|
||||
obj.service_provider_name = service_provider[0].name;
|
||||
}
|
||||
// if there is only one permission and it is VIEW_ONLY, then the user is view only
|
||||
// this is to prevent write operations on the API
|
||||
const is_view_only = permissions.length === 1 && permissions.includes('VIEW_ONLY');
|
||||
const payload = {
|
||||
scope: obj.scope,
|
||||
permissions,
|
||||
is_view_only,
|
||||
...(obj.service_provider_sid && {
|
||||
service_provider_sid: obj.service_provider_sid,
|
||||
service_provider_name: obj.service_provider_name
|
||||
@@ -83,7 +90,8 @@ router.post('/', async(req, res) => {
|
||||
account_name: obj.account_name,
|
||||
service_provider_name: obj.service_provider_name
|
||||
}),
|
||||
user_sid: obj.user_sid
|
||||
user_sid: obj.user_sid,
|
||||
name: username
|
||||
};
|
||||
|
||||
const expiresIn = parseInt(process.env.JWT_EXPIRES_IN || 60) * 60;
|
||||
|
||||
@@ -13,7 +13,8 @@ const {decryptCredential, testWhisper, testDeepgramTTS,
|
||||
testVerbioStt,
|
||||
testSpeechmaticsStt,
|
||||
testCartesia,
|
||||
testVoxistStt} = require('../../utils/speech-utils');
|
||||
testVoxistStt,
|
||||
testOpenAiStt} = require('../../utils/speech-utils');
|
||||
const {DbErrorUnprocessableRequest, DbErrorForbidden, DbErrorBadRequest} = require('../../utils/errors');
|
||||
const {
|
||||
testGoogleTts,
|
||||
@@ -282,6 +283,12 @@ const encryptCredential = (obj) => {
|
||||
const whisperData = JSON.stringify({api_key, model_id});
|
||||
return encrypt(whisperData);
|
||||
|
||||
case 'openai':
|
||||
assert(api_key, 'invalid openai speech credential: api_key is required');
|
||||
assert(model_id, 'invalid openai speech credential: model_id is required');
|
||||
const openaiData = JSON.stringify({api_key, model_id});
|
||||
return encrypt(openaiData);
|
||||
|
||||
case 'verbio':
|
||||
assert(engine_version, 'invalid verbio speech credential: client_id is required');
|
||||
assert(client_id, 'invalid verbio speech credential: client_id is required');
|
||||
@@ -882,6 +889,17 @@ router.get('/:sid/test', async(req, res) => {
|
||||
SpeechCredential.ttsTestResult(sid, false);
|
||||
}
|
||||
}
|
||||
} else if (cred.vendor === 'openai') {
|
||||
if (cred.use_for_stt) {
|
||||
try {
|
||||
await testOpenAiStt(logger, credential);
|
||||
results.stt.status = 'ok';
|
||||
SpeechCredential.sttTestResult(sid, true);
|
||||
} catch (err) {
|
||||
results.stt = {status: 'fail', reason: err.message};
|
||||
SpeechCredential.sttTestResult(sid, false);
|
||||
}
|
||||
}
|
||||
} else if (cred.vendor === 'verbio') {
|
||||
if (cred.use_for_tts) {
|
||||
try {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
const router = require('express').Router();
|
||||
const User = require('../../models/user');
|
||||
const UserPermissions = require('../../models/user-permissions');
|
||||
const Permissions = require('../../models/permissions');
|
||||
const {DbErrorBadRequest, BadRequestError, DbErrorForbidden} = require('../../utils/errors');
|
||||
const {generateHashedPassword, verifyPassword} = require('../../utils/password-utils');
|
||||
const {promisePool} = require('../../db');
|
||||
@@ -38,7 +40,8 @@ const validateRequest = async(user_sid, req) => {
|
||||
email,
|
||||
email_activation_code,
|
||||
force_change,
|
||||
is_active
|
||||
is_active,
|
||||
is_view_only
|
||||
} = payload;
|
||||
|
||||
const [r] = await promisePool.query(retrieveSql, user_sid);
|
||||
@@ -93,7 +96,8 @@ const validateRequest = async(user_sid, req) => {
|
||||
if (email_activation_code && !email) {
|
||||
throw new DbErrorBadRequest('email and email_activation_code both required');
|
||||
}
|
||||
if (!name && !new_password && !email && !initial_password && !force_change && !is_active)
|
||||
if (!name && !new_password && !email && !initial_password && !force_change && !is_active &&
|
||||
is_view_only === undefined)
|
||||
throw new DbErrorBadRequest('no updates requested');
|
||||
|
||||
return user;
|
||||
@@ -140,7 +144,35 @@ const ensureUserRetrievalIsAllowed = (req, user) => {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
async function updateViewOnlyUserPermission(is_view_only, user_sid) {
|
||||
try {
|
||||
const [viewOnlyPermission] = await Permissions.retrieveByName('VIEW_ONLY');
|
||||
if (!viewOnlyPermission) {
|
||||
throw new Error('VIEW_ONLY permission not found');
|
||||
}
|
||||
|
||||
const existingPermissions = await UserPermissions.retrieveByUserIdPermissionSid(
|
||||
user_sid,
|
||||
viewOnlyPermission.permission_sid
|
||||
);
|
||||
if (is_view_only && existingPermissions.length === 0) {
|
||||
await UserPermissions.make({
|
||||
user_sid,
|
||||
permission_sid: viewOnlyPermission.permission_sid,
|
||||
});
|
||||
} else if (!is_view_only && existingPermissions.length > 0) {
|
||||
await UserPermissions.remove(existingPermissions[0].user_permissions_sid);
|
||||
}
|
||||
} catch (err) {
|
||||
throw new Error(`Failed to update user permissions: ${err.message}`);
|
||||
}
|
||||
}
|
||||
async function removeViewOnlyUserPermission(user_id) {
|
||||
const [viewOnlyPermission] = await Permissions.retrieveByName('VIEW_ONLY');
|
||||
if (viewOnlyPermission) {
|
||||
await UserPermissions.remove(user_id, viewOnlyPermission.permission_sid);
|
||||
}
|
||||
}
|
||||
router.get('/', async(req, res) => {
|
||||
const logger = req.app.locals.logger;
|
||||
|
||||
@@ -308,7 +340,14 @@ router.get('/:user_sid', async(req, res) => {
|
||||
}
|
||||
|
||||
ensureUserRetrievalIsAllowed(req, user);
|
||||
|
||||
const [viewOnlyPermission] = await Permissions.retrieveByName('VIEW_ONLY');
|
||||
const existingPermissions = await UserPermissions.retrieveByUserId(
|
||||
user_sid
|
||||
);
|
||||
logger.debug(`existingPermissions of ${user_sid}: ${JSON.stringify(existingPermissions)}`);
|
||||
user.is_view_only = existingPermissions.length === 1 &&
|
||||
existingPermissions[0].permission_sid === viewOnlyPermission.permission_sid;
|
||||
logger.debug(`User ${user_sid} is view-only user: ${user.is_view_only}`);
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const { hashed_password, ...rest } = user;
|
||||
return res.status(200).json(rest);
|
||||
@@ -332,9 +371,9 @@ router.put('/:user_sid', async(req, res) => {
|
||||
is_active,
|
||||
force_change,
|
||||
account_sid,
|
||||
service_provider_sid
|
||||
service_provider_sid,
|
||||
is_view_only
|
||||
} = req.body;
|
||||
|
||||
//if (req.user.user_sid && req.user.user_sid !== user_sid) return res.sendStatus(403);
|
||||
|
||||
if (!hasAdminAuth &&
|
||||
@@ -427,6 +466,8 @@ router.put('/:user_sid', async(req, res) => {
|
||||
//TODO: send email with activation code
|
||||
}
|
||||
}
|
||||
// update user permissions
|
||||
await updateViewOnlyUserPermission(is_view_only, user_sid);
|
||||
res.sendStatus(204);
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
@@ -443,6 +484,8 @@ router.post('/', async(req, res) => {
|
||||
};
|
||||
const allUsers = await User.retrieveAll();
|
||||
delete payload.initial_password;
|
||||
const is_view_only = payload.is_view_only;
|
||||
delete payload.is_view_only;
|
||||
|
||||
try {
|
||||
if (req.body.initial_password) {
|
||||
@@ -464,6 +507,7 @@ router.post('/', async(req, res) => {
|
||||
if (req.user.hasAdminAuth) {
|
||||
logger.debug({payload}, 'POST /users');
|
||||
const uuid = await User.make(payload);
|
||||
await updateViewOnlyUserPermission(is_view_only, uuid);
|
||||
res.status(201).json({user_sid: uuid});
|
||||
}
|
||||
else if (req.user.hasAccountAuth) {
|
||||
@@ -472,6 +516,7 @@ router.post('/', async(req, res) => {
|
||||
...payload,
|
||||
account_sid: req.user.account_sid,
|
||||
});
|
||||
await updateViewOnlyUserPermission(is_view_only, uuid);
|
||||
res.status(201).json({user_sid: uuid});
|
||||
}
|
||||
else if (req.user.hasServiceProviderAuth) {
|
||||
@@ -480,6 +525,7 @@ router.post('/', async(req, res) => {
|
||||
...payload,
|
||||
service_provider_sid: req.user.service_provider_sid,
|
||||
});
|
||||
await updateViewOnlyUserPermission(is_view_only, uuid);
|
||||
res.status(201).json({user_sid: uuid});
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -497,6 +543,8 @@ router.delete('/:user_sid', async(req, res) => {
|
||||
const user = allUsers.filter((user) => user.user_sid === user_sid);
|
||||
|
||||
ensureUserDeletionIsAllowed(req, activeAdminUsers, user);
|
||||
logger.debug(`Removing view-only permission for user ${user_sid}`);
|
||||
await removeViewOnlyUserPermission(user_sid);
|
||||
await User.remove(user_sid);
|
||||
|
||||
/* invalidate the jwt of the deleted user */
|
||||
|
||||
@@ -27,11 +27,17 @@ class DbErrorForbidden extends DbError {
|
||||
super(msg);
|
||||
}
|
||||
}
|
||||
class UserPermissionError extends Error {
|
||||
constructor(msg) {
|
||||
super(msg);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
BadRequestError,
|
||||
DbError,
|
||||
DbErrorBadRequest,
|
||||
DbErrorUnprocessableRequest,
|
||||
DbErrorForbidden
|
||||
DbErrorForbidden,
|
||||
UserPermissionError
|
||||
};
|
||||
|
||||
@@ -1,138 +1,56 @@
|
||||
module.exports = [
|
||||
{
|
||||
name: 'Chinese - general',
|
||||
value: 'zh',
|
||||
},
|
||||
{
|
||||
name: 'Chinese (China)',
|
||||
value: 'zh-CN',
|
||||
},
|
||||
{
|
||||
name: 'Chinese (Taiwan)',
|
||||
value: 'zh-TW',
|
||||
},
|
||||
{
|
||||
name: 'Dutch - general',
|
||||
value: 'nl',
|
||||
},
|
||||
{
|
||||
name: 'English - general',
|
||||
value: 'en',
|
||||
},
|
||||
{
|
||||
name: 'English (Australia)',
|
||||
value: 'en-AU',
|
||||
},
|
||||
{
|
||||
name: 'English (United Kingdom)',
|
||||
value: 'en-GB',
|
||||
},
|
||||
{
|
||||
name: 'English (India)',
|
||||
value: 'en-IN',
|
||||
},
|
||||
{
|
||||
name: 'English (New Zealand)',
|
||||
value: 'en-NZ',
|
||||
},
|
||||
{
|
||||
name: 'English (United States)',
|
||||
value: 'en-US',
|
||||
},
|
||||
{
|
||||
name: 'French - general',
|
||||
value: 'fr',
|
||||
},
|
||||
{
|
||||
name: 'French (Canada)',
|
||||
value: 'fr-CA',
|
||||
},
|
||||
{
|
||||
name: 'German - general',
|
||||
value: 'de',
|
||||
},
|
||||
{
|
||||
name: 'Hindi - general',
|
||||
value: 'hi',
|
||||
},
|
||||
{
|
||||
name: 'Hindi (Roman Script)',
|
||||
value: 'hi-Latin',
|
||||
},
|
||||
{
|
||||
name: 'Indonesian - general',
|
||||
value: 'in',
|
||||
},
|
||||
{
|
||||
name: 'Italian - general',
|
||||
value: 'it',
|
||||
},
|
||||
{
|
||||
name: 'Japanese - general',
|
||||
value: 'ja',
|
||||
},
|
||||
{
|
||||
name: 'Korean - general',
|
||||
value: 'ko',
|
||||
},
|
||||
{
|
||||
name: 'Norwegian - general',
|
||||
value: 'no',
|
||||
},
|
||||
{
|
||||
name: 'Polish - general',
|
||||
value: 'pl',
|
||||
},
|
||||
{
|
||||
name: 'Portuguese - general',
|
||||
value: 'pt',
|
||||
},
|
||||
{
|
||||
name: 'Portuguese (Brazil)',
|
||||
value: 'pt-BR',
|
||||
},
|
||||
{
|
||||
name: 'Portuguese (Portugal)',
|
||||
value: 'pt-PT',
|
||||
},
|
||||
{
|
||||
name: 'Russian - general',
|
||||
value: 'ru',
|
||||
},
|
||||
{
|
||||
name: 'Spanish - general',
|
||||
value: 'es',
|
||||
},
|
||||
{
|
||||
name: 'Spanish (Latin America)',
|
||||
value: 'es-419',
|
||||
},
|
||||
{
|
||||
name: 'Swedish - general',
|
||||
value: 'sv',
|
||||
},
|
||||
{
|
||||
name: 'Turkish - general',
|
||||
value: 'tr',
|
||||
},
|
||||
{
|
||||
name: 'Ukrainian - general',
|
||||
value: 'uk',
|
||||
},
|
||||
{
|
||||
name: 'Flemish - general',
|
||||
value: 'nl-BE',
|
||||
},
|
||||
{
|
||||
name: 'Danish - general',
|
||||
value: 'da',
|
||||
},
|
||||
{
|
||||
name: 'Tamil - general',
|
||||
value: 'ta',
|
||||
},
|
||||
{
|
||||
name: 'Tamasheq - general',
|
||||
value: 'taq',
|
||||
},
|
||||
{ name: 'Multilingual', value: 'multi' },
|
||||
{ name: 'Bulgarian', value: 'bg' },
|
||||
{ name: 'Catalan', value: 'ca' },
|
||||
{ name: 'Chinese (Mandarin, Simplified)', value: 'zh' },
|
||||
{ name: 'Chinese (Mandarin, Simplified - China)', value: 'zh-CN' },
|
||||
{ name: 'Chinese (Mandarin, Simplified - Hans)', value: 'zh-Hans' },
|
||||
{ name: 'Chinese (Mandarin, Traditional)', value: 'zh-TW' },
|
||||
{ name: 'Chinese (Mandarin, Traditional - Hant)', value: 'zh-Hant' },
|
||||
{ name: 'Chinese (Cantonese, Traditional - Hong Kong)', value: 'zh-HK' },
|
||||
{ name: 'Czech', value: 'cs' },
|
||||
{ name: 'Danish', value: 'da' },
|
||||
{ name: 'Danish (Denmark)', value: 'da-DK' },
|
||||
{ name: 'Dutch', value: 'nl' },
|
||||
{ name: 'English', value: 'en' },
|
||||
{ name: 'English (United States)', value: 'en-US' },
|
||||
{ name: 'English (Australia)', value: 'en-AU' },
|
||||
{ name: 'English (United Kingdom)', value: 'en-GB' },
|
||||
{ name: 'English (New Zealand)', value: 'en-NZ' },
|
||||
{ name: 'English (India)', value: 'en-IN' },
|
||||
{ name: 'Estonian', value: 'et' },
|
||||
{ name: 'Finnish', value: 'fi' },
|
||||
{ name: 'Flemish', value: 'nl-BE' },
|
||||
{ name: 'French', value: 'fr' },
|
||||
{ name: 'French (Canada)', value: 'fr-CA' },
|
||||
{ name: 'German', value: 'de' },
|
||||
{ name: 'German (Switzerland)', value: 'de-CH' },
|
||||
{ name: 'Greek', value: 'el' },
|
||||
{ name: 'Hindi', value: 'hi' },
|
||||
{ name: 'Hungarian', value: 'hu' },
|
||||
{ name: 'Indonesian', value: 'id' },
|
||||
{ name: 'Italian', value: 'it' },
|
||||
{ name: 'Japanese', value: 'ja' },
|
||||
{ name: 'Korean', value: 'ko' },
|
||||
{ name: 'Korean (South Korea)', value: 'ko-KR' },
|
||||
{ name: 'Latvian', value: 'lv' },
|
||||
{ name: 'Lithuanian', value: 'lt' },
|
||||
{ name: 'Malay', value: 'ms' },
|
||||
{ name: 'Norwegian', value: 'no' },
|
||||
{ name: 'Polish', value: 'pl' },
|
||||
{ name: 'Portuguese', value: 'pt' },
|
||||
{ name: 'Portuguese (Brazil)', value: 'pt-BR' },
|
||||
{ name: 'Portuguese (Portugal)', value: 'pt-PT' },
|
||||
{ name: 'Romanian', value: 'ro' },
|
||||
{ name: 'Russian', value: 'ru' },
|
||||
{ name: 'Slovak', value: 'sk' },
|
||||
{ name: 'Spanish', value: 'es' },
|
||||
{ name: 'Spanish (Latin America)', value: 'es-419' },
|
||||
{ name: 'Swedish', value: 'sv' },
|
||||
{ name: 'Swedish (Sweden)', value: 'sv-SE' },
|
||||
{ name: 'Thai', value: 'th' },
|
||||
{ name: 'Thai (Thailand)', value: 'th-TH' },
|
||||
{ name: 'Turkish', value: 'tr' },
|
||||
{ name: 'Ukrainian', value: 'uk' },
|
||||
{ name: 'Vietnamese', value: 'vi' }
|
||||
];
|
||||
|
||||
6
lib/utils/speech-data/stt-model-openai.js
Normal file
6
lib/utils/speech-data/stt-model-openai.js
Normal file
@@ -0,0 +1,6 @@
|
||||
module.exports = [
|
||||
{ name: 'Whisper', value: 'whisper-1' },
|
||||
{ name: 'GPT 4o Mini Transcribe', value: 'gpt-4o-mini-transcribe' },
|
||||
{ name: 'GLT 4o Transcribe', value: 'gpt-4o-transcribe' },
|
||||
];
|
||||
|
||||
59
lib/utils/speech-data/stt-openai.js
Normal file
59
lib/utils/speech-data/stt-openai.js
Normal file
@@ -0,0 +1,59 @@
|
||||
module.exports = [
|
||||
{ name: 'Afrikaans', value: 'af' },
|
||||
{ name: 'Arabic', value: 'ar' },
|
||||
{ name: 'Azerbaijani', value: 'az' },
|
||||
{ name: 'Belarusian', value: 'be' },
|
||||
{ name: 'Bulgarian', value: 'bg' },
|
||||
{ name: 'Bosnian', value: 'bs' },
|
||||
{ name: 'Catalan', value: 'ca' },
|
||||
{ name: 'Czech', value: 'cs' },
|
||||
{ name: 'Welsh', value: 'cy' },
|
||||
{ name: 'Danish', value: 'da' },
|
||||
{ name: 'German', value: 'de' },
|
||||
{ name: 'Greek', value: 'el' },
|
||||
{ name: 'English', value: 'en' },
|
||||
{ name: 'Spanish', value: 'es' },
|
||||
{ name: 'Estonian', value: 'et' },
|
||||
{ name: 'Persian', value: 'fa' },
|
||||
{ name: 'Finnish', value: 'fi' },
|
||||
{ name: 'French', value: 'fr' },
|
||||
{ name: 'Galician', value: 'gl' },
|
||||
{ name: 'Hebrew', value: 'he' },
|
||||
{ name: 'Hindi', value: 'hi' },
|
||||
{ name: 'Croatian', value: 'hr' },
|
||||
{ name: 'Hungarian', value: 'hu' },
|
||||
{ name: 'Armenian', value: 'hy' },
|
||||
{ name: 'Indonesian', value: 'id' },
|
||||
{ name: 'Icelandic', value: 'is' },
|
||||
{ name: 'Italian', value: 'it' },
|
||||
{ name: 'Japanese', value: 'ja' },
|
||||
{ name: 'Kazakh', value: 'kk' },
|
||||
{ name: 'Kannada', value: 'kn' },
|
||||
{ name: 'Korean', value: 'ko' },
|
||||
{ name: 'Lithuanian', value: 'lt' },
|
||||
{ name: 'Latvian', value: 'lv' },
|
||||
{ name: 'Maori', value: 'mi' },
|
||||
{ name: 'Macedonian', value: 'mk' },
|
||||
{ name: 'Marathi', value: 'mr' },
|
||||
{ name: 'Malay', value: 'ms' },
|
||||
{ name: 'Nepali', value: 'ne' },
|
||||
{ name: 'Dutch', value: 'nl' },
|
||||
{ name: 'Norwegian', value: 'no' },
|
||||
{ name: 'Polish', value: 'pl' },
|
||||
{ name: 'Portuguese', value: 'pt' },
|
||||
{ name: 'Romanian', value: 'ro' },
|
||||
{ name: 'Russian', value: 'ru' },
|
||||
{ name: 'Slovak', value: 'sk' },
|
||||
{ name: 'Slovenian', value: 'sl' },
|
||||
{ name: 'Serbian', value: 'sr' },
|
||||
{ name: 'Swedish', value: 'sv' },
|
||||
{ name: 'Swahili', value: 'sw' },
|
||||
{ name: 'Tamil', value: 'ta' },
|
||||
{ name: 'Thai', value: 'th' },
|
||||
{ name: 'Tagalog', value: 'tl' },
|
||||
{ name: 'Turkish', value: 'tr' },
|
||||
{ name: 'Ukrainian', value: 'uk' },
|
||||
{ name: 'Urdu', value: 'ur' },
|
||||
{ name: 'Vietnamese', value: 'vi' },
|
||||
{ name: 'Chinese', value: 'zh' },
|
||||
];
|
||||
@@ -4,6 +4,16 @@ module.exports = [
|
||||
value: 'sonic',
|
||||
languages: ['en', 'fr', 'de', 'es', 'pt', 'zh', 'ja', 'hi', 'it', 'ko', 'nl', 'pl', 'ru', 'sv', 'tr']
|
||||
},
|
||||
{
|
||||
name: 'Sonic 2',
|
||||
value: 'sonic-2',
|
||||
languages: ['en', 'fr', 'de', 'es', 'pt', 'zh', 'ja', 'hi', 'it', 'ko', 'nl', 'pl', 'ru', 'sv', 'tr']
|
||||
},
|
||||
{
|
||||
name: 'Sonic Turbo',
|
||||
value: 'sonic-turbo',
|
||||
languages: ['en', 'fr', 'de', 'es', 'pt', 'zh', 'ja', 'hi', 'it', 'ko', 'nl', 'pl', 'ru', 'sv', 'tr']
|
||||
},
|
||||
{ name: 'Sonic Preview', value: 'sonic-preview', languages: ['en'] },
|
||||
{
|
||||
name: 'Sonic 2024-12-12',
|
||||
|
||||
6
lib/utils/speech-data/tts-model-openai.js
Normal file
6
lib/utils/speech-data/tts-model-openai.js
Normal file
@@ -0,0 +1,6 @@
|
||||
module.exports = [
|
||||
{ name: 'TTS-1', value: 'tts-1' },
|
||||
{ name: 'TTS-1-HD', value: 'tts-1-hd' },
|
||||
{ name: 'GPT-4o-Mini-TTS', value: 'gpt-4o-mini-tts' },
|
||||
];
|
||||
|
||||
@@ -20,6 +20,7 @@ const TtsElevenlabsLanguagesVoices = require('./speech-data/tts-elevenlabs');
|
||||
const TtsWhisperLanguagesVoices = require('./speech-data/tts-whisper');
|
||||
const TtsPlayHtLanguagesVoices = require('./speech-data/tts-playht');
|
||||
const TtsVerbioLanguagesVoices = require('./speech-data/tts-verbio');
|
||||
const ttsCartesia = require('./speech-data/tts-cartesia');
|
||||
|
||||
const TtsModelDeepgram = require('./speech-data/tts-model-deepgram');
|
||||
const TtsLanguagesDeepgram = require('./speech-data/tts-deepgram');
|
||||
@@ -29,6 +30,7 @@ const TtsModelPlayHT = require('./speech-data/tts-model-playht');
|
||||
const ttsLanguagesPlayHt = require('./speech-data/tts-languages-playht');
|
||||
const TtsModelRimelabs = require('./speech-data/tts-model-rimelabs');
|
||||
const TtsModelCartesia = require('./speech-data/tts-model-cartesia');
|
||||
const TtsModelOpenai = require('./speech-data/tts-model-openai');
|
||||
|
||||
const SttGoogleLanguagesVoices = require('./speech-data/stt-google');
|
||||
const SttAwsLanguagesVoices = require('./speech-data/stt-aws');
|
||||
@@ -43,8 +45,10 @@ const SttSpeechmaticsLanguagesVoices = require('./speech-data/stt-speechmatics')
|
||||
const SttAssemblyaiLanguagesVoices = require('./speech-data/stt-assemblyai');
|
||||
const SttVoxistLanguagesVoices = require('./speech-data/stt-voxist');
|
||||
const SttVerbioLanguagesVoices = require('./speech-data/stt-verbio');
|
||||
const ttsCartesia = require('./speech-data/tts-cartesia');
|
||||
const ttsModelCartesia = require('./speech-data/tts-model-cartesia');
|
||||
const SttOpenaiLanguagesVoices = require('./speech-data/stt-openai');
|
||||
|
||||
|
||||
const SttModelOpenai = require('./speech-data/stt-model-openai');
|
||||
|
||||
|
||||
const testSonioxStt = async(logger, credentials) => {
|
||||
@@ -477,6 +481,43 @@ const testVerbioStt = async(logger, getVerbioAccessToken, credentials) => {
|
||||
}
|
||||
};
|
||||
|
||||
const testOpenAiStt = async(logger, credentials) => {
|
||||
const {api_key} = credentials;
|
||||
try {
|
||||
// Create a FormData object to properly format the multipart request
|
||||
const formData = new FormData();
|
||||
|
||||
// Add the audio file as 'file' field
|
||||
const audioBuffer = fs.readFileSync(`${__dirname}/../../data/test_audio.wav`);
|
||||
const blob = new Blob([audioBuffer], { type: 'audio/wav' });
|
||||
formData.append('file', blob, 'audio.wav');
|
||||
|
||||
// Add the model parameter (required by OpenAI)
|
||||
formData.append('model', 'whisper-1');
|
||||
|
||||
// Make the request using fetch
|
||||
const response = await fetch('https://api.openai.com/v1/audio/transcriptions', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${api_key}`,
|
||||
'User-Agent': 'jambonz'
|
||||
},
|
||||
body: formData
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`OpenAI API error: ${response.status} ${(await response.json()).error?.message}`);
|
||||
}
|
||||
|
||||
const json = await response.json();
|
||||
logger.debug({json}, 'successfully speech to text from OpenAI');
|
||||
return json;
|
||||
} catch (err) {
|
||||
logger.info({err}, 'OpenAI speech-to-text request failed');
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
const testAssemblyStt = async(logger, credentials) => {
|
||||
const {api_key} = credentials;
|
||||
|
||||
@@ -651,6 +692,10 @@ function decryptCredential(obj, credential, logger, isObscureKey = true) {
|
||||
const o = JSON.parse(decrypt(credential));
|
||||
obj.api_key = isObscureKey ? obscureKey(o.api_key) : o.api_key;
|
||||
obj.model_id = o.model_id;
|
||||
} else if ('openai' === obj.vendor) {
|
||||
const o = JSON.parse(decrypt(credential));
|
||||
obj.api_key = isObscureKey ? obscureKey(o.api_key) : o.api_key;
|
||||
obj.model_id = o.model_id;
|
||||
} else if ('verbio' === obj.vendor) {
|
||||
const o = JSON.parse(decrypt(credential));
|
||||
obj.client_id = o.client_id;
|
||||
@@ -714,6 +759,8 @@ async function getLanguagesAndVoicesForVendor(logger, vendor, credential, getTts
|
||||
return await getLanguagesVoicesForVoxist(credential, getTtsVoices, logger);
|
||||
case 'whisper':
|
||||
return await getLanguagesVoicesForWhisper(credential, getTtsVoices, logger);
|
||||
case 'openai':
|
||||
return await getLanguagesVoicesForOpenAi(credential, getTtsVoices, logger);
|
||||
case 'verbio':
|
||||
return await getLanguagesVoicesForVerbio(credential, getTtsVoices, logger);
|
||||
case 'speechmatics':
|
||||
@@ -1014,6 +1061,10 @@ async function getLanguagesVoicesForWhisper(credential) {
|
||||
return tranform(TtsWhisperLanguagesVoices, undefined, TtsModelWhisper);
|
||||
}
|
||||
|
||||
async function getLanguagesVoicesForOpenAi(credential) {
|
||||
return tranform(undefined, SttOpenaiLanguagesVoices, TtsModelOpenai, SttModelOpenai);
|
||||
}
|
||||
|
||||
async function getLanguagesVoicesForVerbio(credentials, getTtsVoices, logger) {
|
||||
const stt = SttVerbioLanguagesVoices.reduce((acc, v) => {
|
||||
if (!v.version || (credentials && credentials.engine_version === v.version)) {
|
||||
@@ -1034,11 +1085,12 @@ async function getLanguagesVoicesForVerbio(credentials, getTtsVoices, logger) {
|
||||
}
|
||||
}
|
||||
|
||||
function tranform(tts, stt, models) {
|
||||
function tranform(tts, stt, models, sttModels) {
|
||||
return {
|
||||
...(tts && {tts}),
|
||||
...(stt && {stt}),
|
||||
...(models && {models})
|
||||
...(models && {models}),
|
||||
...(sttModels && {sttModels})
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1224,7 +1276,7 @@ const testCartesia = async(logger, synthAudio, credentials) => {
|
||||
async function getLanguagesVoicesForCartesia(credential) {
|
||||
if (credential) {
|
||||
const {model_id} = credential;
|
||||
const {languages} = ttsModelCartesia.find((m) => m.value === model_id);
|
||||
const {languages} = TtsModelCartesia.find((m) => m.value === model_id);
|
||||
const voices = await fetchCartesiaVoices(credential);
|
||||
|
||||
const buildVoice = (d) => (
|
||||
@@ -1301,5 +1353,6 @@ module.exports = {
|
||||
getLanguagesAndVoicesForVendor,
|
||||
testSpeechmaticsStt,
|
||||
testCartesia,
|
||||
testVoxistStt
|
||||
testVoxistStt,
|
||||
testOpenAiStt
|
||||
};
|
||||
|
||||
8
package-lock.json
generated
8
package-lock.json
generated
@@ -16,7 +16,7 @@
|
||||
"@deepgram/sdk": "^1.21.0",
|
||||
"@google-cloud/speech": "^6.5.0",
|
||||
"@google-cloud/storage": "^7.9.0",
|
||||
"@jambonz/db-helpers": "^0.9.3",
|
||||
"@jambonz/db-helpers": "^0.9.11",
|
||||
"@jambonz/lamejs": "^1.2.2",
|
||||
"@jambonz/mw-registrar": "^0.2.7",
|
||||
"@jambonz/realtimedb-helpers": "^0.8.13",
|
||||
@@ -4085,9 +4085,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@jambonz/db-helpers": {
|
||||
"version": "0.9.7",
|
||||
"resolved": "https://registry.npmjs.org/@jambonz/db-helpers/-/db-helpers-0.9.7.tgz",
|
||||
"integrity": "sha512-5qN/CJZJXpbMkMn+8gFn8PpQ0ZImZxp1EjKyxLUlmMn+xgjeNb29c3pjeVt/6EQnBB65jAax6TNsVzVIfpvE2w==",
|
||||
"version": "0.9.11",
|
||||
"resolved": "https://registry.npmjs.org/@jambonz/db-helpers/-/db-helpers-0.9.11.tgz",
|
||||
"integrity": "sha512-L6xw60J0sYEMlR0EkeqolKbk+g08yvN51V8D0+bBfpTxsJUGgHiYhDgeGeTLU5J1FQCGwmj2RI1AVW5aAu6Q1A==",
|
||||
"dependencies": {
|
||||
"cidr-matcher": "^2.1.1",
|
||||
"debug": "^4.3.4",
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
"@deepgram/sdk": "^1.21.0",
|
||||
"@google-cloud/speech": "^6.5.0",
|
||||
"@google-cloud/storage": "^7.9.0",
|
||||
"@jambonz/db-helpers": "^0.9.3",
|
||||
"@jambonz/db-helpers": "^0.9.11",
|
||||
"@jambonz/lamejs": "^1.2.2",
|
||||
"@jambonz/mw-registrar": "^0.2.7",
|
||||
"@jambonz/realtimedb-helpers": "^0.8.13",
|
||||
|
||||
@@ -32,3 +32,11 @@ test('add predefined carriers', (t) => {
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
test('prepare permissions', (t) => {
|
||||
exec(`mysql -h 127.0.0.1 -u root --protocol=tcp --port=3360 -D jambones_test < ${__dirname}/../db/prepare-permissions-test.sql`, (err, stdout, stderr) => {
|
||||
if (err) return t.end(err);
|
||||
t.pass('permissions prepared');
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,6 +13,7 @@ require('./ms-teams');
|
||||
require('./speech-credentials');
|
||||
require('./recent-calls');
|
||||
require('./users');
|
||||
require('./users-view-only');
|
||||
require('./login');
|
||||
require('./webapp_tests');
|
||||
// require('./homer');
|
||||
|
||||
296
test/users-view-only.js
Normal file
296
test/users-view-only.js
Normal file
@@ -0,0 +1,296 @@
|
||||
const test = require('tape') ;
|
||||
const jwt = require('jsonwebtoken');
|
||||
const request = require('request-promise-native').defaults({
|
||||
baseUrl: 'http://127.0.0.1:3000/v1'
|
||||
});
|
||||
const exec = require('child_process').exec ;
|
||||
const {generateHashedPassword} = require('../lib/utils/password-utils');
|
||||
process.on('unhandledRejection', (reason, p) => {
|
||||
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
|
||||
});
|
||||
test('add an admin user', (t) => {
|
||||
exec(`${__dirname}/../db/reset_admin_password.js`, (err, stdout, stderr) => {
|
||||
console.log(stderr);
|
||||
console.log(stdout);
|
||||
if (err) return t.end(err);
|
||||
t.pass('successfully added admin user');
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
test('prepare permissions', (t) => {
|
||||
exec(`mysql -h 127.0.0.1 -u root --protocol=tcp --port=3360 -D jambones_test < ${__dirname}/../db/prepare-permissions-test.sql`, (err, stdout, stderr) => {
|
||||
if (err) return t.end(err);
|
||||
t.pass('permissions prepared');
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
test('view-only user tests', async(t) => {
|
||||
const app = require('../app');
|
||||
const password = 'abcde12345-';
|
||||
try {
|
||||
let result;
|
||||
/* login as admin to get a jwt */
|
||||
result = await request.post('/login', {
|
||||
resolveWithFullResponse: true,
|
||||
json: true,
|
||||
body: {
|
||||
username: 'admin',
|
||||
password: 'admin',
|
||||
}
|
||||
});
|
||||
t.ok(result.statusCode === 200 && result.body.token, 'successfully logged in as admin');
|
||||
const authAdmin = {bearer: result.body.token};
|
||||
const decodedJwt = jwt.verify(result.body.token, process.env.JWT_SECRET);
|
||||
/* add admin user */
|
||||
result = await request.post(`/Users`, {
|
||||
resolveWithFullResponse: true,
|
||||
json: true,
|
||||
auth: authAdmin,
|
||||
body: {
|
||||
name: 'admin2',
|
||||
email: 'admin2@jambonz.com',
|
||||
is_active: true,
|
||||
force_change: true,
|
||||
initial_password: password,
|
||||
}
|
||||
});
|
||||
t.ok(result.statusCode === 201 && result.body.user_sid, 'admin user created');
|
||||
const admin_user_sid = result.body.user_sid;
|
||||
/* add a service provider */
|
||||
result = await request.post('/ServiceProviders', {
|
||||
resolveWithFullResponse: true,
|
||||
auth: authAdmin,
|
||||
json: true,
|
||||
body: {
|
||||
name: 'sp1',
|
||||
}
|
||||
});
|
||||
t.ok(result.statusCode === 201, 'successfully created service provider');
|
||||
const sp_sid = result.body.sid;
|
||||
/* add service_provider read only user */
|
||||
result = await request.post(`/Users`, {
|
||||
resolveWithFullResponse: true,
|
||||
json: true,
|
||||
auth: authAdmin,
|
||||
body: {
|
||||
name: 'service_provider',
|
||||
email: 'sp@jambonz.com',
|
||||
is_active: true,
|
||||
force_change: true,
|
||||
initial_password: password,
|
||||
service_provider_sid: sp_sid,
|
||||
is_view_only: true
|
||||
}
|
||||
});
|
||||
t.ok(result.statusCode === 201 && result.body.user_sid, 'service_provider scope view-only user created');
|
||||
|
||||
// login as service_provider read only user
|
||||
result = await request.post('/login', {
|
||||
resolveWithFullResponse: true,
|
||||
json: true,
|
||||
body: {
|
||||
username: 'service_provider',
|
||||
password: password,
|
||||
}
|
||||
});
|
||||
t.ok(result.statusCode === 200 && result.body.token, 'successfully logged in as service provider view-only user');
|
||||
const spToken = {bearer: result.body.token};
|
||||
const spDecodedJwt = jwt.verify(result.body.token, process.env.JWT_SECRET);
|
||||
try {
|
||||
result = await request.post('/Accounts', {
|
||||
resolveWithFullResponse: true,
|
||||
auth: spToken,
|
||||
json: true,
|
||||
body: {
|
||||
name: 'sample_account',
|
||||
service_provider_sid: sp_sid,
|
||||
registration_hook: {
|
||||
url: 'http://example.com/reg',
|
||||
method: 'get'
|
||||
},
|
||||
webhook_secret: 'foobar'
|
||||
}
|
||||
})
|
||||
} catch(err) {
|
||||
t.ok(err.statusCode === 403, 'As a view-only user, you cannot create an account');
|
||||
}
|
||||
result = await request.post('/Accounts', {
|
||||
resolveWithFullResponse: true,
|
||||
auth: authAdmin,
|
||||
json: true,
|
||||
body: {
|
||||
name: 'sample_account',
|
||||
service_provider_sid: sp_sid,
|
||||
registration_hook: {
|
||||
url: 'http://example.com/reg',
|
||||
method: 'get'
|
||||
},
|
||||
webhook_secret: 'foobar'
|
||||
}
|
||||
})
|
||||
t.ok(result.statusCode === 201, 'successfully created account using admin token');
|
||||
const account_sid = result.body.sid;
|
||||
/* add account scope view-only user */
|
||||
result = await request.post(`/Users`, {
|
||||
resolveWithFullResponse: true,
|
||||
json: true,
|
||||
auth: authAdmin,
|
||||
body: {
|
||||
name: 'account',
|
||||
email: 'account@jambonz.com',
|
||||
is_active: true,
|
||||
force_change: true,
|
||||
initial_password: password,
|
||||
service_provider_sid: sp_sid,
|
||||
account_sid: account_sid,
|
||||
is_view_only: true
|
||||
}
|
||||
});
|
||||
t.ok(result.statusCode === 201 && result.body.user_sid, 'account scope user created');
|
||||
const account_user_sid = result.body.user_sid;
|
||||
// login as account read only user
|
||||
result = await request.post('/login', {
|
||||
resolveWithFullResponse: true,
|
||||
json: true,
|
||||
body: {
|
||||
username: 'account',
|
||||
password: password,
|
||||
}
|
||||
});
|
||||
t.ok(result.statusCode === 200 && result.body.token, 'successfully logged in as account view-only user');
|
||||
let userToken = {bearer: result.body.token};
|
||||
/* add an application which should fail as the logged in user is a view-only user */
|
||||
try {
|
||||
result = await request.post('/Applications', {
|
||||
resolveWithFullResponse: true,
|
||||
auth: userToken,
|
||||
json: true,
|
||||
body: {
|
||||
name: 'daveh',
|
||||
account_sid,
|
||||
call_hook: {
|
||||
url: 'http://example.com'
|
||||
},
|
||||
call_status_hook: {
|
||||
url: 'http://example.com/status',
|
||||
method: 'POST'
|
||||
},
|
||||
messaging_hook: {
|
||||
url: 'http://example.com/sms'
|
||||
},
|
||||
app_json : '[\
|
||||
{\
|
||||
"verb": "play",\
|
||||
"url": "https://example.com/example.mp3",\
|
||||
"timeoutSecs": 10,\
|
||||
"seekOffset": 8000,\
|
||||
"actionHook": "/play/action"\
|
||||
}\
|
||||
]',
|
||||
use_for_fallback_speech: 1,
|
||||
fallback_speech_synthesis_vendor: 'google',
|
||||
fallback_speech_synthesis_language: 'en-US',
|
||||
fallback_speech_synthesis_voice: 'man',
|
||||
fallback_speech_synthesis_label: 'label1',
|
||||
fallback_speech_recognizer_vendor: 'google',
|
||||
fallback_speech_recognizer_language: 'en-US',
|
||||
fallback_speech_recognizer_label: 'label1'
|
||||
}
|
||||
});
|
||||
} catch(err) {
|
||||
t.ok(err.statusCode === 403, 'As a view-only user, you cannot create an application');
|
||||
}
|
||||
// change user as read/write user and create an application - it should succeed
|
||||
result = await request.put(`/Users/${account_user_sid}`, {
|
||||
resolveWithFullResponse: true,
|
||||
json: true,
|
||||
auth: authAdmin,
|
||||
body: {
|
||||
is_view_only: false
|
||||
}
|
||||
});
|
||||
t.ok(result.statusCode === 204, 'successfully updated user to read/write permissions');
|
||||
// login as account read only user
|
||||
result = await request.post('/login', {
|
||||
resolveWithFullResponse: true,
|
||||
json: true,
|
||||
body: {
|
||||
username: 'account',
|
||||
password: password,
|
||||
}
|
||||
});
|
||||
t.ok(result.statusCode === 200 && result.body.token, 'successfully logged in as account read-write user');
|
||||
userToken = {bearer: result.body.token};
|
||||
/* add an application which should succeed as the logged in user is a read-write user */
|
||||
result = await request.post('/Applications', {
|
||||
resolveWithFullResponse: true,
|
||||
auth: userToken,
|
||||
json: true,
|
||||
body: {
|
||||
name: 'daveh',
|
||||
account_sid,
|
||||
call_hook: {
|
||||
url: 'http://example.com'
|
||||
},
|
||||
call_status_hook: {
|
||||
url: 'http://example.com/status',
|
||||
method: 'POST'
|
||||
},
|
||||
messaging_hook: {
|
||||
url: 'http://example.com/sms'
|
||||
},
|
||||
app_json : '[\
|
||||
{\
|
||||
"verb": "play",\
|
||||
"url": "https://example.com/example.mp3",\
|
||||
"timeoutSecs": 10,\
|
||||
"seekOffset": 8000,\
|
||||
"actionHook": "/play/action"\
|
||||
}\
|
||||
]',
|
||||
use_for_fallback_speech: 1,
|
||||
fallback_speech_synthesis_vendor: 'google',
|
||||
fallback_speech_synthesis_language: 'en-US',
|
||||
fallback_speech_synthesis_voice: 'man',
|
||||
fallback_speech_synthesis_label: 'label1',
|
||||
fallback_speech_recognizer_vendor: 'google',
|
||||
fallback_speech_recognizer_language: 'en-US',
|
||||
fallback_speech_recognizer_label: 'label1'
|
||||
}
|
||||
});
|
||||
t.ok(result.statusCode === 201, 'successfully created an application');
|
||||
// change user back to view-only and query the application - it should succeed
|
||||
result = await request.put(`/Users/${account_user_sid}`, {
|
||||
resolveWithFullResponse: true,
|
||||
json: true,
|
||||
auth: authAdmin,
|
||||
body: {
|
||||
is_view_only: true
|
||||
}
|
||||
});
|
||||
t.ok(result.statusCode === 204, 'successfully updated user permission to view-only');
|
||||
// login as account read only user
|
||||
result = await request.post('/login', {
|
||||
resolveWithFullResponse: true,
|
||||
json: true,
|
||||
body: {
|
||||
username: 'account',
|
||||
password: password,
|
||||
}
|
||||
});
|
||||
t.ok(result.statusCode === 200 && result.body.token, 'successfully logged in as account view-only user');
|
||||
userToken = {bearer: result.body.token};
|
||||
result = await request.get('/Applications', {
|
||||
auth: userToken,
|
||||
json: true,
|
||||
});
|
||||
//console.log(`result: ${JSON.stringify(result)}`);
|
||||
t.ok(result.length === 1 , 'successfully queried all applications with view-only user');
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
t.end(err);
|
||||
}
|
||||
});
|
||||
@@ -20,7 +20,13 @@ test('add an admin user', (t) => {
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
test('prepare permissions', (t) => {
|
||||
exec(`mysql -h 127.0.0.1 -u root --protocol=tcp --port=3360 -D jambones_test < ${__dirname}/../db/prepare-permissions-test.sql`, (err, stdout, stderr) => {
|
||||
if (err) return t.end(err);
|
||||
t.pass('permissions prepared');
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
test('user tests', async(t) => {
|
||||
const app = require('../app');
|
||||
const password = 'abcde12345-';
|
||||
|
||||
@@ -24,7 +24,13 @@ test('seeding database for webapp tests', (t) => {
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
test('prepare permissions', (t) => {
|
||||
exec(`mysql -h 127.0.0.1 -u root --protocol=tcp --port=3360 -D jambones_test < ${__dirname}/../db/prepare-permissions-test.sql`, (err, stdout, stderr) => {
|
||||
if (err) return t.end(err);
|
||||
t.pass('permissions prepared');
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
test('webapp tests', async(t) => {
|
||||
const app = require('../app');
|
||||
let sid;
|
||||
|
||||
Reference in New Issue
Block a user